Compare commits
1
Commits
networking
..
server
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8c4eeb9793 |
No files matched your search
+2
-12
@@ -41,7 +41,7 @@ configured player slot connects or disconnects.
|
||||
|
||||
`WebsocketGameClient` implements `IGameServer`.
|
||||
|
||||
- It connects asynchronously to a `ws://` or `wss://` endpoint (`WebsocketEndpoint::parse`).
|
||||
- It connects asynchronously to `ws://127.0.0.1:8080/`.
|
||||
- Received messages are queued by the network thread and applied on the game
|
||||
thread from `update()`.
|
||||
- It maintains an internal `InternalGameServer` replica so existing snapshot
|
||||
@@ -145,13 +145,6 @@ Networking is enabled for Windows and the Emscripten web client:
|
||||
replica, and remote-host code, and links Emscripten's `websocket.js` library.
|
||||
`WebsocketGameClient.cpp` selects the browser WebSocket API on Emscripten; it
|
||||
does not use Boost.Asio or Boost.Beast there.
|
||||
- `wss://` (TLS) is supported by the Windows client only. `proj-windows` also defines
|
||||
`NETWORK_TLS` and links static OpenSSL 3.4.1, which CMake downloads and builds
|
||||
automatically at configure time (`cmake/ThirdParty.cmake`, section 12) using a
|
||||
downloaded portable Strawberry Perl and the nmake from the installed Visual Studio
|
||||
(located through `vswhere`). Builds go to `thirdparty/openssl-3.4.1/install-Debug`
|
||||
and `install-Release`; later configures skip them. `NETWORK_TLS` unset (server tests)
|
||||
means `wss://` endpoints fail with a logged error.
|
||||
- Linux and Android builds do not define `NETWORK` and do not compile the
|
||||
network transport.
|
||||
|
||||
@@ -203,10 +196,7 @@ submission, and contiguous tick consumption.
|
||||
not implemented.
|
||||
- There is no authentication or reconnect/replay history.
|
||||
- A hash mismatch is logged but does not yet trigger checkpoint resynchronization.
|
||||
- The endpoint is hardcoded in the graphical client (`kMultiplayerServerUrl` in
|
||||
`Game.cpp`, currently `wss://api.shadowoverbishkek.com/server001/`).
|
||||
- TLS certificate or hostname problems are logged but deliberately do not abort the
|
||||
connection. `GameServer` itself has no TLS; it must sit behind a TLS-terminating proxy.
|
||||
- The endpoint is hardcoded in the graphical client.
|
||||
- Multiplayer Lua and dialogue behavior remains unsupported.
|
||||
- Determinism is supported for identical executable/content builds. The
|
||||
floating-point simulation is not guaranteed bit-identical across platforms
|
||||
|
||||
@@ -56,30 +56,3 @@ check_and_download("https://github.com/ThePhD/sol2/archive/refs/tags/v3.3.0.zip"
|
||||
|
||||
# 11) SDL2_mixer
|
||||
check_and_download("https://github.com/libsdl-org/SDL_mixer/archive/refs/tags/release-2.8.0.zip" "SDL_mixer-release-2.8.0.zip" "SDL_mixer-release-2.8.0" "CMakeLists.txt")
|
||||
|
||||
# 12) OpenSSL (TLS for wss://) and a native Windows Perl needed to build it
|
||||
if(WIN32)
|
||||
check_and_download("https://github.com/openssl/openssl/releases/download/openssl-3.4.1/openssl-3.4.1.tar.gz" "openssl-3.4.1.tar.gz" "openssl-3.4.1" "Configure")
|
||||
|
||||
# Strawberry Perl portable. The zip has no top-level folder, so it is
|
||||
# extracted into its own directory. (Git's MSYS perl cannot build OpenSSL.)
|
||||
set(STRAWBERRY_PERL_DIR "${THIRDPARTY_DIR}/strawberry-perl")
|
||||
set(STRAWBERRY_PERL_ARCHIVE "${THIRDPARTY_DIR}/strawberry-perl-5.42.3.1-64bit-portable.zip")
|
||||
if(NOT EXISTS "${STRAWBERRY_PERL_DIR}/perl/bin/perl.exe")
|
||||
if(NOT EXISTS "${STRAWBERRY_PERL_ARCHIVE}")
|
||||
message(STATUS "Downloading Strawberry Perl...")
|
||||
file(DOWNLOAD
|
||||
"https://github.com/StrawberryPerl/Perl-Dist-Strawberry/releases/download/SP_54231_64bit/strawberry-perl-5.42.3.1-64bit-portable.zip"
|
||||
"${STRAWBERRY_PERL_ARCHIVE}" SHOW_PROGRESS)
|
||||
endif()
|
||||
message(STATUS "Extracting Strawberry Perl...")
|
||||
file(MAKE_DIRECTORY "${STRAWBERRY_PERL_DIR}")
|
||||
execute_process(
|
||||
COMMAND ${CMAKE_COMMAND} -E tar xf "${STRAWBERRY_PERL_ARCHIVE}"
|
||||
WORKING_DIRECTORY "${STRAWBERRY_PERL_DIR}"
|
||||
)
|
||||
if(NOT EXISTS "${STRAWBERRY_PERL_DIR}/perl/bin/perl.exe")
|
||||
message(FATAL_ERROR "Strawberry Perl extraction failed")
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
@@ -0,0 +1,47 @@
|
||||
# cmake/FetchDependenciesServer.cmake
|
||||
#
|
||||
# Downloads only what the headless GameServer needs (Eigen + Boost headers).
|
||||
# Skips anything already present in thirdparty/. Works on Windows and Linux.
|
||||
|
||||
set(THIRDPARTY_DIR "${CMAKE_CURRENT_LIST_DIR}/../thirdparty")
|
||||
file(MAKE_DIRECTORY "${THIRDPARTY_DIR}")
|
||||
|
||||
macro(server_check_and_download URL ARCHIVE_NAME EXTRACTED_DIR_NAME CHECK_FILE)
|
||||
set(_archive_path "${THIRDPARTY_DIR}/${ARCHIVE_NAME}")
|
||||
set(_src_path "${THIRDPARTY_DIR}/${EXTRACTED_DIR_NAME}")
|
||||
|
||||
if(NOT EXISTS "${_src_path}/${CHECK_FILE}")
|
||||
if(NOT EXISTS "${_archive_path}")
|
||||
message(STATUS "Downloading ${ARCHIVE_NAME}...")
|
||||
file(DOWNLOAD "${URL}" "${_archive_path}"
|
||||
SHOW_PROGRESS STATUS _dl_status)
|
||||
list(GET _dl_status 0 _dl_code)
|
||||
if(NOT _dl_code EQUAL 0)
|
||||
file(REMOVE "${_archive_path}")
|
||||
message(FATAL_ERROR "Failed to download ${URL}: ${_dl_status}")
|
||||
endif()
|
||||
endif()
|
||||
message(STATUS "Extracting ${ARCHIVE_NAME}...")
|
||||
execute_process(
|
||||
COMMAND ${CMAKE_COMMAND} -E tar xf "${_archive_path}"
|
||||
WORKING_DIRECTORY "${THIRDPARTY_DIR}"
|
||||
RESULT_VARIABLE _tar_res)
|
||||
if(NOT _tar_res EQUAL 0)
|
||||
message(FATAL_ERROR "Failed to extract ${_archive_path}")
|
||||
endif()
|
||||
endif()
|
||||
endmacro()
|
||||
|
||||
# Eigen: only needed if no system install is available.
|
||||
find_package(Eigen3 3.3 CONFIG QUIET)
|
||||
if(NOT TARGET Eigen3::Eigen)
|
||||
server_check_and_download(
|
||||
"https://gitlab.com/libeigen/eigen/-/archive/5.0.0/eigen-5.0.0.zip"
|
||||
"eigen-5.0.0.zip" "eigen-5.0.0" "signature_of_eigen3_matrix_library")
|
||||
endif()
|
||||
|
||||
# Boost (header-only usage: asio + beast). Use bundled copy always so the
|
||||
# version matches the Windows build.
|
||||
server_check_and_download(
|
||||
"https://archives.boost.io/release/1.90.0/source/boost_1_90_0.zip"
|
||||
"boost_1_90_0.zip" "boost_1_90_0" "boost/asio.hpp")
|
||||
@@ -726,105 +726,3 @@ set_target_properties(SDL2_mixer_external_lib PROPERTIES
|
||||
INTERFACE_LINK_LIBRARIES
|
||||
"SDL2_external_lib"
|
||||
)
|
||||
# ===========================================
|
||||
# 12) OpenSSL (3.4.1) - TLS for wss:// (Windows only, static libs)
|
||||
# ===========================================
|
||||
if(WIN32)
|
||||
set(OPENSSL_SRC_DIR "${THIRDPARTY_DIR}/openssl-3.4.1")
|
||||
set(OPENSSL_BASE_DIR "${OPENSSL_SRC_DIR}/install")
|
||||
|
||||
set(_have_openssl TRUE)
|
||||
foreach(cfg IN LISTS BUILD_CONFIGS)
|
||||
if(NOT EXISTS "${OPENSSL_BASE_DIR}-${cfg}/lib/libssl.lib" OR
|
||||
NOT EXISTS "${OPENSSL_BASE_DIR}-${cfg}/lib/libcrypto.lib")
|
||||
set(_have_openssl FALSE)
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
if(NOT _have_openssl)
|
||||
# Locate Visual Studio's vcvarsall.bat (provides cl, link, nmake).
|
||||
set(_vswhere "$ENV{ProgramFiles\(x86\)}/Microsoft Visual Studio/Installer/vswhere.exe")
|
||||
set(_vcvarsall "")
|
||||
if(EXISTS "${_vswhere}")
|
||||
execute_process(
|
||||
COMMAND "${_vswhere}" -latest -products * -property installationPath
|
||||
OUTPUT_VARIABLE _vs_path
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
)
|
||||
if(_vs_path AND EXISTS "${_vs_path}/VC/Auxiliary/Build/vcvarsall.bat")
|
||||
set(_vcvarsall "${_vs_path}/VC/Auxiliary/Build/vcvarsall.bat")
|
||||
endif()
|
||||
endif()
|
||||
if(_vcvarsall STREQUAL "")
|
||||
message(FATAL_ERROR "Visual Studio (vcvarsall.bat) not found; it is required to build OpenSSL")
|
||||
endif()
|
||||
|
||||
if(CMAKE_SIZEOF_VOID_P EQUAL 8)
|
||||
set(_ossl_vc_arch x64)
|
||||
set(_ossl_target VC-WIN64A)
|
||||
else()
|
||||
set(_ossl_vc_arch x86)
|
||||
set(_ossl_target VC-WIN32)
|
||||
endif()
|
||||
|
||||
file(TO_NATIVE_PATH "${_vcvarsall}" _vcvarsall_native)
|
||||
file(TO_NATIVE_PATH "${STRAWBERRY_PERL_DIR}/perl/bin" _perl_bin_native)
|
||||
file(TO_NATIVE_PATH "${STRAWBERRY_PERL_DIR}/perl/site/bin" _perl_site_native)
|
||||
|
||||
foreach(cfg IN LISTS BUILD_CONFIGS)
|
||||
set(_ossl_build "${OPENSSL_SRC_DIR}/build-${cfg}")
|
||||
file(REMOVE_RECURSE "${_ossl_build}")
|
||||
file(MAKE_DIRECTORY "${_ossl_build}")
|
||||
file(TO_NATIVE_PATH "${_ossl_build}" _ossl_build_native)
|
||||
file(TO_NATIVE_PATH "${OPENSSL_SRC_DIR}/Configure" _ossl_configure_native)
|
||||
file(TO_NATIVE_PATH "${OPENSSL_BASE_DIR}-${cfg}" _ossl_prefix_native)
|
||||
|
||||
set(_ossl_cfg_flags "")
|
||||
if(cfg STREQUAL "Debug")
|
||||
# /MDd: link the dynamic debug CRT like the game (static libs default to /MTd)
|
||||
set(_ossl_cfg_flags "--debug /MDd")
|
||||
else()
|
||||
set(_ossl_cfg_flags "--release /MD")
|
||||
endif()
|
||||
|
||||
# A batch file avoids cmd.exe quoting problems.
|
||||
set(_ossl_bat "${_ossl_build}/build_openssl.bat")
|
||||
file(WRITE "${_ossl_bat}"
|
||||
"@echo off
|
||||
call \"${_vcvarsall_native}\" ${_ossl_vc_arch} || exit /b 1
|
||||
set \"PATH=${_perl_bin_native};${_perl_site_native};%PATH%\"
|
||||
set CL=/MP
|
||||
cd /d \"${_ossl_build_native}\" || exit /b 1
|
||||
perl \"${_ossl_configure_native}\" ${_ossl_target} no-shared no-asm no-tests no-docs ${_ossl_cfg_flags} --prefix=\"${_ossl_prefix_native}\" --openssldir=\"${_ossl_prefix_native}/ssl\" || exit /b 1
|
||||
nmake || exit /b 1
|
||||
nmake install_sw || exit /b 1
|
||||
")
|
||||
log("Building OpenSSL (${cfg}) - this takes several minutes ...")
|
||||
execute_process(
|
||||
COMMAND cmd /c "${_ossl_bat}"
|
||||
RESULT_VARIABLE _ossl_res
|
||||
)
|
||||
if(NOT _ossl_res EQUAL 0)
|
||||
message(FATAL_ERROR "OpenSSL build failed for ${cfg} (see output above)")
|
||||
endif()
|
||||
endforeach()
|
||||
endif()
|
||||
|
||||
add_library(openssl_crypto_external_lib UNKNOWN IMPORTED GLOBAL)
|
||||
set_target_properties(openssl_crypto_external_lib PROPERTIES
|
||||
IMPORTED_LOCATION_DEBUG "${OPENSSL_BASE_DIR}-Debug/lib/libcrypto.lib"
|
||||
IMPORTED_LOCATION_RELEASE "${OPENSSL_BASE_DIR}-Release/lib/libcrypto.lib"
|
||||
INTERFACE_INCLUDE_DIRECTORIES
|
||||
"$<IF:$<CONFIG:Debug>,${OPENSSL_BASE_DIR}-Debug/include,${OPENSSL_BASE_DIR}-Release/include>"
|
||||
INTERFACE_LINK_LIBRARIES "ws2_32;crypt32;advapi32;user32;gdi32"
|
||||
)
|
||||
|
||||
add_library(openssl_ssl_external_lib UNKNOWN IMPORTED GLOBAL)
|
||||
set_target_properties(openssl_ssl_external_lib PROPERTIES
|
||||
IMPORTED_LOCATION_DEBUG "${OPENSSL_BASE_DIR}-Debug/lib/libssl.lib"
|
||||
IMPORTED_LOCATION_RELEASE "${OPENSSL_BASE_DIR}-Release/lib/libssl.lib"
|
||||
INTERFACE_INCLUDE_DIRECTORIES
|
||||
"$<IF:$<CONFIG:Debug>,${OPENSSL_BASE_DIR}-Debug/include,${OPENSSL_BASE_DIR}-Release/include>"
|
||||
INTERFACE_LINK_LIBRARIES "openssl_crypto_external_lib"
|
||||
)
|
||||
endif()
|
||||
@@ -143,7 +143,6 @@ target_compile_definitions(ShadowOverBishkekDemo PRIVATE
|
||||
_WIN32_WINNT=0x0A00
|
||||
PNG_ENABLED
|
||||
NETWORK
|
||||
NETWORK_TLS
|
||||
)
|
||||
|
||||
# Линкуем с SDL2main, если он вообще установлен
|
||||
@@ -161,7 +160,6 @@ target_link_libraries(ShadowOverBishkekDemo PRIVATE
|
||||
boost_external_lib
|
||||
sol2_external_lib
|
||||
SDL2_mixer_external_lib
|
||||
openssl_ssl_external_lib
|
||||
)
|
||||
|
||||
# Линкуем OpenGL (Windows)
|
||||
|
||||
+33
-18
@@ -9,6 +9,7 @@ set(CMAKE_CXX_EXTENSIONS OFF)
|
||||
set(GAME_ROOT "${CMAKE_CURRENT_LIST_DIR}/..")
|
||||
|
||||
find_package(Threads REQUIRED)
|
||||
include("${GAME_ROOT}/cmake/FetchDependenciesServer.cmake")
|
||||
find_package(Eigen3 3.3 CONFIG QUIET)
|
||||
if(NOT TARGET Eigen3::Eigen)
|
||||
set(EIGEN_BUNDLED_DIR "${GAME_ROOT}/thirdparty/eigen-5.0.0")
|
||||
@@ -105,24 +106,32 @@ endif()
|
||||
add_executable(GameServer src/main.cpp README.md)
|
||||
source_group("Server" FILES src/main.cpp README.md)
|
||||
target_link_libraries(GameServer PRIVATE game_server_core)
|
||||
set(BOOST_BUNDLED_DIR "${GAME_ROOT}/thirdparty/boost_1_90_0")
|
||||
if(NOT EXISTS "${BOOST_BUNDLED_DIR}/boost/asio.hpp")
|
||||
message(FATAL_ERROR
|
||||
"Boost was not found at thirdparty/boost_1_90_0 and could not be "
|
||||
"downloaded.")
|
||||
endif()
|
||||
target_sources(GameServer PRIVATE
|
||||
"${GAME_ROOT}/src/game_server/NetworkProtocol.cpp"
|
||||
"${GAME_ROOT}/src/game_server/NetworkProtocol.h"
|
||||
"${GAME_ROOT}/src/game_server/WebsocketGameServer.cpp"
|
||||
"${GAME_ROOT}/src/game_server/WebsocketGameServer.h")
|
||||
target_include_directories(GameServer PRIVATE "${BOOST_BUNDLED_DIR}")
|
||||
target_compile_definitions(GameServer PRIVATE NETWORK)
|
||||
if(WIN32)
|
||||
set(BOOST_BUNDLED_DIR "${GAME_ROOT}/thirdparty/boost_1_90_0")
|
||||
if(NOT EXISTS "${BOOST_BUNDLED_DIR}/boost/asio.hpp")
|
||||
message(FATAL_ERROR
|
||||
"Boost was not found at thirdparty/boost_1_90_0. Configure the "
|
||||
"graphical project once to populate it.")
|
||||
endif()
|
||||
target_sources(GameServer PRIVATE
|
||||
"${GAME_ROOT}/src/game_server/NetworkProtocol.cpp"
|
||||
"${GAME_ROOT}/src/game_server/NetworkProtocol.h"
|
||||
"${GAME_ROOT}/src/game_server/WebsocketGameServer.cpp"
|
||||
"${GAME_ROOT}/src/game_server/WebsocketGameServer.h")
|
||||
target_include_directories(GameServer PRIVATE "${BOOST_BUNDLED_DIR}")
|
||||
target_compile_definitions(
|
||||
GameServer PRIVATE NETWORK WIN32_LEAN_AND_MEAN _WIN32_WINNT=0x0A00)
|
||||
GameServer PRIVATE WIN32_LEAN_AND_MEAN _WIN32_WINNT=0x0A00)
|
||||
target_link_libraries(GameServer PRIVATE ws2_32 mswsock)
|
||||
endif()
|
||||
|
||||
# The server reads configs from ./resources relative to its working directory,
|
||||
# so keep a copy next to the binary after every build.
|
||||
add_custom_command(TARGET GameServer POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
"${GAME_ROOT}/resources" "$<TARGET_FILE_DIR:GameServer>/resources"
|
||||
COMMENT "Copying resources next to GameServer")
|
||||
|
||||
include(CTest)
|
||||
if(BUILD_TESTING)
|
||||
add_executable(
|
||||
@@ -141,7 +150,8 @@ if(BUILD_TESTING)
|
||||
add_test(
|
||||
NAME GameServer.LocationState
|
||||
COMMAND GameServerStateTests)
|
||||
if(WIN32)
|
||||
# network tests (all platforms)
|
||||
if(TRUE)
|
||||
add_executable(
|
||||
GameServerNetworkProtocolTests
|
||||
tests/NetworkProtocolTests.cpp
|
||||
@@ -161,11 +171,16 @@ if(BUILD_TESTING)
|
||||
target_include_directories(
|
||||
GameServerWebsocketLockstepTests PRIVATE "${BOOST_BUNDLED_DIR}")
|
||||
target_compile_definitions(
|
||||
GameServerWebsocketLockstepTests PRIVATE
|
||||
NETWORK WIN32_LEAN_AND_MEAN _WIN32_WINNT=0x0A00)
|
||||
GameServerWebsocketLockstepTests PRIVATE NETWORK)
|
||||
target_link_libraries(
|
||||
GameServerWebsocketLockstepTests PRIVATE
|
||||
game_server_core ws2_32 mswsock)
|
||||
GameServerWebsocketLockstepTests PRIVATE game_server_core)
|
||||
if(WIN32)
|
||||
target_compile_definitions(
|
||||
GameServerWebsocketLockstepTests PRIVATE
|
||||
WIN32_LEAN_AND_MEAN _WIN32_WINNT=0x0A00)
|
||||
target_link_libraries(
|
||||
GameServerWebsocketLockstepTests PRIVATE ws2_32 mswsock)
|
||||
endif()
|
||||
add_test(
|
||||
NAME GameServer.WebsocketLockstep
|
||||
COMMAND GameServerWebsocketLockstepTests)
|
||||
|
||||
+123
-62
@@ -1,11 +1,37 @@
|
||||
# Headless GameServer
|
||||
|
||||
This target runs the authoritative `InternalGameServer` without SDL, OpenGL,
|
||||
audio, Lua, or rendering dependencies. On Windows it exposes a deterministic
|
||||
lockstep session through a Boost.Beast WebSocket listener.
|
||||
audio, Lua, or rendering dependencies. It exposes a deterministic lockstep
|
||||
session through a Boost.Beast WebSocket listener (Windows and Linux).
|
||||
|
||||
## Requirements
|
||||
|
||||
- CMake >= 3.16, a C++17 compiler (MSVC, or GCC/Clang on Linux)
|
||||
- Ubuntu: `sudo apt install build-essential cmake git unzip curl`
|
||||
- Internet access on the first configure (see below)
|
||||
|
||||
Eigen and Boost are **downloaded automatically** by
|
||||
`cmake/FetchDependenciesServer.cmake` into `thirdparty/` the first time CMake
|
||||
configures the project (Boost zip is ~200 MB). A system Eigen3 is used if
|
||||
installed; Boost is always taken from `thirdparty/boost_1_90_0` so versions match
|
||||
the Windows build. Nothing else (SDL, zlib, libzip, ...) is needed.
|
||||
|
||||
## Build
|
||||
|
||||
Linux (recommended helper, 1 compile job to stay within low RAM):
|
||||
|
||||
```bash
|
||||
server/scripts/build.sh # JOBS=2 server/scripts/build.sh for more
|
||||
```
|
||||
|
||||
Or manually:
|
||||
|
||||
```bash
|
||||
cmake -S server -B server/build -DCMAKE_BUILD_TYPE=Release
|
||||
cmake --build server/build --target GameServer --parallel 1
|
||||
ctest --test-dir server/build --output-on-failure
|
||||
```
|
||||
|
||||
Windows:
|
||||
|
||||
```powershell
|
||||
@@ -14,91 +40,126 @@ cmake --build server/build --config Release --target GameServer
|
||||
ctest --test-dir server/build -C Release --output-on-failure
|
||||
```
|
||||
|
||||
Linux:
|
||||
> **Memory note:** compiling Boost.Beast code can use 1-1.5 GB per translation
|
||||
> unit. On a small VDS keep `--parallel 1`; if the compiler gets OOM-killed, add
|
||||
> temporary swap (`sudo fallocate -l 2G /swapfile && sudo chmod 600 /swapfile &&
|
||||
> sudo mkswap /swapfile && sudo swapon /swapfile`). The running server itself
|
||||
> needs only a few tens of MB.
|
||||
|
||||
After every build the repository's `resources/` directory is copied next to the
|
||||
binary (`server/build/resources`, or `server/build/Release/resources` on
|
||||
Windows). Content paths are resolved from the process working directory, so the
|
||||
server must be started from the directory that contains `resources/`.
|
||||
|
||||
## Run
|
||||
|
||||
```bash
|
||||
cmake -S server -B server/build -DCMAKE_BUILD_TYPE=Release
|
||||
cmake --build server/build --target GameServer --parallel
|
||||
ctest --test-dir server/build --output-on-failure
|
||||
server/scripts/run-server.sh
|
||||
```
|
||||
|
||||
Eigen3 must either be installed or be available at
|
||||
`thirdparty/eigen-5.0.0`. The headless target reads ordinary filesystem resources
|
||||
only; ZIP-backed campaign loading belongs to the later content-loading
|
||||
milestone.
|
||||
|
||||
Run until interrupted:
|
||||
This does `cd server/build` and runs
|
||||
|
||||
```bash
|
||||
./server/build/GameServer
|
||||
./GameServer --campaign battle_zone_multiplayer --address 127.0.0.1 --port 8081
|
||||
```
|
||||
|
||||
For a multi-configuration Windows build, the executable is normally
|
||||
`server/build/Release/GameServer.exe`.
|
||||
Environment overrides: `CAMPAIGN`, `LOCATION`, `ADDRESS`, `PORT`, `BUILD_DIR`.
|
||||
|
||||
For smoke tests, `--ticks N` limits execution and `--no-sleep` skips the
|
||||
real-time delay.
|
||||
|
||||
Start the multiplayer lockstep server:
|
||||
Windows equivalent:
|
||||
|
||||
```powershell
|
||||
.\server\build\Release\GameServer.exe --campaign battle_zone_multiplayer --location uni_exterior
|
||||
.\server\build\Release\GameServer.exe --campaign battle_zone_multiplayer
|
||||
```
|
||||
|
||||
The listener defaults to `ws://0.0.0.0:8080`. Launch the Windows graphical game
|
||||
afterward and select `battle_zone_multiplayer`; that campaign connects to
|
||||
`ws://127.0.0.1:8080`. Other campaigns continue to use the in-process server.
|
||||
Command line options: `--campaign ID`, `--location ID`, `--campaigns PATH`
|
||||
(default `resources/config/campaigns.json`), `--address IP` (default `0.0.0.0`),
|
||||
`--port N` (default `8080`), `--ticks N` and `--no-sleep` (smoke tests).
|
||||
|
||||
The server sends one state snapshot when a player connects, then broadcasts one
|
||||
numbered command bundle every 100 ms. Bundles may be empty. Destination input is
|
||||
scheduled two ticks (200 ms) in the future. Tick IDs, rather than server or
|
||||
client wall-clock values, define simulation time.
|
||||
|
||||
If `--location` is omitted, the campaign's configured start location is used.
|
||||
`--campaigns PATH` selects a different campaign registry. Content paths are
|
||||
resolved from the process working directory, so run from the repository root
|
||||
when using the bundled `resources/` tree.
|
||||
## Production deployment (Ubuntu, systemd + nginx)
|
||||
|
||||
Available startup options:
|
||||
The server listens on loopback only (`127.0.0.1:8081`); nginx terminates TLS and
|
||||
proxies WebSocket traffic.
|
||||
|
||||
```text
|
||||
--campaign ID
|
||||
--location ID
|
||||
--campaigns PATH
|
||||
--address IP
|
||||
--port N
|
||||
--tick-ms 100
|
||||
--ticks N
|
||||
--no-sleep
|
||||
### 1. systemd service
|
||||
|
||||
```bash
|
||||
sudo cp server/deploy/shadow-gameserver.service /etc/systemd/system/
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable --now shadow-gameserver
|
||||
systemctl status shadow-gameserver
|
||||
journalctl -u shadow-gameserver -f # logs
|
||||
```
|
||||
|
||||
Network mode requires a 100 ms tick; another `--tick-ms` value is rejected.
|
||||
The selected campaign's `networkPlayerSlots` lists pre-existing character IDs
|
||||
assigned to connections in order. `networkCompatibility` is a release token
|
||||
that must be changed whenever deterministic content changes; clients reject a
|
||||
different token. The bundled multiplayer Battle Zone has two slots (`player`
|
||||
and `player2`) and rejects additional clients. Disconnecting
|
||||
releases the slot. The bot's random autonomous controller is disabled in
|
||||
multiplayer.
|
||||
The unit caps memory at 512 MB and restarts on failure. After rebuilding run
|
||||
`sudo systemctl restart shadow-gameserver`. Edit `User=`/paths in the unit if the
|
||||
repository lives elsewhere. To run a second server, copy the unit with another
|
||||
`PORT` (and campaign) and add a matching `/server002/` location.
|
||||
|
||||
The graphical client executes presentation behavior after building its runtime
|
||||
`Location`; the headless content pass loads authoritative navigation, NPC,
|
||||
interactive-object, trigger, teleport, taxi, and animation metadata. Lua and
|
||||
dialogue-driven shared multiplayer behavior is not yet supported.
|
||||
### 2. nginx
|
||||
|
||||
## Tests
|
||||
```bash
|
||||
sudo cp server/deploy/nginx-api.shadowoverbishkek.com.conf \
|
||||
/etc/nginx/sites-available/api.shadowoverbishkek.com
|
||||
sudo ln -s /etc/nginx/sites-available/api.shadowoverbishkek.com \
|
||||
/etc/nginx/sites-enabled/
|
||||
sudo nginx -t && sudo systemctl reload nginx
|
||||
```
|
||||
|
||||
CTest covers:
|
||||
`location /server001/` proxies to `http://127.0.0.1:8081/` with WebSocket
|
||||
upgrade headers and a 1 hour read/send timeout (the trailing slashes strip the
|
||||
`/server001` prefix).
|
||||
|
||||
- repeated `LocationGameState::setup()` preserving mutable runtime state;
|
||||
- ticking a prepared location with zero players;
|
||||
- deterministic fixed-100 ms replay behavior;
|
||||
- protocol-v2 parsing, validation, target normalization, state hashing, and
|
||||
two-tick command ordering;
|
||||
- a WebSocket initial-state/slot/destination/tick integration test;
|
||||
- idle server startup;
|
||||
- headless loading of `battle_zone_multiplayer/uni_exterior`.
|
||||
### 3. HTTPS certificate
|
||||
|
||||
The lockstep implementation currently targets identical executable and content
|
||||
builds. Diagnostic state hashes detect divergence, but there is no automatic
|
||||
checkpoint resynchronization or reconnect input history yet.
|
||||
DNS for `api.shadowoverbishkek.com` must point at the VDS first.
|
||||
|
||||
```bash
|
||||
sudo certbot --nginx -d api.shadowoverbishkek.com
|
||||
```
|
||||
|
||||
Certbot issues a Let's Encrypt certificate, adds the `443 ssl` block and an HTTP
|
||||
to HTTPS redirect, and installs auto-renewal (`systemctl list-timers | grep certbot`).
|
||||
|
||||
## Connecting
|
||||
|
||||
Public endpoint: **`wss://api.shadowoverbishkek.com/server001/`**
|
||||
|
||||
Handshake check from any machine (expects `HTTP/1.1 101 Switching Protocols`):
|
||||
|
||||
```bash
|
||||
curl -i -N --max-time 3 \
|
||||
-H "Connection: Upgrade" -H "Upgrade: websocket" \
|
||||
-H "Sec-WebSocket-Version: 13" -H "Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==" \
|
||||
https://api.shadowoverbishkek.com/server001/
|
||||
```
|
||||
|
||||
Or interactively: `npx wscat -c wss://api.shadowoverbishkek.com/server001/`.
|
||||
|
||||
Locally on the VDS, bypassing nginx: `ws://127.0.0.1:8081`.
|
||||
|
||||
> **Client limitation:** `src/game_server/WebsocketGameClient.cpp` currently
|
||||
> speaks plain `ws://host:port` to `/` (no TLS, no path). Until TLS and path
|
||||
> support are added to the client, the graphical game can only connect directly
|
||||
> to a non-proxied server (e.g. `ws://127.0.0.1:8080`), not to the `wss://` URL.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- `Animation metadata failed ... Invalid animation file`: the `.anim` files are
|
||||
Git LFS pointer stubs (`version https://git-lfs...`). Fetch the real files with
|
||||
`git lfs pull` (or copy `resources/` from a full checkout) and rebuild.
|
||||
- `GameServer.LocationState` test currently fails on every platform: it expects
|
||||
`battle_zone` and `battle_zone_multiplayer` to share a start-state file, but
|
||||
`campaigns.json` points them to different files.
|
||||
|
||||
- `Server content setup failed`: the server was not started from the directory
|
||||
containing `resources/` (use `run-server.sh`).
|
||||
- `WebSocket listener setup failed`: port already in use (`ss -ltnp | grep 8081`).
|
||||
- `502 Bad Gateway` from nginx: service not running (`systemctl status
|
||||
shadow-gameserver`).
|
||||
- Connection drops after ~60 s: check `proxy_read_timeout` in the nginx location.
|
||||
- Compiler "Killed" / `internal compiler error`: out of memory, see the memory note.
|
||||
@@ -0,0 +1,47 @@
|
||||
# Install to /etc/nginx/sites-available/api.shadowoverbishkek.com and symlink
|
||||
# into sites-enabled. Then: sudo certbot --nginx -d api.shadowoverbishkek.com
|
||||
server {
|
||||
server_name api.shadowoverbishkek.com;
|
||||
|
||||
access_log /var/log/nginx/api.shadowoverbishkek.com-access.log;
|
||||
error_log /var/log/nginx/api.shadowoverbishkek.com-error.log;
|
||||
|
||||
# wss://api.shadowoverbishkek.com/server001/ -> 127.0.0.1:8081
|
||||
location /server001/ {
|
||||
proxy_pass http://127.0.0.1:8081/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
}
|
||||
|
||||
location / {
|
||||
return 404;
|
||||
}
|
||||
|
||||
listen 443 ssl; # managed by Certbot
|
||||
ssl_certificate /etc/letsencrypt/live/api.shadowoverbishkek.com/fullchain.pem; # managed by Certbot
|
||||
ssl_certificate_key /etc/letsencrypt/live/api.shadowoverbishkek.com/privkey.pem; # managed by Certbot
|
||||
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
|
||||
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
|
||||
|
||||
}
|
||||
|
||||
server {
|
||||
if ($host = api.shadowoverbishkek.com) {
|
||||
return 301 https://$host$request_uri;
|
||||
} # managed by Certbot
|
||||
|
||||
|
||||
server_name api.shadowoverbishkek.com;
|
||||
|
||||
listen 80;
|
||||
return 404; # managed by Certbot
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
[Unit]
|
||||
Description=Shadow Over Bishkek GameServer (server001)
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=ubuntu
|
||||
ExecStart=/home/ubuntu/shadowoverbishkek-work/shadow-over-bishkek001/server/scripts/run-server.sh
|
||||
Environment=PORT=8081
|
||||
Restart=on-failure
|
||||
RestartSec=3
|
||||
MemoryMax=512M
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
Executable
+9
@@ -0,0 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
# Configure and build the headless GameServer (Release).
|
||||
# Usage: server/scripts/build.sh (uses 1 job: safe for 2 GB RAM hosts)
|
||||
# JOBS=4 server/scripts/build.sh
|
||||
set -euo pipefail
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
cmake -S "$ROOT/server" -B "$ROOT/server/build" -DCMAKE_BUILD_TYPE=Release
|
||||
cmake --build "$ROOT/server/build" --target GameServer --parallel "${JOBS:-1}"
|
||||
echo "Built: $ROOT/server/build/GameServer"
|
||||
Executable
+16
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env bash
|
||||
# Start the multiplayer GameServer. Listens on loopback only; nginx proxies
|
||||
# wss://api.shadowoverbishkek.com/server001/ to it.
|
||||
# Overrides: CAMPAIGN, LOCATION, ADDRESS, PORT, BUILD_DIR
|
||||
set -euo pipefail
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
BUILD_DIR="${BUILD_DIR:-$ROOT/server/build}"
|
||||
CAMPAIGN="${CAMPAIGN:-battle_zone_multiplayer}"
|
||||
ADDRESS="${ADDRESS:-127.0.0.1}"
|
||||
PORT="${PORT:-8081}"
|
||||
|
||||
# Content paths are resolved from the working directory.
|
||||
cd "$BUILD_DIR"
|
||||
ARGS=(--campaign "$CAMPAIGN" --address "$ADDRESS" --port "$PORT")
|
||||
[ -n "${LOCATION:-}" ] && ARGS+=(--location "$LOCATION")
|
||||
exec ./GameServer "${ARGS[@]}"
|
||||
+1
-9
@@ -557,15 +557,7 @@ namespace FRG
|
||||
if (remoteCampaignActive) {
|
||||
logger() << "[network] Campaign '" << campaignId
|
||||
<< "' selected; creating WebSocket client" << std::endl;
|
||||
// Use "ws://127.0.0.1:8080/" for a local development server.
|
||||
static const char* const kMultiplayerServerUrl =
|
||||
"wss://api.shadowoverbishkek.com/server001/";
|
||||
WebsocketEndpoint endpoint;
|
||||
if (!endpoint.parse(kMultiplayerServerUrl)) {
|
||||
logger() << "[network] Invalid multiplayer server URL: "
|
||||
<< kMultiplayerServerUrl << std::endl;
|
||||
}
|
||||
websocketGameClient = std::make_unique<WebsocketGameClient>(endpoint);
|
||||
websocketGameClient = std::make_unique<WebsocketGameClient>();
|
||||
remoteGameServerHost =
|
||||
std::make_unique<RemoteGameServerHost>(*websocketGameClient);
|
||||
gameServer.use(*websocketGameClient);
|
||||
|
||||
@@ -5,21 +5,6 @@
|
||||
#else
|
||||
#include <boost/asio.hpp>
|
||||
#include <boost/beast.hpp>
|
||||
#ifdef NETWORK_TLS
|
||||
#include <wincrypt.h>
|
||||
// wincrypt.h defines macros that collide with OpenSSL identifiers.
|
||||
#undef X509_NAME
|
||||
#undef X509_EXTENSIONS
|
||||
#undef X509_CERT_PAIR
|
||||
#undef PKCS7_ISSUER_AND_SERIAL
|
||||
#undef OCSP_REQUEST
|
||||
#undef OCSP_RESPONSE
|
||||
#include <boost/asio/ssl.hpp>
|
||||
#include <boost/beast/ssl.hpp>
|
||||
#include <openssl/err.h>
|
||||
#include <openssl/x509.h>
|
||||
#include <openssl/x509_vfy.h>
|
||||
#endif
|
||||
#endif
|
||||
#include <atomic>
|
||||
#include <deque>
|
||||
@@ -38,104 +23,19 @@ namespace asio = boost::asio;
|
||||
namespace beast = boost::beast;
|
||||
namespace websocket = beast::websocket;
|
||||
using tcp = asio::ip::tcp;
|
||||
|
||||
#ifdef NETWORK_TLS
|
||||
// Loads the Windows "ROOT" certificate store into the OpenSSL context.
|
||||
void loadWindowsRootCertificates(asio::ssl::context& context)
|
||||
{
|
||||
HCERTSTORE store = CertOpenSystemStoreA(0, "ROOT");
|
||||
if (!store) {
|
||||
std::cerr << "[network] Could not open the Windows root certificate store\n";
|
||||
return;
|
||||
}
|
||||
X509_STORE* x509Store = SSL_CTX_get_cert_store(context.native_handle());
|
||||
PCCERT_CONTEXT cert = nullptr;
|
||||
while ((cert = CertEnumCertificatesInStore(store, cert)) != nullptr) {
|
||||
const unsigned char* data = cert->pbCertEncoded;
|
||||
X509* x509 = d2i_X509(nullptr, &data, static_cast<long>(cert->cbCertEncoded));
|
||||
if (x509) {
|
||||
X509_STORE_add_cert(x509Store, x509);
|
||||
X509_free(x509);
|
||||
}
|
||||
}
|
||||
CertCloseStore(store, 0);
|
||||
}
|
||||
|
||||
// Certificate problems are logged but deliberately do not abort the connection.
|
||||
bool logAndAcceptCertificate(bool preverified, asio::ssl::verify_context& context)
|
||||
{
|
||||
if (!preverified) {
|
||||
X509_STORE_CTX* storeContext = context.native_handle();
|
||||
const int error = X509_STORE_CTX_get_error(storeContext);
|
||||
const int depth = X509_STORE_CTX_get_error_depth(storeContext);
|
||||
char subject[256] = "?";
|
||||
if (X509* cert = X509_STORE_CTX_get_current_cert(storeContext))
|
||||
X509_NAME_oneline(X509_get_subject_name(cert), subject, sizeof(subject));
|
||||
std::cerr << "[network] TLS certificate warning (depth " << depth
|
||||
<< ", subject " << subject << "): "
|
||||
<< X509_verify_cert_error_string(error)
|
||||
<< " - continuing anyway\n";
|
||||
}
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
} // namespace
|
||||
|
||||
bool WebsocketEndpoint::parse(const std::string& url)
|
||||
{
|
||||
bool tls = false;
|
||||
std::string rest;
|
||||
if (url.rfind("wss://", 0) == 0) {
|
||||
tls = true;
|
||||
rest = url.substr(6);
|
||||
} else if (url.rfind("ws://", 0) == 0) {
|
||||
rest = url.substr(5);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
std::string authority = rest;
|
||||
std::string newPath = "/";
|
||||
const std::size_t slash = rest.find('/');
|
||||
if (slash != std::string::npos) {
|
||||
authority = rest.substr(0, slash);
|
||||
newPath = rest.substr(slash);
|
||||
}
|
||||
std::string newHost = authority;
|
||||
std::string newPort = tls ? "443" : "80";
|
||||
if (!authority.empty() && authority.front() == '[') { // [ipv6]:port
|
||||
const std::size_t close = authority.find(']');
|
||||
if (close == std::string::npos) return false;
|
||||
newHost = authority.substr(1, close - 1);
|
||||
if (close + 1 < authority.size()) {
|
||||
if (authority[close + 1] != ':') return false;
|
||||
newPort = authority.substr(close + 2);
|
||||
}
|
||||
} else {
|
||||
const std::size_t colon = authority.rfind(':');
|
||||
if (colon != std::string::npos) {
|
||||
newHost = authority.substr(0, colon);
|
||||
newPort = authority.substr(colon + 1);
|
||||
}
|
||||
}
|
||||
if (newHost.empty() || newPort.empty()) return false;
|
||||
host = std::move(newHost);
|
||||
port = std::move(newPort);
|
||||
path = std::move(newPath);
|
||||
useTls = tls;
|
||||
return true;
|
||||
}
|
||||
|
||||
class WebsocketGameClient::Impl {
|
||||
public:
|
||||
#ifdef __EMSCRIPTEN__
|
||||
Impl(std::string host, std::string port, std::string path, bool useTls)
|
||||
Impl(std::string host, std::string port, std::string path)
|
||||
: host_(std::move(host))
|
||||
, port_(std::move(port))
|
||||
, path_(std::move(path))
|
||||
{
|
||||
url_ = std::string(useTls ? "wss://" : "ws://") + host_ + ':' + port_ +
|
||||
url_ = "ws://" + host_ + ':' + port_ +
|
||||
(path_.empty() || path_.front() == '/' ? path_ : "/" + path_);
|
||||
std::cout << "[network] Opening " << url_ << std::endl;
|
||||
|
||||
@@ -243,38 +143,14 @@ private:
|
||||
bool closing_ = false;
|
||||
std::vector<std::string> messages_;
|
||||
#else
|
||||
Impl(std::string host, std::string port, std::string path, bool useTls)
|
||||
:
|
||||
#ifdef NETWORK_TLS
|
||||
sslContext_(asio::ssl::context::tls_client)
|
||||
, resolver_(io_)
|
||||
, secureSocket_(io_, sslContext_)
|
||||
#else
|
||||
resolver_(io_)
|
||||
#endif
|
||||
Impl(std::string host, std::string port, std::string path)
|
||||
: resolver_(io_)
|
||||
, socket_(io_)
|
||||
, host_(std::move(host))
|
||||
, port_(std::move(port))
|
||||
, path_(std::move(path))
|
||||
, useTls_(useTls)
|
||||
{
|
||||
#ifdef NETWORK_TLS
|
||||
sslContext_.set_options(
|
||||
asio::ssl::context::default_workarounds |
|
||||
asio::ssl::context::no_sslv2 | asio::ssl::context::no_sslv3 |
|
||||
asio::ssl::context::no_tlsv1 | asio::ssl::context::no_tlsv1_1);
|
||||
loadWindowsRootCertificates(sslContext_);
|
||||
sslContext_.set_verify_mode(asio::ssl::verify_peer);
|
||||
sslContext_.set_verify_callback(&logAndAcceptCertificate);
|
||||
#else
|
||||
if (useTls_) {
|
||||
std::cerr << "[network] wss:// requested but TLS support was not "
|
||||
"compiled in (NETWORK_TLS)" << std::endl;
|
||||
connectionFailed_.store(true);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
std::cout << "[network] Resolving " << scheme() << host_ << ':' << port_
|
||||
std::cout << "[network] Resolving ws://" << host_ << ':' << port_
|
||||
<< path_ << std::endl;
|
||||
resolve();
|
||||
thread_ = std::thread([this]() { io_.run(); });
|
||||
@@ -308,20 +184,6 @@ private:
|
||||
}
|
||||
|
||||
private:
|
||||
const char* scheme() const { return useTls_ ? "wss://" : "ws://"; }
|
||||
|
||||
template <class F>
|
||||
void withStream(F&& f)
|
||||
{
|
||||
#ifdef NETWORK_TLS
|
||||
if (useTls_) {
|
||||
f(secureSocket_);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
f(socket_);
|
||||
}
|
||||
|
||||
void resolve()
|
||||
{
|
||||
resolver_.async_resolve(
|
||||
@@ -330,97 +192,60 @@ private:
|
||||
if (error) return fail("resolve", error);
|
||||
std::cout << "[network] WebSocket host resolved; connecting"
|
||||
<< std::endl;
|
||||
withStream([&](auto& stream) {
|
||||
asio::async_connect(
|
||||
beast::get_lowest_layer(stream), results,
|
||||
[this](beast::error_code connectError,
|
||||
const tcp::endpoint&) {
|
||||
if (connectError) return fail("connect", connectError);
|
||||
std::cout << "[network] TCP connected" << std::endl;
|
||||
startTlsOrWebsocketHandshake();
|
||||
});
|
||||
});
|
||||
asio::async_connect(
|
||||
socket_.next_layer(), results,
|
||||
[this](beast::error_code connectError,
|
||||
const tcp::endpoint&) {
|
||||
if (connectError) return fail("connect", connectError);
|
||||
std::cout << "[network] TCP connected; starting WebSocket handshake"
|
||||
<< std::endl;
|
||||
socket_.set_option(
|
||||
websocket::stream_base::timeout::suggested(
|
||||
beast::role_type::client));
|
||||
socket_.async_handshake(
|
||||
host_, path_,
|
||||
[this](beast::error_code handshakeError) {
|
||||
if (handshakeError)
|
||||
return fail("handshake", handshakeError);
|
||||
connected_.store(true);
|
||||
std::cout
|
||||
<< "[network] Connected to ws://"
|
||||
<< host_ << ':' << port_ << path_ << std::endl;
|
||||
read();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void startTlsOrWebsocketHandshake()
|
||||
{
|
||||
#ifdef NETWORK_TLS
|
||||
if (useTls_) {
|
||||
SSL* ssl = secureSocket_.next_layer().native_handle();
|
||||
// SNI, and hostname verification (failures are only logged).
|
||||
SSL_set_tlsext_host_name(ssl, const_cast<char*>(host_.c_str()));
|
||||
SSL_set1_host(ssl, host_.c_str());
|
||||
std::cout << "[network] Starting TLS handshake" << std::endl;
|
||||
secureSocket_.next_layer().async_handshake(
|
||||
asio::ssl::stream_base::client,
|
||||
[this](beast::error_code tlsError) {
|
||||
if (tlsError) return fail("TLS handshake", tlsError);
|
||||
std::cout << "[network] TLS established" << std::endl;
|
||||
startWebsocketHandshake();
|
||||
});
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
startWebsocketHandshake();
|
||||
}
|
||||
|
||||
void startWebsocketHandshake()
|
||||
{
|
||||
const bool defaultPort = port_ == (useTls_ ? "443" : "80");
|
||||
const std::string hostHeader = defaultPort ? host_ : host_ + ':' + port_;
|
||||
std::cout << "[network] Starting WebSocket handshake" << std::endl;
|
||||
withStream([&](auto& stream) {
|
||||
stream.set_option(
|
||||
websocket::stream_base::timeout::suggested(
|
||||
beast::role_type::client));
|
||||
stream.async_handshake(
|
||||
hostHeader, path_,
|
||||
[this](beast::error_code handshakeError) {
|
||||
if (handshakeError)
|
||||
return fail("handshake", handshakeError);
|
||||
connected_.store(true);
|
||||
std::cout
|
||||
<< "[network] Connected to " << scheme()
|
||||
<< host_ << ':' << port_ << path_ << std::endl;
|
||||
read();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void read()
|
||||
{
|
||||
withStream([this](auto& stream) {
|
||||
stream.async_read(
|
||||
buffer_,
|
||||
[this](beast::error_code error, std::size_t) {
|
||||
if (error) {
|
||||
connected_.store(false);
|
||||
if (error != websocket::error::closed) fail("read", error);
|
||||
return;
|
||||
}
|
||||
std::string message = beast::buffers_to_string(buffer_.data());
|
||||
buffer_.consume(buffer_.size());
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(messagesMutex_);
|
||||
messages_.push_back(std::move(message));
|
||||
}
|
||||
read();
|
||||
});
|
||||
});
|
||||
socket_.async_read(
|
||||
buffer_,
|
||||
[this](beast::error_code error, std::size_t) {
|
||||
if (error) {
|
||||
connected_.store(false);
|
||||
if (error != websocket::error::closed) fail("read", error);
|
||||
return;
|
||||
}
|
||||
std::string message = beast::buffers_to_string(buffer_.data());
|
||||
buffer_.consume(buffer_.size());
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(messagesMutex_);
|
||||
messages_.push_back(std::move(message));
|
||||
}
|
||||
read();
|
||||
});
|
||||
}
|
||||
|
||||
void write()
|
||||
{
|
||||
withStream([this](auto& stream) {
|
||||
stream.async_write(
|
||||
asio::buffer(outbound_.front()),
|
||||
[this](beast::error_code error, std::size_t) {
|
||||
if (error) return fail("write", error);
|
||||
outbound_.pop_front();
|
||||
if (!outbound_.empty()) write();
|
||||
});
|
||||
});
|
||||
socket_.async_write(
|
||||
asio::buffer(outbound_.front()),
|
||||
[this](beast::error_code error, std::size_t) {
|
||||
if (error) return fail("write", error);
|
||||
outbound_.pop_front();
|
||||
if (!outbound_.empty()) write();
|
||||
});
|
||||
}
|
||||
|
||||
void fail(const char* operation, beast::error_code error)
|
||||
@@ -432,20 +257,13 @@ private:
|
||||
}
|
||||
|
||||
asio::io_context io_;
|
||||
#ifdef NETWORK_TLS
|
||||
asio::ssl::context sslContext_;
|
||||
#endif
|
||||
tcp::resolver resolver_;
|
||||
#ifdef NETWORK_TLS
|
||||
websocket::stream<beast::ssl_stream<tcp::socket>> secureSocket_;
|
||||
#endif
|
||||
websocket::stream<tcp::socket> socket_;
|
||||
beast::flat_buffer buffer_;
|
||||
std::thread thread_;
|
||||
std::string host_;
|
||||
std::string port_;
|
||||
std::string path_;
|
||||
bool useTls_ = false;
|
||||
std::atomic_bool connected_{false};
|
||||
std::atomic_bool connectionFailed_{false};
|
||||
std::mutex messagesMutex_;
|
||||
@@ -455,15 +273,9 @@ private:
|
||||
};
|
||||
|
||||
WebsocketGameClient::WebsocketGameClient(
|
||||
std::string host, std::string port, std::string path, bool useTls)
|
||||
std::string host, std::string port, std::string path)
|
||||
: impl_(std::make_unique<Impl>(
|
||||
std::move(host), std::move(port), std::move(path), useTls))
|
||||
{
|
||||
}
|
||||
|
||||
WebsocketGameClient::WebsocketGameClient(const WebsocketEndpoint& endpoint)
|
||||
: WebsocketGameClient(
|
||||
endpoint.host, endpoint.port, endpoint.path, endpoint.useTls)
|
||||
std::move(host), std::move(port), std::move(path)))
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -11,26 +11,12 @@ namespace FRG {
|
||||
|
||||
class RemoteGameServerHost;
|
||||
|
||||
// Parsed "ws://host[:port][/path]" or "wss://host[:port][/path]".
|
||||
struct WebsocketEndpoint {
|
||||
std::string host = "127.0.0.1";
|
||||
std::string port = "8080";
|
||||
std::string path = "/";
|
||||
bool useTls = false;
|
||||
|
||||
// Returns false (and leaves *this untouched) for an unsupported/malformed URL.
|
||||
// Default ports: 80 for ws://, 443 for wss://.
|
||||
bool parse(const std::string& url);
|
||||
};
|
||||
|
||||
class WebsocketGameClient final : public IGameServer {
|
||||
public:
|
||||
WebsocketGameClient(
|
||||
std::string host = "127.0.0.1",
|
||||
std::string port = "8080",
|
||||
std::string path = "/",
|
||||
bool useTls = false);
|
||||
explicit WebsocketGameClient(const WebsocketEndpoint& endpoint);
|
||||
std::string path = "/");
|
||||
~WebsocketGameClient();
|
||||
|
||||
WebsocketGameClient(const WebsocketGameClient&) = delete;
|
||||
|
||||
Reference in new issue
Block a user