Compare commits
14
Commits
main
...
pathfinding2.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8df016c0a4 | ||
|
|
2fe2ee6c2a | ||
|
|
cf714d4133 | ||
|
|
255181cf31 | ||
|
|
ef966a2197 | ||
|
|
24c963c0a5 | ||
|
|
6cfd47e80d | ||
|
|
0be23ca291 | ||
|
|
0f1e0d66a3 | ||
|
|
63bf981e21 | ||
|
|
f3cafd2bf7 | ||
|
|
6b8aa0efec | ||
|
|
8c4eeb9793 | ||
|
|
28fb81ad45 |
No files matched your search
+26
-6
@@ -41,7 +41,7 @@ configured player slot connects or disconnects.
|
|||||||
|
|
||||||
`WebsocketGameClient` implements `IGameServer`.
|
`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
|
- Received messages are queued by the network thread and applied on the game
|
||||||
thread from `update()`.
|
thread from `update()`.
|
||||||
- It maintains an internal `InternalGameServer` replica so existing snapshot
|
- 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,
|
- `proj-web/CMakeLists.txt` defines `NETWORK`, compiles the shared protocol,
|
||||||
replica, and remote-host code, and links Emscripten's `websocket.js` library.
|
replica, and remote-host code, and links Emscripten's `websocket.js` library.
|
||||||
`WebsocketGameClient.cpp` selects the browser WebSocket API on Emscripten; it
|
`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
|
- Linux and Android builds do not define `NETWORK` and do not compile the
|
||||||
network transport.
|
network transport.
|
||||||
|
|
||||||
@@ -163,15 +171,24 @@ Run it from the repository root:
|
|||||||
.\server\build\Debug\GameServer.exe --campaign battle_zone_multiplayer --location uni_exterior
|
.\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
|
```text
|
||||||
--address IP
|
--address IP
|
||||||
--port N
|
--port N
|
||||||
```
|
```
|
||||||
|
|
||||||
Launch the Windows game and select `battle_zone_multiplayer`. The client connects to
|
Launch the Windows game and select `battle_zone_multiplayer`. The production client
|
||||||
`127.0.0.1:8080`. Selecting another campaign continues to use the local server.
|
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
|
## Verification
|
||||||
|
|
||||||
@@ -196,7 +213,10 @@ submission, and contiguous tick consumption.
|
|||||||
not implemented.
|
not implemented.
|
||||||
- There is no authentication or reconnect/replay history.
|
- There is no authentication or reconnect/replay history.
|
||||||
- A hash mismatch is logged but does not yet trigger checkpoint resynchronization.
|
- 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.
|
- Multiplayer Lua and dialogue behavior remains unsupported.
|
||||||
- Determinism is supported for identical executable/content builds. The
|
- Determinism is supported for identical executable/content builds. The
|
||||||
floating-point simulation is not guaranteed bit-identical across platforms
|
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
|
# 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")
|
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,3 +726,105 @@ set_target_properties(SDL2_mixer_external_lib PROPERTIES
|
|||||||
INTERFACE_LINK_LIBRARIES
|
INTERFACE_LINK_LIBRARIES
|
||||||
"SDL2_external_lib"
|
"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.
|
||||||
@@ -246,7 +246,7 @@ set(EMSCRIPTEN_LINK_FLAGS
|
|||||||
"--preload-file ${CMAKE_CURRENT_SOURCE_DIR}/../resources/start_uni_interior.lua@resources/start_uni_interior.lua"
|
"--preload-file ${CMAKE_CURRENT_SOURCE_DIR}/../resources/start_uni_interior.lua@resources/start_uni_interior.lua"
|
||||||
"--preload-file ${CMAKE_CURRENT_SOURCE_DIR}/../resources/start_uni_exterior.lua@resources/start_uni_exterior.lua"
|
"--preload-file ${CMAKE_CURRENT_SOURCE_DIR}/../resources/start_uni_exterior.lua@resources/start_uni_exterior.lua"
|
||||||
"--preload-file ${CMAKE_CURRENT_SOURCE_DIR}/../resources/start_dorm.lua@resources/start_dorm.lua"
|
"--preload-file ${CMAKE_CURRENT_SOURCE_DIR}/../resources/start_dorm.lua@resources/start_dorm.lua"
|
||||||
"--preload-file ${CMAKE_CURRENT_SOURCE_DIR}/../resources/start_dorm.lua@resources/start_battle_zone.lua"
|
"--preload-file ${CMAKE_CURRENT_SOURCE_DIR}/../resources/start_battle_zone.lua@resources/start_battle_zone.lua"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Применяем настройки линковки
|
# Применяем настройки линковки
|
||||||
|
|||||||
+34
-3
@@ -5,17 +5,28 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
|
||||||
<title>Shadow Over Bishkek</title>
|
<title>Shadow Over Bishkek</title>
|
||||||
<style>
|
<style>
|
||||||
|
:root {
|
||||||
|
/* Visible viewport size, kept in sync by syncViewport() below.
|
||||||
|
100vh on mobile is the *largest* viewport (URL bar hidden) and
|
||||||
|
would push the bottom of the canvas off-screen. */
|
||||||
|
--app-w: 100%;
|
||||||
|
--app-h: 100dvh;
|
||||||
|
}
|
||||||
body, html {
|
body, html {
|
||||||
margin: 0; padding: 0; width: 100%; height: 100%;
|
margin: 0; padding: 0; width: 100%; height: 100%;
|
||||||
overflow: hidden; background-color: #000;
|
overflow: hidden; background-color: #000;
|
||||||
position: fixed;
|
position: fixed; top: 0; left: 0;
|
||||||
}
|
}
|
||||||
#canvas {
|
#canvas {
|
||||||
display: block;
|
display: block;
|
||||||
position: absolute;
|
position: fixed;
|
||||||
top: 0; left: 0;
|
top: 0; left: 0;
|
||||||
width: 100vw; height: 100vh;
|
width: var(--app-w); height: var(--app-h);
|
||||||
border: none;
|
border: none;
|
||||||
|
touch-action: none;
|
||||||
|
-webkit-touch-callout: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
#status { color: white; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); }
|
#status { color: white; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); }
|
||||||
@@ -49,8 +60,28 @@
|
|||||||
loadGameScript();
|
loadGameScript();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function syncViewport() {
|
||||||
|
var vv = window.visualViewport;
|
||||||
|
var w = (vv && vv.width) || window.innerWidth;
|
||||||
|
var h = (vv && vv.height) || window.innerHeight;
|
||||||
|
var root = document.documentElement;
|
||||||
|
root.style.setProperty('--app-w', Math.round(w) + 'px');
|
||||||
|
root.style.setProperty('--app-h', Math.round(h) + 'px');
|
||||||
|
}
|
||||||
|
syncViewport();
|
||||||
|
// Registered before the game script loads, so the CSS size is updated
|
||||||
|
// before the engine's own resize handler reads it.
|
||||||
|
window.addEventListener('resize', syncViewport);
|
||||||
|
if (window.visualViewport) {
|
||||||
|
window.visualViewport.addEventListener('resize', function () {
|
||||||
|
syncViewport();
|
||||||
|
window.dispatchEvent(new Event('resize'));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
window.addEventListener("orientationchange", function() {
|
window.addEventListener("orientationchange", function() {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
syncViewport();
|
||||||
window.dispatchEvent(new Event('resize'));
|
window.dispatchEvent(new Event('resize'));
|
||||||
}, 200);
|
}, 200);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -131,8 +131,14 @@ set_property(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT S
|
|||||||
# include-пути проекта
|
# include-пути проекта
|
||||||
target_include_directories(ShadowOverBishkekDemo PRIVATE
|
target_include_directories(ShadowOverBishkekDemo PRIVATE
|
||||||
"${CMAKE_CURRENT_SOURCE_DIR}/../src"
|
"${CMAKE_CURRENT_SOURCE_DIR}/../src"
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/../thirdparty/Clipper2-Clipper2_2.0.1/CPP/build/Release/include"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
#target_link_directories(ShadowOverBishkekDemo PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/../thirdparty/Clipper2-Clipper2_2.0.1/CPP/build/Release/lib")
|
||||||
|
|
||||||
|
target_link_directories(ShadowOverBishkekDemo PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/../thirdparty/Clipper2-Clipper2_2.0.1/CPP/build_debug/Debug/lib")
|
||||||
|
|
||||||
|
|
||||||
set_target_properties(ShadowOverBishkekDemo PROPERTIES
|
set_target_properties(ShadowOverBishkekDemo PROPERTIES
|
||||||
OUTPUT_NAME "ShadowOverBishkekDemo"
|
OUTPUT_NAME "ShadowOverBishkekDemo"
|
||||||
)
|
)
|
||||||
@@ -143,6 +149,7 @@ target_compile_definitions(ShadowOverBishkekDemo PRIVATE
|
|||||||
_WIN32_WINNT=0x0A00
|
_WIN32_WINNT=0x0A00
|
||||||
PNG_ENABLED
|
PNG_ENABLED
|
||||||
NETWORK
|
NETWORK
|
||||||
|
NETWORK_TLS
|
||||||
)
|
)
|
||||||
|
|
||||||
# Линкуем с SDL2main, если он вообще установлен
|
# Линкуем с SDL2main, если он вообще установлен
|
||||||
@@ -160,6 +167,8 @@ target_link_libraries(ShadowOverBishkekDemo PRIVATE
|
|||||||
boost_external_lib
|
boost_external_lib
|
||||||
sol2_external_lib
|
sol2_external_lib
|
||||||
SDL2_mixer_external_lib
|
SDL2_mixer_external_lib
|
||||||
|
openssl_ssl_external_lib
|
||||||
|
Clipper2.lib
|
||||||
)
|
)
|
||||||
|
|
||||||
# Линкуем OpenGL (Windows)
|
# Линкуем OpenGL (Windows)
|
||||||
@@ -172,6 +181,11 @@ if(WIN32)
|
|||||||
../src/game_server/RemoteGameServerHost.h
|
../src/game_server/RemoteGameServerHost.h
|
||||||
../src/game_server/RemoteGameServerHost.cpp
|
../src/game_server/RemoteGameServerHost.cpp
|
||||||
)
|
)
|
||||||
|
if(MSVC)
|
||||||
|
set_source_files_properties(
|
||||||
|
../src/game_server/WebsocketGameClient.cpp
|
||||||
|
PROPERTIES COMPILE_OPTIONS "/bigobj")
|
||||||
|
endif()
|
||||||
target_link_libraries(
|
target_link_libraries(
|
||||||
ShadowOverBishkekDemo PRIVATE opengl32 ws2_32 mswsock)
|
ShadowOverBishkekDemo PRIVATE opengl32 ws2_32 mswsock)
|
||||||
endif()
|
endif()
|
||||||
|
|||||||
@@ -2,7 +2,10 @@
|
|||||||
"campaigns": [
|
"campaigns": [
|
||||||
{
|
{
|
||||||
"id": "main",
|
"id": "main",
|
||||||
"title": { "ru": "Основная игра", "en": "Main Game" },
|
"title": {
|
||||||
|
"ru": "Основная игра",
|
||||||
|
"en": "Main Game"
|
||||||
|
},
|
||||||
"locations": [
|
"locations": [
|
||||||
"resources/config/locations/location_dorm.json",
|
"resources/config/locations/location_dorm.json",
|
||||||
"resources/config/locations/uni_exterior.json",
|
"resources/config/locations/uni_exterior.json",
|
||||||
@@ -17,12 +20,15 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "battle_zone",
|
"id": "battle_zone",
|
||||||
"title": { "ru": "Зона боя (одиночная)", "en": "Battle Zone (Single Player)" },
|
"title": {
|
||||||
|
"ru": "Зона боя (одиночная)",
|
||||||
|
"en": "Battle Zone (Single Player)"
|
||||||
|
},
|
||||||
"locations": [
|
"locations": [
|
||||||
"resources/config/locations/battle_zone_uni_exterior.json"
|
"resources/config/locations/battle_zone_korea.json"
|
||||||
],
|
],
|
||||||
"startLocation": "uni_exterior",
|
"startLocation": "park_korea",
|
||||||
"startMusic": "music/bishkek univer day.ogg",
|
"startMusic": "music/bishkek fight calm.ogg",
|
||||||
"startState": "resources/config/start_state_battle_zone_singleplayer.json",
|
"startState": "resources/config/start_state_battle_zone_singleplayer.json",
|
||||||
"quests": "resources/config/quests_battle_zone.json",
|
"quests": "resources/config/quests_battle_zone.json",
|
||||||
"tutorial": false,
|
"tutorial": false,
|
||||||
@@ -30,16 +36,24 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "battle_zone_multiplayer",
|
"id": "battle_zone_multiplayer",
|
||||||
"title": { "ru": "Зона боя (сетевая)", "en": "Battle Zone (Multiplayer)" },
|
"title": {
|
||||||
|
"ru": "Зона боя (сетевая)",
|
||||||
|
"en": "Battle Zone (Multiplayer)"
|
||||||
|
},
|
||||||
"locations": [
|
"locations": [
|
||||||
"resources/config/locations/battle_zone_uni_exterior.json"
|
"resources/config/locations/battle_zone_korea.json"
|
||||||
],
|
],
|
||||||
"startLocation": "uni_exterior",
|
"startLocation": "park_korea",
|
||||||
"startMusic": "music/bishkek univer day.ogg",
|
"startMusic": "music/bishkek fight calm.ogg",
|
||||||
"startState": "resources/config/start_state_battle_zone.json",
|
"startState": "resources/config/start_state_battle_zone.json",
|
||||||
"quests": "resources/config/quests_battle_zone.json",
|
"quests": "resources/config/quests_battle_zone.json",
|
||||||
"tutorial": false,
|
"tutorial": false,
|
||||||
"networkPlayerSlots": ["player", "player2"],
|
"networkPlayerSlots": [
|
||||||
|
"player1",
|
||||||
|
"player2",
|
||||||
|
"player3",
|
||||||
|
"player4"
|
||||||
|
],
|
||||||
"networkCompatibility": "battle-zone-lockstep-v5",
|
"networkCompatibility": "battle-zone-lockstep-v5",
|
||||||
"loadingScreen": "resources/w/ui/screen_loading.json"
|
"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",
|
"gameObjects": "resources/w/shelter/game_objects.json",
|
||||||
"interactiveObjects": "resources/w/shelter/interactive_objects.json",
|
"interactiveObjects": "resources/w/shelter/interactive_objects.json",
|
||||||
"npcs": "resources/config/npcs_battle_zone.json",
|
"npcs": "resources/config/npcs_battle_zone.json",
|
||||||
@@ -161,6 +161,168 @@
|
|||||||
"hp": 35,
|
"hp": 35,
|
||||||
"visibleRange" : 6.5,
|
"visibleRange" : 6.5,
|
||||||
"canAttack": true
|
"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",
|
"campaignId": "main",
|
||||||
"chatHistory": [
|
"chatHistory": [
|
||||||
[],
|
[],
|
||||||
@@ -16,8 +25,7 @@
|
|||||||
true
|
true
|
||||||
],
|
],
|
||||||
"currentLocationId": "",
|
"currentLocationId": "",
|
||||||
"globalFloats": {
|
"globalFloats": {},
|
||||||
},
|
|
||||||
"globalInts": {},
|
"globalInts": {},
|
||||||
"inventory": {
|
"inventory": {
|
||||||
"items": []
|
"items": []
|
||||||
@@ -494,6 +502,22 @@
|
|||||||
-15.038200378417969
|
-15.038200378417969
|
||||||
],
|
],
|
||||||
"hp": 100.0,
|
"hp": 100.0,
|
||||||
|
"movedLastTick": false,
|
||||||
|
"movementBlockedTicks": 0,
|
||||||
|
"navigationRetryCooldownIndex": 0,
|
||||||
|
"navigationRetryCooldownTicks": [
|
||||||
|
10,
|
||||||
|
4,
|
||||||
|
5,
|
||||||
|
8,
|
||||||
|
11,
|
||||||
|
9,
|
||||||
|
6,
|
||||||
|
3,
|
||||||
|
2,
|
||||||
|
7
|
||||||
|
],
|
||||||
|
"navigationRetryTicks": 0,
|
||||||
"pathWaypoints": [
|
"pathWaypoints": [
|
||||||
[
|
[
|
||||||
-6.296899795532227,
|
-6.296899795532227,
|
||||||
@@ -530,8 +554,6 @@
|
|||||||
},
|
},
|
||||||
"state": {
|
"state": {
|
||||||
"activeNavigationIndex": 0,
|
"activeNavigationIndex": 0,
|
||||||
"cameraAzimuth": -2.3499999046325684,
|
|
||||||
"cameraInclination": 1.103600025177002,
|
|
||||||
"targetInteractNpcId": "",
|
"targetInteractNpcId": "",
|
||||||
"targetInteractiveObjectId": "",
|
"targetInteractiveObjectId": "",
|
||||||
"targetTeleportZoneId": "",
|
"targetTeleportZoneId": "",
|
||||||
@@ -574,10 +596,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"money": 5500,
|
"money": 5500,
|
||||||
"playerHp": 200.0,
|
|
||||||
"playerMaxHp": 200.0,
|
"playerMaxHp": 200.0,
|
||||||
"players": {
|
"players": {
|
||||||
"player": {
|
"player1": {
|
||||||
"animationFrame": 0.0,
|
"animationFrame": 0.0,
|
||||||
"attackTargetId": "",
|
"attackTargetId": "",
|
||||||
"attack_cooldown": 0.0,
|
"attack_cooldown": 0.0,
|
||||||
@@ -594,6 +615,11 @@
|
|||||||
0.0
|
0.0
|
||||||
],
|
],
|
||||||
"hp": 200.0,
|
"hp": 200.0,
|
||||||
|
"movedLastTick": false,
|
||||||
|
"movementBlockedTicks": 0,
|
||||||
|
"navigationRetryCooldownIndex": 0,
|
||||||
|
"navigationRetryCooldownTicks": [],
|
||||||
|
"navigationRetryTicks": 0,
|
||||||
"pathWaypoints": [
|
"pathWaypoints": [
|
||||||
[
|
[
|
||||||
6.8031005859375,
|
6.8031005859375,
|
||||||
@@ -764,12 +790,14 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"savedAt": "2026-10-30 14:48",
|
"savedAt": "2026-11-10 20:38",
|
||||||
"schemaVersion": 1,
|
"schemaVersion": 5,
|
||||||
"taxiIsCalled": false,
|
"taxiIsCalled": false,
|
||||||
"tutorialJournalScreenOpened": false,
|
"tutorialJournalScreenOpened": false,
|
||||||
"tutorialNeedOpenTaxiScreen": false,
|
"tutorialNeedOpenTaxiScreen": false,
|
||||||
"tutorialPhoneChatScreenOpened": false,
|
"tutorialPhoneChatScreenOpened": false,
|
||||||
"tutorialStep": 0,
|
"tutorialStep": 0,
|
||||||
"tutorialTaxiHintDone": false
|
"tutorialTaxiHintDone": false
|
||||||
|
},
|
||||||
|
"persistenceVersion": 1
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"localState": {
|
"localState": {
|
||||||
"locations": {
|
"locations": {
|
||||||
"uni_exterior": {
|
"park_korea": {
|
||||||
"cameraAzimuth": -2.3499999046325684,
|
"cameraAzimuth": -2.3499999046325684,
|
||||||
"cameraInclination": 1.103600025177002
|
"cameraInclination": 1.103600025177002
|
||||||
}
|
}
|
||||||
@@ -10,9 +10,12 @@
|
|||||||
"networkState": {
|
"networkState": {
|
||||||
"campaignId": "battle_zone",
|
"campaignId": "battle_zone",
|
||||||
"chatHistory": [
|
"chatHistory": [
|
||||||
[],
|
[
|
||||||
[],
|
],
|
||||||
[]
|
[
|
||||||
|
],
|
||||||
|
[
|
||||||
|
]
|
||||||
],
|
],
|
||||||
"chatPreviewMsg": [
|
"chatPreviewMsg": [
|
||||||
"",
|
"",
|
||||||
@@ -27,15 +30,17 @@
|
|||||||
"currentLocationId": "",
|
"currentLocationId": "",
|
||||||
"globalFloats": {
|
"globalFloats": {
|
||||||
},
|
},
|
||||||
"globalInts": {},
|
"globalInts": {
|
||||||
|
},
|
||||||
"inventory": {
|
"inventory": {
|
||||||
"items": []
|
"items": [
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"isDarklands": true,
|
"isDarklands": true,
|
||||||
"isDawn": false,
|
"isDawn": false,
|
||||||
"isNight": true,
|
"isNight": true,
|
||||||
"locations": {
|
"locations": {
|
||||||
"uni_exterior": {
|
"park_korea": {
|
||||||
"dialogue": {
|
"dialogue": {
|
||||||
"active": false
|
"active": false
|
||||||
},
|
},
|
||||||
@@ -44,7 +49,7 @@
|
|||||||
"alpha": 1.0,
|
"alpha": 1.0,
|
||||||
"isActive": true,
|
"isActive": true,
|
||||||
"isAnimating": false,
|
"isAnimating": false,
|
||||||
"positionX": 0.6000000238418579,
|
"positionX": 0.60000002384185791,
|
||||||
"positionY": 0.45736101269721985,
|
"positionY": 0.45736101269721985,
|
||||||
"positionZ": 3.8499999046325684,
|
"positionZ": 3.8499999046325684,
|
||||||
"rotationY": 0.0,
|
"rotationY": 0.0,
|
||||||
@@ -54,7 +59,7 @@
|
|||||||
"alpha": 1.0,
|
"alpha": 1.0,
|
||||||
"isActive": true,
|
"isActive": true,
|
||||||
"isAnimating": false,
|
"isAnimating": false,
|
||||||
"positionX": 1.399999976158142,
|
"positionX": 1.3999999761581421,
|
||||||
"positionY": 0.45736101269721985,
|
"positionY": 0.45736101269721985,
|
||||||
"positionZ": 3.8499999046325684,
|
"positionZ": 3.8499999046325684,
|
||||||
"rotationY": 0.0,
|
"rotationY": 0.0,
|
||||||
@@ -64,7 +69,7 @@
|
|||||||
"alpha": 1.0,
|
"alpha": 1.0,
|
||||||
"isActive": true,
|
"isActive": true,
|
||||||
"isAnimating": false,
|
"isAnimating": false,
|
||||||
"positionX": 2.200000047683716,
|
"positionX": 2.2000000476837158,
|
||||||
"positionY": 0.45736101269721985,
|
"positionY": 0.45736101269721985,
|
||||||
"positionZ": 3.8499999046325684,
|
"positionZ": 3.8499999046325684,
|
||||||
"rotationY": 0.0,
|
"rotationY": 0.0,
|
||||||
@@ -85,7 +90,7 @@
|
|||||||
"isActive": true,
|
"isActive": true,
|
||||||
"isAnimating": false,
|
"isAnimating": false,
|
||||||
"positionX": 0.0,
|
"positionX": 0.0,
|
||||||
"positionY": 3.200000047683716,
|
"positionY": 3.2000000476837158,
|
||||||
"positionZ": 0.0,
|
"positionZ": 0.0,
|
||||||
"rotationY": 0.0,
|
"rotationY": 0.0,
|
||||||
"scale": 1.0
|
"scale": 1.0
|
||||||
@@ -95,7 +100,7 @@
|
|||||||
"isActive": true,
|
"isActive": true,
|
||||||
"isAnimating": false,
|
"isAnimating": false,
|
||||||
"positionX": -1.0,
|
"positionX": -1.0,
|
||||||
"positionY": 0.9150000214576721,
|
"positionY": 0.91500002145767212,
|
||||||
"positionZ": -3.9000000953674316,
|
"positionZ": -3.9000000953674316,
|
||||||
"rotationY": 0.0,
|
"rotationY": 0.0,
|
||||||
"scale": 1.0
|
"scale": 1.0
|
||||||
@@ -105,7 +110,7 @@
|
|||||||
"isActive": true,
|
"isActive": true,
|
||||||
"isAnimating": false,
|
"isAnimating": false,
|
||||||
"positionX": 0.0,
|
"positionX": 0.0,
|
||||||
"positionY": 0.9150000214576721,
|
"positionY": 0.91500002145767212,
|
||||||
"positionZ": -3.9000000953674316,
|
"positionZ": -3.9000000953674316,
|
||||||
"rotationY": 0.0,
|
"rotationY": 0.0,
|
||||||
"scale": 1.0
|
"scale": 1.0
|
||||||
@@ -115,7 +120,7 @@
|
|||||||
"isActive": true,
|
"isActive": true,
|
||||||
"isAnimating": false,
|
"isAnimating": false,
|
||||||
"positionX": 1.0,
|
"positionX": 1.0,
|
||||||
"positionY": 0.9150000214576721,
|
"positionY": 0.91500002145767212,
|
||||||
"positionZ": -3.9000000953674316,
|
"positionZ": -3.9000000953674316,
|
||||||
"rotationY": 0.0,
|
"rotationY": 0.0,
|
||||||
"scale": 1.0
|
"scale": 1.0
|
||||||
@@ -134,7 +139,7 @@
|
|||||||
"facingAngle": 0.0,
|
"facingAngle": 0.0,
|
||||||
"homeDriftCheckTimer": 5.0,
|
"homeDriftCheckTimer": 5.0,
|
||||||
"homePosition": [
|
"homePosition": [
|
||||||
-5.599999904632568,
|
-5.5999999046325684,
|
||||||
0.0,
|
0.0,
|
||||||
-12.0
|
-12.0
|
||||||
],
|
],
|
||||||
@@ -155,14 +160,15 @@
|
|||||||
11
|
11
|
||||||
],
|
],
|
||||||
"navigationRetryTicks": 0,
|
"navigationRetryTicks": 0,
|
||||||
"pathWaypoints": [],
|
"pathWaypoints": [
|
||||||
|
],
|
||||||
"position": [
|
"position": [
|
||||||
-5.599999904632568,
|
-5.5999999046325684,
|
||||||
0.0,
|
0.0,
|
||||||
-12.0
|
-12.0
|
||||||
],
|
],
|
||||||
"requestedWalkTarget": [
|
"requestedWalkTarget": [
|
||||||
-5.599999904632568,
|
-5.5999999046325684,
|
||||||
0.0,
|
0.0,
|
||||||
-12.0
|
-12.0
|
||||||
],
|
],
|
||||||
@@ -170,7 +176,7 @@
|
|||||||
"targetFacingAngle": 0.0,
|
"targetFacingAngle": 0.0,
|
||||||
"visibleRange": 6.5,
|
"visibleRange": 6.5,
|
||||||
"walkTarget": [
|
"walkTarget": [
|
||||||
-5.599999904632568,
|
-5.5999999046325684,
|
||||||
0.0,
|
0.0,
|
||||||
-12.0
|
-12.0
|
||||||
]
|
]
|
||||||
@@ -187,7 +193,7 @@
|
|||||||
"facingAngle": 0.0,
|
"facingAngle": 0.0,
|
||||||
"homeDriftCheckTimer": 5.0,
|
"homeDriftCheckTimer": 5.0,
|
||||||
"homePosition": [
|
"homePosition": [
|
||||||
-6.400000095367432,
|
-6.4000000953674316,
|
||||||
0.0,
|
0.0,
|
||||||
-12.0
|
-12.0
|
||||||
],
|
],
|
||||||
@@ -208,14 +214,15 @@
|
|||||||
6
|
6
|
||||||
],
|
],
|
||||||
"navigationRetryTicks": 0,
|
"navigationRetryTicks": 0,
|
||||||
"pathWaypoints": [],
|
"pathWaypoints": [
|
||||||
|
],
|
||||||
"position": [
|
"position": [
|
||||||
-6.400000095367432,
|
-6.4000000953674316,
|
||||||
0.0,
|
0.0,
|
||||||
-12.0
|
-12.0
|
||||||
],
|
],
|
||||||
"requestedWalkTarget": [
|
"requestedWalkTarget": [
|
||||||
-6.400000095367432,
|
-6.4000000953674316,
|
||||||
0.0,
|
0.0,
|
||||||
-12.0
|
-12.0
|
||||||
],
|
],
|
||||||
@@ -223,7 +230,7 @@
|
|||||||
"targetFacingAngle": 0.0,
|
"targetFacingAngle": 0.0,
|
||||||
"visibleRange": 6.5,
|
"visibleRange": 6.5,
|
||||||
"walkTarget": [
|
"walkTarget": [
|
||||||
-6.400000095367432,
|
-6.4000000953674316,
|
||||||
0.0,
|
0.0,
|
||||||
-12.0
|
-12.0
|
||||||
]
|
]
|
||||||
@@ -240,7 +247,7 @@
|
|||||||
"facingAngle": 0.0,
|
"facingAngle": 0.0,
|
||||||
"homeDriftCheckTimer": 5.0,
|
"homeDriftCheckTimer": 5.0,
|
||||||
"homePosition": [
|
"homePosition": [
|
||||||
-5.599999904632568,
|
-5.5999999046325684,
|
||||||
0.0,
|
0.0,
|
||||||
-12.5
|
-12.5
|
||||||
],
|
],
|
||||||
@@ -261,14 +268,15 @@
|
|||||||
11
|
11
|
||||||
],
|
],
|
||||||
"navigationRetryTicks": 0,
|
"navigationRetryTicks": 0,
|
||||||
"pathWaypoints": [],
|
"pathWaypoints": [
|
||||||
|
],
|
||||||
"position": [
|
"position": [
|
||||||
-5.599999904632568,
|
-5.5999999046325684,
|
||||||
0.0,
|
0.0,
|
||||||
-12.5
|
-12.5
|
||||||
],
|
],
|
||||||
"requestedWalkTarget": [
|
"requestedWalkTarget": [
|
||||||
-5.599999904632568,
|
-5.5999999046325684,
|
||||||
0.0,
|
0.0,
|
||||||
-12.5
|
-12.5
|
||||||
],
|
],
|
||||||
@@ -276,7 +284,7 @@
|
|||||||
"targetFacingAngle": 0.0,
|
"targetFacingAngle": 0.0,
|
||||||
"visibleRange": 6.5,
|
"visibleRange": 6.5,
|
||||||
"walkTarget": [
|
"walkTarget": [
|
||||||
-5.599999904632568,
|
-5.5999999046325684,
|
||||||
0.0,
|
0.0,
|
||||||
-12.5
|
-12.5
|
||||||
]
|
]
|
||||||
@@ -293,7 +301,7 @@
|
|||||||
"facingAngle": 0.0,
|
"facingAngle": 0.0,
|
||||||
"homeDriftCheckTimer": 5.0,
|
"homeDriftCheckTimer": 5.0,
|
||||||
"homePosition": [
|
"homePosition": [
|
||||||
-6.400000095367432,
|
-6.4000000953674316,
|
||||||
0.0,
|
0.0,
|
||||||
-12.5
|
-12.5
|
||||||
],
|
],
|
||||||
@@ -314,14 +322,15 @@
|
|||||||
6
|
6
|
||||||
],
|
],
|
||||||
"navigationRetryTicks": 0,
|
"navigationRetryTicks": 0,
|
||||||
"pathWaypoints": [],
|
"pathWaypoints": [
|
||||||
|
],
|
||||||
"position": [
|
"position": [
|
||||||
-6.400000095367432,
|
-6.4000000953674316,
|
||||||
0.0,
|
0.0,
|
||||||
-12.5
|
-12.5
|
||||||
],
|
],
|
||||||
"requestedWalkTarget": [
|
"requestedWalkTarget": [
|
||||||
-6.400000095367432,
|
-6.4000000953674316,
|
||||||
0.0,
|
0.0,
|
||||||
-12.5
|
-12.5
|
||||||
],
|
],
|
||||||
@@ -329,7 +338,7 @@
|
|||||||
"targetFacingAngle": 0.0,
|
"targetFacingAngle": 0.0,
|
||||||
"visibleRange": 6.5,
|
"visibleRange": 6.5,
|
||||||
"walkTarget": [
|
"walkTarget": [
|
||||||
-6.400000095367432,
|
-6.4000000953674316,
|
||||||
0.0,
|
0.0,
|
||||||
-12.5
|
-12.5
|
||||||
]
|
]
|
||||||
@@ -346,7 +355,7 @@
|
|||||||
"facingAngle": 0.0,
|
"facingAngle": 0.0,
|
||||||
"homeDriftCheckTimer": 5.0,
|
"homeDriftCheckTimer": 5.0,
|
||||||
"homePosition": [
|
"homePosition": [
|
||||||
-5.599999904632568,
|
-5.5999999046325684,
|
||||||
0.0,
|
0.0,
|
||||||
-13.0
|
-13.0
|
||||||
],
|
],
|
||||||
@@ -367,14 +376,15 @@
|
|||||||
11
|
11
|
||||||
],
|
],
|
||||||
"navigationRetryTicks": 0,
|
"navigationRetryTicks": 0,
|
||||||
"pathWaypoints": [],
|
"pathWaypoints": [
|
||||||
|
],
|
||||||
"position": [
|
"position": [
|
||||||
-5.599999904632568,
|
-5.5999999046325684,
|
||||||
0.0,
|
0.0,
|
||||||
-13.0
|
-13.0
|
||||||
],
|
],
|
||||||
"requestedWalkTarget": [
|
"requestedWalkTarget": [
|
||||||
-5.599999904632568,
|
-5.5999999046325684,
|
||||||
0.0,
|
0.0,
|
||||||
-13.0
|
-13.0
|
||||||
],
|
],
|
||||||
@@ -382,7 +392,7 @@
|
|||||||
"targetFacingAngle": 0.0,
|
"targetFacingAngle": 0.0,
|
||||||
"visibleRange": 6.5,
|
"visibleRange": 6.5,
|
||||||
"walkTarget": [
|
"walkTarget": [
|
||||||
-5.599999904632568,
|
-5.5999999046325684,
|
||||||
0.0,
|
0.0,
|
||||||
-13.0
|
-13.0
|
||||||
]
|
]
|
||||||
@@ -399,7 +409,7 @@
|
|||||||
"facingAngle": 0.0,
|
"facingAngle": 0.0,
|
||||||
"homeDriftCheckTimer": 5.0,
|
"homeDriftCheckTimer": 5.0,
|
||||||
"homePosition": [
|
"homePosition": [
|
||||||
-6.400000095367432,
|
-6.4000000953674316,
|
||||||
0.0,
|
0.0,
|
||||||
-13.0
|
-13.0
|
||||||
],
|
],
|
||||||
@@ -420,14 +430,15 @@
|
|||||||
2
|
2
|
||||||
],
|
],
|
||||||
"navigationRetryTicks": 0,
|
"navigationRetryTicks": 0,
|
||||||
"pathWaypoints": [],
|
"pathWaypoints": [
|
||||||
|
],
|
||||||
"position": [
|
"position": [
|
||||||
-6.400000095367432,
|
-6.4000000953674316,
|
||||||
0.0,
|
0.0,
|
||||||
-13.0
|
-13.0
|
||||||
],
|
],
|
||||||
"requestedWalkTarget": [
|
"requestedWalkTarget": [
|
||||||
-6.400000095367432,
|
-6.4000000953674316,
|
||||||
0.0,
|
0.0,
|
||||||
-13.0
|
-13.0
|
||||||
],
|
],
|
||||||
@@ -435,13 +446,338 @@
|
|||||||
"targetFacingAngle": 0.0,
|
"targetFacingAngle": 0.0,
|
||||||
"visibleRange": 6.5,
|
"visibleRange": 6.5,
|
||||||
"walkTarget": [
|
"walkTarget": [
|
||||||
-6.400000095367432,
|
-6.4000000953674316,
|
||||||
0.0,
|
0.0,
|
||||||
-13.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": {
|
"state": {
|
||||||
"activeNavigationIndex": 0,
|
"activeNavigationIndex": 0,
|
||||||
"targetInteractNpcId": "",
|
"targetInteractNpcId": "",
|
||||||
@@ -460,61 +796,23 @@
|
|||||||
"rotation": 0.0,
|
"rotation": 0.0,
|
||||||
"steeringAngle": 0.0,
|
"steeringAngle": 0.0,
|
||||||
"velocity": 0.0,
|
"velocity": 0.0,
|
||||||
"waypoints": [],
|
"waypoints": [
|
||||||
|
],
|
||||||
"wheelAngle": 0.0
|
"wheelAngle": 0.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"teleportZones": {},
|
"teleportZones": {
|
||||||
"triggerZones": {}
|
},
|
||||||
|
"triggerZones": {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"money": 5500,
|
"money": 5500,
|
||||||
"playerHp": 200.0,
|
"playerHp": 200.0,
|
||||||
"playerMaxHp": 200.0,
|
"playerMaxHp": 200.0,
|
||||||
"players": {
|
"players": {
|
||||||
"player2": {
|
"player1": {
|
||||||
"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": {
|
|
||||||
"animationFrame": 0.0,
|
"animationFrame": 0.0,
|
||||||
"attackTargetId": "",
|
"attackTargetId": "",
|
||||||
"attack_cooldown": 0.0,
|
"attack_cooldown": 0.0,
|
||||||
@@ -533,18 +831,18 @@
|
|||||||
"hp": 200.0,
|
"hp": 200.0,
|
||||||
"pathWaypoints": [
|
"pathWaypoints": [
|
||||||
[
|
[
|
||||||
-5.8,
|
-5.4,
|
||||||
0.0,
|
0.0,
|
||||||
12.8
|
12.8
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"position": [
|
"position": [
|
||||||
-5.8,
|
-5.4,
|
||||||
0.0,
|
0.0,
|
||||||
12.8
|
12.8
|
||||||
],
|
],
|
||||||
"requestedWalkTarget": [
|
"requestedWalkTarget": [
|
||||||
-5.8,
|
-5.4,
|
||||||
0.0,
|
0.0,
|
||||||
12.8
|
12.8
|
||||||
],
|
],
|
||||||
@@ -552,14 +850,144 @@
|
|||||||
"targetFacingAngle": 0.0,
|
"targetFacingAngle": 0.0,
|
||||||
"visibleRange": 6.0,
|
"visibleRange": 6.0,
|
||||||
"walkTarget": [
|
"walkTarget": [
|
||||||
-5.8,
|
-5.4,
|
||||||
0.0,
|
0.0,
|
||||||
12.8
|
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": {
|
"questJournal": {
|
||||||
"quests": []
|
"quests": [
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"savedAt": "2026-11-05 17:43",
|
"savedAt": "2026-11-05 17:43",
|
||||||
"schemaVersion": 5,
|
"schemaVersion": 5,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"localState": {
|
"localState": {
|
||||||
"locations": {
|
"locations": {
|
||||||
"uni_exterior": {
|
"park_korea": {
|
||||||
"cameraAzimuth": -2.3499999046325684,
|
"cameraAzimuth": -2.3499999046325684,
|
||||||
"cameraInclination": 1.103600025177002
|
"cameraInclination": 1.103600025177002
|
||||||
}
|
}
|
||||||
@@ -10,9 +10,12 @@
|
|||||||
"networkState": {
|
"networkState": {
|
||||||
"campaignId": "battle_zone",
|
"campaignId": "battle_zone",
|
||||||
"chatHistory": [
|
"chatHistory": [
|
||||||
[],
|
[
|
||||||
[],
|
],
|
||||||
[]
|
[
|
||||||
|
],
|
||||||
|
[
|
||||||
|
]
|
||||||
],
|
],
|
||||||
"chatPreviewMsg": [
|
"chatPreviewMsg": [
|
||||||
"",
|
"",
|
||||||
@@ -27,15 +30,17 @@
|
|||||||
"currentLocationId": "",
|
"currentLocationId": "",
|
||||||
"globalFloats": {
|
"globalFloats": {
|
||||||
},
|
},
|
||||||
"globalInts": {},
|
"globalInts": {
|
||||||
|
},
|
||||||
"inventory": {
|
"inventory": {
|
||||||
"items": []
|
"items": [
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"isDarklands": true,
|
"isDarklands": true,
|
||||||
"isDawn": false,
|
"isDawn": false,
|
||||||
"isNight": true,
|
"isNight": true,
|
||||||
"locations": {
|
"locations": {
|
||||||
"uni_exterior": {
|
"park_korea": {
|
||||||
"dialogue": {
|
"dialogue": {
|
||||||
"active": false
|
"active": false
|
||||||
},
|
},
|
||||||
@@ -44,7 +49,7 @@
|
|||||||
"alpha": 1.0,
|
"alpha": 1.0,
|
||||||
"isActive": true,
|
"isActive": true,
|
||||||
"isAnimating": false,
|
"isAnimating": false,
|
||||||
"positionX": 0.6000000238418579,
|
"positionX": 0.60000002384185791,
|
||||||
"positionY": 0.45736101269721985,
|
"positionY": 0.45736101269721985,
|
||||||
"positionZ": 3.8499999046325684,
|
"positionZ": 3.8499999046325684,
|
||||||
"rotationY": 0.0,
|
"rotationY": 0.0,
|
||||||
@@ -54,7 +59,7 @@
|
|||||||
"alpha": 1.0,
|
"alpha": 1.0,
|
||||||
"isActive": true,
|
"isActive": true,
|
||||||
"isAnimating": false,
|
"isAnimating": false,
|
||||||
"positionX": 1.399999976158142,
|
"positionX": 1.3999999761581421,
|
||||||
"positionY": 0.45736101269721985,
|
"positionY": 0.45736101269721985,
|
||||||
"positionZ": 3.8499999046325684,
|
"positionZ": 3.8499999046325684,
|
||||||
"rotationY": 0.0,
|
"rotationY": 0.0,
|
||||||
@@ -64,7 +69,7 @@
|
|||||||
"alpha": 1.0,
|
"alpha": 1.0,
|
||||||
"isActive": true,
|
"isActive": true,
|
||||||
"isAnimating": false,
|
"isAnimating": false,
|
||||||
"positionX": 2.200000047683716,
|
"positionX": 2.2000000476837158,
|
||||||
"positionY": 0.45736101269721985,
|
"positionY": 0.45736101269721985,
|
||||||
"positionZ": 3.8499999046325684,
|
"positionZ": 3.8499999046325684,
|
||||||
"rotationY": 0.0,
|
"rotationY": 0.0,
|
||||||
@@ -85,7 +90,7 @@
|
|||||||
"isActive": true,
|
"isActive": true,
|
||||||
"isAnimating": false,
|
"isAnimating": false,
|
||||||
"positionX": 0.0,
|
"positionX": 0.0,
|
||||||
"positionY": 3.200000047683716,
|
"positionY": 3.2000000476837158,
|
||||||
"positionZ": 0.0,
|
"positionZ": 0.0,
|
||||||
"rotationY": 0.0,
|
"rotationY": 0.0,
|
||||||
"scale": 1.0
|
"scale": 1.0
|
||||||
@@ -95,7 +100,7 @@
|
|||||||
"isActive": true,
|
"isActive": true,
|
||||||
"isAnimating": false,
|
"isAnimating": false,
|
||||||
"positionX": -1.0,
|
"positionX": -1.0,
|
||||||
"positionY": 0.9150000214576721,
|
"positionY": 0.91500002145767212,
|
||||||
"positionZ": -3.9000000953674316,
|
"positionZ": -3.9000000953674316,
|
||||||
"rotationY": 0.0,
|
"rotationY": 0.0,
|
||||||
"scale": 1.0
|
"scale": 1.0
|
||||||
@@ -105,7 +110,7 @@
|
|||||||
"isActive": true,
|
"isActive": true,
|
||||||
"isAnimating": false,
|
"isAnimating": false,
|
||||||
"positionX": 0.0,
|
"positionX": 0.0,
|
||||||
"positionY": 0.9150000214576721,
|
"positionY": 0.91500002145767212,
|
||||||
"positionZ": -3.9000000953674316,
|
"positionZ": -3.9000000953674316,
|
||||||
"rotationY": 0.0,
|
"rotationY": 0.0,
|
||||||
"scale": 1.0
|
"scale": 1.0
|
||||||
@@ -115,7 +120,7 @@
|
|||||||
"isActive": true,
|
"isActive": true,
|
||||||
"isAnimating": false,
|
"isAnimating": false,
|
||||||
"positionX": 1.0,
|
"positionX": 1.0,
|
||||||
"positionY": 0.9150000214576721,
|
"positionY": 0.91500002145767212,
|
||||||
"positionZ": -3.9000000953674316,
|
"positionZ": -3.9000000953674316,
|
||||||
"rotationY": 0.0,
|
"rotationY": 0.0,
|
||||||
"scale": 1.0
|
"scale": 1.0
|
||||||
@@ -134,7 +139,7 @@
|
|||||||
"facingAngle": 0.0,
|
"facingAngle": 0.0,
|
||||||
"homeDriftCheckTimer": 5.0,
|
"homeDriftCheckTimer": 5.0,
|
||||||
"homePosition": [
|
"homePosition": [
|
||||||
-5.599999904632568,
|
-5.5999999046325684,
|
||||||
0.0,
|
0.0,
|
||||||
-12.0
|
-12.0
|
||||||
],
|
],
|
||||||
@@ -155,14 +160,15 @@
|
|||||||
11
|
11
|
||||||
],
|
],
|
||||||
"navigationRetryTicks": 0,
|
"navigationRetryTicks": 0,
|
||||||
"pathWaypoints": [],
|
"pathWaypoints": [
|
||||||
|
],
|
||||||
"position": [
|
"position": [
|
||||||
-5.599999904632568,
|
-5.5999999046325684,
|
||||||
0.0,
|
0.0,
|
||||||
-12.0
|
-12.0
|
||||||
],
|
],
|
||||||
"requestedWalkTarget": [
|
"requestedWalkTarget": [
|
||||||
-5.599999904632568,
|
-5.5999999046325684,
|
||||||
0.0,
|
0.0,
|
||||||
-12.0
|
-12.0
|
||||||
],
|
],
|
||||||
@@ -170,7 +176,7 @@
|
|||||||
"targetFacingAngle": 0.0,
|
"targetFacingAngle": 0.0,
|
||||||
"visibleRange": 6.5,
|
"visibleRange": 6.5,
|
||||||
"walkTarget": [
|
"walkTarget": [
|
||||||
-5.599999904632568,
|
-5.5999999046325684,
|
||||||
0.0,
|
0.0,
|
||||||
-12.0
|
-12.0
|
||||||
]
|
]
|
||||||
@@ -187,7 +193,7 @@
|
|||||||
"facingAngle": 0.0,
|
"facingAngle": 0.0,
|
||||||
"homeDriftCheckTimer": 5.0,
|
"homeDriftCheckTimer": 5.0,
|
||||||
"homePosition": [
|
"homePosition": [
|
||||||
-6.400000095367432,
|
-6.4000000953674316,
|
||||||
0.0,
|
0.0,
|
||||||
-12.0
|
-12.0
|
||||||
],
|
],
|
||||||
@@ -208,14 +214,15 @@
|
|||||||
6
|
6
|
||||||
],
|
],
|
||||||
"navigationRetryTicks": 0,
|
"navigationRetryTicks": 0,
|
||||||
"pathWaypoints": [],
|
"pathWaypoints": [
|
||||||
|
],
|
||||||
"position": [
|
"position": [
|
||||||
-6.400000095367432,
|
-6.4000000953674316,
|
||||||
0.0,
|
0.0,
|
||||||
-12.0
|
-12.0
|
||||||
],
|
],
|
||||||
"requestedWalkTarget": [
|
"requestedWalkTarget": [
|
||||||
-6.400000095367432,
|
-6.4000000953674316,
|
||||||
0.0,
|
0.0,
|
||||||
-12.0
|
-12.0
|
||||||
],
|
],
|
||||||
@@ -223,7 +230,7 @@
|
|||||||
"targetFacingAngle": 0.0,
|
"targetFacingAngle": 0.0,
|
||||||
"visibleRange": 6.5,
|
"visibleRange": 6.5,
|
||||||
"walkTarget": [
|
"walkTarget": [
|
||||||
-6.400000095367432,
|
-6.4000000953674316,
|
||||||
0.0,
|
0.0,
|
||||||
-12.0
|
-12.0
|
||||||
]
|
]
|
||||||
@@ -240,7 +247,7 @@
|
|||||||
"facingAngle": 0.0,
|
"facingAngle": 0.0,
|
||||||
"homeDriftCheckTimer": 5.0,
|
"homeDriftCheckTimer": 5.0,
|
||||||
"homePosition": [
|
"homePosition": [
|
||||||
-5.599999904632568,
|
-5.5999999046325684,
|
||||||
0.0,
|
0.0,
|
||||||
-12.5
|
-12.5
|
||||||
],
|
],
|
||||||
@@ -261,14 +268,15 @@
|
|||||||
11
|
11
|
||||||
],
|
],
|
||||||
"navigationRetryTicks": 0,
|
"navigationRetryTicks": 0,
|
||||||
"pathWaypoints": [],
|
"pathWaypoints": [
|
||||||
|
],
|
||||||
"position": [
|
"position": [
|
||||||
-5.599999904632568,
|
-5.5999999046325684,
|
||||||
0.0,
|
0.0,
|
||||||
-12.5
|
-12.5
|
||||||
],
|
],
|
||||||
"requestedWalkTarget": [
|
"requestedWalkTarget": [
|
||||||
-5.599999904632568,
|
-5.5999999046325684,
|
||||||
0.0,
|
0.0,
|
||||||
-12.5
|
-12.5
|
||||||
],
|
],
|
||||||
@@ -276,7 +284,7 @@
|
|||||||
"targetFacingAngle": 0.0,
|
"targetFacingAngle": 0.0,
|
||||||
"visibleRange": 6.5,
|
"visibleRange": 6.5,
|
||||||
"walkTarget": [
|
"walkTarget": [
|
||||||
-5.599999904632568,
|
-5.5999999046325684,
|
||||||
0.0,
|
0.0,
|
||||||
-12.5
|
-12.5
|
||||||
]
|
]
|
||||||
@@ -293,7 +301,7 @@
|
|||||||
"facingAngle": 0.0,
|
"facingAngle": 0.0,
|
||||||
"homeDriftCheckTimer": 5.0,
|
"homeDriftCheckTimer": 5.0,
|
||||||
"homePosition": [
|
"homePosition": [
|
||||||
-6.400000095367432,
|
-6.4000000953674316,
|
||||||
0.0,
|
0.0,
|
||||||
-12.5
|
-12.5
|
||||||
],
|
],
|
||||||
@@ -314,14 +322,15 @@
|
|||||||
6
|
6
|
||||||
],
|
],
|
||||||
"navigationRetryTicks": 0,
|
"navigationRetryTicks": 0,
|
||||||
"pathWaypoints": [],
|
"pathWaypoints": [
|
||||||
|
],
|
||||||
"position": [
|
"position": [
|
||||||
-6.400000095367432,
|
-6.4000000953674316,
|
||||||
0.0,
|
0.0,
|
||||||
-12.5
|
-12.5
|
||||||
],
|
],
|
||||||
"requestedWalkTarget": [
|
"requestedWalkTarget": [
|
||||||
-6.400000095367432,
|
-6.4000000953674316,
|
||||||
0.0,
|
0.0,
|
||||||
-12.5
|
-12.5
|
||||||
],
|
],
|
||||||
@@ -329,7 +338,7 @@
|
|||||||
"targetFacingAngle": 0.0,
|
"targetFacingAngle": 0.0,
|
||||||
"visibleRange": 6.5,
|
"visibleRange": 6.5,
|
||||||
"walkTarget": [
|
"walkTarget": [
|
||||||
-6.400000095367432,
|
-6.4000000953674316,
|
||||||
0.0,
|
0.0,
|
||||||
-12.5
|
-12.5
|
||||||
]
|
]
|
||||||
@@ -346,7 +355,7 @@
|
|||||||
"facingAngle": 0.0,
|
"facingAngle": 0.0,
|
||||||
"homeDriftCheckTimer": 5.0,
|
"homeDriftCheckTimer": 5.0,
|
||||||
"homePosition": [
|
"homePosition": [
|
||||||
-5.599999904632568,
|
-5.5999999046325684,
|
||||||
0.0,
|
0.0,
|
||||||
-13.0
|
-13.0
|
||||||
],
|
],
|
||||||
@@ -367,14 +376,15 @@
|
|||||||
11
|
11
|
||||||
],
|
],
|
||||||
"navigationRetryTicks": 0,
|
"navigationRetryTicks": 0,
|
||||||
"pathWaypoints": [],
|
"pathWaypoints": [
|
||||||
|
],
|
||||||
"position": [
|
"position": [
|
||||||
-5.599999904632568,
|
-5.5999999046325684,
|
||||||
0.0,
|
0.0,
|
||||||
-13.0
|
-13.0
|
||||||
],
|
],
|
||||||
"requestedWalkTarget": [
|
"requestedWalkTarget": [
|
||||||
-5.599999904632568,
|
-5.5999999046325684,
|
||||||
0.0,
|
0.0,
|
||||||
-13.0
|
-13.0
|
||||||
],
|
],
|
||||||
@@ -382,7 +392,7 @@
|
|||||||
"targetFacingAngle": 0.0,
|
"targetFacingAngle": 0.0,
|
||||||
"visibleRange": 6.5,
|
"visibleRange": 6.5,
|
||||||
"walkTarget": [
|
"walkTarget": [
|
||||||
-5.599999904632568,
|
-5.5999999046325684,
|
||||||
0.0,
|
0.0,
|
||||||
-13.0
|
-13.0
|
||||||
]
|
]
|
||||||
@@ -399,7 +409,7 @@
|
|||||||
"facingAngle": 0.0,
|
"facingAngle": 0.0,
|
||||||
"homeDriftCheckTimer": 5.0,
|
"homeDriftCheckTimer": 5.0,
|
||||||
"homePosition": [
|
"homePosition": [
|
||||||
-6.400000095367432,
|
-6.4000000953674316,
|
||||||
0.0,
|
0.0,
|
||||||
-13.0
|
-13.0
|
||||||
],
|
],
|
||||||
@@ -420,14 +430,15 @@
|
|||||||
2
|
2
|
||||||
],
|
],
|
||||||
"navigationRetryTicks": 0,
|
"navigationRetryTicks": 0,
|
||||||
"pathWaypoints": [],
|
"pathWaypoints": [
|
||||||
|
],
|
||||||
"position": [
|
"position": [
|
||||||
-6.400000095367432,
|
-6.4000000953674316,
|
||||||
0.0,
|
0.0,
|
||||||
-13.0
|
-13.0
|
||||||
],
|
],
|
||||||
"requestedWalkTarget": [
|
"requestedWalkTarget": [
|
||||||
-6.400000095367432,
|
-6.4000000953674316,
|
||||||
0.0,
|
0.0,
|
||||||
-13.0
|
-13.0
|
||||||
],
|
],
|
||||||
@@ -435,13 +446,338 @@
|
|||||||
"targetFacingAngle": 0.0,
|
"targetFacingAngle": 0.0,
|
||||||
"visibleRange": 6.5,
|
"visibleRange": 6.5,
|
||||||
"walkTarget": [
|
"walkTarget": [
|
||||||
-6.400000095367432,
|
-6.4000000953674316,
|
||||||
0.0,
|
0.0,
|
||||||
-13.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": {
|
"state": {
|
||||||
"activeNavigationIndex": 0,
|
"activeNavigationIndex": 0,
|
||||||
"targetInteractNpcId": "",
|
"targetInteractNpcId": "",
|
||||||
@@ -460,61 +796,23 @@
|
|||||||
"rotation": 0.0,
|
"rotation": 0.0,
|
||||||
"steeringAngle": 0.0,
|
"steeringAngle": 0.0,
|
||||||
"velocity": 0.0,
|
"velocity": 0.0,
|
||||||
"waypoints": [],
|
"waypoints": [
|
||||||
|
],
|
||||||
"wheelAngle": 0.0
|
"wheelAngle": 0.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"teleportZones": {},
|
"teleportZones": {
|
||||||
"triggerZones": {}
|
},
|
||||||
|
"triggerZones": {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"money": 5500,
|
"money": 5500,
|
||||||
"playerHp": 200.0,
|
"playerHp": 200.0,
|
||||||
"playerMaxHp": 200.0,
|
"playerMaxHp": 200.0,
|
||||||
"players": {
|
"players": {
|
||||||
"player2": {
|
"player1": {
|
||||||
"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": {
|
|
||||||
"animationFrame": 0.0,
|
"animationFrame": 0.0,
|
||||||
"attackTargetId": "",
|
"attackTargetId": "",
|
||||||
"attack_cooldown": 0.0,
|
"attack_cooldown": 0.0,
|
||||||
@@ -556,10 +854,140 @@
|
|||||||
0.0,
|
0.0,
|
||||||
12.8
|
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": {
|
"questJournal": {
|
||||||
"quests": []
|
"quests": [
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"savedAt": "2026-11-05 17:43",
|
"savedAt": "2026-11-05 17:43",
|
||||||
"schemaVersion": 5,
|
"schemaVersion": 5,
|
||||||
|
|||||||
@@ -125,7 +125,7 @@
|
|||||||
"y": 10,
|
"y": 10,
|
||||||
"width": 160,
|
"width": 160,
|
||||||
"height": 36,
|
"height": 36,
|
||||||
"text": "v. 1.0.7",
|
"text": "v. 1.1.2",
|
||||||
"fontSize": 24,
|
"fontSize": 24,
|
||||||
"textCentered": false,
|
"textCentered": false,
|
||||||
"topAligned": true,
|
"topAligned": true,
|
||||||
|
|||||||
@@ -116,7 +116,7 @@
|
|||||||
"y": 10,
|
"y": 10,
|
||||||
"width": 160,
|
"width": 160,
|
||||||
"height": 36,
|
"height": 36,
|
||||||
"text": "v. 1.0.7",
|
"text": "v. 1.1.2",
|
||||||
"fontSize": 24,
|
"fontSize": 24,
|
||||||
"textCentered": false,
|
"textCentered": false,
|
||||||
"topAligned": true,
|
"topAligned": true,
|
||||||
|
|||||||
@@ -125,7 +125,7 @@
|
|||||||
"y": 10,
|
"y": 10,
|
||||||
"width": 160,
|
"width": 160,
|
||||||
"height": 36,
|
"height": 36,
|
||||||
"text": "v. 1.0.7",
|
"text": "v. 1.1.2",
|
||||||
"fontSize": 24,
|
"fontSize": 24,
|
||||||
"textCentered": false,
|
"textCentered": false,
|
||||||
"topAligned": true,
|
"topAligned": true,
|
||||||
|
|||||||
@@ -125,7 +125,7 @@
|
|||||||
"y": 10,
|
"y": 10,
|
||||||
"width": 160,
|
"width": 160,
|
||||||
"height": 36,
|
"height": 36,
|
||||||
"text": "v. 1.0.7",
|
"text": "v. 1.1.2",
|
||||||
"fontSize": 24,
|
"fontSize": 24,
|
||||||
"textCentered": false,
|
"textCentered": false,
|
||||||
"topAligned": true,
|
"topAligned": true,
|
||||||
|
|||||||
+28
-7
@@ -9,6 +9,7 @@ set(CMAKE_CXX_EXTENSIONS OFF)
|
|||||||
set(GAME_ROOT "${CMAKE_CURRENT_LIST_DIR}/..")
|
set(GAME_ROOT "${CMAKE_CURRENT_LIST_DIR}/..")
|
||||||
|
|
||||||
find_package(Threads REQUIRED)
|
find_package(Threads REQUIRED)
|
||||||
|
include("${GAME_ROOT}/cmake/FetchDependenciesServer.cmake")
|
||||||
find_package(Eigen3 3.3 CONFIG QUIET)
|
find_package(Eigen3 3.3 CONFIG QUIET)
|
||||||
if(NOT TARGET Eigen3::Eigen)
|
if(NOT TARGET Eigen3::Eigen)
|
||||||
set(EIGEN_BUNDLED_DIR "${GAME_ROOT}/thirdparty/eigen-5.0.0")
|
set(EIGEN_BUNDLED_DIR "${GAME_ROOT}/thirdparty/eigen-5.0.0")
|
||||||
@@ -105,12 +106,11 @@ endif()
|
|||||||
add_executable(GameServer src/main.cpp README.md)
|
add_executable(GameServer src/main.cpp README.md)
|
||||||
source_group("Server" FILES src/main.cpp README.md)
|
source_group("Server" FILES src/main.cpp README.md)
|
||||||
target_link_libraries(GameServer PRIVATE game_server_core)
|
target_link_libraries(GameServer PRIVATE game_server_core)
|
||||||
if(WIN32)
|
|
||||||
set(BOOST_BUNDLED_DIR "${GAME_ROOT}/thirdparty/boost_1_90_0")
|
set(BOOST_BUNDLED_DIR "${GAME_ROOT}/thirdparty/boost_1_90_0")
|
||||||
if(NOT EXISTS "${BOOST_BUNDLED_DIR}/boost/asio.hpp")
|
if(NOT EXISTS "${BOOST_BUNDLED_DIR}/boost/asio.hpp")
|
||||||
message(FATAL_ERROR
|
message(FATAL_ERROR
|
||||||
"Boost was not found at thirdparty/boost_1_90_0. Configure the "
|
"Boost was not found at thirdparty/boost_1_90_0 and could not be "
|
||||||
"graphical project once to populate it.")
|
"downloaded.")
|
||||||
endif()
|
endif()
|
||||||
target_sources(GameServer PRIVATE
|
target_sources(GameServer PRIVATE
|
||||||
"${GAME_ROOT}/src/game_server/NetworkProtocol.cpp"
|
"${GAME_ROOT}/src/game_server/NetworkProtocol.cpp"
|
||||||
@@ -118,11 +118,20 @@ if(WIN32)
|
|||||||
"${GAME_ROOT}/src/game_server/WebsocketGameServer.cpp"
|
"${GAME_ROOT}/src/game_server/WebsocketGameServer.cpp"
|
||||||
"${GAME_ROOT}/src/game_server/WebsocketGameServer.h")
|
"${GAME_ROOT}/src/game_server/WebsocketGameServer.h")
|
||||||
target_include_directories(GameServer PRIVATE "${BOOST_BUNDLED_DIR}")
|
target_include_directories(GameServer PRIVATE "${BOOST_BUNDLED_DIR}")
|
||||||
|
target_compile_definitions(GameServer PRIVATE NETWORK)
|
||||||
|
if(WIN32)
|
||||||
target_compile_definitions(
|
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)
|
target_link_libraries(GameServer PRIVATE ws2_32 mswsock)
|
||||||
endif()
|
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)
|
include(CTest)
|
||||||
if(BUILD_TESTING)
|
if(BUILD_TESTING)
|
||||||
add_executable(
|
add_executable(
|
||||||
@@ -141,7 +150,8 @@ if(BUILD_TESTING)
|
|||||||
add_test(
|
add_test(
|
||||||
NAME GameServer.LocationState
|
NAME GameServer.LocationState
|
||||||
COMMAND GameServerStateTests)
|
COMMAND GameServerStateTests)
|
||||||
if(WIN32)
|
# network tests (all platforms)
|
||||||
|
if(TRUE)
|
||||||
add_executable(
|
add_executable(
|
||||||
GameServerNetworkProtocolTests
|
GameServerNetworkProtocolTests
|
||||||
tests/NetworkProtocolTests.cpp
|
tests/NetworkProtocolTests.cpp
|
||||||
@@ -163,9 +173,20 @@ if(BUILD_TESTING)
|
|||||||
target_compile_definitions(
|
target_compile_definitions(
|
||||||
GameServerWebsocketLockstepTests PRIVATE
|
GameServerWebsocketLockstepTests PRIVATE
|
||||||
NETWORK WIN32_LEAN_AND_MEAN _WIN32_WINNT=0x0A00)
|
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(
|
target_link_libraries(
|
||||||
|
GameServerWebsocketLockstepTests PRIVATE game_server_core)
|
||||||
|
if(WIN32)
|
||||||
|
target_compile_definitions(
|
||||||
GameServerWebsocketLockstepTests PRIVATE
|
GameServerWebsocketLockstepTests PRIVATE
|
||||||
game_server_core ws2_32 mswsock)
|
WIN32_LEAN_AND_MEAN _WIN32_WINNT=0x0A00)
|
||||||
|
target_link_libraries(
|
||||||
|
GameServerWebsocketLockstepTests PRIVATE ws2_32 mswsock)
|
||||||
|
endif()
|
||||||
add_test(
|
add_test(
|
||||||
NAME GameServer.WebsocketLockstep
|
NAME GameServer.WebsocketLockstep
|
||||||
COMMAND GameServerWebsocketLockstepTests)
|
COMMAND GameServerWebsocketLockstepTests)
|
||||||
@@ -175,7 +196,7 @@ if(BUILD_TESTING)
|
|||||||
COMMAND GameServer --ticks 2 --no-sleep)
|
COMMAND GameServer --ticks 2 --no-sleep)
|
||||||
add_test(
|
add_test(
|
||||||
NAME GameServer.BattleZoneSharedPlayer
|
NAME GameServer.BattleZoneSharedPlayer
|
||||||
COMMAND GameServer --campaign battle_zone --location uni_exterior
|
COMMAND GameServer --campaign battle_zone --location park_korea
|
||||||
--ticks 2 --no-sleep)
|
--ticks 2 --no-sleep)
|
||||||
set_tests_properties(
|
set_tests_properties(
|
||||||
GameServer.BattleZoneSharedPlayer
|
GameServer.BattleZoneSharedPlayer
|
||||||
|
|||||||
+123
-62
@@ -1,11 +1,37 @@
|
|||||||
# Headless GameServer
|
# Headless GameServer
|
||||||
|
|
||||||
This target runs the authoritative `InternalGameServer` without SDL, OpenGL,
|
This target runs the authoritative `InternalGameServer` without SDL, OpenGL,
|
||||||
audio, Lua, or rendering dependencies. On Windows it exposes a deterministic
|
audio, Lua, or rendering dependencies. It exposes a deterministic lockstep
|
||||||
lockstep session through a Boost.Beast WebSocket listener.
|
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
|
## 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:
|
Windows:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
@@ -14,91 +40,126 @@ cmake --build server/build --config Release --target GameServer
|
|||||||
ctest --test-dir server/build -C Release --output-on-failure
|
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
|
```bash
|
||||||
cmake -S server -B server/build -DCMAKE_BUILD_TYPE=Release
|
server/scripts/run-server.sh
|
||||||
cmake --build server/build --target GameServer --parallel
|
|
||||||
ctest --test-dir server/build --output-on-failure
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Eigen3 must either be installed or be available at
|
This does `cd server/build` and runs
|
||||||
`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:
|
|
||||||
|
|
||||||
```bash
|
```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
|
Environment overrides: `CAMPAIGN`, `LOCATION`, `ADDRESS`, `PORT`, `BUILD_DIR`.
|
||||||
`server/build/Release/GameServer.exe`.
|
|
||||||
|
|
||||||
For smoke tests, `--ticks N` limits execution and `--no-sleep` skips the
|
Windows equivalent:
|
||||||
real-time delay.
|
|
||||||
|
|
||||||
Start the multiplayer lockstep server:
|
|
||||||
|
|
||||||
```powershell
|
```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
|
Command line options: `--campaign ID`, `--location ID`, `--campaigns PATH`
|
||||||
afterward and select `battle_zone_multiplayer`; that campaign connects to
|
(default `resources/config/campaigns.json`), `--address IP` (default `0.0.0.0`),
|
||||||
`ws://127.0.0.1:8080`. Other campaigns continue to use the in-process server.
|
`--port N` (default `8080`), `--ticks N` and `--no-sleep` (smoke tests).
|
||||||
|
|
||||||
The server sends one state snapshot when a player connects, then broadcasts one
|
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
|
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
|
scheduled two ticks (200 ms) in the future. Tick IDs, rather than server or
|
||||||
client wall-clock values, define simulation time.
|
client wall-clock values, define simulation time.
|
||||||
|
|
||||||
If `--location` is omitted, the campaign's configured start location is used.
|
## Production deployment (Ubuntu, systemd + nginx)
|
||||||
`--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.
|
|
||||||
|
|
||||||
Available startup options:
|
The server listens on loopback only (`127.0.0.1:8081`); nginx terminates TLS and
|
||||||
|
proxies WebSocket traffic.
|
||||||
|
|
||||||
```text
|
### 1. systemd service
|
||||||
--campaign ID
|
|
||||||
--location ID
|
```bash
|
||||||
--campaigns PATH
|
sudo cp server/deploy/shadow-gameserver.service /etc/systemd/system/
|
||||||
--address IP
|
sudo systemctl daemon-reload
|
||||||
--port N
|
sudo systemctl enable --now shadow-gameserver
|
||||||
--tick-ms 100
|
systemctl status shadow-gameserver
|
||||||
--ticks N
|
journalctl -u shadow-gameserver -f # logs
|
||||||
--no-sleep
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Network mode requires a 100 ms tick; another `--tick-ms` value is rejected.
|
The unit caps memory at 512 MB and restarts on failure. After rebuilding run
|
||||||
The selected campaign's `networkPlayerSlots` lists pre-existing character IDs
|
`sudo systemctl restart shadow-gameserver`. Edit `User=`/paths in the unit if the
|
||||||
assigned to connections in order. `networkCompatibility` is a release token
|
repository lives elsewhere. To run a second server, copy the unit with another
|
||||||
that must be changed whenever deterministic content changes; clients reject a
|
`PORT` (and campaign) and add a matching `/server002/` location.
|
||||||
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 graphical client executes presentation behavior after building its runtime
|
### 2. nginx
|
||||||
`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.
|
|
||||||
|
|
||||||
## 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;
|
### 3. HTTPS certificate
|
||||||
- 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`.
|
|
||||||
|
|
||||||
The lockstep implementation currently targets identical executable and content
|
DNS for `api.shadowoverbishkek.com` must point at the VDS first.
|
||||||
builds. Diagnostic state hashes detect divergence, but there is no automatic
|
|
||||||
checkpoint resynchronization or reconnect input history yet.
|
```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[@]}"
|
||||||
@@ -79,7 +79,7 @@ int main()
|
|||||||
return fail("Battle Zone modes do not share their initial state");
|
return fail("Battle Zone modes do not share their initial state");
|
||||||
}
|
}
|
||||||
if (multiplayerBattleZoneConfig->networkPlayerSlots.size() != 2 ||
|
if (multiplayerBattleZoneConfig->networkPlayerSlots.size() != 2 ||
|
||||||
multiplayerBattleZoneConfig->networkPlayerSlots[0] != "player" ||
|
multiplayerBattleZoneConfig->networkPlayerSlots[0] != "player1" ||
|
||||||
multiplayerBattleZoneConfig->networkPlayerSlots[1] !=
|
multiplayerBattleZoneConfig->networkPlayerSlots[1] !=
|
||||||
"player2" ||
|
"player2" ||
|
||||||
multiplayerBattleZoneConfig->networkCompatibility.empty()) {
|
multiplayerBattleZoneConfig->networkCompatibility.empty()) {
|
||||||
@@ -140,7 +140,7 @@ int main()
|
|||||||
disabledPlayer->enabled = false;
|
disabledPlayer->enabled = false;
|
||||||
state.location->targetInteractiveObjectId = "object";
|
state.location->targetInteractiveObjectId = "object";
|
||||||
state.location->targetInteractNpcId = "npc";
|
state.location->targetInteractNpcId = "npc";
|
||||||
FRG::PlayerStateMap disabledPlayers{{"player", disabledPlayer}};
|
FRG::PlayerStateMap disabledPlayers{{"player1", disabledPlayer}};
|
||||||
const FRG::LocationSimulationEvents disabledEvents =
|
const FRG::LocationSimulationEvents disabledEvents =
|
||||||
state.update(50, disabledPlayers, false);
|
state.update(50, disabledPlayers, false);
|
||||||
if (!disabledEvents.interactiveObjects.empty() ||
|
if (!disabledEvents.interactiveObjects.empty() ||
|
||||||
@@ -165,7 +165,7 @@ int main()
|
|||||||
playerTwo->enabled = true;
|
playerTwo->enabled = true;
|
||||||
playerTwo->position = {2.f, 0.f, 0.f};
|
playerTwo->position = {2.f, 0.f, 0.f};
|
||||||
FRG::PlayerStateMap combatPlayers{
|
FRG::PlayerStateMap combatPlayers{
|
||||||
{"player", playerOne}, {"player2", playerTwo}};
|
{"player1", playerOne}, {"player2", playerTwo}};
|
||||||
|
|
||||||
state.update(100, combatPlayers, false);
|
state.update(100, combatPlayers, false);
|
||||||
if (hostileNpc.attackTargetId != "player2") {
|
if (hostileNpc.attackTargetId != "player2") {
|
||||||
@@ -181,7 +181,7 @@ int main()
|
|||||||
|
|
||||||
playerTwo->hp = 0.f;
|
playerTwo->hp = 0.f;
|
||||||
state.update(100, combatPlayers, false);
|
state.update(100, combatPlayers, false);
|
||||||
if (hostileNpc.attackTargetId != "player") {
|
if (hostileNpc.attackTargetId != "player1") {
|
||||||
return fail("Hostile NPC did not acquire a replacement after target death");
|
return fail("Hostile NPC did not acquire a replacement after target death");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,6 +194,59 @@ int main()
|
|||||||
return fail("Hostile NPC did not return home after losing its target");
|
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 = "player1";
|
||||||
|
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;
|
FRG::CampaignConfig battleZone;
|
||||||
battleZone.id = "battle_zone";
|
battleZone.id = "battle_zone";
|
||||||
battleZone.questsPath = "resources/config/quests_battle_zone.json";
|
battleZone.questsPath = "resources/config/quests_battle_zone.json";
|
||||||
@@ -214,22 +267,22 @@ int main()
|
|||||||
FRG::CampaignConfig multiplayerBattleZone = battleZone;
|
FRG::CampaignConfig multiplayerBattleZone = battleZone;
|
||||||
multiplayerBattleZone.id = "battle_zone_multiplayer";
|
multiplayerBattleZone.id = "battle_zone_multiplayer";
|
||||||
multiplayerBattleZone.networkPlayerSlots = {
|
multiplayerBattleZone.networkPlayerSlots = {
|
||||||
"player", "player2"};
|
"player1", "player2"};
|
||||||
networkServer.startCampaign(multiplayerBattleZone, {}, true, false);
|
networkServer.startCampaign(multiplayerBattleZone, {}, true, false);
|
||||||
const auto networkSnapshot = networkServer.snapshot();
|
const auto networkSnapshot = networkServer.snapshot();
|
||||||
if (networkSnapshot->campaignId != "battle_zone_multiplayer")
|
if (networkSnapshot->campaignId != "battle_zone_multiplayer")
|
||||||
return fail("Network campaign ID was not retained");
|
return fail("Network campaign ID was not retained");
|
||||||
if (networkSnapshot->players.count("player") != 1)
|
if (networkSnapshot->players.count("player1") != 1)
|
||||||
return fail("Network campaign shared player was not created");
|
return fail("Network campaign shared player was not created");
|
||||||
if (networkSnapshot->players.count("player2") != 0)
|
if (networkSnapshot->players.count("player2") != 0)
|
||||||
return fail("Network campaign unexpectedly created the test bot");
|
return fail("Network campaign unexpectedly created the test bot");
|
||||||
if (networkSnapshot->players.at("player").state.enabled)
|
if (networkSnapshot->players.at("player1").state.enabled)
|
||||||
return fail("Network campaign player slot was enabled before connection");
|
return fail("Network campaign player slot was enabled before connection");
|
||||||
|
|
||||||
nlohmann::json loadedNetworkState;
|
nlohmann::json loadedNetworkState;
|
||||||
networkServer.save(loadedNetworkState);
|
networkServer.save(loadedNetworkState);
|
||||||
loadedNetworkState["players"]["player2"] =
|
loadedNetworkState["players"]["player2"] =
|
||||||
loadedNetworkState["players"]["player"];
|
loadedNetworkState["players"]["player1"];
|
||||||
loadedNetworkState["players"]["player2"]["position"] =
|
loadedNetworkState["players"]["player2"]["position"] =
|
||||||
{4.f, 0.f, 5.f};
|
{4.f, 0.f, 5.f};
|
||||||
networkServer.load(loadedNetworkState);
|
networkServer.load(loadedNetworkState);
|
||||||
@@ -255,7 +308,7 @@ int main()
|
|||||||
networkServer.submit(FRG::SetCharacterFacingAngleCommand{
|
networkServer.submit(FRG::SetCharacterFacingAngleCommand{
|
||||||
locationId, "player2", 1.5f, true});
|
locationId, "player2", 1.5f, true});
|
||||||
networkServer.submit(FRG::SetCharacterFaceTargetCommand{
|
networkServer.submit(FRG::SetCharacterFaceTargetCommand{
|
||||||
locationId, "player2", "player"});
|
locationId, "player2", "player1"});
|
||||||
networkServer.submit(FRG::SetCharacterEnabledCommand{
|
networkServer.submit(FRG::SetCharacterEnabledCommand{
|
||||||
locationId, "player2", false});
|
locationId, "player2", false});
|
||||||
const auto commandSnapshot = networkServer.snapshot();
|
const auto commandSnapshot = networkServer.snapshot();
|
||||||
@@ -318,6 +371,7 @@ int main()
|
|||||||
moving->hp = 10.f;
|
moving->hp = 10.f;
|
||||||
moving->pathWaypoints = {Eigen::Vector3f(8.f, 0.f, 0.f)};
|
moving->pathWaypoints = {Eigen::Vector3f(8.f, 0.f, 0.f)};
|
||||||
moving->movedLastTick = true;
|
moving->movedLastTick = true;
|
||||||
|
moving->attackTargetId = "stationary";
|
||||||
FRG::PlayerStateMap occupancyPlayers{
|
FRG::PlayerStateMap occupancyPlayers{
|
||||||
{"stationary", stationary}, {"moving", moving}};
|
{"stationary", stationary}, {"moving", moving}};
|
||||||
if (occupancyState.collectPathObstacles(
|
if (occupancyState.collectPathObstacles(
|
||||||
@@ -325,7 +379,11 @@ int main()
|
|||||||
occupancyState.collectPathObstacles(
|
occupancyState.collectPathObstacles(
|
||||||
"requester", occupancyPlayers, "moving").size() != 2 ||
|
"requester", occupancyPlayers, "moving").size() != 2 ||
|
||||||
!occupancyState.collectPathObstacles(
|
!occupancyState.collectPathObstacles(
|
||||||
"stationary", occupancyPlayers).empty()) {
|
"stationary", occupancyPlayers).empty() ||
|
||||||
|
!occupancyState.collectPathObstacles(
|
||||||
|
"moving", occupancyPlayers).empty() ||
|
||||||
|
occupancyState.collectPathObstacles(
|
||||||
|
"moving", occupancyPlayers, "stationary").size() != 1) {
|
||||||
return fail("Stationary obstacle collection policy is incorrect");
|
return fail("Stationary obstacle collection policy is incorrect");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,12 +23,12 @@ int main()
|
|||||||
const nlohmann::json original{
|
const nlohmann::json original{
|
||||||
{"schemaVersion", 2},
|
{"schemaVersion", 2},
|
||||||
{"campaignId", "battle_zone_multiplayer"},
|
{"campaignId", "battle_zone_multiplayer"},
|
||||||
{"players", {{"player", {{"hp", 200.0}}}}},
|
{"players", {{"player1", {{"hp", 200.0}}}}},
|
||||||
{"locations", {{"uni_exterior", {{"state", savedLocationState}}}}}
|
{"locations", {{"park_korea", {{"state", savedLocationState}}}}}
|
||||||
};
|
};
|
||||||
FRG::InitialStateMessage initial;
|
FRG::InitialStateMessage initial;
|
||||||
initial.state = original;
|
initial.state = original;
|
||||||
initial.assignedPlayerId = "player";
|
initial.assignedPlayerId = "player1";
|
||||||
initial.snapshotTick = 42;
|
initial.snapshotTick = 42;
|
||||||
initial.compatibilityFingerprint = "lockstep-v2:battle_zone_multiplayer";
|
initial.compatibilityFingerprint = "lockstep-v2:battle_zone_multiplayer";
|
||||||
const std::string message = FRG::makeInitialStateMessage(initial);
|
const std::string message = FRG::makeInitialStateMessage(initial);
|
||||||
@@ -40,11 +40,11 @@ int main()
|
|||||||
require(parsed.state == original, "state payload did not round trip");
|
require(parsed.state == original, "state payload did not round trip");
|
||||||
require(parsed.snapshotTick == 42, "snapshot tick did not round trip");
|
require(parsed.snapshotTick == 42, "snapshot tick did not round trip");
|
||||||
require(
|
require(
|
||||||
parsed.assignedPlayerId == "player",
|
parsed.assignedPlayerId == "player1",
|
||||||
"assigned player did not round trip");
|
"assigned player did not round trip");
|
||||||
require(
|
require(
|
||||||
!parsed.state["locations"]["uni_exterior"]["state"].contains("cameraAzimuth") &&
|
!parsed.state["locations"]["park_korea"]["state"].contains("cameraAzimuth") &&
|
||||||
!parsed.state["locations"]["uni_exterior"]["state"].contains("cameraInclination"),
|
!parsed.state["locations"]["park_korea"]["state"].contains("cameraInclination"),
|
||||||
"local camera state leaked into the network payload");
|
"local camera state leaked into the network payload");
|
||||||
|
|
||||||
FRG::DestinationInputMessage input;
|
FRG::DestinationInputMessage input;
|
||||||
@@ -87,11 +87,11 @@ int main()
|
|||||||
FRG::TickMessage tick;
|
FRG::TickMessage tick;
|
||||||
tick.tickId = 43;
|
tick.tickId = 43;
|
||||||
tick.commands.push_back(FRG::ScheduledDestinationCommand{
|
tick.commands.push_back(FRG::ScheduledDestinationCommand{
|
||||||
1, "player", parsedInput.target});
|
1, "player1", parsedInput.target});
|
||||||
tick.commands.push_back(FRG::ScheduledCharacterEnabledCommand{
|
tick.commands.push_back(FRG::ScheduledCharacterEnabledCommand{
|
||||||
2, "uni_exterior", "player2", true});
|
2, "park_korea", "player2", true});
|
||||||
tick.commands.push_back(FRG::ScheduledCharacterActionCommand{
|
tick.commands.push_back(FRG::ScheduledCharacterActionCommand{
|
||||||
3, "player", "set_target", "hostile_npc"});
|
3, "player1", "set_target", "hostile_npc"});
|
||||||
tick.stateHash = "0123456789abcdef";
|
tick.stateHash = "0123456789abcdef";
|
||||||
FRG::TickMessage parsedTick;
|
FRG::TickMessage parsedTick;
|
||||||
require(
|
require(
|
||||||
@@ -147,9 +147,9 @@ int main()
|
|||||||
|
|
||||||
FRG::LockstepTimeline timeline(2);
|
FRG::LockstepTimeline timeline(2);
|
||||||
const std::uint64_t firstSequence =
|
const std::uint64_t firstSequence =
|
||||||
timeline.schedule("player", Eigen::Vector3f(1.f, 0.f, 1.f));
|
timeline.schedule("player1", Eigen::Vector3f(1.f, 0.f, 1.f));
|
||||||
const std::uint64_t secondSequence =
|
const std::uint64_t secondSequence =
|
||||||
timeline.schedule("player_2", Eigen::Vector3f(2.f, 0.f, 2.f));
|
timeline.schedule("player2", Eigen::Vector3f(2.f, 0.f, 2.f));
|
||||||
const FRG::TickMessage emptyTick = timeline.advance();
|
const FRG::TickMessage emptyTick = timeline.advance();
|
||||||
require(
|
require(
|
||||||
emptyTick.tickId == 1 && emptyTick.commands.empty(),
|
emptyTick.tickId == 1 && emptyTick.commands.empty(),
|
||||||
@@ -163,8 +163,8 @@ int main()
|
|||||||
commandTick.commands[1]).commandSequence == secondSequence,
|
commandTick.commands[1]).commandSequence == secondSequence,
|
||||||
"scheduled commands were not finalized in stable order");
|
"scheduled commands were not finalized in stable order");
|
||||||
|
|
||||||
timeline.scheduleCharacterEnabled("uni_exterior", "player2", true);
|
timeline.scheduleCharacterEnabled("park_korea", "player2", true);
|
||||||
timeline.scheduleCharacterAction("player", "clear_attack_target");
|
timeline.scheduleCharacterAction("player1", "clear_attack_target");
|
||||||
const FRG::TickMessage presenceTick = timeline.advance();
|
const FRG::TickMessage presenceTick = timeline.advance();
|
||||||
require(
|
require(
|
||||||
presenceTick.commands.size() == 1 &&
|
presenceTick.commands.size() == 1 &&
|
||||||
|
|||||||
@@ -32,15 +32,40 @@ bool waitFor(Predicate predicate)
|
|||||||
|
|
||||||
int main()
|
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;
|
FRG::CampaignConfig campaign;
|
||||||
campaign.id = "test_lockstep";
|
campaign.id = "test_lockstep";
|
||||||
campaign.networkPlayerSlots = {"player", "player2"};
|
campaign.networkPlayerSlots = {"player1", "player2"};
|
||||||
|
|
||||||
FRG::InternalGameServer authoritative;
|
FRG::InternalGameServer authoritative;
|
||||||
authoritative.startCampaign(campaign, {}, true, false);
|
authoritative.startCampaign(campaign, {}, true, false);
|
||||||
nlohmann::json initialState;
|
nlohmann::json initialState;
|
||||||
authoritative.save(initialState);
|
authoritative.save(initialState);
|
||||||
initialState["players"]["player2"] = initialState["players"]["player"];
|
initialState["players"]["player2"] = initialState["players"]["player1"];
|
||||||
FRG::LocationGameState testLocation;
|
FRG::LocationGameState testLocation;
|
||||||
auto hostileNpc = std::make_shared<FRG::CharacterState>();
|
auto hostileNpc = std::make_shared<FRG::CharacterState>();
|
||||||
hostileNpc->enabled = true;
|
hostileNpc->enabled = true;
|
||||||
@@ -52,7 +77,7 @@ int main()
|
|||||||
initialState["locations"]["test_location"] = std::move(testLocationState);
|
initialState["locations"]["test_location"] = std::move(testLocationState);
|
||||||
authoritative.load(initialState);
|
authoritative.load(initialState);
|
||||||
FRG::WebsocketGameServer server(
|
FRG::WebsocketGameServer server(
|
||||||
authoritative, {"player", "player2"}, "lockstep-v2:test_lockstep",
|
authoritative, {"player1", "player2"}, "lockstep-v2:test_lockstep",
|
||||||
"127.0.0.1", 0);
|
"127.0.0.1", 0);
|
||||||
auto client = std::make_unique<FRG::WebsocketGameClient>(
|
auto client = std::make_unique<FRG::WebsocketGameClient>(
|
||||||
"127.0.0.1", std::to_string(server.port()), "/");
|
"127.0.0.1", std::to_string(server.port()), "/");
|
||||||
@@ -66,7 +91,7 @@ int main()
|
|||||||
return client->initialized();
|
return client->initialized();
|
||||||
}),
|
}),
|
||||||
"client did not receive initial state");
|
"client did not receive initial state");
|
||||||
require(client->localPlayerId() == "player", "wrong player slot assigned");
|
require(client->localPlayerId() == "player1", "wrong player slot assigned");
|
||||||
|
|
||||||
auto secondClient = std::make_unique<FRG::WebsocketGameClient>(
|
auto secondClient = std::make_unique<FRG::WebsocketGameClient>(
|
||||||
"127.0.0.1", std::to_string(server.port()), "/");
|
"127.0.0.1", std::to_string(server.port()), "/");
|
||||||
@@ -95,9 +120,9 @@ int main()
|
|||||||
client->update(0);
|
client->update(0);
|
||||||
secondClient->update(0);
|
secondClient->update(0);
|
||||||
return client->snapshot()->players.at("player2").state.enabled &&
|
return client->snapshot()->players.at("player2").state.enabled &&
|
||||||
secondClient->snapshot()->players.at("player").state.enabled;
|
secondClient->snapshot()->players.at("player1").state.enabled;
|
||||||
}) &&
|
}) &&
|
||||||
authoritative.snapshot()->players.at("player").state.enabled &&
|
authoritative.snapshot()->players.at("player1").state.enabled &&
|
||||||
authoritative.snapshot()->players.at("player2").state.enabled,
|
authoritative.snapshot()->players.at("player2").state.enabled,
|
||||||
"connection presence was not broadcast to all replicas");
|
"connection presence was not broadcast to all replicas");
|
||||||
|
|
||||||
@@ -115,16 +140,16 @@ int main()
|
|||||||
const auto first = client->snapshot();
|
const auto first = client->snapshot();
|
||||||
const auto second = secondClient->snapshot();
|
const auto second = secondClient->snapshot();
|
||||||
const auto host = authoritative.snapshot();
|
const auto host = authoritative.snapshot();
|
||||||
return first->players.at("player").state.requestedWalkTarget.isApprox(
|
return first->players.at("player1").state.requestedWalkTarget.isApprox(
|
||||||
Eigen::Vector3f(1.23f, 0.f, 2.35f)) &&
|
Eigen::Vector3f(1.23f, 0.f, 2.35f)) &&
|
||||||
second->players.at("player2").state.requestedWalkTarget.isApprox(
|
second->players.at("player2").state.requestedWalkTarget.isApprox(
|
||||||
Eigen::Vector3f(-1.23f, 0.f, 3.46f)) &&
|
Eigen::Vector3f(-1.23f, 0.f, 3.46f)) &&
|
||||||
first->players.at("player2").state.requestedWalkTarget.isApprox(
|
first->players.at("player2").state.requestedWalkTarget.isApprox(
|
||||||
second->players.at("player2").state.requestedWalkTarget) &&
|
second->players.at("player2").state.requestedWalkTarget) &&
|
||||||
second->players.at("player").state.requestedWalkTarget.isApprox(
|
second->players.at("player1").state.requestedWalkTarget.isApprox(
|
||||||
first->players.at("player").state.requestedWalkTarget) &&
|
first->players.at("player1").state.requestedWalkTarget) &&
|
||||||
host->players.at("player").state.requestedWalkTarget.isApprox(
|
host->players.at("player1").state.requestedWalkTarget.isApprox(
|
||||||
first->players.at("player").state.requestedWalkTarget) &&
|
first->players.at("player1").state.requestedWalkTarget) &&
|
||||||
host->players.at("player2").state.requestedWalkTarget.isApprox(
|
host->players.at("player2").state.requestedWalkTarget.isApprox(
|
||||||
second->players.at("player2").state.requestedWalkTarget);
|
second->players.at("player2").state.requestedWalkTarget);
|
||||||
});
|
});
|
||||||
@@ -135,9 +160,9 @@ int main()
|
|||||||
std::cerr << "destination revisions " << first->revision << ", "
|
std::cerr << "destination revisions " << first->revision << ", "
|
||||||
<< second->revision << ", " << host->revision << "\n"
|
<< second->revision << ", " << host->revision << "\n"
|
||||||
<< "player targets "
|
<< "player targets "
|
||||||
<< first->players.at("player").state.requestedWalkTarget.transpose()
|
<< first->players.at("player1").state.requestedWalkTarget.transpose()
|
||||||
<< " / "
|
<< " / "
|
||||||
<< host->players.at("player").state.requestedWalkTarget.transpose()
|
<< host->players.at("player1").state.requestedWalkTarget.transpose()
|
||||||
<< "\nplayer2 targets "
|
<< "\nplayer2 targets "
|
||||||
<< second->players.at("player2").state.requestedWalkTarget.transpose()
|
<< second->players.at("player2").state.requestedWalkTarget.transpose()
|
||||||
<< " / "
|
<< " / "
|
||||||
@@ -158,11 +183,11 @@ int main()
|
|||||||
waitFor([&]() {
|
waitFor([&]() {
|
||||||
client->update(0);
|
client->update(0);
|
||||||
secondClient->update(0);
|
secondClient->update(0);
|
||||||
return client->snapshot()->players.at("player").state.attackTargetId ==
|
return client->snapshot()->players.at("player1").state.attackTargetId ==
|
||||||
"hostile_npc" &&
|
"hostile_npc" &&
|
||||||
secondClient->snapshot()->players.at("player").state.attackTargetId ==
|
secondClient->snapshot()->players.at("player1").state.attackTargetId ==
|
||||||
"hostile_npc" &&
|
"hostile_npc" &&
|
||||||
authoritative.snapshot()->players.at("player").state.attackTargetId ==
|
authoritative.snapshot()->players.at("player1").state.attackTargetId ==
|
||||||
"hostile_npc";
|
"hostile_npc";
|
||||||
}),
|
}),
|
||||||
"target command was not delayed and rebroadcast");
|
"target command was not delayed and rebroadcast");
|
||||||
@@ -177,9 +202,9 @@ int main()
|
|||||||
waitFor([&]() {
|
waitFor([&]() {
|
||||||
client->update(0);
|
client->update(0);
|
||||||
secondClient->update(0);
|
secondClient->update(0);
|
||||||
return client->snapshot()->players.at("player").state.attackTargetId.empty() &&
|
return client->snapshot()->players.at("player1").state.attackTargetId.empty() &&
|
||||||
secondClient->snapshot()->players.at("player").state.attackTargetId.empty() &&
|
secondClient->snapshot()->players.at("player1").state.attackTargetId.empty() &&
|
||||||
authoritative.snapshot()->players.at("player").state.attackTargetId.empty();
|
authoritative.snapshot()->players.at("player1").state.attackTargetId.empty();
|
||||||
}),
|
}),
|
||||||
"clear attack target command was not delayed and rebroadcast");
|
"clear attack target command was not delayed and rebroadcast");
|
||||||
|
|
||||||
|
|||||||
+8
-12
@@ -93,8 +93,7 @@ void CharacterState::setTarget(
|
|||||||
const Eigen::Vector3f& target,
|
const Eigen::Vector3f& target,
|
||||||
std::string callbackName,
|
std::string callbackName,
|
||||||
const LocationNetworkState& location,
|
const LocationNetworkState& location,
|
||||||
const std::string& entityId,
|
const std::string& entityId)
|
||||||
const std::vector<PathFinder::DynamicObstacle>& dynamicObstacles)
|
|
||||||
{
|
{
|
||||||
const Eigen::Vector3f normalizedTarget(target.x(), 0.f, target.z());
|
const Eigen::Vector3f normalizedTarget(target.x(), 0.f, target.z());
|
||||||
const bool sameRequestedTarget =
|
const bool sameRequestedTarget =
|
||||||
@@ -115,10 +114,9 @@ void CharacterState::setTarget(
|
|||||||
movementBlockedTicks = 0;
|
movementBlockedTicks = 0;
|
||||||
navigationRetryTicks = 0;
|
navigationRetryTicks = 0;
|
||||||
blockingCharacterId.clear();
|
blockingCharacterId.clear();
|
||||||
pathWaypoints = location.navigationMaps.empty()
|
|
||||||
? std::vector<Eigen::Vector3f>{}
|
pathWaypoints = location.navigationMaps[location.activeNavigationIndex].findPathToNearest(this->position, target);
|
||||||
: location.planPathExceptMyself(
|
|
||||||
position, normalizedTarget, entityId, dynamicObstacles);
|
|
||||||
currentWaypointIndex = 0;
|
currentWaypointIndex = 0;
|
||||||
if (!pathWaypoints.empty()) {
|
if (!pathWaypoints.empty()) {
|
||||||
for (Eigen::Vector3f& waypoint : pathWaypoints) waypoint.y() = 0.f;
|
for (Eigen::Vector3f& waypoint : pathWaypoints) waypoint.y() = 0.f;
|
||||||
@@ -132,15 +130,13 @@ void CharacterState::setTarget(
|
|||||||
|
|
||||||
void CharacterState::forceReplan(
|
void CharacterState::forceReplan(
|
||||||
const LocationNetworkState& location,
|
const LocationNetworkState& location,
|
||||||
const std::string& entityId,
|
const std::string& entityId)
|
||||||
const std::vector<PathFinder::DynamicObstacle>& dynamicObstacles)
|
|
||||||
{
|
{
|
||||||
const Eigen::Vector3f normalizedTarget(
|
const Eigen::Vector3f normalizedTarget(
|
||||||
requestedWalkTarget.x(), 0.f, requestedWalkTarget.z());
|
requestedWalkTarget.x(), 0.f, requestedWalkTarget.z());
|
||||||
pathWaypoints = location.navigationMaps.empty()
|
|
||||||
? std::vector<Eigen::Vector3f>{}
|
pathWaypoints = location.navigationMaps[location.activeNavigationIndex].findPathToNearest(this->position, normalizedTarget);
|
||||||
: location.planPathExceptMyself(
|
|
||||||
position, normalizedTarget, entityId, dynamicObstacles);
|
|
||||||
currentWaypointIndex = 0;
|
currentWaypointIndex = 0;
|
||||||
movementBlockedTicks = 0;
|
movementBlockedTicks = 0;
|
||||||
navigationRetryTicks = 0;
|
navigationRetryTicks = 0;
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ public:
|
|||||||
bool showWeapon = true;
|
bool showWeapon = true;
|
||||||
|
|
||||||
// --- Stable character references ---
|
// --- Stable character references ---
|
||||||
// Empty means no target. "player" is the campaign player; NPC IDs are
|
// Empty means no target. "player1" is the campaign player; NPC IDs are
|
||||||
// location-local and validated while loading location content.
|
// location-local and validated while loading location content.
|
||||||
std::string attackTargetId;
|
std::string attackTargetId;
|
||||||
std::string faceTargetId;
|
std::string faceTargetId;
|
||||||
@@ -151,13 +151,9 @@ public:
|
|||||||
void setTarget(const Eigen::Vector3f& target,
|
void setTarget(const Eigen::Vector3f& target,
|
||||||
std::string callbackName,
|
std::string callbackName,
|
||||||
const LocationNetworkState& location,
|
const LocationNetworkState& location,
|
||||||
const std::string& entityId,
|
const std::string& entityId);
|
||||||
const std::vector<PathFinder::DynamicObstacle>&
|
|
||||||
dynamicObstacles = {});
|
|
||||||
void forceReplan(const LocationNetworkState& location,
|
void forceReplan(const LocationNetworkState& location,
|
||||||
const std::string& entityId,
|
const std::string& entityId);
|
||||||
const std::vector<PathFinder::DynamicObstacle>&
|
|
||||||
dynamicObstacles = {});
|
|
||||||
void stopAndRotateToTarget(std::string characterId);
|
void stopAndRotateToTarget(std::string characterId);
|
||||||
void resetPlayerAfterDeath();
|
void resetPlayerAfterDeath();
|
||||||
void tryAttackCombo();
|
void tryAttackCombo();
|
||||||
|
|||||||
+16
-2
@@ -557,7 +557,21 @@ namespace FRG
|
|||||||
if (remoteCampaignActive) {
|
if (remoteCampaignActive) {
|
||||||
logger() << "[network] Campaign '" << campaignId
|
logger() << "[network] Campaign '" << campaignId
|
||||||
<< "' selected; creating WebSocket client" << std::endl;
|
<< "' 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/";
|
||||||
|
//static const char* const kMultiplayerServerUrl = "ws://127.0.0.1:8080/";
|
||||||
|
|
||||||
|
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 =
|
remoteGameServerHost =
|
||||||
std::make_unique<RemoteGameServerHost>(*websocketGameClient);
|
std::make_unique<RemoteGameServerHost>(*websocketGameClient);
|
||||||
gameServer.use(*websocketGameClient);
|
gameServer.use(*websocketGameClient);
|
||||||
@@ -680,7 +694,7 @@ namespace FRG
|
|||||||
// Developer workflow: uncomment to (re)generate the campaign's start-state JSON
|
// Developer workflow: uncomment to (re)generate the campaign's start-state JSON
|
||||||
// from this freshly-rebuilt state after changing gameplay/content JSON or Lua.
|
// from this freshly-rebuilt state after changing gameplay/content JSON or Lua.
|
||||||
// Re-comment before shipping.
|
// Re-comment before shipping.
|
||||||
saveInitialState();
|
//saveInitialState();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+13
-14
@@ -851,8 +851,8 @@ namespace FRG
|
|||||||
float t0 = (worldMin[axis] - rayOrigin[axis]) * invD;
|
float t0 = (worldMin[axis] - rayOrigin[axis]) * invD;
|
||||||
float t1 = (worldMax[axis] - rayOrigin[axis]) * invD;
|
float t1 = (worldMax[axis] - rayOrigin[axis]) * invD;
|
||||||
if (invD < 0.0f) std::swap(t0, t1);
|
if (invD < 0.0f) std::swap(t0, t1);
|
||||||
tMin = max(tMin, t0);
|
tMin = std::max(tMin, t0);
|
||||||
tMax = min(tMax, t1);
|
tMax = std::min(tMax, t1);
|
||||||
if (tMax < tMin) { miss = true; break; }
|
if (tMax < tMin) { miss = true; break; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -890,11 +890,11 @@ namespace FRG
|
|||||||
}
|
}
|
||||||
|
|
||||||
Character* Location::raycastNpcs(const Eigen::Vector3f& rayOrigin, const Eigen::Vector3f& rayDir, float maxDistance) {
|
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
|
// 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.
|
// 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_RADIUS = 0.35f;
|
||||||
static constexpr float NPC_CLICK_HEIGHT = 1.6f;
|
static constexpr float NPC_CLICK_HEIGHT = 1.8f;
|
||||||
static constexpr float NPC_PICK_EPSILON = 1e-6f;
|
static constexpr float NPC_PICK_EPSILON = 1e-6f;
|
||||||
|
|
||||||
Character* closestNpc = nullptr;
|
Character* closestNpc = nullptr;
|
||||||
@@ -1838,14 +1838,13 @@ namespace FRG
|
|||||||
state->activeNavigationIndex >= static_cast<int>(state->navigationMaps.size())) {
|
state->activeNavigationIndex >= static_cast<int>(state->navigationMaps.size())) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const auto& areas =
|
const auto& area =
|
||||||
state->navigationMaps[state->activeNavigationIndex].getAreas();
|
state->navigationMaps[state->activeNavigationIndex].getArea();
|
||||||
const bool found = std::any_of(
|
if (area.name != areaName)
|
||||||
areas.begin(), areas.end(),
|
{
|
||||||
[&](const PathFinder::NavigationArea& area) {
|
return false;
|
||||||
return area.name == areaName;
|
}
|
||||||
});
|
|
||||||
if (!found) return false;
|
|
||||||
gameServer_.submit(SetNavigationAreaAvailableCommand{
|
gameServer_.submit(SetNavigationAreaAvailableCommand{
|
||||||
locationId_, areaName, available});
|
locationId_, areaName, available});
|
||||||
return true;
|
return true;
|
||||||
@@ -2253,7 +2252,7 @@ namespace FRG
|
|||||||
const float minInclination = M_PI * 30.f / 180.f;
|
const float minInclination = M_PI * 30.f / 180.f;
|
||||||
const float maxInclination = M_PI * 0.5f;
|
const float maxInclination = M_PI * 0.5f;
|
||||||
localState_->cameraInclination =
|
localState_->cameraInclination =
|
||||||
max(minInclination, min(maxInclination, inclination));
|
std::max(minInclination, std::min(maxInclination, inclination));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,12 +30,12 @@ namespace FRG
|
|||||||
state->activeNavigationIndex >= static_cast<int>(state->navigationMaps.size())) return;
|
state->activeNavigationIndex >= static_cast<int>(state->navigationMaps.size())) return;
|
||||||
const PathFinder& navigation =
|
const PathFinder& navigation =
|
||||||
state->navigationMaps[state->activeNavigationIndex];
|
state->navigationMaps[state->activeNavigationIndex];
|
||||||
const float y = navigation.getFloorY() + 0.02f;
|
const float y = static_cast<float>(navigation.getFloorY()) + 0.02f;
|
||||||
const Eigen::Vector3f red(1.0f, 0.0f, 0.0f);
|
const Eigen::Vector3f red(1.0f, 0.0f, 0.0f);
|
||||||
for (const auto& obs : navigation.getObstaclePolygons()) {
|
for (const auto& obs : navigation.getObstaclePolygons()) {
|
||||||
if (obs.polygon.size() < 3) continue;
|
if (obs.polygon.size() < 3) continue;
|
||||||
VertexRenderStruct mesh;
|
VertexRenderStruct mesh;
|
||||||
mesh.data = CreatePolygonFloor(obs.polygon, y, red);
|
mesh.data = CreatePolygonFloor(toEigenVector(obs.polygon), y, red);
|
||||||
mesh.RefreshVBO();
|
mesh.RefreshVBO();
|
||||||
navigationEditorNavMeshes.push_back(std::move(mesh));
|
navigationEditorNavMeshes.push_back(std::move(mesh));
|
||||||
}
|
}
|
||||||
@@ -60,7 +60,7 @@ namespace FRG
|
|||||||
if (!state || state->activeNavigationIndex < 0 ||
|
if (!state || state->activeNavigationIndex < 0 ||
|
||||||
state->activeNavigationIndex >= static_cast<int>(state->navigationMaps.size())) return;
|
state->activeNavigationIndex >= static_cast<int>(state->navigationMaps.size())) return;
|
||||||
const float y =
|
const float y =
|
||||||
state->navigationMaps[state->activeNavigationIndex].getFloorY() + 0.05f;
|
static_cast<float>(state->navigationMaps[state->activeNavigationIndex].getFloorY()) + 0.05f;
|
||||||
const float s = 0.2f;
|
const float s = 0.2f;
|
||||||
|
|
||||||
for (const auto& pt : navigationEditorPoints) {
|
for (const auto& pt : navigationEditorPoints) {
|
||||||
@@ -133,7 +133,7 @@ namespace FRG
|
|||||||
PathFinder::ObstaclePolygon poly;
|
PathFinder::ObstaclePolygon poly;
|
||||||
poly.name = "editor_obstacle_" + std::to_string(++navigationEditorObstacleCounter);
|
poly.name = "editor_obstacle_" + std::to_string(++navigationEditorObstacleCounter);
|
||||||
for (const auto& pt : navigationEditorPoints) {
|
for (const auto& pt : navigationEditorPoints) {
|
||||||
poly.polygon.emplace_back(pt.x(), pt.z());
|
poly.polygon.emplace_back(static_cast<FRG::fp>(pt.x()), static_cast<FRG::fp>(pt.z()));
|
||||||
}
|
}
|
||||||
|
|
||||||
loc.gameServer_.submit(AddNavigationObstacleCommand{
|
loc.gameServer_.submit(AddNavigationObstacleCommand{
|
||||||
@@ -146,10 +146,10 @@ namespace FRG
|
|||||||
if (!state || state->activeNavigationIndex < 0 ||
|
if (!state || state->activeNavigationIndex < 0 ||
|
||||||
state->activeNavigationIndex >= static_cast<int>(state->navigationMaps.size())) return;
|
state->activeNavigationIndex >= static_cast<int>(state->navigationMaps.size())) return;
|
||||||
const float y =
|
const float y =
|
||||||
state->navigationMaps[state->activeNavigationIndex].getFloorY() + 0.02f;
|
static_cast<float>(state->navigationMaps[state->activeNavigationIndex].getFloorY()) + 0.02f;
|
||||||
const Eigen::Vector3f red(1.0f, 0.0f, 0.0f);
|
const Eigen::Vector3f red(1.0f, 0.0f, 0.0f);
|
||||||
VertexRenderStruct mesh;
|
VertexRenderStruct mesh;
|
||||||
mesh.data = CreatePolygonFloor(poly.polygon, y, red);
|
mesh.data = CreatePolygonFloor(toEigenVector(poly.polygon), y, red);
|
||||||
mesh.RefreshVBO();
|
mesh.RefreshVBO();
|
||||||
navigationEditorNavMeshes.push_back(std::move(mesh));
|
navigationEditorNavMeshes.push_back(std::move(mesh));
|
||||||
}
|
}
|
||||||
@@ -379,9 +379,9 @@ namespace FRG
|
|||||||
|
|
||||||
auto& target = loc.interactiveObjects[selectedInteractiveObjectIndex];
|
auto& target = loc.interactiveObjects[selectedInteractiveObjectIndex];
|
||||||
target.state.boundsMin = Eigen::Vector3f(
|
target.state.boundsMin = Eigen::Vector3f(
|
||||||
min(localA.x(), localB.x()), 0.f, min(localA.z(), localB.z()));
|
std::min(localA.x(), localB.x()), 0.f, std::min(localA.z(), localB.z()));
|
||||||
target.state.boundsMax = Eigen::Vector3f(
|
target.state.boundsMax = Eigen::Vector3f(
|
||||||
max(localA.x(), localB.x()), 1.f, max(localA.z(), localB.z()));
|
std::max(localA.x(), localB.x()), 1.f, std::max(localA.z(), localB.z()));
|
||||||
|
|
||||||
boundsClickCount = 0;
|
boundsClickCount = 0;
|
||||||
buildInteractiveObjectBoundsMeshes();
|
buildInteractiveObjectBoundsMeshes();
|
||||||
|
|||||||
+115
-44
@@ -126,6 +126,7 @@ const CharacterState* LocationGameState::findCharacter(
|
|||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
std::vector<PathFinder::DynamicObstacle> LocationGameState::collectPathObstacles(
|
std::vector<PathFinder::DynamicObstacle> LocationGameState::collectPathObstacles(
|
||||||
const std::string& requesterId,
|
const std::string& requesterId,
|
||||||
const PlayerStateMap& players,
|
const PlayerStateMap& players,
|
||||||
@@ -140,16 +141,21 @@ std::vector<PathFinder::DynamicObstacle> LocationGameState::collectPathObstacles
|
|||||||
std::sort(characters.begin(), characters.end(),
|
std::sort(characters.begin(), characters.end(),
|
||||||
[](const auto& lhs, const auto& rhs) { return lhs.first < rhs.first; });
|
[](const auto& lhs, const auto& rhs) { return lhs.first < rhs.first; });
|
||||||
|
|
||||||
|
const CharacterState* requester = findCharacter(requesterId, players);
|
||||||
|
const std::string attackTargetId =
|
||||||
|
requester ? requester->attackTargetId : std::string();
|
||||||
std::vector<PathFinder::DynamicObstacle> obstacles;
|
std::vector<PathFinder::DynamicObstacle> obstacles;
|
||||||
obstacles.reserve(characters.size());
|
obstacles.reserve(characters.size());
|
||||||
for (const auto& [id, character] : characters) {
|
for (const auto& [id, character] : characters) {
|
||||||
if (id == requesterId || !character->enabled || character->hp <= 0.f)
|
if (id == requesterId || !character->enabled || character->hp <= 0.f)
|
||||||
continue;
|
continue;
|
||||||
|
if (id == attackTargetId && id != forcedObstacleId)
|
||||||
|
continue;
|
||||||
if (character->movedLastTick && id != forcedObstacleId) continue;
|
if (character->movedLastTick && id != forcedObstacleId) continue;
|
||||||
obstacles.push_back({character->position, character->collisionRadius});
|
obstacles.push_back({character->position, character->collisionRadius});
|
||||||
}
|
}
|
||||||
return obstacles;
|
return obstacles;
|
||||||
}
|
}*/
|
||||||
|
|
||||||
bool LocationGameState::setCharacterDestination(
|
bool LocationGameState::setCharacterDestination(
|
||||||
const std::string& id,
|
const std::string& id,
|
||||||
@@ -161,8 +167,7 @@ bool LocationGameState::setCharacterDestination(
|
|||||||
CharacterState* character = findCharacter(id, players);
|
CharacterState* character = findCharacter(id, players);
|
||||||
if (!character || !location) return false;
|
if (!character || !location) return false;
|
||||||
character->setTarget(
|
character->setTarget(
|
||||||
target, std::move(callbackName), *location, id,
|
target, std::move(callbackName), *location, id);
|
||||||
collectPathObstacles(id, players, forcedObstacleId));
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,10 +178,81 @@ bool LocationGameState::replanCharacter(
|
|||||||
{
|
{
|
||||||
CharacterState* character = findCharacter(id, players);
|
CharacterState* character = findCharacter(id, players);
|
||||||
if (!character || !location) return false;
|
if (!character || !location) return false;
|
||||||
character->forceReplan(
|
if (!forcedObstacleId.empty() && !character->isPlayer &&
|
||||||
*location, id, collectPathObstacles(id, players, forcedObstacleId));
|
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);
|
||||||
return true;
|
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);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
character->forceReplan(
|
||||||
|
*location, id);
|
||||||
|
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()
|
void LocationGameState::startTaxi()
|
||||||
{
|
{
|
||||||
@@ -321,13 +397,6 @@ LocationSimulationEvents LocationGameState::update(
|
|||||||
if (player) playerIds.push_back(id);
|
if (player) playerIds.push_back(id);
|
||||||
std::sort(playerIds.begin(), playerIds.end());
|
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;
|
std::vector<std::string> npcIds;
|
||||||
npcIds.reserve(npcs.size());
|
npcIds.reserve(npcs.size());
|
||||||
for (const auto& [id, npc] : npcs)
|
for (const auto& [id, npc] : npcs)
|
||||||
@@ -343,30 +412,8 @@ LocationSimulationEvents LocationGameState::update(
|
|||||||
continue;
|
continue;
|
||||||
|
|
||||||
const std::string previousTarget = npc.attackTargetId;
|
const std::string previousTarget = npc.attackTargetId;
|
||||||
const auto locked = players.find(previousTarget);
|
const std::string selectedTarget =
|
||||||
const bool lockValid =
|
selectNpcTarget(npc, players, true);
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedTarget != previousTarget) {
|
if (selectedTarget != previousTarget) {
|
||||||
npc.stopInPlace();
|
npc.stopInPlace();
|
||||||
@@ -386,7 +433,7 @@ LocationSimulationEvents LocationGameState::update(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/*
|
||||||
std::vector<CharacterMovementObstacle> movementObstacles;
|
std::vector<CharacterMovementObstacle> movementObstacles;
|
||||||
movementObstacles.reserve(playerIds.size() + npcIds.size());
|
movementObstacles.reserve(playerIds.size() + npcIds.size());
|
||||||
auto addMovementObstacle =
|
auto addMovementObstacle =
|
||||||
@@ -419,8 +466,9 @@ LocationSimulationEvents LocationGameState::update(
|
|||||||
std::sort(
|
std::sort(
|
||||||
movementObstacles.begin(), movementObstacles.end(),
|
movementObstacles.begin(), movementObstacles.end(),
|
||||||
[](const CharacterMovementObstacle& lhs,
|
[](const CharacterMovementObstacle& lhs,
|
||||||
const CharacterMovementObstacle& rhs) { return lhs.id < rhs.id; });
|
const CharacterMovementObstacle& rhs) { return lhs.id < rhs.id; });*/
|
||||||
|
|
||||||
|
/*
|
||||||
auto advance = [&](const std::string& id, CharacterState& state) {
|
auto advance = [&](const std::string& id, CharacterState& state) {
|
||||||
CharacterUpdateContext context;
|
CharacterUpdateContext context;
|
||||||
context.characterId = id;
|
context.characterId = id;
|
||||||
@@ -450,14 +498,36 @@ LocationSimulationEvents LocationGameState::update(
|
|||||||
events.deathAnimationComplete || events.navigationRequested) {
|
events.deathAnimationComplete || events.navigationRequested) {
|
||||||
result.characters.push_back({id, std::move(events)});
|
result.characters.push_back({id, std::move(events)});
|
||||||
}
|
}
|
||||||
};
|
};*/
|
||||||
|
/*
|
||||||
for (const std::string& id : playerIds)
|
for (const std::string& id : playerIds)
|
||||||
if (players.at(id)->enabled)
|
if (players.at(id)->enabled)
|
||||||
advance(id, *players.at(id));
|
advance(id, *players.at(id));
|
||||||
for (const std::string& id : npcIds)
|
for (const std::string& id : npcIds)
|
||||||
|
if (npcs.at(id)->enablesd)
|
||||||
|
advance(id, *npcs.at(id));*/
|
||||||
|
|
||||||
|
for (const std::string& id : playerIds)
|
||||||
|
{
|
||||||
|
if (players.at(id)->enabled)
|
||||||
|
{
|
||||||
|
CharacterUpdateContext context;
|
||||||
|
context.characterId = id;
|
||||||
|
auto& state = *players.at(id);
|
||||||
|
CharacterUpdateEvents events = state.update(deltaMs, context);
|
||||||
|
//advance(id, *players.at(id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const std::string& id : npcIds)
|
||||||
|
{
|
||||||
if (npcs.at(id)->enabled)
|
if (npcs.at(id)->enabled)
|
||||||
advance(id, *npcs.at(id));
|
{
|
||||||
|
//advance(id, *npcs.at(id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
for (const CharacterSimulationEvent& source : result.characters) {
|
for (const CharacterSimulationEvent& source : result.characters) {
|
||||||
if (!source.events.attackImpact) continue;
|
if (!source.events.attackImpact) continue;
|
||||||
@@ -474,7 +544,8 @@ LocationSimulationEvents LocationGameState::update(
|
|||||||
source.events.attackDirection});
|
source.events.attackDirection});
|
||||||
}
|
}
|
||||||
|
|
||||||
auto player = players.find("player");
|
/*
|
||||||
|
auto player = players.find("player1");
|
||||||
if (!interactiveObjectsLocked &&
|
if (!interactiveObjectsLocked &&
|
||||||
!location->targetInteractiveObjectId.empty() &&
|
!location->targetInteractiveObjectId.empty() &&
|
||||||
player != players.end() && player->second && player->second->enabled) {
|
player != players.end() && player->second && player->second->enabled) {
|
||||||
@@ -502,11 +573,11 @@ LocationSimulationEvents LocationGameState::update(
|
|||||||
(player->second->position - npc->position).norm();
|
(player->second->position - npc->position).norm();
|
||||||
if (distance <= NPC_TALK_DISTANCE) {
|
if (distance <= NPC_TALK_DISTANCE) {
|
||||||
player->second->stopInPlace();
|
player->second->stopInPlace();
|
||||||
npc->faceTargetId = "player";
|
npc->faceTargetId = "player1";
|
||||||
result.npcInteractionId = location->targetInteractNpcId;
|
result.npcInteractionId = location->targetInteractNpcId;
|
||||||
location->targetInteractNpcId.clear();
|
location->targetInteractNpcId.clear();
|
||||||
}
|
}
|
||||||
}
|
}*/
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,10 +64,10 @@ struct LocationGameState {
|
|||||||
CharacterState* findCharacter(const std::string& id, PlayerStateMap& players);
|
CharacterState* findCharacter(const std::string& id, PlayerStateMap& players);
|
||||||
const CharacterState* findCharacter(
|
const CharacterState* findCharacter(
|
||||||
const std::string& id, const PlayerStateMap& players) const;
|
const std::string& id, const PlayerStateMap& players) const;
|
||||||
std::vector<PathFinder::DynamicObstacle> collectPathObstacles(
|
/*std::vector<PathFinder::DynamicObstacle> collectPathObstacles(
|
||||||
const std::string& requesterId,
|
const std::string& requesterId,
|
||||||
const PlayerStateMap& players,
|
const PlayerStateMap& players,
|
||||||
const std::string& forcedObstacleId = {}) const;
|
const std::string& forcedObstacleId = {}) const;*/
|
||||||
bool setCharacterDestination(
|
bool setCharacterDestination(
|
||||||
const std::string& id,
|
const std::string& id,
|
||||||
const Eigen::Vector3f& target,
|
const Eigen::Vector3f& target,
|
||||||
@@ -86,6 +86,10 @@ struct LocationGameState {
|
|||||||
void load(const nlohmann::json& in);
|
void load(const nlohmann::json& in);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
std::string selectNpcTarget(
|
||||||
|
const CharacterState& npc,
|
||||||
|
const PlayerStateMap& players,
|
||||||
|
bool preserveValidLock) const;
|
||||||
bool updateNpcCar(std::int64_t deltaMs);
|
bool updateNpcCar(std::int64_t deltaMs);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ void loadSinglePlayerPersistence(
|
|||||||
|
|
||||||
|
|
||||||
// Entity ownership stays above this navigation-only layer.
|
// Entity ownership stays above this navigation-only layer.
|
||||||
std::vector<Eigen::Vector3f> LocationNetworkState::planPathExceptMyself(
|
/**td::vector<Eigen::Vector3f> LocationNetworkState::planPathExceptMyself(
|
||||||
const Eigen::Vector3f& start,
|
const Eigen::Vector3f& start,
|
||||||
const Eigen::Vector3f& end,
|
const Eigen::Vector3f& end,
|
||||||
const std::string& entityId,
|
const std::string& entityId,
|
||||||
@@ -135,7 +135,7 @@ void loadSinglePlayerPersistence(
|
|||||||
return {};
|
return {};
|
||||||
return navigationMaps[activeNavigationIndex].findPathToNearest(
|
return navigationMaps[activeNavigationIndex].findPathToNearest(
|
||||||
start, end, dynamicObstacles);
|
start, end, dynamicObstacles);
|
||||||
}
|
}*/
|
||||||
|
|
||||||
void LocationNetworkState::initNavigation(
|
void LocationNetworkState::initNavigation(
|
||||||
const std::vector<std::string>& paths, const std::string& zipFile)
|
const std::vector<std::string>& paths, const std::string& zipFile)
|
||||||
|
|||||||
+2
-2
@@ -41,11 +41,11 @@ struct LocationNetworkState : public ISaveable {
|
|||||||
//This value is populated from the JSON configs and should not be changed during the regular gameplay.
|
//This value is populated from the JSON configs and should not be changed during the regular gameplay.
|
||||||
//It might be changed only in the editor mode during development.
|
//It might be changed only in the editor mode during development.
|
||||||
std::vector<PathFinder> navigationMaps;
|
std::vector<PathFinder> navigationMaps;
|
||||||
std::vector<Eigen::Vector3f> planPathExceptMyself(
|
/*std::vector<Eigen::Vector3f> planPathExceptMyself(
|
||||||
const Eigen::Vector3f& start,
|
const Eigen::Vector3f& start,
|
||||||
const Eigen::Vector3f& end,
|
const Eigen::Vector3f& end,
|
||||||
const std::string& entityId,
|
const std::string& entityId,
|
||||||
const std::vector<PathFinder::DynamicObstacle>& dynamicObstacles = {}) const;
|
const std::vector<PathFinder::DynamicObstacle>& dynamicObstacles = {}) const;*/
|
||||||
void initNavigation(
|
void initNavigation(
|
||||||
const std::vector<std::string>& paths,
|
const std::vector<std::string>& paths,
|
||||||
const std::string& zipFile = std::string());
|
const std::string& zipFile = std::string());
|
||||||
|
|||||||
+6
-7
@@ -17,10 +17,6 @@
|
|||||||
|
|
||||||
namespace FRG {
|
namespace FRG {
|
||||||
|
|
||||||
#ifdef __EMSCRIPTEN__
|
|
||||||
//extern Game* Game::s_instance;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Localization
|
// Localization
|
||||||
static const std::string EMPTY_LANGUAGE_RU = u8"(пусто)";
|
static const std::string EMPTY_LANGUAGE_RU = u8"(пусто)";
|
||||||
static const std::string EMPTY_LANGUAGE_EN = "(empty)";
|
static const std::string EMPTY_LANGUAGE_EN = "(empty)";
|
||||||
@@ -81,7 +77,7 @@ namespace FRG {
|
|||||||
pushWord(currentWord);
|
pushWord(currentWord);
|
||||||
if (!currentLine.empty()) flushLine();
|
if (!currentLine.empty()) flushLine();
|
||||||
|
|
||||||
return max(1, lines);
|
return std::max(1, lines);
|
||||||
}
|
}
|
||||||
|
|
||||||
static std::string trimChatPreview(const std::string& msg, int maxChars = 22) {
|
static std::string trimChatPreview(const std::string& msg, int maxChars = 22) {
|
||||||
@@ -408,6 +404,9 @@ namespace FRG {
|
|||||||
const std::string& loc = snapshot->currentLocationId;
|
const std::string& loc = snapshot->currentLocationId;
|
||||||
if (loc == "uni_exterior") return snapshot->isDarklands ? hudUniExtDarkRoot : hudUniExtRoot;
|
if (loc == "uni_exterior") return snapshot->isDarklands ? hudUniExtDarkRoot : hudUniExtRoot;
|
||||||
if (loc == "uni_interior") return snapshot->isDarklands ? hudUniIntDarkRoot : hudUniIntRoot;
|
if (loc == "uni_interior") return snapshot->isDarklands ? hudUniIntDarkRoot : hudUniIntRoot;
|
||||||
|
// Other locations (e.g. campaigns that start directly in Darklands) have no
|
||||||
|
// dedicated HUD; reuse the Darklands one so the health bar is shown.
|
||||||
|
if (snapshot->isDarklands && !loc.empty()) return hudUniExtDarkRoot;
|
||||||
return hudDormRoot; // dorm, or a new game before the location is set
|
return hudDormRoot; // dorm, or a new game before the location is set
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2254,9 +2253,9 @@ namespace FRG {
|
|||||||
|
|
||||||
float MenuManager::ToastEntry::currentAlpha() const {
|
float MenuManager::ToastEntry::currentAlpha() const {
|
||||||
switch (state) {
|
switch (state) {
|
||||||
case State::FadeIn: return min(timer / TOAST_FADE_MS, 1.0f);
|
case State::FadeIn: return std::min(timer / TOAST_FADE_MS, 1.0f);
|
||||||
case State::Visible: return 1.0f;
|
case State::Visible: return 1.0f;
|
||||||
case State::FadeOut: return max(1.0f - timer / TOAST_FADE_MS, 0.0f);
|
case State::FadeOut: return std::max(1.0f - timer / TOAST_FADE_MS, 0.0f);
|
||||||
}
|
}
|
||||||
return 0.0f;
|
return 0.0f;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ namespace FRG {
|
|||||||
ScriptEngine::~ScriptEngine() = default;
|
ScriptEngine::~ScriptEngine() = default;
|
||||||
|
|
||||||
void ScriptEngine::init(Location* loc, AudioPlayerAsync* audioPlayer, const std::string& scriptPath) {
|
void ScriptEngine::init(Location* loc, AudioPlayerAsync* audioPlayer, const std::string& scriptPath) {
|
||||||
|
logger() << "ScriptEngine::init with script path: " << scriptPath << std::endl;
|
||||||
impl = std::make_unique<Impl>();
|
impl = std::make_unique<Impl>();
|
||||||
sol::state& lua = impl->lua;
|
sol::state& lua = impl->lua;
|
||||||
|
|
||||||
@@ -172,7 +173,7 @@ namespace FRG {
|
|||||||
<< npcId << "'\n";
|
<< npcId << "'\n";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
loc->faceCharacter(*npc, "player");
|
loc->faceCharacter(*npc, "player1");
|
||||||
});
|
});
|
||||||
|
|
||||||
api.set_function("player_walk_to",
|
api.set_function("player_walk_to",
|
||||||
|
|||||||
@@ -293,7 +293,7 @@ LocationDefinition LocationContentLoader::load(
|
|||||||
for (const auto& item : root.value("npcs", json::array())) {
|
for (const auto& item : root.value("npcs", json::array())) {
|
||||||
NpcData data;
|
NpcData data;
|
||||||
data.id = item.at("id").get<std::string>();
|
data.id = item.at("id").get<std::string>();
|
||||||
if (data.id.empty() || data.id == "player" ||
|
if (data.id.empty() || data.id == "player1" ||
|
||||||
!ids.insert(data.id).second) {
|
!ids.insert(data.id).second) {
|
||||||
throw std::runtime_error(
|
throw std::runtime_error(
|
||||||
"Missing, duplicate, or reserved NPC id in " +
|
"Missing, duplicate, or reserved NPC id in " +
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
#include <variant>
|
#include <variant>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
#include "../navigation/PathFinder.h"
|
||||||
|
|
||||||
namespace FRG {
|
namespace FRG {
|
||||||
|
|
||||||
@@ -110,7 +111,7 @@ struct SetNavigationAreaAvailableCommand {
|
|||||||
struct AddNavigationObstacleCommand {
|
struct AddNavigationObstacleCommand {
|
||||||
std::string locationId;
|
std::string locationId;
|
||||||
std::string name;
|
std::string name;
|
||||||
std::vector<Eigen::Vector2f> polygon;
|
std::vector<Vec2fp> polygon;
|
||||||
};
|
};
|
||||||
struct AddInventoryItemCommand { std::string itemId; };
|
struct AddInventoryItemCommand { std::string itemId; };
|
||||||
struct RemoveInventoryItemCommand { std::string itemId; };
|
struct RemoveInventoryItemCommand { std::string itemId; };
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ private:
|
|||||||
GameSnapshot buildSnapshot() const;
|
GameSnapshot buildSnapshot() const;
|
||||||
|
|
||||||
GameState state_;
|
GameState state_;
|
||||||
std::string localPlayerId_ = "player";
|
std::string localPlayerId_ = "player1";
|
||||||
std::string resourceZipFile_;
|
std::string resourceZipFile_;
|
||||||
std::vector<std::string> networkPlayerSlots_;
|
std::vector<std::string> networkPlayerSlots_;
|
||||||
std::int64_t battleZoneTestBotElapsedMs_ = 0;
|
std::int64_t battleZoneTestBotElapsedMs_ = 0;
|
||||||
|
|||||||
@@ -5,6 +5,21 @@
|
|||||||
#else
|
#else
|
||||||
#include <boost/asio.hpp>
|
#include <boost/asio.hpp>
|
||||||
#include <boost/beast.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
|
#endif
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <deque>
|
#include <deque>
|
||||||
@@ -23,19 +38,104 @@ namespace asio = boost::asio;
|
|||||||
namespace beast = boost::beast;
|
namespace beast = boost::beast;
|
||||||
namespace websocket = beast::websocket;
|
namespace websocket = beast::websocket;
|
||||||
using tcp = asio::ip::tcp;
|
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
|
#endif
|
||||||
|
|
||||||
} // namespace
|
} // 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 {
|
class WebsocketGameClient::Impl {
|
||||||
public:
|
public:
|
||||||
#ifdef __EMSCRIPTEN__
|
#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))
|
: host_(std::move(host))
|
||||||
, port_(std::move(port))
|
, port_(std::move(port))
|
||||||
, path_(std::move(path))
|
, path_(std::move(path))
|
||||||
{
|
{
|
||||||
url_ = "ws://" + host_ + ':' + port_ +
|
url_ = std::string(useTls ? "wss://" : "ws://") + host_ + ':' + port_ +
|
||||||
(path_.empty() || path_.front() == '/' ? path_ : "/" + path_);
|
(path_.empty() || path_.front() == '/' ? path_ : "/" + path_);
|
||||||
std::cout << "[network] Opening " << url_ << std::endl;
|
std::cout << "[network] Opening " << url_ << std::endl;
|
||||||
|
|
||||||
@@ -45,7 +145,7 @@ public:
|
|||||||
attributes.createOnMainThread = EM_TRUE;
|
attributes.createOnMainThread = EM_TRUE;
|
||||||
socket_ = emscripten_websocket_new(&attributes);
|
socket_ = emscripten_websocket_new(&attributes);
|
||||||
if (socket_ <= 0) {
|
if (socket_ <= 0) {
|
||||||
fail("create WebSocket");
|
fail("create WebSocket", socket_);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,11 +205,21 @@ private:
|
|||||||
}
|
}
|
||||||
|
|
||||||
static EM_BOOL onClose(
|
static EM_BOOL onClose(
|
||||||
int, const EmscriptenWebSocketCloseEvent*, void* userData)
|
int, const EmscriptenWebSocketCloseEvent* event, void* userData)
|
||||||
{
|
{
|
||||||
auto* self = static_cast<Impl*>(userData);
|
auto* self = static_cast<Impl*>(userData);
|
||||||
self->connected_ = false;
|
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;
|
return EM_TRUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,11 +236,13 @@ private:
|
|||||||
return EM_TRUE;
|
return EM_TRUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
void fail(const char* operation)
|
void fail(const char* operation, int result = 0)
|
||||||
{
|
{
|
||||||
connected_ = false;
|
connected_ = false;
|
||||||
connectionFailed_ = true;
|
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_;
|
std::string host_;
|
||||||
@@ -143,14 +255,38 @@ private:
|
|||||||
bool closing_ = false;
|
bool closing_ = false;
|
||||||
std::vector<std::string> messages_;
|
std::vector<std::string> messages_;
|
||||||
#else
|
#else
|
||||||
Impl(std::string host, std::string port, std::string path)
|
Impl(std::string host, std::string port, std::string path, bool useTls)
|
||||||
: resolver_(io_)
|
:
|
||||||
|
#ifdef NETWORK_TLS
|
||||||
|
sslContext_(asio::ssl::context::tls_client)
|
||||||
|
, resolver_(io_)
|
||||||
|
, secureSocket_(io_, sslContext_)
|
||||||
|
#else
|
||||||
|
resolver_(io_)
|
||||||
|
#endif
|
||||||
, socket_(io_)
|
, socket_(io_)
|
||||||
, host_(std::move(host))
|
, host_(std::move(host))
|
||||||
, port_(std::move(port))
|
, port_(std::move(port))
|
||||||
, path_(std::move(path))
|
, 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;
|
<< path_ << std::endl;
|
||||||
resolve();
|
resolve();
|
||||||
thread_ = std::thread([this]() { io_.run(); });
|
thread_ = std::thread([this]() { io_.run(); });
|
||||||
@@ -184,6 +320,20 @@ private:
|
|||||||
}
|
}
|
||||||
|
|
||||||
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()
|
void resolve()
|
||||||
{
|
{
|
||||||
resolver_.async_resolve(
|
resolver_.async_resolve(
|
||||||
@@ -192,34 +342,68 @@ private:
|
|||||||
if (error) return fail("resolve", error);
|
if (error) return fail("resolve", error);
|
||||||
std::cout << "[network] WebSocket host resolved; connecting"
|
std::cout << "[network] WebSocket host resolved; connecting"
|
||||||
<< std::endl;
|
<< std::endl;
|
||||||
|
withStream([&](auto& stream) {
|
||||||
asio::async_connect(
|
asio::async_connect(
|
||||||
socket_.next_layer(), results,
|
beast::get_lowest_layer(stream), results,
|
||||||
[this](beast::error_code connectError,
|
[this](beast::error_code connectError,
|
||||||
const tcp::endpoint&) {
|
const tcp::endpoint&) {
|
||||||
if (connectError) return fail("connect", connectError);
|
if (connectError) return fail("connect", connectError);
|
||||||
std::cout << "[network] TCP connected; starting WebSocket handshake"
|
std::cout << "[network] TCP connected" << std::endl;
|
||||||
<< std::endl;
|
startTlsOrWebsocketHandshake();
|
||||||
socket_.set_option(
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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(
|
websocket::stream_base::timeout::suggested(
|
||||||
beast::role_type::client));
|
beast::role_type::client));
|
||||||
socket_.async_handshake(
|
stream.async_handshake(
|
||||||
host_, path_,
|
hostHeader, path_,
|
||||||
[this](beast::error_code handshakeError) {
|
[this](beast::error_code handshakeError) {
|
||||||
if (handshakeError)
|
if (handshakeError)
|
||||||
return fail("handshake", handshakeError);
|
return fail("handshake", handshakeError);
|
||||||
connected_.store(true);
|
connected_.store(true);
|
||||||
std::cout
|
std::cout
|
||||||
<< "[network] Connected to ws://"
|
<< "[network] Connected to " << scheme()
|
||||||
<< host_ << ':' << port_ << path_ << std::endl;
|
<< host_ << ':' << port_ << path_ << std::endl;
|
||||||
read();
|
read();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void read()
|
void read()
|
||||||
{
|
{
|
||||||
socket_.async_read(
|
withStream([this](auto& stream) {
|
||||||
|
stream.async_read(
|
||||||
buffer_,
|
buffer_,
|
||||||
[this](beast::error_code error, std::size_t) {
|
[this](beast::error_code error, std::size_t) {
|
||||||
if (error) {
|
if (error) {
|
||||||
@@ -235,17 +419,20 @@ private:
|
|||||||
}
|
}
|
||||||
read();
|
read();
|
||||||
});
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void write()
|
void write()
|
||||||
{
|
{
|
||||||
socket_.async_write(
|
withStream([this](auto& stream) {
|
||||||
|
stream.async_write(
|
||||||
asio::buffer(outbound_.front()),
|
asio::buffer(outbound_.front()),
|
||||||
[this](beast::error_code error, std::size_t) {
|
[this](beast::error_code error, std::size_t) {
|
||||||
if (error) return fail("write", error);
|
if (error) return fail("write", error);
|
||||||
outbound_.pop_front();
|
outbound_.pop_front();
|
||||||
if (!outbound_.empty()) write();
|
if (!outbound_.empty()) write();
|
||||||
});
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void fail(const char* operation, beast::error_code error)
|
void fail(const char* operation, beast::error_code error)
|
||||||
@@ -257,13 +444,20 @@ private:
|
|||||||
}
|
}
|
||||||
|
|
||||||
asio::io_context io_;
|
asio::io_context io_;
|
||||||
|
#ifdef NETWORK_TLS
|
||||||
|
asio::ssl::context sslContext_;
|
||||||
|
#endif
|
||||||
tcp::resolver resolver_;
|
tcp::resolver resolver_;
|
||||||
|
#ifdef NETWORK_TLS
|
||||||
|
websocket::stream<beast::ssl_stream<tcp::socket>> secureSocket_;
|
||||||
|
#endif
|
||||||
websocket::stream<tcp::socket> socket_;
|
websocket::stream<tcp::socket> socket_;
|
||||||
beast::flat_buffer buffer_;
|
beast::flat_buffer buffer_;
|
||||||
std::thread thread_;
|
std::thread thread_;
|
||||||
std::string host_;
|
std::string host_;
|
||||||
std::string port_;
|
std::string port_;
|
||||||
std::string path_;
|
std::string path_;
|
||||||
|
bool useTls_ = false;
|
||||||
std::atomic_bool connected_{false};
|
std::atomic_bool connected_{false};
|
||||||
std::atomic_bool connectionFailed_{false};
|
std::atomic_bool connectionFailed_{false};
|
||||||
std::mutex messagesMutex_;
|
std::mutex messagesMutex_;
|
||||||
@@ -273,9 +467,15 @@ private:
|
|||||||
};
|
};
|
||||||
|
|
||||||
WebsocketGameClient::WebsocketGameClient(
|
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>(
|
: 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;
|
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 {
|
class WebsocketGameClient final : public IGameServer {
|
||||||
public:
|
public:
|
||||||
WebsocketGameClient(
|
WebsocketGameClient(
|
||||||
std::string host = "127.0.0.1",
|
std::string host = "127.0.0.1",
|
||||||
std::string port = "8080",
|
std::string port = "8080",
|
||||||
std::string path = "/");
|
std::string path = "/",
|
||||||
|
bool useTls = false);
|
||||||
|
explicit WebsocketGameClient(const WebsocketEndpoint& endpoint);
|
||||||
~WebsocketGameClient();
|
~WebsocketGameClient();
|
||||||
|
|
||||||
WebsocketGameClient(const WebsocketGameClient&) = delete;
|
WebsocketGameClient(const WebsocketGameClient&) = delete;
|
||||||
|
|||||||
+28
-13
@@ -2,6 +2,7 @@
|
|||||||
#include "Environment.h"
|
#include "Environment.h"
|
||||||
#include "utils/Utils.h"
|
#include "utils/Utils.h"
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
#include <cmath>
|
||||||
#ifdef __ANDROID__
|
#ifdef __ANDROID__
|
||||||
#include <android/log.h>
|
#include <android/log.h>
|
||||||
#endif
|
#endif
|
||||||
@@ -75,14 +76,27 @@ extern "C" void EMSCRIPTEN_KEEPALIVE onFileSystemLoaded() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Single definition of the "logical" viewport size: the *visible* area of the page
|
||||||
|
// (visualViewport), not 100vh, which on mobile includes the area under the URL bar.
|
||||||
|
static void getViewportCssSize(int& w, int& h) {
|
||||||
|
w = EM_ASM_INT({
|
||||||
|
var vv = window.visualViewport;
|
||||||
|
return Math.round((vv && vv.width) || window.innerWidth);
|
||||||
|
});
|
||||||
|
h = EM_ASM_INT({
|
||||||
|
var vv = window.visualViewport;
|
||||||
|
return Math.round((vv && vv.height) || window.innerHeight);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
static void applyResize(int logicalW, int logicalH) {
|
static void applyResize(int logicalW, int logicalH) {
|
||||||
// Получаем коэффициент плотности пикселей (например, 2.625 на Pixel или 3.0 на Samsung)
|
// Получаем коэффициент плотности пикселей (например, 2.625 на Pixel или 3.0 на Samsung)
|
||||||
double dpr = emscripten_get_device_pixel_ratio();
|
double dpr = emscripten_get_device_pixel_ratio();
|
||||||
//double dpr = 1; // low quality
|
//double dpr = 1; // low quality
|
||||||
|
|
||||||
// Вычисляем реальные физические пиксели
|
// Вычисляем реальные физические пиксели
|
||||||
int physicalW = static_cast<int>(logicalW * dpr);
|
int physicalW = static_cast<int>(std::lround(logicalW * dpr));
|
||||||
int physicalH = static_cast<int>(logicalH * dpr);
|
int physicalH = static_cast<int>(std::lround(logicalH * dpr));
|
||||||
|
|
||||||
// Устанавливаем размер внутреннего буфера канваса
|
// Устанавливаем размер внутреннего буфера канваса
|
||||||
emscripten_set_canvas_element_size("#canvas", physicalW, physicalH);
|
emscripten_set_canvas_element_size("#canvas", physicalW, physicalH);
|
||||||
@@ -94,6 +108,11 @@ static void applyResize(int logicalW, int logicalH) {
|
|||||||
SDL_SetWindowSize(FRG::Environment::window, physicalW, physicalH);
|
SDL_SetWindowSize(FRG::Environment::window, physicalW, physicalH);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SDL may have written an inline CSS size derived from the physical pixels, which
|
||||||
|
// overrides the stylesheet. Re-assert the CSS box so its aspect always matches the
|
||||||
|
// drawing buffer (otherwise the image is stretched / cropped on mobile).
|
||||||
|
emscripten_set_element_css_size("#canvas", logicalW, logicalH);
|
||||||
|
|
||||||
// Пушим событие, чтобы движок пересчитал матрицы проекции
|
// Пушим событие, чтобы движок пересчитал матрицы проекции
|
||||||
SDL_Event e = {};
|
SDL_Event e = {};
|
||||||
e.type = SDL_WINDOWEVENT;
|
e.type = SDL_WINDOWEVENT;
|
||||||
@@ -118,8 +137,8 @@ void MainLoop() {
|
|||||||
|
|
||||||
// Теперь DOM перестроен, можно забирать актуальные размеры.
|
// Теперь DOM перестроен, можно забирать актуальные размеры.
|
||||||
// Если canvas должен занимать все окно браузера:
|
// Если canvas должен занимать все окно браузера:
|
||||||
int canvasW = EM_ASM_INT({ return window.innerWidth; });
|
int canvasW, canvasH;
|
||||||
int canvasH = EM_ASM_INT({ return window.innerHeight; });
|
getViewportCssSize(canvasW, canvasH);
|
||||||
|
|
||||||
applyResize(canvasW, canvasH);
|
applyResize(canvasW, canvasH);
|
||||||
}
|
}
|
||||||
@@ -141,14 +160,10 @@ EM_BOOL onWebGLContextRestored(int /*eventType*/, const void* /*reserved*/, void
|
|||||||
|
|
||||||
|
|
||||||
EM_BOOL onWindowResized(int /*eventType*/, const EmscriptenUiEvent* e, void* /*userData*/) {
|
EM_BOOL onWindowResized(int /*eventType*/, const EmscriptenUiEvent* e, void* /*userData*/) {
|
||||||
double w, h;
|
int w, h;
|
||||||
emscripten_get_element_css_size("window", &w, &h);
|
getViewportCssSize(w, h);
|
||||||
|
|
||||||
// Если "window" не поддерживается вашей версией SDK, используем данные из e:
|
applyResize(w, h);
|
||||||
// int logicalW = e->windowInnerWidth;
|
|
||||||
// int logicalH = e->windowInnerHeight;
|
|
||||||
|
|
||||||
applyResize(static_cast<int>(w), static_cast<int>(h));
|
|
||||||
return EM_FALSE;
|
return EM_FALSE;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,8 +196,8 @@ int main(int argc, char* argv[]) {
|
|||||||
emscripten_set_resize_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, nullptr, EM_FALSE, onWindowResized);
|
emscripten_set_resize_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, nullptr, EM_FALSE, onWindowResized);
|
||||||
emscripten_set_fullscreenchange_callback(EMSCRIPTEN_EVENT_TARGET_DOCUMENT, nullptr, EM_TRUE, onFullscreenChanged);
|
emscripten_set_fullscreenchange_callback(EMSCRIPTEN_EVENT_TARGET_DOCUMENT, nullptr, EM_TRUE, onFullscreenChanged);
|
||||||
|
|
||||||
int canvasW = EM_ASM_INT({ return window.innerWidth; });
|
int canvasW, canvasH;
|
||||||
int canvasH = EM_ASM_INT({ return window.innerHeight; });
|
getViewportCssSize(canvasW, canvasH);
|
||||||
applyResize(canvasW, canvasH);
|
applyResize(canvasW, canvasH);
|
||||||
|
|
||||||
SDL_SetHint(SDL_HINT_MOUSE_TOUCH_EVENTS, "0");
|
SDL_SetHint(SDL_HINT_MOUSE_TOUCH_EVENTS, "0");
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ namespace {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void PathFinder::build(const std::string& configPath,
|
void PathFinderOld::build(const std::string& configPath,
|
||||||
const std::string& zipPath)
|
const std::string& zipPath)
|
||||||
{
|
{
|
||||||
ready = false;
|
ready = false;
|
||||||
@@ -115,22 +115,23 @@ void PathFinder::build(const std::string& configPath,
|
|||||||
<< ", cell=" << cellSize << ", areas=" << areas.size() << "\n";
|
<< ", cell=" << cellSize << ", areas=" << areas.size() << "\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<Eigen::Vector3f> PathFinder::findPath(const Eigen::Vector3f& start,
|
std::vector<Eigen::Vector3f> PathFinderOld::findPath(const Eigen::Vector3f& start,
|
||||||
const Eigen::Vector3f& end) const
|
const Eigen::Vector3f& end) const
|
||||||
{
|
{
|
||||||
const std::lock_guard<std::mutex> lock(*queryMutex);
|
//const std::lock_guard<std::mutex> lock(*queryMutex);
|
||||||
return runSearch(start, end, 0, false);
|
//return runSearch(start, end, 0, false);
|
||||||
|
return std::vector<Eigen::Vector3f>{start, end};
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<Eigen::Vector3f> PathFinder::findPath(const Eigen::Vector3f& start,
|
/*
|
||||||
|
std::vector<Eigen::Vector3f> PathFinderOld::findPath(const Eigen::Vector3f& start,
|
||||||
const Eigen::Vector3f& end,
|
const Eigen::Vector3f& end,
|
||||||
const std::vector<DynamicObstacle>& dynamicObstacles) const
|
const std::vector<DynamicObstacle>& dynamicObstacles) const
|
||||||
{
|
{
|
||||||
const std::lock_guard<std::mutex> lock(*queryMutex);
|
const std::lock_guard<std::mutex> lock(*queryMutex);
|
||||||
return runSearch(start, end, buildDynamicOverlay(dynamicObstacles), false);
|
return runSearch(start, end, buildDynamicOverlay(dynamicObstacles), false);
|
||||||
}
|
}*/
|
||||||
|
std::uint32_t PathFinderOld::buildDynamicOverlay(
|
||||||
std::uint32_t PathFinder::buildDynamicOverlay(
|
|
||||||
const std::vector<DynamicObstacle>& dynamicObstacles) const
|
const std::vector<DynamicObstacle>& dynamicObstacles) const
|
||||||
{
|
{
|
||||||
const size_t cellCount = walkable.size();
|
const size_t cellCount = walkable.size();
|
||||||
@@ -172,7 +173,7 @@ std::uint32_t PathFinder::buildDynamicOverlay(
|
|||||||
return dynamicGeneration;
|
return dynamicGeneration;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool PathFinder::isCellWalkableOverlay(
|
bool PathFinderOld::isCellWalkableOverlay(
|
||||||
const Cell& cell, std::uint32_t generation) const
|
const Cell& cell, std::uint32_t generation) const
|
||||||
{
|
{
|
||||||
if (!isCellWalkable(cell)) return false;
|
if (!isCellWalkable(cell)) return false;
|
||||||
@@ -180,7 +181,7 @@ bool PathFinder::isCellWalkableOverlay(
|
|||||||
dynamicBlockedGeneration[static_cast<size_t>(indexOf(cell))] != generation;
|
dynamicBlockedGeneration[static_cast<size_t>(indexOf(cell))] != generation;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool PathFinder::findNearestWalkableCellOverlay(
|
bool PathFinderOld::findNearestWalkableCellOverlay(
|
||||||
const Eigen::Vector3f& point, Cell& out, std::uint32_t generation) const
|
const Eigen::Vector3f& point, Cell& out, std::uint32_t generation) const
|
||||||
{
|
{
|
||||||
Cell origin;
|
Cell origin;
|
||||||
@@ -212,7 +213,7 @@ bool PathFinder::findNearestWalkableCellOverlay(
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool PathFinder::hasLineOfSightOverlay(
|
bool PathFinderOld::hasLineOfSightOverlay(
|
||||||
const Cell& from, const Cell& to, std::uint32_t generation) const
|
const Cell& from, const Cell& to, std::uint32_t generation) const
|
||||||
{
|
{
|
||||||
int x = from.x;
|
int x = from.x;
|
||||||
@@ -244,7 +245,7 @@ bool PathFinder::hasLineOfSightOverlay(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<PathFinder::Cell> PathFinder::smoothCellsOverlay(
|
std::vector<PathFinderOld::Cell> PathFinderOld::smoothCellsOverlay(
|
||||||
const std::vector<Cell>& cells, std::uint32_t generation) const
|
const std::vector<Cell>& cells, std::uint32_t generation) const
|
||||||
{
|
{
|
||||||
if (cells.size() <= 2) return cells;
|
if (cells.size() <= 2) return cells;
|
||||||
@@ -263,7 +264,7 @@ std::vector<PathFinder::Cell> PathFinder::smoothCellsOverlay(
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<Eigen::Vector3f> PathFinder::runSearch(
|
std::vector<Eigen::Vector3f> PathFinderOld::runSearch(
|
||||||
const Eigen::Vector3f& start,
|
const Eigen::Vector3f& start,
|
||||||
const Eigen::Vector3f& end,
|
const Eigen::Vector3f& end,
|
||||||
std::uint32_t obstacleGeneration,
|
std::uint32_t obstacleGeneration,
|
||||||
@@ -385,23 +386,25 @@ std::vector<Eigen::Vector3f> PathFinder::runSearch(
|
|||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<Eigen::Vector3f> PathFinder::findPathToNearest(
|
std::vector<Eigen::Vector3f> PathFinderOld::findPathToNearest(
|
||||||
const Eigen::Vector3f& start, const Eigen::Vector3f& end) const
|
const Eigen::Vector3f& start, const Eigen::Vector3f& end) const
|
||||||
{
|
{
|
||||||
const std::lock_guard<std::mutex> lock(*queryMutex);
|
//const std::lock_guard<std::mutex> lock(*queryMutex);
|
||||||
return runSearch(start, end, 0, true);
|
// return runSearch(start, end, 0, true);
|
||||||
|
return std::vector<Eigen::Vector3f>{start, end};
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<Eigen::Vector3f> PathFinder::findPathToNearest(
|
/*
|
||||||
|
std::vector<Eigen::Vector3f> PathFinderOld::findPathToNearest(
|
||||||
const Eigen::Vector3f& start, const Eigen::Vector3f& end,
|
const Eigen::Vector3f& start, const Eigen::Vector3f& end,
|
||||||
const std::vector<DynamicObstacle>& dynamicObstacles) const
|
const std::vector<DynamicObstacle>& dynamicObstacles) const
|
||||||
{
|
{
|
||||||
const std::lock_guard<std::mutex> lock(*queryMutex);
|
const std::lock_guard<std::mutex> lock(*queryMutex);
|
||||||
return runSearch(
|
return runSearch(
|
||||||
start, end, buildDynamicOverlay(dynamicObstacles), true);
|
start, end, buildDynamicOverlay(dynamicObstacles), true);
|
||||||
}
|
}*/
|
||||||
|
|
||||||
bool PathFinder::setAreaAvailable(const std::string& areaName, bool available)
|
bool PathFinderOld::setAreaAvailable(const std::string& areaName, bool available)
|
||||||
{
|
{
|
||||||
bool found = false;
|
bool found = false;
|
||||||
bool changed = false;
|
bool changed = false;
|
||||||
@@ -420,13 +423,13 @@ bool PathFinder::setAreaAvailable(const std::string& areaName, bool available)
|
|||||||
return found;
|
return found;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool PathFinder::isWalkable(const Eigen::Vector3f& point) const
|
bool PathFinderOld::isWalkable(const Eigen::Vector3f& point) const
|
||||||
{
|
{
|
||||||
Cell cell;
|
Cell cell;
|
||||||
return worldToCell(point, cell) && isCellWalkable(cell, walkable);
|
return worldToCell(point, cell) && isCellWalkable(cell, walkable);
|
||||||
}
|
}
|
||||||
|
|
||||||
void PathFinder::addObstaclePolygon(const ObstaclePolygon& polygon)
|
void PathFinderOld::addObstaclePolygon(const ObstaclePolygon& polygon)
|
||||||
{
|
{
|
||||||
obstaclePolygons.push_back(polygon);
|
obstaclePolygons.push_back(polygon);
|
||||||
if (ready) {
|
if (ready) {
|
||||||
@@ -434,7 +437,7 @@ void PathFinder::addObstaclePolygon(const ObstaclePolygon& polygon)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool PathFinder::saveConfig(const std::string& path) const
|
bool PathFinderOld::saveConfig(const std::string& path) const
|
||||||
{
|
{
|
||||||
json root;
|
json root;
|
||||||
root["cellSize"] = cellSize;
|
root["cellSize"] = cellSize;
|
||||||
@@ -481,7 +484,7 @@ bool PathFinder::saveConfig(const std::string& path) const
|
|||||||
return saveJsonToFile(root, path);
|
return saveJsonToFile(root, path);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool PathFinder::saveGrid(const std::string& path) const
|
bool PathFinderOld::saveGrid(const std::string& path) const
|
||||||
{
|
{
|
||||||
#if defined(_WIN32)
|
#if defined(_WIN32)
|
||||||
if (!ready) return false;
|
if (!ready) return false;
|
||||||
@@ -510,7 +513,7 @@ bool PathFinder::saveGrid(const std::string& path) const
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
bool PathFinder::loadGrid(const std::string& path, const std::string& zipPath)
|
bool PathFinderOld::loadGrid(const std::string& path, const std::string& zipPath)
|
||||||
{
|
{
|
||||||
std::string content;
|
std::string content;
|
||||||
try {
|
try {
|
||||||
@@ -565,7 +568,7 @@ bool PathFinder::loadGrid(const std::string& path, const std::string& zipPath)
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void PathFinder::loadConfig(const std::string& configPath, const std::string& zipPath)
|
void PathFinderOld::loadConfig(const std::string& configPath, const std::string& zipPath)
|
||||||
{
|
{
|
||||||
cellSize = 0.4f;
|
cellSize = 0.4f;
|
||||||
agentRadius = 0.45f;
|
agentRadius = 0.45f;
|
||||||
@@ -640,7 +643,7 @@ void PathFinder::loadConfig(const std::string& configPath, const std::string& zi
|
|||||||
boundaryPadding = (std::max)(boundaryPadding, 0.0f);
|
boundaryPadding = (std::max)(boundaryPadding, 0.0f);
|
||||||
}
|
}
|
||||||
|
|
||||||
void PathFinder::resetGridBounds()
|
void PathFinderOld::resetGridBounds()
|
||||||
{
|
{
|
||||||
if (areas.empty()) {
|
if (areas.empty()) {
|
||||||
gridWidth = 0;
|
gridWidth = 0;
|
||||||
@@ -682,14 +685,14 @@ void PathFinder::resetGridBounds()
|
|||||||
gridDepth = static_cast<int>(std::ceil((maxZ - minZ) / cellSize));
|
gridDepth = static_cast<int>(std::ceil((maxZ - minZ) / cellSize));
|
||||||
}
|
}
|
||||||
|
|
||||||
void PathFinder::rebuildWalkableGrid()
|
void PathFinderOld::rebuildWalkableGrid()
|
||||||
{
|
{
|
||||||
walkable.assign(static_cast<size_t>(gridWidth * gridDepth), 0);
|
walkable.assign(static_cast<size_t>(gridWidth * gridDepth), 0);
|
||||||
markAvailableAreasWalkable();
|
markAvailableAreasWalkable();
|
||||||
markObstaclePolygonsBlocked();
|
markObstaclePolygonsBlocked();
|
||||||
}
|
}
|
||||||
|
|
||||||
void PathFinder::markAvailableAreasWalkable()
|
void PathFinderOld::markAvailableAreasWalkable()
|
||||||
{
|
{
|
||||||
const auto insideAvailableArea = [this](const Eigen::Vector2f& point) {
|
const auto insideAvailableArea = [this](const Eigen::Vector2f& point) {
|
||||||
for (const NavigationArea& area : areas) {
|
for (const NavigationArea& area : areas) {
|
||||||
@@ -763,7 +766,7 @@ void PathFinder::markAvailableAreasWalkable()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void PathFinder::markObstaclePolygonsBlocked()
|
void PathFinderOld::markObstaclePolygonsBlocked()
|
||||||
{
|
{
|
||||||
if (obstaclePolygons.empty()) {
|
if (obstaclePolygons.empty()) {
|
||||||
return;
|
return;
|
||||||
@@ -800,7 +803,7 @@ void PathFinder::markObstaclePolygonsBlocked()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool PathFinder::worldToCell(const Eigen::Vector3f& point, Cell& out) const
|
bool PathFinderOld::worldToCell(const Eigen::Vector3f& point, Cell& out) const
|
||||||
{
|
{
|
||||||
if (gridWidth <= 0 || gridDepth <= 0) {
|
if (gridWidth <= 0 || gridDepth <= 0) {
|
||||||
out = {};
|
out = {};
|
||||||
@@ -812,7 +815,7 @@ bool PathFinder::worldToCell(const Eigen::Vector3f& point, Cell& out) const
|
|||||||
return isInsideGrid(out);
|
return isInsideGrid(out);
|
||||||
}
|
}
|
||||||
|
|
||||||
Eigen::Vector3f PathFinder::cellCenter(const Cell& cell) const
|
Eigen::Vector3f PathFinderOld::cellCenter(const Cell& cell) const
|
||||||
{
|
{
|
||||||
return {
|
return {
|
||||||
minX + (static_cast<float>(cell.x) + 0.5f) * cellSize,
|
minX + (static_cast<float>(cell.x) + 0.5f) * cellSize,
|
||||||
@@ -821,17 +824,17 @@ Eigen::Vector3f PathFinder::cellCenter(const Cell& cell) const
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
bool PathFinder::isInsideGrid(const Cell& cell) const
|
bool PathFinderOld::isInsideGrid(const Cell& cell) const
|
||||||
{
|
{
|
||||||
return cell.x >= 0 && cell.z >= 0 && cell.x < gridWidth && cell.z < gridDepth;
|
return cell.x >= 0 && cell.z >= 0 && cell.x < gridWidth && cell.z < gridDepth;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool PathFinder::isCellWalkable(const Cell& cell) const
|
bool PathFinderOld::isCellWalkable(const Cell& cell) const
|
||||||
{
|
{
|
||||||
return isCellWalkable(cell, walkable);
|
return isCellWalkable(cell, walkable);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool PathFinder::isCellWalkable(const Cell& cell, const std::vector<unsigned char>& walkableGrid) const
|
bool PathFinderOld::isCellWalkable(const Cell& cell, const std::vector<unsigned char>& walkableGrid) const
|
||||||
{
|
{
|
||||||
const int idx = indexOf(cell);
|
const int idx = indexOf(cell);
|
||||||
return isInsideGrid(cell) &&
|
return isInsideGrid(cell) &&
|
||||||
@@ -840,17 +843,17 @@ bool PathFinder::isCellWalkable(const Cell& cell, const std::vector<unsigned cha
|
|||||||
walkableGrid[static_cast<size_t>(idx)] != 0;
|
walkableGrid[static_cast<size_t>(idx)] != 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
int PathFinder::indexOf(const Cell& cell) const
|
int PathFinderOld::indexOf(const Cell& cell) const
|
||||||
{
|
{
|
||||||
return cell.z * gridWidth + cell.x;
|
return cell.z * gridWidth + cell.x;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool PathFinder::findNearestWalkableCell(const Eigen::Vector3f& point, Cell& out) const
|
bool PathFinderOld::findNearestWalkableCell(const Eigen::Vector3f& point, Cell& out) const
|
||||||
{
|
{
|
||||||
return findNearestWalkableCell(point, out, walkable);
|
return findNearestWalkableCell(point, out, walkable);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool PathFinder::findNearestWalkableCell(const Eigen::Vector3f& point, Cell& out, const std::vector<unsigned char>& walkableGrid) const
|
bool PathFinderOld::findNearestWalkableCell(const Eigen::Vector3f& point, Cell& out, const std::vector<unsigned char>& walkableGrid) const
|
||||||
{
|
{
|
||||||
Cell origin;
|
Cell origin;
|
||||||
if (!worldToCell(point, origin)) {
|
if (!worldToCell(point, origin)) {
|
||||||
@@ -885,12 +888,12 @@ bool PathFinder::findNearestWalkableCell(const Eigen::Vector3f& point, Cell& out
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool PathFinder::hasLineOfSight(const Cell& from, const Cell& to) const
|
bool PathFinderOld::hasLineOfSight(const Cell& from, const Cell& to) const
|
||||||
{
|
{
|
||||||
return hasLineOfSight(from, to, walkable);
|
return hasLineOfSight(from, to, walkable);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool PathFinder::hasLineOfSight(const Cell& from, const Cell& to, const std::vector<unsigned char>& walkableGrid) const
|
bool PathFinderOld::hasLineOfSight(const Cell& from, const Cell& to, const std::vector<unsigned char>& walkableGrid) const
|
||||||
{
|
{
|
||||||
const Eigen::Vector3f a = cellCenter(from);
|
const Eigen::Vector3f a = cellCenter(from);
|
||||||
const Eigen::Vector3f b = cellCenter(to);
|
const Eigen::Vector3f b = cellCenter(to);
|
||||||
@@ -909,12 +912,12 @@ bool PathFinder::hasLineOfSight(const Cell& from, const Cell& to, const std::vec
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<PathFinder::Cell> PathFinder::smoothCells(const std::vector<Cell>& cells) const
|
std::vector<PathFinderOld::Cell> PathFinderOld::smoothCells(const std::vector<Cell>& cells) const
|
||||||
{
|
{
|
||||||
return smoothCells(cells, walkable);
|
return smoothCells(cells, walkable);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<PathFinder::Cell> PathFinder::smoothCells(const std::vector<Cell>& cells, const std::vector<unsigned char>& walkableGrid) const
|
std::vector<PathFinderOld::Cell> PathFinderOld::smoothCells(const std::vector<Cell>& cells, const std::vector<unsigned char>& walkableGrid) const
|
||||||
{
|
{
|
||||||
if (cells.size() <= 2) {
|
if (cells.size() <= 2) {
|
||||||
return cells;
|
return cells;
|
||||||
@@ -937,7 +940,7 @@ std::vector<PathFinder::Cell> PathFinder::smoothCells(const std::vector<Cell>& c
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool PathFinder::pointInPolygon(float x, float z, const std::vector<Eigen::Vector2f>& polygon)
|
bool PathFinderOld::pointInPolygon(float x, float z, const std::vector<Eigen::Vector2f>& polygon)
|
||||||
{
|
{
|
||||||
bool inside = false;
|
bool inside = false;
|
||||||
size_t previous = polygon.size() - 1;
|
size_t previous = polygon.size() - 1;
|
||||||
|
|||||||
+536
-4
@@ -1,15 +1,547 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <Eigen/Core>
|
#include <Eigen/Core>
|
||||||
|
#include <algorithm>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
#include <limits>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
|
#include <queue>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
#ifdef min
|
||||||
|
#undef min
|
||||||
|
#endif
|
||||||
|
#ifdef max
|
||||||
|
#undef max
|
||||||
|
#endif
|
||||||
|
#include "../../thirdparty/fpm-1.1.0/include/fpm/fixed.hpp"
|
||||||
|
#include "../../thirdparty/fpm-1.1.0/include/fpm/math.hpp"
|
||||||
|
|
||||||
|
// Сторонние библиотеки для геометрии (Header-only / легко подключаемые):
|
||||||
|
// 1. Clipper2: https://github.com/PolyClipping/Clipper2/releases/tag/Clipper2_2.0.1
|
||||||
|
// 2. CDT (Constrained Delaunay Triangulation): https://github.com/artem-ogre/CDT
|
||||||
|
|
||||||
|
#include "../../thirdparty/Clipper2-Clipper2_2.0.1/CPP/build/Release/include/clipper2/clipper.h"
|
||||||
|
#include "../../thirdparty/CDT-2.0.2/CDT/build/release/include/CDT.h"
|
||||||
|
|
||||||
namespace FRG {
|
namespace FRG {
|
||||||
|
|
||||||
|
|
||||||
|
//using fp = fpm::fixed<std::int32_t, std::int64_t, 16>;
|
||||||
|
inline constexpr unsigned int FP_FRACTION_BITS = 12;
|
||||||
|
using fp = fpm::fixed<std::int32_t, std::int64_t, FP_FRACTION_BITS>;
|
||||||
|
|
||||||
|
struct Vec2fp {
|
||||||
|
fp x{ 0 };
|
||||||
|
fp y{ 0 };
|
||||||
|
|
||||||
|
Vec2fp() = default;
|
||||||
|
Vec2fp(fp _x, fp _y) : x(_x), y(_y) {}
|
||||||
|
|
||||||
|
// Конвертация из float только на этапе загрузки/ввода
|
||||||
|
static Vec2fp fromFloat(float fx, float fy) {
|
||||||
|
return Vec2fp(fp(fx), fp(fy));
|
||||||
|
}
|
||||||
|
|
||||||
|
Vec2fp operator+(const Vec2fp& o) const { return { x + o.x, y + o.y }; }
|
||||||
|
Vec2fp operator-(const Vec2fp& o) const { return { x - o.x, y - o.y }; }
|
||||||
|
Vec2fp operator*(fp s) const { return { x * s, y * s }; }
|
||||||
|
Vec2fp operator/(fp s) const { return { x / s, y / s }; }
|
||||||
|
bool operator==(const Vec2fp& o) const { return x == o.x && y == o.y; }
|
||||||
|
bool operator!=(const Vec2fp& o) const { return !(*this == o); }
|
||||||
|
fp dot(const Vec2fp& o) const { return x * o.x + y * o.y; }
|
||||||
|
|
||||||
|
// 2D Векторное произведение (Z-компонента): определяет, с какой стороны лежит точка
|
||||||
|
fp cross(const Vec2fp& o) const { return x * o.y - y * o.x; }
|
||||||
|
|
||||||
|
fp lengthSq() const { return x * x + y * y; }
|
||||||
|
fp length() const { return fpm::sqrt(lengthSq()); }
|
||||||
|
|
||||||
|
Eigen::Vector2f toEigen() const {
|
||||||
|
return Eigen::Vector2f(static_cast<float>(x), static_cast<float>(y));
|
||||||
|
}
|
||||||
|
|
||||||
|
static Vec2fp fromEigen(const Eigen::Vector2f& v) {
|
||||||
|
return Vec2fp(fp(v.x()), fp(v.y()));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Vec3fp {
|
||||||
|
fp x{ 0 }, y{ 0 }, z{ 0 };
|
||||||
|
Vec3fp() = default;
|
||||||
|
Vec3fp(fp _x, fp _y, fp _z) : x(_x), y(_y), z(_z) {}
|
||||||
|
|
||||||
|
static Vec3fp fromEigen(const Eigen::Vector3f& v) {
|
||||||
|
return { fp(v.x()), fp(v.y()), fp(v.z()) };
|
||||||
|
}
|
||||||
|
Eigen::Vector3f toEigen() const {
|
||||||
|
return Eigen::Vector3f(static_cast<float>(x), static_cast<float>(y), static_cast<float>(z));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
inline std::vector<Eigen::Vector2f> toEigenVector(const std::vector<FRG::Vec2fp>& src) {
|
||||||
|
std::vector<Eigen::Vector2f> dst;
|
||||||
|
dst.reserve(src.size());
|
||||||
|
for (const auto& pt : src) {
|
||||||
|
dst.emplace_back(static_cast<float>(pt.x), static_cast<float>(pt.y));
|
||||||
|
}
|
||||||
|
return dst;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline std::vector<FRG::Vec2fp> fromEigenVector(const std::vector<Eigen::Vector2f>& src) {
|
||||||
|
std::vector<FRG::Vec2fp> dst;
|
||||||
|
dst.reserve(src.size());
|
||||||
|
for (const auto& pt : src) {
|
||||||
|
dst.emplace_back(FRG::fp(pt.x()), FRG::fp(pt.y()));
|
||||||
|
}
|
||||||
|
return dst;
|
||||||
|
}
|
||||||
|
|
||||||
class PathFinder {
|
class PathFinder {
|
||||||
|
public:
|
||||||
|
struct NavigationArea {
|
||||||
|
std::string name;
|
||||||
|
bool available = true;
|
||||||
|
std::vector<Vec2fp> polygon;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ObstaclePolygon {
|
||||||
|
std::string name;
|
||||||
|
std::vector<Vec2fp> polygon;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct NavTriangle {
|
||||||
|
int id = -1;
|
||||||
|
Vec2fp vertices[3];
|
||||||
|
Vec2fp center;
|
||||||
|
int neighbors[3] = { -1, -1, -1 }; // Индексы соседних треугольников по ребрам (0-1, 1-2, 2-0)
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Portal {
|
||||||
|
Vec2fp left;
|
||||||
|
Vec2fp right;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Масштаб для Clipper2 (так как Clipper работает с целыми int64_t,
|
||||||
|
// сохраняем точность в 10000 единиц на 1 метр)
|
||||||
|
static constexpr int64_t CLIPPER_SCALE = 10000;
|
||||||
|
|
||||||
|
void build(const std::string& configPath = "", const std::string& zipPath = "") {
|
||||||
|
// Здесь может быть парсинг configPath в area и obstaclePolygons.
|
||||||
|
// После заполнения геометрии строим NavMesh:
|
||||||
|
area.name = "main";
|
||||||
|
area.polygon = {
|
||||||
|
Vec2fp::fromFloat(-100.f, -100.f),
|
||||||
|
Vec2fp::fromFloat(-100.f, 100.f),
|
||||||
|
Vec2fp::fromFloat(100.f, 100.f),
|
||||||
|
Vec2fp::fromFloat(100.f, -100.f)
|
||||||
|
};
|
||||||
|
|
||||||
|
area.available = true;
|
||||||
|
|
||||||
|
obstaclePolygons.push_back({
|
||||||
|
"p1", {Vec2fp::fromFloat(-10.f, -10.f),
|
||||||
|
Vec2fp::fromFloat(-10.f, 10.f),
|
||||||
|
Vec2fp::fromFloat(10.f, 10.f),
|
||||||
|
Vec2fp::fromFloat(10.f, -10.f)}
|
||||||
|
});
|
||||||
|
|
||||||
|
//ready = true;
|
||||||
|
|
||||||
|
generateNavMesh();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Построение NavMesh: Объединение полигонов (Clipper2) + Триангуляция (CDT)
|
||||||
|
void generateNavMesh() {
|
||||||
|
navMesh.clear();
|
||||||
|
if (area.polygon.size() < 3) return;
|
||||||
|
|
||||||
|
using namespace Clipper2Lib;
|
||||||
|
|
||||||
|
// Вспомогательная лямбда: безопасно переводит fp в координаты Clipper2 (int64_t)
|
||||||
|
auto toClipperCoord = [](fp val) -> int64_t {
|
||||||
|
return (static_cast<int64_t>(val.raw_value()) * CLIPPER_SCALE) / (1LL << FP_FRACTION_BITS);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 1. Переводим контур локации (NavigationArea) в формат Clipper2
|
||||||
|
Paths64 subject;
|
||||||
|
Path64 outerPath;
|
||||||
|
for (const auto& pt : area.polygon) {
|
||||||
|
outerPath.push_back(Point64(toClipperCoord(pt.x), toClipperCoord(pt.y)));
|
||||||
|
}
|
||||||
|
subject.push_back(outerPath);
|
||||||
|
|
||||||
|
// 2. Собираем все препятствия
|
||||||
|
Paths64 clips;
|
||||||
|
for (const auto& obs : obstaclePolygons) {
|
||||||
|
if (obs.polygon.size() < 3) continue;
|
||||||
|
Path64 clipPath;
|
||||||
|
for (const auto& pt : obs.polygon) {
|
||||||
|
clipPath.push_back(Point64(toClipperCoord(pt.x), toClipperCoord(pt.y)));
|
||||||
|
}
|
||||||
|
clips.push_back(clipPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Булево объединение пересекающихся препятствий и вычитание их из локации
|
||||||
|
// Clipper автоматически объединяет пересекающиеся полигоны внутри Difference
|
||||||
|
PolyTree64 solTree;
|
||||||
|
Clipper64 clipper;
|
||||||
|
clipper.AddSubject(subject);
|
||||||
|
clipper.AddClip(clips);
|
||||||
|
//clipper.Execute(ClipType::Difference, FillRule::NonZero, solTree);
|
||||||
|
clipper.Execute(ClipType::Difference, FillRule::EvenOdd, solTree);
|
||||||
|
|
||||||
|
// 4. Подготовка данных для Constrained Delaunay Triangulation (CDT)
|
||||||
|
std::vector<CDT::V2d<double>> cdtVertices;
|
||||||
|
std::vector<CDT::Edge> cdtEdges;
|
||||||
|
|
||||||
|
auto extractPolygons = [&](const PolyPath64* node, auto& selfRef) -> void {
|
||||||
|
if (!node->Polygon().empty()) {
|
||||||
|
const Path64& path = node->Polygon();
|
||||||
|
std::size_t startIdx = cdtVertices.size();
|
||||||
|
std::size_t count = path.size();
|
||||||
|
|
||||||
|
for (std::size_t i = 0; i < count; ++i) {
|
||||||
|
double x = static_cast<double>(path[i].x) / CLIPPER_SCALE;
|
||||||
|
double y = static_cast<double>(path[i].y) / CLIPPER_SCALE;
|
||||||
|
cdtVertices.push_back({ x, y });
|
||||||
|
cdtEdges.push_back(CDT::Edge(startIdx + i, startIdx + (i + 1) % count));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (std::size_t i = 0; i < node->Count(); ++i) {
|
||||||
|
selfRef(node->Child(i), selfRef);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (std::size_t i = 0; i < solTree.Count(); ++i) {
|
||||||
|
extractPolygons(solTree.Child(i), extractPolygons);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cdtVertices.empty()) return;
|
||||||
|
|
||||||
|
CDT::RemoveDuplicatesAndRemapEdges(cdtVertices, cdtEdges);
|
||||||
|
|
||||||
|
// 5. Выполняем триангуляцию и удаляем треугольники внутри дыр (препятствий)
|
||||||
|
CDT::Triangulation<double> cdt;
|
||||||
|
cdt.insertVertices(cdtVertices);
|
||||||
|
cdt.insertEdges(cdtEdges);
|
||||||
|
cdt.eraseOuterTrianglesAndHoles();
|
||||||
|
|
||||||
|
// 6. Конвертируем результат в детерминированный граф NavTriangle (Q32.32)
|
||||||
|
const auto& triangles = cdt.triangles;
|
||||||
|
const auto& vertices = cdt.vertices;
|
||||||
|
navMesh.resize(triangles.size());
|
||||||
|
|
||||||
|
for (std::size_t i = 0; i < triangles.size(); ++i) {
|
||||||
|
NavTriangle& navTri = navMesh[i];
|
||||||
|
navTri.id = static_cast<int>(i);
|
||||||
|
|
||||||
|
for (int j = 0; j < 3; ++j) {
|
||||||
|
const auto& v = vertices[triangles[i].vertices[j]];
|
||||||
|
navTri.vertices[j] = Vec2fp(fp(v.x), fp(v.y));
|
||||||
|
|
||||||
|
// Соседи в CDT хранятся напротив вершин, перепривязываем к ребрам (j, j+1)
|
||||||
|
auto neighborIdx = triangles[i].neighbors[j];
|
||||||
|
navTri.neighbors[j] = (neighborIdx != CDT::noNeighbor) ? static_cast<int>(neighborIdx) : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
navTri.center = (navTri.vertices[0] + navTri.vertices[1] + navTri.vertices[2]) / fp(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
ready = !navMesh.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Основной метод поиска пути (плоскость XZ, Y — высота)
|
||||||
|
std::vector<Vec3fp> findPathToNearest(const Vec3fp& start3D, const Vec3fp& end3D) const {
|
||||||
|
if (!ready || !area.available) return { start3D };
|
||||||
|
|
||||||
|
Vec2fp startPt(start3D.x, start3D.z);
|
||||||
|
Vec2fp targetPt(end3D.x, end3D.z);
|
||||||
|
|
||||||
|
// 1. Находим треугольники для старта и финиша.
|
||||||
|
// Если точка попала в препятствие (вне NavMesh), ищем ближайшую точку на ребрах NavMesh
|
||||||
|
int startTri = findTriangleContaining(startPt);
|
||||||
|
if (startTri == -1) {
|
||||||
|
startPt = findClosestPointOnNavMesh(startPt, startTri);
|
||||||
|
}
|
||||||
|
|
||||||
|
int endTri = findTriangleContaining(targetPt);
|
||||||
|
if (endTri == -1) {
|
||||||
|
targetPt = findClosestPointOnNavMesh(targetPt, endTri);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startTri == -1 || endTri == -1) return { start3D };
|
||||||
|
|
||||||
|
// Если старт и конец в одном треугольнике — идем по прямой
|
||||||
|
if (startTri == endTri) {
|
||||||
|
return {
|
||||||
|
start3D,
|
||||||
|
Vec3fp(targetPt.x, floorY, targetPt.y)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Поиск коридора из треугольников через A*
|
||||||
|
std::vector<int> triangleCorridor = runAStar(startTri, endTri, startPt, targetPt);
|
||||||
|
if (triangleCorridor.empty()) return { start3D };
|
||||||
|
|
||||||
|
// 3. Сглаживание пути через Funnel Algorithm (Натянутая нить)
|
||||||
|
std::vector<Vec2fp> smoothPath2D = stringPullFunnel(startPt, targetPt, triangleCorridor);
|
||||||
|
|
||||||
|
// 4. Формируем выходной массив 3D точек
|
||||||
|
std::vector<Vec3fp> result;
|
||||||
|
result.reserve(smoothPath2D.size());
|
||||||
|
for (const auto& pt : smoothPath2D) {
|
||||||
|
result.emplace_back(pt.x, floorY, pt.y);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Перегрузка для совместимости со старым Eigen-интерфейсом
|
||||||
|
std::vector<Eigen::Vector3f> findPathToNearest(const Eigen::Vector3f& start, const Eigen::Vector3f& end) const {
|
||||||
|
auto pathFp = findPathToNearest(Vec3fp::fromEigen(start), Vec3fp::fromEigen(end));
|
||||||
|
std::vector<Eigen::Vector3f> out;
|
||||||
|
out.reserve(pathFp.size());
|
||||||
|
for (const auto& p : pathFp) out.push_back(p.toEigen());
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isWalkable(const Vec3fp& point) const {
|
||||||
|
return findTriangleContaining(Vec2fp(point.x, point.z)) != -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isWalkable(const Eigen::Vector3f& point) const {
|
||||||
|
return isWalkable(Vec3fp::fromEigen(point));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool setAreaAvailable(const std::string& areaName, bool available) {
|
||||||
|
if (area.name == areaName) {
|
||||||
|
area.available = available;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isReady() const { return ready; }
|
||||||
|
const NavigationArea& getArea() const { return area; }
|
||||||
|
void setArea(const NavigationArea& a) { area = a; }
|
||||||
|
|
||||||
|
fp getFloorY() const { return floorY; }
|
||||||
|
void setFloorY(fp y) { floorY = y; }
|
||||||
|
|
||||||
|
void addObstaclePolygon(const ObstaclePolygon& polygon) {
|
||||||
|
obstaclePolygons.push_back(polygon);
|
||||||
|
}
|
||||||
|
const std::vector<ObstaclePolygon>& getObstaclePolygons() const { return obstaclePolygons; }
|
||||||
|
|
||||||
|
bool saveConfig(const std::string& path) const { return true; }
|
||||||
|
bool saveGrid(const std::string& path) const { return true; }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
NavigationArea area;
|
||||||
|
std::vector<ObstaclePolygon> obstaclePolygons;
|
||||||
|
std::vector<NavTriangle> navMesh;
|
||||||
|
fp floorY{ 0 };
|
||||||
|
bool ready = false;
|
||||||
|
|
||||||
|
// Проверка нахождения точки внутри треугольника (через знаки векторных произведений)
|
||||||
|
static bool isPointInTriangle(const Vec2fp& p, const Vec2fp& a, const Vec2fp& b, const Vec2fp& c) {
|
||||||
|
fp d1 = (b - a).cross(p - a);
|
||||||
|
fp d2 = (c - b).cross(p - b);
|
||||||
|
fp d3 = (a - c).cross(p - c);
|
||||||
|
|
||||||
|
bool hasNeg = (d1 < fp(0)) || (d2 < fp(0)) || (d3 < fp(0));
|
||||||
|
bool hasPos = (d1 > fp(0)) || (d2 > fp(0)) || (d3 > fp(0));
|
||||||
|
return !(hasNeg && hasPos);
|
||||||
|
}
|
||||||
|
|
||||||
|
int findTriangleContaining(const Vec2fp& pt) const {
|
||||||
|
for (const auto& tri : navMesh) {
|
||||||
|
if (isPointInTriangle(pt, tri.vertices[0], tri.vertices[1], tri.vertices[2])) {
|
||||||
|
return tri.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проекция точки на отрезок AB
|
||||||
|
static Vec2fp closestPointOnSegment(const Vec2fp& p, const Vec2fp& a, const Vec2fp& b) {
|
||||||
|
Vec2fp ab = b - a;
|
||||||
|
fp lenSq = ab.lengthSq();
|
||||||
|
if (lenSq == fp(0)) return a;
|
||||||
|
|
||||||
|
fp t = (p - a).dot(ab) / lenSq;
|
||||||
|
t = std::max(fp(0), std::min(fp(1), t));
|
||||||
|
return a + ab * t;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vec2fp findClosestPointOnNavMesh(const Vec2fp& pt, int& outTriId) const {
|
||||||
|
fp minDistSq = std::numeric_limits<fp>::max();
|
||||||
|
Vec2fp bestPt = pt;
|
||||||
|
outTriId = -1;
|
||||||
|
|
||||||
|
for (const auto& tri : navMesh) {
|
||||||
|
for (int i = 0; i < 3; ++i) {
|
||||||
|
// В CDT neighbors[i] — это сосед через ребро (vertices[i], vertices[(i + 1) % 3])
|
||||||
|
if (tri.neighbors[i] == -1) {
|
||||||
|
const Vec2fp& a = tri.vertices[i];
|
||||||
|
const Vec2fp& b = tri.vertices[(i + 1) % 3];
|
||||||
|
Vec2fp proj = closestPointOnSegment(pt, a, b);
|
||||||
|
fp distSq = (pt - proj).lengthSq();
|
||||||
|
|
||||||
|
if (distSq < minDistSq) {
|
||||||
|
minDistSq = distSq;
|
||||||
|
bestPt = proj;
|
||||||
|
outTriId = tri.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bestPt;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Поиск пути A* по центрам треугольников
|
||||||
|
std::vector<int> runAStar(int startTri, int endTri, const Vec2fp& startPt, const Vec2fp& endPt) const {
|
||||||
|
struct NodeRecord {
|
||||||
|
int triId;
|
||||||
|
fp fScore;
|
||||||
|
// В случае равенства fScore сортируем по triId для 100% детерминизма
|
||||||
|
bool operator>(const NodeRecord& o) const {
|
||||||
|
if (fScore != o.fScore) return fScore > o.fScore;
|
||||||
|
return triId > o.triId;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
std::priority_queue<NodeRecord, std::vector<NodeRecord>, std::greater<NodeRecord>> openSet;
|
||||||
|
std::vector<fp> gScore(navMesh.size(), std::numeric_limits<fp>::max());
|
||||||
|
std::vector<int> cameFrom(navMesh.size(), -1);
|
||||||
|
std::vector<bool> closed(navMesh.size(), false);
|
||||||
|
|
||||||
|
gScore[startTri] = fp(0);
|
||||||
|
openSet.push({ startTri, (navMesh[startTri].center - endPt).length() });
|
||||||
|
|
||||||
|
while (!openSet.empty()) {
|
||||||
|
int current = openSet.top().triId;
|
||||||
|
openSet.pop();
|
||||||
|
|
||||||
|
if (current == endTri) {
|
||||||
|
std::vector<int> path;
|
||||||
|
for (int curr = endTri; curr != -1; curr = cameFrom[curr]) {
|
||||||
|
path.push_back(curr);
|
||||||
|
}
|
||||||
|
std::reverse(path.begin(), path.end());
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (closed[current]) continue;
|
||||||
|
closed[current] = true;
|
||||||
|
|
||||||
|
const NavTriangle& currTri = navMesh[current];
|
||||||
|
for (int i = 0; i < 3; ++i) {
|
||||||
|
int neighbor = currTri.neighbors[i];
|
||||||
|
if (neighbor == -1 || closed[neighbor]) continue;
|
||||||
|
|
||||||
|
fp stepCost = (currTri.center - navMesh[neighbor].center).length();
|
||||||
|
fp tentativeG = gScore[current] + stepCost;
|
||||||
|
|
||||||
|
if (tentativeG < gScore[neighbor]) {
|
||||||
|
cameFrom[neighbor] = current;
|
||||||
|
gScore[neighbor] = tentativeG;
|
||||||
|
fp h = (navMesh[neighbor].center - endPt).length();
|
||||||
|
openSet.push({ neighbor, tentativeG + h });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Извлечение общих вершин (портала) между двумя соседними треугольниками
|
||||||
|
// Извлечение общих вершин (портала) между двумя соседними треугольниками
|
||||||
|
Portal getPortal(int fromTriId, int toTriId) const {
|
||||||
|
const NavTriangle& t1 = navMesh[fromTriId];
|
||||||
|
for (int i = 0; i < 3; ++i) {
|
||||||
|
if (t1.neighbors[i] == toTriId) {
|
||||||
|
// При обходе треугольника CCW и движении изнутри наружу через ребро i -> (i + 1) % 3:
|
||||||
|
// вершина i находится СЛЕВА, а вершина (i + 1) % 3 — СПРАВА
|
||||||
|
return Portal{ t1.vertices[i], t1.vertices[(i + 1) % 3] };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Portal{ t1.center, t1.center };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Алгоритм натянутой нити (Simple Stupid Funnel Algorithm)
|
||||||
|
std::vector<Vec2fp> stringPullFunnel(const Vec2fp& startPt, const Vec2fp& endPt, const std::vector<int>& corridor) const {
|
||||||
|
std::vector<Portal> portals;
|
||||||
|
portals.reserve(corridor.size() + 2);
|
||||||
|
portals.push_back({ startPt, startPt });
|
||||||
|
|
||||||
|
for (std::size_t i = 0; i + 1 < corridor.size(); ++i) {
|
||||||
|
portals.push_back(getPortal(corridor[i], corridor[i + 1]));
|
||||||
|
}
|
||||||
|
portals.push_back({ endPt, endPt });
|
||||||
|
|
||||||
|
std::vector<Vec2fp> pts;
|
||||||
|
pts.push_back(startPt);
|
||||||
|
|
||||||
|
Vec2fp portalApex = startPt;
|
||||||
|
Vec2fp portalLeft = startPt;
|
||||||
|
Vec2fp portalRight = startPt;
|
||||||
|
int apexIndex = 0, leftIndex = 0, rightIndex = 0;
|
||||||
|
|
||||||
|
for (int i = 1; i < static_cast<int>(portals.size()); ++i) {
|
||||||
|
const Vec2fp& left = portals[i].left;
|
||||||
|
const Vec2fp& right = portals[i].right;
|
||||||
|
|
||||||
|
// Обновляем правую границу воронки
|
||||||
|
if ((portalRight - portalApex).cross(right - portalApex) <= fp(0)) {
|
||||||
|
if (portalApex == portalRight || (portalLeft - portalApex).cross(right - portalApex) > fp(0)) {
|
||||||
|
portalRight = right;
|
||||||
|
rightIndex = i;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
pts.push_back(portalLeft);
|
||||||
|
portalApex = portalLeft;
|
||||||
|
apexIndex = leftIndex;
|
||||||
|
portalLeft = portalApex;
|
||||||
|
portalRight = portalApex;
|
||||||
|
leftIndex = apexIndex;
|
||||||
|
rightIndex = apexIndex;
|
||||||
|
i = apexIndex;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обновляем левую границу воронки
|
||||||
|
if ((portalLeft - portalApex).cross(left - portalApex) >= fp(0)) {
|
||||||
|
if (portalApex == portalLeft || (portalRight - portalApex).cross(left - portalApex) < fp(0)) {
|
||||||
|
portalLeft = left;
|
||||||
|
leftIndex = i;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
pts.push_back(portalRight);
|
||||||
|
portalApex = portalRight;
|
||||||
|
apexIndex = rightIndex;
|
||||||
|
portalLeft = portalApex;
|
||||||
|
portalRight = portalApex;
|
||||||
|
leftIndex = apexIndex;
|
||||||
|
rightIndex = apexIndex;
|
||||||
|
i = apexIndex;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pts.back() != endPt) {
|
||||||
|
pts.push_back(endPt);
|
||||||
|
}
|
||||||
|
return pts;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
class PathFinderOld {
|
||||||
public:
|
public:
|
||||||
|
|
||||||
struct ObstaclePolygon {
|
struct ObstaclePolygon {
|
||||||
@@ -39,18 +571,18 @@ public:
|
|||||||
std::vector<Eigen::Vector3f> findPath(const Eigen::Vector3f& start,
|
std::vector<Eigen::Vector3f> findPath(const Eigen::Vector3f& start,
|
||||||
const Eigen::Vector3f& end) const;
|
const Eigen::Vector3f& end) const;
|
||||||
|
|
||||||
std::vector<Eigen::Vector3f> findPath(const Eigen::Vector3f& start,
|
/*std::vector<Eigen::Vector3f> findPath(const Eigen::Vector3f& start,
|
||||||
const Eigen::Vector3f& end,
|
const Eigen::Vector3f& end,
|
||||||
const std::vector<DynamicObstacle>& dynamicObstacles) const;
|
const std::vector<DynamicObstacle>& dynamicObstacles) const;
|
||||||
|
*/
|
||||||
// Like findPath, but when the destination is unreachable the character
|
// Like findPath, but when the destination is unreachable the character
|
||||||
// walks as close as possible instead of not moving at all.
|
// walks as close as possible instead of not moving at all.
|
||||||
std::vector<Eigen::Vector3f> findPathToNearest(const Eigen::Vector3f& start,
|
std::vector<Eigen::Vector3f> findPathToNearest(const Eigen::Vector3f& start,
|
||||||
const Eigen::Vector3f& end) const;
|
const Eigen::Vector3f& end) const;
|
||||||
std::vector<Eigen::Vector3f> findPathToNearest(const Eigen::Vector3f& start,
|
/*std::vector<Eigen::Vector3f> findPathToNearest(const Eigen::Vector3f& start,
|
||||||
const Eigen::Vector3f& end,
|
const Eigen::Vector3f& end,
|
||||||
const std::vector<DynamicObstacle>& dynamicObstacles) const;
|
const std::vector<DynamicObstacle>& dynamicObstacles) const;
|
||||||
|
*/
|
||||||
bool setAreaAvailable(const std::string& areaName, bool available);
|
bool setAreaAvailable(const std::string& areaName, bool available);
|
||||||
bool isReady() const { return ready; }
|
bool isReady() const { return ready; }
|
||||||
bool isWalkable(const Eigen::Vector3f& point) const;
|
bool isWalkable(const Eigen::Vector3f& point) const;
|
||||||
|
|||||||
Reference in new issue
Block a user