diff --git a/.efrocachemap b/.efrocachemap index 359ac37a..a66e3a86 100644 --- a/.efrocachemap +++ b/.efrocachemap @@ -3995,49 +3995,49 @@ "assets/src/ba_data/python/ba/_generated/__init__.py": "https://files.ballistica.net/cache/ba1/ee/e8/cad05aa531c7faf7ff7b96db7f6e", "assets/src/ba_data/python/ba/_generated/enums.py": "https://files.ballistica.net/cache/ba1/b2/e5/0ee0561e16257a32830645239f34", "ballisticacore-windows/Generic/BallisticaCore.ico": "https://files.ballistica.net/cache/ba1/89/c0/e32c7d2a35dc9aef57cc73b0911a", - "build/prefab/full/linux_arm64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/02/e0/0336db3c3989a1768271ee7a12ba", - "build/prefab/full/linux_arm64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/3d/8c/9689aa6ff3fa826fc3f524b3115f", - "build/prefab/full/linux_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/00/04/1ea1f6833f131458cbc8e7039591", - "build/prefab/full/linux_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/d9/f7/c87406c9229cbe5aa9439e3562c5", - "build/prefab/full/linux_x86_64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/66/1e/53122c4f96bca326aaa42d5cb98b", - "build/prefab/full/linux_x86_64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/6b/28/ee8a01e3155e05c5c970ace23107", - "build/prefab/full/linux_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/89/5e/fbf702ecbd5376efe63720632883", - "build/prefab/full/linux_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/c2/c4/958ba681823bcad105bc51d6b3ac", - "build/prefab/full/mac_arm64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/95/55/3188c8463c50ce1cb7182788d0b8", - "build/prefab/full/mac_arm64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/70/10/b99df359d806c3f4fc3c91685749", - "build/prefab/full/mac_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/3e/40/9778e11391ca3832caf982a03832", - "build/prefab/full/mac_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/7a/0c/756391725d7bbbab568b8d946753", - "build/prefab/full/mac_x86_64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/b9/fe/e0f9747f9f03647d1becb31ac08d", - "build/prefab/full/mac_x86_64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/ee/d1/b0e387dfdbe8ac032456d4124bdf", - "build/prefab/full/mac_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/10/db/85cb3b50f58df978a42f5b199c0d", - "build/prefab/full/mac_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/3f/77/d66d99ba56643c20493a56cb3dba", - "build/prefab/full/windows_x86_gui/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/2b/23/eb989bada6166e03bf5756717194", - "build/prefab/full/windows_x86_gui/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/73/26/4670e939bfef0ad417f56d532fc0", - "build/prefab/full/windows_x86_server/debug/dist/BallisticaCoreHeadless.exe": "https://files.ballistica.net/cache/ba1/04/73/6951027e00d62310e59eca1737c3", - "build/prefab/full/windows_x86_server/release/dist/BallisticaCoreHeadless.exe": "https://files.ballistica.net/cache/ba1/1d/31/7698b0ee7abe6090d62295df170e", - "build/prefab/lib/linux_arm64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/02/ca/38de2750e629dd5f193a7c00a7d9", - "build/prefab/lib/linux_arm64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/7a/b3/e30adb7116efb17abe63ce70def8", - "build/prefab/lib/linux_arm64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/e9/c3/191d440460fbb95eb8c131081c4a", - "build/prefab/lib/linux_arm64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/ed/06/e6d9a361c5df654e5db27bec0999", - "build/prefab/lib/linux_x86_64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/a8/a5/70d83d086f2658e42c5d1e11aa53", - "build/prefab/lib/linux_x86_64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/b2/09/4105d848598a9cb1695f8be31696", - "build/prefab/lib/linux_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/3f/57/1ec9697d93ade3ef0a29d8ad2008", - "build/prefab/lib/linux_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/e6/54/c828ae0046a1023f28a02edffc4f", - "build/prefab/lib/mac_arm64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/92/ef/bbe7906558281bb1a57a9ebe4ad1", - "build/prefab/lib/mac_arm64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/62/ea/d5de2257b62b625308df5760ce32", - "build/prefab/lib/mac_arm64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/e5/a5/6304f685116de2c62c9d4b445dab", - "build/prefab/lib/mac_arm64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/a0/86/1a7de5eb337f446ef952a0572214", - "build/prefab/lib/mac_x86_64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/cb/f1/f26f8c525637c9a29b511a6ffc2f", - "build/prefab/lib/mac_x86_64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/49/ca/11a87c0fecc6220b9756caa2a86e", - "build/prefab/lib/mac_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/59/65/0b8fb1aeb2ae528f31d1455bb180", - "build/prefab/lib/mac_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/9f/88/c4ec00dff29865d4899395067b03", - "build/prefab/lib/windows/Debug_Win32/BallisticaCoreGenericInternal.lib": "https://files.ballistica.net/cache/ba1/6c/05/f66f58d20fab5d0e658961ac65b5", + "build/prefab/full/linux_arm64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/e1/8c/2ee1f6a353878bf6cef93870f424", + "build/prefab/full/linux_arm64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/eb/a9/fa25af1dbae41645d1c670e36404", + "build/prefab/full/linux_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/a9/42/4f64a2f1ea9421f883e34f29793f", + "build/prefab/full/linux_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/41/ad/bebe0575f1e1c4f2e09ef522b326", + "build/prefab/full/linux_x86_64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/05/f8/4dcd0854a6ac62afe9f22736c1c7", + "build/prefab/full/linux_x86_64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/e3/7a/f510a5e6c5f69feb1c9ed139a29b", + "build/prefab/full/linux_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/ea/cf/ceabc09c70adab68291bb9fa816f", + "build/prefab/full/linux_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/87/f7/3b5ec7037e46b6b58f650048e39c", + "build/prefab/full/mac_arm64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/6a/46/fa941d72c4f0b8c998f9403d5608", + "build/prefab/full/mac_arm64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/f2/f3/ae030d3f548d6ab7586a867f1c7a", + "build/prefab/full/mac_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/da/c0/9af017be0becf14ab1fc1ee96254", + "build/prefab/full/mac_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/b6/63/f870c817beebd8ee046d2dfc0364", + "build/prefab/full/mac_x86_64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/1c/2c/3aa53aeab23a9816877787e3ab88", + "build/prefab/full/mac_x86_64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/64/b5/a615b79eee5bea7bb8138195fb00", + "build/prefab/full/mac_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/0b/6a/d385633d0f26444362fbd79b262d", + "build/prefab/full/mac_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/42/14/7505a806e090eca242dfb3ebb136", + "build/prefab/full/windows_x86_gui/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/54/be/f241c6b573f203851221eaa3d58b", + "build/prefab/full/windows_x86_gui/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/a4/d3/44dd3f15ba30b945c0064fcbd0f5", + "build/prefab/full/windows_x86_server/debug/dist/BallisticaCoreHeadless.exe": "https://files.ballistica.net/cache/ba1/62/ed/67a279057579f659bed20b32f44e", + "build/prefab/full/windows_x86_server/release/dist/BallisticaCoreHeadless.exe": "https://files.ballistica.net/cache/ba1/b1/cf/a68c3ced4167252df2cba97f3233", + "build/prefab/lib/linux_arm64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/d3/e8/063c0165376117c575262aa34e8e", + "build/prefab/lib/linux_arm64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/4a/ac/490695a5b3def64b27e5293c3abb", + "build/prefab/lib/linux_arm64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/76/6e/318f1a492c6d861f0f2eba2cf174", + "build/prefab/lib/linux_arm64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/9f/f8/2ab60dff814ac7652328150fa70d", + "build/prefab/lib/linux_x86_64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/49/28/12f42bffe9da914a7bed5fca11e5", + "build/prefab/lib/linux_x86_64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/24/c2/124f383e647b185c644e2d8a6c01", + "build/prefab/lib/linux_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/94/99/b39898c43f81f5f85e13aebed284", + "build/prefab/lib/linux_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/90/04/1035d3fcc228298846a6293f3d8b", + "build/prefab/lib/mac_arm64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/e1/0a/92574d0a13fe1cff98ea380b80f5", + "build/prefab/lib/mac_arm64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/50/47/f0285e4ef62926f50f7bc737ff7d", + "build/prefab/lib/mac_arm64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/49/e7/4a8abfd1d32071165cfd3693d085", + "build/prefab/lib/mac_arm64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/ff/97/87a7d0fd328e28c42e069ad6f9cf", + "build/prefab/lib/mac_x86_64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/71/4e/59cb0a44813ce3a291b7665f3981", + "build/prefab/lib/mac_x86_64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/2d/77/38da1bc0cf1f1a63fd0b7c408d15", + "build/prefab/lib/mac_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/72/fc/96d3489a0237c5727fbe2288b7c9", + "build/prefab/lib/mac_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/64/7a/c1889e8bc60641cdb465f142caac", + "build/prefab/lib/windows/Debug_Win32/BallisticaCoreGenericInternal.lib": "https://files.ballistica.net/cache/ba1/b0/6e/7f5f4a266768a9800ae9e95f1200", "build/prefab/lib/windows/Debug_Win32/BallisticaCoreGenericInternal.pdb": "https://files.ballistica.net/cache/ba1/e9/54/b71bde6d97129447853b43879d5b", - "build/prefab/lib/windows/Debug_Win32/BallisticaCoreHeadlessInternal.lib": "https://files.ballistica.net/cache/ba1/66/90/610ebef166a7729d764f8ef34d5d", + "build/prefab/lib/windows/Debug_Win32/BallisticaCoreHeadlessInternal.lib": "https://files.ballistica.net/cache/ba1/d1/68/701d8bc9740ecbf54b21c5cc2255", "build/prefab/lib/windows/Debug_Win32/BallisticaCoreHeadlessInternal.pdb": "https://files.ballistica.net/cache/ba1/09/a7/875d4894eac1ec9bdbc2634b4815", - "build/prefab/lib/windows/Release_Win32/BallisticaCoreGenericInternal.lib": "https://files.ballistica.net/cache/ba1/96/bc/6d79640b1553959e85df1c12fb8d", + "build/prefab/lib/windows/Release_Win32/BallisticaCoreGenericInternal.lib": "https://files.ballistica.net/cache/ba1/89/46/802c29697180eaf95d4afb9a258e", "build/prefab/lib/windows/Release_Win32/BallisticaCoreGenericInternal.pdb": "https://files.ballistica.net/cache/ba1/a0/64/5b30f7f3139e59e54ad5244e9101", - "build/prefab/lib/windows/Release_Win32/BallisticaCoreHeadlessInternal.lib": "https://files.ballistica.net/cache/ba1/35/f8/8e2e0eb328b241af4e73b81d59a9", + "build/prefab/lib/windows/Release_Win32/BallisticaCoreHeadlessInternal.lib": "https://files.ballistica.net/cache/ba1/4d/1f/6bda20a013ce0be6fbd603fcf42c", "build/prefab/lib/windows/Release_Win32/BallisticaCoreHeadlessInternal.pdb": "https://files.ballistica.net/cache/ba1/b2/5c/3008c02fffd966044e7ba803f52d", "src/ballistica/generated/python_embedded/binding.inc": "https://files.ballistica.net/cache/ba1/7d/3e/229a581cb2454ed856f1d8b564a7", "src/ballistica/generated/python_embedded/bootstrap.inc": "https://files.ballistica.net/cache/ba1/d3/db/e73d4dcf1280d5f677c3cf8b47c3" diff --git a/CHANGELOG.md b/CHANGELOG.md index decc6ed4..4a40476d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -### 1.7.6 (build 20681, api 7, 2022-08-10) +### 1.7.6 (build 20683, api 7, 2022-08-10) - Cleaned up da MetaSubsystem code. - It is now possible to tell the meta system about arbitrary classes (ba_meta export foo.bar.Class) instead of just the preset types 'plugin', 'game', etc. - Newly discovered plugins are now activated immediately instead of requiring a restart. diff --git a/ballisticacore-cmake/CMakeLists.txt b/ballisticacore-cmake/CMakeLists.txt index 63c0d7a9..f93aa0ce 100644 --- a/ballisticacore-cmake/CMakeLists.txt +++ b/ballisticacore-cmake/CMakeLists.txt @@ -300,6 +300,7 @@ add_executable(ballisticacore ${BA_SRC_ROOT}/ballistica/dynamics/part.h ${BA_SRC_ROOT}/ballistica/dynamics/rigid_body.cc ${BA_SRC_ROOT}/ballistica/dynamics/rigid_body.h + ${BA_SRC_ROOT}/ballistica/game/account.cc ${BA_SRC_ROOT}/ballistica/game/account.h ${BA_SRC_ROOT}/ballistica/game/client_controller_interface.h ${BA_SRC_ROOT}/ballistica/game/connection/connection.cc @@ -317,6 +318,7 @@ add_executable(ballisticacore ${BA_SRC_ROOT}/ballistica/game/friend_score_set.h ${BA_SRC_ROOT}/ballistica/game/game.cc ${BA_SRC_ROOT}/ballistica/game/game.h + ${BA_SRC_ROOT}/ballistica/game/game_stream.cc ${BA_SRC_ROOT}/ballistica/game/game_stream.h ${BA_SRC_ROOT}/ballistica/game/host_activity.cc ${BA_SRC_ROOT}/ballistica/game/host_activity.h @@ -325,10 +327,15 @@ add_executable(ballisticacore ${BA_SRC_ROOT}/ballistica/game/player_spec.cc ${BA_SRC_ROOT}/ballistica/game/player_spec.h ${BA_SRC_ROOT}/ballistica/game/score_to_beat.h + ${BA_SRC_ROOT}/ballistica/game/session/client_session.cc ${BA_SRC_ROOT}/ballistica/game/session/client_session.h + ${BA_SRC_ROOT}/ballistica/game/session/host_session.cc ${BA_SRC_ROOT}/ballistica/game/session/host_session.h + ${BA_SRC_ROOT}/ballistica/game/session/net_client_session.cc ${BA_SRC_ROOT}/ballistica/game/session/net_client_session.h + ${BA_SRC_ROOT}/ballistica/game/session/replay_client_session.cc ${BA_SRC_ROOT}/ballistica/game/session/replay_client_session.h + ${BA_SRC_ROOT}/ballistica/game/session/session.cc ${BA_SRC_ROOT}/ballistica/game/session/session.h ${BA_SRC_ROOT}/ballistica/generic/base64.cc ${BA_SRC_ROOT}/ballistica/generic/base64.h diff --git a/ballisticacore-windows/Generic/BallisticaCoreGeneric.vcxproj b/ballisticacore-windows/Generic/BallisticaCoreGeneric.vcxproj index f5cef4ae..541493f4 100644 --- a/ballisticacore-windows/Generic/BallisticaCoreGeneric.vcxproj +++ b/ballisticacore-windows/Generic/BallisticaCoreGeneric.vcxproj @@ -291,6 +291,7 @@ + @@ -308,6 +309,7 @@ + @@ -316,10 +318,15 @@ + + + + + diff --git a/ballisticacore-windows/Generic/BallisticaCoreGeneric.vcxproj.filters b/ballisticacore-windows/Generic/BallisticaCoreGeneric.vcxproj.filters index baf512a4..c799e77f 100644 --- a/ballisticacore-windows/Generic/BallisticaCoreGeneric.vcxproj.filters +++ b/ballisticacore-windows/Generic/BallisticaCoreGeneric.vcxproj.filters @@ -307,6 +307,9 @@ ballistica\dynamics + + ballistica\game + ballistica\game @@ -358,6 +361,9 @@ ballistica\game + + ballistica\game + ballistica\game @@ -382,18 +388,33 @@ ballistica\game + + ballistica\game\session + ballistica\game\session + + ballistica\game\session + ballistica\game\session + + ballistica\game\session + ballistica\game\session + + ballistica\game\session + ballistica\game\session + + ballistica\game\session + ballistica\game\session diff --git a/ballisticacore-windows/Headless/BallisticaCoreHeadless.vcxproj b/ballisticacore-windows/Headless/BallisticaCoreHeadless.vcxproj index dcca3aa9..2d56df4c 100644 --- a/ballisticacore-windows/Headless/BallisticaCoreHeadless.vcxproj +++ b/ballisticacore-windows/Headless/BallisticaCoreHeadless.vcxproj @@ -286,6 +286,7 @@ + @@ -303,6 +304,7 @@ + @@ -311,10 +313,15 @@ + + + + + diff --git a/ballisticacore-windows/Headless/BallisticaCoreHeadless.vcxproj.filters b/ballisticacore-windows/Headless/BallisticaCoreHeadless.vcxproj.filters index baf512a4..c799e77f 100644 --- a/ballisticacore-windows/Headless/BallisticaCoreHeadless.vcxproj.filters +++ b/ballisticacore-windows/Headless/BallisticaCoreHeadless.vcxproj.filters @@ -307,6 +307,9 @@ ballistica\dynamics + + ballistica\game + ballistica\game @@ -358,6 +361,9 @@ ballistica\game + + ballistica\game + ballistica\game @@ -382,18 +388,33 @@ ballistica\game + + ballistica\game\session + ballistica\game\session + + ballistica\game\session + ballistica\game\session + + ballistica\game\session + ballistica\game\session + + ballistica\game\session + ballistica\game\session + + ballistica\game\session + ballistica\game\session diff --git a/src/ballistica/ballistica.cc b/src/ballistica/ballistica.cc index c41a7ccd..78db8d83 100644 --- a/src/ballistica/ballistica.cc +++ b/src/ballistica/ballistica.cc @@ -21,7 +21,7 @@ namespace ballistica { // These are set automatically via script; don't modify them here. -const int kAppBuildNumber = 20681; +const int kAppBuildNumber = 20683; const char* kAppVersion = "1.7.6"; // Our standalone globals. diff --git a/src/ballistica/game/account.cc b/src/ballistica/game/account.cc new file mode 100644 index 00000000..27cc54ff --- /dev/null +++ b/src/ballistica/game/account.cc @@ -0,0 +1,207 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/game/account.h" + +#include "ballistica/app/app_globals.h" +#include "ballistica/app/app_internal.h" +#include "ballistica/game/game.h" +#include "ballistica/generic/utils.h" +#include "ballistica/platform/platform.h" + +namespace ballistica { + +auto Account::AccountTypeFromString(const std::string& val) -> V1AccountType { + if (val == "Game Center") { + return V1AccountType::kGameCenter; + } else if (val == "Game Circle") { + return V1AccountType::kGameCircle; + } else if (val == "Google Play") { + return V1AccountType::kGooglePlay; + } else if (val == "Steam") { + return V1AccountType::kSteam; + } else if (val == "Oculus") { + return V1AccountType::kOculus; + } else if (val == "NVIDIA China") { + return V1AccountType::kNvidiaChina; + } else if (val == "Test") { + return V1AccountType::kTest; + } else if (val == "Local") { + return V1AccountType::kDevice; + } else if (val == "Server") { + return V1AccountType::kServer; + } else if (val == "V2") { + return V1AccountType::kV2; + } else { + return V1AccountType::kInvalid; + } +} + +auto Account::AccountTypeToString(V1AccountType type) -> std::string { + switch (type) { + case V1AccountType::kGameCenter: + return "Game Center"; + case V1AccountType::kGameCircle: + return "Game Circle"; + case V1AccountType::kGooglePlay: + return "Google Play"; + case V1AccountType::kSteam: + return "Steam"; + case V1AccountType::kOculus: + return "Oculus"; + case V1AccountType::kTest: + return "Test"; + case V1AccountType::kDevice: + return "Local"; + case V1AccountType::kServer: + return "Server"; + case V1AccountType::kNvidiaChina: + return "NVIDIA China"; + case V1AccountType::kV2: + return "V2"; + default: + return ""; + } +} + +auto Account::AccountTypeToIconString(V1AccountType type) -> std::string { + switch (type) { + case V1AccountType::kTest: + return g_game->CharStr(SpecialChar::kTestAccount); + case V1AccountType::kNvidiaChina: + return g_game->CharStr(SpecialChar::kNvidiaLogo); + case V1AccountType::kGooglePlay: + return g_game->CharStr(SpecialChar::kGooglePlayGamesLogo); + case V1AccountType::kSteam: + return g_game->CharStr(SpecialChar::kSteamLogo); + case V1AccountType::kOculus: + return g_game->CharStr(SpecialChar::kOculusLogo); + case V1AccountType::kGameCenter: + return g_game->CharStr(SpecialChar::kGameCenterLogo); + case V1AccountType::kGameCircle: + return g_game->CharStr(SpecialChar::kGameCircleLogo); + case V1AccountType::kDevice: + case V1AccountType::kServer: + return g_game->CharStr(SpecialChar::kLocalAccount); + case V1AccountType::kV2: + return g_game->CharStr(SpecialChar::kV2Logo); + default: + return ""; + } +} + +Account::Account() = default; + +auto Account::GetLoginName() -> std::string { + std::lock_guard lock(mutex_); + return login_name_; +} + +auto Account::GetLoginID() -> std::string { + std::lock_guard lock(mutex_); + return login_id_; +} + +auto Account::GetToken() -> std::string { + std::lock_guard lock(mutex_); + return token_; +} + +auto Account::GetExtra() -> std::string { + std::lock_guard lock(mutex_); + return extra_; +} + +auto Account::GetExtra2() -> std::string { + std::lock_guard lock(mutex_); + return extra_2_; +} + +auto Account::GetLoginState(int* state_num) -> V1LoginState { + std::lock_guard lock(mutex_); + if (state_num) { + *state_num = login_state_num_; + } + return login_state_; +} + +void Account::SetExtra(const std::string& extra) { + std::lock_guard lock(mutex_); + extra_ = extra; +} + +void Account::SetExtra2(const std::string& extra) { + std::lock_guard lock(mutex_); + extra_2_ = extra; +} + +void Account::SetToken(const std::string& account_id, + const std::string& token) { + std::lock_guard lock(mutex_); + // Hmm, does this compare logic belong in here? + if (login_id_ == account_id) { + token_ = token; + } +} + +void Account::SetLogin(V1AccountType account_type, V1LoginState login_state, + const std::string& login_name, + const std::string& login_id) { + bool call_login_did_change = false; + { + std::lock_guard lock(mutex_); + + // We call out to Python so need to be in game thread. + assert(InGameThread()); + if (login_state_ != login_state + || g_app_globals->account_type != account_type || login_id_ != login_id + || login_name_ != login_name) { + // Special case: if they sent a sign-out for an account type that is + // currently not signed in, ignore it. + if (login_state == V1LoginState::kSignedOut + && (account_type != g_app_globals->account_type)) { + // No-op. + } else { + login_state_ = login_state; + g_app_globals->account_type = account_type; + login_id_ = login_id; + login_name_ = Utils::GetValidUTF8(login_name.c_str(), "gthm"); + + // If they signed out of an account, account type switches to invalid. + if (login_state == V1LoginState::kSignedOut) { + g_app_globals->account_type = V1AccountType::kInvalid; + } + login_state_num_ += 1; + call_login_did_change = true; + } + } + } + if (call_login_did_change) { + // Inform a few subsystems of the change. + g_app_internal->V1LoginDidChange(); + g_platform->V1LoginDidChange(); + } +} + +void Account::SetProductsPurchased(const std::vector& products) { + std::lock_guard lock(mutex_); + std::unordered_map purchases_old = product_purchases_; + product_purchases_.clear(); + for (auto&& i : products) { + product_purchases_[i] = true; + } + if (product_purchases_ != purchases_old) { + product_purchases_state_++; + } +} + +auto Account::GetProductPurchased(const std::string& product) -> bool { + std::lock_guard lock(mutex_); + auto i = product_purchases_.find(product); + if (i == product_purchases_.end()) { + return false; + } else { + return i->second; + } +} + +} // namespace ballistica diff --git a/src/ballistica/game/game_stream.cc b/src/ballistica/game/game_stream.cc new file mode 100644 index 00000000..db1de9a8 --- /dev/null +++ b/src/ballistica/game/game_stream.cc @@ -0,0 +1,1249 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/game/game_stream.h" + +#include "ballistica/app/app_globals.h" +#include "ballistica/dynamics/bg/bg_dynamics.h" +#include "ballistica/dynamics/material/material.h" +#include "ballistica/dynamics/material/material_action.h" +#include "ballistica/dynamics/material/material_component.h" +#include "ballistica/dynamics/material/material_condition_node.h" +#include "ballistica/dynamics/part.h" +#include "ballistica/game/connection/connection_set.h" +#include "ballistica/game/connection/connection_to_client.h" +#include "ballistica/game/session/host_session.h" +#include "ballistica/media/component/collide_model.h" +#include "ballistica/media/component/data.h" +#include "ballistica/media/component/model.h" +#include "ballistica/media/component/sound.h" +#include "ballistica/media/component/texture.h" +#include "ballistica/media/media_server.h" +#include "ballistica/networking/networking.h" +#include "ballistica/scene/node/node_attribute.h" +#include "ballistica/scene/node/node_type.h" +#include "ballistica/scene/scene.h" + +namespace ballistica { + +GameStream::GameStream(HostSession* host_session, bool save_replay) + : time_(0), + host_session_(host_session), + next_flush_time_(0), + last_physics_correction_time_(0), + last_send_time_(0), + writing_replay_(false) { + if (save_replay) { + // Sanity check - we should only ever be writing one replay at once. + if (g_app_globals->replay_open) { + Log("ERROR: g_replay_open true at replay start; shouldn't happen."); + } + assert(g_media_server); + g_media_server->PushBeginWriteReplayCall(); + writing_replay_ = true; + g_app_globals->replay_open = true; + } + + // If we're the live output-stream from a host-session, + // take responsibility for feeding all clients to this device. + if (host_session_) { + g_game->connections()->RegisterClientController(this); + } +} + +GameStream::~GameStream() { + // Ship our last commands (if it matters..) + Flush(); + + if (writing_replay_) { + // Sanity check: We should only ever be writing one replay at once. + if (!g_app_globals->replay_open) { + Log("ERROR: g_replay_open false at replay close; shouldn't happen."); + } + g_app_globals->replay_open = false; + assert(g_media_server); + g_media_server->PushEndWriteReplayCall(); + writing_replay_ = false; + } + + // If we're wired to the host-session, go ahead and release clients. + if (host_session_) { + g_game->connections()->UnregisterClientController(this); + +#pragma clang diagnostic push +#pragma ide diagnostic ignored "UnreachableCode" + + // Also, in the host-session case, make sure everything cleaned itself up. + if (g_buildconfig.debug_build()) { + size_t count; + count = GetPointerCount(scenes_); + if (count != 0) { + Log("ERROR: " + std::to_string(count) + + " scene graphs in output stream at shutdown"); + } + count = GetPointerCount(nodes_); + if (count != 0) { + Log("ERROR: " + std::to_string(count) + + " nodes in output stream at shutdown"); + } + count = GetPointerCount(materials_); + if (count != 0) { + Log("ERROR: " + std::to_string(count) + + " materials in output stream at shutdown"); + } + count = GetPointerCount(textures_); + if (count != 0) { + Log("ERROR: " + std::to_string(count) + + " textures in output stream at shutdown"); + } + count = GetPointerCount(models_); + if (count != 0) { + Log("ERROR: " + std::to_string(count) + + " models in output stream at shutdown"); + } + count = GetPointerCount(sounds_); + if (count != 0) { + Log("ERROR: " + std::to_string(count) + + " sounds in output stream at shutdown"); + } + count = GetPointerCount(collide_models_); + if (count != 0) { + Log("ERROR: " + std::to_string(count) + + " collide_models in output stream at shutdown"); + } + } + } + +#pragma clang diagnostic pop +} + +// Pull the current built-up message. +auto GameStream::GetOutMessage() const -> std::vector { + assert(!host_session_); // this should only be getting used for + // standalone temp ones.. + if (!out_command_.empty()) { + Log("Error: GameStream shutting down with non-empty outCommand"); + } + return out_message_; +} + +template +auto GameStream::GetPointerCount(const std::vector& vec) -> size_t { + size_t count = 0; + + auto size = vec.size(); + T* const* vals = vec.data(); + for (size_t i = 0; i < size; i++) { + if (vals[i] != nullptr) { + count++; + } + } + return count; +} + +// Given a vector of pointers, return an index to an available (nullptr) entry, +// expanding the vector if need be. +template +auto GameStream::GetFreeIndex(std::vector* vec, + std::vector* free_indices) -> size_t { + // If we have any free indices, use one of them. + if (!free_indices->empty()) { + size_t val = free_indices->back(); + free_indices->pop_back(); + return val; + } + + // No free indices; expand the vec and return the new index. + vec->push_back(nullptr); + return vec->size() - 1; +} + +// Add an entry. +template +void GameStream::Add(T* val, std::vector* vec, + std::vector* free_indices) { + // This should only get used when we're being driven by the host-session. + assert(host_session_); + assert(val); + assert(val->stream_id() == -1); + size_t index = GetFreeIndex(vec, free_indices); + (*vec)[index] = val; + val->set_stream_id(index); +} + +// Remove an entry. +template +void GameStream::Remove(T* val, std::vector* vec, + std::vector* free_indices) { + assert(val); + assert(val->stream_id() >= 0); + assert(static_cast(vec->size()) > val->stream_id()); + assert((*vec)[val->stream_id()] == val); + (*vec)[val->stream_id()] = nullptr; + + // Add this to our list of available slots to recycle. + free_indices->push_back(val->stream_id()); + val->clear_stream_id(); +} + +void GameStream::Fail() { + Log("Error writing replay file"); + if (writing_replay_) { + // Sanity check: We should only ever be writing one replay at once. + if (!g_app_globals->replay_open) { + Log("ERROR: g_replay_open false at replay close; shouldn't happen."); + } + assert(g_media_server); + g_media_server->PushEndWriteReplayCall(); + writing_replay_ = false; + g_app_globals->replay_open = false; + } +} + +void GameStream::Flush() { + if (!out_command_.empty()) + Log("Error: GameStream flushing down with non-empty outCommand"); + if (!out_message_.empty()) { + ShipSessionCommandsMessage(); + } +} + +#pragma clang diagnostic push +#pragma ide diagnostic ignored "ConstantParameter" + +// Writes just a command. +void GameStream::WriteCommand(SessionCommand cmd) { + assert(out_command_.empty()); + + // For now just use full size values. + size_t size = 0; + out_command_.resize(size + 1); + uint8_t* ptr = &out_command_[size]; + *ptr = static_cast(cmd); +} + +#pragma clang diagnostic pop + +// Writes a command plus an int to the stream, using whatever size is optimal. +void GameStream::WriteCommandInt32(SessionCommand cmd, int32_t value) { + assert(out_command_.empty()); + + // For now just use full size values. + size_t size = 0; + out_command_.resize(size + 5); + uint8_t* ptr = &out_command_[size]; + *(ptr++) = static_cast(cmd); + int32_t vals[] = {value}; + memcpy(ptr, vals, 4); +} + +void GameStream::WriteCommandInt32_2(SessionCommand cmd, int32_t value1, + int32_t value2) { + assert(out_command_.empty()); + + // For now just use full size vals. + size_t size = 0; + out_command_.resize(size + 9); + uint8_t* ptr = &out_command_[size]; + *(ptr++) = static_cast(cmd); + int32_t vals[] = {value1, value2}; + memcpy(ptr, vals, 8); +} + +void GameStream::WriteCommandInt32_3(SessionCommand cmd, int32_t value1, + int32_t value2, int32_t value3) { + assert(out_command_.empty()); + + // For now just use full size vals. + size_t size = 0; + out_command_.resize(size + 13); + uint8_t* ptr = &out_command_[size]; + *(ptr++) = static_cast(cmd); + int32_t vals[] = {value1, value2, value3}; + memcpy(ptr, vals, 12); +} + +void GameStream::WriteCommandInt32_4(SessionCommand cmd, int32_t value1, + int32_t value2, int32_t value3, + int32_t value4) { + assert(out_command_.empty()); + + // For now just use full size vals. + size_t size = 0; + out_command_.resize(size + 17); + uint8_t* ptr = &out_command_[size]; + *(ptr++) = static_cast(cmd); + int32_t vals[] = {value1, value2, value3, value4}; + memcpy(ptr, vals, 16); +} + +// FIXME: We don't actually support sending out 64 bit values yet, but +// adding these placeholders for if/when we do. +// They will also catch values greater than 32 bits in debug mode. +// We'll need a protocol update to add support for 64 bit over the wire. +void GameStream::WriteCommandInt64(SessionCommand cmd, int64_t value) { + WriteCommandInt32(cmd, static_cast_check_fit(value)); +} + +void GameStream::WriteCommandInt64_2(SessionCommand cmd, int64_t value1, + int64_t value2) { + WriteCommandInt32_2(cmd, static_cast_check_fit(value1), + static_cast_check_fit(value2)); +} + +void GameStream::WriteCommandInt64_3(SessionCommand cmd, int64_t value1, + int64_t value2, int64_t value3) { + WriteCommandInt32_3(cmd, static_cast_check_fit(value1), + static_cast_check_fit(value2), + static_cast_check_fit(value3)); +} + +void GameStream::WriteCommandInt64_4(SessionCommand cmd, int64_t value1, + int64_t value2, int64_t value3, + int64_t value4) { + WriteCommandInt32_4(cmd, static_cast_check_fit(value1), + static_cast_check_fit(value2), + static_cast_check_fit(value3), + static_cast_check_fit(value4)); +} + +void GameStream::WriteString(const std::string& s) { + // Write length int. + auto string_size = s.size(); + auto size = out_command_.size(); + out_command_.resize(size + 4 + s.size()); + memcpy(&out_command_[size], &string_size, 4); + if (string_size > 0) { + memcpy(&out_command_[size + 4], s.c_str(), string_size); + } +} + +void GameStream::WriteFloat(float val) { + auto size = static_cast(out_command_.size()); + out_command_.resize(size + sizeof(val)); + memcpy(&out_command_[size], &val, 4); +} + +void GameStream::WriteFloats(size_t count, const float* vals) { + assert(count > 0); + auto size = out_command_.size(); + size_t vals_size = sizeof(float) * count; + out_command_.resize(size + vals_size); + memcpy(&(out_command_[size]), vals, vals_size); +} + +void GameStream::WriteInts32(size_t count, const int32_t* vals) { + assert(count > 0); + auto size = out_command_.size(); + size_t vals_size = sizeof(int32_t) * count; + out_command_.resize(size + vals_size); + memcpy(&(out_command_[size]), vals, vals_size); +} + +void GameStream::WriteInts64(size_t count, const int64_t* vals) { + // FIXME: we don't actually support writing 64 bit values to the wire + // at the moment; will need a protocol update for that. + // This is just implemented as a placeholder. + std::vector vals32(count); + for (size_t i = 0; i < count; i++) { + vals32[i] = static_cast_check_fit(vals[i]); + } + WriteInts32(count, vals32.data()); +} + +void GameStream::WriteChars(size_t count, const char* vals) { + assert(count > 0); + auto size = out_command_.size(); + auto vals_size = static_cast(count); + out_command_.resize(size + vals_size); + memcpy(&(out_command_[size]), vals, vals_size); +} + +void GameStream::ShipSessionCommandsMessage() { + BA_PRECONDITION(!out_message_.empty()); + + // Send this message to all client-connections we're attached to. + for (auto& connection : connections_to_clients_) { + (*connection).SendReliableMessage(out_message_); + } + if (writing_replay_) { + AddMessageToReplay(out_message_); + } + out_message_.clear(); + last_send_time_ = GetRealTime(); +} + +void GameStream::AddMessageToReplay(const std::vector& message) { + assert(writing_replay_); + assert(g_media_server); + + assert(!message.empty()); + if (g_buildconfig.debug_build()) { + switch (message[0]) { + case BA_MESSAGE_SESSION_RESET: + case BA_MESSAGE_SESSION_COMMANDS: + case BA_MESSAGE_SESSION_DYNAMICS_CORRECTION: + break; + default: + throw Exception("unexpected message going to replay: " + + std::to_string(static_cast(message[0]))); + } + } + + g_media_server->PushAddMessageToReplayCall(message); +} + +void GameStream::SendPhysicsCorrection(bool blend) { + assert(host_session_); + + std::vector > messages; + host_session_->GetCorrectionMessages(blend, &messages); + + // FIXME - have to send reliably at the moment since these will most likely be + // bigger than our unreliable packet limit. :-( + for (auto& message : messages) { + for (auto& connections_to_client : connections_to_clients_) { + (*connections_to_client).SendReliableMessage(message); + } + if (writing_replay_) { + AddMessageToReplay(message); + } + } +} + +void GameStream::EndCommand(bool is_time_set) { + assert(!out_command_.empty()); + + int out_message_size; + if (out_message_.empty()) { + // Init the message if we're the first command on it. + out_message_.resize(1); + out_message_[0] = BA_MESSAGE_SESSION_COMMANDS; + out_message_size = 1; + } else { + out_message_size = static_cast(out_message_.size()); + } + + out_message_.resize(out_message_size + 2 + + out_command_.size()); // command length plus data + + auto val = static_cast(out_command_.size()); + memcpy(&(out_message_[out_message_size]), &val, 2); + memcpy(&(out_message_[out_message_size + 2]), &(out_command_[0]), + out_command_.size()); + + // When attached to a host-session, send this message to clients if it's been + // long enough. Also send off occasional correction packets. + if (host_session_) { + // Now if its been long enough *AND* this is a time-step command, send. + millisecs_t real_time = GetRealTime(); + millisecs_t diff = real_time - last_send_time_; + if (is_time_set && diff >= g_app_globals->buffer_time) { + ShipSessionCommandsMessage(); + + // Also, as long as we're here, fire off a physics-correction packet every + // now and then. + + // IMPORTANT: We only do this right after shipping off our pending session + // commands; otherwise the client will get the correction that accounts + // for commands that they haven't been sent yet. + diff = real_time - last_physics_correction_time_; + if (diff >= g_app_globals->dynamics_sync_time) { + last_physics_correction_time_ = real_time; + SendPhysicsCorrection(true); + } + } + } + out_command_.clear(); +} + +auto GameStream::IsValidScene(Scene* s) -> bool { + if (!host_session_) { + return true; // We don't build lists in this mode so can't verify this. + } + return (s != nullptr && s->stream_id() >= 0 + && s->stream_id() < static_cast(scenes_.size()) + && scenes_[s->stream_id()] == s); +} + +auto GameStream::IsValidNode(Node* n) -> bool { + if (!host_session_) { + return true; // We don't build lists in this mode so can't verify this. + } + return (n != nullptr && n->stream_id() >= 0 + && n->stream_id() < static_cast(nodes_.size()) + && nodes_[n->stream_id()] == n); +} + +auto GameStream::IsValidTexture(Texture* n) -> bool { + if (!host_session_) { + return true; // We don't build lists in this mode so can't verify this. + } + return (n != nullptr && n->stream_id() >= 0 + && n->stream_id() < static_cast(textures_.size()) + && textures_[n->stream_id()] == n); +} + +auto GameStream::IsValidModel(Model* n) -> bool { + if (!host_session_) { + return true; // We don't build lists in this mode so can't verify this. + } + return (n != nullptr && n->stream_id() >= 0 + && n->stream_id() < static_cast(models_.size()) + && models_[n->stream_id()] == n); +} + +auto GameStream::IsValidSound(Sound* n) -> bool { + if (!host_session_) { + return true; // We don't build lists in this mode so can't verify this. + } + return (n != nullptr && n->stream_id() >= 0 + && n->stream_id() < static_cast(sounds_.size()) + && sounds_[n->stream_id()] == n); +} + +auto GameStream::IsValidData(Data* n) -> bool { + if (!host_session_) { + return true; // We don't build lists in this mode so can't verify this. + } + return (n != nullptr && n->stream_id() >= 0 + && n->stream_id() < static_cast(datas_.size()) + && datas_[n->stream_id()] == n); +} + +auto GameStream::IsValidCollideModel(CollideModel* n) -> bool { + if (!host_session_) { + return true; // We don't build lists in this mode so can't verify this. + } + return (n != nullptr && n->stream_id() >= 0 + && n->stream_id() < static_cast(collide_models_.size()) + && collide_models_[n->stream_id()] == n); +} + +auto GameStream::IsValidMaterial(Material* n) -> bool { + if (!host_session_) { + return true; // We don't build lists in this mode so can't verify this. + } + return (n != nullptr && n->stream_id() >= 0 + && n->stream_id() < static_cast(materials_.size()) + && materials_[n->stream_id()] == n); +} + +void GameStream::SetTime(millisecs_t t) { + if (time_ == t) { + return; // Ignore redundants. + } + millisecs_t diff = t - time_; + if (diff > 255) { + Log("Error: GameStream got time diff > 255; not expected."); + diff = 255; + } + WriteCommandInt64(SessionCommand::kBaseTimeStep, diff); + time_ = t; + EndCommand(true); +} + +void GameStream::AddScene(Scene* s) { + // Host mode. + if (host_session_) { + Add(s, &scenes_, &free_indices_scene_graphs_); + s->SetOutputStream(this); + } else { + // Dump mode. + assert(s->stream_id() != -1); + } + WriteCommandInt64_2(SessionCommand::kAddSceneGraph, s->stream_id(), + s->time()); + EndCommand(); +} + +void GameStream::RemoveScene(Scene* s) { + WriteCommandInt64(SessionCommand::kRemoveSceneGraph, s->stream_id()); + Remove(s, &scenes_, &free_indices_scene_graphs_); + EndCommand(); +} + +void GameStream::StepScene(Scene* s) { + assert(IsValidScene(s)); + WriteCommandInt64(SessionCommand::kStepSceneGraph, s->stream_id()); + EndCommand(); +} + +void GameStream::AddNode(Node* n) { + assert(n); + if (host_session_) { + Add(n, &nodes_, &free_indices_nodes_); + } else { + assert(n && n->stream_id() != -1); + } + + Scene* sg = n->scene(); + assert(IsValidScene(sg)); + WriteCommandInt64_3(SessionCommand::kAddNode, sg->stream_id(), + n->type()->id(), n->stream_id()); + EndCommand(); +} + +void GameStream::NodeOnCreate(Node* n) { + assert(IsValidNode(n)); + WriteCommandInt64(SessionCommand::kNodeOnCreate, n->stream_id()); + EndCommand(); +} + +void GameStream::SetForegroundScene(Scene* sg) { + assert(IsValidScene(sg)); + WriteCommandInt64(SessionCommand::kSetForegroundSceneGraph, sg->stream_id()); + EndCommand(); +} + +void GameStream::RemoveNode(Node* n) { + assert(IsValidNode(n)); + WriteCommandInt64(SessionCommand::kRemoveNode, n->stream_id()); + Remove(n, &nodes_, &free_indices_nodes_); + EndCommand(); +} + +void GameStream::AddTexture(Texture* t) { + // Register an ID in host mode. + if (host_session_) { + Add(t, &textures_, &free_indices_textures_); + } else { + assert(t && t->stream_id() != -1); + } + Scene* sg = t->scene(); + assert(IsValidScene(sg)); + WriteCommandInt64_2(SessionCommand::kAddTexture, sg->stream_id(), + t->stream_id()); + WriteString(t->name()); + EndCommand(); +} + +void GameStream::RemoveTexture(Texture* t) { + assert(IsValidTexture(t)); + WriteCommandInt64(SessionCommand::kRemoveTexture, t->stream_id()); + Remove(t, &textures_, &free_indices_textures_); + EndCommand(); +} + +void GameStream::AddModel(Model* t) { + // Register an ID in host mode. + if (host_session_) { + Add(t, &models_, &free_indices_models_); + } else { + assert(t && t->stream_id() != -1); + } + Scene* sg = t->scene(); + assert(IsValidScene(sg)); + WriteCommandInt64_2(SessionCommand::kAddModel, sg->stream_id(), + t->stream_id()); + WriteString(t->name()); + EndCommand(); +} + +void GameStream::RemoveModel(Model* t) { + assert(IsValidModel(t)); + WriteCommandInt64(SessionCommand::kRemoveModel, t->stream_id()); + Remove(t, &models_, &free_indices_models_); + EndCommand(); +} + +void GameStream::AddSound(Sound* t) { + // Register an ID in host mode. + if (host_session_) { + Add(t, &sounds_, &free_indices_sounds_); + } else { + assert(t && t->stream_id() != -1); + } + Scene* sg = t->scene(); + assert(IsValidScene(sg)); + WriteCommandInt64_2(SessionCommand::kAddSound, sg->stream_id(), + t->stream_id()); + WriteString(t->name()); + EndCommand(); +} + +void GameStream::RemoveSound(Sound* t) { + assert(IsValidSound(t)); + WriteCommandInt64(SessionCommand::kRemoveSound, t->stream_id()); + Remove(t, &sounds_, &free_indices_sounds_); + EndCommand(); +} + +void GameStream::AddData(Data* t) { + // Register an ID in host mode. + if (host_session_) { + Add(t, &datas_, &free_indices_datas_); + } else { + assert(t && t->stream_id() != -1); + } + Scene* sg = t->scene(); + assert(IsValidScene(sg)); + WriteCommandInt64_2(SessionCommand::kAddData, sg->stream_id(), + t->stream_id()); + WriteString(t->name()); + EndCommand(); +} + +void GameStream::RemoveData(Data* t) { + assert(IsValidData(t)); + WriteCommandInt64(SessionCommand::kRemoveData, t->stream_id()); + Remove(t, &datas_, &free_indices_datas_); + EndCommand(); +} + +void GameStream::AddCollideModel(CollideModel* t) { + if (host_session_) { + Add(t, &collide_models_, &free_indices_collide_models_); + } else { + assert(t && t->stream_id() != -1); + } + Scene* sg = t->scene(); + assert(IsValidScene(sg)); + WriteCommandInt64_2(SessionCommand::kAddCollideModel, sg->stream_id(), + t->stream_id()); + WriteString(t->name()); + EndCommand(); +} + +void GameStream::RemoveCollideModel(CollideModel* t) { + assert(IsValidCollideModel(t)); + WriteCommandInt64(SessionCommand::kRemoveCollideModel, t->stream_id()); + Remove(t, &collide_models_, &free_indices_collide_models_); + EndCommand(); +} + +void GameStream::AddMaterial(Material* m) { + if (host_session_) { + Add(m, &materials_, &free_indices_materials_); + } else { + assert(m && m->stream_id() != -1); + } + Scene* sg = m->scene(); + assert(IsValidScene(sg)); + WriteCommandInt64_2(SessionCommand::kAddMaterial, sg->stream_id(), + m->stream_id()); + EndCommand(); +} + +void GameStream::RemoveMaterial(Material* m) { + assert(IsValidMaterial(m)); + WriteCommandInt64(SessionCommand::kRemoveMaterial, m->stream_id()); + Remove(m, &materials_, &free_indices_materials_); + EndCommand(); +} + +void GameStream::AddMaterialComponent(Material* m, MaterialComponent* c) { + assert(IsValidMaterial(m)); + auto flattened_size = c->GetFlattenedSize(); + assert(flattened_size > 0 && flattened_size < 10000); + WriteCommandInt64_2(SessionCommand::kAddMaterialComponent, m->stream_id(), + static_cast_check_fit(flattened_size)); + size_t size = out_command_.size(); + out_command_.resize(size + flattened_size); + char* ptr = reinterpret_cast(&out_command_[size]); + char* ptr2 = ptr; + c->Flatten(&ptr2, this); + size_t actual_size = ptr2 - ptr; + if (actual_size != flattened_size) { + throw Exception("Expected flattened_size " + std::to_string(flattened_size) + + " got " + std::to_string(actual_size)); + } + EndCommand(); +} + +void GameStream::ConnectNodeAttribute(Node* src_node, + NodeAttributeUnbound* src_attr, + Node* dst_node, + NodeAttributeUnbound* dst_attr) { + assert(IsValidNode(src_node)); + assert(IsValidNode(dst_node)); + assert(src_attr->node_type() == src_node->type()); + assert(dst_attr->node_type() == dst_node->type()); + if (src_node->scene() != dst_node->scene()) { + throw Exception("Nodes are from different scenes"); + } + assert(src_node->scene() == dst_node->scene()); + WriteCommandInt64_4(SessionCommand::kConnectNodeAttribute, + src_node->stream_id(), src_attr->index(), + dst_node->stream_id(), dst_attr->index()); + EndCommand(); +} + +void GameStream::NodeMessage(Node* node, const char* buffer, size_t size) { + assert(IsValidNode(node)); + BA_PRECONDITION(size > 0 && size < 10000); + WriteCommandInt64_2(SessionCommand::kNodeMessage, node->stream_id(), + static_cast_check_fit(size)); + WriteChars(size, buffer); + EndCommand(); +} + +void GameStream::SetNodeAttr(const NodeAttribute& attr, float val) { + assert(IsValidNode(attr.node)); + WriteCommandInt64_2(SessionCommand::kSetNodeAttrFloat, attr.node->stream_id(), + attr.index()); + WriteFloat(val); + EndCommand(); +} + +void GameStream::SetNodeAttr(const NodeAttribute& attr, int64_t val) { + assert(IsValidNode(attr.node)); + WriteCommandInt64_3(SessionCommand::kSetNodeAttrInt32, attr.node->stream_id(), + attr.index(), val); + EndCommand(); +} + +void GameStream::SetNodeAttr(const NodeAttribute& attr, bool val) { + assert(IsValidNode(attr.node)); + WriteCommandInt64_3(SessionCommand::kSetNodeAttrBool, attr.node->stream_id(), + attr.index(), val); + EndCommand(); +} + +void GameStream::SetNodeAttr(const NodeAttribute& attr, + const std::vector& vals) { + assert(IsValidNode(attr.node)); + size_t count{vals.size()}; + WriteCommandInt64_3(SessionCommand::kSetNodeAttrFloats, + attr.node->stream_id(), attr.index(), + static_cast_check_fit(count)); + if (count > 0) { + WriteFloats(count, vals.data()); + } + EndCommand(); +} + +void GameStream::SetNodeAttr(const NodeAttribute& attr, + const std::vector& vals) { + assert(IsValidNode(attr.node)); + size_t count{vals.size()}; + WriteCommandInt64_3(SessionCommand::kSetNodeAttrInt32s, + attr.node->stream_id(), attr.index(), + static_cast_check_fit(count)); + if (count > 0) { + WriteInts64(count, vals.data()); + } + EndCommand(); +} + +void GameStream::SetNodeAttr(const NodeAttribute& attr, + const std::string& val) { + assert(IsValidNode(attr.node)); + WriteCommandInt64_2(SessionCommand::kSetNodeAttrString, + attr.node->stream_id(), attr.index()); + WriteString(val); + EndCommand(); +} + +void GameStream::SetNodeAttr(const NodeAttribute& attr, Node* val) { + assert(IsValidNode(attr.node)); + if (val) { + assert(IsValidNode(val)); + if (attr.node->scene() != val->scene()) { + throw Exception("nodes are from different scenes"); + } + WriteCommandInt64_3(SessionCommand::kSetNodeAttrNode, + attr.node->stream_id(), attr.index(), val->stream_id()); + } else { + WriteCommandInt64_2(SessionCommand::kSetNodeAttrNodeNull, + attr.node->stream_id(), attr.index()); + } + EndCommand(); +} + +void GameStream::SetNodeAttr(const NodeAttribute& attr, + const std::vector& vals) { + assert(IsValidNode(attr.node)); + if (g_buildconfig.debug_build()) { + for (auto val : vals) { + assert(IsValidNode(val)); + } + } + size_t count{vals.size()}; + std::vector vals_out; + if (count > 0) { + vals_out.resize(count); + Scene* scene = attr.node->scene(); + for (size_t i = 0; i < count; i++) { + if (vals[i]->scene() != scene) { + throw Exception("nodes are from different scenes"); + } + vals_out[i] = static_cast_check_fit(vals[i]->stream_id()); + } + } + WriteCommandInt64_3(SessionCommand::kSetNodeAttrNodes, attr.node->stream_id(), + attr.index(), static_cast_check_fit(count)); + if (count > 0) { + WriteInts32(count, vals_out.data()); + } + EndCommand(); +} + +void GameStream::SetNodeAttr(const NodeAttribute& attr, Player* val) { + // cout << "SET PLAYER ATTR " << attr.getIndex() << endl; +} + +void GameStream::SetNodeAttr(const NodeAttribute& attr, + const std::vector& vals) { + assert(IsValidNode(attr.node)); + if (g_buildconfig.debug_build()) { + for (auto val : vals) { + assert(IsValidMaterial(val)); + } + } + size_t count = vals.size(); + std::vector vals_out; + if (count > 0) { + vals_out.resize(count); + Scene* scene = attr.node->scene(); + for (size_t i = 0; i < count; i++) { + if (vals[i]->scene() != scene) { + throw Exception("material/node are from different scenes"); + } + vals_out[i] = static_cast_check_fit(vals[i]->stream_id()); + } + } + WriteCommandInt64_3(SessionCommand::kSetNodeAttrMaterials, + attr.node->stream_id(), attr.index(), + static_cast_check_fit(count)); + if (count > 0) { + WriteInts32(count, &(vals_out[0])); + } + EndCommand(); +} + +void GameStream::SetNodeAttr(const NodeAttribute& attr, Texture* val) { + if (val) { + assert(IsValidNode(attr.node)); + assert(IsValidTexture(val)); + if (attr.node->scene() != val->scene()) { + throw Exception("texture/node are from different scenes"); + } + WriteCommandInt64_3(SessionCommand::kSetNodeAttrTexture, + attr.node->stream_id(), attr.index(), val->stream_id()); + } else { + WriteCommandInt64_2(SessionCommand::kSetNodeAttrTextureNull, + attr.node->stream_id(), attr.index()); + } + EndCommand(); +} + +void GameStream::SetNodeAttr(const NodeAttribute& attr, + const std::vector& vals) { + assert(IsValidNode(attr.node)); + if (g_buildconfig.debug_build()) { + for (auto val : vals) { + assert(IsValidTexture(val)); + } + } + size_t count{vals.size()}; + std::vector vals_out; + if (count > 0) { + vals_out.resize(count); + Scene* scene{attr.node->scene()}; + for (size_t i = 0; i < count; i++) { + if (vals[i]->scene() != scene) { + throw Exception("texture/node are from different scenes"); + } + vals_out[i] = static_cast_check_fit(vals[i]->stream_id()); + } + } + WriteCommandInt64_3(SessionCommand::kSetNodeAttrTextures, + attr.node->stream_id(), attr.index(), + static_cast_check_fit(count)); + if (count > 0) { + WriteInts32(count, vals_out.data()); + } + EndCommand(); +} + +void GameStream::SetNodeAttr(const NodeAttribute& attr, Sound* val) { + if (val) { + assert(IsValidNode(attr.node)); + assert(IsValidSound(val)); + if (attr.node->scene() != val->scene()) { + throw Exception("sound/node are from different scenes"); + } + WriteCommandInt64_3(SessionCommand::kSetNodeAttrSound, + attr.node->stream_id(), attr.index(), val->stream_id()); + } else { + WriteCommandInt64_2(SessionCommand::kSetNodeAttrSoundNull, + attr.node->stream_id(), attr.index()); + } + EndCommand(); +} + +void GameStream::SetNodeAttr(const NodeAttribute& attr, + const std::vector& vals) { + assert(IsValidNode(attr.node)); + if (g_buildconfig.debug_build()) { + for (auto val : vals) { + assert(IsValidSound(val)); + } + } + size_t count{vals.size()}; + std::vector vals_out; + if (count > 0) { + vals_out.resize(count); + Scene* scene = attr.node->scene(); + for (size_t i = 0; i < count; i++) { + if (vals[i]->scene() != scene) { + throw Exception("sound/node are from different scenes"); + } + vals_out[i] = static_cast_check_fit(vals[i]->stream_id()); + } + } + WriteCommandInt64_3(SessionCommand::kSetNodeAttrSounds, + attr.node->stream_id(), attr.index(), + static_cast_check_fit(count)); + if (count > 0) { + WriteInts32(count, &(vals_out[0])); + } + EndCommand(); +} + +void GameStream::SetNodeAttr(const NodeAttribute& attr, Model* val) { + if (val) { + assert(IsValidNode(attr.node)); + assert(IsValidModel(val)); + if (attr.node->scene() != val->scene()) { + throw Exception("model/node are from different scenes"); + } + WriteCommandInt64_3(SessionCommand::kSetNodeAttrModel, + attr.node->stream_id(), attr.index(), val->stream_id()); + } else { + WriteCommandInt64_2(SessionCommand::kSetNodeAttrModelNull, + attr.node->stream_id(), attr.index()); + } + EndCommand(); +} + +void GameStream::SetNodeAttr(const NodeAttribute& attr, + const std::vector& vals) { + assert(IsValidNode(attr.node)); + if (g_buildconfig.debug_build()) { + for (auto val : vals) { + assert(IsValidModel(val)); + } + } + size_t count = vals.size(); + std::vector vals_out; + if (count > 0) { + vals_out.resize(count); + Scene* scene = attr.node->scene(); + for (size_t i = 0; i < count; i++) { + if (vals[i]->scene() != scene) { + throw Exception("model/node are from different scenes"); + } + vals_out[i] = static_cast_check_fit(vals[i]->stream_id()); + } + } + WriteCommandInt64_3(SessionCommand::kSetNodeAttrModels, + attr.node->stream_id(), attr.index(), + static_cast_check_fit(count)); + if (count > 0) { + WriteInts32(count, &(vals_out[0])); + } + EndCommand(); +} +void GameStream::SetNodeAttr(const NodeAttribute& attr, CollideModel* val) { + if (val) { + assert(IsValidNode(attr.node)); + assert(IsValidCollideModel(val)); + if (attr.node->scene() != val->scene()) { + throw Exception("collide_model/node are from different scenes"); + } + WriteCommandInt64_3(SessionCommand::kSetNodeAttrCollideModel, + attr.node->stream_id(), attr.index(), val->stream_id()); + } else { + WriteCommandInt64_2(SessionCommand::kSetNodeAttrCollideModelNull, + attr.node->stream_id(), attr.index()); + } + EndCommand(); +} +void GameStream::SetNodeAttr(const NodeAttribute& attr, + const std::vector& vals) { + assert(IsValidNode(attr.node)); + if (g_buildconfig.debug_build()) { + for (auto val : vals) { + assert(IsValidCollideModel(val)); + } + } + size_t count = vals.size(); + std::vector vals_out; + if (count > 0) { + vals_out.resize(count); + Scene* scene = attr.node->scene(); + for (size_t i = 0; i < count; i++) { + if (vals[i]->scene() != scene) { + throw Exception("collide_model/node are from different scenes"); + } + vals_out[i] = static_cast_check_fit(vals[i]->stream_id()); + } + } + WriteCommandInt64_3(SessionCommand::kSetNodeAttrCollideModels, + attr.node->stream_id(), attr.index(), + static_cast_check_fit(count)); + if (count > 0) { + WriteInts32(count, &(vals_out[0])); + } + EndCommand(); +} + +void GameStream::PlaySoundAtPosition(Sound* sound, float volume, float x, + float y, float z) { + assert(IsValidSound(sound)); + assert(IsValidScene(sound->scene())); + + // FIXME: We shouldn't need to be passing all these as full floats. :-( + WriteCommandInt64(SessionCommand::kPlaySoundAtPosition, sound->stream_id()); + WriteFloat(volume); + WriteFloat(x); + WriteFloat(y); + WriteFloat(z); + EndCommand(); +} + +void GameStream::EmitBGDynamics(const BGDynamicsEmission& e) { + WriteCommandInt64_4(SessionCommand::kEmitBGDynamics, + static_cast(e.emit_type), e.count, + static_cast(e.chunk_type), + static_cast(e.tendril_type)); + float fvals[8]; + fvals[0] = e.position.x; + fvals[1] = e.position.y; + fvals[2] = e.position.z; + fvals[3] = e.velocity.x; + fvals[4] = e.velocity.y; + fvals[5] = e.velocity.z; + fvals[6] = e.scale; + fvals[7] = e.spread; + WriteFloats(8, fvals); + EndCommand(); +} + +void GameStream::PlaySound(Sound* sound, float volume) { + assert(IsValidSound(sound)); + assert(IsValidScene(sound->scene())); + + // FIXME: We shouldn't need to be passing all these as full floats. :-( + WriteCommandInt64(SessionCommand::kPlaySound, sound->stream_id()); + WriteFloat(volume); + EndCommand(); +} + +void GameStream::ScreenMessageTop(const std::string& val, float r, float g, + float b, Texture* texture, + Texture* tint_texture, float tint_r, + float tint_g, float tint_b, float tint2_r, + float tint2_g, float tint2_b) { + assert(IsValidTexture(texture)); + assert(IsValidTexture(tint_texture)); + assert(IsValidScene(texture->scene())); + assert(IsValidScene(tint_texture->scene())); + WriteCommandInt64_2(SessionCommand::kScreenMessageTop, texture->stream_id(), + tint_texture->stream_id()); + WriteString(val); + float f[9]; + f[0] = r; + f[1] = g; + f[2] = b; + f[3] = tint_r; + f[4] = tint_g; + f[5] = tint_b; + f[6] = tint2_r; + f[7] = tint2_g; + f[8] = tint2_b; + WriteFloats(9, f); + EndCommand(); +} + +void GameStream::ScreenMessageBottom(const std::string& val, float r, float g, + float b) { + WriteCommand(SessionCommand::kScreenMessageBottom); + WriteString(val); + float color[3]; + color[0] = r; + color[1] = g; + color[2] = b; + WriteFloats(3, color); + EndCommand(); +} + +auto GameStream::GetSoundID(Sound* s) -> int64_t { + assert(IsValidSound(s)); + return s->stream_id(); +} + +auto GameStream::GetMaterialID(Material* m) -> int64_t { + assert(IsValidMaterial(m)); + return m->stream_id(); +} + +void GameStream::OnClientConnected(ConnectionToClient* c) { + // Sanity check - abort if its on either of our lists already. + for (auto& connections_to_client : connections_to_clients_) { + if (connections_to_client == c) { + Log("Error: GameStream::OnClientConnected() got duplicate connection."); + return; + } + } + for (auto& i : connections_to_clients_ignored_) { + if (i == c) { + Log("Error: GameStream::OnClientConnected() got duplicate connection."); + return; + } + } + + { + // First thing, we need to flush all pending session-commands to clients. + // The host-session's current state is the result of having already run + // these commands locally, so if we leave them on the list while 'restoring' + // the new client to our state they'll get essentially double-applied, which + // is bad. (ie: a delete-node command will get called but the node will + // already be gone) + Flush(); + + connections_to_clients_.push_back(c); + + // We create a temporary output stream just for the purpose of building + // a giant session-commands message to reconstruct everything in our + // host-session in its current form. + GameStream out(nullptr, false); + + // Ask the host-session that we came from to dump it's complete state. + host_session_->DumpFullState(&out); + + // Grab the message that's been built up. + // If its not empty, send it to the client. + std::vector out_message = out.GetOutMessage(); + if (!out_message.empty()) { + c->SendReliableMessage(out_message); + } + + // Also send a correction packet to sync up all our dynamics. + // (technically could do this *just* for the new client) + SendPhysicsCorrection(false); + } +} + +void GameStream::OnClientDisconnected(ConnectionToClient* c) { + // Search for it on either our ignored or regular lists. + for (auto i = connections_to_clients_.begin(); + i != connections_to_clients_.end(); i++) { + if (*i == c) { + connections_to_clients_.erase(i); + return; + } + } + for (auto i = connections_to_clients_ignored_.begin(); + i != connections_to_clients_ignored_.end(); i++) { + if (*i == c) { + connections_to_clients_ignored_.erase(i); + return; + } + } + Log("Error: GameStream::OnClientDisconnected() called for connection not on " + "lists"); +} + +} // namespace ballistica diff --git a/src/ballistica/game/session/client_session.cc b/src/ballistica/game/session/client_session.cc new file mode 100644 index 00000000..138d8443 --- /dev/null +++ b/src/ballistica/game/session/client_session.cc @@ -0,0 +1,1122 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/game/session/client_session.h" + +#include "ballistica/app/app_globals.h" +#include "ballistica/audio/audio.h" +#include "ballistica/dynamics/bg/bg_dynamics.h" +#include "ballistica/dynamics/material/material.h" +#include "ballistica/dynamics/material/material_action.h" +#include "ballistica/dynamics/material/material_component.h" +#include "ballistica/dynamics/material/material_condition_node.h" +#include "ballistica/dynamics/rigid_body.h" +#include "ballistica/game/game_stream.h" +#include "ballistica/graphics/graphics.h" +#include "ballistica/media/component/collide_model.h" +#include "ballistica/media/component/model.h" +#include "ballistica/media/component/sound.h" +#include "ballistica/media/component/texture.h" +#include "ballistica/networking/networking.h" +#include "ballistica/python/python.h" +#include "ballistica/scene/node/node_attribute.h" +#include "ballistica/scene/node/node_type.h" +#include "ballistica/scene/scene.h" + +namespace ballistica { + +ClientSession::ClientSession() { ClearSessionObjs(); } + +void ClientSession::Reset(bool rewind) { + assert(!shutting_down_); + OnReset(rewind); +} + +void ClientSession::OnReset(bool rewind) { + ClearSessionObjs(); + target_base_time_ = 0.0; + base_time_ = 0; +} + +void ClientSession::ClearSessionObjs() { + scenes_.clear(); + nodes_.clear(); + textures_.clear(); + models_.clear(); + sounds_.clear(); + collide_models_.clear(); + materials_.clear(); + commands_pending_.clear(); + commands_.clear(); + base_time_buffered_ = 0; +} + +auto ClientSession::DoesFillScreen() const -> bool { + // Look for any scene that has something that covers the background. + // NOLINTNEXTLINE(readability-use-anyofallof) + for (const auto& scene : scenes_) { + if ((scene.exists()) && (*scene).has_bg_cover()) { + return true; + } + } + return false; +} + +void ClientSession::Draw(FrameDef* f) { + // Just go through and draw all of our scenes. + for (auto&& i : scenes_) { + // NOTE - here we draw scenes in the order they were created, but + // in a host-session we draw session first followed by activities + // (that should be the same order in both cases, but just something to keep + // in mind...) + if (i.exists()) { + i->Draw(f); + } + } +} + +auto ClientSession::ReadByte() -> uint8_t { + if (current_cmd_ptr_ > &(current_cmd_[0]) + current_cmd_.size() - 1) { + throw Exception("state read error"); + } + return *(current_cmd_ptr_++); +} + +auto ClientSession::ReadInt32() -> int32_t { + if (current_cmd_ptr_ > &(current_cmd_[0]) + current_cmd_.size() - 4) { + throw Exception("state read error"); + } + int32_t val; + memcpy(&val, current_cmd_ptr_, sizeof(val)); + current_cmd_ptr_ += 4; + return val; +} + +auto ClientSession::ReadFloat() -> float { + if (current_cmd_ptr_ > &(current_cmd_[0]) + current_cmd_.size() - 4) { + throw Exception("state read error"); + } + float val; + memcpy(&val, current_cmd_ptr_, 4); + current_cmd_ptr_ += 4; + return val; +} + +void ClientSession::ReadFloats(int count, float* vals) { + int size = 4 * count; + if (current_cmd_ptr_ > &(current_cmd_[0]) + current_cmd_.size() - size) { + throw Exception("state read error"); + } + memcpy(vals, current_cmd_ptr_, static_cast(size)); + current_cmd_ptr_ += size; +} + +void ClientSession::ReadInt32s(int count, int32_t* vals) { + int size = 4 * count; + if (current_cmd_ptr_ > &(current_cmd_[0]) + current_cmd_.size() - size) { + throw Exception("state read error"); + } + memcpy(vals, current_cmd_ptr_, static_cast(size)); + current_cmd_ptr_ += size; +} + +void ClientSession::ReadChars(int count, char* vals) { + int size = count; + if (current_cmd_ptr_ > &(current_cmd_[0]) + current_cmd_.size() - size) { + throw Exception("state read error"); + } + memcpy(vals, current_cmd_ptr_, static_cast(size)); + current_cmd_ptr_ += size; +} + +void ClientSession::ReadInt32_3(int32_t* vals) { + size_t size = 3 * 4; + if (current_cmd_ptr_ > &(current_cmd_[0]) + current_cmd_.size() - size) { + throw Exception("state read error"); + } + memcpy(vals, current_cmd_ptr_, size); + current_cmd_ptr_ += size; +} + +void ClientSession::ReadInt32_4(int32_t* vals) { + size_t size = 4 * 4; + if (current_cmd_ptr_ > &(current_cmd_[0]) + current_cmd_.size() - size) { + throw Exception("state read error"); + } + memcpy(vals, current_cmd_ptr_, size); + current_cmd_ptr_ += size; +} + +void ClientSession::ReadInt32_2(int32_t* vals) { + size_t size = 2 * 4; + if (current_cmd_ptr_ > &(current_cmd_[0]) + current_cmd_.size() - size) { + throw Exception("state read error"); + } + memcpy(vals, current_cmd_ptr_, size); + current_cmd_ptr_ += size; +} + +auto ClientSession::ReadString() -> std::string { + if (current_cmd_ptr_ > &(current_cmd_[0]) + current_cmd_.size() - 4) { + throw Exception("state read error"); + } + int32_t size; + memcpy(&size, current_cmd_ptr_, sizeof(size)); + current_cmd_ptr_ += 4; + std::vector buffer(static_cast(size + 1)); + if (current_cmd_ptr_ > &(current_cmd_[0]) + current_cmd_.size() - size) { + throw Exception("state read error"); + } + memcpy(&(buffer[0]), current_cmd_ptr_, static_cast(size)); + current_cmd_ptr_ += size; + return &(buffer[0]); +} + +void ClientSession::Update(int time_advance) { + if (shutting_down_) { + return; + } + + // Allow replays to modulate speed, etc. + // QUESTION: can we just use consume_rate_ for this? + time_advance = GetActualTimeAdvance(time_advance); + + target_base_time_ += static_cast(time_advance) * consume_rate_; + + try { + // Read and run all events up to our target time. + while (base_time_ < target_base_time_) { + // If we need to do something explicit to keep messages flowing in. + // (informing the replay thread to feed us more, etc.). + FetchMessages(); + + // If we've got another command on the list, pull it and run it. + if (!commands_.empty()) { + // Debugging: if this was previously pointed at a buffer, make sure we + // went exactly to the end. + if (g_buildconfig.debug_build()) { + if (current_cmd_ptr_ != nullptr) { + if (current_cmd_ptr_ != &(current_cmd_[0]) + current_cmd_.size()) { + Log("SIZE ERROR FOR CMD " + + std::to_string(static_cast(current_cmd_[0])) + + " expected " + std::to_string(current_cmd_.size()) + " got " + + std::to_string(current_cmd_ptr_ - &(current_cmd_[0]))); + } + } + assert(current_cmd_ptr_ == current_cmd_.data() + current_cmd_.size()); + } + current_cmd_ = commands_.front(); + commands_.pop_front(); + current_cmd_ptr_ = &(current_cmd_[0]); + } else { + // Let the subclass know this happened. Replays may want to pause + // playback until more data comes in but things like net-play may want + // to just soldier on and skip ahead once data comes in. + OnCommandBufferUnderrun(); + return; + } + + auto cmd = static_cast(ReadByte()); + + switch (cmd) { + case SessionCommand::kBaseTimeStep: { + int32_t stepsize = ReadInt32(); + BA_PRECONDITION(stepsize > 0); + if (stepsize > 10000) { + throw Exception( + "got abnormally large stepsize; probably a corrupt stream"); + } + base_time_buffered_ -= stepsize; + BA_PRECONDITION(base_time_buffered_ >= 0); + base_time_ += stepsize; + break; + } + case SessionCommand::kDynamicsCorrection: { + bool blend = current_cmd_[1]; + uint32_t offset = 2; + uint16_t node_count; + memcpy(&node_count, current_cmd_.data() + offset, sizeof(node_count)); + offset += 2; + for (int i = 0; i < node_count; i++) { + uint32_t node_id; + memcpy(&node_id, current_cmd_.data() + offset, sizeof(node_id)); + offset += 4; + int body_count = current_cmd_[offset++]; + Node* n = + (node_id < nodes_.size()) ? nodes_[node_id].get() : nullptr; + for (int j = 0; j < body_count; j++) { + int bodyid = current_cmd_[offset++]; + uint16_t body_data_len; + memcpy(&body_data_len, current_cmd_.data() + offset, + sizeof(body_data_len)); + RigidBody* b = n ? n->GetRigidBody(bodyid) : nullptr; + offset += 2; + const char* p1 = reinterpret_cast(&(current_cmd_[offset])); + const char* p2 = p1; + if (b) { + dBodyID body = b->body(); + const dReal* p = dBodyGetPosition(body); + float old_x = p[0]; + float old_y = p[1]; + float old_z = p[2]; + b->ExtractFull(&p2); + if (p2 - p1 != body_data_len) + throw Exception("Invalid rbd correction data"); + if (blend) { + b->AddBlendOffset(old_x - p[0], old_y - p[1], old_z - p[2]); + } + } + offset += body_data_len; + if (offset > current_cmd_.size()) { + throw Exception("Invalid rbd correction data"); + } + } + if (offset > current_cmd_.size()) + throw Exception("Invalid rbd correction data"); + + // Extract custom per-node data. + uint16_t custom_data_len; + memcpy(&custom_data_len, current_cmd_.data() + offset, + sizeof(custom_data_len)); + offset += 2; + if (custom_data_len != 0) { + std::vector data(custom_data_len); + memcpy(&(data[0]), &(current_cmd_[offset]), custom_data_len); + if (n) n->ApplyResyncData(data); + offset += custom_data_len; + } + if (offset > current_cmd_.size()) { + throw Exception("Invalid rbd correction data"); + } + } + if (offset != current_cmd_.size()) { + throw Exception("invalid rbd correction data"); + } + current_cmd_ptr_ = &(current_cmd_[0]) + offset; + + break; + } + case SessionCommand::kEndOfFile: { + // EOF can happen anytime if they run out of disk space/etc. + // We should expect any state. + Reset(true); + break; + } + case SessionCommand::kAddSceneGraph: { + int32_t cmdvals[2]; + ReadInt32_2(cmdvals); + int32_t id = cmdvals[0]; + millisecs_t starttime = cmdvals[1]; + if (id < 0 || id > 100) { + throw Exception("invalid scene id"); + } + if (static_cast(scenes_.size()) < (id + 1)) { + scenes_.resize(static_cast(id) + 1); + } + assert(!scenes_[id].exists()); + scenes_[id] = Object::New(starttime); + scenes_[id]->set_stream_id(id); + break; + } + case SessionCommand::kRemoveSceneGraph: { + int32_t id = ReadInt32(); + GetScene(id); // Make sure it's valid. + scenes_[id].Clear(); + break; + } + case SessionCommand::kStepSceneGraph: { + int32_t val = ReadInt32(); + Scene* sg = GetScene(val); + sg->Step(); + break; + } + case SessionCommand::kAddNode: { + int32_t vals[3]; // scene-id, nodetype-id, node-id + ReadInt32_3(vals); + Scene* scene = GetScene(vals[0]); + assert(g_app_globals != nullptr); + if (vals[1] < 0 + || vals[1] >= static_cast( + g_app_globals->node_types_by_id.size())) { + throw Exception("invalid node type id"); + } + + NodeType* node_type = g_app_globals->node_types_by_id[vals[1]]; + + // Fail if we get a ridiculous number of nodes. + // FIXME: should enforce this on the server side too. + int id = vals[2]; + if (id < 0 || id > 10000) { + throw Exception("invalid node id"); + } + if (static_cast(nodes_.size()) < (id + 1)) { + nodes_.resize(static_cast(id) + 1); + } + assert(!nodes_[id].exists()); + { + ScopedSetContext _cp(this); + nodes_[id] = scene->NewNode(node_type->name(), "", nullptr); + nodes_[id]->set_stream_id(id); + } + break; + } + case SessionCommand::kSetForegroundSceneGraph: { + Scene* scene = GetScene(ReadInt32()); + g_game->SetForegroundScene(scene); + break; + } + case SessionCommand::kNodeMessage: { + int32_t vals[2]; + ReadInt32_2(vals); + Node* n = GetNode(vals[0]); + int32_t msg_size = vals[1]; + if (msg_size < 1 || msg_size > 10000) { + throw Exception("invalid message"); + } + std::vector buffer(static_cast(msg_size)); + ReadChars(msg_size, &buffer[0]); + n->DispatchNodeMessage(&buffer[0]); + break; + } + case SessionCommand::kConnectNodeAttribute: { + int32_t vals[4]; + ReadInt32_4(vals); + Node* src_node = GetNode(vals[0]); + Node* dst_node = GetNode(vals[2]); + NodeAttributeUnbound* src_attr = + src_node->type()->GetAttribute(static_cast(vals[1])); + NodeAttributeUnbound* dst_attr = + dst_node->type()->GetAttribute(static_cast(vals[3])); + src_node->ConnectAttribute(src_attr, dst_node, dst_attr); + break; + } + case SessionCommand::kNodeOnCreate: { + Node* n = GetNode(ReadInt32()); + n->OnCreate(); + break; + } + case SessionCommand::kAddMaterial: { + int32_t vals[2]; // scene-id, material-id + ReadInt32_2(vals); + Scene* scene = GetScene(vals[0]); + // Fail if we get a ridiculous number of materials. + // FIXME: should enforce this on the server side too. + int id = vals[1]; + if (vals[1] < 0 || vals[1] >= 1000) { + throw Exception("invalid material id"); + } + if (static_cast(materials_.size()) < (id + 1)) { + materials_.resize(static_cast(id) + 1); + } + assert(!materials_[id].exists()); + materials_[id] = Object::New("", scene); + materials_[id]->stream_id_ = id; + break; + } + case SessionCommand::kRemoveMaterial: { + int id = ReadInt32(); + GetMaterial(id); // make sure its valid + materials_[id].Clear(); + break; + } + case SessionCommand::kAddMaterialComponent: { + int32_t cmdvals[2]; + ReadInt32_2(cmdvals); + Material* m = GetMaterial(cmdvals[0]); + int component_size = cmdvals[1]; + if (component_size < 1 || component_size > 10000) { + throw Exception("invalid component"); + } + std::vector buffer(static_cast(component_size)); + ReadChars(component_size, &buffer[0]); + auto c(Object::New()); + const char* ptr1 = &buffer[0]; + const char* ptr2 = ptr1; + c->Restore(&ptr2, this); + BA_PRECONDITION(ptr2 - ptr1 == component_size); + m->AddComponent(c); + break; + } + case SessionCommand::kAddTexture: { + int32_t vals[2]; // scene-id, texture-id + ReadInt32_2(vals); + std::string name = ReadString(); + Scene* scene = GetScene(vals[0]); + // Fail if we get a ridiculous number of textures. + // FIXME: Should enforce this on the server side too. + int id = vals[1]; + if (vals[1] < 0 || vals[1] >= 1000) { + throw Exception("invalid texture id"); + } + if (static_cast(textures_.size()) < (id + 1)) { + textures_.resize(static_cast(id) + 1); + } + assert(!textures_[id].exists()); + textures_[id] = Object::New(name, scene); + textures_[id]->stream_id_ = id; + break; + } + case SessionCommand::kRemoveTexture: { + int id = ReadInt32(); + GetTexture(id); // make sure its valid + textures_[id].Clear(); + break; + } + case SessionCommand::kAddModel: { + int32_t vals[2]; // scene-id, model-id + ReadInt32_2(vals); + std::string name = ReadString(); + Scene* scene = GetScene(vals[0]); + + // Fail if we get a ridiculous number of models. + // FIXME: Should enforce this on the server side too. + int id = vals[1]; + if (vals[1] < 0 || vals[1] >= 1000) { + throw Exception("invalid model id"); + } + if (static_cast(models_.size()) < (id + 1)) { + models_.resize(static_cast(id) + 1); + } + assert(!models_[id].exists()); + models_[id] = Object::New(name, scene); + models_[id]->stream_id_ = id; + break; + } + case SessionCommand::kRemoveModel: { + int id = ReadInt32(); + GetModel(id); // make sure its valid + models_[id].Clear(); + break; + } + case SessionCommand::kAddSound: { + int32_t vals[2]; // scene-id, sound-id + ReadInt32_2(vals); + std::string name = ReadString(); + Scene* scene = GetScene(vals[0]); + // Fail if we get a ridiculous number of sounds. + // FIXME: Should enforce this on the server side too. + int id = vals[1]; + if (vals[1] < 0 || vals[1] >= 1000) { + throw Exception("invalid sound id"); + } + if (static_cast(sounds_.size()) < (id + 1)) { + sounds_.resize(static_cast(id) + 1); + } + assert(!sounds_[id].exists()); + sounds_[id] = Object::New(name, scene); + sounds_[id]->stream_id_ = id; + break; + } + case SessionCommand::kRemoveSound: { + int id = ReadInt32(); + GetSound(id); // Make sure its valid. + sounds_[id].Clear(); + break; + } + case SessionCommand::kAddCollideModel: { + int32_t vals[2]; // scene-id, collide_model-id + ReadInt32_2(vals); + std::string name = ReadString(); + Scene* scene = GetScene(vals[0]); + + // Fail if we get a ridiculous number of collide_models. + // FIXME: Should enforce this on the server side too. + int id = vals[1]; + if (vals[1] < 0 || vals[1] >= 1000) { + throw Exception("invalid collide_model id"); + } + if (static_cast(collide_models_.size()) < (id + 1)) { + collide_models_.resize(static_cast(id) + 1); + } + assert(!collide_models_[id].exists()); + collide_models_[id] = Object::New(name, scene); + collide_models_[id]->stream_id_ = id; + break; + } + case SessionCommand::kRemoveCollideModel: { + int id = ReadInt32(); + GetCollideModel(id); // make sure its valid + collide_models_[id].Clear(); + break; + } + case SessionCommand::kRemoveNode: { + int id = ReadInt32(); + Node* n = GetNode(id); + n->scene()->DeleteNode(n); + assert(!nodes_[id].exists()); + break; + } + case SessionCommand::kSetNodeAttrFloat: { + int vals[2]; + ReadInt32_2(vals); + GetNode(vals[0])->GetAttribute(vals[1]).Set(ReadFloat()); + break; + } + case SessionCommand::kSetNodeAttrInt32: { + int32_t vals[3]; + ReadInt32_3(vals); + + // Note; we currently deal in 64 bit ints locally but read/write 32 + // bit over the wire. + GetNode(vals[0])->GetAttribute(vals[1]).Set( + static_cast(vals[2])); + break; + } + case SessionCommand::kSetNodeAttrBool: { + int vals[3]; + ReadInt32_3(vals); + GetNode(vals[0])->GetAttribute(vals[1]).Set( + static_cast(vals[2])); + break; + } + case SessionCommand::kSetNodeAttrFloats: { + int cmdvals[3]; + ReadInt32_3(cmdvals); + int count = cmdvals[2]; + if (count < 0 || count > 1000) { + throw Exception("invalid array size (" + std::to_string(count) + + ")"); + } + std::vector vals(static_cast(count)); + if (count > 0) { + ReadFloats(count, &(vals[0])); + } + GetNode(cmdvals[0])->GetAttribute(cmdvals[1]).Set(vals); + break; + } + case SessionCommand::kSetNodeAttrInt32s: { + int cmdvals[3]; + ReadInt32_3(cmdvals); + int count = cmdvals[2]; + if (count < 0 || count > 1000) { + throw Exception("invalid array size (" + std::to_string(count) + + ")"); + } + std::vector vals(static_cast(count)); + if (count > 0) { + ReadInt32s(count, &(vals[0])); + } + + // Note: we currently deal in 64 bit ints locally but read/write 32 + // bit over the wire. Convert. + std::vector vals64(static_cast(count)); + for (int i = 0; i < count; i++) { + vals64[i] = vals[i]; + } + GetNode(cmdvals[0])->GetAttribute(cmdvals[1]).Set(vals64); + break; + } + case SessionCommand::kSetNodeAttrString: { + int vals[2]; + ReadInt32_2(vals); + GetNode(vals[0])->GetAttribute(vals[1]).Set(ReadString()); + break; + } + case SessionCommand::kSetNodeAttrNode: { + int vals[3]; + ReadInt32_3(vals); + GetNode(vals[0])->GetAttribute(vals[1]).Set(GetNode(vals[2])); + break; + } + case SessionCommand::kSetNodeAttrNodeNull: { + int cmdvals[2]; + ReadInt32_2(cmdvals); + Node* val = nullptr; + GetNode(cmdvals[0])->GetAttribute(cmdvals[1]).Set(val); + break; + } + case SessionCommand::kSetNodeAttrTextureNull: { + int cmdvals[2]; + ReadInt32_2(cmdvals); + Texture* val = nullptr; + GetNode(cmdvals[0])->GetAttribute(cmdvals[1]).Set(val); + break; + } + case SessionCommand::kSetNodeAttrSoundNull: { + int cmdvals[2]; + ReadInt32_2(cmdvals); + Sound* val = nullptr; + GetNode(cmdvals[0])->GetAttribute(cmdvals[1]).Set(val); + break; + } + case SessionCommand::kSetNodeAttrModelNull: { + int cmdvals[2]; + ReadInt32_2(cmdvals); + Model* val = nullptr; + GetNode(cmdvals[0])->GetAttribute(cmdvals[1]).Set(val); + break; + } + case SessionCommand::kSetNodeAttrCollideModelNull: { + int cmdvals[2]; + ReadInt32_2(cmdvals); + CollideModel* val = nullptr; + GetNode(cmdvals[0])->GetAttribute(cmdvals[1]).Set(val); + break; + } + case SessionCommand::kSetNodeAttrNodes: { + int cmdvals[3]; + ReadInt32_3(cmdvals); + int count = cmdvals[2]; + if (count < 0 || count > 1000) { + throw Exception("invalid array size (" + std::to_string(count) + + ")"); + } + std::vector vals_in(static_cast(count)); + std::vector vals(static_cast(count)); + if (count > 0) { + ReadInt32s(count, &(vals_in[0])); + } + for (int i = 0; i < count; i++) { + vals[i] = GetNode(vals_in[i]); + } + GetNode(cmdvals[0])->GetAttribute(cmdvals[1]).Set(vals); + break; + } + case SessionCommand::kSetNodeAttrTexture: { + int cmdvals[3]; + ReadInt32_3(cmdvals); + Texture* val = GetTexture(cmdvals[2]); + GetNode(cmdvals[0])->GetAttribute(cmdvals[1]).Set(val); + break; + } + case SessionCommand::kSetNodeAttrTextures: { + int cmdvals[3]; + ReadInt32_3(cmdvals); + int count = cmdvals[2]; + if (count < 0 || count > 1000) { + throw Exception("invalid array size (" + std::to_string(count) + + ")"); + } + std::vector vals_in(static_cast(count)); + std::vector vals(static_cast(count)); + if (count > 0) { + ReadInt32s(count, &(vals_in[0])); + } + for (int i = 0; i < count; i++) { + vals[i] = GetTexture(vals_in[i]); + } + GetNode(cmdvals[0])->GetAttribute(cmdvals[1]).Set(vals); + break; + } + case SessionCommand::kSetNodeAttrSound: { + int cmdvals[3]; + ReadInt32_3(cmdvals); + Sound* val = GetSound(cmdvals[2]); + GetNode(cmdvals[0])->GetAttribute(cmdvals[1]).Set(val); + break; + } + case SessionCommand::kSetNodeAttrSounds: { + int cmdvals[3]; + ReadInt32_3(cmdvals); + int count = cmdvals[2]; + if (count < 0 || count > 1000) { + throw Exception("invalid array size (" + std::to_string(count) + + ")"); + } + std::vector vals_in(static_cast(count)); + std::vector vals(static_cast(count)); + if (count > 0) { + ReadInt32s(count, &(vals_in[0])); + } + for (int i = 0; i < count; i++) { + vals[i] = GetSound(vals_in[i]); + } + GetNode(cmdvals[0])->GetAttribute(cmdvals[1]).Set(vals); + break; + } + case SessionCommand::kSetNodeAttrModel: { + int cmdvals[3]; + ReadInt32_3(cmdvals); + Model* val = GetModel(cmdvals[2]); + GetNode(cmdvals[0])->GetAttribute(cmdvals[1]).Set(val); + break; + } + case SessionCommand::kSetNodeAttrModels: { + int cmdvals[3]; + ReadInt32_3(cmdvals); + int count = cmdvals[2]; + if (count < 0 || count > 1000) { + throw Exception("invalid array size (" + std::to_string(count) + + ")"); + } + std::vector vals_in(static_cast(count)); + std::vector vals(static_cast(count)); + if (count > 0) { + ReadInt32s(count, &(vals_in[0])); + } + for (int i = 0; i < count; i++) { + vals[i] = GetModel(vals_in[i]); + } + GetNode(cmdvals[0])->GetAttribute(cmdvals[1]).Set(vals); + break; + } + case SessionCommand::kSetNodeAttrCollideModel: { + int cmdvals[3]; + ReadInt32_3(cmdvals); + CollideModel* val = GetCollideModel(cmdvals[2]); + GetNode(cmdvals[0])->GetAttribute(cmdvals[1]).Set(val); + break; + } + case SessionCommand::kSetNodeAttrCollideModels: { + int cmdvals[3]; + ReadInt32_3(cmdvals); + int count = cmdvals[2]; + if (count < 0 || count > 1000) { + throw Exception("invalid array size (" + std::to_string(count) + + ")"); + } + std::vector vals_in(static_cast(count)); + std::vector vals(static_cast(count)); + if (count > 0) { + ReadInt32s(count, &(vals_in[0])); + } + for (int i = 0; i < count; i++) { + vals[i] = GetCollideModel(vals_in[i]); + } + GetNode(cmdvals[0])->GetAttribute(cmdvals[1]).Set(vals); + break; + } + case SessionCommand::kSetNodeAttrMaterials: { + int cmdvals[3]; + ReadInt32_3(cmdvals); + int count = cmdvals[2]; + if (count < 0 || count > 1000) { + throw Exception("invalid array size (" + std::to_string(count) + + ")"); + } + std::vector vals_in(static_cast(count)); + std::vector vals(static_cast(count)); + if (count > 0) { + ReadInt32s(count, &(vals_in[0])); + } + for (int i = 0; i < count; i++) { + vals[i] = GetMaterial(vals_in[i]); + } + GetNode(cmdvals[0])->GetAttribute(cmdvals[1]).Set(vals); + break; + } + case SessionCommand::kPlaySound: { + Sound* sound = GetSound(ReadInt32()); + float volume = ReadFloat(); + g_audio->PlaySound(sound->GetSoundData(), volume); + break; + } + case SessionCommand::kScreenMessageBottom: { + std::string val = ReadString(); + Vector3f color{}; + ReadFloats(3, color.v); + ScreenMessage(val, color); + break; + } + case SessionCommand::kScreenMessageTop: { + int cmdvals[2]; + ReadInt32_2(cmdvals); + Texture* texture = GetTexture(cmdvals[0]); + Texture* tint_texture = GetTexture(cmdvals[1]); + std::string s = ReadString(); + float f[9]; + ReadFloats(9, f); + g_graphics->AddScreenMessage( + s, Vector3f(f[0], f[1], f[2]), true, texture, tint_texture, + Vector3f(f[3], f[4], f[5]), Vector3f(f[6], f[7], f[8])); + break; + } + case SessionCommand::kPlaySoundAtPosition: { + Sound* sound = GetSound(ReadInt32()); + float volume = ReadFloat(); + float x = ReadFloat(); + float y = ReadFloat(); + float z = ReadFloat(); + g_audio->PlaySoundAtPosition(sound->GetSoundData(), volume, x, y, z); + break; + } + case SessionCommand::kEmitBGDynamics: { + int cmdvals[4]; + ReadInt32_4(cmdvals); + float vals[8]; + ReadFloats(8, vals); + if (g_bg_dynamics != nullptr) { + BGDynamicsEmission e; + e.emit_type = (BGDynamicsEmitType)cmdvals[0]; + e.count = cmdvals[1]; + e.chunk_type = (BGDynamicsChunkType)cmdvals[2]; + e.tendril_type = (BGDynamicsTendrilType)cmdvals[3]; + e.position.x = vals[0]; + e.position.y = vals[1]; + e.position.z = vals[2]; + e.velocity.x = vals[3]; + e.velocity.y = vals[4]; + e.velocity.z = vals[5]; + e.scale = vals[6]; + e.spread = vals[7]; + g_bg_dynamics->Emit(e); + } + break; + } + default: + throw Exception("unrecognized stream command: " + + std::to_string(static_cast(cmd))); + } + } + } catch (const std::exception& e) { + Error(e.what()); + } +} // NOLINT (yes this is too long) + +ClientSession::~ClientSession() = default; + +void ClientSession::ScreenSizeChanged() { + // Let all our scenes know. + for (auto&& i : scenes_) { + if (Scene* sg = i.get()) { + sg->ScreenSizeChanged(); + } + } +} + +void ClientSession::LanguageChanged() { + // Let all our scenes know. + for (auto&& i : scenes_) { + if (Scene* sg = i.get()) { + sg->LanguageChanged(); + } + } +} + +auto ClientSession::GetScene(int id) const -> Scene* { + if (id < 0 || id >= static_cast(scenes_.size())) { + throw Exception("Invalid scene id"); + } + Scene* sg = scenes_[id].get(); + if (!sg) { + throw Exception("Invalid scene id"); + } + return sg; +} +auto ClientSession::GetNode(int id) const -> Node* { + if (id < 0 || id >= static_cast(nodes_.size())) { + throw Exception("Invalid node (out of range)"); + } + Node* n = nodes_[id].get(); + if (!n) { + throw Exception("Invalid node id (empty slot)"); + } + return n; +} +auto ClientSession::GetMaterial(int id) const -> Material* { + if (id < 0 || id >= static_cast(materials_.size())) { + throw Exception("Invalid material (out of range)"); + } + Material* n = materials_[id].get(); + if (!n) { + throw Exception("Invalid material id (empty slot)"); + } + return n; +} +auto ClientSession::GetTexture(int id) const -> Texture* { + if (id < 0 || id >= static_cast(textures_.size())) { + throw Exception("Invalid texture (out of range)"); + } + Texture* n = textures_[id].get(); + if (!n) { + throw Exception("Invalid texture id (empty slot)"); + } + return n; +} +auto ClientSession::GetModel(int id) const -> Model* { + if (id < 0 || id >= static_cast(models_.size())) { + throw Exception("Invalid model (out of range)"); + } + Model* n = models_[id].get(); + if (!n) { + throw Exception("Invalid model id (empty slot)"); + } + return n; +} +auto ClientSession::GetSound(int id) const -> Sound* { + if (id < 0 || id >= static_cast(sounds_.size())) { + throw Exception("Invalid sound (out of range)"); + } + Sound* n = sounds_[id].get(); + if (!n) { + throw Exception("Invalid sound id (empty slot)"); + } + return n; +} +auto ClientSession::GetCollideModel(int id) const -> CollideModel* { + if (id < 0 || id >= static_cast(collide_models_.size())) { + throw Exception("Invalid collide_model (out of range)"); + } + CollideModel* n = collide_models_[id].get(); + if (!n) { + throw Exception("Invalid collide_model id (empty slot)"); + } + return n; +} + +void ClientSession::Error(const std::string& description) { + Log("ERROR: client session error: " + description); + End(); +} + +void ClientSession::End() { + if (shutting_down_) return; + shutting_down_ = true; + g_python->PushObjCall(Python::ObjID::kLaunchMainMenuSessionCall); +} + +void ClientSession::HandleSessionMessage(const std::vector& buffer) { + assert(InGameThread()); + + BA_PRECONDITION(!buffer.empty()); + + switch (buffer[0]) { + case BA_MESSAGE_SESSION_RESET: { + // Hmmm; been a while since I wrote this, but wondering why reset isn't + // just a session-command. (Do we not want it added to replay streams?...) + Reset(false); + break; + } + + case BA_MESSAGE_SESSION_COMMANDS: { + // This is simply 16 bit length followed by command up to the end of the + // packet. Break it apart and feed each command to the client session. + uint32_t offset = 1; + std::vector sub_buffer; + while (true) { + uint16_t size; + memcpy(&size, &(buffer[offset]), 2); + if (offset + size > buffer.size()) { + Error("invalid state message"); + return; + } + sub_buffer.resize(size); + memcpy(&(sub_buffer[0]), &(buffer[offset + 2]), sub_buffer.size()); + AddCommand(sub_buffer); + offset += 2 + size; // move to next command + if (offset == buffer.size()) { + // let's also use this opportunity to graph our command-buffer size + // for network debugging... if (NetGraph *graph = + // g_graphics->GetClientSessionStepBufferGraph()) { + // graph->addSample(GetRealTime(), steps_on_list_); + // } + + break; + } + } + break; + } + + case BA_MESSAGE_SESSION_DYNAMICS_CORRECTION: { + // Just drop this in the game's command-stream verbatim, except switch its + // state-ID to a command-ID. + std::vector buffer_out = buffer; + buffer_out[0] = static_cast(SessionCommand::kDynamicsCorrection); + AddCommand(buffer_out); + break; + } + + default: + throw Exception("ClientSession::HandleSessionMessage " + ObjToString(this) + + "got unrecognized message : " + + std::to_string(static_cast(buffer[0])) + + " of size " + std::to_string(buffer.size())); + break; + } +} + +// Add a single command in. +void ClientSession::AddCommand(const std::vector& command) { + // If this is a time-step command, we can dump everything we've been building + // up onto the list to be chewed through by the interpreter (we don't want to + // add things until we have the *entire* step, so we don't wind up rendering + // things halfway through some change, etc.). + commands_pending_.push_back(command); + if (!command.empty()) { + if (command[0] == static_cast(SessionCommand::kBaseTimeStep)) { + // Keep a tally of how much stepped time we've built up. + base_time_buffered_ += command[1]; + + // Let subclasses know we just received a step in case they'd like + // to factor it in for rate adjustments/etc. + OnBaseTimeStepAdded(command[1]); + + for (auto&& i : commands_pending_) { + commands_.push_back(i); + } + commands_pending_.clear(); + } + } +} + +auto ClientSession::GetForegroundContext() -> Context { return Context(this); } + +void ClientSession::GetCorrectionMessages( + bool blend, std::vector >* messages) { + std::vector message; + for (auto&& i : scenes_) { + if (Scene* sg = i.get()) { + message = sg->GetCorrectionMessage(blend); + // A correction packet of size 4 is empty; ignore it. + if (message.size() > 4) { + messages->push_back(message); + } + } + } +} + +void ClientSession::DumpFullState(GameStream* out) { + // Add all scenes. + for (auto&& i : scenes()) { + if (Scene* sg = i.get()) { + sg->Dump(out); + } + } + + // Before doing any nodes, we need to create all materials. + // (but *not* their components, which may reference the nodes that we haven't + // made yet) + for (auto&& i : materials()) { + if (Material* m = i.get()) { + out->AddMaterial(m); + } + } + + // Add all media. + for (auto&& i : textures()) { + if (Texture* t = i.get()) { + out->AddTexture(t); + } + } + for (auto&& i : models()) { + if (Model* s = i.get()) { + out->AddModel(s); + } + } + for (auto&& i : sounds()) { + if (Sound* s = i.get()) { + out->AddSound(s); + } + } + for (auto&& i : collide_models()) { + if (CollideModel* s = i.get()) { + out->AddCollideModel(s); + } + } + + // Add all scene nodes. + for (auto&& i : scenes()) { + if (Scene* sg = i.get()) { + sg->DumpNodes(out); + } + } + + // Now fill out materials since we know all the nodes/etc. that they + // refer to exist. + for (auto&& i : materials()) { + if (Material* m = i.get()) { + m->DumpComponents(out); + } + } +} + +} // namespace ballistica diff --git a/src/ballistica/game/session/host_session.cc b/src/ballistica/game/session/host_session.cc new file mode 100644 index 00000000..4664206a --- /dev/null +++ b/src/ballistica/game/session/host_session.cc @@ -0,0 +1,762 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/game/session/host_session.h" + +#include "ballistica/game/game_stream.h" +#include "ballistica/game/host_activity.h" +#include "ballistica/game/player.h" +#include "ballistica/generic/timer.h" +#include "ballistica/graphics/graphics.h" +#include "ballistica/input/device/input_device.h" +#include "ballistica/media/component/data.h" +#include "ballistica/media/component/model.h" +#include "ballistica/media/component/sound.h" +#include "ballistica/media/component/texture.h" +#include "ballistica/python/python.h" +#include "ballistica/python/python_command.h" +#include "ballistica/python/python_context_call.h" +#include "ballistica/python/python_sys.h" + +namespace ballistica { + +HostSession::HostSession(PyObject* session_type_obj) + : last_kick_idle_players_decrement_time_(GetRealTime()) { + assert(g_game); + assert(InGameThread()); + assert(session_type_obj != nullptr); + + ScopedSetContext cp(this); + + // FIXME: Should be an attr of the session class, not hard-coded. + is_main_menu_ = + static_cast(strstr(Python::ObjToString(session_type_obj).c_str(), + "bastd.mainmenu.MainMenuSession")); + // Log("MAIN MENU? " + std::to_string(is_main_menu())); + + kick_idle_players_ = g_game->kick_idle_players(); + + // Create a timer to step our session scene. + step_scene_timer_ = + base_timers_.NewTimer(base_time_, kGameStepMilliseconds, 0, -1, + NewLambdaRunnable([this] { StepScene(); })); + + // Set up our output-stream, which will go to a replay and/or the network. + // We don't dump to a replay if we're doing the main menu; that replay + // would be boring. + bool do_replay = !is_main_menu_; + + // At the moment headless-server don't write replays. + if (HeadlessMode()) { + do_replay = false; + } + + output_stream_ = Object::New(this, do_replay); + + // Make a scene for our session-level nodes, etc. + scene_ = Object::New(0); + if (output_stream_.exists()) { + output_stream_->AddScene(scene_.get()); + } + + // Fade in from our current blackness. + g_graphics->FadeScreen(true, 250, nullptr); + + // Start by showing the progress bar instead of hitching. + g_graphics->EnableProgressBar(true); + + // Now's a good time to run garbage collection; there should be pretty much + // no game stuff to speak of in existence (provided the last session went + // down peacefully). + g_python->obj(Python::ObjID::kGarbageCollectSessionEndCall).Call(); + + // Instantiate our Python Session instance. + PythonRef obj; + PythonRef session_type(session_type_obj, PythonRef::kAcquire); + { + Python::ScopedCallLabel label("Session instantiation"); + obj = session_type.Call(); + } + if (!obj.exists()) { + throw Exception("Error creating game session: '" + session_type.Str() + + "'"); + } + + // The session python object should have called + // _ba.register_session() in its constructor to set session_py_obj_. + if (session_py_obj_ != obj) { + throw Exception("session not set up correctly"); + } + + // Lastly, keep the python layer fed with our latest player count in case + // it is updating the master-server with our current/max player counts. + g_game->SetPublicPartyPlayerCount(static_cast(players_.size())); +} + +auto HostSession::GetHostSession() -> HostSession* { return this; } + +void HostSession::DestroyHostActivity(HostActivity* a) { + BA_PRECONDITION(a); + BA_PRECONDITION(a->GetHostSession() == this); + if (a == foreground_host_activity_.get()) { + foreground_host_activity_.Clear(); + } + + // Clear it from our activities list if its still on there. + for (auto i = host_activities_.begin(); i < host_activities_.end(); i++) { + if (i->get() == a) { + host_activities_.erase(i); + return; + } + } + + // The only reason it wouldn't be there should be because the activity is + // dying due our clearing of the list in our destructor; make sure that's + // the case. + assert(shutting_down_); +} + +auto HostSession::GetMutableScene() -> Scene* { + assert(scene_.exists()); + return scene_.get(); +} + +void HostSession::DebugSpeedMultChanged() { + // FIXME - should we progress our own scene faster/slower depending on + // this too? Is there really a need to? + + // Let all our activities know. + for (auto&& i : host_activities_) { + i->DebugSpeedMultChanged(); + } +} + +void HostSession::ScreenSizeChanged() { + // Let our internal scene know. + scene()->ScreenSizeChanged(); + + // Also let all our activities know. + for (auto&& i : host_activities_) { + i->ScreenSizeChanged(); + } +} + +void HostSession::LanguageChanged() { + // Let our internal scene know. + scene()->LanguageChanged(); + + // Also let all our activities know. + for (auto&& i : host_activities_) { + i->LanguageChanged(); + } +} + +void HostSession::GraphicsQualityChanged(GraphicsQuality q) { + // Let our internal scene know. + scene()->GraphicsQualityChanged(q); + + // Let all our activities know. + for (auto&& i : host_activities_) { + i->GraphicsQualityChanged(q); + } +} + +auto HostSession::DoesFillScreen() const -> bool { + // FIXME not necessarily the case. + return true; +} + +void HostSession::Draw(FrameDef* f) { + // First draw our session scene. + scene()->Draw(f); + + // Let all our activities draw their own scenes/etc. + for (auto&& i : host_activities_) { + i->Draw(f); + } +} + +auto HostSession::NewTimer(TimerMedium length, bool repeat, + const Object::Ref& runnable) -> int { + if (shutting_down_) { + BA_LOG_PYTHON_TRACE_ONCE( + "WARNING: Creating game timer during host-session shutdown"); + return 123; // dummy... + } + if (length == 0 && repeat) { + throw Exception("Can't add game-timer with length 0 and repeat on"); + } + if (length < 0) { + throw Exception("Timer length cannot be < 0 (got " + std::to_string(length) + + ")"); + } + int offset = 0; + Timer* t = sim_timers_.NewTimer(scene()->time(), length, offset, + repeat ? -1 : 0, runnable); + return t->id(); +} + +void HostSession::DeleteTimer(int timer_id) { + assert(InGameThread()); + if (shutting_down_) return; + sim_timers_.DeleteTimer(timer_id); +} + +auto HostSession::GetSound(const std::string& name) -> Object::Ref { + if (shutting_down_) { + throw Exception("can't load assets during session shutdown"); + } + return Media::GetMedia(&sounds_, name, scene()); +} + +auto HostSession::GetData(const std::string& name) -> Object::Ref { + if (shutting_down_) { + throw Exception("can't load assets during session shutdown"); + } + return Media::GetMedia(&datas_, name, scene()); +} + +auto HostSession::GetTexture(const std::string& name) -> Object::Ref { + if (shutting_down_) { + throw Exception("can't load assets during session shutdown"); + } + return Media::GetMedia(&textures_, name, scene()); +} +auto HostSession::GetModel(const std::string& name) -> Object::Ref { + if (shutting_down_) { + throw Exception("can't load media during session shutdown"); + } + return Media::GetMedia(&models_, name, scene()); +} + +auto HostSession::GetForegroundContext() -> Context { + HostActivity* a = foreground_host_activity_.get(); + if (a) { + return Context(a); + } + return Context(this); +} + +void HostSession::RequestPlayer(InputDevice* device) { + assert(InGameThread()); + + // Ignore if we have no Python session obj. + if (!GetSessionPyObj()) { + Log("Error: HostSession::RequestPlayer() called w/no session_py_obj_."); + return; + } + + // Need to at least temporarily create and attach to a player for passing to + // the callback. + int player_id = next_player_id_++; + auto player(Object::New(player_id, this)); + players_.push_back(player); + device->AttachToLocalPlayer(player.get()); + + // Ask the python layer to accept/deny this guy. + bool accept; + { + // Set the session as context. + ScopedSetContext cp(this); + accept = static_cast( + session_py_obj_.GetAttr("_request_player") + .Call(PythonRef(Py_BuildValue("(O)", player->BorrowPyRef()), + PythonRef::kSteal)) + .ValueAsInt()); + if (accept) { + player->set_accepted(true); + } else { + RemovePlayer(player.get()); + } + } + + // If he was accepted, update our game roster with the new info. + if (accept) { + g_game->UpdateGameRoster(); + } + + // Lastly, keep the python layer fed with our latest player count in case it + // is updating the master-server with our current/max player counts. + g_game->SetPublicPartyPlayerCount(static_cast(players_.size())); +} + +void HostSession::RemovePlayer(Player* player) { + assert(player); + + for (auto i = players_.begin(); i != players_.end(); ++i) { + if (i->get() == player) { + // Grab a ref to keep the player alive, pull him off the list, then call + // his leaving callback. + Object::Ref player2 = *i; + players_.erase(i); + + // Only make the callback for this player if they were accepted. + if (player2->accepted()) { + IssuePlayerLeft(player2.get()); + } + + // Update our game roster with the departure. + g_game->UpdateGameRoster(); + + // Lastly, keep the python layer fed with our latest player count in case + // it is updating the master-server with our current/max player counts. + g_game->SetPublicPartyPlayerCount(static_cast(players_.size())); + + return; + } + } + BA_LOG_ERROR_TRACE("Player not found in HostSession::RemovePlayer()"); +} + +void HostSession::IssuePlayerLeft(Player* player) { + assert(player); + assert(InGameThread()); + + try { + if (GetSessionPyObj()) { + if (player) { + // Make sure we're the context for session callbacks. + ScopedSetContext cp(this); + Python::ScopedCallLabel label("Session on_player_leave"); + session_py_obj_.GetAttr("on_player_leave") + .Call(PythonRef(Py_BuildValue("(O)", player->BorrowPyRef()), + PythonRef::kSteal)); + } else { + BA_LOG_PYTHON_TRACE_ONCE("missing player on IssuePlayerLeft"); + } + } else { + Log("WARNING: HostSession: IssuePlayerLeft caled with no " + "session_py_obj_"); + } + } catch (const std::exception& e) { + Log(std::string("Error calling on_player_leave(): ") + e.what()); + } +} + +void HostSession::SetKickIdlePlayers(bool enable) { + // If this has changed, reset our disconnect-time reporting. + assert(InGameThread()); + if (enable != kick_idle_players_) { + last_kick_idle_players_decrement_time_ = GetRealTime(); + } + kick_idle_players_ = enable; +} + +void HostSession::SetForegroundHostActivity(HostActivity* a) { + assert(a); + assert(InGameThread()); + + if (shutting_down_) { + Log("WARNING: SetForegroundHostActivity called during session shutdown; " + "ignoring."); + return; + } + + // Sanity check: make sure the one provided is part of this session. + bool found = false; + for (auto&& i : host_activities_) { + if (i == a) { + found = true; + break; + } + } + if ((a->GetHostSession() != this) || !found) { + throw Exception("HostActivity is not part of this HostSession"); + } + + foreground_host_activity_ = a; + + // Now go through telling each host-activity whether it's foregrounded or not. + // FIXME: Dying sessions never get told they're un-foregrounded.. could that + // ever be a problem? + bool session_is_foreground = (g_game->GetForegroundSession() != nullptr); + for (auto&& i : host_activities_) { + i->SetIsForeground(session_is_foreground && (i == a)); + } +} + +void HostSession::AddHostActivity(HostActivity* a) { + host_activities_.emplace_back(a); +} + +// Called by the constructor of the session python object. +void HostSession::RegisterPySession(PyObject* obj) { + session_py_obj_.Acquire(obj); +} + +// Given an activity python type, instantiates and returns a new activity. +auto HostSession::NewHostActivity(PyObject* activity_type_obj, + PyObject* settings_obj) -> PyObject* { + PythonRef activity_type(activity_type_obj, PythonRef::kAcquire); + if (!activity_type.CallableCheck()) { + throw Exception("Invalid HostActivity type passed; not callable"); + } + + // First generate our C++ activity instance and point the context at it. + auto activity(Object::New(this)); + AddHostActivity(activity.get()); + + ScopedSetContext cp(activity.get()); + + // Now instantiate the Python instance.. pass args if some were provided, or + // an empty dict otherwise. + PythonRef args; + if (settings_obj == Py_None) { + args.Steal(Py_BuildValue("({})")); + } else { + args.Steal(Py_BuildValue("(O)", settings_obj)); + } + + PythonRef result = activity_type.Call(args); + if (!result.exists()) { + throw Exception("HostActivity creation failed"); + } + + // If all went well, the python activity constructor should have called + // _ba.register_activity(), so we should be able to get at the same python + // activity we just instantiated through the c++ class. + if (activity->GetPyActivity() != result.get()) { + throw Exception("Error on HostActivity construction"); + } + + PyObject* obj = result.get(); + Py_INCREF(obj); + return obj; +} + +auto HostSession::RegisterPyActivity(PyObject* activity_obj) -> HostActivity* { + // The context should be pointing to an unregistered HostActivity; + // register and return it. + HostActivity* activity = Context::current().GetHostActivity(); + if (!activity) + throw Exception( + "No current activity in RegisterPyActivity; did you remember to call " + "ba.newHostActivity() to instantiate your activity?"); + activity->RegisterPyActivity(activity_obj); + return activity; +} + +void HostSession::DecrementPlayerTimeOuts(millisecs_t millisecs) { + for (auto&& i : players_) { + Player* player = i.get(); + assert(player); + if (player->time_out() < millisecs) { + std::string kick_str = + g_game->GetResourceString("kickIdlePlayersKickedText"); + Utils::StringReplaceOne(&kick_str, "${NAME}", player->GetName()); + ScreenMessage(kick_str); + RemovePlayer(player); + return; // Bail for this round since we prolly mucked with the list. + } else if (player->time_out() > BA_PLAYER_TIME_OUT_WARN + && (player->time_out() - millisecs <= BA_PLAYER_TIME_OUT_WARN)) { + std::string kick_str_1 = + g_game->GetResourceString("kickIdlePlayersWarning1Text"); + Utils::StringReplaceOne(&kick_str_1, "${NAME}", player->GetName()); + Utils::StringReplaceOne(&kick_str_1, "${COUNT}", + std::to_string(BA_PLAYER_TIME_OUT_WARN / 1000)); + ScreenMessage(kick_str_1); + ScreenMessage(g_game->GetResourceString("kickIdlePlayersWarning2Text")); + } + player->set_time_out(player->time_out() - millisecs); + } +} + +void HostSession::ProcessPlayerTimeOuts() { + millisecs_t real_time = GetRealTime(); + + if (foreground_host_activity_.exists() + && foreground_host_activity_->game_speed() > 0.0 + && !foreground_host_activity_->paused() + && foreground_host_activity_->getAllowKickIdlePlayers() + && kick_idle_players_) { + // Let's only do this every now and then. + if (real_time - last_kick_idle_players_decrement_time_ > 1000) { + DecrementPlayerTimeOuts(real_time + - last_kick_idle_players_decrement_time_); + last_kick_idle_players_decrement_time_ = real_time; + } + } else { + // If we're not kicking, we still store the latest time (so it doesnt + // accumulate for when we start again). + last_kick_idle_players_decrement_time_ = real_time; + } +} + +void HostSession::StepScene() { + // Run up our game-time timers. + sim_timers_.Run(scene()->time()); + + // And step. + scene()->Step(); +} + +void HostSession::Update(int time_advance) { + assert(InGameThread()); + + // We can be killed at any time, so let's keep an eye out for that. + WeakRef test_ref(this); + assert(test_ref.exists()); + + ProcessPlayerTimeOuts(); + + GameStream* output_stream = GetGameStream(); + + // Advance base time by the specified amount, + // firing all timers along the way. + millisecs_t target_base_time = base_time_ + time_advance; + while (!base_timers_.empty() + && (base_time_ + base_timers_.GetTimeToNextExpire(base_time_) + <= target_base_time)) { + base_time_ += base_timers_.GetTimeToNextExpire(base_time_); + if (output_stream) { + output_stream->SetTime(base_time_); + } + base_timers_.Run(base_time_); + } + base_time_ = target_base_time; + if (output_stream) { + output_stream->SetTime(base_time_); + } + assert(test_ref.exists()); + + // Update our activities (iterate via weak-refs as this list may change under + // us at any time). + std::vector > activities = + PointersToWeakRefs(RefsToPointers(host_activities_)); + for (auto&& i : activities) { + if (i.exists()) { + i->Update(time_advance); + assert(test_ref.exists()); + } + } + assert(test_ref.exists()); + + // Periodically prune various dead refs. + if (base_time_ > next_prune_time_) { + PruneDeadMapRefs(&textures_); + PruneDeadMapRefs(&sounds_); + PruneDeadMapRefs(&models_); + PruneDeadRefs(&python_calls_); + next_prune_time_ = base_time_ + 5000; + } + assert(test_ref.exists()); +} + +HostSession::~HostSession() { + try { + shutting_down_ = true; + + // Put the scene in shut-down mode before we start killing stuff + // (this generates warnings, suppresses messages, etc). + scene_->set_shutting_down(true); + + // Clear out all python calls registered in our context + // (should wipe out refs to our session and prevent them from running + // without a valid session context). + for (auto&& i : python_calls_) { + if (i.exists()) { + i->MarkDead(); + } + } + + // Mark all our media dead to clear it out of our output-stream cleanly. + for (auto&& i : textures_) { + if (i.second.exists()) { + i.second->MarkDead(); + } + } + for (auto&& i : models_) { + if (i.second.exists()) { + i.second->MarkDead(); + } + } + for (auto&& i : sounds_) { + if (i.second.exists()) { + i.second->MarkDead(); + } + } + + // Clear our timers and scene; this should wipe out any remaining refs + // to our session scene. + base_timers_.Clear(); + sim_timers_.Clear(); + scene_.Clear(); + + // Kill our python session object. + { + ScopedSetContext cp(this); + session_py_obj_.Release(); + } + + // Kill any remaining activity data. Generally all activities should die + // when the session python object goes down, but lets clean up in case any + // didn't. + for (auto&& i : host_activities_) { + ScopedSetContext cp{Object::Ref(i)}; + i.Clear(); + } + + // Report outstanding calls. There shouldn't be any at this point. Actually + // it turns out there's generally 1; whichever call was responsible for + // killing this activity will still be in progress.. so let's report on 2 or + // more I guess. + if (g_buildconfig.debug_build()) { + PruneDeadRefs(&python_calls_); + if (python_calls_.size() > 1) { + std::string s = "WARNING: " + std::to_string(python_calls_.size()) + + " live PythonContextCalls at shutdown for " + + "HostSession" + " (1 call is expected):"; + int count = 1; + for (auto&& i : python_calls_) { + s += ("\n " + std::to_string(count++) + ": " + + i->GetObjectDescription()); + } + Log(s); + } + } + } catch (const std::exception& e) { + Log("Exception in HostSession destructor: " + std::string(e.what())); + } +} + +void HostSession::RegisterCall(PythonContextCall* call) { + assert(call); + python_calls_.emplace_back(call); + + // If we're shutting down, just kill the call immediately. + // (we turn all of our calls to no-ops as we shut down). + if (shutting_down_) { + Log("WARNING: adding call to expired session; call will not function: " + + call->GetObjectDescription()); + call->MarkDead(); + } +} + +auto HostSession::GetUnusedPlayerName(Player* p, const std::string& base_name) + -> std::string { + // Now find the first non-taken variation. + int index = 1; + std::string name_test; + while (true) { + if (index > 1) { + name_test = base_name + " " + std::to_string(index); + } else { + name_test = base_name; + } + bool name_found = false; + for (auto&& j : players_) { + if ((j->GetName() == name_test) && (j.get() != p)) { + name_found = true; + break; + } + } + if (!name_found) break; + index += 1; + } + return name_test; +} + +void HostSession::DumpFullState(GameStream* out) { + // Add session-scene. + if (scene_.exists()) { + scene_->Dump(out); + } + + // Dump media associated with session-scene. + for (auto&& i : textures_) { + if (Texture* t = i.second.get()) { + out->AddTexture(t); + } + } + for (auto&& i : sounds_) { + if (Sound* s = i.second.get()) { + out->AddSound(s); + } + } + for (auto&& i : models_) { + if (Model* s = i.second.get()) { + out->AddModel(s); + } + } + + // Dump session-scene's nodes. + if (scene_.exists()) { + scene_->DumpNodes(out); + } + + // Now let our activities dump themselves. + for (auto&& i : host_activities_) { + i->DumpFullState(out); + } +} + +void HostSession::GetCorrectionMessages( + bool blend, std::vector >* messages) { + std::vector message; + + // Grab correction for session scene (though there shouldn't be one). + if (scene_.exists()) { + message = scene_->GetCorrectionMessage(blend); + if (message.size() > 4) { + // A correction packet of size 4 is empty; ignore it. + messages->push_back(message); + } + } + + // Now do same for activity scenes. + for (auto&& i : host_activities_) { + if (HostActivity* ha = i.get()) { + if (Scene* sg = ha->scene()) { + message = sg->GetCorrectionMessage(blend); + if (message.size() > 4) { + // A correction packet of size 4 is empty; ignore it. + messages->push_back(message); + } + } + } + } +} + +auto HostSession::NewTimer(TimeType timetype, TimerMedium length, bool repeat, + const Object::Ref& runnable) -> int { + // Make sure the runnable passed in is reference-managed already + // (we may not add an initial reference ourself). + assert(runnable->is_valid_refcounted_object()); + + // We currently support game and base timers. + switch (timetype) { + case TimeType::kSim: + case TimeType::kBase: + // Game and base timers are the same thing for us. + return NewTimer(length, repeat, runnable); + default: + // Gall back to default for descriptive error otherwise. + return ContextTarget::NewTimer(timetype, length, repeat, runnable); + } +} + +void HostSession::DeleteTimer(TimeType timetype, int timer_id) { + switch (timetype) { + case TimeType::kSim: + case TimeType::kBase: + // Game and base timers are the same thing for us. + DeleteTimer(timer_id); + break; + default: + // Fall back to default for descriptive error otherwise. + ContextTarget::DeleteTimer(timetype, timer_id); + break; + } +} + +auto HostSession::GetTime(TimeType timetype) -> millisecs_t { + switch (timetype) { + case TimeType::kSim: + case TimeType::kBase: + return scene_->time(); + default: + // Fall back to default for descriptive error otherwise. + return ContextTarget::GetTime(timetype); + } +} + +} // namespace ballistica diff --git a/src/ballistica/game/session/net_client_session.cc b/src/ballistica/game/session/net_client_session.cc new file mode 100644 index 00000000..a60407c9 --- /dev/null +++ b/src/ballistica/game/session/net_client_session.cc @@ -0,0 +1,201 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/game/session/net_client_session.h" + +#include "ballistica/app/app_globals.h" +#include "ballistica/game/connection/connection_to_host.h" +#include "ballistica/graphics/graphics.h" +#include "ballistica/graphics/net_graph.h" +#include "ballistica/media/media_server.h" + +namespace ballistica { + +NetClientSession::NetClientSession() { + // Sanity check: we should only ever be writing one replay at once. + if (g_app_globals->replay_open) { + Log("ERROR: g_replay_open true at netclient start; shouldn't happen."); + } + assert(g_media_server); + g_media_server->PushBeginWriteReplayCall(); + writing_replay_ = true; + g_app_globals->replay_open = true; +} + +NetClientSession::~NetClientSession() { + if (writing_replay_) { + // Sanity check: we should only ever be writing one replay at once. + if (!g_app_globals->replay_open) { + Log("ERROR: g_replay_open false at net-client close; shouldn't happen."); + } + g_app_globals->replay_open = false; + assert(g_media_server); + g_media_server->PushEndWriteReplayCall(); + writing_replay_ = false; + } +} + +void NetClientSession::SetConnectionToHost(ConnectionToHost* c) { + connection_to_host_ = c; +} + +void NetClientSession::OnCommandBufferUnderrun() { + // We currently don't do anything here; we want to just power + // through hitches and keep aiming for our target time. + // (though perhaps we could take note here for analytics purposes). + // printf("Underrun at %d\n", GetRealTime()); + // fflush(stdout); +} + +void NetClientSession::Update(int time_advance) { + if (shutting_down()) { + return; + } + + // Now do standard step. + ClientSession::Update(time_advance); + + // And update our timing to try and ensure we don't run out of buffer. + UpdateBuffering(); +} + +auto NetClientSession::GetBucketNum() -> int { + return (delay_sample_counter_ / g_app_globals->delay_bucket_samples) + % static_cast(buckets_.size()); +} + +auto NetClientSession::UpdateBuffering() -> void { + // Keep record of the most and least amount of time we've had buffered + // recently, and slow down/speed up a bit based on that. + { + // Change bucket every `g_delay_samples` samples. + int bucketnum{GetBucketNum()}; + int bucket_iteration = + delay_sample_counter_ % g_app_globals->delay_bucket_samples; + delay_sample_counter_++; + SampleBucket& bucket{buckets_[bucketnum]}; + if (bucket_iteration == 0) { + bucket.max_delay_from_projection = 0; + } + + // After the last sample in each bucket, update our smoothed values with + // the full sample set in the bucket. + if (bucket_iteration == g_app_globals->delay_bucket_samples - 1) { + float smoothing = 0.7f; + last_bucket_max_delay_ = + static_cast(bucket.max_delay_from_projection); + max_delay_smoothed_ = + smoothing * max_delay_smoothed_ + + (1.0f - smoothing) + * static_cast(bucket.max_delay_from_projection); + } + auto now = GetRealTime(); + + // We want target-base-time to wind up at our projected time minus some + // safety offset to account for buffering fluctuations. + + // We might want to consider exposing this value or calculate it in a smart + // way based on conditions. 0.0 gives us lowest latency possible but makes + // lag spikes very noticeable. 1.0 should avoid most lag spikes. Higher + // values even moreso at the price of latency; + float safety_amt{1.0}; + + float to_ideal_offset = + static_cast(ProjectedBaseTime(now) - target_base_time()) + - safety_amt * max_delay_smoothed_; + + // How aggressively we throttle the game speed up or down to accommodate lag + // spikes. + float speed_change_aggression{0.004f}; + float new_consume_rate = std::min( + 10.0f, + std::max(0.5f, 1.0f + speed_change_aggression * to_ideal_offset)); + set_consume_rate(new_consume_rate); + + if (g_graphics->network_debug_info_display_enabled()) { + if (NetGraph* graph = + g_graphics->GetDebugGraph("1: packet delay", false)) { + graph->AddSample(now, current_delay_); + } + if (NetGraph* graph = + g_graphics->GetDebugGraph("2: max delay bucketed", false)) { + graph->AddSample(now, last_bucket_max_delay_); + } + if (NetGraph* graph = + g_graphics->GetDebugGraph("3: filtered delay", false)) { + graph->AddSample(now, max_delay_smoothed_); + } + if (NetGraph* graph = g_graphics->GetDebugGraph("4: run rate", false)) { + graph->AddSample(now, new_consume_rate); + } + if (NetGraph* graph = + g_graphics->GetDebugGraph("5: time buffered", true)) { + graph->AddSample(now, base_time_buffered()); + } + } + } +} + +auto NetClientSession::OnReset(bool rewind) -> void { + // Resets should never happen for us after we start, right?... + base_time_received_ = 0; + last_base_time_receive_time_ = 0; + leading_base_time_received_ = 0; + leading_base_time_receive_time_ = 0; + ClientSession::OnReset(rewind); +} + +auto NetClientSession::OnBaseTimeStepAdded(int step) -> void { + auto now = GetRealTime(); + + millisecs_t new_base_time_received = base_time_received_ + step; + + // We want to be able to project as close as possible to what the + // current base time is based on when we receive steps (regardless of lag + // spikes). To do this, we only factor in steps we receive if their times are + // newer than what we get projecting forward from the last one. + bool use; + if (leading_base_time_receive_time_ == 0) { + use = true; + } else { + millisecs_t projected = ProjectedBaseTime(now); + + // Hopefully we'll keep refreshing our leading value consistently + // but force the issue if it becomes too old. + use = (new_base_time_received >= projected + || (now - leading_base_time_receive_time_ > 250)); + + // Keep track of the biggest recent delays we get compared to the projected + // time. (we can use this when calcing how much to buffer to avoid stutter). + if (new_base_time_received < projected) { + auto& bucket{buckets_[GetBucketNum()]}; + current_delay_ = bucket.max_delay_from_projection = + std::max(bucket.max_delay_from_projection, + static_cast(projected - new_base_time_received)); + + } else { + current_delay_ = 0.0f; + } + } + + base_time_received_ = new_base_time_received; + last_base_time_receive_time_ = now; + + if (use) { + leading_base_time_received_ = new_base_time_received; + leading_base_time_receive_time_ = now; + } +} + +void NetClientSession::HandleSessionMessage( + const std::vector& message) { + // Do the standard thing, but also write this message straight to our replay + // stream if we have one. + ClientSession::HandleSessionMessage(message); + + if (writing_replay_) { + assert(g_media_server); + g_media_server->PushAddMessageToReplayCall(message); + } +} + +} // namespace ballistica diff --git a/src/ballistica/game/session/replay_client_session.cc b/src/ballistica/game/session/replay_client_session.cc new file mode 100644 index 00000000..f924439f --- /dev/null +++ b/src/ballistica/game/session/replay_client_session.cc @@ -0,0 +1,257 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/game/session/replay_client_session.h" + +#include "ballistica/dynamics/material/material.h" +#include "ballistica/game/connection/connection_set.h" +#include "ballistica/game/connection/connection_to_client.h" +#include "ballistica/game/game_stream.h" +#include "ballistica/generic/huffman.h" +#include "ballistica/generic/utils.h" +#include "ballistica/math/vector3f.h" +#include "ballistica/networking/networking.h" +#include "ballistica/platform/platform.h" +#include "ballistica/scene/scene.h" + +namespace ballistica { + +auto ReplayClientSession::GetActualTimeAdvance(int advance_in) -> int { + return static_cast( + round(advance_in * pow(2.0f, g_game->replay_speed_exponent()))); +} + +ReplayClientSession::ReplayClientSession(std::string filename) + : file_name_(std::move(filename)) { + // take responsibility for feeding all clients to this device.. + g_game->connections()->RegisterClientController(this); + + // go ahead and just do a reset here, which will get things going.. + Reset(true); +} + +ReplayClientSession::~ReplayClientSession() { + // we no longer are responsible for feeding clients to this device.. + g_game->connections()->UnregisterClientController(this); + + if (file_) { + fclose(file_); + file_ = nullptr; + } +} + +void ReplayClientSession::OnCommandBufferUnderrun() { ResetTargetBaseTime(); } + +void ReplayClientSession::OnClientConnected(ConnectionToClient* c) { + // sanity check - abort if its on either of our lists already + for (ConnectionToClient* i : connections_to_clients_) { + if (i == c) { + Log("Error: ReplayClientSession::OnClientConnected()" + " got duplicate connection"); + return; + } + } + for (ConnectionToClient* i : connections_to_clients_ignored_) { + if (i == c) { + Log("Error: ReplayClientSession::OnClientConnected()" + " got duplicate connection"); + return; + } + } + + // if we've sent *any* commands out to clients so far, we currently have to + // ignore new connections (need to rebuild state to match current session + // state) + { + connections_to_clients_.push_back(c); + + // we create a temporary output stream just for the purpose of building + // a giant session-commands message that we can send to the client + // to build its state up to where we are currently. + GameStream out(nullptr, false); + + // go ahead and dump our full state.. + DumpFullState(&out); + + // grab the message that's been built up.. + // if its not empty, send it to the client. + std::vector out_message = out.GetOutMessage(); + if (!out_message.empty()) { + c->SendReliableMessage(out_message); + } + + // also send a correction packet to sync up all our dynamics + // (technically could do this *just* for the new client) + { + std::vector > messages; + bool blend = false; + GetCorrectionMessages(blend, &messages); + + // FIXME - have to send reliably at the moment since these will most + // likely be bigger than our unreliable packet limit.. :-( + for (auto&& i : messages) { + for (auto&& j : connections_to_clients_) { + j->SendReliableMessage(i); + } + } + } + } +} + +void ReplayClientSession::OnClientDisconnected(ConnectionToClient* c) { + // Search for it on either our ignored or regular lists. + for (auto i = connections_to_clients_.begin(); + i != connections_to_clients_.end(); i++) { + if (*i == c) { + connections_to_clients_.erase(i); + return; + } + } + for (auto i = connections_to_clients_ignored_.begin(); + i != connections_to_clients_ignored_.end(); i++) { + if (*i == c) { + connections_to_clients_ignored_.erase(i); + return; + } + } + Log("Error: ReplayClientSession::OnClientDisconnected()" + " called for connection not on lists"); +} + +void ReplayClientSession::FetchMessages() { + if (!file_ || shutting_down()) { + return; + } + + // If we have no messages left, read from the file until we get some. + while (commands().empty()) { + std::vector buffer; + uint8_t len8; + uint32_t len32; + + // Read the size of the message. + // the first byte represents the actual size if the value is < 254 + // if it is 254, the 2 bytes after it represent size + // if it is 255, the 4 bytes after it represent size + if (fread(&len8, 1, 1, file_) != 1) { + // So they know to be done when they reach the end of the command list + // (instead of just waiting for more commands) + add_end_of_file_command(); + fclose(file_); + file_ = nullptr; + return; + } + if (len8 < 254) { + len32 = len8; + } else { + // Pull 16 bit len. + if (len8 == 254) { + uint16_t len16; + if (fread(&len16, 2, 1, file_) != 1) { + // so they know to be done when they reach the end of the command list + // (instead of just waiting for more commands) + add_end_of_file_command(); + fclose(file_); + file_ = nullptr; + return; + } + assert(len16 >= 254); + len32 = len16; + } else { + // Pull 32 bit len. + if (fread(&len32, 4, 1, file_) != 1) { + // so they know to be done when they reach the end of the command list + // (instead of just waiting for more commands) + add_end_of_file_command(); + fclose(file_); + file_ = nullptr; + return; + } + assert(len32 > 65535); + } + } + + // Read and decompress the actual message. + BA_PRECONDITION(len32 > 0); + buffer.resize(len32); + if (fread(&(buffer[0]), len32, 1, file_) != 1) { + add_end_of_file_command(); + fclose(file_); + file_ = nullptr; + return; + } + std::vector data_decompressed = + g_utils->huffman()->decompress(buffer); + HandleSessionMessage(data_decompressed); + + // Also send it to all client-connections we're attached to. + // NOTE: We currently are sending everything as reliable; we can maybe do + // unreliable for certain type of messages. Though perhaps when passing + // around replays maybe its best to keep everything intact. + have_sent_client_message_ = true; + for (auto&& i : connections_to_clients_) { + i->SendReliableMessage(data_decompressed); + } + message_fetch_num_++; + } +} + +void ReplayClientSession::Error(const std::string& description) { + // Close the replay, announce something went wrong with it, and then do + // standard error response.. + ScreenMessage(g_game->GetResourceString("replayReadErrorText"), {1, 0, 0}); + if (file_) { + fclose(file_); + file_ = nullptr; + } + ClientSession::Error(description); +} + +void ReplayClientSession::OnReset(bool rewind) { + // Handles base resetting. + ClientSession::OnReset(rewind); + + // If we've got any clients attached to us, tell them to reset as well. + for (auto&& i : connections_to_clients_) { + i->SendReliableMessage(std::vector(1, BA_MESSAGE_SESSION_RESET)); + } + + // If rewinding, pop back to the start of our file. + if (rewind) { + if (file_) { + fclose(file_); + file_ = nullptr; + } + + file_ = g_platform->FOpen(file_name_.c_str(), "rb"); + if (!file_) { + Error("can't open file for reading"); + return; + } + + // Read file ID and version to make sure we support this file. + uint32_t file_id; + if ((fread(&file_id, sizeof(file_id), 1, file_) != 1)) { + Error("error reading file_id"); + return; + } + if (file_id != kBrpFileID) { + Error("incorrect file_id"); + return; + } + + // Make sure its a compatible protocol version. + uint16_t version; + if (fread(&version, sizeof(version), 1, file_) != 1) { + Error("error reading version"); + return; + } + if (version > kProtocolVersion || version < kProtocolVersionMin) { + ScreenMessage(g_game->GetResourceString("replayVersionErrorText"), + {1, 0, 0}); + End(); + return; + } + } +} + +} // namespace ballistica diff --git a/src/ballistica/game/session/session.cc b/src/ballistica/game/session/session.cc new file mode 100644 index 00000000..07a8293b --- /dev/null +++ b/src/ballistica/game/session/session.cc @@ -0,0 +1,37 @@ +// Released under the MIT License. See LICENSE for details. + +#include "ballistica/game/session/session.h" + +#include "ballistica/app/app_globals.h" +#include "ballistica/game/game.h" + +namespace ballistica { + +Session::Session() { + g_app_globals->session_count++; + + // New sessions immediately become foreground. + g_game->SetForegroundSession(this); +} + +Session::~Session() { g_app_globals->session_count--; } + +void Session::Update(int time_advance) {} + +auto Session::GetForegroundContext() -> Context { return Context(); } + +void Session::Draw(FrameDef*) {} + +void Session::ScreenSizeChanged() {} + +void Session::LanguageChanged() {} + +void Session::GraphicsQualityChanged(GraphicsQuality q) {} + +void Session::DebugSpeedMultChanged() {} + +void Session::DumpFullState(GameStream* out) { + Log("Session::DumpFullState() being called; shouldn't happen."); +} + +} // namespace ballistica