From 18ec4dc2640110ebebab8dab849feebb81c64073 Mon Sep 17 00:00:00 2001 From: Eric Froemling Date: Fri, 2 Oct 2020 15:21:39 -0500 Subject: [PATCH] Yet more C++ stuff --- .efrocachemap | 14 +- src/ballistica/game/account.cc | 199 -- src/ballistica/game/game.cc | 3165 ----------------- src/ballistica/game/game_stream.cc | 1242 ------- src/ballistica/game/host_activity.cc | 528 --- src/ballistica/game/host_activity.h | 2 +- src/ballistica/game/player.cc | 418 --- src/ballistica/game/player_spec.cc | 109 - src/ballistica/game/session/client_session.cc | 1070 ------ src/ballistica/game/session/host_session.cc | 765 ---- .../game/session/net_client_session.cc | 147 - .../game/session/replay_client_session.cc | 321 -- src/ballistica/game/session/session.cc | 36 - 13 files changed, 8 insertions(+), 8008 deletions(-) delete mode 100644 src/ballistica/game/account.cc delete mode 100644 src/ballistica/game/game.cc delete mode 100644 src/ballistica/game/game_stream.cc delete mode 100644 src/ballistica/game/host_activity.cc delete mode 100644 src/ballistica/game/player.cc delete mode 100644 src/ballistica/game/player_spec.cc delete mode 100644 src/ballistica/game/session/client_session.cc delete mode 100644 src/ballistica/game/session/host_session.cc delete mode 100644 src/ballistica/game/session/net_client_session.cc delete mode 100644 src/ballistica/game/session/replay_client_session.cc delete mode 100644 src/ballistica/game/session/session.cc diff --git a/.efrocachemap b/.efrocachemap index 9987ce92..f968205e 100644 --- a/.efrocachemap +++ b/.efrocachemap @@ -3934,14 +3934,14 @@ "assets/build/windows/Win32/vcruntime140d.dll": "https://files.ballistica.net/cache/ba1/50/8d/bc2600ac9491f1b14d659709451f", "build/prefab/linux-server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/ac/96/c3b9934061393fe09cc90ff24b8d", "build/prefab/linux-server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/38/2b/5641b3b40846f74f232771ac0457", - "build/prefab/linux/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/07/e7/d8f0add439e55e3cce5e5768c80f", + "build/prefab/linux/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/fd/10/7681acdbd8feccb27175d6ab8609", "build/prefab/linux/release/ballisticacore": "https://files.ballistica.net/cache/ba1/fd/72/faa94ff6532a95c121fcb5a4f788", - "build/prefab/mac-server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/d2/d1/99514fbe084fb0480d75f92ecb2c", + "build/prefab/mac-server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/97/99/5ba65477f8b846beb98d146a1d2c", "build/prefab/mac-server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/dd/5d/f8c5b24579236bef5209d7089044", - "build/prefab/mac/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/5f/ff/5d34815d90dd4cd36f2a6f587958", + "build/prefab/mac/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/ba/af/659cd48bd1be9b22ba3006ccb5f9", "build/prefab/mac/release/ballisticacore": "https://files.ballistica.net/cache/ba1/45/1e/cea9badaf52032adb40e6c3b5e21", - "build/prefab/windows-server/debug/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/5d/63/96c2bbbedc03bd23824d7354b07d", - "build/prefab/windows-server/release/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/44/94/4fa92ae4a1e726fb0b37e626e107", - "build/prefab/windows/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/66/fd/8bb36157e75f78caa5373d9def18", - "build/prefab/windows/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/af/2d/7546ed3c987435a743442603c21b" + "build/prefab/windows-server/debug/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/9c/53/c0a2b1c2ee30397db0eb367b688c", + "build/prefab/windows-server/release/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/8b/a1/c3471ecf846cce50d9220f7214c3", + "build/prefab/windows/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/fa/95/87cc2ad7f0e780b02cd9ac633d3d", + "build/prefab/windows/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/31/6f/9a29e7100425f8a364208ba40f4f" } \ No newline at end of file diff --git a/src/ballistica/game/account.cc b/src/ballistica/game/account.cc deleted file mode 100644 index b432f315..00000000 --- a/src/ballistica/game/account.cc +++ /dev/null @@ -1,199 +0,0 @@ -// Copyright (c) 2011-2020 Eric Froemling - -#include "ballistica/game/account.h" - -#include "ballistica/app/app_globals.h" -#include "ballistica/game/game.h" -#include "ballistica/generic/utils.h" -#include "ballistica/python/python.h" - -namespace ballistica { - -auto Account::AccountTypeFromString(const std::string& val) -> AccountType { - if (val == "Game Center") { - return AccountType::kGameCenter; - } else if (val == "Game Circle") { - return AccountType::kGameCircle; - } else if (val == "Google Play") { - return AccountType::kGooglePlay; - } else if (val == "Steam") { - return AccountType::kSteam; - } else if (val == "Oculus") { - return AccountType::kOculus; - } else if (val == "NVIDIA China") { - return AccountType::kNvidiaChina; - } else if (val == "Test") { - return AccountType::kTest; - } else if (val == "Local") { - return AccountType::kDevice; - } else if (val == "Server") { - return AccountType::kServer; - } else { - return AccountType::kInvalid; - } -} - -auto Account::AccountTypeToString(AccountType type) -> std::string { - switch (type) { - case AccountType::kGameCenter: - return "Game Center"; - case AccountType::kGameCircle: - return "Game Circle"; - case AccountType::kGooglePlay: - return "Google Play"; - case AccountType::kSteam: - return "Steam"; - case AccountType::kOculus: - return "Oculus"; - case AccountType::kTest: - return "Test"; - case AccountType::kDevice: - return "Local"; - case AccountType::kServer: - return "Server"; - case AccountType::kNvidiaChina: - return "NVIDIA China"; - default: - return ""; - } -} - -auto Account::AccountTypeToIconString(AccountType type) -> std::string { - switch (type) { - case AccountType::kTest: - return g_game->CharStr(SpecialChar::kTestAccount); - case AccountType::kNvidiaChina: - return g_game->CharStr(SpecialChar::kNvidiaLogo); - case AccountType::kGooglePlay: - return g_game->CharStr(SpecialChar::kGooglePlayGamesLogo); - case AccountType::kSteam: - return g_game->CharStr(SpecialChar::kSteamLogo); - case AccountType::kOculus: - return g_game->CharStr(SpecialChar::kOculusLogo); - case AccountType::kGameCenter: - return g_game->CharStr(SpecialChar::kGameCenterLogo); - case AccountType::kGameCircle: - return g_game->CharStr(SpecialChar::kGameCircleLogo); - case AccountType::kDevice: - case AccountType::kServer: - return g_game->CharStr(SpecialChar::kLocalAccount); - default: - return ""; - } -} - -Account::Account() = default; - -auto Account::GetAccountName() -> std::string { - std::lock_guard lock(mutex_); - return account_name_; -} - -auto Account::GetAccountID() -> std::string { - std::lock_guard lock(mutex_); - return account_id_; -} - -auto Account::GetAccountToken() -> std::string { - std::lock_guard lock(mutex_); - return account_token_; -} - -auto Account::GetAccountExtra() -> std::string { - std::lock_guard lock(mutex_); - return account_extra_; -} - -auto Account::GetAccountExtra2() -> std::string { - std::lock_guard lock(mutex_); - return account_extra_2_; -} - -auto Account::GetAccountState(int* state_num) -> AccountState { - std::lock_guard lock(mutex_); - if (state_num) { - *state_num = account_state_num_; - } - return account_state_; -} - -void Account::SetAccountExtra(const std::string& extra) { - std::lock_guard lock(mutex_); - account_extra_ = extra; -} - -void Account::SetAccountExtra2(const std::string& extra) { - std::lock_guard lock(mutex_); - account_extra_2_ = extra; -} - -void Account::SetAccountToken(const std::string& account_id, - const std::string& token) { - std::lock_guard lock(mutex_); - // Hmm does this compare logic belong in here? - if (account_id_ == account_id) { - account_token_ = token; - } -} - -void Account::SetAccount(AccountType account_type, AccountState account_state, - const std::string& account_name, - const std::string& account_id) { - bool call_account_changed = false; - { - std::lock_guard lock(mutex_); - - // We call out to python so need to be in game thread. - assert(InGameThread()); - if (account_state_ != account_state - || g_app_globals->account_type != account_type - || account_id_ != account_id || account_name_ != account_name) { - // Special case: if they sent a sign-out for an account type that is. - // currently not signed in, ignore it. - if (account_state == AccountState::kSignedOut - && (account_type != g_app_globals->account_type)) { - // No-op. - } else { - account_state_ = account_state; - g_app_globals->account_type = account_type; - account_id_ = account_id; - account_name_ = Utils::GetValidUTF8(account_name.c_str(), "gthm"); - - // If they signed out of an account, account type switches to invalid. - if (account_state == AccountState::kSignedOut) { - g_app_globals->account_type = AccountType::kInvalid; - } - account_state_num_ += 1; - call_account_changed = true; - } - } - } - if (call_account_changed) { - // Inform python layer this has changed. - g_python->AccountChanged(); - } -} - -void Account::SetProductsPurchased(const std::vector& products) { - std::lock_guard lock(mutex_); - std::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.cc b/src/ballistica/game/game.cc deleted file mode 100644 index 77c563f3..00000000 --- a/src/ballistica/game/game.cc +++ /dev/null @@ -1,3165 +0,0 @@ -// Copyright (c) 2011-2020 Eric Froemling - -#include "ballistica/game/game.h" - -#include -#include -#include - -#include "ballistica/app/app.h" -#include "ballistica/app/app_config.h" -#include "ballistica/audio/audio.h" -#include "ballistica/core/thread.h" -#include "ballistica/dynamics/bg/bg_dynamics.h" -#include "ballistica/game/account.h" -#include "ballistica/game/connection/connection_to_client_udp.h" -#include "ballistica/game/connection/connection_to_host_udp.h" -#include "ballistica/game/friend_score_set.h" -#include "ballistica/game/host_activity.h" -#include "ballistica/game/player.h" -#include "ballistica/game/score_to_beat.h" -#include "ballistica/game/session/client_session.h" -#include "ballistica/game/session/host_session.h" -#include "ballistica/game/session/net_client_session.h" -#include "ballistica/game/session/replay_client_session.h" -#include "ballistica/generic/json.h" -#include "ballistica/generic/timer.h" -#include "ballistica/graphics/graphics.h" -#include "ballistica/graphics/graphics_server.h" -#include "ballistica/graphics/text/text_graphics.h" -#include "ballistica/input/device/client_input_device.h" -#include "ballistica/input/device/keyboard_input.h" -#include "ballistica/input/device/touch_input.h" -#include "ballistica/math/vector3f.h" -#include "ballistica/networking/network_write_module.h" -#include "ballistica/networking/networking.h" -#include "ballistica/networking/sockaddr.h" -#include "ballistica/networking/telnet_server.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" -#include "ballistica/scene/node/globals_node.h" -#include "ballistica/ui/console.h" -#include "ballistica/ui/root_ui.h" -#include "ballistica/ui/ui.h" -#include "ballistica/ui/widget/root_widget.h" -#include "ballistica/ui/widget/text_widget.h" - -namespace ballistica { - -/// How long a kick vote lasts. -const int kKickVoteDuration = 30000; - -/// How long everyone has to wait to start a new kick vote after a failed one. -const int kKickVoteFailRetryDelay = 60000; - -/// Extra delay for the initiator of a failed vote. -const int kKickVoteFailRetryDelayInitiatorExtra = 120000; - -// Minimum clients that must be present for a kick vote to count. -// (for non-headless builds we require more votes since the host doesn't count -// but may be playing (in a 2on2 with 3 clients, don't want 2 clients able to -// kick). -// NOLINTNEXTLINE(cert-err58-cpp) -const int kKickVoteMinimumClients = (g_buildconfig.headless_build() ? 3 : 4); - -const int kMaxChatMessages = 40; - -// Go with 5 minute ban. -const int kKickBanSeconds = 5 * 60; - -Game::Game(Thread* thread) - : Module("game", thread), - game_roster_(cJSON_CreateArray()), - realtimers_(new TimerList()) { - assert(g_game == nullptr); - g_game = this; - - try { - // Spin up some other game-thread-based stuff. - AppConfig::Init(); - assert(g_graphics == nullptr); - g_graphics = g_platform->CreateGraphics(); - TextGraphics::Init(); - Media::Init(); - Audio::Init(); - if (!HeadlessMode()) { - BGDynamics::Init(); - } - - InitSpecialChars(); - - Context::Init(); - - // Waaah does UI need to be a bs::Object? - // Update: yes it does in order to be a context target. - // (need to be able to create weak-refs to it). - assert(g_ui == nullptr); - g_ui = Object::NewUnmanaged(); - - assert(g_networking == nullptr); - g_networking = new Networking(); - - assert(g_input == nullptr); - g_input = new Input(); - - // Init python and apply our settings immediately. - // This way we can get started loading stuff in the background - // and it'll come in with the correct texture quality etc. - assert(g_python == nullptr); - g_python = new Python(); - g_python->Reset(true); - - // We're the thread that 'owns' python so we need to wrangle the GIL. - thread->SetOwnsPython(); - } catch (const std::exception& e) { - // If anything went wrong, trigger a deferred error. - // This way it is more likely we can show a fatal error dialog - // since the main thread won't be blocking waiting for us to init. - std::string what = e.what(); - PushCall([what] { - // Just throw a standard exception since our what already - // contains a stack trace; if we throw an Exception we wind - // up with a useless second one. - throw std::logic_error(what.c_str()); - }); - } -} - -void Game::InitSpecialChars() { - std::lock_guard lock(special_char_mutex_); - - special_char_strings_[SpecialChar::kDownArrow] = "\xee\x80\x84"; - special_char_strings_[SpecialChar::kUpArrow] = "\xee\x80\x83"; - special_char_strings_[SpecialChar::kLeftArrow] = "\xee\x80\x81"; - special_char_strings_[SpecialChar::kRightArrow] = "\xee\x80\x82"; - special_char_strings_[SpecialChar::kTopButton] = "\xee\x80\x86"; - special_char_strings_[SpecialChar::kLeftButton] = "\xee\x80\x85"; - special_char_strings_[SpecialChar::kRightButton] = "\xee\x80\x87"; - special_char_strings_[SpecialChar::kBottomButton] = "\xee\x80\x88"; - special_char_strings_[SpecialChar::kDelete] = "\xee\x80\x89"; - special_char_strings_[SpecialChar::kShift] = "\xee\x80\x8A"; - special_char_strings_[SpecialChar::kBack] = "\xee\x80\x8B"; - special_char_strings_[SpecialChar::kLogoFlat] = "\xee\x80\x8C"; - special_char_strings_[SpecialChar::kRewindButton] = "\xee\x80\x8D"; - special_char_strings_[SpecialChar::kPlayPauseButton] = "\xee\x80\x8E"; - special_char_strings_[SpecialChar::kFastForwardButton] = "\xee\x80\x8F"; - special_char_strings_[SpecialChar::kDpadCenterButton] = "\xee\x80\x90"; - - special_char_strings_[SpecialChar::kOuyaButtonO] = "\xee\x80\x99"; - special_char_strings_[SpecialChar::kOuyaButtonU] = "\xee\x80\x9A"; - special_char_strings_[SpecialChar::kOuyaButtonY] = "\xee\x80\x9B"; - special_char_strings_[SpecialChar::kOuyaButtonA] = "\xee\x80\x9C"; - special_char_strings_[SpecialChar::kOuyaLogo] = "\xee\x80\x9D"; - special_char_strings_[SpecialChar::kLogo] = "\xee\x80\x9E"; - special_char_strings_[SpecialChar::kTicket] = "\xee\x80\x9F"; - special_char_strings_[SpecialChar::kGooglePlayGamesLogo] = "\xee\x80\xA0"; - special_char_strings_[SpecialChar::kGameCenterLogo] = "\xee\x80\xA1"; - special_char_strings_[SpecialChar::kDiceButton1] = "\xee\x80\xA2"; - special_char_strings_[SpecialChar::kDiceButton2] = "\xee\x80\xA3"; - special_char_strings_[SpecialChar::kDiceButton3] = "\xee\x80\xA4"; - special_char_strings_[SpecialChar::kDiceButton4] = "\xee\x80\xA5"; - special_char_strings_[SpecialChar::kGameCircleLogo] = "\xee\x80\xA6"; - special_char_strings_[SpecialChar::kPartyIcon] = "\xee\x80\xA7"; - special_char_strings_[SpecialChar::kTestAccount] = "\xee\x80\xA8"; - special_char_strings_[SpecialChar::kTicketBacking] = "\xee\x80\xA9"; - special_char_strings_[SpecialChar::kTrophy1] = "\xee\x80\xAA"; - special_char_strings_[SpecialChar::kTrophy2] = "\xee\x80\xAB"; - special_char_strings_[SpecialChar::kTrophy3] = "\xee\x80\xAC"; - special_char_strings_[SpecialChar::kTrophy0a] = "\xee\x80\xAD"; - special_char_strings_[SpecialChar::kTrophy0b] = "\xee\x80\xAE"; - special_char_strings_[SpecialChar::kTrophy4] = "\xee\x80\xAF"; - special_char_strings_[SpecialChar::kLocalAccount] = "\xee\x80\xB0"; - special_char_strings_[SpecialChar::kAlibabaLogo] = "\xee\x80\xB1"; - - special_char_strings_[SpecialChar::kFlagUnitedStates] = "\xee\x80\xB2"; - special_char_strings_[SpecialChar::kFlagMexico] = "\xee\x80\xB3"; - special_char_strings_[SpecialChar::kFlagGermany] = "\xee\x80\xB4"; - special_char_strings_[SpecialChar::kFlagBrazil] = "\xee\x80\xB5"; - special_char_strings_[SpecialChar::kFlagRussia] = "\xee\x80\xB6"; - special_char_strings_[SpecialChar::kFlagChina] = "\xee\x80\xB7"; - special_char_strings_[SpecialChar::kFlagUnitedKingdom] = "\xee\x80\xB8"; - special_char_strings_[SpecialChar::kFlagCanada] = "\xee\x80\xB9"; - special_char_strings_[SpecialChar::kFlagIndia] = "\xee\x80\xBA"; - special_char_strings_[SpecialChar::kFlagJapan] = "\xee\x80\xBB"; - special_char_strings_[SpecialChar::kFlagFrance] = "\xee\x80\xBC"; - special_char_strings_[SpecialChar::kFlagIndonesia] = "\xee\x80\xBD"; - special_char_strings_[SpecialChar::kFlagItaly] = "\xee\x80\xBE"; - special_char_strings_[SpecialChar::kFlagSouthKorea] = "\xee\x80\xBF"; - special_char_strings_[SpecialChar::kFlagNetherlands] = "\xee\x81\x80"; - - special_char_strings_[SpecialChar::kFedora] = "\xee\x81\x81"; - special_char_strings_[SpecialChar::kHal] = "\xee\x81\x82"; - special_char_strings_[SpecialChar::kCrown] = "\xee\x81\x83"; - special_char_strings_[SpecialChar::kYinYang] = "\xee\x81\x84"; - special_char_strings_[SpecialChar::kEyeBall] = "\xee\x81\x85"; - special_char_strings_[SpecialChar::kSkull] = "\xee\x81\x86"; - special_char_strings_[SpecialChar::kHeart] = "\xee\x81\x87"; - special_char_strings_[SpecialChar::kDragon] = "\xee\x81\x88"; - special_char_strings_[SpecialChar::kHelmet] = "\xee\x81\x89"; - special_char_strings_[SpecialChar::kMushroom] = "\xee\x81\x8A"; - - special_char_strings_[SpecialChar::kNinjaStar] = "\xee\x81\x8B"; - special_char_strings_[SpecialChar::kVikingHelmet] = "\xee\x81\x8C"; - special_char_strings_[SpecialChar::kMoon] = "\xee\x81\x8D"; - special_char_strings_[SpecialChar::kSpider] = "\xee\x81\x8E"; - special_char_strings_[SpecialChar::kFireball] = "\xee\x81\x8F"; - - special_char_strings_[SpecialChar::kFlagUnitedArabEmirates] = "\xee\x81\x90"; - special_char_strings_[SpecialChar::kFlagQatar] = "\xee\x81\x91"; - special_char_strings_[SpecialChar::kFlagEgypt] = "\xee\x81\x92"; - special_char_strings_[SpecialChar::kFlagKuwait] = "\xee\x81\x93"; - special_char_strings_[SpecialChar::kFlagAlgeria] = "\xee\x81\x94"; - special_char_strings_[SpecialChar::kFlagSaudiArabia] = "\xee\x81\x95"; - special_char_strings_[SpecialChar::kFlagMalaysia] = "\xee\x81\x96"; - special_char_strings_[SpecialChar::kFlagCzechRepublic] = "\xee\x81\x97"; - special_char_strings_[SpecialChar::kFlagAustralia] = "\xee\x81\x98"; - special_char_strings_[SpecialChar::kFlagSingapore] = "\xee\x81\x99"; - - special_char_strings_[SpecialChar::kOculusLogo] = "\xee\x81\x9A"; - special_char_strings_[SpecialChar::kSteamLogo] = "\xee\x81\x9B"; - special_char_strings_[SpecialChar::kNvidiaLogo] = "\xee\x81\x9C"; - - special_char_strings_[SpecialChar::kFlagIran] = "\xee\x81\x9D"; - special_char_strings_[SpecialChar::kFlagPoland] = "\xee\x81\x9E"; - special_char_strings_[SpecialChar::kFlagArgentina] = "\xee\x81\x9F"; - special_char_strings_[SpecialChar::kFlagPhilippines] = "\xee\x81\xA0"; - special_char_strings_[SpecialChar::kFlagChile] = "\xee\x81\xA1"; - - special_char_strings_[SpecialChar::kMikirog] = "\xee\x81\xA2"; -} - -void Game::SetGameRoster(cJSON* r) { - if (game_roster_ != nullptr) { - cJSON_Delete(game_roster_); - } - game_roster_ = r; -} - -void Game::ResetActivityTracking() { - largest_draw_time_increment_since_last_reset_ = 0; - first_draw_real_time_ = last_draw_real_time_ = g_platform->GetTicks(); -} - -void Game::RegisterClientController(ClientControllerInterface* c) { - // This shouldn't happen, but if there's already a controller registered, - // detach all clients from it. - if (client_controller_) { - Log("RegisterClientController() called " - "but already have a controller; bad."); - for (auto&& i : connections_to_clients_) { - assert(i.second.exists()); - i.second->SetController(nullptr); - } - } - - // Ok, now assign the new and attach all currently-connected clients to it. - client_controller_ = c; - if (client_controller_) { - for (auto&& i : connections_to_clients_) { - assert(i.second.exists()); - if (i.second->can_communicate()) { - i.second->SetController(client_controller_); - } - } - } -} - -void Game::UnregisterClientController(ClientControllerInterface* c) { - assert(c); - - // This shouldn't happen. - if (client_controller_ != c) { - Log("UnregisterClientController() called with a non-registered " - "controller"); - return; - } - - // Ok, detach all our controllers from this guy. - if (client_controller_) { - for (auto&& i : connections_to_clients_) { - i.second->SetController(nullptr); - } - } - client_controller_ = nullptr; -} - -#if BA_VR_BUILD - -void Game::PushVRHandsState(const VRHandsState& state) { - PushCall([this, state] { vr_hands_state_ = state; }); -} - -#endif // BA_VR_BUILD - -void Game::PushMediaPruneCall(int level) { - PushCall([level] { - assert(InGameThread()); - g_media->Prune(level); - }); -} - -void Game::PushSetAccountTokenCall(const std::string& account_id, - const std::string& token) { - PushCall( - [account_id, token] { g_account->SetAccountToken(account_id, token); }); -} - -void Game::PushSetAccountCall(AccountType account_type, - AccountState account_state, - const std::string& account_name, - const std::string& account_id) { - PushCall([this, account_type, account_state, account_name, account_id] { - g_account->SetAccount(account_type, account_state, account_name, - account_id); - }); -} - -void Game::PushInitialScreenCreatedCall() { - PushCall([this] { InitialScreenCreated(); }); -} - -void Game::InitialScreenCreated() { - assert(InGameThread()); - - // Ok; graphics-server is telling us we've got a screen. - - // We can now let the media thread go to town pre-loading system media - // while we wait. - g_media->LoadSystemMedia(); - - // FIXME: ideally we should create this as part of bootstrapping, but - // we need it to be possible to load textures/etc. before the renderer - // exists. - if (!HeadlessMode()) { - assert(!g_app_globals->console); - g_app_globals->console = new Console(); - } - - // Set up our timers. - process_timer_ = - NewThreadTimer(0, true, NewLambdaRunnable([this] { Process(); })); - media_prune_timer_ = - NewThreadTimer(2345, true, NewLambdaRunnable([this] { Prune(); })); - - // Normally we schedule updates when we're asked to draw a frame. - // In headless mode, however, we're not drawing, so we need a dedicated - // timer to take its place. - if (HeadlessMode()) { - headless_update_timer_ = - NewThreadTimer(8, true, NewLambdaRunnable([this] { Update(); })); - } - - RunAppLaunchCommands(); -} - -void Game::PushPurchaseTransactionCall(const std::string& item, - const std::string& receipt, - const std::string& signature, - const std::string& order_id, - bool user_initiated) { - PushCall([this, item, receipt, signature, order_id, user_initiated] { - PurchaseTransaction(item, receipt, signature, order_id, user_initiated); - }); -} - -void Game::PurchaseTransaction(const std::string& item, - const std::string& receipt, - const std::string& signature, - const std::string& order_id, - bool user_initiated) { - assert(InGameThread()); - g_python->AddPurchaseTransaction(item, receipt, signature, order_id, - user_initiated); -} - -void Game::Prune() { g_media->Prune(); } - -void Game::PushAdViewCompleteCall(const std::string& purpose, - bool actually_showed) { - PushCall([this, purpose, actually_showed] { - AdViewComplete(purpose, actually_showed); - }); -} - -void Game::AdViewComplete(const std::string& purpose, bool actually_showed) { - assert(InGameThread()); - CallAdCompletionCall(actually_showed); - - // If they *actually* viewed an ad, and it was a between-game ad, inform the - // user that they can disable them. - if (purpose == "between_game" && actually_showed) { - g_python->obj(Python::ObjID::kRemoveInGameAdsMessageCall).Call(); - } - RunGeneralAdComplete(actually_showed); -} - -void Game::RunGeneralAdComplete(bool actually_showed) { - assert(InGameThread()); - PythonRef ad_complete_call = - g_python->obj(Python::ObjID::kAccountClient).GetAttr("ad_complete"); - if (ad_complete_call.exists()) { - PythonRef args(Py_BuildValue("(Oi)", actually_showed ? Py_True : Py_False, - static_cast(g_platform->GetTicks() - - last_ad_start_time_)), - PythonRef::kSteal); - ad_complete_call.Call(args); - } else { - Log("Error on ad-complete call"); - } -} - -void Game::PushAnalyticsCall(const std::string& type, int increment) { - PushCall([this, type, increment] { Analytics(type, increment); }); -} - -void Game::Analytics(const std::string& type, int increment) { - assert(InGameThread()); - g_python->HandleAnalytics(type, increment); -} - -void Game::PushAwardAdTicketsCall() { - PushCall([this] { AwardAdTickets(); }); -} - -void Game::AwardAdTickets() { - try { - PythonRef add_transaction = - g_python->obj(Python::ObjID::kAccountClient).GetAttr("add_transaction"); - PythonRef args(Py_BuildValue("({ss})", "type", "AWARD_AD_TICKETS"), - PythonRef::kSteal); - if (add_transaction.exists()) add_transaction.Call(args); - g_python->RunTransactions(); - } catch (const std::exception& e) { - Log("Error in AwardAdTicketsMessage: " + std::string(e.what())); - } -} - -void Game::PushAwardAdTournamentEntryCall() { - PushCall([this] { AwardAdTournamentEntry(); }); -} - -void Game::AwardAdTournamentEntry() { - try { - PythonRef add_transaction = - g_python->obj(Python::ObjID::kAccountClient).GetAttr("add_transaction"); - PythonRef args(Py_BuildValue("({ss})", "type", "AWARD_AD_TOURNAMENT_ENTRY"), - PythonRef::kSteal); - if (add_transaction.exists()) add_transaction.Call(args); - g_python->RunTransactions(); - } catch (const std::exception& e) { - Log("Error in AwardAdTournamentEntryMessage: " + std::string(e.what())); - } -} - -// Launch into main menu or whatever else. -void Game::RunAppLaunchCommands() { - assert(!ran_app_launch_commands_); - - // First off, run our python app-launch call. - { - // Run this in the UI context. - ScopedSetContext cp(GetUIContext()); - g_python->obj(Python::ObjID::kOnAppLaunchCall).Call(); - } - - // If we were passed launch command args, run them. - if (!g_app_globals->game_commands.empty()) { - bool success = PythonCommand(g_app_globals->game_commands, BA_BCFN).Run(); - if (!success) { - exit(1); - } - } - - // If the stuff we just ran didn't result in a session, create a default one. - if (!foreground_session_.exists()) { - RunMainMenu(); - } - - UpdateProcessTimer(); - - ran_app_launch_commands_ = true; -} - -Game::~Game() = default; - -// Set up our sleeping based on what we're doing. -void Game::UpdateProcessTimer() { - assert(InGameThread()); - - // This might get called before we set up our timer in some cases. (such as - // very early) should be safe to ignore since we update the interval - // explicitly after creating the timers. - if (!process_timer_) return; - - // If there's loading to do, keep at it rather vigorously. - if (have_pending_loads_) { - assert(process_timer_); - process_timer_->SetLength(1); - } else { - // Otherwise we've got nothing to do; go to sleep until something changes. - assert(process_timer_); - process_timer_->SetLength(-1); - } -} - -void Game::PruneSessions() { - bool have_dead_session = false; - for (auto&& i : sessions_) { - if (i.exists()) { - // If this session is no longer foreground and is ready to die, kill it. - if (i.exists() && i.get() != foreground_session_.get()) { - try { - i.Clear(); - } catch (const std::exception& e) { - Log("Exception killing Session: " + std::string(e.what())); - } - have_dead_session = true; - } - } else { - have_dead_session = true; - } - } - if (have_dead_session) { - std::vector > live_list; - for (auto&& i : sessions_) { - if (i.exists()) { - live_list.push_back(i); - } - } - sessions_.swap(live_list); - } -} - -void Game::SetClientInfoFromMasterServer(const std::string& client_token, - PyObject* info_obj) { - // NOLINTNEXTLINE (python doing bitwise math on signed int) - if (!PyDict_Check(info_obj)) { - Log("got non-dict for master-server client info for token " + client_token - + ": " + Python::ObjToString(info_obj)); - return; - } - for (ConnectionToClient* client : GetConnectionsToClients()) { - if (client->token() == client_token) { - client->HandleMasterServerClientInfo(info_obj); - - // Roster will now include account-id... - game_roster_dirty_ = true; - break; - } - } -} - -void Game::UpdateKickVote() { - if (!kick_vote_in_progress_) { - return; - } - ConnectionToClient* kick_vote_starter = kick_vote_starter_.get(); - ConnectionToClient* kick_vote_target = kick_vote_target_.get(); - - // If the target is no longer with us, silently end. - if (kick_vote_target == nullptr) { - kick_vote_in_progress_ = false; - return; - } - millisecs_t current_time = GetRealTime(); - int total_client_count = 0; - int yes_votes = 0; - int no_votes = 0; - - // Tally current votes for connected clients; if anything has changed, print - // the update and possibly perform the kick. - for (ConnectionToClient* client : GetConnectionsToClients()) { - ++total_client_count; - if (client->kick_voted_) { - if (client->kick_vote_choice_) { - ++yes_votes; - } else { - ++no_votes; - } - } - } - bool vote_failed = false; - - // If we've fallen below the minimum necessary voters or time has run out, - // fail. - if (total_client_count < kKickVoteMinimumClients) { - vote_failed = true; - } - if (current_time > kick_vote_end_time_) { - vote_failed = true; - } - - if (vote_failed) { - SendScreenMessageToClients(R"({"r":"kickVoteFailedText"})", 1, 1, 0); - kick_vote_in_progress_ = false; - - // Disallow kicking for a while for everyone.. but ESPECIALLY so for the guy - // who launched the failed vote. - for (ConnectionToClient* client : GetConnectionsToClients()) { - millisecs_t delay = kKickVoteFailRetryDelay; - if (client == kick_vote_starter) { - delay += kKickVoteFailRetryDelayInitiatorExtra; - } - client->next_kick_vote_allow_time_ = - std::max(client->next_kick_vote_allow_time_, current_time + delay); - } - } else { - int votes_required; - switch (total_client_count) { - case 1: - case 2: - votes_required = 2; // Shouldn't actually be possible. - break; - case 3: - votes_required = HeadlessMode() ? 2 : 3; - break; - case 4: - votes_required = 3; - break; - case 5: - votes_required = HeadlessMode() ? 3 : 4; - break; - case 6: - votes_required = 4; - break; - case 7: - votes_required = HeadlessMode() ? 4 : 5; - break; - default: - votes_required = total_client_count - 3; - break; - } - int votes_needed = votes_required - yes_votes; - if (votes_needed <= 0) { - // ZOMG the vote passed; perform the kick. - SendScreenMessageToClients( - R"({"r":"kickOccurredText","s":[["${NAME}",)" - + Utils::GetJSONString(kick_vote_target->GetCombinedSpec() - .GetDisplayString() - .c_str()) - + "]]}", - 1, 1, 0); - kick_vote_in_progress_ = false; - DisconnectClient(kick_vote_target->id(), kKickBanSeconds); - - } else if (votes_needed != last_kick_votes_needed_) { - last_kick_votes_needed_ = votes_needed; - SendScreenMessageToClients(R"({"r":"votesNeededText","s":[["${NUMBER}",")" - + std::to_string(votes_needed) + "\"]]}", - 1, 1, 0); - } - } -} - -// Bring our scenes, real-time timers, etc up to date. -void Game::Update() { - assert(InGameThread()); - millisecs_t real_time = GetRealTime(); - g_platform->SetDebugKey("LastUpdateTime", - std::to_string(Platform::GetCurrentMilliseconds())); - if (first_update_) { - master_time_offset_ = master_time_ - real_time; - first_update_ = false; - } - in_update_ = true; - g_input->Update(); - UpdateKickVote(); - - // Send the game roster to our clients if it's changed recently. - if (game_roster_dirty_) { - if (real_time > last_game_roster_send_time_ + 2500) { - // Now send it to all connected clients. - std::vector msg = GetGameRosterMessage(); - for (auto&& c : GetConnectionsToClients()) { - c->SendReliableMessage(msg); - } - game_roster_dirty_ = false; - last_game_roster_send_time_ = real_time; - } - } - - // First do housekeeping on our client/host connections. - for (auto&& i : connections_to_clients_) { - BA_IFDEBUG(Object::WeakRef test_ref(i.second)); - i.second->Update(); - - // Make sure the connection didn't kill itself in the update. - assert(test_ref.exists()); - } - - if (connection_to_host_.exists()) { - connection_to_host_->Update(); - } - - // Ok, here's the deal: - // This is where we regulate the speed of everything that's running under us - // (sessions, activities, frame_def-creation, etc) - // we have a master_time which we try to have match real-time as closely - // as possible (unless we physically aren't fast enough to get everything - // done, in which case it'll be slower). We also increment our underlying - // machinery in 8ms increments (1/120 of a second) and try to do 2 updates - // each time we're called, since we're usually being called in a 60hz refresh - // cycle and that'll line our draws up perfectly with our sim steps. - - // TODO(ericf): On modern systems (VR and otherwise) we'll see 80hz, 90hz, - // 120hz, 240hz, etc. It would be great to generalize this to gravitate - // towards clean step patterns in all cases, not just the 60hz and 90hz cases - // we handle now. In general we want stuff like 1,1,2,1,1,2,1,1,2, not - // 1,1,1,2,1,2,2,1,1. - - // Figure out where our net-time *should* be getting to to match real-time. - millisecs_t target_master_time = real_time + master_time_offset_; - millisecs_t amount_behind = target_master_time - master_time_; - - // Normally we assume 60hz so we gravitate towards 2 steps per update to line - // up with our 120hz update timing. - int target_steps = 2; - -#if BA_RIFT_BUILD - // On Rift VR mode we're running 90hz, so lets aim for 1/2/1/2 steps to hit - // our 120hz target. - if (IsVRMode()) { - target_steps = rift_step_index_ + 1; - rift_step_index_ = !rift_step_index_; - } -#endif // BA_RIFT_BUILD - - // Ideally we should be behind by 16 (or 8 for single steps); if its - // *slightly* more than that, let our timing slip a tiny bit to maintain sync. - // This lets us match framerates that are a tiny bit slower than 60hz, such as - // seems to be the case with the Gear VR. - if (amount_behind > 16) { - master_time_offset_ -= 1; - - //.. and recalc these.. - target_master_time = real_time + master_time_offset_; - amount_behind = target_master_time - master_time_; - } - - // if we've fallen behind by a lot, just cut our losses - if (amount_behind > 50) { - master_time_offset_ -= (amount_behind - 50); - target_master_time = real_time + master_time_offset_; - } - - // min/max net-time targets we can aim for; gives us about a steps worth of - // wiggle room to try and keep our exact target cadence - millisecs_t min_target_master_time = - target_master_time >= 8 ? (target_master_time - 8) : 0; - millisecs_t max_target_master_time = target_master_time + 8; - - // run up our real-time timers - realtimers_->Run(real_time); - - // Run session updates until we catch up with projected base time (or run out - // of time). - int step = 1; - - while (true) { - // Try to stick to our target step count whenever possible, but if we get - // too far off target we may need to bail earlier/later. - if (step > target_steps) { - // As long as we're within a step of where we should be, bail now. - if (master_time_ >= min_target_master_time) break; - } else { - // If we've gone too far already, bail. - if (master_time_ >= max_target_master_time) { - // Log("BAILING EARLY"); - // On rift if this is a 2-step and we bailed after 1, aim for 2 again - // next time (otherwise we'll always get 3 singles in a row when this - // happens). -#if BA_RIFT_BUILD - if (IsVRMode() && target_steps == 2 && step == 2) { - rift_step_index_ = !rift_step_index_; - } -#endif // BA_RIFT_BUILD - break; - } - } - - // Update our UI scene/etc. - g_ui->Update(8); - - // Update all of our sessions. - for (auto&& i : sessions_) { - assert(i.exists()); - i->Update(8); - } - - last_session_update_master_time_ = master_time_; - - // Go ahead and prune dead ones. - PruneSessions(); - - // Advance master time.. - master_time_ += 8; - - // Bail if we spend too much time in here. - millisecs_t new_real_time = GetRealTime(); - if (new_real_time - real_time > 30) { - break; - } - step++; - } - in_update_ = false; -} - -// Reset the game to a blank slate. -void Game::Reset() { - assert(InGameThread()); - - // Tear down any existing setup. - // This should allow high-level objects to die gracefully. - assert(g_python->inited()); - - // Tear down our existing session. - foreground_session_.Clear(); - PruneSessions(); - - // If all is well our sessions should all be dead. - if (g_app_globals->session_count != 0) { - Log("Error: session-count is non-zero (" - + std::to_string(g_app_globals->session_count) + ") on Game::Reset."); - } - - // Note: we don't clear real-time timers anymore. Should we?.. - g_ui->Reset(); - g_input->Reset(); - g_graphics->Reset(); - g_python->Reset(); - g_audio->Reset(); - - if (!HeadlessMode()) { - // If we haven't, send a first frame_def to the graphics thread to kick - // things off (it'll start sending us requests for more after it gets the - // first). - if (!have_sent_initial_frame_def_) { - g_graphics->BuildAndPushFrameDef(); - have_sent_initial_frame_def_ = true; - } - } -} - -auto Game::IsInUIContext() const -> bool { - return (g_ui && Context::current().target.get() == g_ui); -} - -void Game::PushShowURLCall(const std::string& url) { - PushCall([url] { - assert(InGameThread()); - assert(g_python); - g_python->ShowURL(url); - }); -} - -auto Game::GetForegroundContext() -> Context { - Session* s = GetForegroundSession(); - if (s) { - return s->GetForegroundContext(); - } else { - return Context(); - } -} - -void Game::PushBackButtonCall(InputDevice* input_device) { - PushCall([this, input_device] { - assert(InGameThread()); - - // Ignore if UI isn't up yet. - if (!g_ui || !g_ui->overlay_root_widget() || !g_ui->screen_root_widget()) { - return; - } - - // If there's a UI up, send along a cancel message. - if (g_ui->overlay_root_widget()->GetChildCount() != 0 - || g_ui->screen_root_widget()->GetChildCount() != 0) { - g_ui->root_widget()->HandleMessage( - WidgetMessage(WidgetMessage::Type::kCancel)); - } else { - // If there's no main screen or overlay windows, ask for a menu owned by - // this device. - MainMenuPress(input_device); - } - }); -} - -void Game::PushStringEditSetCall(const std::string& value) { - PushCall([value] { - if (!g_ui) { - Log("Error: No ui on StringEditSetEvent."); - return; - } -#if BA_OSTYPE_ANDROID - TextWidget* w = TextWidget::GetAndroidStringEditWidget(); - if (w) { - w->SetText(value); - } -#else - throw Exception(); // Shouldn't get here. -#endif - }); -} - -void Game::PushStringEditCancelCall() { - PushCall([] { - if (!g_ui) { - Log("Error: No ui in PushStringEditCancelCall."); - return; - } - }); -} - -// Called by a newly made Session instance to set itself as the current -// session. -void Game::SetForegroundSession(Session* s) { - assert(InGameThread()); - foreground_session_ = s; -} - -void Game::SetForegroundScene(Scene* sg) { - assert(InGameThread()); - if (foreground_scene_.get() != sg) { - foreground_scene_ = sg; - - // If this scene has a globals-node, put it in charge of stuff. - if (GlobalsNode* g = sg->globals_node()) { - g->SetAsForeground(); - } - } -} - -void Game::LaunchClientSession() { - if (in_update_) { - throw Exception( - "can't launch a session from within a session update; use " - "ba.pushcall()"); - } - assert(InGameThread()); - - // Don't want to pick up any old stuff in here. - ScopedSetContext cp(nullptr); - - // This should kill any current session and get us back to a blank slate. - Reset(); - - // Create the new session. - Object::WeakRef old_foreground_session(foreground_session_); - try { - auto s(Object::New()); - sessions_.push_back(s); - - // It should have set itself as FG. - assert(foreground_session_ == s); - } catch (const std::exception& e) { - // If it failed, restore the previous current session and re-throw. - SetForegroundSession(old_foreground_session.get()); - throw Exception(std::string("HostSession failed: ") + e.what()); - } -} - -void Game::LaunchReplaySession(const std::string& file_name) { - if (in_update_) - throw Exception( - "can't launch a session from within a session update; use " - "ba.pushcall()"); - - assert(InGameThread()); - - // Don't want to pick up any old stuff in here. - ScopedSetContext cp(nullptr); - - // This should kill any current session and get us back to a blank slate. - Reset(); - - // Create the new session. - Object::WeakRef old_foreground_session(foreground_session_); - try { - auto s(Object::New(file_name)); - sessions_.push_back(s); - - // It should have set itself as FG. - assert(foreground_session_ == s); - } catch (const std::exception& e) { - // If it failed, restore the previous current session and re-throw the - // exception. - SetForegroundSession(old_foreground_session.get()); - throw Exception(std::string("HostSession failed: ") + e.what()); - } -} - -void Game::LaunchHostSession(PyObject* session_type_obj, - BenchmarkType benchmark_type) { - if (in_update_) { - throw Exception( - "can't call host_session() from within session update; use " - "ba.pushcall()"); - } - - assert(InGameThread()); - - // If for some reason we're still attached to a host, kill the connection. - if (connection_to_host_.exists()) { - Log("Had host-connection during LaunchHostSession(); shouldn't happen."); - connection_to_host_->RequestDisconnect(); - connection_to_host_.Clear(); - has_connection_to_host_ = false; - UpdateGameRoster(); - } - - // Don't want to pick up any old stuff in here. - ScopedSetContext cp(nullptr); - - // This should kill any current session and get us back to a blank slate. - Reset(); - - Object::WeakRef old_foreground_session(foreground_session_); - try { - // Create the new session. - auto s(Object::New(session_type_obj)); - s->set_benchmark_type(benchmark_type); - sessions_.emplace_back(s); - - // It should have set itself as FG. - assert(foreground_session_ == s); - } catch (const std::exception& e) { - // If it failed, restore the previous session context and re-throw the - // exception. - SetForegroundSession(old_foreground_session.get()); - throw Exception(std::string("HostSession failed: ") + e.what()); - } -} - -void Game::RunMainMenu() { - PushCall([this] { - if (g_app_globals->shutting_down) { - return; - } - assert(g_python); - assert(InGameThread()); - PythonRef result = - g_python->obj(Python::ObjID::kLaunchMainMenuSessionCall).Call(); - if (!result.exists()) { - throw Exception("error running main menu"); - } - }); -} - -// Commands run via the in-game console. These are a bit more 'casual' and run -// in the current visible context. - -void Game::PushInGameConsoleScriptCommand(const std::string& command) { - PushCall([this, command] { - // These are always run in whichever context is 'visible'. - ScopedSetContext cp(GetForegroundContext()); - PythonCommand cmd(command, ""); - if (!g_app_globals->user_ran_commands) { - g_app_globals->user_ran_commands = true; - } - if (cmd.CanEval()) { - PyObject* obj = cmd.RunReturnObj(true); - if (obj && obj != Py_None) { - PyObject* s = PyObject_Repr(obj); - if (s) { - const char* c = PyUnicode_AsUTF8(s); - if (g_app_globals->console) { - g_app_globals->console->Print(std::string(c) + "\n"); - } - Py_DECREF(s); - } - Py_DECREF(obj); - } - } else { - // Not eval-able; just run it. - cmd.Run(); - } - }); -} - -// Commands run via stdin. -void Game::PushStdinScriptCommand(const std::string& command) { - PushCall([this, command] { - // These are always run in whichever context is 'visible'. - ScopedSetContext cp(GetForegroundContext()); - PythonCommand cmd(command, ""); - if (!g_app_globals->user_ran_commands) { - g_app_globals->user_ran_commands = true; - } - - // Eval this if possible (so we can possibly print return value). - if (cmd.CanEval()) { - if (PyObject* obj = cmd.RunReturnObj(true)) { - // Print the value if we're running directly from a terminal - // (or being run under the server-manager) - if ((IsStdinATerminal() || g_app->server_wrapper_managed()) - && obj != Py_None) { - PyObject* s = PyObject_Repr(obj); - if (s) { - const char* c = PyUnicode_AsUTF8(s); - printf("%s\n", c); - fflush(stdout); - Py_DECREF(s); - } - } - Py_DECREF(obj); - } - } else { - // Can't eval it; just run it. - cmd.Run(); - } - }); -} - -void Game::PushInterruptSignalCall() { - PushCall([this] { - assert(InGameThread()); - - // Special case; when running under the server-wrapper, we completely - // ignore interrupt signals (the wrapper acts on them). - if (g_app->server_wrapper_managed()) { - return; - } - - // Just go through _ba.quit() - // FIXME: Shouldn't need to go out to the python layer here... - g_python->obj(Python::ObjID::kQuitCall).Call(); - }); -} - -void Game::PushAskUserForTelnetAccessCall() { - PushCall([this] { - assert(InGameThread()); - ScopedSetContext cp(GetUIContext()); - g_python->obj(Python::ObjID::kTelnetAccessRequestCall).Call(); - }); -} - -void Game::HandleThreadPause() { - // Give userspace python stuff a chance to pause. - ScopedSetContext cp(GetUIContextTarget()); - g_python->obj(Python::ObjID::kOnAppPauseCall).Call(); - - // Tell our account client to commit any outstanding changes to disk. - g_python->CommitLocalData(); -} - -// void Game::PushTelnetScriptCommand(const std::string& command) { -// PushCall([this, command] { -// // These are always run in whichever context is 'visible'. -// ScopedSetContext cp(GetForegroundContext()); -// if (!g_app_globals->user_ran_commands) { -// g_app_globals->user_ran_commands = true; -// } -// PythonCommand cmd(command, ""); -// if (cmd.CanEval()) { -// PyObject* obj = cmd.RunReturnObj(true); -// if (obj && obj != Py_None) { -// PyObject* s = PyObject_Repr(obj); -// if (s) { -// const char* c = PyUnicode_AsUTF8(s); -// PushTelnetPrintCall(std::string(c) + "\n"); -// Py_DECREF(s); -// } -// Py_DECREF(obj); -// } -// } else { -// // Not eval-able; just run it. -// cmd.Run(); -// } -// PushTelnetPrintCall("ballisticacore> "); -// }); -// } - -// void Game::PushTelnetPrintCall(const std::string& message) { -// PushCall([message] { -// if (g_app_globals->telnet_server) { -// g_app_globals->telnet_server->Print(message); -// } -// }); -// } - -void Game::PushPythonCall(const Object::Ref& call) { - // Since we're mucking with refs, need to limit to game thread. - BA_PRECONDITION(InGameThread()); - BA_PRECONDITION(call->object_strong_ref_count() > 0); - PushCall([call] { - assert(call.exists()); - call->Run(); - }); -} - -void Game::PushPythonCallArgs(const Object::Ref& call, - const PythonRef& args) { - // Since we're mucking with refs, need to limit to game thread. - BA_PRECONDITION(InGameThread()); - BA_PRECONDITION(call->object_strong_ref_count() > 0); - PushCall([call, args] { - assert(call.exists()); - call->Run(args.get()); - }); -} - -void Game::PushPythonWeakCall(const Object::WeakRef& call) { - // Since we're mucking with refs, need to limit to game thread. - BA_PRECONDITION(InGameThread()); - - // Even though we only hold a weak ref, we expect a valid strong-reffed - // object to be passed in. - assert(call.exists() && call->object_strong_ref_count() > 0); - - PushCall([call] { - if (call.exists()) { - Python::ScopedCallLabel label("PythonWeakCallMessage"); - call->Run(); - } - }); -} - -void Game::PushPythonWeakCallArgs( - const Object::WeakRef& call, const PythonRef& args) { - // Since we're mucking with refs, need to limit to game thread. - BA_PRECONDITION(InGameThread()); - - // Even though we only hold a weak ref, we expect a valid strong-reffed - // object to be passed in. - assert(call.exists() && call->object_strong_ref_count() > 0); - - PushCall([call, args] { - if (call.exists()) call->Run(args.get()); - }); -} - -void Game::PushPythonRawCallable(PyObject* callable) { - PushCall([this, callable] { - assert(InGameThread()); - - // Lets run this in the UI context. - // (can add other options if we need later) - ScopedSetContext cp(GetUIContext()); - - // This event contains a raw python obj with an incremented ref-count. - auto call(Object::New(callable)); - Py_DECREF(callable); // now just held by call - - call->Run(); - }); -} - -void Game::PushScreenMessage(const std::string& message, - const Vector3f& color) { - PushCall([message, color] { g_graphics->AddScreenMessage(message, color); }); -} - -void Game::SetReplaySpeedExponent(int val) { - replay_speed_exponent_ = std::min(3, std::max(-3, val)); - replay_speed_mult_ = powf(2.0f, static_cast(replay_speed_exponent_)); -} - -void Game::SetDebugSpeedExponent(int val) { - debug_speed_exponent_ = val; - debug_speed_mult_ = powf(2.0f, static_cast(debug_speed_exponent_)); - - Session* s = GetForegroundSession(); - if (s) s->DebugSpeedMultChanged(); -} - -void Game::ChangeGameSpeed(int offs) { - assert(InGameThread()); - - // if we're in a replay session, adjust playback speed there - if (dynamic_cast(GetForegroundSession())) { - int old_speed = replay_speed_exponent(); - SetReplaySpeedExponent(replay_speed_exponent() + offs); - if (old_speed != replay_speed_exponent()) { - ScreenMessage( - "{\"r\":\"watchWindow.playbackSpeedText\"," - "\"s\":[[\"${SPEED}\",\"" - + std::to_string(replay_speed_mult()) + "\"]]}"); - } - return; - } - // Otherwise, in debug build, we allow speeding/slowing anything. -#if BA_DEBUG_BUILD - debug_speed_exponent_ += offs; - debug_speed_mult_ = powf(2.0f, static_cast(debug_speed_exponent_)); - ScreenMessage("DEBUG GAME SPEED TO " + std::to_string(debug_speed_mult_)); - Session* s = GetForegroundSession(); - if (s) { - s->DebugSpeedMultChanged(); - } -#endif // BA_DEBUG_BUILD -} - -auto Game::GetUIContext() const -> Context { - return Context(GetUIContextTarget()); -} - -void Game::PushToggleManualCameraCall() { - PushCall([] { g_graphics->ToggleManualCamera(); }); -} - -void Game::PushToggleDebugInfoDisplayCall() { - PushCall([] { g_graphics->ToggleDebugInfoDisplay(); }); -} - -void Game::PushToggleCollisionGeometryDisplayCall() { - PushCall([] { g_graphics->ToggleDebugDraw(); }); -} - -void Game::PushMainMenuPressCall(InputDevice* device) { - PushCall([this, device] { MainMenuPress(device); }); -} - -void Game::MainMenuPress(InputDevice* device) { - assert(InGameThread()); - g_python->HandleDeviceMenuPress(device); -} - -void Game::PushScreenResizeCall(float virtual_width, float virtual_height, - float pixel_width, float pixel_height) { - PushCall([=] { - ScreenResize(virtual_width, virtual_height, pixel_width, pixel_height); - }); -} - -void Game::ScreenResize(float virtual_width, float virtual_height, - float pixel_width, float pixel_height) { - assert(InGameThread()); - assert(g_graphics != nullptr); - if (g_graphics) { - g_graphics->ScreenResize(virtual_width, virtual_height, pixel_width, - pixel_height); - } - if (g_ui) { - g_ui->ScreenSizeChanged(); - } - if (Session* session = GetForegroundSession()) { - session->ScreenSizeChanged(); - } -} - -void Game::PushGameServiceAchievementListCall( - const std::set& achievements) { - PushCall([this, achievements] { GameServiceAchievementList(achievements); }); -} - -void Game::GameServiceAchievementList( - const std::set& achievements) { - assert(g_python); - assert(InGameThread()); - g_python->DispatchRemoteAchievementList(achievements); -} - -void Game::PushScoresToBeatResponseCall(bool success, - const std::list& scores, - void* py_callback) { - PushCall([this, success, scores, py_callback] { - ScoresToBeatResponse(success, scores, py_callback); - }); -} - -void Game::ScoresToBeatResponse(bool success, - const std::list& scores, - void* py_callback) { - assert(g_python); - assert(InGameThread()); - g_python->DispatchScoresToBeatResponse(success, scores, py_callback); -} - -void Game::PushPlaySoundCall(SystemSoundID sound) { - PushCall([sound] { g_audio->PlaySound(g_media->GetSound(sound)); }); -} - -void Game::PushFriendScoreSetCall(const FriendScoreSet& score_set) { - PushCall([score_set] { g_python->HandleFriendScoresCB(score_set); }); -} - -void Game::PushConfirmQuitCall() { - PushCall([this] { - assert(InGameThread()); - if (HeadlessMode()) { - Log("PushConfirmQuitCall() unhandled on headless."); - } else { - // If input is locked, just quit immediately.. a confirm screen wouldn't - // work anyway - if (g_input->IsInputLocked() - || (g_app_globals->console != nullptr - && g_app_globals->console->active())) { - // Just go through _ba.quit() - // FIXME: Shouldn't need to go out to the python layer here... - g_python->obj(Python::ObjID::kQuitCall).Call(); - return; - } else { - // this needs to be run in the UI context - ScopedSetContext cp(GetUIContextTarget()); - - g_audio->PlaySound(g_media->GetSound(SystemSoundID::kSwish)); - g_python->obj(Python::ObjID::kQuitWindowCall).Call(); - - // if we have a keyboard, give it UI ownership - InputDevice* keyboard = g_input->keyboard_input(); - if (keyboard) { - g_ui->SetUIInputDevice(keyboard); - } - } - } - }); -} - -void Game::Draw() { - g_graphics->BuildAndPushFrameDef(); - - // Now bring the game up to date. - // By doing this *after* shipping a new frame_def we're reducing the - // chance of frame drops at the expense of adding a bit of visual latency. - // Could maybe try to be smart about which to do first, but not sure - // if its worth it. - Update(); - - // Update our cheat tests. - millisecs_t now = g_platform->GetTicks(); - millisecs_t elapsed = now - last_draw_real_time_; - if (elapsed > largest_draw_time_increment_since_last_reset_) { - largest_draw_time_increment_since_last_reset_ = elapsed; - } - last_draw_real_time_ = now; - - // Sanity test: can make sure our scene is taking exactly 2 steps - // per frame here.. (should generally be the case on 60hz devices). - if (explicit_bool(false)) { - static int64_t last_step = 0; - HostActivity* ha = GetForegroundContext().GetHostActivity(); - if (ha) { - int64_t step = ha->scene()->stepnum(); - Log(std::to_string(step - last_step)); - last_step = step; - } - } -} - -void Game::PushFrameDefRequest() { - PushCall([this] { Draw(); }); -} - -void Game::PushOnAppResumeCall() { - PushCall([] { - // Wipe out whatever input device was in control of the UI. - assert(g_ui); - g_ui->SetUIInputDevice(nullptr); - }); -} - -// Look through everything in our config dict and act on it. -void Game::ApplyConfig() { - assert(InGameThread()); - - // Not relevant for fullscreen anymore - // since we're fullscreen windows everywhere. - int width = 800; - int height = 600; - - // Texture quality. - TextureQuality texture_quality_requested; - std::string texqualstr = - g_app_config->Resolve(AppConfig::StringID::kTextureQuality); - - if (texqualstr == "Auto") { - texture_quality_requested = TextureQuality::kAuto; - } else if (texqualstr == "High") { - texture_quality_requested = TextureQuality::kHigh; - } else if (texqualstr == "Medium") { - texture_quality_requested = TextureQuality::kMedium; - } else if (texqualstr == "Low") { - texture_quality_requested = TextureQuality::kLow; - } else { - Log("Invalid texture quality: '" + texqualstr + "'; defaulting to low."); - texture_quality_requested = TextureQuality::kLow; - } - - // Graphics quality. - std::string gqualstr = - g_app_config->Resolve(AppConfig::StringID::kGraphicsQuality); - GraphicsQuality graphics_quality_requested; - - if (gqualstr == "Auto") { - graphics_quality_requested = GraphicsQuality::kAuto; - } else if (gqualstr == "Higher") { - graphics_quality_requested = GraphicsQuality::kHigher; - } else if (gqualstr == "High") { - graphics_quality_requested = GraphicsQuality::kHigh; - } else if (gqualstr == "Medium") { - graphics_quality_requested = GraphicsQuality::kMedium; - } else if (gqualstr == "Low") { - graphics_quality_requested = GraphicsQuality::kLow; - } else { - Log("Error: Invalid graphics quality: '" + gqualstr - + "'; defaulting to auto."); - graphics_quality_requested = GraphicsQuality::kAuto; - } - - // Android res string. - std::string android_res = - g_app_config->Resolve(AppConfig::StringID::kResolutionAndroid); - - bool fullscreen = g_app_config->Resolve(AppConfig::BoolID::kFullscreen); - - // Note: when the graphics-thread applies the first set-screen event it will - // trigger the remainder of startup such as media-loading; make sure nothing - // below this will affect that. - g_graphics_server->PushSetScreenCall(fullscreen, width, height, - texture_quality_requested, - graphics_quality_requested, android_res); - - // FIXME: The graphics server should kick this off *AFTER* it sets the actual - // quality values; here we're just sending along our requested values which - // is wrong. If there's a session up, inform it of the (potential) change. - Session* session = GetForegroundSession(); - if (session) { - session->GraphicsQualityChanged(graphics_quality_requested); - } - - if (!HeadlessMode()) { - g_app_globals->remote_server_accepting_connections = - g_app_config->Resolve(AppConfig::BoolID::kEnableRemoteApp); - } - - chat_muted_ = g_app_config->Resolve(AppConfig::BoolID::kChatMuted); - g_graphics->set_show_fps(g_app_config->Resolve(AppConfig::BoolID::kShowFPS)); - - // Set tv border (for both client and server). - bool tv_border = g_app_config->Resolve(AppConfig::BoolID::kTVBorder); - g_graphics_server->PushCall( - [tv_border] { g_graphics_server->set_tv_border(tv_border); }); - - // FIXME: this should exist either on the client or the server; not both. - // (and should be communicated via frameldefs/etc.) - g_graphics->set_tv_border(tv_border); - - g_graphics_server->PushSetScreenGammaCall( - g_app_config->Resolve(AppConfig::FloatID::kScreenGamma)); - g_graphics_server->PushSetScreenPixelScaleCall( - g_app_config->Resolve(AppConfig::FloatID::kScreenPixelScale)); - - TextWidget::set_always_use_internal_keyboard( - g_app_config->Resolve(AppConfig::BoolID::kAlwaysUseInternalKeyboard)); - - // V-sync setting. - std::string v_sync = - g_app_config->Resolve(AppConfig::StringID::kVerticalSync); - bool do_v_sync{}; - bool auto_v_sync{}; - if (v_sync == "Auto") { - do_v_sync = true; - auto_v_sync = true; - } else if (v_sync == "Always") { - do_v_sync = true; - auto_v_sync = false; - } else if (v_sync == "Never") { - do_v_sync = false; - auto_v_sync = false; - } else { - do_v_sync = false; - auto_v_sync = false; - Log("Error: Invalid 'Vertical Sync' value: '" + v_sync + "'"); - } - g_graphics_server->PushSetVSyncCall(do_v_sync, auto_v_sync); - - g_audio->SetVolumes(g_app_config->Resolve(AppConfig::FloatID::kMusicVolume), - g_app_config->Resolve(AppConfig::FloatID::kSoundVolume)); - - // Kick-idle-players setting (hmm is this still relevant?). - auto* host_session = dynamic_cast(foreground_session_.get()); - kick_idle_players_ = - g_app_config->Resolve(AppConfig::BoolID::kKickIdlePlayers); - if (host_session) { - host_session->SetKickIdlePlayers(kick_idle_players_); - } - - // Input doesn't yet exist when we first run; it updates itself initially when - // it comes up (but we do thereafter). - // if (input_) { - assert(g_input); - g_input->ApplyAppConfig(); - // } - - // Set up network ports/states. - int port = g_app_config->Resolve(AppConfig::IntID::kPort); - int telnet_port = g_app_config->Resolve(AppConfig::IntID::kTelnetPort); - - // NOTE: Hard disabling telnet for now in headless builds; - // it was being exploited to own servers. - bool enable_telnet = - g_buildconfig.headless_build() - ? false - : g_app_config->Resolve(AppConfig::BoolID::kEnableTelnet); - std::string telnet_password = - g_app_config->Resolve(AppConfig::StringID::kTelnetPassword); - - g_app->PushNetworkSetupCall(port, telnet_port, enable_telnet, - telnet_password); - - bool disable_camera_shake = - g_app_config->Resolve(AppConfig::BoolID::kDisableCameraShake); - g_graphics->set_camera_shake_disabled(disable_camera_shake); - - bool disable_camera_gyro = - g_app_config->Resolve(AppConfig::BoolID::kDisableCameraGyro); - g_graphics->set_camera_gyro_explicitly_disabled(disable_camera_gyro); - - // Any platform-specific settings. - g_platform->ApplyConfig(); -} - -void Game::PushApplyConfigCall() { - PushCall([this] { ApplyConfig(); }); -} - -void Game::PushRemoveGraphicsServerRenderHoldCall() { - PushCall([] { - // This call acts as a flush of sorts; when it goes through, - // we push a call to the graphics server saying its ok for it - // to start rendering again. Thus any already-queued-up - // frame_defs or whatnot will be ignored. - g_graphics_server->PushRemoveRenderHoldCall(); - }); -} - -void Game::PushSetFriendListCall(const std::vector& friends) { - PushCall([friends] { g_python->DispatchFriendList(friends); }); -} - -void Game::PushFreeMediaComponentRefsCall( - const std::vector*>& components) { - PushCall([components] { - for (auto&& i : components) { - delete i; - } - }); -} - -void Game::PushHavePendingLoadsDoneCall() { - PushCall([] { g_media->ClearPendingLoadsDoneList(); }); -} - -void Game::ToggleConsole() { - assert(InGameThread()); - if (auto console = g_app_globals->console) { - console->ToggleState(); - } -} - -void Game::PushConsolePrintCall(const std::string& msg) { - PushCall([msg] { - // Send them to the console if its been created or store them - // for when it is (unless we're headless in which case it never will). - if (auto console = g_app_globals->console) { - console->Print(msg); - } else if (!HeadlessMode()) { - g_app_globals->console_startup_messages += msg; - } - }); -} - -void Game::PushHavePendingLoadsCall() { - PushCall([this] { - have_pending_loads_ = true; - UpdateProcessTimer(); - }); -} - -void Game::PushShutdownCall(bool soft) { - PushCall([this, soft] { Shutdown(soft); }); -} - -void Game::Shutdown(bool soft) { - assert(InGameThread()); - - if (!g_app_globals->shutting_down) { - g_app_globals->shutting_down = true; - - // Nuke the app if we get stuck shutting down. - Utils::StartSuicideTimer("shutdown", 10000); - - // Call our shutdown callback. - g_python->obj(Python::ObjID::kShutdownCall).Call(); - - // If we have any client/host connections, give them - // a chance to shoot off disconnect packets or whatnot. - for (auto& connection : connections_to_clients_) { - connection.second->RequestDisconnect(); - } - if (connection_to_host_.exists()) { - connection_to_host_->RequestDisconnect(); - } - - // Let's do the same stuff we do when our thread is pausing. (committing - // account-client to disk, etc). - HandleThreadPause(); - - // Attempt to report/store outstanding log stuff. - Python::PutLog(false); - - // Ideally we'd want to give some of the above stuff - // a few seconds to complete, but just calling it done for now. - g_app->PushShutdownCompleteCall(); - } -} - -void Game::ResetInput() { - assert(InGameThread()); - g_input->ResetKeyboardHeldKeys(); - g_input->ResetJoyStickHeldButtons(); -} - -auto Game::RemovePlayer(Player* player) -> void { - assert(InGameThread()); - if (HostSession* host_session = player->GetHostSession()) { - host_session->RemovePlayer(player); - } else { - Log("Got RemovePlayer call but have no host_session"); - } -} - -auto Game::NewRealTimer(millisecs_t length, bool repeat, - const Object::Ref& runnable) -> int { - int offset = 0; - Timer* t = realtimers_->NewTimer(GetRealTime(), length, offset, - repeat ? -1 : 0, runnable); - return t->id(); -} - -void Game::DeleteRealTimer(int timer_id) { realtimers_->DeleteTimer(timer_id); } - -void Game::SetRealTimerLength(int timer_id, millisecs_t length) { - Timer* t = realtimers_->GetTimer(timer_id); - if (t) { - t->SetLength(length); - } else { - Log("Error: Game::SetRealTimerLength() called on nonexistent timer."); - } -} - -void Game::Process() { - have_pending_loads_ = g_media->RunPendingLoadsGameThread(); - UpdateProcessTimer(); -} - -void Game::SetLanguageKeys(const std::map& language) { - assert(InGameThread()); - { - std::lock_guard lock(language_mutex_); - language_ = language; - } - - // Let's also inform existing session stuff so it can update itself. - if (Session* session = GetForegroundSession()) { - session->LanguageChanged(); - } - - // As well as existing UI stuff. - if (Widget* root_widget = g_ui->root_widget()) { - root_widget->OnLanguageChange(); - } - - // Also clear translations on all screen-messages. - g_graphics->ClearScreenMessageTranslations(); -} - -auto DoCompileResourceString(cJSON* obj) -> std::string { - assert(InGameThread()); - assert(obj != nullptr); - - std::string result; - - // If its got a "r" key, look it up as a resource.. (with optional fallback). - cJSON* resource = cJSON_GetObjectItem(obj, "r"); - if (resource == nullptr) { - resource = cJSON_GetObjectItem(obj, "resource"); - // As of build 14318, complain if we find long key names; hope to remove - // them soon. - if (resource != nullptr) { - static bool printed = false; - if (!printed) { - printed = true; - char* c = cJSON_Print(obj); - BA_LOG_ONCE("found long key 'resource' in raw lstr json: " - + std::string(c)); - free(c); - } - } - } - if (resource != nullptr) { - // Look for fallback-resource. - cJSON* fallback_resource = cJSON_GetObjectItem(obj, "f"); - if (fallback_resource == nullptr) { - fallback_resource = cJSON_GetObjectItem(obj, "fallback"); - - // As of build 14318, complain if we find old long key names; hope to - // remove them soon. - if (fallback_resource != nullptr) { - static bool printed = false; - if (!printed) { - printed = true; - char* c = cJSON_Print(obj); - BA_LOG_ONCE("found long key 'fallback' in raw lstr json: " - + std::string(c)); - free(c); - } - } - } - cJSON* fallback_value = cJSON_GetObjectItem(obj, "fv"); - result = g_python->GetResource( - resource->valuestring, - fallback_resource ? fallback_resource->valuestring : nullptr, - fallback_value ? fallback_value->valuestring : nullptr); - } else { - // Apparently not a resource; lets try as a translation ("t" keys). - cJSON* translate = cJSON_GetObjectItem(obj, "t"); - if (translate == nullptr) { - translate = cJSON_GetObjectItem(obj, "translate"); - - // As of build 14318, complain if we find long key names; hope to remove - // them soon. - if (translate != nullptr) { - static bool printed = false; - if (!printed) { - printed = true; - char* c = cJSON_Print(obj); - BA_LOG_ONCE("found long key 'translate' in raw lstr json: " - + std::string(c)); - free(c); - } - } - } - if (translate != nullptr) { - if (translate->type != cJSON_Array - || cJSON_GetArraySize(translate) != 2) { - throw Exception("Expected a 2 member array for translate"); - } - cJSON* category = cJSON_GetArrayItem(translate, 0); - if (category->type != cJSON_String) { - throw Exception( - "First member of translate array (category) must be a string"); - } - cJSON* value = cJSON_GetArrayItem(translate, 1); - if (value->type != cJSON_String) { - throw Exception( - "Second member of translate array (value) must be a string"); - } - result = - g_python->GetTranslation(category->valuestring, value->valuestring); - } else { - // Lastly try it as a value ("value" or "v"). - // (can be useful for feeding explicit strings while still allowing - // translated subs - cJSON* value = cJSON_GetObjectItem(obj, "v"); - if (value == nullptr) { - value = cJSON_GetObjectItem(obj, "value"); - - // As of build 14318, complain if we find long key names; hope to remove - // them soon. - if (value != nullptr) { - static bool printed = false; - if (!printed) { - printed = true; - char* c = cJSON_Print(obj); - BA_LOG_ONCE("found long key 'value' in raw lstr json: " - + std::string(c)); - free(c); - } - } - } - if (value != nullptr) { - if (value->type != cJSON_String) { - throw Exception("Expected a string for value"); - } - result = value->valuestring; - } else { - throw Exception("no 'resource', 'translate', or 'value' keys found"); - } - } - } - // Ok; now no matter what it was, see if it contains any subs and replace - // them. - // ("subs" or "s") - cJSON* subs = cJSON_GetObjectItem(obj, "s"); - if (subs == nullptr) { - subs = cJSON_GetObjectItem(obj, "subs"); - - // As of build 14318, complain if we find long key names; hope to remove - // them soon. - if (subs != nullptr) { - static bool printed = false; - if (!printed) { - printed = true; - char* c = cJSON_Print(obj); - BA_LOG_ONCE("found long key 'subs' in raw lstr json: " - + std::string(c)); - free(c); - } - } - } - if (subs != nullptr) { - if (subs->type != cJSON_Array) { - throw Exception("expected an array for 'subs'"); - } - int subsCount = cJSON_GetArraySize(subs); - for (int i = 0; i < subsCount; i++) { - cJSON* sub = cJSON_GetArrayItem(subs, i); - if (sub->type != cJSON_Array || cJSON_GetArraySize(sub) != 2) { - throw Exception( - "Invalid subs entry; expected length 2 list of sub/replacement."); - } - - // First item should be a string. - cJSON* key = cJSON_GetArrayItem(sub, 0); - if (key->type != cJSON_String) { - throw Exception("Sub keys must be strings."); - } - std::string s_key = key->valuestring; - - // Second item can be a string or a dict; if its a dict, we go recursive. - cJSON* value = cJSON_GetArrayItem(sub, 1); - std::string s_val; - if (value->type == cJSON_String) { - s_val = value->valuestring; - } else if (value->type == cJSON_Object) { - s_val = DoCompileResourceString(value); - } else { - throw Exception("Sub values must be strings or dicts."); - } - - // Replace *ALL* occurrences. - // FIXME: Using this simple logic, If our replace value contains our - // search value we get an infinite loop. For now, just error in that case. - if (s_val.find(s_key) != std::string::npos) { - throw Exception("Subs replace string cannot contain search string."); - } - while (true) { - size_t pos = result.find(s_key); - if (pos == std::string::npos) { - break; - } - result.replace(pos, s_key.size(), s_val); - } - } - } - return result; -} - -auto Game::CompileResourceString(const std::string& s, const std::string& loc, - bool* valid) -> std::string { - assert(InGameThread()); - assert(g_python != nullptr); - - bool dummyvalid; - if (valid == nullptr) { - valid = &dummyvalid; - } - - // Quick out: if it doesn't start with a { and end with a }, treat it as a - // literal and just return it as-is. - if (s.size() < 2 || s[0] != '{' || s[s.size() - 1] != '}') { - *valid = true; - return s; - } - - cJSON* root = cJSON_Parse(s.c_str()); - if (root == nullptr) { - Log("CompileResourceString failed (loc " + loc + "); invalid json: '" + s - + "'"); - *valid = false; - return ""; - } - std::string result; - try { - result = DoCompileResourceString(root); - *valid = true; - } catch (const std::exception& e) { - Log("CompileResourceString failed (loc " + loc - + "): " + std::string(e.what()) + "; str='" + s + "'"); - result = ""; - *valid = false; - } - cJSON_Delete(root); - return result; -} - -auto Game::GetResourceString(const std::string& key) -> std::string { - std::string val; - { - std::lock_guard lock(language_mutex_); - auto i = language_.find(key); - if (i != language_.end()) { - val = i->second; - } - } - return val; -} - -auto Game::CharStr(SpecialChar id) -> std::string { - std::lock_guard lock(special_char_mutex_); - std::string val; - auto i = special_char_strings_.find(id); - if (i != special_char_strings_.end()) { - val = i->second; - } else { - BA_LOG_PYTHON_TRACE_ONCE("invalid key in CharStr(): '" - + std::to_string(static_cast(id)) + "'"); - val = "?"; - } - return val; -} - -void Game::HandleClientDisconnected(int id) { - auto i = connections_to_clients_.find(id); - if (i != connections_to_clients_.end()) { - bool was_connected = i->second->can_communicate(); - std::string leaver_spec = i->second->peer_spec().GetSpecString(); - std::vector leave_msg(leaver_spec.size() + 1); - leave_msg[0] = BA_MESSAGE_PARTY_MEMBER_LEFT; - memcpy(&(leave_msg[1]), leaver_spec.c_str(), leaver_spec.size()); - connections_to_clients_.erase(i); - - // If the client was connected, they were on the roster. - // We need to update it and send it to all remaining clients since they're - // gone. Also inform everyone who just left so they can announce it - // (technically could consolidate these messages but whatever...). - if (was_connected) { - UpdateGameRoster(); - for (auto&& connection : connections_to_clients_) { - if (ShouldAnnouncePartyJoinsAndLeaves()) { - connection.second->SendReliableMessage(leave_msg); - } - } - } - } -} - -void Game::PushClientDisconnectedCall(int id) { - PushCall([this, id] { HandleClientDisconnected(id); }); -} - -auto Game::ShouldAnnouncePartyJoinsAndLeaves() -> bool { - assert(InGameThread()); - - // At the moment we don't announce these for public internet parties.. (too - // much noise). - return !public_party_enabled(); -} - -void Game::CleanUpBeforeConnectingToHost() { - // We can't have connected clients and a host-connection at the same time. - // Make a minimal attempt to disconnect any client connections we have, but - // get them off the list immediately. - // FIXME: Should we have a 'purgatory' for dying client connections?.. - // (they may not get the single 'go away' packet we send here) - ForceDisconnectClients(); - - // Also make sure our public party state is off; this will inform the server - // that it should not be handing out our address to anyone. - assert(g_python); - SetPublicPartyEnabled(false); -} - -auto Game::DisconnectClient(int client_id, int ban_seconds) -> bool { - assert(InGameThread()); - - if (connection_to_host_.exists()) { - // Kick-votes first appeared in 14248 - if (connection_to_host_->build_number() < 14248) { - return false; - } - if (client_id > 255) { - Log("DisconnectClient got client_id > 255 (" + std::to_string(client_id) - + ")"); - } else { - std::vector msg_out(2); - msg_out[0] = BA_MESSAGE_KICK_VOTE; - msg_out[1] = static_cast_check_fit(client_id); - connection_to_host_->SendReliableMessage(msg_out); - return true; - } - } else { - // No host connection - look for clients. - auto i = connections_to_clients_.find(client_id); - - if (i != connections_to_clients_.end()) { - // If this is considered a kick, add an entry to our banned list so we - // know not to let them back in for a while. - if (ban_seconds > 0) { - BanPlayer(i->second->peer_spec(), 1000 * ban_seconds); - } - i->second->RequestDisconnect(); - - // Do the official local disconnect immediately with the sounds and all - // that. - PushClientDisconnectedCall(client_id); - - return true; - } - } - return false; -} - -void Game::ForceDisconnectClients() { - for (auto&& i : connections_to_clients_) { - if (ConnectionToClient* client = i.second.get()) { - client->RequestDisconnect(); - } - } - connections_to_clients_.clear(); -} - -void Game::PushHostConnectedUDPCall(const SockAddr& addr, - bool print_connect_progress) { - PushCall([this, addr, print_connect_progress] { - // Attempt to disconnect any clients we have, turn off public-party - // advertising, etc. - CleanUpBeforeConnectingToHost(); - print_udp_connect_progress_ = print_connect_progress; - connection_to_host_ = Object::New(addr); - has_connection_to_host_ = true; - printed_host_disconnect_ = false; - }); -} - -void Game::PushDisconnectFromHostCall() { - PushCall([this] { - if (connection_to_host_.exists()) { - connection_to_host_->RequestDisconnect(); - } - }); -} - -void Game::PushDisconnectedFromHostCall() { - PushCall([this] { - if (connection_to_host_.exists()) { - bool was_connected = connection_to_host_->can_communicate(); - connection_to_host_.Clear(); - has_connection_to_host_ = false; - - // Clear out our party roster. - UpdateGameRoster(); - - // Go back to main menu *if* the connection was fully connected. - // Otherwise we're still probably sitting at the main menu - // so no need to reset it. - if (was_connected) { - RunMainMenu(); - } - } - }); -} - -void Game::SendScreenMessageToAll(const std::string& s, float r, float g, - float b) { - SendScreenMessageToClients(s, r, g, b); - ScreenMessage(s, {r, g, b}); -} - -void Game::SendScreenMessageToClients(const std::string& s, float r, float g, - float b) { - for (auto&& i : connections_to_clients_) { - if (i.second.exists() && i.second->can_communicate()) { - i.second->SendScreenMessage(s, r, g, b); - } - } -} - -void Game::SendScreenMessageToSpecificClients(const std::string& s, float r, - float g, float b, - const std::vector& clients) { - for (auto&& i : connections_to_clients_) { - if (i.second.exists() && i.second->can_communicate()) { - // Only send if this client is in our list. - for (auto c : clients) { - if (c == i.second->id()) { - i.second->SendScreenMessage(s, r, g, b); - break; - } - } - } - } - - // Now print locally only if -1 is in our list. - for (auto c : clients) { - if (c == -1) { - ScreenMessage(s, {r, g, b}); - break; - } - } -} - -auto Game::GetConnectionToHostUDP() -> ConnectionToHostUDP* { - ConnectionToHost* h = connection_to_host_.get(); - return h ? h->GetAsUDP() : nullptr; -} - -void Game::PushPartyInviteCall(const std::string& name, - const std::string& invite_id) { - PushCall([this, name, invite_id] { PartyInvite(name, invite_id); }); -} - -void Game::PartyInvite(const std::string& name, const std::string& invite_id) { - assert(InGameThread()); - g_python->PartyInvite(name, invite_id); -} - -void Game::PushPartyInviteRevokeCall(const std::string& invite_id) { - PushCall([this, invite_id] { PartyInviteRevoke(invite_id); }); -} - -void Game::PartyInviteRevoke(const std::string& invite_id) { - assert(InGameThread()); - g_python->PartyInviteRevoke(invite_id); -} - -void Game::PushUDPConnectionPacketCall(const std::vector& data, - const SockAddr& addr) { - PushCall([this, data, addr] { UDPConnectionPacket(data, addr); }); -} - -// Called for low level packets coming in pertaining to udp -// host/client-connections. -void Game::UDPConnectionPacket(const std::vector& data_in, - const SockAddr& addr) { - assert(!data_in.empty()); - - const uint8_t* data = &(data_in[0]); - auto data_size = static_cast(data_in.size()); - - switch (data[0]) { - case BA_PACKET_CLIENT_ACCEPT: { - if (data_size == 3) { - uint8_t request_id = data[2]; - - // If we have a udp-host-connection and its request-id matches, we're - // accepted; hooray! - ConnectionToHostUDP* hc = GetConnectionToHostUDP(); - if (hc && hc->request_id() == request_id) { - hc->set_client_id(data[1]); - } - } - break; - } - case BA_PACKET_DISCONNECT_FROM_CLIENT_REQUEST: { - if (data_size == 2) { - // Client is telling us (host) that it wants to disconnect. - uint8_t client_id = data[1]; - - // Wipe that client out (if it still exists). - PushClientDisconnectedCall(client_id); - - // Now send an ack so they know it's been taken care of. - g_network_write_module->PushSendToCall( - {BA_PACKET_DISCONNECT_FROM_CLIENT_ACK, client_id}, addr); - } - break; - } - case BA_PACKET_DISCONNECT_FROM_CLIENT_ACK: { - if (data_size == 2) { - // Host is telling us (client) that we've been disconnected. - uint8_t client_id = data[1]; - ConnectionToHostUDP* hc = GetConnectionToHostUDP(); - if (hc && hc->client_id() == client_id) { - PushDisconnectedFromHostCall(); - } - } - break; - } - case BA_PACKET_DISCONNECT_FROM_HOST_REQUEST: { - if (data_size == 2) { - uint8_t client_id = data[1]; - - // Host is telling us (client) to disconnect. - ConnectionToHostUDP* hc = GetConnectionToHostUDP(); - if (hc && hc->client_id() == client_id) { - PushDisconnectedFromHostCall(); - } - - // Now send an ack so they know it's been taken care of. - g_network_write_module->PushSendToCall( - {BA_PACKET_DISCONNECT_FROM_HOST_ACK, client_id}, addr); - } - break; - } - case BA_PACKET_DISCONNECT_FROM_HOST_ACK: { - break; - } - case BA_PACKET_CLIENT_GAMEPACKET_COMPRESSED: { - if (data_size > 2) { - uint8_t client_id = data[1]; - auto i = connections_to_clients_.find(client_id); - if (i != connections_to_clients_.end()) { - // FIXME: could change HandleGamePacketCompressed to avoid this - // copy. - std::vector data2(data_size - 2); - memcpy(data2.data(), data + 2, data_size - 2); - i->second->HandleGamePacketCompressed(data2); - return; - } else { - // Send a disconnect request aimed at them. - g_network_write_module->PushSendToCall( - {BA_PACKET_DISCONNECT_FROM_HOST_REQUEST, client_id}, addr); - } - } - break; - } - - case BA_PACKET_HOST_GAMEPACKET_COMPRESSED: { - if (data_size > 2) { - uint8_t request_id = data[1]; - - ConnectionToHostUDP* hc = GetConnectionToHostUDP(); - if (hc && hc->request_id() == request_id) { - // FIXME: Should change HandleGamePacketCompressed to avoid this copy. - std::vector data2(data_size - 2); - memcpy(data2.data(), data + 2, data_size - 2); - hc->HandleGamePacketCompressed(data2); - } - } - break; - } - - case BA_PACKET_CLIENT_DENY: - case BA_PACKET_CLIENT_DENY_PARTY_FULL: - case BA_PACKET_CLIENT_DENY_ALREADY_IN_PARTY: - case BA_PACKET_CLIENT_DENY_VERSION_MISMATCH: { - if (data_size == 2) { - uint8_t request_id = data[1]; - ConnectionToHostUDP* hc = GetConnectionToHostUDP(); - - // If they're for-sure rejecting *this* connection, kill it. - if (hc && hc->request_id() == request_id) { - bool keep_trying = false; - - // OBSOLETE BUT HERE FOR BACKWARDS COMPAT WITH 1.4.98 servers. - // Newer servers never deny us in this way and simply include - // their protocol version in the handshake they send us, allowing us - // to decide whether we support talking to them or not. - if (data[0] == BA_PACKET_CLIENT_DENY_VERSION_MISMATCH) { - // If we've got more protocols we can try, keep trying to connect - // with our other protocols until one works or we run out. - // FIXME: We should move this logic to the gamepacket or message - // level so it works for all connection types. - keep_trying = hc->SwitchProtocol(); - if (!keep_trying) { - if (!printed_host_disconnect_) { - ScreenMessage( - GetResourceString("connectionFailedVersionMismatchText"), - {1, 0, 0}); - printed_host_disconnect_ = true; - } - } - } else if (data[0] == BA_PACKET_CLIENT_DENY_PARTY_FULL) { - if (!printed_host_disconnect_) { - if (print_udp_connect_progress_) { - ScreenMessage( - GetResourceString("connectionFailedPartyFullText"), - {1, 0, 0}); - } - printed_host_disconnect_ = true; - } - } else if (data[0] == BA_PACKET_CLIENT_DENY_ALREADY_IN_PARTY) { - if (!printed_host_disconnect_) { - ScreenMessage( - GetResourceString("connectionFailedHostAlreadyInPartyText"), - {1, 0, 0}); - printed_host_disconnect_ = true; - } - } else { - if (!printed_host_disconnect_) { - ScreenMessage(GetResourceString("connectionRejectedText"), - {1, 0, 0}); - printed_host_disconnect_ = true; - } - } - if (!keep_trying) { - PushDisconnectedFromHostCall(); - } - } - } - break; - } - case BA_PACKET_CLIENT_REQUEST: { - if (data_size > 4) { - // Bytes 2 and 3 are their protocol ID, byte 4 is request ID, the rest - // is session-id. - uint16_t protocol_id; - memcpy(&protocol_id, data + 1, 2); - uint8_t request_id = data[3]; - - // They also send us their session-ID which should - // be completely unique to them; we can use this to lump client - // requests together and such. - std::vector client_name_buffer(data_size - 4 + 1); - memcpy(&(client_name_buffer[0]), data + 4, data_size - 4); - client_name_buffer[data_size - 4] = 0; // terminate string - std::string client_name = &(client_name_buffer[0]); - - if (static_cast(connections_to_clients_.size() + 1) - >= public_party_max_size()) { - // If we've reached our party size limit (including ourself in that - // count), reject. - - // Newer version have a specific party-full message; send that first - // but also follow up with a generic deny message for older clients. - g_network_write_module->PushSendToCall( - {BA_PACKET_CLIENT_DENY_PARTY_FULL, request_id}, addr); - - g_network_write_module->PushSendToCall( - {BA_PACKET_CLIENT_DENY, request_id}, addr); - - } else if (connection_to_host_.exists()) { - // If we're connected to someone else, we can't have clients. - g_network_write_module->PushSendToCall( - {BA_PACKET_CLIENT_DENY_ALREADY_IN_PARTY, request_id}, addr); - } else { - // Otherwise go ahead and make them a new client connection. - Object::Ref connection_to_client; - - // Go through and see if we already have a client-connection for - // this request-id. - for (auto&& i : connections_to_clients_) { - if (ConnectionToClientUDP* cc_udp = i.second->GetAsUDP()) { - if (cc_udp->client_name() == client_name) { - connection_to_client = cc_udp; - break; - } - } - } - if (!connection_to_client.exists()) { - // Create them a client object. - // Try to find an unused client-id in the range 0-255. - int client_id = 0; - bool found = false; - for (int i = 0; i < 256; i++) { - int test_id = (next_connection_to_client_id_ + i) % 255; - if (connections_to_clients_.find(test_id) - == connections_to_clients_.end()) { - client_id = test_id; - found = true; - break; - } - } - next_connection_to_client_id_++; - - // If all 255 slots are taken (whaaaaaaa?), reject them. - if (!found) { - std::vector msg_out(2); - msg_out[0] = BA_PACKET_CLIENT_DENY; - msg_out[1] = request_id; - g_network_write_module->PushSendToCall(msg_out, addr); - Log("All client slots full; really?.."); - break; - } - connection_to_client = Object::New( - addr, client_name, request_id, client_id); - connections_to_clients_[client_id] = connection_to_client; - } - - // If we got to this point, regardless of whether - // we already had a connection or not, tell them - // they're accepted. - std::vector msg_out(3); - msg_out[0] = BA_PACKET_CLIENT_ACCEPT; - assert(connection_to_client->id() < 256); - msg_out[1] = - static_cast_check_fit(connection_to_client->id()); - msg_out[2] = request_id; - g_network_write_module->PushSendToCall(msg_out, addr); - } - } - break; - } - default: - // Assuming we can get random other noise in here; - // should just silently ignore. - break; - } -} - -// Can probably kill this. -auto Game::GetConnectionsToClients() -> std::vector { - std::vector connections; - connections.reserve(connections_to_clients_.size()); - for (auto& connections_to_client : connections_to_clients_) { - if (connections_to_client.second.exists()) { - connections.push_back(connections_to_client.second.get()); - } else { - Log("HAVE NONEXISTENT CONNECTION_TO_CLIENT IN LIST; UNEXPECTED"); - } - } - return connections; -} - -#if BA_GOOGLE_BUILD - -void Game::PushClientDisconnectedGooglePlayCall(int google_id) { - PushCall([this, google_id] { - int id = ClientIDFromGooglePlayClientID(google_id); - HandleClientDisconnected(id); - }); -} - -int Game::ClientIDFromGooglePlayClientID(int google_id) { - auto i = google_play_id_to_client_id_map_.find(google_id); - if (i != google_play_id_to_client_id_map_.end()) { - return i->second; - } else { - BA_LOG_ONCE("ClientIDFromGooglePlayClientID failed for id " - + std::to_string(google_id)); - return -1; - } -} - -int Game::GooglePlayClientIDFromClientID(int client_id) { - auto i = client_id_to_google_play_id_map_.find(client_id); - if (i != client_id_to_google_play_id_map_.end()) { - return i->second; - } else { - BA_LOG_ONCE("client_id_to_google_play_id_map_ failed for id " - + std::to_string(client_id)); - return -1; - } -} - -// Called for Google Play connections. -void Game::PushCompressedGamePacketFromHostGooglePlayCall( - const std::vector& data) { - PushCall([this, data] { - if (!connection_to_host_.exists()) { - Log("Error: Got host game-packet message but have no host."); - return; - } - connection_to_host_->HandleGamePacketCompressed(data); - }); -} - -// Called for Google Play connections. -void Game::PushCompressedGamePacketFromClientGooglePlayCall( - int google_client_id, const std::vector& data) { - PushCall([this, google_client_id, data] { - int client_id = ClientIDFromGooglePlayClientID(google_client_id); - auto i = connections_to_clients_.find(client_id); - if (i == connections_to_clients_.end()) { - Log("Error: Got data-from-client msg for nonexistent client."); - return; - } - i->second->HandleGamePacketCompressed(data); - }); -} - -void Game::PushClientConnectedGooglePlayCall(int id) { - PushCall([this, id] { - // Find a free ballistica client_id. - int client_id = 0; - bool found = false; - for (int i = 0; i < 256; i++) { - int test_id = (next_connection_to_client_id_ + i) % 255; - if (connections_to_clients_.find(test_id) - == connections_to_clients_.end()) { - client_id = test_id; - found = true; - break; - } - } - next_connection_to_client_id_++; - - if (found) { - google_play_id_to_client_id_map_[id] = client_id; - client_id_to_google_play_id_map_[client_id] = id; - if (connections_to_clients_.find(client_id) - != connections_to_clients_.end()) { - Log("Error: Got client-connected message" - " for already existing client-id."); - } - connections_to_clients_[client_id] = - g_platform->AndroidGPGSNewConnectionToClient(client_id); - } else { - Log("no client_id available in ClientConnectedGooglePlayCall"); - } - }); -} - -void Game::PushHostConnectedGooglePlayCall() { - PushCall([this] { - // Attempt to disconnect any existing clients we have, turn off public-party - // advertising, etc. - CleanUpBeforeConnectingToHost(); - connection_to_host_ = g_platform->AndroidGPGSNewConnectionToHost(); - has_connection_to_host_ = true; - printed_host_disconnect_ = false; - }); -} - -int Game::GetGooglePlayClientCount() const { - assert(InGameThread()); - int count = 0; - for (auto&& i : connections_to_clients_) { - if (i.second.exists() && i.second->can_communicate() - && g_platform->AndroidIsGPGSConnectionToClient(i.second.get())) { - count++; - } - } - return count; -} -#endif // BA_GOOGLE_BUILD - -auto Game::GetConnectedClientCount() const -> int { - assert(InGameThread()); - int count = 0; - for (auto&& i : connections_to_clients_) { - if (i.second.exists() && i.second->can_communicate()) { - count++; - } - } - return count; -} - -auto Game::GetPartySize() const -> int { - assert(InGameThread()); - assert(game_roster_ != nullptr); - return cJSON_GetArraySize(game_roster_); -} - -void Game::LocalDisplayChatMessage(const std::vector& buffer) { - // 1 type byte, 1 spec-len byte, 1 or more spec chars, 0 or more msg chars. - if (buffer.size() > 3) { - size_t spec_len = buffer[1]; - if (spec_len > 0 && spec_len + 2 <= buffer.size()) { - size_t msg_len = buffer.size() - spec_len - 2; - std::vector b1(spec_len + 1); - memcpy(&(b1[0]), &(buffer[2]), spec_len); - b1[spec_len] = 0; - std::vector b2(msg_len + 1); - if (msg_len > 0) { - memcpy(&(b2[0]), &(buffer[2 + spec_len]), msg_len); - } - b2[msg_len] = 0; - - std::string final_message = - PlayerSpec(b1.data()).GetDisplayString() + ": " + b2.data(); - - // Store it locally. - chat_messages_.push_back(final_message); - while (chat_messages_.size() > kMaxChatMessages) { - chat_messages_.pop_front(); - } - - // Show it on the screen if they don't have their chat window open - // (and don't have chat muted). - if (!g_ui->root_ui()->party_window_open()) { - if (!chat_muted_) { - ScreenMessage(final_message, {0.7f, 1.0f, 0.7f}); - } - } else { - // Party window is open - notify it that there's a new message. - g_python->HandleLocalChatMessage(final_message); - } - if (!chat_muted_) { - g_audio->PlaySound(g_media->GetSound(SystemSoundID::kTap)); - } - } - } -} - -void Game::SendChatMessage(const std::string& message, - const std::vector* clients, - const std::string* sender_override) { - // Sending to particular clients is only applicable while hosting. - if (clients != nullptr && connection_to_host() != nullptr) { - throw Exception("Can't send chat message to specific clients as a client."); - } - - // Same with overriding sender name - if (sender_override != nullptr && connection_to_host() != nullptr) { - throw Exception( - "Can't send chat message with sender_override as a client."); - } - - std::string our_spec_string; - - if (sender_override != nullptr) { - std::string override_final = *sender_override; - if (override_final.size() > kMaxPartyNameCombinedSize) { - override_final.resize(kMaxPartyNameCombinedSize); - override_final += "..."; - } - our_spec_string = - PlayerSpec::GetDummyPlayerSpec(override_final).GetSpecString(); - } else { - if (connection_to_host() != nullptr) { - // NOTE - we send our own spec string with the chat message whether we're - // a client or server.. however on protocol version 30+ this is ignored - // by the server and replaced with a spec string it generates for us. - // so once we know we're connected to a 30+ server we can start sending - // blank strings as a client. - // (not that it really matters; chat messages are tiny overall) - our_spec_string = PlayerSpec::GetAccountPlayerSpec().GetSpecString(); - } else { - // As a host we want to do the equivalent of - // ConnectionToClient::GetCombinedSpec() except for local connections (so - // send our name as the combination of local players if possible). Look - // for players coming from this client-connection; if we find any, make a - // spec out of their name(s). - std::string p_name_combined; - if (auto* hs = dynamic_cast(GetForegroundSession())) { - for (auto&& p : hs->players()) { - InputDevice* input_device = p->GetInputDevice(); - if (p->accepted() && p->name_is_real() && input_device != nullptr - && !input_device->IsRemoteClient()) { - if (!p_name_combined.empty()) { - p_name_combined += "/"; - } - p_name_combined += p->GetName(); - } - } - } - if (p_name_combined.size() > kMaxPartyNameCombinedSize) { - p_name_combined.resize(kMaxPartyNameCombinedSize); - p_name_combined += "..."; - } - if (!p_name_combined.empty()) { - our_spec_string = - PlayerSpec::GetDummyPlayerSpec(p_name_combined).GetSpecString(); - } else { - our_spec_string = PlayerSpec::GetAccountPlayerSpec().GetSpecString(); - } - } - } - - // If we find a newline, only take the first line (prevent people from - // covering the screen with obnoxious chat messages). - std::string message2 = message; - size_t nlpos = message2.find('\n'); - if (nlpos != std::string::npos) { - message2 = message2.substr(0, nlpos); - } - - // If we're the host, run filters before we send the message out. - // If the filter kills the message, don't send. - bool allow_message = g_python->FilterChatMessage(&message2, -1); - if (!allow_message) { - return; - } - - // 1 byte type + 1 byte spec-string-length + message. - std::vector msg_out(1 + 1 + our_spec_string.size() - + message2.size()); - msg_out[0] = BA_MESSAGE_CHAT; - size_t spec_size = our_spec_string.size(); - assert(spec_size < 256); - msg_out[1] = static_cast(spec_size); - memcpy(&(msg_out[2]), our_spec_string.c_str(), spec_size); - memcpy(&(msg_out[2 + spec_size]), message2.c_str(), message2.size()); - - // If we're a client, send this to the host (it will make its way back to us - // when they send to clients). - if (ConnectionToHost* hc = connection_to_host()) { - hc->SendReliableMessage(msg_out); - } else { - // Ok we're the host. - - // Send to all (or at least some) connected clients. - for (auto&& i : connections_to_clients_) { - // Skip if its going to specific ones and this one doesn't match. - if (clients != nullptr) { - auto found = false; - for (auto&& c : *clients) { - if (c == i.second->id()) { - found = true; - } - } - if (!found) { - continue; - } - } - - if (i.second->can_communicate()) { - i.second->SendReliableMessage(msg_out); - } - } - - // And display locally if the message is addressed to all. - if (clients == nullptr) { - LocalDisplayChatMessage(msg_out); - } - } -} - -auto Game::GetGameRosterMessage() -> std::vector { - // This message is simply a flattened json string of our roster (including - // terminating char). - char* s = cJSON_PrintUnformatted(game_roster_); - // printf("ROSTER MESSAGE %s\n", s); - auto s_len = strlen(s); - std::vector msg(1 + s_len + 1); - msg[0] = BA_MESSAGE_PARTY_ROSTER; - memcpy(&(msg[1]), s, s_len + 1); - free(s); - - return msg; -} - -auto Game::IsPlayerBanned(const PlayerSpec& spec) -> bool { - millisecs_t current_time = GetRealTime(); - - // Now is a good time to prune no-longer-banned specs. - while (!banned_players_.empty() - && banned_players_.front().first < current_time) { - banned_players_.pop_front(); - } - for (auto&& test_spec : banned_players_) { - if (test_spec.second == spec) { - return true; - } - } - return false; -} - -void Game::StartKickVote(ConnectionToClient* starter, - ConnectionToClient* target) { - // Restrict votes per client. - millisecs_t current_time = GetRealTime(); - - if (starter == target) { - // Don't let anyone kick themselves. - starter->SendScreenMessage(R"({"r":"kickVoteCantKickSelfText",)" - R"("f":"kickVoteFailedText"})", - 1, 0, 0); - } else if (target->IsAdmin()) { - // Admins are immune to kicking - starter->SendScreenMessage(R"({"r":"kickVoteCantKickAdminText",)" - R"("f":"kickVoteFailedText"})", - 1, 0, 0); - } else if (starter->IsAdmin()) { - // Admin doing the kicking succeeds instantly. - SendScreenMessageToClients( - R"({"r":"kickOccurredText","s":[["${NAME}",)" - + Utils::GetJSONString( - target->GetCombinedSpec().GetDisplayString().c_str()) - + "]]}", - 1, 1, 0); - DisconnectClient(target->id(), kKickBanSeconds); - starter->SendScreenMessage(R"({"r":"kickVoteCantKickAdminText",)" - R"("f":"kickVoteFailedText"})", - 1, 0, 0); - } else if (!kick_voting_enabled_) { - // No kicking otherwise if its disabled. - starter->SendScreenMessage(R"({"r":"kickVotingDisabledText",)" - R"("f":"kickVoteFailedText"})", - 1, 0, 0); - } else if (kick_vote_in_progress_) { - // Vote in progress error. - starter->SendScreenMessage(R"({"r":"voteInProgressText"})", 1, 0, 0); - } else if (GetConnectedClientCount() < kKickVoteMinimumClients) { - // There's too few clients to effectively vote. - starter->SendScreenMessage(R"({"r":"kickVoteFailedNotEnoughVotersText",)" - R"("f":"kickVoteFailedText"})", - 1, 0, 0); - } else if (current_time < starter->next_kick_vote_allow_time_) { - // Not yet allowed error. - starter->SendScreenMessage( - R"({"r":"voteDelayText","s":[["${NUMBER}",")" - + std::to_string(std::max( - millisecs_t{1}, - (starter->next_kick_vote_allow_time_ - current_time) / 1000)) - + "\"]]}", - 1, 0, 0); - } else { - std::vector connected_clients = - GetConnectionsToClients(); - - // Ok, kick off a vote.. (send the question and instructions to everyone - // except the starter and the target). - for (auto&& client : connected_clients) { - if (client != starter && client != target) { - client->SendScreenMessage( - R"({"r":"kickQuestionText","s":[["${NAME}",)" - + Utils::GetJSONString( - target->GetCombinedSpec().GetDisplayString().c_str()) - + "]]}", - 1, 1, 0); - client->SendScreenMessage(R"({"r":"kickWithChatText","s":)" - R"([["${YES}","'1'"],["${NO}","'0'"]]})", - 1, 1, 0); - } else { - // For the kicker/kickee, simply print that a kick vote has been - // started. - client->SendScreenMessage( - R"({"r":"kickVoteStartedText","s":[["${NAME}",)" - + Utils::GetJSONString( - target->GetCombinedSpec().GetDisplayString().c_str()) - + "]]}", - 1, 1, 0); - } - } - kick_vote_end_time_ = current_time + kKickVoteDuration; - kick_vote_in_progress_ = true; - last_kick_votes_needed_ = -1; // make sure we print starting num - - // Keep track of who started the vote. - kick_vote_starter_ = starter; - kick_vote_target_ = target; - - // Reset votes for all connected clients. - for (ConnectionToClient* client : GetConnectionsToClients()) { - if (client == starter) { - client->kick_voted_ = true; - client->kick_vote_choice_ = true; - } else { - client->kick_voted_ = false; - } - } - } -} - -void Game::BanPlayer(const PlayerSpec& spec, millisecs_t duration) { - banned_players_.emplace_back(GetRealTime() + duration, spec); -} - -void Game::UpdateGameRoster() { - assert(InGameThread()); - - assert(game_roster_ != nullptr); - if (game_roster_ != nullptr) { - cJSON_Delete(game_roster_); - } - - // Our party-roster is just a json array of dicts containing player-specs. - game_roster_ = cJSON_CreateArray(); - - int total_party_size = 1; // include ourself here.. - - // Add ourself first (that's currently how they know we're the party leader) - // ..but only if we have a connected client (otherwise our party is - // considered 'empty'). - - // UPDATE: starting with our big ui revision we'll always include ourself - // here - bool include_self = (GetConnectedClientCount() > 0); -#if BA_TOOLBAR_TEST - include_self = true; -#endif // BA_TOOLBAR_TEST - - if (auto* hs = dynamic_cast(GetForegroundSession())) { - // Add our host-y self. - if (include_self) { - cJSON* client_dict = cJSON_CreateObject(); - cJSON_AddItemToObject( - client_dict, "spec", - cJSON_CreateString( - PlayerSpec::GetAccountPlayerSpec().GetSpecString().c_str())); - - // Add our list of local players. - cJSON* player_array = cJSON_CreateArray(); - for (auto&& p : hs->players()) { - InputDevice* input_device = p->GetInputDevice(); - - // Add some basic info for each local player (only ones with real - // names though; don't wanna send , etc). - if (p->accepted() && p->name_is_real() && input_device != nullptr - && !input_device->IsRemoteClient()) { - cJSON* player_dict = cJSON_CreateObject(); - cJSON_AddItemToObject(player_dict, "n", - cJSON_CreateString(p->GetName().c_str())); - cJSON_AddItemToObject(player_dict, "nf", - cJSON_CreateString(p->GetName(true).c_str())); - cJSON_AddItemToObject(player_dict, "i", cJSON_CreateNumber(p->id())); - cJSON_AddItemToArray(player_array, player_dict); - } - } - cJSON_AddItemToObject(client_dict, "p", player_array); - cJSON_AddItemToObject( - client_dict, "i", - cJSON_CreateNumber(-1)); // -1 client_id means we're the host. - cJSON_AddItemToArray(game_roster_, client_dict); - } - - // Add all connected clients. - for (auto&& i : connections_to_clients_) { - if (i.second->can_communicate()) { - cJSON* client_dict = cJSON_CreateObject(); - cJSON_AddItemToObject( - client_dict, "spec", - cJSON_CreateString(i.second->peer_spec().GetSpecString().c_str())); - - // Add their public account id (or None if we don't have it) - // cJSON* player_accountid{}; - // if (i.second->peer_public_account_id() == "") { - // player_accountid = cJSON_CreateNull(); - // } else { - // player_accountid = - // cJSON_CreateString(i.second->peer_public_account_id().c_str()); - // } - // cJSON_AddItemToObject(client_dict, "a", player_accountid); - - // Also add their list of players. - cJSON* player_array = cJSON_CreateArray(); - - // Include all players that are remote and coming from this same - // client connection. - for (auto&& p : hs->players()) { - InputDevice* input_device = p->GetInputDevice(); - if (p->accepted() && p->name_is_real() && input_device != nullptr - && input_device->IsRemoteClient()) { - auto* cid = static_cast(input_device); - ConnectionToClient* ctc = cid->connection_to_client(); - - // Add some basic info for each remote player. - if (ctc != nullptr && ctc == i.second.get()) { - cJSON* player_dict = cJSON_CreateObject(); - cJSON_AddItemToObject(player_dict, "n", - cJSON_CreateString(p->GetName().c_str())); - cJSON_AddItemToObject( - player_dict, "nf", - cJSON_CreateString(p->GetName(true).c_str())); - cJSON_AddItemToObject(player_dict, "i", - cJSON_CreateNumber(p->id())); - cJSON_AddItemToArray(player_array, player_dict); - } - } - } - cJSON_AddItemToObject(client_dict, "p", player_array); - cJSON_AddItemToObject(client_dict, "i", - cJSON_CreateNumber(i.second->id())); - cJSON_AddItemToArray(game_roster_, client_dict); - total_party_size += 1; - } - } - } - - // Keep the python layer informed on our number of connections; it may want - // to pass the info along to the master server if we're hosting a public - // party. - SetPublicPartySize(total_party_size); - - // Mark the roster as dirty so we know we need to send it to everyone soon. - game_roster_dirty_ = true; -} - -void Game::SetAdCompletionCall(PyObject* obj, bool pass_actually_showed) { - if (obj == Py_None) { - ad_completion_callback_.Clear(); - } else { - ad_completion_callback_ = Object::New(obj); - } - ad_completion_callback_pass_actually_showed_ = pass_actually_showed; - last_ad_start_time_ = g_platform->GetTicks(); -} - -void Game::CallAdCompletionCall(bool actually_showed) { - if (ad_completion_callback_.exists()) { - if (ad_completion_callback_pass_actually_showed_) { - PythonRef args(Py_BuildValue("(O)", actually_showed ? Py_True : Py_False), - PythonRef::kSteal); - ad_completion_callback_->Run(args); - } else { - ad_completion_callback_->Run(); - } - ad_completion_callback_.Clear(); // These are single-fire callbacks. - } -} - -void Game::SetPublicPartyEnabled(bool val) { - assert(InGameThread()); - if (val == public_party_enabled_) { - return; - } - public_party_enabled_ = val; - PushPublicPartyState(); -} - -void Game::SetPublicPartySize(int count) { - assert(InGameThread()); - if (count == public_party_size_) { - return; - } - public_party_size_ = count; - - // Push our new state to the server *ONLY* if public-party is turned on - // (wasteful otherwise). - if (public_party_enabled_) { - PushPublicPartyState(); - } -} - -void Game::SetPublicPartyMaxSize(int count) { - assert(InGameThread()); - if (count == public_party_max_size_) { - return; - } - public_party_max_size_ = count; - - // Push our new state to the server *ONLY* if public-party is turned on - // (wasteful otherwise). - if (public_party_enabled_) { - PushPublicPartyState(); - } -} - -void Game::SetPublicPartyName(const std::string& name) { - assert(InGameThread()); - if (name == public_party_name_) { - return; - } - public_party_name_ = name; - - // Push our new state to the server *ONLY* if public-party is turned on - // (wasteful otherwise). - if (public_party_enabled_) { - PushPublicPartyState(); - } -} - -void Game::SetPublicPartyStatsURL(const std::string& url) { - assert(InGameThread()); - if (url == public_party_stats_url_) { - return; - } - public_party_stats_url_ = url; - - // Push our new state to the server *ONLY* if public-party is turned on - // (wasteful otherwise). - if (public_party_enabled_) { - PushPublicPartyState(); - } -} - -void Game::SetPublicPartyPlayerCount(int count) { - assert(InGameThread()); - if (count == public_party_player_count_) { - return; - } - public_party_player_count_ = count; - - // Push our new state to the server *ONLY* if public-party is turned on - // (wasteful otherwise). - if (public_party_enabled_) { - PushPublicPartyState(); - } -} - -void Game::PushPublicPartyState() { - assert(InGameThread()); - PythonRef call = g_python->obj(Python::ObjID::kAccountClient) - .GetAttr("set_public_party_state"); - if (call.exists()) { - PythonRef args( - Py_BuildValue( - "(iiiiisss)", static_cast(public_party_enabled_), - public_party_size_, public_party_max_size_, - public_party_player_count_, public_party_max_player_count_, - public_party_name_.c_str(), public_party_min_league_.c_str(), - public_party_stats_url_.c_str()), - PythonRef::kSteal); - call.Call(args); - } else { - Log("Error on pushPublicPartyState call"); - } -} - -} // namespace ballistica diff --git a/src/ballistica/game/game_stream.cc b/src/ballistica/game/game_stream.cc deleted file mode 100644 index 8dd47091..00000000 --- a/src/ballistica/game/game_stream.cc +++ /dev/null @@ -1,1242 +0,0 @@ -// Copyright (c) 2011-2020 Eric Froemling - -#include "ballistica/game/game_stream.h" - -#include - -#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_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/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 saveReplay) - : time_(0), - host_session_(host_session), - next_flush_time_(0), - last_physics_correction_time_(0), - last_send_time_(0), - writing_replay_(false) { - if (saveReplay) { - // 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->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->UnregisterClientController(this); - - // Also, in the host-session case, make sure everything cleaned itself up. -#if BA_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"); - } -#endif // BA_DEBUG_BUILD - } -} - -// 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(); - } -} - -// 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); -} - -// 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 BA_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]))); - } -#endif // BA_DEBUG_BUILD - 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 BA_DEBUG_BUILD - for (auto val : vals) { - assert(IsValidNode(val)); - } -#endif - 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/host_activity.cc b/src/ballistica/game/host_activity.cc deleted file mode 100644 index ce4a1f58..00000000 --- a/src/ballistica/game/host_activity.cc +++ /dev/null @@ -1,528 +0,0 @@ -// Copyright (c) 2011-2020 Eric Froemling - -#include "ballistica/game/host_activity.h" - -#include - -#include "ballistica/dynamics/material/material.h" -#include "ballistica/game/game_stream.h" -#include "ballistica/game/player.h" -#include "ballistica/game/session/host_session.h" -#include "ballistica/generic/lambda_runnable.h" -#include "ballistica/generic/timer.h" -#include "ballistica/input/device/input_device.h" -#include "ballistica/media/component/collide_model.h" -#include "ballistica/media/component/data.h" -#include "ballistica/media/component/model.h" -#include "ballistica/media/component/texture.h" -#include "ballistica/python/python.h" -#include "ballistica/python/python_context_call.h" -#include "ballistica/python/python_sys.h" -#include "ballistica/scene/node/globals_node.h" -#include "ballistica/scene/node/node_type.h" -#include "ballistica/scene/scene.h" - -namespace ballistica { - -HostActivity::HostActivity(HostSession* host_session) { - // Store a link to the HostSession and add ourself to it. - host_session_ = host_session; - - // Create our game timer - gets called whenever game should step. - step_scene_timer_ = - base_timers_.NewTimer(base_time_, kGameStepMilliseconds, 0, -1, - NewLambdaRunnable([this] { StepScene(); })); - SetGameSpeed(1.0f); - { - ScopedSetContext cp(this); // So scene picks us up as context. - scene_ = Object::New(0); - - // If there's an output stream, add to it. - if (GameStream* out = host_session->GetGameStream()) { - out->AddScene(scene_.get()); - } - } -} - -HostActivity::~HostActivity() { - 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 activity and prevent them from running without - // a valid activity 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(); - } - } - for (auto&& i : collide_models_) { - if (i.second.exists()) { - i.second->MarkDead(); - } - } - for (auto&& i : materials_) { - if (i.exists()) { - i->MarkDead(); - } - } - - // Clear our timers and scene; this should wipe out any remaining refs to our - // python activity, allowing it to die. - base_timers_.Clear(); - sim_timers_.Clear(); - scene_.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 BA_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 " - + "HostActivity" + " (1 call is expected):"; - int count = 1; - for (auto& python_call : python_calls_) - s += "\n " + std::to_string(count++) + ": " - + (*python_call).GetObjectDescription(); - Log(s); - } -#endif // BA_DEBUG_BUILD -} - -auto HostActivity::GetGameStream() const -> GameStream* { - if (!host_session_.exists()) return nullptr; - return host_session_->GetGameStream(); -} - -void HostActivity::StepScene() { - int cycle_count = 1; - if (host_session_->benchmark_type() == BenchmarkType::kCPU) { - cycle_count = 100; - } - - for (int cycle = 0; cycle < cycle_count; ++cycle) { - assert(InGameThread()); - - // Clear our player-positions for this step. - // FIXME: Move this to scene and/or player node. - assert(host_session_.exists()); - for (auto&& player : host_session_->players()) { - assert(player.exists()); - player->set_have_position(false); - } - - // Run our sim-time timers. - sim_timers_.Run(scene()->time()); - - // Send die-messages/etc to out-of-bounds stuff. - HandleOutOfBoundsNodes(); - - scene()->Step(); - } -} - -void HostActivity::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 activity; call will not function: " - + call->GetObjectDescription()); - call->MarkDead(); - } -} - -void HostActivity::start() { - if (_started) { - Log("Error: Start called twice for activity."); - } - _started = true; -} - -auto HostActivity::GetAsHostActivity() -> HostActivity* { return this; } - -auto HostActivity::NewMaterial(const std::string& name) - -> Object::Ref { - if (shutting_down_) { - throw Exception("can't create materials during activity shutdown"); - } - - auto m(Object::New(name, scene())); - materials_.emplace_back(m); - return m; -} - -auto HostActivity::GetTexture(const std::string& name) -> Object::Ref { - if (shutting_down_) { - throw Exception("can't load assets during activity shutdown"); - } - return Media::GetMedia(&textures_, name, scene()); -} - -auto HostActivity::GetSound(const std::string& name) -> Object::Ref { - if (shutting_down_) { - throw Exception("can't load assets during activity shutdown"); - } - return Media::GetMedia(&sounds_, name, scene()); -} - -auto HostActivity::GetData(const std::string& name) -> Object::Ref { - if (shutting_down_) { - throw Exception("can't load assets during activity shutdown"); - } - return Media::GetMedia(&datas_, name, scene()); -} - -auto HostActivity::GetModel(const std::string& name) -> Object::Ref { - if (shutting_down_) { - throw Exception("can't load assets during activity shutdown"); - } - return Media::GetMedia(&models_, name, scene()); -} - -auto HostActivity::GetCollideModel(const std::string& name) - -> Object::Ref { - if (shutting_down_) { - throw Exception("can't load assets during activity shutdown"); - } - return Media::GetMedia(&collide_models_, name, scene()); -} - -void HostActivity::SetPaused(bool val) { - if (paused_ == val) { - return; - } - paused_ = val; - UpdateStepTimerLength(); -} - -void HostActivity::SetGameSpeed(float speed) { - if (speed == game_speed_) { - return; - } - assert(speed >= 0.0f); - game_speed_ = speed; - UpdateStepTimerLength(); -} - -void HostActivity::UpdateStepTimerLength() { - if (game_speed_ == 0.0f || paused_) { - step_scene_timer_->SetLength(-1, true, base_time_); - } else { - step_scene_timer_->SetLength( - std::max(1, static_cast( - round(static_cast(kGameStepMilliseconds) - / (game_speed_ * g_game->debug_speed_mult())))), - true, base_time_); - } -} - -void HostActivity::HandleOutOfBoundsNodes() { - if (scene()->out_of_bounds_nodes().empty()) { - out_of_bounds_in_a_row_ = 0; - return; - } - - // Make sure someone's handling our out-of-bounds messages. - out_of_bounds_in_a_row_++; - if (out_of_bounds_in_a_row_ > 100) { - Log("Warning: 100 consecutive out-of-bounds messages sent." - " They are probably not being handled properly"); - int j = 0; - for (auto&& i : scene()->out_of_bounds_nodes()) { - j++; - Node* n = i.get(); - if (n) { - std::string dstr; - PyObject* delegate = n->GetDelegate(); - if (delegate) { - dstr = PythonRef(delegate, PythonRef::kAcquire).Str(); - } - Log(" node #" + std::to_string(j) + ": type='" + n->type()->name() - + "' addr=" + Utils::PtrToString(i.get()) + " name='" + n->label() - + "' delegate=" + dstr); - } - } - out_of_bounds_in_a_row_ = 0; - } - - // Send out-of-bounds messages to newly out-of-bounds nodes. - for (auto&& i : scene()->out_of_bounds_nodes()) { - Node* n = i.get(); - if (n) { - n->DispatchOutOfBoundsMessage(); - } - } -} - -void HostActivity::RegisterPyActivity(PyObject* pyActivityObj) { - assert(pyActivityObj && pyActivityObj != Py_None); - assert(!py_activity_weak_ref_.exists()); - - // Store a python weak-ref to this activity. - py_activity_weak_ref_.Steal(PyWeakref_NewRef(pyActivityObj, nullptr)); -} - -auto HostActivity::GetPyActivity() const -> PyObject* { - PyObject* obj = py_activity_weak_ref_.get(); - if (!obj) return Py_None; - return PyWeakref_GetObject(obj); -} - -auto HostActivity::GetHostSession() -> HostSession* { - return host_session_.get(); -} - -auto HostActivity::GetMutableScene() -> Scene* { - Scene* sg = scene_.get(); - assert(sg); - return sg; -} - -void HostActivity::SetIsForeground(bool val) { - // If we're foreground, set our scene as foreground. - Scene* sg = scene(); - if (val && sg) { - // Set it locally. - g_game->SetForegroundScene(sg); - - // Also push it to clients. - if (GameStream* out = GetGameStream()) { - out->SetForegroundScene(scene_.get()); - } - } -} - -auto HostActivity::globals_node() const -> GlobalsNode* { - return globals_node_.get(); -} - -auto HostActivity::NewSimTimer(millisecs_t length, bool repeat, - const Object::Ref& runnable) -> int { - if (shutting_down_) { - BA_LOG_PYTHON_TRACE_ONCE( - "WARNING: Creating game timer during host-activity 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(); -} - -auto HostActivity::NewBaseTimer(millisecs_t length, bool repeat, - const Object::Ref& runnable) -> int { - if (shutting_down_) { - BA_LOG_PYTHON_TRACE_ONCE( - "WARNING: Creating session-time timer during host-activity shutdown"); - return 123; // dummy... - } - if (length == 0 && repeat) { - throw Exception("Can't add session-time timer with length 0 and repeat on"); - } - if (length < 0) { - throw Exception("Timer length cannot be < 0"); - } - - int offset = 0; - Timer* t = base_timers_.NewTimer(base_time_, length, offset, repeat ? -1 : 0, - runnable); - return t->id(); -} - -void HostActivity::DeleteSimTimer(int timer_id) { - assert(InGameThread()); - if (shutting_down_) return; - sim_timers_.DeleteTimer(timer_id); -} - -void HostActivity::DeleteBaseTimer(int timer_id) { - assert(InGameThread()); - if (shutting_down_) return; - base_timers_.DeleteTimer(timer_id); -} - -auto HostActivity::Update(millisecs_t time_advance) -> millisecs_t { - 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()); - - // If we haven't been told to start yet, don't do anything more. - if (!_started) { - return 1000; - } - - // Advance base time by the specified amount, stopping at 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_); - base_timers_.Run(base_time_); - if (!test_ref.exists()) { - return 1000; // The last timer run might have killed us. - } - } - base_time_ = target_base_time; - - // Periodically prune various dead refs. - if (base_time_ > next_prune_time_) { - PruneDeadMapRefs(&textures_); - PruneDeadMapRefs(&sounds_); - PruneDeadMapRefs(&collide_models_); - PruneDeadMapRefs(&models_); - PruneDeadRefs(&materials_); - PruneDeadRefs(&python_calls_); - next_prune_time_ = base_time_ + 5000; - } - - // Return the time until the next timer goes off. - return base_timers_.empty() ? 1000 - : base_timers_.GetTimeToNextExpire(base_time_); -} - -void HostActivity::ScreenSizeChanged() { scene()->ScreenSizeChanged(); } -void HostActivity::LanguageChanged() { scene()->LanguageChanged(); } -void HostActivity::DebugSpeedMultChanged() { UpdateStepTimerLength(); } -void HostActivity::GraphicsQualityChanged(GraphicsQuality q) { - scene()->GraphicsQualityChanged(q); -} - -void HostActivity::Draw(FrameDef* frame_def) { - if (!_started) return; - scene()->Draw(frame_def); -} - -void HostActivity::DumpFullState(GameStream* out) { - // Add our scene. - if (scene_.exists()) { - scene_->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 our media. - 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); - } - } - for (auto&& i : collide_models_) { - if (CollideModel* m = i.second.get()) { - out->AddCollideModel(m); - } - } - - // Add scene's nodes. - if (scene_.exists()) { - scene_->DumpNodes(out); - } - - // Ok, now we can fill out our materials since nodes/etc they reference - // exists. - for (auto&& i : materials_) { - if (Material* m = i.get()) { - m->DumpComponents(out); - } - } -} - -auto HostActivity::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: - return NewSimTimer(length, repeat, runnable); - case TimeType::kBase: - return NewBaseTimer(length, repeat, runnable); - default: - // Fall back to default for descriptive error otherwise. - return ContextTarget::NewTimer(timetype, length, repeat, runnable); - } -} - -void HostActivity::DeleteTimer(TimeType timetype, int timer_id) { - switch (timetype) { - case TimeType::kSim: - DeleteSimTimer(timer_id); - break; - case TimeType::kBase: - DeleteBaseTimer(timer_id); - break; - default: - // Fall back to default for descriptive error otherwise. - ContextTarget::DeleteTimer(timetype, timer_id); - break; - } -} - -auto HostActivity::GetTime(TimeType timetype) -> millisecs_t { - switch (timetype) { - case TimeType::kSim: - return scene()->time(); - case TimeType::kBase: - return base_time(); - default: - // Fall back to default for descriptive error otherwise. - return ContextTarget::GetTime(timetype); - } -} - -} // namespace ballistica diff --git a/src/ballistica/game/host_activity.h b/src/ballistica/game/host_activity.h index c8c9f530..36820f3c 100644 --- a/src/ballistica/game/host_activity.h +++ b/src/ballistica/game/host_activity.h @@ -63,7 +63,7 @@ class HostActivity : public ContextTarget { auto globals_node() const -> GlobalsNode*; void SetPaused(bool val); auto paused() const -> bool { return paused_; } - void setAllowKickIdlePlayers(bool val) { allow_kick_idle_players_ = val; } + void set_allow_kick_idle_players(bool val) { allow_kick_idle_players_ = val; } auto getAllowKickIdlePlayers() const -> bool { return allow_kick_idle_players_; } diff --git a/src/ballistica/game/player.cc b/src/ballistica/game/player.cc deleted file mode 100644 index 2afdd229..00000000 --- a/src/ballistica/game/player.cc +++ /dev/null @@ -1,418 +0,0 @@ -// Copyright (c) 2011-2020 Eric Froemling - -#include "ballistica/game/player.h" - -#include - -#include "ballistica/game/host_activity.h" -#include "ballistica/game/session/host_session.h" -#include "ballistica/generic/utils.h" -#include "ballistica/input/device/joystick.h" -#include "ballistica/python/class/python_class_session_player.h" -#include "ballistica/python/python.h" -#include "ballistica/python/python_context_call.h" -#include "ballistica/scene/node/node_attribute.h" -#include "ballistica/scene/node/node_type.h" - -namespace ballistica { - -Player::Player(int id_in, HostSession* host_session) - : id_(id_in), creation_time_(GetRealTime()), host_session_(host_session) { - assert(host_session); - assert(InGameThread()); -} - -Player::~Player() { - assert(InGameThread()); - - // If we have an input-device attached to us, detach it. - InputDevice* input_device = input_device_.get(); - if (input_device) { - input_device->DetachFromPlayer(); - } - - // Release our ref to ourself if we have one. - if (py_ref_) { - Py_DECREF(py_ref_); - } -} - -auto Player::GetName(bool full, bool icon) const -> std::string { - std::string n = full ? full_name_ : name_; - - // Quasi-hacky: if they ask for no icon, strip the first char off our string - // if its in the custom-use-range. - if (!icon) { - std::vector uni = Utils::UnicodeFromUTF8(n, "3f94f4f"); - if (!uni.empty() && uni[0] >= 0xE000 && uni[0] <= 0xF8FF) { - uni.erase(uni.begin()); - } - return Utils::UTF8FromUnicode(uni); - } else { - return n; - } -} - -auto Player::GetHostActivity() const -> HostActivity* { - return host_activity_.get(); -} - -void Player::SetHostActivity(HostActivity* a) { - assert(InGameThread()); - - // Make sure we get pulled out of one activity before being added to another. - if (a && in_activity_) { - std::string old_name = - host_activity_.exists() - ? PythonRef(host_activity_->GetPyActivity(), PythonRef::kAcquire) - .Str() - : ""; - std::string new_name = - PythonRef(a->GetPyActivity(), PythonRef::kAcquire).Str(); - BA_LOG_PYTHON_TRACE_ONCE( - "Player::SetHostActivity() called when already in an activity (old=" - + old_name + ", new=" + new_name + ")"); - } else if (!a && !in_activity_) { - BA_LOG_PYTHON_TRACE_ONCE( - "Player::SetHostActivity() called with nullptr when not in an " - "activity"); - } - host_activity_ = a; - in_activity_ = (a != nullptr); -} - -void Player::SetPosition(const Vector3f& position) { - position_ = position; - have_position_ = true; -} - -void Player::ResetInput() { - // Hold a ref to ourself while clearing this to make sure - // we don't die midway as a result of freeing something. - Object::Ref ref(this); - calls_.clear(); - left_held_ = right_held_ = up_held_ = down_held_ = have_position_ = false; -} - -void Player::SetPyTeam(PyObject* team) { - if (team != nullptr && team != Py_None) { - // We store a weak-ref to this. - py_team_weak_ref_.Steal(PyWeakref_NewRef(team, nullptr)); - } else { - py_team_weak_ref_.Release(); - } -} - -auto Player::GetPyTeam() -> PyObject* { - PyObject* obj = py_team_weak_ref_.get(); - if (!obj) { - return Py_None; - } - return PyWeakref_GetObject(obj); -} - -void Player::SetPyCharacter(PyObject* character) { - if (character != nullptr && character != Py_None) { - py_character_.Acquire(character); - } else { - py_character_.Release(); - } -} - -auto Player::GetPyCharacter() -> PyObject* { - return py_character_.exists() ? py_character_.get() : Py_None; -} - -void Player::SetPyColor(PyObject* c) { py_color_.Acquire(c); } -auto Player::GetPyColor() -> PyObject* { - return py_color_.exists() ? py_color_.get() : Py_None; -} - -void Player::SetPyHighlight(PyObject* c) { py_highlight_.Acquire(c); } -auto Player::GetPyHighlight() -> PyObject* { - return py_highlight_.exists() ? py_highlight_.get() : Py_None; -} - -void Player::SetPyActivityPlayer(PyObject* c) { py_activityplayer_.Acquire(c); } -auto Player::GetPyActivityPlayer() -> PyObject* { - return py_activityplayer_.exists() ? py_activityplayer_.get() : Py_None; -} - -auto Player::GetPyRef(bool new_ref) -> PyObject* { - assert(InGameThread()); - if (py_ref_ == nullptr) { - py_ref_ = PythonClassSessionPlayer::Create(this); - } - if (new_ref) { - Py_INCREF(py_ref_); - } - return py_ref_; -} - -void Player::AssignInputCall(InputType type, PyObject* call_obj) { - assert(InGameThread()); - assert(static_cast(type) >= 0 - && static_cast(type) < static_cast(InputType::kLast)); - - // Special case: if they're assigning hold-position-press or - // hold-position-release, or any direction events, we add in a hold-position - // press/release event before we deliver any other events.. that way newly - // created stuff is informed of the hold state and doesn't wrongly think they - // should start moving. - switch (type) { - case InputType::kHoldPositionPress: - case InputType::kHoldPositionRelease: - case InputType::kLeftPress: - case InputType::kLeftRelease: - case InputType::kRightPress: - case InputType::kUpPress: - case InputType::kUpRelease: - case InputType::kDownPress: - case InputType::kDownRelease: - case InputType::kUpDown: - case InputType::kLeftRight: { - send_hold_state_ = true; - break; - } - default: - break; - } - if (call_obj) { - calls_[static_cast(type)] = Object::New(call_obj); - } else { - calls_[static_cast(type)].Clear(); - } - - // If they assigned l/r, immediately send an update for its current value. - if (type == InputType::kLeftRight) { - RunInput(type, lr_state_); - } - - // Same for up/down. - if (type == InputType::kUpDown) { - RunInput(type, ud_state_); - } - - // Same for run. - if (type == InputType::kRun) { - RunInput(type, run_state_); - } - - // Same for fly. - if (type == InputType::kFlyPress && fly_held_) { - RunInput(type); - } -} - -void Player::RunInput(InputType type, float value) { - assert(InGameThread()); - - const float threshold = kJoystickDiscreteThresholdFloat; - - // Most input commands cause us to reset the player's time-out - // there are a few exceptions though - very small analog values - // get ignored since they can come through without user intervention. - bool reset_time_out = true; - if (type == InputType::kLeftRight || type == InputType::kUpDown) { - if (std::abs(value) < 0.3f) { - reset_time_out = false; - } - } - if (type == InputType::kRun) { - if (value < 0.3f) { - reset_time_out = false; - } - } - - // Also ignore hold-position stuff since it can come through without user - // interaction. - if ((type == InputType::kHoldPositionPress) - || (type == InputType::kHoldPositionRelease)) - reset_time_out = false; - - if (reset_time_out) { - time_out_ = BA_PLAYER_TIME_OUT; - } - - // Keep track of the hold-position state that comes through here. - // any-time hold position buttons are re-assigned, we subsequently - // re-send the current hold-state so whatever its driving starts out correctly - // held if need be. - if (type == InputType::kHoldPositionPress) { - hold_position_ = true; - } else if (type == InputType::kHoldPositionRelease) { - hold_position_ = false; - } else if (type == InputType::kFlyPress) { - fly_held_ = true; - } else if (type == InputType::kFlyRelease) { - fly_held_ = false; - } - - // If we were supposed to deliver hold-state, go ahead and do that first. - if (send_hold_state_) { - send_hold_state_ = false; - if (hold_position_) { - RunInput(InputType::kHoldPositionPress); - } else { - RunInput(InputType::kHoldPositionRelease); - } - } - - // Let's make our life simpler by converting held-position-joystick-events.. - { - // We need to store these since we might look at them during a hold-position - // event when we don't have their originating events available. - if (type == InputType::kLeftRight) { - lr_state_ = value; - } - if (type == InputType::kUpDown) { - ud_state_ = value; - } - if (type == InputType::kRun) { - run_state_ = value; - } - - // Special input commands - keep track of left/right and up/down positions - // so we can deliver simple "leftUp", "leftDown", etc type of events - // in addition to the standard absolute leftRight positions, etc. - if (type == InputType::kLeftRight || type == InputType::kHoldPositionPress - || type == InputType::kHoldPositionRelease) { - float arg = lr_state_; - if (hold_position_) { - arg = 0.0f; // Throttle is off. - } - if (left_held_) { - if (arg > -threshold) { - left_held_ = false; - RunInput(InputType::kLeftRelease); - } - } else if (right_held_) { - if (arg < threshold) { - right_held_ = false; - RunInput(InputType::kRightRelease); - } - } else { - if (arg >= threshold) { - if (!left_held_ && !up_held_ && !down_held_) { - right_held_ = true; - RunInput(InputType::kRightPress); - } - } else if (arg <= -threshold) { - if (!right_held_ && !up_held_ && !down_held_) { - left_held_ = true; - RunInput(InputType::kLeftPress); - } - } - } - } - if (type == InputType::kUpDown || type == InputType::kHoldPositionPress - || type == InputType::kHoldPositionRelease) { - float arg = ud_state_; - if (hold_position_) arg = 0.0f; // throttle is off; - if (up_held_) { - if (arg < threshold) { - up_held_ = false; - RunInput(InputType::kUpRelease); - } - } else if (down_held_) { - if (arg > -threshold) { - down_held_ = false; - RunInput(InputType::kDownRelease); - } - } else { - if (arg <= -threshold) { - if (!left_held_ && !right_held_ && !up_held_) { - down_held_ = true; - RunInput(InputType::kDownPress); - } - } else if (arg >= threshold) { - if (!left_held_ && !up_held_ && !right_held_) { - up_held_ = true; - RunInput(InputType::kUpPress); - } - } - } - } - } - - auto j = calls_.find(static_cast(type)); - if (j != calls_.end() && j->second.exists()) { - if (type == InputType::kRun) { - PythonRef args( - Py_BuildValue("(f)", std::min(1.0f, std::max(0.0f, value))), - PythonRef::kSteal); - j->second->Run(args.get()); - } else if (type == InputType::kLeftRight || type == InputType::kUpDown) { - PythonRef args( - Py_BuildValue("(f)", std::min(1.0f, std::max(-1.0f, value))), - PythonRef::kSteal); - j->second->Run(args.get()); - } else { - j->second->Run(); - } - } -} - -auto Player::GetHostSession() const -> HostSession* { - return host_session_.get(); -} - -void Player::SetName(const std::string& name, const std::string& full_name, - bool is_real) { - assert(InGameThread()); - HostSession* host_session = GetHostSession(); - BA_PRECONDITION(host_session); - name_is_real_ = is_real; - name_ = host_session->GetUnusedPlayerName(this, name); - full_name_ = full_name; - - // If we're already in the game and our name is changing, we need to update - // the roster. - if (accepted_) { - g_game->UpdateGameRoster(); - } -} - -void Player::InputCommand(InputType type, float value) { - assert(InGameThread()); - switch (type) { - case InputType::kUpDown: - case InputType::kLeftRight: - case InputType::kRun: - RunInput(type, value); - break; - // case InputType::kReset: - // Log("Error: FIXME: player-input-reset command unimplemented"); - // break; - default: - RunInput(type); - break; - } -} - -void Player::SetInputDevice(InputDevice* input_device) { - input_device_ = input_device; -} - -auto Player::GetPublicAccountID() const -> std::string { - assert(InGameThread()); - if (input_device_.exists()) { - return input_device_->GetPublicAccountID(); - } - return ""; -} - -void Player::SetIcon(const std::string& tex_name, - const std::string& tint_tex_name, - const std::vector& tint_color, - const std::vector& tint2_color) { - assert(tint_color.size() == 3); - assert(tint2_color.size() == 3); - icon_tex_name_ = tex_name; - icon_tint_tex_name_ = tint_tex_name; - icon_tint_color_ = tint_color; - icon_tint2_color_ = tint2_color; - icon_set_ = true; -} - -} // namespace ballistica diff --git a/src/ballistica/game/player_spec.cc b/src/ballistica/game/player_spec.cc deleted file mode 100644 index ecdb7462..00000000 --- a/src/ballistica/game/player_spec.cc +++ /dev/null @@ -1,109 +0,0 @@ -// Copyright (c) 2011-2020 Eric Froemling - -#include "ballistica/game/player_spec.h" - -#include - -#include "ballistica/app/app_globals.h" -#include "ballistica/game/account.h" -#include "ballistica/generic/json.h" -#include "ballistica/generic/utils.h" -#include "ballistica/platform/platform.h" - -namespace ballistica { - -PlayerSpec::PlayerSpec() : account_type_(AccountType::kInvalid) {} - -PlayerSpec::PlayerSpec(const std::string& s) { - cJSON* root_obj = cJSON_Parse(s.c_str()); - bool success = false; - if (root_obj) { - cJSON* name_obj = cJSON_GetObjectItem(root_obj, "n"); - cJSON* short_name_obj = cJSON_GetObjectItem(root_obj, "sn"); - cJSON* account_obj = cJSON_GetObjectItem(root_obj, "a"); - if (name_obj && short_name_obj && account_obj) { - name_ = Utils::GetValidUTF8(name_obj->valuestring, "psps"); - short_name_ = Utils::GetValidUTF8(short_name_obj->valuestring, "psps2"); - - // Account type may technically be something we don't recognize, - // but that's ok.. it'll just be 'invalid' to us in that case - account_type_ = Account::AccountTypeFromString(account_obj->valuestring); - success = true; - } - cJSON_Delete(root_obj); - } - if (!success) { - Log("Error creating PlayerSpec from string: '" + s + "'"); - name_ = ""; - short_name_ = ""; - account_type_ = AccountType::kInvalid; - } -} - -auto PlayerSpec::GetDisplayString() const -> std::string { - return Account::AccountTypeToIconString(account_type_) + name_; -} - -auto PlayerSpec::GetShortName() const -> std::string { - if (short_name_.empty()) { - return name_; - } - return short_name_; -} - -auto PlayerSpec::operator==(const PlayerSpec& spec) const -> bool { - // NOTE: need to add account ID in here once that's available - return (spec.name_ == name_ && spec.short_name_ == short_name_ - && spec.account_type_ == account_type_); -} - -auto PlayerSpec::GetSpecString() const -> std::string { - cJSON* root; - root = cJSON_CreateObject(); - cJSON_AddStringToObject(root, "n", name_.c_str()); - cJSON_AddStringToObject(root, "a", - Account::AccountTypeToString(account_type_).c_str()); - cJSON_AddStringToObject(root, "sn", short_name_.c_str()); - char* out = cJSON_PrintUnformatted(root); - std::string out_s = out; - free(out); - cJSON_Delete(root); - - // We should never allow ourself to have all this add up to more than 256. - assert(out_s.size() < 256); - - return out_s; -} - -auto PlayerSpec::GetAccountPlayerSpec() -> PlayerSpec { - PlayerSpec spec; - if (g_account->GetAccountState() == AccountState::kSignedIn) { - spec.account_type_ = g_app_globals->account_type; - spec.name_ = - Utils::GetValidUTF8(g_account->GetAccountName().c_str(), "bsgaps"); - } else { - spec.name_ = - Utils::GetValidUTF8(g_platform->GetDeviceName().c_str(), "bsgaps2"); - } - if (spec.name_.size() > 100) { - // FIXME should perhaps clamp this in unicode space - Log("account name size too long: '" + spec.name_ + "'"); - spec.name_.resize(100); - spec.name_ = Utils::GetValidUTF8(spec.name_.c_str(), "bsgaps3"); - } - return spec; -} - -auto PlayerSpec::GetDummyPlayerSpec(const std::string& name) -> PlayerSpec { - PlayerSpec spec; - spec.name_ = Utils::GetValidUTF8(name.c_str(), "bsgdps1"); - if (spec.name_.size() > 100) { - // FIXME should perhaps clamp this in unicode space - Log("dummy player spec name too long: '" + spec.name_ + "'"); - spec.name_.resize(100); - spec.name_ = Utils::GetValidUTF8(spec.name_.c_str(), "bsgdps2"); - } - return spec; -} - -} // namespace ballistica diff --git a/src/ballistica/game/session/client_session.cc b/src/ballistica/game/session/client_session.cc deleted file mode 100644 index d0013f3c..00000000 --- a/src/ballistica/game/session/client_session.cc +++ /dev/null @@ -1,1070 +0,0 @@ -// Copyright (c) 2011-2020 Eric Froemling - -#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/graphics/graphics.h" -#include "ballistica/media/component/collide_model.h" -#include "ballistica/media/component/model.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 { - -// How many buckets we keep around for calculating max-lag-spikes. -const int kDelayBufferBuckets = 20; - -ClientSession::ClientSession() - : base_time_(0), - least_buffered_count_list_(kDelayBufferBuckets, 0), - most_buffered_count_list_(kDelayBufferBuckets, 0), - adjust_counter_(0), - buffer_count_list_index_(0), - steps_on_list_(0), - current_cmd_ptr_(nullptr), - shutting_down_(false) { - ClearSessionObjs(); -} - -void ClientSession::Reset(bool rewind) { - assert(!shutting_down_); - OnReset(rewind); -} - -void ClientSession::OnReset(bool rewind) { - ClearSessionObjs(); - target_base_time_ = 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(); - steps_on_list_ = 0; -} - -auto ClientSession::DoesFillScreen() const -> bool { - // Look for any scene that has something that covers the background. - 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. - time_advance = GetActualTimeAdvance(time_advance); - - target_base_time_ += static_cast(time_advance) * correction_; - - 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 BA_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()); -#endif - - current_cmd_ = commands_.front(); - commands_.pop_front(); - current_cmd_ptr_ = &(current_cmd_[0]); - } else { - // Any time we run out of commands we immediately pull our target time - // back to where we currently are at. We want to stay *behind* the - // "buffering line". - target_base_time_ = base_time_; - 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"); - } - steps_on_list_ -= stepsize; - BA_PRECONDITION(steps_on_list_ >= 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]->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: { -#if !BA_HEADLESS_BUILD - BGDynamicsEmission e; -#endif - int cmdvals[4]; - ReadInt32_4(cmdvals); -#if !BA_HEADLESS_BUILD - e.emit_type = (BGDynamicsEmitType)cmdvals[0]; - e.count = cmdvals[1]; - e.chunk_type = (BGDynamicsChunkType)cmdvals[2]; - e.tendril_type = (BGDynamicsTendrilType)cmdvals[3]; -#endif - float vals[8]; - ReadFloats(8, vals); -#if !BA_HEADLESS_BUILD - 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); -#endif - 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 replays?...) - 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 subBuffer; - while (true) { - uint16_t size; - memcpy(&size, &(buffer[offset]), 2); - if (offset + size > buffer.size()) { - Error("invalid state message"); - return; - } - subBuffer.resize(size); - memcpy(&(subBuffer[0]), &(buffer[offset + 2]), subBuffer.size()); - AddCommand(subBuffer); - offset += 2 + size; // move to next command - if (offset == buffer.size()) { - // lets 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 bufferOut = buffer; - bufferOut[0] = static_cast(SessionCommand::kDynamicsCorrection); - AddCommand(bufferOut); - 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. - steps_on_list_ += 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); - } - } - } -} - -} // namespace ballistica diff --git a/src/ballistica/game/session/host_session.cc b/src/ballistica/game/session/host_session.cc deleted file mode 100644 index 914beba7..00000000 --- a/src/ballistica/game/session/host_session.cc +++ /dev/null @@ -1,765 +0,0 @@ -// Copyright (c) 2011-2020 Eric Froemling - -#include "ballistica/game/session/host_session.h" - -#include "ballistica/core/context.h" -#include "ballistica/game/game_stream.h" -#include "ballistica/game/host_activity.h" -#include "ballistica/game/player.h" -#include "ballistica/generic/lambda_runnable.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" -#include "ballistica/scene/scene.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_; - // Log("DO REPLAY? " + std::to_string(do_replay)); - - // At the moment headless-server don't write replays. -#if BA_HEADLESS_BUILD - do_replay = false; -#endif // BA_HEADLESS_BUILD - 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::kGarbageCollectCall).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, - // stopping at 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 BA_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); - } -#endif // BA_DEBUG_BUILD - } 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 deleted file mode 100644 index c555cebe..00000000 --- a/src/ballistica/game/session/net_client_session.cc +++ /dev/null @@ -1,147 +0,0 @@ -// Copyright (c) 2011-2020 Eric Froemling - -#include "ballistica/game/session/net_client_session.h" - -#include -#include - -#include "ballistica/app/app_globals.h" -#include "ballistica/game/connection/connection_to_host.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() { - // Any time we run out of data, hit the brakes on our playback speed. - // Update: maybe not. - // correction_ *= 0.99f; -} - -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(); -} - -void NetClientSession::UpdateBuffering() { - // if (NetGraph *graph = g_graphics->debug_graph_1()) { - // graph->addSample(GetRealTime(), steps_on_list_); - // } - - // 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. - { - int bucket_count = static_cast(least_buffered_count_list_.size()); - - // Change bucket every g_delay_samples samples. - int bucket = (buffer_count_list_index_ / g_app_globals->delay_samples) - % bucket_count; - int bucket_iteration = - buffer_count_list_index_ % g_app_globals->delay_samples; - - // *Set* the value the first iteration in each bucket; do *min* after that. - if (bucket_iteration == 0) { - least_buffered_count_list_[bucket] = steps_on_list_; - most_buffered_count_list_[bucket] = steps_on_list_; - } else { - least_buffered_count_list_[bucket] = - std::min(least_buffered_count_list_[bucket], steps_on_list_); - most_buffered_count_list_[bucket] = - std::max(most_buffered_count_list_[bucket], steps_on_list_); - - // After the last sample in each bucket, feed the max bucket value in - // as the 'low pass' buffer-count. The low-pass curve minus our largest - // spike value should be where we want to aim for in the buffer. - if (bucket_iteration == g_app_globals->delay_samples - 1) { - float smoothing = 0.5f; - low_pass_smoothed_ = - smoothing * low_pass_smoothed_ - + (1.0f - smoothing) - * static_cast(most_buffered_count_list_[bucket]); - } - } - - // Keep track of the largest min/max difference in our sample segments. - int largest_spike = 0; - - buffer_count_list_index_++; - for (int i = 1; i < bucket_count; i++) { - int spike = most_buffered_count_list_[i] - least_buffered_count_list_[i]; - if (spike > largest_spike) { - largest_spike = spike; - } - } - - // Slowly adjust largest spike value based on the biggest in recent history. - { - float smoothing = 0.95f; - largest_spike_smoothed_ = - smoothing * largest_spike_smoothed_ - + (1.0f - smoothing) * static_cast(largest_spike); - } - - // Low pass is the most buffered data we've had in the most recent slot. - float ideal_offset = low_pass_smoothed_ - largest_spike_smoothed_ * 1.0f; - - // Any time we've got no current buffered data, slow down fast. - // (otherwise we can get stuck cruising along with no 0 buffered data and - // things get real jerky looking) - if (steps_on_list_ == 0) { - ideal_offset -= 100.0f; - } - float smoothing = 0.0f; - correction_ = smoothing * correction_ - + (1.0f - smoothing) * (1.0f + 0.002f * ideal_offset); - correction_ = std::min(1.5f, std::max(0.5f, correction_)); - // if (NetGraph *graph = g_graphics->debug_graph_2()) { - // graph->addSample(GetRealTime(), correction_); - // } - } -} -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 deleted file mode 100644 index f38c8362..00000000 --- a/src/ballistica/game/session/replay_client_session.cc +++ /dev/null @@ -1,321 +0,0 @@ -// Copyright (c) 2011-2020 Eric Froemling - -#include "ballistica/game/session/replay_client_session.h" - -#include -#include -#include -#include - -#include "ballistica/dynamics/material/material.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)), - file_(nullptr), - message_fetch_num_(0), - have_sent_client_message_(false) { - // take responsibility for feeding all clients to this device.. - g_game->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->UnregisterClientController(this); - - if (file_) { - fclose(file_); - file_ = nullptr; - } -} - -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) - commands_.emplace_back(1, - static_cast(SessionCommand::kEndOfFile)); - 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) - commands_.emplace_back( - 1, static_cast(SessionCommand::kEndOfFile)); - 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) - commands_.emplace_back( - 1, static_cast(SessionCommand::kEndOfFile)); - 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) { - commands_.emplace_back(1, - static_cast(SessionCommand::kEndOfFile)); - 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; - } - } -} - -void ReplayClientSession::DumpFullState(GameStream* out) { - // This shouldn't actually be replay-specific. Should move this up to - // ClientSession perhaps? - - // 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 all the nodes/etc they reference exist. - for (auto&& i : materials_) { - if (Material* m = i.get()) { - m->DumpComponents(out); - } - } -} - -} // namespace ballistica diff --git a/src/ballistica/game/session/session.cc b/src/ballistica/game/session/session.cc deleted file mode 100644 index 756e5d9f..00000000 --- a/src/ballistica/game/session/session.cc +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (c) 2011-2020 Eric Froemling - -#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