baplus
Closed-source bits of ballistica.
This code concerns sensitive things like accounts and master-server communication so the native C++ parts of it remain closed. Native precompiled static libraries of this portion are provided for those who want to compile the rest of the engine, or a fully open-source app can also be built by removing this feature-set.
1# Released under the MIT License. See LICENSE for details. 2# 3"""Closed-source bits of ballistica. 4 5This code concerns sensitive things like accounts and master-server 6communication so the native C++ parts of it remain closed. Native 7precompiled static libraries of this portion are provided for those who 8want to compile the rest of the engine, or a fully open-source app 9can also be built by removing this feature-set. 10""" 11 12from __future__ import annotations 13 14# Note: there's not much here. Most interaction with this feature-set 15# should go through ba*.app.plus. 16 17import logging 18 19from baplus._cloud import CloudSubsystem 20from baplus._appsubsystem import PlusAppSubsystem 21 22__all__ = [ 23 'CloudSubsystem', 24 'PlusAppSubsystem', 25] 26 27# Sanity check: we want to keep ballistica's dependencies and 28# bootstrapping order clearly defined; let's check a few particular 29# modules to make sure they never directly or indirectly import us 30# before their own execs complete. 31if __debug__: 32 for _mdl in 'babase', '_babase': 33 if not hasattr(__import__(_mdl), '_REACHED_END_OF_MODULE'): 34 logging.warning( 35 '%s was imported before %s finished importing;' 36 ' should not happen.', 37 __name__, 38 _mdl, 39 )
27class CloudSubsystem(babase.AppSubsystem): 28 """Manages communication with cloud components.""" 29 30 def __init__(self) -> None: 31 super().__init__() 32 self.on_connectivity_changed_callbacks: CallbackSet[ 33 Callable[[bool], None] 34 ] = CallbackSet() 35 36 @property 37 def connected(self) -> bool: 38 """Property equivalent of CloudSubsystem.is_connected().""" 39 return self.is_connected() 40 41 def is_connected(self) -> bool: 42 """Return whether a connection to the cloud is present. 43 44 This is a good indicator (though not for certain) that sending 45 messages will succeed. 46 """ 47 return False # Needs to be overridden 48 49 def on_connectivity_changed(self, connected: bool) -> None: 50 """Called when cloud connectivity state changes.""" 51 babase.balog.debug('Connectivity is now %s.', connected) 52 53 plus = babase.app.plus 54 assert plus is not None 55 56 # Fire any registered callbacks for this. 57 for call in self.on_connectivity_changed_callbacks.getcalls(): 58 try: 59 call(connected) 60 except Exception: 61 logging.exception('Error in connectivity-changed callback.') 62 63 @overload 64 def send_message_cb( 65 self, 66 msg: bacommon.cloud.LoginProxyRequestMessage, 67 on_response: Callable[ 68 [bacommon.cloud.LoginProxyRequestResponse | Exception], None 69 ], 70 ) -> None: ... 71 72 @overload 73 def send_message_cb( 74 self, 75 msg: bacommon.cloud.LoginProxyStateQueryMessage, 76 on_response: Callable[ 77 [bacommon.cloud.LoginProxyStateQueryResponse | Exception], None 78 ], 79 ) -> None: ... 80 81 @overload 82 def send_message_cb( 83 self, 84 msg: bacommon.cloud.LoginProxyCompleteMessage, 85 on_response: Callable[[None | Exception], None], 86 ) -> None: ... 87 88 @overload 89 def send_message_cb( 90 self, 91 msg: bacommon.cloud.PingMessage, 92 on_response: Callable[[bacommon.cloud.PingResponse | Exception], None], 93 ) -> None: ... 94 95 @overload 96 def send_message_cb( 97 self, 98 msg: bacommon.cloud.SignInMessage, 99 on_response: Callable[ 100 [bacommon.cloud.SignInResponse | Exception], None 101 ], 102 ) -> None: ... 103 104 @overload 105 def send_message_cb( 106 self, 107 msg: bacommon.cloud.ManageAccountMessage, 108 on_response: Callable[ 109 [bacommon.cloud.ManageAccountResponse | Exception], None 110 ], 111 ) -> None: ... 112 113 @overload 114 def send_message_cb( 115 self, 116 msg: bacommon.cloud.StoreQueryMessage, 117 on_response: Callable[ 118 [bacommon.cloud.StoreQueryResponse | Exception], None 119 ], 120 ) -> None: ... 121 122 @overload 123 def send_message_cb( 124 self, 125 msg: bacommon.bs.PrivatePartyMessage, 126 on_response: Callable[ 127 [bacommon.bs.PrivatePartyResponse | Exception], None 128 ], 129 ) -> None: ... 130 131 @overload 132 def send_message_cb( 133 self, 134 msg: bacommon.bs.InboxRequestMessage, 135 on_response: Callable[ 136 [bacommon.bs.InboxRequestResponse | Exception], None 137 ], 138 ) -> None: ... 139 140 @overload 141 def send_message_cb( 142 self, 143 msg: bacommon.bs.ClientUIActionMessage, 144 on_response: Callable[ 145 [bacommon.bs.ClientUIActionResponse | Exception], None 146 ], 147 ) -> None: ... 148 149 @overload 150 def send_message_cb( 151 self, 152 msg: bacommon.bs.ChestInfoMessage, 153 on_response: Callable[ 154 [bacommon.bs.ChestInfoResponse | Exception], None 155 ], 156 ) -> None: ... 157 158 @overload 159 def send_message_cb( 160 self, 161 msg: bacommon.bs.ChestActionMessage, 162 on_response: Callable[ 163 [bacommon.bs.ChestActionResponse | Exception], None 164 ], 165 ) -> None: ... 166 167 @overload 168 def send_message_cb( 169 self, 170 msg: bacommon.bs.ScoreSubmitMessage, 171 on_response: Callable[ 172 [bacommon.bs.ScoreSubmitResponse | Exception], None 173 ], 174 ) -> None: ... 175 176 def send_message_cb( 177 self, 178 msg: Message, 179 on_response: Callable[[Any], None], 180 ) -> None: 181 """Asynchronously send a message to the cloud from the logic thread. 182 183 The provided on_response call will be run in the logic thread 184 and passed either the response or the error that occurred. 185 """ 186 raise NotImplementedError( 187 'Cloud functionality is not present in this build.' 188 ) 189 190 @overload 191 def send_message( 192 self, msg: bacommon.cloud.WorkspaceFetchMessage 193 ) -> bacommon.cloud.WorkspaceFetchResponse: ... 194 195 @overload 196 def send_message( 197 self, msg: bacommon.cloud.MerchAvailabilityMessage 198 ) -> bacommon.cloud.MerchAvailabilityResponse: ... 199 200 @overload 201 def send_message( 202 self, msg: bacommon.cloud.TestMessage 203 ) -> bacommon.cloud.TestResponse: ... 204 205 def send_message(self, msg: Message) -> Response | None: 206 """Synchronously send a message to the cloud. 207 208 Must be called from a background thread. 209 """ 210 raise NotImplementedError( 211 'Cloud functionality is not present in this build.' 212 ) 213 214 @overload 215 async def send_message_async( 216 self, msg: bacommon.cloud.SendInfoMessage 217 ) -> bacommon.cloud.SendInfoResponse: ... 218 219 @overload 220 async def send_message_async( 221 self, msg: bacommon.cloud.TestMessage 222 ) -> bacommon.cloud.TestResponse: ... 223 224 async def send_message_async(self, msg: Message) -> Response | None: 225 """Synchronously send a message to the cloud. 226 227 Must be called from the logic thread. 228 """ 229 raise NotImplementedError( 230 'Cloud functionality is not present in this build.' 231 ) 232 233 def subscribe_test( 234 self, updatecall: Callable[[int | None], None] 235 ) -> babase.CloudSubscription: 236 """Subscribe to some test data.""" 237 raise NotImplementedError( 238 'Cloud functionality is not present in this build.' 239 ) 240 241 def subscribe_classic_account_data( 242 self, 243 updatecall: Callable[[bacommon.bs.ClassicAccountLiveData], None], 244 ) -> babase.CloudSubscription: 245 """Subscribe to classic account data.""" 246 raise NotImplementedError( 247 'Cloud functionality is not present in this build.' 248 ) 249 250 def unsubscribe(self, subscription_id: int) -> None: 251 """Unsubscribe from some subscription. 252 253 Do not call this manually; it is called by CloudSubscription. 254 """ 255 raise NotImplementedError( 256 'Cloud functionality is not present in this build.' 257 )
Manages communication with cloud components.
36 @property 37 def connected(self) -> bool: 38 """Property equivalent of CloudSubsystem.is_connected().""" 39 return self.is_connected()
Property equivalent of CloudSubsystem.is_connected().
41 def is_connected(self) -> bool: 42 """Return whether a connection to the cloud is present. 43 44 This is a good indicator (though not for certain) that sending 45 messages will succeed. 46 """ 47 return False # Needs to be overridden
Return whether a connection to the cloud is present.
This is a good indicator (though not for certain) that sending messages will succeed.
49 def on_connectivity_changed(self, connected: bool) -> None: 50 """Called when cloud connectivity state changes.""" 51 babase.balog.debug('Connectivity is now %s.', connected) 52 53 plus = babase.app.plus 54 assert plus is not None 55 56 # Fire any registered callbacks for this. 57 for call in self.on_connectivity_changed_callbacks.getcalls(): 58 try: 59 call(connected) 60 except Exception: 61 logging.exception('Error in connectivity-changed callback.')
Called when cloud connectivity state changes.
176 def send_message_cb( 177 self, 178 msg: Message, 179 on_response: Callable[[Any], None], 180 ) -> None: 181 """Asynchronously send a message to the cloud from the logic thread. 182 183 The provided on_response call will be run in the logic thread 184 and passed either the response or the error that occurred. 185 """ 186 raise NotImplementedError( 187 'Cloud functionality is not present in this build.' 188 )
Asynchronously send a message to the cloud from the logic thread.
The provided on_response call will be run in the logic thread and passed either the response or the error that occurred.
205 def send_message(self, msg: Message) -> Response | None: 206 """Synchronously send a message to the cloud. 207 208 Must be called from a background thread. 209 """ 210 raise NotImplementedError( 211 'Cloud functionality is not present in this build.' 212 )
Synchronously send a message to the cloud.
Must be called from a background thread.
224 async def send_message_async(self, msg: Message) -> Response | None: 225 """Synchronously send a message to the cloud. 226 227 Must be called from the logic thread. 228 """ 229 raise NotImplementedError( 230 'Cloud functionality is not present in this build.' 231 )
Synchronously send a message to the cloud.
Must be called from the logic thread.
233 def subscribe_test( 234 self, updatecall: Callable[[int | None], None] 235 ) -> babase.CloudSubscription: 236 """Subscribe to some test data.""" 237 raise NotImplementedError( 238 'Cloud functionality is not present in this build.' 239 )
Subscribe to some test data.
241 def subscribe_classic_account_data( 242 self, 243 updatecall: Callable[[bacommon.bs.ClassicAccountLiveData], None], 244 ) -> babase.CloudSubscription: 245 """Subscribe to classic account data.""" 246 raise NotImplementedError( 247 'Cloud functionality is not present in this build.' 248 )
Subscribe to classic account data.
250 def unsubscribe(self, subscription_id: int) -> None: 251 """Unsubscribe from some subscription. 252 253 Do not call this manually; it is called by CloudSubscription. 254 """ 255 raise NotImplementedError( 256 'Cloud functionality is not present in this build.' 257 )
Unsubscribe from some subscription.
Do not call this manually; it is called by CloudSubscription.
22class PlusAppSubsystem(AppSubsystem): 23 """Subsystem for plus functionality in the app. 24 25 The single shared instance of this app can be accessed at 26 babase.app.plus. Note that it is possible for this to be None if the 27 plus package is not present, and code should handle that case 28 gracefully. 29 """ 30 31 # pylint: disable=too-many-public-methods 32 33 # Note: this is basically just a wrapper around _baplus for 34 # type-checking purposes. Maybe there's some smart way we could skip 35 # the overhead of this wrapper at runtime. 36 37 accounts: AccountV2Subsystem 38 cloud: CloudSubsystem 39 40 @override 41 def on_app_loading(self) -> None: 42 _baplus.on_app_loading() 43 self.accounts.on_app_loading() 44 45 @staticmethod 46 def add_v1_account_transaction( 47 transaction: dict, callback: Callable | None = None 48 ) -> None: 49 """(internal)""" 50 return _baplus.add_v1_account_transaction(transaction, callback) 51 52 @staticmethod 53 def game_service_has_leaderboard(game: str, config: str) -> bool: 54 """(internal) 55 56 Given a game and config string, returns whether there is a leaderboard 57 for it on the game service. 58 """ 59 return _baplus.game_service_has_leaderboard(game, config) 60 61 @staticmethod 62 def get_master_server_address(source: int = -1, version: int = 1) -> str: 63 """(internal) 64 65 Return the address of the master server. 66 """ 67 return _baplus.get_master_server_address(source, version) 68 69 @staticmethod 70 def get_classic_news_show() -> str: 71 """(internal)""" 72 return _baplus.get_classic_news_show() 73 74 @staticmethod 75 def get_price(item: str) -> str | None: 76 """(internal)""" 77 return _baplus.get_price(item) 78 79 @staticmethod 80 def get_v1_account_product_purchased(item: str) -> bool: 81 """(internal)""" 82 return _baplus.get_v1_account_product_purchased(item) 83 84 @staticmethod 85 def get_v1_account_product_purchases_state() -> int: 86 """(internal)""" 87 return _baplus.get_v1_account_product_purchases_state() 88 89 @staticmethod 90 def get_v1_account_display_string(full: bool = True) -> str: 91 """(internal)""" 92 return _baplus.get_v1_account_display_string(full) 93 94 @staticmethod 95 def get_v1_account_misc_read_val(name: str, default_value: Any) -> Any: 96 """(internal)""" 97 return _baplus.get_v1_account_misc_read_val(name, default_value) 98 99 @staticmethod 100 def get_v1_account_misc_read_val_2(name: str, default_value: Any) -> Any: 101 """(internal)""" 102 return _baplus.get_v1_account_misc_read_val_2(name, default_value) 103 104 @staticmethod 105 def get_v1_account_misc_val(name: str, default_value: Any) -> Any: 106 """(internal)""" 107 return _baplus.get_v1_account_misc_val(name, default_value) 108 109 @staticmethod 110 def get_v1_account_name() -> str: 111 """(internal)""" 112 return _baplus.get_v1_account_name() 113 114 @staticmethod 115 def get_v1_account_public_login_id() -> str | None: 116 """(internal)""" 117 return _baplus.get_v1_account_public_login_id() 118 119 @staticmethod 120 def get_v1_account_state() -> str: 121 """(internal)""" 122 return _baplus.get_v1_account_state() 123 124 @staticmethod 125 def get_v1_account_state_num() -> int: 126 """(internal)""" 127 return _baplus.get_v1_account_state_num() 128 129 @staticmethod 130 def get_v1_account_ticket_count() -> int: 131 """(internal) 132 133 Return the number of tickets for the current account. 134 """ 135 return _baplus.get_v1_account_ticket_count() 136 137 @staticmethod 138 def get_v1_account_type() -> str: 139 """(internal)""" 140 return _baplus.get_v1_account_type() 141 142 @staticmethod 143 def get_v2_fleet() -> str: 144 """(internal)""" 145 return _baplus.get_v2_fleet() 146 147 @staticmethod 148 def have_outstanding_v1_account_transactions() -> bool: 149 """(internal)""" 150 return _baplus.have_outstanding_v1_account_transactions() 151 152 @staticmethod 153 def in_game_purchase(item: str, price: int) -> None: 154 """(internal)""" 155 return _baplus.in_game_purchase(item, price) 156 157 @staticmethod 158 def is_blessed() -> bool: 159 """(internal)""" 160 return _baplus.is_blessed() 161 162 @staticmethod 163 def mark_config_dirty() -> None: 164 """(internal) 165 166 Category: General Utility Functions 167 """ 168 return _baplus.mark_config_dirty() 169 170 @staticmethod 171 def power_ranking_query(callback: Callable, season: Any = None) -> None: 172 """(internal)""" 173 return _baplus.power_ranking_query(callback, season) 174 175 @staticmethod 176 def purchase(item: str) -> None: 177 """(internal)""" 178 return _baplus.purchase(item) 179 180 @staticmethod 181 def report_achievement( 182 achievement: str, pass_to_account: bool = True 183 ) -> None: 184 """(internal)""" 185 return _baplus.report_achievement(achievement, pass_to_account) 186 187 @staticmethod 188 def reset_achievements() -> None: 189 """(internal)""" 190 return _baplus.reset_achievements() 191 192 @staticmethod 193 def restore_purchases() -> None: 194 """(internal)""" 195 return _baplus.restore_purchases() 196 197 @staticmethod 198 def run_v1_account_transactions() -> None: 199 """(internal)""" 200 return _baplus.run_v1_account_transactions() 201 202 @staticmethod 203 def sign_in_v1(account_type: str) -> None: 204 """(internal) 205 206 Category: General Utility Functions 207 """ 208 return _baplus.sign_in_v1(account_type) 209 210 @staticmethod 211 def sign_out_v1(v2_embedded: bool = False) -> None: 212 """(internal) 213 214 Category: General Utility Functions 215 """ 216 return _baplus.sign_out_v1(v2_embedded) 217 218 @staticmethod 219 def submit_score( 220 game: str, 221 config: str, 222 name: Any, 223 score: int | None, 224 callback: Callable, 225 *, 226 order: str = 'increasing', 227 tournament_id: str | None = None, 228 score_type: str = 'points', 229 campaign: str | None = None, 230 level: str | None = None, 231 ) -> None: 232 """(internal) 233 234 Submit a score to the server; callback will be called with the results. 235 As a courtesy, please don't send fake scores to the server. I'd prefer 236 to devote my time to improving the game instead of trying to make the 237 score server more mischief-proof. 238 """ 239 return _baplus.submit_score( 240 game, 241 config, 242 name, 243 score, 244 callback, 245 order, 246 tournament_id, 247 score_type, 248 campaign, 249 level, 250 ) 251 252 @staticmethod 253 def tournament_query( 254 callback: Callable[[dict | None], None], args: dict 255 ) -> None: 256 """(internal)""" 257 return _baplus.tournament_query(callback, args) 258 259 @staticmethod 260 def supports_purchases() -> bool: 261 """Does this platform support in-app-purchases?""" 262 return _baplus.supports_purchases() 263 264 @staticmethod 265 def have_incentivized_ad() -> bool: 266 """Is an incentivized ad available?""" 267 return _baplus.have_incentivized_ad() 268 269 @staticmethod 270 def has_video_ads() -> bool: 271 """Are video ads available?""" 272 return _baplus.has_video_ads() 273 274 @staticmethod 275 def can_show_ad() -> bool: 276 """Can we show an ad?""" 277 return _baplus.can_show_ad() 278 279 @staticmethod 280 def show_ad( 281 purpose: str, on_completion_call: Callable[[], None] | None = None 282 ) -> None: 283 """Show an ad.""" 284 _baplus.show_ad(purpose, on_completion_call) 285 286 @staticmethod 287 def show_ad_2( 288 purpose: str, on_completion_call: Callable[[bool], None] | None = None 289 ) -> None: 290 """Show an ad.""" 291 _baplus.show_ad_2(purpose, on_completion_call) 292 293 @staticmethod 294 def show_game_service_ui( 295 show: str = 'general', 296 game: str | None = None, 297 game_version: str | None = None, 298 ) -> None: 299 """Show game-service provided UI.""" 300 _baplus.show_game_service_ui(show, game, game_version)
Subsystem for plus functionality in the app.
The single shared instance of this app can be accessed at babase.app.plus. Note that it is possible for this to be None if the plus package is not present, and code should handle that case gracefully.
40 @override 41 def on_app_loading(self) -> None: 42 _baplus.on_app_loading() 43 self.accounts.on_app_loading()
Called when the app reaches the loading state.
Note that subsystems created after the app switches to the loading state will not receive this callback. Subsystems created by plugins are an example of this.
259 @staticmethod 260 def supports_purchases() -> bool: 261 """Does this platform support in-app-purchases?""" 262 return _baplus.supports_purchases()
Does this platform support in-app-purchases?
264 @staticmethod 265 def have_incentivized_ad() -> bool: 266 """Is an incentivized ad available?""" 267 return _baplus.have_incentivized_ad()
Is an incentivized ad available?
269 @staticmethod 270 def has_video_ads() -> bool: 271 """Are video ads available?""" 272 return _baplus.has_video_ads()
Are video ads available?
274 @staticmethod 275 def can_show_ad() -> bool: 276 """Can we show an ad?""" 277 return _baplus.can_show_ad()
Can we show an ad?
279 @staticmethod 280 def show_ad( 281 purpose: str, on_completion_call: Callable[[], None] | None = None 282 ) -> None: 283 """Show an ad.""" 284 _baplus.show_ad(purpose, on_completion_call)
Show an ad.
286 @staticmethod 287 def show_ad_2( 288 purpose: str, on_completion_call: Callable[[bool], None] | None = None 289 ) -> None: 290 """Show an ad.""" 291 _baplus.show_ad_2(purpose, on_completion_call)
Show an ad.
293 @staticmethod 294 def show_game_service_ui( 295 show: str = 'general', 296 game: str | None = None, 297 game_version: str | None = None, 298 ) -> None: 299 """Show game-service provided UI.""" 300 _baplus.show_game_service_ui(show, game, game_version)
Show game-service provided UI.