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            )
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    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.

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:
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.

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

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

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

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

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

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
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.

@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:
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?

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

@staticmethod
def has_video_ads() -> bool:
269    @staticmethod
270    def has_video_ads() -> bool:
271        """Are video ads available?"""
272        return _baplus.has_video_ads()

Are video ads available?

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

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

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

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