baclassic package

class baclassic.Achievement(name: str, icon: TextureHandle, icon_color: tuple[float, float, float], level_name: str, *, award: int, hard_mode_only: bool = False)[source]

Bases: object

Represents attributes and state for an individual achievement.

announce_completion(sound: bool = True) → None[source]

Kick off an announcement for this achievement’s completion.

property complete: bool

Whether this Achievement is currently complete.

create_display(x: float, y: float, delay: float, *, outdelay: float | None = None, color: Sequence[float] | None = None, style: str = 'post_game') → list[Actor][source]

Create a display for the Achievement.

Shows the Achievement icon, name, and description.

property description: Lstr

Get a babase.Lstr for the Achievement’s brief description.

property description_complete: Lstr

Get a babase.Lstr for the Achievement’s description when complete.

property description_complete_langstr: LangStr

The brief description for this achievement once earned.

This is the LangStr flavor of description_complete; see display_name_langstr.

property description_full: Lstr

Get a babase.Lstr for the Achievement’s full description.

property description_full_complete: Lstr

Get a babase.Lstr for the Achievement’s full desc. when completed.

property description_full_complete_langstr: LangStr

The full description for this achievement once earned.

This is the LangStr flavor of description_full_complete; see display_name_langstr.

property description_full_langstr: LangStr

The full description for this achievement.

This is the LangStr flavor of description_full; see display_name_langstr.

property description_langstr: LangStr

The brief description for this achievement.

This is the LangStr flavor of description; see display_name_langstr.

property display_name: Lstr

Return a babase.Lstr for this Achievement’s name.

property display_name_langstr: LangStr

The localized name for this achievement.

This is the LangStr flavor of display_name. It exists only for the transition; once api 9 support ends, display_name returns this and this property goes away with the removal of api 10.

get_icon_color(complete: bool) → Sequence[float][source]

Return the color tint for this Achievement’s icon.

get_icon_texture(complete: bool) → Texture[source]

Return the icon texture to display for this achievement

get_icon_ui_texture(complete: bool) → bauiv1.Texture[source]

Return the icon texture to display for this achievement

property hard_mode_only: bool

Whether this Achievement is only unlockable in hard-mode.

property level_name: str

The name of the level this achievement applies to.

property name: str

The name of this achievement.

property power_ranking_value: int

Get the power-ranking award value for this achievement.

set_complete(complete: bool = True) → None[source]

Set an achievement’s completed state.

note this only sets local state; use a transaction to actually award achievements.

show_completion_banner(sound: bool = True) → None[source]

Create the banner/sound for an acquired achievement announcement.

class baclassic.AchievementSubsystem[source]

Bases: object

Subsystem for achievement handling.

Access the single shared instance of this class at ‘ba.app.ach’.

achievements_for_coop_level(level_name: str) → list[Achievement][source]

Given a level name, return achievements available for it.

award_local_achievement(achname: str) → None[source]

For non-game-based achievements such as controller-connection.

display_achievement_banner(achname: str) → None[source]

Display a completion banner for an achievement.

(internal)

Used for server-driven achievements.

get_achievement(name: str) → Achievement[source]

Return an Achievement by name.

set_completed_achievements(achs: Sequence[str]) → None[source]

Set the current state of completed achievements.

(internal)

All achievements not included here will be set incomplete.

baclassic.CHEST_APPEARANCE_DISPLAY_INFOS: dict[ClassicChestAppearance, ChestAppearanceDisplayInfo] = {ClassicChestAppearance.L2: ChestAppearanceDisplayInfo(texclosed=TextureHandle(_apvernum=807, _name='textures/chest_icon'), texclosedtint=TextureHandle(_apvernum=807, _name='textures/chest_icon_tint'), texopen=TextureHandle(_apvernum=807, _name='textures/chest_open_icon'), texopentint=TextureHandle(_apvernum=807, _name='textures/chest_open_icon_tint'), color=(0.8, 1.0, 0.93), tint=(0.65, 1.0, 0.8), tint2=(0.65, 1.0, 0.8), tint3=(1.0, 1.0, 1.0)), ClassicChestAppearance.L3: ChestAppearanceDisplayInfo(texclosed=TextureHandle(_apvernum=807, _name='textures/chest_icon'), texclosedtint=TextureHandle(_apvernum=807, _name='textures/chest_icon_tint'), texopen=TextureHandle(_apvernum=807, _name='textures/chest_open_icon'), texopentint=TextureHandle(_apvernum=807, _name='textures/chest_open_icon_tint'), color=(0.75, 0.9, 1.3), tint=(0.7, 1, 1.9), tint2=(0.7, 1, 1.9), tint3=(1.0, 1.0, 1.0)), ClassicChestAppearance.L4: ChestAppearanceDisplayInfo(texclosed=TextureHandle(_apvernum=807, _name='textures/chest_icon'), texclosedtint=TextureHandle(_apvernum=807, _name='textures/chest_icon_tint'), texopen=TextureHandle(_apvernum=807, _name='textures/chest_open_icon'), texopentint=TextureHandle(_apvernum=807, _name='textures/chest_open_icon_tint'), color=(0.7, 1.0, 1.4), tint=(1.4, 1.6, 2.0), tint2=(1.4, 1.6, 2.0), tint3=(1.0, 1.0, 1.0)), ClassicChestAppearance.L5: ChestAppearanceDisplayInfo(texclosed=TextureHandle(_apvernum=807, _name='textures/chest_icon'), texclosedtint=TextureHandle(_apvernum=807, _name='textures/chest_icon_tint'), texopen=TextureHandle(_apvernum=807, _name='textures/chest_open_icon'), texopentint=TextureHandle(_apvernum=807, _name='textures/chest_open_icon_tint'), color=(0.75, 0.5, 2.4), tint=(1.0, 0.8, 0.0), tint2=(1.0, 0.8, 0.0), tint3=(1.0, 1.0, 1.0)), ClassicChestAppearance.L6: ChestAppearanceDisplayInfo(texclosed=TextureHandle(_apvernum=807, _name='textures/chest_icon'), texclosedtint=TextureHandle(_apvernum=807, _name='textures/chest_icon_tint'), texopen=TextureHandle(_apvernum=807, _name='textures/chest_open_icon'), texopentint=TextureHandle(_apvernum=807, _name='textures/chest_open_icon_tint'), color=(1.1, 0.8, 0.0), tint=(2, 2, 2), tint2=(2, 2, 2), tint3=(1.0, 1.0, 1.0))}

Components for the classic BombSquad experience.

This package/feature-set contains functionality related to the classic BombSquad experience. Note that much legacy BombSquad code is still a bit tangled and thus this feature-set is largely inseperable from bascenev1 and bauiv1. Future feature-sets will be designed in a more modular way.

baclassic.CHEST_APPEARANCE_DISPLAY_INFO_DEFAULT: baclassic._chest.ChestAppearanceDisplayInfo = ChestAppearanceDisplayInfo(texclosed=TextureHandle(_apvernum=807, _name='textures/chest_icon'), texclosedtint=TextureHandle(_apvernum=807, _name='textures/chest_icon_tint'), texopen=TextureHandle(_apvernum=807, _name='textures/chest_open_icon'), texopentint=TextureHandle(_apvernum=807, _name='textures/chest_open_icon_tint'), color=(1, 1, 1), tint=(1, 1, 1), tint2=(1, 1, 1), tint3=(1.0, 1.0, 1.0))

Fallback ChestAppearanceDisplayInfo used when a chest’s declared appearance has no entry in CHEST_APPEARANCE_DISPLAY_INFOS.

class baclassic.ChestAppearanceDisplayInfo(texclosed: bascenev1.TextureHandle, texclosedtint: bascenev1.TextureHandle, texopen: bascenev1.TextureHandle, texopentint: bascenev1.TextureHandle, color: tuple[float, float, float], tint: tuple[float, float, float], tint2: tuple[float, float, float], tint3: tuple[float, float, float] = (1.0, 1.0, 1.0))[source]

Bases: object

Info about how to locally display chest appearances.

color: tuple[float, float, float]
texclosed: bascenev1.TextureHandle
texclosedtint: bascenev1.TextureHandle
texopen: bascenev1.TextureHandle
texopentint: bascenev1.TextureHandle
tint: tuple[float, float, float]
tint2: tuple[float, float, float]
tint3: tuple[float, float, float] = (1.0, 1.0, 1.0)

Through the tint texture’s blue channel; white is no effect.

class baclassic.ClassicAppMode[source]

Bases: AppMode

AppMode for the classic BombSquad experience.

classmethod can_handle_intent(intent: AppIntent) → bool[source]

Override this to define indent handling for an app-mode.

get_dev_console_ui_debug_draw_toggles() → list[DevConsoleToggleDef][source]

Define debug-drawing toggles to show up in the UI dev console.

These join the tab’s own in its ‘Debug Drawing’ row (in the console’s expanded view). For ui that the tab itself can’t know about, such as a ui package’s layout guides.

get_dev_console_ui_tab_buttons() → list[DevConsoleButtonDef][source]

Define buttons to show up in the UI dev console.

This can be useful for exposing UI code examples or debugging functionality.

handle_intent(intent: AppIntent) → None[source]

Handle an intent.

new_app_mode_config() → AppModeConfig[source]

Create a fresh config describing how this mode should run.

Called by the framework each time this mode is about to become the active one. Override to return your mode’s own AppModeConfig subclass, built with the mode’s defaults; the framework then offers it to plugins and passes the final result to on_activate().

A fresh instance is built per activation, so nothing can persist between modes (or activations) by construction.

on_activate(config: AppModeConfig) → None[source]

Called when the mode is becoming the active one for the app.

config is the object created by new_app_mode_config() for this activation, possibly amended by plugins. Modes that define their own config type should begin with assert isinstance(config, TheirConfig) to recover the concrete type, then set themselves (and any app-subsystems they use) up from it.

on_app_active_changed() → None[source]

Called when the app’s active state changes while in this app-mode.

This corresponds to the app’s active attr. App-active state becomes false when the app is hidden, minimized, backgrounded, etc. The app-mode may want to take action such as pausing a running game or saving state when this occurs.

On platforms such as mobile where apps get suspended and later silently terminated by the OS, this is likely to be the last reliable place to save state/etc.

To best cover both mobile and desktop style platforms, actions such as saving state should generally happen in response to both on_deactivate() and on_app_active_changed() (when active is False).

on_control_permission_request(request: ControlPermissionRequest, on_result: Callable[[ControlPermission], None]) → None[source]

Ask the user whether something may control the app.

‘Control’ is deliberately broad: running commands, reading this app’s log, and whatever that grows into. Answer via on_result, which may be called later (from a dialog) or immediately (from policy).

The default answers CANNOT_ASK, which is the honest answer for a mode with no way to prompt – the request is then held and re-put once a mode that can ask activates. An app-mode with a UI should override this; one running without a user present (a dedicated server) should answer ALLOW, since nobody is there to ask and the operator already owns the account.

on_deactivate() → None[source]

Called when the mode stops being the active one for the app.

On platforms where the app is explicitly exited (such as desktop PC) this will also be called at app shutdown.

To best cover both mobile and desktop style platforms, actions such as saving state should generally happen in response to both on_deactivate() and on_app_active_changed() (when active is False).

on_engine_did_reset() → None[source]

Called just after classic resets the engine.

This happens at various times such as session switches.

on_engine_will_reset() → None[source]

Called just before classic resets the engine.

This happens at various times such as session switches.

on_main_ui_dismissed() → None[source]

Called when the main-window goes away, leaving none.

However that happens: backing out of a top-level window, a menu press, a resume button and so on. (Navigating between main windows doesn’t count.) The default does nothing.

class baclassic.ClassicAppModeConfig[source]

Bases: AppModeConfig

Describes how a ClassicAppMode should run.

Built fresh (with classic’s defaults) for each activation via ClassicAppMode.new_app_mode_config(); amend attrs here – from a mode subclass’s override of that method or a plugin’s on_app_mode_config() – to alter how the mode sets itself up.

base_assets

The classic-flavored art baked into base’s own draw paths (debris, smoke, VR hands, reflections; see babase.BaseAssetSet). Starts at classic’s real art; reassign any slot to restyle.

scene_assets

The art the scene_v1 node layer draws itself with (see bascenev1.SceneV1AssetSet). Every slot starts at scene_v1’s own art; reassign any to restyle.

ui_assets

The art the ui layer will be drawn with (see bauiv1.UIAssetSet). Starts out as classic’s standard look; reassign any slot to restyle.

class baclassic.ClassicAppSubsystem[source]

Bases: AppSubsystem

Subsystem for classic bombsquad functionality in the app.

The single shared instance of this app can be accessed at babase.app.classic. Note that it is possible for babase.app.classic to be None if the classic package is not present, and futureproof code should handle that case gracefully.

class MusicPlayMode(*values)

Bases: Enum

Influences behavior when playing music.

REGULAR = 'regular'
TEST = 'test'
class V2AuthData(account_id: str, account_tag: str, player_profiles: dict, classic_purchases: list[str] | None, cloud_characters: list[str] | None, expire_time: float)[source]

Bases: object

Authenticated data we store for accepted clients.

account_id: str
account_tag: str
classic_purchases: list[str] | None

List of classic-inventory purchase legacy-ids owned by the connecting account, or None when the master server didn’t provide one (unknown — fall back to the legacy hacky character-list path).

cloud_characters: list[str] | None

The account’s cloud profiles, each as one cloud-composed character json (name + icon + spaz), or None when the master server didn’t provide them (older master or unknown) — in which case the lobby falls back to the legacy player_profiles with the legacy-profile warning.

expire_time: float
player_profiles: dict
class V2AuthRequest(account_id: str, account_tag: str, account_create_time: datetime.datetime, account_total_active_days: int, app_instance_signature: str, address_signature: str, device_signature: str, classic_purchases: list[str] | None = None)[source]

Bases: object

What is passed in to V2 auth handler.

account_create_time: datetime.datetime

When the connecting account was created.

account_id: str

V2 account id of the connecting account (a-XXX).

account_tag: str

Globally unique tag of the connecting account.

account_total_active_days: int

Total number of days the connecting account has been active.

address_signature: str

An abstract value generated from the connecting client’s ip address. Can be used to identify repeat connection attempts/etc.

app_instance_signature: str

An abstract value generated from the connecting client’s app-instance-id. Can be used to identify repeat connection attempts/etc. Note that this value changes each time the client re-launches the app.

classic_purchases: list[str] | None = None

List of classic-inventory purchase legacy-ids owned by the connecting account (e.g. 'characters.kronk'). None when the master server isn’t able to determine this — for example on older master-server versions or when the account has no classic-inventory record. Custom handlers should treat None as “unknown” and not as “owns nothing”.

device_signature: str

An abstract value generated from the connecting client’s device. Can be used to identify repeat connection attempts/etc. This value may change over time for a given device but should be mostly constant.

class V2AuthResponse(allow: bool, error_message: str | None = None)[source]

Bases: object

What a V2 auth handler returns.

allow: bool

Whether to allow this client to enter the server.

error_message: str | None = None

A message to be shown to the client if allow is False. A default (localized) rejection message is shown if this is None. Clients show this text verbatim; it is not translated.

account_name_depiction

The signed-in account’s name as the cloud composes it, a bacommon.depiction.NameDepiction as json fed live by the cloud (empty when signed out or not known yet). Pass it as an image or button widget’s depiction to show the name.

add_coop_practice_level(level: Level) → None[source]

Adds an individual level to the ‘practice’ section in Co-op.

can_show_interstitial() → bool[source]

Is this an appropriate time for an interstitial ad?

character_preview

Shows characters at the master server’s request (someone trying one they are building; see _characterpreview).

cloud_profiles

The primary account’s cloud profiles (synced + cached).

create_in_game_tournament_prize_image(entry: dict[str, Any], index: int, position: tuple[float, float]) → None[source]

Given a tournament entry, return strings for its prize levels.

async default_v2_auth_handler(request: V2AuthRequest) → V2AuthResponse[source]

Default auth handler function. Just allows everyone.

get_input_device_config(inputdevice: InputDevice, default: bool) → tuple[dict, str][source]

Given an input device, return its config dict in the app config.

The dict will be created if it does not exist.

get_input_device_map_hash(inputdevice: InputDevice) → str[source]

Given an input device, return hash based on its raw input values.

get_input_device_mapped_value(device: InputDevice, name: str, default: bool = False) → Any[source]

Return a mapped value for an input device.

This checks the user config and falls back to default values where available.

get_next_tip() → LangStr[source]

Returns the next tip to be displayed.

get_player_colors() → list[tuple[float, float, float]][source]

Return user-selectable player colors.

get_player_profile_colors(profilename: str | None, profiles: dict[str, dict[str, Any]] | None = None) → tuple[tuple[float, float, float], tuple[float, float, float]][source]

Given a profile, return colors for them.

get_player_profile_icon(profilename: str) → str[source]

Given a profile name, returns an icon string for it.

(non-account profiles only)

get_tournament_prize_strings(entry: dict[str, Any], include_tickets: bool) → list[str][source]

Given a tournament entry, return strings for its prize levels.

getcampaign(name: str) → Campaign[source]

Return a campaign by name.

getmaps(playtype: str) → list[str][source]

Return a list of bascenev1.Map types supporting a playtype str.

Maps supporting a given playtype must provide a particular set of features and lend themselves to a certain style of play.

Play Types:

‘melee’

General fighting map. Has one or more ‘spawn’ locations.

‘team_flag’

For games such as Capture The Flag where each team spawns by a flag. Has two or more ‘spawn’ locations, each with a corresponding ‘flag’ location (based on index).

‘single_flag’

For games such as King of the Hill or Keep Away where multiple teams are fighting over a single flag. Has two or more ‘spawn’ locations and 1 ‘flag_default’ location.

‘conquest’

For games such as Conquest where flags are spread throughout the map - has 2+ ‘flag’ locations, 2+ ‘spawn_by_flag’ locations.

‘king_of_the_hill’ - has 2+ ‘spawn’ locations,

1+ ‘flag_default’ locations, and 1+ ‘powerup_spawn’ locations

‘hockey’

For hockey games. Has two ‘goal’ locations, corresponding ‘spawn’ locations, and one ‘flag_default’ location (for where puck spawns)

‘football’

For football games. Has two ‘goal’ locations, corresponding ‘spawn’ locations, and one ‘flag_default’ location (for where flag/ball/etc. spawns)

‘race’

For racing games where players much touch each region in order. Has two or more ‘race_point’ locations.

host_ui

The buttons, pages and party notices a host gives its clients (and shows itself).

invoke_main_menu_ui() → None[source]

Bring up main menu ui.

is_game_unlocked(game: str) → bool[source]

Is a particular game unlocked?

launch_coop_game(game: str, force: bool = False, args: dict | None = None) → bool[source]

High level way to launch a local co-op session.

property legacy_user_agent_string: str

String containing various bits of info about OS/device/etc.

on_app_loading() → None[source]

Called when the app reaches the LOADING state.

Note that subsystems created after the app switches to the loading state will not receive this callback. Subsystems created by plugins are an example of this.

on_app_shutdown() → None[source]

Called when the app enters the SHUTTING_DOWN state.

on_app_suspend() → None[source]

Called when the app enters the SUSPENDED state.

on_app_unsuspend() → None[source]

Called when the app exits the SUSPENDED state.

pause() → None[source]

Pause the game due to a user request or menu popping up.

If there’s a foreground host-activity that says it’s pausable, tell it to pause. Note: we now no longer pause if there are connected clients.

property platform: str

Name of the current platform.

Examples are: ‘mac’, ‘windows’, android’.

preload_map_preview_media() → None[source]

Preload media needed for map preview UIs.

profile_browser_window(transition: str = 'in_right', origin_widget: Widget | None = None) → None[source]

Pop up a browser window from within a game.

required_purchases_for_game(game: str) → list[str][source]

Return which purchase (if any) is required for a game.

resume() → None[source]

Resume the game due to a user request or menu closing.

If there’s a foreground host-activity that’s currently paused, tell it to resume.

return_to_main_menu_session_gracefully(reset_ui: bool = True) → None[source]

Attempt to cleanly get back to the main menu.

run_cpu_benchmark() → None[source]

Kick off a benchmark to test cpu speeds.

run_media_reload_benchmark() → None[source]

Kick off a benchmark to test media reloading speeds.

run_stress_test(*, playlist_type: str = 'Random', playlist_name: str = '__default__', player_count: int = 8, round_duration: int = 30, attract_mode: bool = False, randomize: bool = True, churn: bool = True) → None[source]

Run a stress test.

With randomize False the playlist plays in listed order; with churn False all fake players join at once and stay (both for repeatable performance A/B measurements).

save_ui_state() → None[source]

Store our current place in the UI.

set_tournament_prize_image(entry: dict[str, Any], index: int, image: Widget) → None[source]

Given a tournament entry, return strings for its prize levels.

property subplatform: str

String for subplatform.

Can be empty. For the ‘android’ platform, subplatform may be ‘google’, ‘amazon’, etc.

v2_auth_handler: Callable[[ClassicAppSubsystem.V2AuthRequest], Awaitable[ClassicAppSubsystem.V2AuthResponse]]

V2 authentication handler.

To customize who is allowed in your server, assign your own custom handler function to this attribute. This will be called for all connecting clients before they are allowed in the game. Note that protocol must be set to 36 or newer and authenticate_clients must be enabled. For servers you can set those values in the server config.

class baclassic.ClientEffectContext(targets: EffectTargets | None = None)[source]

Bases: object

What client-effects are running alongside, if anything.

Effects that need a context (animating things, say) do nothing without the relevant piece.

targets: EffectTargets | None = None

Targets for bacommon.clienteffect.KeyframeAnimation.

class baclassic.EffectTarget(*args, **kwargs)[source]

Bases: Protocol

Something client-effects can animate.

Implemented by whatever UI system the effects run alongside (a doc-ui window’s widgets, say); effects know nothing about it beyond this.

set_animation_state(*, offset: tuple[float, float], scale: float, opacity: float | None) → None[source]

Show the target offset and scaled from its rest state.

opacity is absolute; None for its resting opacity.

class baclassic.EffectTargets(*args, **kwargs)[source]

Bases: Protocol

A set of animatable targets client-effects can look up by id.

get_target(target_id: str) → EffectTarget | None[source]

Return the target for an id, if there is a live one.

class baclassic.HostUIButton(label: str | babase.LangStr | LangStrSpec, route: DocUIRoute, placement: Literal['in_game_menu', 'party_menu'] = 'in_game_menu', layout: bacommon.docui.v2.WindowLayout = WindowLayout.WIDE_TALL)[source]

Bases: object

A button a host puts on its clients’ (and its own) ui.

Pressing it opens the page for route in a doc-ui window, which the host’s HostUIHandler serves.

label: str | babase.LangStr | LangStrSpec

The label. Plain text is shown as-is; a language-string must come from one of the host’s own asset-packages (clients refuse anything else).

layout: bacommon.docui.v2.WindowLayout = 'wt'

The window layout to open the page in. Wide and tall by default, since host pages often list everyone in the party.

placement: Literal['in_game_menu', 'party_menu'] = 'in_game_menu'

Where the button goes.

route: DocUIRoute

The page the button opens.

class baclassic.HostUIHandler[source]

Bases: object

Serves the doc-ui pages a host’s buttons open.

Adding your own pages takes three pieces: routes naming the pages, a handler serving them, and buttons opening them. A complete plugin-sized example:

from dataclasses import dataclass

from efro.dataclassio import ioprepped
from bacommon.docui.routes import DocUIRoute, family_members
import bacommon.docui.v2 as dui2
from bacommon.langstr import LangStrSpecValue
import babase
import baclassic

# 1. Routes: one family class, one dataclass per page (fields
# are the page's args).
class MyRoute(DocUIRoute):
    @classmethod
    def get_route_types(cls) -> tuple[type[DocUIRoute], ...]:
        return family_members(AnyMyRoute)

@ioprepped
@dataclass
class Rules(MyRoute, path='/myplugin/rules'):
    pass

AnyMyRoute = Rules

# 2. A handler serving them (everything else goes to super()).
class MyHandler(baclassic.HostUIHandler):
    def fulfill_request(self, request, client_id):
        try:
            route = MyRoute.from_request(request)
        except Exception:
            return super().fulfill_request(request, client_id)
        assert isinstance(route, Rules)
        lit = LangStrSpecValue.literal
        return dui2.Response(
            page=dui2.Page(
                title=lit('Rules'),
                rows=[
                    dui2.ButtonRow(
                        title=lit('Be nice.'),
                        buttons=[
                            dui2.Button(
                                label=lit('Ok'),
                                action=dui2.Local(close_window=True),
                            )
                        ],
                    )
                ],
            )
        )

# 3. Plug in: the handler, plus a button opening the page
# (alongside the default ones).
host_ui = babase.app.classic.host_ui
host_ui.handler = MyHandler()
host_ui.set_buttons(
    host_ui.get_buttons()
    + [baclassic.HostUIButton(label='Rules', route=Rules())]
)

To change the default stats page rather than add one, see baclassic.HostUIStatsPage. If something goes wrong building a page (an exception, or returning something that isn’t a response), the error is logged here and the client gets its standard error page.

Keeping pages cheap. fulfill_request() runs in the logic thread, the one the game itself runs in, so time spent there is time the game stands still. A page built from a handful of values costs next to nothing, and that’s the common case. A page that grows (big tables, sorting or filtering many records, file or network access) should do the heavy part elsewhere:

  1. Make fulfill_request() async.

  2. Gather what you need from the game first, on the logic thread (game objects such as players and activities may only be touched there), into plain values.

  3. Build the page from those values in a background thread with await self.run_in_bg(build_page, values). That function must not touch game objects or ui.

Handlers that spend more than about 10 ms on the logic thread get a warning saying so. (Turning the page into wire data and compressing it already happens in the background.)

fulfill_request(request: bacommon.docui.v2.Request, client_id: int) → bacommon.docui.v2.Response | Awaitable[bacommon.docui.v2.Response][source]

Return the page for a request from a client.

client_id is -1 for the host’s own requests. Runs in the logic thread, so game state can be read freely; return an awaitable (or make this async) to answer asynchronously. Remember that requests come from clients and are untrusted input.

async run_in_bg(call: Callable[[], T]) → T[source]

Run a call in a background thread; return its result.

For the heavy parts of building a page (see the class docs). call must not touch game objects or ui; hand it plain values gathered beforehand (functools.partial or a lambda does that nicely). Time spent here doesn’t count toward the slow-handler warning.

class baclassic.HostUIKickVote(target_client_id: int, target_name: str, starter_client_id: int, end_time: float, votes: dict[int, bool]=<factory>)[source]

Bases: object

A kick vote in progress.

end_time: float

App-time the vote runs out.

starter_client_id: int

Who started the vote.

target_client_id: int

Who would be kicked.

target_name: str

Their name (as of the vote starting, so it outlives them).

votes: dict[int, bool]

Votes so far, by client id (True for yes). The starter votes yes.

class baclassic.HostUIKickVotes[source]

Bases: object

The standard kick vote, run by classic hosts.

Lives at app.classic.host_ui.kick_votes. Clients pick someone in their party window and choose to kick them, which opens this vote’s page. There they can start a vote; everyone else (who can see host pages: build 23067 and up) gets a ‘Kick X?’ notice with the party icon and ‘…’ button flashing, and votes on the page the ‘…’ menu leads to. Admins (see host_ui.admins) and the host kick without a vote and can’t be kicked.

Customizing. The methods come in sections, in this order:

  • The rules – every number and decision (vote length, minimum voters, votes required, cooldowns, ban time, who’s an admin, who votes, why a vote can’t start).

  • What people see – every bit of text; return your own (plain strings are fine) to reword anything.

  • Running votes – the machinery that applies the rules.

  • The page – the vote’s doc-ui page.

Subclass this, copy in just the methods you want to change (those in the first two sections use only public api), and assign an instance to host_ui.kick_votes; for example, for longer votes:

class MyKickVotes(baclassic.HostUIKickVotes):
    def get_vote_duration(self) -> float:
        return 60.0

babase.app.classic.host_ui.kick_votes = MyKickVotes()

To replace kick votes with something else entirely, turn this off (set_enabled(), or the server config’s enable_default_kick_voting) and point baclassic.HostUISubsystem.set_kick_route() at a page of your own.

The vote’s notice uses the single party notice clients have; the most recently posted notice wins, so a notice posted during a vote replaces the vote’s (and the vote’s ending replaces whatever is showing then).

Votes starting and ending, and kicks, are logged (ba.net, at info level); a kick that fails is a warning.

build_page(client_id: int, target_client_id: int | None) → Response[source]

The vote’s page, for one client.

target_client_id is who they chose to kick, if they came here to kick someone. Refreshes itself every second. Its text comes from the methods above; the layout is engine machinery, so override those rather than this.

cast_vote(client_id: int, yes: bool) → None[source]

Record a vote (ignored if there’s no vote or they voted).

property enabled: bool

Whether kick voting is on.

end_vote(passed: bool, quiet: bool = False) → None[source]

End the vote in progress, kicking its target if it passed.

quiet ends it without telling anyone how it went (the target left, say).

get_admin_kick_text() → str | LangStr[source]

The button an admin (or the host) kicks with instead.

get_ban_time() → int[source]

Seconds a kicked client is kept from rejoining.

get_buttons(client_id: int) → list[HostUIButton][source]

Buttons a client gets from us (on top of the host’s own).

While a vote runs, a button to its page in everyone’s party menu.

get_failed_text() → str | LangStr[source]

The notice everyone gets when a vote fails.

get_failed_vote_delay(starter: bool) → float[source]

Seconds before someone can start a vote after one fails.

starter is whether they started the failed one.

get_host_ui() → HostUISubsystem[source]

The host-ui subsystem we run on (app.classic.host_ui).

get_kick_route(client_id: int) → DocUIRoute[source]

The page clients get when they choose to kick someone.

get_kicked_text(name: str) → str | LangStr[source]

The screen message everyone gets when someone is kicked.

get_minimum_clients() → int[source]

Fewest voting clients (not counting the host) for a vote.

A host that is also playing doesn’t count, so require one more then (two of three clients in a 2-on-2 shouldn’t be able to kick the third).

get_new_client_delay() → float[source]

Seconds a client must have been here to start a vote.

get_no_text() → str | LangStr[source]

The no button’s label.

get_no_vote_text() → str | LangStr[source]

The page when there’s no vote and nobody to kick.

get_notice_duration() → float[source]

How long the failed-vote notice shows (seconds).

get_question_text(name: str) → str | LangStr[source]

‘Kick X?’: the voters’ notice, and the page’s question.

get_refusal(starter_client_id: int, target_client_id: int) → KickVoteRefusal | None[source]

Why a client can’t start a vote to kick another (None: they can).

Admins (and the host) skip this; they kick outright.

get_refusal_text(refusal: KickVoteRefusal, starter_client_id: int) → str | LangStr[source]

Why a vote can’t start, as told to whoever tried.

get_start_text() → str | LangStr[source]

The button that starts a vote.

get_start_wait(client_id: int) → float[source]

Seconds until a client may start a vote (0 if they may now).

get_started_text(name: str) → str | LangStr[source]

What the starter and the target are told.

get_time_left_text(ends: datetime) → str | LangStr[source]

The countdown under the vote’s buttons.

ends is when the vote runs out; clients count down to it live. (Passing a datetime to a string’s duration param does that; see bacommon.langstr.LangStrSpecTimeTarget.)

get_title_text() → str | LangStr[source]

The vote page’s title (also its party-menu button’s label).

get_vote_duration() → float[source]

How long a vote runs (seconds).

get_voters() → list[int][source]

Who votes: clients that can see host pages, plus a playing host.

(Clients too old for host pages can’t take part at all.)

get_votes_needed() → int[source]

More yes votes the vote in progress needs (0 if none running).

get_votes_needed_text(count: int) → str | LangStr[source]

The tally: how many more yes votes are needed.

get_votes_required(client_count: int) → int[source]

Yes votes needed to kick, given the voting clients present.

client_count doesn’t include the host; a host that is also playing counts as one more participant (and votes too).

get_voting_client_count() → int[source]

How many voters are clients (that is, not the host).

get_yes_text() → str | LangStr[source]

The yes button’s label.

is_admin(client_id: int) → bool[source]

Whether a client is an admin (or the host; client id -1).

kick(client_id: int, name: str | None = None) → None[source]

Kick a client (banning them for get_ban_time()).

set_enabled(enabled: bool) → None[source]

Turn kick voting on or off.

Also offers (or stops offering) this vote’s page as where clients go to kick someone (see baclassic.HostUISubsystem.set_kick_route()).

start_vote(starter_client_id: int, target_client_id: int) → KickVoteRefusal | None[source]

Start a vote (or, for admins, just kick).

Returns why it couldn’t be started, if it couldn’t.

update() → None[source]

Check the vote in progress: end it if it passed or failed.

Runs whenever a vote is cast, and every second.

vote: HostUIKickVote | None

The vote in progress, if any.

class baclassic.HostUIStatsColumn(title: str | babase.LangStr, width: float, value: Callable[[bascenev1.SessionPlayer, HostUIStatsContext], str | babase.LangStr | bacommon.depiction.Depiction | None], hide_when_empty: bool = False)[source]

Bases: object

One column of values in the stats table, right of the player.

hide_when_empty: bool = False

Leave the column out when nobody has a value for it (rather than showing a column of dashes).

title: str | babase.LangStr

The column’s title. A language-string must come from one of the host’s own asset-packages (clients refuse anything else).

value: Callable[[bascenev1.SessionPlayer, HostUIStatsContext], str | babase.LangStr | bacommon.depiction.Depiction | None]

A player’s value – text, a language-string (each client shows it in its own language), a depiction, or None for none (drawn as a dash). Called on the logic thread for every player each time the page is built, so keep it cheap.

width: float

The column’s width (the whole table is about 800 wide).

class baclassic.HostUIStatsContext(account_ids: dict[int, str], records: dict[int, PlayerRecord])[source]

Bases: object

What stats column values may need beyond the player itself.

Gathered once per page, so columns can look things up cheaply.

account_ids: dict[int, str]

Verified account ids by client id (-1 being the host’s own); see bascenev1.get_game_roster().

record(player: SessionPlayer) → PlayerRecord | None[source]

A player’s stats record, if they have one.

Records’ accum* values are the current game’s (despite the name; they reset as each game starts) and are what score screens show; the plain ones run for the whole series.

records: dict[int, PlayerRecord]

The session’s stats records (what between-round score screens show), by their players’ ids; see record().

class baclassic.HostUIStatsPage[source]

Bases: object

The stats page classic hosts serve: a table of the party.

One row per player who has joined (icon and name, then a value per column), in scoreboard order during games. Serve your own variation by subclassing this, overriding get_columns() (or anything else), and assigning an instance to app.classic.host_ui.stats_page; for example, to add a column:

class MyStatsPage(baclassic.HostUIStatsPage):
    def get_columns(self) -> list[baclassic.HostUIStatsColumn]:
        return super().get_columns() + [
            baclassic.HostUIStatsColumn(
                'Streak', 70.0, self.streak_value
            )
        ]

    def streak_value(self, player, context):
        rec = context.record(player)
        return None if rec is None else str(rec.streak)

babase.app.classic.host_ui.stats_page = MyStatsPage()

Subclass rather than copy the whole class: copy in just the methods you want to change. Those hold only the decisions (title, refresh rate, columns, which players and in what order, cell values) and use only public api, while the default columns’ localized titles, the table layout and wire details stay in the engine, so they keep working as it updates. A table under a route of your own works the same way: subclass this, and return self.build(route) from your baclassic.HostUIHandler. Pages are built on the logic thread; see baclassic.HostUIHandler for keeping big ones cheap.

account_column() → HostUIStatsColumn[source]

The default Account column (hidden when nobody has one).

account_value(player: bascenev1.SessionPlayer, context: HostUIStatsContext) → bacommon.depiction.Depiction | None[source]

A player’s verified account, as its name depiction.

Known for the host’s own account and for clients that joined with v2 auth; so with client auth off, usually None.

build(route: DocUIRoute) → bacommon.docui.v2.Response[source]

Build the page; route is the one being served.

(It is what the page refreshes itself with.) Gathers players and values through the methods above and lays out the table; the layout is engine machinery, so override those rather than this.

deaths_column() → HostUIStatsColumn[source]

The default Deaths column.

deaths_value(player: SessionPlayer, context: HostUIStatsContext) → str | None[source]

A player’s deaths this game.

get_columns() → list[HostUIStatsColumn][source]

The table’s columns, left to right (after the player’s).

get_players(session: Session) → list[SessionPlayer][source]

The players to list (before sorting).

By default everyone who has joined; players still choosing a profile in the lobby have no name or look yet.

get_refresh_interval() → float | None[source]

How often (seconds) the page refreshes while open (None: never).

get_title() → str | LangStr[source]

The page’s title.

kills_column() → HostUIStatsColumn[source]

The default Kills column.

kills_value(player: SessionPlayer, context: HostUIStatsContext) → str | None[source]

A player’s kills this game.

ping_column() → HostUIStatsColumn[source]

The default Ping column.

ping_value(player: SessionPlayer, context: HostUIStatsContext) → str | None[source]

A player’s connection’s ping in ms (as the host measures it).

score_column() → HostUIStatsColumn[source]

The default Score column.

score_value(player: SessionPlayer, context: HostUIStatsContext) → str | None[source]

A player’s score this game (as score screens show it).

sort_players(players: list[SessionPlayer], context: HostUIStatsContext, session: Session) → list[SessionPlayer][source]

Players in the order to list them.

By default scoreboard order during a game: grouped by team in team games, then by score (highest first), ties going to more kills and then fewer deaths, players without stats yet last. Outside a game (nobody has stats), join order.

class baclassic.HostUISubsystem[source]

Bases: object

Host-defined ui, from both ends.

Access the single shared instance of this class via app.classic.host_ui.

As a host: set the buttons everyone gets with set_buttons() (or one client’s with set_client_buttons()), post notices with post_notice(), flash things with flash_party_icon() and flash_party_menu(), and serve pages by assigning handler. Clients are kept up to date automatically, joiners included.

As a client, this holds what our host has declared to us.

admins: set[str]

V2 account ids (a-XXX) of this host’s admins. Admins can’t be kicked by vote, and kick without one.

clear_notice(*, clients: Sequence[int] | None = None) → None[source]

Take down the notice (for some clients, or everyone).

client_has_host_ui(client_id: int) → bool[source]

Whether a client can see host buttons, notices, and pages.

(Clients predating them can’t.) True for the host (-1) while it has ui.

flash_party_icon(duration: float, *, clients: Sequence[int] | None = None) → None[source]

Flash the party icon for a while (0 to stop).

flash_party_menu(duration: float, *, clients: Sequence[int] | None = None) → None[source]

Flash the party window’s ‘…’ button for a while (0 to stop).

get_account_name_depiction(account_id: str) → bacommon.depiction.Depiction | None[source]

An account’s name as it draws in game, if we know it.

Known for our own account, and for clients that joined with v2 auth (so it is verified by the master server); see get_game_roster() for each client’s account id. None otherwise.

get_buttons() → list[HostUIButton][source]

The buttons every client gets (unless overridden per client).

get_client_account_id(client_id: int) → str | None[source]

A client’s verified v2 account id (a-XXX), if it has one.

get_client_ids() → list[int][source]

Ids of the clients connected to us (not including the host).

get_client_name(client_id: int) → str | None[source]

A client’s name as the party list shows it (None if not here).

handler: HostUIHandler

Serves the pages host buttons open.

kick_votes: HostUIKickVotes

The standard kick vote (assign a subclass to customize it).

post_notice(text: str | LangStr | LangStrSpec, duration: float, *, clients: Sequence[int] | None = None) → None[source]

Show a notice below the party icon for a while.

Replaces any notice showing (the most recently posted notice wins). clients limits it to some clients (-1 is the host); None means everyone. A language-string must come from one of the host’s own asset-packages.

refresh_clients() → None[source]

Send everyone their current state again.

For when something the state is built from changed without going through this class (such as the buttons baclassic.HostUIKickVotes.get_buttons() adds).

set_buttons(buttons: Sequence[HostUIButton]) → None[source]

Set the buttons every client gets.

Clients with their own (see set_client_buttons()) keep theirs.

set_client_buttons(client_id: int, buttons: Sequence[HostUIButton] | None) → None[source]

Give one client its own set of buttons (None to undo).

Use -1 for the host’s own ui. Forgotten when the client leaves.

set_kick_route(get_route: Callable[[int], DocUIRoute] | None) → None[source]

Set where clients go when they choose to kick someone.

get_route is called with a client id and returns the page for kicking them (which your handler serves); clients offer kicking on party members only while one is set. None stops offering it. By default the standard kick vote’s page (see baclassic.HostUIKickVotes).

stats_page: HostUIStatsPage

The default stats page (assign a subclass to customize it).

class baclassic.KickVoteRefusal(*values)[source]

Bases: Enum

Why a kick vote can’t be started.

ADMIN = 'admin'

The host and admins can’t be kicked.

COOLDOWN = 'cooldown'

The starter has to wait a while (they just joined, or a vote just failed).

DISABLED = 'disabled'

Kick voting is turned off.

IN_PROGRESS = 'in_progress'

A vote is already running.

NOT_ENOUGH_PLAYERS = 'not_enough_players'

Too few voters for a vote to mean anything.

NOT_PRESENT = 'not_present'

The player isn’t here (anymore).

SELF = 'self'

Nobody can kick themselves.

class baclassic.MusicPlayer[source]

Bases: object

Wrangles soundtrack music playback.

Music can be played either through the game itself or via a platform-specific external player.

on_app_shutdown() → None[source]

Called on final app shutdown.

on_play(entry: Any) → None[source]

Called when a new song/playlist/etc should be played.

on_select_entry(callback: Callable[[Any], None], current_entry: Any, selection_target_name: str) → bauiv1.MainWindow[source]

Present a GUI to select an entry.

The callback should be called with a valid entry or None to signify that the default soundtrack should be used..

on_set_volume(volume: float) → None[source]

Called when the volume should be changed.

on_stop() → None[source]

Called when the music should stop.

play(entry: Any) → None[source]

Play provided entry.

select_entry(callback: Callable[[Any], None], current_entry: Any, selection_target_name: str) → bauiv1.MainWindow[source]

Summons a UI to select a new soundtrack entry.

set_volume(volume: float) → None[source]

Set player volume (value should be between 0 and 1).

shutdown() → None[source]

Shutdown music playback completely.

stop() → None[source]

Stop any playback that is occurring.

baclassic.reload_hooks() → None[source]

Reload functions and other objects held by the native layer. Call this if you replace things in a hooks module to get the native layer to see your changes.

Submodules

baclassic.macmusicapp module

Music playback functionality using the Mac Music (formerly iTunes) app.

class baclassic.macmusicapp.MacMusicAppMusicPlayer[source]

Bases: MusicPlayer

A music-player that utilizes the macOS Music.app for playback.

Allows selecting playlists as entries.

get_playlists(callback: Callable) → None[source]

Asynchronously fetch the list of available iTunes playlists.

on_app_shutdown() → None[source]

Called on final app shutdown.

on_play(entry: Any) → None[source]

Called when a new song/playlist/etc should be played.

on_select_entry(callback: Callable[[Any], None], current_entry: Any, selection_target_name: str) → bauiv1.MainWindow[source]

Present a GUI to select an entry.

The callback should be called with a valid entry or None to signify that the default soundtrack should be used..

on_set_volume(volume: float) → None[source]

Called when the volume should be changed.

on_stop() → None[source]

Called when the music should stop.

baclassic.osmusic module

Music playback using OS functionality exposed through the C++ layer.

class baclassic.osmusic.OSMusicPlayer[source]

Bases: MusicPlayer

Music player that talks to internal C++ layer for functionality.

(internal)

classmethod get_valid_music_file_extensions() → list[str][source]

Return file extensions for types playable on this device.

on_app_shutdown() → None[source]

Called on final app shutdown.

on_play(entry: Any) → None[source]

Called when a new song/playlist/etc should be played.

on_select_entry(callback: Callable[[Any], None], current_entry: Any, selection_target_name: str) → bauiv1.MainWindow[source]

Present a GUI to select an entry.

The callback should be called with a valid entry or None to signify that the default soundtrack should be used..

on_set_volume(volume: float) → None[source]

Called when the volume should be changed.

on_stop() → None[source]

Called when the music should stop.