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            )
class CloudSubsystem(babase._appsubsystem.AppSubsystem):
 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.

on_connectivity_changed_callbacks: efro.call.CallbackSet[typing.Callable[[bool], NoneType]]
connected: bool
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().

def is_connected(self) -> bool:
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.

def on_connectivity_changed(self, connected: bool) -> None:
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.

def send_message_cb( self, msg: efro.message.Message, on_response: Callable[[Any], NoneType]) -> None:
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.

def send_message( self, msg: efro.message.Message) -> efro.message.Response | None:
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.

async def send_message_async( self, msg: efro.message.Message) -> efro.message.Response | None:
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.

def subscribe_test( self, updatecall: Callable[[int | None], NoneType]) -> babase.CloudSubscription:
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.

def subscribe_classic_account_data( self, updatecall: Callable[[bacommon.bs.ClassicAccountLiveData], NoneType]) -> babase.CloudSubscription:
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.

def unsubscribe(self, subscription_id: int) -> None:
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.

class PlusAppSubsystem(babase._appsubsystem.AppSubsystem):
 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.

@override
def on_app_loading(self) -> None:
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.

@staticmethod
def supports_purchases() -> bool:
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?

@staticmethod
def have_incentivized_ad() -> bool:
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?

@staticmethod
def has_video_ads() -> bool:
260    @staticmethod
261    def has_video_ads() -> bool:
262        """Are video ads available?"""
263        return _baplus.has_video_ads()

Are video ads available?

@staticmethod
def can_show_ad() -> bool:
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?

@staticmethod
def show_ad( purpose: str, on_completion_call: Optional[Callable[[], NoneType]] = None) -> None:
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.

@staticmethod
def show_ad_2( purpose: str, on_completion_call: Optional[Callable[[bool], NoneType]] = None) -> None:
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.

@staticmethod
def show_game_service_ui( show: str = 'general', game: str | None = None, game_version: str | None = None) -> None:
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.