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 can 9also 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 @overload 177 def send_message_cb( 178 self, 179 msg: bacommon.cloud.SecureDataCheckMessage, 180 on_response: Callable[ 181 [bacommon.cloud.SecureDataCheckResponse | Exception], None 182 ], 183 ) -> None: ... 184 185 @overload 186 def send_message_cb( 187 self, 188 msg: bacommon.cloud.SecureDataCheckerRequest, 189 on_response: Callable[ 190 [bacommon.cloud.SecureDataCheckerResponse | Exception], None 191 ], 192 ) -> None: ... 193 194 def send_message_cb( 195 self, 196 msg: Message, 197 on_response: Callable[[Any], None], 198 ) -> None: 199 """Asynchronously send a message to the cloud from the logic thread. 200 201 The provided on_response call will be run in the logic thread 202 and passed either the response or the error that occurred. 203 """ 204 raise NotImplementedError( 205 'Cloud functionality is not present in this build.' 206 ) 207 208 @overload 209 def send_message( 210 self, msg: bacommon.cloud.WorkspaceFetchMessage 211 ) -> bacommon.cloud.WorkspaceFetchResponse: ... 212 213 @overload 214 def send_message( 215 self, msg: bacommon.cloud.MerchAvailabilityMessage 216 ) -> bacommon.cloud.MerchAvailabilityResponse: ... 217 218 @overload 219 def send_message( 220 self, msg: bacommon.cloud.TestMessage 221 ) -> bacommon.cloud.TestResponse: ... 222 223 def send_message(self, msg: Message) -> Response | None: 224 """Synchronously send a message to the cloud. 225 226 Must be called from a background thread. 227 """ 228 raise NotImplementedError( 229 'Cloud functionality is not present in this build.' 230 ) 231 232 @overload 233 async def send_message_async( 234 self, msg: bacommon.cloud.SendInfoMessage 235 ) -> bacommon.cloud.SendInfoResponse: ... 236 237 @overload 238 async def send_message_async( 239 self, msg: bacommon.cloud.TestMessage 240 ) -> bacommon.cloud.TestResponse: ... 241 242 async def send_message_async(self, msg: Message) -> Response | None: 243 """Synchronously send a message to the cloud. 244 245 Must be called from the logic thread. 246 """ 247 raise NotImplementedError( 248 'Cloud functionality is not present in this build.' 249 ) 250 251 def subscribe_test( 252 self, updatecall: Callable[[int | None], None] 253 ) -> babase.CloudSubscription: 254 """Subscribe to some test data.""" 255 raise NotImplementedError( 256 'Cloud functionality is not present in this build.' 257 ) 258 259 def subscribe_classic_account_data( 260 self, 261 updatecall: Callable[[bacommon.bs.ClassicAccountLiveData], None], 262 ) -> babase.CloudSubscription: 263 """Subscribe to classic account data.""" 264 raise NotImplementedError( 265 'Cloud functionality is not present in this build.' 266 ) 267 268 def unsubscribe(self, subscription_id: int) -> None: 269 """Unsubscribe from some subscription. 270 271 Do not call this manually; it is called by CloudSubscription. 272 """ 273 raise NotImplementedError( 274 'Cloud functionality is not present in this build.' 275 )
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.
194 def send_message_cb( 195 self, 196 msg: Message, 197 on_response: Callable[[Any], None], 198 ) -> None: 199 """Asynchronously send a message to the cloud from the logic thread. 200 201 The provided on_response call will be run in the logic thread 202 and passed either the response or the error that occurred. 203 """ 204 raise NotImplementedError( 205 'Cloud functionality is not present in this build.' 206 )
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.
223 def send_message(self, msg: Message) -> Response | None: 224 """Synchronously send a message to the cloud. 225 226 Must be called from a background thread. 227 """ 228 raise NotImplementedError( 229 'Cloud functionality is not present in this build.' 230 )
Synchronously send a message to the cloud.
Must be called from a background thread.
242 async def send_message_async(self, msg: Message) -> Response | None: 243 """Synchronously send a message to the cloud. 244 245 Must be called from the logic thread. 246 """ 247 raise NotImplementedError( 248 'Cloud functionality is not present in this build.' 249 )
Synchronously send a message to the cloud.
Must be called from the logic thread.
251 def subscribe_test( 252 self, updatecall: Callable[[int | None], None] 253 ) -> babase.CloudSubscription: 254 """Subscribe to some test data.""" 255 raise NotImplementedError( 256 'Cloud functionality is not present in this build.' 257 )
Subscribe to some test data.
259 def subscribe_classic_account_data( 260 self, 261 updatecall: Callable[[bacommon.bs.ClassicAccountLiveData], None], 262 ) -> babase.CloudSubscription: 263 """Subscribe to classic account data.""" 264 raise NotImplementedError( 265 'Cloud functionality is not present in this build.' 266 )
Subscribe to classic account data.
268 def unsubscribe(self, subscription_id: int) -> None: 269 """Unsubscribe from some subscription. 270 271 Do not call this manually; it is called by CloudSubscription. 272 """ 273 raise NotImplementedError( 274 'Cloud functionality is not present in this build.' 275 )
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 return _baplus.mark_config_dirty() 166 167 @staticmethod 168 def power_ranking_query(callback: Callable, season: Any = None) -> None: 169 """(internal)""" 170 return _baplus.power_ranking_query(callback, season) 171 172 @staticmethod 173 def purchase(item: str) -> None: 174 """(internal)""" 175 return _baplus.purchase(item) 176 177 @staticmethod 178 def report_achievement( 179 achievement: str, pass_to_account: bool = True 180 ) -> None: 181 """(internal)""" 182 return _baplus.report_achievement(achievement, pass_to_account) 183 184 @staticmethod 185 def reset_achievements() -> None: 186 """(internal)""" 187 return _baplus.reset_achievements() 188 189 @staticmethod 190 def restore_purchases() -> None: 191 """(internal)""" 192 return _baplus.restore_purchases() 193 194 @staticmethod 195 def run_v1_account_transactions() -> None: 196 """(internal)""" 197 return _baplus.run_v1_account_transactions() 198 199 @staticmethod 200 def sign_in_v1(account_type: str) -> None: 201 """(internal)""" 202 return _baplus.sign_in_v1(account_type) 203 204 @staticmethod 205 def sign_out_v1(v2_embedded: bool = False) -> None: 206 """(internal)""" 207 return _baplus.sign_out_v1(v2_embedded) 208 209 @staticmethod 210 def submit_score( 211 game: str, 212 config: str, 213 name: Any, 214 score: int | None, 215 callback: Callable, 216 *, 217 order: str = 'increasing', 218 tournament_id: str | None = None, 219 score_type: str = 'points', 220 campaign: str | None = None, 221 level: str | None = None, 222 ) -> None: 223 """(internal) 224 225 Submit a score to the server; callback will be called with the results. 226 As a courtesy, please don't send fake scores to the server. I'd prefer 227 to devote my time to improving the game instead of trying to make the 228 score server more mischief-proof. 229 """ 230 return _baplus.submit_score( 231 game, 232 config, 233 name, 234 score, 235 callback, 236 order, 237 tournament_id, 238 score_type, 239 campaign, 240 level, 241 ) 242 243 @staticmethod 244 def tournament_query( 245 callback: Callable[[dict | None], None], args: dict 246 ) -> None: 247 """(internal)""" 248 return _baplus.tournament_query(callback, args) 249 250 @staticmethod 251 def supports_purchases() -> bool: 252 """Does this platform support in-app-purchases?""" 253 return _baplus.supports_purchases() 254 255 @staticmethod 256 def have_incentivized_ad() -> bool: 257 """Is an incentivized ad available?""" 258 return _baplus.have_incentivized_ad() 259 260 @staticmethod 261 def has_video_ads() -> bool: 262 """Are video ads available?""" 263 return _baplus.has_video_ads() 264 265 @staticmethod 266 def can_show_ad() -> bool: 267 """Can we show an ad?""" 268 return _baplus.can_show_ad() 269 270 @staticmethod 271 def show_ad( 272 purpose: str, on_completion_call: Callable[[], None] | None = None 273 ) -> None: 274 """Show an ad.""" 275 _baplus.show_ad(purpose, on_completion_call) 276 277 @staticmethod 278 def show_ad_2( 279 purpose: str, on_completion_call: Callable[[bool], None] | None = None 280 ) -> None: 281 """Show an ad.""" 282 _baplus.show_ad_2(purpose, on_completion_call) 283 284 @staticmethod 285 def show_game_service_ui( 286 show: str = 'general', 287 game: str | None = None, 288 game_version: str | None = None, 289 ) -> None: 290 """Show game-service provided UI.""" 291 _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.
250 @staticmethod 251 def supports_purchases() -> bool: 252 """Does this platform support in-app-purchases?""" 253 return _baplus.supports_purchases()
Does this platform support in-app-purchases?
255 @staticmethod 256 def have_incentivized_ad() -> bool: 257 """Is an incentivized ad available?""" 258 return _baplus.have_incentivized_ad()
Is an incentivized ad available?
260 @staticmethod 261 def has_video_ads() -> bool: 262 """Are video ads available?""" 263 return _baplus.has_video_ads()
Are video ads available?
265 @staticmethod 266 def can_show_ad() -> bool: 267 """Can we show an ad?""" 268 return _baplus.can_show_ad()
Can we show an ad?
270 @staticmethod 271 def show_ad( 272 purpose: str, on_completion_call: Callable[[], None] | None = None 273 ) -> None: 274 """Show an ad.""" 275 _baplus.show_ad(purpose, on_completion_call)
Show an ad.
277 @staticmethod 278 def show_ad_2( 279 purpose: str, on_completion_call: Callable[[bool], None] | None = None 280 ) -> None: 281 """Show an ad.""" 282 _baplus.show_ad_2(purpose, on_completion_call)
Show an ad.
284 @staticmethod 285 def show_game_service_ui( 286 show: str = 'general', 287 game: str | None = None, 288 game_version: str | None = None, 289 ) -> None: 290 """Show game-service provided UI.""" 291 _baplus.show_game_service_ui(show, game, game_version)
Show game-service provided UI.