mirror of
https://github.com/ayozetr/bloodborne_linux.git
synced 2026-11-11 01:11:44 -10:00
Compare commits
84
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d79bcb27a6 | ||
|
|
cb3252eb62 | ||
|
|
3f07ea173c | ||
|
|
219be762f8 | ||
|
|
74e061091b | ||
|
|
bd1b901698 | ||
|
|
ab6e5f4162 | ||
|
|
fd253355e5 | ||
|
|
fdd924b5e7 | ||
|
|
a8f913a9c3 | ||
|
|
14ae3076ca | ||
|
|
27d5d3802f | ||
|
|
cc4999adc5 | ||
|
|
9b90958953 | ||
|
|
cc28a796ae | ||
|
|
004c8d40ad | ||
|
|
7191ac0ec7 | ||
|
|
1cc27f4223 | ||
|
|
e107d9e56f | ||
|
|
aedbe8779e | ||
|
|
fab33687cf | ||
|
|
49e6c535bb | ||
|
|
5531775af3 | ||
|
|
597ad6bad9 | ||
|
|
ea13d1c243 | ||
|
|
27f3c6d061 | ||
|
|
ed7dbcf626 | ||
|
|
189c03b25e | ||
|
|
a1ba326fdc | ||
|
|
cc30687384 | ||
|
|
3b826a415e | ||
|
|
084c7f7d6d | ||
|
|
23728115f0 | ||
|
|
eb20620aaf | ||
|
|
037a24df0e | ||
|
|
d637e22258 | ||
|
|
95878adbba | ||
|
|
dac2621116 | ||
|
|
5a88481947 | ||
|
|
636621403f | ||
|
|
35acaa9e59 | ||
|
|
a0148d4930 | ||
|
|
7e7cedd4b0 | ||
|
|
ba79de106f | ||
|
|
8f67573bd3 | ||
|
|
21e0edd40a | ||
|
|
a7e6d44076 | ||
|
|
2b8d9e1fe1 | ||
|
|
1ccef67d2c | ||
|
|
a643904df0 | ||
|
|
a3e31c7227 | ||
|
|
6b12b22376 | ||
|
|
a36201f796 | ||
|
|
3c2033b5d0 | ||
|
|
d5c2611f72 | ||
|
|
73f3bb512a | ||
|
|
5e86d0bde7 | ||
|
|
30b3409674 | ||
|
|
b3c8b1dadd | ||
|
|
697af8936f | ||
|
|
576a214b5f | ||
|
|
8d1db30f9e | ||
|
|
16a08695c3 | ||
|
|
b4d74c88f7 | ||
|
|
bb3a9d126d | ||
|
|
3d2b8b3067 | ||
|
|
186a342750 | ||
|
|
2bcc093bd9 | ||
|
|
8937a721ca | ||
|
|
4f3187843f | ||
|
|
da5561d6c7 | ||
|
|
f9186dbb8d | ||
|
|
1b93552dca | ||
|
|
d7a94e5dfc | ||
|
|
e2b3f73e71 | ||
|
|
1ce06532c2 | ||
|
|
49a37b4459 | ||
|
|
25cdef7482 | ||
|
|
9413c82bfb | ||
|
|
f4a12043f6 | ||
|
|
7e02e06139 | ||
|
|
7135aa9288 | ||
|
|
48de7625b5 | ||
|
|
1ca5a39b3a |
No files matched your search
@@ -176,13 +176,21 @@ bash launcher/bb-launcher.sh # launcher/install-desktop.sh adds it to th
|
||||
```
|
||||
|
||||
By default the game folder is expected next to the repository (`../CUSA03173`). Saves and the
|
||||
shader cache go to `user/` (the launcher lets you choose another folder); 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,
|
||||
shader cache go to `user/` (the launcher lets you choose another folder, and imports saves from
|
||||
shadPS4, another port or a PS4 save exported unencrypted, after backing up the current ones in
|
||||
`user/savedata-backups/`; `scripts/save_import.py`); settings to `bbport.ini`. A gamepad is used
|
||||
through SDL3 (the launcher's *Controls → Controller* picks one when several are connected,
|
||||
`BB_GAMEPAD=<GUID or part of the name>`; with none picked, the one you press a button on or move
|
||||
takes over). The keyboard works too,
|
||||
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
|
||||
the key items. On a controller without a touchpad (Xbox), Back + d-pad left / right clicks the
|
||||
left / right half. *Controls* in the launcher also swaps the sticks and inverts the controller's
|
||||
camera (`pad_swap_sticks`, `pad_invert_look_x`, `pad_invert_look_y` in bbport.ini). With *Direct
|
||||
mouse camera* (`mouse_camera=direct`) the mouse turns the camera itself, 1:1, with no stick dead
|
||||
zone or turn curve (the game's 1.09 code is hooked while the window holds the mouse; locked on, the
|
||||
mouse moves the stick again so a flick switches targets). There is no PSN: the *Play online / offline* screen is skipped and the game opens
|
||||
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.
|
||||
@@ -242,7 +250,8 @@ For the game debug menu, install `DbgFont14h.ccm` and `DbgFont14h.tpf` from
|
||||
the unsafe patch. Open it with the left touchpad / Tab (with the debug menu on, the left half
|
||||
no longer opens the gestures); Backspace is the right touchpad.
|
||||
Touch coordinates are forwarded from SDL gamepads; Back/Select emulates a left click on
|
||||
pads without a touch surface. The port's settings menu remains Insert / L3+R3.
|
||||
pads without a touch surface, Back + d-pad left / right a click on that half (while Back is bound
|
||||
to the touchpad only, the default). The port's settings menu remains Insert / L3+R3.
|
||||
|
||||
GPU occlusion queries still use synthetic pixel counters (`PixelPipeStatDump`), and
|
||||
`IT_SET_PREDICATION` is unimplemented. Free camera allows visual investigation; it does
|
||||
|
||||
+11
-4
@@ -169,12 +169,19 @@ bash launcher/bb-launcher.sh # launcher/install-desktop.sh добави
|
||||
```
|
||||
|
||||
По умолчанию папка игры ищется рядом с репозиторием (`../CUSA03173`). Сохранения и кэш шейдеров
|
||||
— в `user/` (в лаунчере можно выбрать другую папку), настройки — в `bbport.ini`. Геймпад через
|
||||
SDL3 (если их несколько, нужный выбирается в лаунчере: *Управление → Контроллер*;
|
||||
`BB_GAMEPAD=<GUID или часть имени>`). Клавиатура тоже работает, и при подключённом геймпаде
|
||||
— в `user/` (в лаунчере можно выбрать другую папку и импортировать сохранения из shadPS4,
|
||||
другого порта или выгруженные с PS4 без шифрования; текущие сначала копируются в
|
||||
`user/savedata-backups/`; `scripts/save_import.py`), настройки — в `bbport.ini`. Геймпад через
|
||||
SDL3 (если их несколько, нужный выбирается в лаунчере: *Управление → Контроллер*,
|
||||
`BB_GAMEPAD=<GUID или часть имени>`; если не выбран, управляет тот, на котором нажали кнопку или
|
||||
двинули стик). Клавиатура тоже работает, и при подключённом геймпаде
|
||||
(на Steam Deck он есть всегда); обе раскладки меняются в лаунчере (*Управление*). Имя персонажа
|
||||
вводится с клавиатуры в окне поверх игры. Тачпад: левая половина (Tab, Back/Select) открывает
|
||||
жесты, правая (Backspace) — личные вещи. PSN нет: экран «Играть по сети / вне сети» пропускается,
|
||||
жесты, правая (Backspace) — личные вещи. На геймпаде без тачпада (Xbox) Back + крестовина
|
||||
влево / вправо нажимает левую / правую половину. В лаунчере (*Управление*) можно поменять стики
|
||||
местами и инвертировать камеру геймпада (`pad_swap_sticks`, `pad_invert_look_x`, `pad_invert_look_y` в bbport.ini). *Прямая камера мышью*
|
||||
(`mouse_camera=direct`): мышь поворачивает камеру сама, 1:1, без мёртвой зоны и кривой стика (код
|
||||
игры 1.09 перехватывается, пока окно держит мышь; при захвате цели мышь снова двигает стик). PSN нет: экран «Играть по сети / вне сети» пропускается,
|
||||
игра сразу открывает главное меню вне сети (патч игры; в лаунчере *Эффекты игры → Пропускать
|
||||
выбор «по сети / вне сети»*, `BB_SKIP_NETWORK_CHOICE=0` — показывать). При нескольких мониторах нужный выбирается в
|
||||
лаунчере: *Экран → Монитор* (`BB_DISPLAY=<номер или часть имени>`).
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# Bloodborne for Linux
|
||||
|
||||
[](https://github.com/ayozetr/bloodborne_linux/releases/latest)
|
||||
[](https://github.com/ayozetr/bloodborne_linux/releases/latest)
|
||||
[](LICENSE)
|
||||
[](#features)
|
||||
[](https://ko-fi.com/ayozetr)
|
||||
|
||||
The PlayStation 4 version of *Bloodborne* running natively on Linux. The game's own x86-64 code
|
||||
runs directly on the CPU and its graphics are translated to Vulkan. It is not an emulator.
|
||||
|
||||
@@ -13,9 +19,9 @@ by deadinside28. It adds the features of [Supermedo's Windows port](https://gith
|
||||
|
||||
## Features
|
||||
|
||||
- **Online play (beta)** through [The Hunter's Dream](https://thehuntersdream.com) and shadNet:
|
||||
messages, bloodstains, ghosts, bells and summons. Linux and Windows players can play together,
|
||||
through shadPS4's public server or [one of your own](docs/SERVER.md).
|
||||
- **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
|
||||
@@ -39,7 +45,8 @@ by deadinside28. It adds the features of [Supermedo's Windows port](https://gith
|
||||
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.
|
||||
a new AppImage over the old one, or any earlier release from 0.5-linux.1 on, to go back to one
|
||||
that worked better.
|
||||
|
||||
## Requirements
|
||||
|
||||
@@ -76,22 +83,23 @@ With [Nix](https://nixos.org), `bash build.sh` gets every dependency from `shell
|
||||
|
||||
### Online play
|
||||
|
||||
1. Register a shadNet account at [shadnet.shadps4.net](https://shadnet.shadps4.net), or get one
|
||||
from the owner of your group's own server.
|
||||
2. In the launcher, open **Online**, turn on **Play online**, choose the shadNet server and enter
|
||||
your account name (not your email) and password. **Test the connection**: every line should
|
||||
be green.
|
||||
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.
|
||||
|
||||
Everyone in a session needs game version 1.09 and the same servers. Turn cheats off when you
|
||||
play with others.
|
||||
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.
|
||||
|
||||
**Co-op needs your own server for now:** shadPS4's public server does not answer on its UDP port,
|
||||
so summons do not connect through it (messages, bloodstains and ghosts do).
|
||||
[docs/SERVER.md](docs/SERVER.md) sets one up with Docker and Tailscale; it has been tested with a
|
||||
Linux and a Windows player.
|
||||
|
||||
Co-op connects the players directly over UDP. UPnP opens the port on the router by itself; with
|
||||
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).
|
||||
@@ -146,6 +154,8 @@ If this fork is useful to you and you would like to support it, you can on
|
||||
- [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.
|
||||
@@ -157,6 +167,9 @@ If this fork is useful to you and you would like to support it, you can on
|
||||
which textures keyboard prompts need (after the idea of
|
||||
[mod 460](https://www.nexusmods.com/bloodborne/mods/460)); bbport draws its own.
|
||||
- [quick-sharun](https://github.com/pkgforge-dev/Anylinux-AppImages) for the AppImage.
|
||||
- [ps4-pkg-extractor](https://github.com/paulomanrique/ps4-pkg-extractor) by paulomanrique
|
||||
(Unlicense), on [orbis-pkg-util](https://crates.io/crates/orbis-pkg-util): the launcher's
|
||||
*Install from PKG*.
|
||||
- Game patches by Kyo, Lance McDonald, auser1337, illusion, emoose and the Bloodborne community.
|
||||
|
||||
Full list of components and licenses: [BBPORT.md](BBPORT.md#credits-and-licenses).
|
||||
|
||||
@@ -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/
|
||||
@@ -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.
|
||||
@@ -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()
|
||||
@@ -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" }
|
||||
]
|
||||
}
|
||||
@@ -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>.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,103 @@
|
||||
// shadNet wire protocol: 15-byte packet framing, command / error / notification
|
||||
// numbers, and the field numbers of the messages the bridge reads or writes.
|
||||
// Sources: shadnet-p2p's protocol.h and shadnet.proto, the port's client.cpp.
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
namespace shadnet {
|
||||
|
||||
constexpr size_t kHeaderSize = 15;
|
||||
constexpr uint32_t kMaxPacketSize = 0x800000;
|
||||
constexpr uint32_t kProtocolVersion = 1;
|
||||
|
||||
enum PacketType : uint8_t { Request = 0, Reply = 1, Notification = 2, ServerInfo = 3 };
|
||||
|
||||
namespace cmd {
|
||||
constexpr uint16_t Login = 0, Terminate = 1, AddFriend = 8, RemoveFriend = 9, AddBlock = 10,
|
||||
RemoveBlock = 11, GetServerFeatures = 12, SetClientVersion = 13, GetToken = 39,
|
||||
SetAppearOffline = 40, ContextStart = 100, CreateRoom = 101, JoinRoom = 102, LeaveRoom = 103,
|
||||
SearchRoom = 104, RequestSignalingInfos = 105, ContextStop = 106,
|
||||
SetRoomDataInternal = 108, SetRoomDataExternal = 109, KickoutRoomMember = 110,
|
||||
GetWorldInfoList = 111;
|
||||
}
|
||||
|
||||
namespace err {
|
||||
constexpr uint8_t NoError = 0, Malformed = 1, Invalid = 2, InvalidInput = 3, LoginError = 5,
|
||||
LoginInvalidPassword = 8, RoomMissing = 14, NotFound = 26, Unsupported = 33;
|
||||
}
|
||||
|
||||
namespace notify {
|
||||
constexpr uint16_t RoomEvent = 10;
|
||||
}
|
||||
|
||||
namespace room_event {
|
||||
constexpr uint32_t MemberJoined = 0x1101, MemberLeft = 0x1102, KickedOut = 0x1103;
|
||||
}
|
||||
|
||||
namespace cause {
|
||||
constexpr uint32_t Leave = 1, Kickout = 2, ServerOperation = 4, MemberDisappeared = 5;
|
||||
}
|
||||
|
||||
constexpr uint32_t kOwnerFlag = 0x80000000; // SCE_NP_MATCHING2_ROOMMEMBER_FLAG_ATTR_OWNER
|
||||
|
||||
struct Header {
|
||||
uint8_t type = 0;
|
||||
uint16_t command = 0;
|
||||
uint32_t size = 0;
|
||||
uint64_t packet_id = 0;
|
||||
};
|
||||
|
||||
inline bool parse_header(const std::string& buf, Header& h) {
|
||||
if (buf.size() < kHeaderSize) return false;
|
||||
const char* p = buf.data();
|
||||
h.type = static_cast<uint8_t>(p[0]);
|
||||
std::memcpy(&h.command, p + 1, 2);
|
||||
std::memcpy(&h.size, p + 3, 4);
|
||||
std::memcpy(&h.packet_id, p + 7, 8);
|
||||
return true;
|
||||
}
|
||||
|
||||
inline std::string build_packet(uint8_t type, uint16_t command, uint64_t packet_id, const std::string& payload) {
|
||||
std::string out(kHeaderSize, '\0');
|
||||
const uint32_t size = static_cast<uint32_t>(kHeaderSize + payload.size());
|
||||
out[0] = static_cast<char>(type);
|
||||
std::memcpy(&out[1], &command, 2);
|
||||
std::memcpy(&out[3], &size, 4);
|
||||
std::memcpy(&out[7], &packet_id, 8);
|
||||
return out + payload;
|
||||
}
|
||||
|
||||
inline std::string build_server_info() {
|
||||
std::string v(4, '\0');
|
||||
std::memcpy(v.data(), &kProtocolVersion, 4);
|
||||
return build_packet(ServerInfo, 0, 0, v);
|
||||
}
|
||||
|
||||
inline std::string build_reply(uint16_t command, uint64_t packet_id, uint8_t error, const std::string& payload) {
|
||||
return build_packet(Reply, command, packet_id, std::string(1, static_cast<char>(error)) + payload);
|
||||
}
|
||||
|
||||
// Every proto payload is framed as a u32-LE length + the message.
|
||||
inline std::string proto_blob(const std::string& msg) {
|
||||
std::string out(4, '\0');
|
||||
const uint32_t n = static_cast<uint32_t>(msg.size());
|
||||
std::memcpy(out.data(), &n, 4);
|
||||
return out + msg;
|
||||
}
|
||||
|
||||
inline bool parse_proto_blob(const std::string& payload, std::string& msg) {
|
||||
if (payload.size() < 4) return false;
|
||||
uint32_t n;
|
||||
std::memcpy(&n, payload.data(), 4);
|
||||
msg = payload.substr(4, n);
|
||||
return true;
|
||||
}
|
||||
|
||||
inline std::string build_notification(uint16_t type, const std::string& msg) {
|
||||
return build_packet(Notification, type, 0, proto_blob(msg));
|
||||
}
|
||||
|
||||
} // namespace shadnet
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,395 @@
|
||||
"""shadNet wire protocol: 15-byte packet framing + message schemas.
|
||||
|
||||
Sources (read from the actual code, not the stale docs):
|
||||
- Packet layout: shadnet-p2p-main/documentation/protocol.md
|
||||
- Command IDs: shadnet-p2p-main/src/protocol.h (== the port's client.h;
|
||||
matching commands live at 100-115, NOT 12-23)
|
||||
- Message fields: the port's gpu/shadps4/shadnet/shadnet.proto (subset used
|
||||
by the client) cross-checked against shadnet-p2p's fuller proto.
|
||||
"""
|
||||
|
||||
import struct
|
||||
|
||||
import proto3
|
||||
|
||||
HEADER_SIZE = 15
|
||||
MAX_PACKET_SIZE = 0x800000
|
||||
PROTOCOL_VERSION = 1
|
||||
|
||||
PACKET_REQUEST = 0
|
||||
PACKET_REPLY = 1
|
||||
PACKET_NOTIFICATION = 2
|
||||
PACKET_SERVER_INFO = 3
|
||||
|
||||
|
||||
class Command:
|
||||
Login = 0
|
||||
Terminate = 1
|
||||
Create = 2
|
||||
Delete = 3
|
||||
AddFriend = 8
|
||||
RemoveFriend = 9
|
||||
AddBlock = 10
|
||||
RemoveBlock = 11
|
||||
GetServerFeatures = 12
|
||||
SetClientVersion = 13
|
||||
GetToken = 39
|
||||
SetAppearOffline = 40
|
||||
ContextStart = 100
|
||||
CreateRoom = 101
|
||||
JoinRoom = 102
|
||||
LeaveRoom = 103
|
||||
SearchRoom = 104
|
||||
RequestSignalingInfos = 105
|
||||
ContextStop = 106
|
||||
SetUserInfo = 107
|
||||
SetRoomDataInternal = 108
|
||||
SetRoomDataExternal = 109
|
||||
KickoutRoomMember = 110
|
||||
GetWorldInfoList = 111
|
||||
GetRoomDataExternalList = 112
|
||||
GetUserInfoList = 113
|
||||
GetRoomMemberDataExternalList = 114
|
||||
SendRoomMessage = 115
|
||||
GetBoardInfos = 30
|
||||
RecordScore = 31
|
||||
RecordScoreData = 32
|
||||
GetScoreData = 33
|
||||
GetScoreRange = 34
|
||||
GetScoreFriends = 35
|
||||
GetScoreNpid = 36
|
||||
|
||||
|
||||
class Error:
|
||||
NoError = 0
|
||||
Malformed = 1
|
||||
Invalid = 2
|
||||
InvalidInput = 3
|
||||
TooSoon = 4
|
||||
LoginError = 5
|
||||
LoginAlreadyLoggedIn = 6
|
||||
LoginInvalidPassword = 8
|
||||
LoginInvalidToken = 9
|
||||
RoomMissing = 14
|
||||
RoomAlreadyJoined = 15
|
||||
RoomFull = 16
|
||||
Unauthorized = 23
|
||||
NotFound = 26
|
||||
Unsupported = 33
|
||||
|
||||
|
||||
class Notify:
|
||||
FriendQuery = 5
|
||||
FriendNew = 6
|
||||
FriendLost = 7
|
||||
FriendStatus = 8
|
||||
RoomEvent = 10
|
||||
RoomMessage = 11
|
||||
|
||||
|
||||
# Orbis room event ids (bbhost np_matching2.cpp + shadnet-p2p matching.md)
|
||||
class RoomEvent:
|
||||
MemberJoined = 0x1101
|
||||
MemberLeft = 0x1102
|
||||
KickedOut = 0x1103
|
||||
RoomDestroyed = 0x1104
|
||||
RoomDataUpdatedInternal = 0x1106
|
||||
|
||||
|
||||
class EventCause:
|
||||
Leave = 1
|
||||
Kickout = 2
|
||||
ServerOperation = 4
|
||||
MemberDisappeared = 5
|
||||
|
||||
|
||||
# ---- protobuf message schemas (field number -> (name, kind)) ----------------
|
||||
|
||||
MATCHING_BIN_ATTR = {
|
||||
1: ("attr_id", "varint"),
|
||||
2: ("data", "bytes"),
|
||||
3: ("update_date", "varint"),
|
||||
4: ("update_member_id", "varint"),
|
||||
}
|
||||
|
||||
MATCHING_INT_ATTR = {
|
||||
1: ("attr_id", "varint"),
|
||||
2: ("attr_value", "varint"),
|
||||
}
|
||||
|
||||
MATCHING_ROOM_GROUP = {
|
||||
1: ("group_id", "varint"),
|
||||
2: ("has_passwd", "bool"),
|
||||
3: ("has_label", "bool"),
|
||||
4: ("label", "bytes"),
|
||||
5: ("slot_count", "varint"),
|
||||
6: ("num_members", "varint"),
|
||||
}
|
||||
|
||||
MATCHING_ROOM_MEMBER_DATA = {
|
||||
1: ("npid", "string"),
|
||||
2: ("member_id", "varint"),
|
||||
3: ("team_id", "varint"),
|
||||
4: ("is_owner", "bool"),
|
||||
5: ("group", ("message", MATCHING_ROOM_GROUP)),
|
||||
6: ("bin_attrs_internal", ("repeated", ("message", MATCHING_BIN_ATTR))),
|
||||
7: ("join_date", "varint"),
|
||||
8: ("nat_type", "varint"),
|
||||
9: ("flag_attr", "varint"),
|
||||
10: ("addr", "string"),
|
||||
11: ("port", "varint"),
|
||||
12: ("group_id", "varint"),
|
||||
13: ("account_id", "varint"),
|
||||
14: ("platform", "varint"),
|
||||
}
|
||||
|
||||
MATCHING_ROOM_DATA_INTERNAL = {
|
||||
1: ("public_slots", "varint"),
|
||||
2: ("private_slots", "varint"),
|
||||
3: ("open_public_slots", "varint"),
|
||||
4: ("open_private_slots", "varint"),
|
||||
5: ("max_slot", "varint"),
|
||||
6: ("server_id", "varint"),
|
||||
7: ("world_id", "varint"),
|
||||
8: ("lobby_id", "varint"),
|
||||
9: ("room_id", "varint"),
|
||||
10: ("passwd_slot_mask", "varint"),
|
||||
11: ("joined_slot_mask", "varint"),
|
||||
12: ("groups", ("repeated", ("message", MATCHING_ROOM_GROUP))),
|
||||
13: ("flags", "varint"),
|
||||
14: ("bin_attrs_internal", ("repeated", ("message", MATCHING_BIN_ATTR))),
|
||||
}
|
||||
|
||||
CREATE_JOIN_ROOM_RESPONSE = {
|
||||
1: ("room_data", ("message", MATCHING_ROOM_DATA_INTERNAL)),
|
||||
2: ("members", ("repeated", ("message", MATCHING_ROOM_MEMBER_DATA))),
|
||||
3: ("me_member_id", "varint"),
|
||||
4: ("owner_member_id", "varint"),
|
||||
}
|
||||
|
||||
MATCHING_WORLD = {
|
||||
1: ("world_id", "varint"),
|
||||
2: ("lobbies_num", "varint"),
|
||||
3: ("max_lobby_members", "varint"),
|
||||
4: ("lobby_members_num", "varint"),
|
||||
5: ("rooms_num", "varint"),
|
||||
6: ("room_members_num", "varint"),
|
||||
}
|
||||
|
||||
LOGIN_REQUEST = {
|
||||
1: ("npid", "string"),
|
||||
2: ("password", "string"),
|
||||
3: ("token", "string"),
|
||||
4: ("title_id", "string"),
|
||||
5: ("title_name", "string"),
|
||||
6: ("appear_offline", "bool"),
|
||||
}
|
||||
|
||||
LOGIN_REPLY = {
|
||||
1: ("avatar_url", "string"),
|
||||
2: ("user_id", "varint"),
|
||||
3: ("friends", ("repeated", ("message", {
|
||||
1: ("npid", "string"),
|
||||
2: ("online", "bool"),
|
||||
3: ("presence", "bytes"),
|
||||
}))),
|
||||
4: ("friend_requests_sent", ("repeated", "string")),
|
||||
5: ("friend_requests_received", ("repeated", "string")),
|
||||
6: ("blocked", ("repeated", "string")),
|
||||
}
|
||||
|
||||
SERVER_FEATURES_REPLY = {1: ("matching2_enabled", "bool")}
|
||||
|
||||
GET_TOKEN_REPLY = {
|
||||
1: ("token", "string"),
|
||||
2: ("user_id", "varint"),
|
||||
3: ("npid", "string"),
|
||||
}
|
||||
|
||||
CONTEXT_START_REQUEST = {1: ("ctx_id", "varint")}
|
||||
CONTEXT_STOP_REQUEST = {1: ("ctx_id", "varint")}
|
||||
|
||||
CREATE_ROOM_REQUEST = {
|
||||
1: ("req_id", "varint"),
|
||||
2: ("max_slots", "varint"),
|
||||
3: ("team_id", "varint"),
|
||||
4: ("world_id", "varint"),
|
||||
5: ("lobby_id", "varint"),
|
||||
6: ("flags", "varint"),
|
||||
7: ("group_config_count", "varint"),
|
||||
8: ("allowed_user_count", "varint"),
|
||||
9: ("blocked_user_count", "varint"),
|
||||
10: ("internal_bin_attr_count", "varint"),
|
||||
11: ("external_search_int_attrs", ("repeated", ("message", MATCHING_INT_ATTR))),
|
||||
12: ("external_search_bin_attrs", ("repeated", ("message", MATCHING_BIN_ATTR))),
|
||||
13: ("external_bin_attrs", ("repeated", ("message", MATCHING_BIN_ATTR))),
|
||||
14: ("member_bin_attrs", ("repeated", ("message", MATCHING_BIN_ATTR))),
|
||||
15: ("join_group_label_present", "bool"),
|
||||
16: ("room_password_present", "bool"),
|
||||
17: ("sig_type", "varint"),
|
||||
18: ("sig_flag", "varint"),
|
||||
19: ("sig_main_member", "varint"),
|
||||
20: ("internal_bin_attrs", ("repeated", ("message", MATCHING_BIN_ATTR))),
|
||||
21: ("user_id_kind", "varint"),
|
||||
22: ("allowed_online_ids", ("repeated", "string")),
|
||||
23: ("blocked_online_ids", ("repeated", "string")),
|
||||
24: ("allowed_account_ids", ("repeated", "varint")),
|
||||
25: ("blocked_account_ids", ("repeated", "varint")),
|
||||
26: ("group_configs", ("repeated", ("message", {
|
||||
1: ("slot_count", "varint"),
|
||||
2: ("has_label", "bool"),
|
||||
3: ("label", "bytes"),
|
||||
4: ("has_passwd", "bool"),
|
||||
}))),
|
||||
27: ("room_password", "bytes"),
|
||||
28: ("has_passwd_slot_mask", "bool"),
|
||||
29: ("passwd_slot_mask", "varint"),
|
||||
30: ("join_group_label", "bytes"),
|
||||
}
|
||||
|
||||
CREATE_ROOM_REPLY = {
|
||||
1: ("room_id", "varint"),
|
||||
2: ("server_id", "varint"),
|
||||
3: ("world_id", "varint"),
|
||||
4: ("lobby_id", "varint"),
|
||||
5: ("member_id", "varint"),
|
||||
6: ("max_slots", "varint"),
|
||||
7: ("flags", "varint"),
|
||||
8: ("cur_members", "varint"),
|
||||
9: ("details", ("message", CREATE_JOIN_ROOM_RESPONSE)),
|
||||
}
|
||||
|
||||
JOIN_ROOM_REQUEST = {
|
||||
1: ("room_id", "varint"),
|
||||
2: ("req_id", "varint"),
|
||||
3: ("team_id", "varint"),
|
||||
4: ("join_flags", "varint"),
|
||||
5: ("blocked_user_count", "varint"),
|
||||
6: ("member_bin_attrs", ("repeated", ("message", MATCHING_BIN_ATTR))),
|
||||
7: ("room_password_present", "bool"),
|
||||
8: ("join_group_label_present", "bool"),
|
||||
9: ("user_id_kind", "varint"),
|
||||
10: ("blocked_online_ids", ("repeated", "string")),
|
||||
11: ("blocked_account_ids", ("repeated", "varint")),
|
||||
}
|
||||
|
||||
JOIN_ROOM_REPLY = {
|
||||
1: ("room_id", "varint"),
|
||||
2: ("member_id", "varint"),
|
||||
3: ("max_slots", "varint"),
|
||||
4: ("flags", "varint"),
|
||||
5: ("cur_members", "varint"),
|
||||
6: ("details", ("message", CREATE_JOIN_ROOM_RESPONSE)),
|
||||
}
|
||||
|
||||
LEAVE_ROOM_REQUEST = {1: ("room_id", "varint"), 2: ("req_id", "varint")}
|
||||
LEAVE_ROOM_REPLY = {1: ("room_id", "varint")}
|
||||
|
||||
REQUEST_SIGNALING_INFOS_REQUEST = {1: ("target_npid", "string")}
|
||||
REQUEST_SIGNALING_INFOS_REPLY = {
|
||||
1: ("target_npid", "string"),
|
||||
2: ("target_ip", "string"),
|
||||
3: ("target_port", "varint"),
|
||||
4: ("target_member_id", "varint"),
|
||||
}
|
||||
|
||||
SET_ROOM_DATA_INTERNAL_REQUEST = {
|
||||
1: ("req_id", "varint"),
|
||||
2: ("room_id", "varint"),
|
||||
3: ("flag_filter", "varint"),
|
||||
4: ("flag_attr", "varint"),
|
||||
5: ("bin_attrs", ("repeated", ("message", MATCHING_BIN_ATTR))),
|
||||
6: ("has_passwd_mask", "bool"),
|
||||
7: ("passwd_slot_mask", "varint"),
|
||||
}
|
||||
SET_ROOM_DATA_INTERNAL_REPLY = {1: ("room_id", "varint")}
|
||||
|
||||
SET_ROOM_DATA_EXTERNAL_REQUEST = {
|
||||
1: ("req_id", "varint"),
|
||||
2: ("room_id", "varint"),
|
||||
3: ("search_int_attrs", ("repeated", ("message", MATCHING_INT_ATTR))),
|
||||
4: ("search_bin_attrs", ("repeated", ("message", MATCHING_BIN_ATTR))),
|
||||
5: ("ext_bin_attrs", ("repeated", ("message", MATCHING_BIN_ATTR))),
|
||||
}
|
||||
SET_ROOM_DATA_EXTERNAL_REPLY = {1: ("room_id", "varint")}
|
||||
|
||||
KICKOUT_ROOM_MEMBER_REQUEST = {
|
||||
1: ("room_id", "varint"),
|
||||
2: ("req_id", "varint"),
|
||||
3: ("target_member_id", "varint"),
|
||||
4: ("block_kick_flag", "varint"),
|
||||
}
|
||||
KICKOUT_ROOM_MEMBER_REPLY = {1: ("room_id", "varint")}
|
||||
|
||||
SEARCH_ROOM_REPLY = {
|
||||
1: ("rooms", ("repeated", ("message", {
|
||||
1: ("max_slot", "varint"),
|
||||
2: ("cur_members", "varint"),
|
||||
3: ("flags", "varint"),
|
||||
4: ("server_id", "varint"),
|
||||
5: ("world_id", "varint"),
|
||||
6: ("lobby_id", "varint"),
|
||||
7: ("room_id", "varint"),
|
||||
}))),
|
||||
2: ("range_start", "varint"),
|
||||
3: ("range_total", "varint"),
|
||||
4: ("range_result", "varint"),
|
||||
}
|
||||
|
||||
GET_WORLD_INFO_LIST_REQUEST = {1: ("server_id", "varint")}
|
||||
GET_WORLD_INFO_LIST_REPLY = {1: ("worlds", ("repeated", ("message", MATCHING_WORLD)))}
|
||||
|
||||
FRIEND_COMMAND_REQUEST = {1: ("npid", "string")}
|
||||
|
||||
NOTIFY_ROOM_EVENT = {
|
||||
1: ("ctx_id", "varint"),
|
||||
2: ("room_id", "varint"),
|
||||
3: ("event", "varint"),
|
||||
4: ("event_cause", "varint"),
|
||||
5: ("error_code", "varint"),
|
||||
6: ("member", ("message", MATCHING_ROOM_MEMBER_DATA)),
|
||||
7: ("bin_attrs", ("repeated", ("message", MATCHING_BIN_ATTR))),
|
||||
8: ("flags", "varint"),
|
||||
9: ("has_passwd_mask", "bool"),
|
||||
10: ("passwd_slot_mask", "varint"),
|
||||
}
|
||||
|
||||
SET_APPEAR_OFFLINE_REQUEST = {1: ("appear_offline", "bool")}
|
||||
|
||||
|
||||
# ---- packet framing ---------------------------------------------------------
|
||||
|
||||
def build_packet(packet_type, command, packet_id, payload=b""):
|
||||
size = HEADER_SIZE + len(payload)
|
||||
return struct.pack("<BHIQ", packet_type, command, size, packet_id) + payload
|
||||
|
||||
|
||||
def build_server_info():
|
||||
return build_packet(PACKET_SERVER_INFO, 0, 0, struct.pack("<I", PROTOCOL_VERSION))
|
||||
|
||||
|
||||
def build_reply(command, packet_id, error, payload=b""):
|
||||
return build_packet(PACKET_REPLY, command, packet_id, bytes([error]) + payload)
|
||||
|
||||
|
||||
def build_notification(notification_type, payload):
|
||||
return build_packet(PACKET_NOTIFICATION, notification_type, 0, payload)
|
||||
|
||||
|
||||
def proto_blob(message_bytes):
|
||||
"""The u32-LE length prefix + message framing every proto payload uses."""
|
||||
return struct.pack("<I", len(message_bytes)) + message_bytes
|
||||
|
||||
|
||||
def parse_proto_blob(payload):
|
||||
if len(payload) < 4:
|
||||
return None
|
||||
(size,) = struct.unpack_from("<I", payload, 0)
|
||||
return payload[4:4 + size]
|
||||
|
||||
|
||||
def parse_header(buf):
|
||||
if len(buf) < HEADER_SIZE:
|
||||
return None
|
||||
packet_type, command, size, packet_id = struct.unpack_from("<BHIQ", buf, 0)
|
||||
return packet_type, command, size, packet_id
|
||||
@@ -0,0 +1,778 @@
|
||||
#!/usr/bin/env python3
|
||||
"""End-to-end tests for the C++ bridge (bin/bbbridge-cli.exe).
|
||||
|
||||
The same checks the Python bridge passed (its test_offline.py and test_p2p.py),
|
||||
run against the C++ build over real sockets: a fake bbhost server, a fake
|
||||
STUN/relay server, a shadNet client, fake port and peer UDP sockets. The
|
||||
protobuf / shadNet codecs used to read the bridge's answers are the Python
|
||||
bridge's own (proto3.py, shadnet_proto.py) - an independent implementation.
|
||||
|
||||
python tests/e2e/test_e2e.py [path\\to\\bbbridge-cli.exe] (Linux: the path to bbbridge-cli)
|
||||
"""
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import socket
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
import urllib.request
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
sys.path.insert(0, HERE)
|
||||
import proto3 # noqa: E402
|
||||
import shadnet_proto as sp # noqa: E402
|
||||
|
||||
EXE = sys.argv[1] if len(sys.argv) > 1 else os.path.join(HERE, "..", "..", "bin", "bbbridge-cli.exe")
|
||||
HOST = "127.0.0.1"
|
||||
NPID, HOST_NPID = "tester01", "hostplayer"
|
||||
TIMEOUT = 5.0
|
||||
PASSED, FAILED = [], []
|
||||
|
||||
|
||||
def check(name, cond, detail=""):
|
||||
(PASSED if cond else FAILED).append((name, detail))
|
||||
print(" [%s] %s%s" % ("PASS" if cond else "FAIL", name, "" if cond else " " + str(detail)))
|
||||
|
||||
|
||||
def account_id_for(npid):
|
||||
d = hashlib.sha256(("bbbridge:" + npid).encode()).digest()
|
||||
return (1 << 52) | (int.from_bytes(d[:8], "big") & ((1 << 48) - 1))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- fake bbhost
|
||||
|
||||
class FakeBBHost(BaseHTTPRequestHandler):
|
||||
protocol_version = "HTTP/1.1"
|
||||
|
||||
def log_message(self, *a):
|
||||
pass
|
||||
|
||||
def _json(self, obj, status=200):
|
||||
body = json.dumps(obj).encode()
|
||||
self.send_response(status)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.send_header("Content-Length", str(len(body)))
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
|
||||
def do_POST(self):
|
||||
n = int(self.headers.get("Content-Length", 0) or 0)
|
||||
try:
|
||||
body = json.loads(self.rfile.read(n).decode()) if n else {}
|
||||
except ValueError:
|
||||
body = {}
|
||||
path = self.path.split("?")[0]
|
||||
st = self.server.state
|
||||
with st["lock"]:
|
||||
st["calls"].append(path)
|
||||
emit = st["emit_event"]
|
||||
event = st.get("event")
|
||||
r = {"ResKind": 0}
|
||||
if path == "/auth/account/create":
|
||||
r.update(Token="tok-123", OnlineId=body.get("Name"), RecoveryCode="RC-42")
|
||||
elif path == "/auth/recover":
|
||||
r.update(Token="tok-456", OnlineId=body.get("Name"), RecoveryCode="RC-43")
|
||||
elif path == "/mp/matching2/create_room":
|
||||
r.update(RoomId=777, SessionId="sess-A", MemberId=1, OwnerMemberId=1, MaxMembers=5, Members=[])
|
||||
elif path == "/mp/matching2/join_room":
|
||||
r.update(SessionId="sess-B", MemberId=2, OwnerMemberId=1, MaxMembers=5, Members=[
|
||||
{"MemberId": 1, "OnlineId": HOST_NPID, "LocalAddr": "10.0.0.9", "LocalPort": 5551,
|
||||
"Addr": "9.9.9.8", "Port": 5552, "MappedAddr": "9.9.9.8", "MappedPort": 5552}])
|
||||
elif path == "/mp/matching2/heartbeat":
|
||||
r.update(InRoom=1)
|
||||
elif path == "/np/signaling/resolve":
|
||||
if self.headers.get("X-BB-Token") == "tok-expired":
|
||||
self._json({"ResKind": 1, "Message": "bad or expired token"}, 401)
|
||||
return
|
||||
r.update(Addr="9.9.9.9", Port=5555)
|
||||
elif path == "/auth/device/start":
|
||||
st["device_polls"] = 0
|
||||
r.update(DeviceCode="dev-1", UserCode="ABCD-EFGH", VerificationUri="http://example.test/link",
|
||||
Interval=1, ExpiresIn=60, Device=body.get("DeviceName"))
|
||||
elif path == "/auth/device/poll":
|
||||
st["device_polls"] = st.get("device_polls", 0) + 1
|
||||
if body.get("DeviceCode") == "dev-1" and st["device_polls"] >= 2:
|
||||
r.update(Status="approved", Token="tok-discord", OnlineId="discorduser")
|
||||
else:
|
||||
r.update(Status="pending")
|
||||
elif path == "/auth/web_code":
|
||||
if body.get("Token") != "tok-discord":
|
||||
self._json({"ResKind": 1, "Message": "unknown token"})
|
||||
return
|
||||
r.update(Code="WXYZ-1234")
|
||||
elif path == "/np/events/poll":
|
||||
r.update(NextCursor=body.get("Cursor", 0) + 1, HasEvent=bool(emit))
|
||||
if emit:
|
||||
r.update(event or {"Name": "room_member_joined", "MemberId": 2, "OnlineId": HOST_NPID,
|
||||
"RoomId": 777, "LocalAddr": "10.0.0.9", "LocalPort": 5551,
|
||||
"Addr": "9.9.9.8", "Port": 5552, "MappedAddr": "9.9.9.8",
|
||||
"MappedPort": 5552})
|
||||
elif path == "/summon_messenger/create":
|
||||
r.update(MessageId="SummonDataCreateResponse")
|
||||
st["summon_token"] = self.headers.get("X-BB-Token", "")
|
||||
elif path == "/gs/echo":
|
||||
r.update(Token=self.headers.get("X-BB-Token", ""), UA=self.headers.get("User-Agent", ""), Body=body)
|
||||
elif path not in ("/mp/matching2/context_start", "/mp/matching2/leave_room", "/auth/logout",
|
||||
"/mp/matching2/signaling_update", "/np/events/ack"):
|
||||
self._json({"ResKind": 1, "Message": "unknown path"}, 404)
|
||||
return
|
||||
self._json(r)
|
||||
|
||||
|
||||
class QuietServer(ThreadingHTTPServer):
|
||||
def handle_error(self, request, client_address):
|
||||
pass # a bridge we terminate drops its keep-alive connections; that is expected
|
||||
|
||||
|
||||
def fake_bbhost(port):
|
||||
srv = QuietServer((HOST, port), FakeBBHost)
|
||||
srv.state = {"lock": threading.Lock(), "calls": [], "emit_event": False, "event": None}
|
||||
threading.Thread(target=srv.serve_forever, daemon=True).start()
|
||||
return srv
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- bridge process
|
||||
|
||||
class ExeBridge:
|
||||
def __init__(self, cfg):
|
||||
self.dir = tempfile.mkdtemp(prefix="bbbridge-e2e-")
|
||||
self.cfg = dict(cfg, state_file=os.path.join(self.dir, "state.json"), debug_endpoints=True)
|
||||
path = os.path.join(self.dir, "config.json")
|
||||
with open(path, "w") as fh:
|
||||
json.dump(self.cfg, fh)
|
||||
self.log = os.path.join(self.dir, "bridge.log")
|
||||
self.proc = subprocess.Popen([EXE, "--config", path, "--log", self.log],
|
||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
deadline = time.time() + 10
|
||||
while time.time() < deadline:
|
||||
try:
|
||||
urllib.request.urlopen("http://%s:%d/status" % (HOST, self.cfg["webapi_port"]), timeout=1).close()
|
||||
return
|
||||
except OSError:
|
||||
time.sleep(0.1)
|
||||
raise RuntimeError("bridge did not start; log: %s" % self.log)
|
||||
|
||||
def _url(self, path):
|
||||
return "http://%s:%d%s" % (HOST, self.cfg["webapi_port"], path)
|
||||
|
||||
def debug(self):
|
||||
with urllib.request.urlopen(self._url("/debug/state"), timeout=TIMEOUT) as r:
|
||||
return json.loads(r.read())
|
||||
|
||||
def post(self, path, obj):
|
||||
req = urllib.request.Request(self._url(path), data=json.dumps(obj).encode(), method="POST",
|
||||
headers={"Content-Type": "application/json"})
|
||||
with urllib.request.urlopen(req, timeout=TIMEOUT) as r:
|
||||
return json.loads(r.read())
|
||||
|
||||
def peer(self, online_id):
|
||||
return next((p for p in self.debug()["peers"] if p["online_id"] == online_id), None)
|
||||
|
||||
def stop(self):
|
||||
self.proc.terminate()
|
||||
self.proc.wait(10)
|
||||
|
||||
|
||||
class ShadNetClient:
|
||||
def __init__(self, port):
|
||||
self.sock = socket.create_connection((HOST, port), timeout=TIMEOUT)
|
||||
self.next_id = 1
|
||||
self.queue = []
|
||||
ptype, _, _, payload = self._read()
|
||||
assert ptype == sp.PACKET_SERVER_INFO and struct.unpack("<I", payload)[0] == sp.PROTOCOL_VERSION
|
||||
|
||||
def _exact(self, n):
|
||||
buf = b""
|
||||
while len(buf) < n:
|
||||
c = self.sock.recv(n - len(buf))
|
||||
if not c:
|
||||
raise ConnectionError("closed")
|
||||
buf += c
|
||||
return buf
|
||||
|
||||
def _read(self):
|
||||
ptype, cmd, size, pid = sp.parse_header(self._exact(sp.HEADER_SIZE))
|
||||
return ptype, cmd, pid, self._exact(size - sp.HEADER_SIZE) if size > sp.HEADER_SIZE else b""
|
||||
|
||||
def request(self, command, msg=None):
|
||||
payload = sp.proto_blob(msg) if msg is not None else b""
|
||||
self.sock.sendall(sp.build_packet(sp.PACKET_REQUEST, command, self.next_id, payload))
|
||||
self.next_id += 1
|
||||
deadline = time.time() + TIMEOUT * 2
|
||||
while time.time() < deadline:
|
||||
ptype, cmd, _, raw = self._read()
|
||||
if ptype == sp.PACKET_REPLY and cmd == command:
|
||||
return (raw[0] if raw else 255), sp.parse_proto_blob(raw[1:])
|
||||
self.queue.append((ptype, cmd, raw))
|
||||
raise TimeoutError(command)
|
||||
|
||||
def notification(self, kind, timeout=3.0):
|
||||
deadline = time.time() + timeout
|
||||
while time.time() < deadline:
|
||||
for i, (ptype, cmd, raw) in enumerate(self.queue):
|
||||
if ptype == sp.PACKET_NOTIFICATION and cmd == kind:
|
||||
return self.queue.pop(i)[2]
|
||||
try:
|
||||
self.sock.settimeout(0.5)
|
||||
ptype, cmd, _, raw = self._read()
|
||||
except (socket.timeout, TimeoutError):
|
||||
continue
|
||||
finally:
|
||||
self.sock.settimeout(TIMEOUT)
|
||||
self.queue.append((ptype, cmd, raw))
|
||||
return None
|
||||
|
||||
def close(self):
|
||||
self.sock.close()
|
||||
|
||||
|
||||
def stun_ping(npid, udp_port):
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
s.settimeout(TIMEOUT)
|
||||
s.sendto(b"\x01" + npid.encode()[:16].ljust(16, b"\0") + socket.inet_aton(HOST), (HOST, udp_port))
|
||||
data, _ = s.recvfrom(64)
|
||||
port = s.getsockname()[1]
|
||||
s.close()
|
||||
return socket.inet_ntoa(data[:4]), struct.unpack(">H", data[4:6])[0], port
|
||||
|
||||
|
||||
def get_json(url):
|
||||
with urllib.request.urlopen(url, timeout=TIMEOUT) as r:
|
||||
return r.status, json.loads(r.read())
|
||||
|
||||
|
||||
def post_json(url, obj, headers=None):
|
||||
req = urllib.request.Request(url, data=json.dumps(obj).encode(), method="POST",
|
||||
headers=dict({"Content-Type": "application/json"}, **(headers or {})))
|
||||
with urllib.request.urlopen(req, timeout=TIMEOUT) as r:
|
||||
return json.loads(r.read())
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- A: server path
|
||||
|
||||
def part_a():
|
||||
print("-- A: server path (gateway off)")
|
||||
ports = {"tcp": 42313, "udp": 42314, "webapi": 42315, "bbhost": 42671, "gs": 42316}
|
||||
fake = fake_bbhost(ports["bbhost"])
|
||||
up = "http://%s:%d" % (HOST, ports["bbhost"])
|
||||
br = ExeBridge({"bbhost_server": up, "community_server": up, "bind_host": HOST,
|
||||
"tcp_port": ports["tcp"], "udp_port": ports["udp"], "webapi_port": ports["webapi"],
|
||||
"p2p_gateway": False, "local_addr": HOST, "poll_interval": 0.05,
|
||||
"game_proxy_routes": {str(ports["gs"]): up}, "create_accounts": True})
|
||||
c = None
|
||||
try:
|
||||
c = ShadNetClient(ports["tcp"])
|
||||
err, blob = c.request(sp.Command.Login, proto3.encode_message(sp.LOGIN_REQUEST, {1: NPID, 4: "CUSA03173"}))
|
||||
reply = proto3.decode_message(sp.LOGIN_REPLY, blob) if blob else {}
|
||||
check("login accepted with the per-player account id",
|
||||
err == 0 and reply.get("user_id") == account_id_for(NPID), (err, reply))
|
||||
check("bbhost account created", "/auth/account/create" in fake.state["calls"])
|
||||
err, blob = c.request(sp.Command.GetServerFeatures)
|
||||
check("matching2 enabled", err == 0 and proto3.decode_message(sp.SERVER_FEATURES_REPLY, blob).get("matching2_enabled") is True)
|
||||
err, _ = c.request(sp.Command.SetClientVersion)
|
||||
check("client version accepted", err == 0, err)
|
||||
err, blob = c.request(sp.Command.GetToken)
|
||||
tok = proto3.decode_message(sp.GET_TOKEN_REPLY, blob)
|
||||
check("token relayed", err == 0 and tok.get("token") == "tok-123" and tok.get("npid") == NPID, tok)
|
||||
|
||||
ip, port, local = stun_ping(NPID, ports["udp"])
|
||||
check("stun reply", ip == HOST and port == local, (ip, port, local))
|
||||
time.sleep(0.2)
|
||||
check("stun registry", br.debug()["registry"].get(NPID) == [HOST, local], br.debug()["registry"])
|
||||
# bbport sends its ping inside a P2P signaling frame (FF FF FF FF first): registered, no echo.
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
s.settimeout(0.5)
|
||||
s.sendto(b"\xff\xff\xff\xff\x01" + b"framed".ljust(16, b"\0") + socket.inet_aton(HOST), (HOST, ports["udp"]))
|
||||
framed_port = s.getsockname()[1]
|
||||
try:
|
||||
s.recvfrom(64)
|
||||
echoed = True
|
||||
except socket.timeout:
|
||||
echoed = False
|
||||
s.close()
|
||||
check("framed stun ping registered without an echo",
|
||||
br.debug()["registry"].get("framed") == [HOST, framed_port] and not echoed, br.debug()["registry"])
|
||||
|
||||
err, _ = c.request(sp.Command.ContextStart, proto3.encode_message(sp.CONTEXT_START_REQUEST, {1: 7}))
|
||||
check("context_start", err == 0 and "/mp/matching2/context_start" in fake.state["calls"], err)
|
||||
|
||||
err, blob = c.request(sp.Command.CreateRoom, proto3.encode_message(sp.CREATE_ROOM_REQUEST, {1: 1, 2: 5}))
|
||||
reply = proto3.decode_message(sp.CREATE_ROOM_REPLY, blob) if blob else {}
|
||||
check("create_room accepted", err == 0 and reply.get("room_id") == 777 and reply.get("member_id") == 1
|
||||
and reply.get("max_slots") == 5, (err, reply))
|
||||
details = reply.get("details") or {}
|
||||
check("create_room details", details.get("me_member_id") == 1 and details.get("owner_member_id") == 1
|
||||
and (details.get("room_data") or {}).get("room_id") == 777, details)
|
||||
me = [m for m in details.get("members") or [] if m.get("member_id") == 1]
|
||||
check("create_room lists ourselves (port needs members.me/owner)",
|
||||
len(me) == 1 and me[0].get("npid") == NPID and me[0].get("is_owner") is True, details.get("members"))
|
||||
|
||||
with fake.state["lock"]:
|
||||
fake.state["emit_event"] = True
|
||||
raw = c.notification(sp.Notify.RoomEvent)
|
||||
with fake.state["lock"]:
|
||||
fake.state["emit_event"] = False
|
||||
ev = proto3.decode_message(sp.NOTIFY_ROOM_EVENT, sp.parse_proto_blob(raw)) if raw else {}
|
||||
check("room_member_joined notification", ev.get("event") == sp.RoomEvent.MemberJoined
|
||||
and ev.get("room_id") == 777 and (ev.get("member") or {}).get("npid") == HOST_NPID
|
||||
and (ev.get("member") or {}).get("member_id") == 2, ev)
|
||||
|
||||
err, blob = c.request(sp.Command.RequestSignalingInfos,
|
||||
proto3.encode_message(sp.REQUEST_SIGNALING_INFOS_REQUEST, {1: HOST_NPID}))
|
||||
sig = proto3.decode_message(sp.REQUEST_SIGNALING_INFOS_REPLY, blob) if blob else {}
|
||||
check("signaling resolve", err == 0 and sig.get("target_ip") == "9.9.9.9" and sig.get("target_port") == 5555, sig)
|
||||
|
||||
webapi = "http://%s:%d" % (HOST, ports["webapi"])
|
||||
check("webapi /status", get_json(webapi + "/status")[1].get("ok") is True)
|
||||
ok = True
|
||||
for name in ("friendList", "blockList"):
|
||||
status, body = get_json("%s/v1/users/%s/%s?fields=npId&limit=50" % (webapi, NPID, name))
|
||||
ok = ok and status == 200 and body.get(name) == [] and body.get("totalResults") == 0
|
||||
check("webapi friendList/blockList answer empty lists", ok)
|
||||
ad = {"SessionId": "sess-A", "UserId": 1, "AreaId": 3, "SummonData": "QUJD", "MatchingLevel": 44}
|
||||
r = post_json(webapi + "/summon_messenger/create", ad)
|
||||
check("summon create proxied", r.get("ResKind") == 0 and r.get("MessageId") == "SummonDataCreateResponse", r)
|
||||
check("summon proxy carries the account token", fake.state.get("summon_token") == "tok-123",
|
||||
fake.state.get("summon_token"))
|
||||
echo = post_json("http://%s:%d/gs/echo" % (HOST, ports["gs"]), {"x": 1}, {"User-Agent": "BloodborneGame"})
|
||||
check("game proxy forwards with token and own UA", echo.get("Token") == "tok-123" and echo.get("Body") == {"x": 1}
|
||||
and echo.get("UA", "").startswith("BloodborneGame bbbridge/"), echo)
|
||||
check("advertisement captured", (br.debug().get("last_advertisement") or {}).get("AreaId") == 3)
|
||||
|
||||
err, blob = c.request(sp.Command.LeaveRoom, proto3.encode_message(sp.LEAVE_ROOM_REQUEST, {1: 777, 2: 2}))
|
||||
check("leave_room accepted", err == 0 and proto3.decode_message(sp.LEAVE_ROOM_REPLY, blob).get("room_id") == 777
|
||||
and "/mp/matching2/leave_room" in fake.state["calls"])
|
||||
err, _ = c.request(sp.Command.Terminate)
|
||||
closed = False
|
||||
try:
|
||||
c.sock.settimeout(2)
|
||||
closed = not c.sock.recv(16)
|
||||
except OSError:
|
||||
closed = True
|
||||
check("terminate closes the connection", err == 0 and closed)
|
||||
|
||||
c2 = ShadNetClient(ports["tcp"])
|
||||
err, _ = c2.request(sp.Command.Login, proto3.encode_message(sp.LOGIN_REQUEST, {1: NPID}))
|
||||
_, blob = c2.request(sp.Command.GetToken)
|
||||
c2.close()
|
||||
check("second login uses the stored token, no recover",
|
||||
err == 0 and fake.state["calls"].count("/auth/recover") == 0
|
||||
and proto3.decode_message(sp.GET_TOKEN_REPLY, blob).get("token") == "tok-123")
|
||||
finally:
|
||||
if c:
|
||||
c.close()
|
||||
br.stop()
|
||||
fake.shutdown()
|
||||
fake.server_close()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- B: gateway
|
||||
|
||||
class FakeStun(threading.Thread):
|
||||
def __init__(self):
|
||||
super().__init__(daemon=True)
|
||||
self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
self.sock.bind((HOST, 0))
|
||||
self.sock.settimeout(0.2)
|
||||
self.port = self.sock.getsockname()[1]
|
||||
self.relay_port = 41000
|
||||
self.token = b"RELAYTOK"
|
||||
self.requests, self.relayed = [], []
|
||||
self.stop = False
|
||||
|
||||
def run(self):
|
||||
while not self.stop:
|
||||
try:
|
||||
data, src = self.sock.recvfrom(65536)
|
||||
except (socket.timeout, OSError):
|
||||
continue
|
||||
if data[:2] == b"\xfbR":
|
||||
self.relayed.append(data)
|
||||
continue
|
||||
if len(data) < 20 or struct.unpack(">H", data[:2])[0] != 1:
|
||||
continue
|
||||
self.requests.append(data)
|
||||
attrs = struct.pack(">HHBBH4s", 0x0001, 8, 0, 1, 5678, socket.inet_aton("1.2.3.4"))
|
||||
if self.relay_port and len(data) >= 36 and data[20:22] == b"\x81\x00":
|
||||
v = b"bbr1" + self.token + struct.pack(">H", self.relay_port) + socket.inet_aton(src[0]) + struct.pack(">H", src[1])
|
||||
attrs += struct.pack(">HH", 0x8101, len(v)) + v
|
||||
self.sock.sendto(struct.pack(">HH", 0x0101, len(attrs)) + data[4:20] + attrs, src)
|
||||
|
||||
|
||||
def udp(timeout=2.0):
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
s.bind((HOST, 0))
|
||||
s.settimeout(timeout)
|
||||
return s
|
||||
|
||||
|
||||
def recv_until(sock, pred, timeout=2.0):
|
||||
deadline = time.time() + timeout
|
||||
while time.time() < deadline:
|
||||
sock.settimeout(max(0.05, deadline - time.time()))
|
||||
try:
|
||||
data, src = sock.recvfrom(65536)
|
||||
except socket.timeout:
|
||||
return None
|
||||
if pred(data):
|
||||
return data, src
|
||||
return None
|
||||
|
||||
|
||||
def handshake(kind, frm, to, from_conn=7, nonce=0):
|
||||
return b"SHAD" + bytes([0x07, kind]) + frm.encode().ljust(16, b"\0") + to.encode().ljust(16, b"\0") + \
|
||||
struct.pack("<IIIHHQ", from_conn, 0, 0, 0, 0, nonce)
|
||||
|
||||
|
||||
def m2(kind, room, frm_member, to_member, frm_id, nonce=0):
|
||||
return b"SHAD" + bytes([0x21, kind]) + struct.pack("<QHH", room, frm_member, to_member) + \
|
||||
frm_id.encode().ljust(16, b"\0") + struct.pack("<IHHQ", 0, 0, 0, nonce)
|
||||
|
||||
|
||||
def part_b():
|
||||
print("-- B: gateway over real sockets")
|
||||
stun = FakeStun()
|
||||
stun.start()
|
||||
ports = {"tcp": 43313, "udp": 43314, "webapi": 43315}
|
||||
br = ExeBridge({"bbhost_server": "http://%s:1" % HOST, "bind_host": HOST, "tcp_port": ports["tcp"],
|
||||
"udp_port": ports["udp"], "webapi_port": ports["webapi"], "local_addr": HOST,
|
||||
"stun_server": "%s:%d" % (HOST, stun.port), "stun_refresh": 0.3, "game_proxy_routes": {}})
|
||||
port_sock, peer_sock = udp(), udp()
|
||||
try:
|
||||
deadline = time.time() + 4
|
||||
while time.time() < deadline and not (br.debug().get("mapped") and br.debug().get("relay_port")):
|
||||
time.sleep(0.1)
|
||||
st = br.debug()
|
||||
check("STUN request carries BBHOST-HELLO", bool(stun.requests) and stun.requests[0][20:24] == b"\x81\x00\x00\x0c")
|
||||
check("STUN mapping learned", st.get("mapped") == ["1.2.3.4", 5678], st.get("mapped"))
|
||||
check("relay turned on", st.get("relay_port") == 41000, st.get("relay_port"))
|
||||
|
||||
stun.relay_port = 0 # direct part: the next refresh turns the relay off
|
||||
deadline = time.time() + 3
|
||||
while time.time() < deadline and br.debug().get("relay_port"):
|
||||
time.sleep(0.1)
|
||||
inner = tuple(br.post("/debug/add_peer", {
|
||||
"member_id": 2, "online_id": "hunter", "endpoint": list(peer_sock.getsockname()),
|
||||
"port_online_id": "porthost", "port_member_id": 1, "room_id": 777,
|
||||
"port_endpoint": list(port_sock.getsockname())})["inner"])
|
||||
check("inner endpoint on loopback", inner[0] == HOST and inner[1] > 0, inner)
|
||||
opening = recv_until(port_sock, lambda d: d[4:9] == b"SHAD\x10")
|
||||
check("peer opens with ActivationRequest to the port",
|
||||
opening is not None and opening[0][9] == 1 and opening[1] == inner, opening)
|
||||
|
||||
port_sock.sendto(struct.pack(">HH", 40, 30) + b"hello-peer", inner)
|
||||
got = recv_until(peer_sock, lambda d: d[:1] == b"\xff")
|
||||
check("port game data reaches the peer in bbhost framing",
|
||||
got is not None and got[0] == b"\xff\xc3\x28\x1ehello-peer", got)
|
||||
outer_port = got[1][1] if got else 0
|
||||
peer_sock.sendto(b"\xff\xc3\x1e\x28hello-port", (HOST, outer_port))
|
||||
got = recv_until(port_sock, lambda d: d.endswith(b"hello-port"))
|
||||
check("peer game data reaches the port in port framing",
|
||||
got is not None and got[0] == struct.pack(">HH", 30, 40) + b"hello-port" and got[1] == inner, got)
|
||||
peer_sock.sendto(b"\xfebbhp\0\0\0", (HOST, outer_port))
|
||||
got = recv_until(port_sock, lambda d: d[:4] != b"\xff\xff\xff\xff", timeout=0.4)
|
||||
check("hole-punch probe not delivered to the port", got is None, got)
|
||||
|
||||
sig = struct.pack(">HH", 0xFFFF, 0xFFFF)
|
||||
port_sock.sendto(sig + handshake(1, "porthost", "hunter"), inner)
|
||||
check("port's handshake Offer answered with Accept",
|
||||
recv_until(port_sock, lambda d: d[4:10] == b"SHAD\x07\x02") is not None)
|
||||
port_sock.sendto(sig + m2(3, 777, 1, 2, "porthost", nonce=42), inner)
|
||||
got = recv_until(port_sock, lambda d: d[4:10] == b"SHAD\x21\x04")
|
||||
check("port's matching2 Check answered with CheckAck",
|
||||
got is not None and struct.unpack("<Q", got[0][4 + 42:4 + 50])[0] == 42)
|
||||
|
||||
stun.relay_port = 41000
|
||||
deadline = time.time() + 3
|
||||
while time.time() < deadline and not br.debug().get("relay_port"):
|
||||
time.sleep(0.1)
|
||||
inner3 = tuple(br.post("/debug/add_peer", {
|
||||
"member_id": 3, "online_id": "relayed", "endpoint": [HOST, 41001], "port_online_id": "porthost",
|
||||
"port_member_id": 1, "room_id": 777, "port_endpoint": list(port_sock.getsockname())})["inner"])
|
||||
port_sock.sendto(struct.pack(">HH", 40, 30) + b"via-relay", inner3)
|
||||
deadline = time.time() + 2
|
||||
while time.time() < deadline and not stun.relayed:
|
||||
time.sleep(0.05)
|
||||
expect = b"\xfbR" + stun.token + struct.pack(">H", 41001) + b"\xff\xc3\x28\x1evia-relay"
|
||||
check("datagram for a relayed peer goes framed to the STUN port", stun.relayed[:1] == [expect], stun.relayed[:1])
|
||||
stun.sock.sendto(b"\xfbr" + struct.pack(">H", 41001) + b"\xff\xc3\x1e\x28back", (HOST, outer_port))
|
||||
got = recv_until(port_sock, lambda d: d.endswith(b"back"))
|
||||
check("relay delivery reaches the port from the right member", got is not None and got[1] == inner3, got)
|
||||
|
||||
br.post("/debug/remove_peer", {"member_id": 2})
|
||||
check("removed peer forgotten", br.peer("hunter") is None)
|
||||
finally:
|
||||
stun.stop = True
|
||||
br.stop()
|
||||
port_sock.close()
|
||||
peer_sock.close()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- C: bridge + gateway
|
||||
|
||||
def part_c():
|
||||
print("-- C: bridge flows with the gateway on")
|
||||
ports = {"tcp": 44313, "udp": 44314, "webapi": 44315, "bbhost": 44671}
|
||||
fake = fake_bbhost(ports["bbhost"])
|
||||
up = "http://%s:%d" % (HOST, ports["bbhost"])
|
||||
br = ExeBridge({"bbhost_server": up, "community_server": up, "bind_host": HOST, "tcp_port": ports["tcp"],
|
||||
"udp_port": ports["udp"], "webapi_port": ports["webapi"], "local_addr": HOST,
|
||||
"poll_interval": 0.05, "stun_server": "off", "game_proxy_routes": {}, "create_accounts": True})
|
||||
c = None
|
||||
try:
|
||||
c = ShadNetClient(ports["tcp"])
|
||||
c.request(sp.Command.Login, proto3.encode_message(sp.LOGIN_REQUEST, {1: NPID}))
|
||||
stun_ping(NPID, ports["udp"])
|
||||
c.request(sp.Command.ContextStart, proto3.encode_message(sp.CONTEXT_START_REQUEST, {1: 7}))
|
||||
with fake.state["lock"]:
|
||||
fake.state["event"] = {"Name": "guest_invite", "RoomId": 777, "HostOnlineId": HOST_NPID,
|
||||
"HostAddr": "9.9.9.8", "HostPort": 5552, "HostLocalAddr": "10.0.0.9",
|
||||
"HostLocalPort": 5551, "HostMappedAddr": "9.9.9.8", "HostMappedPort": 5552,
|
||||
"HostArea": 3, "HostLevel": 26}
|
||||
fake.state["emit_event"] = True
|
||||
deadline = time.time() + 3
|
||||
while time.time() < deadline and br.peer(HOST_NPID) is None:
|
||||
time.sleep(0.05)
|
||||
with fake.state["lock"]:
|
||||
fake.state["emit_event"] = False
|
||||
fake.state["event"] = None
|
||||
seeded = br.peer(HOST_NPID)
|
||||
check("guest_invite seeds the host in the gateway", seeded is not None)
|
||||
|
||||
err, blob = c.request(sp.Command.JoinRoom, proto3.encode_message(sp.JOIN_ROOM_REQUEST, {1: 777, 2: 1}))
|
||||
reply = proto3.decode_message(sp.JOIN_ROOM_REPLY, blob) if blob else {}
|
||||
host = next((m for m in (reply.get("details") or {}).get("members") or [] if m.get("npid") == HOST_NPID), {})
|
||||
link = br.peer(HOST_NPID)
|
||||
check("joining the invited room keeps the host's inner endpoint",
|
||||
seeded is not None and link is not None and link["inner"] == seeded["inner"], (seeded, link))
|
||||
check("the join fills in the port's member id for the handshakes",
|
||||
link is not None and link["port_member_id"] == 2 and link["room_id"] == 777, link)
|
||||
check("joined room member points at the gateway",
|
||||
err == 0 and link is not None and host.get("addr") == HOST and host.get("port") == link["inner"][1], host)
|
||||
err, blob = c.request(sp.Command.RequestSignalingInfos,
|
||||
proto3.encode_message(sp.REQUEST_SIGNALING_INFOS_REQUEST, {1: HOST_NPID}))
|
||||
sig = proto3.decode_message(sp.REQUEST_SIGNALING_INFOS_REPLY, blob) if blob else {}
|
||||
check("signaling infos point at the gateway",
|
||||
err == 0 and link is not None and sig.get("target_ip") == HOST and sig.get("target_port") == link["inner"][1], sig)
|
||||
|
||||
err, blob = c.request(sp.Command.RequestSignalingInfos,
|
||||
proto3.encode_message(sp.REQUEST_SIGNALING_INFOS_REQUEST, {1: "summonedguest"}))
|
||||
sig2 = proto3.decode_message(sp.REQUEST_SIGNALING_INFOS_REPLY, blob) if blob else {}
|
||||
early = br.peer("summonedguest")
|
||||
check("signaling infos for a non-member resolve through the server",
|
||||
err == 0 and early is not None and sig2.get("target_port") == early["inner"][1], (sig2, early))
|
||||
with fake.state["lock"]:
|
||||
fake.state["event"] = {"Name": "room_member_joined", "MemberId": 3, "RoomId": 777,
|
||||
"OnlineId": "summonedguest", "Addr": "9.9.9.9", "Port": 5555}
|
||||
fake.state["emit_event"] = True
|
||||
deadline = time.time() + 3
|
||||
while time.time() < deadline and (br.peer("summonedguest") or {}).get("member_id") != 3:
|
||||
time.sleep(0.05)
|
||||
with fake.state["lock"]:
|
||||
fake.state["emit_event"] = False
|
||||
fake.state["event"] = None
|
||||
joined = br.peer("summonedguest")
|
||||
check("the guest joining keeps the inner endpoint the port already uses",
|
||||
joined is not None and early is not None and joined["member_id"] == 3 and joined["inner"] == early["inner"]
|
||||
and joined["peer_member_id"] == 3 and all(p["member_id"] > 0 for p in br.debug()["peers"]), joined)
|
||||
|
||||
time.sleep(5.6)
|
||||
check("heartbeat inside the server's 15 s window", fake.state["calls"].count("/mp/matching2/heartbeat") >= 1)
|
||||
c.request(sp.Command.LeaveRoom, proto3.encode_message(sp.LEAVE_ROOM_REQUEST, {1: 777}))
|
||||
check("leaving the room drops the peers", br.debug()["peers"] == [], br.debug()["peers"])
|
||||
finally:
|
||||
if c:
|
||||
c.close()
|
||||
br.stop()
|
||||
fake.shutdown()
|
||||
fake.server_close()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- D: Discord sign-in
|
||||
|
||||
def part_d():
|
||||
print("-- D: sign in with Discord, website code, expired token")
|
||||
port = 45671
|
||||
fake = fake_bbhost(port)
|
||||
up = "http://%s:%d" % (HOST, port)
|
||||
d = tempfile.mkdtemp(prefix="bbbridge-e2e-")
|
||||
state = os.path.join(d, "state.json")
|
||||
cfg_path = os.path.join(d, "config.json")
|
||||
with open(cfg_path, "w") as fh:
|
||||
json.dump({"auth_server": up, "bbhost_server": up, "state_file": state}, fh)
|
||||
try:
|
||||
r = subprocess.run([EXE, "--config", cfg_path, "--link-discord"], capture_output=True, text=True, timeout=30)
|
||||
check("device code shown to the player", "ABCD-EFGH" in r.stdout and "http://example.test/link" in r.stdout,
|
||||
r.stdout)
|
||||
check("approval signs in", r.returncode == 0 and "signed in as discorduser" in r.stdout, r.stdout + r.stderr)
|
||||
with open(state) as fh:
|
||||
acc = json.load(fh)["accounts"].get("discorduser", {})
|
||||
check("Discord account stored under its name",
|
||||
acc.get("token") == "tok-discord" and acc.get("linked") == "discord" and not acc.get("recovery_code"), acc)
|
||||
r = subprocess.run([EXE, "--config", cfg_path, "--web-code", "discorduser"], capture_output=True, text=True,
|
||||
timeout=30)
|
||||
check("website sign-in code", r.returncode == 0 and r.stdout.strip() == "WXYZ-1234", r.stdout + r.stderr)
|
||||
|
||||
def cli(*args):
|
||||
return subprocess.run([EXE, "--config", cfg_path, *args], capture_output=True, text=True, timeout=30)
|
||||
|
||||
def accounts():
|
||||
with open(state) as fh:
|
||||
return json.load(fh)["accounts"]
|
||||
|
||||
r = cli("--create", "pcplayer")
|
||||
check("create account on this PC", r.returncode == 0 and accounts().get("pcplayer", {}).get("recovery_code") == "RC-42"
|
||||
and accounts()["pcplayer"].get("token") == "tok-123", r.stdout + str(accounts().get("pcplayer")))
|
||||
r = cli("--recover", "pcplayer", "RC-42")
|
||||
check("recover account keeps the server's new code", r.returncode == 0
|
||||
and accounts()["pcplayer"].get("recovery_code") == "RC-43" and accounts()["pcplayer"].get("token") == "tok-456",
|
||||
accounts().get("pcplayer"))
|
||||
r = cli("--sign-out", "pcplayer")
|
||||
check("sign out ends the token, keeps the recovery code", r.returncode == 0 and "/auth/logout" in fake.state["calls"]
|
||||
and "token" not in accounts()["pcplayer"] and accounts()["pcplayer"].get("recovery_code") == "RC-43",
|
||||
accounts().get("pcplayer"))
|
||||
r = cli("--sign-out", "discorduser")
|
||||
check("signing out of a Discord account forgets it here", r.returncode == 0 and "discorduser" not in accounts())
|
||||
acc = dict(acc) # the bridge part below uses the Discord account as linked earlier
|
||||
creates = fake.state["calls"].count("/auth/account/create") # the bridge must add none below
|
||||
|
||||
br = ExeBridge({"bbhost_server": up, "community_server": up, "auth_server": up, "bind_host": HOST,
|
||||
"tcp_port": 45313, "udp_port": 45314, "webapi_port": 45315, "p2p_gateway": False,
|
||||
"local_addr": HOST, "game_proxy_routes": {}})
|
||||
try:
|
||||
with open(br.cfg["state_file"], "w") as fh:
|
||||
json.dump({"accounts": {"discorduser": acc, "expired": {
|
||||
"bbhost_name": "expired", "token": "tok-expired", "online_id": "expired", "linked": "discord"}}}, fh)
|
||||
c = ShadNetClient(45313)
|
||||
err, _ = c.request(sp.Command.Login, proto3.encode_message(sp.LOGIN_REQUEST, {1: "discorduser"}))
|
||||
_, blob = c.request(sp.Command.GetToken)
|
||||
c.close()
|
||||
check("the bridge plays as the Discord account",
|
||||
err == 0 and proto3.decode_message(sp.GET_TOKEN_REPLY, blob).get("token") == "tok-discord"
|
||||
and fake.state["calls"].count("/auth/account/create") == creates)
|
||||
c = ShadNetClient(45313)
|
||||
err, _ = c.request(sp.Command.Login, proto3.encode_message(sp.LOGIN_REQUEST, {1: "expired"}))
|
||||
c.close()
|
||||
with open(br.log) as fh:
|
||||
said = "has expired" in fh.read()
|
||||
check("an expired Discord sign-in is refused, not replaced by a new account",
|
||||
err == sp.Error.LoginError and said and fake.state["calls"].count("/auth/account/create") == creates, err)
|
||||
c = ShadNetClient(45313)
|
||||
err, _ = c.request(sp.Command.Login, proto3.encode_message(sp.LOGIN_REQUEST, {1: "stranger"}))
|
||||
c.close()
|
||||
with open(br.log) as fh:
|
||||
said = "use 'Sign in with Discord' in bbbridge first" in fh.read()
|
||||
check("no account on this PC: sign in with Discord first, nothing created",
|
||||
err == sp.Error.LoginError and said and fake.state["calls"].count("/auth/account/create") == creates, err)
|
||||
finally:
|
||||
br.stop()
|
||||
finally:
|
||||
fake.shutdown()
|
||||
fake.server_close()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- E: bbport setup
|
||||
|
||||
def part_e():
|
||||
print("-- E: routing a fresh bbport (sandboxed APPDATA)")
|
||||
tmp = tempfile.mkdtemp(prefix="bbbridge-e2e-")
|
||||
appdata, bbport = os.path.join(tmp, "AppData"), os.path.join(tmp, "bloodborne")
|
||||
user = os.path.join(bbport, "user")
|
||||
os.makedirs(os.path.join(appdata, "bbport-launcher"))
|
||||
os.makedirs(user)
|
||||
open(os.path.join(bbport, "Bloodborne.exe"), "wb").close()
|
||||
mine = {"https://example.test:1": "http://127.0.0.1:1"}
|
||||
with open(os.path.join(user, "host_overrides.custom.json"), "w") as fh:
|
||||
json.dump(mine, fh)
|
||||
sfile = os.path.join(appdata, "bbport-launcher", "settings.json")
|
||||
fresh = {"ui_language": "", "game_dir": os.path.join(bbport, "CUSA03173"), "user_dir": "", "online": False,
|
||||
"online_server": "srv.shadps4.net:31313", "online_webapi": "", "online_npid": "", "online_password": "",
|
||||
"online_upnp": True, "extra_env": "BB_GPU_LOG=info SHADPS4_P2P_PORT=1"}
|
||||
with open(sfile, "w") as fh:
|
||||
json.dump(fresh, fh)
|
||||
env = dict(os.environ, APPDATA=appdata)
|
||||
|
||||
def route(on):
|
||||
return subprocess.run([EXE, "--route", "on" if on else "off", "--bbport", bbport], env=env,
|
||||
capture_output=True, text=True, timeout=30)
|
||||
|
||||
def settings():
|
||||
with open(sfile) as fh:
|
||||
return json.load(fh)
|
||||
|
||||
r = route(True)
|
||||
s = settings()
|
||||
check("route on: bbport points at the bridge", r.returncode == 0 and "routed=1" in r.stdout and s["online"] is True
|
||||
and s["online_server"] == "127.0.0.1:31313" and s["online_webapi"] == "http://127.0.0.1:31315", r.stdout + str(s))
|
||||
check("route on: an empty password gets a placeholder (bbport skips online without one)",
|
||||
s["online_password"] == "bbbridge", s["online_password"])
|
||||
check("route on: extra variables merged, P2P port pinned once",
|
||||
s["extra_env"].split() == ["BB_GPU_LOG=info", "SHADPS4_P2P_PORT=3658"], s["extra_env"])
|
||||
check("route on: npid untouched, key order kept", s["online_npid"] == "" and list(s)[:4] == list(fresh)[:4], list(s))
|
||||
ov = json.load(open(os.path.join(user, "host_overrides.custom.json")))
|
||||
check("route on: overrides written, the player's own backed up",
|
||||
"_bbbridge" in ov and json.load(open(os.path.join(user, "host_overrides.custom.json.before-bbbridge"))) == mine)
|
||||
r = route(False)
|
||||
s = settings()
|
||||
check("route off: back to shadNet, password and variables restored",
|
||||
r.returncode == 0 and s["online_server"] == "srv.shadps4.net:31313" and s["online_webapi"] == ""
|
||||
and s["online_password"] == "" and s["extra_env"] == "BB_GPU_LOG=info", s)
|
||||
check("route off: the player's own overrides restored",
|
||||
json.load(open(os.path.join(user, "host_overrides.custom.json"))) == mine)
|
||||
s["online_password"] = "secret"
|
||||
with open(sfile, "w") as fh:
|
||||
json.dump(s, fh)
|
||||
route(True)
|
||||
on_pw = settings()["online_password"]
|
||||
route(False)
|
||||
check("a real shadNet password is left alone", on_pw == "secret" and settings()["online_password"] == "secret",
|
||||
(on_pw, settings()["online_password"]))
|
||||
|
||||
def set_settings(**kv):
|
||||
s = settings()
|
||||
s.update(kv)
|
||||
with open(sfile, "w") as fh:
|
||||
json.dump(s, fh)
|
||||
|
||||
# the Python bbbridge's file is ours too: replaced, then removed - not kept as the player's
|
||||
ovfile = os.path.join(user, "host_overrides.custom.json")
|
||||
with open(ovfile, "w") as fh:
|
||||
json.dump({"_note": "bbbridge: the game's own server traffic goes through the bridge",
|
||||
"https://thehuntersdream.com:20443": "http://127.0.0.1:31316"}, fh)
|
||||
route(True)
|
||||
route(False)
|
||||
check("route off: an old (Python) bbbridge overrides file is removed, not restored",
|
||||
not os.path.exists(ovfile) and not os.path.exists(ovfile + ".before-bbbridge"),
|
||||
os.listdir(user))
|
||||
|
||||
# the app puts the Hunter's Dream account in while routed; off brings the shadNet one back
|
||||
for name, pw in (("hunter_sn", "secret"), ("renamed_sn", "secret2")): # also when changed in between
|
||||
set_settings(online_npid=name, online_password=pw)
|
||||
route(True)
|
||||
set_settings(online_npid="discord_name")
|
||||
route(False)
|
||||
s = settings()
|
||||
check("route off: the shadNet account comes back (%s)" % name,
|
||||
s["online_npid"] == name and s["online_password"] == pw and s["online_server"] == "srv.shadps4.net:31313",
|
||||
(s["online_npid"], s["online_server"]))
|
||||
|
||||
|
||||
def main():
|
||||
print("== bbbridge C++ end-to-end (%s) ==" % os.path.abspath(EXE))
|
||||
part_a()
|
||||
part_b()
|
||||
part_c()
|
||||
part_d()
|
||||
if os.name == "nt": # routing bbport for Windows (setup.cpp); on Linux the bbport launcher does it
|
||||
part_e()
|
||||
print("\n%d passed, %d failed" % (len(PASSED), len(FAILED)))
|
||||
sys.exit(1 if FAILED else 0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
*/
|
||||
Vendored
+16789
File diff suppressed because it is too large.
Load diff
Vendored
+3714
File diff suppressed because it is too large.
Load diff
+4674
File diff suppressed because it is too large.
Load diff
+618
@@ -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
@@ -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
@@ -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*)¤t_time);
|
||||
io.DeltaTime = (float)(current_time - bd->Time) / bd->TicksPerSecond;
|
||||
bd->Time = current_time;
|
||||
|
||||
// Update OS mouse position
|
||||
ImGui_ImplWin32_UpdateMouseData(io);
|
||||
|
||||
// Process workarounds for known Windows key handling issues
|
||||
ImGui_ImplWin32_ProcessKeyEventsWorkarounds(io);
|
||||
|
||||
// Update OS mouse cursor with the cursor requested by imgui
|
||||
ImGuiMouseCursor mouse_cursor = io.MouseDrawCursor ? ImGuiMouseCursor_None : ImGui::GetMouseCursor();
|
||||
if (bd->LastMouseCursor != mouse_cursor)
|
||||
{
|
||||
bd->LastMouseCursor = mouse_cursor;
|
||||
ImGui_ImplWin32_UpdateMouseCursor(io, mouse_cursor);
|
||||
}
|
||||
|
||||
// Update game controllers (if enabled and available)
|
||||
ImGui_ImplWin32_UpdateGamepads(io);
|
||||
}
|
||||
|
||||
// Map VK_xxx to ImGuiKey_xxx.
|
||||
// Not static to allow third-party code to use that if they want to (but undocumented)
|
||||
ImGuiKey ImGui_ImplWin32_KeyEventToImGuiKey(WPARAM wParam, LPARAM lParam);
|
||||
ImGuiKey ImGui_ImplWin32_KeyEventToImGuiKey(WPARAM wParam, LPARAM lParam)
|
||||
{
|
||||
// There is no distinct VK_xxx for keypad enter, instead it is VK_RETURN + KF_EXTENDED.
|
||||
if ((wParam == VK_RETURN) && (HIWORD(lParam) & KF_EXTENDED))
|
||||
return ImGuiKey_KeypadEnter;
|
||||
|
||||
switch (wParam)
|
||||
{
|
||||
case VK_TAB: return ImGuiKey_Tab;
|
||||
case VK_LEFT: return ImGuiKey_LeftArrow;
|
||||
case VK_RIGHT: return ImGuiKey_RightArrow;
|
||||
case VK_UP: return ImGuiKey_UpArrow;
|
||||
case VK_DOWN: return ImGuiKey_DownArrow;
|
||||
case VK_PRIOR: return ImGuiKey_PageUp;
|
||||
case VK_NEXT: return ImGuiKey_PageDown;
|
||||
case VK_HOME: return ImGuiKey_Home;
|
||||
case VK_END: return ImGuiKey_End;
|
||||
case VK_INSERT: return ImGuiKey_Insert;
|
||||
case VK_DELETE: return ImGuiKey_Delete;
|
||||
case VK_BACK: return ImGuiKey_Backspace;
|
||||
case VK_SPACE: return ImGuiKey_Space;
|
||||
case VK_RETURN: return ImGuiKey_Enter;
|
||||
case VK_ESCAPE: return ImGuiKey_Escape;
|
||||
case VK_OEM_7: return ImGuiKey_Apostrophe;
|
||||
case VK_OEM_COMMA: return ImGuiKey_Comma;
|
||||
case VK_OEM_MINUS: return ImGuiKey_Minus;
|
||||
case VK_OEM_PERIOD: return ImGuiKey_Period;
|
||||
case VK_OEM_2: return ImGuiKey_Slash;
|
||||
case VK_OEM_1: return ImGuiKey_Semicolon;
|
||||
case VK_OEM_PLUS: return ImGuiKey_Equal;
|
||||
case VK_OEM_4: return ImGuiKey_LeftBracket;
|
||||
case VK_OEM_5: return ImGuiKey_Backslash;
|
||||
case VK_OEM_6: return ImGuiKey_RightBracket;
|
||||
case VK_OEM_3: return ImGuiKey_GraveAccent;
|
||||
case VK_CAPITAL: return ImGuiKey_CapsLock;
|
||||
case VK_SCROLL: return ImGuiKey_ScrollLock;
|
||||
case VK_NUMLOCK: return ImGuiKey_NumLock;
|
||||
case VK_SNAPSHOT: return ImGuiKey_PrintScreen;
|
||||
case VK_PAUSE: return ImGuiKey_Pause;
|
||||
case VK_NUMPAD0: return ImGuiKey_Keypad0;
|
||||
case VK_NUMPAD1: return ImGuiKey_Keypad1;
|
||||
case VK_NUMPAD2: return ImGuiKey_Keypad2;
|
||||
case VK_NUMPAD3: return ImGuiKey_Keypad3;
|
||||
case VK_NUMPAD4: return ImGuiKey_Keypad4;
|
||||
case VK_NUMPAD5: return ImGuiKey_Keypad5;
|
||||
case VK_NUMPAD6: return ImGuiKey_Keypad6;
|
||||
case VK_NUMPAD7: return ImGuiKey_Keypad7;
|
||||
case VK_NUMPAD8: return ImGuiKey_Keypad8;
|
||||
case VK_NUMPAD9: return ImGuiKey_Keypad9;
|
||||
case VK_DECIMAL: return ImGuiKey_KeypadDecimal;
|
||||
case VK_DIVIDE: return ImGuiKey_KeypadDivide;
|
||||
case VK_MULTIPLY: return ImGuiKey_KeypadMultiply;
|
||||
case VK_SUBTRACT: return ImGuiKey_KeypadSubtract;
|
||||
case VK_ADD: return ImGuiKey_KeypadAdd;
|
||||
case VK_LSHIFT: return ImGuiKey_LeftShift;
|
||||
case VK_LCONTROL: return ImGuiKey_LeftCtrl;
|
||||
case VK_LMENU: return ImGuiKey_LeftAlt;
|
||||
case VK_LWIN: return ImGuiKey_LeftSuper;
|
||||
case VK_RSHIFT: return ImGuiKey_RightShift;
|
||||
case VK_RCONTROL: return ImGuiKey_RightCtrl;
|
||||
case VK_RMENU: return ImGuiKey_RightAlt;
|
||||
case VK_RWIN: return ImGuiKey_RightSuper;
|
||||
case VK_APPS: return ImGuiKey_Menu;
|
||||
case '0': return ImGuiKey_0;
|
||||
case '1': return ImGuiKey_1;
|
||||
case '2': return ImGuiKey_2;
|
||||
case '3': return ImGuiKey_3;
|
||||
case '4': return ImGuiKey_4;
|
||||
case '5': return ImGuiKey_5;
|
||||
case '6': return ImGuiKey_6;
|
||||
case '7': return ImGuiKey_7;
|
||||
case '8': return ImGuiKey_8;
|
||||
case '9': return ImGuiKey_9;
|
||||
case 'A': return ImGuiKey_A;
|
||||
case 'B': return ImGuiKey_B;
|
||||
case 'C': return ImGuiKey_C;
|
||||
case 'D': return ImGuiKey_D;
|
||||
case 'E': return ImGuiKey_E;
|
||||
case 'F': return ImGuiKey_F;
|
||||
case 'G': return ImGuiKey_G;
|
||||
case 'H': return ImGuiKey_H;
|
||||
case 'I': return ImGuiKey_I;
|
||||
case 'J': return ImGuiKey_J;
|
||||
case 'K': return ImGuiKey_K;
|
||||
case 'L': return ImGuiKey_L;
|
||||
case 'M': return ImGuiKey_M;
|
||||
case 'N': return ImGuiKey_N;
|
||||
case 'O': return ImGuiKey_O;
|
||||
case 'P': return ImGuiKey_P;
|
||||
case 'Q': return ImGuiKey_Q;
|
||||
case 'R': return ImGuiKey_R;
|
||||
case 'S': return ImGuiKey_S;
|
||||
case 'T': return ImGuiKey_T;
|
||||
case 'U': return ImGuiKey_U;
|
||||
case 'V': return ImGuiKey_V;
|
||||
case 'W': return ImGuiKey_W;
|
||||
case 'X': return ImGuiKey_X;
|
||||
case 'Y': return ImGuiKey_Y;
|
||||
case 'Z': return ImGuiKey_Z;
|
||||
case VK_F1: return ImGuiKey_F1;
|
||||
case VK_F2: return ImGuiKey_F2;
|
||||
case VK_F3: return ImGuiKey_F3;
|
||||
case VK_F4: return ImGuiKey_F4;
|
||||
case VK_F5: return ImGuiKey_F5;
|
||||
case VK_F6: return ImGuiKey_F6;
|
||||
case VK_F7: return ImGuiKey_F7;
|
||||
case VK_F8: return ImGuiKey_F8;
|
||||
case VK_F9: return ImGuiKey_F9;
|
||||
case VK_F10: return ImGuiKey_F10;
|
||||
case VK_F11: return ImGuiKey_F11;
|
||||
case VK_F12: return ImGuiKey_F12;
|
||||
case VK_F13: return ImGuiKey_F13;
|
||||
case VK_F14: return ImGuiKey_F14;
|
||||
case VK_F15: return ImGuiKey_F15;
|
||||
case VK_F16: return ImGuiKey_F16;
|
||||
case VK_F17: return ImGuiKey_F17;
|
||||
case VK_F18: return ImGuiKey_F18;
|
||||
case VK_F19: return ImGuiKey_F19;
|
||||
case VK_F20: return ImGuiKey_F20;
|
||||
case VK_F21: return ImGuiKey_F21;
|
||||
case VK_F22: return ImGuiKey_F22;
|
||||
case VK_F23: return ImGuiKey_F23;
|
||||
case VK_F24: return ImGuiKey_F24;
|
||||
case VK_BROWSER_BACK: return ImGuiKey_AppBack;
|
||||
case VK_BROWSER_FORWARD: return ImGuiKey_AppForward;
|
||||
default: return ImGuiKey_None;
|
||||
}
|
||||
}
|
||||
|
||||
// Allow compilation with old Windows SDK. MinGW doesn't have default _WIN32_WINNT/WINVER versions.
|
||||
#ifndef WM_MOUSEHWHEEL
|
||||
#define WM_MOUSEHWHEEL 0x020E
|
||||
#endif
|
||||
#ifndef DBT_DEVNODES_CHANGED
|
||||
#define DBT_DEVNODES_CHANGED 0x0007
|
||||
#endif
|
||||
|
||||
// Helper to obtain the source of mouse messages.
|
||||
// See https://learn.microsoft.com/en-us/windows/win32/tablet/system-events-and-mouse-messages
|
||||
// Prefer to call this at the top of the message handler to avoid the possibility of other Win32 calls interfering with this.
|
||||
static ImGuiMouseSource ImGui_ImplWin32_GetMouseSourceFromMessageExtraInfo()
|
||||
{
|
||||
LPARAM extra_info = ::GetMessageExtraInfo();
|
||||
if ((extra_info & 0xFFFFFF80) == 0xFF515700)
|
||||
return ImGuiMouseSource_Pen;
|
||||
if ((extra_info & 0xFFFFFF80) == 0xFF515780)
|
||||
return ImGuiMouseSource_TouchScreen;
|
||||
return ImGuiMouseSource_Mouse;
|
||||
}
|
||||
|
||||
// Win32 message handler (process Win32 mouse/keyboard inputs, etc.)
|
||||
// Call from your application's message handler. Keep calling your message handler unless this function returns TRUE.
|
||||
// When implementing your own backend, you can read the io.WantCaptureMouse, io.WantCaptureKeyboard flags to tell if Dear ImGui wants to use your inputs.
|
||||
// - When io.WantCaptureMouse is true, do not dispatch mouse input data to your main application, or clear/overwrite your copy of the mouse data.
|
||||
// - When io.WantCaptureKeyboard is true, do not dispatch keyboard input data to your main application, or clear/overwrite your copy of the keyboard data.
|
||||
// Generally you may always pass all inputs to Dear ImGui, and hide them from your application based on those two flags.
|
||||
// PS: We treat DBLCLK messages as regular mouse down messages, so this code will work on windows classes that have the CS_DBLCLKS flag set. Our own example app code doesn't set this flag.
|
||||
|
||||
// Copy either line into your .cpp file to forward declare the function:
|
||||
extern IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandler(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam); // Use ImGui::GetCurrentContext()
|
||||
extern IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandlerEx(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam, ImGuiIO& io); // Doesn't use ImGui::GetCurrentContext()
|
||||
|
||||
IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandler(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
|
||||
{
|
||||
// Most backends don't have silent checks like this one, but we need it because WndProc are called early in CreateWindow().
|
||||
// We silently allow both context or just only backend data to be nullptr.
|
||||
if (ImGui::GetCurrentContext() == NULL)
|
||||
return 0;
|
||||
return ImGui_ImplWin32_WndProcHandlerEx(hwnd, msg, wParam, lParam, ImGui::GetIO());
|
||||
}
|
||||
|
||||
// This version is in theory thread-safe in the sense that no path should access ImGui::GetCurrentContext().
|
||||
IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandlerEx(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam, ImGuiIO& io)
|
||||
{
|
||||
ImGui_ImplWin32_Data* bd = ImGui_ImplWin32_GetBackendData(io);
|
||||
if (bd == NULL)
|
||||
return 0;
|
||||
switch (msg)
|
||||
{
|
||||
case WM_MOUSEMOVE:
|
||||
case WM_NCMOUSEMOVE:
|
||||
{
|
||||
// We need to call TrackMouseEvent in order to receive WM_MOUSELEAVE events
|
||||
ImGuiMouseSource mouse_source = ImGui_ImplWin32_GetMouseSourceFromMessageExtraInfo();
|
||||
const int area = (msg == WM_MOUSEMOVE) ? 1 : 2;
|
||||
bd->MouseHwnd = hwnd;
|
||||
if (bd->MouseTrackedArea != area)
|
||||
{
|
||||
TRACKMOUSEEVENT tme_cancel = { sizeof(tme_cancel), TME_CANCEL, hwnd, 0 };
|
||||
TRACKMOUSEEVENT tme_track = { sizeof(tme_track), (DWORD)((area == 2) ? (TME_LEAVE | TME_NONCLIENT) : TME_LEAVE), hwnd, 0 };
|
||||
if (bd->MouseTrackedArea != 0)
|
||||
::TrackMouseEvent(&tme_cancel);
|
||||
::TrackMouseEvent(&tme_track);
|
||||
bd->MouseTrackedArea = area;
|
||||
}
|
||||
POINT mouse_pos = { (LONG)GET_X_LPARAM(lParam), (LONG)GET_Y_LPARAM(lParam) };
|
||||
if (msg == WM_NCMOUSEMOVE && ::ScreenToClient(hwnd, &mouse_pos) == FALSE) // WM_NCMOUSEMOVE are provided in absolute coordinates.
|
||||
return 0;
|
||||
io.AddMouseSourceEvent(mouse_source);
|
||||
io.AddMousePosEvent((float)mouse_pos.x, (float)mouse_pos.y);
|
||||
return 0;
|
||||
}
|
||||
case WM_MOUSELEAVE:
|
||||
case WM_NCMOUSELEAVE:
|
||||
{
|
||||
const int area = (msg == WM_MOUSELEAVE) ? 1 : 2;
|
||||
if (bd->MouseTrackedArea == area)
|
||||
{
|
||||
if (bd->MouseHwnd == hwnd)
|
||||
bd->MouseHwnd = nullptr;
|
||||
bd->MouseTrackedArea = 0;
|
||||
io.AddMousePosEvent(-FLT_MAX, -FLT_MAX);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
case WM_DESTROY:
|
||||
if (bd->MouseHwnd == hwnd && bd->MouseTrackedArea != 0)
|
||||
{
|
||||
TRACKMOUSEEVENT tme_cancel = { sizeof(tme_cancel), TME_CANCEL, hwnd, 0 };
|
||||
::TrackMouseEvent(&tme_cancel);
|
||||
bd->MouseHwnd = nullptr;
|
||||
bd->MouseTrackedArea = 0;
|
||||
io.AddMousePosEvent(-FLT_MAX, -FLT_MAX);
|
||||
}
|
||||
return 0;
|
||||
case WM_LBUTTONDOWN: case WM_LBUTTONDBLCLK:
|
||||
case WM_RBUTTONDOWN: case WM_RBUTTONDBLCLK:
|
||||
case WM_MBUTTONDOWN: case WM_MBUTTONDBLCLK:
|
||||
case WM_XBUTTONDOWN: case WM_XBUTTONDBLCLK:
|
||||
{
|
||||
ImGuiMouseSource mouse_source = ImGui_ImplWin32_GetMouseSourceFromMessageExtraInfo();
|
||||
int button = 0;
|
||||
if (msg == WM_LBUTTONDOWN || msg == WM_LBUTTONDBLCLK) { button = 0; }
|
||||
if (msg == WM_RBUTTONDOWN || msg == WM_RBUTTONDBLCLK) { button = 1; }
|
||||
if (msg == WM_MBUTTONDOWN || msg == WM_MBUTTONDBLCLK) { button = 2; }
|
||||
if (msg == WM_XBUTTONDOWN || msg == WM_XBUTTONDBLCLK) { button = (GET_XBUTTON_WPARAM(wParam) == XBUTTON1) ? 3 : 4; }
|
||||
if (bd->MouseButtonsDown == 0 && ::GetCapture() == nullptr)
|
||||
::SetCapture(hwnd); // Allow us to read mouse coordinates when dragging mouse outside of our window bounds.
|
||||
bd->MouseButtonsDown |= 1 << button;
|
||||
io.AddMouseSourceEvent(mouse_source);
|
||||
io.AddMouseButtonEvent(button, true);
|
||||
return 0;
|
||||
}
|
||||
case WM_LBUTTONUP:
|
||||
case WM_RBUTTONUP:
|
||||
case WM_MBUTTONUP:
|
||||
case WM_XBUTTONUP:
|
||||
{
|
||||
ImGuiMouseSource mouse_source = ImGui_ImplWin32_GetMouseSourceFromMessageExtraInfo();
|
||||
int button = 0;
|
||||
if (msg == WM_LBUTTONUP) { button = 0; }
|
||||
if (msg == WM_RBUTTONUP) { button = 1; }
|
||||
if (msg == WM_MBUTTONUP) { button = 2; }
|
||||
if (msg == WM_XBUTTONUP) { button = (GET_XBUTTON_WPARAM(wParam) == XBUTTON1) ? 3 : 4; }
|
||||
bd->MouseButtonsDown &= ~(1 << button);
|
||||
if (bd->MouseButtonsDown == 0 && ::GetCapture() == hwnd)
|
||||
::ReleaseCapture();
|
||||
io.AddMouseSourceEvent(mouse_source);
|
||||
io.AddMouseButtonEvent(button, false);
|
||||
return 0;
|
||||
}
|
||||
case WM_MOUSEWHEEL:
|
||||
io.AddMouseWheelEvent(0.0f, (float)GET_WHEEL_DELTA_WPARAM(wParam) / (float)WHEEL_DELTA);
|
||||
return 0;
|
||||
case WM_MOUSEHWHEEL:
|
||||
io.AddMouseWheelEvent(-(float)GET_WHEEL_DELTA_WPARAM(wParam) / (float)WHEEL_DELTA, 0.0f);
|
||||
return 0;
|
||||
case WM_KEYDOWN:
|
||||
case WM_KEYUP:
|
||||
case WM_SYSKEYDOWN:
|
||||
case WM_SYSKEYUP:
|
||||
{
|
||||
const bool is_key_down = (msg == WM_KEYDOWN || msg == WM_SYSKEYDOWN);
|
||||
if (wParam < 256)
|
||||
{
|
||||
// Submit modifiers
|
||||
ImGui_ImplWin32_UpdateKeyModifiers(io);
|
||||
|
||||
// Obtain virtual key code and convert to ImGuiKey
|
||||
const ImGuiKey key = ImGui_ImplWin32_KeyEventToImGuiKey(wParam, lParam);
|
||||
const int vk = (int)wParam;
|
||||
const int scancode = (int)LOBYTE(HIWORD(lParam));
|
||||
|
||||
// Special behavior for VK_SNAPSHOT / ImGuiKey_PrintScreen as Windows doesn't emit the key down event.
|
||||
if (key == ImGuiKey_PrintScreen && !is_key_down)
|
||||
ImGui_ImplWin32_AddKeyEvent(io, key, true, vk, scancode);
|
||||
|
||||
// Submit key event
|
||||
if (key != ImGuiKey_None)
|
||||
ImGui_ImplWin32_AddKeyEvent(io, key, is_key_down, vk, scancode);
|
||||
|
||||
// Submit individual left/right modifier events
|
||||
if (vk == VK_SHIFT)
|
||||
{
|
||||
// Important: Shift keys tend to get stuck when pressed together, missing key-up events are corrected in ImGui_ImplWin32_ProcessKeyEventsWorkarounds()
|
||||
if (IsVkDown(VK_LSHIFT) == is_key_down) { ImGui_ImplWin32_AddKeyEvent(io, ImGuiKey_LeftShift, is_key_down, VK_LSHIFT, scancode); }
|
||||
if (IsVkDown(VK_RSHIFT) == is_key_down) { ImGui_ImplWin32_AddKeyEvent(io, ImGuiKey_RightShift, is_key_down, VK_RSHIFT, scancode); }
|
||||
}
|
||||
else if (vk == VK_CONTROL)
|
||||
{
|
||||
if (IsVkDown(VK_LCONTROL) == is_key_down) { ImGui_ImplWin32_AddKeyEvent(io, ImGuiKey_LeftCtrl, is_key_down, VK_LCONTROL, scancode); }
|
||||
if (IsVkDown(VK_RCONTROL) == is_key_down) { ImGui_ImplWin32_AddKeyEvent(io, ImGuiKey_RightCtrl, is_key_down, VK_RCONTROL, scancode); }
|
||||
}
|
||||
else if (vk == VK_MENU)
|
||||
{
|
||||
if (IsVkDown(VK_LMENU) == is_key_down) { ImGui_ImplWin32_AddKeyEvent(io, ImGuiKey_LeftAlt, is_key_down, VK_LMENU, scancode); }
|
||||
if (IsVkDown(VK_RMENU) == is_key_down) { ImGui_ImplWin32_AddKeyEvent(io, ImGuiKey_RightAlt, is_key_down, VK_RMENU, scancode); }
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
case WM_SETFOCUS:
|
||||
case WM_KILLFOCUS:
|
||||
io.AddFocusEvent(msg == WM_SETFOCUS);
|
||||
return 0;
|
||||
case WM_INPUTLANGCHANGE:
|
||||
ImGui_ImplWin32_UpdateKeyboardCodePage(io);
|
||||
return 0;
|
||||
case WM_CHAR:
|
||||
if (::IsWindowUnicode(hwnd))
|
||||
{
|
||||
// You can also use ToAscii()+GetKeyboardState() to retrieve characters.
|
||||
if (wParam > 0 && wParam < 0x10000)
|
||||
io.AddInputCharacterUTF16((unsigned short)wParam);
|
||||
}
|
||||
else
|
||||
{
|
||||
wchar_t wch = 0;
|
||||
::MultiByteToWideChar(bd->KeyboardCodePage, MB_PRECOMPOSED, (char*)&wParam, 1, &wch, 1);
|
||||
io.AddInputCharacter(wch);
|
||||
}
|
||||
return 0;
|
||||
case WM_SETCURSOR:
|
||||
// This is required to restore cursor when transitioning from e.g resize borders to client area.
|
||||
if (LOWORD(lParam) == HTCLIENT && ImGui_ImplWin32_UpdateMouseCursor(io, bd->LastMouseCursor))
|
||||
return 1;
|
||||
return 0;
|
||||
case WM_DEVICECHANGE:
|
||||
#ifndef IMGUI_IMPL_WIN32_DISABLE_GAMEPAD
|
||||
if ((UINT)wParam == DBT_DEVNODES_CHANGED)
|
||||
bd->WantUpdateHasGamepad = true;
|
||||
#endif
|
||||
return 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
//--------------------------------------------------------------------------------------------------------
|
||||
// DPI-related helpers (optional)
|
||||
//--------------------------------------------------------------------------------------------------------
|
||||
// - Use to enable DPI awareness without having to create an application manifest.
|
||||
// - Your own app may already do this via a manifest or explicit calls. This is mostly useful for our examples/ apps.
|
||||
// - In theory we could call simple functions from Windows SDK such as SetProcessDPIAware(), SetProcessDpiAwareness(), etc.
|
||||
// but most of the functions provided by Microsoft require Windows 8.1/10+ SDK at compile time and Windows 8/10+ at runtime,
|
||||
// neither we want to require the user to have. So we dynamically select and load those functions to avoid dependencies.
|
||||
//---------------------------------------------------------------------------------------------------------
|
||||
// This is the scheme successfully used by GLFW (from which we borrowed some of the code) and other apps aiming to be highly portable.
|
||||
// ImGui_ImplWin32_EnableDpiAwareness() is just a helper called by main.cpp, we don't call it automatically.
|
||||
// If you are trying to implement your own backend for your own engine, you may ignore that noise.
|
||||
//---------------------------------------------------------------------------------------------------------
|
||||
|
||||
// Perform our own check with RtlVerifyVersionInfo() instead of using functions from <VersionHelpers.h> as they
|
||||
// require a manifest to be functional for checks above 8.1. See https://github.com/ocornut/imgui/issues/4200
|
||||
static BOOL _IsWindowsVersionOrGreater(WORD major, WORD minor, WORD)
|
||||
{
|
||||
typedef LONG(WINAPI* PFN_RtlVerifyVersionInfo)(OSVERSIONINFOEXW*, ULONG, ULONGLONG);
|
||||
static PFN_RtlVerifyVersionInfo RtlVerifyVersionInfoFn = nullptr;
|
||||
if (RtlVerifyVersionInfoFn == nullptr)
|
||||
if (HMODULE ntdllModule = ::GetModuleHandleA("ntdll.dll"))
|
||||
RtlVerifyVersionInfoFn = (PFN_RtlVerifyVersionInfo)GetProcAddress(ntdllModule, "RtlVerifyVersionInfo");
|
||||
if (RtlVerifyVersionInfoFn == nullptr)
|
||||
return FALSE;
|
||||
|
||||
RTL_OSVERSIONINFOEXW versionInfo = { };
|
||||
ULONGLONG conditionMask = 0;
|
||||
versionInfo.dwOSVersionInfoSize = sizeof(RTL_OSVERSIONINFOEXW);
|
||||
versionInfo.dwMajorVersion = major;
|
||||
versionInfo.dwMinorVersion = minor;
|
||||
VER_SET_CONDITION(conditionMask, VER_MAJORVERSION, VER_GREATER_EQUAL);
|
||||
VER_SET_CONDITION(conditionMask, VER_MINORVERSION, VER_GREATER_EQUAL);
|
||||
return (RtlVerifyVersionInfoFn(&versionInfo, VER_MAJORVERSION | VER_MINORVERSION, conditionMask) == 0) ? TRUE : FALSE;
|
||||
}
|
||||
|
||||
#define _IsWindowsVistaOrGreater() _IsWindowsVersionOrGreater(HIBYTE(0x0600), LOBYTE(0x0600), 0) // _WIN32_WINNT_VISTA
|
||||
#define _IsWindows8OrGreater() _IsWindowsVersionOrGreater(HIBYTE(0x0602), LOBYTE(0x0602), 0) // _WIN32_WINNT_WIN8
|
||||
#define _IsWindows8Point1OrGreater() _IsWindowsVersionOrGreater(HIBYTE(0x0603), LOBYTE(0x0603), 0) // _WIN32_WINNT_WINBLUE
|
||||
#define _IsWindows10OrGreater() _IsWindowsVersionOrGreater(HIBYTE(0x0A00), LOBYTE(0x0A00), 0) // _WIN32_WINNT_WINTHRESHOLD / _WIN32_WINNT_WIN10
|
||||
|
||||
#ifndef DPI_ENUMS_DECLARED
|
||||
typedef enum { PROCESS_DPI_UNAWARE = 0, PROCESS_SYSTEM_DPI_AWARE = 1, PROCESS_PER_MONITOR_DPI_AWARE = 2 } PROCESS_DPI_AWARENESS;
|
||||
typedef enum { MDT_EFFECTIVE_DPI = 0, MDT_ANGULAR_DPI = 1, MDT_RAW_DPI = 2, MDT_DEFAULT = MDT_EFFECTIVE_DPI } MONITOR_DPI_TYPE;
|
||||
#endif
|
||||
#ifndef _DPI_AWARENESS_CONTEXTS_
|
||||
DECLARE_HANDLE(DPI_AWARENESS_CONTEXT);
|
||||
#define DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE (DPI_AWARENESS_CONTEXT)-3
|
||||
#endif
|
||||
#ifndef DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2
|
||||
#define DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 (DPI_AWARENESS_CONTEXT)-4
|
||||
#endif
|
||||
typedef HRESULT(WINAPI* PFN_SetProcessDpiAwareness)(PROCESS_DPI_AWARENESS); // Shcore.lib + dll, Windows 8.1+
|
||||
typedef HRESULT(WINAPI* PFN_GetDpiForMonitor)(HMONITOR, MONITOR_DPI_TYPE, UINT*, UINT*); // Shcore.lib + dll, Windows 8.1+
|
||||
typedef DPI_AWARENESS_CONTEXT(WINAPI* PFN_SetThreadDpiAwarenessContext)(DPI_AWARENESS_CONTEXT); // User32.lib + dll, Windows 10 v1607+ (Creators Update)
|
||||
|
||||
// Helper function to enable DPI awareness without setting up a manifest
|
||||
void ImGui_ImplWin32_EnableDpiAwareness()
|
||||
{
|
||||
if (_IsWindows10OrGreater())
|
||||
{
|
||||
static HINSTANCE user32_dll = ::LoadLibraryA("user32.dll"); // Reference counted per-process
|
||||
if (PFN_SetThreadDpiAwarenessContext SetThreadDpiAwarenessContextFn = (PFN_SetThreadDpiAwarenessContext)::GetProcAddress(user32_dll, "SetThreadDpiAwarenessContext"))
|
||||
{
|
||||
SetThreadDpiAwarenessContextFn(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (_IsWindows8Point1OrGreater())
|
||||
{
|
||||
static HINSTANCE shcore_dll = ::LoadLibraryA("shcore.dll"); // Reference counted per-process
|
||||
if (PFN_SetProcessDpiAwareness SetProcessDpiAwarenessFn = (PFN_SetProcessDpiAwareness)::GetProcAddress(shcore_dll, "SetProcessDpiAwareness"))
|
||||
{
|
||||
SetProcessDpiAwarenessFn(PROCESS_PER_MONITOR_DPI_AWARE);
|
||||
return;
|
||||
}
|
||||
}
|
||||
#if _WIN32_WINNT >= 0x0600
|
||||
::SetProcessDPIAware();
|
||||
#endif
|
||||
}
|
||||
|
||||
#if defined(_MSC_VER) && !defined(NOGDI)
|
||||
#pragma comment(lib, "gdi32") // Link with gdi32.lib for GetDeviceCaps(). MinGW will require linking with '-lgdi32'
|
||||
#endif
|
||||
|
||||
float ImGui_ImplWin32_GetDpiScaleForMonitor(void* monitor)
|
||||
{
|
||||
UINT xdpi = 96, ydpi = 96;
|
||||
if (_IsWindows8Point1OrGreater())
|
||||
{
|
||||
static HINSTANCE shcore_dll = ::LoadLibraryA("shcore.dll"); // Reference counted per-process
|
||||
static PFN_GetDpiForMonitor GetDpiForMonitorFn = nullptr;
|
||||
if (GetDpiForMonitorFn == nullptr && shcore_dll != nullptr)
|
||||
GetDpiForMonitorFn = (PFN_GetDpiForMonitor)::GetProcAddress(shcore_dll, "GetDpiForMonitor");
|
||||
if (GetDpiForMonitorFn != nullptr)
|
||||
{
|
||||
GetDpiForMonitorFn((HMONITOR)monitor, MDT_EFFECTIVE_DPI, &xdpi, &ydpi);
|
||||
IM_ASSERT(xdpi == ydpi); // Please contact me if you hit this assert!
|
||||
return xdpi / 96.0f;
|
||||
}
|
||||
}
|
||||
#ifndef NOGDI
|
||||
const HDC dc = ::GetDC(nullptr);
|
||||
xdpi = ::GetDeviceCaps(dc, LOGPIXELSX);
|
||||
ydpi = ::GetDeviceCaps(dc, LOGPIXELSY);
|
||||
IM_ASSERT(xdpi == ydpi); // Please contact me if you hit this assert!
|
||||
::ReleaseDC(nullptr, dc);
|
||||
#endif
|
||||
return xdpi / 96.0f;
|
||||
}
|
||||
|
||||
float ImGui_ImplWin32_GetDpiScaleForHwnd(void* hwnd)
|
||||
{
|
||||
HMONITOR monitor = ::MonitorFromWindow((HWND)hwnd, MONITOR_DEFAULTTONEAREST);
|
||||
return ImGui_ImplWin32_GetDpiScaleForMonitor(monitor);
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------------------------------------------------
|
||||
// Transparency related helpers (optional)
|
||||
//--------------------------------------------------------------------------------------------------------
|
||||
|
||||
#if defined(_MSC_VER)
|
||||
#pragma comment(lib, "dwmapi") // Link with dwmapi.lib. MinGW will require linking with '-ldwmapi'
|
||||
#endif
|
||||
|
||||
// [experimental]
|
||||
// Borrowed from GLFW's function updateFramebufferTransparency() in src/win32_window.c
|
||||
// (the Dwm* functions are Vista era functions but we are borrowing logic from GLFW)
|
||||
void ImGui_ImplWin32_EnableAlphaCompositing(void* hwnd)
|
||||
{
|
||||
if (!_IsWindowsVistaOrGreater())
|
||||
return;
|
||||
|
||||
BOOL composition;
|
||||
if (FAILED(::DwmIsCompositionEnabled(&composition)) || !composition)
|
||||
return;
|
||||
|
||||
BOOL opaque;
|
||||
DWORD color;
|
||||
if (_IsWindows8OrGreater() || (SUCCEEDED(::DwmGetColorizationColor(&color, &opaque)) && !opaque))
|
||||
{
|
||||
HRGN region = ::CreateRectRgn(0, 0, -1, -1);
|
||||
DWM_BLURBEHIND bb = {};
|
||||
bb.dwFlags = DWM_BB_ENABLE | DWM_BB_BLURREGION;
|
||||
bb.hRgnBlur = region;
|
||||
bb.fEnable = TRUE;
|
||||
::DwmEnableBlurBehindWindow((HWND)hwnd, &bb);
|
||||
::DeleteObject(region);
|
||||
}
|
||||
else
|
||||
{
|
||||
DWM_BLURBEHIND bb = {};
|
||||
bb.dwFlags = DWM_BB_ENABLE;
|
||||
::DwmEnableBlurBehindWindow((HWND)hwnd, &bb);
|
||||
}
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------------------------------------------------
|
||||
|
||||
#if defined(__GNUC__)
|
||||
#pragma GCC diagnostic pop
|
||||
#endif
|
||||
#if defined(__clang__)
|
||||
#pragma clang diagnostic pop
|
||||
#endif
|
||||
|
||||
#endif // #ifndef IMGUI_DISABLE
|
||||
+53
@@ -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
|
||||
+3578
File diff suppressed because it is too large.
Load diff
+4467
File diff suppressed because it is too large.
Load diff
+10310
File diff suppressed because it is too large.
Load diff
+627
@@ -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.
|
||||
------------------------------------------------------------------------------
|
||||
*/
|
||||
+1469
File diff suppressed because it is too large.
Load diff
+5085
File diff suppressed because it is too large.
Load diff
+24765
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,26 @@
|
||||
# Builds the release and packs what players download: dist\bbbridge-<version>-windows.zip
|
||||
# holding a "bbbridge" folder to drop into bbport's folder (next to Bloodborne.exe).
|
||||
# Never includes an account file, logs or build output.
|
||||
# powershell -ExecutionPolicy Bypass -File tools\make_release.ps1
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$root = Split-Path -Parent $PSScriptRoot
|
||||
$vc = 'C:\Program Files\Microsoft Visual Studio\18\Community\VC\Auxiliary\Build\vcvars64.bat'
|
||||
if (-not (Test-Path $vc)) {
|
||||
$vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe"
|
||||
$vc = Join-Path (& $vswhere -latest -products * -property installationPath) 'VC\Auxiliary\Build\vcvars64.bat'
|
||||
}
|
||||
cmd /c "`"$vc`" >nul && cd /d `"$root`" && cmake --preset x64-release >nul && cmake --build --preset x64-release --target bbbridge"
|
||||
if ($LASTEXITCODE -ne 0) { throw 'build failed' }
|
||||
|
||||
$version = (Select-String -Path "$root\CMakeLists.txt" -Pattern 'project\(bbbridge VERSION ([0-9.]+)').Matches[0].Groups[1].Value
|
||||
$stage = Join-Path $root "dist\stage\bbbridge"
|
||||
Remove-Item (Join-Path $root 'dist\stage') -Recurse -Force -ErrorAction SilentlyContinue
|
||||
New-Item -ItemType Directory -Force $stage | Out-Null
|
||||
Copy-Item "$root\build\x64-release\bbbridge.exe", "$root\README.md", "$root\CHANGELOG.md", "$root\LICENSE", "$root\THIRD_PARTY_NOTICES.txt" $stage
|
||||
$zip = Join-Path $root "dist\bbbridge-$version-windows.zip"
|
||||
Remove-Item $zip -Force -ErrorAction SilentlyContinue
|
||||
Compress-Archive -Path $stage -DestinationPath $zip
|
||||
Remove-Item (Join-Path $root 'dist\stage') -Recurse -Force
|
||||
$hash = (Get-FileHash $zip -Algorithm SHA256).Hash.ToLower()
|
||||
"$hash $(Split-Path -Leaf $zip)" | Set-Content -Encoding ascii "$zip.sha256"
|
||||
Write-Output "$zip ($([math]::Round((Get-Item $zip).Length / 1MB, 2)) MB) sha256 $hash"
|
||||
@@ -69,6 +69,30 @@ if [[ -n ${DLSS_SDK_ROOT:-} ]]; then
|
||||
cp "$DLSS_SDK_ROOT/LICENSE.txt" out/NVIDIA-DLSS-LICENSE.txt
|
||||
echo "DLSS bridge: out/libbbport_dlss.so"
|
||||
fi
|
||||
# The Hunter's Dream co-op pool (launcher: Online): bbbridge (MaskZeus, GPL-3.0-or-later) stands in
|
||||
# for the shadNet server on this PC and talks to The Hunter's Dream as bbhost does. Needs libcurl and
|
||||
# OpenSSL's development files; without them the launcher offers shadNet servers only.
|
||||
if pkg-config --exists libcurl openssl; then
|
||||
if cmake -S bbbridge -B out/bbbridge -G Ninja -DCMAKE_BUILD_TYPE=Release >out/bbbridge-build.log 2>&1 &&
|
||||
ninja -C out/bbbridge bbbridge-cli >>out/bbbridge-build.log 2>&1; then
|
||||
echo "The Hunter's Dream bridge: out/bbbridge/bbbridge-cli"
|
||||
else
|
||||
echo "bbbridge build failed (out/bbbridge-build.log): The Hunter's Dream is not offered" >&2
|
||||
fi
|
||||
fi
|
||||
# Installing from PS4 packages (launcher: Install from PKG, scripts/pkg_install.py): pkgextract
|
||||
# (github.com/paulomanrique/ps4-pkg-extractor, Unlicense) at a reviewed revision, with its
|
||||
# Cargo.lock. Needs cargo; without it the launcher asks for a game folder only.
|
||||
pkgextract_rev=f430ce1634fc7953834b545a3946bfbd5fa216fa
|
||||
if [[ ! -x out/pkgextract/bin/pkgextract ]] && command -v cargo >/dev/null; then
|
||||
if cargo install --quiet --locked --root out/pkgextract \
|
||||
--git https://github.com/paulomanrique/ps4-pkg-extractor --rev "$pkgextract_rev" pkgextract \
|
||||
>out/pkgextract-build.log 2>&1; then
|
||||
echo 'PKG extractor: out/pkgextract/bin/pkgextract'
|
||||
else
|
||||
echo 'pkgextract build failed (out/pkgextract-build.log): Install from PKG is not offered' >&2
|
||||
fi
|
||||
fi
|
||||
# $ORIGIN/gpu: packaged copies keep the library next to the binary without patching it.
|
||||
gpu=(-Lout/gpu -lbbgpu -Wl,-rpath,'$ORIGIN/gpu' -Wl,-rpath,"$PWD/out/gpu" -rdynamic)
|
||||
runtime=(src/runtime*.c)
|
||||
@@ -84,7 +108,7 @@ echo "Built $PWD/out/bb-probe"
|
||||
# GPU check for run.sh (live_resolution=auto) and the launcher's gamepad list (--gamepads).
|
||||
"$CC" -std=c11 -O2 -Wall -Wextra -Werror "${includes[@]}" tools/gpu_capabilities.c "${libraries[@]}" -o out/bb-gpu-capabilities
|
||||
if [[ ${1:-} == --test ]]; then
|
||||
"$CC" -std=c11 -O2 -g -Wall -Wextra -Werror -pthread "${includes[@]}" -I. -Isrc tests/test_pad.c "${libraries[@]}" -o out/pad-test
|
||||
"$CC" -std=c11 -O2 -g -Wall -Wextra -Werror -pthread "${includes[@]}" -I. -Isrc tests/test_pad.c "${libraries[@]}" -lm -o out/pad-test
|
||||
out/pad-test
|
||||
"$CC" -std=c11 -O2 -g -Wall -Wextra -Werror -pthread -I. -Isrc tests/test_runtime.c "${runtime[@]}" out/libatrac9.a -lm "${gpu[@]}" "${libraries[@]}" -o out/runtime-test
|
||||
out/runtime-test
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
# Catching a crash with a debugger
|
||||
|
||||
By default a fatal fault or a GPU-library assertion ends the process at once: the log keeps
|
||||
one line (`Host fault`, `Guest fault`, `STOP: GPU library assertion failed`) and nothing else.
|
||||
`BB_FAULT_WAIT=1` keeps the process alive at that point, stopped inside its handler, so a
|
||||
debugger can look at the faulting thread with its registers, stack and memory still there.
|
||||
|
||||
1. Launcher → *Mode → Extra environment*: `BB_FAULT_WAIT=1` (or export it before `run.sh`).
|
||||
Nothing changes while the game runs; it costs nothing.
|
||||
2. When it crashes, the game freezes, a desktop notification says so, and the log ends with
|
||||
`bbport: ... the process stays alive for a debugger: bash tools/debug_attach.sh <pid>`.
|
||||
3. Run that line. gdb attaches, writes `logs/crash-<time>-<pid>.txt` and kills the process.
|
||||
With `--keep` it detaches instead, so `gdb -p <pid>` can be used by hand afterwards.
|
||||
|
||||
The report has every thread's stack, and for the faulting thread its registers, the
|
||||
instructions around `pc` and the top of its stack; for a GPU-library assertion the locals of
|
||||
the asserting thread (`bt full`). bbport's own frames carry file and line (the build is
|
||||
RelWithDebInfo); libc's come from debuginfod when it is reachable. Game code shows as a
|
||||
*guest offset* (pc minus the image base, as in the log's `Guest fault` line); the report gives
|
||||
an `objdump` command that disassembles `out/boot-linked.bin` around it.
|
||||
|
||||
GPU hangs are different: the device is lost and the GPU breadcrumbs in the log name the stuck
|
||||
draw or dispatch; the assertion that follows is caught the same way, but the state of interest
|
||||
is on the GPU.
|
||||
+30
-17
@@ -1,25 +1,37 @@
|
||||
# Online play and co-op
|
||||
|
||||
Online play is a beta. Messages, bloodstains and ghosts come from the community game server,
|
||||
[The Hunter's Dream](https://thehuntersdream.com). Bells and summons go through a shadNet server,
|
||||
and then the players' games connect to each other directly. Linux and Windows players can play
|
||||
together: everyone needs the game at version 1.09 and the same servers.
|
||||
[The Hunter's Dream](https://thehuntersdream.com). Bells and summons go through a co-op pool,
|
||||
and then the players' games connect to each other. Linux and Windows players can play together:
|
||||
everyone needs the game at version 1.09 and the same pool.
|
||||
|
||||
> **Co-op does not connect through shadPS4's public server.** It does not answer the UDP ping
|
||||
> that tells it each player's address, so a summon times out
|
||||
> ([deadinside28/bloodborne_pc#121](https://github.com/deadinside28/bloodborne_pc/issues/121));
|
||||
> the shadPS4 team does not support forks. Messages, bloodstains and ghosts work. For co-op, a
|
||||
> group runs its own server: [SERVER.md](SERVER.md). *Test the connection* tells which case
|
||||
> yours is: the shadNet server's UDP line is red on the public server.
|
||||
> **shadPS4's public shadNet server no longer accepts bbport builds**: it signs the game out
|
||||
> right after the login ("This shadNet server doesn't accept your shadPS4 build" in the log), and
|
||||
> the launcher says so. The two pools for bbport are The Hunter's Dream's and a server of your own.
|
||||
|
||||
## Account
|
||||
## The Hunter's Dream (with bbhost's players)
|
||||
|
||||
1. On shadPS4's public server, register at [shadnet.shadps4.net](https://shadnet.shadps4.net).
|
||||
On your own server, its owner creates the account ([SERVER.md](SERVER.md)).
|
||||
2. In the launcher's **Online** page, turn on **Play online**, choose the **shadNet server**
|
||||
(shadPS4's public one or your own, with its address), and enter the **account name** (not the
|
||||
email) and the password. The password is kept on this computer with the launcher's settings.
|
||||
3. **Test the connection**: the game server, the shadNet server, its UDP port and its WebAPI
|
||||
Choose **The Hunter's Dream (with bbhost players)** in the launcher's **Online** page.
|
||||
[bbbridge](../bbbridge/README.md) by MaskZeus starts with the game, stands in for a shadNet server
|
||||
on this computer and talks to The Hunter's Dream the way bbhost does, its relay included, so no
|
||||
router port is needed. The account is The Hunter's Dream's, kept in `user/bbbridge` (no password):
|
||||
|
||||
- **Sign in with Discord**: the launcher shows a code and opens The Hunter's Dream's page; approve
|
||||
it there. The account is the same one bbhost uses.
|
||||
- **Without Discord**: type a name and **Create**. Keep the recovery code the launcher shows:
|
||||
with the name, it brings the account back on another computer (**Recover**).
|
||||
|
||||
The game's play log (its own telemetry) stays blocked, so The Hunter's Dream's live map does not
|
||||
show you.
|
||||
|
||||
## Your own server
|
||||
|
||||
Its owner creates the account ([SERVER.md](SERVER.md)).
|
||||
|
||||
1. In the launcher's **Online** page, turn on **Play online**, choose **Your own server** with
|
||||
its address, and enter the **account name** (not the email) and the password. The password is
|
||||
kept on this computer with the launcher's settings.
|
||||
2. **Test the connection**: the game server, the shadNet server, its UDP port and its WebAPI
|
||||
should all answer, and with the account filled in, the sign-in should work. A wrong name or
|
||||
password shows there (the server does not say which of the two).
|
||||
|
||||
@@ -73,5 +85,6 @@ Run with `BB_GPU_LOG=info` in the extra variables for the online details. In a s
|
||||
| `Logged in npid='...'` | The shadNet login worked. |
|
||||
| `summon_messenger/create` | Your sign is placed (helper). |
|
||||
| `summon_messenger/get`, `sceNpMatching2CreateJoinRoom`, `summon_messenger/request` | The host found the sign, made the room and sent the summon. |
|
||||
| `peer '...' endpoint unresolved` | shadNet has no address for the other player: its UDP port did not answer (the public server, above). |
|
||||
| `peer '...' endpoint unresolved` | shadNet has no address for the other player: its UDP port did not answer. |
|
||||
| `Online: the shadNet server ... does not accept this build` | The server refuses bbport (shadPS4's public one): use The Hunter's Dream or your own server. |
|
||||
| `event=0(DEAD)` | The connection gave up. |
|
||||
+4
-4
@@ -1,9 +1,9 @@
|
||||
# Hosting your own co-op server
|
||||
|
||||
Bells and summons go through a shadNet server. Players who connect through shadPS4's public one
|
||||
(`srv.shadps4.net`) get no co-op at the moment: it does not answer on its UDP port, so a summon
|
||||
never connects ([deadinside28/bloodborne_pc#121](https://github.com/deadinside28/bloodborne_pc/issues/121)).
|
||||
A group of friends can run their own server instead:
|
||||
Bells and summons go through a shadNet server. shadPS4's public one (`srv.shadps4.net`) no longer
|
||||
accepts bbport builds, and the public pool for bbport is now The Hunter's Dream's, through
|
||||
bbbridge (the launcher's **Online** page; [README](../README.md#online-play)). A group of friends
|
||||
can also run their own server:
|
||||
[shadnet-p2p](https://github.com/Wozzardman/shadnet-p2p) by Wozzardman, the shadNet server with
|
||||
that UDP port working and a summon service of its own.
|
||||
|
||||
|
||||
@@ -180,6 +180,10 @@ target_include_directories(ui-composition-test PRIVATE ..)
|
||||
add_executable(cp-decoder-test EXCLUDE_FROM_ALL ../tests/test_cp_decoder.cpp cp/cp_decoder.cpp)
|
||||
target_compile_options(cp-decoder-test PRIVATE -UNDEBUG -Wall -Wextra -Werror)
|
||||
target_include_directories(cp-decoder-test PRIVATE cp)
|
||||
add_executable(watched-pages-test EXCLUDE_FROM_ALL ../tests/test_watched_pages.cpp)
|
||||
target_compile_options(watched-pages-test PRIVATE -UNDEBUG -Wall -Wextra -Werror)
|
||||
target_include_directories(watched-pages-test PRIVATE shadps4)
|
||||
|
||||
add_executable(settings-test EXCLUDE_FROM_ALL ../tests/test_settings.cpp shim/bbport_settings.cpp)
|
||||
target_compile_options(settings-test PRIVATE -UNDEBUG -Wall -Wextra -Werror)
|
||||
target_include_directories(settings-test PRIVATE .. shim)
|
||||
|
||||
@@ -40,6 +40,9 @@ void bbgpu_trophy_notice(int id, char grade, const char *name_utf8, const char *
|
||||
void bbgpu_mouse_take(double *dx, double *dy, int *wheel_up, int *wheel_down);
|
||||
int bbgpu_mouse_captured(void);
|
||||
void bbgpu_mouse_look_enable(int enabled);
|
||||
/* mouse_camera=direct in bbport.ini: the mouse turns the camera itself (gpu/shim/bloodborne_cam.h)
|
||||
* at this sensitivity (1: 900 counts per radian) instead of moving the right stick. */
|
||||
void bbgpu_mouse_camera_configure(int direct, float sensitivity);
|
||||
/* Patches the loaded image before the game runs (image still writable): libGnm entry hooks. */
|
||||
void bbgpu_patch_image(unsigned char *image, uint64_t size);
|
||||
/* Number of symbols registered by the vendored libraries (diagnostics). */
|
||||
|
||||
@@ -31,9 +31,11 @@ typedef int net_socket;
|
||||
#include <sstream>
|
||||
#endif
|
||||
|
||||
#include <atomic>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
#include <string.h>
|
||||
#include "common/assert.h"
|
||||
@@ -388,6 +390,15 @@ bool NetUtilInternal::RetrieveIp() {
|
||||
}
|
||||
|
||||
int NetUtilInternal::ResolveHostname(const char* hostname, Libraries::Net::OrbisNetInAddr* addr) {
|
||||
// bbport: the game's "Playlog" telemetry (FromSoftware's bb-playlog-prod / bb-playlog-test
|
||||
// buckets on Amazon S3) is not sent anywhere: its host does not resolve, as with no network.
|
||||
if (std::string_view(hostname).find("bb-playlog") != std::string_view::npos) {
|
||||
static std::atomic_flag told = ATOMIC_FLAG_INIT;
|
||||
if (!told.test_and_set()) {
|
||||
LOG_INFO(Lib_Net, "{}: the game's play log upload is blocked", hostname);
|
||||
}
|
||||
return ORBIS_NET_ERETURN;
|
||||
}
|
||||
const addrinfo hints = {
|
||||
.ai_flags = AI_V4MAPPED | AI_ADDRCONFIG,
|
||||
.ai_family = AF_INET,
|
||||
|
||||
@@ -403,6 +403,20 @@ void NpHandler::WorkerThread() {
|
||||
}
|
||||
|
||||
for (s32 uid : dropped) {
|
||||
// bbport: a server that refused this build's version refuses every sign-in after it
|
||||
// (LoginError for a while), and each try makes that wait longer: no reconnect.
|
||||
bool rejected = false;
|
||||
{
|
||||
std::lock_guard lock(m_mutex_clients);
|
||||
if (auto it = m_clients.find(uid); it != m_clients.end() && it->second)
|
||||
rejected = it->second->WasRejected();
|
||||
}
|
||||
if (rejected) {
|
||||
LOG_WARNING(NpHandler, "user_id={} signed out: the server does not accept this "
|
||||
"build; not reconnecting", uid);
|
||||
DisconnectUser(uid);
|
||||
continue;
|
||||
}
|
||||
LOG_WARNING(NpHandler, "user_id={} connection dropped (network); will retry", uid);
|
||||
DisconnectUser(uid); // reports SignedOut + stops/removes the dead client
|
||||
MarkForReconnect(uid); // schedule transparent reconnect (network drop, not logout)
|
||||
|
||||
@@ -217,7 +217,13 @@ public:
|
||||
|
||||
private:
|
||||
NpHandler() = default;
|
||||
~NpHandler() = default;
|
||||
// bbport: Shutdown() is never called; at exit() (an error stop, a test) a joinable worker
|
||||
// made std::thread's destructor abort the process. The worker is told to stop and let go.
|
||||
~NpHandler() {
|
||||
m_worker_running = false;
|
||||
if (m_worker_thread.joinable())
|
||||
m_worker_thread.detach();
|
||||
}
|
||||
|
||||
/// Connect one user. Blocks until connected+authenticated or failed.
|
||||
bool ConnectUser(s32 user_id, const std::string& host, u16 port, const std::string& npid,
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <thread>
|
||||
|
||||
#include "client.h"
|
||||
@@ -537,6 +539,21 @@ bool ShadNetClient::RequestServerFeatures() {
|
||||
return true;
|
||||
}
|
||||
|
||||
// BB_CLIENT_VERSION replaces the default, for a server whose clientversions.cfg expects
|
||||
// another format.
|
||||
std::string ShadNetClient::BuildVersionString() {
|
||||
if (const char* version = std::getenv("BB_CLIENT_VERSION"); version && *version)
|
||||
return version;
|
||||
return "shadPS4 bbport";
|
||||
}
|
||||
|
||||
u64 ShadNetClient::ReportClientVersion() {
|
||||
shadnet::SetClientVersionRequest req;
|
||||
req.set_version(BuildVersionString());
|
||||
LOG_INFO(ShadNet, "Reporting client version '{}'", req.version());
|
||||
return SubmitRequest(CommandType::SetClientVersion, MakeProtoPayload(req));
|
||||
}
|
||||
|
||||
// Packet dispatch
|
||||
|
||||
void ShadNetClient::DispatchPacket(PacketType type, u16 cmd_raw, u64 pkt_id,
|
||||
@@ -553,6 +570,12 @@ void ShadNetClient::DispatchPacket(PacketType type, u16 cmd_raw, u64 pkt_id,
|
||||
case CommandType::GetServerFeatures:
|
||||
HandleServerFeaturesReply(payload);
|
||||
break;
|
||||
case CommandType::SetClientVersion:
|
||||
// Nobody waits for this reply; a refusal arrives as a ClientRejected notification.
|
||||
if (!payload.empty() && static_cast<ErrorType>(payload[0]) != ErrorType::NoError)
|
||||
LOG_WARNING(ShadNet, "SetClientVersion returned error={}",
|
||||
static_cast<int>(payload[0]));
|
||||
break;
|
||||
default:
|
||||
if (onAsyncReply) {
|
||||
// Every reply body starts with an ErrorType byte.
|
||||
@@ -623,6 +646,9 @@ void ShadNetClient::HandleLoginReply(const std::vector<u8>& payload) {
|
||||
LOG_INFO(ShadNet, "Logged in npid='{}' userId={} friends={}", m_npid, m_user_id,
|
||||
m_friends.size());
|
||||
|
||||
// First, before the server's version timeout runs out.
|
||||
ReportClientVersion();
|
||||
|
||||
const u64 pkt_id = m_pkt_counter.fetch_add(1);
|
||||
std::vector<u8> empty_payload;
|
||||
std::vector<u8> pkt = BuildPacket(CommandType::GetToken, pkt_id, empty_payload);
|
||||
@@ -911,6 +937,21 @@ void ShadNetClient::HandleNotification(u16 cmd_raw, const std::vector<u8>& paylo
|
||||
onWebApiPushEvent(n);
|
||||
break;
|
||||
}
|
||||
case NotificationType::ClientRejected: {
|
||||
shadnet::NotifyClientRejected pb;
|
||||
if (!pb.ParseFromString(blob)) {
|
||||
LOG_WARNING(ShadNet, "ClientRejected parse error");
|
||||
break;
|
||||
}
|
||||
LOG_ERROR(ShadNet, "Server rejected this client (reason={}, version='{}'): {}",
|
||||
pb.reason(), pb.version(), pb.message());
|
||||
m_rejected = true;
|
||||
// bbport: for the launcher, which tells the player (the game only says it is signed out).
|
||||
std::printf("Online: the shadNet server %s does not accept this build: %s\n", m_host.c_str(),
|
||||
pb.message().c_str());
|
||||
std::fflush(stdout);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
LOG_DEBUG(ShadNet, "Unknown notification type {}", cmd_raw);
|
||||
break;
|
||||
|
||||
@@ -67,6 +67,7 @@ enum class CommandType : u16 {
|
||||
AddBlock = 10,
|
||||
RemoveBlock = 11,
|
||||
GetServerFeatures = 12,
|
||||
SetClientVersion = 13,
|
||||
GetBoardInfos = 30,
|
||||
RecordScore = 31,
|
||||
RecordScoreData = 32,
|
||||
@@ -105,6 +106,7 @@ enum class NotificationType : u16 {
|
||||
RoomEvent = 10,
|
||||
RoomMessage = 11,
|
||||
WebApiPushEvent = 17, // Generic NP WebApi push event
|
||||
ClientRejected = 19, // The server is signing this client out and says why
|
||||
};
|
||||
|
||||
enum class ErrorType : uint8_t {
|
||||
@@ -267,6 +269,10 @@ public:
|
||||
ShadNetState WaitForAuthenticated();
|
||||
|
||||
bool IsConnected() const;
|
||||
/// The server refused this client's version (ClientRejected): signing in again cannot help.
|
||||
bool WasRejected() const {
|
||||
return m_rejected.load();
|
||||
}
|
||||
bool IsAuthenticated() const;
|
||||
ShadNetState GetState() const;
|
||||
|
||||
@@ -328,6 +334,9 @@ private:
|
||||
void HandleServerFeaturesReply(const std::vector<u8>& payload);
|
||||
void HandleNotification(u16 cmd_raw, const std::vector<u8>& payload);
|
||||
bool RequestServerFeatures();
|
||||
// Tell the server which client this is; a shadNet server may deny clients that stay silent.
|
||||
u64 ReportClientVersion();
|
||||
static std::string BuildVersionString();
|
||||
|
||||
// Helper: read a u32-LE-prefixed proto blob from a byte vector at pos.
|
||||
static std::string ExtractBlob(const std::vector<u8>& p, int pos);
|
||||
@@ -349,6 +358,7 @@ private:
|
||||
|
||||
std::atomic<bool> m_terminate{false};
|
||||
std::atomic<bool> m_connected{false};
|
||||
std::atomic<bool> m_rejected{false};
|
||||
std::atomic<bool> m_authenticated{false};
|
||||
std::atomic<ShadNetState> m_state{ShadNetState::Ok};
|
||||
|
||||
|
||||
@@ -29,6 +29,22 @@ message SetAppearOfflineRequest {
|
||||
bool appear_offline = 1;
|
||||
}
|
||||
|
||||
// Sent right after signing in. A server with clientversions.cfg rules signs out a client
|
||||
// that reports nothing, or a version it denies.
|
||||
message SetClientVersionRequest {
|
||||
string version = 1;
|
||||
}
|
||||
|
||||
// Notification 19: the server is signing this client out, then disconnects.
|
||||
// reason: 1 = client version not accepted
|
||||
// message: text for the player, from the rule that matched
|
||||
// version: the version the server checked ("" = none reported)
|
||||
message NotifyClientRejected {
|
||||
uint32 reason = 1;
|
||||
string message = 2;
|
||||
string version = 3;
|
||||
}
|
||||
|
||||
message ServerFeaturesReply {
|
||||
bool matching2_enabled = 1;
|
||||
}
|
||||
|
||||
@@ -70,10 +70,12 @@ public:
|
||||
}
|
||||
|
||||
std::optional<T> Pop() {
|
||||
if (Size() == 0) {
|
||||
// bbport: checked under the lock: a Clear() from another thread (the player stopping,
|
||||
// the game closing) between the check and front() read an empty queue.
|
||||
std::lock_guard guard(m_mutex);
|
||||
if (m_queue.empty()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
std::lock_guard guard(m_mutex);
|
||||
auto result = std::move(m_queue.front());
|
||||
m_queue.pop();
|
||||
return result;
|
||||
|
||||
@@ -387,6 +387,9 @@ bool AvPlayerSource::GetVideoData(AvPlayerFrameInfoEx& video_info) {
|
||||
}
|
||||
|
||||
auto frame = m_video_frames.Pop();
|
||||
if (!frame.has_value()) {
|
||||
return false; // bbport: cleared since the size check (the player stopped)
|
||||
}
|
||||
video_info = frame->info;
|
||||
|
||||
m_current_video_frame = std::move(frame);
|
||||
@@ -409,6 +412,9 @@ bool AvPlayerSource::GetAudioData(AvPlayerFrameInfo& audio_info) {
|
||||
}
|
||||
|
||||
auto frame = m_audio_frames.Pop();
|
||||
if (!frame.has_value()) {
|
||||
return false; // bbport: cleared since the size check (the player stopped)
|
||||
}
|
||||
m_last_audio_ts = frame->info.timestamp;
|
||||
m_last_data_time = std::chrono::high_resolution_clock::now();
|
||||
m_pause_duration = std::chrono::high_resolution_clock::duration(0);
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <cmath>
|
||||
#include <vector>
|
||||
#include <chrono>
|
||||
#include <atomic>
|
||||
#include <cstdio>
|
||||
#include <time.h>
|
||||
#include <sys/resource.h>
|
||||
@@ -753,8 +754,19 @@ void VideoOutDriver::Flip(const Request& req) {
|
||||
presenter->WaitRendered(req.frame);
|
||||
}
|
||||
|
||||
// Update flip status.
|
||||
auto* port = req.port;
|
||||
// bbport: release the buffer this flip replaced before the game can learn of the flip (flip
|
||||
// status, flip events), not after: the game renders into that buffer next and sets its label
|
||||
// to 1 again, which a later reset here cleared (the flip IRQ then found no label: "Out of
|
||||
// order flip IRQ", video_out.cpp). Frames are prepared at submission (SubmitFlipInternal),
|
||||
// so the buffer is no longer read here. From Awaken's BloodBorneButPc.
|
||||
if (port->prev_index != -1) {
|
||||
port->buffer_labels[port->prev_index] = 0;
|
||||
port->SignalVoLabel();
|
||||
}
|
||||
port->prev_index = req.index;
|
||||
|
||||
// Update flip status.
|
||||
{
|
||||
std::unique_lock lock{port->port_mutex};
|
||||
auto& flip_status = port->flip_status;
|
||||
@@ -780,14 +792,6 @@ void VideoOutDriver::Flip(const Request& req) {
|
||||
(req.flip_arg << 16)));
|
||||
}
|
||||
}
|
||||
|
||||
// Reset prev flip label
|
||||
if (port->prev_index != -1) {
|
||||
port->buffer_labels[port->prev_index] = 0;
|
||||
port->SignalVoLabel();
|
||||
}
|
||||
// save to prev buf index
|
||||
port->prev_index = req.index;
|
||||
}
|
||||
|
||||
void VideoOutDriver::DrawBlankFrame() {
|
||||
@@ -811,8 +815,18 @@ bool VideoOutDriver::SubmitFlip(VideoOutPort* port, s32 index, s64 flip_arg,
|
||||
{
|
||||
std::unique_lock lock{port->port_mutex};
|
||||
if (index != -1 && port->flip_status.flip_pending_num > 16) {
|
||||
LOG_ERROR(Lib_VideoOut, "Flip queue is full");
|
||||
return false;
|
||||
// bbport: an EOP flip has nobody to return the error to: refusing it ended the game
|
||||
// (assertion, exit 23), and dropping it would keep its buffer's label (the game may
|
||||
// wait for it). It is queued anyway; frame preparation waits for a free frame.
|
||||
if (!is_eop) {
|
||||
LOG_ERROR(Lib_VideoOut, "Flip queue is full");
|
||||
return false;
|
||||
}
|
||||
static std::atomic<u32> notes{0};
|
||||
if (notes.fetch_add(1, std::memory_order_relaxed) < 4) {
|
||||
std::printf("VideoOut: %d flips pending; EOP flip queued anyway\n",
|
||||
port->flip_status.flip_pending_num);
|
||||
}
|
||||
}
|
||||
|
||||
if (is_eop) {
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <thread>
|
||||
|
||||
#include "common/assert.h"
|
||||
#include "common/elf_info.h"
|
||||
#include "common/logging/log.h"
|
||||
@@ -351,7 +356,27 @@ s32 sceVideoOutSubmitEopFlip(s32 handle, u32 buf_id, u32 mode, s64 flip_arg, voi
|
||||
Platform::IrqC::Instance()->RegisterOnce(
|
||||
Platform::InterruptId::GfxFlip, [=](Platform::InterruptId irq) {
|
||||
ASSERT_MSG(irq == Platform::InterruptId::GfxFlip, "An unexpected IRQ occured");
|
||||
ASSERT_MSG(port->buffer_labels[buf_id] == 1, "Out of order flip IRQ");
|
||||
// bbport: the label write and this IRQ can arrive out of order (the write may still
|
||||
// be on its way from the draw recording thread; timing changes such as precise frame
|
||||
// pacing make it likelier). This used to stop the game ("Out of order flip IRQ", exit
|
||||
// 23). Wait briefly for the label, bounded since the writer may be this very thread;
|
||||
// then flip anyway: at worst a frame is shown twice, and the game still gets its flip
|
||||
// event (skipping it could leave the game waiting for that buffer forever).
|
||||
if (port->buffer_labels[buf_id] != 1) {
|
||||
const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(20);
|
||||
while (reinterpret_cast<volatile u64&>(port->buffer_labels[buf_id]) != 1 &&
|
||||
std::chrono::steady_clock::now() < deadline) {
|
||||
std::this_thread::yield();
|
||||
}
|
||||
if (port->buffer_labels[buf_id] != 1) {
|
||||
static std::atomic<u32> reported{0};
|
||||
if (reported.fetch_add(1, std::memory_order_relaxed) < 5) {
|
||||
std::printf("VideoOut: flip IRQ for buffer %u before its label was set; "
|
||||
"flipping anyway\n",
|
||||
buf_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
const auto result = driver->SubmitFlip(port, buf_id, flip_arg, true);
|
||||
ASSERT_MSG(result, "EOP flip submission failed");
|
||||
});
|
||||
|
||||
@@ -209,6 +209,16 @@ struct Info : InfoPersistent {
|
||||
return *reinterpret_cast<const T*>(&FlatUserData()[sharp_idx]);
|
||||
}
|
||||
|
||||
/// bbport: the table pointer in user data register ptr_index is null (see ReadUdReg).
|
||||
bool UdPointerNull(u32 ptr_index) const noexcept {
|
||||
if (ptr_index == IR::NumScalarRegs) {
|
||||
return false;
|
||||
}
|
||||
u64 pointer;
|
||||
std::memcpy(&pointer, &UserData()[ptr_index], sizeof(pointer));
|
||||
return (pointer & 0xFFFFFFFFFFFFULL) < 0x10000;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
T ReadUdReg(u32 ptr_index, u32 dword_offset) const noexcept {
|
||||
T data;
|
||||
@@ -217,6 +227,12 @@ struct Info : InfoPersistent {
|
||||
if (ptr_index != IR::NumScalarRegs) {
|
||||
std::memcpy(&base, &ud[ptr_index], sizeof(base));
|
||||
base = reinterpret_cast<const u32*>(VAddr(base) & 0xFFFFFFFFFFFFULL);
|
||||
// bbport: some draws leave the table pointer null (the SRT walker meets the same
|
||||
// loads at 0x0..0xc first). The GPU reads nothing there; here it crashed the game.
|
||||
// A zero descriptor is a null buffer, which the callers skip.
|
||||
if (VAddr(base) < 0x10000) {
|
||||
return T{};
|
||||
}
|
||||
}
|
||||
// bbport: an unbound descriptor table must reach the existing empty-sharp handling.
|
||||
if (!base) {
|
||||
|
||||
@@ -316,7 +316,8 @@ private:
|
||||
}
|
||||
|
||||
[[nodiscard]] u32 Diff() const {
|
||||
ASSERT_MSG(ce_count >= de_count, "DE counter is ahead of CE");
|
||||
// bbport: compared as a signed distance, right across the u32 wrap.
|
||||
ASSERT_MSG(static_cast<s32>(ce_count - de_count) >= 0, "DE counter is ahead of CE");
|
||||
return ce_count - de_count;
|
||||
}
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ std::string_view NameOf(DataFormat fmt) {
|
||||
case DataFormat::FormatBc7:
|
||||
return "FormatBc7";
|
||||
default:
|
||||
UNREACHABLE();
|
||||
return "Unknown"; // bbport: reached while logging a garbage descriptor (was exit 23)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,7 +107,7 @@ std::string_view NameOf(NumberFormat fmt) {
|
||||
case NumberFormat::Ubscaled:
|
||||
return "Unscaled";
|
||||
default:
|
||||
UNREACHABLE();
|
||||
return "Unknown"; // bbport: as above
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -491,7 +491,9 @@ struct Sampler {
|
||||
case AnisoRatio::Sixteen:
|
||||
return 16.0f;
|
||||
default:
|
||||
UNREACHABLE();
|
||||
// bbport: 5-7 (reserved, a stale or garbage sharp) clamp to 16x as the hardware's
|
||||
// maximum; this ended the game (UNREACHABLE, exit 23).
|
||||
return 16.0f;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <bit>
|
||||
#include "common/assert.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "video_core/amdgpu/tiling.h"
|
||||
|
||||
#include <magic_enum/magic_enum.hpp>
|
||||
@@ -11,6 +15,16 @@ namespace AmdGpu {
|
||||
static constexpr u32 MICROTILE_SIZE = 8;
|
||||
static constexpr u32 DRAM_ROW_SIZE = 1024;
|
||||
|
||||
/// bbport: a mode outside the tables (a garbage or unsupported descriptor) is reported a few
|
||||
/// times and treated as a known one (linear, 1x1_16 banks); it ended the game (UNREACHABLE,
|
||||
/// exit 23). At worst that surface renders wrong.
|
||||
static void NoteUnknown(const char* what, u32 value) {
|
||||
static std::atomic<u32> notes{0};
|
||||
if (notes.fetch_add(1, std::memory_order_relaxed) < 8) {
|
||||
LOG_ERROR(Render, "Unknown {} = {}, using a fallback", what, value);
|
||||
}
|
||||
}
|
||||
|
||||
std::string_view NameOf(TileMode tile_mode) {
|
||||
return magic_enum::enum_name(tile_mode);
|
||||
}
|
||||
@@ -62,7 +76,8 @@ ArrayMode GetArrayMode(TileMode tile_mode) {
|
||||
case TileMode::DisplayLinearGeneral:
|
||||
return ArrayMode::ArrayLinearGeneral;
|
||||
default:
|
||||
UNREACHABLE_MSG("Unknown tile mode = {}", u32(tile_mode));
|
||||
NoteUnknown("tile mode", u32(tile_mode));
|
||||
return ArrayMode::ArrayLinearAligned;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,7 +116,8 @@ MicroTileMode GetMicroTileMode(TileMode tile_mode) {
|
||||
case TileMode::Thick3DXThick:
|
||||
return MicroTileMode::Thick;
|
||||
default:
|
||||
UNREACHABLE_MSG("Unknown tile mode = {}", u32(tile_mode));
|
||||
NoteUnknown("tile mode", u32(tile_mode));
|
||||
return MicroTileMode::Display;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,7 +155,8 @@ PipeConfig GetPipeConfig(TileMode tile_mode) {
|
||||
case TileMode::DisplayLinearGeneral:
|
||||
return PipeConfig::P2;
|
||||
default:
|
||||
UNREACHABLE_MSG("Unknown tile mode = {}", u32(tile_mode));
|
||||
NoteUnknown("tile mode", u32(tile_mode));
|
||||
return PipeConfig::P8_32x32_16x16;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,7 +193,8 @@ PipeConfig GetAltPipeConfig(TileMode tile_mode) {
|
||||
case TileMode::DisplayLinearGeneral:
|
||||
return PipeConfig::P2;
|
||||
default:
|
||||
UNREACHABLE_MSG("Unknown tile mode = {}", u32(tile_mode));
|
||||
NoteUnknown("tile mode", u32(tile_mode));
|
||||
return PipeConfig::P16_32x32_8x16;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -213,7 +231,8 @@ u32 GetSampleSplit(TileMode tile_mode) {
|
||||
case TileMode::Thin3DThinPrt:
|
||||
return 2;
|
||||
default:
|
||||
UNREACHABLE_MSG("Unknown tile mode = {}", u32(tile_mode));
|
||||
NoteUnknown("tile mode", u32(tile_mode));
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -253,7 +272,8 @@ u32 GetTileSplitHw(TileMode tile_mode) {
|
||||
case TileMode::Depth2DThinPrt1K:
|
||||
return 1024;
|
||||
default:
|
||||
UNREACHABLE_MSG("Unknown tile mode = {}", u32(tile_mode));
|
||||
NoteUnknown("tile mode", u32(tile_mode));
|
||||
return 64;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -277,7 +297,8 @@ u32 GetBankWidth(MacroTileMode mode) {
|
||||
case MacroTileMode::Mode_1x1_2_Dup3:
|
||||
return 1;
|
||||
default:
|
||||
UNREACHABLE_MSG("Unknown macro tile mode = {}", u32(mode));
|
||||
NoteUnknown("macro tile mode", u32(mode));
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -304,7 +325,8 @@ u32 GetBankHeight(MacroTileMode mode) {
|
||||
case MacroTileMode::Mode_1x8_16:
|
||||
return 8;
|
||||
default:
|
||||
UNREACHABLE_MSG("Unknown macro tile mode = {}", u32(mode));
|
||||
NoteUnknown("macro tile mode", u32(mode));
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -331,7 +353,8 @@ u32 GetNumBanks(MacroTileMode mode) {
|
||||
case MacroTileMode::Mode_1x1_16_Dup2:
|
||||
return 16;
|
||||
default:
|
||||
UNREACHABLE_MSG("Unknown macro tile mode = {}", u32(mode));
|
||||
NoteUnknown("macro tile mode", u32(mode));
|
||||
return 16;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -357,7 +380,8 @@ u32 GetMacrotileAspect(MacroTileMode mode) {
|
||||
case MacroTileMode::Mode_1x4_16_Dup:
|
||||
return 4;
|
||||
default:
|
||||
UNREACHABLE_MSG("Unknown macro tile mode = {}", u32(mode));
|
||||
NoteUnknown("macro tile mode", u32(mode));
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -383,7 +407,8 @@ u32 GetAltBankHeight(MacroTileMode mode) {
|
||||
case MacroTileMode::Mode_1x4_16_Dup:
|
||||
return 4;
|
||||
default:
|
||||
UNREACHABLE_MSG("Unknown macro tile mode = {}", u32(mode));
|
||||
NoteUnknown("macro tile mode", u32(mode));
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -410,7 +435,8 @@ u32 GetAltNumBanks(MacroTileMode mode) {
|
||||
case MacroTileMode::Mode_1x8_16:
|
||||
return 16;
|
||||
default:
|
||||
UNREACHABLE_MSG("Unknown macro tile mode = {}", u32(mode));
|
||||
NoteUnknown("macro tile mode", u32(mode));
|
||||
return 8;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -435,7 +461,8 @@ u32 GetAltMacrotileAspect(MacroTileMode mode) {
|
||||
case MacroTileMode::Mode_1x4_16_Dup:
|
||||
return 2;
|
||||
default:
|
||||
UNREACHABLE_MSG("Unknown macro tile mode = {}", u32(mode));
|
||||
NoteUnknown("macro tile mode", u32(mode));
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -460,7 +487,8 @@ bool IsMacroTiled(ArrayMode array_mode) {
|
||||
case ArrayMode::ArrayPrt3DTiledThick:
|
||||
return true;
|
||||
default:
|
||||
UNREACHABLE_MSG("Unknown array mode = {}", u32(array_mode));
|
||||
NoteUnknown("array mode", u32(array_mode));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -485,7 +513,8 @@ bool IsPrt(ArrayMode array_mode) {
|
||||
case ArrayMode::Array3DTiledXThick:
|
||||
return false;
|
||||
default:
|
||||
UNREACHABLE_MSG("Unknown array mode = {}", u32(array_mode));
|
||||
NoteUnknown("array mode", u32(array_mode));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -511,7 +540,8 @@ u32 GetMicroTileThickness(ArrayMode array_mode) {
|
||||
case ArrayMode::Array3DTiledXThick:
|
||||
return 8;
|
||||
default:
|
||||
UNREACHABLE_MSG("Unknown array mode = {}", u32(array_mode));
|
||||
NoteUnknown("array mode", u32(array_mode));
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -525,7 +555,8 @@ u32 GetPipeCount(PipeConfig pipe_cfg) {
|
||||
case PipeConfig::P16_32x32_8x16:
|
||||
return 16;
|
||||
default:
|
||||
UNREACHABLE_MSG("Unknown pipe config = {}", u32(pipe_cfg));
|
||||
NoteUnknown("pipe config", u32(pipe_cfg));
|
||||
return 8;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -542,19 +573,29 @@ u32 CalculateTileSplit(TileMode tile_mode, ArrayMode array_mode, MicroTileMode m
|
||||
}
|
||||
|
||||
MacroTileMode CalculateMacrotileMode(TileMode tile_mode, u32 bpp, u32 num_samples) {
|
||||
ASSERT_MSG(std::has_single_bit(num_samples) && num_samples <= 16, "Invalid sample count {}",
|
||||
num_samples);
|
||||
ASSERT_MSG(bpp >= 1 && bpp <= 128, "Invalid bpp {}", bpp);
|
||||
// bbport: input out of range (a garbage sharp: MSAA levels, format) is clamped and reported
|
||||
// (NoteUnknown) instead of the assertions that ended the game.
|
||||
if (!std::has_single_bit(num_samples) || num_samples > 16) {
|
||||
NoteUnknown("sample count", num_samples);
|
||||
num_samples = 1;
|
||||
}
|
||||
if (bpp < 1 || bpp > 128) {
|
||||
NoteUnknown("bits per element", bpp);
|
||||
bpp = std::clamp(bpp, 1u, 128u);
|
||||
}
|
||||
|
||||
const ArrayMode array_mode = GetArrayMode(tile_mode);
|
||||
ASSERT_MSG(IsMacroTiled(array_mode), "Tile mode not macro tiled");
|
||||
if (!IsMacroTiled(array_mode)) {
|
||||
NoteUnknown("macro tiled mode", u32(tile_mode));
|
||||
return MacroTileMode::Mode_1x1_16;
|
||||
}
|
||||
|
||||
const MicroTileMode micro_tile_mode = GetMicroTileMode(tile_mode);
|
||||
const u32 tile_thickness = GetMicroTileThickness(array_mode);
|
||||
const u32 tile_bytes_1x = bpp * MICROTILE_SIZE * MICROTILE_SIZE * tile_thickness / 8;
|
||||
const u32 tilesplic = CalculateTileSplit(tile_mode, array_mode, micro_tile_mode, bpp);
|
||||
const u32 tile_bytes = std::min(tilesplic, num_samples * tile_bytes_1x);
|
||||
const u32 mtm_idx = std::bit_width(tile_bytes / 64) - 1;
|
||||
const u32 mtm_idx = std::bit_width(std::max(tile_bytes / 64, 1u)) - 1;
|
||||
return IsPrt(array_mode) ? MacroTileMode(mtm_idx + 8) : MacroTileMode(mtm_idx);
|
||||
}
|
||||
|
||||
|
||||
@@ -102,6 +102,14 @@ void UniqueBuffer::Create(vk::BufferCreateInfo& buffer_ci, MemoryType mem_type,
|
||||
VkBuffer unsafe_buffer{};
|
||||
VkResult result = vmaCreateBuffer(allocator, &buffer_ci_unsafe, &alloc_ci, &unsafe_buffer,
|
||||
&allocation, out_alloc_info);
|
||||
if (result == VK_ERROR_OUT_OF_DEVICE_MEMORY) {
|
||||
// bbport: VRAM full: past the budget, in system memory if need be (see image.cpp).
|
||||
Vulkan::NoteOverBudget("buffer");
|
||||
VmaAllocationCreateInfo over_budget = alloc_ci;
|
||||
over_budget.flags &= ~VMA_ALLOCATION_CREATE_WITHIN_BUDGET_BIT;
|
||||
result = vmaCreateBuffer(allocator, &buffer_ci_unsafe, &over_budget, &unsafe_buffer,
|
||||
&allocation, out_alloc_info);
|
||||
}
|
||||
ASSERT_MSG(result == VK_SUCCESS, "Failed allocating buffer with error {}",
|
||||
vk::to_string(vk::Result{result}));
|
||||
buffer = vk::Buffer{unsafe_buffer};
|
||||
|
||||
@@ -1399,6 +1399,8 @@ void BufferCache::ProcessFaultBuffer() {
|
||||
}
|
||||
|
||||
void BufferCache::SynchronizeDmaBuffers() {
|
||||
dma_second.store(BbStats::coarse_second.load(std::memory_order_relaxed),
|
||||
std::memory_order_relaxed);
|
||||
for (const auto& range : resident_ranges) {
|
||||
const u64 page = range.start >> (ARENA_PAGE_BITS - block_shift);
|
||||
const VAddr device_addr = range.start << block_shift;
|
||||
@@ -1479,6 +1481,24 @@ void BufferCache::EnsureResident(const Buffer* arena, u64 first_block, u64 last_
|
||||
IntervalList bind_ranges;
|
||||
resident_ranges.ForEachGap(first_block, last_block + 1,
|
||||
[&](u64 start, u64 end) { bind_ranges.Add({start, end}); });
|
||||
// bbport: blocks evicted from the 2 MiB groups of this request (EvictIdleCopies) come back
|
||||
// with it, whole. A draw reads past its own range into bytes another binding uploaded (vertex
|
||||
// buffers are bound without a size): back in VRAM with only this range uploaded, those read
|
||||
// zeros until that binding came again, and geometry went missing. (Awaken's BloodBorneButPc.)
|
||||
IntervalList restored;
|
||||
if (!evicted_blocks.Empty() && block_shift <= USE_GROUP_BITS) {
|
||||
const u64 group_shift = USE_GROUP_BITS - block_shift;
|
||||
const u64 from = (first_block >> group_shift) << group_shift;
|
||||
const u64 to = ((last_block >> group_shift) + 1) << group_shift;
|
||||
evicted_blocks.ForEachInRange(from, to, [&](const Interval& range) {
|
||||
const Interval part{std::max(from, range.start), std::min(to, range.end)};
|
||||
restored.Add(part);
|
||||
bind_ranges.Add(part);
|
||||
});
|
||||
if (!restored.Empty()) {
|
||||
evicted_blocks.Subtract(from, to);
|
||||
}
|
||||
}
|
||||
if (bind_ranges.Empty()) {
|
||||
memo = {first_block, last_block, ResidencyGeneration()};
|
||||
return;
|
||||
@@ -1500,6 +1520,8 @@ void BufferCache::EnsureResident(const Buffer* arena, u64 first_block, u64 last_
|
||||
vram_blocks += range.end - range.start;
|
||||
}
|
||||
|
||||
// bbport: blocks put into slots other blocks left, cleared below.
|
||||
boost::container::small_vector<u64, 16> reused;
|
||||
if (vram_blocks != 0) {
|
||||
// bbport: the slots of blocks that left VRAM first, block by block. A request of several
|
||||
// blocks only ever took new space, and the free slots piled up while new 64 MiB chunks
|
||||
@@ -1508,6 +1530,7 @@ void BufferCache::EnsureResident(const Buffer* arena, u64 first_block, u64 last_
|
||||
for (const auto& range : bind_ranges) {
|
||||
u64 block = range.start;
|
||||
for (; block < range.end && free_slot_count != 0; ++block) {
|
||||
reused.push_back(block);
|
||||
const auto [memory, memory_offset] = AllocateResidency(block_size);
|
||||
auto* last = binds.empty() ? nullptr : &binds.back();
|
||||
if (last && last->end == block && last->memory == memory &&
|
||||
@@ -1565,8 +1588,31 @@ void BufferCache::EnsureResident(const Buffer* arena, u64 first_block, u64 last_
|
||||
|
||||
staging.Flush();
|
||||
runtime.CopyBuffer(staging.buffer, bda_pagetable_buffer.get(), copies);
|
||||
|
||||
// bbport: a slot another block left still holds that block's data; only the ranges bound are
|
||||
// uploaded into it. Vertex buffers are bound without a size (dynamic vertex input), so a draw
|
||||
// reading past its range read zeros in new VRAM (the driver clears it) and read the other
|
||||
// block's data in a reused slot: stretched polygons, flashing lights. Cleared like new VRAM,
|
||||
// before the uploads that follow. (Awaken's BloodBorneButPc.)
|
||||
for (size_t i = 0; i < reused.size();) {
|
||||
size_t j = i + 1;
|
||||
while (j < reused.size() && reused[j] == reused[j - 1] + 1) {
|
||||
++j;
|
||||
}
|
||||
runtime.FillBuffer(arena, (reused[i] << block_shift) - arena->cpu_addr,
|
||||
u64(j - i) * block_size, 0u);
|
||||
i = j;
|
||||
}
|
||||
// Evicted blocks back from the game's memory, whole (marked CPU-modified when they left).
|
||||
for (const auto& range : restored) {
|
||||
SynchronizeMemory(arena, range.start << block_shift,
|
||||
static_cast<u32>((range.end - range.start) << block_shift), false, false);
|
||||
}
|
||||
}
|
||||
|
||||
// bbport: VRAM taken by residency chunks, for BB_RESIDENCY_VRAM_LIMIT_MB (tests).
|
||||
static std::atomic<u64> residency_vram_bytes{0};
|
||||
|
||||
std::pair<vk::DeviceMemory, u64> BufferCache::AllocateResidency(u64 bytes) {
|
||||
if (bytes == block_size && free_slot_count != 0) {
|
||||
// bbport: a slot of the fullest chunk: the emptier ones drain and go back to the driver.
|
||||
@@ -1600,7 +1646,6 @@ std::pair<vk::DeviceMemory, u64> BufferCache::AllocateResidency(u64 bytes) {
|
||||
const char* env = std::getenv("BB_RESIDENCY_VRAM_LIMIT_MB");
|
||||
return env ? std::strtoull(env, nullptr, 10) << 20 : 0;
|
||||
}();
|
||||
static std::atomic<u64> vram_bytes{0};
|
||||
const auto device = instance.GetDevice();
|
||||
vk::Result result = vk::Result::eErrorOutOfDeviceMemory;
|
||||
vk::DeviceMemory memory{};
|
||||
@@ -1612,14 +1657,20 @@ std::pair<vk::DeviceMemory, u64> BufferCache::AllocateResidency(u64 bytes) {
|
||||
BbStats::residency_alloc_bytes.fetch_add(size, std::memory_order_relaxed);
|
||||
return memory;
|
||||
}
|
||||
if (!test_limit || vram_bytes.load(std::memory_order_relaxed) + size <= test_limit) {
|
||||
if (!test_limit || residency_vram_bytes.load(std::memory_order_relaxed) + size <= test_limit) {
|
||||
auto vram = device.allocateMemory({.allocationSize = size,
|
||||
.memoryTypeIndex = arena_memory_type_index});
|
||||
result = vram.result;
|
||||
memory = vram.value;
|
||||
}
|
||||
if (result != vk::Result::eSuccess) {
|
||||
// bbport: a failed VRAM allocation counts as full for 30 s (UpdateVramLevel).
|
||||
vram_full_until.store(BbStats::coarse_second.load(std::memory_order_relaxed) + 30,
|
||||
std::memory_order_relaxed);
|
||||
vram_level.store(2, std::memory_order_relaxed);
|
||||
}
|
||||
if (result == vk::Result::eSuccess) {
|
||||
vram_bytes.fetch_add(size, std::memory_order_relaxed);
|
||||
residency_vram_bytes.fetch_add(size, std::memory_order_relaxed);
|
||||
} else if ((result == vk::Result::eErrorOutOfDeviceMemory ||
|
||||
result == vk::Result::eErrorOutOfHostMemory) &&
|
||||
arena_fallback_type_index) {
|
||||
@@ -1661,6 +1712,38 @@ std::pair<vk::DeviceMemory, u64> BufferCache::AllocateResidency(u64 bytes) {
|
||||
size = bytes;
|
||||
memory = allocate(size);
|
||||
}
|
||||
// bbport (deadinside28#163, a 4 GB card: "Failed to allocate 1 MiB for guest memory copies"
|
||||
// while an area loads): chunks emptied in the last 5 s were still held, waiting to go back
|
||||
// to the driver. VRAM full, they go now (the GPU done with them), and the spare block
|
||||
// being allocated in the background is taken if it came; then the request again.
|
||||
u64 released = 0;
|
||||
if (!memory) {
|
||||
released = ReleaseEmptyResidencyChunks(
|
||||
BbStats::coarse_second.load(std::memory_order_relaxed), 0);
|
||||
if (spare_residency.valid()) {
|
||||
const vk::DeviceMemory spare = spare_residency.get();
|
||||
if (spare && size <= ResidencyBlock) {
|
||||
memory = spare;
|
||||
size = ResidencyBlock;
|
||||
} else if (spare) {
|
||||
instance.GetDevice().freeMemory(spare);
|
||||
residency_vram_bytes.fetch_sub(std::min(ResidencyBlock, residency_vram_bytes.load()),
|
||||
std::memory_order_relaxed);
|
||||
BbStats::residency_alloc_bytes.fetch_sub(ResidencyBlock, std::memory_order_relaxed);
|
||||
BbStats::device_free_bytes.fetch_add(ResidencyBlock, std::memory_order_relaxed);
|
||||
released += ResidencyBlock;
|
||||
}
|
||||
}
|
||||
if (!memory && released != 0) {
|
||||
memory = allocate(size);
|
||||
}
|
||||
static std::atomic<u32> told_release{0};
|
||||
if (released != 0 && told_release.fetch_add(1, std::memory_order_relaxed) < 8) {
|
||||
std::printf("Guest memory: VRAM is full: %llu MiB of empty copies freed at once, %s\n",
|
||||
(unsigned long long)(released >> 20),
|
||||
memory ? "the request fits now" : "still not enough");
|
||||
}
|
||||
}
|
||||
const auto& types = instance.GetMemoryProperties();
|
||||
for (u32 i = 0; !memory && i < types.memoryTypeCount; ++i) {
|
||||
if (((arena_memory_type_bits >> i) & 1) && i != arena_memory_type_index) {
|
||||
@@ -1672,7 +1755,10 @@ std::pair<vk::DeviceMemory, u64> BufferCache::AllocateResidency(u64 bytes) {
|
||||
}
|
||||
}
|
||||
}
|
||||
ASSERT_MSG(memory, "Failed to allocate {} MiB for guest memory copies: VRAM is full", size >> 20);
|
||||
ASSERT_MSG(memory,
|
||||
"Failed to allocate {} MiB for guest memory copies: VRAM is full ({} MiB of "
|
||||
"empty copies freed, not enough)",
|
||||
size >> 20, released >> 20);
|
||||
residency_memory = memory;
|
||||
residency_size = size;
|
||||
residency_used = 0;
|
||||
@@ -1681,7 +1767,8 @@ std::pair<vk::DeviceMemory, u64> BufferCache::AllocateResidency(u64 bytes) {
|
||||
const u64 offset = residency_used;
|
||||
residency_used += bytes;
|
||||
residency_chunks[static_cast<VkDeviceMemory>(residency_memory)].used += bytes;
|
||||
if (!spare_residency.valid() && residency_used * 2 >= residency_size) {
|
||||
if (!spare_residency.valid() && residency_used * 2 >= residency_size &&
|
||||
vram_level.load(std::memory_order_relaxed) == 0) {
|
||||
spare_residency = std::async(std::launch::async, allocate, ResidencyBlock);
|
||||
}
|
||||
NoteResidencyUnused();
|
||||
@@ -1838,15 +1925,29 @@ void BufferCache::EvictVramBlock(u64 block) {
|
||||
|
||||
void BufferCache::ProcessIdleBlocks() {
|
||||
const u32 now = BbStats::coarse_second.load(std::memory_order_relaxed);
|
||||
if (now == idle_scan_second || !GuestInPlace() || VramIdleSeconds() == 0) {
|
||||
if (now == idle_scan_second) {
|
||||
return;
|
||||
}
|
||||
idle_scan_second = now;
|
||||
UpdateVramLevel(now);
|
||||
if (VramIdleSeconds() == 0) {
|
||||
return;
|
||||
}
|
||||
if (!GuestInPlace()) {
|
||||
// bbport: the 0.3 memory model (NVIDIA, Intel): VRAM copies that leave under pressure.
|
||||
EvictIdleCopies(now);
|
||||
ReleaseEmptyResidencyChunks(now, 5);
|
||||
return;
|
||||
}
|
||||
if (LayerMode()) {
|
||||
LayerMaintain();
|
||||
LayerProcessIdle();
|
||||
return;
|
||||
}
|
||||
const int level = vram_level.load(std::memory_order_relaxed);
|
||||
idle_seconds_now = level >= 2 ? std::min(VramIdleSeconds(), 3u)
|
||||
: level == 1 ? std::min(VramIdleSeconds(), 15u)
|
||||
: VramIdleSeconds();
|
||||
// Blocks in VRAM whose 2 MiB were not bound for the idle time go back in place
|
||||
// (ProcessDemotions: only blocks of the game's direct memory without GPU-written data).
|
||||
const u64 tick = scheduler.CurrentTick();
|
||||
@@ -1855,7 +1956,7 @@ void BufferCache::ProcessIdleBlocks() {
|
||||
continue; // in place
|
||||
}
|
||||
for (u64 block = range.start; block < range.end; ++block) {
|
||||
if (now - group_use[(block << block_shift) >> USE_GROUP_BITS] >= VramIdleSeconds()) {
|
||||
if (now - group_use[(block << block_shift) >> USE_GROUP_BITS] >= idle_seconds_now) {
|
||||
demotions_waiting.push_back({block, tick, false, true});
|
||||
}
|
||||
}
|
||||
@@ -1865,12 +1966,14 @@ void BufferCache::ProcessIdleBlocks() {
|
||||
// into fuller chunks (AllocateResidency), and the chunk empties. Free slots spread evenly
|
||||
// (half of 1.3 GiB) left no chunk empty. One chunk every 10 s, each once a minute at most
|
||||
// (blocks with GPU-written data stay).
|
||||
if (free_slot_count * block_size >= 128_MB && now - last_evacuation >= 10) {
|
||||
// Under VRAM pressure (vram_level) whatever the free slots, every 2 s when full.
|
||||
if ((free_slot_count * block_size >= 128_MB || level >= 1) &&
|
||||
now - last_evacuation >= (level >= 2 ? 2u : 10u)) {
|
||||
auto sparsest = residency_chunks.end();
|
||||
for (auto it = residency_chunks.begin(); it != residency_chunks.end(); ++it) {
|
||||
const ResidencyChunk& chunk = it->second;
|
||||
if (it->first == static_cast<VkDeviceMemory>(residency_memory) || chunk.used == 0 ||
|
||||
now - chunk.evacuated_second < 60) {
|
||||
now - chunk.evacuated_second < (level >= 2 ? 10u : 60u)) {
|
||||
continue;
|
||||
}
|
||||
if (sparsest == residency_chunks.end() || chunk.used < sparsest->second.used) {
|
||||
@@ -1896,27 +1999,151 @@ void BufferCache::ProcessIdleBlocks() {
|
||||
}
|
||||
}
|
||||
// Chunks without blocks for 5 s, the rebinds away from them long done: back to the driver.
|
||||
ReleaseEmptyResidencyChunks(now, 5);
|
||||
}
|
||||
|
||||
// bbport (after Awaken's BloodBorneButPc 0.9.17): how full VRAM is against the driver's budget.
|
||||
// BB_VRAM_PRESSURE_PERCENT (85): tight from there, full 5 points above; 0 turns it off.
|
||||
void BufferCache::UpdateVramLevel(u32 now) {
|
||||
static const u64 percent = [] {
|
||||
const char* env = std::getenv("BB_VRAM_PRESSURE_PERCENT");
|
||||
return std::min<u64>(env ? std::strtoull(env, nullptr, 10) : 85, 95);
|
||||
}();
|
||||
int level = 0;
|
||||
u64 used = 0, budget = 0;
|
||||
if (percent != 0 && instance.CanReportMemoryUsage()) {
|
||||
used = instance.GetDeviceMemoryUsage();
|
||||
budget = instance.GetDeviceMemoryBudgetNow();
|
||||
if (budget != 0) {
|
||||
level = used * 100 >= budget * (percent + 5) ? 2 : used * 100 >= budget * percent ? 1 : 0;
|
||||
}
|
||||
}
|
||||
if (now < vram_full_until.load(std::memory_order_relaxed)) {
|
||||
level = 2;
|
||||
}
|
||||
const int was = vram_level.exchange(level, std::memory_order_relaxed);
|
||||
if (level != was && vram_level_reports < 40) {
|
||||
++vram_level_reports;
|
||||
static constexpr const char* kInPlace[] = {
|
||||
"room again: the game's data may get VRAM copies",
|
||||
"tight: copies of the game's data unused for 15 s go back to system memory",
|
||||
"full: no new copies of the game's data, unused ones go back after 3 s",
|
||||
};
|
||||
static constexpr const char* kCopies[] = {
|
||||
"room again: the game's data stays in VRAM",
|
||||
"tight: the game's data unused for 30 s leaves VRAM",
|
||||
"full: the game's data unused for 10 s leaves VRAM",
|
||||
};
|
||||
std::printf("Guest memory: VRAM %llu of %llu MiB, %s\n", (unsigned long long)(used >> 20),
|
||||
(unsigned long long)(budget >> 20),
|
||||
GuestInPlace() ? kInPlace[level] : kCopies[level]);
|
||||
}
|
||||
}
|
||||
|
||||
// bbport (after Awaken's BloodBorneButPc 0.9.17): without the PC memory model (it needs an AMD
|
||||
// GPU) every block of the game's memory the GPU used kept its VRAM copy, and a 4 GB card ran out
|
||||
// (deadinside28#163, exit 23). While VRAM is tight the copies unused for a while go. Kept: blocks
|
||||
// with data the GPU wrote (only VRAM has it) or a twin of it, and all of them while shaders read
|
||||
// the game's memory through the page table (dma_second).
|
||||
void BufferCache::EvictIdleCopies(u32 now) {
|
||||
const int level = vram_level.load(std::memory_order_relaxed);
|
||||
if (level == 0) {
|
||||
return;
|
||||
}
|
||||
// Longer than the PC memory model's 15 s and 3 s: an evicted block costs an upload and can show
|
||||
// as late geometry when it comes back.
|
||||
const u32 idle = std::min(VramIdleSeconds(), level >= 2 ? 10u : 30u);
|
||||
const u32 dma = dma_second.load(std::memory_order_relaxed);
|
||||
if (dma != 0 && now - dma < idle) {
|
||||
if (dma_kept_reports < 10 && now - dma_kept_second >= 60) {
|
||||
++dma_kept_reports;
|
||||
dma_kept_second = now;
|
||||
std::printf("Guest memory: VRAM tight, but shaders read the game's memory directly: "
|
||||
"its unused data stays in VRAM\n");
|
||||
}
|
||||
return;
|
||||
}
|
||||
const bool twins_on = TwinsEnabled();
|
||||
std::vector<u64> blocks;
|
||||
for (const auto& range : resident_ranges) {
|
||||
if (!residency_chunks.contains(static_cast<VkDeviceMemory>(range.memory))) {
|
||||
continue;
|
||||
}
|
||||
for (u64 block = range.start; block < range.end; ++block) {
|
||||
const VAddr address = block << block_shift;
|
||||
const u64 group = std::min<u64>(address >> USE_GROUP_BITS, group_use.size() - 1);
|
||||
if (now - group_use[group] < idle || gpu_modified_ranges.Intersects(address, block_size) ||
|
||||
memory_tracker->IsRegionGpuModified(address, block_size)) {
|
||||
continue;
|
||||
}
|
||||
if (twins_on) {
|
||||
// A twin keeps the guest's bytes of a GPU-written page: that block stays.
|
||||
std::scoped_lock lk{twins_mutex};
|
||||
const auto twin = twins.lower_bound(address);
|
||||
if (twin != twins.end() && twin->first < address + block_size) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
blocks.push_back(block);
|
||||
}
|
||||
}
|
||||
if (blocks.empty()) {
|
||||
return;
|
||||
}
|
||||
for (const u64 block : blocks) {
|
||||
EvictVramBlock(block);
|
||||
evicted_blocks.Add({block, block + 1});
|
||||
}
|
||||
// Their page table entries are cleared: a shader reading one through it reports a fault and
|
||||
// the block is bound again (ProcessFaultBuffer) instead of the shader reading unbound memory.
|
||||
for (size_t i = 0; i < blocks.size();) {
|
||||
size_t j = i + 1;
|
||||
while (j < blocks.size() && blocks[j] == blocks[j - 1] + 1) {
|
||||
++j;
|
||||
}
|
||||
runtime.FillBuffer(bda_pagetable_buffer.get(), blocks[i] * sizeof(vk::DeviceAddress),
|
||||
(j - i) * sizeof(vk::DeviceAddress), 0u);
|
||||
i = j;
|
||||
}
|
||||
const u64 bytes = u64(blocks.size()) * block_size;
|
||||
evicted_bytes += bytes;
|
||||
if (evict_reports < 20 &&
|
||||
(evicted_bytes == bytes || (evicted_bytes >> 28) != ((evicted_bytes - bytes) >> 28))) {
|
||||
++evict_reports;
|
||||
std::printf("Guest memory: VRAM tight, %llu MiB of the game's data unused for %u s left VRAM "
|
||||
"so far (uploaded again when used)\n",
|
||||
(unsigned long long)(evicted_bytes >> 20), idle);
|
||||
}
|
||||
}
|
||||
|
||||
u64 BufferCache::ReleaseEmptyResidencyChunks(u32 now, u32 min_seconds) {
|
||||
u64 freed = 0;
|
||||
for (auto it = residency_chunks.begin(); it != residency_chunks.end();) {
|
||||
const ResidencyChunk& chunk = it->second;
|
||||
const VkDeviceMemory memory = it->first;
|
||||
if (chunk.used != 0 || memory == static_cast<VkDeviceMemory>(residency_memory) ||
|
||||
now - chunk.empty_second < 5 || !pending_binds.empty() ||
|
||||
now - chunk.empty_second < min_seconds || !pending_binds.empty() ||
|
||||
!scheduler.GetWorkSemaphore()->IsFree(chunk.empty_tick + 1)) {
|
||||
++it;
|
||||
continue;
|
||||
}
|
||||
free_slot_count -= chunk.free_offsets.size();
|
||||
instance.GetDevice().freeMemory(vk::DeviceMemory{memory});
|
||||
freed += chunk.size;
|
||||
residency_vram_bytes.fetch_sub(std::min(chunk.size, residency_vram_bytes.load()),
|
||||
std::memory_order_relaxed);
|
||||
BbStats::residency_alloc_bytes.fetch_sub(chunk.size, std::memory_order_relaxed);
|
||||
BbStats::device_free_bytes.fetch_add(chunk.size, std::memory_order_relaxed);
|
||||
BbStats::vram_chunks_freed_bytes.fetch_add(chunk.size, std::memory_order_relaxed);
|
||||
it = residency_chunks.erase(it);
|
||||
NoteResidencyUnused();
|
||||
}
|
||||
return freed;
|
||||
}
|
||||
|
||||
bool BufferCache::VramEligible(u64 block, bool garlic_known) {
|
||||
if (!GarlicInVram() || dynamic_blocks.Contains(block)) {
|
||||
if (!GarlicInVram() || dynamic_blocks.Contains(block) ||
|
||||
vram_level.load(std::memory_order_relaxed) >= 2) {
|
||||
return false;
|
||||
}
|
||||
// Diagnostics: BB_GARLIC_VRAM_RANGE=begin,end keeps VRAM copies to guest addresses in
|
||||
@@ -2082,7 +2309,7 @@ void BufferCache::ProcessDemotions() {
|
||||
if (!force &&
|
||||
BbStats::coarse_second.load(std::memory_order_relaxed) -
|
||||
group_use[address >> USE_GROUP_BITS] <
|
||||
VramIdleSeconds()) {
|
||||
idle_seconds_now) {
|
||||
return true; // bound again since
|
||||
}
|
||||
} else if (!cpu_written && VramEligible(block)) {
|
||||
|
||||
@@ -347,11 +347,20 @@ private:
|
||||
std::pair<vk::DeviceMemory, u64> AllocateResidency(u64 bytes);
|
||||
/// bbport: BbStats::residency_unused_bytes after the free list or the current block changed.
|
||||
void NoteResidencyUnused();
|
||||
/// Chunks without blocks back to the driver once the GPU is done with them; `min_seconds`
|
||||
/// empty first (0: at once, VRAM full). Returns the bytes freed.
|
||||
u64 ReleaseEmptyResidencyChunks(u32 now, u32 min_seconds);
|
||||
/// bbport: idle VRAM blocks (see group_use).
|
||||
void NoteUse(VAddr address, u64 size);
|
||||
/// bbport BB_RESIDENCY_MIX=1 (diagnostics): bindings over blocks in VRAM and in place at once.
|
||||
void NoteResidencyMix(u64 first_block, u64 last_block, u64 size, bool is_written);
|
||||
void ProcessIdleBlocks();
|
||||
/// bbport: vram_level from the driver's usage and budget (BB_VRAM_PRESSURE_PERCENT, 85).
|
||||
void UpdateVramLevel(u32 now);
|
||||
/// bbport: without the PC memory model, while VRAM is tight (vram_level): VRAM copies of the
|
||||
/// game's data unused for 30 s (10 s when full) leave VRAM; their slots take the next blocks
|
||||
/// instead of new VRAM, and they are uploaded again from the game's memory when bound.
|
||||
void EvictIdleCopies(u32 now);
|
||||
/// A VRAM slot of a block that left it: reused by the next move to VRAM.
|
||||
void ReleaseResidencySlot(vk::DeviceMemory memory, u64 offset);
|
||||
/// Unbinds a VRAM block of memory the game unmapped (uploaded again if mapped and used).
|
||||
@@ -618,6 +627,21 @@ private:
|
||||
std::unordered_map<VkDeviceMemory, ResidencyChunk> residency_chunks;
|
||||
u32 idle_scan_second = 0;
|
||||
u32 last_evacuation = 0; ///< ProcessIdleBlocks: when a chunk was last emptied
|
||||
/// bbport: how full VRAM is against the driver's budget, once a second (UpdateVramLevel):
|
||||
/// 0 room, 1 tight (idle VRAM copies go back sooner), 2 full (no new ones either).
|
||||
std::atomic<int> vram_level{0};
|
||||
std::atomic<u32> vram_full_until{0}; ///< a failed VRAM allocation counts as full until then
|
||||
u32 vram_level_reports = 0;
|
||||
/// bbport: the second a shader last read the game's memory through the page table
|
||||
/// (SynchronizeDmaBuffers). Those reads do not mark blocks as used, so nothing leaves VRAM
|
||||
/// while such shaders run (EvictIdleCopies).
|
||||
std::atomic<u32> dma_second{0};
|
||||
u64 evicted_bytes = 0;
|
||||
/// Blocks EvictIdleCopies took out of VRAM: they come back with their whole 2 MiB group.
|
||||
TrackedIntervalList<> evicted_blocks;
|
||||
u32 evict_reports = 0, dma_kept_reports = 0, dma_kept_second = 0;
|
||||
/// The seconds a VRAM copy may go unused before it goes back in place, at this level.
|
||||
u32 idle_seconds_now = 60;
|
||||
u32 block_size{};
|
||||
u32 block_shift{};
|
||||
u32 blocks_per_arena_page{};
|
||||
|
||||
@@ -341,7 +341,9 @@ struct PageManager::Impl {
|
||||
} else {
|
||||
BbStats::read_faults.fetch_add(1, std::memory_order_relaxed);
|
||||
BbStats::Timer timer{BbStats::t_read_faults};
|
||||
return rasterizer->ReadMemory(addr, 8, is_gpu_thread);
|
||||
// bbport: within the faulting page (see Rasterizer::OnWriteFault).
|
||||
return rasterizer->ReadMemory(addr, std::min<u64>(8, GetNextPageAddr(addr) - addr),
|
||||
is_gpu_thread);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <atomic>
|
||||
#include "common/assert.h"
|
||||
#include "common/number_utils.h"
|
||||
#include "video_core/amdgpu/pixel_format.h"
|
||||
@@ -786,10 +787,30 @@ vk::Format TrySurfaceFormat(AmdGpu::DataFormat data_format, AmdGpu::NumberFormat
|
||||
|
||||
vk::Format SurfaceFormat(AmdGpu::DataFormat data_format, AmdGpu::NumberFormat num_format) {
|
||||
vk::Format result = surface_format_table[GetSurfaceFormatTableIndex(data_format, num_format)];
|
||||
bool found =
|
||||
result != vk::Format::eUndefined || data_format == AmdGpu::DataFormat::FormatInvalid;
|
||||
ASSERT_MSG(found, "Unknown data_format={} and num_format={}", static_cast<u32>(data_format),
|
||||
static_cast<u32>(num_format));
|
||||
if (result != vk::Format::eUndefined || data_format == AmdGpu::DataFormat::FormatInvalid)
|
||||
[[likely]] {
|
||||
return result;
|
||||
}
|
||||
// bbport: a combination the table lacks (a garbage descriptor) ended the game (assertion,
|
||||
// exit 23). The same data format with another number format keeps the texel size; reported
|
||||
// a few times. At worst that surface renders wrong.
|
||||
using AmdGpu::NumberFormat;
|
||||
for (const NumberFormat other : {NumberFormat::Unorm, NumberFormat::Uint, NumberFormat::Float,
|
||||
NumberFormat::Snorm, NumberFormat::Sint}) {
|
||||
result = surface_format_table[GetSurfaceFormatTableIndex(data_format, other)];
|
||||
if (result != vk::Format::eUndefined) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (result == vk::Format::eUndefined) {
|
||||
result = vk::Format::eR8G8B8A8Unorm;
|
||||
}
|
||||
static std::atomic<u32> notes{0};
|
||||
if (notes.fetch_add(1, std::memory_order_relaxed) < 8) {
|
||||
LOG_ERROR(Render_Vulkan, "Unknown data_format={} and num_format={}, using {}",
|
||||
static_cast<u32>(data_format), static_cast<u32>(num_format),
|
||||
vk::to_string(result));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -1177,8 +1198,15 @@ vk::SampleCountFlagBits RawNumSamples(u32 num_samples) {
|
||||
return vk::SampleCountFlagBits::e8;
|
||||
case 16:
|
||||
return vk::SampleCountFlagBits::e16;
|
||||
default:
|
||||
UNREACHABLE();
|
||||
default: {
|
||||
// bbport: not a sample count (a garbage register or sharp): one sample, reported a few
|
||||
// times; this ended the game (UNREACHABLE, exit 23).
|
||||
static std::atomic<u32> notes{0};
|
||||
if (notes.fetch_add(1, std::memory_order_relaxed) < 8) {
|
||||
LOG_ERROR(Render_Vulkan, "Invalid sample count {}, using 1", num_samples);
|
||||
}
|
||||
return vk::SampleCountFlagBits::e1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -65,6 +65,13 @@ void Init(const Instance& instance) {
|
||||
"located\n");
|
||||
return;
|
||||
}
|
||||
// deadinside28#177: a build against other Vulkan headers left the extension's function null
|
||||
// although the extension was on; the first marker then crashed.
|
||||
if (!VULKAN_HPP_DEFAULT_DISPATCHER.vkCmdWriteBufferMarkerAMD) {
|
||||
std::printf("GPU breadcrumbs: VK_AMD_buffer_marker has no function, GPU hangs will not be "
|
||||
"located\n");
|
||||
return;
|
||||
}
|
||||
const VkBufferCreateInfo buffer_ci = {
|
||||
.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO,
|
||||
.size = ArgsOffset + ArgSlots * 16,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <cstdio>
|
||||
#include <boost/container/small_vector.hpp>
|
||||
|
||||
#include "common/serdes.h"
|
||||
@@ -115,8 +116,13 @@ ComputePipeline::ComputePipeline(const Instance& instance, Scheduler& scheduler,
|
||||
throw Serialization::CorruptData{"cached compute pipeline rejected by the driver: " +
|
||||
vk::to_string(pipeline_result)};
|
||||
}
|
||||
ASSERT_MSG(pipeline_result == vk::Result::eSuccess, "Failed to create compute pipeline: {}",
|
||||
vk::to_string(pipeline_result));
|
||||
// bbport: as for graphics pipelines, one the driver cannot build skips its dispatches.
|
||||
if (pipeline_result != vk::Result::eSuccess) {
|
||||
std::printf("GPU: the driver could not create compute pipeline %s (%s): its dispatches "
|
||||
"are skipped\n",
|
||||
debug_str.c_str(), vk::to_string(pipeline_result).c_str());
|
||||
return;
|
||||
}
|
||||
pipeline = std::move(pipe);
|
||||
SetObjectName(device, *pipeline, "Compute Pipeline {}", debug_str);
|
||||
}
|
||||
|
||||
@@ -37,18 +37,18 @@ bool ReadFile(const std::string& path, std::vector<u8>& data) {
|
||||
return bool(file.read(reinterpret_cast<char*>(data.data()), std::streamsize(data.size())));
|
||||
}
|
||||
|
||||
// The model for the menu's preset. bbport: Performance and Ultra Performance (2x and 3x) use the
|
||||
// balanced model too: the v07 performance and ultra performance models leave trails behind moving
|
||||
// characters in Bloodborne, and the balanced one handles those ratios cleanly (IFreemz's
|
||||
// shadPS4-Bloodborne-DLSS-FSR, 9b2766e). The render size still follows the preset.
|
||||
FfxFsr4ModelPreset ModelPreset(int preset) {
|
||||
switch (preset) {
|
||||
case 0:
|
||||
return FFX_FSR4_MODEL_PRESET_NATIVE_AA;
|
||||
case 1:
|
||||
return FFX_FSR4_MODEL_PRESET_QUALITY;
|
||||
case 2:
|
||||
return FFX_FSR4_MODEL_PRESET_BALANCED;
|
||||
case 3:
|
||||
return FFX_FSR4_MODEL_PRESET_PERFORMANCE;
|
||||
default:
|
||||
return FFX_FSR4_MODEL_PRESET_ULTRA_PERFORMANCE;
|
||||
return FFX_FSR4_MODEL_PRESET_BALANCED;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <cstdio>
|
||||
#include <algorithm>
|
||||
#include <utility>
|
||||
#include <boost/container/small_vector.hpp>
|
||||
@@ -429,8 +430,15 @@ GraphicsPipeline::GraphicsPipeline(
|
||||
throw Serialization::CorruptData{"cached graphics pipeline rejected by the driver: " +
|
||||
vk::to_string(pipeline_result)};
|
||||
}
|
||||
ASSERT_MSG(pipeline_result == vk::Result::eSuccess, "Failed to create graphics pipeline: {}",
|
||||
vk::to_string(pipeline_result));
|
||||
// bbport: a driver that cannot build one pipeline ("ErrorUnknown" on some AMD and older
|
||||
// drivers) stopped the game here (code 23). The pipeline stays empty instead, and the draws
|
||||
// that use it are skipped (PipelineCache): a missing effect rather than a closed game.
|
||||
if (pipeline_result != vk::Result::eSuccess) {
|
||||
std::printf("GPU: the driver could not create graphics pipeline %s (%s): its draws are "
|
||||
"skipped\n",
|
||||
debug_str.c_str(), vk::to_string(pipeline_result).c_str());
|
||||
return;
|
||||
}
|
||||
pipeline = std::move(pipe);
|
||||
SetObjectName(device, *pipeline, "Graphics Pipeline {}", debug_str);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdio>
|
||||
#include <boost/container/static_vector.hpp>
|
||||
#include <fmt/format.h>
|
||||
@@ -21,6 +22,18 @@
|
||||
|
||||
namespace Vulkan {
|
||||
|
||||
void NoteOverBudget(const char* what) {
|
||||
static std::atomic<bool> image_noted{false}, buffer_noted{false};
|
||||
auto& noted = what[0] == 'i' ? image_noted : buffer_noted;
|
||||
if (!noted.exchange(true)) {
|
||||
std::printf("GPU: video memory full: %s memory allocated past the driver's budget (the driver "
|
||||
"may keep it in system memory: slower, but the game goes on). A lower output "
|
||||
"resolution or upscaler preset needs less.\n",
|
||||
what);
|
||||
std::fflush(stdout);
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
/// bbport BB_VRAM_LIMIT_MB=N (tests): the driver's VRAM budget taken as at most N MiB, at startup
|
||||
|
||||
@@ -20,6 +20,10 @@ VK_DEFINE_HANDLE(VmaAllocator)
|
||||
|
||||
namespace Vulkan {
|
||||
|
||||
/// bbport: an allocation the VRAM budget refused, made past it instead (system memory if need
|
||||
/// be). Logged once per kind ("image", "buffer").
|
||||
void NoteOverBudget(const char* what);
|
||||
|
||||
class Instance {
|
||||
public:
|
||||
explicit Instance(bool validation = false, bool crash_diagnostic = false);
|
||||
|
||||
@@ -674,6 +674,13 @@ const GraphicsPipeline* PipelineCache::GetGraphicsPipeline(const DrawIndirectPar
|
||||
it.value() = std::make_unique<GraphicsPipeline>(
|
||||
instance, scheduler, desc_heap, profile, sel.graphics_key, *pipeline_cache, sel.infos,
|
||||
sel.runtime_infos, sel.fetch_shader, sel.modules, sdata, false);
|
||||
// bbport: the driver could not create it. The empty entry keeps the key from compiling
|
||||
// again on every draw (they are skipped); it is not saved, so the next start tries anew.
|
||||
if (!it.value()->IsValid()) {
|
||||
it.value().reset();
|
||||
sel.fetch_shader.reset();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
RegisterPipelineData(sel.graphics_key, pipeline_hash, sdata);
|
||||
++num_new_pipelines;
|
||||
@@ -693,6 +700,10 @@ const GraphicsPipeline* PipelineCache::GetGraphicsPipeline(const DrawIndirectPar
|
||||
|
||||
const GraphicsPipeline* PipelineCache::FinishPipeline(const GraphicsPipelineKey& key,
|
||||
PendingPipeline& job) {
|
||||
if (!job.pipeline || !job.pipeline->IsValid()) {
|
||||
graphics_pipelines[key].reset(); // bbport: as GetGraphicsPipeline, skipped and not saved
|
||||
return nullptr;
|
||||
}
|
||||
const auto pipeline_hash = std::hash<GraphicsPipelineKey>{}(key);
|
||||
RegisterPipelineData(key, pipeline_hash, job.sdata);
|
||||
++num_new_pipelines;
|
||||
@@ -774,6 +785,10 @@ const ComputePipeline* PipelineCache::GetComputePipeline() {
|
||||
it.value() = std::make_unique<ComputePipeline>(instance, scheduler, desc_heap, profile,
|
||||
*pipeline_cache, compute_key, *sel.infos[0],
|
||||
sel.modules[0], sdata, false);
|
||||
if (!it.value()->IsValid()) { // bbport: as for graphics pipelines
|
||||
it.value().reset();
|
||||
return nullptr;
|
||||
}
|
||||
RegisterPipelineData(compute_key, sdata);
|
||||
++num_new_pipelines;
|
||||
|
||||
@@ -1073,6 +1088,19 @@ bool PipelineCache::RefreshGraphicsStages(PipelineSelection& sel) {
|
||||
}
|
||||
|
||||
const auto* vs_info = sel.infos[static_cast<u32>(SwStage::Vertex)];
|
||||
// bbport: a draw whose vertex buffer table pointer is null (reported before a crash at the
|
||||
// cutscene after Rom and in normal play) has no vertex input to build: skipped.
|
||||
if (vs_info && sel.fetch_shader) {
|
||||
for (const auto& attrib : sel.fetch_shader->attributes) {
|
||||
if (vs_info->UdPointerNull(attrib.sgpr_base)) {
|
||||
static std::atomic<u32> reported{0};
|
||||
if (reported.fetch_add(1, std::memory_order_relaxed) < 5) {
|
||||
std::printf("GPU: draw skipped: its vertex buffer table pointer is null\n");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (vs_info && sel.fetch_shader && !instance.IsVertexInputDynamicState()) {
|
||||
// Without vertex input dynamic state, the pipeline needs to specialize on format.
|
||||
// Stride will still be handled outside the pipeline using dynamic state.
|
||||
|
||||
@@ -36,6 +36,11 @@ public:
|
||||
return *pipeline;
|
||||
}
|
||||
|
||||
/// bbport: false when the driver could not create the pipeline (its draws are skipped).
|
||||
bool IsValid() const noexcept {
|
||||
return static_cast<bool>(pipeline);
|
||||
}
|
||||
|
||||
vk::PipelineLayout GetLayout() const noexcept {
|
||||
return *pipeline_layout;
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <csetjmp>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <ctime>
|
||||
#include <filesystem>
|
||||
@@ -95,6 +96,30 @@ bool CanBlitToSwapchain(const vk::PhysicalDevice physical_device, vk::Format for
|
||||
return MakeImageBlit(frame_width, frame_height, swapchain_width, swapchain_height, 0, 0);
|
||||
}
|
||||
|
||||
// bbport: a frame's fence, waited for in 10 s slices. Past the first slice the log says so, once
|
||||
// per wait, with the breadcrumbs (which command the GPU has not finished): without it a GPU that
|
||||
// never finishes the frame froze the game with nothing in the log (issue #5).
|
||||
static vk::Result WaitFrameFence(vk::Device device, vk::Fence fence, const char* where) {
|
||||
constexpr u64 slice = 10'000'000'000ULL;
|
||||
bool told = false;
|
||||
while (true) {
|
||||
const vk::Result result = device.waitForFences(fence, false, slice);
|
||||
if (result != vk::Result::eTimeout) {
|
||||
if (told) {
|
||||
std::printf("Presenter: %s finished after all\n", where);
|
||||
std::fflush(stdout);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
if (!told) {
|
||||
told = true;
|
||||
std::printf("Presenter: %s not finished after 10 s: the GPU is stuck on it\n", where);
|
||||
std::fflush(stdout);
|
||||
Breadcrumbs::ReportStuck(where);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static vk::Rect2D FitImage(s32 frame_width, s32 frame_height, s32 swapchain_width,
|
||||
s32 swapchain_height) {
|
||||
float frame_aspect = static_cast<float>(frame_width) / frame_height;
|
||||
@@ -251,14 +276,11 @@ Frame* Presenter::PrepareLastFrame() {
|
||||
Frame* frame = last_submit_frame;
|
||||
|
||||
while (true) {
|
||||
vk::Result result = instance.GetDevice().waitForFences(frame->present_done, false,
|
||||
std::numeric_limits<u64>::max());
|
||||
vk::Result result =
|
||||
WaitFrameFence(instance.GetDevice(), frame->present_done, "the last frame");
|
||||
if (result == vk::Result::eSuccess) {
|
||||
break;
|
||||
}
|
||||
if (result == vk::Result::eTimeout) {
|
||||
continue;
|
||||
}
|
||||
if (result == vk::Result::eErrorDeviceLost) {
|
||||
Breadcrumbs::ReportDeviceLost("waiting for a frame");
|
||||
}
|
||||
@@ -730,11 +752,14 @@ void Presenter::Present(Frame* frame, bool is_reusing_frame, bool is_game_frame)
|
||||
scheduler.Flush(info);
|
||||
|
||||
// Present to swapchain.
|
||||
// bbport: Recreate outside the lock: it waits for the device under it (Swapchain::WaitIdle).
|
||||
bool presented;
|
||||
{
|
||||
std::scoped_lock submit_lock{Scheduler::submit_mutex};
|
||||
if (!swapchain.Present()) {
|
||||
swapchain.Recreate(window.GetWidth(), window.GetHeight());
|
||||
}
|
||||
presented = swapchain.Present();
|
||||
}
|
||||
if (!presented) {
|
||||
swapchain.Recreate(window.GetWidth(), window.GetHeight());
|
||||
}
|
||||
|
||||
free_frame();
|
||||
@@ -760,7 +785,7 @@ Frame* Presenter::GetRenderFrame() {
|
||||
vk::Result result{};
|
||||
|
||||
const auto wait = [&]() {
|
||||
result = device.waitForFences(frame->present_done, false, std::numeric_limits<u64>::max());
|
||||
result = WaitFrameFence(device, frame->present_done, "a frame to draw into");
|
||||
return result;
|
||||
};
|
||||
|
||||
|
||||
@@ -4244,7 +4244,24 @@ void Rasterizer::OnCpuWrite(VAddr addr, u64 size) {
|
||||
|
||||
bool Rasterizer::OnWriteFault(VAddr addr, bool assume_locks, u64 guest_rip) {
|
||||
DrainDrawPipe();
|
||||
if (!InvalidateMemory(addr, 8, assume_locks)) {
|
||||
// bbport: within the faulting page. 8 bytes from a write to the last bytes of a mapping
|
||||
// reached past its end: IsMapped failed and the fault went unhandled (the game ended).
|
||||
const u64 fault_size = std::min<u64>(8, VideoCore::PageManager::GetNextPageAddr(addr) - addr);
|
||||
// bbport: a page no tracked image protects (a buffer's page: most faults) needs nothing from
|
||||
// the texture cache, whose lock the draw recording thread holds while it creates or uploads an
|
||||
// image: a guest thread writing its constants waited for that (0.3-0.6 s with memory short,
|
||||
// the frame with it). Images learn of CPU writes through their own pages' protection only.
|
||||
// BB_FAULT_SKIP_TEXTURES=0: every fault takes the lock, as before. (Awaken's BloodBorneButPc.)
|
||||
static const bool skip_textures = [] {
|
||||
const char* env = std::getenv("BB_FAULT_SKIP_TEXTURES");
|
||||
return !(env && env[0] == '0');
|
||||
}();
|
||||
if (skip_textures && !texture_cache.WatchesPages(addr, fault_size)) {
|
||||
if (!IsMapped(addr, fault_size)) {
|
||||
return false;
|
||||
}
|
||||
buffer_cache.InvalidateMemory(addr, fault_size, assume_locks);
|
||||
} else if (!InvalidateMemory(addr, fault_size, assume_locks)) {
|
||||
return false;
|
||||
}
|
||||
buffer_cache.ExtendWriteFault(addr, guest_rip);
|
||||
|
||||
@@ -601,6 +601,37 @@ struct SubmissionBatch {
|
||||
class RecordChunk {
|
||||
public:
|
||||
static constexpr size_t Capacity = 128 * 1024;
|
||||
/// Room kept free in the chunk for the command that consumes reserved data.
|
||||
static constexpr size_t CommandReserve = 1024;
|
||||
|
||||
/// Makes room for `bytes` of Allocate() data. Groups that fit stay inline (no allocation);
|
||||
/// a group larger than an empty chunk (e.g. thousands of buffer copies or barriers) gets
|
||||
/// one uninitialized heap block owned by this chunk, freed when the chunk is recycled, so
|
||||
/// no span outlives the commands that read it. False: retire this chunk and retry.
|
||||
/// From AwesomeObserver/bloodborne_pc 54bd756 (Evgeniy Doronin, GPL-2.0-or-later), via
|
||||
/// Awaken's BloodBorneButPc; it replaces an ASSERT that ended the game with exit code 23.
|
||||
bool ReserveData(size_t bytes) {
|
||||
const size_t remaining = Capacity - used;
|
||||
if (remaining < CommandReserve) {
|
||||
return false;
|
||||
}
|
||||
if (spill_remaining >= bytes) {
|
||||
return true;
|
||||
}
|
||||
if (bytes <= remaining - CommandReserve) {
|
||||
spill_cursor = nullptr;
|
||||
spill_remaining = 0;
|
||||
return true;
|
||||
}
|
||||
if (bytes <= Capacity - CommandReserve) {
|
||||
return false;
|
||||
}
|
||||
std::unique_ptr<std::byte[]> block{new std::byte[bytes]};
|
||||
spill_cursor = block.get();
|
||||
spill_remaining = bytes;
|
||||
spill_blocks.push_back(std::move(block));
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Returns false (and leaves `func` untouched) when the chunk has no room.
|
||||
template <typename Func>
|
||||
@@ -625,8 +656,19 @@ public:
|
||||
|
||||
/// Raw storage in the chunk for a command's variable-length data; null when full.
|
||||
void* Allocate(size_t bytes, size_t align) {
|
||||
if (spill_cursor) {
|
||||
void* result = spill_cursor;
|
||||
size_t remaining = spill_remaining;
|
||||
if (!std::align(align, bytes, result, remaining)) {
|
||||
return nullptr;
|
||||
}
|
||||
spill_used += spill_remaining - remaining + bytes;
|
||||
spill_cursor = static_cast<std::byte*>(result) + bytes;
|
||||
spill_remaining = remaining - bytes;
|
||||
return result;
|
||||
}
|
||||
const size_t offset = (used + align - 1) & ~(align - 1);
|
||||
if (offset + bytes > Capacity) {
|
||||
if (offset > Capacity || bytes > Capacity - offset) {
|
||||
return nullptr;
|
||||
}
|
||||
used = offset + bytes;
|
||||
@@ -643,6 +685,9 @@ public:
|
||||
}
|
||||
first = last = nullptr;
|
||||
used = 0;
|
||||
spill_cursor = nullptr;
|
||||
spill_remaining = spill_used = 0;
|
||||
spill_blocks.clear();
|
||||
}
|
||||
|
||||
[[nodiscard]] bool Empty() const noexcept {
|
||||
@@ -650,7 +695,7 @@ public:
|
||||
}
|
||||
|
||||
[[nodiscard]] size_t Size() const noexcept {
|
||||
return used;
|
||||
return used + spill_used;
|
||||
}
|
||||
|
||||
u32 segment = 0; ///< the command buffer segment the chunk is recorded into (Scheduler)
|
||||
@@ -687,6 +732,9 @@ private:
|
||||
|
||||
alignas(64) std::byte storage[Capacity];
|
||||
size_t used = 0;
|
||||
std::vector<std::unique_ptr<std::byte[]>> spill_blocks; ///< oversized data (ReserveData)
|
||||
std::byte* spill_cursor = nullptr;
|
||||
size_t spill_remaining = 0, spill_used = 0;
|
||||
CommandBase* first{};
|
||||
CommandBase* last{};
|
||||
};
|
||||
@@ -867,9 +915,11 @@ public:
|
||||
if (!IsRecordingDeferred()) {
|
||||
return;
|
||||
}
|
||||
ASSERT(bytes + 1024 <= RecordChunk::Capacity);
|
||||
if (RecordChunk::Capacity - CurrentChunk().Size() < bytes + 1024) {
|
||||
// Oversized groups spill into a block owned by the chunk (AwesomeObserver 54bd756).
|
||||
if (!CurrentChunk().ReserveData(bytes)) {
|
||||
RetireChunk();
|
||||
const bool reserved = CurrentChunk().ReserveData(bytes);
|
||||
ASSERT_MSG(reserved, "Cannot reserve {} bytes in an empty recording chunk", bytes);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <limits>
|
||||
#include <cstdio>
|
||||
#include "video_core/renderer_vulkan/vk_breadcrumbs.h"
|
||||
#include "video_core/renderer_vulkan/vk_instance.h"
|
||||
#include "video_core/renderer_vulkan/vk_semaphore.h"
|
||||
@@ -10,7 +10,8 @@
|
||||
|
||||
namespace Vulkan {
|
||||
|
||||
constexpr u64 WAIT_TIMEOUT = std::numeric_limits<u64>::max();
|
||||
// bbport: waits go in 10 s slices so a GPU that never finishes is reported, not waited on silently.
|
||||
constexpr u64 WAIT_TIMEOUT = 10'000'000'000ULL;
|
||||
|
||||
Semaphore::Semaphore(const Instance& instance_) : instance{instance_} {
|
||||
const vk::StructureChain semaphore_chain = {
|
||||
@@ -66,12 +67,28 @@ void Semaphore::Wait(u64 tick) {
|
||||
.pValues = &tick,
|
||||
};
|
||||
|
||||
// bbport: a lost device ends the wait with a report instead of a silent endless loop.
|
||||
// bbport: a lost device ends the wait with a report instead of a silent endless loop, and a
|
||||
// submission still not finished after 10 s says so once with the GPU breadcrumbs.
|
||||
bool told = false;
|
||||
while (true) {
|
||||
const vk::Result result = instance.GetDevice().waitSemaphores(&wait_info, WAIT_TIMEOUT);
|
||||
if (result == vk::Result::eSuccess) {
|
||||
if (told) {
|
||||
std::printf("GPU: submission %llu finished after all\n", (unsigned long long)tick);
|
||||
std::fflush(stdout);
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (result == vk::Result::eTimeout) {
|
||||
if (!told) {
|
||||
told = true;
|
||||
std::printf("GPU: submission %llu not finished after 10 s: the GPU is stuck on it\n",
|
||||
(unsigned long long)tick);
|
||||
std::fflush(stdout);
|
||||
Breadcrumbs::ReportStuck("waiting for a submission");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (result == vk::Result::eErrorDeviceLost) {
|
||||
Breadcrumbs::ReportDeviceLost("waiting for a submission");
|
||||
ASSERT_MSG(false, "Device lost while waiting for a submission");
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <cstdio>
|
||||
#include <limits>
|
||||
#include "common/assert.h"
|
||||
#include "common/logging/log.h"
|
||||
@@ -9,10 +11,22 @@
|
||||
#include "imgui/renderer/imgui_core.h"
|
||||
#include "sdl_window.h"
|
||||
#include "video_core/renderer_vulkan/vk_instance.h"
|
||||
#include "video_core/renderer_vulkan/vk_platform.h"
|
||||
#include "video_core/renderer_vulkan/vk_scheduler.h"
|
||||
#include "video_core/renderer_vulkan/vk_swapchain.h"
|
||||
|
||||
namespace Vulkan {
|
||||
|
||||
/// bbport (from Awaken's BloodBorneButPc): display events (a lost surface, a failed swapchain)
|
||||
/// are reported a few times and the presenter skips frames until a new swapchain works; they
|
||||
/// ended the game (exit 23).
|
||||
static void NoteDisplayEvent(const char* what, vk::Result result) {
|
||||
static std::atomic<u32> notes{0};
|
||||
if (notes.fetch_add(1, std::memory_order_relaxed) < 16) {
|
||||
std::printf("Display: %s: %s\n", what, vk::to_string(result).c_str());
|
||||
}
|
||||
}
|
||||
|
||||
static constexpr vk::SurfaceFormatKHR SURFACE_FORMAT_HDR = {
|
||||
.format = vk::Format::eA2B10G10R10UnormPack32,
|
||||
.colorSpace = vk::ColorSpaceKHR::eHdr10St2084EXT,
|
||||
@@ -37,9 +51,18 @@ void Swapchain::Create(u32 width_, u32 height_) {
|
||||
height = height_;
|
||||
needs_recreation = false;
|
||||
|
||||
// bbport: a lost surface (VK_ERROR_SURFACE_LOST_KHR: a display or driver reset) is made again
|
||||
// on the same window before a swapchain can be created on it.
|
||||
if (surface_lost) {
|
||||
RecreateSurface();
|
||||
}
|
||||
|
||||
// bbport: a minimised window has a 0x0 surface (issue #31); no swapchain can have that extent.
|
||||
// The old one stays (presents are skipped) until the window is back.
|
||||
SetSurfaceProperties();
|
||||
if (!SetSurfaceProperties()) {
|
||||
needs_recreation = true;
|
||||
return;
|
||||
}
|
||||
if (extent.width == 0 || extent.height == 0) {
|
||||
zero_extent = true;
|
||||
needs_recreation = true;
|
||||
@@ -84,15 +107,24 @@ void Swapchain::Create(u32 width_, u32 height_) {
|
||||
};
|
||||
|
||||
auto [swapchain_result, chain] = instance.GetDevice().createSwapchainKHR(swapchain_info);
|
||||
ASSERT_MSG(swapchain_result == vk::Result::eSuccess, "Failed to create swapchain: {}",
|
||||
vk::to_string(swapchain_result));
|
||||
swapchain = chain;
|
||||
// The old swapchain is retired by the call even when it fails.
|
||||
if (old_swapchain) {
|
||||
instance.GetDevice().destroySwapchainKHR(old_swapchain);
|
||||
}
|
||||
if (swapchain_result != vk::Result::eSuccess) {
|
||||
// bbport: no swapchain now (IsPresentable is false): presents are skipped and the next
|
||||
// one tries again (this was an assertion, exit 23).
|
||||
NoteDisplayEvent("creating the swapchain failed", swapchain_result);
|
||||
surface_lost = swapchain_result == vk::Result::eErrorSurfaceLostKHR;
|
||||
needs_recreation = true;
|
||||
return;
|
||||
}
|
||||
swapchain = chain;
|
||||
|
||||
SetupImages();
|
||||
RefreshSemaphores();
|
||||
// The new chain may have fewer images (AwesomeObserver b3164c1): start the indices over.
|
||||
frame_index = image_index = 0;
|
||||
}
|
||||
|
||||
void Swapchain::Recreate(u32 width_, u32 height_) {
|
||||
@@ -106,11 +138,7 @@ void Swapchain::SetHDR(bool hdr) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto result = instance.GetDevice().waitIdle();
|
||||
if (result != vk::Result::eSuccess) {
|
||||
LOG_WARNING(ImGui, "Failed to wait for Vulkan device idle on mode change: {}",
|
||||
vk::to_string(result));
|
||||
}
|
||||
WaitIdle();
|
||||
|
||||
needs_hdr = hdr;
|
||||
Recreate(width, height);
|
||||
@@ -119,24 +147,54 @@ void Swapchain::SetHDR(bool hdr) {
|
||||
}
|
||||
|
||||
bool Swapchain::AcquireNextImage() {
|
||||
if (!swapchain) {
|
||||
needs_recreation = true; // the last Create failed
|
||||
return false;
|
||||
}
|
||||
vk::Device device = instance.GetDevice();
|
||||
vk::Result result =
|
||||
device.acquireNextImageKHR(swapchain, std::numeric_limits<u64>::max(),
|
||||
image_acquired[frame_index], VK_NULL_HANDLE, &image_index);
|
||||
// bbport: in 10 s slices, saying so once when the compositor holds every image that long
|
||||
// (issue #5: a frozen game with nothing in the log).
|
||||
vk::Result result;
|
||||
bool told = false;
|
||||
while ((result = device.acquireNextImageKHR(swapchain, 10'000'000'000ULL,
|
||||
image_acquired[frame_index], VK_NULL_HANDLE,
|
||||
&image_index)) == vk::Result::eTimeout ||
|
||||
result == vk::Result::eNotReady) {
|
||||
if (!told) {
|
||||
told = true;
|
||||
std::printf("Swapchain: no image from the compositor in 10 s (it holds all %zu)\n",
|
||||
images.size());
|
||||
std::fflush(stdout);
|
||||
}
|
||||
}
|
||||
if (told) {
|
||||
std::printf("Swapchain: the compositor gave an image back\n");
|
||||
std::fflush(stdout);
|
||||
}
|
||||
|
||||
switch (result) {
|
||||
case vk::Result::eSuccess:
|
||||
break;
|
||||
case vk::Result::eSuboptimalKHR:
|
||||
// A suboptimal acquire still owns an image and signals the semaphore: present it in the
|
||||
// normal submission, then recreate (Present() returns false). Dropping it left the
|
||||
// semaphore signalled and never waited on. From AwesomeObserver b3164c1 (E. Doronin).
|
||||
needs_recreation = true;
|
||||
return true;
|
||||
case vk::Result::eErrorSurfaceLostKHR:
|
||||
NoteDisplayEvent("acquiring a swapchain image", result);
|
||||
surface_lost = true;
|
||||
needs_recreation = true;
|
||||
break;
|
||||
case vk::Result::eErrorOutOfDateKHR:
|
||||
case vk::Result::eErrorUnknown:
|
||||
needs_recreation = true;
|
||||
break;
|
||||
default:
|
||||
LOG_CRITICAL(Render_Vulkan, "Swapchain acquire returned unknown result {}",
|
||||
vk::to_string(result));
|
||||
UNREACHABLE();
|
||||
// bbport: e.g. a lost exclusive fullscreen: recreated like out-of-date (this was
|
||||
// UNREACHABLE, exit 23).
|
||||
NoteDisplayEvent("acquiring a swapchain image", result);
|
||||
needs_recreation = true;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -155,6 +213,12 @@ bool Swapchain::Present() {
|
||||
auto result = instance.GetPresentQueue().presentKHR(present_info);
|
||||
if (result == vk::Result::eErrorOutOfDateKHR || result == vk::Result::eSuboptimalKHR) {
|
||||
needs_recreation = true;
|
||||
} else if (result != vk::Result::eSuccess && result != vk::Result::eErrorDeviceLost) {
|
||||
// bbport: a lost surface (display or driver reset) or another display event: recreated
|
||||
// like out-of-date (this was an assertion, exit 23). A lost device still stops below.
|
||||
NoteDisplayEvent("presenting", result);
|
||||
surface_lost = result == vk::Result::eErrorSurfaceLostKHR;
|
||||
needs_recreation = true;
|
||||
} else {
|
||||
ASSERT_MSG(result == vk::Result::eSuccess, "Swapchain presentation failed: {}",
|
||||
vk::to_string(result));
|
||||
@@ -236,11 +300,15 @@ void Swapchain::FindPresentMode() {
|
||||
}
|
||||
}
|
||||
|
||||
void Swapchain::SetSurfaceProperties() {
|
||||
bool Swapchain::SetSurfaceProperties() {
|
||||
const auto [capabilities_result, capabilities] =
|
||||
instance.GetPhysicalDevice().getSurfaceCapabilitiesKHR(surface);
|
||||
ASSERT_MSG(capabilities_result == vk::Result::eSuccess,
|
||||
"Failed to query surface capabilities: {}", vk::to_string(capabilities_result));
|
||||
if (capabilities_result != vk::Result::eSuccess) {
|
||||
// bbport: e.g. a lost surface: no swapchain until it is back (this was an assertion).
|
||||
NoteDisplayEvent("querying the surface", capabilities_result);
|
||||
surface_lost = capabilities_result == vk::Result::eErrorSurfaceLostKHR;
|
||||
return false;
|
||||
}
|
||||
|
||||
extent = capabilities.currentExtent;
|
||||
if (capabilities.currentExtent.width == std::numeric_limits<u32>::max()) {
|
||||
@@ -267,23 +335,41 @@ void Swapchain::SetSurfaceProperties() {
|
||||
if (!(capabilities.supportedCompositeAlpha & vk::CompositeAlphaFlagBitsKHR::eOpaque)) {
|
||||
composite_alpha = vk::CompositeAlphaFlagBitsKHR::eInherit;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void Swapchain::Destroy() {
|
||||
vk::Device device = instance.GetDevice();
|
||||
const auto wait_result = device.waitIdle();
|
||||
void Swapchain::WaitIdle() {
|
||||
// bbport: vkDeviceWaitIdle needs every queue of the device externally synchronized: the
|
||||
// recording threads submit to the same queue under this lock (Scheduler::submit_mutex).
|
||||
std::scoped_lock submit_lock{Scheduler::submit_mutex};
|
||||
const auto wait_result = instance.GetDevice().waitIdle();
|
||||
if (wait_result != vk::Result::eSuccess) {
|
||||
LOG_WARNING(Render_Vulkan, "Failed to wait for device to become idle: {}",
|
||||
vk::to_string(wait_result));
|
||||
}
|
||||
}
|
||||
|
||||
void Swapchain::RecreateSurface() {
|
||||
Destroy(); // a swapchain goes before its surface
|
||||
instance.GetInstance().destroySurfaceKHR(surface);
|
||||
surface = CreateSurface(instance.GetInstance(), window);
|
||||
surface_lost = false;
|
||||
std::printf("Display: the window's surface was lost; created again\n");
|
||||
}
|
||||
|
||||
void Swapchain::Destroy() {
|
||||
vk::Device device = instance.GetDevice();
|
||||
WaitIdle();
|
||||
|
||||
for (auto& image_view : images_view) {
|
||||
device.destroyImageView(image_view);
|
||||
}
|
||||
images_view.clear();
|
||||
images.clear();
|
||||
|
||||
if (swapchain) {
|
||||
device.destroySwapchainKHR(swapchain);
|
||||
swapchain = nullptr; // bbport: a failed Create leaves none (IsPresentable)
|
||||
}
|
||||
|
||||
for (const auto& sem : image_acquired) {
|
||||
|
||||
@@ -105,7 +105,14 @@ private:
|
||||
void FindPresentMode();
|
||||
|
||||
/// Sets the surface properties according to device capabilities
|
||||
void SetSurfaceProperties();
|
||||
/// (bbport: false when they cannot be queried, e.g. a lost surface)
|
||||
bool SetSurfaceProperties();
|
||||
|
||||
/// bbport: vkDeviceWaitIdle under Scheduler::submit_mutex (the callers must not hold it).
|
||||
void WaitIdle();
|
||||
|
||||
/// bbport: destroys the swapchain and the lost surface, and creates a new surface.
|
||||
void RecreateSurface();
|
||||
|
||||
/// Destroys current swapchain resources
|
||||
void Destroy();
|
||||
@@ -138,6 +145,7 @@ private:
|
||||
u32 frame_index = 0;
|
||||
bool needs_recreation = true;
|
||||
bool zero_extent = false; ///< bbport: the surface was 0x0 (minimised) at the last Create
|
||||
bool surface_lost = false; ///< bbport: VK_ERROR_SURFACE_LOST_KHR seen: Create makes a new one
|
||||
bool needs_hdr = false; // The game requested HDR swapchain
|
||||
bool supports_hdr = false; // SC supports HDR output
|
||||
};
|
||||
|
||||
@@ -1471,22 +1471,6 @@ void TemporalUpscaler::OnDraw(u64 vs_hash, VideoCore::ImageId color,
|
||||
}
|
||||
return;
|
||||
}
|
||||
// bbport: a UI draw without a color target (a Scaleform mask: color writes off) writes the
|
||||
// stencil that the UI's next draws test. It must go into the UI's output-size depth like
|
||||
// them: drawn into the guest depth, the mask was lost when the next UI draw switched to the
|
||||
// UI depth (PrepareUiDepth clears its stencil), and masked images (the loading screen's item
|
||||
// picture, cut out of a sheet of item pictures) vanished.
|
||||
if (ui_phase && !color && depth && depth != ui_depth && ui_color) {
|
||||
const auto& depth_image = texture_cache.GetImage(depth);
|
||||
const auto& ui_target = texture_cache.GetImage(ui_color);
|
||||
if (depth_image.info.size.width == ui_target.info.size.width &&
|
||||
depth_image.info.size.height == ui_target.info.size.height) {
|
||||
EnsureUiResources(ui_width, ui_height, ui_format, depth_image.info.pixel_format);
|
||||
PrepareUiDepth(depth);
|
||||
ui_depth = depth;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!Scaled() || !color) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -132,6 +132,15 @@ void UniqueImage::Create(const vk::ImageCreateInfo& image_ci) {
|
||||
VmaAllocationInfo alloc_info{};
|
||||
VkResult result = vmaCreateImage(allocator, &image_ci_unsafe, &alloc_ci, &unsafe_image,
|
||||
&allocation, &alloc_info);
|
||||
if (result == VK_ERROR_OUT_OF_DEVICE_MEMORY) {
|
||||
// bbport: VRAM full (4-6 GB cards, high resolutions). Past the budget the driver may keep
|
||||
// the image in system memory: slower, but the game goes on.
|
||||
Vulkan::NoteOverBudget("image");
|
||||
VmaAllocationCreateInfo over_budget = alloc_ci;
|
||||
over_budget.flags &= ~VMA_ALLOCATION_CREATE_WITHIN_BUDGET_BIT;
|
||||
result = vmaCreateImage(allocator, &image_ci_unsafe, &over_budget, &unsafe_image,
|
||||
&allocation, &alloc_info);
|
||||
}
|
||||
ASSERT_MSG(result == VK_SUCCESS, "Failed allocating image with error {}",
|
||||
vk::to_string(vk::Result{result}));
|
||||
image = vk::Image{unsafe_image};
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include "core/memory.h"
|
||||
#include "video_core/buffer_cache/buffer_cache.h"
|
||||
#include "video_core/page_manager.h"
|
||||
#include "video_core/renderer_vulkan/liverpool_to_vk.h"
|
||||
#include "video_core/renderer_vulkan/vk_instance.h"
|
||||
#include "video_core/renderer_vulkan/vk_runtime.h"
|
||||
#include "video_core/renderer_vulkan/vk_scheduler.h"
|
||||
@@ -854,12 +855,18 @@ ImageView& TextureCache::FindDepthTarget(ImageId image_id, const ImageDesc& desc
|
||||
// If there is a stencil attachment, link depth and stencil.
|
||||
if (desc.info.stencil_addr != 0) {
|
||||
ImageId stencil_id{};
|
||||
ForEachImageInRegion(desc.info.stencil_addr, desc.info.stencil_size,
|
||||
[&](ImageId image_id, Image& image) {
|
||||
if (image.info.guest_address == desc.info.stencil_addr) {
|
||||
stencil_id = image_id;
|
||||
}
|
||||
});
|
||||
ForEachImageInRegion(
|
||||
desc.info.stencil_addr, desc.info.stencil_size, [&](ImageId image_id, Image& image) {
|
||||
if (image.info.guest_address != desc.info.stencil_addr) {
|
||||
return;
|
||||
}
|
||||
// Not a color image that reuses the memory: its views would reach the depth
|
||||
// buffer through an invalid view (shadPS4 #5118).
|
||||
if (image.info.pixel_format == vk::Format::eUndefined ||
|
||||
Vulkan::LiverpoolToVK::IsFormatStencilCompatible(image.info.pixel_format)) {
|
||||
stencil_id = image_id;
|
||||
}
|
||||
});
|
||||
if (!stencil_id) {
|
||||
ImageInfo info{};
|
||||
info.guest_address = desc.info.stencil_addr;
|
||||
@@ -1029,6 +1036,8 @@ void TextureCache::RegisterImage(ImageId image_id) {
|
||||
image.flags |= ImageFlagBits::Registered;
|
||||
++registry_generation;
|
||||
total_used_memory += Common::AlignUp(image.info.guest_size, 1024);
|
||||
++image_count;
|
||||
image_bytes += image.info.guest_size;
|
||||
BbStats::live_image_bytes.fetch_add(Common::AlignUp(image.info.guest_size, 1024),
|
||||
std::memory_order_relaxed);
|
||||
BbStats::live_images.fetch_add(1, std::memory_order_relaxed);
|
||||
@@ -1046,6 +1055,8 @@ void TextureCache::UnregisterImage(ImageId image_id) {
|
||||
++registry_generation;
|
||||
lru_cache.Free(image.lru_id);
|
||||
total_used_memory -= Common::AlignUp(image.info.guest_size, 1024);
|
||||
--image_count;
|
||||
image_bytes -= image.info.guest_size;
|
||||
BbStats::live_image_bytes.fetch_sub(Common::AlignUp(image.info.guest_size, 1024),
|
||||
std::memory_order_relaxed);
|
||||
BbStats::live_images.fetch_sub(1, std::memory_order_relaxed);
|
||||
@@ -1080,7 +1091,7 @@ void TextureCache::TrackImage(ImageId image_id) {
|
||||
// Re-track the whole image
|
||||
image.track_addr = image_begin;
|
||||
image.track_addr_end = image_end;
|
||||
tracker.UpdatePageWatchers<1>(image_begin, image.info.guest_size);
|
||||
WatchPages(image_begin, image.info.guest_size);
|
||||
} else {
|
||||
if (image_begin < image.track_addr) {
|
||||
TrackImageHead(image_id);
|
||||
@@ -1103,7 +1114,7 @@ void TextureCache::TrackImageHead(ImageId image_id) {
|
||||
ASSERT(image.track_addr != 0 && image_begin < image.track_addr);
|
||||
const auto size = image.track_addr - image_begin;
|
||||
image.track_addr = image_begin;
|
||||
tracker.UpdatePageWatchers<1>(image_begin, size);
|
||||
WatchPages(image_begin, size);
|
||||
}
|
||||
|
||||
void TextureCache::TrackImageTail(ImageId image_id) {
|
||||
@@ -1119,9 +1130,19 @@ void TextureCache::TrackImageTail(ImageId image_id) {
|
||||
const auto addr = image.track_addr_end;
|
||||
const auto size = image_end - image.track_addr_end;
|
||||
image.track_addr_end = image_end;
|
||||
WatchPages(addr, size);
|
||||
}
|
||||
|
||||
void TextureCache::WatchPages(VAddr addr, u64 size) {
|
||||
watched_pages.Add(addr, size, 1); // before the pages are protected
|
||||
tracker.UpdatePageWatchers<1>(addr, size);
|
||||
}
|
||||
|
||||
void TextureCache::UnwatchPages(VAddr addr, u64 size) {
|
||||
tracker.UpdatePageWatchers<false>(addr, size);
|
||||
watched_pages.Add(addr, size, -1); // after they are unprotected
|
||||
}
|
||||
|
||||
void TextureCache::UntrackImage(ImageId image_id) {
|
||||
auto& image = slot_images[image_id];
|
||||
if (!image.IsTracked()) {
|
||||
@@ -1132,7 +1153,7 @@ void TextureCache::UntrackImage(ImageId image_id) {
|
||||
image.track_addr = 0;
|
||||
image.track_addr_end = 0;
|
||||
if (size != 0) {
|
||||
tracker.UpdatePageWatchers<false>(addr, size);
|
||||
UnwatchPages(addr, size);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1151,7 +1172,7 @@ void TextureCache::UntrackImageHead(ImageId image_id) {
|
||||
// Cehck its hash later.
|
||||
MarkAsMaybeDirty(image_id, image);
|
||||
}
|
||||
tracker.UpdatePageWatchers<false>(image_begin, size);
|
||||
UnwatchPages(image_begin, size);
|
||||
}
|
||||
|
||||
void TextureCache::UntrackImageTail(ImageId image_id) {
|
||||
@@ -1170,7 +1191,57 @@ void TextureCache::UntrackImageTail(ImageId image_id) {
|
||||
// Cehck its hash later.
|
||||
MarkAsMaybeDirty(image_id, image);
|
||||
}
|
||||
tracker.UpdatePageWatchers<false>(addr, size);
|
||||
UnwatchPages(addr, size);
|
||||
}
|
||||
|
||||
// bbport: once a minute, what the game holds: VRAM, the texture cache, the old GPU-written tiled
|
||||
// images the collector cannot free, and the process's memory (resident, and of it anonymous: the
|
||||
// game's own allocations). A log ending in a crash after hours of play then shows what was
|
||||
// growing. After Supermedo/bloodborne_pc 5dc1f11; on Linux from /proc/self/status (statm's first
|
||||
// field is the address space, huge with the game's reservations).
|
||||
void TextureCache::ReportMemory() {
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
if (now - memory_report_time < std::chrono::seconds(60)) {
|
||||
return;
|
||||
}
|
||||
const bool first = memory_report_time == std::chrono::steady_clock::time_point{};
|
||||
memory_report_time = now;
|
||||
if (first) {
|
||||
return; // a minute after the first submission, not at once
|
||||
}
|
||||
// Unused for a minute: the tick at the start of the second a minute ago.
|
||||
const u64 minute_tick = gc_tick_at_second[(gc_second - 60) % gc_tick_at_second.size()];
|
||||
u64 idle = 0, idle_bytes = 0, kept = 0, kept_bytes = 0;
|
||||
{
|
||||
std::scoped_lock lock{mutex};
|
||||
lru_cache.ForEachItemBelow(minute_tick, [&](ImageId image_id) {
|
||||
const auto& image = slot_images[image_id];
|
||||
++idle;
|
||||
idle_bytes += image.info.guest_size;
|
||||
if (image.info.IsTiled() && image.SafeToDownload()) {
|
||||
++kept;
|
||||
kept_bytes += image.info.guest_size;
|
||||
}
|
||||
});
|
||||
}
|
||||
unsigned long long resident_kb = 0, anonymous_kb = 0;
|
||||
if (std::FILE* status = std::fopen("/proc/self/status", "r")) {
|
||||
char line[128];
|
||||
while (std::fgets(line, sizeof(line), status)) {
|
||||
std::sscanf(line, "VmRSS: %llu", &resident_kb);
|
||||
std::sscanf(line, "RssAnon: %llu", &anonymous_kb);
|
||||
}
|
||||
std::fclose(status);
|
||||
}
|
||||
const u64 vram = instance.CanReportMemoryUsage() ? instance.GetDeviceMemoryUsage() : 0;
|
||||
std::printf("Memory: VRAM %llu MiB; textures %llu (%llu MiB), unused for a minute %llu "
|
||||
"(%llu MiB), of them GPU-written tiled %llu (%llu MiB); process %llu MiB in RAM, "
|
||||
"%llu MiB of it anonymous\n",
|
||||
(unsigned long long)(vram >> 20), (unsigned long long)image_count,
|
||||
(unsigned long long)(image_bytes >> 20), (unsigned long long)idle,
|
||||
(unsigned long long)(idle_bytes >> 20), (unsigned long long)kept,
|
||||
(unsigned long long)(kept_bytes >> 20), resident_kb >> 10, anonymous_kb >> 10);
|
||||
std::fflush(stdout);
|
||||
}
|
||||
|
||||
void TextureCache::GarbageCollectImages() {
|
||||
@@ -1220,6 +1291,7 @@ void TextureCache::GarbageCollectImages() {
|
||||
BbStats::coarse_second.store(u32(second), std::memory_order_relaxed);
|
||||
gc_tick_at_second[second % gc_tick_at_second.size()] = gc_tick;
|
||||
}
|
||||
ReportMemory();
|
||||
if (BbToggle::Disabled(BbToggle::TextureCollector)) {
|
||||
return;
|
||||
}
|
||||
@@ -1245,6 +1317,8 @@ void TextureCache::GarbageCollectImages() {
|
||||
return gc_tick_at_second[(second - seconds) % gc_tick_at_second.size()];
|
||||
};
|
||||
const u64 idle_tick = tick_seconds_ago(idle_seconds);
|
||||
// Last used a minute ago or more (lru_touched_tick: real use, not the collector's Touch).
|
||||
const u64 minute_tick = tick_seconds_ago(60);
|
||||
std::scoped_lock lock{mutex};
|
||||
bool pressured = false;
|
||||
bool aggresive = false;
|
||||
@@ -1278,13 +1352,18 @@ void TextureCache::GarbageCollectImages() {
|
||||
// tiled ones cannot be written back at all) neither use up the deletions nor stay first
|
||||
// in line. The oldest ten being such render targets stopped the collector for good, and
|
||||
// VRAM grew with every area visited up to the pressure mark (~10 GB on 16 GB).
|
||||
if ((tiled && download) || (download && !pressured)) {
|
||||
// Past the critical mark, one unused for a minute goes all the same, unwritten (render
|
||||
// targets of an area left behind): out of VRAM the game would stop instead.
|
||||
const bool drop = tiled && download && aggresive && image.lru_touched_tick < minute_tick;
|
||||
if (!drop && ((tiled && download) || (download && !pressured))) {
|
||||
lru_cache.Touch(image.lru_id, gc_tick);
|
||||
BbStats::gc_kept_images.fetch_add(1, std::memory_order_relaxed);
|
||||
return false;
|
||||
}
|
||||
--num_deletions;
|
||||
if (download) {
|
||||
if (drop) {
|
||||
++gc_dropped;
|
||||
} else if (download) {
|
||||
// bbport: synchronously, while the image still protects its pages. A deferred
|
||||
// write-back landed after FreeImage had unprotected them, over whatever the game
|
||||
// had meanwhile stored there (e.g. its heap after unloading an area).
|
||||
@@ -1323,17 +1402,19 @@ void TextureCache::GarbageCollectImages() {
|
||||
lru_cache.ForEachItemBelow(below_tick, clean_up);
|
||||
}
|
||||
// bbport: evictions under memory pressure, at most every 5 s (BB_FRAME_STATS or not).
|
||||
if (pressured || gc_downloads != 0) {
|
||||
if (pressured || gc_downloads != 0 || gc_dropped != 0) {
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
if (now - gc_report_time >= std::chrono::seconds(5)) {
|
||||
std::printf("Texture cache: memory pressure, %llu of %llu MiB (critical %llu): "
|
||||
"%llu images evicted, %llu written back since the last report\n",
|
||||
"%llu images evicted, %llu written back, %llu GPU-written dropped "
|
||||
"since the last report\n",
|
||||
(unsigned long long)(total_used_memory >> 20),
|
||||
(unsigned long long)(pressure_gc_memory >> 20),
|
||||
(unsigned long long)(critical_gc_memory >> 20),
|
||||
(unsigned long long)gc_evictions, (unsigned long long)gc_downloads);
|
||||
(unsigned long long)gc_evictions, (unsigned long long)gc_downloads,
|
||||
(unsigned long long)gc_dropped);
|
||||
gc_report_time = now;
|
||||
gc_evictions = gc_downloads = 0;
|
||||
gc_evictions = gc_downloads = gc_dropped = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#include "common/slot_vector.h"
|
||||
#include "shader_recompiler/resource.h"
|
||||
#include "video_core/multi_level_page_table.h"
|
||||
#include "video_core/texture_cache/watched_pages.h"
|
||||
#include "video_core/texture_cache/blit_helper.h"
|
||||
#include "video_core/texture_cache/image.h"
|
||||
#include "video_core/texture_cache/image_view.h"
|
||||
@@ -100,6 +101,12 @@ public:
|
||||
|
||||
/// bbport: FindImage's access tick for an image found by a memoized lookup. The LRU touch
|
||||
/// FindImage also does happens in UpdateImage (FindTexture), which every binding reaches.
|
||||
/// bbport: whether a tracked image protects a page of [addr, addr + size). Any thread, no
|
||||
/// lock (watched_pages.h): a write fault elsewhere needs nothing from the texture cache.
|
||||
[[nodiscard]] bool WatchesPages(VAddr addr, size_t size) const noexcept {
|
||||
return watched_pages.Any(addr, size);
|
||||
}
|
||||
|
||||
void MarkFound(ImageId image_id) {
|
||||
slot_images[image_id].tick_accessed_last = scheduler.CurrentTick();
|
||||
}
|
||||
@@ -378,6 +385,9 @@ private:
|
||||
void UntrackImage(ImageId image_id);
|
||||
void UntrackImageHead(ImageId image_id);
|
||||
void UntrackImageTail(ImageId image_id);
|
||||
/// The page manager's watchers of an image's pages, and watched_pages with them.
|
||||
void WatchPages(VAddr addr, u64 size);
|
||||
void UnwatchPages(VAddr addr, u64 size);
|
||||
|
||||
void MarkAsMaybeDirty(ImageId image_id, Image& image);
|
||||
static u64 MaybeDirtyHash(const Image& image);
|
||||
@@ -404,6 +414,8 @@ private:
|
||||
AmdGpu::Liverpool* liverpool;
|
||||
BufferCache& buffer_cache;
|
||||
PageManager& tracker;
|
||||
/// bbport: the pages tracked images protect, readable without `mutex` (WatchesPages).
|
||||
WatchedPages watched_pages;
|
||||
BlitHelper blit_helper;
|
||||
TileManager tile_manager;
|
||||
Common::SlotVector<Image> slot_images;
|
||||
@@ -411,8 +423,12 @@ private:
|
||||
tsl::robin_map<u64, Sampler> samplers;
|
||||
std::unordered_set<ImageId> download_images;
|
||||
u64 total_used_memory = 0;
|
||||
u64 gc_evictions = 0, gc_downloads = 0; ///< bbport: pressure report
|
||||
u64 gc_evictions = 0, gc_downloads = 0, gc_dropped = 0; ///< bbport: pressure report
|
||||
std::chrono::steady_clock::time_point gc_report_time{};
|
||||
/// bbport: registered images and their guest size, for the once-a-minute memory line.
|
||||
u64 image_count = 0, image_bytes = 0;
|
||||
std::chrono::steady_clock::time_point memory_report_time{};
|
||||
void ReportMemory();
|
||||
/// bbport: gc_tick at each of the last 64 seconds (GarbageCollectImages).
|
||||
std::array<u64, 64> gc_tick_at_second{};
|
||||
u64 gc_second = 0;
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
// bbport: how many tracked images watch each guest page, readable without the texture cache's
|
||||
// lock. From Awaken's BloodBorneButPc. No Vulkan here: tests/test_watched_pages.cpp.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
namespace VideoCore {
|
||||
|
||||
/// The pages each tracked image protects (TextureCache::WatchPages / UnwatchPages: the ranges the
|
||||
/// page manager is given, counted per page as it counts them), per 4 KiB page. A write fault on a
|
||||
/// page no tracked image watches (a buffer's page: most faults) needs nothing from the texture
|
||||
/// cache, so the guest thread does not take its lock, which the draw recording thread holds while
|
||||
/// it creates or uploads an image (Rasterizer::OnWriteFault).
|
||||
///
|
||||
/// Writers hold the texture cache's lock (one at a time); readers may be any thread. A reader
|
||||
/// racing with a track sees the page either way: unwatched, its write lands before the image's
|
||||
/// upload reads the memory; watched, it takes the lock as before. Counts go up before the pages
|
||||
/// are protected and down after they are unprotected.
|
||||
class WatchedPages {
|
||||
public:
|
||||
static constexpr std::uint32_t PageBits = 12; ///< the page manager's 4 KiB pages
|
||||
static constexpr std::uint32_t ChunkPageBits = 16; ///< 256 MiB of guest memory per chunk
|
||||
static constexpr std::uint32_t AddressBits = 40; ///< guest addresses
|
||||
|
||||
/// `delta` (+1/-1) for every page of [address, address + size). Writers hold the lock.
|
||||
void Add(std::uint64_t address, std::uint64_t size, int delta) {
|
||||
const std::uint64_t first = address >> PageBits;
|
||||
const std::uint64_t end = (address + size + PageSize - 1) >> PageBits;
|
||||
for (std::uint64_t page = first; page < end; ++page) {
|
||||
const std::uint64_t chunk = page >> ChunkPageBits;
|
||||
if (chunk >= NumChunks) {
|
||||
return; // Any() takes those as watched
|
||||
}
|
||||
Count* counts = chunks[chunk].load(std::memory_order_relaxed);
|
||||
if (!counts) {
|
||||
if (delta < 0) {
|
||||
continue;
|
||||
}
|
||||
owned.push_back(std::make_unique<Count[]>(ChunkPages));
|
||||
counts = owned.back().get();
|
||||
chunks[chunk].store(counts, std::memory_order_release);
|
||||
}
|
||||
Count& count = counts[page & (ChunkPages - 1)];
|
||||
const std::uint32_t now = count.load(std::memory_order_relaxed);
|
||||
// One writer: a plain read-modify-write (no locked instruction per page).
|
||||
count.store(static_cast<std::uint16_t>(delta < 0 ? (now ? now - 1 : 0) : now + 1),
|
||||
std::memory_order_release);
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether a tracked image watches a page of [address, address + size). Any thread.
|
||||
[[nodiscard]] bool Any(std::uint64_t address, std::uint64_t size) const noexcept {
|
||||
const std::uint64_t first = address >> PageBits;
|
||||
const std::uint64_t end =
|
||||
(address + std::max<std::uint64_t>(size, 1) + PageSize - 1) >> PageBits;
|
||||
for (std::uint64_t page = first; page < end; ++page) {
|
||||
const std::uint64_t chunk = page >> ChunkPageBits;
|
||||
if (chunk >= NumChunks) {
|
||||
return true; // outside the table: assume watched (the locked path)
|
||||
}
|
||||
const Count* counts = chunks[chunk].load(std::memory_order_acquire);
|
||||
if (counts && counts[page & (ChunkPages - 1)].load(std::memory_order_acquire) != 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private:
|
||||
using Count = std::atomic<std::uint16_t>;
|
||||
static constexpr std::uint64_t PageSize = std::uint64_t{1} << PageBits;
|
||||
static constexpr std::size_t ChunkPages = std::size_t{1} << ChunkPageBits;
|
||||
static constexpr std::size_t NumChunks = std::size_t{1}
|
||||
<< (AddressBits - PageBits - ChunkPageBits);
|
||||
std::array<std::atomic<Count*>, NumChunks> chunks{};
|
||||
std::vector<std::unique_ptr<Count[]>> owned; ///< writers only
|
||||
};
|
||||
|
||||
} // namespace VideoCore
|
||||
+51
-2
@@ -27,12 +27,14 @@
|
||||
#include "common/elf_info.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "common/rdtsc.h"
|
||||
#include "common/signal_context.h"
|
||||
#include "core/libraries/kernel/orbis_error.h"
|
||||
#include "core/libraries/libs.h"
|
||||
#include "core/emulator_settings.h"
|
||||
#include "core/memory.h"
|
||||
#include "core/signals.h"
|
||||
#include "sdl_window.h"
|
||||
#include "bloodborne_cam.h"
|
||||
#include "video_core/renderer_vulkan/vk_rasterizer.h"
|
||||
|
||||
extern "C" {
|
||||
@@ -53,6 +55,7 @@ int32_t* runtime_errno(void);
|
||||
void runtime_memory_set_gpu_hooks(RuntimeGpuRange map, RuntimeGpuRange unmap, RuntimeGpuRange invalidate);
|
||||
void runtime_memory_set_note_write_hook(RuntimeGpuRange note);
|
||||
void runtime_memory_set_cpu_write_hook(RuntimeGpuRange hook);
|
||||
int runtime_memory_restore_access(void* context, uintptr_t address, int repeated);
|
||||
}
|
||||
|
||||
Frontend::WindowSDL* g_window = nullptr;
|
||||
@@ -322,6 +325,37 @@ extern "C" uintptr_t bbgpu_resolve(const char* scoped_nid) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
namespace {
|
||||
// bbport (from Awaken's BloodBorneButPc): the same access (instruction and address) faulting
|
||||
// again within microseconds, many times in a row: the tracking claimed the fault but left the
|
||||
// page protected, and the thread would retry forever. A write the game repeats each frame comes
|
||||
// milliseconds apart.
|
||||
struct RepeatedFault {
|
||||
u64 rip = 0, address = 0, last_tsc = 0;
|
||||
u32 count = 0;
|
||||
};
|
||||
thread_local RepeatedFault repeated_fault;
|
||||
|
||||
bool FaultRepeats(void* ucontext, void* address) {
|
||||
constexpr u64 RepeatCycles = 4'000'000; // ~1 ms
|
||||
constexpr u32 RepeatLimit = 64;
|
||||
auto& r = repeated_fault;
|
||||
const u64 rip = reinterpret_cast<u64>(Common::GetRip(ucontext));
|
||||
const u64 now = Common::FencedRDTSC();
|
||||
if (rip != r.rip || reinterpret_cast<u64>(address) != r.address ||
|
||||
now - r.last_tsc > RepeatCycles) {
|
||||
r = {rip, reinterpret_cast<u64>(address), now, 0};
|
||||
return false;
|
||||
}
|
||||
r.last_tsc = now;
|
||||
if (++r.count < RepeatLimit) {
|
||||
return false;
|
||||
}
|
||||
r.count = 0;
|
||||
return true;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
extern "C" int bbgpu_handle_fault(void* ucontext, void* address) {
|
||||
// BB_LABEL_TRAP (diagnostic): its read-only label pages first. Not passed on to GPU page
|
||||
// tracking (a write fault drains the draw pipe: thousands a second would change the timing
|
||||
@@ -330,9 +364,18 @@ extern "C" int bbgpu_handle_fault(void* ucontext, void* address) {
|
||||
return 1;
|
||||
}
|
||||
if (Core::Signals::Instance()->DispatchAccessViolation(ucontext, address)) {
|
||||
if (FaultRepeats(ucontext, address)) {
|
||||
runtime_memory_restore_access(ucontext, reinterpret_cast<uintptr_t>(address), 1);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
return BbFreeCheck::OnStaleTrapFault(reinterpret_cast<std::uint64_t>(address)) ? 1 : 0;
|
||||
if (BbFreeCheck::OnStaleTrapFault(reinterpret_cast<std::uint64_t>(address))) {
|
||||
return 1;
|
||||
}
|
||||
// bbport: nobody claimed it. A guest page the guest may access but GPU tracking left
|
||||
// protected (a stale protection, a page it no longer knows) gets its access back; ending the
|
||||
// game for it is worse than a GPU copy that may miss a write. Real faults stay unhandled.
|
||||
return runtime_memory_restore_access(ucontext, reinterpret_cast<uintptr_t>(address), 0);
|
||||
}
|
||||
|
||||
extern "C" void bbgpu_patch_image(unsigned char* image, uint64_t size) {
|
||||
@@ -364,11 +407,13 @@ static void KernelServiceThread(std::stop_token stoken) {
|
||||
{
|
||||
std::unique_lock lock{m_asio_req};
|
||||
cv_asio_req.wait(lock, stoken, [] { return asio_requests != 0; });
|
||||
// Taken before the run: a request made while it runs (a timer armed after run()
|
||||
// found no work) wakes the next one instead of being cleared after it.
|
||||
asio_requests.exchange(0);
|
||||
}
|
||||
if (stoken.stop_requested()) break;
|
||||
io_context.run();
|
||||
io_context.restart();
|
||||
asio_requests = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -440,6 +485,10 @@ extern "C" void bbgpu_mouse_look_enable(int enabled) {
|
||||
Frontend::WindowSDL::EnableMouseLook(enabled != 0);
|
||||
}
|
||||
|
||||
extern "C" void bbgpu_mouse_camera_configure(int direct, float sensitivity) {
|
||||
BbCamera::Configure(direct != 0, sensitivity);
|
||||
}
|
||||
|
||||
extern "C" int bbgpu_text_input_begin(const char* initial, const char* prompt) {
|
||||
if (!g_window) return 0;
|
||||
g_window->BeginTextInput(initial ? initial : "", prompt ? prompt : "Text");
|
||||
|
||||
@@ -865,6 +865,13 @@ inline const char* MenuTranslation(MenuLanguage language, const char* english) {
|
||||
"Tastatur: tippen, Rücktaste = löschen, Enter = OK, Esc = abbrechen",
|
||||
"Teclado: digite, Backspace = apagar, Enter = OK, Esc = cancelar",
|
||||
"键盘:直接输入,Backspace = 删除,Enter = 确定,Esc = 取消"}},
|
||||
{"Sound when a trophy is earned",
|
||||
{"Sonido al conseguir un trofeo",
|
||||
"Suono quando si ottiene un trofeo",
|
||||
"Son à l’obtention d’un trophée",
|
||||
"Ton beim Erhalt einer Trophäe",
|
||||
"Som ao conquistar um troféu",
|
||||
"获得奖杯时播放音效"}},
|
||||
{"Notice while shaders compile",
|
||||
{"Aviso mientras se compilan shaders",
|
||||
"Avviso durante la compilazione degli shader",
|
||||
|
||||
@@ -239,6 +239,12 @@ void Menu() {
|
||||
}
|
||||
ImGui::SetNextWindowPos(pos, ImGuiCond_Appearing);
|
||||
ImGui::SetNextWindowSize(ImVec2(620.0f * base_scale, 0.0f), ImGuiCond_Appearing);
|
||||
// A menu taller than the screen would leave its last row (Close, Quit game) off screen: its
|
||||
// height stays within the screen below where it opens, and the contents scroll.
|
||||
ImGui::SetNextWindowSizeConstraints(
|
||||
ImVec2(0.0f, 0.0f),
|
||||
ImVec2(FLT_MAX, std::max(viewport->WorkPos.y + viewport->WorkSize.y - pos.y - 20.0f * base_scale,
|
||||
200.0f * base_scale)));
|
||||
bool keep_open = true;
|
||||
if (!ImGui::Begin(
|
||||
BbSettings::MenuText("Bloodborne - Graphics (Insert / L3+R3)###bbport_settings",
|
||||
@@ -263,6 +269,7 @@ void Menu() {
|
||||
if (ImGui::Combo("Language", &language, BbSettings::MenuLanguageNames,
|
||||
int(BbSettings::MenuLanguage::Count))) {
|
||||
s.menu_language = BbSettings::MenuLanguage(language);
|
||||
s.menu_language_chosen = true;
|
||||
BbSettings::Save();
|
||||
}
|
||||
ImGui::Text(BbSettings::MenuText("%.0f FPS (%.1f ms)", "%.0f FPS (%.1f мс)"),
|
||||
@@ -371,13 +378,14 @@ void Menu() {
|
||||
for (int i = 0; i < BbSettings::PresetCount; ++i) {
|
||||
char label[64];
|
||||
const float scale = BbSettings::PresetScale(i);
|
||||
const int output = s.output_res;
|
||||
int output_width = BbSettings::OutputWidths[s.output_res];
|
||||
int output_height = BbSettings::OutputHeights[s.output_res];
|
||||
BbSettings::CustomOutput(output_width, output_height);
|
||||
std::snprintf(
|
||||
label, sizeof(label),
|
||||
BbSettings::MenuText("%s (x%.1f, render %dx%d)", "%s (x%.1f, рендер %dx%d)"),
|
||||
BbSettings::PresetName(i), scale,
|
||||
int(std::lround(BbSettings::OutputWidths[output] / scale / 2) * 2),
|
||||
int(std::lround(BbSettings::OutputHeights[output] / scale / 2) * 2));
|
||||
BbSettings::PresetName(i), scale, int(std::lround(output_width / scale / 2) * 2),
|
||||
int(std::lround(output_height / scale / 2) * 2));
|
||||
if (ImGui::Selectable(label, i == preset)) {
|
||||
Store(s.preset, i, true);
|
||||
}
|
||||
@@ -500,10 +508,19 @@ void Menu() {
|
||||
ImGui::SeparatorText(BbSettings::MenuText("Output resolution", "Разрешение вывода"));
|
||||
static const char* outputs[] = {"1280 x 720", "1920 x 1080", "2560 x 1440", "3840 x 2160"};
|
||||
int output = s.output_res;
|
||||
// A size from bbport.ini that the list does not have stays until another one is chosen.
|
||||
int custom_width = 0, custom_height = 0;
|
||||
const bool custom = BbSettings::CustomOutput(custom_width, custom_height);
|
||||
char custom_label[32];
|
||||
std::snprintf(custom_label, sizeof(custom_label), "%d x %d", custom_width, custom_height);
|
||||
if (ImGui::BeginCombo(BbSettings::MenuText("Output resolution", "Разрешение вывода"),
|
||||
outputs[output])) {
|
||||
custom ? custom_label : outputs[output])) {
|
||||
if (custom) {
|
||||
ImGui::Selectable(custom_label, true);
|
||||
}
|
||||
for (int i = 0; i < BbSettings::OutputCount; ++i) {
|
||||
if (ImGui::Selectable(outputs[i], i == output)) {
|
||||
if (ImGui::Selectable(outputs[i], !custom && i == output)) {
|
||||
s.custom_output_width = 0; // a listed size replaces the one from bbport.ini
|
||||
Store(s.output_res, i, true);
|
||||
}
|
||||
}
|
||||
@@ -613,6 +630,8 @@ void Menu() {
|
||||
"filling, not a problem. Each place only once.",
|
||||
"«Компиляция шейдеров» в левом нижнем углу: рывок в это время — заполнение кэша "
|
||||
"шейдеров, а не проблема. В каждом месте только один раз."));
|
||||
Checkbox(BbSettings::MenuText("Sound when a trophy is earned", "Звук при получении трофея"),
|
||||
s.trophy_sound);
|
||||
|
||||
ImGui::Spacing();
|
||||
if (ImGui::Button(BbSettings::MenuText("Close", "Закрыть"))) {
|
||||
@@ -620,10 +639,15 @@ void Menu() {
|
||||
}
|
||||
// Issues #74, #116: leaving the game from the pad (this menu opens with L3+R3). A second
|
||||
// press confirms; the window thread then closes like the window's own close button.
|
||||
// One ID for both labels ("###bb_quit"): with a new ID the button lost the pad's or keyboard's
|
||||
// focus at the first press, which disarmed it at once.
|
||||
static bool quit_armed;
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button(quit_armed ? BbSettings::MenuText("Press again to quit", "Нажмите ещё раз для выхода")
|
||||
: BbSettings::MenuText("Quit game", "Выйти из игры"))) {
|
||||
const std::string quit_label =
|
||||
std::string(quit_armed ? BbSettings::MenuText("Press again to quit", "Нажмите ещё раз для выхода")
|
||||
: BbSettings::MenuText("Quit game", "Выйти из игры")) +
|
||||
"###bb_quit";
|
||||
if (ImGui::Button(quit_label.c_str())) {
|
||||
if (quit_armed) {
|
||||
SDL_Event quit{};
|
||||
quit.type = SDL_EVENT_QUIT;
|
||||
@@ -744,6 +768,43 @@ ImTextureData* NoticeIcon(const Notice& n) {
|
||||
return texture;
|
||||
}
|
||||
|
||||
// The trophy chime: a short bell of our own (no audio from the console), two notes and a third
|
||||
// for the platinum, on its own SDL stream beside the game's. From GerasimosKan's trophies
|
||||
// (deadinside28/bloodborne_pc#127). BB_AUDIO=none keeps it silent like the game.
|
||||
void TrophyChime(bool platinum) {
|
||||
static SDL_AudioStream* stream = []() -> SDL_AudioStream* {
|
||||
const char* mode = std::getenv("BB_AUDIO");
|
||||
if ((mode && !std::strcmp(mode, "none")) || !SDL_InitSubSystem(SDL_INIT_AUDIO)) {
|
||||
return nullptr;
|
||||
}
|
||||
const SDL_AudioSpec spec{SDL_AUDIO_F32, 1, 48000};
|
||||
SDL_AudioStream* s =
|
||||
SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &spec, nullptr, nullptr);
|
||||
if (s) {
|
||||
SDL_ResumeAudioStreamDevice(s);
|
||||
}
|
||||
return s;
|
||||
}();
|
||||
if (!stream) {
|
||||
return;
|
||||
}
|
||||
constexpr int rate = 48000;
|
||||
constexpr float two_pi = 6.2831853f, step = 0.09f;
|
||||
std::vector<float> pcm(rate * 6 / 5);
|
||||
const float notes[] = {1318.5f, 1975.5f, 2637.0f}; // E6, B6, E7
|
||||
for (int k = 0; k < (platinum ? 3 : 2); ++k) {
|
||||
for (std::size_t i = std::size_t(k * step * rate); i < pcm.size(); ++i) {
|
||||
const float t = float(i) / rate - k * step, f = notes[k];
|
||||
const float envelope = std::min(t / 0.004f, 1.0f) * std::exp(-t * 5.0f);
|
||||
// A bell: the fundamental and a quickly fading inharmonic partial.
|
||||
pcm[i] += 0.12f * envelope *
|
||||
(std::sin(two_pi * f * t) +
|
||||
0.35f * std::exp(-t * 8.0f) * std::sin(two_pi * 2.76f * f * t));
|
||||
}
|
||||
}
|
||||
SDL_PutAudioStreamData(stream, pcm.data(), int(pcm.size() * sizeof(float)));
|
||||
}
|
||||
|
||||
void TrophyCard() {
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
if (!notice_shown) {
|
||||
@@ -756,6 +817,9 @@ void TrophyCard() {
|
||||
notices.pop_front();
|
||||
notice_shown = true;
|
||||
notice_start = now;
|
||||
if (BbSettings::Get().trophy_sound) {
|
||||
TrophyChime(notice.grade == 'P');
|
||||
}
|
||||
}
|
||||
const float t = std::chrono::duration<float>(now - notice_start).count();
|
||||
if (t > NoticeIn + NoticeHold + NoticeOut) {
|
||||
|
||||
@@ -33,6 +33,7 @@ void Set(Values& v, const std::string& key, const std::string& value) {
|
||||
const auto it = std::find(std::begin(MenuLanguageCodes), std::end(MenuLanguageCodes), value);
|
||||
if (it != std::end(MenuLanguageCodes)) {
|
||||
v.menu_language = MenuLanguage(it - std::begin(MenuLanguageCodes));
|
||||
v.menu_language_chosen = true;
|
||||
} else {
|
||||
std::printf("Settings: unknown menu_language '%s' (expected en, ru, es, it, fr, de, pt_BR or zh_CN)\n",
|
||||
value.c_str());
|
||||
@@ -67,6 +68,8 @@ void Set(Values& v, const std::string& key, const std::string& value) {
|
||||
v.show_fps = i != 0;
|
||||
} else if (key == "shader_notice") {
|
||||
v.shader_notice = i != 0;
|
||||
} else if (key == "trophy_sound") {
|
||||
v.trophy_sound = i != 0;
|
||||
} else if (key == "menu_pos") {
|
||||
float x = -1.0f, y = -1.0f;
|
||||
if (std::sscanf(value.c_str(), "%f,%f", &x, &y) == 2 && x >= 0.0f && x <= 1.0f && y >= 0.0f &&
|
||||
@@ -83,11 +86,19 @@ void Set(Values& v, const std::string& key, const std::string& value) {
|
||||
} else if (key == "live_resolution") {
|
||||
v.live_resolution = value == "auto" ? -1 : std::clamp(i, 0, 1);
|
||||
} else if (key == "output_res") {
|
||||
bool listed = false;
|
||||
for (int r = 0; r < OutputCount; ++r) {
|
||||
if (value == std::to_string(OutputWidths[r]) + "x" + std::to_string(OutputHeights[r])) {
|
||||
v.output_res = r;
|
||||
listed = true;
|
||||
}
|
||||
}
|
||||
int width = 0, height = 0;
|
||||
if (!listed && std::sscanf(value.c_str(), "%dx%d", &width, &height) == 2 && width > 0 &&
|
||||
height > 0) {
|
||||
v.custom_output_width = width;
|
||||
v.custom_output_height = height;
|
||||
}
|
||||
} else {
|
||||
for (int e = 0; e < EffectCount; ++e) {
|
||||
if (key == Effects[e].key) {
|
||||
@@ -133,6 +144,7 @@ MenuLanguage MenuLanguageFromCode(const char* code) {
|
||||
|
||||
void Load() {
|
||||
auto& v = Get();
|
||||
v.custom_output_width = v.custom_output_height = 0;
|
||||
for (int e = 0; e < EffectCount; ++e) {
|
||||
v.effects[e] = Effects[e].default_on;
|
||||
}
|
||||
@@ -149,6 +161,7 @@ void Load() {
|
||||
}
|
||||
}
|
||||
v.menu_language = MenuLanguageFromCode(lang);
|
||||
v.menu_language_chosen = false;
|
||||
}
|
||||
if (FILE* file = std::fopen(Path(), "r")) {
|
||||
char line[256];
|
||||
@@ -190,6 +203,7 @@ void Load() {
|
||||
}
|
||||
v.startup_model_lod = v.model_lod;
|
||||
v.startup_output_res = v.output_res;
|
||||
v.startup_custom_output_width = v.custom_output_width;
|
||||
v.startup_live_resolution = v.live_resolution;
|
||||
}
|
||||
|
||||
@@ -231,11 +245,22 @@ int RenderPreset() {
|
||||
v.upscaler == UpscalerTaa ? NativeAA : v.preset.load();
|
||||
}
|
||||
|
||||
bool CustomOutput(int& width, int& height) {
|
||||
const auto& v = Get();
|
||||
if (v.custom_output_width <= 0 || v.output_res != v.startup_output_res) {
|
||||
return false;
|
||||
}
|
||||
width = v.custom_output_width;
|
||||
height = v.custom_output_height;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ResolutionNeedsRestart() {
|
||||
const auto& v = Get();
|
||||
// TAA needs the live path (native guest targets): run.sh selects it on restart.
|
||||
return FixedRenderSession() &&
|
||||
(v.preset != v.startup_preset || v.output_res != v.startup_output_res ||
|
||||
v.custom_output_width != v.startup_custom_output_width ||
|
||||
(v.upscaler == UpscalerOff) != (v.startup_upscaler == UpscalerOff) ||
|
||||
(v.upscaler == UpscalerTaa) != (v.startup_upscaler == UpscalerTaa));
|
||||
}
|
||||
@@ -252,7 +277,11 @@ void Save() {
|
||||
return std::string(text);
|
||||
};
|
||||
const auto flag = [](bool value) { return std::string(value ? "1" : "0"); };
|
||||
put("menu_language", MenuLanguageCodes[int(v.menu_language.load())]);
|
||||
// Not the launcher's or the system's language the menu started with: that one is not a choice,
|
||||
// and once in the file it would stay whatever the launcher's language becomes.
|
||||
if (v.menu_language_chosen) {
|
||||
put("menu_language", MenuLanguageCodes[int(v.menu_language.load())]);
|
||||
}
|
||||
put("upscaler", UpscalerName(v.upscaler));
|
||||
put("preset", std::to_string(v.preset.load()));
|
||||
put("sharpen", flag(v.sharpen));
|
||||
@@ -266,6 +295,7 @@ void Save() {
|
||||
put("debug_view", std::to_string(v.debug_view.load()));
|
||||
put("show_fps", flag(v.show_fps));
|
||||
put("shader_notice", flag(v.shader_notice));
|
||||
put("trophy_sound", flag(v.trophy_sound));
|
||||
put("fsr4_auto_exposure", flag(v.fsr4_auto_exposure));
|
||||
put("fsr4_invert_jitter", flag(v.fsr4_invert_jitter));
|
||||
// Read by patches.py at start.
|
||||
@@ -273,8 +303,13 @@ void Save() {
|
||||
put(Effects[e].key, flag(v.effects[e]));
|
||||
}
|
||||
put("model_lod", std::to_string(v.model_lod.load()));
|
||||
put("output_res", std::to_string(OutputWidths[v.output_res]) + "x" +
|
||||
std::to_string(OutputHeights[v.output_res]));
|
||||
int custom_width = 0, custom_height = 0;
|
||||
if (CustomOutput(custom_width, custom_height)) {
|
||||
put("output_res", std::to_string(custom_width) + "x" + std::to_string(custom_height));
|
||||
} else {
|
||||
put("output_res", std::to_string(OutputWidths[v.output_res]) + "x" +
|
||||
std::to_string(OutputHeights[v.output_res]));
|
||||
}
|
||||
// Read by run.sh at start.
|
||||
put("live_resolution", v.live_resolution < 0 ? "auto" : flag(v.live_resolution != 0));
|
||||
if (v.menu_x >= 0.0f && v.menu_y >= 0.0f) {
|
||||
|
||||
Loaded 100 of 144 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user