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