updating ba, bastd

This commit is contained in:
Ayush Saini 2022-06-09 01:26:46 +05:30
parent c2ddced5cf
commit 3c43b5661b
169 changed files with 13214 additions and 4232 deletions

View file

@ -6,7 +6,6 @@ This top level module is a collection of most commonly used functionality.
For many modding purposes, the bits exposed here are all you'll need.
In some specific cases you may need to pull in individual submodules instead.
"""
# pylint: disable=unused-import
# pylint: disable=redefined-builtin
from _ba import (
@ -18,7 +17,7 @@ from _ba import (
newnode, playsound, printnodes, printobjects, pushcall, quit, rowwidget,
safecolor, screenmessage, scrollwidget, set_analytics_screen, charstr,
textwidget, time, timer, open_url, widget, clipboard_is_supported,
clipboard_has_text, clipboard_get_text, clipboard_set_text)
clipboard_has_text, clipboard_get_text, clipboard_set_text, getdata)
from ba._activity import Activity
from ba._plugin import PotentialPlugin, Plugin, PluginSubsystem
from ba._actor import Actor
@ -81,13 +80,64 @@ from ba._collision import Collision, getcollision
app: App
__all__ = [
'Achievement', 'AchievementSubsystem', 'Activity', 'ActivityNotFoundError',
'Actor', 'ActorNotFoundError', 'animate', 'animate_array', 'app', 'App',
'AppConfig', 'AppDelegate', 'AssetPackage', 'BoolSetting', 'buttonwidget',
'Call', 'cameraflash', 'camerashake', 'Campaign', 'CelebrateMessage',
'charstr', 'checkboxwidget', 'ChoiceSetting', 'Chooser',
'clipboard_get_text', 'clipboard_has_text', 'clipboard_is_supported',
'clipboard_set_text', 'CollideModel', 'Collision', 'columnwidget',
'containerwidget', 'Context', 'ContextCall', 'ContextError',
'CoopGameActivity', 'CoopSession', 'Data', 'DeathType',
'DelegateNotFoundError', 'Dependency', 'DependencyComponent',
'DependencyError', 'DependencySet', 'DieMessage', 'do_once', 'DropMessage',
'DroppedMessage', 'DualTeamSession', 'emitfx', 'EmptyPlayer', 'EmptyTeam',
'Existable', 'existing', 'FloatChoiceSetting', 'FloatSetting',
'FreeForAllSession', 'FreezeMessage', 'GameActivity', 'GameResults',
'GameTip', 'garbage_collect', 'getactivity', 'getclass', 'getcollidemodel',
'getcollision', 'getdata', 'getmaps', 'getmodel', 'getnodes', 'getsession',
'getsound', 'gettexture', 'HitMessage', 'hscrollwidget', 'imagewidget',
'ImpactDamageMessage', 'InputDevice', 'InputDeviceNotFoundError',
'InputType', 'IntChoiceSetting', 'IntSetting',
'is_browser_likely_available', 'is_point_in_box', 'Keyboard',
'LanguageSubsystem', 'Level', 'Lobby', 'log', 'Lstr', 'Map', 'Material',
'MetadataSubsystem', 'Model', 'MultiTeamSession', 'MusicPlayer',
'MusicPlayMode', 'MusicSubsystem', 'MusicType', 'newactivity', 'newnode',
'Node', 'NodeActor', 'NodeNotFoundError', 'normalized_color',
'NotFoundError', 'open_url', 'OutOfBoundsMessage', 'Permission',
'PickedUpMessage', 'PickUpMessage', 'Player', 'PlayerDiedMessage',
'PlayerInfo', 'PlayerNotFoundError', 'PlayerRecord', 'PlayerScoredMessage',
'playsound', 'Plugin', 'PluginSubsystem', 'PotentialPlugin',
'PowerupAcceptMessage', 'PowerupMessage', 'print_error', 'print_exception',
'printnodes', 'printobjects', 'pushcall', 'quit', 'rowwidget', 'safecolor',
'ScoreConfig', 'ScoreType', 'screenmessage', 'scrollwidget',
'ServerController', 'Session', 'SessionNotFoundError', 'SessionPlayer',
'SessionPlayerNotFoundError', 'SessionTeam', 'SessionTeamNotFoundError',
'set_analytics_screen', 'setmusic', 'Setting', 'ShouldShatterMessage',
'show_damage_count', 'Sound', 'SpecialChar', 'StandLocation',
'StandMessage', 'Stats', 'storagename', 'Team', 'TeamGameActivity',
'TeamNotFoundError', 'Texture', 'textwidget', 'ThawMessage', 'time',
'TimeFormat', 'Timer', 'timer', 'timestring', 'TimeType', 'uicleanupcheck',
'UIController', 'UIScale', 'UISubsystem', 'UNHANDLED', 'Vec3',
'vec3validate', 'verify_object_death', 'WeakCall', 'Widget', 'widget',
'WidgetNotFoundError', 'Window'
]
# Change everything's listed module to simply 'ba' (instead of 'ba.foo.bar').
# Have these things present themselves cleanly as 'ba.Foo'
# instead of 'ba._submodule.Foo'
def _simplify_module_names() -> None:
for attr, obj in globals().items():
if not attr.startswith('_'):
if getattr(obj, '__module__', None) not in [None, 'ba']:
obj.__module__ = 'ba'
import os
# Though pdoc gets confused when we override __module__,
# so let's make an exception for it.
if os.environ.get('BA_DOCS_GENERATION', '0') != '1':
from efro.util import set_canonical_module
globs = globals()
set_canonical_module(
module_globals=globs,
names=[n for n in globs.keys() if not n.startswith('_')])
_simplify_module_names()

267
dist/ba_data/python/ba/_accountv1.py vendored Normal file
View file

@ -0,0 +1,267 @@
# Released under the MIT License. See LICENSE for details.
#
"""Account related functionality."""
from __future__ import annotations
import copy
import time
from typing import TYPE_CHECKING
import _ba
if TYPE_CHECKING:
from typing import Any, Optional
class AccountV1Subsystem:
"""Subsystem for legacy account handling in the app.
Category: **App Classes**
Access the single shared instance of this class at 'ba.app.accounts_v1'.
"""
def __init__(self) -> None:
self.account_tournament_list: Optional[tuple[int, list[str]]] = None
# FIXME: should abstract/structure these.
self.tournament_info: dict = {}
self.league_rank_cache: dict = {}
self.last_post_purchase_message_time: Optional[float] = None
# If we try to run promo-codes due to launch-args/etc we might
# not be signed in yet; go ahead and queue them up in that case.
self.pending_promo_codes: list[str] = []
def on_app_launch(self) -> None:
"""Called when the app is done bootstrapping."""
# Auto-sign-in to a local account in a moment if we're set to.
def do_auto_sign_in() -> None:
if _ba.app.headless_mode or _ba.app.config.get(
'Auto Account State') == 'Local':
_ba.sign_in_v1('Local')
_ba.pushcall(do_auto_sign_in)
def on_app_resume(self) -> None:
"""Should be called when the app is resumed."""
# Mark our cached tourneys as invalid so anyone using them knows
# they might be out of date.
for entry in list(self.tournament_info.values()):
entry['valid'] = False
def handle_account_gained_tickets(self, count: int) -> None:
"""Called when the current account has been awarded tickets.
(internal)
"""
from ba._language import Lstr
_ba.screenmessage(Lstr(resource='getTicketsWindow.receivedTicketsText',
subs=[('${COUNT}', str(count))]),
color=(0, 1, 0))
_ba.playsound(_ba.getsound('cashRegister'))
def cache_league_rank_data(self, data: Any) -> None:
"""(internal)"""
self.league_rank_cache['info'] = copy.deepcopy(data)
def get_cached_league_rank_data(self) -> Any:
"""(internal)"""
return self.league_rank_cache.get('info', None)
def get_league_rank_points(self,
data: Optional[dict[str, Any]],
subset: str = None) -> int:
"""(internal)"""
if data is None:
return 0
# If the data contains an achievement total, use that. otherwise calc
# locally.
if data['at'] is not None:
total_ach_value = data['at']
else:
total_ach_value = 0
for ach in _ba.app.ach.achievements:
if ach.complete:
total_ach_value += ach.power_ranking_value
trophies_total: int = (data['t0a'] * data['t0am'] +
data['t0b'] * data['t0bm'] +
data['t1'] * data['t1m'] +
data['t2'] * data['t2m'] +
data['t3'] * data['t3m'] +
data['t4'] * data['t4m'])
if subset == 'trophyCount':
val: int = (data['t0a'] + data['t0b'] + data['t1'] + data['t2'] +
data['t3'] + data['t4'])
assert isinstance(val, int)
return val
if subset == 'trophies':
assert isinstance(trophies_total, int)
return trophies_total
if subset is not None:
raise ValueError('invalid subset value: ' + str(subset))
if data['p']:
pro_mult = 1.0 + float(
_ba.get_v1_account_misc_read_val('proPowerRankingBoost',
0.0)) * 0.01
else:
pro_mult = 1.0
# For final value, apply our pro mult and activeness-mult.
return int(
(total_ach_value + trophies_total) *
(data['act'] if data['act'] is not None else 1.0) * pro_mult)
def cache_tournament_info(self, info: Any) -> None:
"""(internal)"""
from ba._generated.enums import TimeType, TimeFormat
for entry in info:
cache_entry = self.tournament_info[entry['tournamentID']] = (
copy.deepcopy(entry))
# Also store the time we received this, so we can adjust
# time-remaining values/etc.
cache_entry['timeReceived'] = _ba.time(TimeType.REAL,
TimeFormat.MILLISECONDS)
cache_entry['valid'] = True
def get_purchased_icons(self) -> list[str]:
"""(internal)"""
# pylint: disable=cyclic-import
from ba import _store
if _ba.get_v1_account_state() != 'signed_in':
return []
icons = []
store_items = _store.get_store_items()
for item_name, item in list(store_items.items()):
if item_name.startswith('icons.') and _ba.get_purchased(item_name):
icons.append(item['icon'])
return icons
def ensure_have_account_player_profile(self) -> None:
"""
Ensure the standard account-named player profile exists;
creating if needed.
(internal)
"""
# This only applies when we're signed in.
if _ba.get_v1_account_state() != 'signed_in':
return
# If the short version of our account name currently cant be
# displayed by the game, cancel.
if not _ba.have_chars(_ba.get_v1_account_display_string(full=False)):
return
config = _ba.app.config
if ('Player Profiles' not in config
or '__account__' not in config['Player Profiles']):
# Create a spaz with a nice default purply color.
_ba.add_transaction({
'type': 'ADD_PLAYER_PROFILE',
'name': '__account__',
'profile': {
'character': 'Spaz',
'color': [0.5, 0.25, 1.0],
'highlight': [0.5, 0.25, 1.0]
}
})
_ba.run_transactions()
def have_pro(self) -> bool:
"""Return whether pro is currently unlocked."""
# Check our tickets-based pro upgrade and our two real-IAP based
# upgrades. Also always unlock this stuff in ballistica-core builds.
return bool(
_ba.get_purchased('upgrades.pro')
or _ba.get_purchased('static.pro')
or _ba.get_purchased('static.pro_sale')
or 'ballistica' + 'core' == _ba.appname())
def have_pro_options(self) -> bool:
"""Return whether pro-options are present.
This is True for owners of Pro or for old installs
before Pro was a requirement for these options.
"""
# We expose pro options if the server tells us to
# (which is generally just when we own pro),
# or also if we've been grandfathered in or are using ballistica-core
# builds.
return self.have_pro() or bool(
_ba.get_v1_account_misc_read_val_2('proOptionsUnlocked', False)
or _ba.app.config.get('lc14292', 0) > 1)
def show_post_purchase_message(self) -> None:
"""(internal)"""
from ba._language import Lstr
from ba._generated.enums import TimeType
cur_time = _ba.time(TimeType.REAL)
if (self.last_post_purchase_message_time is None
or cur_time - self.last_post_purchase_message_time > 3.0):
self.last_post_purchase_message_time = cur_time
with _ba.Context('ui'):
_ba.screenmessage(Lstr(resource='updatingAccountText',
fallback_resource='purchasingText'),
color=(0, 1, 0))
_ba.playsound(_ba.getsound('click01'))
def on_account_state_changed(self) -> None:
"""(internal)"""
from ba._language import Lstr
# Run any pending promo codes we had queued up while not signed in.
if _ba.get_v1_account_state(
) == 'signed_in' and self.pending_promo_codes:
for code in self.pending_promo_codes:
_ba.screenmessage(Lstr(resource='submittingPromoCodeText'),
color=(0, 1, 0))
_ba.add_transaction({
'type': 'PROMO_CODE',
'expire_time': time.time() + 5,
'code': code
})
_ba.run_transactions()
self.pending_promo_codes = []
def add_pending_promo_code(self, code: str) -> None:
"""(internal)"""
from ba._language import Lstr
from ba._generated.enums import TimeType
# If we're not signed in, queue up the code to run the next time we
# are and issue a warning if we haven't signed in within the next
# few seconds.
if _ba.get_v1_account_state() != 'signed_in':
def check_pending_codes() -> None:
"""(internal)"""
# If we're still not signed in and have pending codes,
# inform the user that they need to sign in to use them.
if self.pending_promo_codes:
_ba.screenmessage(Lstr(resource='signInForPromoCodeText'),
color=(1, 0, 0))
_ba.playsound(_ba.getsound('error'))
self.pending_promo_codes.append(code)
_ba.timer(6.0, check_pending_codes, timetype=TimeType.REAL)
return
_ba.screenmessage(Lstr(resource='submittingPromoCodeText'),
color=(0, 1, 0))
_ba.add_transaction({
'type': 'PROMO_CODE',
'expire_time': time.time() + 5,
'code': code
})
_ba.run_transactions()

51
dist/ba_data/python/ba/_accountv2.py vendored Normal file
View file

@ -0,0 +1,51 @@
# Released under the MIT License. See LICENSE for details.
#
"""Account related functionality."""
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import Optional
class AccountV2Subsystem:
"""Subsystem for modern account handling in the app.
Category: **App Classes**
Access the single shared instance of this class at 'ba.app.accounts_v2'.
"""
def on_app_launch(self) -> None:
"""Should be called at standard on_app_launch time."""
def set_primary_credentials(self, credentials: Optional[str]) -> None:
"""Set credentials for the primary app account."""
raise RuntimeError('This should be overridden.')
def have_primary_credentials(self) -> bool:
"""Are credentials currently set for the primary app account?
Note that this does not mean these credentials are currently valid;
only that they exist. If/when credentials are validated, the 'primary'
account handle will be set.
"""
raise RuntimeError('This should be overridden.')
@property
def primary(self) -> Optional[AccountV2Handle]:
"""The primary account for the app, or None if not logged in."""
return None
def get_primary(self) -> Optional[AccountV2Handle]:
"""Internal - should be overridden by subclass."""
return None
class AccountV2Handle:
"""Handle for interacting with a v2 account."""
def __init__(self) -> None:
self.tag = '?'

View file

@ -65,7 +65,7 @@ ACH_LEVEL_NAMES = {
class AchievementSubsystem:
"""Subsystem for achievement handling.
Category: App Classes
Category: **App Classes**
Access the single shared instance of this class at 'ba.app.ach'.
"""
@ -409,9 +409,9 @@ def _get_ach_mult(include_pro_bonus: bool = False) -> int:
(just for display; changing this here won't affect actual rewards)
"""
val: int = _ba.get_account_misc_read_val('achAwardMult', 5)
val: int = _ba.get_v1_account_misc_read_val('achAwardMult', 5)
assert isinstance(val, int)
if include_pro_bonus and _ba.app.accounts.have_pro():
if include_pro_bonus and _ba.app.accounts_v1.have_pro():
val *= 2
return val
@ -436,7 +436,7 @@ def _display_next_achievement() -> None:
class Achievement:
"""Represents attributes and state for an individual achievement.
Category: App Classes
Category: **App Classes**
"""
def __init__(self,
@ -496,7 +496,7 @@ class Achievement:
# signed in, lets not show them (otherwise we tend to get
# confusing 'controller connected' achievements popping up while
# waiting to log in which can be confusing).
if _ba.get_account_state() != 'signed_in':
if _ba.get_v1_account_state() != 'signed_in':
return
# If we're being freshly complete, display/report it and whatnot.
@ -592,8 +592,8 @@ class Achievement:
def get_award_ticket_value(self, include_pro_bonus: bool = False) -> int:
"""Get the ticket award value for this achievement."""
val: int = (_ba.get_account_misc_read_val('achAward.' + self._name,
self._award) *
val: int = (_ba.get_v1_account_misc_read_val('achAward.' + self._name,
self._award) *
_get_ach_mult(include_pro_bonus))
assert isinstance(val, int)
return val
@ -601,7 +601,7 @@ class Achievement:
@property
def power_ranking_value(self) -> int:
"""Get the power-ranking award value for this achievement."""
val: int = _ba.get_account_misc_read_val(
val: int = _ba.get_v1_account_misc_read_val(
'achLeaguePoints.' + self._name, self._award)
assert isinstance(val, int)
return val
@ -916,7 +916,6 @@ class Achievement:
def show_completion_banner(self, sound: bool = True) -> None:
"""Create the banner/sound for an acquired achievement announcement."""
from ba import _account
from ba import _gameutils
from bastd.actor.text import Text
from bastd.actor.image import Image
@ -1177,7 +1176,7 @@ class Achievement:
objt.node.host_only = True
# Add the 'x 2' if we've got pro.
if app.accounts.have_pro():
if app.accounts_v1.have_pro():
objt = Text('x 2',
position=(-120 - 180 + 45, 80 + y_offs - 50),
v_attach=Text.VAttach.BOTTOM,

View file

@ -18,10 +18,11 @@ from ba._messages import UNHANDLED
if TYPE_CHECKING:
from typing import Optional, Any
import ba
from bastd.actor.respawnicon import RespawnIcon
# pylint: disable=invalid-name
PlayerType = TypeVar('PlayerType', bound=Player)
TeamType = TypeVar('TeamType', bound=Team)
# pylint: enable=invalid-name
class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
@ -32,104 +33,97 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
Examples of Activities include games, score-screens, cutscenes, etc.
A ba.Session has one 'current' Activity at any time, though their existence
can overlap during transitions.
Attributes:
settings_raw
The settings dict passed in when the activity was made.
This attribute is deprecated and should be avoided when possible;
activities should pull all values they need from the 'settings' arg
passed to the Activity __init__ call.
teams
The list of ba.Teams in the Activity. This gets populated just before
before on_begin() is called and is updated automatically as players
join or leave the game. (at least in free-for-all mode where every
player gets their own team; in teams mode there are always 2 teams
regardless of the player count).
players
The list of ba.Players in the Activity. This gets populated just
before on_begin() is called and is updated automatically as players
join or leave the game.
"""
# pylint: disable=too-many-public-methods
# Annotating attr types at the class level lets us introspect at runtime.
settings_raw: dict[str, Any]
"""The settings dict passed in when the activity was made.
This attribute is deprecated and should be avoided when possible;
activities should pull all values they need from the 'settings' arg
passed to the Activity __init__ call."""
teams: list[TeamType]
"""The list of ba.Team-s in the Activity. This gets populated just
before on_begin() is called and is updated automatically as players
join or leave the game. (at least in free-for-all mode where every
player gets their own team; in teams mode there are always 2 teams
regardless of the player count)."""
players: list[PlayerType]
"""The list of ba.Player-s in the Activity. This gets populated just
before on_begin() is called and is updated automatically as players
join or leave the game."""
# Whether to print every time a player dies. This can be pertinent
# in games such as Death-Match but can be annoying in games where it
# doesn't matter.
announce_player_deaths = False
"""Whether to print every time a player dies. This can be pertinent
in games such as Death-Match but can be annoying in games where it
doesn't matter."""
# Joining activities are for waiting for initial player joins.
# They are treated slightly differently than regular activities,
# mainly in that all players are passed to the activity at once
# instead of as each joins.
is_joining_activity = False
"""Joining activities are for waiting for initial player joins.
They are treated slightly differently than regular activities,
mainly in that all players are passed to the activity at once
instead of as each joins."""
# Whether game-time should still progress when in menus/etc.
allow_pausing = False
"""Whether game-time should still progress when in menus/etc."""
# Whether idle players can potentially be kicked (should not happen in
# menus/etc).
allow_kick_idle_players = True
"""Whether idle players can potentially be kicked (should not happen in
menus/etc)."""
# In vr mode, this determines whether overlay nodes (text, images, etc)
# are created at a fixed position in space or one that moves based on
# the current map. Generally this should be on for games and off for
# transitions/score-screens/etc. that persist between maps.
use_fixed_vr_overlay = False
"""In vr mode, this determines whether overlay nodes (text, images, etc)
are created at a fixed position in space or one that moves based on
the current map. Generally this should be on for games and off for
transitions/score-screens/etc. that persist between maps."""
# If True, runs in slow motion and turns down sound pitch.
slow_motion = False
"""If True, runs in slow motion and turns down sound pitch."""
# Set this to True to inherit slow motion setting from previous
# activity (useful for transitions to avoid hitches).
inherits_slow_motion = False
"""Set this to True to inherit slow motion setting from previous
activity (useful for transitions to avoid hitches)."""
# Set this to True to keep playing the music from the previous activity
# (without even restarting it).
inherits_music = False
"""Set this to True to keep playing the music from the previous activity
(without even restarting it)."""
# Set this to true to inherit VR camera offsets from the previous
# activity (useful for preventing sporadic camera movement
# during transitions).
inherits_vr_camera_offset = False
"""Set this to true to inherit VR camera offsets from the previous
activity (useful for preventing sporadic camera movement
during transitions)."""
# Set this to true to inherit (non-fixed) VR overlay positioning from
# the previous activity (useful for prevent sporadic overlay jostling
# during transitions).
inherits_vr_overlay_center = False
"""Set this to true to inherit (non-fixed) VR overlay positioning from
the previous activity (useful for prevent sporadic overlay jostling
during transitions)."""
# Set this to true to inherit screen tint/vignette colors from the
# previous activity (useful to prevent sudden color changes during
# transitions).
inherits_tint = False
"""Set this to true to inherit screen tint/vignette colors from the
previous activity (useful to prevent sudden color changes during
transitions)."""
# Whether players should be allowed to join in the middle of this
# activity. Note that Sessions may not allow mid-activity-joins even
# if the activity says its ok.
allow_mid_activity_joins: bool = True
"""Whether players should be allowed to join in the middle of this
activity. Note that Sessions may not allow mid-activity-joins even
if the activity says its ok."""
# If the activity fades or transitions in, it should set the length of
# time here so that previous activities will be kept alive for that
# long (avoiding 'holes' in the screen)
# This value is given in real-time seconds.
transition_time = 0.0
"""If the activity fades or transitions in, it should set the length of
time here so that previous activities will be kept alive for that
long (avoiding 'holes' in the screen)
This value is given in real-time seconds."""
# Is it ok to show an ad after this activity ends before showing
# the next activity?
can_show_ad_on_death = False
"""Is it ok to show an ad after this activity ends before showing
the next activity?"""
def __init__(self, settings: dict):
"""Creates an Activity in the current ba.Session.
The activity will not be actually run until ba.Session.setactivity()
The activity will not be actually run until ba.Session.setactivity
is called. 'settings' should be a dict of key/value pairs specific
to the activity.
@ -369,8 +363,8 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
def on_transition_out(self) -> None:
"""Called when your activity begins transitioning out.
Note that this may happen at any time even if end() has not been
called.
Note that this may happen at any time even if ba.Activity.end() has
not been called.
"""
def on_begin(self) -> None:
@ -386,11 +380,12 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
return UNHANDLED
def has_transitioned_in(self) -> bool:
"""Return whether on_transition_in() has been called."""
"""Return whether ba.Activity.on_transition_in()
has been called."""
return self._has_transitioned_in
def has_begun(self) -> bool:
"""Return whether on_begin() has been called."""
"""Return whether ba.Activity.on_begin() has been called."""
return self._has_begun
def has_ended(self) -> bool:
@ -398,7 +393,7 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
return self._has_ended
def is_transitioning_out(self) -> bool:
"""Return whether on_transition_out() has been called."""
"""Return whether ba.Activity.on_transition_out() has been called."""
return self._transitioning_out
def transition_in(self, prev_globals: Optional[ba.Node]) -> None:
@ -517,8 +512,8 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
Subclasses can override this if the activity's player class
requires a custom constructor; otherwise it will be called with
no args. Note that the player object should not be used at this
point as it is not yet fully wired up; wait for on_player_join()
for that.
point as it is not yet fully wired up; wait for
ba.Activity.on_player_join() for that.
"""
del sessionplayer # Unused.
player = self._playertype()
@ -681,6 +676,7 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
sessionplayer.setactivity(None)
sessionplayer.activityplayer = None
# noinspection PyUnresolvedReferences
def _setup_player_and_team_types(self) -> None:
"""Pull player and team types from our typing.Generic params."""

View file

@ -21,7 +21,7 @@ TA = TypeVar('TA', bound='Actor')
class Actor:
"""High level logical entities in a ba.Activity.
Category: Gameplay Classes
Category: **Gameplay Classes**
Actors act as controllers, combining some number of ba.Nodes,
ba.Textures, ba.Sounds, etc. into a high-level cohesive unit.
@ -33,15 +33,16 @@ class Actor:
(killing off or transitioning out their nodes) when the last Python
reference to them disappears, so you can use logic such as:
# Create a flag Actor in our game activity:
from bastd.actor.flag import Flag
self.flag = Flag(position=(0, 10, 0))
# Later, destroy the flag.
# (provided nothing else is holding a reference to it)
# We could also just assign a new flag to this value.
# Either way, the old flag disappears.
self.flag = None
##### Example
>>> # Create a flag Actor in our game activity:
... from bastd.actor.flag import Flag
... self.flag = Flag(position=(0, 10, 0))
...
... # Later, destroy the flag.
... # (provided nothing else is holding a reference to it)
... # We could also just assign a new flag to this value.
... # Either way, the old flag disappears.
... self.flag = None
This is in contrast to the behavior of the more low level ba.Nodes,
which are always explicitly created and destroyed and don't care
@ -51,18 +52,18 @@ class Actor:
if you want an Actor to stick around until explicitly killed
regardless of references.
Another key feature of ba.Actor is its handlemessage() method, which
takes a single arbitrary object as an argument. This provides a safe way
to communicate between ba.Actor, ba.Activity, ba.Session, and any other
class providing a handlemessage() method. The most universally handled
Another key feature of ba.Actor is its ba.Actor.handlemessage() method,
which takes a single arbitrary object as an argument. This provides a safe
way to communicate between ba.Actor, ba.Activity, ba.Session, and any other
class providing a handlemessage() method. The most universally handled
message type for Actors is the ba.DieMessage.
# Another way to kill the flag from the example above:
# We can safely call this on any type with a 'handlemessage' method
# (though its not guaranteed to always have a meaningful effect).
# In this case the Actor instance will still be around, but its exists()
# and is_alive() methods will both return False.
self.flag.handlemessage(ba.DieMessage())
Another way to kill the flag from the example above:
We can safely call this on any type with a 'handlemessage' method
(though its not guaranteed to always have a meaningful effect).
In this case the Actor instance will still be around, but its
ba.Actor.exists() and ba.Actor.is_alive() methods will both return False.
>>> self.flag.handlemessage(ba.DieMessage())
"""
def __init__(self) -> None:
@ -112,7 +113,7 @@ class Actor:
return self
def on_expire(self) -> None:
"""Called for remaining ba.Actors when their ba.Activity shuts down.
"""Called for remaining `ba.Actor`s when their ba.Activity shuts down.
Actors can use this opportunity to clear callbacks or other
references which have the potential of keeping the ba.Activity

View file

@ -15,7 +15,7 @@ if TYPE_CHECKING:
class AdsSubsystem:
"""Subsystem for ads functionality in the app.
Category: App Classes
Category: **App Classes**
Access the single shared instance of this class at 'ba.app.ads'.
"""
@ -77,7 +77,7 @@ class AdsSubsystem:
# No ads without net-connections, etc.
if not _ba.can_show_ad():
show = False
if app.accounts.have_pro():
if app.accounts_v1.have_pro():
show = False # Pro disables interstitials.
try:
session = _ba.get_foreground_host_session()
@ -93,15 +93,15 @@ class AdsSubsystem:
launch_count = app.config.get('launchCount', 0)
# If we're seeing short ads we may want to space them differently.
interval_mult = (_ba.get_account_misc_read_val(
interval_mult = (_ba.get_v1_account_misc_read_val(
'ads.shortIntervalMult', 1.0)
if self.last_ad_was_short else 1.0)
if self.ad_amt is None:
if launch_count <= 1:
self.ad_amt = _ba.get_account_misc_read_val(
self.ad_amt = _ba.get_v1_account_misc_read_val(
'ads.startVal1', 0.99)
else:
self.ad_amt = _ba.get_account_misc_read_val(
self.ad_amt = _ba.get_v1_account_misc_read_val(
'ads.startVal2', 1.0)
interval = None
else:
@ -110,15 +110,15 @@ class AdsSubsystem:
# (we reach our threshold faster the longer we've been
# playing).
base = 'ads' if _ba.has_video_ads() else 'ads2'
min_lc = _ba.get_account_misc_read_val(base + '.minLC', 0.0)
max_lc = _ba.get_account_misc_read_val(base + '.maxLC', 5.0)
min_lc_scale = (_ba.get_account_misc_read_val(
min_lc = _ba.get_v1_account_misc_read_val(base + '.minLC', 0.0)
max_lc = _ba.get_v1_account_misc_read_val(base + '.maxLC', 5.0)
min_lc_scale = (_ba.get_v1_account_misc_read_val(
base + '.minLCScale', 0.25))
max_lc_scale = (_ba.get_account_misc_read_val(
max_lc_scale = (_ba.get_v1_account_misc_read_val(
base + '.maxLCScale', 0.34))
min_lc_interval = (_ba.get_account_misc_read_val(
min_lc_interval = (_ba.get_v1_account_misc_read_val(
base + '.minLCInterval', 360))
max_lc_interval = (_ba.get_account_misc_read_val(
max_lc_interval = (_ba.get_v1_account_misc_read_val(
base + '.maxLCInterval', 300))
if launch_count < min_lc:
lc_amt = 0.0

View file

@ -7,6 +7,7 @@ import random
import logging
from enum import Enum
from typing import TYPE_CHECKING
from concurrent.futures import ThreadPoolExecutor
import _ba
from ba._music import MusicSubsystem
@ -14,21 +15,25 @@ from ba._language import LanguageSubsystem
from ba._ui import UISubsystem
from ba._achievement import AchievementSubsystem
from ba._plugin import PluginSubsystem
from ba._account import AccountSubsystem
from ba._accountv1 import AccountV1Subsystem
from ba._meta import MetadataSubsystem
from ba._ads import AdsSubsystem
from ba._net import NetworkSubsystem
if TYPE_CHECKING:
import ba
from bastd.actor import spazappearance
import asyncio
from typing import Optional, Any, Callable
import ba
from ba.cloud import CloudSubsystem
from bastd.actor import spazappearance
from ba._accountv2 import AccountV2Subsystem
class App:
"""A class for high level app functionality and state.
Category: App Classes
Category: **App Classes**
Use ba.app to access the single shared instance of this class.
@ -38,6 +43,10 @@ class App:
# pylint: disable=too-many-public-methods
# Implementations for these will be filled in by internal libs.
accounts_v2: AccountV2Subsystem
cloud: CloudSubsystem
class State(Enum):
"""High level state the app can be in."""
LAUNCHING = 0
@ -45,6 +54,20 @@ class App:
PAUSED = 2
SHUTTING_DOWN = 3
@property
def aioloop(self) -> asyncio.AbstractEventLoop:
"""The Logic Thread's Asyncio Event Loop.
This allow async tasks to be run in the logic thread.
Note that, at this time, the asyncio loop is encapsulated
and explicitly stepped by the engine's logic thread loop and
thus things like asyncio.get_running_loop() will not return this
loop from most places in the logic thread; only from within a
task explicitly created in this loop.
"""
assert self._aioloop is not None
return self._aioloop
@property
def build_number(self) -> int:
"""Integer build number.
@ -196,6 +219,8 @@ class App:
# refreshed/etc.
self.fg_state = 0
self._aioloop: Optional[asyncio.AbstractEventLoop] = None
self._env = _ba.env()
self.protocol_version: int = self._env['protocol_version']
assert isinstance(self.protocol_version, int)
@ -211,6 +236,11 @@ class App:
assert isinstance(self.iircade_mode, bool)
self.allow_ticket_purchases: bool = not self.iircade_mode
# Default executor which can be used for misc background processing.
# It should also be passed to any asyncio loops we create so that
# everything shares the same single set of threads.
self.threadpool = ThreadPoolExecutor(thread_name_prefix='baworker')
# Misc.
self.tips: list[str] = []
self.stress_test_reset_timer: Optional[ba.Timer] = None
@ -235,7 +265,7 @@ class App:
self.server: Optional[ba.ServerController] = None
self.meta = MetadataSubsystem()
self.accounts = AccountSubsystem()
self.accounts_v1 = AccountV1Subsystem()
self.plugins = PluginSubsystem()
self.music = MusicSubsystem()
self.lang = LanguageSubsystem()
@ -285,11 +315,11 @@ class App:
"""Runs after the app finishes bootstrapping.
(internal)"""
# pylint: disable=too-many-locals
# pylint: disable=cyclic-import
# pylint: disable=too-many-locals
from ba import _asyncio
from ba import _apputils
from ba import _appconfig
from ba import _achievement
from ba import _map
from ba import _campaign
from bastd import appdelegate
@ -299,6 +329,8 @@ class App:
import custom_hooks
custom_hooks.on_app_launch()
self._aioloop = _asyncio.setup_asyncio()
cfg = self.config
self.delegate = appdelegate.AppDelegate()
@ -369,7 +401,8 @@ class App:
_ba.timer(3.0, check_special_offer, timetype=TimeType.REAL)
self.meta.on_app_launch()
self.accounts.on_app_launch()
self.accounts_v2.on_app_launch()
self.accounts_v1.on_app_launch()
self.plugins.on_app_launch()
# See note below in on_app_pause.
@ -407,7 +440,7 @@ class App:
self._app_paused = False
self._update_state()
self.fg_state += 1
self.accounts.on_app_resume()
self.accounts_v1.on_app_resume()
self.music.on_app_resume()
self.plugins.on_app_resume()
@ -431,7 +464,6 @@ class App:
activity: Optional[ba.Activity] = _ba.get_foreground_host_activity()
if (activity is not None and activity.allow_pausing
and not _ba.have_connected_clients()):
from ba import _gameutils
from ba._language import Lstr
from ba._nodeactor import NodeActor
@ -574,7 +606,7 @@ class App:
appname = _ba.appname()
if url.startswith(f'{appname}://code/'):
code = url.replace(f'{appname}://code/', '')
self.accounts.add_pending_promo_code(code)
self.accounts_v1.add_pending_promo_code(code)
else:
_ba.screenmessage(Lstr(resource='errorText'), color=(1, 0, 0))
_ba.playsound(_ba.getsound('error'))

View file

@ -14,7 +14,7 @@ if TYPE_CHECKING:
class AppConfig(dict):
"""A special dict that holds the game's persistent configuration values.
Category: App Classes
Category: **App Classes**
It also provides methods for fetching values with app-defined fallback
defaults, applying contained values to the game, and committing the
@ -126,14 +126,14 @@ def read_config() -> tuple[AppConfig, bool]:
try:
import shutil
shutil.copyfile(config_file_path, config_file_path + '.broken')
except Exception as exc:
print('EXC copying broken config:', exc)
except Exception as exc2:
print('EXC copying broken config:', exc2)
try:
_ba.log('broken config contents:\n' +
config_contents.replace('\000', '<NULL_BYTE>'),
to_stdout=False)
except Exception as exc:
print('EXC logging broken config contents:', exc)
except Exception as exc2:
print('EXC logging broken config contents:', exc2)
config = AppConfig()
# Now attempt to read one of our 'prev' backup copies.
@ -147,15 +147,15 @@ def read_config() -> tuple[AppConfig, bool]:
config = AppConfig()
config_file_healthy = True
print('successfully read backup config.')
except Exception as exc:
print('EXC reading prev backup config:', exc)
except Exception as exc2:
print('EXC reading prev backup config:', exc2)
return config, config_file_healthy
def commit_app_config(force: bool = False) -> None:
"""Commit the config to persistent storage.
Category: General Utility Functions
Category: **General Utility Functions**
(internal)
"""

View file

@ -21,11 +21,12 @@ _asyncio_timer: Optional[ba.Timer] = None
_asyncio_event_loop: Optional[asyncio.AbstractEventLoop] = None
def setup_asyncio() -> None:
"""Setup asyncio functionality for our game thread."""
def setup_asyncio() -> asyncio.AbstractEventLoop:
"""Setup asyncio functionality for the logic thread."""
# pylint: disable=global-statement
import _ba
import ba
from ba._generated.enums import TimeType
assert _ba.in_game_thread()
@ -40,6 +41,7 @@ def setup_asyncio() -> None:
global _asyncio_event_loop # pylint: disable=invalid-name
_asyncio_event_loop = asyncio.new_event_loop()
_asyncio_event_loop.set_default_executor(ba.app.threadpool)
# Ideally we should integrate asyncio into our C++ Thread class's
# low level event loop so that asyncio timers/sockets/etc. could
@ -70,3 +72,5 @@ def setup_asyncio() -> None:
print('TEST AIO TASK ENDING')
_asyncio_event_loop.create_task(aio_test())
return _asyncio_event_loop

View file

@ -23,9 +23,9 @@ def getcampaign(name: str) -> ba.Campaign:
class Campaign:
"""Represents a unique set or series of ba.Levels.
"""Represents a unique set or series of ba.Level-s.
Category: App Classes
Category: **App Classes**
"""
def __init__(self, name: str, sequential: bool = True):
@ -52,7 +52,7 @@ class Campaign:
@property
def levels(self) -> list[ba.Level]:
"""The list of ba.Levels in the Campaign."""
"""The list of ba.Level-s in the Campaign."""
return self._levels
def getlevel(self, name: str) -> ba.Level:

View file

@ -16,7 +16,7 @@ if TYPE_CHECKING:
class Collision:
"""A class providing info about occurring collisions.
Category: Gameplay Classes
Category: **Gameplay Classes**
"""
@property
@ -67,6 +67,6 @@ _collision = Collision()
def getcollision() -> Collision:
"""Return the in-progress collision.
Category: Gameplay Functions
Category: **Gameplay Functions**
"""
return _collision

View file

@ -14,14 +14,16 @@ if TYPE_CHECKING:
from bastd.actor.playerspaz import PlayerSpaz
import ba
# pylint: disable=invalid-name
PlayerType = TypeVar('PlayerType', bound='ba.Player')
TeamType = TypeVar('TeamType', bound='ba.Team')
# pylint: enable=invalid-name
class CoopGameActivity(GameActivity[PlayerType, TeamType]):
"""Base class for cooperative-mode games.
Category: Gameplay Classes
Category: **Gameplay Classes**
"""
# We can assume our session is a CoopSession.

View file

@ -132,7 +132,7 @@ class CoopSession(Session):
# else:
# nextlevel = None
nextlevel=levels[(level.index+1)%len(levels)]
if nextlevel:
gametype = nextlevel.gametype
settings = nextlevel.get_settings()
@ -284,12 +284,12 @@ class CoopSession(Session):
if outcome=="victory" or outcome=="restart" or outcome=="defeat":
outcome = 'next_level'
if (isinstance(activity,
(JoinActivity, CoopScoreScreen, TransitionActivity))) or True:
from features import team_balancer
team_balancer.checkToExitCoop()
@ -355,7 +355,7 @@ class CoopSession(Session):
else:
pass
if False:
playerinfos: list[ba.PlayerInfo]

View file

@ -19,7 +19,7 @@ T = TypeVar('T', bound='DependencyComponent')
class Dependency(Generic[T]):
"""A dependency on a DependencyComponent (with an optional config).
Category: Dependency Classes
Category: **Dependency Classes**
This class is used to request and access functionality provided
by other DependencyComponent classes from a DependencyComponent class.
@ -87,7 +87,7 @@ class Dependency(Generic[T]):
class DependencyComponent:
"""Base class for all classes that can act as or use dependencies.
category: Dependency Classes
Category: **Dependency Classes**
"""
_dep_entry: weakref.ref[DependencyEntry]
@ -146,7 +146,7 @@ class DependencyEntry:
# This allows us to inject its data properly before __init__().
print('creating', self.cls)
instance = self.cls.__new__(self.cls)
# pylint: disable=protected-access
# pylint: disable=protected-access, unnecessary-dunder-call
instance._dep_entry = weakref.ref(self)
instance.__init__() # type: ignore
@ -165,7 +165,7 @@ class DependencyEntry:
class DependencySet(Generic[T]):
"""Set of resolved dependencies and their associated data.
Category: Dependency Classes
Category: **Dependency Classes**
To use DependencyComponents, a set must be created, resolved, and then
loaded. The DependencyComponents are only valid while the set remains
@ -291,7 +291,7 @@ class DependencySet(Generic[T]):
class AssetPackage(DependencyComponent):
"""ba.DependencyComponent representing a bundled package of game assets.
Category: Asset Classes
Category: **Asset Classes**
"""
def __init__(self) -> None:

View file

@ -15,7 +15,7 @@ if TYPE_CHECKING:
class DualTeamSession(MultiTeamSession):
"""ba.Session type for teams mode games.
Category: Gameplay Classes
Category: **Gameplay Classes**
"""
# Base class overrides:

View file

@ -16,7 +16,7 @@ if TYPE_CHECKING:
class DependencyError(Exception):
"""Exception raised when one or more ba.Dependency items are missing.
category: Exception Classes
Category: **Exception Classes**
(this will generally be missing assets).
"""
@ -34,7 +34,7 @@ class DependencyError(Exception):
class ContextError(Exception):
"""Exception raised when a call is made in an invalid context.
category: Exception Classes
Category: **Exception Classes**
Examples of this include calling UI functions within an Activity context
or calling scene manipulation functions outside of a game context.
@ -44,91 +44,91 @@ class ContextError(Exception):
class NotFoundError(Exception):
"""Exception raised when a referenced object does not exist.
category: Exception Classes
Category: **Exception Classes**
"""
class PlayerNotFoundError(NotFoundError):
"""Exception raised when an expected ba.Player does not exist.
category: Exception Classes
Category: **Exception Classes**
"""
class SessionPlayerNotFoundError(NotFoundError):
"""Exception raised when an expected ba.SessionPlayer does not exist.
category: Exception Classes
Category: **Exception Classes**
"""
class TeamNotFoundError(NotFoundError):
"""Exception raised when an expected ba.Team does not exist.
category: Exception Classes
Category: **Exception Classes**
"""
class DelegateNotFoundError(NotFoundError):
"""Exception raised when an expected delegate object does not exist.
category: Exception Classes
Category: **Exception Classes**
"""
class SessionTeamNotFoundError(NotFoundError):
"""Exception raised when an expected ba.SessionTeam does not exist.
category: Exception Classes
Category: **Exception Classes**
"""
class NodeNotFoundError(NotFoundError):
"""Exception raised when an expected ba.Node does not exist.
category: Exception Classes
Category: **Exception Classes**
"""
class ActorNotFoundError(NotFoundError):
"""Exception raised when an expected ba.Actor does not exist.
category: Exception Classes
Category: **Exception Classes**
"""
class ActivityNotFoundError(NotFoundError):
"""Exception raised when an expected ba.Activity does not exist.
category: Exception Classes
Category: **Exception Classes**
"""
class SessionNotFoundError(NotFoundError):
"""Exception raised when an expected ba.Session does not exist.
category: Exception Classes
Category: **Exception Classes**
"""
class InputDeviceNotFoundError(NotFoundError):
"""Exception raised when an expected ba.InputDevice does not exist.
category: Exception Classes
Category: **Exception Classes**
"""
class WidgetNotFoundError(NotFoundError):
"""Exception raised when an expected ba.Widget does not exist.
category: Exception Classes
Category: **Exception Classes**
"""
def print_exception(*args: Any, **keywds: Any) -> None:
"""Print info about an exception along with pertinent context state.
category: General Utility Functions
Category: **General Utility Functions**
Prints all arguments provided along with various info about the
current context and the outstanding exception.
@ -168,7 +168,7 @@ def print_exception(*args: Any, **keywds: Any) -> None:
def print_error(err_str: str, once: bool = False) -> None:
"""Print info about an error along with pertinent context state.
category: General Utility Functions
Category: **General Utility Functions**
Prints all positional arguments provided along with various info about the
current context.

View file

@ -16,7 +16,7 @@ if TYPE_CHECKING:
class FreeForAllSession(MultiTeamSession):
"""ba.Session type for free-for-all mode games.
Category: Gameplay Classes
Category: **Gameplay Classes**
"""
use_teams = False
use_team_colors = False

View file

@ -24,14 +24,16 @@ if TYPE_CHECKING:
from bastd.actor.bomb import TNTSpawner
import ba
# pylint: disable=invalid-name
PlayerType = TypeVar('PlayerType', bound='ba.Player')
TeamType = TypeVar('TeamType', bound='ba.Team')
# pylint: enable=invalid-name
class GameActivity(Activity[PlayerType, TeamType]):
"""Common base class for all game ba.Activities.
category: Gameplay Classes
Category: **Gameplay Classes**
"""
# pylint: disable=too-many-public-methods
@ -237,11 +239,11 @@ class GameActivity(Activity[PlayerType, TeamType]):
self._zoom_message_times: dict[int, float] = {}
self._is_waiting_for_continue = False
self._continue_cost = _ba.get_account_misc_read_val(
self._continue_cost = _ba.get_v1_account_misc_read_val(
'continueStartCost', 25)
self._continue_cost_mult = _ba.get_account_misc_read_val(
self._continue_cost_mult = _ba.get_v1_account_misc_read_val(
'continuesMult', 2)
self._continue_cost_offset = _ba.get_account_misc_read_val(
self._continue_cost_offset = _ba.get_v1_account_misc_read_val(
'continuesOffset', 0)
@property
@ -258,6 +260,7 @@ class GameActivity(Activity[PlayerType, TeamType]):
"""Return a name for this particular game instance."""
return self.get_display_string(self.settings_raw)
# noinspection PyUnresolvedReferences
def get_instance_scoreboard_display_string(self) -> ba.Lstr:
"""Return a name for this particular game instance.
@ -387,7 +390,7 @@ class GameActivity(Activity[PlayerType, TeamType]):
from ba._generated.enums import TimeType
try:
if _ba.get_account_misc_read_val('enableContinues', False):
if _ba.get_v1_account_misc_read_val('enableContinues', False):
session = self.session
# We only support continuing in non-tournament games.
@ -464,7 +467,7 @@ class GameActivity(Activity[PlayerType, TeamType]):
data_t = data['t'] # This used to be the whole payload.
# Keep our cached tourney info up to date
_ba.app.accounts.cache_tournament_info(data_t)
_ba.app.accounts_v1.cache_tournament_info(data_t)
self._setup_tournament_time_limit(
max(5, data_t[0]['timeRemaining']))

View file

@ -27,10 +27,10 @@ class GameResults:
"""
Results for a completed game.
Category: Gameplay Classes
Category: **Gameplay Classes**
Upon completion, a game should fill one of these out and pass it to its
ba.Activity.end() call.
ba.Activity.end call.
"""
def __init__(self) -> None:

View file

@ -29,7 +29,7 @@ TROPHY_CHARS = {
class GameTip:
"""Defines a tip presentable to the user at the start of a game.
Category: Gameplay Classes
Category: **Gameplay Classes**
"""
text: str
icon: Optional[ba.Texture] = None
@ -53,7 +53,7 @@ def animate(node: ba.Node,
suppress_format_warning: bool = False) -> ba.Node:
"""Animate values on a target ba.Node.
Category: Gameplay Functions
Category: **Gameplay Functions**
Creates an 'animcurve' node with the provided values and time as an input,
connect it to the provided attribute, and set it to die with the target.
@ -98,6 +98,7 @@ def animate(node: ba.Node,
# FIXME: Even if we are looping we should have a way to die once we
# get disconnected.
if not loop:
# noinspection PyUnresolvedReferences
_ba.timer(int(mult * items[-1][0]) + 1000,
curve.delete,
timeformat=TimeFormat.MILLISECONDS)
@ -127,9 +128,9 @@ def animate_array(node: ba.Node,
suppress_format_warning: bool = False) -> None:
"""Animate an array of values on a target ba.Node.
Category: Gameplay Functions
Category: **Gameplay Functions**
Like ba.animate(), but operates on array attributes.
Like ba.animate, but operates on array attributes.
"""
# pylint: disable=too-many-locals
combine = _ba.newnode('combine', owner=node, attrs={'size': size})
@ -178,6 +179,7 @@ def animate_array(node: ba.Node,
# curve after its done its job.
if not loop:
# (PyCharm seems to think item is a float, not a tuple)
# noinspection PyUnresolvedReferences
_ba.timer(int(mult * items[-1][0]) + 1000,
curve.delete,
timeformat=TimeFormat.MILLISECONDS)
@ -189,6 +191,7 @@ def animate_array(node: ba.Node,
# once we get disconnected.
if not loop:
# (PyCharm seems to think item is a float, not a tuple)
# noinspection PyUnresolvedReferences
_ba.timer(int(mult * items[-1][0]) + 1000,
combine.delete,
timeformat=TimeFormat.MILLISECONDS)
@ -198,7 +201,7 @@ def show_damage_count(damage: str, position: Sequence[float],
direction: Sequence[float]) -> None:
"""Pop up a damage count at a position in space.
Category: Gameplay Functions
Category: **Gameplay Functions**
"""
lifespan = 1.0
app = _ba.app
@ -253,7 +256,7 @@ def timestring(timeval: float,
suppress_format_warning: bool = False) -> ba.Lstr:
"""Generate a ba.Lstr for displaying a time value.
Category: General Utility Functions
Category: **General Utility Functions**
Given a time value, returns a ba.Lstr with:
(hours if > 0 ) : minutes : seconds : (centiseconds if centi=True).
@ -321,7 +324,7 @@ def timestring(timeval: float,
def cameraflash(duration: float = 999.0) -> None:
"""Create a strobing camera flash effect.
Category: Gameplay Functions
Category: **Gameplay Functions**
(as seen when a team wins a game)
Duration is in seconds.

View file

@ -24,22 +24,23 @@ if TYPE_CHECKING:
class Existable(Protocol):
"""A Protocol for objects supporting an exists() method.
Category: Protocols
Category: **Protocols**
"""
def exists(self) -> bool:
"""Whether this object exists."""
...
# pylint: disable=invalid-name
ExistableType = TypeVar('ExistableType', bound=Existable)
# pylint: enable=invalid-name
T = TypeVar('T')
def existing(obj: Optional[ExistableType]) -> Optional[ExistableType]:
"""Convert invalid references to None for any ba.Existable object.
Category: Gameplay Functions
Category: **Gameplay Functions**
To best support type checking, it is important that invalid references
not be passed around and instead get converted to values of None.
@ -59,7 +60,7 @@ def existing(obj: Optional[ExistableType]) -> Optional[ExistableType]:
def getclass(name: str, subclassof: type[T]) -> type[T]:
"""Given a full class name such as foo.bar.MyClass, return the class.
Category: General Utility Functions
Category: **General Utility Functions**
The class will be checked to make sure it is a subclass of the provided
'subclassof' class, and a TypeError will be raised if not.
@ -140,7 +141,7 @@ def get_type_name(cls: type) -> str:
class _WeakCall:
"""Wrap a callable and arguments into a single callable object.
Category: General Utility Classes
Category: **General Utility Classes**
When passed a bound method as the callable, the instance portion
of it is weak-referenced, meaning the underlying instance is
@ -150,20 +151,30 @@ class _WeakCall:
Think of this as a handy way to tell an object to do something
at some point in the future if it happens to still exist.
# EXAMPLE A: this code will create a FooClass instance and call its
# bar() method 5 seconds later; it will be kept alive even though
# we overwrite its variable with None because the bound method
# we pass as a timer callback (foo.bar) strong-references it
foo = FooClass()
ba.timer(5.0, foo.bar)
foo = None
##### Examples
**EXAMPLE A:** this code will create a FooClass instance and call its
bar() method 5 seconds later; it will be kept alive even though
we overwrite its variable with None because the bound method
we pass as a timer callback (foo.bar) strong-references it
>>> foo = FooClass()
... ba.timer(5.0, foo.bar)
... foo = None
# EXAMPLE B: this code will *not* keep our object alive; it will die
# when we overwrite it with None and the timer will be a no-op when it
# fires
foo = FooClass()
ba.timer(5.0, ba.WeakCall(foo.bar))
foo = None
**EXAMPLE B:** This code will *not* keep our object alive; it will die
when we overwrite it with None and the timer will be a no-op when it
fires
>>> foo = FooClass()
... ba.timer(5.0, ba.WeakCall(foo.bar))
... foo = None
**EXAMPLE C:** Wrap a method call with some positional and keyword args:
>>> myweakcall = ba.WeakCall(self.dostuff, argval1,
... namedarg=argval2)
... # Now we have a single callable to run that whole mess.
... # The same as calling myobj.dostuff(argval1, namedarg=argval2)
... # (provided my_obj still exists; this will do nothing
... # otherwise).
... myweakcall()
Note: additional args and keywords you provide to the WeakCall()
constructor are stored as regular strong-references; you'll need
@ -175,15 +186,6 @@ class _WeakCall:
Pass a callable as the first arg, followed by any number of
arguments or keywords.
# Example: wrap a method call with some positional and
# keyword args:
myweakcall = ba.WeakCall(myobj.dostuff, argval1, namedarg=argval2)
# Now we have a single callable to run that whole mess.
# The same as calling myobj.dostuff(argval1, namedarg=argval2)
# (provided my_obj still exists; this will do nothing otherwise)
myweakcall()
"""
if hasattr(args[0], '__func__'):
self._call = WeakMethod(args[0])
@ -212,13 +214,13 @@ class _WeakCall:
class _Call:
"""Wraps a callable and arguments into a single callable object.
Category: General Utility Classes
Category: **General Utility Classes**
The callable is strong-referenced so it won't die until this
object does.
Note that a bound method (ex: myobj.dosomething) contains a reference
to 'self' (myobj in that case), so you will be keeping that object
Note that a bound method (ex: ``myobj.dosomething``) contains a reference
to ``self`` (``myobj`` in that case), so you will be keeping that object
alive too. Use ba.WeakCall if you want to pass a method to callback
without keeping its object alive.
"""
@ -229,12 +231,12 @@ class _Call:
Pass a callable as the first arg, followed by any number of
arguments or keywords.
# Example: wrap a method call with 1 positional and 1 keyword arg:
mycall = ba.Call(myobj.dostuff, argval1, namedarg=argval2)
# Now we have a single callable to run that whole mess.
# ..the same as calling myobj.dostuff(argval1, namedarg=argval2)
mycall()
##### Example
Wrap a method call with 1 positional and 1 keyword arg:
>>> mycall = ba.Call(myobj.dostuff, argval, namedarg=argval2)
... # Now we have a single callable to run that whole mess.
... # ..the same as calling myobj.dostuff(argval, namedarg=argval2)
... mycall()
"""
self._call = args[0]
self._args = args[1:]
@ -283,7 +285,7 @@ class WeakMethod:
def verify_object_death(obj: object) -> None:
"""Warn if an object does not get freed within a short period.
Category: General Utility Functions
Category: **General Utility Functions**
This can be handy to detect and prevent memory/resource leaks.
"""
@ -304,7 +306,7 @@ def verify_object_death(obj: object) -> None:
def print_active_refs(obj: Any) -> None:
"""Print info about things referencing a given object.
Category: General Utility Functions
Category: **General Utility Functions**
Useful for tracking down cyclical references and causes for zombie objects.
"""
@ -361,7 +363,7 @@ def _verify_object_death(wref: weakref.ref) -> None:
def storagename(suffix: str = None) -> str:
"""Generate a unique name for storing class data in shared places.
Category: General Utility Functions
Category: **General Utility Functions**
This consists of a leading underscore, the module path at the
call site with dots replaced by underscores, the containing class's
@ -371,15 +373,17 @@ def storagename(suffix: str = None) -> str:
Note that this will function even if called in the class definition.
# Example: generate a unique name for storage purposes:
class MyThingie:
# This will give something like '_mymodule_submodule_mythingie_data'.
_STORENAME = ba.storagename('data')
# Use that name to store some data in the Activity we were passed.
def __init__(self, activity):
activity.customdata[self._STORENAME] = {}
##### Examples
Generate a unique name for storage purposes:
>>> class MyThingie:
... # This will give something like
... # '_mymodule_submodule_mythingie_data'.
... _STORENAME = ba.storagename('data')
...
... # Use that name to store some data in the Activity we were
... # passed.
... def __init__(self, activity):
... activity.customdata[self._STORENAME] = {}
"""
frame = inspect.currentframe()
if frame is None:

View file

@ -24,12 +24,11 @@ if TYPE_CHECKING:
def finish_bootstrapping() -> None:
"""Do final bootstrapping related bits."""
from ba._asyncio import setup_asyncio
assert _ba.in_game_thread()
# Kick off our asyncio event handling, allowing us to use coroutines
# in our game thread alongside our internal event handling.
setup_asyncio()
# setup_asyncio()
# Ok, bootstrapping is done; time to get the show started.
_ba.app.on_app_launch()
@ -385,3 +384,8 @@ def hash_strings(inputs: list[str]) -> str:
sha.update(inp.encode())
return sha.hexdigest()
def have_account_v2_credentials() -> bool:
"""Do we have primary account-v2 credentials set?"""
return _ba.app.accounts_v2.have_primary_credentials()

View file

@ -639,5 +639,5 @@ def get_last_player_name_from_input_device(device: ba.InputDevice) -> str:
if profilename == '_random':
profilename = device.get_default_player_name()
if profilename == '__account__':
profilename = _ba.get_account_display_string()
profilename = _ba.get_v1_account_display_string()
return profilename

View file

@ -13,23 +13,21 @@ if TYPE_CHECKING:
class Keyboard:
"""Chars definitions for on-screen keyboard.
Category: App Classes
Category: **App Classes**
Keyboards are discoverable by the meta-tag system
and the user can select which one they want to use.
On-screen keyboard uses chars from active ba.Keyboard.
Attributes:
name
Displays when user selecting this keyboard.
chars
Used for row/column lengths.
pages
Extra chars like emojis.
nums
The 'num' page.
"""
name: str
"""Displays when user selecting this keyboard."""
chars: list[tuple[str, ...]]
"""Used for row/column lengths."""
pages: dict[str, tuple[str, ...]]
"""Extra chars like emojis."""
nums: tuple[str, ...]
"""The 'num' page."""

View file

@ -17,7 +17,7 @@ if TYPE_CHECKING:
class LanguageSubsystem:
"""Wraps up language related app functionality.
Category: App Classes
Category: **App Classes**
To use this class, access the single instance of it at 'ba.app.lang'.
"""
@ -367,7 +367,7 @@ class LanguageSubsystem:
class Lstr:
"""Used to define strings in a language-independent way.
category: General Utility Classes
Category: **General Utility Classes**
These should be used whenever possible in place of hard-coded strings
so that in-game or UI elements show up correctly on all clients in their
@ -376,24 +376,28 @@ class Lstr:
To see available resource keys, look at any of the bs_language_*.py files
in the game or the translations pages at legacy.ballistica.net/translate.
# EXAMPLE 1: specify a string from a resource path
mynode.text = ba.Lstr(resource='audioSettingsWindow.titleText')
##### Examples
EXAMPLE 1: specify a string from a resource path
>>> mynode.text = ba.Lstr(resource='audioSettingsWindow.titleText')
# EXAMPLE 2: specify a translated string via a category and english value;
# if a translated value is available, it will be used; otherwise the
# english value will be. To see available translation categories, look
# under the 'translations' resource section.
mynode.text = ba.Lstr(translate=('gameDescriptions', 'Defeat all enemies'))
EXAMPLE 2: specify a translated string via a category and english
value; if a translated value is available, it will be used; otherwise
the english value will be. To see available translation categories,
look under the 'translations' resource section.
>>> mynode.text = ba.Lstr(translate=('gameDescriptions',
... 'Defeat all enemies'))
# EXAMPLE 3: specify a raw value and some substitutions. Substitutions can
# be used with resource and translate modes as well.
mynode.text = ba.Lstr(value='${A} / ${B}',
subs=[('${A}', str(score)), ('${B}', str(total))])
EXAMPLE 3: specify a raw value and some substitutions. Substitutions
can be used with resource and translate modes as well.
>>> mynode.text = ba.Lstr(value='${A} / ${B}',
... subs=[('${A}', str(score)), ('${B}', str(total))])
# EXAMPLE 4: Lstrs can be nested. This example would display the resource
# at res_a but replace ${NAME} with the value of the resource at res_b
mytextnode.text = ba.Lstr(resource='res_a',
subs=[('${NAME}', ba.Lstr(resource='res_b'))])
EXAMPLE 4: ba.Lstr's can be nested. This example would display the
resource at res_a but replace ${NAME} with the value of the
resource at res_b
>>> mytextnode.text = ba.Lstr(
... resource='res_a',
... subs=[('${NAME}', ba.Lstr(resource='res_b'))])
"""
# pylint: disable=dangerous-default-value
@ -406,7 +410,6 @@ class Lstr:
fallback_value: str = '',
subs: Sequence[tuple[str, Union[str, Lstr]]] = []) -> None:
"""Create an Lstr from a string resource."""
...
# noinspection PyShadowingNames,PyDefaultArgument
@overload
@ -415,7 +418,6 @@ class Lstr:
translate: tuple[str, str],
subs: Sequence[tuple[str, Union[str, Lstr]]] = []) -> None:
"""Create an Lstr by translating a string in a category."""
...
# noinspection PyDefaultArgument
@overload
@ -424,7 +426,6 @@ class Lstr:
value: str,
subs: Sequence[tuple[str, Union[str, Lstr]]] = []) -> None:
"""Create an Lstr from a raw string value."""
...
# pylint: enable=redefined-outer-name, dangerous-default-value

View file

@ -17,7 +17,7 @@ if TYPE_CHECKING:
class Level:
"""An entry in a ba.Campaign consisting of a name, game type, and settings.
category: Gameplay Classes
Category: **Gameplay Classes**
"""
def __init__(self,

View file

@ -410,6 +410,7 @@ class Chooser:
self._profileindex = self._profilenames.index(self._profilename)
else:
self._profileindex = 0
# noinspection PyUnresolvedReferences
self._profilename = self._profilenames[self._profileindex]
def update_position(self) -> None:
@ -451,7 +452,8 @@ class Chooser:
clamp = not full
elif name == '__account__':
try:
name = self._sessionplayer.inputdevice.get_account_name(full)
name = self._sessionplayer.inputdevice.get_v1_account_name(
full)
except Exception:
print_exception('Error getting account name for chooser.')
name = 'Invalid'
@ -893,7 +895,7 @@ class Lobby:
self.character_names_local_unlocked.sort(key=lambda x: x.lower())
# Do any overall prep we need to such as creating account profile.
_ba.app.accounts.ensure_have_account_player_profile()
_ba.app.accounts_v1.ensure_have_account_player_profile()
for chooser in self.choosers:
try:
chooser.reload_profiles()

View file

@ -18,7 +18,7 @@ if TYPE_CHECKING:
def preload_map_preview_media() -> None:
"""Preload media needed for map preview UIs.
Category: Asset Functions
Category: **Asset Functions**
"""
_ba.getmodel('level_select_button_opaque')
_ba.getmodel('level_select_button_transparent')
@ -31,7 +31,7 @@ def preload_map_preview_media() -> None:
def get_filtered_map_name(name: str) -> str:
"""Filter a map name to account for name changes, etc.
Category: Asset Functions
Category: **Asset Functions**
This can be used to support old playlists, etc.
"""
@ -46,7 +46,7 @@ def get_filtered_map_name(name: str) -> str:
def get_map_display_string(name: str) -> ba.Lstr:
"""Return a ba.Lstr for displaying a given map\'s name.
Category: Asset Functions
Category: **Asset Functions**
"""
from ba import _language
return _language.Lstr(translate=('mapsNames', name))
@ -55,7 +55,7 @@ def get_map_display_string(name: str) -> ba.Lstr:
def getmaps(playtype: str) -> list[str]:
"""Return a list of ba.Map types supporting a playtype str.
Category: Asset Functions
Category: **Asset Functions**
Maps supporting a given playtype must provide a particular set of
features and lend themselves to a certain style of play.
@ -104,7 +104,7 @@ def getmaps(playtype: str) -> list[str]:
def get_unowned_maps() -> list[str]:
"""Return the list of local maps not owned by the current account.
Category: Asset Functions
Category: **Asset Functions**
"""
from ba import _store
unowned_maps: set[str] = set()
@ -120,7 +120,7 @@ def get_unowned_maps() -> list[str]:
def get_map_class(name: str) -> type[ba.Map]:
"""Return a map type given a name.
Category: Asset Functions
Category: **Asset Functions**
"""
name = get_filtered_map_name(name)
try:
@ -133,7 +133,7 @@ def get_map_class(name: str) -> type[ba.Map]:
class Map(Actor):
"""A game map.
Category: Gameplay Classes
Category: **Gameplay Classes**
Consists of a collection of terrain nodes, metadata, and other
functionality comprising a game map.
@ -189,7 +189,6 @@ class Map(Actor):
def __init__(self,
vr_overlay_offset: Optional[Sequence[float]] = None) -> None:
"""Instantiate a map."""
from ba import _gameutils
super().__init__()
# This is expected to always be a ba.Node object (whether valid or not)
@ -348,7 +347,7 @@ class Map(Actor):
self, players: Sequence[ba.Player]) -> Sequence[float]:
"""Return a random starting position in one of the FFA spawn areas.
If a list of ba.Players is provided; the returned points will be
If a list of ba.Player-s is provided; the returned points will be
as far from these players as possible.
"""

View file

@ -50,45 +50,38 @@ class DeathType(Enum):
class DieMessage:
"""A message telling an object to die.
Category: Message Classes
Most ba.Actors respond to this.
Attributes:
immediate
If this is set to True, the actor should disappear immediately.
This is for 'removing' stuff from the game more so than 'killing'
it. If False, the actor should die a 'normal' death and can take
its time with lingering corpses, sound effects, etc.
how
The particular reason for death.
Category: **Message Classes**
Most ba.Actor-s respond to this.
"""
immediate: bool = False
"""If this is set to True, the actor should disappear immediately.
This is for 'removing' stuff from the game more so than 'killing'
it. If False, the actor should die a 'normal' death and can take
its time with lingering corpses, sound effects, etc."""
how: DeathType = DeathType.GENERIC
"""The particular reason for death."""
# pylint: disable=invalid-name
PlayerType = TypeVar('PlayerType', bound='ba.Player')
# pylint: enable=invalid-name
class PlayerDiedMessage:
"""A message saying a ba.Player has died.
category: Message Classes
Attributes:
killed
If True, the player was killed;
If False, they left the game or the round ended.
how
The particular type of death.
Category: **Message Classes**
"""
killed: bool
"""If True, the player was killed;
If False, they left the game or the round ended."""
how: ba.DeathType
"""The particular type of death."""
def __init__(self, player: ba.Player, was_killed: bool,
killerplayer: Optional[ba.Player], how: ba.DeathType):
@ -132,41 +125,34 @@ class PlayerDiedMessage:
class StandMessage:
"""A message telling an object to move to a position in space.
Category: Message Classes
Category: **Message Classes**
Used when teleporting players to home base, etc.
Attributes:
position
Where to move to.
angle
The angle to face (in degrees)
"""
position: Sequence[float] = (0.0, 0.0, 0.0)
"""Where to move to."""
angle: float = 0.0
"""The angle to face (in degrees)"""
@dataclass
class PickUpMessage:
"""Tells an object that it has picked something up.
Category: Message Classes
Attributes:
node
The ba.Node that is getting picked up.
Category: **Message Classes**
"""
node: ba.Node
"""The ba.Node that is getting picked up."""
@dataclass
class DropMessage:
"""Tells an object that it has dropped what it was holding.
Category: Message Classes
Category: **Message Classes**
"""
@ -174,35 +160,29 @@ class DropMessage:
class PickedUpMessage:
"""Tells an object that it has been picked up by something.
Category: Message Classes
Attributes:
node
The ba.Node doing the picking up.
Category: **Message Classes**
"""
node: ba.Node
"""The ba.Node doing the picking up."""
@dataclass
class DroppedMessage:
"""Tells an object that it has been dropped.
Category: Message Classes
Attributes:
node
The ba.Node doing the dropping.
Category: **Message Classes**
"""
node: ba.Node
"""The ba.Node doing the dropping."""
@dataclass
class ShouldShatterMessage:
"""Tells an object that it should shatter.
Category: Message Classes
Category: **Message Classes**
"""
@ -210,21 +190,18 @@ class ShouldShatterMessage:
class ImpactDamageMessage:
"""Tells an object that it has been jarred violently.
Category: Message Classes
Attributes:
intensity
The intensity of the impact.
Category: **Message Classes**
"""
intensity: float
"""The intensity of the impact."""
@dataclass
class FreezeMessage:
"""Tells an object to become frozen.
Category: Message Classes
Category: **Message Classes**
As seen in the effects of an ice ba.Bomb.
"""
@ -234,7 +211,7 @@ class FreezeMessage:
class ThawMessage:
"""Tells an object to stop being frozen.
Category: Message Classes
Category: **Message Classes**
"""
@ -242,20 +219,17 @@ class ThawMessage:
class CelebrateMessage:
"""Tells an object to celebrate.
Category: Message Classes
Attributes:
duration
Amount of time to celebrate in seconds.
Category: **Message Classes**
"""
duration: float = 10.0
"""Amount of time to celebrate in seconds."""
class HitMessage:
"""Tells an object it has been hit in some way.
Category: Message Classes
Category: **Message Classes**
This is used by punches, explosions, etc to convey
their effect to a target.

View file

@ -37,7 +37,7 @@ class ScanResults:
class MetadataSubsystem:
"""Subsystem for working with script metadata in the app.
Category: App Classes
Category: **App Classes**
Access the single shared instance of this class at 'ba.app.meta'.
"""

View file

@ -22,7 +22,7 @@ DEFAULT_TEAM_NAMES = ('Blue', 'Red')
class MultiTeamSession(Session):
"""Common base class for ba.DualTeamSession and ba.FreeForAllSession.
Category: Gameplay Classes
Category: **Gameplay Classes**
Free-for-all-mode is essentially just teams-mode with each ba.Player having
their own ba.Team, so there is much overlap in functionality.
@ -141,7 +141,7 @@ class MultiTeamSession(Session):
team.customdata['previous_score'] = team.customdata['score'] = 0
def get_max_players(self) -> int:
"""Return max number of ba.Players allowed to join the game at once."""
"""Return max number of ba.Player-s allowed to join the game at once"""
if self.use_teams:
return _ba.app.config.get('Team Game Max Players', 8)
return _ba.app.config.get('Free-for-All Max Players', 8)

View file

@ -18,7 +18,7 @@ if TYPE_CHECKING:
class MusicType(Enum):
"""Types of music available to play in-game.
Category: Enums
Category: **Enums**
These do not correspond to specific pieces of music, but rather to
'situations'. The actual music played for each type can be overridden
@ -51,7 +51,7 @@ class MusicType(Enum):
class MusicPlayMode(Enum):
"""Influences behavior when playing music.
Category: Enums
Category: **Enums**
"""
REGULAR = 'regular'
TEST = 'test'
@ -61,7 +61,7 @@ class MusicPlayMode(Enum):
class AssetSoundtrackEntry:
"""A music entry using an internal asset.
Category: App Classes
Category: **App Classes**
"""
assetname: str
volume: float = 1.0
@ -120,7 +120,7 @@ ASSET_SOUNDTRACK_ENTRIES: dict[MusicType, AssetSoundtrackEntry] = {
class MusicSubsystem:
"""Subsystem for music playback in the app.
Category: App Classes
Category: **App Classes**
Access the single shared instance of this class at 'ba.app.music'.
"""
@ -385,7 +385,7 @@ class MusicSubsystem:
class MusicPlayer:
"""Wrangles soundtrack music playback.
Category: App Classes
Category: **App Classes**
Music can be played either through the game itself
or via a platform-specific external player.
@ -474,7 +474,7 @@ def setmusic(musictype: Optional[ba.MusicType],
continuous: bool = False) -> None:
"""Set the app to play (or stop playing) a certain type of music.
category: Gameplay Functions
category: **Gameplay Functions**
This function will handle loading and playing sound assets as necessary,
and also supports custom user soundtracks on specific platforms so the
@ -485,7 +485,6 @@ def setmusic(musictype: Optional[ba.MusicType],
if 'continuous' is True and musictype is the same as what is already
playing, the playing track will not be restarted.
"""
from ba import _gameutils
# All we do here now is set a few music attrs on the current globals
# node. The foreground globals' current playing music then gets fed to

View file

@ -14,7 +14,6 @@ import _ba
if TYPE_CHECKING:
from typing import Any, Union, Callable, Optional
import socket
import ba
MasterServerCallback = Callable[[Union[None, dict[str, Any]]], None]
# Timeout for standard functions talking to the master-server/etc.
@ -26,9 +25,14 @@ class NetworkSubsystem:
def __init__(self) -> None:
# Anyone accessing/modifying region_pings should hold this lock.
self.region_pings_lock = threading.Lock()
self.region_pings: dict[str, float] = {}
# Anyone accessing/modifying zone_pings should hold this lock,
# as it is updated by a background thread.
self.zone_pings_lock = threading.Lock()
# Region IDs mapped to average pings. This will remain empty
# until enough pings have been run to be reasonably certain
# that a nearby server has been pinged.
self.zone_pings: dict[str, float] = {}
# For debugging.
self.v1_test_log: str = ''
@ -106,7 +110,7 @@ class MasterServerCallThread(threading.Thread):
def run(self) -> None:
# pylint: disable=too-many-branches, consider-using-with
import urllib.request
import urllib.error
import urllib.parse
import json
from efro.error import is_urllib_network_error
@ -114,19 +118,19 @@ class MasterServerCallThread(threading.Thread):
try:
self._data = _general.utf8_all(self._data)
_ba.set_thread_name('BA_ServerCallThread')
parse = urllib.parse
if self._request_type == 'get':
response = urllib.request.urlopen(
urllib.request.Request(
(_ba.get_master_server_address() + '/' +
self._request + '?' + parse.urlencode(self._data)),
None, {'User-Agent': _ba.app.user_agent_string}),
self._request + '?' +
urllib.parse.urlencode(self._data)), None,
{'User-Agent': _ba.app.user_agent_string}),
timeout=DEFAULT_REQUEST_TIMEOUT_SECONDS)
elif self._request_type == 'post':
response = urllib.request.urlopen(
urllib.request.Request(
_ba.get_master_server_address() + '/' + self._request,
parse.urlencode(self._data).encode(),
urllib.parse.urlencode(self._data).encode(),
{'User-Agent': _ba.app.user_agent_string}),
timeout=DEFAULT_REQUEST_TIMEOUT_SECONDS)
else:

View file

@ -17,7 +17,7 @@ if TYPE_CHECKING:
class NodeActor(Actor):
"""A simple ba.Actor type that wraps a single ba.Node.
Category: Gameplay Classes
Category: **Gameplay Classes**
This Actor will delete its Node when told to die, and it's
exists() call will return whether the Node still exists or not.

View file

@ -16,8 +16,10 @@ if TYPE_CHECKING:
from typing import Optional, Sequence, Any, Union, Callable
import ba
# pylint: disable=invalid-name
PlayerType = TypeVar('PlayerType', bound='ba.Player')
TeamType = TypeVar('TeamType', bound='ba.Team')
# pylint: enable=invalid-name
@dataclass
@ -48,18 +50,15 @@ class Player(Generic[TeamType]):
These correspond to ba.SessionPlayer objects, but are associated with a
single ba.Activity instance. This allows activities to specify their
own custom ba.Player types.
Attributes:
actor
The ba.Actor associated with the player.
"""
# These are instance attrs but we define them at the type level so
# their type annotations are introspectable (for docs generation).
character: str
actor: Optional[ba.Actor]
"""The ba.Actor associated with the player."""
color: Sequence[float]
highlight: Sequence[float]
@ -225,8 +224,7 @@ class Player(Generic[TeamType]):
return self._sessionplayer.exists() and not self._expired
def getname(self, full: bool = False, icon: bool = True) -> str:
"""getname(full: bool = False, icon: bool = True) -> str
"""
Returns the player's name. If icon is True, the long version of the
name may include an icon.
"""
@ -235,8 +233,7 @@ class Player(Generic[TeamType]):
return self._sessionplayer.getname(full=full, icon=icon)
def is_alive(self) -> bool:
"""is_alive() -> bool
"""
Returns True if the player has a ba.Actor assigned and its
is_alive() method return True. False is returned otherwise.
"""
@ -245,8 +242,7 @@ class Player(Generic[TeamType]):
return self.actor is not None and self.actor.is_alive()
def get_icon(self) -> dict[str, Any]:
"""get_icon() -> dict[str, Any]
"""
Returns the character's icon (images, colors, etc contained in a dict)
"""
assert self._postinited
@ -256,9 +252,7 @@ class Player(Generic[TeamType]):
def assigninput(self, inputtype: Union[ba.InputType, tuple[ba.InputType,
...]],
call: Callable) -> None:
"""assigninput(type: Union[ba.InputType, Tuple[ba.InputType, ...]],
call: Callable) -> None
"""
Set the python callable to be run for one or more types of input.
"""
assert self._postinited
@ -266,8 +260,7 @@ class Player(Generic[TeamType]):
return self._sessionplayer.assigninput(type=inputtype, call=call)
def resetinput(self) -> None:
"""resetinput() -> None
"""
Clears out the player's assigned input actions.
"""
assert self._postinited

View file

@ -16,9 +16,9 @@ if TYPE_CHECKING:
class PluginSubsystem:
"""Subsystem for plugin handling in the app.
Category: App Classes
Category: **App Classes**
Access the single shared instance of this class at 'ba.app.plugins'.
Access the single shared instance of this class at `ba.app.plugins`.
"""
def __init__(self) -> None:
@ -96,7 +96,7 @@ class PluginSubsystem:
class PotentialPlugin:
"""Represents a ba.Plugin which can potentially be loaded.
Category: App Classes
Category: **App Classes**
These generally represent plugins which were detected by the
meta-tag scan. However they may also represent plugins which
@ -111,7 +111,7 @@ class PotentialPlugin:
class Plugin:
"""A plugin to alter app behavior in some way.
Category: App Classes
Category: **App Classes**
Plugins are discoverable by the meta-tag system
and the user can select which ones they want to activate.

View file

@ -16,31 +16,27 @@ if TYPE_CHECKING:
class PowerupMessage:
"""A message telling an object to accept a powerup.
Category: Message Classes
Category: **Message Classes**
This message is normally received by touching a ba.PowerupBox.
Attributes:
poweruptype
The type of powerup to be granted (a string).
See ba.Powerup.poweruptype for available type values.
sourcenode
The node the powerup game from, or None otherwise.
If a powerup is accepted, a ba.PowerupAcceptMessage should be sent
back to the sourcenode to inform it of the fact. This will generally
cause the powerup box to make a sound and disappear or whatnot.
"""
poweruptype: str
"""The type of powerup to be granted (a string).
See ba.Powerup.poweruptype for available type values."""
sourcenode: Optional[ba.Node] = None
"""The node the powerup game from, or None otherwise.
If a powerup is accepted, a ba.PowerupAcceptMessage should be sent
back to the sourcenode to inform it of the fact. This will generally
cause the powerup box to make a sound and disappear or whatnot."""
@dataclass
class PowerupAcceptMessage:
"""A message informing a ba.Powerup that it was accepted.
Category: Message Classes
Category: **Message Classes**
This is generally sent in response to a ba.PowerupMessage
to inform the box (or whoever granted it) that it can go away.

View file

@ -73,7 +73,7 @@ def get_player_profile_colors(
color = PLAYER_COLORS[random.randrange(6)]
else:
# First 6 are bright-ish.
color = PLAYER_COLORS[sum([ord(c) for c in profilename]) % 6]
color = PLAYER_COLORS[sum(ord(c) for c in profilename) % 6]
try:
assert profilename is not None
@ -86,8 +86,8 @@ def get_player_profile_colors(
highlight = PLAYER_COLORS[random.randrange(
len(PLAYER_COLORS) - 2)]
else:
highlight = PLAYER_COLORS[sum(
[ord(c) + 1
for c in profilename]) % (len(PLAYER_COLORS) - 2)]
highlight = PLAYER_COLORS[sum(ord(c) + 1
for c in profilename) %
(len(PLAYER_COLORS) - 2)]
return color, highlight

View file

@ -16,7 +16,7 @@ if TYPE_CHECKING:
class ScoreType(Enum):
"""Type of scores.
Category: Enums
Category: **Enums**
"""
SECONDS = 's'
MILLISECONDS = 'ms'
@ -27,30 +27,22 @@ class ScoreType(Enum):
class ScoreConfig:
"""Settings for how a game handles scores.
Category: Gameplay Classes
Attributes:
label
A label show to the user for scores; 'Score', 'Time Survived', etc.
scoretype
How the score value should be displayed.
lower_is_better
Whether lower scores are preferable. Higher scores are by default.
none_is_winner
Whether a value of None is considered better than other scores.
By default it is not.
version
To change high-score lists used by a game without renaming the game,
change this. Defaults to an empty string.
Category: **Gameplay Classes**
"""
label: str = 'Score'
"""A label show to the user for scores; 'Score', 'Time Survived', etc."""
scoretype: ba.ScoreType = ScoreType.POINTS
"""How the score value should be displayed."""
lower_is_better: bool = False
"""Whether lower scores are preferable. Higher scores are by default."""
none_is_winner: bool = False
"""Whether a value of None is considered better than other scores.
By default it is not."""
version: str = ''
"""To change high-score lists used by a game without renaming the game,
change this. Defaults to an empty string."""

View file

@ -77,7 +77,7 @@ def _cmd(command_data: bytes) -> None:
class ServerController:
"""Overall controller for the app in server mode.
Category: App Classes
Category: **App Classes**
"""
def __init__(self, config: ServerConfig) -> None:
@ -227,7 +227,7 @@ class ServerController:
def _prepare_to_serve(self) -> None:
"""Run in a timer to do prep before beginning to serve."""
signed_in = _ba.get_account_state() == 'signed_in'
signed_in = _ba.get_v1_account_state() == 'signed_in'
if not signed_in:
# Signing in to the local server account should not take long;
@ -302,7 +302,7 @@ class ServerController:
appcfg = app.config
sessiontype = self._get_session_type()
if _ba.get_account_state() != 'signed_in':
if _ba.get_v1_account_state() != 'signed_in':
print('WARNING: launch_server_session() expects to run '
'with a signed in server account')

View file

@ -17,9 +17,9 @@ if TYPE_CHECKING:
class Session:
"""Defines a high level series of ba.Activities with a common purpose.
"""Defines a high level series of ba.Activity-es with a common purpose.
category: Gameplay Classes
Category: **Gameplay Classes**
Examples of sessions are ba.FreeForAllSession, ba.DualTeamSession, and
ba.CoopSession.
@ -27,58 +27,48 @@ class Session:
A Session is responsible for wrangling and transitioning between various
ba.Activity instances such as mini-games and score-screens, and for
maintaining state between them (players, teams, score tallies, etc).
Attributes:
sessionteams
All the ba.SessionTeams in the Session. Most things should use the
list of ba.Teams in ba.Activity; not this.
sessionplayers
All ba.SessionPlayers in the Session. Most things should use the
list of ba.Players in ba.Activity; not this. Some players, such as
those who have not yet selected a character, will only be
found on this list.
min_players
The minimum number of players who must be present for the Session
to proceed past the initial joining screen.
max_players
The maximum number of players allowed in the Session.
lobby
The ba.Lobby instance where new ba.Players go to select a
Profile/Team/etc. before being added to games.
Be aware this value may be None if a Session does not allow
any such selection.
use_teams
Whether this session groups players into an explicit set of
teams. If this is off, a unique team is generated for each
player that joins.
use_team_colors
Whether players on a team should all adopt the colors of that
team instead of their own profile colors. This only applies if
use_teams is enabled.
customdata
A shared dictionary for objects to use as storage on this session.
Ensure that keys here are unique to avoid collisions.
"""
use_teams: bool = False
use_team_colors: bool = True
# Note: even though these are instance vars, we annotate them at the
# class level so that docs generation can access their types.
use_teams: bool = False
"""Whether this session groups players into an explicit set of
teams. If this is off, a unique team is generated for each
player that joins."""
use_team_colors: bool = True
"""Whether players on a team should all adopt the colors of that
team instead of their own profile colors. This only applies if
use_teams is enabled."""
# Note: even though these are instance vars, we annotate and document them
# at the class level so that looks better and nobody get lost while
# reading large __init__
lobby: ba.Lobby
"""The ba.Lobby instance where new ba.Player-s go to select a
Profile/Team/etc. before being added to games.
Be aware this value may be None if a Session does not allow
any such selection."""
max_players: int
"""The maximum number of players allowed in the Session."""
min_players: int
"""The minimum number of players who must be present for the Session
to proceed past the initial joining screen"""
sessionplayers: list[ba.SessionPlayer]
"""All ba.SessionPlayers in the Session. Most things should use the
list of ba.Player-s in ba.Activity; not this. Some players, such as
those who have not yet selected a character, will only be
found on this list."""
customdata: dict
"""A shared dictionary for objects to use as storage on this session.
Ensure that keys here are unique to avoid collisions."""
sessionteams: list[ba.SessionTeam]
"""All the ba.SessionTeams in the Session. Most things should use the
list of ba.Team-s in ba.Activity; not this."""
def __init__(self,
depsets: Sequence[ba.DependencySet],

View file

@ -21,20 +21,17 @@ if TYPE_CHECKING:
class PlayerScoredMessage:
"""Informs something that a ba.Player scored.
Category: Message Classes
Attributes:
score
The score value.
Category: **Message Classes**
"""
score: int
"""The score value."""
class PlayerRecord:
"""Stats for an individual player in a ba.Stats object.
Category: Gameplay Classes
Category: **Gameplay Classes**
This does not necessarily correspond to a ba.Player that is
still present (stats may be retained for players that leave
@ -232,7 +229,7 @@ class PlayerRecord:
class Stats:
"""Manages scores and statistics for a ba.Session.
category: Gameplay Classes
Category: **Gameplay Classes**
"""
def __init__(self) -> None:

View file

@ -366,11 +366,11 @@ def get_store_layout() -> dict[str, list[dict[str, Any]]]:
'games.ninja_fight', 'games.meteor_shower', 'games.target_practice'
]
}]
if _ba.get_account_misc_read_val('xmas', False):
if _ba.get_v1_account_misc_read_val('xmas', False):
store_layout['characters'][0]['items'].append('characters.santa')
store_layout['characters'][0]['items'].append('characters.wizard')
store_layout['characters'][0]['items'].append('characters.cyborg')
if _ba.get_account_misc_read_val('easter', False):
if _ba.get_v1_account_misc_read_val('easter', False):
store_layout['characters'].append({
'title': 'store.holidaySpecialText',
'items': ['characters.bunny']
@ -401,10 +401,10 @@ def get_clean_price(price_string: str) -> str:
def get_available_purchase_count(tab: str = None) -> int:
"""(internal)"""
try:
if _ba.get_account_state() != 'signed_in':
if _ba.get_v1_account_state() != 'signed_in':
return 0
count = 0
our_tickets = _ba.get_account_ticket_count()
our_tickets = _ba.get_v1_account_ticket_count()
store_data = get_store_layout()
if tab is not None:
tabs = [(tab, store_data[tab])]
@ -425,7 +425,8 @@ def _calc_count_for_tab(tabval: list[dict[str, Any]], our_tickets: int,
count: int) -> int:
for section in tabval:
for item in section['items']:
ticket_cost = _ba.get_account_misc_read_val('price.' + item, None)
ticket_cost = _ba.get_v1_account_misc_read_val(
'price.' + item, None)
if ticket_cost is not None:
if (our_tickets >= ticket_cost
and not _ba.get_purchased(item)):
@ -447,7 +448,7 @@ def get_available_sale_time(tab: str) -> Optional[int]:
# Calc time for our pro sale (old special case).
if tab == 'extras':
config = app.config
if app.accounts.have_pro():
if app.accounts_v1.have_pro():
return None
# If we haven't calced/loaded start times yet.
@ -462,7 +463,7 @@ def get_available_sale_time(tab: str) -> Optional[int]:
# We start the timer once we get the duration from
# the server.
start_duration = _ba.get_account_misc_read_val(
start_duration = _ba.get_v1_account_misc_read_val(
'proSaleDurationMinutes', None)
if start_duration is not None:
app.pro_sale_start_time = int(
@ -488,7 +489,7 @@ def get_available_sale_time(tab: str) -> Optional[int]:
sale_times.append(val)
# Now look for sales in this tab.
sales_raw = _ba.get_account_misc_read_val('sales', {})
sales_raw = _ba.get_v1_account_misc_read_val('sales', {})
store_layout = get_store_layout()
for section in store_layout[tab]:
for item in section['items']:

View file

@ -17,39 +17,32 @@ if TYPE_CHECKING:
class SessionTeam:
"""A team of one or more ba.SessionPlayers.
Category: Gameplay Classes
Category: **Gameplay Classes**
Note that a SessionPlayer *always* has a SessionTeam;
in some cases, such as free-for-all ba.Sessions,
each SessionTeam consists of just one SessionPlayer.
Attributes:
name
The team's name.
id
The unique numeric id of the team.
color
The team's color.
players
The list of ba.SessionPlayers on the team.
customdata
A dict for use by the current ba.Session for
storing data associated with this team.
Unlike customdata, this persists for the duration
of the session.
"""
# Annotate our attr types at the class level so they're introspectable.
name: Union[ba.Lstr, str]
"""The team's name."""
color: tuple[float, ...] # FIXME: can't we make this fixed len?
"""The team's color."""
players: list[ba.SessionPlayer]
"""The list of ba.SessionPlayer-s on the team."""
customdata: dict
"""A dict for use by the current ba.Session for
storing data associated with this team.
Unlike customdata, this persists for the duration
of the session."""
id: int
"""The unique numeric id of the team."""
def __init__(self,
team_id: int = 0,
@ -73,13 +66,15 @@ class SessionTeam:
self.customdata = {}
# pylint: disable=invalid-name
PlayerType = TypeVar('PlayerType', bound='ba.Player')
# pylint: enable=invalid-name
class Team(Generic[PlayerType]):
"""A team in a specific ba.Activity.
Category: Gameplay Classes
Category: **Gameplay Classes**
These correspond to ba.SessionTeam objects, but are created per activity
so that the activity can use its own custom team subclass.
@ -197,7 +192,7 @@ class Team(Generic[PlayerType]):
class EmptyTeam(Team['ba.EmptyPlayer']):
"""An empty player for use by Activities that don't need to define one.
Category: Gameplay Classes
Category: **Gameplay Classes**
ba.Player and ba.Team are 'Generic' types, and so passing those top level
classes as type arguments when defining a ba.Activity reduces type safety.

View file

@ -17,14 +17,16 @@ if TYPE_CHECKING:
from bastd.actor.playerspaz import PlayerSpaz
import ba
# pylint: disable=invalid-name
PlayerType = TypeVar('PlayerType', bound='ba.Player')
TeamType = TypeVar('TeamType', bound='ba.Team')
# pylint: enable=invalid-name
class TeamGameActivity(GameActivity[PlayerType, TeamType]):
"""Base class for teams and free-for-all mode games.
Category: Gameplay Classes
Category: **Gameplay Classes**
(Free-for-all is essentially just a special case where every
ba.Player has their own ba.Team)
@ -120,7 +122,7 @@ class TeamGameActivity(GameActivity[PlayerType, TeamType]):
unless 'announce_winning_team' is False.
(for results without a single most-important winner).
"""
# pylint: disable=arguments-differ
# pylint: disable=arguments-renamed
from ba._coopsession import CoopSession
from ba._multiteamsession import MultiTeamSession
from ba._general import Call

View file

@ -18,7 +18,7 @@ if TYPE_CHECKING:
class UISubsystem:
"""Consolidated UI functionality for the app.
Category: App Classes
Category: **App Classes**
To use this class, access the single instance of it at 'ba.app.ui'.
"""

93
dist/ba_data/python/ba/cloud.py vendored Normal file
View file

@ -0,0 +1,93 @@
# Released under the MIT License. See LICENSE for details.
#
"""Functionality related to the cloud."""
from __future__ import annotations
from typing import TYPE_CHECKING, overload
import _ba
if TYPE_CHECKING:
from typing import Union, Callable, Any
from efro.message import Message
import bacommon.cloud
# TODO: Should make it possible to define a protocol in bacommon.cloud and
# autogenerate this. That would give us type safety between this and
# internal protocols.
class CloudSubsystem:
"""Used for communicating with the cloud."""
def is_connected(self) -> bool:
"""Return whether a connection to the cloud is present.
This is a good indicator (though not for certain) that sending
messages will succeed.
"""
return False # Needs to be overridden
@overload
def send_message(
self,
msg: bacommon.cloud.LoginProxyRequestMessage,
on_response: Callable[
[Union[bacommon.cloud.LoginProxyRequestResponse,
Exception]], None],
) -> None:
...
@overload
def send_message(
self,
msg: bacommon.cloud.LoginProxyStateQueryMessage,
on_response: Callable[
[Union[bacommon.cloud.LoginProxyStateQueryResponse,
Exception]], None],
) -> None:
...
@overload
def send_message(
self,
msg: bacommon.cloud.LoginProxyCompleteMessage,
on_response: Callable[[Union[None, Exception]], None],
) -> None:
...
@overload
def send_message(
self,
msg: bacommon.cloud.CredentialsCheckMessage,
on_response: Callable[
[Union[bacommon.cloud.CredentialsCheckResponse, Exception]], None],
) -> None:
...
@overload
def send_message(
self,
msg: bacommon.cloud.AccountSessionReleaseMessage,
on_response: Callable[[Union[None, Exception]], None],
) -> None:
...
def send_message(
self,
msg: Message,
on_response: Callable[[Any], None],
) -> None:
"""Asynchronously send a message to the cloud from the game thread.
The provided on_response call will be run in the logic thread
and passed either the response or the error that occurred.
"""
from ba._general import Call
del msg # Unused.
_ba.pushcall(
Call(on_response,
RuntimeError('Cloud functionality is not available.')))

View file

@ -7,8 +7,6 @@ or disappear without warning, so should be avoided (or used sparingly and
defensively) in mods.
"""
# pylint: disable=unused-import
from ba._map import (get_unowned_maps, get_map_class, register_map,
preload_map_preview_media, get_map_display_string,
get_filtered_map_name)
@ -39,3 +37,24 @@ from ba._store import (get_available_sale_time, get_available_purchase_count,
get_store_item, get_clean_price)
from ba._tournament import get_tournament_prize_strings
from ba._gameutils import get_trophy_string
__all__ = [
'get_unowned_maps', 'get_map_class', 'register_map',
'preload_map_preview_media', 'get_map_display_string',
'get_filtered_map_name', 'commit_app_config', 'get_device_value',
'get_input_map_hash', 'get_input_device_config', 'getclass', 'json_prep',
'get_type_name', 'JoinActivity', 'ScoreScreenActivity',
'is_browser_likely_available', 'get_remote_app_name',
'should_submit_debug_info', 'run_gpu_benchmark', 'run_cpu_benchmark',
'run_media_reload_benchmark', 'run_stress_test', 'getcampaign',
'PlayerProfilesChangedMessage', 'DEFAULT_TEAM_COLORS',
'DEFAULT_TEAM_NAMES', 'do_play_music', 'master_server_get',
'master_server_post', 'get_ip_address_type',
'DEFAULT_REQUEST_TIMEOUT_SECONDS', 'get_default_powerup_distribution',
'get_player_profile_colors', 'get_player_profile_icon',
'get_player_colors', 'get_next_tip', 'get_default_free_for_all_playlist',
'get_default_teams_playlist', 'filter_playlist', 'get_available_sale_time',
'get_available_purchase_count', 'get_store_item_name_translated',
'get_store_item_display_size', 'get_store_layout', 'get_store_item',
'get_clean_price', 'get_tournament_prize_strings', 'get_trophy_string'
]

71
dist/ba_data/python/bacommon/bacloud.py vendored Normal file
View file

@ -0,0 +1,71 @@
# Released under the MIT License. See LICENSE for details.
#
"""Functionality related to the bacloud tool."""
from __future__ import annotations
from dataclasses import dataclass
from typing import TYPE_CHECKING, Optional
from efro.dataclassio import ioprepped
if TYPE_CHECKING:
pass
@ioprepped
@dataclass
class Response:
# noinspection PyUnresolvedReferences
"""Response sent from the bacloud server to the client.
Attributes:
message: If present, client should print this message before any other
response processing (including error handling) occurs.
message_end: end arg for message print() call.
error: If present, client should abort with this error message.
delay_seconds: How long to wait before proceeding with remaining
response (can be useful when waiting for server progress in a loop).
login: If present, a token that should be stored client-side and passed
with subsequent commands.
logout: If True, any existing client-side token should be discarded.
dir_manifest: If present, client should generate a manifest of this dir.
It should be added to end_command args as 'manifest'.
uploads: If present, client should upload the requested files (arg1)
individually to a server command (arg2) with provided args (arg3).
uploads_inline: If present, a list of pathnames that should be base64
gzipped and uploaded to an 'uploads_inline' dict in end_command args.
This should be limited to relatively small files.
downloads_inline: If present, pathnames mapped to base64 gzipped data to
be written to the client. This should only be used for relatively
small files as they are all included inline as part of the response.
deletes: If present, file paths that should be deleted on the client.
dir_prune_empty: If present, all empty dirs under this one should be
removed.
open_url: If present, url to display to the user.
input_prompt: If present, a line of input is read and placed into
end_command args as 'input'. The first value is the prompt printed
before reading and the second is whether it should be read as a
password (without echoing to the terminal).
end_message: If present, a message that should be printed after all other
response processing is done.
end_message_end: end arg for end_message print() call.
end_command: If present, this command is run with these args at the end
of response processing.
"""
message: Optional[str] = None
message_end: str = '\n'
error: Optional[str] = None
delay_seconds: float = 0.0
login: Optional[str] = None
logout: bool = False
dir_manifest: Optional[str] = None
uploads: Optional[tuple[list[str], str, dict]] = None
uploads_inline: Optional[list[str]] = None
downloads_inline: Optional[dict[str, str]] = None
deletes: Optional[list[str]] = None
dir_prune_empty: Optional[str] = None
open_url: Optional[str] = None
input_prompt: Optional[tuple[str, bool]] = None
end_message: Optional[str] = None
end_message_end: str = '\n'
end_command: Optional[tuple[str, dict]] = None

103
dist/ba_data/python/bacommon/cloud.py vendored Normal file
View file

@ -0,0 +1,103 @@
# Released under the MIT License. See LICENSE for details.
#
"""Functionality related to cloud functionality."""
from __future__ import annotations
from dataclasses import dataclass
from typing import TYPE_CHECKING, Annotated, Optional
from enum import Enum
from efro.message import Message, Response
from efro.dataclassio import ioprepped, IOAttrs
if TYPE_CHECKING:
pass
@ioprepped
@dataclass
class LoginProxyRequestMessage(Message):
"""Request send to the cloud to ask for a login-proxy."""
@classmethod
def get_response_types(cls) -> list[type[Response]]:
return [LoginProxyRequestResponse]
@ioprepped
@dataclass
class LoginProxyRequestResponse(Response):
"""Response to a request for a login proxy."""
# URL to direct the user to for login.
url: Annotated[str, IOAttrs('u')]
# Proxy-Login id for querying results.
proxyid: Annotated[str, IOAttrs('p')]
# Proxy-Login key for querying results.
proxykey: Annotated[str, IOAttrs('k')]
@ioprepped
@dataclass
class LoginProxyStateQueryMessage(Message):
"""Soo.. how is that login proxy going?"""
proxyid: Annotated[str, IOAttrs('p')]
proxykey: Annotated[str, IOAttrs('k')]
@classmethod
def get_response_types(cls) -> list[type[Response]]:
return [LoginProxyStateQueryResponse]
@ioprepped
@dataclass
class LoginProxyStateQueryResponse(Response):
"""Here's the info on that login-proxy you asked about, boss."""
class State(Enum):
"""States a login-proxy can be in."""
WAITING = 'waiting'
SUCCESS = 'success'
FAIL = 'fail'
state: Annotated[State, IOAttrs('s')]
# On success, these will be filled out.
credentials: Annotated[Optional[str], IOAttrs('tk')]
@ioprepped
@dataclass
class LoginProxyCompleteMessage(Message):
"""Just so you know, we're done with this proxy."""
proxyid: Annotated[str, IOAttrs('p')]
@ioprepped
@dataclass
class AccountSessionReleaseMessage(Message):
"""We're done using this particular session."""
token: Annotated[str, IOAttrs('tk')]
@ioprepped
@dataclass
class CredentialsCheckMessage(Message):
"""Are our current credentials valid?"""
@classmethod
def get_response_types(cls) -> list[type[Response]]:
return [CredentialsCheckResponse]
@ioprepped
@dataclass
class CredentialsCheckResponse(Response):
"""Info returned when checking credentials."""
verified: Annotated[bool, IOAttrs('v')]
# Current account tag (good time to check if it has changed).
tag: Annotated[str, IOAttrs('t')]

View file

@ -17,7 +17,7 @@ if TYPE_CHECKING:
@dataclass
class ServerNodeEntry:
"""Information about a specific server."""
region: Annotated[str, IOAttrs('r')]
zone: Annotated[str, IOAttrs('r')]
address: Annotated[str, IOAttrs('a')]
port: Annotated[int, IOAttrs('p')]

View file

@ -52,8 +52,9 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]):
ba.app.ach.achievements_for_coop_level(self._campaign.name + ':' +
settings['level']))
self._account_type = (_ba.get_account_type() if
_ba.get_account_state() == 'signed_in' else None)
self._account_type = (_ba.get_v1_account_type()
if _ba.get_v1_account_state() == 'signed_in' else
None)
self._game_service_icon_color: Optional[Sequence[float]]
self._game_service_achievements_texture: Optional[ba.Texture]
@ -631,7 +632,7 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]):
if ba.app.server is None:
# If we're running in normal non-headless build, show this text
# because only host can continue the game.
adisp = _ba.get_account_display_string()
adisp = _ba.get_v1_account_display_string()
txt = Text(ba.Lstr(resource='waitingForHostText',
subs=[('${HOST}', adisp)]),
maxwidth=300,
@ -732,7 +733,7 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]):
'scoreVersion': sver,
'scores': our_high_scores_all
})
if _ba.get_account_state() != 'signed_in':
if _ba.get_v1_account_state() != 'signed_in':
# We expect this only in kiosk mode; complain otherwise.
if not (ba.app.demo_mode or ba.app.arcade_mode):
print('got not-signed-in at score-submit; unexpected')
@ -841,6 +842,7 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]):
if display_scores[i][1] is None:
name_str = '-'
else:
# noinspection PyUnresolvedReferences
name_str = ', '.join([
p['name'] for p in display_scores[i][1]['players']
])
@ -1259,8 +1261,8 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]):
try:
tournament_id = self.session.tournament_id
if tournament_id is not None:
if tournament_id in ba.app.accounts.tournament_info:
tourney_info = ba.app.accounts.tournament_info[
if tournament_id in ba.app.accounts_v1.tournament_info:
tourney_info = ba.app.accounts_v1.tournament_info[
tournament_id]
# pylint: disable=unbalanced-tuple-unpacking
pr1, pv1, pr2, pv2, pr3, pv3 = (

View file

@ -94,6 +94,7 @@ class MultiTeamScoreScreenActivity(ScoreScreenActivity):
assert self.stats
valid_players = list(self.stats.get_records().items())
# noinspection PyUnresolvedReferences
def _get_player_score_set_entry(
player: ba.SessionPlayer) -> Optional[ba.PlayerRecord]:
for p_rec in valid_players:

View file

@ -16,118 +16,118 @@ from bastd.gameutils import SharedObjects
if TYPE_CHECKING:
from typing import Any, Sequence, Optional, Callable
# pylint: disable=invalid-name
PlayerType = TypeVar('PlayerType', bound='ba.Player')
# pylint: enable=invalid-name
class BombFactory:
"""Wraps up media and other resources used by ba.Bombs.
category: Gameplay Classes
Category: **Gameplay Classes**
A single instance of this is shared between all bombs
and can be retrieved via bastd.actor.bomb.get_factory().
Attributes:
bomb_model
The ba.Model of a standard or ice bomb.
sticky_bomb_model
The ba.Model of a sticky-bomb.
impact_bomb_model
The ba.Model of an impact-bomb.
land_mine_model
The ba.Model of a land-mine.
tnt_model
The ba.Model of a tnt box.
regular_tex
The ba.Texture for regular bombs.
ice_tex
The ba.Texture for ice bombs.
sticky_tex
The ba.Texture for sticky bombs.
impact_tex
The ba.Texture for impact bombs.
impact_lit_tex
The ba.Texture for impact bombs with lights lit.
land_mine_tex
The ba.Texture for land-mines.
land_mine_lit_tex
The ba.Texture for land-mines with the light lit.
tnt_tex
The ba.Texture for tnt boxes.
hiss_sound
The ba.Sound for the hiss sound an ice bomb makes.
debris_fall_sound
The ba.Sound for random falling debris after an explosion.
wood_debris_fall_sound
A ba.Sound for random wood debris falling after an explosion.
explode_sounds
A tuple of ba.Sounds for explosions.
freeze_sound
A ba.Sound of an ice bomb freezing something.
fuse_sound
A ba.Sound of a burning fuse.
activate_sound
A ba.Sound for an activating impact bomb.
warn_sound
A ba.Sound for an impact bomb about to explode due to time-out.
bomb_material
A ba.Material applied to all bombs.
normal_sound_material
A ba.Material that generates standard bomb noises on impacts, etc.
sticky_material
A ba.Material that makes 'splat' sounds and makes collisions softer.
land_mine_no_explode_material
A ba.Material that keeps land-mines from blowing up.
Applied to land-mines when they are created to allow land-mines to
touch without exploding.
land_mine_blast_material
A ba.Material applied to activated land-mines that causes them to
explode on impact.
impact_blast_material
A ba.Material applied to activated impact-bombs that causes them to
explode on impact.
blast_material
A ba.Material applied to bomb blast geometry which triggers impact
events with what it touches.
dink_sounds
A tuple of ba.Sounds for when bombs hit the ground.
sticky_impact_sound
The ba.Sound for a squish made by a sticky bomb hitting something.
roll_sound
ba.Sound for a rolling bomb.
"""
bomb_model: ba.Model
"""The ba.Model of a standard or ice bomb."""
sticky_bomb_model: ba.Model
"""The ba.Model of a sticky-bomb."""
impact_bomb_model: ba.Model
"""The ba.Model of an impact-bomb."""
land_mine_model: ba.Model
"""The ba.Model of a land-mine."""
tnt_model: ba.Model
"""The ba.Model of a tnt box."""
regular_tex: ba.Texture
"""The ba.Texture for regular bombs."""
ice_tex: ba.Texture
"""The ba.Texture for ice bombs."""
sticky_tex: ba.Texture
"""The ba.Texture for sticky bombs."""
impact_tex: ba.Texture
"""The ba.Texture for impact bombs."""
impact_lit_tex: ba.Texture
"""The ba.Texture for impact bombs with lights lit."""
land_mine_tex: ba.Texture
"""The ba.Texture for land-mines."""
land_mine_lit_tex: ba.Texture
"""The ba.Texture for land-mines with the light lit."""
tnt_tex: ba.Texture
"""The ba.Texture for tnt boxes."""
hiss_sound: ba.Sound
"""The ba.Sound for the hiss sound an ice bomb makes."""
debris_fall_sound: ba.Sound
"""The ba.Sound for random falling debris after an explosion."""
wood_debris_fall_sound: ba.Sound
"""A ba.Sound for random wood debris falling after an explosion."""
explode_sounds: Sequence[ba.Sound]
"""A tuple of ba.Sound-s for explosions."""
freeze_sound: ba.Sound
"""A ba.Sound of an ice bomb freezing something."""
fuse_sound: ba.Sound
"""A ba.Sound of a burning fuse."""
activate_sound: ba.Sound
"""A ba.Sound for an activating impact bomb."""
warn_sound: ba.Sound
"""A ba.Sound for an impact bomb about to explode due to time-out."""
bomb_material: ba.Material
"""A ba.Material applied to all bombs."""
normal_sound_material: ba.Material
"""A ba.Material that generates standard bomb noises on impacts, etc."""
sticky_material: ba.Material
"""A ba.Material that makes 'splat' sounds and makes collisions softer."""
land_mine_no_explode_material: ba.Material
"""A ba.Material that keeps land-mines from blowing up.
Applied to land-mines when they are created to allow land-mines to
touch without exploding."""
land_mine_blast_material: ba.Material
"""A ba.Material applied to activated land-mines that causes them to
explode on impact."""
impact_blast_material: ba.Material
"""A ba.Material applied to activated impact-bombs that causes them to
explode on impact."""
blast_material: ba.Material
"""A ba.Material applied to bomb blast geometry which triggers impact
events with what it touches."""
dink_sounds: Sequence[ba.Sound]
"""A tuple of ba.Sound-s for when bombs hit the ground."""
sticky_impact_sound: ba.Sound
"""The ba.Sound for a squish made by a sticky bomb hitting something."""
roll_sound: ba.Sound
"""ba.Sound for a rolling bomb."""
_STORENAME = ba.storagename()
@classmethod

View file

@ -15,38 +15,36 @@ if TYPE_CHECKING:
class FlagFactory:
"""Wraps up media and other resources used by ba.Flags.
"""Wraps up media and other resources used by `Flag`s.
category: Gameplay Classes
Category: **Gameplay Classes**
A single instance of this is shared between all flags
and can be retrieved via bastd.actor.flag.get_factory().
Attributes:
flagmaterial
The ba.Material applied to all ba.Flags.
impact_sound
The ba.Sound used when a ba.Flag hits the ground.
skid_sound
The ba.Sound used when a ba.Flag skids along the ground.
no_hit_material
A ba.Material that prevents contact with most objects;
applied to 'non-touchable' flags.
flag_texture
The ba.Texture for flags.
and can be retrieved via FlagFactory.get().
"""
flagmaterial: ba.Material
"""The ba.Material applied to all `Flag`s."""
impact_sound: ba.Sound
"""The ba.Sound used when a `Flag` hits the ground."""
skid_sound: ba.Sound
"""The ba.Sound used when a `Flag` skids along the ground."""
no_hit_material: ba.Material
"""A ba.Material that prevents contact with most objects;
applied to 'non-touchable' flags."""
flag_texture: ba.Texture
"""The ba.Texture for flags."""
_STORENAME = ba.storagename()
def __init__(self) -> None:
"""Instantiate a FlagFactory.
"""Instantiate a `FlagFactory`.
You shouldn't need to do this; call bastd.actor.flag.get_factory() to
You shouldn't need to do this; call FlagFactory.get() to
get a shared instance.
"""
shared = SharedObjects.get()
@ -109,7 +107,7 @@ class FlagFactory:
@classmethod
def get(cls) -> FlagFactory:
"""Get/create a shared FlagFactory instance."""
"""Get/create a shared `FlagFactory` instance."""
activity = ba.getactivity()
factory = activity.customdata.get(cls._STORENAME)
if factory is None:
@ -121,58 +119,47 @@ class FlagFactory:
@dataclass
class FlagPickedUpMessage:
"""A message saying a ba.Flag has been picked up.
"""A message saying a `Flag` has been picked up.
category: Message Classes
Category: **Message Classes**
"""
Attributes:
flag
The ba.Flag that has been picked up.
node
The ba.Node doing the picking up.
"""
flag: Flag
"""The `Flag` that has been picked up."""
node: ba.Node
"""The ba.Node doing the picking up."""
@dataclass
class FlagDiedMessage:
"""A message saying a ba.Flag has died.
"""A message saying a `Flag` has died.
category: Message Classes
Attributes:
flag
The ba.Flag that died.
Category: **Message Classes**
"""
flag: Flag
"""The `Flag` that died."""
@dataclass
class FlagDroppedMessage:
"""A message saying a ba.Flag has been dropped.
"""A message saying a `Flag` has been dropped.
category: Message Classes
Attributes:
flag
The ba.Flag that was dropped.
node
The ba.Node that was holding it.
Category: **Message Classes**
"""
flag: Flag
"""The `Flag` that was dropped."""
node: ba.Node
"""The ba.Node that was holding it."""
class Flag(ba.Actor):
"""A flag; used in games such as capture-the-flag or king-of-the-hill.
category: Gameplay Classes
Category: **Gameplay Classes**
Can be stationary or carry-able by players.
"""
@ -189,7 +176,7 @@ class Flag(ba.Actor):
useful for things like king-of-the-hill where players should
not be moving the flag around.
'materials can be a list of extra ba.Materials to apply to the flag.
'materials can be a list of extra `ba.Material`s to apply to the flag.
If 'dropped_timeout' is provided (in seconds), the flag will die
after remaining untouched for that long once it has been moved

View file

@ -12,36 +12,36 @@ from spazmod import modifyspaz
if TYPE_CHECKING:
from typing import Any, Sequence, Optional, Literal
# pylint: disable=invalid-name
PlayerType = TypeVar('PlayerType', bound=ba.Player)
TeamType = TypeVar('TeamType', bound=ba.Team)
# pylint: enable=invalid-name
class PlayerSpazHurtMessage:
"""A message saying a ba.PlayerSpaz was hurt.
"""A message saying a PlayerSpaz was hurt.
category: Message Classes
Attributes:
spaz
The ba.PlayerSpaz that was hurt
Category: **Message Classes**
"""
spaz: PlayerSpaz
"""The PlayerSpaz that was hurt"""
def __init__(self, spaz: PlayerSpaz):
"""Instantiate with the given ba.Spaz value."""
self.spaz = spaz
class PlayerSpaz(Spaz):
"""A ba.Spaz subclass meant to be controlled by a ba.Player.
"""A Spaz subclass meant to be controlled by a ba.Player.
category: Gameplay Classes
Category: **Gameplay Classes**
When a PlayerSpaz dies, it delivers a ba.PlayerDiedMessage
to the current ba.Activity. (unless the death was the result of the
player leaving the game, in which case no message is sent)
When a PlayerSpaz is hurt, it delivers a ba.PlayerSpazHurtMessage
When a PlayerSpaz is hurt, it delivers a PlayerSpazHurtMessage
to the current ba.Activity.
"""
@ -72,8 +72,7 @@ class PlayerSpaz(Spaz):
self._player = player
self._drive_player_position()
import custom_hooks
custom_hooks.playerspaz_init(self, self.node, self._player)
modifyspaz.main(self, self.node, self._player)
# Overloads to tell the type system our return type based on doraise val.

View file

@ -23,69 +23,67 @@ class _TouchedMessage:
class PowerupBoxFactory:
"""A collection of media and other resources used by ba.Powerups.
category: Gameplay Classes
Category: **Gameplay Classes**
A single instance of this is shared between all powerups
and can be retrieved via ba.Powerup.get_factory().
Attributes:
model
The ba.Model of the powerup box.
model_simple
A simpler ba.Model of the powerup box, for use in shadows, etc.
tex_bomb
Triple-bomb powerup ba.Texture.
tex_punch
Punch powerup ba.Texture.
tex_ice_bombs
Ice bomb powerup ba.Texture.
tex_sticky_bombs
Sticky bomb powerup ba.Texture.
tex_shield
Shield powerup ba.Texture.
tex_impact_bombs
Impact-bomb powerup ba.Texture.
tex_health
Health powerup ba.Texture.
tex_land_mines
Land-mine powerup ba.Texture.
tex_curse
Curse powerup ba.Texture.
health_powerup_sound
ba.Sound played when a health powerup is accepted.
powerup_sound
ba.Sound played when a powerup is accepted.
powerdown_sound
ba.Sound that can be used when powerups wear off.
powerup_material
ba.Material applied to powerup boxes.
powerup_accept_material
Powerups will send a ba.PowerupMessage to anything they touch
that has this ba.Material applied.
"""
model: ba.Model
"""The ba.Model of the powerup box."""
model_simple: ba.Model
"""A simpler ba.Model of the powerup box, for use in shadows, etc."""
tex_bomb: ba.Texture
"""Triple-bomb powerup ba.Texture."""
tex_punch: ba.Texture
"""Punch powerup ba.Texture."""
tex_ice_bombs: ba.Texture
"""Ice bomb powerup ba.Texture."""
tex_sticky_bombs: ba.Texture
"""Sticky bomb powerup ba.Texture."""
tex_shield: ba.Texture
"""Shield powerup ba.Texture."""
tex_impact_bombs: ba.Texture
"""Impact-bomb powerup ba.Texture."""
tex_health: ba.Texture
"""Health powerup ba.Texture."""
tex_land_mines: ba.Texture
"""Land-mine powerup ba.Texture."""
tex_curse: ba.Texture
"""Curse powerup ba.Texture."""
health_powerup_sound: ba.Sound
"""ba.Sound played when a health powerup is accepted."""
powerup_sound: ba.Sound
"""ba.Sound played when a powerup is accepted."""
powerdown_sound: ba.Sound
"""ba.Sound that can be used when powerups wear off."""
powerup_material: ba.Material
"""ba.Material applied to powerup boxes."""
powerup_accept_material: ba.Material
"""Powerups will send a ba.PowerupMessage to anything they touch
that has this ba.Material applied."""
_STORENAME = ba.storagename()
def __init__(self) -> None:
"""Instantiate a PowerupBoxFactory.
You shouldn't need to do this; call ba.Powerup.get_factory()
You shouldn't need to do this; call Powerup.get_factory()
to get a shared instance.
"""
from ba.internal import get_default_powerup_distribution
@ -191,18 +189,16 @@ class PowerupBox(ba.Actor):
This will deliver a ba.PowerupMessage to anything that touches it
which has the ba.PowerupBoxFactory.powerup_accept_material applied.
Attributes:
poweruptype
The string powerup type. This can be 'triple_bombs', 'punch',
'ice_bombs', 'impact_bombs', 'land_mines', 'sticky_bombs', 'shield',
'health', or 'curse'.
node
The 'prop' ba.Node representing this box.
"""
poweruptype: str
"""The string powerup type. This can be 'triple_bombs', 'punch',
'ice_bombs', 'impact_bombs', 'land_mines', 'sticky_bombs', 'shield',
'health', or 'curse'."""
node: ba.Node
"""The 'prop' ba.Node representing this box."""
def __init__(self,
position: Sequence[float] = (0.0, 1.0, 0.0),
poweruptype: str = 'triple_bombs',

View file

@ -16,29 +16,27 @@ if TYPE_CHECKING:
class Spawner:
"""Utility for delayed spawning of objects.
category: Gameplay Classes
Category: **Gameplay Classes**
Creates a light flash and sends a ba.Spawner.SpawnMessage
Creates a light flash and sends a Spawner.SpawnMessage
to the current activity after a delay.
"""
class SpawnMessage:
"""Spawn message sent by a ba.Spawner after its delay has passed.
"""Spawn message sent by a Spawner after its delay has passed.
category: Message Classes
Attributes:
spawner
The ba.Spawner we came from.
data
The data object passed by the user.
pt
The spawn position.
Category: **Message Classes**
"""
spawner: Spawner
"""The ba.Spawner we came from."""
data: Any
"""The data object passed by the user."""
pt: Sequence[float]
"""The spawn position."""
def __init__(
self,
spawner: Spawner,

View file

@ -16,7 +16,6 @@ from bastd.gameutils import SharedObjects
if TYPE_CHECKING:
from typing import Any, Sequence, Optional, Union, Callable
from bastd.actor.spazfactory import SpazFactory
POWERUP_WEAR_OFF_TIME = 20000
BASE_PUNCH_COOLDOWN = 400
@ -42,23 +41,21 @@ class Spaz(ba.Actor):
"""
Base class for various Spazzes.
category: Gameplay Classes
Category: **Gameplay Classes**
A Spaz is the standard little humanoid character in the game.
It can be controlled by a player or by AI, and can have
various different appearances. The name 'Spaz' is not to be
confused with the 'Spaz' character in the game, which is just
one of the skins available for instances of this class.
Attributes:
node
The 'spaz' ba.Node.
"""
# pylint: disable=too-many-public-methods
# pylint: disable=too-many-locals
node: ba.Node
"""The 'spaz' ba.Node."""
points_mult = 1
curse_time: Optional[float] = 5.0
default_bomb_count = 1

View file

@ -27,17 +27,15 @@ PRO_BOT_HIGHLIGHT = (0.6, 0.1, 0.05)
class SpazBotPunchedMessage:
"""A message saying a ba.SpazBot got punched.
category: Message Classes
Attributes:
spazbot
The ba.SpazBot that got punched.
damage
How much damage was done to the ba.SpazBot.
Category: **Message Classes**
"""
spazbot: SpazBot
"""The ba.SpazBot that got punched."""
damage: int
"""How much damage was done to the SpazBot."""
def __init__(self, spazbot: SpazBot, damage: int):
"""Instantiate a message with the given values."""
self.spazbot = spazbot
@ -47,20 +45,18 @@ class SpazBotPunchedMessage:
class SpazBotDiedMessage:
"""A message saying a ba.SpazBot has died.
category: Message Classes
Attributes:
spazbot
The ba.SpazBot that was killed.
killerplayer
The ba.Player that killed it (or None).
how
The particular type of death.
Category: **Message Classes**
"""
spazbot: SpazBot
"""The SpazBot that was killed."""
killerplayer: Optional[ba.Player]
"""The ba.Player that killed it (or None)."""
how: ba.DeathType
"""The particular type of death."""
def __init__(self, spazbot: SpazBot, killerplayer: Optional[ba.Player],
how: ba.DeathType):
"""Instantiate with given values."""
@ -72,7 +68,7 @@ class SpazBotDiedMessage:
class SpazBot(Spaz):
"""A really dumb AI version of ba.Spaz.
category: Bot Classes
Category: **Bot Classes**
Add these to a ba.BotSet to use them.

View file

@ -11,72 +11,70 @@ from bastd.gameutils import SharedObjects
import _ba
if TYPE_CHECKING:
from typing import Any
from typing import Any, Sequence
class SpazFactory:
"""Wraps up media and other resources used by ba.Spaz instances.
Category: Gameplay Classes
Category: **Gameplay Classes**
Generally one of these is created per ba.Activity and shared
between all spaz instances. Use ba.Spaz.get_factory() to return
between all spaz instances. Use ba.Spaz.get_factory() to return
the shared factory for the current activity.
Attributes:
impact_sounds_medium
A tuple of ba.Sounds for when a ba.Spaz hits something kinda hard.
impact_sounds_hard
A tuple of ba.Sounds for when a ba.Spaz hits something really hard.
impact_sounds_harder
A tuple of ba.Sounds for when a ba.Spaz hits something really
really hard.
single_player_death_sound
The sound that plays for an 'important' spaz death such as in
co-op games.
punch_sound
A standard punch ba.Sound.
punch_sound_strong
A tuple of stronger sounding punch ba.Sounds.
punch_sound_stronger
A really really strong sounding punch ba.Sound.
swish_sound
A punch swish ba.Sound.
block_sound
A ba.Sound for when an attack is blocked by invincibility.
shatter_sound
A ba.Sound for when a frozen ba.Spaz shatters.
splatter_sound
A ba.Sound for when a ba.Spaz blows up via curse.
spaz_material
A ba.Material applied to all of parts of a ba.Spaz.
roller_material
A ba.Material applied to the invisible roller ball body that
a ba.Spaz uses for locomotion.
punch_material
A ba.Material applied to the 'fist' of a ba.Spaz.
pickup_material
A ba.Material applied to the 'grabber' body of a ba.Spaz.
curse_material
A ba.Material applied to a cursed ba.Spaz that triggers an explosion.
"""
impact_sounds_medium: Sequence[ba.Sound]
"""A tuple of ba.Sound-s for when a ba.Spaz hits something kinda hard."""
impact_sounds_hard: Sequence[ba.Sound]
"""A tuple of ba.Sound-s for when a ba.Spaz hits something really hard."""
impact_sounds_harder: Sequence[ba.Sound]
"""A tuple of ba.Sound-s for when a ba.Spaz hits something really
really hard."""
single_player_death_sound: ba.Sound
"""The sound that plays for an 'important' spaz death such as in
co-op games."""
punch_sound: ba.Sound
"""A standard punch ba.Sound."""
punch_sound_strong: Sequence[ba.Sound]
"""A tuple of stronger sounding punch ba.Sounds."""
punch_sound_stronger: ba.Sound
"""A really really strong sounding punch ba.Sound."""
swish_sound: ba.Sound
"""A punch swish ba.Sound."""
block_sound: ba.Sound
"""A ba.Sound for when an attack is blocked by invincibility."""
shatter_sound: ba.Sound
"""A ba.Sound for when a frozen ba.Spaz shatters."""
splatter_sound: ba.Sound
"""A ba.Sound for when a ba.Spaz blows up via curse."""
spaz_material: ba.Material
"""A ba.Material applied to all of parts of a ba.Spaz."""
roller_material: ba.Material
"""A ba.Material applied to the invisible roller ball body that
a ba.Spaz uses for locomotion."""
punch_material: ba.Material
"""A ba.Material applied to the 'fist' of a ba.Spaz."""
pickup_material: ba.Material
"""A ba.Material applied to the 'grabber' body of a ba.Spaz."""
curse_material: ba.Material
"""A ba.Material applied to a cursed ba.Spaz that triggers an explosion."""
_STORENAME = ba.storagename()
def _preload(self, character: str) -> None:
@ -210,14 +208,14 @@ class SpazFactory:
# Lets load some basic rules.
# (allows them to be tweaked from the master server)
self.shield_decay_rate = _ba.get_account_misc_read_val('rsdr', 10.0)
self.punch_cooldown = _ba.get_account_misc_read_val('rpc', 400)
self.punch_cooldown_gloves = (_ba.get_account_misc_read_val(
self.shield_decay_rate = _ba.get_v1_account_misc_read_val('rsdr', 10.0)
self.punch_cooldown = _ba.get_v1_account_misc_read_val('rpc', 400)
self.punch_cooldown_gloves = (_ba.get_v1_account_misc_read_val(
'rpcg', 300))
self.punch_power_scale = _ba.get_account_misc_read_val('rpp', 1.2)
self.punch_power_scale_gloves = (_ba.get_account_misc_read_val(
self.punch_power_scale = _ba.get_v1_account_misc_read_val('rpp', 1.2)
self.punch_power_scale_gloves = (_ba.get_v1_account_misc_read_val(
'rppg', 1.4))
self.max_shield_spillover_damage = (_ba.get_account_misc_read_val(
self.max_shield_spillover_damage = (_ba.get_v1_account_misc_read_val(
'rsms', 500))
def get_style(self, character: str) -> str:

View file

@ -175,12 +175,16 @@ class AssaultGame(ba.TeamGameActivity[Player, Team]):
def _handle_base_collide(self, team: Team) -> None:
try:
player = ba.getcollision().opposingnode.getdelegate(
PlayerSpaz, True).getplayer(Player, True)
spaz = ba.getcollision().opposingnode.getdelegate(PlayerSpaz, True)
except ba.NotFoundError:
return
if not player.is_alive():
if not spaz.is_alive():
return
try:
player = spaz.getplayer(Player, True)
except ba.NotFoundError:
return
# If its another team's player, they scored.

View file

@ -274,6 +274,11 @@ class CaptureTheFlagGame(ba.TeamGameActivity[Player, Team]):
# If the enemy flag is already here, score!
if team.enemy_flag_at_base:
# And show team name which scored (but actually we could
# show here player who returned enemy flag).
self.show_zoom_message(ba.Lstr(resource='nameScoresText',
subs=[('${NAME}', team.name)]),
color=team.color)
self._score(team)
else:
team.enemy_flag_at_base = True
@ -435,11 +440,14 @@ class CaptureTheFlagGame(ba.TeamGameActivity[Player, Team]):
"""
player: Optional[Player]
try:
player = ba.getcollision().sourcenode.getdelegate(
PlayerSpaz, True).getplayer(Player, True)
spaz = ba.getcollision().sourcenode.getdelegate(PlayerSpaz, True)
except ba.NotFoundError:
# This can happen if the player leaves but his corpse touches/etc.
player = None
return
if not spaz.is_alive():
return
player = spaz.getplayer(Player, True)
if player:
player.touching_own_flag += (1 if connecting else -1)

View file

@ -239,11 +239,15 @@ class KingOfTheHillGame(ba.TeamGameActivity[Player, Team]):
def _handle_player_flag_region_collide(self, colliding: bool) -> None:
try:
player = ba.getcollision().opposingnode.getdelegate(
PlayerSpaz, True).getplayer(Player, True)
spaz = ba.getcollision().sourcenode.getdelegate(PlayerSpaz, True)
except ba.NotFoundError:
return
if not spaz.is_alive():
return
player = spaz.getplayer(Player, True)
# Different parts of us can collide so a single value isn't enough
# also don't count it if we're dead (flying heads shouldn't be able to
# win the game :-)

View file

@ -224,9 +224,15 @@ class RaceGame(ba.TeamGameActivity[Player, Team]):
collision = ba.getcollision()
try:
region = collision.sourcenode.getdelegate(RaceRegion, True)
player = collision.opposingnode.getdelegate(PlayerSpaz,
True).getplayer(
Player, True)
spaz = collision.opposingnode.getdelegate(PlayerSpaz, True)
except ba.NotFoundError:
return
if not spaz.is_alive():
return
try:
player = spaz.getplayer(Player, True)
except ba.NotFoundError:
return
@ -263,9 +269,9 @@ class RaceGame(ba.TeamGameActivity[Player, Team]):
# Otherwise its the max.
if isinstance(self.session, ba.DualTeamSession
) and self._entire_team_must_finish:
team.lap = min([p.lap for p in team.players])
team.lap = min(p.lap for p in team.players)
else:
team.lap = max([p.lap for p in team.players])
team.lap = max(p.lap for p in team.players)
# A player is finishing.
if player.lap == self._laps:

View file

@ -51,6 +51,7 @@ class Point(Enum):
@dataclass
class Spawn:
"""Defines a bot spawn event."""
# noinspection PyUnresolvedReferences
type: type[SpazBot]
path: int = 0
point: Optional[Point] = None

View file

@ -67,7 +67,7 @@ class MainMenuActivity(ba.Activity[ba.Player, ba.Team]):
# host is navigating menus while they're just staring at an
# empty-ish screen.
tval = ba.Lstr(resource='hostIsNavigatingMenusText',
subs=[('${HOST}', _ba.get_account_display_string())])
subs=[('${HOST}', _ba.get_v1_account_display_string())])
self._host_is_navigating_text = ba.NodeActor(
ba.newnode('text',
attrs={
@ -274,7 +274,7 @@ class MainMenuActivity(ba.Activity[ba.Player, ba.Team]):
# We now want to wait until we're signed in before fetching news.
def _try_fetching_news(self) -> None:
if _ba.get_account_state() == 'signed_in':
if _ba.get_v1_account_state() == 'signed_in':
self._fetch_news()
self._fetch_timer = None
@ -282,7 +282,7 @@ class MainMenuActivity(ba.Activity[ba.Player, ba.Team]):
ba.app.main_menu_last_news_fetch_time = time.time()
# UPDATE - We now just pull news from MRVs.
news = _ba.get_account_misc_read_val('n', None)
news = _ba.get_v1_account_misc_read_val('n', None)
if news is not None:
self._got_news(news)
@ -757,7 +757,7 @@ class MainMenuActivity(ba.Activity[ba.Player, ba.Team]):
})
def _get_custom_logo_tex_name(self) -> Optional[str]:
if _ba.get_account_misc_read_val('easter', False):
if _ba.get_v1_account_misc_read_val('easter', False):
return 'logoEaster'
return None

View file

@ -15,7 +15,7 @@ def show_sign_in_prompt(account_type: str = None) -> None:
if account_type == 'Google Play':
ConfirmWindow(
ba.Lstr(resource='notSignedInGooglePlayErrorText'),
lambda: _ba.sign_in('Google Play'),
lambda: _ba.sign_in_v1('Google Play'),
ok_text=ba.Lstr(resource='accountSettingsWindow.signInText'),
width=460,
height=130)

View file

@ -50,7 +50,7 @@ class AccountLinkWindow(ba.Window):
autoselect=True,
icon=ba.gettexture('crossOut'),
iconscale=1.2)
maxlinks = _ba.get_account_misc_read_val('maxLinkAccounts', 5)
maxlinks = _ba.get_v1_account_misc_read_val('maxLinkAccounts', 5)
ba.textwidget(
parent=self._root_widget,
position=(self._width * 0.5, self._height * 0.56),
@ -84,7 +84,7 @@ class AccountLinkWindow(ba.Window):
def _generate_press(self) -> None:
from bastd.ui import account
if _ba.get_account_state() != 'signed_in':
if _ba.get_v1_account_state() != 'signed_in':
account.show_sign_in_prompt()
return
ba.screenmessage(

View file

@ -45,10 +45,11 @@ class AccountSettingsWindow(ba.Window):
self._r = 'accountSettingsWindow'
self._modal = modal
self._needs_refresh = False
self._signed_in = (_ba.get_account_state() == 'signed_in')
self._account_state_num = _ba.get_account_state_num()
self._show_linked = (self._signed_in and _ba.get_account_misc_read_val(
'allowAccountLinking2', False))
self._signed_in = (_ba.get_v1_account_state() == 'signed_in')
self._account_state_num = _ba.get_v1_account_state_num()
self._show_linked = (self._signed_in
and _ba.get_v1_account_misc_read_val(
'allowAccountLinking2', False))
self._check_sign_in_timer = ba.Timer(1.0,
ba.WeakCall(self._update),
timetype=ba.TimeType.REAL,
@ -57,7 +58,7 @@ class AccountSettingsWindow(ba.Window):
# Currently we can only reset achievements on game-center.
account_type: Optional[str]
if self._signed_in:
account_type = _ba.get_account_type()
account_type = _ba.get_v1_account_type()
else:
account_type = None
self._can_reset_achievements = (account_type == 'Game Center')
@ -91,7 +92,7 @@ class AccountSettingsWindow(ba.Window):
self._show_sign_in_buttons.append('Local')
# Ditto with shiny new V2 ones.
if bool(False):
if bool(True):
self._show_sign_in_buttons.append('V2')
top_extra = 15 if uiscale is ba.UIScale.SMALL else 0
@ -158,10 +159,10 @@ class AccountSettingsWindow(ba.Window):
# Hmm should update this to use get_account_state_num.
# Theoretically if we switch from one signed-in account to another
# in the background this would break.
account_state_num = _ba.get_account_state_num()
account_state = _ba.get_account_state()
account_state_num = _ba.get_v1_account_state_num()
account_state = _ba.get_v1_account_state()
show_linked = (self._signed_in and _ba.get_account_misc_read_val(
show_linked = (self._signed_in and _ba.get_v1_account_misc_read_val(
'allowAccountLinking2', False))
if (account_state_num != self._account_state_num
@ -190,8 +191,8 @@ class AccountSettingsWindow(ba.Window):
# pylint: disable=cyclic-import
from bastd.ui import confirm
account_state = _ba.get_account_state()
account_type = (_ba.get_account_type()
account_state = _ba.get_v1_account_state()
account_type = (_ba.get_v1_account_type()
if account_state == 'signed_in' else 'unknown')
is_google = account_type == 'Google Play'
@ -225,7 +226,7 @@ class AccountSettingsWindow(ba.Window):
game_service_button_space = 60.0
show_linked_accounts_text = (self._signed_in
and _ba.get_account_misc_read_val(
and _ba.get_v1_account_misc_read_val(
'allowAccountLinking2', False))
linked_accounts_text_space = 60.0
@ -254,17 +255,22 @@ class AccountSettingsWindow(ba.Window):
player_profiles_button_space = 100.0
show_link_accounts_button = (self._signed_in
and _ba.get_account_misc_read_val(
and _ba.get_v1_account_misc_read_val(
'allowAccountLinking2', False))
link_accounts_button_space = 70.0
show_unlink_accounts_button = show_link_accounts_button
unlink_accounts_button_space = 90.0
show_sign_out_button = (self._signed_in
and account_type in ['Local', 'Google Play'])
show_sign_out_button = (self._signed_in and account_type
in ['Local', 'Google Play', 'V2'])
sign_out_button_space = 70.0
show_cancel_v2_sign_in_button = (
account_state == 'signing_in'
and ba.app.accounts_v2.have_primary_credentials())
cancel_v2_sign_in_button_space = 70.0
if self._subcontainer is not None:
self._subcontainer.delete()
self._sub_height = 60.0
@ -308,6 +314,8 @@ class AccountSettingsWindow(ba.Window):
self._sub_height += unlink_accounts_button_space
if show_sign_out_button:
self._sub_height += sign_out_button_space
if show_cancel_v2_sign_in_button:
self._sub_height += cancel_v2_sign_in_button_space
self._subcontainer = ba.containerwidget(parent=self._scrollwidget,
size=(self._sub_width,
self._sub_height),
@ -327,7 +335,7 @@ class AccountSettingsWindow(ba.Window):
size=(0, 0),
text=ba.Lstr(
resource='accountSettingsWindow.deviceSpecificAccountText',
subs=[('${NAME}', _ba.get_account_display_string())]),
subs=[('${NAME}', _ba.get_v1_account_display_string())]),
scale=0.7,
color=(0.5, 0.5, 0.6),
maxwidth=self._sub_width * 0.9,
@ -581,7 +589,7 @@ class AccountSettingsWindow(ba.Window):
if show_game_service_button:
button_width = 300
v -= game_service_button_space * 0.85
account_type = _ba.get_account_type()
account_type = _ba.get_v1_account_type()
if account_type == 'Game Center':
account_type_name = ba.Lstr(resource='gameCenterText')
elif account_type == 'Game Circle':
@ -849,6 +857,24 @@ class AccountSettingsWindow(ba.Window):
right_widget=_ba.get_special_widget('party_button'))
ba.widget(edit=btn, left_widget=bbtn, show_buffer_bottom=15)
if show_cancel_v2_sign_in_button:
v -= cancel_v2_sign_in_button_space
self._cancel_v2_sign_in_button = btn = ba.buttonwidget(
parent=self._subcontainer,
position=((self._sub_width - button_width) * 0.5, v),
size=(button_width, 60),
label=ba.Lstr(resource='cancelText'),
color=(0.55, 0.5, 0.6),
textcolor=(0.75, 0.7, 0.8),
autoselect=True,
on_activate_call=self._cancel_v2_sign_in_press)
if first_selectable is None:
first_selectable = btn
if ba.app.ui.use_toolbars:
ba.widget(edit=btn,
right_widget=_ba.get_special_widget('party_button'))
ba.widget(edit=btn, left_widget=bbtn, show_buffer_bottom=15)
# Whatever the topmost selectable thing is, we want it to scroll all
# the way up when we select it.
if first_selectable is not None:
@ -863,8 +889,8 @@ class AccountSettingsWindow(ba.Window):
def _on_achievements_press(self) -> None:
# pylint: disable=cyclic-import
from bastd.ui import achievements
account_state = _ba.get_account_state()
account_type = (_ba.get_account_type()
account_state = _ba.get_v1_account_state()
account_type = (_ba.get_v1_account_type()
if account_state == 'signed_in' else 'unknown')
# for google play we use the built-in UI; otherwise pop up our own
if account_type == 'Google Play':
@ -889,7 +915,7 @@ class AccountSettingsWindow(ba.Window):
# let's not proceed..
if _ba.get_public_login_id() is None:
return False
accounts = _ba.get_account_misc_read_val_2('linkedAccounts', [])
accounts = _ba.get_v1_account_misc_read_val_2('linkedAccounts', [])
return len(accounts) > 1
def _update_unlink_accounts_button(self) -> None:
@ -911,8 +937,8 @@ class AccountSettingsWindow(ba.Window):
num = int(time.time()) % 4
accounts_str = num * '.' + (4 - num) * ' '
else:
accounts = _ba.get_account_misc_read_val_2('linkedAccounts', [])
# our_account = _bs.get_account_display_string()
accounts = _ba.get_v1_account_misc_read_val_2('linkedAccounts', [])
# our_account = _bs.get_v1_account_display_string()
# accounts = [a for a in accounts if a != our_account]
# accounts_str = u', '.join(accounts) if accounts else
# ba.Lstr(translate=('settingNames', 'None'))
@ -951,7 +977,7 @@ class AccountSettingsWindow(ba.Window):
if self._tickets_text is None:
return
try:
tc_str = str(_ba.get_account_ticket_count())
tc_str = str(_ba.get_v1_account_ticket_count())
except Exception:
ba.print_exception()
tc_str = '-'
@ -963,7 +989,7 @@ class AccountSettingsWindow(ba.Window):
if self._account_name_text is None:
return
try:
name_str = _ba.get_account_display_string()
name_str = _ba.get_v1_account_display_string()
except Exception:
ba.print_exception()
name_str = '??'
@ -1005,22 +1031,37 @@ class AccountSettingsWindow(ba.Window):
pbrowser.ProfileBrowserWindow(
origin_widget=self._player_profiles_button)
def _cancel_v2_sign_in_press(self) -> None:
# Just say we don't wanna be signed in anymore.
ba.app.accounts_v2.set_primary_credentials(None)
# Speed UI updates along.
ba.timer(0.1, ba.WeakCall(self._update), timetype=ba.TimeType.REAL)
def _sign_out_press(self) -> None:
_ba.sign_out()
if ba.app.accounts_v2.have_primary_credentials():
ba.app.accounts_v2.set_primary_credentials(None)
else:
_ba.sign_out_v1()
cfg = ba.app.config
# Take note that its our *explicit* intention to not be signed in at
# this point.
# Also take note that its our *explicit* intention to not be
# signed in at this point (affects v1 accounts).
cfg['Auto Account State'] = 'signed_out'
cfg.commit()
ba.buttonwidget(edit=self._sign_out_button,
label=ba.Lstr(resource=self._r + '.signingOutText'))
# Speed UI updates along.
ba.timer(0.1, ba.WeakCall(self._update), timetype=ba.TimeType.REAL)
def _sign_in_press(self,
account_type: str,
show_test_warning: bool = True) -> None:
del show_test_warning # unused
_ba.sign_in(account_type)
_ba.sign_in_v1(account_type)
# Make note of the type account we're *wanting* to be signed in with.
cfg = ba.app.config

View file

@ -81,7 +81,7 @@ class AccountUnlinkWindow(ba.Window):
if our_login_id is None:
entries = []
else:
account_infos = _ba.get_account_misc_read_val_2(
account_infos = _ba.get_v1_account_misc_read_val_2(
'linkedAccounts2', [])
entries = [{
'name': ai['d'],

View file

@ -4,36 +4,90 @@
from __future__ import annotations
import logging
from typing import TYPE_CHECKING
import ba
import _ba
from efro.error import CommunicationError
import bacommon.cloud
if TYPE_CHECKING:
from typing import Any, Optional
from typing import Union, Optional
STATUS_CHECK_INTERVAL_SECONDS = 2.0
class V2SignInWindow(ba.Window):
"""A window allowing signing in to a v2 account."""
def __init__(self, origin_widget: ba.Widget):
from ba.internal import is_browser_likely_available
logincode = '1412345'
address = (
f'{_ba.get_master_server_address(version=2)}?login={logincode}')
self._width = 600
self._height = 500
self._height = 550
self._proxyid: Optional[str] = None
self._proxykey: Optional[str] = None
uiscale = ba.app.ui.uiscale
super().__init__(root_widget=ba.containerwidget(
size=(self._width, self._height),
transition='in_scale',
scale_origin_stack_offset=origin_widget.get_screen_space_center(),
scale=(1.25 if uiscale is ba.UIScale.SMALL else
1.0 if uiscale is ba.UIScale.MEDIUM else 0.85)))
1.05 if uiscale is ba.UIScale.MEDIUM else 0.9)))
self._loading_text = ba.textwidget(
parent=self._root_widget,
position=(self._width * 0.5, self._height * 0.5),
h_align='center',
v_align='center',
size=(0, 0),
maxwidth=0.9 * self._width,
text=ba.Lstr(value='${A}...',
subs=[('${A}', ba.Lstr(resource='loadingText'))]),
)
self._cancel_button = ba.buttonwidget(
parent=self._root_widget,
position=(30, self._height - 65),
size=(130, 50),
scale=0.8,
label=ba.Lstr(resource='cancelText'),
on_activate_call=self._done,
autoselect=True,
textcolor=(0.75, 0.7, 0.8),
)
ba.containerwidget(edit=self._root_widget,
cancel_button=self._cancel_button)
self._update_timer: Optional[ba.Timer] = None
# Ask the cloud for a proxy login id.
ba.app.cloud.send_message(bacommon.cloud.LoginProxyRequestMessage(),
on_response=ba.WeakCall(
self._on_proxy_request_response))
def _on_proxy_request_response(
self, response: Union[bacommon.cloud.LoginProxyRequestResponse,
Exception]
) -> None:
from ba.internal import is_browser_likely_available
# Something went wrong. Show an error message and that's it.
if isinstance(response, Exception):
ba.textwidget(
edit=self._loading_text,
text=ba.Lstr(resource='internal.unavailableNoConnectionText'),
color=(1, 0, 0))
return
# Show link(s) the user can use to log in.
address = _ba.get_master_server_address(version=2) + response.url
address_pretty = address.removeprefix('https://')
ba.textwidget(
parent=self._root_widget,
position=(self._width * 0.5, self._height - 85),
position=(self._width * 0.5, self._height - 95),
size=(0, 0),
text=ba.Lstr(
resource='accountSettingsWindow.v2LinkInstructionsText'),
@ -45,19 +99,19 @@ class V2SignInWindow(ba.Window):
if is_browser_likely_available():
ba.buttonwidget(parent=self._root_widget,
position=((self._width * 0.5 - button_width * 0.5),
self._height - 175),
self._height - 185),
autoselect=True,
size=(button_width, 60),
label=ba.Lstr(value=address),
label=ba.Lstr(value=address_pretty),
color=(0.55, 0.5, 0.6),
textcolor=(0.75, 0.7, 0.8),
on_activate_call=lambda: ba.open_url(address))
qroffs = 0.0
else:
ba.textwidget(parent=self._root_widget,
position=(self._width * 0.5, self._height - 135),
position=(self._width * 0.5, self._height - 145),
size=(0, 0),
text=ba.Lstr(value=address),
text=ba.Lstr(value=address_pretty),
flatness=1.0,
maxwidth=self._width,
scale=0.75,
@ -65,28 +119,75 @@ class V2SignInWindow(ba.Window):
v_align='center')
qroffs = 20.0
self._cancel_button = ba.buttonwidget(
parent=self._root_widget,
position=(30, self._height - 55),
size=(130, 50),
scale=0.8,
label=ba.Lstr(resource='cancelText'),
# color=(0.6, 0.5, 0.6),
on_activate_call=self._done,
autoselect=True,
textcolor=(0.75, 0.7, 0.8),
# icon=ba.gettexture('crossOut'),
# iconscale=1.2
)
ba.containerwidget(edit=self._root_widget,
cancel_button=self._cancel_button)
qr_size = 270
ba.imagewidget(parent=self._root_widget,
position=(self._width * 0.5 - qr_size * 0.5,
self._height * 0.34 + qroffs - qr_size * 0.5),
self._height * 0.36 + qroffs - qr_size * 0.5),
size=(qr_size, qr_size),
texture=_ba.get_qrcode_texture(address))
# Start querying for results.
self._proxyid = response.proxyid
self._proxykey = response.proxykey
ba.timer(STATUS_CHECK_INTERVAL_SECONDS,
ba.WeakCall(self._ask_for_status))
def _ask_for_status(self) -> None:
assert self._proxyid is not None
assert self._proxykey is not None
ba.app.cloud.send_message(bacommon.cloud.LoginProxyStateQueryMessage(
proxyid=self._proxyid, proxykey=self._proxykey),
on_response=ba.WeakCall(self._got_status))
def _got_status(
self, response: Union[bacommon.cloud.LoginProxyStateQueryResponse,
Exception]
) -> None:
# For now, if anything goes wrong on the server-side, just abort
# with a vague error message. Can be more verbose later if need be.
if (isinstance(response, bacommon.cloud.LoginProxyStateQueryResponse)
and response.state is response.State.FAIL):
ba.playsound(ba.getsound('error'))
ba.screenmessage(ba.Lstr(resource='errorText'), color=(1, 0, 0))
self._done()
return
# If we got a token, set ourself as signed in. Hooray!
if (isinstance(response, bacommon.cloud.LoginProxyStateQueryResponse)
and response.state is response.State.SUCCESS):
assert response.credentials is not None
ba.app.accounts_v2.set_primary_credentials(response.credentials)
# As a courtesy, tell the server we're done with this proxy
# so it can clean up (not a huge deal if this fails)
assert self._proxyid is not None
try:
ba.app.cloud.send_message(
bacommon.cloud.LoginProxyCompleteMessage(
proxyid=self._proxyid),
on_response=ba.WeakCall(self._proxy_complete_response))
except CommunicationError:
pass
except Exception:
logging.warning(
'Unexpected error sending login-proxy-complete message',
exc_info=True)
self._done()
return
# If we're still waiting, ask again soon.
if (isinstance(response, Exception)
or response.state is response.State.WAITING):
ba.timer(STATUS_CHECK_INTERVAL_SECONDS,
ba.WeakCall(self._ask_for_status))
def _proxy_complete_response(self, response: Union[None,
Exception]) -> None:
del response # Not used.
# We could do something smart like retry on exceptions here, but
# this isn't critical so we'll just let anything slide.
def _done(self) -> None:
ba.containerwidget(edit=self._root_widget, transition='out_scale')

View file

@ -91,7 +91,7 @@ class AccountViewerWindow(popup.PopupWindow):
# In cases where the user most likely has a browser/email, lets
# offer a 'report this user' button.
if (is_browser_likely_available() and _ba.get_account_misc_read_val(
if (is_browser_likely_available() and _ba.get_v1_account_misc_read_val(
'showAccountExtrasMenu', False)):
self._extras_menu_button = ba.buttonwidget(

View file

@ -60,15 +60,14 @@ class AppInviteWindow(ba.Window):
resource='gatherWindow.earnTicketsForRecommendingAmountText',
fallback_resource=(
'gatherWindow.earnTicketsForRecommendingText'),
subs=[
('${COUNT}',
str(_ba.get_account_misc_read_val('friendTryTickets',
300))),
('${YOU_COUNT}',
str(
_ba.get_account_misc_read_val('friendTryAwardTickets',
100)))
]))
subs=[('${COUNT}',
str(
_ba.get_v1_account_misc_read_val(
'friendTryTickets', 300))),
('${YOU_COUNT}',
str(
_ba.get_v1_account_misc_read_val(
'friendTryAwardTickets', 100)))]))
or_text = ba.Lstr(resource='orText',
subs=[('${A}', ''),
@ -129,17 +128,18 @@ class AppInviteWindow(ba.Window):
ba.playsound(ba.getsound('error'))
return
if _ba.get_account_state() == 'signed_in':
if _ba.get_v1_account_state() == 'signed_in':
ba.set_analytics_screen('App Invite UI')
_ba.show_app_invite(
ba.Lstr(resource='gatherWindow.appInviteTitleText',
subs=[('${APP_NAME}', ba.Lstr(resource='titleText'))
]).evaluate(),
ba.Lstr(resource='gatherWindow.appInviteMessageText',
subs=[('${COUNT}', str(self._data['tickets'])),
('${NAME}', _ba.get_account_name().split()[0]),
('${APP_NAME}', ba.Lstr(resource='titleText'))
]).evaluate(), self._data['code'])
subs=[
('${COUNT}', str(self._data['tickets'])),
('${NAME}', _ba.get_v1_account_name().split()[0]),
('${APP_NAME}', ba.Lstr(resource='titleText'))
]).evaluate(), self._data['code'])
else:
ba.playsound(ba.getsound('error'))
@ -256,7 +256,7 @@ class ShowFriendCodeWindow(ba.Window):
]).evaluate(),
ba.Lstr(resource='gatherWindow.appInviteMessageText',
subs=[('${COUNT}', str(self._data['tickets'])),
('${NAME}', _ba.get_account_name().split()[0]),
('${NAME}', _ba.get_v1_account_name().split()[0]),
('${APP_NAME}', ba.Lstr(resource='titleText'))
]).evaluate(), self._data['code'])
@ -264,7 +264,7 @@ class ShowFriendCodeWindow(ba.Window):
import urllib.parse
# If somehow we got signed out.
if _ba.get_account_state() != 'signed_in':
if _ba.get_v1_account_state() != 'signed_in':
ba.screenmessage(ba.Lstr(resource='notSignedInText'),
color=(1, 0, 0))
ba.playsound(ba.getsound('error'))
@ -273,7 +273,7 @@ class ShowFriendCodeWindow(ba.Window):
ba.set_analytics_screen('Email Friend Code')
subject = (ba.Lstr(resource='gatherWindow.friendHasSentPromoCodeText').
evaluate().replace(
'${NAME}', _ba.get_account_name()).replace(
'${NAME}', _ba.get_v1_account_name()).replace(
'${APP_NAME}',
ba.Lstr(resource='titleText').evaluate()).replace(
'${COUNT}', str(self._data['tickets'])))
@ -304,7 +304,7 @@ def handle_app_invites_press(force_code: bool = False) -> None:
"""(internal)"""
app = ba.app
do_app_invites = (app.platform == 'android' and app.subplatform == 'google'
and _ba.get_account_misc_read_val(
and _ba.get_v1_account_misc_read_val(
'enableAppInvites', False) and not app.on_tv)
if force_code:
do_app_invites = False

View file

@ -156,7 +156,7 @@ class CharacterPicker(popup.PopupWindow):
def _on_store_press(self) -> None:
from bastd.ui.account import show_sign_in_prompt
from bastd.ui.store.browser import StoreBrowserWindow
if _ba.get_account_state() != 'signed_in':
if _ba.get_v1_account_state() != 'signed_in':
show_sign_in_prompt()
return
self._transition_out()

View file

@ -94,7 +94,7 @@ class ColorPicker(PopupWindow):
on_activate_call=ba.WeakCall(self._select_other))
# Custom colors are limited to pro currently.
if not ba.app.accounts.have_pro():
if not ba.app.accounts_v1.have_pro():
ba.imagewidget(parent=self.root_widget,
position=(50, 12),
size=(30, 30),
@ -118,7 +118,7 @@ class ColorPicker(PopupWindow):
from bastd.ui import purchase
# Requires pro.
if not ba.app.accounts.have_pro():
if not ba.app.accounts_v1.have_pro():
purchase.PurchaseWindow(items=['pro'])
self._transition_out()
return
@ -249,6 +249,7 @@ class ColorPickerExact(PopupWindow):
# color to the delegate, so start doing that.
self._update_for_color()
# noinspection PyUnresolvedReferences
def _update_for_color(self) -> None:
if not self.root_widget:
return

View file

@ -17,13 +17,11 @@ class ConfigCheckBox:
It will automatically save and apply the config when its
value changes.
Attributes:
widget
The underlying ba.Widget instance.
"""
widget: ba.Widget
"""The underlying ba.Widget instance."""
def __init__(self,
parent: ba.Widget,
configkey: str,
@ -65,22 +63,20 @@ class ConfigNumberEdit:
It will automatically save and apply the config when its
value changes.
Attributes:
nametext
The text widget displaying the name.
valuetext
The text widget displaying the current value.
minusbutton
The button widget used to reduce the value.
plusbutton
The button widget used to increase the value.
"""
nametext: ba.Widget
"""The text widget displaying the name."""
valuetext: ba.Widget
"""The text widget displaying the current value."""
minusbutton: ba.Widget
"""The button widget used to reduce the value."""
plusbutton: ba.Widget
"""The button widget used to increase the value."""
def __init__(self,
parent: ba.Widget,
configkey: str,

View file

@ -142,9 +142,9 @@ class ContinuesWindow(ba.Window):
self._on_cancel()
return
if _ba.get_account_state() == 'signed_in':
if _ba.get_v1_account_state() == 'signed_in':
sval = (ba.charstr(ba.SpecialChar.TICKET) +
str(_ba.get_account_ticket_count()))
str(_ba.get_v1_account_ticket_count()))
else:
sval = '?'
if self._tickets_text is not None:
@ -176,14 +176,14 @@ class ContinuesWindow(ba.Window):
ba.playsound(ba.getsound('error'))
else:
# If somehow we got signed out...
if _ba.get_account_state() != 'signed_in':
if _ba.get_v1_account_state() != 'signed_in':
ba.screenmessage(ba.Lstr(resource='notSignedInText'),
color=(1, 0, 0))
ba.playsound(ba.getsound('error'))
return
# If it appears we don't have enough tickets, offer to buy more.
tickets = _ba.get_account_ticket_count()
tickets = _ba.get_v1_account_ticket_count()
if tickets < self._cost:
# FIXME: Should we start the timer back up again after?
self._counting_down = False

View file

@ -92,7 +92,7 @@ class CoopBrowserWindow(ba.Window):
self._tourney_data_up_to_date = False
self._campaign_difficulty = _ba.get_account_misc_val(
self._campaign_difficulty = _ba.get_v1_account_misc_val(
'campaignDifficulty', 'easy')
super().__init__(root_widget=ba.containerwidget(
@ -235,7 +235,7 @@ class CoopBrowserWindow(ba.Window):
self._subcontainer: Optional[ba.Widget] = None
# Take note of our account state; we'll refresh later if this changes.
self._account_state_num = _ba.get_account_state_num()
self._account_state_num = _ba.get_v1_account_state_num()
# Same for fg/bg state.
self._fg_state = app.fg_state
@ -251,14 +251,14 @@ class CoopBrowserWindow(ba.Window):
# If we've got a cached tournament list for our account and info for
# each one of those tournaments, go ahead and display it as a
# starting point.
if (app.accounts.account_tournament_list is not None
and app.accounts.account_tournament_list[0]
== _ba.get_account_state_num()
and all(t_id in app.accounts.tournament_info
for t_id in app.accounts.account_tournament_list[1])):
if (app.accounts_v1.account_tournament_list is not None
and app.accounts_v1.account_tournament_list[0]
== _ba.get_v1_account_state_num() and all(
t_id in app.accounts_v1.tournament_info
for t_id in app.accounts_v1.account_tournament_list[1])):
tourney_data = [
app.accounts.tournament_info[t_id]
for t_id in app.accounts.account_tournament_list[1]
app.accounts_v1.tournament_info[t_id]
for t_id in app.accounts_v1.account_tournament_list[1]
]
self._update_for_data(tourney_data)
@ -269,6 +269,7 @@ class CoopBrowserWindow(ba.Window):
repeat=True)
self._update()
# noinspection PyUnresolvedReferences
@staticmethod
def _preload_modules() -> None:
"""Preload modules we use (called in bg thread)."""
@ -299,7 +300,7 @@ class CoopBrowserWindow(ba.Window):
self._tourney_data_up_to_date = False
# If our account state has changed, do a full request.
account_state_num = _ba.get_account_state_num()
account_state_num = _ba.get_v1_account_state_num()
if account_state_num != self._account_state_num:
self._account_state_num = account_state_num
self._save_state()
@ -357,7 +358,7 @@ class CoopBrowserWindow(ba.Window):
try:
ba.imagewidget(
edit=self._hard_button_lock_image,
opacity=0.0 if ba.app.accounts.have_pro_options() else 1.0)
opacity=0.0 if ba.app.accounts_v1.have_pro_options() else 1.0)
except Exception:
ba.print_exception('Error updating campaign lock.')
@ -479,7 +480,7 @@ class CoopBrowserWindow(ba.Window):
tbtn['required_league'] = (None if 'requiredLeague' not in entry
else entry['requiredLeague'])
game = ba.app.accounts.tournament_info[
game = ba.app.accounts_v1.tournament_info[
tbtn['tournament_id']]['game']
if game is None:
@ -490,7 +491,7 @@ class CoopBrowserWindow(ba.Window):
else:
campaignname, levelname = game.split(':')
campaign = getcampaign(campaignname)
max_players = ba.app.accounts.tournament_info[
max_players = ba.app.accounts_v1.tournament_info[
tbtn['tournament_id']]['maxPlayers']
txt = ba.Lstr(
value='${A} ${B}',
@ -524,7 +525,7 @@ class CoopBrowserWindow(ba.Window):
tbtn['allow_ads'] = allow_ads = entry['allowAds']
final_fee: Optional[int] = (None if fee_var is None else
_ba.get_account_misc_read_val(
_ba.get_v1_account_misc_read_val(
fee_var, '?'))
final_fee_str: Union[str, ba.Lstr]
@ -539,9 +540,9 @@ class CoopBrowserWindow(ba.Window):
ba.charstr(ba.SpecialChar.TICKET_BACKING) +
str(final_fee))
ad_tries_remaining = ba.app.accounts.tournament_info[
ad_tries_remaining = ba.app.accounts_v1.tournament_info[
tbtn['tournament_id']]['adTriesRemaining']
free_tries_remaining = ba.app.accounts.tournament_info[
free_tries_remaining = ba.app.accounts_v1.tournament_info[
tbtn['tournament_id']]['freeTriesRemaining']
# Now, if this fee allows ads and we support video ads, show
@ -591,7 +592,7 @@ class CoopBrowserWindow(ba.Window):
def _on_tournament_query_response(self, data: Optional[dict[str,
Any]]) -> None:
accounts = ba.app.accounts
accounts = ba.app.accounts_v1
if data is not None:
tournament_data = data['t'] # This used to be the whole payload.
self._last_tournament_query_response_time = ba.time(
@ -605,9 +606,11 @@ class CoopBrowserWindow(ba.Window):
accounts.cache_tournament_info(tournament_data)
# Also cache the current tourney list/order for this account.
accounts.account_tournament_list = (_ba.get_account_state_num(), [
e['tournamentID'] for e in tournament_data
])
accounts.account_tournament_list = (_ba.get_v1_account_state_num(),
[
e['tournamentID']
for e in tournament_data
])
self._doing_tournament_query = False
self._update_for_data(tournament_data)
@ -616,7 +619,8 @@ class CoopBrowserWindow(ba.Window):
# pylint: disable=cyclic-import
from bastd.ui.purchase import PurchaseWindow
if difficulty != self._campaign_difficulty:
if difficulty == 'hard' and not ba.app.accounts.have_pro_options():
if (difficulty == 'hard'
and not ba.app.accounts_v1.have_pro_options()):
PurchaseWindow(items=['pro'])
return
ba.playsound(ba.getsound('gunCocking'))
@ -871,7 +875,7 @@ class CoopBrowserWindow(ba.Window):
# no tournaments).
if self._tournament_button_count == 0:
unavailable_text = ba.Lstr(resource='unavailableText')
if _ba.get_account_state() != 'signed_in':
if _ba.get_v1_account_state() != 'signed_in':
unavailable_text = ba.Lstr(
value='${A} (${B})',
subs=[('${A}', unavailable_text),
@ -942,7 +946,7 @@ class CoopBrowserWindow(ba.Window):
]
# Show easter-egg-hunt either if its easter or we own it.
if _ba.get_account_misc_read_val(
if _ba.get_v1_account_misc_read_val(
'easter', False) or _ba.get_purchased('games.easter_egg_hunt'):
items = [
'Challenges:Easter Egg Hunt', 'Challenges:Pro Easter Egg Hunt'
@ -1345,7 +1349,7 @@ class CoopBrowserWindow(ba.Window):
# pylint: disable=cyclic-import
from bastd.ui.account import show_sign_in_prompt
from bastd.ui.league.rankwindow import LeagueRankWindow
if _ba.get_account_state() != 'signed_in':
if _ba.get_v1_account_state() != 'signed_in':
show_sign_in_prompt()
return
self._save_state()
@ -1362,7 +1366,7 @@ class CoopBrowserWindow(ba.Window):
) -> None:
# pylint: disable=cyclic-import
from bastd.ui.account import show_sign_in_prompt
if _ba.get_account_state() != 'signed_in':
if _ba.get_v1_account_state() != 'signed_in':
show_sign_in_prompt()
return
self._save_state()
@ -1426,7 +1430,7 @@ class CoopBrowserWindow(ba.Window):
# Do a bit of pre-flight for tournament options.
if tournament_button is not None:
if _ba.get_account_state() != 'signed_in':
if _ba.get_v1_account_state() != 'signed_in':
show_sign_in_prompt()
return
@ -1464,7 +1468,7 @@ class CoopBrowserWindow(ba.Window):
return
# Game is whatever the tournament tells us it is.
game = ba.app.accounts.tournament_info[
game = ba.app.accounts_v1.tournament_info[
tournament_button['tournament_id']]['game']
if tournament_button is None and game == 'Easy:The Last Stand':
@ -1480,8 +1484,8 @@ class CoopBrowserWindow(ba.Window):
if tournament_button is None and game in (
'Challenges:Infinite Runaround',
'Challenges:Infinite Onslaught'
) and not ba.app.accounts.have_pro():
if _ba.get_account_state() != 'signed_in':
) and not ba.app.accounts_v1.have_pro():
if _ba.get_v1_account_state() != 'signed_in':
show_sign_in_prompt()
else:
PurchaseWindow(items=['pro'])
@ -1507,7 +1511,7 @@ class CoopBrowserWindow(ba.Window):
if (tournament_button is None and required_purchase is not None
and not _ba.get_purchased(required_purchase)):
if _ba.get_account_state() != 'signed_in':
if _ba.get_v1_account_state() != 'signed_in':
show_sign_in_prompt()
else:
PurchaseWindow(items=[required_purchase])

View file

@ -199,7 +199,7 @@ class GameButton:
# Hard-code games we haven't unlocked.
if ((game in ('Challenges:Infinite Runaround',
'Challenges:Infinite Onslaught')
and not ba.app.accounts.have_pro())
and not ba.app.accounts_v1.have_pro())
or (game in ('Challenges:Meteor Shower', )
and not _ba.get_purchased('games.meteor_shower'))
or (game in ('Challenges:Target Practice',

View file

@ -151,7 +151,7 @@ class GatherWindow(ba.Window):
tabdefs: list[tuple[GatherWindow.TabID, ba.Lstr]] = [
(self.TabID.ABOUT, ba.Lstr(resource=self._r + '.aboutText'))
]
if _ba.get_account_misc_read_val('enablePublicParties', True):
if _ba.get_v1_account_misc_read_val('enablePublicParties', True):
tabdefs.append((self.TabID.INTERNET,
ba.Lstr(resource=self._r + '.publicText')))
tabdefs.append(

View file

@ -52,7 +52,8 @@ class AboutGatherTab(GatherTab):
include_invite = True
msc_scale = 1.1
c_height_2 = min(region_height, string_height * msc_scale + 100)
try_tickets = _ba.get_account_misc_read_val('friendTryTickets', None)
try_tickets = _ba.get_v1_account_misc_read_val('friendTryTickets',
None)
if try_tickets is None:
include_invite = False
self._container = ba.containerwidget(
@ -106,7 +107,7 @@ class AboutGatherTab(GatherTab):
def _invite_to_try_press(self) -> None:
from bastd.ui.account import show_sign_in_prompt
from bastd.ui.appinvite import handle_app_invites_press
if _ba.get_account_state() != 'signed_in':
if _ba.get_v1_account_state() != 'signed_in':
show_sign_in_prompt()
return
handle_app_invites_press()

View file

@ -17,7 +17,6 @@ import ba
if TYPE_CHECKING:
from typing import Any, Optional, Union, Callable
from bastd.ui.gather import GatherWindow
from bastd.ui.confirm import ConfirmWindow
def _safe_set_text(txt: Optional[ba.Widget],

View file

@ -166,7 +166,7 @@ class PrivateGatherTab(GatherTab):
elif hcfg.session_type == 'teams':
sessiontype = ba.DualTeamSession
else:
raise RuntimeError('fInvalid sessiontype: {hcfg.session_type}')
raise RuntimeError(f'Invalid sessiontype: {hcfg.session_type}')
pvars = PlaylistTypeVars(sessiontype)
playlist_name = ba.app.config.get(
@ -225,7 +225,7 @@ class PrivateGatherTab(GatherTab):
def _update_currency_ui(self) -> None:
# Keep currency count up to date if applicable.
try:
t_str = str(_ba.get_account_ticket_count())
t_str = str(_ba.get_v1_account_ticket_count())
except Exception:
t_str = '?'
if self._get_tickets_button:
@ -245,7 +245,7 @@ class PrivateGatherTab(GatherTab):
if self._state.sub_tab is SubTabType.HOST:
# If we're not signed in, just refresh to show that.
if (_ba.get_account_state() != 'signed_in'
if (_ba.get_v1_account_state() != 'signed_in'
and self._showing_not_signed_in_screen):
self._refresh_sub_tab()
else:
@ -254,7 +254,7 @@ class PrivateGatherTab(GatherTab):
if (self._last_hosting_state_query_time is None
or now - self._last_hosting_state_query_time > 15.0):
self._debug_server_comm('querying private party state')
if _ba.get_account_state() == 'signed_in':
if _ba.get_v1_account_state() == 'signed_in':
_ba.add_transaction(
{
'type': 'PRIVATE_PARTY_QUERY',
@ -437,7 +437,7 @@ class PrivateGatherTab(GatherTab):
# pylint: disable=too-many-branches
# pylint: disable=too-many-statements
if _ba.get_account_state() != 'signed_in':
if _ba.get_v1_account_state() != 'signed_in':
ba.textwidget(parent=self._container,
size=(0, 0),
h_align='center',
@ -776,7 +776,7 @@ class PrivateGatherTab(GatherTab):
or self._waiting_for_initial_state):
return
if _ba.get_account_state() != 'signed_in':
if _ba.get_v1_account_state() != 'signed_in':
ba.screenmessage(ba.Lstr(resource='notSignedInErrorText'))
ba.playsound(ba.getsound('error'))
self._refresh_sub_tab()
@ -795,7 +795,7 @@ class PrivateGatherTab(GatherTab):
if self._hostingstate.tickets_to_host_now > 0:
ticket_count: Optional[int]
try:
ticket_count = _ba.get_account_ticket_count()
ticket_count = _ba.get_v1_account_ticket_count()
except Exception:
# FIXME: should add a ba.NotSignedInError we can use here.
ticket_count = None
@ -809,7 +809,7 @@ class PrivateGatherTab(GatherTab):
{
'type': 'PRIVATE_PARTY_START',
'config': dataclass_to_dict(self._hostingconfig),
'region_pings': ba.app.net.region_pings,
'region_pings': ba.app.net.zone_pings,
'expire_time': time.time() + 20,
},
callback=ba.WeakCall(self._hosting_state_response))

View file

@ -88,8 +88,8 @@ class UIRow:
if party.clean_display_index == index:
return
ping_good = _ba.get_account_misc_read_val('pingGood', 100)
ping_med = _ba.get_account_misc_read_val('pingMed', 500)
ping_good = _ba.get_v1_account_misc_read_val('pingGood', 100)
ping_med = _ba.get_v1_account_misc_read_val('pingMed', 500)
self._clear()
hpos = 20
@ -122,8 +122,8 @@ class UIRow:
if party.stats_addr:
url = party.stats_addr.replace(
'${ACCOUNT}',
_ba.get_account_misc_read_val_2('resolvedAccountID',
'UNKNOWN'))
_ba.get_v1_account_misc_read_val_2('resolvedAccountID',
'UNKNOWN'))
self._stats_button = ba.buttonwidget(
color=(0.3, 0.6, 0.94),
textcolor=(1.0, 1.0, 1.0),
@ -793,7 +793,7 @@ class PublicGatherTab(GatherTab):
self._process_pending_party_infos()
# Anytime we sign in/out, make sure we refresh our list.
signed_in = _ba.get_account_state() == 'signed_in'
signed_in = _ba.get_v1_account_state() == 'signed_in'
if self._signed_in != signed_in:
self._signed_in = signed_in
self._party_lists_dirty = True
@ -986,7 +986,7 @@ class PublicGatherTab(GatherTab):
p[1].index))
# If signed out or errored, show no parties.
if (_ba.get_account_state() != 'signed_in'
if (_ba.get_v1_account_state() != 'signed_in'
or not self._have_valid_server_list):
self._parties_displayed = {}
else:
@ -1023,11 +1023,11 @@ class PublicGatherTab(GatherTab):
# Fire off a new public-party query periodically.
if (self._last_server_list_query_time is None
or now - self._last_server_list_query_time > 0.001 *
_ba.get_account_misc_read_val('pubPartyRefreshMS', 10000)):
_ba.get_v1_account_misc_read_val('pubPartyRefreshMS', 10000)):
self._last_server_list_query_time = now
if DEBUG_SERVER_COMMUNICATION:
print('REQUESTING SERVER LIST')
if _ba.get_account_state() == 'signed_in':
if _ba.get_v1_account_state() == 'signed_in':
_ba.add_transaction(
{
'type': 'PUBLIC_PARTY_QUERY',
@ -1156,7 +1156,7 @@ class PublicGatherTab(GatherTab):
def _on_start_advertizing_press(self) -> None:
from bastd.ui.account import show_sign_in_prompt
if _ba.get_account_state() != 'signed_in':
if _ba.get_v1_account_state() != 'signed_in':
show_sign_in_prompt()
return

View file

@ -179,22 +179,27 @@ class GetCurrencyWindow(ba.Window):
c2txt = ba.Lstr(
resource=rsrc,
subs=[('${COUNT}',
str(_ba.get_account_misc_read_val('tickets2Amount', 500)))])
str(_ba.get_v1_account_misc_read_val('tickets2Amount',
500)))])
c3txt = ba.Lstr(
resource=rsrc,
subs=[('${COUNT}',
str(_ba.get_account_misc_read_val('tickets3Amount',
1500)))])
subs=[
('${COUNT}',
str(_ba.get_v1_account_misc_read_val('tickets3Amount', 1500)))
])
c4txt = ba.Lstr(
resource=rsrc,
subs=[('${COUNT}',
str(_ba.get_account_misc_read_val('tickets4Amount',
5000)))])
subs=[
('${COUNT}',
str(_ba.get_v1_account_misc_read_val('tickets4Amount', 5000)))
])
c5txt = ba.Lstr(
resource=rsrc,
subs=[('${COUNT}',
str(_ba.get_account_misc_read_val('tickets5Amount',
15000)))])
subs=[
('${COUNT}',
str(_ba.get_v1_account_misc_read_val('tickets5Amount',
15000)))
])
h = 110.0
@ -261,7 +266,7 @@ class GetCurrencyWindow(ba.Window):
label=ba.Lstr(resource=self._r + '.ticketsFromASponsorText',
subs=[('${COUNT}',
str(
_ba.get_account_misc_read_val(
_ba.get_v1_account_misc_read_val(
'sponsorTickets', 5)))]),
tex_name='ticketsMore',
enabled=self._enable_ad_button,
@ -301,11 +306,10 @@ class GetCurrencyWindow(ba.Window):
size=b_size_3,
label=ba.Lstr(
resource='gatherWindow.earnTicketsForRecommendingText',
subs=[
('${COUNT}',
str(_ba.get_account_misc_read_val(
'sponsorTickets', 5)))
]),
subs=[('${COUNT}',
str(
_ba.get_v1_account_misc_read_val(
'sponsorTickets', 5)))]),
tex_name='ticketsMore',
enabled=True,
tex_opacity=0.6,
@ -336,7 +340,7 @@ class GetCurrencyWindow(ba.Window):
'.youHaveText').evaluate().partition('${COUNT}')[0].strip())
txt2 = (ba.Lstr(
resource=self._r +
'.youHaveText').evaluate().rpartition('${COUNT}')[0].strip())
'.youHaveText').evaluate().rpartition('${COUNT}')[-1].strip())
ba.textwidget(parent=self._root_widget,
text=txt1,
@ -427,16 +431,16 @@ class GetCurrencyWindow(ba.Window):
import datetime
# if we somehow get signed out, just die..
if _ba.get_account_state() != 'signed_in':
if _ba.get_v1_account_state() != 'signed_in':
self._back()
return
self._ticket_count = _ba.get_account_ticket_count()
self._ticket_count = _ba.get_v1_account_ticket_count()
# update our incentivized ad button depending on whether ads are
# available
if self._ad_button is not None:
next_reward_ad_time = _ba.get_account_misc_read_val_2(
next_reward_ad_time = _ba.get_v1_account_misc_read_val_2(
'nextRewardAdTime', None)
if next_reward_ad_time is not None:
next_reward_ad_time = datetime.datetime.utcfromtimestamp(
@ -494,8 +498,9 @@ class GetCurrencyWindow(ba.Window):
app = ba.app
if ((app.test_build or
(app.platform == 'android'
and app.subplatform in ['oculus', 'cardboard'])) and
_ba.get_account_misc_read_val('allowAccountLinking2', False)):
and app.subplatform in ['oculus', 'cardboard']))
and _ba.get_v1_account_misc_read_val('allowAccountLinking2',
False)):
ba.screenmessage(ba.Lstr(resource=self._r +
'.unavailableLinkAccountText'),
color=(1, 0.5, 0))
@ -509,7 +514,7 @@ class GetCurrencyWindow(ba.Window):
from bastd.ui import appinvite
from ba.internal import master_server_get
if item == 'app_invite':
if _ba.get_account_state() != 'signed_in':
if _ba.get_v1_account_state() != 'signed_in':
account.show_sign_in_prompt()
return
appinvite.handle_app_invites_press()
@ -554,7 +559,7 @@ class GetCurrencyWindow(ba.Window):
if item == 'ad':
import datetime
# if ads are disabled until some time, error..
next_reward_ad_time = _ba.get_account_misc_read_val_2(
next_reward_ad_time = _ba.get_v1_account_misc_read_val_2(
'nextRewardAdTime', None)
if next_reward_ad_time is not None:
next_reward_ad_time = datetime.datetime.utcfromtimestamp(

View file

@ -40,7 +40,7 @@ class IconPicker(popup.PopupWindow):
self._transitioning_out = False
self._icons = [ba.charstr(ba.SpecialChar.LOGO)
] + ba.app.accounts.get_purchased_icons()
] + ba.app.accounts_v1.get_purchased_icons()
count = len(self._icons)
columns = 4
rows = int(math.ceil(float(count) / columns))
@ -137,7 +137,7 @@ class IconPicker(popup.PopupWindow):
def _on_store_press(self) -> None:
from bastd.ui.account import show_sign_in_prompt
from bastd.ui.store.browser import StoreBrowserWindow
if _ba.get_account_state() != 'signed_in':
if _ba.get_v1_account_state() != 'signed_in':
show_sign_in_prompt()
return
self._transition_out()

View file

@ -360,7 +360,7 @@ class KioskWindow(ba.Window):
def _update(self) -> None:
# Kiosk-mode is designed to be used signed-out... try for force
# the issue.
if _ba.get_account_state() == 'signed_in':
if _ba.get_v1_account_state() == 'signed_in':
# _bs.sign_out()
# FIXME: Try to delete player profiles here too.
pass

View file

@ -94,7 +94,7 @@ class LeagueRankButton:
self._smooth_update_timer: Optional[ba.Timer] = None
# Take note of our account state; we'll refresh later if this changes.
self._account_state_num = _ba.get_account_state_num()
self._account_state_num = _ba.get_v1_account_state_num()
self._last_power_ranking_query_time: Optional[float] = None
self._doing_power_ranking_query = False
self.set_position(position)
@ -106,7 +106,7 @@ class LeagueRankButton:
self._update()
# If we've got cached power-ranking data already, apply it.
data = ba.app.accounts.get_cached_league_rank_data()
data = ba.app.accounts_v1.get_cached_league_rank_data()
if data is not None:
self._update_for_league_rank_data(data)
@ -224,7 +224,7 @@ class LeagueRankButton:
in_top = data is not None and data['rank'] is not None
do_percent = False
if data is None or _ba.get_account_state() != 'signed_in':
if data is None or _ba.get_v1_account_state() != 'signed_in':
self._percent = self._rank = None
status_text = '-'
elif in_top:
@ -248,7 +248,8 @@ class LeagueRankButton:
self._percent = self._rank = None
status_text = '-'
else:
our_points = ba.app.accounts.get_league_rank_points(data)
our_points = ba.app.accounts_v1.get_league_rank_points(
data)
progress = float(our_points) / data['scores'][-1][1]
self._percent = int(progress * 100.0)
self._rank = None
@ -327,14 +328,14 @@ class LeagueRankButton:
def _on_power_ranking_query_response(
self, data: Optional[dict[str, Any]]) -> None:
self._doing_power_ranking_query = False
ba.app.accounts.cache_league_rank_data(data)
ba.app.accounts_v1.cache_league_rank_data(data)
self._update_for_league_rank_data(data)
def _update(self) -> None:
cur_time = ba.time(ba.TimeType.REAL)
# If our account state has changed, refresh our UI.
account_state_num = _ba.get_account_state_num()
account_state_num = _ba.get_v1_account_state_num()
if account_state_num != self._account_state_num:
self._account_state_num = account_state_num

View file

@ -118,13 +118,13 @@ class LeagueRankWindow(ba.Window):
self._season: Optional[str] = None
# take note of our account state; we'll refresh later if this changes
self._account_state = _ba.get_account_state()
self._account_state = _ba.get_v1_account_state()
self._refresh()
self._restore_state()
# if we've got cached power-ranking data already, display it
info = ba.app.accounts.get_cached_league_rank_data()
info = ba.app.accounts_v1.get_cached_league_rank_data()
if info is not None:
self._update_for_league_rank_data(info)
@ -155,7 +155,8 @@ class LeagueRankWindow(ba.Window):
resource='coopSelectWindow.activenessAllTimeInfoText'
if self._season == 'a' else 'coopSelectWindow.activenessInfoText',
subs=[('${MAX}',
str(_ba.get_account_misc_read_val('activenessMax', 1.0)))])
str(_ba.get_v1_account_misc_read_val('activenessMax',
1.0)))])
confirm.ConfirmWindow(txt,
cancel_button=False,
width=460,
@ -164,17 +165,15 @@ class LeagueRankWindow(ba.Window):
def _on_pro_mult_press(self) -> None:
from bastd.ui import confirm
txt = ba.Lstr(
resource='coopSelectWindow.proMultInfoText',
subs=[
('${PERCENT}',
str(_ba.get_account_misc_read_val('proPowerRankingBoost',
10))),
('${PRO}',
ba.Lstr(resource='store.bombSquadProNameText',
subs=[('${APP_NAME}', ba.Lstr(resource='titleText'))
]))
])
txt = ba.Lstr(resource='coopSelectWindow.proMultInfoText',
subs=[('${PERCENT}',
str(
_ba.get_v1_account_misc_read_val(
'proPowerRankingBoost', 10))),
('${PRO}',
ba.Lstr(resource='store.bombSquadProNameText',
subs=[('${APP_NAME}',
ba.Lstr(resource='titleText'))]))])
confirm.ConfirmWindow(txt,
cancel_button=False,
width=460,
@ -196,7 +195,7 @@ class LeagueRankWindow(ba.Window):
self._doing_power_ranking_query = False
# important: *only* cache this if we requested the current season..
if data is not None and data.get('s', None) is None:
ba.app.accounts.cache_league_rank_data(data)
ba.app.accounts_v1.cache_league_rank_data(data)
# always store a copy locally though (even for other seasons)
self._league_rank_data = copy.deepcopy(data)
self._update_for_league_rank_data(data)
@ -209,7 +208,7 @@ class LeagueRankWindow(ba.Window):
cur_time = ba.time(ba.TimeType.REAL)
# if our account state has changed, refresh our UI
account_state = _ba.get_account_state()
account_state = _ba.get_v1_account_state()
if account_state != self._account_state:
self._account_state = account_state
self._save_state()
@ -353,7 +352,7 @@ class LeagueRankWindow(ba.Window):
maxwidth=200)
self._activity_mult_button: Optional[ba.Widget]
if _ba.get_account_misc_read_val('act', False):
if _ba.get_v1_account_misc_read_val('act', False):
self._activity_mult_button = ba.buttonwidget(
parent=w_parent,
position=(h2 - 60, v2 + 10),
@ -594,7 +593,7 @@ class LeagueRankWindow(ba.Window):
# pylint: disable=too-many-locals
if not self._root_widget:
return
accounts = ba.app.accounts
accounts = ba.app.accounts_v1
in_top = (data is not None and data['rank'] is not None)
eq_text = self._rdict.powerRankingPointsEqualsText
pts_txt = self._rdict.powerRankingPointsText
@ -603,7 +602,7 @@ class LeagueRankWindow(ba.Window):
finished_season_unranked = False
self._can_do_more_button = True
extra_text = ''
if _ba.get_account_state() != 'signed_in':
if _ba.get_v1_account_state() != 'signed_in':
status_text = '(' + ba.Lstr(
resource='notSignedInText').evaluate() + ')'
elif in_top:
@ -790,7 +789,8 @@ class LeagueRankWindow(ba.Window):
have_pro = False if data is None else data['p']
pro_mult = 1.0 + float(
_ba.get_account_misc_read_val('proPowerRankingBoost', 0.0)) * 0.01
_ba.get_v1_account_misc_read_val('proPowerRankingBoost',
0.0)) * 0.01
# pylint: disable=consider-using-f-string
ba.textwidget(edit=self._pro_mult_text,
text=' -' if

View file

@ -67,15 +67,16 @@ class MainMenuWindow(ba.Window):
self._restore_state()
# Keep an eye on a few things and refresh if they change.
self._account_state = _ba.get_account_state()
self._account_state_num = _ba.get_account_state_num()
self._account_type = (_ba.get_account_type()
self._account_state = _ba.get_v1_account_state()
self._account_state_num = _ba.get_v1_account_state_num()
self._account_type = (_ba.get_v1_account_type()
if self._account_state == 'signed_in' else None)
self._refresh_timer = ba.Timer(1.0,
ba.WeakCall(self._check_refresh),
repeat=True,
timetype=ba.TimeType.REAL)
# noinspection PyUnresolvedReferences
@staticmethod
def _preload_modules() -> None:
"""Preload modules we use (called in bg thread)."""
@ -121,9 +122,9 @@ class MainMenuWindow(ba.Window):
ba.print_exception('Error showing get-remote-app info')
def _get_store_char_tex(self) -> str:
return ('storeCharacterXmas' if _ba.get_account_misc_read_val(
return ('storeCharacterXmas' if _ba.get_v1_account_misc_read_val(
'xmas', False) else
'storeCharacterEaster' if _ba.get_account_misc_read_val(
'storeCharacterEaster' if _ba.get_v1_account_misc_read_val(
'easter', False) else 'storeCharacter')
def _check_refresh(self) -> None:
@ -137,13 +138,13 @@ class MainMenuWindow(ba.Window):
return
store_char_tex = self._get_store_char_tex()
account_state_num = _ba.get_account_state_num()
account_state_num = _ba.get_v1_account_state_num()
if (account_state_num != self._account_state_num
or store_char_tex != self._store_char_tex):
self._store_char_tex = store_char_tex
self._account_state_num = account_state_num
account_state = self._account_state = (_ba.get_account_state())
self._account_type = (_ba.get_account_type()
account_state = self._account_state = (_ba.get_v1_account_state())
self._account_type = (_ba.get_v1_account_type()
if account_state == 'signed_in' else None)
self._save_state()
self._refresh()
@ -212,8 +213,8 @@ class MainMenuWindow(ba.Window):
on_activate_call=self._settings)
# Scattered eggs on easter.
if _ba.get_account_misc_read_val('easter',
False) and not self._in_game:
if _ba.get_v1_account_misc_read_val('easter',
False) and not self._in_game:
icon_size = 34
ba.imagewidget(parent=self._root_widget,
position=(h - icon_size * 0.5 - 15,
@ -309,7 +310,7 @@ class MainMenuWindow(ba.Window):
transition_delay=self._tdelay)
# Scattered eggs on easter.
if _ba.get_account_misc_read_val('easter', False):
if _ba.get_v1_account_misc_read_val('easter', False):
icon_size = 30
ba.imagewidget(parent=self._root_widget,
position=(h - icon_size * 0.5 + 25,
@ -426,8 +427,8 @@ class MainMenuWindow(ba.Window):
self._height = 200.0
enable_account_button = True
account_type_name: Union[str, ba.Lstr]
if _ba.get_account_state() == 'signed_in':
account_type_name = _ba.get_account_display_string()
if _ba.get_v1_account_state() == 'signed_in':
account_type_name = _ba.get_v1_account_display_string()
account_type_icon = None
account_textcolor = (1.0, 1.0, 1.0)
else:
@ -617,8 +618,8 @@ class MainMenuWindow(ba.Window):
enable_sound=account_type_enable_button_sound)
# Scattered eggs on easter.
if _ba.get_account_misc_read_val('easter',
False) and not self._in_game:
if _ba.get_v1_account_misc_read_val('easter',
False) and not self._in_game:
icon_size = 32
ba.imagewidget(parent=self._root_widget,
position=(h - icon_size * 0.5 + 35,
@ -647,8 +648,8 @@ class MainMenuWindow(ba.Window):
self._how_to_play_button = btn
# Scattered eggs on easter.
if _ba.get_account_misc_read_val('easter',
False) and not self._in_game:
if _ba.get_v1_account_misc_read_val('easter',
False) and not self._in_game:
icon_size = 28
ba.imagewidget(parent=self._root_widget,
position=(h - icon_size * 0.5 + 30,
@ -850,7 +851,7 @@ class MainMenuWindow(ba.Window):
# pylint: disable=cyclic-import
from bastd.ui.store.browser import StoreBrowserWindow
from bastd.ui.account import show_sign_in_prompt
if _ba.get_account_state() != 'signed_in':
if _ba.get_v1_account_state() != 'signed_in':
show_sign_in_prompt()
return
self._save_state()

View file

@ -320,8 +320,8 @@ class PartyQueueWindow(ba.Window):
if -1 not in self._dudes_by_id:
dude = self.Dude(
self, response['d'], self._initial_offset, True,
_ba.get_account_misc_read_val_2('resolvedAccountID', None),
_ba.get_account_display_string())
_ba.get_v1_account_misc_read_val_2('resolvedAccountID', None),
_ba.get_v1_account_display_string())
self._dudes_by_id[-1] = dude
self._dudes.append(dude)
else:
@ -341,6 +341,7 @@ class PartyQueueWindow(ba.Window):
self._dudes_by_id[enemy_id].claimed = True
# remove unclaimed dudes from both of our lists
# noinspection PyUnresolvedReferences
self._dudes_by_id = dict([
item for item in list(self._dudes_by_id.items()) if item[1].claimed
])
@ -456,11 +457,11 @@ class PartyQueueWindow(ba.Window):
"""Boost was pressed."""
from bastd.ui import account
from bastd.ui import getcurrency
if _ba.get_account_state() != 'signed_in':
if _ba.get_v1_account_state() != 'signed_in':
account.show_sign_in_prompt()
return
if _ba.get_account_ticket_count() < self._boost_tickets:
if _ba.get_v1_account_ticket_count() < self._boost_tickets:
ba.playsound(ba.getsound('error'))
getcurrency.show_get_tickets_prompt()
return
@ -497,17 +498,17 @@ class PartyQueueWindow(ba.Window):
# Update boost button color based on if we have enough moola.
if self._boost_button is not None:
can_boost = (
(_ba.get_account_state() == 'signed_in'
and _ba.get_account_ticket_count() >= self._boost_tickets))
(_ba.get_v1_account_state() == 'signed_in'
and _ba.get_v1_account_ticket_count() >= self._boost_tickets))
ba.buttonwidget(edit=self._boost_button,
color=(0, 1, 0) if can_boost else (0.7, 0.7, 0.7))
# Update ticket-count.
if self._tickets_text is not None:
if self._boost_button is not None:
if _ba.get_account_state() == 'signed_in':
if _ba.get_v1_account_state() == 'signed_in':
val = ba.charstr(ba.SpecialChar.TICKET) + str(
_ba.get_account_ticket_count())
_ba.get_v1_account_ticket_count())
else:
val = ba.charstr(ba.SpecialChar.TICKET) + '???'
ba.textwidget(edit=self._tickets_text, text=val)
@ -517,7 +518,7 @@ class PartyQueueWindow(ba.Window):
current_time = ba.time(ba.TimeType.REAL)
if (self._last_transaction_time is None
or current_time - self._last_transaction_time >
0.001 * _ba.get_account_misc_read_val('pqInt', 5000)):
0.001 * _ba.get_v1_account_misc_read_val('pqInt', 5000)):
self._last_transaction_time = current_time
_ba.add_transaction(
{

View file

@ -417,6 +417,7 @@ class PlayWindow(ba.Window):
self._restore_state()
# noinspection PyUnresolvedReferences
@staticmethod
def _preload_modules() -> None:
"""Preload modules we use (called in bg thread)."""
@ -446,7 +447,7 @@ class PlayWindow(ba.Window):
# pylint: disable=cyclic-import
from bastd.ui.account import show_sign_in_prompt
from bastd.ui.coop.browser import CoopBrowserWindow
if _ba.get_account_state() != 'signed_in':
if _ba.get_v1_account_state() != 'signed_in':
show_sign_in_prompt()
return
self._save_state()

View file

@ -176,7 +176,7 @@ class PlaylistAddGameWindow(ba.Window):
def _on_get_more_games_press(self) -> None:
from bastd.ui.account import show_sign_in_prompt
from bastd.ui.store.browser import StoreBrowserWindow
if _ba.get_account_state() != 'signed_in':
if _ba.get_v1_account_state() != 'signed_in':
show_sign_in_prompt()
return
StoreBrowserWindow(modal=True,

View file

@ -140,7 +140,7 @@ class PlaylistBrowserWindow(ba.Window):
def _ensure_standard_playlists_exist(self) -> None:
# On new installations, go ahead and create a few playlists
# besides the hard-coded default one:
if not _ba.get_account_misc_val('madeStandardPlaylists', False):
if not _ba.get_v1_account_misc_val('madeStandardPlaylists', False):
_ba.add_transaction({
'type':
'ADD_PLAYLIST',

View file

@ -253,7 +253,7 @@ class PlaylistCustomizeBrowserWindow(ba.Window):
self._update()
def _update(self) -> None:
have = ba.app.accounts.have_pro_options()
have = ba.app.accounts_v1.have_pro_options()
for lock in self._lock_images:
ba.imagewidget(edit=lock, opacity=0.0 if have else 1.0)
@ -383,7 +383,7 @@ class PlaylistCustomizeBrowserWindow(ba.Window):
# pylint: disable=cyclic-import
from bastd.ui.playlist.editcontroller import PlaylistEditController
from bastd.ui.purchase import PurchaseWindow
if not ba.app.accounts.have_pro_options():
if not ba.app.accounts_v1.have_pro_options():
PurchaseWindow(items=['pro'])
return
@ -407,7 +407,7 @@ class PlaylistCustomizeBrowserWindow(ba.Window):
# pylint: disable=cyclic-import
from bastd.ui.playlist.editcontroller import PlaylistEditController
from bastd.ui.purchase import PurchaseWindow
if not ba.app.accounts.have_pro_options():
if not ba.app.accounts_v1.have_pro_options():
PurchaseWindow(items=['pro'])
return
if self._selected_playlist_name is None:
@ -445,7 +445,7 @@ class PlaylistCustomizeBrowserWindow(ba.Window):
from bastd.ui.playlist import share
# Gotta be signed in for this to work.
if _ba.get_account_state() != 'signed_in':
if _ba.get_v1_account_state() != 'signed_in':
ba.screenmessage(ba.Lstr(resource='notSignedInErrorText'),
color=(1, 0, 0))
ba.playsound(ba.getsound('error'))
@ -472,12 +472,12 @@ class PlaylistCustomizeBrowserWindow(ba.Window):
def _share_playlist(self) -> None:
# pylint: disable=cyclic-import
from bastd.ui.purchase import PurchaseWindow
if not ba.app.accounts.have_pro_options():
if not ba.app.accounts_v1.have_pro_options():
PurchaseWindow(items=['pro'])
return
# Gotta be signed in for this to work.
if _ba.get_account_state() != 'signed_in':
if _ba.get_v1_account_state() != 'signed_in':
ba.screenmessage(ba.Lstr(resource='notSignedInErrorText'),
color=(1, 0, 0))
ba.playsound(ba.getsound('error'))
@ -508,7 +508,7 @@ class PlaylistCustomizeBrowserWindow(ba.Window):
# pylint: disable=cyclic-import
from bastd.ui.purchase import PurchaseWindow
from bastd.ui.confirm import ConfirmWindow
if not ba.app.accounts.have_pro_options():
if not ba.app.accounts_v1.have_pro_options():
PurchaseWindow(items=['pro'])
return
@ -534,7 +534,7 @@ class PlaylistCustomizeBrowserWindow(ba.Window):
# pylint: disable=too-many-branches
# pylint: disable=cyclic-import
from bastd.ui.purchase import PurchaseWindow
if not ba.app.accounts.have_pro_options():
if not ba.app.accounts_v1.have_pro_options():
PurchaseWindow(items=['pro'])
return
if self._selected_playlist_name is None:

View file

@ -210,7 +210,7 @@ class PlaylistMapSelectWindow(ba.Window):
def _on_store_press(self) -> None:
from bastd.ui import account
from bastd.ui.store.browser import StoreBrowserWindow
if _ba.get_account_state() != 'signed_in':
if _ba.get_v1_account_state() != 'signed_in':
account.show_sign_in_prompt()
return
StoreBrowserWindow(modal=True,

View file

@ -250,7 +250,7 @@ class PlayOptionsWindow(popup.PopupWindow):
autoselect=True,
textcolor=(0.8, 0.8, 0.8),
label=ba.Lstr(resource='teamNamesColorText'))
if not ba.app.accounts.have_pro():
if not ba.app.accounts_v1.have_pro():
ba.imagewidget(
parent=self.root_widget,
size=(30, 30),
@ -348,8 +348,8 @@ class PlayOptionsWindow(popup.PopupWindow):
from bastd.ui.account import show_sign_in_prompt
from bastd.ui.teamnamescolors import TeamNamesColorsWindow
from bastd.ui.purchase import PurchaseWindow
if not ba.app.accounts.have_pro():
if _ba.get_account_state() != 'signed_in':
if not ba.app.accounts_v1.have_pro():
if _ba.get_v1_account_state() != 'signed_in':
show_sign_in_prompt()
else:
PurchaseWindow(items=['pro'])

View file

@ -15,7 +15,9 @@ if TYPE_CHECKING:
class PopupWindow:
"""A transient window that positions and scales itself for visibility."""
"""A transient window that positions and scales itself for visibility.
Category: UI Classes"""
def __init__(self,
position: tuple[float, float],

Some files were not shown because too many files have changed in this diff Show more