diff --git a/.efrocachemap b/.efrocachemap index b914499d..b55135bd 100644 --- a/.efrocachemap +++ b/.efrocachemap @@ -3932,24 +3932,24 @@ "assets/build/windows/Win32/ucrtbased.dll": "https://files.ballistica.net/cache/ba1/b5/85/f8b6d0558ddb87267f34254b1450", "assets/build/windows/Win32/vc_redist.x86.exe": "https://files.ballistica.net/cache/ba1/1c/e1/4a1a2eddda2f4aebd5f8b64ab08e", "assets/build/windows/Win32/vcruntime140d.dll": "https://files.ballistica.net/cache/ba1/50/8d/bc2600ac9491f1b14d659709451f", - "build/prefab/full/linux_x86_64/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/19/53/2a9c168ffd8bc53475e8c9e139ed", - "build/prefab/full/linux_x86_64/release/ballisticacore": "https://files.ballistica.net/cache/ba1/20/79/28f2821d36d20be52eebee1475fa", - "build/prefab/full/linux_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/6e/4e/1f59b95892fdd6ebf55159c0fb69", - "build/prefab/full/linux_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/3c/64/8790eac167ed9cb6ced1b4e43a34", - "build/prefab/full/mac_x86_64/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/06/6c/1ffc0a07960c3272304b1b5f858a", - "build/prefab/full/mac_x86_64/release/ballisticacore": "https://files.ballistica.net/cache/ba1/6c/fd/193b81fcda6c6716e3dcaa7f7cb8", - "build/prefab/full/mac_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/0f/30/8ac58a8ebc1b73dc335d82d7cecb", - "build/prefab/full/mac_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/26/a7/dc477a2297d27eb7fbb14a662619", - "build/prefab/full/windows_x86/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/70/07/1e15c6fcdbf35ceda17a5d324394", - "build/prefab/full/windows_x86/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/9b/2b/59a36a36dd90410b4a94726a3e4b", - "build/prefab/full/windows_x86_server/debug/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/fc/36/6bafb410c3ad3d458545fc495cfd", - "build/prefab/full/windows_x86_server/release/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/f3/29/0e5332b1ab93ea7c564fea7ce980", - "build/prefab/lib/linux_x86_64/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/4d/01/29abecb0b2bd764ad3c261f1727e", - "build/prefab/lib/linux_x86_64/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/68/c6/61c27c594a5b67abb7cd0a2a0f51", - "build/prefab/lib/linux_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/79/a9/480cd7947a83b5e52359945c450e", - "build/prefab/lib/linux_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/53/96/32edc65468aa4546f245192238c3", - "build/prefab/lib/mac_x86_64/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/18/34/0c7bc3754d9dc5a796133484a891", - "build/prefab/lib/mac_x86_64/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/0a/69/49203680f51913705a4f6263ef01", - "build/prefab/lib/mac_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/08/ea/3732982a8df8b9510b9ae4bb3ace", - "build/prefab/lib/mac_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/9c/31/60fcef7fd4e08d4ebb1859f4c9a3" + "build/prefab/full/linux_x86_64/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/50/39/970b2b3f334bdbfd72469cd27f8c", + "build/prefab/full/linux_x86_64/release/ballisticacore": "https://files.ballistica.net/cache/ba1/bd/46/92d63ccc123d646df92cd1545d0a", + "build/prefab/full/linux_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/d0/8e/be85a2a57eaeb26f9d09899e6811", + "build/prefab/full/linux_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/ee/f8/28c1f4606f3d2234dbbe79990c61", + "build/prefab/full/mac_x86_64/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/2d/02/5843eaa3f9118883523042379292", + "build/prefab/full/mac_x86_64/release/ballisticacore": "https://files.ballistica.net/cache/ba1/89/dc/8b965051d0c27d5812d80ecac1d3", + "build/prefab/full/mac_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/16/89/34ea43bcd3901c8e5157460c37d6", + "build/prefab/full/mac_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/ae/72/30fe5cbfa936477b4f61e861a7ae", + "build/prefab/full/windows_x86/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/e6/5d/4402c2e9193641ee78d20c85ff3d", + "build/prefab/full/windows_x86/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/02/0b/dcdc0dd9147dd9a56992572aeb60", + "build/prefab/full/windows_x86_server/debug/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/02/87/645ddc08b2bdb90fcc69430a7b0b", + "build/prefab/full/windows_x86_server/release/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/3f/ce/60d6f026d3f22d800538ffd69da8", + "build/prefab/lib/linux_x86_64/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/15/54/bfba7d740c7221a5d46e8e21c756", + "build/prefab/lib/linux_x86_64/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/4b/1f/ca36bea671a5b88a7e2ccf2e4c4a", + "build/prefab/lib/linux_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/57/2d/e4b9a67cb21131cdcdfb8287f9e7", + "build/prefab/lib/linux_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/59/b6/6ffc20f2c0253180496d2dae968c", + "build/prefab/lib/mac_x86_64/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/b9/0a/dc300391e73286380eab87f6e742", + "build/prefab/lib/mac_x86_64/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/97/76/7e35dd32e4507a4521a74d8c1b03", + "build/prefab/lib/mac_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/9d/60/716cde2b1ec5efb8eedfa1a978f1", + "build/prefab/lib/mac_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/a2/a2/70a0bda587c2405a67c6709a5d0a" } \ No newline at end of file diff --git a/assets/src/ba_data/python/bastd/ui/settings/advanced.py b/assets/src/ba_data/python/bastd/ui/settings/advanced.py index 4294d56c..062c96c2 100644 --- a/assets/src/ba_data/python/bastd/ui/settings/advanced.py +++ b/assets/src/ba_data/python/bastd/ui/settings/advanced.py @@ -22,7 +22,6 @@ class AdvancedSettingsWindow(ba.Window): origin_widget: ba.Widget = None): # pylint: disable=too-many-statements from ba.internal import master_server_get - import threading # Preload some modules we use in a background thread so we won't @@ -65,7 +64,8 @@ class AdvancedSettingsWindow(ba.Window): # In vr-mode, the internal keyboard is currently the *only* option, # so no need to show this. - self._show_always_use_internal_keyboard = (not app.vr_mode) + self._show_always_use_internal_keyboard = (not app.vr_mode + and not app.iircade_mode) self._scroll_width = self._width - (100 + 2 * x_inset) self._scroll_height = self._height - 115.0 diff --git a/ballisticacore-cmake/.idea/dictionaries/ericf.xml b/ballisticacore-cmake/.idea/dictionaries/ericf.xml index db058f62..4aaad6d6 100644 --- a/ballisticacore-cmake/.idea/dictionaries/ericf.xml +++ b/ballisticacore-cmake/.idea/dictionaries/ericf.xml @@ -12,8 +12,8 @@ ack'ed acked acks - aclass aclass's + aclass activityplayer addrs adjoint @@ -41,6 +41,7 @@ appconfig appname appnameupper + appstate asci assigninput athome @@ -144,8 +145,8 @@ cmath cmds cmdvals - codewarrior codewarrior's + codewarrior cofnodes collapseable collidable @@ -399,6 +400,7 @@ ifaddrs ifdebug iiiiisss + iircade illum ilock imagewidget diff --git a/ballisticacore-cmake/CMakeLists.txt b/ballisticacore-cmake/CMakeLists.txt index 833edf39..358cf738 100644 --- a/ballisticacore-cmake/CMakeLists.txt +++ b/ballisticacore-cmake/CMakeLists.txt @@ -274,11 +274,13 @@ add_executable(ballisticacore ${BA_SRC_ROOT}/ballistica/game/account.h ${BA_SRC_ROOT}/ballistica/game/client_controller_interface.h ${BA_SRC_ROOT}/ballistica/game/connection/connection.h + ${BA_SRC_ROOT}/ballistica/game/connection/connection_set.h ${BA_SRC_ROOT}/ballistica/game/connection/connection_to_client.h ${BA_SRC_ROOT}/ballistica/game/connection/connection_to_client_udp.h ${BA_SRC_ROOT}/ballistica/game/connection/connection_to_host.h ${BA_SRC_ROOT}/ballistica/game/connection/connection_to_host_udp.h ${BA_SRC_ROOT}/ballistica/game/friend_score_set.h + ${BA_SRC_ROOT}/ballistica/game/game.cc ${BA_SRC_ROOT}/ballistica/game/game.h ${BA_SRC_ROOT}/ballistica/game/game_stream.h ${BA_SRC_ROOT}/ballistica/game/host_activity.h diff --git a/docs/ba_module.md b/docs/ba_module.md index fb9cd596..c75fff49 100644 --- a/docs/ba_module.md +++ b/docs/ba_module.md @@ -1,5 +1,5 @@ -

last updated on 2020-10-13 for Ballistica version 1.5.26 build 20205

+

last updated on 2020-10-15 for Ballistica version 1.5.26 build 20213

This page documents the Python classes and functions in the 'ba' module, which are the ones most relevant to modding in Ballistica. If you come across something you feel should be included here or could be better explained, please let me know. Happy modding!


diff --git a/src/ballistica/ballistica.cc b/src/ballistica/ballistica.cc index 63c974a1..38385608 100644 --- a/src/ballistica/ballistica.cc +++ b/src/ballistica/ballistica.cc @@ -29,7 +29,7 @@ namespace ballistica { // These are set automatically via script; don't change here. -const int kAppBuildNumber = 20210; +const int kAppBuildNumber = 20215; const char* kAppVersion = "1.5.26"; // Our standalone globals. diff --git a/src/ballistica/core/types.h b/src/ballistica/core/types.h index 0457d6e5..d9b6c903 100644 --- a/src/ballistica/core/types.h +++ b/src/ballistica/core/types.h @@ -68,6 +68,7 @@ class CollideModelData; class Collision; class CollisionCache; class Connection; +class ConnectionSet; class ConnectionToClient; class Context; class ContextTarget; diff --git a/src/ballistica/game/connection/connection_set.h b/src/ballistica/game/connection/connection_set.h new file mode 100644 index 00000000..62e6ef18 --- /dev/null +++ b/src/ballistica/game/connection/connection_set.h @@ -0,0 +1,136 @@ +// Released under the MIT License. See LICENSE for details. + +#ifndef BALLISTICA_GAME_CONNECTION_CONNECTION_SET_H_ +#define BALLISTICA_GAME_CONNECTION_CONNECTION_SET_H_ + +#include +#include +#include + +#include "ballistica/core/object.h" + +namespace ballistica { + +class ConnectionSet { + public: + ConnectionSet(); + + // Whoever wants to wrangle current client connections should call this + // to register itself. Note that it must explicitly call unregister when + // unregistering itself. + auto RegisterClientController(ClientControllerInterface* c) -> void; + auto UnregisterClientController(ClientControllerInterface* c) -> void; + + // Quick test as to whether there are clients. Does not check if they are + // fully connected. + auto has_connection_to_clients() const -> bool { + assert(InGameThread()); + return (!connections_to_clients_.empty()); + } + + // Returns our host-connection or nullptr if there is none. + auto connection_to_host() -> ConnectionToHost* { + return connection_to_host_.get(); + } + auto GetConnectionToHostUDP() -> ConnectionToHostUDP*; + + auto connections_to_clients() + -> const std::map >& { + return connections_to_clients_; + } + auto client_controller() -> ClientControllerInterface* { + return client_controller_; + } + + // Simple thread safe query. + auto has_connection_to_host() const -> bool { + return has_connection_to_host_; + } + + auto Update() -> void; + auto Shutdown() -> void; + auto PrepareForLaunchHostSession() -> void; + auto HandleClientDisconnected(int id) -> void; + // Returns true if disconnect attempts are supported. + auto DisconnectClient(int client_id, int ban_seconds) -> bool; + auto ForceDisconnectClients() -> void; + auto PushHostConnectedUDPCall(const SockAddr& addr, + bool print_connect_progress) -> void; + auto PushDisconnectFromHostCall() -> void; + auto PushDisconnectedFromHostCall() -> void; + auto GetPrintUDPConnectProgress() const -> bool { + return print_udp_connect_progress_; + } + auto PushUDPConnectionPacketCall(const std::vector& data, + const SockAddr& addr) -> void; + // Return our client connections (if any). + // FIXME: this prunes invalid connections, but it is necessary? + // Can we just use connections_to_clients() for direct access? + auto GetConnectionsToClients() -> std::vector; + + // Return the number of connections-to-client with "connected" status true. + auto GetConnectedClientCount() const -> int; + + // For applying player-profiles data from the master-server. + auto SetClientInfoFromMasterServer(const std::string& client_token, + PyObject* info) -> void; + + auto SendChatMessage(const std::string& message, + const std::vector* clients = nullptr, + const std::string* sender_override = nullptr) -> void; + + // Send a screen message to all connected clients AND print it on the host. + auto SendScreenMessageToAll(const std::string& s, float r, float g, float b) + -> void; + + // send a screen message to all connected clients + auto SendScreenMessageToClients(const std::string& s, float r, float g, + float b) -> void; + + // Send a screen message to specific connected clients (those matching the IDs + // specified) the id -1 can be used to specify the host. + auto SendScreenMessageToSpecificClients(const std::string& s, float r, + float g, float b, + const std::vector& clients) + -> void; + +#if BA_GOOGLE_BUILD + auto PushClientDisconnectedGooglePlayCall(int id) -> void; + int GetGooglePlayClientCount() const; + auto PushHostConnectedGooglePlayCall() -> void; + auto PushClientConnectedGooglePlayCall(int id) -> void; + auto PushCompressedGamePacketFromHostGooglePlayCall( + const std::vector& data) -> void; + auto PushCompressedGamePacketFromClientGooglePlayCall( + int google_client_id, const std::vector& data) -> void; + auto ClientIDFromGooglePlayClientID(int google_id) -> int; + auto GooglePlayClientIDFromClientID(int client_id) -> int; +#endif + + auto UDPConnectionPacket(const std::vector& data, + const SockAddr& addr) -> void; + auto PushClientDisconnectedCall(int id) -> void; + + private: + // Try to minimize the chance a garbage packet will have this id. + int next_connection_to_client_id_{113}; + std::map > connections_to_clients_; + Object::Ref connection_to_host_; + ClientControllerInterface* client_controller_{}; + + // Simple flag for thread-safe access. + bool has_connection_to_host_{}; + bool print_udp_connect_progress_{true}; + + // Prevents us from printing multiple 'you got disconnected' messages. + bool printed_host_disconnect_{}; + +#if BA_GOOGLE_BUILD + std::map google_play_id_to_client_id_map_; + std::map client_id_to_google_play_id_map_; +#endif +}; + +} // namespace ballistica + +#endif // BALLISTICA_GAME_CONNECTION_CONNECTION_SET_H_ diff --git a/src/ballistica/game/connection/connection_to_host_udp.h b/src/ballistica/game/connection/connection_to_host_udp.h index a56de77f..d318a8e0 100644 --- a/src/ballistica/game/connection/connection_to_host_udp.h +++ b/src/ballistica/game/connection/connection_to_host_udp.h @@ -34,7 +34,7 @@ class ConnectionToHostUDP : public ConnectionToHost { bool did_die_{}; void Die(); void SendDisconnectRequest(); - millisecs_t last_client_i_d_request_time_{}; + millisecs_t last_client_id_request_time_{}; millisecs_t last_disconnect_request_time_{}; int client_id_{}; millisecs_t last_host_response_time_{}; diff --git a/src/ballistica/game/game.cc b/src/ballistica/game/game.cc new file mode 100644 index 00000000..c6bd0b97 --- /dev/null +++ b/src/ballistica/game/game.cc @@ -0,0 +1,2212 @@ +// Released under the MIT License. See LICENSE for details. + +#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_set.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()), + connections_(std::make_unique()) { + 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(); +} + +#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::Prune() { g_media->Prune(); } + +void Game::PushAwardAdTicketsCall() { + PushCall([] { AppInternalAwardAdTickets(); }); +} + +void Game::PushAwardAdTournamentEntryCall() { + PushCall([] { AppInternalAwardAdTournamentEntry(); }); +} + +// 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::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 : connections()->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) { + connections()->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 : + connections()->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. + connections()->SendScreenMessageToClients( + R"({"r":"kickOccurredText","s":[["${NAME}",)" + + Utils::GetJSONString(kick_vote_target->GetCombinedSpec() + .GetDisplayString() + .c_str()) + + "]]}", + 1, 1, 0); + kick_vote_in_progress_ = false; + connections_->DisconnectClient(kick_vote_target->id(), kKickBanSeconds); + + } else if (votes_needed != last_kick_votes_needed_) { + last_kick_votes_needed_ = votes_needed; + connections()->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 : connections()->GetConnectionsToClients()) { + c->SendReliableMessage(msg); + } + game_roster_dirty_ = false; + last_game_roster_send_time_ = real_time; + } + } + + connections_->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()); + + connections_->PrepareForLaunchHostSession(); + + // 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. + AppInternalOnGameThreadPause(); +} + +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 (g_buildconfig.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(); + } + } +} + +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()); + AppInternalDispatchRemoteAchievementList(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). + // FIXME: this should exist either on the client or the server; not both. + // (and should be communicated via frameldefs/etc.) + bool tv_border = g_app_config->Resolve(AppConfig::BoolID::kTVBorder); + g_graphics_server->PushCall( + [tv_border] { g_graphics_server->set_tv_border(tv_border); }); + 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_); + } + + 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::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(); + + connections_->Shutdown(); + + // 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. + AppInternalPutLog(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; +} + +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) + connections_->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); +} + +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); +} + +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)); + } + } + } +} + +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. + connections()->SendScreenMessageToClients( + R"({"r":"kickOccurredText","s":[["${NAME}",)" + + Utils::GetJSONString( + target->GetCombinedSpec().GetDisplayString().c_str()) + + "]]}", + 1, 1, 0); + connections()->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 (connections()->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 = + connections()->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 : + connections()->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 = (connections()->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()->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 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::SetPublicPartyEnabled(bool val) { + assert(InGameThread()); + if (val == public_party_enabled_) { + return; + } + public_party_enabled_ = val; + AppInternalPushPublicPartyState(); +} + +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_) { + AppInternalPushPublicPartyState(); + } +} + +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_) { + AppInternalPushPublicPartyState(); + } +} + +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_) { + AppInternalPushPublicPartyState(); + } +} + +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_) { + AppInternalPushPublicPartyState(); + } +} + +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_) { + AppInternalPushPublicPartyState(); + } +} + +} // namespace ballistica diff --git a/src/ballistica/game/game.h b/src/ballistica/game/game.h index 7dabd7ad..0409879c 100644 --- a/src/ballistica/game/game.h +++ b/src/ballistica/game/game.h @@ -38,8 +38,6 @@ class Game : public Module { const std::string& token) -> void; auto PushAwardAdTicketsCall() -> void; auto PushAwardAdTournamentEntryCall() -> void; - auto PushUDPConnectionPacketCall(const std::vector& data, - const SockAddr& addr) -> void; auto PushPartyInviteCall(const std::string& name, const std::string& invite_id) -> void; auto PushPartyInviteRevokeCall(const std::string& invite_id) -> void; @@ -106,27 +104,11 @@ class Game : public Module { auto PushBackButtonCall(InputDevice* input_device) -> void; auto PushOnAppResumeCall() -> void; auto PushFrameDefRequest() -> void; - auto PushDisconnectFromHostCall() -> void; - auto PushClientDisconnectedCall(int id) -> void; - auto PushHostConnectedUDPCall(const SockAddr& addr, - bool print_connect_progress) -> void; - auto PushDisconnectedFromHostCall() -> void; auto ChangeGameSpeed(int offs) -> void; auto ResetInput() -> void; auto RunMainMenu() -> void; auto HandleThreadPause() -> void override; -#if BA_GOOGLE_BUILD - auto PushClientDisconnectedGooglePlayCall(int id) -> void; - int GetGooglePlayClientCount() const; - auto PushHostConnectedGooglePlayCall() -> void; - auto PushClientConnectedGooglePlayCall(int id) -> void; - auto PushCompressedGamePacketFromHostGooglePlayCall( - const std::vector& data) -> void; - auto PushCompressedGamePacketFromClientGooglePlayCall( - int google_client_id, const std::vector& data) -> void; -#endif - #if BA_VR_BUILD auto PushVRHandsState(const VRHandsState& state) -> void; const VRHandsState& vr_hands_state() const { return vr_hands_state_; } @@ -179,35 +161,6 @@ class Game : public Module { auto replay_speed_exponent() const -> int { return replay_speed_exponent_; } auto replay_speed_mult() const -> float { return replay_speed_mult_; } - // Returns our host-connection or nullptr if there is none. - auto connection_to_host() -> ConnectionToHost* { - return connection_to_host_.get(); - } - auto GetConnectionToHostUDP() -> ConnectionToHostUDP*; - - // Send a screen message to all connected clients AND print it on the host. - auto SendScreenMessageToAll(const std::string& s, float r, float g, float b) - -> void; - - // send a screen message to all connected clients - auto SendScreenMessageToClients(const std::string& s, float r, float g, - float b) -> void; - - // Send a screen message to specific connected clients (those matching the IDs - // specified) the id -1 can be used to specify the host. - auto SendScreenMessageToSpecificClients(const std::string& s, float r, - float g, float b, - const std::vector& clients) - -> void; - - // Return our client connections (if any). - // FIXME: this prunes invalid connections, but it is necessary? - // Can we just use connections_to_clients() for direct access? - auto GetConnectionsToClients() -> std::vector; - - // Return the number of connections-to-client with "connected" status true. - auto GetConnectedClientCount() const -> int; - auto GetPartySize() const -> int; auto last_connection_to_client_join_time() const -> millisecs_t { return last_connection_to_client_join_time_; @@ -216,34 +169,12 @@ class Game : public Module { last_connection_to_client_join_time_ = val; } - // Simple thread safe query. - auto has_connection_to_host() const -> bool { - return has_connection_to_host_; - } - auto game_roster() const -> cJSON* { return game_roster_; } - auto SendChatMessage(const std::string& message, - const std::vector* clients = nullptr, - const std::string* sender_override = nullptr) -> void; - - // Quick test as to whether there are clients. Does not check if they are - // fully connected. - auto has_connection_to_clients() const -> bool { - assert(InGameThread()); - return (!connections_to_clients_.empty()); - } - auto chat_messages() const -> const std::list& { return chat_messages_; } - // Whoever wants to wrangle current client connections should call this - // to register itself. Note that it must explicitly call unregister when - // unregistering itself. - auto RegisterClientController(ClientControllerInterface* c) -> void; - auto UnregisterClientController(ClientControllerInterface* c) -> void; - // Used to know which globals is in control currently/etc. auto GetForegroundScene() const -> Scene* { assert(InGameThread()); @@ -251,19 +182,10 @@ class Game : public Module { } auto SetForegroundScene(Scene* sg) -> void; - // Returns true if disconnect attempts are supported. - auto DisconnectClient(int client_id, int ban_seconds) -> bool; auto UpdateGameRoster() -> void; auto IsPlayerBanned(const PlayerSpec& spec) -> bool; auto BanPlayer(const PlayerSpec& spec, millisecs_t duration) -> void; - // For applying player-profiles data from the master-server. - auto SetClientInfoFromMasterServer(const std::string& client_token, - PyObject* info) -> void; - auto GetPrintUDPConnectProgress() const -> bool { - return print_udp_connect_progress_; - } - // For cheat detection. Returns the largest amount of time that has passed // between frames since our last reset (for detecting memory modification // UIs/etc). @@ -298,20 +220,8 @@ class Game : public Module { return admin_public_ids_; } - auto connections_to_clients() - -> const std::map >& { - return connections_to_clients_; - } - auto client_controller() -> ClientControllerInterface* { - return client_controller_; - } auto kick_vote_in_progress() const -> bool { return kick_vote_in_progress_; } -#if BA_GOOGLE_BUILD - auto ClientIDFromGooglePlayClientID(int google_id) -> int; - auto GooglePlayClientIDFromClientID(int client_id) -> int; -#endif - auto SetPublicPartyEnabled(bool val) -> void; auto public_party_enabled() const { return public_party_enabled_; } auto public_party_size() const { return public_party_size_; } @@ -333,12 +243,16 @@ class Game : public Module { auto public_party_player_count() const { return public_party_player_count_; } auto SetPublicPartyPlayerCount(int count) -> void; auto ran_app_launch_commands() const { return ran_app_launch_commands_; } + auto CleanUpBeforeConnectingToHost() -> void; + auto connections() -> ConnectionSet* { + assert(connections_.get()); + return connections_.get(); + } + auto mark_game_roster_dirty() -> void { game_roster_dirty_ = true; } private: auto InitSpecialChars() -> void; auto Draw() -> void; - auto UDPConnectionPacket(const std::vector& data, - const SockAddr& addr) -> void; auto PartyInvite(const std::string& name, const std::string& invite_id) -> void; auto PartyInviteRevoke(const std::string& invite_id) -> void; @@ -358,8 +272,6 @@ class Game : public Module { int rift_step_index_{}; #endif - auto HandleClientDisconnected(int id) -> void; - auto ForceDisconnectClients() -> void; auto Prune() -> void; // Periodic pruning of dead stuff. auto Update() -> void; auto Process() -> void; @@ -370,21 +282,11 @@ class Game : public Module { auto UpdateProcessTimer() -> void; auto Reset() -> void; auto GetGameRosterMessage() -> std::vector; - auto CleanUpBeforeConnectingToHost() -> void; auto Shutdown(bool soft) -> void; - std::map google_play_id_to_client_id_map_; - std::map client_id_to_google_play_id_map_; - bool print_udp_connect_progress_{true}; + std::unique_ptr connections_; std::list > banned_players_; - ClientControllerInterface* client_controller_{}; std::list chat_messages_; - - // Simple flag for thread-safe access. - bool has_connection_to_host_{}; - - // Prevents us from printing multiple 'you got disconnected' messages. - bool printed_host_disconnect_{}; bool chat_muted_{}; bool first_update_{true}; bool game_roster_dirty_{}; @@ -423,10 +325,6 @@ class Game : public Module { bool kick_voting_enabled_{true}; std::set admin_public_ids_; - // Try to minimize the chance a garbage packet will have this id. - int next_connection_to_client_id_{113}; - std::map > connections_to_clients_; - Object::Ref connection_to_host_; cJSON* game_roster_{}; millisecs_t kick_vote_end_time_{}; bool kick_vote_in_progress_{}; diff --git a/src/ballistica/game/game_stream.h b/src/ballistica/game/game_stream.h index 794e522e..9da9e19d 100644 --- a/src/ballistica/game/game_stream.h +++ b/src/ballistica/game/game_stream.h @@ -17,70 +17,75 @@ class GameStream : public Object, public ClientControllerInterface { public: GameStream(HostSession* host_session, bool saveReplay); ~GameStream() override; - void SetTime(millisecs_t t); - void AddScene(Scene* s); - void RemoveScene(Scene* s); - void StepScene(Scene* s); - void AddNode(Node* n); - void NodeOnCreate(Node* n); - void RemoveNode(Node* n); - void SetForegroundScene(Scene* sg); - void AddMaterial(Material* m); - void RemoveMaterial(Material* m); - void AddMaterialComponent(Material* m, MaterialComponent* c); - void AddTexture(Texture* t); - void RemoveTexture(Texture* t); - void AddModel(Model* t); - void RemoveModel(Model* t); - void AddSound(Sound* t); - void RemoveSound(Sound* t); - void AddData(Data* d); - void RemoveData(Data* d); - void AddCollideModel(CollideModel* t); - void RemoveCollideModel(CollideModel* t); - void ConnectNodeAttribute(Node* src_node, NodeAttributeUnbound* src_attr, - Node* dst_node, NodeAttributeUnbound* dst_attr); - void NodeMessage(Node* node, const char* buffer, size_t size); - void SetNodeAttr(const NodeAttribute& attr, float val); - void SetNodeAttr(const NodeAttribute& attr, int64_t val); - void SetNodeAttr(const NodeAttribute& attr, bool val); - void SetNodeAttr(const NodeAttribute& attr, const std::vector& vals); - void SetNodeAttr(const NodeAttribute& attr, const std::vector& vals); - void SetNodeAttr(const NodeAttribute& attr, const std::string& val); - void SetNodeAttr(const NodeAttribute& attr, Node* n); - void SetNodeAttr(const NodeAttribute& attr, const std::vector& vals); - void SetNodeAttr(const NodeAttribute& attr, Player* n); - void SetNodeAttr(const NodeAttribute& attr, - const std::vector& vals); - void SetNodeAttr(const NodeAttribute& attr, Texture* n); - void SetNodeAttr(const NodeAttribute& attr, - const std::vector& vals); - void SetNodeAttr(const NodeAttribute& attr, Sound* n); - void SetNodeAttr(const NodeAttribute& attr, const std::vector& vals); - void SetNodeAttr(const NodeAttribute& attr, Model* n); - void SetNodeAttr(const NodeAttribute& attr, const std::vector& vals); - void SetNodeAttr(const NodeAttribute& attr, CollideModel* n); - void SetNodeAttr(const NodeAttribute& attr, - const std::vector& vals); - void PlaySoundAtPosition(Sound* sound, float volume, float x, float y, - float z); - void PlaySound(Sound* sound, float volume); - void EmitBGDynamics(const BGDynamicsEmission& e); + auto SetTime(millisecs_t t) -> void; + auto AddScene(Scene* s) -> void; + auto RemoveScene(Scene* s) -> void; + auto StepScene(Scene* s) -> void; + auto AddNode(Node* n) -> void; + auto NodeOnCreate(Node* n) -> void; + auto RemoveNode(Node* n) -> void; + auto SetForegroundScene(Scene* sg) -> void; + auto AddMaterial(Material* m) -> void; + auto RemoveMaterial(Material* m) -> void; + auto AddMaterialComponent(Material* m, MaterialComponent* c) -> void; + auto AddTexture(Texture* t) -> void; + auto RemoveTexture(Texture* t) -> void; + auto AddModel(Model* t) -> void; + auto RemoveModel(Model* t) -> void; + auto AddSound(Sound* t) -> void; + auto RemoveSound(Sound* t) -> void; + auto AddData(Data* d) -> void; + auto RemoveData(Data* d) -> void; + auto AddCollideModel(CollideModel* t) -> void; + auto RemoveCollideModel(CollideModel* t) -> void; + auto ConnectNodeAttribute(Node* src_node, NodeAttributeUnbound* src_attr, + Node* dst_node, NodeAttributeUnbound* dst_attr) + -> void; + auto NodeMessage(Node* node, const char* buffer, size_t size) -> void; + auto SetNodeAttr(const NodeAttribute& attr, float val) -> void; + auto SetNodeAttr(const NodeAttribute& attr, int64_t val) -> void; + auto SetNodeAttr(const NodeAttribute& attr, bool val) -> void; + auto SetNodeAttr(const NodeAttribute& attr, const std::vector& vals) + -> void; + auto SetNodeAttr(const NodeAttribute& attr, const std::vector& vals) + -> void; + auto SetNodeAttr(const NodeAttribute& attr, const std::string& val) -> void; + auto SetNodeAttr(const NodeAttribute& attr, Node* n) -> void; + auto SetNodeAttr(const NodeAttribute& attr, const std::vector& vals) + -> void; + auto SetNodeAttr(const NodeAttribute& attr, Player* n) -> void; + auto SetNodeAttr(const NodeAttribute& attr, + const std::vector& vals) -> void; + auto SetNodeAttr(const NodeAttribute& attr, Texture* n) -> void; + auto SetNodeAttr(const NodeAttribute& attr, const std::vector& vals) + -> void; + auto SetNodeAttr(const NodeAttribute& attr, Sound* n) -> void; + auto SetNodeAttr(const NodeAttribute& attr, const std::vector& vals) + -> void; + auto SetNodeAttr(const NodeAttribute& attr, Model* n) -> void; + auto SetNodeAttr(const NodeAttribute& attr, const std::vector& vals) + -> void; + auto SetNodeAttr(const NodeAttribute& attr, CollideModel* n) -> void; + auto SetNodeAttr(const NodeAttribute& attr, + const std::vector& vals) -> void; + auto PlaySoundAtPosition(Sound* sound, float volume, float x, float y, + float z) -> void; + auto PlaySound(Sound* sound, float volume) -> void; + auto EmitBGDynamics(const BGDynamicsEmission& e) -> void; auto GetSoundID(Sound* s) -> int64_t; auto GetMaterialID(Material* m) -> int64_t; - void ScreenMessageBottom(const std::string& val, float r, float g, float b); - void ScreenMessageTop(const std::string& val, float r, float g, float b, + auto ScreenMessageBottom(const std::string& val, float r, float g, float b) + -> void; + auto 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); - void OnClientConnected(ConnectionToClient* c) override; - void OnClientDisconnected(ConnectionToClient* c) override; + float tint2_g, float tint2_b) -> void; + auto OnClientConnected(ConnectionToClient* c) -> void override; + auto OnClientDisconnected(ConnectionToClient* c) -> void override; auto GetOutMessage() const -> std::vector; private: - HostSession* host_session_; - - // Make sure the scene is in our stream. + // Make sure various components are part of our stream. auto IsValidScene(Scene* val) -> bool; auto IsValidNode(Node* val) -> bool; auto IsValidTexture(Texture* val) -> bool; @@ -89,9 +94,49 @@ class GameStream : public Object, public ClientControllerInterface { auto IsValidData(Data* val) -> bool; auto IsValidCollideModel(CollideModel* val) -> bool; auto IsValidMaterial(Material* val) -> bool; + + auto Flush() -> void; + auto AddMessageToReplay(const std::vector& message) -> void; + auto Fail() -> void; + + auto ShipSessionCommandsMessage() -> void; + auto SendPhysicsCorrection(bool blend) -> void; + auto EndCommand(bool is_time_set = false) -> void; + auto WriteString(const std::string& s) -> void; + auto WriteFloat(float val) -> void; + auto WriteFloats(size_t count, const float* vals) -> void; + auto WriteInts32(size_t count, const int32_t* vals) -> void; + auto WriteInts64(size_t count, const int64_t* vals) -> void; + auto WriteChars(size_t count, const char* vals) -> void; + auto WriteCommand(SessionCommand cmd) -> void; + auto WriteCommandInt32(SessionCommand cmd, int32_t value) -> void; + auto WriteCommandInt64(SessionCommand cmd, int64_t value) -> void; + auto WriteCommandInt32_2(SessionCommand cmd, int32_t value1, int32_t value2) + -> void; + auto WriteCommandInt64_2(SessionCommand cmd, int64_t value1, int64_t value2) + -> void; + auto WriteCommandInt32_3(SessionCommand cmd, int32_t value1, int32_t value2, + int32_t value3) -> void; + auto WriteCommandInt64_3(SessionCommand cmd, int64_t value1, int64_t value2, + int64_t value3) -> void; + auto WriteCommandInt32_4(SessionCommand cmd, int32_t value1, int32_t value2, + int32_t value3, int32_t value4) -> void; + auto WriteCommandInt64_4(SessionCommand cmd, int64_t value1, int64_t value2, + int64_t value3, int64_t value4) -> void; + template + auto GetPointerCount(const std::vector& vec) -> size_t; + template + auto GetFreeIndex(std::vector* vec, std::vector* free_indices) + -> size_t; + template + auto Add(T* val, std::vector* vec, std::vector* free_indices) + -> void; + template + auto Remove(T* val, std::vector* vec, std::vector* free_indices) + -> void; + + HostSession* host_session_; millisecs_t next_flush_time_; - void Flush(); - void AddMessageToReplay(const std::vector& message); // Individual command going into the commands-messages. std::vector out_command_; @@ -101,40 +146,8 @@ class GameStream : public Object, public ClientControllerInterface { std::vector connections_to_clients_; std::vector connections_to_clients_ignored_; bool writing_replay_; - void Fail(); millisecs_t last_physics_correction_time_; millisecs_t last_send_time_; - void ShipSessionCommandsMessage(); - void SendPhysicsCorrection(bool blend); - void EndCommand(bool is_time_set = false); - void WriteString(const std::string& s); - void WriteFloat(float val); - void WriteFloats(size_t count, const float* vals); - void WriteInts32(size_t count, const int32_t* vals); - void WriteInts64(size_t count, const int64_t* vals); - void WriteChars(size_t count, const char* vals); - void WriteCommand(SessionCommand cmd); - void WriteCommandInt32(SessionCommand cmd, int32_t value); - void WriteCommandInt64(SessionCommand cmd, int64_t value); - void WriteCommandInt32_2(SessionCommand cmd, int32_t value1, int32_t value2); - void WriteCommandInt64_2(SessionCommand cmd, int64_t value1, int64_t value2); - void WriteCommandInt32_3(SessionCommand cmd, int32_t value1, int32_t value2, - int32_t value3); - void WriteCommandInt64_3(SessionCommand cmd, int64_t value1, int64_t value2, - int64_t value3); - void WriteCommandInt32_4(SessionCommand cmd, int32_t value1, int32_t value2, - int32_t value3, int32_t value4); - void WriteCommandInt64_4(SessionCommand cmd, int64_t value1, int64_t value2, - int64_t value3, int64_t value4); - template - auto GetPointerCount(const std::vector& vec) -> size_t; - template - auto GetFreeIndex(std::vector* vec, std::vector* free_indices) - -> size_t; - template - void Add(T* val, std::vector* vec, std::vector* free_indices); - template - void Remove(T* val, std::vector* vec, std::vector* free_indices); millisecs_t time_; std::vector scenes_; std::vector free_indices_scene_graphs_; diff --git a/src/ballistica/graphics/graphics.cc b/src/ballistica/graphics/graphics.cc index e74db670..6ca43146 100644 --- a/src/ballistica/graphics/graphics.cc +++ b/src/ballistica/graphics/graphics.cc @@ -9,6 +9,7 @@ #include "ballistica/app/app.h" #include "ballistica/app/app_globals.h" #include "ballistica/dynamics/bg/bg_dynamics.h" +#include "ballistica/game/connection/connection_set.h" #include "ballistica/game/connection/connection_to_client.h" #include "ballistica/game/connection/connection_to_host.h" #include "ballistica/game/session/session.h" @@ -295,7 +296,8 @@ void Graphics::DrawMiscOverlays(RenderPass* pass) { bool show = false; // Add in/out data for any host connection. - if (ConnectionToHost* connection_to_host = g_game->connection_to_host()) { + if (ConnectionToHost* connection_to_host = + g_game->connections()->connection_to_host()) { if (connection_to_host->can_communicate()) show = true; in_size += connection_to_host->GetBytesInPerSecond(); in_size_compressed += connection_to_host->GetBytesInPerSecondCompressed(); @@ -309,7 +311,7 @@ void Graphics::DrawMiscOverlays(RenderPass* pass) { ping = connection_to_host->average_ping(); } else { int connected_count = 0; - for (auto&& i : g_game->connections_to_clients()) { + for (auto&& i : g_game->connections()->connections_to_clients()) { ConnectionToClient* client = i.second.get(); if (client->can_communicate()) { show = true; diff --git a/src/ballistica/input/device/joystick.cc b/src/ballistica/input/device/joystick.cc index 4bf4692b..269a3153 100644 --- a/src/ballistica/input/device/joystick.cc +++ b/src/ballistica/input/device/joystick.cc @@ -8,6 +8,7 @@ #include "ballistica/app/app.h" #include "ballistica/app/app_globals.h" #include "ballistica/audio/audio.h" +#include "ballistica/game/connection/connection_set.h" #include "ballistica/game/player.h" #include "ballistica/graphics/renderer.h" #include "ballistica/python/python.h" @@ -1024,7 +1025,7 @@ void Joystick::HandleSDLEvent(const SDL_Event* e) { // FIXME: Need a call we can make for this. bool do_party_button = false; int party_size = g_game->GetPartySize(); - if (party_size > 1 || g_game->connection_to_host() + if (party_size > 1 || g_game->connections()->connection_to_host() || g_ui->root_ui()->always_draw_party_icon()) { do_party_button = true; } diff --git a/src/ballistica/networking/networking.h b/src/ballistica/networking/networking.h index fa4c8816..0bff150d 100644 --- a/src/ballistica/networking/networking.h +++ b/src/ballistica/networking/networking.h @@ -137,22 +137,6 @@ class Networking { }; auto GetScanResults() -> std::vector; - /// Sends a POST request to the master server and returns the response. - /// path should be something like "/mystatspage". - /// Throws std::exceptions (NOT ballistica Exceptions) if something goes - /// wrong. - static auto MasterServerPost( - const std::string& path, - const std::map& parameters, - bool use_fallback_addr = false) -> std::string; - - /// Sends a GET request to the master server and returns the response. - /// path should be something like "/mystatspage". - /// Throws std::exceptions (NOT ballistica Exceptions) if something goes - /// wrong. - static auto MasterServerGet(const std::string& path, - bool use_fallback_addr = false) -> std::string; - private: void PruneScanResults(); struct ScanResultsEntryPriv; diff --git a/src/ballistica/python/methods/python_methods_app.cc b/src/ballistica/python/methods/python_methods_app.cc index 260430b1..e4385070 100644 --- a/src/ballistica/python/methods/python_methods_app.cc +++ b/src/ballistica/python/methods/python_methods_app.cc @@ -8,6 +8,7 @@ #include "ballistica/app/app.h" #include "ballistica/app/app_globals.h" #include "ballistica/core/logging.h" +#include "ballistica/game/connection/connection_set.h" #include "ballistica/game/game_stream.h" #include "ballistica/game/host_activity.h" #include "ballistica/game/session/host_session.h" @@ -501,10 +502,11 @@ auto PyScreenMessage(PyObject* self, PyObject* args, PyObject* keywds) std::vector client_ids; if (clients_obj != Py_None) { std::vector client_ids2 = Python::GetPyInts(clients_obj); - g_game->SendScreenMessageToSpecificClients(message, color.x, color.y, - color.z, client_ids2); + g_game->connections()->SendScreenMessageToSpecificClients( + message, color.x, color.y, color.z, client_ids2); } else { - g_game->SendScreenMessageToAll(message, color.x, color.y, color.z); + g_game->connections()->SendScreenMessageToAll(message, color.x, color.y, + color.z); } } else { // Currently specifying client_ids only works for transient messages; we'd diff --git a/src/ballistica/python/methods/python_methods_gameplay.cc b/src/ballistica/python/methods/python_methods_gameplay.cc index bd45ab7a..9286a5d6 100644 --- a/src/ballistica/python/methods/python_methods_gameplay.cc +++ b/src/ballistica/python/methods/python_methods_gameplay.cc @@ -11,6 +11,7 @@ #include "ballistica/dynamics/collision.h" #include "ballistica/dynamics/dynamics.h" #include "ballistica/dynamics/material/material_action.h" +#include "ballistica/game/connection/connection_set.h" #include "ballistica/game/connection/connection_to_client.h" #include "ballistica/game/game_stream.h" #include "ballistica/game/host_activity.h" @@ -467,8 +468,9 @@ auto PyGetGameRoster(PyObject* self, PyObject* args, PyObject* keywds) if (clientid == -1) { account_id = AppInternalGetPublicAccountID(); } else { - auto client2 = g_game->connections_to_clients().find(clientid); - if (client2 != g_game->connections_to_clients().end()) { + auto client2 = + g_game->connections()->connections_to_clients().find(clientid); + if (client2 != g_game->connections()->connections_to_clients().end()) { account_id = client2->second->peer_public_account_id(); } } diff --git a/src/ballistica/python/methods/python_methods_ui.cc b/src/ballistica/python/methods/python_methods_ui.cc index 7668c073..02686359 100644 --- a/src/ballistica/python/methods/python_methods_ui.cc +++ b/src/ballistica/python/methods/python_methods_ui.cc @@ -8,6 +8,7 @@ #include "ballistica/app/app.h" #include "ballistica/app/app_globals.h" #include "ballistica/game/account.h" +#include "ballistica/game/connection/connection_set.h" #include "ballistica/game/game.h" #include "ballistica/input/input.h" #include "ballistica/python/python.h" @@ -2053,7 +2054,7 @@ auto PyChatMessage(PyObject* self, PyObject* args, PyObject* keywds) std::vector clients = Python::GetPyInts(clients_obj); clients_p = &clients; } - g_game->SendChatMessage(message, clients_p, sender_override_p); + g_game->connections()->SendChatMessage(message, clients_p, sender_override_p); Py_RETURN_NONE; BA_PYTHON_CATCH; } @@ -2150,7 +2151,8 @@ auto PyCanShowAd(PyObject* self, PyObject* args, PyObject* keywds) // them or whatnot) also disallow ads if remote apps are connected; at least // on android ads pause our activity which disconnects the remote app.. (could // potentially still allow on other platforms; should verify..) - if (g_game->connection_to_host() || g_game->has_connection_to_clients() + if (g_game->connections()->connection_to_host() + || g_game->connections()->has_connection_to_clients() || g_input->HaveRemoteAppController()) { Py_RETURN_FALSE; } @@ -2262,7 +2264,8 @@ auto PyIsPartyIconVisible(PyObject* self, PyObject* args, PyObject* keywds) Platform::SetLastPyCall("is_party_icon_visible"); BA_PYTHON_TRY; bool party_button_active = - (g_game->GetConnectedClientCount() > 0 || g_game->connection_to_host() + (g_game->connections()->GetConnectedClientCount() > 0 + || g_game->connections()->connection_to_host() || g_ui->root_ui()->always_draw_party_icon()); if (party_button_active) { Py_RETURN_TRUE; diff --git a/src/ballistica/ui/root_ui.cc b/src/ballistica/ui/root_ui.cc index 078f78f0..5fbe3823 100644 --- a/src/ballistica/ui/root_ui.cc +++ b/src/ballistica/ui/root_ui.cc @@ -5,6 +5,7 @@ #include #include +#include "ballistica/game/connection/connection_set.h" #include "ballistica/game/game.h" #include "ballistica/game/session/host_session.h" #include "ballistica/graphics/component/simple_component.h" @@ -46,7 +47,7 @@ RootUI::~RootUI() = default; void RootUI::TogglePartyWindowKeyPress() { assert(InGameThread()); - if (g_game->GetPartySize() > 1 || g_game->connection_to_host() + if (g_game->GetPartySize() > 1 || g_game->connections()->connection_to_host() || always_draw_party_icon()) { ActivatePartyIcon(); } @@ -81,8 +82,9 @@ auto RootUI::HandleMouseButtonDown(float x, float y) -> bool { if (explicit_bool(DO_OLD_MENU_PARTY_BUTTONS)) { bool party_button_active = (!party_window_open_ - && (g_game->GetConnectedClientCount() > 0 - || g_game->connection_to_host() || always_draw_party_icon())); + && (g_game->connections()->GetConnectedClientCount() > 0 + || g_game->connections()->connection_to_host() + || always_draw_party_icon())); float party_button_left = menu_active ? 2 * menu_button_size_ : menu_button_size_; float party_button_right = menu_active ? menu_button_size_ : 0; @@ -202,7 +204,7 @@ void RootUI::Draw(FrameDef* frame_def) { // (this probably shouldn't live here). bool draw_connected_players_icon = false; int party_size = g_game->GetPartySize(); - bool is_host = (g_game->connection_to_host() == nullptr); + bool is_host = (g_game->connections()->connection_to_host() == nullptr); millisecs_t last_connection_to_client_join_time = g_game->last_connection_to_client_join_time(); @@ -211,7 +213,7 @@ void RootUI::Draw(FrameDef* frame_def) { && real_time - last_connection_to_client_join_time < 5000); if (!party_window_open_ - && (party_size != 0 || g_game->connection_to_host() + && (party_size != 0 || g_game->connections()->connection_to_host() || always_draw_party_icon_)) { draw_connected_players_icon = true; } diff --git a/tools/efrotools/pcommand.py b/tools/efrotools/pcommand.py index f9fa2ea6..0e2f1241 100644 --- a/tools/efrotools/pcommand.py +++ b/tools/efrotools/pcommand.py @@ -116,23 +116,30 @@ def _trim_docstring(docstring: str) -> str: def _spelling(words: List[str]) -> None: - fname = '.idea/dictionaries/ericf.xml' - with open(fname) as infile: - lines = infile.read().splitlines() - if lines[2] != ' ': - raise RuntimeError('Unexpected dictionary format.') - added_count = 0 - for word in words: - line = f' {word.lower()}' - if line not in lines: - lines.insert(3, line) - added_count += 1 + import os + for fname in [ + '.idea/dictionaries/ericf.xml', + 'ballisticacore-cmake/.idea/dictionaries/ericf.xml' + ]: + if not os.path.exists(fname): + continue + with open(fname) as infile: + lines = infile.read().splitlines() + if lines[2] != ' ': + raise RuntimeError('Unexpected dictionary format.') + added_count = 0 + for word in words: + line = f' {word.lower()}' + if line not in lines: + lines.insert(3, line) + added_count += 1 - with open(fname, 'w') as outfile: - # Sort lines in the words section. - assert all(l.startswith(' ') for l in lines[3:-3]) - outfile.write('\n'.join(lines[:3] + sorted(lines[3:-3]) + lines[-3:])) - print('Added', added_count, 'words to the dictionary.') + with open(fname, 'w') as outfile: + # Sort lines in the words section. + assert all(l.startswith(' ') for l in lines[3:-3]) + outfile.write('\n'.join(lines[:3] + sorted(lines[3:-3]) + + lines[-3:])) + print(f'Added {added_count} words to {fname}.') def spelling_all() -> None: