Compare commits
5
Commits
main
..
networking2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0f1e0d66a3 | ||
|
|
63bf981e21 | ||
|
|
f3cafd2bf7 | ||
|
|
6b8aa0efec | ||
|
|
28fb81ad45 |
No files matched your search
+26
-6
@@ -41,7 +41,7 @@ configured player slot connects or disconnects.
|
||||
|
||||
`WebsocketGameClient` implements `IGameServer`.
|
||||
|
||||
- It connects asynchronously to `ws://127.0.0.1:8080/`.
|
||||
- It connects asynchronously to a `ws://` or `wss://` endpoint (`WebsocketEndpoint::parse`).
|
||||
- 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
|
||||
@@ -144,7 +144,15 @@ Networking is enabled for Windows and the Emscripten web client:
|
||||
- `proj-web/CMakeLists.txt` defines `NETWORK`, compiles the shared protocol,
|
||||
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.
|
||||
does not use Boost.Asio, Boost.Beast, or OpenSSL there. The browser performs
|
||||
the TLS handshake and certificate/hostname validation for `wss://`.
|
||||
- `proj-windows` 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 native `wss://` endpoints fail with a logged error.
|
||||
- Linux and Android builds do not define `NETWORK` and do not compile the
|
||||
network transport.
|
||||
|
||||
@@ -163,15 +171,24 @@ Run it from the repository root:
|
||||
.\server\build\Debug\GameServer.exe --campaign battle_zone_multiplayer --location uni_exterior
|
||||
```
|
||||
|
||||
The listener defaults to `0.0.0.0:8080`. It can be changed with:
|
||||
The listener defaults to `0.0.0.0:8080` and serves plain WebSocket traffic. It can be changed with:
|
||||
|
||||
```text
|
||||
--address IP
|
||||
--port N
|
||||
```
|
||||
|
||||
Launch the Windows game and select `battle_zone_multiplayer`. The client connects to
|
||||
`127.0.0.1:8080`. Selecting another campaign continues to use the local server.
|
||||
Launch the Windows game and select `battle_zone_multiplayer`. The production client
|
||||
default is `wss://api.shadowoverbishkek.com/server001/`; the endpoint is currently
|
||||
configured in `Game.cpp`. Selecting another campaign continues to use the local
|
||||
server.
|
||||
|
||||
For the Web build, serve the page over HTTPS and ensure the TLS-terminating reverse
|
||||
proxy presents a valid certificate for `api.shadowoverbishkek.com`, accepts the
|
||||
WebSocket upgrade at `/server001/`, and forwards the upgraded connection to the
|
||||
plain server listener. A page served over HTTPS cannot connect to a `ws://` endpoint
|
||||
because browsers block that mixed-content request. Certificate and hostname errors
|
||||
are enforced by the browser and cannot be bypassed by the C++ client.
|
||||
|
||||
## Verification
|
||||
|
||||
@@ -196,7 +213,10 @@ 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.
|
||||
- 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.
|
||||
- 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,3 +56,30 @@ 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()
|
||||
@@ -726,3 +726,105 @@ 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()
|
||||
@@ -0,0 +1,85 @@
|
||||
# Hostile NPC Behavior
|
||||
|
||||
## Authority and update order
|
||||
|
||||
Hostile NPC behavior is simulated by `LocationGameState` and
|
||||
`CharacterState`. Rendering objects do not select targets and do not change
|
||||
NPC combat state.
|
||||
|
||||
During each authoritative simulation tick:
|
||||
|
||||
1. The location gathers the enabled players and NPCs visible to the
|
||||
simulation.
|
||||
2. Each hostile NPC selects or retains a player target.
|
||||
3. Every character advances using the same tick-start observations.
|
||||
4. Movement reservations and attack damage are resolved deterministically.
|
||||
5. Navigation recovery events are applied by `InternalGameServer`.
|
||||
|
||||
The resulting state and recovery decisions are replicated through the existing
|
||||
WebSocket lockstep stream. NPC target changes are simulation consequences, not
|
||||
client commands.
|
||||
|
||||
## Eligible players and nearest selection
|
||||
|
||||
An eligible target must:
|
||||
|
||||
- exist in the campaign player map;
|
||||
- be enabled;
|
||||
- have positive health; and
|
||||
- be within the NPC's `visibleRange` when a new target is selected.
|
||||
|
||||
Distance is measured in the X/Z plane. If two players are equally close,
|
||||
their stable player IDs are compared lexicographically. This gives every
|
||||
lockstep replica the same result regardless of hash-map iteration order.
|
||||
|
||||
## Ordinary target locking
|
||||
|
||||
Once a hostile NPC has a valid target, it keeps that target during ordinary
|
||||
ticks. The target remains locked even if it moves outside `visibleRange`.
|
||||
The lock ends when the player dies or becomes disabled.
|
||||
|
||||
When a lock ends, the NPC selects the nearest eligible player in range. If no
|
||||
player qualifies, it clears the attack target, leaves combat state, and
|
||||
requests a route to its `homePosition`.
|
||||
|
||||
## Movement blockage and target reconsideration
|
||||
|
||||
NPC movement uses a retry schedule after a character blocks its path. The
|
||||
blocked NPC first stops and waits for its deterministic navigation retry
|
||||
cooldown. When the retry event emits `forceNavigationReplan`, the authoritative
|
||||
location state performs a fresh target selection across all eligible players.
|
||||
|
||||
This blocked-replan selection differs from ordinary target locking:
|
||||
|
||||
- the current target is not privileged;
|
||||
- the nearest eligible player may remain the target;
|
||||
- a nearer eligible player replaces the current target;
|
||||
- the route is rebuilt from the NPC's current position toward the selected
|
||||
player's current position.
|
||||
|
||||
If no eligible player is in range during blocked recovery, the NPC clears its
|
||||
stale chase target and returns toward its home position instead. The blocker is
|
||||
still passed to path planning so the immediate replan does not route through
|
||||
the character that caused the recovery event.
|
||||
|
||||
## Death, disconnect, and attack safety
|
||||
|
||||
Dead or disabled players are excluded from target acquisition. A target that
|
||||
dies or disconnects is therefore invalidated on the next authoritative
|
||||
selection pass, and the NPC either switches to another player or returns home.
|
||||
|
||||
Attack impacts are also validated at application time. Damage is ignored if
|
||||
the recorded target no longer exists, is disabled, or has already reached
|
||||
zero health. This prevents a queued animation impact from damaging a stale
|
||||
target.
|
||||
|
||||
## Regression expectations
|
||||
|
||||
The state and pathfinding tests should preserve these guarantees:
|
||||
|
||||
- nearest in-range player selection is deterministic;
|
||||
- a valid ordinary lock is retained;
|
||||
- blocked NPC recovery can replace the target and rebuild the chase route;
|
||||
- no-target blocked recovery clears combat and routes home;
|
||||
- dead and disabled players cannot be selected or damaged;
|
||||
- retry cooldowns and blocker identity remain deterministic.
|
||||
@@ -143,6 +143,7 @@ target_compile_definitions(ShadowOverBishkekDemo PRIVATE
|
||||
_WIN32_WINNT=0x0A00
|
||||
PNG_ENABLED
|
||||
NETWORK
|
||||
NETWORK_TLS
|
||||
)
|
||||
|
||||
# Линкуем с SDL2main, если он вообще установлен
|
||||
@@ -160,6 +161,7 @@ target_link_libraries(ShadowOverBishkekDemo PRIVATE
|
||||
boost_external_lib
|
||||
sol2_external_lib
|
||||
SDL2_mixer_external_lib
|
||||
openssl_ssl_external_lib
|
||||
)
|
||||
|
||||
# Линкуем OpenGL (Windows)
|
||||
@@ -172,6 +174,11 @@ if(WIN32)
|
||||
../src/game_server/RemoteGameServerHost.h
|
||||
../src/game_server/RemoteGameServerHost.cpp
|
||||
)
|
||||
if(MSVC)
|
||||
set_source_files_properties(
|
||||
../src/game_server/WebsocketGameClient.cpp
|
||||
PROPERTIES COMPILE_OPTIONS "/bigobj")
|
||||
endif()
|
||||
target_link_libraries(
|
||||
ShadowOverBishkekDemo PRIVATE opengl32 ws2_32 mswsock)
|
||||
endif()
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
"campaigns": [
|
||||
{
|
||||
"id": "main",
|
||||
"title": { "ru": "Основная игра", "en": "Main Game" },
|
||||
"title": {
|
||||
"ru": "Основная игра",
|
||||
"en": "Main Game"
|
||||
},
|
||||
"locations": [
|
||||
"resources/config/locations/location_dorm.json",
|
||||
"resources/config/locations/uni_exterior.json",
|
||||
@@ -17,12 +20,15 @@
|
||||
},
|
||||
{
|
||||
"id": "battle_zone",
|
||||
"title": { "ru": "Зона боя (одиночная)", "en": "Battle Zone (Single Player)" },
|
||||
"title": {
|
||||
"ru": "Зона боя (одиночная)",
|
||||
"en": "Battle Zone (Single Player)"
|
||||
},
|
||||
"locations": [
|
||||
"resources/config/locations/battle_zone_uni_exterior.json"
|
||||
"resources/config/locations/battle_zone_korea.json"
|
||||
],
|
||||
"startLocation": "uni_exterior",
|
||||
"startMusic": "music/bishkek univer day.ogg",
|
||||
"startLocation": "park_korea",
|
||||
"startMusic": "music/bishkek fight calm.ogg",
|
||||
"startState": "resources/config/start_state_battle_zone_singleplayer.json",
|
||||
"quests": "resources/config/quests_battle_zone.json",
|
||||
"tutorial": false,
|
||||
@@ -30,16 +36,24 @@
|
||||
},
|
||||
{
|
||||
"id": "battle_zone_multiplayer",
|
||||
"title": { "ru": "Зона боя (сетевая)", "en": "Battle Zone (Multiplayer)" },
|
||||
"title": {
|
||||
"ru": "Зона боя (сетевая)",
|
||||
"en": "Battle Zone (Multiplayer)"
|
||||
},
|
||||
"locations": [
|
||||
"resources/config/locations/battle_zone_uni_exterior.json"
|
||||
"resources/config/locations/battle_zone_korea.json"
|
||||
],
|
||||
"startLocation": "uni_exterior",
|
||||
"startMusic": "music/bishkek univer day.ogg",
|
||||
"startLocation": "park_korea",
|
||||
"startMusic": "music/bishkek fight calm.ogg",
|
||||
"startState": "resources/config/start_state_battle_zone.json",
|
||||
"quests": "resources/config/quests_battle_zone.json",
|
||||
"tutorial": false,
|
||||
"networkPlayerSlots": ["player", "player2"],
|
||||
"networkPlayerSlots": [
|
||||
"player1",
|
||||
"player2",
|
||||
"player3",
|
||||
"player4"
|
||||
],
|
||||
"networkCompatibility": "battle-zone-lockstep-v5",
|
||||
"loadingScreen": "resources/w/ui/screen_loading.json"
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"id": "uni_exterior",
|
||||
"id": "park_korea",
|
||||
"gameObjects": "resources/w/shelter/game_objects.json",
|
||||
"interactiveObjects": "resources/w/shelter/interactive_objects.json",
|
||||
"npcs": "resources/config/npcs_battle_zone.json",
|
||||
@@ -161,6 +161,168 @@
|
||||
"hp": 35,
|
||||
"visibleRange" : 6.5,
|
||||
"canAttack": true
|
||||
},
|
||||
{
|
||||
"id": "ghost_07x",
|
||||
"name": "Тень",
|
||||
"meshTextures": {
|
||||
"Shadow_NPC_Rigged": "resources/w/shadow/Shadow_BaseColor.png"
|
||||
},
|
||||
"animationIdlePath": "resources/w/shadow/shadow_idle003.anim",
|
||||
"animationWalkPath": "resources/w/shadow/shadow_idle003.anim",
|
||||
"animationActionIdlePath": "resources/w/shadow/shadow_idle003.anim",
|
||||
"animationActionAttackPath": "resources/w/shadow/shadow_attack003.anim",
|
||||
"animationStandToActionPath": "resources/w/shadow/shadow_idle003.anim",
|
||||
"animationActionToStandPath": "resources/w/shadow/shadow_idle003.anim",
|
||||
"animationActionToDeathPath": "resources/w/shadow/shadow_die003.anim",
|
||||
"animationDeathIdlePath": "resources/w/shadow/shadow_dead003.anim",
|
||||
"positionX": -5.6,
|
||||
"positionY": 0.0,
|
||||
"positionZ": -14.0,
|
||||
"walkSpeed": 1.5,
|
||||
"rotationSpeed": 8.0,
|
||||
"modelScale": 1.0,
|
||||
"modelCorrectionRotX": -90.0,
|
||||
"modelCorrectionRotY": 180.0,
|
||||
"modelCorrectionRotZ": 0.0,
|
||||
"hp": 35,
|
||||
"visibleRange" : 6.5,
|
||||
"canAttack": true
|
||||
},
|
||||
{
|
||||
"id": "ghost_08x",
|
||||
"name": "Тень",
|
||||
"meshTextures": {
|
||||
"Shadow_NPC_Rigged": "resources/w/shadow/Shadow_BaseColor.png"
|
||||
},
|
||||
"animationIdlePath": "resources/w/shadow/shadow_idle003.anim",
|
||||
"animationWalkPath": "resources/w/shadow/shadow_idle003.anim",
|
||||
"animationActionIdlePath": "resources/w/shadow/shadow_idle003.anim",
|
||||
"animationActionAttackPath": "resources/w/shadow/shadow_attack003.anim",
|
||||
"animationStandToActionPath": "resources/w/shadow/shadow_idle003.anim",
|
||||
"animationActionToStandPath": "resources/w/shadow/shadow_idle003.anim",
|
||||
"animationActionToDeathPath": "resources/w/shadow/shadow_die003.anim",
|
||||
"animationDeathIdlePath": "resources/w/shadow/shadow_dead003.anim",
|
||||
"positionX": -6.4,
|
||||
"positionY": 0.0,
|
||||
"positionZ": -14.0,
|
||||
"walkSpeed": 1.5,
|
||||
"rotationSpeed": 8.0,
|
||||
"modelScale": 1.0,
|
||||
"modelCorrectionRotX": -90.0,
|
||||
"modelCorrectionRotY": 180.0,
|
||||
"modelCorrectionRotZ": 0.0,
|
||||
"hp": 35,
|
||||
"visibleRange" : 6.5,
|
||||
"canAttack": true
|
||||
},
|
||||
{
|
||||
"id": "ghost_09x",
|
||||
"name": "Тень",
|
||||
"meshTextures": {
|
||||
"Shadow_NPC_Rigged": "resources/w/shadow/Shadow_BaseColor.png"
|
||||
},
|
||||
"animationIdlePath": "resources/w/shadow/shadow_idle003.anim",
|
||||
"animationWalkPath": "resources/w/shadow/shadow_idle003.anim",
|
||||
"animationActionIdlePath": "resources/w/shadow/shadow_idle003.anim",
|
||||
"animationActionAttackPath": "resources/w/shadow/shadow_attack003.anim",
|
||||
"animationStandToActionPath": "resources/w/shadow/shadow_idle003.anim",
|
||||
"animationActionToStandPath": "resources/w/shadow/shadow_idle003.anim",
|
||||
"animationActionToDeathPath": "resources/w/shadow/shadow_die003.anim",
|
||||
"animationDeathIdlePath": "resources/w/shadow/shadow_dead003.anim",
|
||||
"positionX": -5.6,
|
||||
"positionY": 0.0,
|
||||
"positionZ": -14.5,
|
||||
"walkSpeed": 1.5,
|
||||
"rotationSpeed": 8.0,
|
||||
"modelScale": 1.0,
|
||||
"modelCorrectionRotX": -90.0,
|
||||
"modelCorrectionRotY": 180.0,
|
||||
"modelCorrectionRotZ": 0.0,
|
||||
"hp": 35,
|
||||
"visibleRange" : 6.5,
|
||||
"canAttack": true
|
||||
},
|
||||
{
|
||||
"id": "ghost_10x",
|
||||
"name": "Тень",
|
||||
"meshTextures": {
|
||||
"Shadow_NPC_Rigged": "resources/w/shadow/Shadow_BaseColor.png"
|
||||
},
|
||||
"animationIdlePath": "resources/w/shadow/shadow_idle003.anim",
|
||||
"animationWalkPath": "resources/w/shadow/shadow_idle003.anim",
|
||||
"animationActionIdlePath": "resources/w/shadow/shadow_idle003.anim",
|
||||
"animationActionAttackPath": "resources/w/shadow/shadow_attack003.anim",
|
||||
"animationStandToActionPath": "resources/w/shadow/shadow_idle003.anim",
|
||||
"animationActionToStandPath": "resources/w/shadow/shadow_idle003.anim",
|
||||
"animationActionToDeathPath": "resources/w/shadow/shadow_die003.anim",
|
||||
"animationDeathIdlePath": "resources/w/shadow/shadow_dead003.anim",
|
||||
"positionX": -6.4,
|
||||
"positionY": 0.0,
|
||||
"positionZ": -14.5,
|
||||
"walkSpeed": 1.5,
|
||||
"rotationSpeed": 8.0,
|
||||
"modelScale": 1.0,
|
||||
"modelCorrectionRotX": -90.0,
|
||||
"modelCorrectionRotY": 180.0,
|
||||
"modelCorrectionRotZ": 0.0,
|
||||
"hp": 35,
|
||||
"visibleRange" : 6.5,
|
||||
"canAttack": true
|
||||
},
|
||||
{
|
||||
"id": "ghost_11x",
|
||||
"name": "Тень",
|
||||
"meshTextures": {
|
||||
"Shadow_NPC_Rigged": "resources/w/shadow/Shadow_BaseColor.png"
|
||||
},
|
||||
"animationIdlePath": "resources/w/shadow/shadow_idle003.anim",
|
||||
"animationWalkPath": "resources/w/shadow/shadow_idle003.anim",
|
||||
"animationActionIdlePath": "resources/w/shadow/shadow_idle003.anim",
|
||||
"animationActionAttackPath": "resources/w/shadow/shadow_attack003.anim",
|
||||
"animationStandToActionPath": "resources/w/shadow/shadow_idle003.anim",
|
||||
"animationActionToStandPath": "resources/w/shadow/shadow_idle003.anim",
|
||||
"animationActionToDeathPath": "resources/w/shadow/shadow_die003.anim",
|
||||
"animationDeathIdlePath": "resources/w/shadow/shadow_dead003.anim",
|
||||
"positionX": -5.6,
|
||||
"positionY": 0.0,
|
||||
"positionZ": -15.0,
|
||||
"walkSpeed": 1.5,
|
||||
"rotationSpeed": 8.0,
|
||||
"modelScale": 1.0,
|
||||
"modelCorrectionRotX": -90.0,
|
||||
"modelCorrectionRotY": 180.0,
|
||||
"modelCorrectionRotZ": 0.0,
|
||||
"hp": 35,
|
||||
"visibleRange" : 6.5,
|
||||
"canAttack": true
|
||||
},
|
||||
{
|
||||
"id": "ghost_12x",
|
||||
"name": "Тень",
|
||||
"meshTextures": {
|
||||
"Shadow_NPC_Rigged": "resources/w/shadow/Shadow_BaseColor.png"
|
||||
},
|
||||
"animationIdlePath": "resources/w/shadow/shadow_idle003.anim",
|
||||
"animationWalkPath": "resources/w/shadow/shadow_idle003.anim",
|
||||
"animationActionIdlePath": "resources/w/shadow/shadow_idle003.anim",
|
||||
"animationActionAttackPath": "resources/w/shadow/shadow_attack003.anim",
|
||||
"animationStandToActionPath": "resources/w/shadow/shadow_idle003.anim",
|
||||
"animationActionToStandPath": "resources/w/shadow/shadow_idle003.anim",
|
||||
"animationActionToDeathPath": "resources/w/shadow/shadow_die003.anim",
|
||||
"animationDeathIdlePath": "resources/w/shadow/shadow_dead003.anim",
|
||||
"positionX": -6.4,
|
||||
"positionY": 0.0,
|
||||
"positionZ": -15.0,
|
||||
"walkSpeed": 1.5,
|
||||
"rotationSpeed": 8.0,
|
||||
"modelScale": 1.0,
|
||||
"modelCorrectionRotX": -90.0,
|
||||
"modelCorrectionRotY": 180.0,
|
||||
"modelCorrectionRotZ": 0.0,
|
||||
"hp": 35,
|
||||
"visibleRange" : 6.5,
|
||||
"canAttack": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,4 +1,13 @@
|
||||
{
|
||||
"localState": {
|
||||
"locations": {
|
||||
"location_dorm": {
|
||||
"cameraAzimuth": -2.3499999046325684,
|
||||
"cameraInclination": 1.103600025177002
|
||||
}
|
||||
}
|
||||
},
|
||||
"networkState": {
|
||||
"campaignId": "main",
|
||||
"chatHistory": [
|
||||
[],
|
||||
@@ -16,8 +25,7 @@
|
||||
true
|
||||
],
|
||||
"currentLocationId": "",
|
||||
"globalFloats": {
|
||||
},
|
||||
"globalFloats": {},
|
||||
"globalInts": {},
|
||||
"inventory": {
|
||||
"items": []
|
||||
@@ -494,6 +502,22 @@
|
||||
-15.038200378417969
|
||||
],
|
||||
"hp": 100.0,
|
||||
"movedLastTick": false,
|
||||
"movementBlockedTicks": 0,
|
||||
"navigationRetryCooldownIndex": 0,
|
||||
"navigationRetryCooldownTicks": [
|
||||
10,
|
||||
4,
|
||||
5,
|
||||
8,
|
||||
11,
|
||||
9,
|
||||
6,
|
||||
3,
|
||||
2,
|
||||
7
|
||||
],
|
||||
"navigationRetryTicks": 0,
|
||||
"pathWaypoints": [
|
||||
[
|
||||
-6.296899795532227,
|
||||
@@ -530,8 +554,6 @@
|
||||
},
|
||||
"state": {
|
||||
"activeNavigationIndex": 0,
|
||||
"cameraAzimuth": -2.3499999046325684,
|
||||
"cameraInclination": 1.103600025177002,
|
||||
"targetInteractNpcId": "",
|
||||
"targetInteractiveObjectId": "",
|
||||
"targetTeleportZoneId": "",
|
||||
@@ -574,10 +596,9 @@
|
||||
}
|
||||
},
|
||||
"money": 5500,
|
||||
"playerHp": 200.0,
|
||||
"playerMaxHp": 200.0,
|
||||
"players": {
|
||||
"player": {
|
||||
"player1": {
|
||||
"animationFrame": 0.0,
|
||||
"attackTargetId": "",
|
||||
"attack_cooldown": 0.0,
|
||||
@@ -594,6 +615,11 @@
|
||||
0.0
|
||||
],
|
||||
"hp": 200.0,
|
||||
"movedLastTick": false,
|
||||
"movementBlockedTicks": 0,
|
||||
"navigationRetryCooldownIndex": 0,
|
||||
"navigationRetryCooldownTicks": [],
|
||||
"navigationRetryTicks": 0,
|
||||
"pathWaypoints": [
|
||||
[
|
||||
6.8031005859375,
|
||||
@@ -764,12 +790,14 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"savedAt": "2026-10-30 14:48",
|
||||
"schemaVersion": 1,
|
||||
"savedAt": "2026-11-10 20:38",
|
||||
"schemaVersion": 5,
|
||||
"taxiIsCalled": false,
|
||||
"tutorialJournalScreenOpened": false,
|
||||
"tutorialNeedOpenTaxiScreen": false,
|
||||
"tutorialPhoneChatScreenOpened": false,
|
||||
"tutorialStep": 0,
|
||||
"tutorialTaxiHintDone": false
|
||||
},
|
||||
"persistenceVersion": 1
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"localState": {
|
||||
"locations": {
|
||||
"uni_exterior": {
|
||||
"park_korea": {
|
||||
"cameraAzimuth": -2.3499999046325684,
|
||||
"cameraInclination": 1.103600025177002
|
||||
}
|
||||
@@ -10,9 +10,12 @@
|
||||
"networkState": {
|
||||
"campaignId": "battle_zone",
|
||||
"chatHistory": [
|
||||
[],
|
||||
[],
|
||||
[]
|
||||
[
|
||||
],
|
||||
[
|
||||
],
|
||||
[
|
||||
]
|
||||
],
|
||||
"chatPreviewMsg": [
|
||||
"",
|
||||
@@ -27,15 +30,17 @@
|
||||
"currentLocationId": "",
|
||||
"globalFloats": {
|
||||
},
|
||||
"globalInts": {},
|
||||
"globalInts": {
|
||||
},
|
||||
"inventory": {
|
||||
"items": []
|
||||
"items": [
|
||||
]
|
||||
},
|
||||
"isDarklands": true,
|
||||
"isDawn": false,
|
||||
"isNight": true,
|
||||
"locations": {
|
||||
"uni_exterior": {
|
||||
"park_korea": {
|
||||
"dialogue": {
|
||||
"active": false
|
||||
},
|
||||
@@ -44,7 +49,7 @@
|
||||
"alpha": 1.0,
|
||||
"isActive": true,
|
||||
"isAnimating": false,
|
||||
"positionX": 0.6000000238418579,
|
||||
"positionX": 0.60000002384185791,
|
||||
"positionY": 0.45736101269721985,
|
||||
"positionZ": 3.8499999046325684,
|
||||
"rotationY": 0.0,
|
||||
@@ -54,7 +59,7 @@
|
||||
"alpha": 1.0,
|
||||
"isActive": true,
|
||||
"isAnimating": false,
|
||||
"positionX": 1.399999976158142,
|
||||
"positionX": 1.3999999761581421,
|
||||
"positionY": 0.45736101269721985,
|
||||
"positionZ": 3.8499999046325684,
|
||||
"rotationY": 0.0,
|
||||
@@ -64,7 +69,7 @@
|
||||
"alpha": 1.0,
|
||||
"isActive": true,
|
||||
"isAnimating": false,
|
||||
"positionX": 2.200000047683716,
|
||||
"positionX": 2.2000000476837158,
|
||||
"positionY": 0.45736101269721985,
|
||||
"positionZ": 3.8499999046325684,
|
||||
"rotationY": 0.0,
|
||||
@@ -85,7 +90,7 @@
|
||||
"isActive": true,
|
||||
"isAnimating": false,
|
||||
"positionX": 0.0,
|
||||
"positionY": 3.200000047683716,
|
||||
"positionY": 3.2000000476837158,
|
||||
"positionZ": 0.0,
|
||||
"rotationY": 0.0,
|
||||
"scale": 1.0
|
||||
@@ -95,7 +100,7 @@
|
||||
"isActive": true,
|
||||
"isAnimating": false,
|
||||
"positionX": -1.0,
|
||||
"positionY": 0.9150000214576721,
|
||||
"positionY": 0.91500002145767212,
|
||||
"positionZ": -3.9000000953674316,
|
||||
"rotationY": 0.0,
|
||||
"scale": 1.0
|
||||
@@ -105,7 +110,7 @@
|
||||
"isActive": true,
|
||||
"isAnimating": false,
|
||||
"positionX": 0.0,
|
||||
"positionY": 0.9150000214576721,
|
||||
"positionY": 0.91500002145767212,
|
||||
"positionZ": -3.9000000953674316,
|
||||
"rotationY": 0.0,
|
||||
"scale": 1.0
|
||||
@@ -115,7 +120,7 @@
|
||||
"isActive": true,
|
||||
"isAnimating": false,
|
||||
"positionX": 1.0,
|
||||
"positionY": 0.9150000214576721,
|
||||
"positionY": 0.91500002145767212,
|
||||
"positionZ": -3.9000000953674316,
|
||||
"rotationY": 0.0,
|
||||
"scale": 1.0
|
||||
@@ -134,7 +139,7 @@
|
||||
"facingAngle": 0.0,
|
||||
"homeDriftCheckTimer": 5.0,
|
||||
"homePosition": [
|
||||
-5.599999904632568,
|
||||
-5.5999999046325684,
|
||||
0.0,
|
||||
-12.0
|
||||
],
|
||||
@@ -155,14 +160,15 @@
|
||||
11
|
||||
],
|
||||
"navigationRetryTicks": 0,
|
||||
"pathWaypoints": [],
|
||||
"pathWaypoints": [
|
||||
],
|
||||
"position": [
|
||||
-5.599999904632568,
|
||||
-5.5999999046325684,
|
||||
0.0,
|
||||
-12.0
|
||||
],
|
||||
"requestedWalkTarget": [
|
||||
-5.599999904632568,
|
||||
-5.5999999046325684,
|
||||
0.0,
|
||||
-12.0
|
||||
],
|
||||
@@ -170,7 +176,7 @@
|
||||
"targetFacingAngle": 0.0,
|
||||
"visibleRange": 6.5,
|
||||
"walkTarget": [
|
||||
-5.599999904632568,
|
||||
-5.5999999046325684,
|
||||
0.0,
|
||||
-12.0
|
||||
]
|
||||
@@ -187,7 +193,7 @@
|
||||
"facingAngle": 0.0,
|
||||
"homeDriftCheckTimer": 5.0,
|
||||
"homePosition": [
|
||||
-6.400000095367432,
|
||||
-6.4000000953674316,
|
||||
0.0,
|
||||
-12.0
|
||||
],
|
||||
@@ -208,14 +214,15 @@
|
||||
6
|
||||
],
|
||||
"navigationRetryTicks": 0,
|
||||
"pathWaypoints": [],
|
||||
"pathWaypoints": [
|
||||
],
|
||||
"position": [
|
||||
-6.400000095367432,
|
||||
-6.4000000953674316,
|
||||
0.0,
|
||||
-12.0
|
||||
],
|
||||
"requestedWalkTarget": [
|
||||
-6.400000095367432,
|
||||
-6.4000000953674316,
|
||||
0.0,
|
||||
-12.0
|
||||
],
|
||||
@@ -223,7 +230,7 @@
|
||||
"targetFacingAngle": 0.0,
|
||||
"visibleRange": 6.5,
|
||||
"walkTarget": [
|
||||
-6.400000095367432,
|
||||
-6.4000000953674316,
|
||||
0.0,
|
||||
-12.0
|
||||
]
|
||||
@@ -240,7 +247,7 @@
|
||||
"facingAngle": 0.0,
|
||||
"homeDriftCheckTimer": 5.0,
|
||||
"homePosition": [
|
||||
-5.599999904632568,
|
||||
-5.5999999046325684,
|
||||
0.0,
|
||||
-12.5
|
||||
],
|
||||
@@ -261,14 +268,15 @@
|
||||
11
|
||||
],
|
||||
"navigationRetryTicks": 0,
|
||||
"pathWaypoints": [],
|
||||
"pathWaypoints": [
|
||||
],
|
||||
"position": [
|
||||
-5.599999904632568,
|
||||
-5.5999999046325684,
|
||||
0.0,
|
||||
-12.5
|
||||
],
|
||||
"requestedWalkTarget": [
|
||||
-5.599999904632568,
|
||||
-5.5999999046325684,
|
||||
0.0,
|
||||
-12.5
|
||||
],
|
||||
@@ -276,7 +284,7 @@
|
||||
"targetFacingAngle": 0.0,
|
||||
"visibleRange": 6.5,
|
||||
"walkTarget": [
|
||||
-5.599999904632568,
|
||||
-5.5999999046325684,
|
||||
0.0,
|
||||
-12.5
|
||||
]
|
||||
@@ -293,7 +301,7 @@
|
||||
"facingAngle": 0.0,
|
||||
"homeDriftCheckTimer": 5.0,
|
||||
"homePosition": [
|
||||
-6.400000095367432,
|
||||
-6.4000000953674316,
|
||||
0.0,
|
||||
-12.5
|
||||
],
|
||||
@@ -314,14 +322,15 @@
|
||||
6
|
||||
],
|
||||
"navigationRetryTicks": 0,
|
||||
"pathWaypoints": [],
|
||||
"pathWaypoints": [
|
||||
],
|
||||
"position": [
|
||||
-6.400000095367432,
|
||||
-6.4000000953674316,
|
||||
0.0,
|
||||
-12.5
|
||||
],
|
||||
"requestedWalkTarget": [
|
||||
-6.400000095367432,
|
||||
-6.4000000953674316,
|
||||
0.0,
|
||||
-12.5
|
||||
],
|
||||
@@ -329,7 +338,7 @@
|
||||
"targetFacingAngle": 0.0,
|
||||
"visibleRange": 6.5,
|
||||
"walkTarget": [
|
||||
-6.400000095367432,
|
||||
-6.4000000953674316,
|
||||
0.0,
|
||||
-12.5
|
||||
]
|
||||
@@ -346,7 +355,7 @@
|
||||
"facingAngle": 0.0,
|
||||
"homeDriftCheckTimer": 5.0,
|
||||
"homePosition": [
|
||||
-5.599999904632568,
|
||||
-5.5999999046325684,
|
||||
0.0,
|
||||
-13.0
|
||||
],
|
||||
@@ -367,14 +376,15 @@
|
||||
11
|
||||
],
|
||||
"navigationRetryTicks": 0,
|
||||
"pathWaypoints": [],
|
||||
"pathWaypoints": [
|
||||
],
|
||||
"position": [
|
||||
-5.599999904632568,
|
||||
-5.5999999046325684,
|
||||
0.0,
|
||||
-13.0
|
||||
],
|
||||
"requestedWalkTarget": [
|
||||
-5.599999904632568,
|
||||
-5.5999999046325684,
|
||||
0.0,
|
||||
-13.0
|
||||
],
|
||||
@@ -382,7 +392,7 @@
|
||||
"targetFacingAngle": 0.0,
|
||||
"visibleRange": 6.5,
|
||||
"walkTarget": [
|
||||
-5.599999904632568,
|
||||
-5.5999999046325684,
|
||||
0.0,
|
||||
-13.0
|
||||
]
|
||||
@@ -399,7 +409,7 @@
|
||||
"facingAngle": 0.0,
|
||||
"homeDriftCheckTimer": 5.0,
|
||||
"homePosition": [
|
||||
-6.400000095367432,
|
||||
-6.4000000953674316,
|
||||
0.0,
|
||||
-13.0
|
||||
],
|
||||
@@ -420,14 +430,15 @@
|
||||
2
|
||||
],
|
||||
"navigationRetryTicks": 0,
|
||||
"pathWaypoints": [],
|
||||
"pathWaypoints": [
|
||||
],
|
||||
"position": [
|
||||
-6.400000095367432,
|
||||
-6.4000000953674316,
|
||||
0.0,
|
||||
-13.0
|
||||
],
|
||||
"requestedWalkTarget": [
|
||||
-6.400000095367432,
|
||||
-6.4000000953674316,
|
||||
0.0,
|
||||
-13.0
|
||||
],
|
||||
@@ -435,13 +446,338 @@
|
||||
"targetFacingAngle": 0.0,
|
||||
"visibleRange": 6.5,
|
||||
"walkTarget": [
|
||||
-6.400000095367432,
|
||||
-6.4000000953674316,
|
||||
0.0,
|
||||
-13.0
|
||||
]
|
||||
},
|
||||
"ghost_07x": {
|
||||
"animationFrame": 0.0,
|
||||
"attackTargetId": "",
|
||||
"attack_cooldown": 0.0,
|
||||
"battle_state": 0,
|
||||
"currentState": 0,
|
||||
"currentWaypointIndex": 0,
|
||||
"enabled": true,
|
||||
"faceTargetId": "",
|
||||
"facingAngle": 0.0,
|
||||
"homeDriftCheckTimer": 5.0,
|
||||
"homePosition": [
|
||||
-5.5999999046325684,
|
||||
0.0,
|
||||
-14.0
|
||||
],
|
||||
"hp": 35.0,
|
||||
"movedLastTick": false,
|
||||
"movementBlockedTicks": 0,
|
||||
"navigationRetryCooldownIndex": 0,
|
||||
"navigationRetryCooldownTicks": [
|
||||
2,
|
||||
3,
|
||||
10,
|
||||
5,
|
||||
6,
|
||||
8,
|
||||
7,
|
||||
9,
|
||||
4,
|
||||
11
|
||||
],
|
||||
"navigationRetryTicks": 0,
|
||||
"pathWaypoints": [
|
||||
],
|
||||
"position": [
|
||||
-5.5999999046325684,
|
||||
0.0,
|
||||
-14.0
|
||||
],
|
||||
"requestedWalkTarget": [
|
||||
-5.5999999046325684,
|
||||
0.0,
|
||||
-14.0
|
||||
],
|
||||
"showWeapon": true,
|
||||
"targetFacingAngle": 0.0,
|
||||
"visibleRange": 6.5,
|
||||
"walkTarget": [
|
||||
-5.5999999046325684,
|
||||
0.0,
|
||||
-14.0
|
||||
]
|
||||
},
|
||||
"ghost_08x": {
|
||||
"animationFrame": 0.0,
|
||||
"attackTargetId": "",
|
||||
"attack_cooldown": 0.0,
|
||||
"battle_state": 0,
|
||||
"currentState": 0,
|
||||
"currentWaypointIndex": 0,
|
||||
"enabled": true,
|
||||
"faceTargetId": "",
|
||||
"facingAngle": 0.0,
|
||||
"homeDriftCheckTimer": 5.0,
|
||||
"homePosition": [
|
||||
-6.4000000953674316,
|
||||
0.0,
|
||||
-14.0
|
||||
],
|
||||
"hp": 35.0,
|
||||
"movedLastTick": false,
|
||||
"movementBlockedTicks": 0,
|
||||
"navigationRetryCooldownIndex": 0,
|
||||
"navigationRetryCooldownTicks": [
|
||||
8,
|
||||
11,
|
||||
3,
|
||||
10,
|
||||
2,
|
||||
9,
|
||||
5,
|
||||
4,
|
||||
7,
|
||||
6
|
||||
],
|
||||
"navigationRetryTicks": 0,
|
||||
"pathWaypoints": [
|
||||
],
|
||||
"position": [
|
||||
-6.4000000953674316,
|
||||
0.0,
|
||||
-14.0
|
||||
],
|
||||
"requestedWalkTarget": [
|
||||
-6.4000000953674316,
|
||||
0.0,
|
||||
-14.0
|
||||
],
|
||||
"showWeapon": true,
|
||||
"targetFacingAngle": 0.0,
|
||||
"visibleRange": 6.5,
|
||||
"walkTarget": [
|
||||
-6.4000000953674316,
|
||||
0.0,
|
||||
-14.0
|
||||
]
|
||||
},
|
||||
"ghost_09x": {
|
||||
"animationFrame": 0.0,
|
||||
"attackTargetId": "",
|
||||
"attack_cooldown": 0.0,
|
||||
"battle_state": 0,
|
||||
"currentState": 0,
|
||||
"currentWaypointIndex": 0,
|
||||
"enabled": true,
|
||||
"faceTargetId": "",
|
||||
"facingAngle": 0.0,
|
||||
"homeDriftCheckTimer": 5.0,
|
||||
"homePosition": [
|
||||
-5.5999999046325684,
|
||||
0.0,
|
||||
-14.5
|
||||
],
|
||||
"hp": 35.0,
|
||||
"movedLastTick": false,
|
||||
"movementBlockedTicks": 0,
|
||||
"navigationRetryCooldownIndex": 0,
|
||||
"navigationRetryCooldownTicks": [
|
||||
2,
|
||||
4,
|
||||
8,
|
||||
3,
|
||||
6,
|
||||
10,
|
||||
9,
|
||||
7,
|
||||
5,
|
||||
11
|
||||
],
|
||||
"navigationRetryTicks": 0,
|
||||
"pathWaypoints": [
|
||||
],
|
||||
"position": [
|
||||
-5.5999999046325684,
|
||||
0.0,
|
||||
-14.5
|
||||
],
|
||||
"requestedWalkTarget": [
|
||||
-5.5999999046325684,
|
||||
0.0,
|
||||
-14.5
|
||||
],
|
||||
"showWeapon": true,
|
||||
"targetFacingAngle": 0.0,
|
||||
"visibleRange": 6.5,
|
||||
"walkTarget": [
|
||||
-5.5999999046325684,
|
||||
0.0,
|
||||
-14.5
|
||||
]
|
||||
},
|
||||
"ghost_10x": {
|
||||
"animationFrame": 0.0,
|
||||
"attackTargetId": "",
|
||||
"attack_cooldown": 0.0,
|
||||
"battle_state": 0,
|
||||
"currentState": 0,
|
||||
"currentWaypointIndex": 0,
|
||||
"enabled": true,
|
||||
"faceTargetId": "",
|
||||
"facingAngle": 0.0,
|
||||
"homeDriftCheckTimer": 5.0,
|
||||
"homePosition": [
|
||||
-6.4000000953674316,
|
||||
0.0,
|
||||
-14.5
|
||||
],
|
||||
"hp": 35.0,
|
||||
"movedLastTick": false,
|
||||
"movementBlockedTicks": 0,
|
||||
"navigationRetryCooldownIndex": 0,
|
||||
"navigationRetryCooldownTicks": [
|
||||
3,
|
||||
4,
|
||||
7,
|
||||
10,
|
||||
5,
|
||||
11,
|
||||
9,
|
||||
2,
|
||||
8,
|
||||
6
|
||||
],
|
||||
"navigationRetryTicks": 0,
|
||||
"pathWaypoints": [
|
||||
],
|
||||
"position": [
|
||||
-6.4000000953674316,
|
||||
0.0,
|
||||
-14.5
|
||||
],
|
||||
"requestedWalkTarget": [
|
||||
-6.4000000953674316,
|
||||
0.0,
|
||||
-12.5
|
||||
],
|
||||
"showWeapon": true,
|
||||
"targetFacingAngle": 0.0,
|
||||
"visibleRange": 6.5,
|
||||
"walkTarget": [
|
||||
-6.4000000953674316,
|
||||
0.0,
|
||||
-14.5
|
||||
]
|
||||
},
|
||||
"ghost_11x": {
|
||||
"animationFrame": 0.0,
|
||||
"attackTargetId": "",
|
||||
"attack_cooldown": 0.0,
|
||||
"battle_state": 0,
|
||||
"currentState": 0,
|
||||
"currentWaypointIndex": 0,
|
||||
"enabled": true,
|
||||
"faceTargetId": "",
|
||||
"facingAngle": 0.0,
|
||||
"homeDriftCheckTimer": 5.0,
|
||||
"homePosition": [
|
||||
-5.5999999046325684,
|
||||
0.0,
|
||||
-15.0
|
||||
],
|
||||
"hp": 35.0,
|
||||
"movedLastTick": false,
|
||||
"movementBlockedTicks": 0,
|
||||
"navigationRetryCooldownIndex": 0,
|
||||
"navigationRetryCooldownTicks": [
|
||||
8,
|
||||
7,
|
||||
2,
|
||||
9,
|
||||
4,
|
||||
10,
|
||||
6,
|
||||
5,
|
||||
3,
|
||||
11
|
||||
],
|
||||
"navigationRetryTicks": 0,
|
||||
"pathWaypoints": [
|
||||
],
|
||||
"position": [
|
||||
-5.5999999046325684,
|
||||
0.0,
|
||||
-15.0
|
||||
],
|
||||
"requestedWalkTarget": [
|
||||
-5.5999999046325684,
|
||||
0.0,
|
||||
-15.0
|
||||
],
|
||||
"showWeapon": true,
|
||||
"targetFacingAngle": 0.0,
|
||||
"visibleRange": 6.5,
|
||||
"walkTarget": [
|
||||
-5.5999999046325684,
|
||||
0.0,
|
||||
-15.0
|
||||
]
|
||||
},
|
||||
"ghost_12x": {
|
||||
"animationFrame": 0.0,
|
||||
"attackTargetId": "",
|
||||
"attack_cooldown": 0.0,
|
||||
"battle_state": 0,
|
||||
"currentState": 0,
|
||||
"currentWaypointIndex": 0,
|
||||
"enabled": true,
|
||||
"faceTargetId": "",
|
||||
"facingAngle": 0.0,
|
||||
"homeDriftCheckTimer": 5.0,
|
||||
"homePosition": [
|
||||
-6.4000000953674316,
|
||||
0.0,
|
||||
-15.0
|
||||
],
|
||||
"hp": 35.0,
|
||||
"movedLastTick": false,
|
||||
"movementBlockedTicks": 0,
|
||||
"navigationRetryCooldownIndex": 0,
|
||||
"navigationRetryCooldownTicks": [
|
||||
7,
|
||||
11,
|
||||
5,
|
||||
4,
|
||||
3,
|
||||
10,
|
||||
9,
|
||||
8,
|
||||
6,
|
||||
2
|
||||
],
|
||||
"navigationRetryTicks": 0,
|
||||
"pathWaypoints": [
|
||||
],
|
||||
"position": [
|
||||
-6.4000000953674316,
|
||||
0.0,
|
||||
-15.0
|
||||
],
|
||||
"requestedWalkTarget": [
|
||||
-6.4000000953674316,
|
||||
0.0,
|
||||
-15.0
|
||||
],
|
||||
"showWeapon": true,
|
||||
"targetFacingAngle": 0.0,
|
||||
"visibleRange": 6.5,
|
||||
"walkTarget": [
|
||||
-6.4000000953674316,
|
||||
0.0,
|
||||
-15.0
|
||||
]
|
||||
}
|
||||
},
|
||||
"scriptGlobals": {},
|
||||
"scriptGlobals": {
|
||||
},
|
||||
"state": {
|
||||
"activeNavigationIndex": 0,
|
||||
"targetInteractNpcId": "",
|
||||
@@ -460,61 +796,23 @@
|
||||
"rotation": 0.0,
|
||||
"steeringAngle": 0.0,
|
||||
"velocity": 0.0,
|
||||
"waypoints": [],
|
||||
"waypoints": [
|
||||
],
|
||||
"wheelAngle": 0.0
|
||||
}
|
||||
}
|
||||
},
|
||||
"teleportZones": {},
|
||||
"triggerZones": {}
|
||||
"teleportZones": {
|
||||
},
|
||||
"triggerZones": {
|
||||
}
|
||||
}
|
||||
},
|
||||
"money": 5500,
|
||||
"playerHp": 200.0,
|
||||
"playerMaxHp": 200.0,
|
||||
"players": {
|
||||
"player2": {
|
||||
"animationFrame": 0.0,
|
||||
"attackTargetId": "",
|
||||
"attack_cooldown": 0.0,
|
||||
"battle_state": 0,
|
||||
"currentState": 0,
|
||||
"currentWaypointIndex": 0,
|
||||
"enabled": false,
|
||||
"faceTargetId": "",
|
||||
"facingAngle": 0.0,
|
||||
"homeDriftCheckTimer": 5.0,
|
||||
"homePosition": [
|
||||
0.0,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"hp": 200.0,
|
||||
"pathWaypoints": [[
|
||||
-6.4,
|
||||
0.0,
|
||||
13.0
|
||||
]],
|
||||
"position": [
|
||||
-6.4,
|
||||
0.0,
|
||||
13.0
|
||||
],
|
||||
"requestedWalkTarget": [
|
||||
-6.4,
|
||||
0.0,
|
||||
13.0
|
||||
],
|
||||
"showWeapon": true,
|
||||
"targetFacingAngle": 0.0,
|
||||
"visibleRange": 6.0,
|
||||
"walkTarget": [
|
||||
-6.4,
|
||||
0.0,
|
||||
13.0
|
||||
]
|
||||
},
|
||||
"player": {
|
||||
"player1": {
|
||||
"animationFrame": 0.0,
|
||||
"attackTargetId": "",
|
||||
"attack_cooldown": 0.0,
|
||||
@@ -533,18 +831,18 @@
|
||||
"hp": 200.0,
|
||||
"pathWaypoints": [
|
||||
[
|
||||
-5.8,
|
||||
-5.4,
|
||||
0.0,
|
||||
12.8
|
||||
]
|
||||
],
|
||||
"position": [
|
||||
-5.8,
|
||||
-5.4,
|
||||
0.0,
|
||||
12.8
|
||||
],
|
||||
"requestedWalkTarget": [
|
||||
-5.8,
|
||||
-5.4,
|
||||
0.0,
|
||||
12.8
|
||||
],
|
||||
@@ -552,14 +850,144 @@
|
||||
"targetFacingAngle": 0.0,
|
||||
"visibleRange": 6.0,
|
||||
"walkTarget": [
|
||||
-5.8,
|
||||
-5.4,
|
||||
0.0,
|
||||
12.8
|
||||
]
|
||||
},
|
||||
"player2": {
|
||||
"animationFrame": 0.0,
|
||||
"attackTargetId": "",
|
||||
"attack_cooldown": 0.0,
|
||||
"battle_state": 0,
|
||||
"currentState": 0,
|
||||
"currentWaypointIndex": 0,
|
||||
"enabled": false,
|
||||
"faceTargetId": "",
|
||||
"facingAngle": 0.0,
|
||||
"homeDriftCheckTimer": 5.0,
|
||||
"homePosition": [
|
||||
0.0,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"hp": 200.0,
|
||||
"pathWaypoints": [
|
||||
[
|
||||
-6.8,
|
||||
0.0,
|
||||
13.0
|
||||
]
|
||||
],
|
||||
"position": [
|
||||
-6.8,
|
||||
0.0,
|
||||
13.0
|
||||
],
|
||||
"requestedWalkTarget": [
|
||||
-6.8,
|
||||
0.0,
|
||||
13.0
|
||||
],
|
||||
"showWeapon": true,
|
||||
"targetFacingAngle": 0.0,
|
||||
"visibleRange": 6.0,
|
||||
"walkTarget": [
|
||||
-6.8,
|
||||
0.0,
|
||||
13.0
|
||||
]
|
||||
},
|
||||
"player3": {
|
||||
"animationFrame": 0.0,
|
||||
"attackTargetId": "",
|
||||
"attack_cooldown": 0.0,
|
||||
"battle_state": 0,
|
||||
"currentState": 0,
|
||||
"currentWaypointIndex": 0,
|
||||
"enabled": false,
|
||||
"faceTargetId": "",
|
||||
"facingAngle": 0.0,
|
||||
"homeDriftCheckTimer": 5.0,
|
||||
"homePosition": [
|
||||
0.0,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"hp": 200.0,
|
||||
"pathWaypoints": [
|
||||
[
|
||||
-5.4,
|
||||
0.0,
|
||||
14.8
|
||||
]
|
||||
],
|
||||
"position": [
|
||||
-5.4,
|
||||
0.0,
|
||||
14.8
|
||||
],
|
||||
"requestedWalkTarget": [
|
||||
-5.4,
|
||||
0.0,
|
||||
14.8
|
||||
],
|
||||
"showWeapon": true,
|
||||
"targetFacingAngle": 0.0,
|
||||
"visibleRange": 6.0,
|
||||
"walkTarget": [
|
||||
-5.4,
|
||||
0.0,
|
||||
14.8
|
||||
]
|
||||
},
|
||||
"player4": {
|
||||
"animationFrame": 0.0,
|
||||
"attackTargetId": "",
|
||||
"attack_cooldown": 0.0,
|
||||
"battle_state": 0,
|
||||
"currentState": 0,
|
||||
"currentWaypointIndex": 0,
|
||||
"enabled": false,
|
||||
"faceTargetId": "",
|
||||
"facingAngle": 0.0,
|
||||
"homeDriftCheckTimer": 5.0,
|
||||
"homePosition": [
|
||||
0.0,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"hp": 200.0,
|
||||
"pathWaypoints": [
|
||||
[
|
||||
-6.8,
|
||||
0.0,
|
||||
15.0
|
||||
]
|
||||
],
|
||||
"position": [
|
||||
-6.8,
|
||||
0.0,
|
||||
15.0
|
||||
],
|
||||
"requestedWalkTarget": [
|
||||
-6.8,
|
||||
0.0,
|
||||
15.0
|
||||
],
|
||||
"showWeapon": true,
|
||||
"targetFacingAngle": 0.0,
|
||||
"visibleRange": 6.0,
|
||||
"walkTarget": [
|
||||
-6.8,
|
||||
0.0,
|
||||
15.0
|
||||
]
|
||||
}
|
||||
},
|
||||
"questJournal": {
|
||||
"quests": []
|
||||
"quests": [
|
||||
]
|
||||
},
|
||||
"savedAt": "2026-11-05 17:43",
|
||||
"schemaVersion": 5,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"localState": {
|
||||
"locations": {
|
||||
"uni_exterior": {
|
||||
"park_korea": {
|
||||
"cameraAzimuth": -2.3499999046325684,
|
||||
"cameraInclination": 1.103600025177002
|
||||
}
|
||||
@@ -10,9 +10,12 @@
|
||||
"networkState": {
|
||||
"campaignId": "battle_zone",
|
||||
"chatHistory": [
|
||||
[],
|
||||
[],
|
||||
[]
|
||||
[
|
||||
],
|
||||
[
|
||||
],
|
||||
[
|
||||
]
|
||||
],
|
||||
"chatPreviewMsg": [
|
||||
"",
|
||||
@@ -27,15 +30,17 @@
|
||||
"currentLocationId": "",
|
||||
"globalFloats": {
|
||||
},
|
||||
"globalInts": {},
|
||||
"globalInts": {
|
||||
},
|
||||
"inventory": {
|
||||
"items": []
|
||||
"items": [
|
||||
]
|
||||
},
|
||||
"isDarklands": true,
|
||||
"isDawn": false,
|
||||
"isNight": true,
|
||||
"locations": {
|
||||
"uni_exterior": {
|
||||
"park_korea": {
|
||||
"dialogue": {
|
||||
"active": false
|
||||
},
|
||||
@@ -44,7 +49,7 @@
|
||||
"alpha": 1.0,
|
||||
"isActive": true,
|
||||
"isAnimating": false,
|
||||
"positionX": 0.6000000238418579,
|
||||
"positionX": 0.60000002384185791,
|
||||
"positionY": 0.45736101269721985,
|
||||
"positionZ": 3.8499999046325684,
|
||||
"rotationY": 0.0,
|
||||
@@ -54,7 +59,7 @@
|
||||
"alpha": 1.0,
|
||||
"isActive": true,
|
||||
"isAnimating": false,
|
||||
"positionX": 1.399999976158142,
|
||||
"positionX": 1.3999999761581421,
|
||||
"positionY": 0.45736101269721985,
|
||||
"positionZ": 3.8499999046325684,
|
||||
"rotationY": 0.0,
|
||||
@@ -64,7 +69,7 @@
|
||||
"alpha": 1.0,
|
||||
"isActive": true,
|
||||
"isAnimating": false,
|
||||
"positionX": 2.200000047683716,
|
||||
"positionX": 2.2000000476837158,
|
||||
"positionY": 0.45736101269721985,
|
||||
"positionZ": 3.8499999046325684,
|
||||
"rotationY": 0.0,
|
||||
@@ -85,7 +90,7 @@
|
||||
"isActive": true,
|
||||
"isAnimating": false,
|
||||
"positionX": 0.0,
|
||||
"positionY": 3.200000047683716,
|
||||
"positionY": 3.2000000476837158,
|
||||
"positionZ": 0.0,
|
||||
"rotationY": 0.0,
|
||||
"scale": 1.0
|
||||
@@ -95,7 +100,7 @@
|
||||
"isActive": true,
|
||||
"isAnimating": false,
|
||||
"positionX": -1.0,
|
||||
"positionY": 0.9150000214576721,
|
||||
"positionY": 0.91500002145767212,
|
||||
"positionZ": -3.9000000953674316,
|
||||
"rotationY": 0.0,
|
||||
"scale": 1.0
|
||||
@@ -105,7 +110,7 @@
|
||||
"isActive": true,
|
||||
"isAnimating": false,
|
||||
"positionX": 0.0,
|
||||
"positionY": 0.9150000214576721,
|
||||
"positionY": 0.91500002145767212,
|
||||
"positionZ": -3.9000000953674316,
|
||||
"rotationY": 0.0,
|
||||
"scale": 1.0
|
||||
@@ -115,7 +120,7 @@
|
||||
"isActive": true,
|
||||
"isAnimating": false,
|
||||
"positionX": 1.0,
|
||||
"positionY": 0.9150000214576721,
|
||||
"positionY": 0.91500002145767212,
|
||||
"positionZ": -3.9000000953674316,
|
||||
"rotationY": 0.0,
|
||||
"scale": 1.0
|
||||
@@ -134,7 +139,7 @@
|
||||
"facingAngle": 0.0,
|
||||
"homeDriftCheckTimer": 5.0,
|
||||
"homePosition": [
|
||||
-5.599999904632568,
|
||||
-5.5999999046325684,
|
||||
0.0,
|
||||
-12.0
|
||||
],
|
||||
@@ -155,14 +160,15 @@
|
||||
11
|
||||
],
|
||||
"navigationRetryTicks": 0,
|
||||
"pathWaypoints": [],
|
||||
"pathWaypoints": [
|
||||
],
|
||||
"position": [
|
||||
-5.599999904632568,
|
||||
-5.5999999046325684,
|
||||
0.0,
|
||||
-12.0
|
||||
],
|
||||
"requestedWalkTarget": [
|
||||
-5.599999904632568,
|
||||
-5.5999999046325684,
|
||||
0.0,
|
||||
-12.0
|
||||
],
|
||||
@@ -170,7 +176,7 @@
|
||||
"targetFacingAngle": 0.0,
|
||||
"visibleRange": 6.5,
|
||||
"walkTarget": [
|
||||
-5.599999904632568,
|
||||
-5.5999999046325684,
|
||||
0.0,
|
||||
-12.0
|
||||
]
|
||||
@@ -187,7 +193,7 @@
|
||||
"facingAngle": 0.0,
|
||||
"homeDriftCheckTimer": 5.0,
|
||||
"homePosition": [
|
||||
-6.400000095367432,
|
||||
-6.4000000953674316,
|
||||
0.0,
|
||||
-12.0
|
||||
],
|
||||
@@ -208,14 +214,15 @@
|
||||
6
|
||||
],
|
||||
"navigationRetryTicks": 0,
|
||||
"pathWaypoints": [],
|
||||
"pathWaypoints": [
|
||||
],
|
||||
"position": [
|
||||
-6.400000095367432,
|
||||
-6.4000000953674316,
|
||||
0.0,
|
||||
-12.0
|
||||
],
|
||||
"requestedWalkTarget": [
|
||||
-6.400000095367432,
|
||||
-6.4000000953674316,
|
||||
0.0,
|
||||
-12.0
|
||||
],
|
||||
@@ -223,7 +230,7 @@
|
||||
"targetFacingAngle": 0.0,
|
||||
"visibleRange": 6.5,
|
||||
"walkTarget": [
|
||||
-6.400000095367432,
|
||||
-6.4000000953674316,
|
||||
0.0,
|
||||
-12.0
|
||||
]
|
||||
@@ -240,7 +247,7 @@
|
||||
"facingAngle": 0.0,
|
||||
"homeDriftCheckTimer": 5.0,
|
||||
"homePosition": [
|
||||
-5.599999904632568,
|
||||
-5.5999999046325684,
|
||||
0.0,
|
||||
-12.5
|
||||
],
|
||||
@@ -261,14 +268,15 @@
|
||||
11
|
||||
],
|
||||
"navigationRetryTicks": 0,
|
||||
"pathWaypoints": [],
|
||||
"pathWaypoints": [
|
||||
],
|
||||
"position": [
|
||||
-5.599999904632568,
|
||||
-5.5999999046325684,
|
||||
0.0,
|
||||
-12.5
|
||||
],
|
||||
"requestedWalkTarget": [
|
||||
-5.599999904632568,
|
||||
-5.5999999046325684,
|
||||
0.0,
|
||||
-12.5
|
||||
],
|
||||
@@ -276,7 +284,7 @@
|
||||
"targetFacingAngle": 0.0,
|
||||
"visibleRange": 6.5,
|
||||
"walkTarget": [
|
||||
-5.599999904632568,
|
||||
-5.5999999046325684,
|
||||
0.0,
|
||||
-12.5
|
||||
]
|
||||
@@ -293,7 +301,7 @@
|
||||
"facingAngle": 0.0,
|
||||
"homeDriftCheckTimer": 5.0,
|
||||
"homePosition": [
|
||||
-6.400000095367432,
|
||||
-6.4000000953674316,
|
||||
0.0,
|
||||
-12.5
|
||||
],
|
||||
@@ -314,14 +322,15 @@
|
||||
6
|
||||
],
|
||||
"navigationRetryTicks": 0,
|
||||
"pathWaypoints": [],
|
||||
"pathWaypoints": [
|
||||
],
|
||||
"position": [
|
||||
-6.400000095367432,
|
||||
-6.4000000953674316,
|
||||
0.0,
|
||||
-12.5
|
||||
],
|
||||
"requestedWalkTarget": [
|
||||
-6.400000095367432,
|
||||
-6.4000000953674316,
|
||||
0.0,
|
||||
-12.5
|
||||
],
|
||||
@@ -329,7 +338,7 @@
|
||||
"targetFacingAngle": 0.0,
|
||||
"visibleRange": 6.5,
|
||||
"walkTarget": [
|
||||
-6.400000095367432,
|
||||
-6.4000000953674316,
|
||||
0.0,
|
||||
-12.5
|
||||
]
|
||||
@@ -346,7 +355,7 @@
|
||||
"facingAngle": 0.0,
|
||||
"homeDriftCheckTimer": 5.0,
|
||||
"homePosition": [
|
||||
-5.599999904632568,
|
||||
-5.5999999046325684,
|
||||
0.0,
|
||||
-13.0
|
||||
],
|
||||
@@ -367,14 +376,15 @@
|
||||
11
|
||||
],
|
||||
"navigationRetryTicks": 0,
|
||||
"pathWaypoints": [],
|
||||
"pathWaypoints": [
|
||||
],
|
||||
"position": [
|
||||
-5.599999904632568,
|
||||
-5.5999999046325684,
|
||||
0.0,
|
||||
-13.0
|
||||
],
|
||||
"requestedWalkTarget": [
|
||||
-5.599999904632568,
|
||||
-5.5999999046325684,
|
||||
0.0,
|
||||
-13.0
|
||||
],
|
||||
@@ -382,7 +392,7 @@
|
||||
"targetFacingAngle": 0.0,
|
||||
"visibleRange": 6.5,
|
||||
"walkTarget": [
|
||||
-5.599999904632568,
|
||||
-5.5999999046325684,
|
||||
0.0,
|
||||
-13.0
|
||||
]
|
||||
@@ -399,7 +409,7 @@
|
||||
"facingAngle": 0.0,
|
||||
"homeDriftCheckTimer": 5.0,
|
||||
"homePosition": [
|
||||
-6.400000095367432,
|
||||
-6.4000000953674316,
|
||||
0.0,
|
||||
-13.0
|
||||
],
|
||||
@@ -420,14 +430,15 @@
|
||||
2
|
||||
],
|
||||
"navigationRetryTicks": 0,
|
||||
"pathWaypoints": [],
|
||||
"pathWaypoints": [
|
||||
],
|
||||
"position": [
|
||||
-6.400000095367432,
|
||||
-6.4000000953674316,
|
||||
0.0,
|
||||
-13.0
|
||||
],
|
||||
"requestedWalkTarget": [
|
||||
-6.400000095367432,
|
||||
-6.4000000953674316,
|
||||
0.0,
|
||||
-13.0
|
||||
],
|
||||
@@ -435,13 +446,338 @@
|
||||
"targetFacingAngle": 0.0,
|
||||
"visibleRange": 6.5,
|
||||
"walkTarget": [
|
||||
-6.400000095367432,
|
||||
-6.4000000953674316,
|
||||
0.0,
|
||||
-13.0
|
||||
]
|
||||
},
|
||||
"ghost_07x": {
|
||||
"animationFrame": 0.0,
|
||||
"attackTargetId": "",
|
||||
"attack_cooldown": 0.0,
|
||||
"battle_state": 0,
|
||||
"currentState": 0,
|
||||
"currentWaypointIndex": 0,
|
||||
"enabled": true,
|
||||
"faceTargetId": "",
|
||||
"facingAngle": 0.0,
|
||||
"homeDriftCheckTimer": 5.0,
|
||||
"homePosition": [
|
||||
-5.5999999046325684,
|
||||
0.0,
|
||||
-14.0
|
||||
],
|
||||
"hp": 35.0,
|
||||
"movedLastTick": false,
|
||||
"movementBlockedTicks": 0,
|
||||
"navigationRetryCooldownIndex": 0,
|
||||
"navigationRetryCooldownTicks": [
|
||||
2,
|
||||
3,
|
||||
10,
|
||||
5,
|
||||
6,
|
||||
8,
|
||||
7,
|
||||
9,
|
||||
4,
|
||||
11
|
||||
],
|
||||
"navigationRetryTicks": 0,
|
||||
"pathWaypoints": [
|
||||
],
|
||||
"position": [
|
||||
-5.5999999046325684,
|
||||
0.0,
|
||||
-14.0
|
||||
],
|
||||
"requestedWalkTarget": [
|
||||
-5.5999999046325684,
|
||||
0.0,
|
||||
-14.0
|
||||
],
|
||||
"showWeapon": true,
|
||||
"targetFacingAngle": 0.0,
|
||||
"visibleRange": 6.5,
|
||||
"walkTarget": [
|
||||
-5.5999999046325684,
|
||||
0.0,
|
||||
-14.0
|
||||
]
|
||||
},
|
||||
"ghost_08x": {
|
||||
"animationFrame": 0.0,
|
||||
"attackTargetId": "",
|
||||
"attack_cooldown": 0.0,
|
||||
"battle_state": 0,
|
||||
"currentState": 0,
|
||||
"currentWaypointIndex": 0,
|
||||
"enabled": true,
|
||||
"faceTargetId": "",
|
||||
"facingAngle": 0.0,
|
||||
"homeDriftCheckTimer": 5.0,
|
||||
"homePosition": [
|
||||
-6.4000000953674316,
|
||||
0.0,
|
||||
-14.0
|
||||
],
|
||||
"hp": 35.0,
|
||||
"movedLastTick": false,
|
||||
"movementBlockedTicks": 0,
|
||||
"navigationRetryCooldownIndex": 0,
|
||||
"navigationRetryCooldownTicks": [
|
||||
8,
|
||||
11,
|
||||
3,
|
||||
10,
|
||||
2,
|
||||
9,
|
||||
5,
|
||||
4,
|
||||
7,
|
||||
6
|
||||
],
|
||||
"navigationRetryTicks": 0,
|
||||
"pathWaypoints": [
|
||||
],
|
||||
"position": [
|
||||
-6.4000000953674316,
|
||||
0.0,
|
||||
-14.0
|
||||
],
|
||||
"requestedWalkTarget": [
|
||||
-6.4000000953674316,
|
||||
0.0,
|
||||
-14.0
|
||||
],
|
||||
"showWeapon": true,
|
||||
"targetFacingAngle": 0.0,
|
||||
"visibleRange": 6.5,
|
||||
"walkTarget": [
|
||||
-6.4000000953674316,
|
||||
0.0,
|
||||
-14.0
|
||||
]
|
||||
},
|
||||
"ghost_09x": {
|
||||
"animationFrame": 0.0,
|
||||
"attackTargetId": "",
|
||||
"attack_cooldown": 0.0,
|
||||
"battle_state": 0,
|
||||
"currentState": 0,
|
||||
"currentWaypointIndex": 0,
|
||||
"enabled": true,
|
||||
"faceTargetId": "",
|
||||
"facingAngle": 0.0,
|
||||
"homeDriftCheckTimer": 5.0,
|
||||
"homePosition": [
|
||||
-5.5999999046325684,
|
||||
0.0,
|
||||
-14.5
|
||||
],
|
||||
"hp": 35.0,
|
||||
"movedLastTick": false,
|
||||
"movementBlockedTicks": 0,
|
||||
"navigationRetryCooldownIndex": 0,
|
||||
"navigationRetryCooldownTicks": [
|
||||
2,
|
||||
4,
|
||||
8,
|
||||
3,
|
||||
6,
|
||||
10,
|
||||
9,
|
||||
7,
|
||||
5,
|
||||
11
|
||||
],
|
||||
"navigationRetryTicks": 0,
|
||||
"pathWaypoints": [
|
||||
],
|
||||
"position": [
|
||||
-5.5999999046325684,
|
||||
0.0,
|
||||
-14.5
|
||||
],
|
||||
"requestedWalkTarget": [
|
||||
-5.5999999046325684,
|
||||
0.0,
|
||||
-14.5
|
||||
],
|
||||
"showWeapon": true,
|
||||
"targetFacingAngle": 0.0,
|
||||
"visibleRange": 6.5,
|
||||
"walkTarget": [
|
||||
-5.5999999046325684,
|
||||
0.0,
|
||||
-14.5
|
||||
]
|
||||
},
|
||||
"ghost_10x": {
|
||||
"animationFrame": 0.0,
|
||||
"attackTargetId": "",
|
||||
"attack_cooldown": 0.0,
|
||||
"battle_state": 0,
|
||||
"currentState": 0,
|
||||
"currentWaypointIndex": 0,
|
||||
"enabled": true,
|
||||
"faceTargetId": "",
|
||||
"facingAngle": 0.0,
|
||||
"homeDriftCheckTimer": 5.0,
|
||||
"homePosition": [
|
||||
-6.4000000953674316,
|
||||
0.0,
|
||||
-14.5
|
||||
],
|
||||
"hp": 35.0,
|
||||
"movedLastTick": false,
|
||||
"movementBlockedTicks": 0,
|
||||
"navigationRetryCooldownIndex": 0,
|
||||
"navigationRetryCooldownTicks": [
|
||||
3,
|
||||
4,
|
||||
7,
|
||||
10,
|
||||
5,
|
||||
11,
|
||||
9,
|
||||
2,
|
||||
8,
|
||||
6
|
||||
],
|
||||
"navigationRetryTicks": 0,
|
||||
"pathWaypoints": [
|
||||
],
|
||||
"position": [
|
||||
-6.4000000953674316,
|
||||
0.0,
|
||||
-14.5
|
||||
],
|
||||
"requestedWalkTarget": [
|
||||
-6.4000000953674316,
|
||||
0.0,
|
||||
-12.5
|
||||
],
|
||||
"showWeapon": true,
|
||||
"targetFacingAngle": 0.0,
|
||||
"visibleRange": 6.5,
|
||||
"walkTarget": [
|
||||
-6.4000000953674316,
|
||||
0.0,
|
||||
-14.5
|
||||
]
|
||||
},
|
||||
"ghost_11x": {
|
||||
"animationFrame": 0.0,
|
||||
"attackTargetId": "",
|
||||
"attack_cooldown": 0.0,
|
||||
"battle_state": 0,
|
||||
"currentState": 0,
|
||||
"currentWaypointIndex": 0,
|
||||
"enabled": true,
|
||||
"faceTargetId": "",
|
||||
"facingAngle": 0.0,
|
||||
"homeDriftCheckTimer": 5.0,
|
||||
"homePosition": [
|
||||
-5.5999999046325684,
|
||||
0.0,
|
||||
-15.0
|
||||
],
|
||||
"hp": 35.0,
|
||||
"movedLastTick": false,
|
||||
"movementBlockedTicks": 0,
|
||||
"navigationRetryCooldownIndex": 0,
|
||||
"navigationRetryCooldownTicks": [
|
||||
8,
|
||||
7,
|
||||
2,
|
||||
9,
|
||||
4,
|
||||
10,
|
||||
6,
|
||||
5,
|
||||
3,
|
||||
11
|
||||
],
|
||||
"navigationRetryTicks": 0,
|
||||
"pathWaypoints": [
|
||||
],
|
||||
"position": [
|
||||
-5.5999999046325684,
|
||||
0.0,
|
||||
-15.0
|
||||
],
|
||||
"requestedWalkTarget": [
|
||||
-5.5999999046325684,
|
||||
0.0,
|
||||
-15.0
|
||||
],
|
||||
"showWeapon": true,
|
||||
"targetFacingAngle": 0.0,
|
||||
"visibleRange": 6.5,
|
||||
"walkTarget": [
|
||||
-5.5999999046325684,
|
||||
0.0,
|
||||
-15.0
|
||||
]
|
||||
},
|
||||
"ghost_12x": {
|
||||
"animationFrame": 0.0,
|
||||
"attackTargetId": "",
|
||||
"attack_cooldown": 0.0,
|
||||
"battle_state": 0,
|
||||
"currentState": 0,
|
||||
"currentWaypointIndex": 0,
|
||||
"enabled": true,
|
||||
"faceTargetId": "",
|
||||
"facingAngle": 0.0,
|
||||
"homeDriftCheckTimer": 5.0,
|
||||
"homePosition": [
|
||||
-6.4000000953674316,
|
||||
0.0,
|
||||
-15.0
|
||||
],
|
||||
"hp": 35.0,
|
||||
"movedLastTick": false,
|
||||
"movementBlockedTicks": 0,
|
||||
"navigationRetryCooldownIndex": 0,
|
||||
"navigationRetryCooldownTicks": [
|
||||
7,
|
||||
11,
|
||||
5,
|
||||
4,
|
||||
3,
|
||||
10,
|
||||
9,
|
||||
8,
|
||||
6,
|
||||
2
|
||||
],
|
||||
"navigationRetryTicks": 0,
|
||||
"pathWaypoints": [
|
||||
],
|
||||
"position": [
|
||||
-6.4000000953674316,
|
||||
0.0,
|
||||
-15.0
|
||||
],
|
||||
"requestedWalkTarget": [
|
||||
-6.4000000953674316,
|
||||
0.0,
|
||||
-15.0
|
||||
],
|
||||
"showWeapon": true,
|
||||
"targetFacingAngle": 0.0,
|
||||
"visibleRange": 6.5,
|
||||
"walkTarget": [
|
||||
-6.4000000953674316,
|
||||
0.0,
|
||||
-15.0
|
||||
]
|
||||
}
|
||||
},
|
||||
"scriptGlobals": {},
|
||||
"scriptGlobals": {
|
||||
},
|
||||
"state": {
|
||||
"activeNavigationIndex": 0,
|
||||
"targetInteractNpcId": "",
|
||||
@@ -460,61 +796,23 @@
|
||||
"rotation": 0.0,
|
||||
"steeringAngle": 0.0,
|
||||
"velocity": 0.0,
|
||||
"waypoints": [],
|
||||
"waypoints": [
|
||||
],
|
||||
"wheelAngle": 0.0
|
||||
}
|
||||
}
|
||||
},
|
||||
"teleportZones": {},
|
||||
"triggerZones": {}
|
||||
"teleportZones": {
|
||||
},
|
||||
"triggerZones": {
|
||||
}
|
||||
}
|
||||
},
|
||||
"money": 5500,
|
||||
"playerHp": 200.0,
|
||||
"playerMaxHp": 200.0,
|
||||
"players": {
|
||||
"player2": {
|
||||
"animationFrame": 0.0,
|
||||
"attackTargetId": "",
|
||||
"attack_cooldown": 0.0,
|
||||
"battle_state": 0,
|
||||
"currentState": 0,
|
||||
"currentWaypointIndex": 0,
|
||||
"enabled": false,
|
||||
"faceTargetId": "",
|
||||
"facingAngle": 0.0,
|
||||
"homeDriftCheckTimer": 5.0,
|
||||
"homePosition": [
|
||||
0.0,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"hp": 200.0,
|
||||
"pathWaypoints": [[
|
||||
-6.8,
|
||||
0.0,
|
||||
13.0
|
||||
]],
|
||||
"position": [
|
||||
-6.8,
|
||||
0.0,
|
||||
13.0
|
||||
],
|
||||
"requestedWalkTarget": [
|
||||
-6.8,
|
||||
0.0,
|
||||
13.0
|
||||
],
|
||||
"showWeapon": true,
|
||||
"targetFacingAngle": 0.0,
|
||||
"visibleRange": 6.0,
|
||||
"walkTarget": [
|
||||
-6.8,
|
||||
0.0,
|
||||
13.0
|
||||
]
|
||||
},
|
||||
"player": {
|
||||
"player1": {
|
||||
"animationFrame": 0.0,
|
||||
"attackTargetId": "",
|
||||
"attack_cooldown": 0.0,
|
||||
@@ -556,10 +854,140 @@
|
||||
0.0,
|
||||
12.8
|
||||
]
|
||||
},
|
||||
"player2": {
|
||||
"animationFrame": 0.0,
|
||||
"attackTargetId": "",
|
||||
"attack_cooldown": 0.0,
|
||||
"battle_state": 0,
|
||||
"currentState": 0,
|
||||
"currentWaypointIndex": 0,
|
||||
"enabled": false,
|
||||
"faceTargetId": "",
|
||||
"facingAngle": 0.0,
|
||||
"homeDriftCheckTimer": 5.0,
|
||||
"homePosition": [
|
||||
0.0,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"hp": 200.0,
|
||||
"pathWaypoints": [
|
||||
[
|
||||
-6.8,
|
||||
0.0,
|
||||
13.0
|
||||
]
|
||||
],
|
||||
"position": [
|
||||
-6.8,
|
||||
0.0,
|
||||
13.0
|
||||
],
|
||||
"requestedWalkTarget": [
|
||||
-6.8,
|
||||
0.0,
|
||||
13.0
|
||||
],
|
||||
"showWeapon": true,
|
||||
"targetFacingAngle": 0.0,
|
||||
"visibleRange": 6.0,
|
||||
"walkTarget": [
|
||||
-6.8,
|
||||
0.0,
|
||||
13.0
|
||||
]
|
||||
},
|
||||
"player3": {
|
||||
"animationFrame": 0.0,
|
||||
"attackTargetId": "",
|
||||
"attack_cooldown": 0.0,
|
||||
"battle_state": 0,
|
||||
"currentState": 0,
|
||||
"currentWaypointIndex": 0,
|
||||
"enabled": false,
|
||||
"faceTargetId": "",
|
||||
"facingAngle": 0.0,
|
||||
"homeDriftCheckTimer": 5.0,
|
||||
"homePosition": [
|
||||
0.0,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"hp": 200.0,
|
||||
"pathWaypoints": [
|
||||
[
|
||||
-5.4,
|
||||
0.0,
|
||||
14.8
|
||||
]
|
||||
],
|
||||
"position": [
|
||||
-5.4,
|
||||
0.0,
|
||||
14.8
|
||||
],
|
||||
"requestedWalkTarget": [
|
||||
-5.4,
|
||||
0.0,
|
||||
14.8
|
||||
],
|
||||
"showWeapon": true,
|
||||
"targetFacingAngle": 0.0,
|
||||
"visibleRange": 6.0,
|
||||
"walkTarget": [
|
||||
-5.4,
|
||||
0.0,
|
||||
14.8
|
||||
]
|
||||
},
|
||||
"player4": {
|
||||
"animationFrame": 0.0,
|
||||
"attackTargetId": "",
|
||||
"attack_cooldown": 0.0,
|
||||
"battle_state": 0,
|
||||
"currentState": 0,
|
||||
"currentWaypointIndex": 0,
|
||||
"enabled": false,
|
||||
"faceTargetId": "",
|
||||
"facingAngle": 0.0,
|
||||
"homeDriftCheckTimer": 5.0,
|
||||
"homePosition": [
|
||||
0.0,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"hp": 200.0,
|
||||
"pathWaypoints": [
|
||||
[
|
||||
-6.8,
|
||||
0.0,
|
||||
15.0
|
||||
]
|
||||
],
|
||||
"position": [
|
||||
-6.8,
|
||||
0.0,
|
||||
15.0
|
||||
],
|
||||
"requestedWalkTarget": [
|
||||
-6.8,
|
||||
0.0,
|
||||
15.0
|
||||
],
|
||||
"showWeapon": true,
|
||||
"targetFacingAngle": 0.0,
|
||||
"visibleRange": 6.0,
|
||||
"walkTarget": [
|
||||
-6.8,
|
||||
0.0,
|
||||
15.0
|
||||
]
|
||||
}
|
||||
},
|
||||
"questJournal": {
|
||||
"quests": []
|
||||
"quests": [
|
||||
]
|
||||
},
|
||||
"savedAt": "2026-11-05 17:43",
|
||||
"schemaVersion": 5,
|
||||
|
||||
@@ -163,6 +163,11 @@ if(BUILD_TESTING)
|
||||
target_compile_definitions(
|
||||
GameServerWebsocketLockstepTests PRIVATE
|
||||
NETWORK WIN32_LEAN_AND_MEAN _WIN32_WINNT=0x0A00)
|
||||
if(MSVC)
|
||||
set_source_files_properties(
|
||||
"${GAME_ROOT}/src/game_server/WebsocketGameClient.cpp"
|
||||
PROPERTIES COMPILE_OPTIONS "/bigobj")
|
||||
endif()
|
||||
target_link_libraries(
|
||||
GameServerWebsocketLockstepTests PRIVATE
|
||||
game_server_core ws2_32 mswsock)
|
||||
|
||||
@@ -194,6 +194,59 @@ int main()
|
||||
return fail("Hostile NPC did not return home after losing its target");
|
||||
}
|
||||
|
||||
auto blocker = std::make_shared<FRG::CharacterState>();
|
||||
blocker->enabled = true;
|
||||
blocker->hp = 50.f;
|
||||
blocker->position = {0.8f, 0.f, 0.f};
|
||||
state.npcs["blocker"] = blocker;
|
||||
hostileNpc.position = Eigen::Vector3f::Zero();
|
||||
hostileNpc.homePosition = {5.f, 0.f, 0.f};
|
||||
hostileNpc.attackTargetId = "player";
|
||||
hostileNpc.requestedWalkTarget = playerOne->position;
|
||||
hostileNpc.walkTarget = playerOne->position;
|
||||
hostileNpc.pathWaypoints = {playerOne->position};
|
||||
hostileNpc.currentWaypointIndex = 0;
|
||||
hostileNpc.movementBlockedTicks = 0;
|
||||
playerOne->enabled = true;
|
||||
playerOne->hp = 75.f;
|
||||
playerOne->position = {4.f, 0.f, 0.f};
|
||||
playerTwo->enabled = true;
|
||||
playerTwo->hp = 75.f;
|
||||
playerTwo->position = {2.f, 0.f, 0.f};
|
||||
|
||||
bool blockedRetryRequested = false;
|
||||
for (int tick = 0; tick < 10 && !blockedRetryRequested; ++tick) {
|
||||
const FRG::LocationSimulationEvents events =
|
||||
state.update(100, combatPlayers, false);
|
||||
for (const auto& event : events.characters) {
|
||||
if (event.characterId == "npc" &&
|
||||
event.events.forceNavigationReplan) {
|
||||
blockedRetryRequested = true;
|
||||
if (!state.replanCharacter(
|
||||
"npc", combatPlayers,
|
||||
event.events.blockingCharacterId)) {
|
||||
return fail("Blocked NPC replan was rejected");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!blockedRetryRequested ||
|
||||
hostileNpc.attackTargetId != "player2" ||
|
||||
(hostileNpc.requestedWalkTarget - playerTwo->position).norm() >
|
||||
FRG::CharacterState::kWalkThreshold) {
|
||||
return fail("Blocked NPC did not switch to the nearest player");
|
||||
}
|
||||
|
||||
playerOne->enabled = false;
|
||||
playerTwo->enabled = false;
|
||||
if (!state.replanCharacter("npc", combatPlayers, "blocker") ||
|
||||
!hostileNpc.attackTargetId.empty() ||
|
||||
hostileNpc.battle_state != 0 ||
|
||||
(hostileNpc.requestedWalkTarget - hostileNpc.homePosition).norm() >
|
||||
FRG::CharacterState::kWalkThreshold) {
|
||||
return fail("Blocked NPC did not return home without an eligible target");
|
||||
}
|
||||
|
||||
FRG::CampaignConfig battleZone;
|
||||
battleZone.id = "battle_zone";
|
||||
battleZone.questsPath = "resources/config/quests_battle_zone.json";
|
||||
|
||||
@@ -32,6 +32,31 @@ bool waitFor(Predicate predicate)
|
||||
|
||||
int main()
|
||||
{
|
||||
FRG::WebsocketEndpoint secureEndpoint;
|
||||
require(
|
||||
secureEndpoint.parse(
|
||||
"wss://api.shadowoverbishkek.com/server001/"),
|
||||
"secure WebSocket endpoint was rejected");
|
||||
require(
|
||||
secureEndpoint.host == "api.shadowoverbishkek.com" &&
|
||||
secureEndpoint.port == "443" &&
|
||||
secureEndpoint.path == "/server001/" &&
|
||||
secureEndpoint.useTls,
|
||||
"secure WebSocket endpoint was parsed incorrectly");
|
||||
FRG::WebsocketEndpoint insecureEndpoint;
|
||||
require(
|
||||
insecureEndpoint.parse("ws://127.0.0.1:8080/"),
|
||||
"insecure WebSocket endpoint was rejected");
|
||||
require(
|
||||
insecureEndpoint.host == "127.0.0.1" &&
|
||||
insecureEndpoint.port == "8080" &&
|
||||
!insecureEndpoint.useTls,
|
||||
"insecure WebSocket endpoint was parsed incorrectly");
|
||||
FRG::WebsocketEndpoint invalidEndpoint;
|
||||
require(
|
||||
!invalidEndpoint.parse("https://api.shadowoverbishkek.com/"),
|
||||
"non-WebSocket endpoint was accepted");
|
||||
|
||||
FRG::CampaignConfig campaign;
|
||||
campaign.id = "test_lockstep";
|
||||
campaign.networkPlayerSlots = {"player", "player2"};
|
||||
|
||||
+14
-2
@@ -557,7 +557,19 @@ namespace FRG
|
||||
if (remoteCampaignActive) {
|
||||
logger() << "[network] Campaign '" << campaignId
|
||||
<< "' selected; creating WebSocket client" << std::endl;
|
||||
websocketGameClient = std::make_unique<WebsocketGameClient>();
|
||||
// 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;
|
||||
remoteCampaignActive = false;
|
||||
gameServer.use(internalGameServer);
|
||||
gameServerHost.use(localGameServerHost);
|
||||
return;
|
||||
}
|
||||
websocketGameClient = std::make_unique<WebsocketGameClient>(endpoint);
|
||||
remoteGameServerHost =
|
||||
std::make_unique<RemoteGameServerHost>(*websocketGameClient);
|
||||
gameServer.use(*websocketGameClient);
|
||||
@@ -680,7 +692,7 @@ namespace FRG
|
||||
// Developer workflow: uncomment to (re)generate the campaign's start-state JSON
|
||||
// from this freshly-rebuilt state after changing gameplay/content JSON or Lua.
|
||||
// Re-comment before shipping.
|
||||
saveInitialState();
|
||||
//saveInitialState();
|
||||
|
||||
}
|
||||
|
||||
|
||||
+3
-3
@@ -890,11 +890,11 @@ namespace FRG
|
||||
}
|
||||
|
||||
Character* Location::raycastNpcs(const Eigen::Vector3f& rayOrigin, const Eigen::Vector3f& rayDir, float maxDistance) {
|
||||
// Every NPC is treated as a vertical cylinder: radius 0.25m, height 1.6m,
|
||||
// Every NPC is treated as a vertical cylinder
|
||||
// base at npc->position (the model's foot). Intersection = circle hit in the
|
||||
// XZ plane, then clip the entry/exit t against the [yFoot, yFoot+height] slab.
|
||||
static constexpr float NPC_CLICK_RADIUS = 0.25f;
|
||||
static constexpr float NPC_CLICK_HEIGHT = 1.6f;
|
||||
static constexpr float NPC_CLICK_RADIUS = 0.35f;
|
||||
static constexpr float NPC_CLICK_HEIGHT = 1.8f;
|
||||
static constexpr float NPC_PICK_EPSILON = 1e-6f;
|
||||
|
||||
Character* closestNpc = nullptr;
|
||||
|
||||
+77
-33
@@ -173,11 +173,84 @@ bool LocationGameState::replanCharacter(
|
||||
{
|
||||
CharacterState* character = findCharacter(id, players);
|
||||
if (!character || !location) return false;
|
||||
if (!forcedObstacleId.empty() && !character->isPlayer &&
|
||||
character->canAttack && character->hp > 0.f) {
|
||||
const std::string selectedTarget =
|
||||
selectNpcTarget(*character, players, false);
|
||||
if (selectedTarget != character->attackTargetId) {
|
||||
character->stopInPlace();
|
||||
character->attackTargetId = selectedTarget;
|
||||
character->battle_state = 0;
|
||||
character->attack = 0;
|
||||
}
|
||||
|
||||
if (!selectedTarget.empty()) {
|
||||
const CharacterState* target = findCharacter(selectedTarget, players);
|
||||
if (target) {
|
||||
character->setTarget(
|
||||
target->position, {}, *location, id,
|
||||
collectPathObstacles(id, players, forcedObstacleId));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
character->attackTargetId.clear();
|
||||
character->battle_state = 0;
|
||||
character->attack = 0;
|
||||
const Eigen::Vector3f homeTarget(
|
||||
character->homePosition.x(), 0.f, character->homePosition.z());
|
||||
if ((homeTarget - character->position).norm() >
|
||||
CharacterState::kWalkThreshold) {
|
||||
character->setTarget(
|
||||
homeTarget, {}, *location, id,
|
||||
collectPathObstacles(id, players, forcedObstacleId));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
character->forceReplan(
|
||||
*location, id, collectPathObstacles(id, players, forcedObstacleId));
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string LocationGameState::selectNpcTarget(
|
||||
const CharacterState& npc,
|
||||
const PlayerStateMap& players,
|
||||
bool preserveValidLock) const
|
||||
{
|
||||
if (preserveValidLock) {
|
||||
const auto locked = players.find(npc.attackTargetId);
|
||||
if (locked != players.end() && locked->second &&
|
||||
locked->second->enabled && locked->second->hp > 0.f)
|
||||
return npc.attackTargetId;
|
||||
}
|
||||
|
||||
std::vector<std::string> playerIds;
|
||||
playerIds.reserve(players.size());
|
||||
for (const auto& [id, player] : players)
|
||||
if (player && player->enabled && player->hp > 0.f)
|
||||
playerIds.push_back(id);
|
||||
std::sort(playerIds.begin(), playerIds.end());
|
||||
|
||||
const float visibleRangeSquared = npc.visibleRange * npc.visibleRange;
|
||||
std::string selectedTarget;
|
||||
float nearestDistanceSquared = visibleRangeSquared;
|
||||
for (const std::string& playerId : playerIds) {
|
||||
const Eigen::Vector3f delta =
|
||||
players.at(playerId)->position - npc.position;
|
||||
const float distanceSquared =
|
||||
delta.x() * delta.x() + delta.z() * delta.z();
|
||||
if (distanceSquared <= nearestDistanceSquared &&
|
||||
(selectedTarget.empty() ||
|
||||
distanceSquared < nearestDistanceSquared ||
|
||||
(distanceSquared == nearestDistanceSquared &&
|
||||
playerId < selectedTarget))) {
|
||||
selectedTarget = playerId;
|
||||
nearestDistanceSquared = distanceSquared;
|
||||
}
|
||||
}
|
||||
return selectedTarget;
|
||||
}
|
||||
|
||||
void LocationGameState::startTaxi()
|
||||
{
|
||||
location->taxiCar.waypoints = location->taxiCarWaypoints;
|
||||
@@ -321,13 +394,6 @@ LocationSimulationEvents LocationGameState::update(
|
||||
if (player) playerIds.push_back(id);
|
||||
std::sort(playerIds.begin(), playerIds.end());
|
||||
|
||||
std::vector<std::string> eligiblePlayerIds;
|
||||
for (const std::string& id : playerIds) {
|
||||
const auto& player = players.at(id);
|
||||
if (player->enabled && player->hp > 0.f)
|
||||
eligiblePlayerIds.push_back(id);
|
||||
}
|
||||
|
||||
std::vector<std::string> npcIds;
|
||||
npcIds.reserve(npcs.size());
|
||||
for (const auto& [id, npc] : npcs)
|
||||
@@ -343,30 +409,8 @@ LocationSimulationEvents LocationGameState::update(
|
||||
continue;
|
||||
|
||||
const std::string previousTarget = npc.attackTargetId;
|
||||
const auto locked = players.find(previousTarget);
|
||||
const bool lockValid =
|
||||
locked != players.end() && locked->second &&
|
||||
locked->second->enabled && locked->second->hp > 0.f;
|
||||
std::string selectedTarget = lockValid ? previousTarget : std::string();
|
||||
|
||||
if (!lockValid) {
|
||||
float nearestDistanceSquared = npc.visibleRange * npc.visibleRange;
|
||||
for (const std::string& playerId : eligiblePlayerIds) {
|
||||
const Eigen::Vector3f delta =
|
||||
players.at(playerId)->position - npc.position;
|
||||
const float distanceSquared =
|
||||
delta.x() * delta.x() + delta.z() * delta.z();
|
||||
if (distanceSquared <= nearestDistanceSquared) {
|
||||
if (selectedTarget.empty() ||
|
||||
distanceSquared < nearestDistanceSquared ||
|
||||
(distanceSquared == nearestDistanceSquared &&
|
||||
playerId < selectedTarget)) {
|
||||
selectedTarget = playerId;
|
||||
nearestDistanceSquared = distanceSquared;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const std::string selectedTarget =
|
||||
selectNpcTarget(npc, players, true);
|
||||
|
||||
if (selectedTarget != previousTarget) {
|
||||
npc.stopInPlace();
|
||||
@@ -474,7 +518,7 @@ LocationSimulationEvents LocationGameState::update(
|
||||
source.events.attackDirection});
|
||||
}
|
||||
|
||||
auto player = players.find("player");
|
||||
auto player = players.find("player1");
|
||||
if (!interactiveObjectsLocked &&
|
||||
!location->targetInteractiveObjectId.empty() &&
|
||||
player != players.end() && player->second && player->second->enabled) {
|
||||
@@ -502,7 +546,7 @@ LocationSimulationEvents LocationGameState::update(
|
||||
(player->second->position - npc->position).norm();
|
||||
if (distance <= NPC_TALK_DISTANCE) {
|
||||
player->second->stopInPlace();
|
||||
npc->faceTargetId = "player";
|
||||
npc->faceTargetId = "player1";
|
||||
result.npcInteractionId = location->targetInteractNpcId;
|
||||
location->targetInteractNpcId.clear();
|
||||
}
|
||||
|
||||
@@ -86,6 +86,10 @@ struct LocationGameState {
|
||||
void load(const nlohmann::json& in);
|
||||
|
||||
private:
|
||||
std::string selectNpcTarget(
|
||||
const CharacterState& npc,
|
||||
const PlayerStateMap& players,
|
||||
bool preserveValidLock) const;
|
||||
bool updateNpcCar(std::int64_t deltaMs);
|
||||
};
|
||||
|
||||
|
||||
@@ -172,7 +172,7 @@ namespace FRG {
|
||||
<< npcId << "'\n";
|
||||
return;
|
||||
}
|
||||
loc->faceCharacter(*npc, "player");
|
||||
loc->faceCharacter(*npc, "player1");
|
||||
});
|
||||
|
||||
api.set_function("player_walk_to",
|
||||
|
||||
@@ -51,7 +51,7 @@ private:
|
||||
GameSnapshot buildSnapshot() const;
|
||||
|
||||
GameState state_;
|
||||
std::string localPlayerId_ = "player";
|
||||
std::string localPlayerId_ = "player1";
|
||||
std::string resourceZipFile_;
|
||||
std::vector<std::string> networkPlayerSlots_;
|
||||
std::int64_t battleZoneTestBotElapsedMs_ = 0;
|
||||
|
||||
@@ -5,6 +5,21 @@
|
||||
#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>
|
||||
@@ -23,19 +38,104 @@ 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)
|
||||
Impl(std::string host, std::string port, std::string path, bool useTls)
|
||||
: host_(std::move(host))
|
||||
, port_(std::move(port))
|
||||
, path_(std::move(path))
|
||||
{
|
||||
url_ = "ws://" + host_ + ':' + port_ +
|
||||
url_ = std::string(useTls ? "wss://" : "ws://") + host_ + ':' + port_ +
|
||||
(path_.empty() || path_.front() == '/' ? path_ : "/" + path_);
|
||||
std::cout << "[network] Opening " << url_ << std::endl;
|
||||
|
||||
@@ -45,7 +145,7 @@ public:
|
||||
attributes.createOnMainThread = EM_TRUE;
|
||||
socket_ = emscripten_websocket_new(&attributes);
|
||||
if (socket_ <= 0) {
|
||||
fail("create WebSocket");
|
||||
fail("create WebSocket", socket_);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -105,11 +205,21 @@ private:
|
||||
}
|
||||
|
||||
static EM_BOOL onClose(
|
||||
int, const EmscriptenWebSocketCloseEvent*, void* userData)
|
||||
int, const EmscriptenWebSocketCloseEvent* event, void* userData)
|
||||
{
|
||||
auto* self = static_cast<Impl*>(userData);
|
||||
self->connected_ = false;
|
||||
if (!self->closing_) self->fail("WebSocket close");
|
||||
if (!self->closing_) {
|
||||
std::cerr << "[network] WebSocket closed";
|
||||
if (event) {
|
||||
std::cerr << " (code " << event->code;
|
||||
if (event->reason[0] != '\0')
|
||||
std::cerr << ", reason: " << event->reason;
|
||||
std::cerr << ')';
|
||||
}
|
||||
std::cerr << '\n';
|
||||
self->connectionFailed_ = true;
|
||||
}
|
||||
return EM_TRUE;
|
||||
}
|
||||
|
||||
@@ -126,11 +236,13 @@ private:
|
||||
return EM_TRUE;
|
||||
}
|
||||
|
||||
void fail(const char* operation)
|
||||
void fail(const char* operation, int result = 0)
|
||||
{
|
||||
connected_ = false;
|
||||
connectionFailed_ = true;
|
||||
std::cerr << "[network] " << operation << " failed" << std::endl;
|
||||
std::cerr << "[network] " << operation << " failed";
|
||||
if (result != 0) std::cerr << " (result " << result << ')';
|
||||
std::cerr << std::endl;
|
||||
}
|
||||
|
||||
std::string host_;
|
||||
@@ -143,14 +255,38 @@ private:
|
||||
bool closing_ = false;
|
||||
std::vector<std::string> messages_;
|
||||
#else
|
||||
Impl(std::string host, std::string port, std::string path)
|
||||
: resolver_(io_)
|
||||
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
|
||||
, socket_(io_)
|
||||
, host_(std::move(host))
|
||||
, port_(std::move(port))
|
||||
, path_(std::move(path))
|
||||
, useTls_(useTls)
|
||||
{
|
||||
std::cout << "[network] Resolving ws://" << host_ << ':' << port_
|
||||
#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_
|
||||
<< path_ << std::endl;
|
||||
resolve();
|
||||
thread_ = std::thread([this]() { io_.run(); });
|
||||
@@ -184,6 +320,20 @@ 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(
|
||||
@@ -192,34 +342,68 @@ private:
|
||||
if (error) return fail("resolve", error);
|
||||
std::cout << "[network] WebSocket host resolved; connecting"
|
||||
<< std::endl;
|
||||
withStream([&](auto& stream) {
|
||||
asio::async_connect(
|
||||
socket_.next_layer(), results,
|
||||
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; starting WebSocket handshake"
|
||||
<< std::endl;
|
||||
socket_.set_option(
|
||||
std::cout << "[network] TCP connected" << std::endl;
|
||||
startTlsOrWebsocketHandshake();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
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));
|
||||
socket_.async_handshake(
|
||||
host_, path_,
|
||||
stream.async_handshake(
|
||||
hostHeader, path_,
|
||||
[this](beast::error_code handshakeError) {
|
||||
if (handshakeError)
|
||||
return fail("handshake", handshakeError);
|
||||
connected_.store(true);
|
||||
std::cout
|
||||
<< "[network] Connected to ws://"
|
||||
<< "[network] Connected to " << scheme()
|
||||
<< host_ << ':' << port_ << path_ << std::endl;
|
||||
read();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void read()
|
||||
{
|
||||
socket_.async_read(
|
||||
withStream([this](auto& stream) {
|
||||
stream.async_read(
|
||||
buffer_,
|
||||
[this](beast::error_code error, std::size_t) {
|
||||
if (error) {
|
||||
@@ -235,17 +419,20 @@ private:
|
||||
}
|
||||
read();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void write()
|
||||
{
|
||||
socket_.async_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();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void fail(const char* operation, beast::error_code error)
|
||||
@@ -257,13 +444,20 @@ 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_;
|
||||
@@ -273,9 +467,15 @@ private:
|
||||
};
|
||||
|
||||
WebsocketGameClient::WebsocketGameClient(
|
||||
std::string host, std::string port, std::string path)
|
||||
std::string host, std::string port, std::string path, bool useTls)
|
||||
: impl_(std::make_unique<Impl>(
|
||||
std::move(host), std::move(port), std::move(path)))
|
||||
std::move(host), std::move(port), std::move(path), useTls))
|
||||
{
|
||||
}
|
||||
|
||||
WebsocketGameClient::WebsocketGameClient(const WebsocketEndpoint& endpoint)
|
||||
: WebsocketGameClient(
|
||||
endpoint.host, endpoint.port, endpoint.path, endpoint.useTls)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -11,12 +11,26 @@ 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 = "/");
|
||||
std::string path = "/",
|
||||
bool useTls = false);
|
||||
explicit WebsocketGameClient(const WebsocketEndpoint& endpoint);
|
||||
~WebsocketGameClient();
|
||||
|
||||
WebsocketGameClient(const WebsocketGameClient&) = delete;
|
||||
|
||||
Reference in new issue
Block a user