bascenev1

Gameplay-centric api for classic BombSquad.

  1# Released under the MIT License. See LICENSE for details.
  2#
  3"""Gameplay-centric api for classic BombSquad."""
  4
  5# ba_meta require api 9
  6
  7# The stuff we expose here at the top level is our 'public' api for use
  8# from other modules/packages. Code *within* this package should import
  9# things from this package's submodules directly to reduce the chance of
 10# dependency loops. The exception is TYPE_CHECKING blocks and
 11# annotations since those aren't evaluated at runtime.
 12
 13import logging
 14
 15# Aside from our own stuff, we also bundle a number of things from ba or
 16# other modules; the goal is to let most simple mods rely solely on this
 17# module to keep things simple.
 18
 19from efro.util import set_canonical_module_names
 20from babase import (
 21    add_clean_frame_callback,
 22    app,
 23    App,
 24    AppIntent,
 25    AppIntentDefault,
 26    AppIntentExec,
 27    AppMode,
 28    apptime,
 29    AppTime,
 30    apptimer,
 31    AppTimer,
 32    Call,
 33    ContextError,
 34    ContextRef,
 35    displaytime,
 36    DisplayTime,
 37    displaytimer,
 38    DisplayTimer,
 39    existing,
 40    fade_screen,
 41    get_remote_app_name,
 42    increment_analytics_count,
 43    InputType,
 44    is_point_in_box,
 45    lock_all_input,
 46    Lstr,
 47    NodeNotFoundError,
 48    normalized_color,
 49    NotFoundError,
 50    PlayerNotFoundError,
 51    Plugin,
 52    pushcall,
 53    safecolor,
 54    screenmessage,
 55    set_analytics_screen,
 56    SessionNotFoundError,
 57    storagename,
 58    timestring,
 59    UIScale,
 60    unlock_all_input,
 61    Vec3,
 62    WeakCall,
 63)
 64
 65from _bascenev1 import (
 66    ActivityData,
 67    basetime,
 68    basetimer,
 69    BaseTimer,
 70    camerashake,
 71    capture_gamepad_input,
 72    capture_keyboard_input,
 73    chatmessage,
 74    client_info_query_response,
 75    CollisionMesh,
 76    connect_to_party,
 77    Data,
 78    disconnect_client,
 79    disconnect_from_host,
 80    emitfx,
 81    end_host_scanning,
 82    get_chat_messages,
 83    get_connection_to_host_info,
 84    get_connection_to_host_info_2,
 85    get_foreground_host_activity,
 86    get_foreground_host_session,
 87    get_game_port,
 88    get_game_roster,
 89    get_local_active_input_devices_count,
 90    get_public_party_enabled,
 91    get_public_party_max_size,
 92    get_random_names,
 93    get_replay_speed_exponent,
 94    get_ui_input_device,
 95    getactivity,
 96    getcollisionmesh,
 97    getdata,
 98    getinputdevice,
 99    getmesh,
100    getnodes,
101    getsession,
102    getsound,
103    gettexture,
104    have_connected_clients,
105    have_touchscreen_input,
106    host_scan_cycle,
107    InputDevice,
108    is_in_replay,
109    is_replay_paused,
110    ls_input_devices,
111    ls_objects,
112    Material,
113    Mesh,
114    new_host_session,
115    new_replay_session,
116    newactivity,
117    newnode,
118    Node,
119    pause_replay,
120    printnodes,
121    protocol_version,
122    release_gamepad_input,
123    release_keyboard_input,
124    reset_random_player_names,
125    resume_replay,
126    seek_replay,
127    broadcastmessage,
128    SessionData,
129    SessionPlayer,
130    set_admins,
131    set_authenticate_clients,
132    set_debug_speed_exponent,
133    set_enable_default_kick_voting,
134    set_internal_music,
135    set_map_bounds,
136    set_master_server_source,
137    set_public_party_enabled,
138    set_public_party_max_size,
139    set_public_party_name,
140    set_public_party_public_address_ipv4,
141    set_public_party_public_address_ipv6,
142    set_public_party_queue_enabled,
143    set_public_party_stats_url,
144    set_replay_speed_exponent,
145    set_touchscreen_editing,
146    Sound,
147    Texture,
148    time,
149    timer,
150    Timer,
151)
152from bascenev1._activity import Activity
153from bascenev1._activitytypes import JoinActivity, ScoreScreenActivity
154from bascenev1._actor import Actor
155from bascenev1._campaign import init_campaigns, Campaign
156from bascenev1._collision import Collision, getcollision
157from bascenev1._coopgame import CoopGameActivity
158from bascenev1._coopsession import CoopSession
159from bascenev1._debug import print_live_object_warnings
160from bascenev1._dependency import (
161    Dependency,
162    DependencyComponent,
163    DependencySet,
164    AssetPackage,
165)
166from bascenev1._dualteamsession import DualTeamSession
167from bascenev1._freeforallsession import FreeForAllSession
168from bascenev1._gameactivity import GameActivity
169from bascenev1._gameresults import GameResults
170from bascenev1._gameutils import (
171    animate,
172    animate_array,
173    BaseTime,
174    cameraflash,
175    GameTip,
176    get_trophy_string,
177    show_damage_count,
178    Time,
179)
180from bascenev1._level import Level
181from bascenev1._lobby import Lobby, Chooser
182from bascenev1._map import (
183    get_filtered_map_name,
184    get_map_class,
185    get_map_display_string,
186    Map,
187    register_map,
188)
189from bascenev1._messages import (
190    CelebrateMessage,
191    DeathType,
192    DieMessage,
193    DropMessage,
194    DroppedMessage,
195    FreezeMessage,
196    HitMessage,
197    ImpactDamageMessage,
198    OutOfBoundsMessage,
199    PickedUpMessage,
200    PickUpMessage,
201    PlayerDiedMessage,
202    PlayerProfilesChangedMessage,
203    ShouldShatterMessage,
204    StandMessage,
205    ThawMessage,
206    UNHANDLED,
207)
208from bascenev1._multiteamsession import (
209    MultiTeamSession,
210    DEFAULT_TEAM_COLORS,
211    DEFAULT_TEAM_NAMES,
212)
213from bascenev1._music import MusicType, setmusic
214from bascenev1._net import HostInfo
215from bascenev1._nodeactor import NodeActor
216from bascenev1._powerup import get_default_powerup_distribution
217from bascenev1._profile import (
218    get_player_colors,
219    get_player_profile_icon,
220    get_player_profile_colors,
221)
222from bascenev1._player import PlayerInfo, Player, EmptyPlayer, StandLocation
223from bascenev1._playlist import (
224    get_default_free_for_all_playlist,
225    get_default_teams_playlist,
226    filter_playlist,
227)
228from bascenev1._powerup import PowerupMessage, PowerupAcceptMessage
229from bascenev1._score import ScoreType, ScoreConfig
230from bascenev1._settings import (
231    BoolSetting,
232    ChoiceSetting,
233    FloatChoiceSetting,
234    FloatSetting,
235    IntChoiceSetting,
236    IntSetting,
237    Setting,
238)
239from bascenev1._session import (
240    Session,
241    set_player_rejoin_cooldown,
242    set_max_players_override,
243)
244from bascenev1._stats import PlayerScoredMessage, PlayerRecord, Stats
245from bascenev1._team import SessionTeam, Team, EmptyTeam
246from bascenev1._teamgame import TeamGameActivity
247
248__all__ = [
249    'Activity',
250    'ActivityData',
251    'Actor',
252    'animate',
253    'animate_array',
254    'add_clean_frame_callback',
255    'app',
256    'App',
257    'AppIntent',
258    'AppIntentDefault',
259    'AppIntentExec',
260    'AppMode',
261    'AppTime',
262    'apptime',
263    'apptimer',
264    'AppTimer',
265    'AssetPackage',
266    'basetime',
267    'BaseTime',
268    'basetimer',
269    'BaseTimer',
270    'BoolSetting',
271    'Call',
272    'cameraflash',
273    'camerashake',
274    'Campaign',
275    'capture_gamepad_input',
276    'capture_keyboard_input',
277    'CelebrateMessage',
278    'chatmessage',
279    'ChoiceSetting',
280    'Chooser',
281    'client_info_query_response',
282    'Collision',
283    'CollisionMesh',
284    'connect_to_party',
285    'ContextError',
286    'ContextRef',
287    'CoopGameActivity',
288    'CoopSession',
289    'Data',
290    'DeathType',
291    'DEFAULT_TEAM_COLORS',
292    'DEFAULT_TEAM_NAMES',
293    'Dependency',
294    'DependencyComponent',
295    'DependencySet',
296    'DieMessage',
297    'disconnect_client',
298    'disconnect_from_host',
299    'displaytime',
300    'DisplayTime',
301    'displaytimer',
302    'DisplayTimer',
303    'DropMessage',
304    'DroppedMessage',
305    'DualTeamSession',
306    'emitfx',
307    'EmptyPlayer',
308    'EmptyTeam',
309    'end_host_scanning',
310    'existing',
311    'fade_screen',
312    'filter_playlist',
313    'FloatChoiceSetting',
314    'FloatSetting',
315    'FreeForAllSession',
316    'FreezeMessage',
317    'GameActivity',
318    'GameResults',
319    'GameTip',
320    'get_chat_messages',
321    'get_connection_to_host_info',
322    'get_connection_to_host_info_2',
323    'get_default_free_for_all_playlist',
324    'get_default_teams_playlist',
325    'get_default_powerup_distribution',
326    'get_filtered_map_name',
327    'get_foreground_host_activity',
328    'get_foreground_host_session',
329    'get_game_port',
330    'get_game_roster',
331    'get_game_roster',
332    'get_local_active_input_devices_count',
333    'get_map_class',
334    'get_map_display_string',
335    'get_player_colors',
336    'get_player_profile_colors',
337    'get_player_profile_icon',
338    'get_public_party_enabled',
339    'get_public_party_max_size',
340    'get_random_names',
341    'get_remote_app_name',
342    'get_replay_speed_exponent',
343    'get_trophy_string',
344    'get_ui_input_device',
345    'getactivity',
346    'getcollision',
347    'getcollisionmesh',
348    'getdata',
349    'getinputdevice',
350    'getmesh',
351    'getnodes',
352    'getsession',
353    'getsound',
354    'gettexture',
355    'have_connected_clients',
356    'have_touchscreen_input',
357    'HitMessage',
358    'HostInfo',
359    'host_scan_cycle',
360    'ImpactDamageMessage',
361    'increment_analytics_count',
362    'init_campaigns',
363    'InputDevice',
364    'InputType',
365    'IntChoiceSetting',
366    'IntSetting',
367    'is_in_replay',
368    'is_point_in_box',
369    'is_replay_paused',
370    'JoinActivity',
371    'Level',
372    'Lobby',
373    'lock_all_input',
374    'ls_input_devices',
375    'ls_objects',
376    'Lstr',
377    'Map',
378    'Material',
379    'Mesh',
380    'MultiTeamSession',
381    'MusicType',
382    'new_host_session',
383    'new_replay_session',
384    'newactivity',
385    'newnode',
386    'Node',
387    'NodeActor',
388    'NodeNotFoundError',
389    'normalized_color',
390    'NotFoundError',
391    'OutOfBoundsMessage',
392    'pause_replay',
393    'PickedUpMessage',
394    'PickUpMessage',
395    'Player',
396    'PlayerDiedMessage',
397    'PlayerProfilesChangedMessage',
398    'PlayerInfo',
399    'PlayerNotFoundError',
400    'PlayerRecord',
401    'PlayerScoredMessage',
402    'Plugin',
403    'PowerupAcceptMessage',
404    'PowerupMessage',
405    'print_live_object_warnings',
406    'printnodes',
407    'protocol_version',
408    'pushcall',
409    'register_map',
410    'release_gamepad_input',
411    'release_keyboard_input',
412    'reset_random_player_names',
413    'resume_replay',
414    'seek_replay',
415    'safecolor',
416    'screenmessage',
417    'ScoreConfig',
418    'ScoreScreenActivity',
419    'ScoreType',
420    'SessionNotFoundError',
421    'broadcastmessage',
422    'Session',
423    'SessionData',
424    'SessionPlayer',
425    'SessionTeam',
426    'set_admins',
427    'set_analytics_screen',
428    'set_authenticate_clients',
429    'set_debug_speed_exponent',
430    'set_debug_speed_exponent',
431    'set_enable_default_kick_voting',
432    'set_internal_music',
433    'set_map_bounds',
434    'set_master_server_source',
435    'set_public_party_enabled',
436    'set_public_party_max_size',
437    'set_public_party_name',
438    'set_public_party_public_address_ipv4',
439    'set_public_party_public_address_ipv6',
440    'set_public_party_queue_enabled',
441    'set_public_party_stats_url',
442    'set_player_rejoin_cooldown',
443    'set_max_players_override',
444    'set_replay_speed_exponent',
445    'set_touchscreen_editing',
446    'setmusic',
447    'Setting',
448    'ShouldShatterMessage',
449    'show_damage_count',
450    'Sound',
451    'StandLocation',
452    'StandMessage',
453    'Stats',
454    'storagename',
455    'Team',
456    'TeamGameActivity',
457    'Texture',
458    'ThawMessage',
459    'time',
460    'Time',
461    'timer',
462    'Timer',
463    'timestring',
464    'UIScale',
465    'UNHANDLED',
466    'unlock_all_input',
467    'Vec3',
468    'WeakCall',
469]
470
471# We want stuff here to show up as bascenev1.Foo instead of
472# bascenev1._submodule.Foo.
473set_canonical_module_names(globals())
474
475# Sanity check: we want to keep ballistica's dependencies and
476# bootstrapping order clearly defined; let's check a few particular
477# modules to make sure they never directly or indirectly import us
478# before their own execs complete.
479if __debug__:
480    for _mdl in 'babase', '_babase':
481        if not hasattr(__import__(_mdl), '_REACHED_END_OF_MODULE'):
482            logging.warning(
483                '%s was imported before %s finished importing;'
484                ' should not happen.',
485                __name__,
486                _mdl,
487            )
class Activity(bascenev1.DependencyComponent, typing.Generic[~PlayerT, ~TeamT]):
 26class Activity(DependencyComponent, Generic[PlayerT, TeamT]):
 27    """Units of execution wrangled by a bascenev1.Session.
 28
 29    Examples of Activities include games, score-screens, cutscenes, etc.
 30    A bascenev1.Session has one 'current' Activity at any time, though
 31    their existence can overlap during transitions.
 32    """
 33
 34    # pylint: disable=too-many-public-methods
 35
 36    settings_raw: dict[str, Any]
 37    """The settings dict passed in when the activity was made.
 38       This attribute is deprecated and should be avoided when possible;
 39       activities should pull all values they need from the 'settings' arg
 40       passed to the Activity __init__ call."""
 41
 42    teams: list[TeamT]
 43    """The list of bascenev1.Team-s in the Activity. This gets populated just
 44       before on_begin() is called and is updated automatically as players
 45       join or leave the game. (at least in free-for-all mode where every
 46       player gets their own team; in teams mode there are always 2 teams
 47       regardless of the player count)."""
 48
 49    players: list[PlayerT]
 50    """The list of bascenev1.Player-s in the Activity. This gets populated
 51       just before on_begin() is called and is updated automatically as
 52       players join or leave the game."""
 53
 54    announce_player_deaths = False
 55    """Whether to print every time a player dies. This can be pertinent
 56       in games such as Death-Match but can be annoying in games where it
 57       doesn't matter."""
 58
 59    is_joining_activity = False
 60    """Joining activities are for waiting for initial player joins.
 61       They are treated slightly differently than regular activities,
 62       mainly in that all players are passed to the activity at once
 63       instead of as each joins."""
 64
 65    allow_pausing = False
 66    """Whether game-time should still progress when in menus/etc."""
 67
 68    allow_kick_idle_players = True
 69    """Whether idle players can potentially be kicked (should not happen in
 70       menus/etc)."""
 71
 72    use_fixed_vr_overlay = False
 73    """In vr mode, this determines whether overlay nodes (text, images, etc)
 74       are created at a fixed position in space or one that moves based on
 75       the current map. Generally this should be on for games and off for
 76       transitions/score-screens/etc. that persist between maps."""
 77
 78    slow_motion = False
 79    """If True, runs in slow motion and turns down sound pitch."""
 80
 81    inherits_slow_motion = False
 82    """Set this to True to inherit slow motion setting from previous
 83       activity (useful for transitions to avoid hitches)."""
 84
 85    inherits_music = False
 86    """Set this to True to keep playing the music from the previous activity
 87       (without even restarting it)."""
 88
 89    inherits_vr_camera_offset = False
 90    """Set this to true to inherit VR camera offsets from the previous
 91       activity (useful for preventing sporadic camera movement
 92       during transitions)."""
 93
 94    inherits_vr_overlay_center = False
 95    """Set this to true to inherit (non-fixed) VR overlay positioning from
 96       the previous activity (useful for prevent sporadic overlay jostling
 97       during transitions)."""
 98
 99    inherits_tint = False
100    """Set this to true to inherit screen tint/vignette colors from the
101       previous activity (useful to prevent sudden color changes during
102       transitions)."""
103
104    allow_mid_activity_joins: bool = True
105    """Whether players should be allowed to join in the middle of this
106       activity. Note that Sessions may not allow mid-activity-joins even
107       if the activity says its ok."""
108
109    transition_time = 0.0
110    """If the activity fades or transitions in, it should set the length of
111       time here so that previous activities will be kept alive for that
112       long (avoiding 'holes' in the screen)
113       This value is given in real-time seconds."""
114
115    can_show_ad_on_death = False
116    """Is it ok to show an ad after this activity ends before showing
117       the next activity?"""
118
119    def __init__(self, settings: dict):
120        """Creates an Activity in the current bascenev1.Session.
121
122        The activity will not be actually run until
123        bascenev1.Session.setactivity is called. 'settings' should be a
124        dict of key/value pairs specific to the activity.
125
126        Activities should preload as much of their media/etc as possible in
127        their constructor, but none of it should actually be used until they
128        are transitioned in.
129        """
130        super().__init__()
131
132        # Create our internal engine data.
133        self._activity_data = _bascenev1.register_activity(self)
134
135        assert isinstance(settings, dict)
136        assert _bascenev1.getactivity() is self
137
138        self._globalsnode: bascenev1.Node | None = None
139
140        # Player/Team types should have been specified as type args;
141        # grab those.
142        self._playertype: type[PlayerT]
143        self._teamtype: type[TeamT]
144        self._setup_player_and_team_types()
145
146        # FIXME: Relocate or remove the need for this stuff.
147        self.paused_text: bascenev1.Actor | None = None
148
149        self._session = weakref.ref(_bascenev1.getsession())
150
151        # Preloaded data for actors, maps, etc; indexed by type.
152        self.preloads: dict[type, Any] = {}
153
154        # Hopefully can eventually kill this; activities should
155        # validate/store whatever settings they need at init time
156        # (in a more type-safe way).
157        self.settings_raw = settings
158
159        self._has_transitioned_in = False
160        self._has_begun = False
161        self._has_ended = False
162        self._activity_death_check_timer: bascenev1.AppTimer | None = None
163        self._expired = False
164        self._delay_delete_players: list[PlayerT] = []
165        self._delay_delete_teams: list[TeamT] = []
166        self._players_that_left: list[weakref.ref[PlayerT]] = []
167        self._teams_that_left: list[weakref.ref[TeamT]] = []
168        self._transitioning_out = False
169
170        # A handy place to put most actors; this list is pruned of dead
171        # actors regularly and these actors are insta-killed as the activity
172        # is dying.
173        self._actor_refs: list[bascenev1.Actor] = []
174        self._actor_weak_refs: list[weakref.ref[bascenev1.Actor]] = []
175        self._last_prune_dead_actors_time = babase.apptime()
176        self._prune_dead_actors_timer: bascenev1.Timer | None = None
177
178        self.teams = []
179        self.players = []
180
181        self.lobby = None
182        self._stats: bascenev1.Stats | None = None
183        self._customdata: dict | None = {}
184
185    def __del__(self) -> None:
186        # If the activity has been run then we should have already cleaned
187        # it up, but we still need to run expire calls for un-run activities.
188        if not self._expired:
189            with babase.ContextRef.empty():
190                self._expire()
191
192        # Inform our owner that we officially kicked the bucket.
193        if self._transitioning_out:
194            session = self._session()
195            if session is not None:
196                babase.pushcall(
197                    babase.Call(
198                        session.transitioning_out_activity_was_freed,
199                        self.can_show_ad_on_death,
200                    )
201                )
202
203    @property
204    def context(self) -> bascenev1.ContextRef:
205        """A context-ref pointing at this activity."""
206        return self._activity_data.context()
207
208    @property
209    def globalsnode(self) -> bascenev1.Node:
210        """The 'globals' bascenev1.Node for the activity. This contains various
211        global controls and values.
212        """
213        node = self._globalsnode
214        if not node:
215            raise babase.NodeNotFoundError()
216        return node
217
218    @property
219    def stats(self) -> bascenev1.Stats:
220        """The stats instance accessible while the activity is running.
221
222        If access is attempted before or after, raises a
223        bascenev1.NotFoundError.
224        """
225        if self._stats is None:
226            raise babase.NotFoundError()
227        return self._stats
228
229    def on_expire(self) -> None:
230        """Called when your activity is being expired.
231
232        If your activity has created anything explicitly that may be retaining
233        a strong reference to the activity and preventing it from dying, you
234        should clear that out here. From this point on your activity's sole
235        purpose in life is to hit zero references and die so the next activity
236        can begin.
237        """
238
239    @property
240    def customdata(self) -> dict:
241        """Entities needing to store simple data with an activity can put it
242        here. This dict will be deleted when the activity expires, so contained
243        objects generally do not need to worry about handling expired
244        activities.
245        """
246        assert not self._expired
247        assert isinstance(self._customdata, dict)
248        return self._customdata
249
250    @property
251    def expired(self) -> bool:
252        """Whether the activity is expired.
253
254        An activity is set as expired when shutting down.
255        At this point no new nodes, timers, etc should be made,
256        run, etc, and the activity should be considered a 'zombie'.
257        """
258        return self._expired
259
260    @property
261    def playertype(self) -> type[PlayerT]:
262        """The type of bascenev1.Player this Activity is using."""
263        return self._playertype
264
265    @property
266    def teamtype(self) -> type[TeamT]:
267        """The type of bascenev1.Team this Activity is using."""
268        return self._teamtype
269
270    def set_has_ended(self, val: bool) -> None:
271        """(internal)"""
272        self._has_ended = val
273
274    def expire(self) -> None:
275        """Begin the process of tearing down the activity.
276
277        (internal)
278        """
279
280        # Create an app-timer that watches a weak-ref of this activity
281        # and reports any lingering references keeping it alive.
282        # We store the timer on the activity so as soon as the activity dies
283        # it gets cleaned up.
284        with babase.ContextRef.empty():
285            ref = weakref.ref(self)
286            self._activity_death_check_timer = babase.AppTimer(
287                5.0,
288                babase.Call(self._check_activity_death, ref, [0]),
289                repeat=True,
290            )
291
292        # Run _expire in an empty context; nothing should be happening in
293        # there except deleting things which requires no context.
294        # (plus, _expire() runs in the destructor for un-run activities
295        # and we can't properly provide context in that situation anyway; might
296        # as well be consistent).
297        if not self._expired:
298            with babase.ContextRef.empty():
299                self._expire()
300        else:
301            raise RuntimeError(
302                f'destroy() called when already expired for {self}.'
303            )
304
305    def retain_actor(self, actor: bascenev1.Actor) -> None:
306        """Add a strong-reference to a bascenev1.Actor to this Activity.
307
308        The reference will be lazily released once bascenev1.Actor.exists()
309        returns False for the Actor. The bascenev1.Actor.autoretain() method
310        is a convenient way to access this same functionality.
311        """
312        if __debug__:
313            from bascenev1._actor import Actor
314
315            assert isinstance(actor, Actor)
316        self._actor_refs.append(actor)
317
318    def add_actor_weak_ref(self, actor: bascenev1.Actor) -> None:
319        """Add a weak-reference to a bascenev1.Actor to the bascenev1.Activity.
320
321        (called by the bascenev1.Actor base class)
322        """
323        if __debug__:
324            from bascenev1._actor import Actor
325
326            assert isinstance(actor, Actor)
327        self._actor_weak_refs.append(weakref.ref(actor))
328
329    @property
330    def session(self) -> bascenev1.Session:
331        """The bascenev1.Session this bascenev1.Activity belongs to.
332
333        Raises a :class:`~bascenev1.SessionNotFoundError` if the Session
334        no longer exists.
335        """
336        session = self._session()
337        if session is None:
338            raise babase.SessionNotFoundError()
339        return session
340
341    def on_player_join(self, player: PlayerT) -> None:
342        """Called when a new bascenev1.Player has joined the Activity.
343
344        (including the initial set of Players)
345        """
346
347    def on_player_leave(self, player: PlayerT) -> None:
348        """Called when a bascenev1.Player is leaving the Activity."""
349
350    def on_team_join(self, team: TeamT) -> None:
351        """Called when a new bascenev1.Team joins the Activity.
352
353        (including the initial set of Teams)
354        """
355
356    def on_team_leave(self, team: TeamT) -> None:
357        """Called when a bascenev1.Team leaves the Activity."""
358
359    def on_transition_in(self) -> None:
360        """Called when the Activity is first becoming visible.
361
362        Upon this call, the Activity should fade in backgrounds,
363        start playing music, etc. It does not yet have access to players
364        or teams, however. They remain owned by the previous Activity
365        up until bascenev1.Activity.on_begin() is called.
366        """
367
368    def on_transition_out(self) -> None:
369        """Called when your activity begins transitioning out.
370
371        Note that this may happen at any time even if bascenev1.Activity.end()
372        has not been called.
373        """
374
375    def on_begin(self) -> None:
376        """Called once the previous Activity has finished transitioning out.
377
378        At this point the activity's initial players and teams are filled in
379        and it should begin its actual game logic.
380        """
381
382    def handlemessage(self, msg: Any) -> Any:
383        """General message handling; can be passed any message object."""
384        del msg  # Unused arg.
385        return UNHANDLED
386
387    def has_transitioned_in(self) -> bool:
388        """Return whether bascenev1.Activity.on_transition_in() has run."""
389        return self._has_transitioned_in
390
391    def has_begun(self) -> bool:
392        """Return whether bascenev1.Activity.on_begin() has run."""
393        return self._has_begun
394
395    def has_ended(self) -> bool:
396        """Return whether the activity has commenced ending."""
397        return self._has_ended
398
399    def is_transitioning_out(self) -> bool:
400        """Return whether bascenev1.Activity.on_transition_out() has run."""
401        return self._transitioning_out
402
403    def transition_in(self, prev_globals: bascenev1.Node | None) -> None:
404        """Called by Session to kick off transition-in.
405
406        (internal)
407        """
408        assert not self._has_transitioned_in
409        self._has_transitioned_in = True
410
411        # Set up the globals node based on our settings.
412        with self.context:
413            glb = self._globalsnode = _bascenev1.newnode('globals')
414
415            # Now that it's going to be front and center,
416            # set some global values based on what the activity wants.
417            glb.use_fixed_vr_overlay = self.use_fixed_vr_overlay
418            glb.allow_kick_idle_players = self.allow_kick_idle_players
419            if self.inherits_slow_motion and prev_globals is not None:
420                glb.slow_motion = prev_globals.slow_motion
421            else:
422                glb.slow_motion = self.slow_motion
423            if self.inherits_music and prev_globals is not None:
424                glb.music_continuous = True  # Prevent restarting same music.
425                glb.music = prev_globals.music
426                glb.music_count += 1
427            if self.inherits_vr_camera_offset and prev_globals is not None:
428                glb.vr_camera_offset = prev_globals.vr_camera_offset
429            if self.inherits_vr_overlay_center and prev_globals is not None:
430                glb.vr_overlay_center = prev_globals.vr_overlay_center
431                glb.vr_overlay_center_enabled = (
432                    prev_globals.vr_overlay_center_enabled
433                )
434
435            # If they want to inherit tint from the previous self.
436            if self.inherits_tint and prev_globals is not None:
437                glb.tint = prev_globals.tint
438                glb.vignette_outer = prev_globals.vignette_outer
439                glb.vignette_inner = prev_globals.vignette_inner
440
441            # Start pruning our various things periodically.
442            self._prune_dead_actors()
443            self._prune_dead_actors_timer = _bascenev1.Timer(
444                5.17, self._prune_dead_actors, repeat=True
445            )
446
447            _bascenev1.timer(13.3, self._prune_delay_deletes, repeat=True)
448
449            # Also start our low-level scene running.
450            self._activity_data.start()
451
452            try:
453                self.on_transition_in()
454            except Exception:
455                logging.exception('Error in on_transition_in for %s.', self)
456
457        # Tell the C++ layer that this activity is the main one, so it uses
458        # settings from our globals, directs various events to us, etc.
459        self._activity_data.make_foreground()
460
461    def transition_out(self) -> None:
462        """Called by the Session to start us transitioning out."""
463        assert not self._transitioning_out
464        self._transitioning_out = True
465        with self.context:
466            try:
467                self.on_transition_out()
468            except Exception:
469                logging.exception('Error in on_transition_out for %s.', self)
470
471    def begin(self, session: bascenev1.Session) -> None:
472        """Begin the activity.
473
474        (internal)
475        """
476
477        assert not self._has_begun
478
479        # Inherit stats from the session.
480        self._stats = session.stats
481
482        # Add session's teams in.
483        for team in session.sessionteams:
484            self.add_team(team)
485
486        # Add session's players in.
487        for player in session.sessionplayers:
488            self.add_player(player)
489
490        self._has_begun = True
491
492        # Let the activity do its thing.
493        with self.context:
494            # Note: do we want to catch errors here?
495            # Currently I believe we wind up canceling the
496            # activity launch; just wanna be sure that is intentional.
497            self.on_begin()
498
499    def end(
500        self, results: Any = None, delay: float = 0.0, force: bool = False
501    ) -> None:
502        """Commences Activity shutdown and delivers results to the Session.
503
504        'delay' is the time delay before the Activity actually ends
505        (in seconds). Further calls to end() will be ignored up until
506        this time, unless 'force' is True, in which case the new results
507        will replace the old.
508        """
509
510        # Ask the session to end us.
511        self.session.end_activity(self, results, delay, force)
512
513    def create_player(self, sessionplayer: bascenev1.SessionPlayer) -> PlayerT:
514        """Create the Player instance for this Activity.
515
516        Subclasses can override this if the activity's player class
517        requires a custom constructor; otherwise it will be called with
518        no args. Note that the player object should not be used at this
519        point as it is not yet fully wired up; wait for
520        bascenev1.Activity.on_player_join() for that.
521        """
522        del sessionplayer  # Unused.
523        player = self._playertype()
524        return player
525
526    def create_team(self, sessionteam: bascenev1.SessionTeam) -> TeamT:
527        """Create the Team instance for this Activity.
528
529        Subclasses can override this if the activity's team class
530        requires a custom constructor; otherwise it will be called with
531        no args. Note that the team object should not be used at this
532        point as it is not yet fully wired up; wait for on_team_join()
533        for that.
534        """
535        del sessionteam  # Unused.
536        team = self._teamtype()
537        return team
538
539    def add_player(self, sessionplayer: bascenev1.SessionPlayer) -> None:
540        """(internal)"""
541        assert sessionplayer.sessionteam is not None
542        sessionplayer.resetinput()
543        sessionteam = sessionplayer.sessionteam
544        assert sessionplayer in sessionteam.players
545        team = sessionteam.activityteam
546        assert team is not None
547        sessionplayer.setactivity(self)
548        with self.context:
549            sessionplayer.activityplayer = player = self.create_player(
550                sessionplayer
551            )
552            player.postinit(sessionplayer)
553
554            assert player not in team.players
555            team.players.append(player)
556            assert player in team.players
557
558            assert player not in self.players
559            self.players.append(player)
560            assert player in self.players
561
562            try:
563                self.on_player_join(player)
564            except Exception:
565                logging.exception('Error in on_player_join for %s.', self)
566
567    def remove_player(self, sessionplayer: bascenev1.SessionPlayer) -> None:
568        """Remove a player from the Activity while it is running.
569
570        (internal)
571        """
572        assert not self.expired
573
574        player: Any = sessionplayer.activityplayer
575        assert isinstance(player, self._playertype)
576        team: Any = sessionplayer.sessionteam.activityteam
577        assert isinstance(team, self._teamtype)
578
579        assert player in team.players
580        team.players.remove(player)
581        assert player not in team.players
582
583        assert player in self.players
584        self.players.remove(player)
585        assert player not in self.players
586
587        # This should allow our bascenev1.Player instance to die.
588        # Complain if that doesn't happen.
589        # verify_object_death(player)
590
591        with self.context:
592            try:
593                self.on_player_leave(player)
594            except Exception:
595                logging.exception('Error in on_player_leave for %s.', self)
596            try:
597                player.leave()
598            except Exception:
599                logging.exception('Error on leave for %s in %s.', player, self)
600
601            self._reset_session_player_for_no_activity(sessionplayer)
602
603        # Add the player to a list to keep it around for a while. This is
604        # to discourage logic from firing on player object death, which
605        # may not happen until activity end if something is holding refs
606        # to it.
607        self._delay_delete_players.append(player)
608        self._players_that_left.append(weakref.ref(player))
609
610    def add_team(self, sessionteam: bascenev1.SessionTeam) -> None:
611        """Add a team to the Activity
612
613        (internal)
614        """
615        assert not self.expired
616
617        with self.context:
618            sessionteam.activityteam = team = self.create_team(sessionteam)
619            team.postinit(sessionteam)
620            self.teams.append(team)
621            try:
622                self.on_team_join(team)
623            except Exception:
624                logging.exception('Error in on_team_join for %s.', self)
625
626    def remove_team(self, sessionteam: bascenev1.SessionTeam) -> None:
627        """Remove a team from a Running Activity
628
629        (internal)
630        """
631        assert not self.expired
632        assert sessionteam.activityteam is not None
633
634        team: Any = sessionteam.activityteam
635        assert isinstance(team, self._teamtype)
636
637        assert team in self.teams
638        self.teams.remove(team)
639        assert team not in self.teams
640
641        with self.context:
642            # Make a decent attempt to persevere if user code breaks.
643            try:
644                self.on_team_leave(team)
645            except Exception:
646                logging.exception('Error in on_team_leave for %s.', self)
647            try:
648                team.leave()
649            except Exception:
650                logging.exception('Error on leave for %s in %s.', team, self)
651
652            sessionteam.activityteam = None
653
654        # Add the team to a list to keep it around for a while. This is
655        # to discourage logic from firing on team object death, which
656        # may not happen until activity end if something is holding refs
657        # to it.
658        self._delay_delete_teams.append(team)
659        self._teams_that_left.append(weakref.ref(team))
660
661    def _reset_session_player_for_no_activity(
662        self, sessionplayer: bascenev1.SessionPlayer
663    ) -> None:
664        # Let's be extra-defensive here: killing a node/input-call/etc
665        # could trigger user-code resulting in errors, but we would still
666        # like to complete the reset if possible.
667        try:
668            sessionplayer.setnode(None)
669        except Exception:
670            logging.exception(
671                'Error resetting SessionPlayer node on %s for %s.',
672                sessionplayer,
673                self,
674            )
675        try:
676            sessionplayer.resetinput()
677        except Exception:
678            logging.exception(
679                'Error resetting SessionPlayer input on %s for %s.',
680                sessionplayer,
681                self,
682            )
683
684        # These should never fail I think...
685        sessionplayer.setactivity(None)
686        sessionplayer.activityplayer = None
687
688    # noinspection PyUnresolvedReferences
689    def _setup_player_and_team_types(self) -> None:
690        """Pull player and team types from our typing.Generic params."""
691
692        # TODO: There are proper calls for pulling these in Python 3.8;
693        # should update this code when we adopt that.
694        # NOTE: If we get Any as PlayerT or TeamT (generally due
695        # to no generic params being passed) we automatically use the
696        # base class types, but also warn the user since this will mean
697        # less type safety for that class. (its better to pass the base
698        # player/team types explicitly vs. having them be Any)
699        if not TYPE_CHECKING:
700            self._playertype = type(self).__orig_bases__[-1].__args__[0]
701            if not isinstance(self._playertype, type):
702                self._playertype = Player
703                print(
704                    f'ERROR: {type(self)} was not passed a Player'
705                    f' type argument; please explicitly pass bascenev1.Player'
706                    f' if you do not want to override it.'
707                )
708            self._teamtype = type(self).__orig_bases__[-1].__args__[1]
709            if not isinstance(self._teamtype, type):
710                self._teamtype = Team
711                print(
712                    f'ERROR: {type(self)} was not passed a Team'
713                    f' type argument; please explicitly pass bascenev1.Team'
714                    f' if you do not want to override it.'
715                )
716        assert issubclass(self._playertype, Player)
717        assert issubclass(self._teamtype, Team)
718
719    @classmethod
720    def _check_activity_death(
721        cls, activity_ref: weakref.ref[Activity], counter: list[int]
722    ) -> None:
723        """Sanity check to make sure an Activity was destroyed properly.
724
725        Receives a weakref to a bascenev1.Activity which should have torn
726        itself down due to no longer being referenced anywhere. Will complain
727        and/or print debugging info if the Activity still exists.
728        """
729        try:
730            activity = activity_ref()
731            print(
732                'ERROR: Activity is not dying when expected:',
733                activity,
734                '(warning ' + str(counter[0] + 1) + ')',
735            )
736            print(
737                'This means something is still strong-referencing it.\n'
738                'Check out methods such as efro.debug.printrefs() to'
739                ' help debug this sort of thing.'
740            )
741            # Note: no longer calling gc.get_referrers() here because it's
742            # usage can bork stuff. (see notes at top of efro.debug)
743            counter[0] += 1
744            if counter[0] == 4:
745                print('Killing app due to stuck activity... :-(')
746                babase.quit()
747
748        except Exception:
749            logging.exception('Error on _check_activity_death.')
750
751    def _expire(self) -> None:
752        """Put the activity in a state where it can be garbage-collected.
753
754        This involves clearing anything that might be holding a reference
755        to it, etc.
756        """
757        assert not self._expired
758        self._expired = True
759
760        try:
761            self.on_expire()
762        except Exception:
763            logging.exception('Error in Activity on_expire() for %s.', self)
764
765        try:
766            self._customdata = None
767        except Exception:
768            logging.exception('Error clearing customdata for %s.', self)
769
770        # Don't want to be holding any delay-delete refs at this point.
771        self._prune_delay_deletes()
772
773        self._expire_actors()
774        self._expire_players()
775        self._expire_teams()
776
777        # This will kill all low level stuff: Timers, Nodes, etc., which
778        # should clear up any remaining refs to our Activity and allow us
779        # to die peacefully.
780        try:
781            self._activity_data.expire()
782        except Exception:
783            logging.exception('Error expiring _activity_data for %s.', self)
784
785    def _expire_actors(self) -> None:
786        # Expire all Actors.
787        for actor_ref in self._actor_weak_refs:
788            actor = actor_ref()
789            if actor is not None:
790                babase.verify_object_death(actor)
791                try:
792                    actor.on_expire()
793                except Exception:
794                    logging.exception(
795                        'Error in Actor.on_expire() for %s.', actor_ref()
796                    )
797
798    def _expire_players(self) -> None:
799        # Issue warnings for any players that left the game but don't
800        # get freed soon.
801        for ex_player in (p() for p in self._players_that_left):
802            if ex_player is not None:
803                babase.verify_object_death(ex_player)
804
805        for player in self.players:
806            # This should allow our bascenev1.Player instance to be freed.
807            # Complain if that doesn't happen.
808            babase.verify_object_death(player)
809
810            try:
811                player.expire()
812            except Exception:
813                logging.exception('Error expiring %s.', player)
814
815            # Reset the SessionPlayer to a not-in-an-activity state.
816            try:
817                sessionplayer = player.sessionplayer
818                self._reset_session_player_for_no_activity(sessionplayer)
819            except babase.SessionPlayerNotFoundError:
820                # Conceivably, someone could have held on to a Player object
821                # until now whos underlying SessionPlayer left long ago...
822                pass
823            except Exception:
824                logging.exception('Error expiring %s.', player)
825
826    def _expire_teams(self) -> None:
827        # Issue warnings for any teams that left the game but don't
828        # get freed soon.
829        for ex_team in (p() for p in self._teams_that_left):
830            if ex_team is not None:
831                babase.verify_object_death(ex_team)
832
833        for team in self.teams:
834            # This should allow our bascenev1.Team instance to die.
835            # Complain if that doesn't happen.
836            babase.verify_object_death(team)
837
838            try:
839                team.expire()
840            except Exception:
841                logging.exception('Error expiring %s.', team)
842
843            try:
844                sessionteam = team.sessionteam
845                sessionteam.activityteam = None
846            except babase.SessionTeamNotFoundError:
847                # It is expected that Team objects may last longer than
848                # the SessionTeam they came from (game objects may hold
849                # team references past the point at which the underlying
850                # player/team has left the game)
851                pass
852            except Exception:
853                logging.exception('Error expiring Team %s.', team)
854
855    def _prune_delay_deletes(self) -> None:
856        self._delay_delete_players.clear()
857        self._delay_delete_teams.clear()
858
859        # Clear out any dead weak-refs.
860        self._teams_that_left = [
861            t for t in self._teams_that_left if t() is not None
862        ]
863        self._players_that_left = [
864            p for p in self._players_that_left if p() is not None
865        ]
866
867    def _prune_dead_actors(self) -> None:
868        self._last_prune_dead_actors_time = babase.apptime()
869
870        # Prune our strong refs when the Actor's exists() call gives False
871        self._actor_refs = [a for a in self._actor_refs if a.exists()]
872
873        # Prune our weak refs once the Actor object has been freed.
874        self._actor_weak_refs = [
875            a for a in self._actor_weak_refs if a() is not None
876        ]

Units of execution wrangled by a bascenev1.Session.

Examples of Activities include games, score-screens, cutscenes, etc. A bascenev1.Session has one 'current' Activity at any time, though their existence can overlap during transitions.

settings_raw: dict[str, typing.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[~TeamT]

The list of bascenev1.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[~PlayerT]

The list of bascenev1.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.

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.

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.

allow_pausing = False

Whether game-time should still progress when in menus/etc.

allow_kick_idle_players = True

Whether idle players can potentially be kicked (should not happen in menus/etc).

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.

slow_motion = False

If True, runs in slow motion and turns down sound pitch.

inherits_slow_motion = False

Set this to True to inherit slow motion setting from previous activity (useful for transitions to avoid hitches).

inherits_music = False

Set this to True to keep playing the music from the previous activity (without even restarting it).

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

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

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

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.

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.

can_show_ad_on_death = False

Is it ok to show an ad after this activity ends before showing the next activity?

paused_text: Actor | None
preloads: dict[type, typing.Any]
lobby
context: _babase.ContextRef
203    @property
204    def context(self) -> bascenev1.ContextRef:
205        """A context-ref pointing at this activity."""
206        return self._activity_data.context()

A context-ref pointing at this activity.

globalsnode: _bascenev1.Node
208    @property
209    def globalsnode(self) -> bascenev1.Node:
210        """The 'globals' bascenev1.Node for the activity. This contains various
211        global controls and values.
212        """
213        node = self._globalsnode
214        if not node:
215            raise babase.NodeNotFoundError()
216        return node

The 'globals' bascenev1.Node for the activity. This contains various global controls and values.

stats: Stats
218    @property
219    def stats(self) -> bascenev1.Stats:
220        """The stats instance accessible while the activity is running.
221
222        If access is attempted before or after, raises a
223        bascenev1.NotFoundError.
224        """
225        if self._stats is None:
226            raise babase.NotFoundError()
227        return self._stats

The stats instance accessible while the activity is running.

If access is attempted before or after, raises a bascenev1.NotFoundError.

def on_expire(self) -> None:
229    def on_expire(self) -> None:
230        """Called when your activity is being expired.
231
232        If your activity has created anything explicitly that may be retaining
233        a strong reference to the activity and preventing it from dying, you
234        should clear that out here. From this point on your activity's sole
235        purpose in life is to hit zero references and die so the next activity
236        can begin.
237        """

Called when your activity is being expired.

If your activity has created anything explicitly that may be retaining a strong reference to the activity and preventing it from dying, you should clear that out here. From this point on your activity's sole purpose in life is to hit zero references and die so the next activity can begin.

customdata: dict
239    @property
240    def customdata(self) -> dict:
241        """Entities needing to store simple data with an activity can put it
242        here. This dict will be deleted when the activity expires, so contained
243        objects generally do not need to worry about handling expired
244        activities.
245        """
246        assert not self._expired
247        assert isinstance(self._customdata, dict)
248        return self._customdata

Entities needing to store simple data with an activity can put it here. This dict will be deleted when the activity expires, so contained objects generally do not need to worry about handling expired activities.

expired: bool
250    @property
251    def expired(self) -> bool:
252        """Whether the activity is expired.
253
254        An activity is set as expired when shutting down.
255        At this point no new nodes, timers, etc should be made,
256        run, etc, and the activity should be considered a 'zombie'.
257        """
258        return self._expired

Whether the activity is expired.

An activity is set as expired when shutting down. At this point no new nodes, timers, etc should be made, run, etc, and the activity should be considered a 'zombie'.

playertype: type[~PlayerT]
260    @property
261    def playertype(self) -> type[PlayerT]:
262        """The type of bascenev1.Player this Activity is using."""
263        return self._playertype

The type of bascenev1.Player this Activity is using.

teamtype: type[~TeamT]
265    @property
266    def teamtype(self) -> type[TeamT]:
267        """The type of bascenev1.Team this Activity is using."""
268        return self._teamtype

The type of bascenev1.Team this Activity is using.

def retain_actor(self, actor: Actor) -> None:
305    def retain_actor(self, actor: bascenev1.Actor) -> None:
306        """Add a strong-reference to a bascenev1.Actor to this Activity.
307
308        The reference will be lazily released once bascenev1.Actor.exists()
309        returns False for the Actor. The bascenev1.Actor.autoretain() method
310        is a convenient way to access this same functionality.
311        """
312        if __debug__:
313            from bascenev1._actor import Actor
314
315            assert isinstance(actor, Actor)
316        self._actor_refs.append(actor)

Add a strong-reference to a bascenev1.Actor to this Activity.

The reference will be lazily released once bascenev1.Actor.exists() returns False for the Actor. The bascenev1.Actor.autoretain() method is a convenient way to access this same functionality.

def add_actor_weak_ref(self, actor: Actor) -> None:
318    def add_actor_weak_ref(self, actor: bascenev1.Actor) -> None:
319        """Add a weak-reference to a bascenev1.Actor to the bascenev1.Activity.
320
321        (called by the bascenev1.Actor base class)
322        """
323        if __debug__:
324            from bascenev1._actor import Actor
325
326            assert isinstance(actor, Actor)
327        self._actor_weak_refs.append(weakref.ref(actor))

Add a weak-reference to a bascenev1.Actor to the bascenev1.Activity.

(called by the bascenev1.Actor base class)

session: Session
329    @property
330    def session(self) -> bascenev1.Session:
331        """The bascenev1.Session this bascenev1.Activity belongs to.
332
333        Raises a :class:`~bascenev1.SessionNotFoundError` if the Session
334        no longer exists.
335        """
336        session = self._session()
337        if session is None:
338            raise babase.SessionNotFoundError()
339        return session

The bascenev1.Session this bascenev1.Activity belongs to.

Raises a ~bascenev1.SessionNotFoundError if the Session no longer exists.

def on_player_join(self, player: ~PlayerT) -> None:
341    def on_player_join(self, player: PlayerT) -> None:
342        """Called when a new bascenev1.Player has joined the Activity.
343
344        (including the initial set of Players)
345        """

Called when a new bascenev1.Player has joined the Activity.

(including the initial set of Players)

def on_player_leave(self, player: ~PlayerT) -> None:
347    def on_player_leave(self, player: PlayerT) -> None:
348        """Called when a bascenev1.Player is leaving the Activity."""

Called when a bascenev1.Player is leaving the Activity.

def on_team_join(self, team: ~TeamT) -> None:
350    def on_team_join(self, team: TeamT) -> None:
351        """Called when a new bascenev1.Team joins the Activity.
352
353        (including the initial set of Teams)
354        """

Called when a new bascenev1.Team joins the Activity.

(including the initial set of Teams)

def on_team_leave(self, team: ~TeamT) -> None:
356    def on_team_leave(self, team: TeamT) -> None:
357        """Called when a bascenev1.Team leaves the Activity."""

Called when a bascenev1.Team leaves the Activity.

def on_transition_in(self) -> None:
359    def on_transition_in(self) -> None:
360        """Called when the Activity is first becoming visible.
361
362        Upon this call, the Activity should fade in backgrounds,
363        start playing music, etc. It does not yet have access to players
364        or teams, however. They remain owned by the previous Activity
365        up until bascenev1.Activity.on_begin() is called.
366        """

Called when the Activity is first becoming visible.

Upon this call, the Activity should fade in backgrounds, start playing music, etc. It does not yet have access to players or teams, however. They remain owned by the previous Activity up until bascenev1.Activity.on_begin() is called.

def on_transition_out(self) -> None:
368    def on_transition_out(self) -> None:
369        """Called when your activity begins transitioning out.
370
371        Note that this may happen at any time even if bascenev1.Activity.end()
372        has not been called.
373        """

Called when your activity begins transitioning out.

Note that this may happen at any time even if bascenev1.Activity.end() has not been called.

def on_begin(self) -> None:
375    def on_begin(self) -> None:
376        """Called once the previous Activity has finished transitioning out.
377
378        At this point the activity's initial players and teams are filled in
379        and it should begin its actual game logic.
380        """

Called once the previous Activity has finished transitioning out.

At this point the activity's initial players and teams are filled in and it should begin its actual game logic.

def handlemessage(self, msg: Any) -> Any:
382    def handlemessage(self, msg: Any) -> Any:
383        """General message handling; can be passed any message object."""
384        del msg  # Unused arg.
385        return UNHANDLED

General message handling; can be passed any message object.

def has_transitioned_in(self) -> bool:
387    def has_transitioned_in(self) -> bool:
388        """Return whether bascenev1.Activity.on_transition_in() has run."""
389        return self._has_transitioned_in

Return whether bascenev1.Activity.on_transition_in() has run.

def has_begun(self) -> bool:
391    def has_begun(self) -> bool:
392        """Return whether bascenev1.Activity.on_begin() has run."""
393        return self._has_begun

Return whether bascenev1.Activity.on_begin() has run.

def has_ended(self) -> bool:
395    def has_ended(self) -> bool:
396        """Return whether the activity has commenced ending."""
397        return self._has_ended

Return whether the activity has commenced ending.

def is_transitioning_out(self) -> bool:
399    def is_transitioning_out(self) -> bool:
400        """Return whether bascenev1.Activity.on_transition_out() has run."""
401        return self._transitioning_out
def transition_out(self) -> None:
461    def transition_out(self) -> None:
462        """Called by the Session to start us transitioning out."""
463        assert not self._transitioning_out
464        self._transitioning_out = True
465        with self.context:
466            try:
467                self.on_transition_out()
468            except Exception:
469                logging.exception('Error in on_transition_out for %s.', self)

Called by the Session to start us transitioning out.

def end( self, results: Any = None, delay: float = 0.0, force: bool = False) -> None:
499    def end(
500        self, results: Any = None, delay: float = 0.0, force: bool = False
501    ) -> None:
502        """Commences Activity shutdown and delivers results to the Session.
503
504        'delay' is the time delay before the Activity actually ends
505        (in seconds). Further calls to end() will be ignored up until
506        this time, unless 'force' is True, in which case the new results
507        will replace the old.
508        """
509
510        # Ask the session to end us.
511        self.session.end_activity(self, results, delay, force)

Commences Activity shutdown and delivers results to the Session.

'delay' is the time delay before the Activity actually ends (in seconds). Further calls to end() will be ignored up until this time, unless 'force' is True, in which case the new results will replace the old.

def create_player(self, sessionplayer: _bascenev1.SessionPlayer) -> ~PlayerT:
513    def create_player(self, sessionplayer: bascenev1.SessionPlayer) -> PlayerT:
514        """Create the Player instance for this Activity.
515
516        Subclasses can override this if the activity's player class
517        requires a custom constructor; otherwise it will be called with
518        no args. Note that the player object should not be used at this
519        point as it is not yet fully wired up; wait for
520        bascenev1.Activity.on_player_join() for that.
521        """
522        del sessionplayer  # Unused.
523        player = self._playertype()
524        return player

Create the Player instance for this Activity.

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 bascenev1.Activity.on_player_join() for that.

def create_team(self, sessionteam: SessionTeam) -> ~TeamT:
526    def create_team(self, sessionteam: bascenev1.SessionTeam) -> TeamT:
527        """Create the Team instance for this Activity.
528
529        Subclasses can override this if the activity's team class
530        requires a custom constructor; otherwise it will be called with
531        no args. Note that the team object should not be used at this
532        point as it is not yet fully wired up; wait for on_team_join()
533        for that.
534        """
535        del sessionteam  # Unused.
536        team = self._teamtype()
537        return team

Create the Team instance for this Activity.

Subclasses can override this if the activity's team class requires a custom constructor; otherwise it will be called with no args. Note that the team object should not be used at this point as it is not yet fully wired up; wait for on_team_join() for that.

class Actor:
 30class Actor:
 31    """High level logical entities in a bascenev1.Activity.
 32
 33    Actors act as controllers, combining some number of Nodes, Textures,
 34    Sounds, etc. into a high-level cohesive unit.
 35
 36    Some example actors include the Bomb, Flag, and Spaz classes that
 37    live in the bascenev1lib.actor.* modules.
 38
 39    One key feature of Actors is that they generally 'die' (killing off
 40    or transitioning out their nodes) when the last Python reference to
 41    them disappears, so you can use logic such as:
 42
 43    ##### Example
 44    >>> # Create a flag Actor in our game activity:
 45    ... from bascenev1lib.actor.flag import Flag
 46    ... self.flag = Flag(position=(0, 10, 0))
 47    ...
 48    ... # Later, destroy the flag.
 49    ... # (provided nothing else is holding a reference to it)
 50    ... # We could also just assign a new flag to this value.
 51    ... # Either way, the old flag disappears.
 52    ... self.flag = None
 53
 54    This is in contrast to the behavior of the more low level
 55    bascenev1.Node, which is always explicitly created and destroyed
 56    and doesn't care how many Python references to it exist.
 57
 58    Note, however, that you can use the bascenev1.Actor.autoretain() method
 59    if you want an Actor to stick around until explicitly killed
 60    regardless of references.
 61
 62    Another key feature of bascenev1.Actor is its
 63    bascenev1.Actor.handlemessage() method, which takes a single arbitrary
 64    object as an argument. This provides a safe way to communicate between
 65    bascenev1.Actor, bascenev1.Activity, bascenev1.Session, and any other
 66    class providing a handlemessage() method. The most universally handled
 67    message type for Actors is the bascenev1.DieMessage.
 68
 69    Another way to kill the flag from the example above:
 70    We can safely call this on any type with a 'handlemessage' method
 71    (though its not guaranteed to always have a meaningful effect).
 72    In this case the Actor instance will still be around, but its
 73    bascenev1.Actor.exists() and bascenev1.Actor.is_alive() methods will
 74    both return False.
 75    >>> self.flag.handlemessage(bascenev1.DieMessage())
 76    """
 77
 78    def __init__(self) -> None:
 79        """Instantiates an Actor in the current bascenev1.Activity."""
 80
 81        if __debug__:
 82            self._root_actor_init_called = True
 83        activity = _bascenev1.getactivity()
 84        self._activity = weakref.ref(activity)
 85        activity.add_actor_weak_ref(self)
 86
 87    def __del__(self) -> None:
 88        try:
 89            # Unexpired Actors send themselves a DieMessage when going down.
 90            # That way we can treat DieMessage handling as the single
 91            # point-of-action for death.
 92            if not self.expired:
 93                self.handlemessage(DieMessage())
 94        except Exception:
 95            logging.exception(
 96                'Error in bascenev1.Actor.__del__() for %s.', self
 97            )
 98
 99    def handlemessage(self, msg: Any) -> Any:
100        """General message handling; can be passed any message object."""
101        assert not self.expired
102
103        # By default, actors going out-of-bounds simply kill themselves.
104        if isinstance(msg, OutOfBoundsMessage):
105            return self.handlemessage(DieMessage(how=DeathType.OUT_OF_BOUNDS))
106
107        return UNHANDLED
108
109    def autoretain(self: ActorT) -> ActorT:
110        """Keep this Actor alive without needing to hold a reference to it.
111
112        This keeps the bascenev1.Actor in existence by storing a reference
113        to it with the bascenev1.Activity it was created in. The reference
114        is lazily released once bascenev1.Actor.exists() returns False for
115        it or when the Activity is set as expired.  This can be a convenient
116        alternative to storing references explicitly just to keep a
117        bascenev1.Actor from dying.
118        For convenience, this method returns the bascenev1.Actor it is called
119        with, enabling chained statements such as:
120        myflag = bascenev1.Flag().autoretain()
121        """
122        activity = self._activity()
123        if activity is None:
124            raise babase.ActivityNotFoundError()
125        activity.retain_actor(self)
126        return self
127
128    def on_expire(self) -> None:
129        """Called for remaining `bascenev1.Actor`s when their activity dies.
130
131        Actors can use this opportunity to clear callbacks or other
132        references which have the potential of keeping the bascenev1.Activity
133        alive inadvertently (Activities can not exit cleanly while
134        any Python references to them remain.)
135
136        Once an actor is expired (see bascenev1.Actor.is_expired()) it should
137        no longer perform any game-affecting operations (creating, modifying,
138        or deleting nodes, media, timers, etc.) Attempts to do so will
139        likely result in errors.
140        """
141
142    @property
143    def expired(self) -> bool:
144        """Whether the Actor is expired.
145
146        (see bascenev1.Actor.on_expire())
147        """
148        activity = self.getactivity(doraise=False)
149        return True if activity is None else activity.expired
150
151    def exists(self) -> bool:
152        """Returns whether the Actor is still present in a meaningful way.
153
154        Note that a dying character should still return True here as long as
155        their corpse is visible; this is about presence, not being 'alive'
156        (see bascenev1.Actor.is_alive() for that).
157
158        If this returns False, it is assumed the Actor can be completely
159        deleted without affecting the game; this call is often used
160        when pruning lists of Actors, such as with bascenev1.Actor.autoretain()
161
162        The default implementation of this method always return True.
163
164        Note that the boolean operator for the Actor class calls this method,
165        so a simple "if myactor" test will conveniently do the right thing
166        even if myactor is set to None.
167        """
168        return True
169
170    def __bool__(self) -> bool:
171        # Cleaner way to test existence; friendlier to None values.
172        return self.exists()
173
174    def is_alive(self) -> bool:
175        """Returns whether the Actor is 'alive'.
176
177        What this means is up to the Actor.
178        It is not a requirement for Actors to be able to die;
179        just that they report whether they consider themselves
180        to be alive or not. In cases where dead/alive is
181        irrelevant, True should be returned.
182        """
183        return True
184
185    @property
186    def activity(self) -> bascenev1.Activity:
187        """The Activity this Actor was created in.
188
189        Raises a bascenev1.ActivityNotFoundError if the Activity no longer
190        exists.
191        """
192        activity = self._activity()
193        if activity is None:
194            raise babase.ActivityNotFoundError()
195        return activity
196
197    # Overloads to convey our exact return type depending on 'doraise' value.
198
199    @overload
200    def getactivity(
201        self, doraise: Literal[True] = True
202    ) -> bascenev1.Activity: ...
203
204    @overload
205    def getactivity(
206        self, doraise: Literal[False]
207    ) -> bascenev1.Activity | None: ...
208
209    def getactivity(self, doraise: bool = True) -> bascenev1.Activity | None:
210        """Return the bascenev1.Activity this Actor is associated with.
211
212        If the Activity no longer exists, raises a
213        bascenev1.ActivityNotFoundError or returns None depending on whether
214        'doraise' is True.
215        """
216        activity = self._activity()
217        if activity is None and doraise:
218            raise babase.ActivityNotFoundError()
219        return activity

High level logical entities in a bascenev1.Activity.

Actors act as controllers, combining some number of Nodes, Textures, Sounds, etc. into a high-level cohesive unit.

Some example actors include the Bomb, Flag, and Spaz classes that live in the bascenev1lib.actor.* modules.

One key feature of Actors is that they generally 'die' (killing off or transitioning out their nodes) when the last Python reference to them disappears, so you can use logic such as:

Example
>>> # Create a flag Actor in our game activity:
... from bascenev1lib.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 bascenev1.Node, which is always explicitly created and destroyed and doesn't care how many Python references to it exist.

Note, however, that you can use the bascenev1.Actor.autoretain() method if you want an Actor to stick around until explicitly killed regardless of references.

Another key feature of bascenev1.Actor is its bascenev1.Actor.handlemessage() method, which takes a single arbitrary object as an argument. This provides a safe way to communicate between bascenev1.Actor, bascenev1.Activity, bascenev1.Session, and any other class providing a handlemessage() method. The most universally handled message type for Actors is the bascenev1.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 bascenev1.Actor.exists() and bascenev1.Actor.is_alive() methods will both return False.

>>> self.flag.handlemessage(bascenev1.DieMessage())
Actor()
78    def __init__(self) -> None:
79        """Instantiates an Actor in the current bascenev1.Activity."""
80
81        if __debug__:
82            self._root_actor_init_called = True
83        activity = _bascenev1.getactivity()
84        self._activity = weakref.ref(activity)
85        activity.add_actor_weak_ref(self)

Instantiates an Actor in the current bascenev1.Activity.

def handlemessage(self, msg: Any) -> Any:
 99    def handlemessage(self, msg: Any) -> Any:
100        """General message handling; can be passed any message object."""
101        assert not self.expired
102
103        # By default, actors going out-of-bounds simply kill themselves.
104        if isinstance(msg, OutOfBoundsMessage):
105            return self.handlemessage(DieMessage(how=DeathType.OUT_OF_BOUNDS))
106
107        return UNHANDLED

General message handling; can be passed any message object.

def autoretain(self: ~ActorT) -> ~ActorT:
109    def autoretain(self: ActorT) -> ActorT:
110        """Keep this Actor alive without needing to hold a reference to it.
111
112        This keeps the bascenev1.Actor in existence by storing a reference
113        to it with the bascenev1.Activity it was created in. The reference
114        is lazily released once bascenev1.Actor.exists() returns False for
115        it or when the Activity is set as expired.  This can be a convenient
116        alternative to storing references explicitly just to keep a
117        bascenev1.Actor from dying.
118        For convenience, this method returns the bascenev1.Actor it is called
119        with, enabling chained statements such as:
120        myflag = bascenev1.Flag().autoretain()
121        """
122        activity = self._activity()
123        if activity is None:
124            raise babase.ActivityNotFoundError()
125        activity.retain_actor(self)
126        return self

Keep this Actor alive without needing to hold a reference to it.

This keeps the bascenev1.Actor in existence by storing a reference to it with the bascenev1.Activity it was created in. The reference is lazily released once bascenev1.Actor.exists() returns False for it or when the Activity is set as expired. This can be a convenient alternative to storing references explicitly just to keep a bascenev1.Actor from dying. For convenience, this method returns the bascenev1.Actor it is called with, enabling chained statements such as: myflag = bascenev1.Flag().autoretain()

def on_expire(self) -> None:
128    def on_expire(self) -> None:
129        """Called for remaining `bascenev1.Actor`s when their activity dies.
130
131        Actors can use this opportunity to clear callbacks or other
132        references which have the potential of keeping the bascenev1.Activity
133        alive inadvertently (Activities can not exit cleanly while
134        any Python references to them remain.)
135
136        Once an actor is expired (see bascenev1.Actor.is_expired()) it should
137        no longer perform any game-affecting operations (creating, modifying,
138        or deleting nodes, media, timers, etc.) Attempts to do so will
139        likely result in errors.
140        """

Called for remaining bascenev1.Actors when their activity dies.

Actors can use this opportunity to clear callbacks or other references which have the potential of keeping the bascenev1.Activity alive inadvertently (Activities can not exit cleanly while any Python references to them remain.)

Once an actor is expired (see bascenev1.Actor.is_expired()) it should no longer perform any game-affecting operations (creating, modifying, or deleting nodes, media, timers, etc.) Attempts to do so will likely result in errors.

expired: bool
142    @property
143    def expired(self) -> bool:
144        """Whether the Actor is expired.
145
146        (see bascenev1.Actor.on_expire())
147        """
148        activity = self.getactivity(doraise=False)
149        return True if activity is None else activity.expired

Whether the Actor is expired.

(see bascenev1.Actor.on_expire())

def exists(self) -> bool:
151    def exists(self) -> bool:
152        """Returns whether the Actor is still present in a meaningful way.
153
154        Note that a dying character should still return True here as long as
155        their corpse is visible; this is about presence, not being 'alive'
156        (see bascenev1.Actor.is_alive() for that).
157
158        If this returns False, it is assumed the Actor can be completely
159        deleted without affecting the game; this call is often used
160        when pruning lists of Actors, such as with bascenev1.Actor.autoretain()
161
162        The default implementation of this method always return True.
163
164        Note that the boolean operator for the Actor class calls this method,
165        so a simple "if myactor" test will conveniently do the right thing
166        even if myactor is set to None.
167        """
168        return True

Returns whether the Actor is still present in a meaningful way.

Note that a dying character should still return True here as long as their corpse is visible; this is about presence, not being 'alive' (see bascenev1.Actor.is_alive() for that).

If this returns False, it is assumed the Actor can be completely deleted without affecting the game; this call is often used when pruning lists of Actors, such as with bascenev1.Actor.autoretain()

The default implementation of this method always return True.

Note that the boolean operator for the Actor class calls this method, so a simple "if myactor" test will conveniently do the right thing even if myactor is set to None.

def is_alive(self) -> bool:
174    def is_alive(self) -> bool:
175        """Returns whether the Actor is 'alive'.
176
177        What this means is up to the Actor.
178        It is not a requirement for Actors to be able to die;
179        just that they report whether they consider themselves
180        to be alive or not. In cases where dead/alive is
181        irrelevant, True should be returned.
182        """
183        return True

Returns whether the Actor is 'alive'.

What this means is up to the Actor. It is not a requirement for Actors to be able to die; just that they report whether they consider themselves to be alive or not. In cases where dead/alive is irrelevant, True should be returned.

activity: Activity
185    @property
186    def activity(self) -> bascenev1.Activity:
187        """The Activity this Actor was created in.
188
189        Raises a bascenev1.ActivityNotFoundError if the Activity no longer
190        exists.
191        """
192        activity = self._activity()
193        if activity is None:
194            raise babase.ActivityNotFoundError()
195        return activity

The Activity this Actor was created in.

Raises a bascenev1.ActivityNotFoundError if the Activity no longer exists.

def getactivity(self, doraise: bool = True) -> Activity | None:
209    def getactivity(self, doraise: bool = True) -> bascenev1.Activity | None:
210        """Return the bascenev1.Activity this Actor is associated with.
211
212        If the Activity no longer exists, raises a
213        bascenev1.ActivityNotFoundError or returns None depending on whether
214        'doraise' is True.
215        """
216        activity = self._activity()
217        if activity is None and doraise:
218            raise babase.ActivityNotFoundError()
219        return activity

Return the bascenev1.Activity this Actor is associated with.

If the Activity no longer exists, raises a bascenev1.ActivityNotFoundError or returns None depending on whether 'doraise' is True.

def animate( node: _bascenev1.Node, attr: str, keys: dict[float, float], loop: bool = False, offset: float = 0) -> _bascenev1.Node:
 49def animate(
 50    node: bascenev1.Node,
 51    attr: str,
 52    keys: dict[float, float],
 53    loop: bool = False,
 54    offset: float = 0,
 55) -> bascenev1.Node:
 56    """Animate values on a target bascenev1.Node.
 57
 58    Creates an 'animcurve' node with the provided values and time as an input,
 59    connect it to the provided attribute, and set it to die with the target.
 60    Key values are provided as time:value dictionary pairs.  Time values are
 61    relative to the current time. By default, times are specified in seconds,
 62    but timeformat can also be set to MILLISECONDS to recreate the old behavior
 63    (prior to ba 1.5) of taking milliseconds. Returns the animcurve node.
 64    """
 65    items = list(keys.items())
 66    items.sort()
 67
 68    curve = _bascenev1.newnode(
 69        'animcurve',
 70        owner=node,
 71        name='Driving ' + str(node) + ' \'' + attr + '\'',
 72    )
 73
 74    # We take seconds but operate on milliseconds internally.
 75    mult = 1000
 76
 77    curve.times = [int(mult * time) for time, val in items]
 78    curve.offset = int(_bascenev1.time() * 1000.0) + int(mult * offset)
 79    curve.values = [val for time, val in items]
 80    curve.loop = loop
 81
 82    # If we're not looping, set a timer to kill this curve
 83    # after its done its job.
 84    # FIXME: Even if we are looping we should have a way to die once we
 85    #  get disconnected.
 86    if not loop:
 87        # noinspection PyUnresolvedReferences
 88        _bascenev1.timer(
 89            (int(mult * items[-1][0]) + 1000) / 1000.0, curve.delete
 90        )
 91
 92    # Do the connects last so all our attrs are in place when we push initial
 93    # values through.
 94
 95    # We operate in either activities or sessions..
 96    try:
 97        globalsnode = _bascenev1.getactivity().globalsnode
 98    except babase.ActivityNotFoundError:
 99        globalsnode = _bascenev1.getsession().sessionglobalsnode
100
101    globalsnode.connectattr('time', curve, 'in')
102    curve.connectattr('out', node, attr)
103    return curve

Animate values on a target bascenev1.Node.

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. Key values are provided as time:value dictionary pairs. Time values are relative to the current time. By default, times are specified in seconds, but timeformat can also be set to MILLISECONDS to recreate the old behavior (prior to ba 1.5) of taking milliseconds. Returns the animcurve node.

def animate_array( node: _bascenev1.Node, attr: str, size: int, keys: dict[float, typing.Sequence[float]], *, loop: bool = False, offset: float = 0) -> None:
106def animate_array(
107    node: bascenev1.Node,
108    attr: str,
109    size: int,
110    keys: dict[float, Sequence[float]],
111    *,
112    loop: bool = False,
113    offset: float = 0,
114) -> None:
115    """Animate an array of values on a target bascenev1.Node.
116
117    Like bs.animate, but operates on array attributes.
118    """
119    combine = _bascenev1.newnode('combine', owner=node, attrs={'size': size})
120    items = list(keys.items())
121    items.sort()
122
123    # We take seconds but operate on milliseconds internally.
124    mult = 1000
125
126    # We operate in either activities or sessions..
127    try:
128        globalsnode = _bascenev1.getactivity().globalsnode
129    except babase.ActivityNotFoundError:
130        globalsnode = _bascenev1.getsession().sessionglobalsnode
131
132    for i in range(size):
133        curve = _bascenev1.newnode(
134            'animcurve',
135            owner=node,
136            name=(
137                'Driving ' + str(node) + ' \'' + attr + '\' member ' + str(i)
138            ),
139        )
140        globalsnode.connectattr('time', curve, 'in')
141        curve.times = [int(mult * time) for time, val in items]
142        curve.values = [val[i] for time, val in items]
143        curve.offset = int(_bascenev1.time() * 1000.0) + int(mult * offset)
144        curve.loop = loop
145        curve.connectattr('out', combine, 'input' + str(i))
146
147        # If we're not looping, set a timer to kill this
148        # curve after its done its job.
149        if not loop:
150            # (PyCharm seems to think item is a float, not a tuple)
151            # noinspection PyUnresolvedReferences
152            _bascenev1.timer(
153                (int(mult * items[-1][0]) + 1000) / 1000.0,
154                curve.delete,
155            )
156    combine.connectattr('output', node, attr)
157
158    # If we're not looping, set a timer to kill the combine once
159    # the job is done.
160    # FIXME: Even if we are looping we should have a way to die
161    #  once we get disconnected.
162    if not loop:
163        # (PyCharm seems to think item is a float, not a tuple)
164        # noinspection PyUnresolvedReferences
165        _bascenev1.timer(
166            (int(mult * items[-1][0]) + 1000) / 1000.0, combine.delete
167        )

Animate an array of values on a target bascenev1.Node.

Like bs.animate, but operates on array attributes.

app = <App object>
class App:
  52class App:
  53    """High level Ballistica app functionality and state.
  54
  55    Access the single shared instance of this class via the "app" attr
  56    available on various high level modules such as :mod:`bauiv1` and
  57    :mod:`bascenev1`.
  58    """
  59
  60    # pylint: disable=too-many-public-methods
  61
  62    class State(Enum):
  63        """High level state the app can be in."""
  64
  65        #: The app has not yet begun starting and should not be used in
  66        #: any way.
  67        NOT_STARTED = 0
  68
  69        #: The native layer is spinning up its machinery (screens,
  70        #: renderers, etc.). Nothing should happen in the Python layer
  71        #: until this completes.
  72        NATIVE_BOOTSTRAPPING = 1
  73
  74        #: Python app subsystems are being inited but should not yet
  75        #: interact or do any work.
  76        INITING = 2
  77
  78        #: Python app subsystems are inited and interacting, but the app
  79        #: has not yet embarked on a high level course of action. It is
  80        #: doing initial account logins, workspace & asset downloads,
  81        #: etc.
  82        LOADING = 3
  83
  84        #: All pieces are in place and the app is now doing its thing.
  85        RUNNING = 4
  86
  87        #: Used on platforms such as mobile where the app basically needs
  88        #: to shut down while backgrounded. In this state, all event
  89        #: loops are suspended and all graphics and audio must cease
  90        #: completely. Be aware that the suspended state can be entered
  91        #: from any other state including NATIVE_BOOTSTRAPPING and
  92        #: SHUTTING_DOWN.
  93        SUSPENDED = 5
  94
  95        #: The app is shutting down. This process may involve sending
  96        #: network messages or other things that can take up to a few
  97        #: seconds, so ideally graphics and audio should remain
  98        #: functional (with fades or spinners or whatever to show
  99        #: something is happening).
 100        SHUTTING_DOWN = 6
 101
 102        #: The app has completed shutdown. Any code running here should
 103        #: be basically immediate.
 104        SHUTDOWN_COMPLETE = 7
 105
 106    class DefaultAppModeSelector(AppModeSelector):
 107        """Decides which AppMode to use to handle AppIntents.
 108
 109        This default version is generated by the project updater based
 110        on the 'default_app_modes' value in the projectconfig.
 111
 112        It is also possible to modify app mode selection behavior by
 113        setting app.mode_selector to an instance of a custom
 114        AppModeSelector subclass. This is a good way to go if you are
 115        modifying app behavior dynamically via a plugin instead of
 116        statically in a spinoff project.
 117        """
 118
 119        @override
 120        def app_mode_for_intent(
 121            self, intent: AppIntent
 122        ) -> type[AppMode] | None:
 123            # pylint: disable=cyclic-import
 124
 125            # __DEFAULT_APP_MODE_SELECTION_BEGIN__
 126            # This section generated by batools.appmodule; do not edit.
 127
 128            # Ask our default app modes to handle it.
 129            # (generated from 'default_app_modes' in projectconfig).
 130            import baclassic
 131            import babase
 132
 133            for appmode in [
 134                baclassic.ClassicAppMode,
 135                babase.EmptyAppMode,
 136            ]:
 137                if appmode.can_handle_intent(intent):
 138                    return appmode
 139
 140            return None
 141
 142            # __DEFAULT_APP_MODE_SELECTION_END__
 143
 144    # A few things defined as non-optional values but not actually
 145    # available until the app starts.
 146    plugins: PluginSubsystem
 147    lang: LanguageSubsystem
 148    health_monitor: AppHealthMonitor
 149
 150    # Define some other types here in the class-def so docs-generators
 151    # are more likely to know about them.
 152    config: AppConfig
 153    env: babase.Env
 154    state: State
 155    threadpool: ThreadPoolExecutorPlus
 156    meta: MetadataSubsystem
 157    net: NetworkSubsystem
 158    workspaces: WorkspaceSubsystem
 159    components: AppComponentSubsystem
 160    stringedit: StringEditSubsystem
 161    devconsole: DevConsoleSubsystem
 162    fg_state: int
 163
 164    #: How long we allow shutdown tasks to run before killing them.
 165    #: Currently the entire app hard-exits if shutdown takes 15 seconds,
 166    #: so we need to keep it under that. Staying above 10 should allow
 167    #: 10 second network timeouts to happen though.
 168    SHUTDOWN_TASK_TIMEOUT_SECONDS = 12
 169
 170    def __init__(self) -> None:
 171        """(internal)
 172
 173        Do not instantiate this class. You can access the single shared
 174        instance of it through various high level packages: 'babase.app',
 175        'bascenev1.app', 'bauiv1.app', etc.
 176        """
 177
 178        # Hack for docs-generation: we can be imported with dummy modules
 179        # instead of our actual binary ones, but we don't function.
 180        if os.environ.get('BA_RUNNING_WITH_DUMMY_MODULES') == '1':
 181            return
 182
 183        # Wrap our raw app config in our special wrapper and pass it to
 184        # the native layer.
 185        self.config = AppConfig(_babase.get_initial_app_config())
 186        _babase.set_app_config(self.config)
 187
 188        self.env = _babase.Env()
 189        self.state = self.State.NOT_STARTED
 190
 191        # Default executor which can be used for misc background
 192        # processing. It should also be passed to any additional asyncio
 193        # loops we create so that everything shares the same single set
 194        # of worker threads.
 195        self.threadpool = ThreadPoolExecutorPlus(
 196            thread_name_prefix='baworker',
 197            initializer=self._thread_pool_thread_init,
 198        )
 199        self.meta = MetadataSubsystem()
 200        self.net = NetworkSubsystem()
 201        self.workspaces = WorkspaceSubsystem()
 202        self.components = AppComponentSubsystem()
 203        self.stringedit = StringEditSubsystem()
 204        self.devconsole = DevConsoleSubsystem()
 205
 206        # This is incremented any time the app is backgrounded or
 207        # foregrounded; can be a simple way to determine if network data
 208        # should be refreshed/etc.
 209        self.fg_state = 0
 210
 211        self._subsystems: list[AppSubsystem] = []
 212        self._native_bootstrapping_completed = False
 213        self._init_completed = False
 214        self._meta_scan_completed = False
 215        self._native_start_called = False
 216        self._native_suspended = False
 217        self._native_shutdown_called = False
 218        self._native_shutdown_complete_called = False
 219        self._initial_sign_in_completed = False
 220        self._called_on_initing = False
 221        self._called_on_loading = False
 222        self._called_on_running = False
 223        self._subsystem_registration_ended = False
 224        self._pending_apply_app_config = False
 225        self._asyncio_loop: asyncio.AbstractEventLoop | None = None
 226        self._asyncio_tasks: set[asyncio.Task] = set()
 227        self._asyncio_timer: babase.AppTimer | None = None
 228        self._pending_intent: AppIntent | None = None
 229        self._intent: AppIntent | None = None
 230        self._mode_selector: babase.AppModeSelector | None = None
 231        self._mode_instances: dict[type[AppMode], AppMode] = {}
 232        self._mode: AppMode | None = None
 233        self._shutdown_task: asyncio.Task[None] | None = None
 234        self._shutdown_tasks: list[Coroutine[None, None, None]] = [
 235            self._wait_for_shutdown_suppressions(),
 236            self._fade_and_shutdown_graphics(),
 237            self._fade_and_shutdown_audio(),
 238        ]
 239        self._pool_thread_count = 0
 240
 241        # We hold a lock while lazy-loading our subsystem properties so
 242        # we don't spin up any subsystem more than once, but the lock is
 243        # recursive so that the subsystems can instantiate other
 244        # subsystems.
 245        self._subsystem_property_lock = RLock()
 246        self._subsystem_property_data: dict[str, AppSubsystem | bool] = {}
 247
 248    def postinit(self) -> None:
 249        """Called after we've been inited and assigned to babase.app.
 250
 251        Anything that accesses babase.app as part of its init process
 252        must go here instead of __init__.
 253        """
 254
 255        # Hack for docs-generation: We can be imported with dummy
 256        # modules instead of our actual binary ones, but we don't
 257        # function.
 258        if os.environ.get('BA_RUNNING_WITH_DUMMY_MODULES') == '1':
 259            return
 260
 261        self.lang = LanguageSubsystem()
 262        self.plugins = PluginSubsystem()
 263
 264    @property
 265    def active(self) -> bool:
 266        """Whether the app is currently front and center.
 267
 268        This will be False when the app is hidden, other activities
 269        are covering it, etc. (depending on the platform).
 270        """
 271        return _babase.app_is_active()
 272
 273    @property
 274    def mode(self) -> AppMode | None:
 275        """The app's current mode."""
 276        assert _babase.in_logic_thread()
 277        return self._mode
 278
 279    @property
 280    def asyncio_loop(self) -> asyncio.AbstractEventLoop:
 281        """The logic thread's asyncio event loop.
 282
 283        This allow async tasks to be run in the logic thread.
 284
 285        Generally you should call App.create_async_task() to schedule
 286        async code to run instead of using this directly. That will
 287        handle retaining the task and logging errors automatically.
 288        Only schedule tasks onto asyncio_loop yourself when you intend
 289        to hold on to the returned task and await its results. Releasing
 290        the task reference can lead to subtle bugs such as unreported
 291        errors and garbage-collected tasks disappearing before their
 292        work is done.
 293
 294        Note that, at this time, the asyncio loop is encapsulated
 295        and explicitly stepped by the engine's logic thread loop and
 296        thus things like asyncio.get_running_loop() will unintuitively
 297        *not* return this loop from most places in the logic thread;
 298        only from within a task explicitly created in this loop.
 299        Hopefully this situation will be improved in the future with a
 300        unified event loop.
 301        """
 302        assert _babase.in_logic_thread()
 303        assert self._asyncio_loop is not None
 304        return self._asyncio_loop
 305
 306    def create_async_task(
 307        self, coro: Coroutine[Any, Any, T], *, name: str | None = None
 308    ) -> None:
 309        """Create a fully managed async task.
 310
 311        This will automatically retain and release a reference to the task
 312        and log any exceptions that occur in it. If you need to await a task
 313        or otherwise need more control, schedule a task directly using
 314        App.asyncio_loop.
 315        """
 316        assert _babase.in_logic_thread()
 317
 318        # We hold a strong reference to the task until it is done.
 319        # Otherwise it is possible for it to be garbage collected and
 320        # disappear midway if the caller does not hold on to the
 321        # returned task, which seems like a great way to introduce
 322        # hard-to-track bugs.
 323        task = self.asyncio_loop.create_task(coro, name=name)
 324        self._asyncio_tasks.add(task)
 325        task.add_done_callback(self._on_task_done)
 326
 327    def _on_task_done(self, task: asyncio.Task) -> None:
 328        # Report any errors that occurred.
 329        try:
 330            exc = task.exception()
 331            if exc is not None:
 332                logging.error(
 333                    "Error in async task '%s'.", task.get_name(), exc_info=exc
 334                )
 335        except Exception:
 336            logging.exception('Error reporting async task error.')
 337
 338        self._asyncio_tasks.remove(task)
 339
 340    @property
 341    def mode_selector(self) -> babase.AppModeSelector:
 342        """Controls which app-modes are used for handling given intents.
 343
 344        Plugins can override this to change high level app behavior and
 345        spinoff projects can change the default implementation for the
 346        same effect.
 347        """
 348        if self._mode_selector is None:
 349            raise RuntimeError(
 350                'mode_selector cannot be used until the app reaches'
 351                ' the running state.'
 352            )
 353        return self._mode_selector
 354
 355    @mode_selector.setter
 356    def mode_selector(self, selector: babase.AppModeSelector) -> None:
 357        self._mode_selector = selector
 358
 359    def _get_subsystem_property(
 360        self, ssname: str, create_call: Callable[[], AppSubsystem | None]
 361    ) -> AppSubsystem | None:
 362
 363        # Quick-out: if a subsystem is present, just return it; no
 364        # locking necessary.
 365        val = self._subsystem_property_data.get(ssname)
 366        if val is not None:
 367            if val is False:
 368                # False means subsystem is confirmed as unavailable.
 369                return None
 370            if val is not True:
 371                # A subsystem has been set. Return it.
 372                return val
 373
 374        # Anything else (no val present or val True) requires locking.
 375        with self._subsystem_property_lock:
 376            val = self._subsystem_property_data.get(ssname)
 377            if val is not None:
 378                if val is False:
 379                    # False means confirmed as not present.
 380                    return None
 381                if val is True:
 382                    # True means this property is already being loaded,
 383                    # and the fact that we're holding the lock means
 384                    # we're doing the loading, so this is a dependency
 385                    # loop. Not good.
 386                    raise RuntimeError(
 387                        f'Subsystem dependency loop detected for {ssname}'
 388                    )
 389                # Must be an instantiated subsystem. Noice.
 390                return val
 391
 392            # Ok, there's nothing here for it. Instantiate and set it
 393            # while we hold the lock. Set a placeholder value of True
 394            # while we load so we can error if something we're loading
 395            # tries to recursively load us.
 396            self._subsystem_property_data[ssname] = True
 397
 398            # Do our one attempt to create the singleton.
 399            val = create_call()
 400            self._subsystem_property_data[ssname] = (
 401                False if val is None else val
 402            )
 403
 404        return val
 405
 406    # __FEATURESET_APP_SUBSYSTEM_PROPERTIES_BEGIN__
 407    # This section generated by batools.appmodule; do not edit.
 408
 409    @property
 410    def classic(self) -> ClassicAppSubsystem | None:
 411        """Our classic subsystem (if available)."""
 412        return self._get_subsystem_property(
 413            'classic', self._create_classic_subsystem
 414        )  # type: ignore
 415
 416    @staticmethod
 417    def _create_classic_subsystem() -> ClassicAppSubsystem | None:
 418        # pylint: disable=cyclic-import
 419        try:
 420            from baclassic import ClassicAppSubsystem
 421
 422            return ClassicAppSubsystem()
 423        except ImportError:
 424            return None
 425        except Exception:
 426            logging.exception('Error importing baclassic.')
 427            return None
 428
 429    @property
 430    def plus(self) -> PlusAppSubsystem | None:
 431        """Our plus subsystem (if available)."""
 432        return self._get_subsystem_property(
 433            'plus', self._create_plus_subsystem
 434        )  # type: ignore
 435
 436    @staticmethod
 437    def _create_plus_subsystem() -> PlusAppSubsystem | None:
 438        # pylint: disable=cyclic-import
 439        try:
 440            from baplus import PlusAppSubsystem
 441
 442            return PlusAppSubsystem()
 443        except ImportError:
 444            return None
 445        except Exception:
 446            logging.exception('Error importing baplus.')
 447            return None
 448
 449    @property
 450    def ui_v1(self) -> UIV1AppSubsystem:
 451        """Our ui_v1 subsystem (always available)."""
 452        return self._get_subsystem_property(
 453            'ui_v1', self._create_ui_v1_subsystem
 454        )  # type: ignore
 455
 456    @staticmethod
 457    def _create_ui_v1_subsystem() -> UIV1AppSubsystem:
 458        # pylint: disable=cyclic-import
 459
 460        from bauiv1 import UIV1AppSubsystem
 461
 462        return UIV1AppSubsystem()
 463
 464    # __FEATURESET_APP_SUBSYSTEM_PROPERTIES_END__
 465
 466    def register_subsystem(self, subsystem: AppSubsystem) -> None:
 467        """Called by the AppSubsystem class. Do not use directly."""
 468
 469        # We only allow registering new subsystems if we've not yet
 470        # reached the 'running' state. This ensures that all subsystems
 471        # receive a consistent set of callbacks starting with
 472        # on_app_running().
 473
 474        if self._subsystem_registration_ended:
 475            raise RuntimeError(
 476                'Subsystems can no longer be registered at this point.'
 477            )
 478        self._subsystems.append(subsystem)
 479
 480    def add_shutdown_task(self, coro: Coroutine[None, None, None]) -> None:
 481        """Add a task to be run on app shutdown.
 482
 483        Note that shutdown tasks will be canceled after
 484        :py:const:`SHUTDOWN_TASK_TIMEOUT_SECONDS` if they are still
 485        running.
 486        """
 487        if (
 488            self.state is self.State.SHUTTING_DOWN
 489            or self.state is self.State.SHUTDOWN_COMPLETE
 490        ):
 491            stname = self.state.name
 492            raise RuntimeError(
 493                f'Cannot add shutdown tasks with current state {stname}.'
 494            )
 495        self._shutdown_tasks.append(coro)
 496
 497    def run(self) -> None:
 498        """Run the app to completion.
 499
 500        Note that this only works on builds where Ballistica manages
 501        its own event loop.
 502        """
 503        _babase.run_app()
 504
 505    def set_intent(self, intent: AppIntent) -> None:
 506        """Set the intent for the app.
 507
 508        Intent defines what the app is trying to do at a given time.
 509        This call is asynchronous; the intent switch will happen in the
 510        logic thread in the near future. If set_intent is called
 511        repeatedly before the change takes place, the final intent to be
 512        set will be used.
 513        """
 514
 515        # Mark this one as pending. We do this synchronously so that the
 516        # last one marked actually takes effect if there is overlap
 517        # (doing this in the bg thread could result in race conditions).
 518        self._pending_intent = intent
 519
 520        # Do the actual work of calcing our app-mode/etc. in a bg thread
 521        # since it may block for a moment to load modules/etc.
 522        self.threadpool.submit_no_wait(self._set_intent, intent)
 523
 524    def push_apply_app_config(self) -> None:
 525        """Internal. Use app.config.apply() to apply app config changes."""
 526        # To be safe, let's run this by itself in the event loop.
 527        # This avoids potential trouble if this gets called mid-draw or
 528        # something like that.
 529        self._pending_apply_app_config = True
 530        _babase.pushcall(self._apply_app_config, raw=True)
 531
 532    def on_native_start(self) -> None:
 533        """Called by the native layer when the app is being started."""
 534        assert _babase.in_logic_thread()
 535        assert not self._native_start_called
 536        self._native_start_called = True
 537        self._update_state()
 538
 539    def on_native_bootstrapping_complete(self) -> None:
 540        """Called by the native layer once its ready to rock."""
 541        assert _babase.in_logic_thread()
 542        assert not self._native_bootstrapping_completed
 543        self._native_bootstrapping_completed = True
 544        self._update_state()
 545
 546    def on_native_suspend(self) -> None:
 547        """Called by the native layer when the app is suspended."""
 548        assert _babase.in_logic_thread()
 549        assert not self._native_suspended  # Should avoid redundant calls.
 550        self._native_suspended = True
 551        self._update_state()
 552
 553    def on_native_unsuspend(self) -> None:
 554        """Called by the native layer when the app suspension ends."""
 555        assert _babase.in_logic_thread()
 556        assert self._native_suspended  # Should avoid redundant calls.
 557        self._native_suspended = False
 558        self._update_state()
 559
 560    def on_native_shutdown(self) -> None:
 561        """Called by the native layer when the app starts shutting down."""
 562        assert _babase.in_logic_thread()
 563        self._native_shutdown_called = True
 564        self._update_state()
 565
 566    def on_native_shutdown_complete(self) -> None:
 567        """Called by the native layer when the app is done shutting down."""
 568        assert _babase.in_logic_thread()
 569        self._native_shutdown_complete_called = True
 570        self._update_state()
 571
 572    def on_native_active_changed(self) -> None:
 573        """Called by the native layer when the app active state changes."""
 574        assert _babase.in_logic_thread()
 575        if self._mode is not None:
 576            self._mode.on_app_active_changed()
 577
 578    def handle_deep_link(self, url: str) -> None:
 579        """Handle a deep link URL."""
 580        from babase._language import Lstr
 581
 582        assert _babase.in_logic_thread()
 583
 584        appname = _babase.appname()
 585        if url.startswith(f'{appname}://code/'):
 586            code = url.replace(f'{appname}://code/', '')
 587            if self.classic is not None:
 588                self.classic.accounts.add_pending_promo_code(code)
 589        else:
 590            try:
 591                _babase.screenmessage(
 592                    Lstr(resource='errorText'), color=(1, 0, 0)
 593                )
 594                _babase.getsimplesound('error').play()
 595            except ImportError:
 596                pass
 597
 598    def on_initial_sign_in_complete(self) -> None:
 599        """Called when initial sign-in (or lack thereof) completes.
 600
 601        This normally gets called by the plus subsystem. The
 602        initial-sign-in process may include tasks such as syncing
 603        account workspaces or other data so it may take a substantial
 604        amount of time.
 605        """
 606        assert _babase.in_logic_thread()
 607        assert not self._initial_sign_in_completed
 608
 609        # Tell meta it can start scanning extra stuff that just showed
 610        # up (namely account workspaces).
 611        self.meta.start_extra_scan()
 612
 613        self._initial_sign_in_completed = True
 614        self._update_state()
 615
 616    def set_ui_scale(self, scale: babase.UIScale) -> None:
 617        """Change ui-scale on the fly.
 618
 619        Currently this is mainly for debugging and will not be called as
 620        part of normal app operation.
 621        """
 622        assert _babase.in_logic_thread()
 623
 624        # Apply to the native layer.
 625        _babase.set_ui_scale(scale.name.lower())
 626
 627        # Inform all subsystems that something screen-related has
 628        # changed. We assume subsystems won't be added at this point so
 629        # we can use the list directly.
 630        assert self._subsystem_registration_ended
 631        for subsystem in self._subsystems:
 632            try:
 633                subsystem.on_ui_scale_change()
 634            except Exception:
 635                logging.exception(
 636                    'Error in on_ui_scale_change() for subsystem %s.', subsystem
 637                )
 638
 639    def on_screen_size_change(self) -> None:
 640        """Screen size has changed."""
 641
 642        # Inform all app subsystems in the same order they were inited.
 643        # Operate on a copy of the list here because this can be called
 644        # while subsystems are still being added.
 645        for subsystem in self._subsystems.copy():
 646            try:
 647                subsystem.on_screen_size_change()
 648            except Exception:
 649                logging.exception(
 650                    'Error in on_screen_size_change() for subsystem %s.',
 651                    subsystem,
 652                )
 653
 654    def _set_intent(self, intent: AppIntent) -> None:
 655        from babase._appmode import AppMode
 656
 657        # This should be happening in a bg thread.
 658        assert not _babase.in_logic_thread()
 659        try:
 660            # Ask the selector what app-mode to use for this intent.
 661            if self.mode_selector is None:
 662                raise RuntimeError('No AppModeSelector set.')
 663
 664            modetype: type[AppMode] | None
 665
 666            # Special case - for testing we may force a specific
 667            # app-mode to handle this intent instead of going through our
 668            # usual selector.
 669            forced_mode_type = getattr(intent, '_force_app_mode_handler', None)
 670            if isinstance(forced_mode_type, type) and issubclass(
 671                forced_mode_type, AppMode
 672            ):
 673                modetype = forced_mode_type
 674            else:
 675                modetype = self.mode_selector.app_mode_for_intent(intent)
 676
 677            # NOTE: Since intents are somewhat high level things,
 678            # perhaps we should do some universal thing like a
 679            # screenmessage saying 'The app cannot handle the request'
 680            # on failure.
 681
 682            if modetype is None:
 683                raise RuntimeError(
 684                    f'No app-mode found to handle app-intent'
 685                    f' type {type(intent)}.'
 686                )
 687
 688            # Make sure the app-mode the selector gave us *actually*
 689            # supports the intent.
 690            if not modetype.can_handle_intent(intent):
 691                raise RuntimeError(
 692                    f'Intent {intent} cannot be handled by AppMode type'
 693                    f' {modetype} (selector {self.mode_selector}'
 694                    f' incorrectly thinks that it can be).'
 695                )
 696
 697            # Ok; seems legit. Now instantiate the mode if necessary and
 698            # kick back to the logic thread to apply.
 699            mode = self._mode_instances.get(modetype)
 700            if mode is None:
 701                self._mode_instances[modetype] = mode = modetype()
 702            _babase.pushcall(
 703                partial(self._apply_intent, intent, mode),
 704                from_other_thread=True,
 705            )
 706        except Exception:
 707            logging.exception('Error setting app intent to %s.', intent)
 708            _babase.pushcall(
 709                partial(self._display_set_intent_error, intent),
 710                from_other_thread=True,
 711            )
 712
 713    def _apply_intent(self, intent: AppIntent, mode: AppMode) -> None:
 714        assert _babase.in_logic_thread()
 715
 716        # ONLY apply this intent if it is still the most recent one
 717        # submitted.
 718        if intent is not self._pending_intent:
 719            return
 720
 721        # If the app-mode for this intent is different than the active
 722        # one, switch modes.
 723        if type(mode) is not type(self._mode):
 724            if self._mode is None:
 725                is_initial_mode = True
 726            else:
 727                is_initial_mode = False
 728                try:
 729                    self._mode.on_deactivate()
 730                except Exception:
 731                    logging.exception(
 732                        'Error deactivating app-mode %s.', self._mode
 733                    )
 734
 735            # Reset all subsystems. We assume subsystems won't be added
 736            # at this point so we can use the list directly.
 737            assert self._subsystem_registration_ended
 738            for subsystem in self._subsystems:
 739                try:
 740                    subsystem.reset()
 741                except Exception:
 742                    logging.exception(
 743                        'Error in reset() for subsystem %s.', subsystem
 744                    )
 745
 746            self._mode = mode
 747            try:
 748                mode.on_activate()
 749            except Exception:
 750                # Hmm; what should we do in this case?...
 751                logging.exception('Error activating app-mode %s.', mode)
 752
 753            # Let the world know when we first have an app-mode; certain
 754            # app stuff such as input processing can proceed at that
 755            # point.
 756            if is_initial_mode:
 757                _babase.on_initial_app_mode_set()
 758
 759        try:
 760            mode.handle_intent(intent)
 761        except Exception:
 762            logging.exception(
 763                'Error handling intent %s in app-mode %s.', intent, mode
 764            )
 765
 766    def _display_set_intent_error(self, intent: AppIntent) -> None:
 767        """Show the *user* something went wrong setting an intent."""
 768        from babase._language import Lstr
 769
 770        del intent
 771        _babase.screenmessage(Lstr(resource='errorText'), color=(1, 0, 0))
 772        _babase.getsimplesound('error').play()
 773
 774    def _on_initing(self) -> None:
 775        """Called when the app enters the initing state.
 776
 777        Here we can put together subsystems and other pieces for the
 778        app, but most things should not be doing any work yet.
 779        """
 780        # pylint: disable=cyclic-import
 781        from babase import _asyncio
 782        from babase import _appconfig
 783        from babase._apputils import AppHealthMonitor
 784        from babase import _env
 785
 786        assert _babase.in_logic_thread()
 787
 788        _env.on_app_state_initing()
 789
 790        self._asyncio_loop = _asyncio.setup_asyncio()
 791        self.health_monitor = AppHealthMonitor()
 792
 793        # __FEATURESET_APP_SUBSYSTEM_CREATE_BEGIN__
 794        # This section generated by batools.appmodule; do not edit.
 795
 796        # Poke these attrs to create all our subsystems.
 797        _ = self.plus
 798        _ = self.classic
 799        _ = self.ui_v1
 800
 801        # __FEATURESET_APP_SUBSYSTEM_CREATE_END__
 802
 803        # We're a pretty short-lived state. This should flip us to
 804        # 'loading'.
 805        self._init_completed = True
 806        self._update_state()
 807
 808    def _on_loading(self) -> None:
 809        """Called when we enter the loading state.
 810
 811        At this point, all built-in pieces of the app should be in place
 812        and can start talking to each other and doing work. Though at a
 813        high level, the goal of the app at this point is only to sign in
 814        to initial accounts, download workspaces, and otherwise prepare
 815        itself to really 'run'.
 816        """
 817        assert _babase.in_logic_thread()
 818
 819        # Get meta-system scanning built-in stuff in the bg.
 820        self.meta.start_scan(scan_complete_cb=self._on_meta_scan_complete)
 821
 822        # Inform all app subsystems in the same order they were inited.
 823        # Operate on a copy of the list here because subsystems can
 824        # still be added at this point.
 825        for subsystem in self._subsystems.copy():
 826            try:
 827                subsystem.on_app_loading()
 828            except Exception:
 829                logging.exception(
 830                    'Error in on_app_loading() for subsystem %s.', subsystem
 831                )
 832
 833        # Normally plus tells us when initial sign-in is done. If plus
 834        # is not present, however, we just do it ourself so we can
 835        # proceed on to the running state.
 836        if self.plus is None:
 837            _babase.pushcall(self.on_initial_sign_in_complete)
 838
 839    def _on_meta_scan_complete(self) -> None:
 840        """Called when meta-scan is done doing its thing."""
 841        assert _babase.in_logic_thread()
 842
 843        # Now that we know what's out there, build our final plugin set.
 844        self.plugins.on_meta_scan_complete()
 845
 846        assert not self._meta_scan_completed
 847        self._meta_scan_completed = True
 848        self._update_state()
 849
 850    def _on_running(self) -> None:
 851        """Called when we enter the running state.
 852
 853        At this point, all workspaces, initial accounts, etc. are in place
 854        and we can actually get started doing whatever we're gonna do.
 855        """
 856        assert _babase.in_logic_thread()
 857
 858        # Let our native layer know.
 859        _babase.on_app_running()
 860
 861        # Set a default app-mode-selector if none has been set yet
 862        # by a plugin or whatnot.
 863        if self._mode_selector is None:
 864            self._mode_selector = self.DefaultAppModeSelector()
 865
 866        # Inform all app subsystems in the same order they were
 867        # registered. Operate on a copy here because subsystems can
 868        # still be added at this point.
 869        #
 870        # NOTE: Do we need to allow registering still at this point? If
 871        # something gets registered here, it won't have its
 872        # on_app_running callback called. Hmm; I suppose that's the only
 873        # way that plugins can register subsystems though.
 874        for subsystem in self._subsystems.copy():
 875            try:
 876                subsystem.on_app_running()
 877            except Exception:
 878                logging.exception(
 879                    'Error in on_app_running() for subsystem %s.', subsystem
 880                )
 881
 882        # Cut off new subsystem additions at this point.
 883        self._subsystem_registration_ended = True
 884
 885        # If 'exec' code was provided to the app, always kick that off
 886        # here as an intent.
 887        exec_cmd = _babase.exec_arg()
 888        if exec_cmd is not None:
 889            self.set_intent(AppIntentExec(exec_cmd))
 890        elif self._pending_intent is None:
 891            # Otherwise tell the app to do its default thing *only* if a
 892            # plugin hasn't already told it to do something.
 893            self.set_intent(AppIntentDefault())
 894
 895    def _apply_app_config(self) -> None:
 896        assert _babase.in_logic_thread()
 897
 898        lifecyclelog.info('apply-app-config')
 899
 900        # If multiple apply calls have been made, only actually apply
 901        # once.
 902        if not self._pending_apply_app_config:
 903            return
 904
 905        _pending_apply_app_config = False
 906
 907        # Inform all app subsystems in the same order they were inited.
 908        # Operate on a copy here because subsystems may still be able to
 909        # be added at this point.
 910        for subsystem in self._subsystems.copy():
 911            try:
 912                subsystem.do_apply_app_config()
 913            except Exception:
 914                logging.exception(
 915                    'Error in do_apply_app_config() for subsystem %s.',
 916                    subsystem,
 917                )
 918
 919        # Let the native layer do its thing.
 920        _babase.do_apply_app_config()
 921
 922    def _update_state(self) -> None:
 923        # pylint: disable=too-many-branches
 924        assert _babase.in_logic_thread()
 925
 926        # Shutdown-complete trumps absolutely all.
 927        if self._native_shutdown_complete_called:
 928            if self.state is not self.State.SHUTDOWN_COMPLETE:
 929                self.state = self.State.SHUTDOWN_COMPLETE
 930                lifecyclelog.info('app-state is now %s', self.state.name)
 931                self._on_shutdown_complete()
 932
 933        # Shutdown trumps all. Though we can't start shutting down until
 934        # init is completed since we need our asyncio stuff to exist for
 935        # the shutdown process.
 936        elif self._native_shutdown_called and self._init_completed:
 937            # Entering shutdown state:
 938            if self.state is not self.State.SHUTTING_DOWN:
 939                self.state = self.State.SHUTTING_DOWN
 940                applog.info('Shutting down...')
 941                lifecyclelog.info('app-state is now %s', self.state.name)
 942                self._on_shutting_down()
 943
 944        elif self._native_suspended:
 945            # Entering suspended state:
 946            if self.state is not self.State.SUSPENDED:
 947                self.state = self.State.SUSPENDED
 948                self._on_suspend()
 949        else:
 950            # Leaving suspended state:
 951            if self.state is self.State.SUSPENDED:
 952                self._on_unsuspend()
 953
 954            # Entering or returning to running state
 955            if self._initial_sign_in_completed and self._meta_scan_completed:
 956                if self.state != self.State.RUNNING:
 957                    self.state = self.State.RUNNING
 958                    lifecyclelog.info('app-state is now %s', self.state.name)
 959                    if not self._called_on_running:
 960                        self._called_on_running = True
 961                        self._on_running()
 962
 963            # Entering or returning to loading state:
 964            elif self._init_completed:
 965                if self.state is not self.State.LOADING:
 966                    self.state = self.State.LOADING
 967                    lifecyclelog.info('app-state is now %s', self.state.name)
 968                    if not self._called_on_loading:
 969                        self._called_on_loading = True
 970                        self._on_loading()
 971
 972            # Entering or returning to initing state:
 973            elif self._native_bootstrapping_completed:
 974                if self.state is not self.State.INITING:
 975                    self.state = self.State.INITING
 976                    lifecyclelog.info('app-state is now %s', self.state.name)
 977                    if not self._called_on_initing:
 978                        self._called_on_initing = True
 979                        self._on_initing()
 980
 981            # Entering or returning to native bootstrapping:
 982            elif self._native_start_called:
 983                if self.state is not self.State.NATIVE_BOOTSTRAPPING:
 984                    self.state = self.State.NATIVE_BOOTSTRAPPING
 985                    lifecyclelog.info('app-state is now %s', self.state.name)
 986            else:
 987                # Only logical possibility left is NOT_STARTED, in which
 988                # case we should not be getting called.
 989                logging.warning(
 990                    'App._update_state called while in %s state;'
 991                    ' should not happen.',
 992                    self.state.value,
 993                    stack_info=True,
 994                )
 995
 996    async def _shutdown(self) -> None:
 997        import asyncio
 998
 999        _babase.lock_all_input()
1000        try:
1001            async with asyncio.TaskGroup() as task_group:
1002                for task_coro in self._shutdown_tasks:
1003                    # Note: Mypy currently complains if we don't take
1004                    # this return value, but we don't actually need to.
1005                    # https://github.com/python/mypy/issues/15036
1006                    _ = task_group.create_task(
1007                        self._run_shutdown_task(task_coro)
1008                    )
1009        except* Exception:
1010            logging.exception('Unexpected error(s) in shutdown.')
1011
1012        # Note: ideally we should run this directly here, but currently
1013        # it does some legacy stuff which blocks, so running it here
1014        # gives us asyncio task-took-too-long warnings. If we can
1015        # convert those to nice graceful async tasks we should revert
1016        # this to a direct call.
1017        _babase.pushcall(_babase.complete_shutdown)
1018
1019    async def _run_shutdown_task(
1020        self, coro: Coroutine[None, None, None]
1021    ) -> None:
1022        """Run a shutdown task; report errors and abort if taking too long."""
1023        import asyncio
1024
1025        task = asyncio.create_task(coro)
1026        try:
1027            await asyncio.wait_for(task, self.SHUTDOWN_TASK_TIMEOUT_SECONDS)
1028        except Exception:
1029            logging.exception('Error in shutdown task (%s).', coro)
1030
1031    def _on_suspend(self) -> None:
1032        """Called when the app goes to a suspended state."""
1033        assert _babase.in_logic_thread()
1034
1035        # Suspend all app subsystems in the opposite order they were inited.
1036        for subsystem in reversed(self._subsystems):
1037            try:
1038                subsystem.on_app_suspend()
1039            except Exception:
1040                logging.exception(
1041                    'Error in on_app_suspend() for subsystem %s.', subsystem
1042                )
1043
1044    def _on_unsuspend(self) -> None:
1045        """Called when unsuspending."""
1046        assert _babase.in_logic_thread()
1047        self.fg_state += 1
1048
1049        # Unsuspend all app subsystems in the same order they were inited.
1050        for subsystem in self._subsystems:
1051            try:
1052                subsystem.on_app_unsuspend()
1053            except Exception:
1054                logging.exception(
1055                    'Error in on_app_unsuspend() for subsystem %s.', subsystem
1056                )
1057
1058    def _on_shutting_down(self) -> None:
1059        """(internal)"""
1060        assert _babase.in_logic_thread()
1061
1062        # Inform app subsystems that we're shutting down in the opposite
1063        # order they were inited.
1064        for subsystem in reversed(self._subsystems):
1065            try:
1066                subsystem.on_app_shutdown()
1067            except Exception:
1068                logging.exception(
1069                    'Error in on_app_shutdown() for subsystem %s.', subsystem
1070                )
1071
1072        # Now kick off any async shutdown task(s).
1073        assert self._asyncio_loop is not None
1074        self._shutdown_task = self._asyncio_loop.create_task(self._shutdown())
1075
1076    def _on_shutdown_complete(self) -> None:
1077        """(internal)"""
1078        assert _babase.in_logic_thread()
1079
1080        # Deactivate any active app-mode. This allows things like saving
1081        # state to happen naturally without needing to handle
1082        # app-shutdown as a special case.
1083        if self._mode is not None:
1084            try:
1085                self._mode.on_deactivate()
1086            except Exception:
1087                logging.exception(
1088                    'Error deactivating app-mode %s at app shutdown.',
1089                    self._mode,
1090                )
1091            self._mode = None
1092
1093        # Inform app subsystems that we're done shutting down in the opposite
1094        # order they were inited.
1095        for subsystem in reversed(self._subsystems):
1096            try:
1097                subsystem.on_app_shutdown_complete()
1098            except Exception:
1099                logging.exception(
1100                    'Error in on_app_shutdown_complete() for subsystem %s.',
1101                    subsystem,
1102                )
1103
1104    async def _wait_for_shutdown_suppressions(self) -> None:
1105        import asyncio
1106
1107        # Spin and wait for anything blocking shutdown to complete.
1108        starttime = _babase.apptime()
1109        lifecyclelog.info('shutdown-suppress-wait begin')
1110        while _babase.shutdown_suppress_count() > 0:
1111            await asyncio.sleep(0.001)
1112        lifecyclelog.info('shutdown-suppress-wait end')
1113        duration = _babase.apptime() - starttime
1114        if duration > 1.0:
1115            logging.warning(
1116                'Shutdown-suppressions lasted longer than ideal '
1117                '(%.2f seconds).',
1118                duration,
1119            )
1120
1121    async def _fade_and_shutdown_graphics(self) -> None:
1122        import asyncio
1123
1124        # Kick off a short fade and give it time to complete.
1125        lifecyclelog.info('fade-and-shutdown-graphics begin')
1126        _babase.fade_screen(False, time=0.15)
1127        await asyncio.sleep(0.15)
1128
1129        # Now tell the graphics system to go down and wait until
1130        # it has done so.
1131        _babase.graphics_shutdown_begin()
1132        while not _babase.graphics_shutdown_is_complete():
1133            await asyncio.sleep(0.01)
1134        lifecyclelog.info('fade-and-shutdown-graphics end')
1135
1136    async def _fade_and_shutdown_audio(self) -> None:
1137        import asyncio
1138
1139        # Tell the audio system to go down and give it a bit of
1140        # time to do so gracefully.
1141        lifecyclelog.info('fade-and-shutdown-audio begin')
1142        _babase.audio_shutdown_begin()
1143        await asyncio.sleep(0.15)
1144        while not _babase.audio_shutdown_is_complete():
1145            await asyncio.sleep(0.01)
1146        lifecyclelog.info('fade-and-shutdown-audio end')
1147
1148    def _thread_pool_thread_init(self) -> None:
1149        # Help keep things clear in profiling tools/etc.
1150        self._pool_thread_count += 1
1151        _babase.set_thread_name(f'ballistica worker-{self._pool_thread_count}')

High level Ballistica app functionality and state.

Access the single shared instance of this class via the "app" attr available on various high level modules such as bauiv1 and bascenev1.

health_monitor: babase.AppHealthMonitor
env: _babase.Env
state: App.State
net: babase._net.NetworkSubsystem
workspaces: babase._workspace.WorkspaceSubsystem
components: babase._appcomponent.AppComponentSubsystem
fg_state: int
SHUTDOWN_TASK_TIMEOUT_SECONDS = 12
def postinit(self) -> None:
248    def postinit(self) -> None:
249        """Called after we've been inited and assigned to babase.app.
250
251        Anything that accesses babase.app as part of its init process
252        must go here instead of __init__.
253        """
254
255        # Hack for docs-generation: We can be imported with dummy
256        # modules instead of our actual binary ones, but we don't
257        # function.
258        if os.environ.get('BA_RUNNING_WITH_DUMMY_MODULES') == '1':
259            return
260
261        self.lang = LanguageSubsystem()
262        self.plugins = PluginSubsystem()

Called after we've been inited and assigned to babase.app.

Anything that accesses babase.app as part of its init process must go here instead of __init__.

active: bool
264    @property
265    def active(self) -> bool:
266        """Whether the app is currently front and center.
267
268        This will be False when the app is hidden, other activities
269        are covering it, etc. (depending on the platform).
270        """
271        return _babase.app_is_active()

Whether the app is currently front and center.

This will be False when the app is hidden, other activities are covering it, etc. (depending on the platform).

mode: AppMode | None
273    @property
274    def mode(self) -> AppMode | None:
275        """The app's current mode."""
276        assert _babase.in_logic_thread()
277        return self._mode

The app's current mode.

asyncio_loop: asyncio.events.AbstractEventLoop
279    @property
280    def asyncio_loop(self) -> asyncio.AbstractEventLoop:
281        """The logic thread's asyncio event loop.
282
283        This allow async tasks to be run in the logic thread.
284
285        Generally you should call App.create_async_task() to schedule
286        async code to run instead of using this directly. That will
287        handle retaining the task and logging errors automatically.
288        Only schedule tasks onto asyncio_loop yourself when you intend
289        to hold on to the returned task and await its results. Releasing
290        the task reference can lead to subtle bugs such as unreported
291        errors and garbage-collected tasks disappearing before their
292        work is done.
293
294        Note that, at this time, the asyncio loop is encapsulated
295        and explicitly stepped by the engine's logic thread loop and
296        thus things like asyncio.get_running_loop() will unintuitively
297        *not* return this loop from most places in the logic thread;
298        only from within a task explicitly created in this loop.
299        Hopefully this situation will be improved in the future with a
300        unified event loop.
301        """
302        assert _babase.in_logic_thread()
303        assert self._asyncio_loop is not None
304        return self._asyncio_loop

The logic thread's asyncio event loop.

This allow async tasks to be run in the logic thread.

Generally you should call App.create_async_task() to schedule async code to run instead of using this directly. That will handle retaining the task and logging errors automatically. Only schedule tasks onto asyncio_loop yourself when you intend to hold on to the returned task and await its results. Releasing the task reference can lead to subtle bugs such as unreported errors and garbage-collected tasks disappearing before their work is done.

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 unintuitively not return this loop from most places in the logic thread; only from within a task explicitly created in this loop. Hopefully this situation will be improved in the future with a unified event loop.

def create_async_task(self, coro: Coroutine[Any, Any, ~T], *, name: str | None = None) -> None:
306    def create_async_task(
307        self, coro: Coroutine[Any, Any, T], *, name: str | None = None
308    ) -> None:
309        """Create a fully managed async task.
310
311        This will automatically retain and release a reference to the task
312        and log any exceptions that occur in it. If you need to await a task
313        or otherwise need more control, schedule a task directly using
314        App.asyncio_loop.
315        """
316        assert _babase.in_logic_thread()
317
318        # We hold a strong reference to the task until it is done.
319        # Otherwise it is possible for it to be garbage collected and
320        # disappear midway if the caller does not hold on to the
321        # returned task, which seems like a great way to introduce
322        # hard-to-track bugs.
323        task = self.asyncio_loop.create_task(coro, name=name)
324        self._asyncio_tasks.add(task)
325        task.add_done_callback(self._on_task_done)

Create a fully managed async task.

This will automatically retain and release a reference to the task and log any exceptions that occur in it. If you need to await a task or otherwise need more control, schedule a task directly using App.asyncio_loop.

mode_selector: babase.AppModeSelector
340    @property
341    def mode_selector(self) -> babase.AppModeSelector:
342        """Controls which app-modes are used for handling given intents.
343
344        Plugins can override this to change high level app behavior and
345        spinoff projects can change the default implementation for the
346        same effect.
347        """
348        if self._mode_selector is None:
349            raise RuntimeError(
350                'mode_selector cannot be used until the app reaches'
351                ' the running state.'
352            )
353        return self._mode_selector

Controls which app-modes are used for handling given intents.

Plugins can override this to change high level app behavior and spinoff projects can change the default implementation for the same effect.

classic: baclassic.ClassicAppSubsystem | None
409    @property
410    def classic(self) -> ClassicAppSubsystem | None:
411        """Our classic subsystem (if available)."""
412        return self._get_subsystem_property(
413            'classic', self._create_classic_subsystem
414        )  # type: ignore

Our classic subsystem (if available).

plus: baplus.PlusAppSubsystem | None
429    @property
430    def plus(self) -> PlusAppSubsystem | None:
431        """Our plus subsystem (if available)."""
432        return self._get_subsystem_property(
433            'plus', self._create_plus_subsystem
434        )  # type: ignore

Our plus subsystem (if available).

ui_v1: bauiv1.UIV1AppSubsystem
449    @property
450    def ui_v1(self) -> UIV1AppSubsystem:
451        """Our ui_v1 subsystem (always available)."""
452        return self._get_subsystem_property(
453            'ui_v1', self._create_ui_v1_subsystem
454        )  # type: ignore

Our ui_v1 subsystem (always available).

def register_subsystem(self, subsystem: babase.AppSubsystem) -> None:
466    def register_subsystem(self, subsystem: AppSubsystem) -> None:
467        """Called by the AppSubsystem class. Do not use directly."""
468
469        # We only allow registering new subsystems if we've not yet
470        # reached the 'running' state. This ensures that all subsystems
471        # receive a consistent set of callbacks starting with
472        # on_app_running().
473
474        if self._subsystem_registration_ended:
475            raise RuntimeError(
476                'Subsystems can no longer be registered at this point.'
477            )
478        self._subsystems.append(subsystem)

Called by the AppSubsystem class. Do not use directly.

def add_shutdown_task(self, coro: Coroutine[NoneType, NoneType, NoneType]) -> None:
480    def add_shutdown_task(self, coro: Coroutine[None, None, None]) -> None:
481        """Add a task to be run on app shutdown.
482
483        Note that shutdown tasks will be canceled after
484        :py:const:`SHUTDOWN_TASK_TIMEOUT_SECONDS` if they are still
485        running.
486        """
487        if (
488            self.state is self.State.SHUTTING_DOWN
489            or self.state is self.State.SHUTDOWN_COMPLETE
490        ):
491            stname = self.state.name
492            raise RuntimeError(
493                f'Cannot add shutdown tasks with current state {stname}.'
494            )
495        self._shutdown_tasks.append(coro)

Add a task to be run on app shutdown.

Note that shutdown tasks will be canceled after SHUTDOWN_TASK_TIMEOUT_SECONDS if they are still running.

def run(self) -> None:
497    def run(self) -> None:
498        """Run the app to completion.
499
500        Note that this only works on builds where Ballistica manages
501        its own event loop.
502        """
503        _babase.run_app()

Run the app to completion.

Note that this only works on builds where Ballistica manages its own event loop.

def set_intent(self, intent: AppIntent) -> None:
505    def set_intent(self, intent: AppIntent) -> None:
506        """Set the intent for the app.
507
508        Intent defines what the app is trying to do at a given time.
509        This call is asynchronous; the intent switch will happen in the
510        logic thread in the near future. If set_intent is called
511        repeatedly before the change takes place, the final intent to be
512        set will be used.
513        """
514
515        # Mark this one as pending. We do this synchronously so that the
516        # last one marked actually takes effect if there is overlap
517        # (doing this in the bg thread could result in race conditions).
518        self._pending_intent = intent
519
520        # Do the actual work of calcing our app-mode/etc. in a bg thread
521        # since it may block for a moment to load modules/etc.
522        self.threadpool.submit_no_wait(self._set_intent, intent)

Set the intent for the app.

Intent defines what the app is trying to do at a given time. This call is asynchronous; the intent switch will happen in the logic thread in the near future. If set_intent is called repeatedly before the change takes place, the final intent to be set will be used.

def push_apply_app_config(self) -> None:
524    def push_apply_app_config(self) -> None:
525        """Internal. Use app.config.apply() to apply app config changes."""
526        # To be safe, let's run this by itself in the event loop.
527        # This avoids potential trouble if this gets called mid-draw or
528        # something like that.
529        self._pending_apply_app_config = True
530        _babase.pushcall(self._apply_app_config, raw=True)

Internal. Use app.config.apply() to apply app config changes.

def on_native_start(self) -> None:
532    def on_native_start(self) -> None:
533        """Called by the native layer when the app is being started."""
534        assert _babase.in_logic_thread()
535        assert not self._native_start_called
536        self._native_start_called = True
537        self._update_state()

Called by the native layer when the app is being started.

def on_native_bootstrapping_complete(self) -> None:
539    def on_native_bootstrapping_complete(self) -> None:
540        """Called by the native layer once its ready to rock."""
541        assert _babase.in_logic_thread()
542        assert not self._native_bootstrapping_completed
543        self._native_bootstrapping_completed = True
544        self._update_state()

Called by the native layer once its ready to rock.

def on_native_suspend(self) -> None:
546    def on_native_suspend(self) -> None:
547        """Called by the native layer when the app is suspended."""
548        assert _babase.in_logic_thread()
549        assert not self._native_suspended  # Should avoid redundant calls.
550        self._native_suspended = True
551        self._update_state()

Called by the native layer when the app is suspended.

def on_native_unsuspend(self) -> None:
553    def on_native_unsuspend(self) -> None:
554        """Called by the native layer when the app suspension ends."""
555        assert _babase.in_logic_thread()
556        assert self._native_suspended  # Should avoid redundant calls.
557        self._native_suspended = False
558        self._update_state()

Called by the native layer when the app suspension ends.

def on_native_shutdown(self) -> None:
560    def on_native_shutdown(self) -> None:
561        """Called by the native layer when the app starts shutting down."""
562        assert _babase.in_logic_thread()
563        self._native_shutdown_called = True
564        self._update_state()

Called by the native layer when the app starts shutting down.

def on_native_shutdown_complete(self) -> None:
566    def on_native_shutdown_complete(self) -> None:
567        """Called by the native layer when the app is done shutting down."""
568        assert _babase.in_logic_thread()
569        self._native_shutdown_complete_called = True
570        self._update_state()

Called by the native layer when the app is done shutting down.

def on_native_active_changed(self) -> None:
572    def on_native_active_changed(self) -> None:
573        """Called by the native layer when the app active state changes."""
574        assert _babase.in_logic_thread()
575        if self._mode is not None:
576            self._mode.on_app_active_changed()

Called by the native layer when the app active state changes.

def on_initial_sign_in_complete(self) -> None:
598    def on_initial_sign_in_complete(self) -> None:
599        """Called when initial sign-in (or lack thereof) completes.
600
601        This normally gets called by the plus subsystem. The
602        initial-sign-in process may include tasks such as syncing
603        account workspaces or other data so it may take a substantial
604        amount of time.
605        """
606        assert _babase.in_logic_thread()
607        assert not self._initial_sign_in_completed
608
609        # Tell meta it can start scanning extra stuff that just showed
610        # up (namely account workspaces).
611        self.meta.start_extra_scan()
612
613        self._initial_sign_in_completed = True
614        self._update_state()

Called when initial sign-in (or lack thereof) completes.

This normally gets called by the plus subsystem. The initial-sign-in process may include tasks such as syncing account workspaces or other data so it may take a substantial amount of time.

def set_ui_scale(self, scale: UIScale) -> None:
616    def set_ui_scale(self, scale: babase.UIScale) -> None:
617        """Change ui-scale on the fly.
618
619        Currently this is mainly for debugging and will not be called as
620        part of normal app operation.
621        """
622        assert _babase.in_logic_thread()
623
624        # Apply to the native layer.
625        _babase.set_ui_scale(scale.name.lower())
626
627        # Inform all subsystems that something screen-related has
628        # changed. We assume subsystems won't be added at this point so
629        # we can use the list directly.
630        assert self._subsystem_registration_ended
631        for subsystem in self._subsystems:
632            try:
633                subsystem.on_ui_scale_change()
634            except Exception:
635                logging.exception(
636                    'Error in on_ui_scale_change() for subsystem %s.', subsystem
637                )

Change ui-scale on the fly.

Currently this is mainly for debugging and will not be called as part of normal app operation.

def on_screen_size_change(self) -> None:
639    def on_screen_size_change(self) -> None:
640        """Screen size has changed."""
641
642        # Inform all app subsystems in the same order they were inited.
643        # Operate on a copy of the list here because this can be called
644        # while subsystems are still being added.
645        for subsystem in self._subsystems.copy():
646            try:
647                subsystem.on_screen_size_change()
648            except Exception:
649                logging.exception(
650                    'Error in on_screen_size_change() for subsystem %s.',
651                    subsystem,
652                )

Screen size has changed.

class App.State(enum.Enum):
 62    class State(Enum):
 63        """High level state the app can be in."""
 64
 65        #: The app has not yet begun starting and should not be used in
 66        #: any way.
 67        NOT_STARTED = 0
 68
 69        #: The native layer is spinning up its machinery (screens,
 70        #: renderers, etc.). Nothing should happen in the Python layer
 71        #: until this completes.
 72        NATIVE_BOOTSTRAPPING = 1
 73
 74        #: Python app subsystems are being inited but should not yet
 75        #: interact or do any work.
 76        INITING = 2
 77
 78        #: Python app subsystems are inited and interacting, but the app
 79        #: has not yet embarked on a high level course of action. It is
 80        #: doing initial account logins, workspace & asset downloads,
 81        #: etc.
 82        LOADING = 3
 83
 84        #: All pieces are in place and the app is now doing its thing.
 85        RUNNING = 4
 86
 87        #: Used on platforms such as mobile where the app basically needs
 88        #: to shut down while backgrounded. In this state, all event
 89        #: loops are suspended and all graphics and audio must cease
 90        #: completely. Be aware that the suspended state can be entered
 91        #: from any other state including NATIVE_BOOTSTRAPPING and
 92        #: SHUTTING_DOWN.
 93        SUSPENDED = 5
 94
 95        #: The app is shutting down. This process may involve sending
 96        #: network messages or other things that can take up to a few
 97        #: seconds, so ideally graphics and audio should remain
 98        #: functional (with fades or spinners or whatever to show
 99        #: something is happening).
100        SHUTTING_DOWN = 6
101
102        #: The app has completed shutdown. Any code running here should
103        #: be basically immediate.
104        SHUTDOWN_COMPLETE = 7

High level state the app can be in.

NOT_STARTED = <State.NOT_STARTED: 0>
NATIVE_BOOTSTRAPPING = <State.NATIVE_BOOTSTRAPPING: 1>
INITING = <State.INITING: 2>
LOADING = <State.LOADING: 3>
RUNNING = <State.RUNNING: 4>
SUSPENDED = <State.SUSPENDED: 5>
SHUTTING_DOWN = <State.SHUTTING_DOWN: 6>
SHUTDOWN_COMPLETE = <State.SHUTDOWN_COMPLETE: 7>
class App.DefaultAppModeSelector(babase._appmodeselector.AppModeSelector):
106    class DefaultAppModeSelector(AppModeSelector):
107        """Decides which AppMode to use to handle AppIntents.
108
109        This default version is generated by the project updater based
110        on the 'default_app_modes' value in the projectconfig.
111
112        It is also possible to modify app mode selection behavior by
113        setting app.mode_selector to an instance of a custom
114        AppModeSelector subclass. This is a good way to go if you are
115        modifying app behavior dynamically via a plugin instead of
116        statically in a spinoff project.
117        """
118
119        @override
120        def app_mode_for_intent(
121            self, intent: AppIntent
122        ) -> type[AppMode] | None:
123            # pylint: disable=cyclic-import
124
125            # __DEFAULT_APP_MODE_SELECTION_BEGIN__
126            # This section generated by batools.appmodule; do not edit.
127
128            # Ask our default app modes to handle it.
129            # (generated from 'default_app_modes' in projectconfig).
130            import baclassic
131            import babase
132
133            for appmode in [
134                baclassic.ClassicAppMode,
135                babase.EmptyAppMode,
136            ]:
137                if appmode.can_handle_intent(intent):
138                    return appmode
139
140            return None
141
142            # __DEFAULT_APP_MODE_SELECTION_END__

Decides which AppMode to use to handle AppIntents.

This default version is generated by the project updater based on the 'default_app_modes' value in the projectconfig.

It is also possible to modify app mode selection behavior by setting app.mode_selector to an instance of a custom AppModeSelector subclass. This is a good way to go if you are modifying app behavior dynamically via a plugin instead of statically in a spinoff project.

@override
def app_mode_for_intent( self, intent: AppIntent) -> type[AppMode] | None:
119        @override
120        def app_mode_for_intent(
121            self, intent: AppIntent
122        ) -> type[AppMode] | None:
123            # pylint: disable=cyclic-import
124
125            # __DEFAULT_APP_MODE_SELECTION_BEGIN__
126            # This section generated by batools.appmodule; do not edit.
127
128            # Ask our default app modes to handle it.
129            # (generated from 'default_app_modes' in projectconfig).
130            import baclassic
131            import babase
132
133            for appmode in [
134                baclassic.ClassicAppMode,
135                babase.EmptyAppMode,
136            ]:
137                if appmode.can_handle_intent(intent):
138                    return appmode
139
140            return None
141
142            # __DEFAULT_APP_MODE_SELECTION_END__

Given an AppIntent, return the AppMode that should handle it.

If None is returned, the AppIntent will be ignored.

This may be called in a background thread, so avoid any calls limited to logic thread use/etc.

class AppIntent:
13class AppIntent:
14    """A high level directive given to the app."""

A high level directive given to the app.

class AppIntentDefault(bascenev1.AppIntent):
17class AppIntentDefault(AppIntent):
18    """Tells the app to simply run in its default mode."""

Tells the app to simply run in its default mode.

class AppIntentExec(bascenev1.AppIntent):
21class AppIntentExec(AppIntent):
22    """Tells the app to exec some Python code."""
23
24    def __init__(self, code: str):
25        self.code = code

Tells the app to exec some Python code.

AppIntentExec(code: str)
24    def __init__(self, code: str):
25        self.code = code
code
class AppMode:
 14class AppMode:
 15    """A high level mode for the app."""
 16
 17    @classmethod
 18    def get_app_experience(cls) -> AppExperience:
 19        """Return the overall experience provided by this mode."""
 20        raise NotImplementedError('AppMode subclasses must override this.')
 21
 22    @classmethod
 23    def can_handle_intent(cls, intent: AppIntent) -> bool:
 24        """Return whether this mode can handle the provided intent.
 25
 26        For this to return True, the AppMode must claim to support the
 27        provided intent (via its _can_handle_intent() method) AND the
 28        AppExperience associated with the AppMode must be supported by
 29        the current app and runtime environment.
 30        """
 31        # TODO: check AppExperience against current environment.
 32        return cls._can_handle_intent(intent)
 33
 34    @classmethod
 35    def _can_handle_intent(cls, intent: AppIntent) -> bool:
 36        """Return whether our mode can handle the provided intent.
 37
 38        AppModes should override this to communicate what they can
 39        handle. Note that AppExperience does not have to be considered
 40        here; that is handled automatically by the can_handle_intent()
 41        call.
 42        """
 43        raise NotImplementedError('AppMode subclasses must override this.')
 44
 45    def handle_intent(self, intent: AppIntent) -> None:
 46        """Handle an intent."""
 47        raise NotImplementedError('AppMode subclasses must override this.')
 48
 49    def on_activate(self) -> None:
 50        """Called when the mode is becoming the active one fro the app."""
 51
 52    def on_deactivate(self) -> None:
 53        """Called when the mode stops being the active one for the app.
 54
 55        On platforms where the app is explicitly exited (such as desktop
 56        PC) this will also be called at app shutdown.
 57
 58        To best cover both mobile and desktop style platforms, actions
 59        such as saving state should generally happen in response to both
 60        on_deactivate() and on_app_active_changed() (when active is
 61        False).
 62        """
 63
 64    def on_app_active_changed(self) -> None:
 65        """Called when app active state changes while in this app-mode.
 66
 67        This corresponds to :attr:`babase.App.active`. App-active state
 68        becomes false when the app is hidden, minimized, backgrounded,
 69        etc. The app-mode may want to take action such as pausing a
 70        running game or saving state when this occurs.
 71
 72        On platforms such as mobile where apps get suspended and later
 73        silently terminated by the OS, this is likely to be the last
 74        reliable place to save state/etc.
 75
 76        To best cover both mobile and desktop style platforms, actions
 77        such as saving state should generally happen in response to both
 78        on_deactivate() and on_app_active_changed() (when active is
 79        False).
 80        """
 81
 82    def on_purchase_process_begin(
 83        self, item_id: str, user_initiated: bool
 84    ) -> None:
 85        """Called when in-app-purchase processing is beginning.
 86
 87        This call happens after a purchase has been completed locally
 88        but before its receipt/info is sent to the master-server to
 89        apply to the account.
 90        """
 91        # pylint: disable=cyclic-import
 92        import babase
 93
 94        del item_id  # Unused.
 95
 96        # Show nothing for stuff not directly kicked off by the user.
 97        if not user_initiated:
 98            return
 99
100        babase.screenmessage(
101            babase.Lstr(resource='updatingAccountText'),
102            color=(0, 1, 0),
103        )
104        # Ick; we can be called early in the bootstrapping process
105        # before we're allowed to load assets. Guard against that.
106        if babase.asset_loads_allowed():
107            babase.getsimplesound('click01').play()
108
109    def on_purchase_process_end(
110        self, item_id: str, user_initiated: bool, applied: bool
111    ) -> None:
112        """Called when in-app-purchase processing completes.
113
114        Each call to on_purchase_process_begin will be followed up by a
115        call to this method. If the purchase was found to be valid and
116        was applied to the account, applied will be True. In the case of
117        redundant or invalid purchases or communication failures it will
118        be False.
119        """
120        # pylint: disable=cyclic-import
121        import babase
122
123        # Ignore this; we want to announce newly applied stuff even if
124        # it was from a different launch or client or whatever.
125        del user_initiated
126
127        # If the purchase wasn't applied, do nothing. This likely means it
128        # was redundant or something else harmless.
129        if not applied:
130            return
131
132        # By default just announce the item id we got. Real app-modes
133        # probably want to do something more specific based on item-id.
134        babase.screenmessage(
135            babase.Lstr(
136                translate=('serverResponses', 'You got a ${ITEM}!'),
137                subs=[('${ITEM}', item_id)],
138            ),
139            color=(0, 1, 0),
140        )
141        if babase.asset_loads_allowed():
142            babase.getsimplesound('cashRegister').play()

A high level mode for the app.

@classmethod
def get_app_experience(cls) -> bacommon.app.AppExperience:
17    @classmethod
18    def get_app_experience(cls) -> AppExperience:
19        """Return the overall experience provided by this mode."""
20        raise NotImplementedError('AppMode subclasses must override this.')

Return the overall experience provided by this mode.

@classmethod
def can_handle_intent(cls, intent: AppIntent) -> bool:
22    @classmethod
23    def can_handle_intent(cls, intent: AppIntent) -> bool:
24        """Return whether this mode can handle the provided intent.
25
26        For this to return True, the AppMode must claim to support the
27        provided intent (via its _can_handle_intent() method) AND the
28        AppExperience associated with the AppMode must be supported by
29        the current app and runtime environment.
30        """
31        # TODO: check AppExperience against current environment.
32        return cls._can_handle_intent(intent)

Return whether this mode can handle the provided intent.

For this to return True, the AppMode must claim to support the provided intent (via its _can_handle_intent() method) AND the AppExperience associated with the AppMode must be supported by the current app and runtime environment.

def handle_intent(self, intent: AppIntent) -> None:
45    def handle_intent(self, intent: AppIntent) -> None:
46        """Handle an intent."""
47        raise NotImplementedError('AppMode subclasses must override this.')

Handle an intent.

def on_activate(self) -> None:
49    def on_activate(self) -> None:
50        """Called when the mode is becoming the active one fro the app."""

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

def on_deactivate(self) -> None:
52    def on_deactivate(self) -> None:
53        """Called when the mode stops being the active one for the app.
54
55        On platforms where the app is explicitly exited (such as desktop
56        PC) this will also be called at app shutdown.
57
58        To best cover both mobile and desktop style platforms, actions
59        such as saving state should generally happen in response to both
60        on_deactivate() and on_app_active_changed() (when active is
61        False).
62        """

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

def on_app_active_changed(self) -> None:
64    def on_app_active_changed(self) -> None:
65        """Called when app active state changes while in this app-mode.
66
67        This corresponds to :attr:`babase.App.active`. App-active state
68        becomes false when the app is hidden, minimized, backgrounded,
69        etc. The app-mode may want to take action such as pausing a
70        running game or saving state when this occurs.
71
72        On platforms such as mobile where apps get suspended and later
73        silently terminated by the OS, this is likely to be the last
74        reliable place to save state/etc.
75
76        To best cover both mobile and desktop style platforms, actions
77        such as saving state should generally happen in response to both
78        on_deactivate() and on_app_active_changed() (when active is
79        False).
80        """

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

This corresponds to babase.App.active. 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).

def on_purchase_process_begin(self, item_id: str, user_initiated: bool) -> None:
 82    def on_purchase_process_begin(
 83        self, item_id: str, user_initiated: bool
 84    ) -> None:
 85        """Called when in-app-purchase processing is beginning.
 86
 87        This call happens after a purchase has been completed locally
 88        but before its receipt/info is sent to the master-server to
 89        apply to the account.
 90        """
 91        # pylint: disable=cyclic-import
 92        import babase
 93
 94        del item_id  # Unused.
 95
 96        # Show nothing for stuff not directly kicked off by the user.
 97        if not user_initiated:
 98            return
 99
100        babase.screenmessage(
101            babase.Lstr(resource='updatingAccountText'),
102            color=(0, 1, 0),
103        )
104        # Ick; we can be called early in the bootstrapping process
105        # before we're allowed to load assets. Guard against that.
106        if babase.asset_loads_allowed():
107            babase.getsimplesound('click01').play()

Called when in-app-purchase processing is beginning.

This call happens after a purchase has been completed locally but before its receipt/info is sent to the master-server to apply to the account.

def on_purchase_process_end(self, item_id: str, user_initiated: bool, applied: bool) -> None:
109    def on_purchase_process_end(
110        self, item_id: str, user_initiated: bool, applied: bool
111    ) -> None:
112        """Called when in-app-purchase processing completes.
113
114        Each call to on_purchase_process_begin will be followed up by a
115        call to this method. If the purchase was found to be valid and
116        was applied to the account, applied will be True. In the case of
117        redundant or invalid purchases or communication failures it will
118        be False.
119        """
120        # pylint: disable=cyclic-import
121        import babase
122
123        # Ignore this; we want to announce newly applied stuff even if
124        # it was from a different launch or client or whatever.
125        del user_initiated
126
127        # If the purchase wasn't applied, do nothing. This likely means it
128        # was redundant or something else harmless.
129        if not applied:
130            return
131
132        # By default just announce the item id we got. Real app-modes
133        # probably want to do something more specific based on item-id.
134        babase.screenmessage(
135            babase.Lstr(
136                translate=('serverResponses', 'You got a ${ITEM}!'),
137                subs=[('${ITEM}', item_id)],
138            ),
139            color=(0, 1, 0),
140        )
141        if babase.asset_loads_allowed():
142            babase.getsimplesound('cashRegister').play()

Called when in-app-purchase processing completes.

Each call to on_purchase_process_begin will be followed up by a call to this method. If the purchase was found to be valid and was applied to the account, applied will be True. In the case of redundant or invalid purchases or communication failures it will be False.

AppTime = AppTime
def apptime() -> AppTime:
540def apptime() -> babase.AppTime:
541    """Return the current app-time in seconds.
542
543    App-time is a monotonic time value; it starts at 0.0 when the app
544    launches and will never jump by large amounts or go backwards, even if
545    the system time changes. Its progression will pause when the app is in
546    a suspended state.
547
548    Note that the AppTime returned here is simply float; it just has a
549    unique type in the type-checker's eyes to help prevent it from being
550    accidentally used with time functionality expecting other time types.
551    """
552    import babase  # pylint: disable=cyclic-import
553
554    return babase.AppTime(0.0)

Return the current app-time in seconds.

App-time is a monotonic time value; it starts at 0.0 when the app launches and will never jump by large amounts or go backwards, even if the system time changes. Its progression will pause when the app is in a suspended state.

Note that the AppTime returned here is simply float; it just has a unique type in the type-checker's eyes to help prevent it from being accidentally used with time functionality expecting other time types.

def apptimer(time: float, call: Callable[[], Any]) -> None:
557def apptimer(time: float, call: Callable[[], Any]) -> None:
558    """Schedule a callable object to run based on app-time.
559
560    This function creates a one-off timer which cannot be canceled or
561    modified once created. If you require the ability to do so, or need
562    a repeating timer, use the babase.AppTimer class instead.
563
564    Args:
565        time: Length of time in seconds that the timer will wait before
566            firing.
567
568        call: A callable Python object. Note that the timer will retain a
569            strong reference to the callable for as long as the timer
570            exists, so you may want to look into concepts such as
571            babase.WeakCall if that is not desired.
572
573    Example: Print some stuff through time:
574      >>> babase.screenmessage('hello from now!')
575      >>> babase.apptimer(1.0, babase.Call(babase.screenmessage,
576      ...                 'hello from the future!'))
577      >>> babase.apptimer(2.0, babase.Call(babase.screenmessage,
578      ...                 'hello from the future 2!'))
579    """
580    return None

Schedule a callable object to run based on app-time.

This function creates a one-off timer which cannot be canceled or modified once created. If you require the ability to do so, or need a repeating timer, use the babase.AppTimer class instead.

Args: time: Length of time in seconds that the timer will wait before firing.

call: A callable Python object. Note that the timer will retain a
    strong reference to the callable for as long as the timer
    exists, so you may want to look into concepts such as
    babase.WeakCall if that is not desired.

Example: Print some stuff through time:

>>> babase.screenmessage('hello from now!')
>>> babase.apptimer(1.0, babase.Call(babase.screenmessage,
...                 'hello from the future!'))
>>> babase.apptimer(2.0, babase.Call(babase.screenmessage,
...                 'hello from the future 2!'))
class AppTimer:
55class AppTimer:
56    """Timers are used to run code at later points in time.
57
58    This class encapsulates a timer based on app-time.
59    The underlying timer will be destroyed when this object is no longer
60    referenced. If you do not want to worry about keeping a reference to
61    your timer around, use the babase.apptimer() function instead to get a
62    one-off timer.
63
64    ##### Arguments
65    ###### time
66    > Length of time in seconds that the timer will wait before firing.
67
68    ###### call
69    > A callable Python object. Remember that the timer will retain a
70    strong reference to the callable for as long as it exists, so you
71    may want to look into concepts such as babase.WeakCall if that is not
72    desired.
73
74    ###### repeat
75    > If True, the timer will fire repeatedly, with each successive
76    firing having the same delay as the first.
77
78    ##### Example
79
80    Use a Timer object to print repeatedly for a few seconds:
81    ... def say_it():
82    ...     babase.screenmessage('BADGER!')
83    ... def stop_saying_it():
84    ...     global g_timer
85    ...     g_timer = None
86    ...     babase.screenmessage('MUSHROOM MUSHROOM!')
87    ... # Create our timer; it will run as long as we have the self.t ref.
88    ... g_timer = babase.AppTimer(0.3, say_it, repeat=True)
89    ... # Now fire off a one-shot timer to kill it.
90    ... babase.apptimer(3.89, stop_saying_it)
91    """
92
93    def __init__(
94        self, time: float, call: Callable[[], Any], repeat: bool = False
95    ) -> None:
96        pass

Timers are used to run code at later points in time.

This class encapsulates a timer based on app-time. The underlying timer will be destroyed when this object is no longer referenced. If you do not want to worry about keeping a reference to your timer around, use the babase.apptimer() function instead to get a one-off timer.

Arguments
time

Length of time in seconds that the timer will wait before firing.

call

A callable Python object. Remember that the timer will retain a strong reference to the callable for as long as it exists, so you may want to look into concepts such as babase.WeakCall if that is not desired.

repeat

If True, the timer will fire repeatedly, with each successive firing having the same delay as the first.

Example

Use a Timer object to print repeatedly for a few seconds: ... def say_it(): ... babase.screenmessage('BADGER!') ... def stop_saying_it(): ... global g_timer ... g_timer = None ... babase.screenmessage('MUSHROOM MUSHROOM!') ... # Create our timer; it will run as long as we have the self.t ref. ... g_timer = babase.AppTimer(0.3, say_it, repeat=True) ... # Now fire off a one-shot timer to kill it. ... babase.apptimer(3.89, stop_saying_it)

AppTimer(time: float, call: Callable[[], Any], repeat: bool = False)
93    def __init__(
94        self, time: float, call: Callable[[], Any], repeat: bool = False
95    ) -> None:
96        pass
class AssetPackage(bascenev1.DependencyComponent):
292class AssetPackage(DependencyComponent):
293    """bascenev1.DependencyComponent representing a package of assets."""
294
295    def __init__(self) -> None:
296        super().__init__()
297
298        # This is used internally by the get_package_xxx calls.
299        self.context = babase.ContextRef()
300
301        entry = self._dep_entry()
302        assert entry is not None
303        assert isinstance(entry.config, str)
304        self.package_id = entry.config
305        print(f'LOADING ASSET PACKAGE {self.package_id}')
306
307    @override
308    @classmethod
309    def dep_is_present(cls, config: Any = None) -> bool:
310        assert isinstance(config, str)
311
312        # Temp: hard-coding for a single asset-package at the moment.
313        if config == 'stdassets@1':
314            return True
315        return False
316
317    def gettexture(self, name: str) -> bascenev1.Texture:
318        """Load a named bascenev1.Texture from the AssetPackage.
319
320        Behavior is similar to bascenev1.gettexture()
321        """
322        return _bascenev1.get_package_texture(self, name)
323
324    def getmesh(self, name: str) -> bascenev1.Mesh:
325        """Load a named bascenev1.Mesh from the AssetPackage.
326
327        Behavior is similar to bascenev1.getmesh()
328        """
329        return _bascenev1.get_package_mesh(self, name)
330
331    def getcollisionmesh(self, name: str) -> bascenev1.CollisionMesh:
332        """Load a named bascenev1.CollisionMesh from the AssetPackage.
333
334        Behavior is similar to bascenev1.getcollisionmesh()
335        """
336        return _bascenev1.get_package_collision_mesh(self, name)
337
338    def getsound(self, name: str) -> bascenev1.Sound:
339        """Load a named bascenev1.Sound from the AssetPackage.
340
341        Behavior is similar to bascenev1.getsound()
342        """
343        return _bascenev1.get_package_sound(self, name)
344
345    def getdata(self, name: str) -> bascenev1.Data:
346        """Load a named bascenev1.Data from the AssetPackage.
347
348        Behavior is similar to bascenev1.getdata()
349        """
350        return _bascenev1.get_package_data(self, name)

bascenev1.DependencyComponent representing a package of assets.

AssetPackage()
295    def __init__(self) -> None:
296        super().__init__()
297
298        # This is used internally by the get_package_xxx calls.
299        self.context = babase.ContextRef()
300
301        entry = self._dep_entry()
302        assert entry is not None
303        assert isinstance(entry.config, str)
304        self.package_id = entry.config
305        print(f'LOADING ASSET PACKAGE {self.package_id}')

Instantiate a DependencyComponent.

context
package_id
@override
@classmethod
def dep_is_present(cls, config: Any = None) -> bool:
307    @override
308    @classmethod
309    def dep_is_present(cls, config: Any = None) -> bool:
310        assert isinstance(config, str)
311
312        # Temp: hard-coding for a single asset-package at the moment.
313        if config == 'stdassets@1':
314            return True
315        return False

Return whether this component/config is present on this device.

def gettexture(self, name: str) -> _bascenev1.Texture:
317    def gettexture(self, name: str) -> bascenev1.Texture:
318        """Load a named bascenev1.Texture from the AssetPackage.
319
320        Behavior is similar to bascenev1.gettexture()
321        """
322        return _bascenev1.get_package_texture(self, name)

Load a named bascenev1.Texture from the AssetPackage.

Behavior is similar to bascenev1.gettexture()

def getmesh(self, name: str) -> _bascenev1.Mesh:
324    def getmesh(self, name: str) -> bascenev1.Mesh:
325        """Load a named bascenev1.Mesh from the AssetPackage.
326
327        Behavior is similar to bascenev1.getmesh()
328        """
329        return _bascenev1.get_package_mesh(self, name)

Load a named bascenev1.Mesh from the AssetPackage.

Behavior is similar to bascenev1.getmesh()

def getcollisionmesh(self, name: str) -> _bascenev1.CollisionMesh:
331    def getcollisionmesh(self, name: str) -> bascenev1.CollisionMesh:
332        """Load a named bascenev1.CollisionMesh from the AssetPackage.
333
334        Behavior is similar to bascenev1.getcollisionmesh()
335        """
336        return _bascenev1.get_package_collision_mesh(self, name)

Load a named bascenev1.CollisionMesh from the AssetPackage.

Behavior is similar to bascenev1.getcollisionmesh()

def getsound(self, name: str) -> _bascenev1.Sound:
338    def getsound(self, name: str) -> bascenev1.Sound:
339        """Load a named bascenev1.Sound from the AssetPackage.
340
341        Behavior is similar to bascenev1.getsound()
342        """
343        return _bascenev1.get_package_sound(self, name)

Load a named bascenev1.Sound from the AssetPackage.

Behavior is similar to bascenev1.getsound()

def getdata(self, name: str) -> _bascenev1.Data:
345    def getdata(self, name: str) -> bascenev1.Data:
346        """Load a named bascenev1.Data from the AssetPackage.
347
348        Behavior is similar to bascenev1.getdata()
349        """
350        return _bascenev1.get_package_data(self, name)

Load a named bascenev1.Data from the AssetPackage.

Behavior is similar to bascenev1.getdata()

def basetime() -> BaseTime:
941def basetime() -> bascenev1.BaseTime:
942    """Return the base-time in seconds for the current scene-v1 context.
943
944    Base-time is a time value that progresses at a constant rate for a scene,
945    even when the scene is sped up, slowed down, or paused. It may, however,
946    speed up or slow down due to replay speed adjustments or may slow down
947    if the cpu is overloaded.
948    Note that the value returned here is simply a float; it just has a
949    unique type in the type-checker's eyes to help prevent it from being
950    accidentally used with time functionality expecting other time types.
951    """
952    import bascenev1  # pylint: disable=cyclic-import
953
954    return bascenev1.BaseTime(0.0)

Return the base-time in seconds for the current scene-v1 context.

Base-time is a time value that progresses at a constant rate for a scene, even when the scene is sped up, slowed down, or paused. It may, however, speed up or slow down due to replay speed adjustments or may slow down if the cpu is overloaded. Note that the value returned here is simply a float; it just has a unique type in the type-checker's eyes to help prevent it from being accidentally used with time functionality expecting other time types.

BaseTime = BaseTime
def basetimer(time: float, call: Callable[[], Any], repeat: bool = False) -> None:
959def basetimer(
960    time: float, call: Callable[[], Any], repeat: bool = False
961) -> None:
962    """Schedule a call to run at a later point in scene base-time.
963    Base-time is a value that progresses at a constant rate for a scene,
964     even when the scene is sped up, slowed down, or paused. It may,
965     however, speed up or slow down due to replay speed adjustments or may
966     slow down if the cpu is overloaded.
967
968    This function adds a timer to the current scene context.
969    This timer cannot be canceled or modified once created. If you
970     require the ability to do so, use the bascenev1.BaseTimer class
971     instead.
972
973    ##### Arguments
974    ###### time (float)
975    > Length of time in seconds that the timer will wait before firing.
976
977    ###### call (Callable[[], Any])
978    > A callable Python object. Remember that the timer will retain a
979    strong reference to the callable for the duration of the timer, so you
980    may want to look into concepts such as babase.WeakCall if that is not
981    desired.
982
983    ###### repeat (bool)
984    > If True, the timer will fire repeatedly, with each successive
985    firing having the same delay as the first.
986
987    ##### Examples
988    Print some stuff through time:
989    >>> import bascenev1 as bs
990    >>> bs.screenmessage('hello from now!')
991    >>> bs.basetimer(1.0, bs.Call(bs.screenmessage, 'hello from the future!'))
992    >>> bs.basetimer(2.0, bs.Call(bs.screenmessage,
993    ...                       'hello from the future 2!'))
994    """
995    return None

Schedule a call to run at a later point in scene base-time. Base-time is a value that progresses at a constant rate for a scene, even when the scene is sped up, slowed down, or paused. It may, however, speed up or slow down due to replay speed adjustments or may slow down if the cpu is overloaded.

This function adds a timer to the current scene context. This timer cannot be canceled or modified once created. If you require the ability to do so, use the bascenev1.BaseTimer class instead.

Arguments
time (float)

Length of time in seconds that the timer will wait before firing.

call (Callable[[], Any])

A callable Python object. Remember that the timer will retain a strong reference to the callable for the duration of the timer, so you may want to look into concepts such as babase.WeakCall if that is not desired.

repeat (bool)

If True, the timer will fire repeatedly, with each successive firing having the same delay as the first.

Examples

Print some stuff through time:

>>> import bascenev1 as bs
>>> bs.screenmessage('hello from now!')
>>> bs.basetimer(1.0, bs.Call(bs.screenmessage, 'hello from the future!'))
>>> bs.basetimer(2.0, bs.Call(bs.screenmessage,
...                       'hello from the future 2!'))
class BaseTimer:
 82class BaseTimer:
 83    """Timers are used to run code at later points in time.
 84
 85    This class encapsulates a base-time timer in the current scene
 86    context.
 87    The underlying timer will be destroyed when either this object is
 88    no longer referenced or when its Context (Activity, etc.) dies. If you
 89    do not want to worry about keeping a reference to your timer around,
 90    you should use the bascenev1.basetimer() function instead.
 91
 92    ###### time (float)
 93    > Length of time in seconds that the timer will wait
 94    before firing.
 95
 96    ###### call (Callable[[], Any])
 97    > A callable Python object. Remember that the timer will retain a
 98    strong reference to the callable for as long as it exists, so you
 99    may want to look into concepts such as babase.WeakCall if that is not
100    desired.
101
102    ###### repeat (bool)
103    > If True, the timer will fire repeatedly, with each successive
104    firing having the same delay as the first.
105
106    ##### Example
107
108    Use a BaseTimer object to print repeatedly for a few seconds:
109    >>> import bascenev1 as bs
110    ... def say_it():
111    ...     bs.screenmessage('BADGER!')
112    ... def stop_saying_it():
113    ...     global g_timer
114    ...     g_timer = None
115    ...     bs.screenmessage('MUSHROOM MUSHROOM!')
116    ... # Create our timer; it will run as long as we have the self.t ref.
117    ... g_timer = bs.BaseTimer(0.3, say_it, repeat=True)
118    ... # Now fire off a one-shot timer to kill it.
119    ... bs.basetimer(3.89, stop_saying_it)
120    """
121
122    def __init__(
123        self, time: float, call: Callable[[], Any], repeat: bool = False
124    ) -> None:
125        pass

Timers are used to run code at later points in time.

This class encapsulates a base-time timer in the current scene context. The underlying timer will be destroyed when either this object is no longer referenced or when its Context (Activity, etc.) dies. If you do not want to worry about keeping a reference to your timer around, you should use the bascenev1.basetimer() function instead.

time (float)

Length of time in seconds that the timer will wait before firing.

call (Callable[[], Any])

A callable Python object. Remember that the timer will retain a strong reference to the callable for as long as it exists, so you may want to look into concepts such as babase.WeakCall if that is not desired.

repeat (bool)

If True, the timer will fire repeatedly, with each successive firing having the same delay as the first.

Example

Use a BaseTimer object to print repeatedly for a few seconds:

>>> import bascenev1 as bs
... def say_it():
...     bs.screenmessage('BADGER!')
... def stop_saying_it():
...     global g_timer
...     g_timer = None
...     bs.screenmessage('MUSHROOM MUSHROOM!')
... # Create our timer; it will run as long as we have the self.t ref.
... g_timer = bs.BaseTimer(0.3, say_it, repeat=True)
... # Now fire off a one-shot timer to kill it.
... bs.basetimer(3.89, stop_saying_it)
BaseTimer(time: float, call: Callable[[], Any], repeat: bool = False)
122    def __init__(
123        self, time: float, call: Callable[[], Any], repeat: bool = False
124    ) -> None:
125        pass
@dataclass
class BoolSetting(bascenev1.Setting):
23@dataclass
24class BoolSetting(Setting):
25    """A boolean game setting."""
26
27    default: bool

A boolean game setting.

BoolSetting(name: str, default: bool)
default: bool
Call = <class 'babase._general._Call'>
def cameraflash(duration: float = 999.0) -> None:
230def cameraflash(duration: float = 999.0) -> None:
231    """Create a strobing camera flash effect.
232
233    (as seen when a team wins a game)
234    Duration is in seconds.
235    """
236    # pylint: disable=too-many-locals
237    from bascenev1._nodeactor import NodeActor
238
239    x_spread = 10
240    y_spread = 5
241    positions = [
242        [-x_spread, -y_spread],
243        [0, -y_spread],
244        [0, y_spread],
245        [x_spread, -y_spread],
246        [x_spread, y_spread],
247        [-x_spread, y_spread],
248    ]
249    times = [0, 2700, 1000, 1800, 500, 1400]
250
251    # Store this on the current activity so we only have one at a time.
252    # FIXME: Need a type safe way to do this.
253    activity = _bascenev1.getactivity()
254    activity.camera_flash_data = []  # type: ignore
255    for i in range(6):
256        light = NodeActor(
257            _bascenev1.newnode(
258                'light',
259                attrs={
260                    'position': (positions[i][0], 0, positions[i][1]),
261                    'radius': 1.0,
262                    'lights_volumes': False,
263                    'height_attenuated': False,
264                    'color': (0.2, 0.2, 0.8),
265                },
266            )
267        )
268        sval = 1.87
269        iscale = 1.3
270        tcombine = _bascenev1.newnode(
271            'combine',
272            owner=light.node,
273            attrs={
274                'size': 3,
275                'input0': positions[i][0],
276                'input1': 0,
277                'input2': positions[i][1],
278            },
279        )
280        assert light.node
281        tcombine.connectattr('output', light.node, 'position')
282        xval = positions[i][0]
283        yval = positions[i][1]
284        spd = 0.5 + random.random()
285        spd2 = 0.5 + random.random()
286        animate(
287            tcombine,
288            'input0',
289            {
290                0.0: xval + 0,
291                0.069 * spd: xval + 10.0,
292                0.143 * spd: xval - 10.0,
293                0.201 * spd: xval + 0,
294            },
295            loop=True,
296        )
297        animate(
298            tcombine,
299            'input2',
300            {
301                0.0: yval + 0,
302                0.15 * spd2: yval + 10.0,
303                0.287 * spd2: yval - 10.0,
304                0.398 * spd2: yval + 0,
305            },
306            loop=True,
307        )
308        animate(
309            light.node,
310            'intensity',
311            {
312                0.0: 0,
313                0.02 * sval: 0,
314                0.05 * sval: 0.8 * iscale,
315                0.08 * sval: 0,
316                0.1 * sval: 0,
317            },
318            loop=True,
319            offset=times[i],
320        )
321        _bascenev1.timer(
322            (times[i] + random.randint(1, int(duration)) * 40 * sval) / 1000.0,
323            light.node.delete,
324        )
325        activity.camera_flash_data.append(light)  # type: ignore

Create a strobing camera flash effect.

(as seen when a team wins a game) Duration is in seconds.

def camerashake(intensity: float = 1.0) -> None:
1023def camerashake(intensity: float = 1.0) -> None:
1024    """Shake the camera.
1025
1026    Note that some cameras and/or platforms (such as VR) may not display
1027    camera-shake, so do not rely on this always being visible to the
1028    player as a gameplay cue.
1029    """
1030    return None

Shake the camera.

Note that some cameras and/or platforms (such as VR) may not display camera-shake, so do not rely on this always being visible to the player as a gameplay cue.

class Campaign:
22class Campaign:
23    """Represents a unique set or series of :class:`bascenev1.Level`s."""
24
25    def __init__(
26        self,
27        name: str,
28        sequential: bool = True,
29        levels: list[bascenev1.Level] | None = None,
30    ):
31        self._name = name
32        self._sequential = sequential
33        self._levels: list[bascenev1.Level] = []
34        if levels is not None:
35            for level in levels:
36                self.addlevel(level)
37
38    @property
39    def name(self) -> str:
40        """The name of the Campaign."""
41        return self._name
42
43    @property
44    def sequential(self) -> bool:
45        """Whether this Campaign's levels must be played in sequence."""
46        return self._sequential
47
48    def addlevel(
49        self, level: bascenev1.Level, index: int | None = None
50    ) -> None:
51        """Adds a baclassic.Level to the Campaign."""
52        if level.campaign is not None:
53            raise RuntimeError('Level already belongs to a campaign.')
54        level.set_campaign(self, len(self._levels))
55        if index is None:
56            self._levels.append(level)
57        else:
58            self._levels.insert(index, level)
59
60    @property
61    def levels(self) -> list[bascenev1.Level]:
62        """The list of baclassic.Level-s in the Campaign."""
63        return self._levels
64
65    def getlevel(self, name: str) -> bascenev1.Level:
66        """Return a contained baclassic.Level by name."""
67
68        for level in self._levels:
69            if level.name == name:
70                return level
71        raise babase.NotFoundError(
72            "Level '" + name + "' not found in campaign '" + self.name + "'"
73        )
74
75    def reset(self) -> None:
76        """Reset state for the Campaign."""
77        babase.app.config.setdefault('Campaigns', {})[self._name] = {}
78
79    # FIXME should these give/take baclassic.Level instances instead
80    #  of level names?..
81    def set_selected_level(self, levelname: str) -> None:
82        """Set the Level currently selected in the UI (by name)."""
83        self.configdict['Selection'] = levelname
84        babase.app.config.commit()
85
86    def get_selected_level(self) -> str:
87        """Return the name of the Level currently selected in the UI."""
88        val = self.configdict.get('Selection', self._levels[0].name)
89        assert isinstance(val, str)
90        return val
91
92    @property
93    def configdict(self) -> dict[str, Any]:
94        """Return the live config dict for this campaign."""
95        val: dict[str, Any] = babase.app.config.setdefault(
96            'Campaigns', {}
97        ).setdefault(self._name, {})
98        assert isinstance(val, dict)
99        return val

Represents a unique set or series of bascenev1.Levels.

Campaign( name: str, sequential: bool = True, levels: list[Level] | None = None)
25    def __init__(
26        self,
27        name: str,
28        sequential: bool = True,
29        levels: list[bascenev1.Level] | None = None,
30    ):
31        self._name = name
32        self._sequential = sequential
33        self._levels: list[bascenev1.Level] = []
34        if levels is not None:
35            for level in levels:
36                self.addlevel(level)
name: str
38    @property
39    def name(self) -> str:
40        """The name of the Campaign."""
41        return self._name

The name of the Campaign.

sequential: bool
43    @property
44    def sequential(self) -> bool:
45        """Whether this Campaign's levels must be played in sequence."""
46        return self._sequential

Whether this Campaign's levels must be played in sequence.

def addlevel(self, level: Level, index: int | None = None) -> None:
48    def addlevel(
49        self, level: bascenev1.Level, index: int | None = None
50    ) -> None:
51        """Adds a baclassic.Level to the Campaign."""
52        if level.campaign is not None:
53            raise RuntimeError('Level already belongs to a campaign.')
54        level.set_campaign(self, len(self._levels))
55        if index is None:
56            self._levels.append(level)
57        else:
58            self._levels.insert(index, level)

Adds a baclassic.Level to the Campaign.

levels: list[Level]
60    @property
61    def levels(self) -> list[bascenev1.Level]:
62        """The list of baclassic.Level-s in the Campaign."""
63        return self._levels

The list of baclassic.Level-s in the Campaign.

def getlevel(self, name: str) -> Level:
65    def getlevel(self, name: str) -> bascenev1.Level:
66        """Return a contained baclassic.Level by name."""
67
68        for level in self._levels:
69            if level.name == name:
70                return level
71        raise babase.NotFoundError(
72            "Level '" + name + "' not found in campaign '" + self.name + "'"
73        )

Return a contained baclassic.Level by name.

def reset(self) -> None:
75    def reset(self) -> None:
76        """Reset state for the Campaign."""
77        babase.app.config.setdefault('Campaigns', {})[self._name] = {}

Reset state for the Campaign.

def set_selected_level(self, levelname: str) -> None:
81    def set_selected_level(self, levelname: str) -> None:
82        """Set the Level currently selected in the UI (by name)."""
83        self.configdict['Selection'] = levelname
84        babase.app.config.commit()

Set the Level currently selected in the UI (by name).

def get_selected_level(self) -> str:
86    def get_selected_level(self) -> str:
87        """Return the name of the Level currently selected in the UI."""
88        val = self.configdict.get('Selection', self._levels[0].name)
89        assert isinstance(val, str)
90        return val

Return the name of the Level currently selected in the UI.

configdict: dict[str, typing.Any]
92    @property
93    def configdict(self) -> dict[str, Any]:
94        """Return the live config dict for this campaign."""
95        val: dict[str, Any] = babase.app.config.setdefault(
96            'Campaigns', {}
97        ).setdefault(self._name, {})
98        assert isinstance(val, dict)
99        return val

Return the live config dict for this campaign.

@dataclass
class CelebrateMessage:
190@dataclass
191class CelebrateMessage:
192    """Tells an object to celebrate."""
193
194    duration: float = 10.0
195    """Amount of time to celebrate in seconds."""

Tells an object to celebrate.

CelebrateMessage(duration: float = 10.0)
duration: float = 10.0

Amount of time to celebrate in seconds.

@dataclass
class ChoiceSetting(bascenev1.Setting):
50@dataclass
51class ChoiceSetting(Setting):
52    """A setting with multiple choices."""
53
54    choices: list[tuple[str, Any]]

A setting with multiple choices.

ChoiceSetting(name: str, default: Any, choices: list[tuple[str, typing.Any]])
choices: list[tuple[str, typing.Any]]
class Chooser:
181class Chooser:
182    """A character/team selector for a bascenev1.Player."""
183
184    def __del__(self) -> None:
185        # Just kill off our base node; the rest should go down with it.
186        if self._text_node:
187            self._text_node.delete()
188
189    def __init__(
190        self,
191        vpos: float,
192        sessionplayer: bascenev1.SessionPlayer,
193        lobby: 'Lobby',
194    ) -> None:
195        self._deek_sound = _bascenev1.getsound('deek')
196        self._click_sound = _bascenev1.getsound('click01')
197        self._punchsound = _bascenev1.getsound('punch01')
198        self._swish_sound = _bascenev1.getsound('punchSwish')
199        self._errorsound = _bascenev1.getsound('error')
200        self._mask_texture = _bascenev1.gettexture('characterIconMask')
201        self._vpos = vpos
202        self._lobby = weakref.ref(lobby)
203        self._sessionplayer = sessionplayer
204        self._inited = False
205        self._dead = False
206        self._text_node: bascenev1.Node | None = None
207        self._profilename = ''
208        self._profilenames: list[str] = []
209        self._ready: bool = False
210        self._character_names: list[str] = []
211        self._last_change: Sequence[float | int] = (0, 0)
212        self._profiles: dict[str, dict[str, Any]] = {}
213
214        app = babase.app
215        assert app.classic is not None
216
217        # Load available player profiles either from the local config or
218        # from the remote device.
219        self.reload_profiles()
220
221        # Note: this is just our local index out of available teams; *not*
222        # the team-id!
223        self._selected_team_index: int = self.lobby.next_add_team
224
225        # Store a persistent random character index and colors; we'll use this
226        # for the '_random' profile. Let's use their input_device id to seed
227        # it. This will give a persistent character for them between games
228        # and will distribute characters nicely if everyone is random.
229        self._random_color, self._random_highlight = get_player_profile_colors(
230            None
231        )
232
233        # To calc our random character we pick a random one out of our
234        # unlocked list and then locate that character's index in the full
235        # list.
236        char_index_offset: int = app.classic.lobby_random_char_index_offset
237        self._random_character_index = (
238            sessionplayer.inputdevice.id + char_index_offset
239        ) % len(self._character_names)
240
241        # Attempt to set an initial profile based on what was used previously
242        # for this input-device, etc.
243        self._profileindex = self._select_initial_profile()
244        self._profilename = self._profilenames[self._profileindex]
245
246        self._text_node = _bascenev1.newnode(
247            'text',
248            delegate=self,
249            attrs={
250                'position': (-100, self._vpos),
251                'maxwidth': 160,
252                'shadow': 0.5,
253                'vr_depth': -20,
254                'h_align': 'left',
255                'v_align': 'center',
256                'v_attach': 'top',
257            },
258        )
259        animate(self._text_node, 'scale', {0: 0, 0.1: 1.0})
260        self.icon = _bascenev1.newnode(
261            'image',
262            owner=self._text_node,
263            attrs={
264                'position': (-130, self._vpos + 20),
265                'mask_texture': self._mask_texture,
266                'vr_depth': -10,
267                'attach': 'topCenter',
268            },
269        )
270
271        animate_array(self.icon, 'scale', 2, {0: (0, 0), 0.1: (45, 45)})
272
273        # Set our initial name to '<choosing player>' in case anyone asks.
274        self._sessionplayer.setname(
275            babase.Lstr(resource='choosingPlayerText').evaluate(), real=False
276        )
277
278        # Init these to our rando but they should get switched to the
279        # selected profile (if any) right after.
280        self._character_index = self._random_character_index
281        self._color = self._random_color
282        self._highlight = self._random_highlight
283
284        self.update_from_profile()
285        self.update_position()
286        self._inited = True
287
288        self._set_ready(False)
289
290    def _select_initial_profile(self) -> int:
291        app = babase.app
292        assert app.classic is not None
293        profilenames = self._profilenames
294        inputdevice = self._sessionplayer.inputdevice
295
296        # If we've got a set profile name for this device, work backwards
297        # from that to get our index.
298        dprofilename = app.config.get('Default Player Profiles', {}).get(
299            inputdevice.name + ' ' + inputdevice.unique_identifier
300        )
301        if dprofilename is not None and dprofilename in profilenames:
302            # If we got '__account__' and its local and we haven't marked
303            # anyone as the 'account profile' device yet, mark this guy as
304            # it. (prevents the next joiner from getting the account
305            # profile too).
306            if (
307                dprofilename == '__account__'
308                and not inputdevice.is_remote_client
309                and app.classic.lobby_account_profile_device_id is None
310            ):
311                app.classic.lobby_account_profile_device_id = inputdevice.id
312            return profilenames.index(dprofilename)
313
314        # We want to mark the first local input-device in the game
315        # as the 'account profile' device.
316        if (
317            not inputdevice.is_remote_client
318            and not inputdevice.is_controller_app
319        ):
320            if (
321                app.classic.lobby_account_profile_device_id is None
322                and '__account__' in profilenames
323            ):
324                app.classic.lobby_account_profile_device_id = inputdevice.id
325
326        # If this is the designated account-profile-device, try to default
327        # to the account profile.
328        if (
329            inputdevice.id == app.classic.lobby_account_profile_device_id
330            and '__account__' in profilenames
331        ):
332            return profilenames.index('__account__')
333
334        # If this is the controller app, it defaults to using a random
335        # profile (since we can pull the random name from the app).
336        if inputdevice.is_controller_app and '_random' in profilenames:
337            return profilenames.index('_random')
338
339        # If its a client connection, for now just force
340        # the account profile if possible.. (need to provide a
341        # way for clients to specify/remember their default
342        # profile on remote servers that do not already know them).
343        if inputdevice.is_remote_client and '__account__' in profilenames:
344            return profilenames.index('__account__')
345
346        # Cycle through our non-random profiles once; after
347        # that, everyone gets random.
348        while app.classic.lobby_random_profile_index < len(
349            profilenames
350        ) and profilenames[app.classic.lobby_random_profile_index] in (
351            '_random',
352            '__account__',
353            '_edit',
354        ):
355            app.classic.lobby_random_profile_index += 1
356        if app.classic.lobby_random_profile_index < len(profilenames):
357            profileindex: int = app.classic.lobby_random_profile_index
358            app.classic.lobby_random_profile_index += 1
359            return profileindex
360        assert '_random' in profilenames
361        return profilenames.index('_random')
362
363    @property
364    def sessionplayer(self) -> bascenev1.SessionPlayer:
365        """The bascenev1.SessionPlayer associated with this chooser."""
366        return self._sessionplayer
367
368    @property
369    def ready(self) -> bool:
370        """Whether this chooser is checked in as ready."""
371        return self._ready
372
373    def set_vpos(self, vpos: float) -> None:
374        """(internal)"""
375        self._vpos = vpos
376
377    def set_dead(self, val: bool) -> None:
378        """(internal)"""
379        self._dead = val
380
381    @property
382    def sessionteam(self) -> bascenev1.SessionTeam:
383        """Return this chooser's currently selected bascenev1.SessionTeam."""
384        return self.lobby.sessionteams[self._selected_team_index]
385
386    @property
387    def lobby(self) -> bascenev1.Lobby:
388        """The chooser's baclassic.Lobby."""
389        lobby = self._lobby()
390        if lobby is None:
391            raise babase.NotFoundError('Lobby does not exist.')
392        return lobby
393
394    def get_lobby(self) -> bascenev1.Lobby | None:
395        """Return this chooser's lobby if it still exists; otherwise None."""
396        return self._lobby()
397
398    def update_from_profile(self) -> None:
399        """Set character/colors based on the current profile."""
400        assert babase.app.classic is not None
401        self._profilename = self._profilenames[self._profileindex]
402        if self._profilename == '_edit':
403            pass
404        elif self._profilename == '_random':
405            self._character_index = self._random_character_index
406            self._color = self._random_color
407            self._highlight = self._random_highlight
408        else:
409            character = self._profiles[self._profilename]['character']
410
411            # At the moment we're not properly pulling the list
412            # of available characters from clients, so profiles might use a
413            # character not in their list. For now, just go ahead and add
414            # a character name to their list as long as we're aware of it.
415            # This just means they won't always be able to override their
416            # character to others they own, but profile characters
417            # should work (and we validate profiles on the master server
418            # so no exploit opportunities)
419            if (
420                character not in self._character_names
421                and character in babase.app.classic.spaz_appearances
422            ):
423                self._character_names.append(character)
424            self._character_index = self._character_names.index(character)
425            self._color, self._highlight = get_player_profile_colors(
426                self._profilename, profiles=self._profiles
427            )
428        self._update_icon()
429        self._update_text()
430
431    def reload_profiles(self) -> None:
432        """Reload all player profiles."""
433
434        app = babase.app
435        env = app.env
436        assert app.classic is not None
437
438        # Re-construct our profile index and other stuff since the profile
439        # list might have changed.
440        input_device = self._sessionplayer.inputdevice
441        is_remote = input_device.is_remote_client
442        is_test_input = input_device.is_test_input
443
444        # Pull this player's list of unlocked characters.
445        if is_remote:
446            # TODO: Pull this from the remote player.
447            # (but make sure to filter it to the ones we've got).
448            self._character_names = ['Spaz']
449        else:
450            self._character_names = self.lobby.character_names_local_unlocked
451
452        # If we're a local player, pull our local profiles from the config.
453        # Otherwise ask the remote-input-device for its profile list.
454        if is_remote:
455            self._profiles = input_device.get_player_profiles()
456        else:
457            self._profiles = app.config.get('Player Profiles', {})
458
459        # These may have come over the wire from an older
460        # (non-unicode/non-json) version.
461        # Make sure they conform to our standards
462        # (unicode strings, no tuples, etc)
463        self._profiles = app.classic.json_prep(self._profiles)
464
465        # Filter out any characters we're unaware of.
466        for profile in list(self._profiles.items()):
467            if (
468                profile[1].get('character', '')
469                not in app.classic.spaz_appearances
470            ):
471                profile[1]['character'] = 'Spaz'
472
473        # Add in a random one so we're ok even if there's no user profiles.
474        self._profiles['_random'] = {}
475
476        # In kiosk mode we disable account profiles to force random.
477        if env.demo or env.arcade:
478            if '__account__' in self._profiles:
479                del self._profiles['__account__']
480
481        # For local devices, add it an 'edit' option which will pop up
482        # the profile window.
483        if not is_remote and not is_test_input and not (env.demo or env.arcade):
484            self._profiles['_edit'] = {}
485
486        # Build a sorted name list we can iterate through.
487        self._profilenames = list(self._profiles.keys())
488        self._profilenames.sort(key=lambda x: x.lower())
489
490        if self._profilename in self._profilenames:
491            self._profileindex = self._profilenames.index(self._profilename)
492        else:
493            self._profileindex = 0
494            # noinspection PyUnresolvedReferences
495            self._profilename = self._profilenames[self._profileindex]
496
497    def update_position(self) -> None:
498        """Update this chooser's position."""
499
500        assert self._text_node
501        spacing = 350
502        sessionteams = self.lobby.sessionteams
503        offs = (
504            spacing * -0.5 * len(sessionteams)
505            + spacing * self._selected_team_index
506            + 250
507        )
508        if len(sessionteams) > 1:
509            offs -= 35
510        animate_array(
511            self._text_node,
512            'position',
513            2,
514            {0: self._text_node.position, 0.1: (-100 + offs, self._vpos + 23)},
515        )
516        animate_array(
517            self.icon,
518            'position',
519            2,
520            {0: self.icon.position, 0.1: (-130 + offs, self._vpos + 22)},
521        )
522
523    def get_character_name(self) -> str:
524        """Return the selected character name."""
525        return self._character_names[self._character_index]
526
527    def _do_nothing(self) -> None:
528        """Does nothing! (hacky way to disable callbacks)"""
529
530    def _getname(self, full: bool = False) -> str:
531        name_raw = name = self._profilenames[self._profileindex]
532        clamp = False
533        if name == '_random':
534            try:
535                name = self._sessionplayer.inputdevice.get_default_player_name()
536            except Exception:
537                logging.exception('Error getting _random chooser name.')
538                name = 'Invalid'
539            clamp = not full
540        elif name == '__account__':
541            try:
542                name = self._sessionplayer.inputdevice.get_v1_account_name(full)
543            except Exception:
544                logging.exception('Error getting account name for chooser.')
545                name = 'Invalid'
546            clamp = not full
547        elif name == '_edit':
548            # Explicitly flattening this to a str; it's only relevant on
549            # the host so that's ok.
550            name = babase.Lstr(
551                resource='createEditPlayerText',
552                fallback_resource='editProfileWindow.titleNewText',
553            ).evaluate()
554        else:
555            # If we have a regular profile marked as global with an icon,
556            # use it (for full only).
557            if full:
558                try:
559                    if self._profiles[name_raw].get('global', False):
560                        icon = (
561                            self._profiles[name_raw]['icon']
562                            if 'icon' in self._profiles[name_raw]
563                            else babase.charstr(babase.SpecialChar.LOGO)
564                        )
565                        name = icon + name
566                except Exception:
567                    logging.exception('Error applying global icon.')
568            else:
569                # We now clamp non-full versions of names so there's at
570                # least some hope of reading them in-game.
571                clamp = True
572
573        if clamp:
574            if len(name) > 10:
575                name = name[:10] + '...'
576        return name
577
578    def _set_ready(self, ready: bool) -> None:
579        # pylint: disable=cyclic-import
580
581        classic = babase.app.classic
582        assert classic is not None
583
584        profilename = self._profilenames[self._profileindex]
585
586        # Handle '_edit' as a special case.
587        if profilename == '_edit' and ready:
588            with babase.ContextRef.empty():
589
590                classic.profile_browser_window()
591
592                # Give their input-device UI ownership too (prevent
593                # someone else from snatching it in crowded games).
594                babase.set_ui_input_device(self._sessionplayer.inputdevice.id)
595            return
596
597        if not ready:
598            self._sessionplayer.assigninput(
599                babase.InputType.LEFT_PRESS,
600                babase.Call(self.handlemessage, ChangeMessage('team', -1)),
601            )
602            self._sessionplayer.assigninput(
603                babase.InputType.RIGHT_PRESS,
604                babase.Call(self.handlemessage, ChangeMessage('team', 1)),
605            )
606            self._sessionplayer.assigninput(
607                babase.InputType.BOMB_PRESS,
608                babase.Call(self.handlemessage, ChangeMessage('character', 1)),
609            )
610            self._sessionplayer.assigninput(
611                babase.InputType.UP_PRESS,
612                babase.Call(
613                    self.handlemessage, ChangeMessage('profileindex', -1)
614                ),
615            )
616            self._sessionplayer.assigninput(
617                babase.InputType.DOWN_PRESS,
618                babase.Call(
619                    self.handlemessage, ChangeMessage('profileindex', 1)
620                ),
621            )
622            self._sessionplayer.assigninput(
623                (
624                    babase.InputType.JUMP_PRESS,
625                    babase.InputType.PICK_UP_PRESS,
626                    babase.InputType.PUNCH_PRESS,
627                ),
628                babase.Call(self.handlemessage, ChangeMessage('ready', 1)),
629            )
630            self._ready = False
631            self._update_text()
632            self._sessionplayer.setname('untitled', real=False)
633        else:
634            self._sessionplayer.assigninput(
635                (
636                    babase.InputType.LEFT_PRESS,
637                    babase.InputType.RIGHT_PRESS,
638                    babase.InputType.UP_PRESS,
639                    babase.InputType.DOWN_PRESS,
640                    babase.InputType.JUMP_PRESS,
641                    babase.InputType.BOMB_PRESS,
642                    babase.InputType.PICK_UP_PRESS,
643                ),
644                self._do_nothing,
645            )
646            self._sessionplayer.assigninput(
647                (
648                    babase.InputType.JUMP_PRESS,
649                    babase.InputType.BOMB_PRESS,
650                    babase.InputType.PICK_UP_PRESS,
651                    babase.InputType.PUNCH_PRESS,
652                ),
653                babase.Call(self.handlemessage, ChangeMessage('ready', 0)),
654            )
655
656            # Store the last profile picked by this input for reuse.
657            input_device = self._sessionplayer.inputdevice
658            name = input_device.name
659            unique_id = input_device.unique_identifier
660            device_profiles = babase.app.config.setdefault(
661                'Default Player Profiles', {}
662            )
663
664            # Make an exception if we have no custom profiles and are set
665            # to random; in that case we'll want to start picking up custom
666            # profiles if/when one is made so keep our setting cleared.
667            special = ('_random', '_edit', '__account__')
668            have_custom_profiles = any(p not in special for p in self._profiles)
669
670            profilekey = name + ' ' + unique_id
671            if profilename == '_random' and not have_custom_profiles:
672                if profilekey in device_profiles:
673                    del device_profiles[profilekey]
674            else:
675                device_profiles[profilekey] = profilename
676            babase.app.config.commit()
677
678            # Set this player's short and full name.
679            self._sessionplayer.setname(
680                self._getname(), self._getname(full=True), real=True
681            )
682            self._ready = True
683            self._update_text()
684
685            # Inform the session that this player is ready.
686            _bascenev1.getsession().handlemessage(PlayerReadyMessage(self))
687
688    def _handle_ready_msg(self, ready: bool) -> None:
689        force_team_switch = False
690
691        # Team auto-balance kicks us to another team if we try to
692        # join the team with the most players.
693        if not self._ready:
694            if babase.app.config.get('Auto Balance Teams', False):
695                lobby = self.lobby
696                sessionteams = lobby.sessionteams
697                if len(sessionteams) > 1:
698                    # First, calc how many players are on each team
699                    # ..we need to count both active players and
700                    # choosers that have been marked as ready.
701                    team_player_counts = {}
702                    for sessionteam in sessionteams:
703                        team_player_counts[sessionteam.id] = len(
704                            sessionteam.players
705                        )
706                    for chooser in lobby.choosers:
707                        if chooser.ready:
708                            team_player_counts[chooser.sessionteam.id] += 1
709                    largest_team_size = max(team_player_counts.values())
710                    smallest_team_size = min(team_player_counts.values())
711
712                    # Force switch if we're on the biggest sessionteam
713                    # and there's a smaller one available.
714                    if (
715                        largest_team_size != smallest_team_size
716                        and team_player_counts[self.sessionteam.id]
717                        >= largest_team_size
718                    ):
719                        force_team_switch = True
720
721        # Either force switch teams, or actually for realsies do the set-ready.
722        if force_team_switch:
723            self._errorsound.play()
724            self.handlemessage(ChangeMessage('team', 1))
725        else:
726            self._punchsound.play()
727            self._set_ready(ready)
728
729    # TODO: should handle this at the engine layer so this is unnecessary.
730    def _handle_repeat_message_attack(self) -> None:
731        now = babase.apptime()
732        count = self._last_change[1]
733        if now - self._last_change[0] < QUICK_CHANGE_INTERVAL:
734            count += 1
735            if count > MAX_QUICK_CHANGE_COUNT:
736                _bascenev1.disconnect_client(
737                    self._sessionplayer.inputdevice.client_id
738                )
739        elif now - self._last_change[0] > QUICK_CHANGE_RESET_INTERVAL:
740            count = 0
741        self._last_change = (now, count)
742
743    def handlemessage(self, msg: Any) -> Any:
744        """Standard generic message handler."""
745
746        if isinstance(msg, ChangeMessage):
747            self._handle_repeat_message_attack()
748
749            # If we've been removed from the lobby, ignore this stuff.
750            if self._dead:
751                logging.error('chooser got ChangeMessage after dying')
752                return
753
754            if not self._text_node:
755                logging.error('got ChangeMessage after nodes died')
756                return
757
758            if msg.what == 'team':
759                sessionteams = self.lobby.sessionteams
760                if len(sessionteams) > 1:
761                    self._swish_sound.play()
762                self._selected_team_index = (
763                    self._selected_team_index + msg.value
764                ) % len(sessionteams)
765                self._update_text()
766                self.update_position()
767                self._update_icon()
768
769            elif msg.what == 'profileindex':
770                if len(self._profilenames) == 1:
771                    # This should be pretty hard to hit now with
772                    # automatic local accounts.
773                    _bascenev1.getsound('error').play()
774                else:
775                    # Pick the next player profile and assign our name
776                    # and character based on that.
777                    self._deek_sound.play()
778                    self._profileindex = (self._profileindex + msg.value) % len(
779                        self._profilenames
780                    )
781                    self.update_from_profile()
782
783            elif msg.what == 'character':
784                self._click_sound.play()
785                # update our index in our local list of characters
786                self._character_index = (
787                    self._character_index + msg.value
788                ) % len(self._character_names)
789                self._update_text()
790                self._update_icon()
791
792            elif msg.what == 'ready':
793                self._handle_ready_msg(bool(msg.value))
794
795    def _update_text(self) -> None:
796        assert self._text_node is not None
797        if self._ready:
798            # Once we're ready, we've saved the name, so lets ask the system
799            # for it so we get appended numbers and stuff.
800            text = babase.Lstr(value=self._sessionplayer.getname(full=True))
801            text = babase.Lstr(
802                value='${A} (${B})',
803                subs=[
804                    ('${A}', text),
805                    ('${B}', babase.Lstr(resource='readyText')),
806                ],
807            )
808        else:
809            text = babase.Lstr(value=self._getname(full=True))
810
811        can_switch_teams = len(self.lobby.sessionteams) > 1
812
813        # Flash as we're coming in.
814        fin_color = babase.safecolor(self.get_color()) + (1,)
815        if not self._inited:
816            animate_array(
817                self._text_node,
818                'color',
819                4,
820                {0.15: fin_color, 0.25: (2, 2, 2, 1), 0.35: fin_color},
821            )
822        else:
823            # Blend if we're in teams mode; switch instantly otherwise.
824            if can_switch_teams:
825                animate_array(
826                    self._text_node,
827                    'color',
828                    4,
829                    {0: self._text_node.color, 0.1: fin_color},
830                )
831            else:
832                self._text_node.color = fin_color
833
834        self._text_node.text = text
835
836    def get_color(self) -> Sequence[float]:
837        """Return the currently selected color."""
838        val: Sequence[float]
839        if self.lobby.use_team_colors:
840            val = self.lobby.sessionteams[self._selected_team_index].color
841        else:
842            val = self._color
843        if len(val) != 3:
844            print('get_color: ignoring invalid color of len', len(val))
845            val = (0, 1, 0)
846        return val
847
848    def get_highlight(self) -> Sequence[float]:
849        """Return the currently selected highlight."""
850        if self._profilenames[self._profileindex] == '_edit':
851            return 0, 1, 0
852
853        # If we're using team colors we wanna make sure our highlight color
854        # isn't too close to any other team's color.
855        highlight = list(self._highlight)
856        if self.lobby.use_team_colors:
857            for i, sessionteam in enumerate(self.lobby.sessionteams):
858                if i != self._selected_team_index:
859                    # Find the dominant component of this sessionteam's color
860                    # and adjust ours so that the component is
861                    # not super-dominant.
862                    max_val = 0.0
863                    max_index = 0
864                    for j in range(3):
865                        if sessionteam.color[j] > max_val:
866                            max_val = sessionteam.color[j]
867                            max_index = j
868                    that_color_for_us = highlight[max_index]
869                    our_second_biggest = max(
870                        highlight[(max_index + 1) % 3],
871                        highlight[(max_index + 2) % 3],
872                    )
873                    diff = that_color_for_us - our_second_biggest
874                    if diff > 0:
875                        highlight[max_index] -= diff * 0.6
876                        highlight[(max_index + 1) % 3] += diff * 0.3
877                        highlight[(max_index + 2) % 3] += diff * 0.2
878        return highlight
879
880    def getplayer(self) -> bascenev1.SessionPlayer:
881        """Return the player associated with this chooser."""
882        return self._sessionplayer
883
884    def _update_icon(self) -> None:
885        assert babase.app.classic is not None
886        if self._profilenames[self._profileindex] == '_edit':
887            tex = _bascenev1.gettexture('black')
888            tint_tex = _bascenev1.gettexture('black')
889            self.icon.color = (1, 1, 1)
890            self.icon.texture = tex
891            self.icon.tint_texture = tint_tex
892            self.icon.tint_color = (0, 1, 0)
893            return
894
895        try:
896            tex_name = babase.app.classic.spaz_appearances[
897                self._character_names[self._character_index]
898            ].icon_texture
899            tint_tex_name = babase.app.classic.spaz_appearances[
900                self._character_names[self._character_index]
901            ].icon_mask_texture
902        except Exception:
903            logging.exception('Error updating char icon list')
904            tex_name = 'neoSpazIcon'
905            tint_tex_name = 'neoSpazIconColorMask'
906
907        tex = _bascenev1.gettexture(tex_name)
908        tint_tex = _bascenev1.gettexture(tint_tex_name)
909
910        self.icon.color = (1, 1, 1)
911        self.icon.texture = tex
912        self.icon.tint_texture = tint_tex
913        clr = self.get_color()
914        clr2 = self.get_highlight()
915
916        can_switch_teams = len(self.lobby.sessionteams) > 1
917
918        # If we're initing, flash.
919        if not self._inited:
920            animate_array(
921                self.icon,
922                'color',
923                3,
924                {0.15: (1, 1, 1), 0.25: (2, 2, 2), 0.35: (1, 1, 1)},
925            )
926
927        # Blend in teams mode; switch instantly in ffa-mode.
928        if can_switch_teams:
929            animate_array(
930                self.icon, 'tint_color', 3, {0: self.icon.tint_color, 0.1: clr}
931            )
932        else:
933            self.icon.tint_color = clr
934        self.icon.tint2_color = clr2
935
936        # Store the icon info the the player.
937        self._sessionplayer.set_icon_info(tex_name, tint_tex_name, clr, clr2)

A character/team selector for a bascenev1.Player.

Chooser( vpos: float, sessionplayer: _bascenev1.SessionPlayer, lobby: Lobby)
189    def __init__(
190        self,
191        vpos: float,
192        sessionplayer: bascenev1.SessionPlayer,
193        lobby: 'Lobby',
194    ) -> None:
195        self._deek_sound = _bascenev1.getsound('deek')
196        self._click_sound = _bascenev1.getsound('click01')
197        self._punchsound = _bascenev1.getsound('punch01')
198        self._swish_sound = _bascenev1.getsound('punchSwish')
199        self._errorsound = _bascenev1.getsound('error')
200        self._mask_texture = _bascenev1.gettexture('characterIconMask')
201        self._vpos = vpos
202        self._lobby = weakref.ref(lobby)
203        self._sessionplayer = sessionplayer
204        self._inited = False
205        self._dead = False
206        self._text_node: bascenev1.Node | None = None
207        self._profilename = ''
208        self._profilenames: list[str] = []
209        self._ready: bool = False
210        self._character_names: list[str] = []
211        self._last_change: Sequence[float | int] = (0, 0)
212        self._profiles: dict[str, dict[str, Any]] = {}
213
214        app = babase.app
215        assert app.classic is not None
216
217        # Load available player profiles either from the local config or
218        # from the remote device.
219        self.reload_profiles()
220
221        # Note: this is just our local index out of available teams; *not*
222        # the team-id!
223        self._selected_team_index: int = self.lobby.next_add_team
224
225        # Store a persistent random character index and colors; we'll use this
226        # for the '_random' profile. Let's use their input_device id to seed
227        # it. This will give a persistent character for them between games
228        # and will distribute characters nicely if everyone is random.
229        self._random_color, self._random_highlight = get_player_profile_colors(
230            None
231        )
232
233        # To calc our random character we pick a random one out of our
234        # unlocked list and then locate that character's index in the full
235        # list.
236        char_index_offset: int = app.classic.lobby_random_char_index_offset
237        self._random_character_index = (
238            sessionplayer.inputdevice.id + char_index_offset
239        ) % len(self._character_names)
240
241        # Attempt to set an initial profile based on what was used previously
242        # for this input-device, etc.
243        self._profileindex = self._select_initial_profile()
244        self._profilename = self._profilenames[self._profileindex]
245
246        self._text_node = _bascenev1.newnode(
247            'text',
248            delegate=self,
249            attrs={
250                'position': (-100, self._vpos),
251                'maxwidth': 160,
252                'shadow': 0.5,
253                'vr_depth': -20,
254                'h_align': 'left',
255                'v_align': 'center',
256                'v_attach': 'top',
257            },
258        )
259        animate(self._text_node, 'scale', {0: 0, 0.1: 1.0})
260        self.icon = _bascenev1.newnode(
261            'image',
262            owner=self._text_node,
263            attrs={
264                'position': (-130, self._vpos + 20),
265                'mask_texture': self._mask_texture,
266                'vr_depth': -10,
267                'attach': 'topCenter',
268            },
269        )
270
271        animate_array(self.icon, 'scale', 2, {0: (0, 0), 0.1: (45, 45)})
272
273        # Set our initial name to '<choosing player>' in case anyone asks.
274        self._sessionplayer.setname(
275            babase.Lstr(resource='choosingPlayerText').evaluate(), real=False
276        )
277
278        # Init these to our rando but they should get switched to the
279        # selected profile (if any) right after.
280        self._character_index = self._random_character_index
281        self._color = self._random_color
282        self._highlight = self._random_highlight
283
284        self.update_from_profile()
285        self.update_position()
286        self._inited = True
287
288        self._set_ready(False)
icon
sessionplayer: _bascenev1.SessionPlayer
363    @property
364    def sessionplayer(self) -> bascenev1.SessionPlayer:
365        """The bascenev1.SessionPlayer associated with this chooser."""
366        return self._sessionplayer

The bascenev1.SessionPlayer associated with this chooser.

ready: bool
368    @property
369    def ready(self) -> bool:
370        """Whether this chooser is checked in as ready."""
371        return self._ready

Whether this chooser is checked in as ready.

sessionteam: SessionTeam
381    @property
382    def sessionteam(self) -> bascenev1.SessionTeam:
383        """Return this chooser's currently selected bascenev1.SessionTeam."""
384        return self.lobby.sessionteams[self._selected_team_index]

Return this chooser's currently selected bascenev1.SessionTeam.

lobby: Lobby
386    @property
387    def lobby(self) -> bascenev1.Lobby:
388        """The chooser's baclassic.Lobby."""
389        lobby = self._lobby()
390        if lobby is None:
391            raise babase.NotFoundError('Lobby does not exist.')
392        return lobby

The chooser's baclassic.Lobby.

def get_lobby(self) -> Lobby | None:
394    def get_lobby(self) -> bascenev1.Lobby | None:
395        """Return this chooser's lobby if it still exists; otherwise None."""
396        return self._lobby()

Return this chooser's lobby if it still exists; otherwise None.

def update_from_profile(self) -> None:
398    def update_from_profile(self) -> None:
399        """Set character/colors based on the current profile."""
400        assert babase.app.classic is not None
401        self._profilename = self._profilenames[self._profileindex]
402        if self._profilename == '_edit':
403            pass
404        elif self._profilename == '_random':
405            self._character_index = self._random_character_index
406            self._color = self._random_color
407            self._highlight = self._random_highlight
408        else:
409            character = self._profiles[self._profilename]['character']
410
411            # At the moment we're not properly pulling the list
412            # of available characters from clients, so profiles might use a
413            # character not in their list. For now, just go ahead and add
414            # a character name to their list as long as we're aware of it.
415            # This just means they won't always be able to override their
416            # character to others they own, but profile characters
417            # should work (and we validate profiles on the master server
418            # so no exploit opportunities)
419            if (
420                character not in self._character_names
421                and character in babase.app.classic.spaz_appearances
422            ):
423                self._character_names.append(character)
424            self._character_index = self._character_names.index(character)
425            self._color, self._highlight = get_player_profile_colors(
426                self._profilename, profiles=self._profiles
427            )
428        self._update_icon()
429        self._update_text()

Set character/colors based on the current profile.

def reload_profiles(self) -> None:
431    def reload_profiles(self) -> None:
432        """Reload all player profiles."""
433
434        app = babase.app
435        env = app.env
436        assert app.classic is not None
437
438        # Re-construct our profile index and other stuff since the profile
439        # list might have changed.
440        input_device = self._sessionplayer.inputdevice
441        is_remote = input_device.is_remote_client
442        is_test_input = input_device.is_test_input
443
444        # Pull this player's list of unlocked characters.
445        if is_remote:
446            # TODO: Pull this from the remote player.
447            # (but make sure to filter it to the ones we've got).
448            self._character_names = ['Spaz']
449        else:
450            self._character_names = self.lobby.character_names_local_unlocked
451
452        # If we're a local player, pull our local profiles from the config.
453        # Otherwise ask the remote-input-device for its profile list.
454        if is_remote:
455            self._profiles = input_device.get_player_profiles()
456        else:
457            self._profiles = app.config.get('Player Profiles', {})
458
459        # These may have come over the wire from an older
460        # (non-unicode/non-json) version.
461        # Make sure they conform to our standards
462        # (unicode strings, no tuples, etc)
463        self._profiles = app.classic.json_prep(self._profiles)
464
465        # Filter out any characters we're unaware of.
466        for profile in list(self._profiles.items()):
467            if (
468                profile[1].get('character', '')
469                not in app.classic.spaz_appearances
470            ):
471                profile[1]['character'] = 'Spaz'
472
473        # Add in a random one so we're ok even if there's no user profiles.
474        self._profiles['_random'] = {}
475
476        # In kiosk mode we disable account profiles to force random.
477        if env.demo or env.arcade:
478            if '__account__' in self._profiles:
479                del self._profiles['__account__']
480
481        # For local devices, add it an 'edit' option which will pop up
482        # the profile window.
483        if not is_remote and not is_test_input and not (env.demo or env.arcade):
484            self._profiles['_edit'] = {}
485
486        # Build a sorted name list we can iterate through.
487        self._profilenames = list(self._profiles.keys())
488        self._profilenames.sort(key=lambda x: x.lower())
489
490        if self._profilename in self._profilenames:
491            self._profileindex = self._profilenames.index(self._profilename)
492        else:
493            self._profileindex = 0
494            # noinspection PyUnresolvedReferences
495            self._profilename = self._profilenames[self._profileindex]

Reload all player profiles.

def update_position(self) -> None:
497    def update_position(self) -> None:
498        """Update this chooser's position."""
499
500        assert self._text_node
501        spacing = 350
502        sessionteams = self.lobby.sessionteams
503        offs = (
504            spacing * -0.5 * len(sessionteams)
505            + spacing * self._selected_team_index
506            + 250
507        )
508        if len(sessionteams) > 1:
509            offs -= 35
510        animate_array(
511            self._text_node,
512            'position',
513            2,
514            {0: self._text_node.position, 0.1: (-100 + offs, self._vpos + 23)},
515        )
516        animate_array(
517            self.icon,
518            'position',
519            2,
520            {0: self.icon.position, 0.1: (-130 + offs, self._vpos + 22)},
521        )

Update this chooser's position.

def get_character_name(self) -> str:
523    def get_character_name(self) -> str:
524        """Return the selected character name."""
525        return self._character_names[self._character_index]

Return the selected character name.

def handlemessage(self, msg: Any) -> Any:
743    def handlemessage(self, msg: Any) -> Any:
744        """Standard generic message handler."""
745
746        if isinstance(msg, ChangeMessage):
747            self._handle_repeat_message_attack()
748
749            # If we've been removed from the lobby, ignore this stuff.
750            if self._dead:
751                logging.error('chooser got ChangeMessage after dying')
752                return
753
754            if not self._text_node:
755                logging.error('got ChangeMessage after nodes died')
756                return
757
758            if msg.what == 'team':
759                sessionteams = self.lobby.sessionteams
760                if len(sessionteams) > 1:
761                    self._swish_sound.play()
762                self._selected_team_index = (
763                    self._selected_team_index + msg.value
764                ) % len(sessionteams)
765                self._update_text()
766                self.update_position()
767                self._update_icon()
768
769            elif msg.what == 'profileindex':
770                if len(self._profilenames) == 1:
771                    # This should be pretty hard to hit now with
772                    # automatic local accounts.
773                    _bascenev1.getsound('error').play()
774                else:
775                    # Pick the next player profile and assign our name
776                    # and character based on that.
777                    self._deek_sound.play()
778                    self._profileindex = (self._profileindex + msg.value) % len(
779                        self._profilenames
780                    )
781                    self.update_from_profile()
782
783            elif msg.what == 'character':
784                self._click_sound.play()
785                # update our index in our local list of characters
786                self._character_index = (
787                    self._character_index + msg.value
788                ) % len(self._character_names)
789                self._update_text()
790                self._update_icon()
791
792            elif msg.what == 'ready':
793                self._handle_ready_msg(bool(msg.value))

Standard generic message handler.

def get_color(self) -> Sequence[float]:
836    def get_color(self) -> Sequence[float]:
837        """Return the currently selected color."""
838        val: Sequence[float]
839        if self.lobby.use_team_colors:
840            val = self.lobby.sessionteams[self._selected_team_index].color
841        else:
842            val = self._color
843        if len(val) != 3:
844            print('get_color: ignoring invalid color of len', len(val))
845            val = (0, 1, 0)
846        return val

Return the currently selected color.

def get_highlight(self) -> Sequence[float]:
848    def get_highlight(self) -> Sequence[float]:
849        """Return the currently selected highlight."""
850        if self._profilenames[self._profileindex] == '_edit':
851            return 0, 1, 0
852
853        # If we're using team colors we wanna make sure our highlight color
854        # isn't too close to any other team's color.
855        highlight = list(self._highlight)
856        if self.lobby.use_team_colors:
857            for i, sessionteam in enumerate(self.lobby.sessionteams):
858                if i != self._selected_team_index:
859                    # Find the dominant component of this sessionteam's color
860                    # and adjust ours so that the component is
861                    # not super-dominant.
862                    max_val = 0.0
863                    max_index = 0
864                    for j in range(3):
865                        if sessionteam.color[j] > max_val:
866                            max_val = sessionteam.color[j]
867                            max_index = j
868                    that_color_for_us = highlight[max_index]
869                    our_second_biggest = max(
870                        highlight[(max_index + 1) % 3],
871                        highlight[(max_index + 2) % 3],
872                    )
873                    diff = that_color_for_us - our_second_biggest
874                    if diff > 0:
875                        highlight[max_index] -= diff * 0.6
876                        highlight[(max_index + 1) % 3] += diff * 0.3
877                        highlight[(max_index + 2) % 3] += diff * 0.2
878        return highlight

Return the currently selected highlight.

def getplayer(self) -> _bascenev1.SessionPlayer:
880    def getplayer(self) -> bascenev1.SessionPlayer:
881        """Return the player associated with this chooser."""
882        return self._sessionplayer

Return the player associated with this chooser.

class Collision:
17class Collision:
18    """A class providing info about occurring collisions."""
19
20    @property
21    def position(self) -> bascenev1.Vec3:
22        """The position of the current collision."""
23        return babase.Vec3(_bascenev1.get_collision_info('position'))
24
25    @property
26    def sourcenode(self) -> bascenev1.Node:
27        """The node containing the material triggering the current callback.
28
29        Throws a bascenev1.NodeNotFoundError if the node does not exist,
30        though the node should always exist (at least at the start of the
31        collision callback).
32        """
33        node = _bascenev1.get_collision_info('sourcenode')
34        assert isinstance(node, (_bascenev1.Node, type(None)))
35        if not node:
36            raise babase.NodeNotFoundError()
37        return node
38
39    @property
40    def opposingnode(self) -> bascenev1.Node:
41        """The node the current callback material node is hitting.
42
43        Throws a bascenev1.NodeNotFoundError if the node does not exist.
44        This can be expected in some cases such as in 'disconnect'
45        callbacks triggered by deleting a currently-colliding node.
46        """
47        node = _bascenev1.get_collision_info('opposingnode')
48        assert isinstance(node, (_bascenev1.Node, type(None)))
49        if not node:
50            raise babase.NodeNotFoundError()
51        return node
52
53    @property
54    def opposingbody(self) -> int:
55        """The body index on the opposing node in the current collision."""
56        body = _bascenev1.get_collision_info('opposingbody')
57        assert isinstance(body, int)
58        return body

A class providing info about occurring collisions.

position: _babase.Vec3
20    @property
21    def position(self) -> bascenev1.Vec3:
22        """The position of the current collision."""
23        return babase.Vec3(_bascenev1.get_collision_info('position'))

The position of the current collision.

sourcenode: _bascenev1.Node
25    @property
26    def sourcenode(self) -> bascenev1.Node:
27        """The node containing the material triggering the current callback.
28
29        Throws a bascenev1.NodeNotFoundError if the node does not exist,
30        though the node should always exist (at least at the start of the
31        collision callback).
32        """
33        node = _bascenev1.get_collision_info('sourcenode')
34        assert isinstance(node, (_bascenev1.Node, type(None)))
35        if not node:
36            raise babase.NodeNotFoundError()
37        return node

The node containing the material triggering the current callback.

Throws a bascenev1.NodeNotFoundError if the node does not exist, though the node should always exist (at least at the start of the collision callback).

opposingnode: _bascenev1.Node
39    @property
40    def opposingnode(self) -> bascenev1.Node:
41        """The node the current callback material node is hitting.
42
43        Throws a bascenev1.NodeNotFoundError if the node does not exist.
44        This can be expected in some cases such as in 'disconnect'
45        callbacks triggered by deleting a currently-colliding node.
46        """
47        node = _bascenev1.get_collision_info('opposingnode')
48        assert isinstance(node, (_bascenev1.Node, type(None)))
49        if not node:
50            raise babase.NodeNotFoundError()
51        return node

The node the current callback material node is hitting.

Throws a bascenev1.NodeNotFoundError if the node does not exist. This can be expected in some cases such as in 'disconnect' callbacks triggered by deleting a currently-colliding node.

opposingbody: int
53    @property
54    def opposingbody(self) -> int:
55        """The body index on the opposing node in the current collision."""
56        body = _bascenev1.get_collision_info('opposingbody')
57        assert isinstance(body, int)
58        return body

The body index on the opposing node in the current collision.

class CollisionMesh:
128class CollisionMesh:
129    """A reference to a collision-mesh.
130
131    Use bascenev1.getcollisionmesh() to instantiate one.
132    """
133
134    pass

A reference to a collision-mesh.

Use bascenev1.getcollisionmesh() to instantiate one.

class ContextError(builtins.Exception):
16class ContextError(Exception):
17    """Exception raised when a call is made in an invalid context.
18
19    Examples of this include calling UI functions within an Activity
20    context or calling scene manipulation functions outside of a game
21    context.
22    """

Exception raised when a call is made in an invalid context.

Examples of this include calling UI functions within an Activity context or calling scene manipulation functions outside of a game context.

class ContextRef:
146class ContextRef:
147    """Store or use a ballistica context.
148
149    Many operations such as bascenev1.newnode() or bascenev1.gettexture()
150    operate implicitly on a current 'context'. A context is some sort of
151    state that functionality can implicitly use. Context determines, for
152    example, which scene nodes or textures get added to without having to
153    specify it explicitly in the newnode()/gettexture() call. Contexts can
154    also affect object lifecycles; for example a babase.ContextCall will
155    become a no-op when the context it was created in is destroyed.
156
157    In general, if you are a modder, you should not need to worry about
158    contexts; mod code should mostly be getting run in the correct
159    context and timers and other callbacks will take care of saving
160    and restoring contexts automatically. There may be rare cases,
161    however, where you need to deal directly with contexts, and that is
162    where this class comes in.
163
164    Creating a babase.ContextRef() will capture a reference to the current
165    context. Other modules may provide ways to access their contexts; for
166    example a bascenev1.Activity instance has a 'context' attribute. You
167    can also use babase.ContextRef.empty() to create a reference to *no*
168    context. Some code such as UI calls may expect this and may complain
169    if you try to use them within a context.
170
171    ##### Usage
172    ContextRefs are generally used with the Python 'with' statement, which
173    sets the context they point to as current on entry and resets it to
174    the previous value on exit.
175
176    ##### Example
177    Explicitly create a few UI bits with no context set.
178    (UI stuff may complain if called within a context):
179    >>> with bui.ContextRef.empty():
180    ...     my_container = bui.containerwidget()
181    """
182
183    def __init__(
184        self,
185    ) -> None:
186        pass
187
188    def __enter__(self) -> None:
189        """Support for "with" statement."""
190        pass
191
192    def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> Any:
193        """Support for "with" statement."""
194        pass
195
196    @classmethod
197    def empty(cls) -> ContextRef:
198        """Return a ContextRef pointing to no context.
199
200        This is useful when code should be run free of a context.
201        For example, UI code generally insists on being run this way.
202        Otherwise, callbacks set on the UI could inadvertently stop working
203        due to a game activity ending, which would be unintuitive behavior.
204        """
205        return ContextRef()
206
207    def is_empty(self) -> bool:
208        """Whether the context was created as empty."""
209        return bool()
210
211    def is_expired(self) -> bool:
212        """Whether the context has expired."""
213        return bool()

Store or use a ballistica context.

Many operations such as bascenev1.newnode() or bascenev1.gettexture() operate implicitly on a current 'context'. A context is some sort of state that functionality can implicitly use. Context determines, for example, which scene nodes or textures get added to without having to specify it explicitly in the newnode()/gettexture() call. Contexts can also affect object lifecycles; for example a babase.ContextCall will become a no-op when the context it was created in is destroyed.

In general, if you are a modder, you should not need to worry about contexts; mod code should mostly be getting run in the correct context and timers and other callbacks will take care of saving and restoring contexts automatically. There may be rare cases, however, where you need to deal directly with contexts, and that is where this class comes in.

Creating a babase.ContextRef() will capture a reference to the current context. Other modules may provide ways to access their contexts; for example a bascenev1.Activity instance has a 'context' attribute. You can also use babase.ContextRef.empty() to create a reference to no context. Some code such as UI calls may expect this and may complain if you try to use them within a context.

Usage

ContextRefs are generally used with the Python 'with' statement, which sets the context they point to as current on entry and resets it to the previous value on exit.

Example

Explicitly create a few UI bits with no context set. (UI stuff may complain if called within a context):

>>> with bui.ContextRef.empty():
...     my_container = bui.containerwidget()
@classmethod
def empty(cls) -> _babase.ContextRef:
196    @classmethod
197    def empty(cls) -> ContextRef:
198        """Return a ContextRef pointing to no context.
199
200        This is useful when code should be run free of a context.
201        For example, UI code generally insists on being run this way.
202        Otherwise, callbacks set on the UI could inadvertently stop working
203        due to a game activity ending, which would be unintuitive behavior.
204        """
205        return ContextRef()

Return a ContextRef pointing to no context.

This is useful when code should be run free of a context. For example, UI code generally insists on being run this way. Otherwise, callbacks set on the UI could inadvertently stop working due to a game activity ending, which would be unintuitive behavior.

def is_empty(self) -> bool:
207    def is_empty(self) -> bool:
208        """Whether the context was created as empty."""
209        return bool()

Whether the context was created as empty.

def is_expired(self) -> bool:
211    def is_expired(self) -> bool:
212        """Whether the context has expired."""
213        return bool()

Whether the context has expired.

class CoopGameActivity(bascenev1.GameActivity[~PlayerT, ~TeamT]):
 26class CoopGameActivity(GameActivity[PlayerT, TeamT]):
 27    """Base class for cooperative-mode games."""
 28
 29    # We can assume our session is a CoopSession.
 30    session: bascenev1.CoopSession
 31
 32    @override
 33    @classmethod
 34    def supports_session_type(
 35        cls, sessiontype: type[bascenev1.Session]
 36    ) -> bool:
 37        from bascenev1._coopsession import CoopSession
 38
 39        return issubclass(sessiontype, CoopSession)
 40
 41    def __init__(self, settings: dict):
 42        super().__init__(settings)
 43
 44        # Cache these for efficiency.
 45        self._achievements_awarded: set[str] = set()
 46
 47        self._life_warning_beep: bascenev1.Actor | None = None
 48        self._life_warning_beep_timer: bascenev1.Timer | None = None
 49        self._warn_beeps_sound = _bascenev1.getsound('warnBeeps')
 50
 51    @override
 52    def on_begin(self) -> None:
 53        super().on_begin()
 54
 55        # Show achievements remaining.
 56        env = babase.app.env
 57        if not (env.demo or env.arcade):
 58            _bascenev1.timer(
 59                3.8, babase.WeakCall(self._show_remaining_achievements)
 60            )
 61
 62        # Preload achievement images in case we get some.
 63        _bascenev1.timer(2.0, babase.WeakCall(self._preload_achievements))
 64
 65    # FIXME: this is now redundant with activityutils.getscoreconfig();
 66    #  need to kill this.
 67    def get_score_type(self) -> str:
 68        """
 69        Return the score unit this co-op game uses ('point', 'seconds', etc.)
 70        """
 71        return 'points'
 72
 73    def _get_coop_level_name(self) -> str:
 74        assert self.session.campaign is not None
 75        return self.session.campaign.name + ':' + str(self.settings_raw['name'])
 76
 77    def celebrate(self, duration: float) -> None:
 78        """Tells all existing player-controlled characters to celebrate.
 79
 80        Can be useful in co-op games when the good guys score or complete
 81        a wave.
 82        duration is given in seconds.
 83        """
 84        from bascenev1._messages import CelebrateMessage
 85
 86        for player in self.players:
 87            if player.actor:
 88                player.actor.handlemessage(CelebrateMessage(duration))
 89
 90    def _preload_achievements(self) -> None:
 91        assert babase.app.classic is not None
 92        achievements = babase.app.classic.ach.achievements_for_coop_level(
 93            self._get_coop_level_name()
 94        )
 95        for ach in achievements:
 96            ach.get_icon_texture(True)
 97
 98    def _show_remaining_achievements(self) -> None:
 99        # pylint: disable=cyclic-import
100        from bascenev1lib.actor.text import Text
101
102        assert babase.app.classic is not None
103        ts_h_offs = 30
104        v_offs = -200
105        achievements = [
106            a
107            for a in babase.app.classic.ach.achievements_for_coop_level(
108                self._get_coop_level_name()
109            )
110            if not a.complete
111        ]
112        vrmode = babase.app.env.vr
113        if achievements:
114            Text(
115                babase.Lstr(resource='achievementsRemainingText'),
116                host_only=True,
117                position=(ts_h_offs - 10 + 40, v_offs - 10),
118                transition=Text.Transition.FADE_IN,
119                scale=1.1,
120                h_attach=Text.HAttach.LEFT,
121                v_attach=Text.VAttach.TOP,
122                color=(1, 1, 1.2, 1) if vrmode else (0.8, 0.8, 1.0, 1.0),
123                flatness=1.0 if vrmode else 0.6,
124                shadow=1.0 if vrmode else 0.5,
125                transition_delay=0.0,
126                transition_out_delay=1.3 if self.slow_motion else 4.0,
127            ).autoretain()
128            hval = 70
129            vval = -50
130            tdelay = 0.0
131            for ach in achievements:
132                tdelay += 0.05
133                ach.create_display(
134                    hval + 40,
135                    vval + v_offs,
136                    0 + tdelay,
137                    outdelay=1.3 if self.slow_motion else 4.0,
138                    style='in_game',
139                )
140                vval -= 55
141
142    @override
143    def spawn_player_spaz(
144        self,
145        player: PlayerT,
146        position: Sequence[float] = (0.0, 0.0, 0.0),
147        angle: float | None = None,
148    ) -> PlayerSpaz:
149        """Spawn and wire up a standard player spaz."""
150        spaz = super().spawn_player_spaz(player, position, angle)
151
152        # Deaths are noteworthy in co-op games.
153        spaz.play_big_death_sound = True
154        return spaz
155
156    def _award_achievement(
157        self, achievement_name: str, sound: bool = True
158    ) -> None:
159        """Award an achievement.
160
161        Returns True if a banner will be shown;
162        False otherwise
163        """
164
165        classic = babase.app.classic
166        plus = babase.app.plus
167        if classic is None or plus is None:
168            logging.warning(
169                '_award_achievement is a no-op without classic and plus.'
170            )
171            return
172
173        if achievement_name in self._achievements_awarded:
174            return
175
176        ach = classic.ach.get_achievement(achievement_name)
177
178        # If we're in the easy campaign and this achievement is hard-mode-only,
179        # ignore it.
180        try:
181            campaign = self.session.campaign
182            assert campaign is not None
183            if ach.hard_mode_only and campaign.name == 'Easy':
184                return
185        except Exception:
186            logging.exception('Error in _award_achievement.')
187
188        # If we haven't awarded this one, check to see if we've got it.
189        # If not, set it through the game service *and* add a transaction
190        # for it.
191        if not ach.complete:
192            self._achievements_awarded.add(achievement_name)
193
194            # Report new achievements to the game-service.
195            plus.report_achievement(achievement_name)
196
197            # ...and to our account.
198            plus.add_v1_account_transaction(
199                {'type': 'ACHIEVEMENT', 'name': achievement_name}
200            )
201
202            # Now bring up a celebration banner.
203            ach.announce_completion(sound=sound)
204
205    def fade_to_red(self) -> None:
206        """Fade the screen to red; (such as when the good guys have lost)."""
207        from bascenev1 import _gameutils
208
209        c_existing = self.globalsnode.tint
210        cnode = _bascenev1.newnode(
211            'combine',
212            attrs={
213                'input0': c_existing[0],
214                'input1': c_existing[1],
215                'input2': c_existing[2],
216                'size': 3,
217            },
218        )
219        _gameutils.animate(cnode, 'input1', {0: c_existing[1], 2.0: 0})
220        _gameutils.animate(cnode, 'input2', {0: c_existing[2], 2.0: 0})
221        cnode.connectattr('output', self.globalsnode, 'tint')
222
223    def setup_low_life_warning_sound(self) -> None:
224        """Set up a beeping noise to play when any players are near death."""
225        self._life_warning_beep = None
226        self._life_warning_beep_timer = _bascenev1.Timer(
227            1.0, babase.WeakCall(self._update_life_warning), repeat=True
228        )
229
230    def _update_life_warning(self) -> None:
231        # Beep continuously if anyone is close to death.
232        should_beep = False
233        for player in self.players:
234            if player.is_alive():
235                # FIXME: Should abstract this instead of
236                #  reading hitpoints directly.
237                if getattr(player.actor, 'hitpoints', 999) < 200:
238                    should_beep = True
239                    break
240        if should_beep and self._life_warning_beep is None:
241            from bascenev1._nodeactor import NodeActor
242
243            self._life_warning_beep = NodeActor(
244                _bascenev1.newnode(
245                    'sound',
246                    attrs={
247                        'sound': self._warn_beeps_sound,
248                        'positional': False,
249                        'loop': True,
250                    },
251                )
252            )
253        if self._life_warning_beep is not None and not should_beep:
254            self._life_warning_beep = None

Base class for cooperative-mode games.

CoopGameActivity(settings: dict)
41    def __init__(self, settings: dict):
42        super().__init__(settings)
43
44        # Cache these for efficiency.
45        self._achievements_awarded: set[str] = set()
46
47        self._life_warning_beep: bascenev1.Actor | None = None
48        self._life_warning_beep_timer: bascenev1.Timer | None = None
49        self._warn_beeps_sound = _bascenev1.getsound('warnBeeps')

Instantiate the Activity.

session: Session
329    @property
330    def session(self) -> bascenev1.Session:
331        """The bascenev1.Session this bascenev1.Activity belongs to.
332
333        Raises a :class:`~bascenev1.SessionNotFoundError` if the Session
334        no longer exists.
335        """
336        session = self._session()
337        if session is None:
338            raise babase.SessionNotFoundError()
339        return session

The bascenev1.Session this bascenev1.Activity belongs to.

Raises a ~bascenev1.SessionNotFoundError if the Session no longer exists.

@override
@classmethod
def supports_session_type(cls, sessiontype: type[Session]) -> bool:
32    @override
33    @classmethod
34    def supports_session_type(
35        cls, sessiontype: type[bascenev1.Session]
36    ) -> bool:
37        from bascenev1._coopsession import CoopSession
38
39        return issubclass(sessiontype, CoopSession)

Return whether this game supports the provided Session type.

@override
def on_begin(self) -> None:
51    @override
52    def on_begin(self) -> None:
53        super().on_begin()
54
55        # Show achievements remaining.
56        env = babase.app.env
57        if not (env.demo or env.arcade):
58            _bascenev1.timer(
59                3.8, babase.WeakCall(self._show_remaining_achievements)
60            )
61
62        # Preload achievement images in case we get some.
63        _bascenev1.timer(2.0, babase.WeakCall(self._preload_achievements))

Called once the previous Activity has finished transitioning out.

At this point the activity's initial players and teams are filled in and it should begin its actual game logic.

def get_score_type(self) -> str:
67    def get_score_type(self) -> str:
68        """
69        Return the score unit this co-op game uses ('point', 'seconds', etc.)
70        """
71        return 'points'

Return the score unit this co-op game uses ('point', 'seconds', etc.)

def celebrate(self, duration: float) -> None:
77    def celebrate(self, duration: float) -> None:
78        """Tells all existing player-controlled characters to celebrate.
79
80        Can be useful in co-op games when the good guys score or complete
81        a wave.
82        duration is given in seconds.
83        """
84        from bascenev1._messages import CelebrateMessage
85
86        for player in self.players:
87            if player.actor:
88                player.actor.handlemessage(CelebrateMessage(duration))

Tells all existing player-controlled characters to celebrate.

Can be useful in co-op games when the good guys score or complete a wave. duration is given in seconds.

@override
def spawn_player_spaz( self, player: ~PlayerT, position: Sequence[float] = (0.0, 0.0, 0.0), angle: float | None = None) -> bascenev1lib.actor.playerspaz.PlayerSpaz:
142    @override
143    def spawn_player_spaz(
144        self,
145        player: PlayerT,
146        position: Sequence[float] = (0.0, 0.0, 0.0),
147        angle: float | None = None,
148    ) -> PlayerSpaz:
149        """Spawn and wire up a standard player spaz."""
150        spaz = super().spawn_player_spaz(player, position, angle)
151
152        # Deaths are noteworthy in co-op games.
153        spaz.play_big_death_sound = True
154        return spaz

Spawn and wire up a standard player spaz.

def fade_to_red(self) -> None:
205    def fade_to_red(self) -> None:
206        """Fade the screen to red; (such as when the good guys have lost)."""
207        from bascenev1 import _gameutils
208
209        c_existing = self.globalsnode.tint
210        cnode = _bascenev1.newnode(
211            'combine',
212            attrs={
213                'input0': c_existing[0],
214                'input1': c_existing[1],
215                'input2': c_existing[2],
216                'size': 3,
217            },
218        )
219        _gameutils.animate(cnode, 'input1', {0: c_existing[1], 2.0: 0})
220        _gameutils.animate(cnode, 'input2', {0: c_existing[2], 2.0: 0})
221        cnode.connectattr('output', self.globalsnode, 'tint')

Fade the screen to red; (such as when the good guys have lost).

def setup_low_life_warning_sound(self) -> None:
223    def setup_low_life_warning_sound(self) -> None:
224        """Set up a beeping noise to play when any players are near death."""
225        self._life_warning_beep = None
226        self._life_warning_beep_timer = _bascenev1.Timer(
227            1.0, babase.WeakCall(self._update_life_warning), repeat=True
228        )

Set up a beeping noise to play when any players are near death.

class CoopSession(bascenev1.Session):
 23class CoopSession(Session):
 24    """A bascenev1.Session which runs cooperative-mode games.
 25
 26    These generally consist of 1-4 players against
 27    the computer and include functionality such as
 28    high score lists.
 29    """
 30
 31    use_teams = True
 32    use_team_colors = False
 33    allow_mid_activity_joins = False
 34
 35    # Note: even though these are instance vars, we annotate them at the
 36    # class level so that docs generation can access their types.
 37
 38    campaign: bascenev1.Campaign | None
 39    """The baclassic.Campaign instance this Session represents, or None if
 40       there is no associated Campaign."""
 41
 42    def __init__(self) -> None:
 43        """Instantiate a co-op mode session."""
 44        # pylint: disable=cyclic-import
 45        from bascenev1lib.activity.coopjoin import CoopJoinActivity
 46
 47        babase.increment_analytics_count('Co-op session start')
 48        app = babase.app
 49        classic = app.classic
 50        assert classic is not None
 51
 52        # If they passed in explicit min/max, honor that.
 53        # Otherwise defer to user overrides or defaults.
 54        if 'min_players' in classic.coop_session_args:
 55            min_players = classic.coop_session_args['min_players']
 56        else:
 57            min_players = 1
 58        if 'max_players' in classic.coop_session_args:
 59            max_players = classic.coop_session_args['max_players']
 60        else:
 61            max_players = app.config.get('Coop Game Max Players', 4)
 62        if 'submit_score' in classic.coop_session_args:
 63            submit_score = classic.coop_session_args['submit_score']
 64        else:
 65            submit_score = True
 66
 67        # print('FIXME: COOP SESSION WOULD CALC DEPS.')
 68        depsets: Sequence[bascenev1.DependencySet] = []
 69
 70        super().__init__(
 71            depsets,
 72            team_names=TEAM_NAMES,
 73            team_colors=TEAM_COLORS,
 74            min_players=min_players,
 75            max_players=max_players,
 76            submit_score=submit_score,
 77        )
 78
 79        # Tournament-ID if we correspond to a co-op tournament (otherwise None)
 80        self.tournament_id: str | None = classic.coop_session_args.get(
 81            'tournament_id'
 82        )
 83
 84        self.campaign = classic.getcampaign(
 85            classic.coop_session_args['campaign']
 86        )
 87        self.campaign_level_name: str = classic.coop_session_args['level']
 88
 89        self._ran_tutorial_activity = False
 90        self._tutorial_activity: bascenev1.Activity | None = None
 91        self._custom_menu_ui: list[dict[str, Any]] = []
 92
 93        # Start our joining screen.
 94        self.setactivity(_bascenev1.newactivity(CoopJoinActivity))
 95
 96        self._next_game_instance: bascenev1.GameActivity | None = None
 97        self._next_game_level_name: str | None = None
 98        self._update_on_deck_game_instances()
 99
100    def get_current_game_instance(self) -> bascenev1.GameActivity:
101        """Get the game instance currently being played."""
102        return self._current_game_instance
103
104    @override
105    def should_allow_mid_activity_joins(
106        self, activity: bascenev1.Activity
107    ) -> bool:
108        # pylint: disable=cyclic-import
109        from bascenev1._gameactivity import GameActivity
110
111        # Disallow any joins in the middle of the game.
112        if isinstance(activity, GameActivity):
113            return False
114
115        return True
116
117    def _update_on_deck_game_instances(self) -> None:
118        # pylint: disable=cyclic-import
119        from bascenev1._gameactivity import GameActivity
120
121        classic = babase.app.classic
122        assert classic is not None
123
124        # Instantiate levels we may be running soon to let them load in the bg.
125
126        # Build an instance for the current level.
127        assert self.campaign is not None
128        level = self.campaign.getlevel(self.campaign_level_name)
129        gametype = level.gametype
130        settings = level.get_settings()
131
132        # Make sure all settings the game expects are present.
133        neededsettings = gametype.get_available_settings(type(self))
134        for setting in neededsettings:
135            if setting.name not in settings:
136                settings[setting.name] = setting.default
137
138        newactivity = _bascenev1.newactivity(gametype, settings)
139        assert isinstance(newactivity, GameActivity)
140        self._current_game_instance: GameActivity = newactivity
141
142        # Find the next level and build an instance for it too.
143        levels = self.campaign.levels
144        level = self.campaign.getlevel(self.campaign_level_name)
145
146        nextlevel: bascenev1.Level | None
147        if level.index < len(levels) - 1:
148            nextlevel = levels[level.index + 1]
149        else:
150            nextlevel = None
151        if nextlevel:
152            gametype = nextlevel.gametype
153            settings = nextlevel.get_settings()
154
155            # Make sure all settings the game expects are present.
156            neededsettings = gametype.get_available_settings(type(self))
157            for setting in neededsettings:
158                if setting.name not in settings:
159                    settings[setting.name] = setting.default
160
161            # We wanna be in the activity's context while taking it down.
162            newactivity = _bascenev1.newactivity(gametype, settings)
163            assert isinstance(newactivity, GameActivity)
164            self._next_game_instance = newactivity
165            self._next_game_level_name = nextlevel.name
166        else:
167            self._next_game_instance = None
168            self._next_game_level_name = None
169
170        # Special case:
171        # If our current level is 'onslaught training', instantiate
172        # our tutorial so its ready to go. (if we haven't run it yet).
173        if (
174            self.campaign_level_name == 'Onslaught Training'
175            and self._tutorial_activity is None
176            and not self._ran_tutorial_activity
177        ):
178            from bascenev1lib.tutorial import TutorialActivity
179
180            self._tutorial_activity = _bascenev1.newactivity(TutorialActivity)
181
182    @override
183    def get_custom_menu_entries(self) -> list[dict[str, Any]]:
184        return self._custom_menu_ui
185
186    @override
187    def on_player_leave(self, sessionplayer: bascenev1.SessionPlayer) -> None:
188        super().on_player_leave(sessionplayer)
189
190        _bascenev1.timer(2.0, babase.WeakCall(self._handle_empty_activity))
191
192    def _handle_empty_activity(self) -> None:
193        """Handle cases where all players have left the current activity."""
194
195        from bascenev1._gameactivity import GameActivity
196
197        activity = self.getactivity()
198        if activity is None:
199            return  # Hmm what should we do in this case?
200
201        # If there are still players in the current activity, we're good.
202        if activity.players:
203            return
204
205        # If there are *not* players in the current activity but there
206        # *are* in the session:
207        if not activity.players and self.sessionplayers:
208            # If we're in a game, we should restart to pull in players
209            # currently waiting in the session.
210            if isinstance(activity, GameActivity):
211                # Never restart tourney games however; just end the session
212                # if all players are gone.
213                if self.tournament_id is not None:
214                    self.end()
215                else:
216                    self.restart()
217
218        # Hmm; no players anywhere. Let's end the entire session if we're
219        # running a GUI (or just the current game if we're running headless).
220        else:
221            if babase.app.env.gui:
222                self.end()
223            else:
224                if isinstance(activity, GameActivity):
225                    with activity.context:
226                        activity.end_game()
227
228    def _on_tournament_restart_menu_press(
229        self, resume_callback: Callable[[], Any]
230    ) -> None:
231        # pylint: disable=cyclic-import
232        from bascenev1._gameactivity import GameActivity
233
234        assert babase.app.classic is not None
235        activity = self.getactivity()
236        if activity is not None and not activity.expired:
237            assert self.tournament_id is not None
238            assert isinstance(activity, GameActivity)
239            babase.app.classic.tournament_entry_window(
240                tournament_id=self.tournament_id,
241                tournament_activity=activity,
242                on_close_call=resume_callback,
243            )
244
245    def restart(self) -> None:
246        """Restart the current game activity."""
247
248        # Tell the current activity to end with a 'restart' outcome.
249        # We use 'force' so that we apply even if end has already been called
250        # (but is in its delay period).
251
252        # Make an exception if there's no players left. Otherwise this
253        # can override the default session end that occurs in that case.
254        if not self.sessionplayers:
255            return
256
257        # This method may get called from the UI context so make sure we
258        # explicitly run in the activity's context.
259        activity = self.getactivity()
260        if activity is not None and not activity.expired:
261            activity.can_show_ad_on_death = True
262            with activity.context:
263                activity.end(results={'outcome': 'restart'}, force=True)
264
265    # noinspection PyUnresolvedReferences
266    @override
267    def on_activity_end(
268        self, activity: bascenev1.Activity, results: Any
269    ) -> None:
270        """Method override for co-op sessions.
271
272        Jumps between co-op games and score screens.
273        """
274        # pylint: disable=too-many-branches
275        # pylint: disable=too-many-locals
276        # pylint: disable=too-many-statements
277        # pylint: disable=cyclic-import
278        from bascenev1lib.activity.coopscore import CoopScoreScreen
279        from bascenev1lib.tutorial import TutorialActivity
280
281        from bascenev1._gameresults import GameResults
282        from bascenev1._player import PlayerInfo
283        from bascenev1._activitytypes import JoinActivity, TransitionActivity
284        from bascenev1._coopgame import CoopGameActivity
285        from bascenev1._score import ScoreType
286
287        app = babase.app
288        env = app.env
289        classic = app.classic
290        assert classic is not None
291
292        # If we're running a TeamGameActivity we'll have a GameResults
293        # as results. Otherwise its an old CoopGameActivity so its giving
294        # us a dict of random stuff.
295        if isinstance(results, GameResults):
296            outcome = 'defeat'  # This can't be 'beaten'.
297        else:
298            outcome = '' if results is None else results.get('outcome', '')
299
300        # If we're running with a gui and at any point we have no
301        # in-game players, quit out of the session (this can happen if
302        # someone leaves in the tutorial for instance).
303        if env.gui:
304            active_players = [p for p in self.sessionplayers if p.in_game]
305            if not active_players:
306                self.end()
307                return
308
309        # If we're in a between-round activity or a restart-activity,
310        # hop into a round.
311        if isinstance(
312            activity, (JoinActivity, CoopScoreScreen, TransitionActivity)
313        ):
314            if outcome == 'next_level':
315                if self._next_game_instance is None:
316                    raise RuntimeError()
317                assert self._next_game_level_name is not None
318                self.campaign_level_name = self._next_game_level_name
319                next_game = self._next_game_instance
320            else:
321                next_game = self._current_game_instance
322
323            # Special case: if we're coming from a joining-activity
324            # and will be going into onslaught-training, show the
325            # tutorial first.
326            if (
327                isinstance(activity, JoinActivity)
328                and self.campaign_level_name == 'Onslaught Training'
329                and not (env.demo or env.arcade)
330            ):
331                if self._tutorial_activity is None:
332                    raise RuntimeError('Tutorial not preloaded properly.')
333                self.setactivity(self._tutorial_activity)
334                self._tutorial_activity = None
335                self._ran_tutorial_activity = True
336                self._custom_menu_ui = []
337
338            # Normal case; launch the next round.
339            else:
340                # Reset stats for the new activity.
341                self.stats.reset()
342                for player in self.sessionplayers:
343                    # Skip players that are still choosing a team.
344                    if player.in_game:
345                        self.stats.register_sessionplayer(player)
346                self.stats.setactivity(next_game)
347
348                # Now flip the current activity..
349                self.setactivity(next_game)
350
351                if not (env.demo or env.arcade):
352                    if (
353                        self.tournament_id is not None
354                        and classic.coop_session_args['submit_score']
355                    ):
356                        self._custom_menu_ui = [
357                            {
358                                'label': babase.Lstr(resource='restartText'),
359                                'resume_on_call': False,
360                                'call': babase.WeakCall(
361                                    self._on_tournament_restart_menu_press
362                                ),
363                            }
364                        ]
365                    else:
366                        self._custom_menu_ui = [
367                            {
368                                'label': babase.Lstr(resource='restartText'),
369                                'call': babase.WeakCall(self.restart),
370                            }
371                        ]
372
373        # If we were in a tutorial, just pop a transition to get to the
374        # actual round.
375        elif isinstance(activity, TutorialActivity):
376            self.setactivity(_bascenev1.newactivity(TransitionActivity))
377        else:
378            playerinfos: list[bascenev1.PlayerInfo]
379
380            # Generic team games.
381            if isinstance(results, GameResults):
382                playerinfos = results.playerinfos
383                score = results.get_sessionteam_score(results.sessionteams[0])
384                fail_message = None
385                score_order = (
386                    'decreasing' if results.lower_is_better else 'increasing'
387                )
388                if results.scoretype in (
389                    ScoreType.SECONDS,
390                    ScoreType.MILLISECONDS,
391                ):
392                    scoretype = 'time'
393
394                    # ScoreScreen wants hundredths of a second.
395                    if score is not None:
396                        if results.scoretype is ScoreType.SECONDS:
397                            score *= 100
398                        elif results.scoretype is ScoreType.MILLISECONDS:
399                            score //= 10
400                        else:
401                            raise RuntimeError('FIXME')
402                else:
403                    if results.scoretype is not ScoreType.POINTS:
404                        print(f'Unknown ScoreType:' f' "{results.scoretype}"')
405                    scoretype = 'points'
406
407            # Old coop-game-specific results; should migrate away from these.
408            else:
409                playerinfos = results.get('playerinfos')
410                score = results['score'] if 'score' in results else None
411                fail_message = (
412                    results['fail_message']
413                    if 'fail_message' in results
414                    else None
415                )
416                score_order = (
417                    results['score_order']
418                    if 'score_order' in results
419                    else 'increasing'
420                )
421                activity_score_type = (
422                    activity.get_score_type()
423                    if isinstance(activity, CoopGameActivity)
424                    else None
425                )
426                assert activity_score_type is not None
427                scoretype = activity_score_type
428
429            # Validate types.
430            if playerinfos is not None:
431                assert isinstance(playerinfos, list)
432                assert all(isinstance(i, PlayerInfo) for i in playerinfos)
433
434            # Looks like we were in a round - check the outcome and
435            # go from there.
436            if outcome == 'restart':
437                # This will pop up back in the same round.
438                self.setactivity(_bascenev1.newactivity(TransitionActivity))
439            else:
440                self.setactivity(
441                    _bascenev1.newactivity(
442                        CoopScoreScreen,
443                        {
444                            'playerinfos': playerinfos,
445                            'score': score,
446                            'fail_message': fail_message,
447                            'score_order': score_order,
448                            'score_type': scoretype,
449                            'outcome': outcome,
450                            'campaign': self.campaign,
451                            'level': self.campaign_level_name,
452                        },
453                    )
454                )
455
456        # No matter what, get the next 2 levels ready to go.
457        self._update_on_deck_game_instances()

A bascenev1.Session which runs cooperative-mode games.

These generally consist of 1-4 players against the computer and include functionality such as high score lists.

CoopSession()
42    def __init__(self) -> None:
43        """Instantiate a co-op mode session."""
44        # pylint: disable=cyclic-import
45        from bascenev1lib.activity.coopjoin import CoopJoinActivity
46
47        babase.increment_analytics_count('Co-op session start')
48        app = babase.app
49        classic = app.classic
50        assert classic is not None
51
52        # If they passed in explicit min/max, honor that.
53        # Otherwise defer to user overrides or defaults.
54        if 'min_players' in classic.coop_session_args:
55            min_players = classic.coop_session_args['min_players']
56        else:
57            min_players = 1
58        if 'max_players' in classic.coop_session_args:
59            max_players = classic.coop_session_args['max_players']
60        else:
61            max_players = app.config.get('Coop Game Max Players', 4)
62        if 'submit_score' in classic.coop_session_args:
63            submit_score = classic.coop_session_args['submit_score']
64        else:
65            submit_score = True
66
67        # print('FIXME: COOP SESSION WOULD CALC DEPS.')
68        depsets: Sequence[bascenev1.DependencySet] = []
69
70        super().__init__(
71            depsets,
72            team_names=TEAM_NAMES,
73            team_colors=TEAM_COLORS,
74            min_players=min_players,
75            max_players=max_players,
76            submit_score=submit_score,
77        )
78
79        # Tournament-ID if we correspond to a co-op tournament (otherwise None)
80        self.tournament_id: str | None = classic.coop_session_args.get(
81            'tournament_id'
82        )
83
84        self.campaign = classic.getcampaign(
85            classic.coop_session_args['campaign']
86        )
87        self.campaign_level_name: str = classic.coop_session_args['level']
88
89        self._ran_tutorial_activity = False
90        self._tutorial_activity: bascenev1.Activity | None = None
91        self._custom_menu_ui: list[dict[str, Any]] = []
92
93        # Start our joining screen.
94        self.setactivity(_bascenev1.newactivity(CoopJoinActivity))
95
96        self._next_game_instance: bascenev1.GameActivity | None = None
97        self._next_game_level_name: str | None = None
98        self._update_on_deck_game_instances()

Instantiate a co-op mode session.

use_teams = True

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 = False

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.

allow_mid_activity_joins = False
campaign: Campaign | None

The baclassic.Campaign instance this Session represents, or None if there is no associated Campaign.

tournament_id: str | None
campaign_level_name: str
def get_current_game_instance(self) -> GameActivity:
100    def get_current_game_instance(self) -> bascenev1.GameActivity:
101        """Get the game instance currently being played."""
102        return self._current_game_instance

Get the game instance currently being played.

@override
def should_allow_mid_activity_joins(self, activity: Activity) -> bool:
104    @override
105    def should_allow_mid_activity_joins(
106        self, activity: bascenev1.Activity
107    ) -> bool:
108        # pylint: disable=cyclic-import
109        from bascenev1._gameactivity import GameActivity
110
111        # Disallow any joins in the middle of the game.
112        if isinstance(activity, GameActivity):
113            return False
114
115        return True

Ask ourself if we should allow joins during an Activity.

Note that for a join to be allowed, both the Session and Activity have to be ok with it (via this function and the Activity.allow_mid_activity_joins property.

@override
def get_custom_menu_entries(self) -> list[dict[str, typing.Any]]:
182    @override
183    def get_custom_menu_entries(self) -> list[dict[str, Any]]:
184        return self._custom_menu_ui

Subclasses can override this to provide custom menu entries.

The returned value should be a list of dicts, each containing a 'label' and 'call' entry, with 'label' being the text for the entry and 'call' being the callable to trigger if the entry is pressed.

@override
def on_player_leave(self, sessionplayer: _bascenev1.SessionPlayer) -> None:
186    @override
187    def on_player_leave(self, sessionplayer: bascenev1.SessionPlayer) -> None:
188        super().on_player_leave(sessionplayer)
189
190        _bascenev1.timer(2.0, babase.WeakCall(self._handle_empty_activity))

Called when a previously-accepted bascenev1.SessionPlayer leaves.

def restart(self) -> None:
245    def restart(self) -> None:
246        """Restart the current game activity."""
247
248        # Tell the current activity to end with a 'restart' outcome.
249        # We use 'force' so that we apply even if end has already been called
250        # (but is in its delay period).
251
252        # Make an exception if there's no players left. Otherwise this
253        # can override the default session end that occurs in that case.
254        if not self.sessionplayers:
255            return
256
257        # This method may get called from the UI context so make sure we
258        # explicitly run in the activity's context.
259        activity = self.getactivity()
260        if activity is not None and not activity.expired:
261            activity.can_show_ad_on_death = True
262            with activity.context:
263                activity.end(results={'outcome': 'restart'}, force=True)

Restart the current game activity.

@override
def on_activity_end(self, activity: Activity, results: Any) -> None:
266    @override
267    def on_activity_end(
268        self, activity: bascenev1.Activity, results: Any
269    ) -> None:
270        """Method override for co-op sessions.
271
272        Jumps between co-op games and score screens.
273        """
274        # pylint: disable=too-many-branches
275        # pylint: disable=too-many-locals
276        # pylint: disable=too-many-statements
277        # pylint: disable=cyclic-import
278        from bascenev1lib.activity.coopscore import CoopScoreScreen
279        from bascenev1lib.tutorial import TutorialActivity
280
281        from bascenev1._gameresults import GameResults
282        from bascenev1._player import PlayerInfo
283        from bascenev1._activitytypes import JoinActivity, TransitionActivity
284        from bascenev1._coopgame import CoopGameActivity
285        from bascenev1._score import ScoreType
286
287        app = babase.app
288        env = app.env
289        classic = app.classic
290        assert classic is not None
291
292        # If we're running a TeamGameActivity we'll have a GameResults
293        # as results. Otherwise its an old CoopGameActivity so its giving
294        # us a dict of random stuff.
295        if isinstance(results, GameResults):
296            outcome = 'defeat'  # This can't be 'beaten'.
297        else:
298            outcome = '' if results is None else results.get('outcome', '')
299
300        # If we're running with a gui and at any point we have no
301        # in-game players, quit out of the session (this can happen if
302        # someone leaves in the tutorial for instance).
303        if env.gui:
304            active_players = [p for p in self.sessionplayers if p.in_game]
305            if not active_players:
306                self.end()
307                return
308
309        # If we're in a between-round activity or a restart-activity,
310        # hop into a round.
311        if isinstance(
312            activity, (JoinActivity, CoopScoreScreen, TransitionActivity)
313        ):
314            if outcome == 'next_level':
315                if self._next_game_instance is None:
316                    raise RuntimeError()
317                assert self._next_game_level_name is not None
318                self.campaign_level_name = self._next_game_level_name
319                next_game = self._next_game_instance
320            else:
321                next_game = self._current_game_instance
322
323            # Special case: if we're coming from a joining-activity
324            # and will be going into onslaught-training, show the
325            # tutorial first.
326            if (
327                isinstance(activity, JoinActivity)
328                and self.campaign_level_name == 'Onslaught Training'
329                and not (env.demo or env.arcade)
330            ):
331                if self._tutorial_activity is None:
332                    raise RuntimeError('Tutorial not preloaded properly.')
333                self.setactivity(self._tutorial_activity)
334                self._tutorial_activity = None
335                self._ran_tutorial_activity = True
336                self._custom_menu_ui = []
337
338            # Normal case; launch the next round.
339            else:
340                # Reset stats for the new activity.
341                self.stats.reset()
342                for player in self.sessionplayers:
343                    # Skip players that are still choosing a team.
344                    if player.in_game:
345                        self.stats.register_sessionplayer(player)
346                self.stats.setactivity(next_game)
347
348                # Now flip the current activity..
349                self.setactivity(next_game)
350
351                if not (env.demo or env.arcade):
352                    if (
353                        self.tournament_id is not None
354                        and classic.coop_session_args['submit_score']
355                    ):
356                        self._custom_menu_ui = [
357                            {
358                                'label': babase.Lstr(resource='restartText'),
359                                'resume_on_call': False,
360                                'call': babase.WeakCall(
361                                    self._on_tournament_restart_menu_press
362                                ),
363                            }
364                        ]
365                    else:
366                        self._custom_menu_ui = [
367                            {
368                                'label': babase.Lstr(resource='restartText'),
369                                'call': babase.WeakCall(self.restart),
370                            }
371                        ]
372
373        # If we were in a tutorial, just pop a transition to get to the
374        # actual round.
375        elif isinstance(activity, TutorialActivity):
376            self.setactivity(_bascenev1.newactivity(TransitionActivity))
377        else:
378            playerinfos: list[bascenev1.PlayerInfo]
379
380            # Generic team games.
381            if isinstance(results, GameResults):
382                playerinfos = results.playerinfos
383                score = results.get_sessionteam_score(results.sessionteams[0])
384                fail_message = None
385                score_order = (
386                    'decreasing' if results.lower_is_better else 'increasing'
387                )
388                if results.scoretype in (
389                    ScoreType.SECONDS,
390                    ScoreType.MILLISECONDS,
391                ):
392                    scoretype = 'time'
393
394                    # ScoreScreen wants hundredths of a second.
395                    if score is not None:
396                        if results.scoretype is ScoreType.SECONDS:
397                            score *= 100
398                        elif results.scoretype is ScoreType.MILLISECONDS:
399                            score //= 10
400                        else:
401                            raise RuntimeError('FIXME')
402                else:
403                    if results.scoretype is not ScoreType.POINTS:
404                        print(f'Unknown ScoreType:' f' "{results.scoretype}"')
405                    scoretype = 'points'
406
407            # Old coop-game-specific results; should migrate away from these.
408            else:
409                playerinfos = results.get('playerinfos')
410                score = results['score'] if 'score' in results else None
411                fail_message = (
412                    results['fail_message']
413                    if 'fail_message' in results
414                    else None
415                )
416                score_order = (
417                    results['score_order']
418                    if 'score_order' in results
419                    else 'increasing'
420                )
421                activity_score_type = (
422                    activity.get_score_type()
423                    if isinstance(activity, CoopGameActivity)
424                    else None
425                )
426                assert activity_score_type is not None
427                scoretype = activity_score_type
428
429            # Validate types.
430            if playerinfos is not None:
431                assert isinstance(playerinfos, list)
432                assert all(isinstance(i, PlayerInfo) for i in playerinfos)
433
434            # Looks like we were in a round - check the outcome and
435            # go from there.
436            if outcome == 'restart':
437                # This will pop up back in the same round.
438                self.setactivity(_bascenev1.newactivity(TransitionActivity))
439            else:
440                self.setactivity(
441                    _bascenev1.newactivity(
442                        CoopScoreScreen,
443                        {
444                            'playerinfos': playerinfos,
445                            'score': score,
446                            'fail_message': fail_message,
447                            'score_order': score_order,
448                            'score_type': scoretype,
449                            'outcome': outcome,
450                            'campaign': self.campaign,
451                            'level': self.campaign_level_name,
452                        },
453                    )
454                )
455
456        # No matter what, get the next 2 levels ready to go.
457        self._update_on_deck_game_instances()

Method override for co-op sessions.

Jumps between co-op games and score screens.

class Data:
137class Data:
138    """A reference to a data object.
139
140    Use bascenev1.getdata() to instantiate one.
141    """
142
143    def getvalue(self) -> Any:
144        """Return the data object's value.
145
146        This can consist of anything representable by json (dicts, lists,
147        numbers, bools, None, etc).
148        Note that this call will block if the data has not yet been loaded,
149        so it can be beneficial to plan a short bit of time between when
150        the data object is requested and when it's value is accessed.
151        """
152        return _uninferrable()

A reference to a data object.

Use bascenev1.getdata() to instantiate one.

def getvalue(self) -> Any:
143    def getvalue(self) -> Any:
144        """Return the data object's value.
145
146        This can consist of anything representable by json (dicts, lists,
147        numbers, bools, None, etc).
148        Note that this call will block if the data has not yet been loaded,
149        so it can be beneficial to plan a short bit of time between when
150        the data object is requested and when it's value is accessed.
151        """
152        return _uninferrable()

Return the data object's value.

This can consist of anything representable by json (dicts, lists, numbers, bools, None, etc). Note that this call will block if the data has not yet been loaded, so it can be beneficial to plan a short bit of time between when the data object is requested and when it's value is accessed.

class DeathType(enum.Enum):
35class DeathType(Enum):
36    """A reason for a death."""
37
38    GENERIC = 'generic'
39    OUT_OF_BOUNDS = 'out_of_bounds'
40    IMPACT = 'impact'
41    FALL = 'fall'
42    REACHED_GOAL = 'reached_goal'
43    LEFT_GAME = 'left_game'

A reason for a death.

GENERIC = <DeathType.GENERIC: 'generic'>
OUT_OF_BOUNDS = <DeathType.OUT_OF_BOUNDS: 'out_of_bounds'>
IMPACT = <DeathType.IMPACT: 'impact'>
FALL = <DeathType.FALL: 'fall'>
REACHED_GOAL = <DeathType.REACHED_GOAL: 'reached_goal'>
LEFT_GAME = <DeathType.LEFT_GAME: 'left_game'>
DEFAULT_TEAM_COLORS = ((0.1, 0.25, 1.0), (1.0, 0.25, 0.2))
DEFAULT_TEAM_NAMES = ('Blue', 'Red')
class Dependency(typing.Generic[~T]):
23class Dependency(Generic[T]):
24    """A dependency on a DependencyComponent (with an optional config).
25
26    This class is used to request and access functionality provided
27    by other DependencyComponent classes from a DependencyComponent class.
28    The class functions as a descriptor, allowing dependencies to
29    be added at a class level much the same as properties or methods
30    and then used with class instances to access those dependencies.
31    For instance, if you do 'floofcls = bascenev1.Dependency(FloofClass)'
32    you would then be able to instantiate a FloofClass in your class's
33    methods via self.floofcls().
34    """
35
36    def __init__(self, cls: type[T], config: Any = None):
37        """Instantiate a Dependency given a bascenev1.DependencyComponent type.
38
39        Optionally, an arbitrary object can be passed as 'config' to
40        influence dependency calculation for the target class.
41        """
42        self.cls: type[T] = cls
43        self.config = config
44        self._hash: int | None = None
45
46    def get_hash(self) -> int:
47        """Return the dependency's hash, calculating it if necessary."""
48        from efro.util import make_hash
49
50        if self._hash is None:
51            self._hash = make_hash((self.cls, self.config))
52        return self._hash
53
54    def __get__(self, obj: Any, cls: Any = None) -> T:
55        if not isinstance(obj, DependencyComponent):
56            if obj is None:
57                raise TypeError(
58                    'Dependency must be accessed through an instance.'
59                )
60            raise TypeError(
61                f'Dependency cannot be added to class of type {type(obj)}'
62                ' (class must inherit from bascenev1.DependencyComponent).'
63            )
64
65        # We expect to be instantiated from an already living
66        # DependencyComponent with valid dep-data in place..
67        assert cls is not None
68
69        # Get the DependencyEntry this instance is associated with and from
70        # there get back to the DependencySet
71        entry = getattr(obj, '_dep_entry')
72        if entry is None:
73            raise RuntimeError('Invalid dependency access.')
74        entry = entry()
75        assert isinstance(entry, DependencyEntry)
76        depset = entry.depset()
77        assert isinstance(depset, DependencySet)
78
79        if not depset.resolved:
80            raise RuntimeError(
81                "Can't access data on an unresolved DependencySet."
82            )
83
84        # Look up the data in the set based on the hash for this Dependency.
85        assert self._hash in depset.entries
86        entry = depset.entries[self._hash]
87        assert isinstance(entry, DependencyEntry)
88        retval = entry.get_component()
89        assert isinstance(retval, self.cls)
90        return retval

A dependency on a DependencyComponent (with an optional config).

This class is used to request and access functionality provided by other DependencyComponent classes from a DependencyComponent class. The class functions as a descriptor, allowing dependencies to be added at a class level much the same as properties or methods and then used with class instances to access those dependencies. For instance, if you do 'floofcls = bascenev1.Dependency(FloofClass)' you would then be able to instantiate a FloofClass in your class's methods via self.floofcls().

Dependency(cls: type[~T], config: Any = None)
36    def __init__(self, cls: type[T], config: Any = None):
37        """Instantiate a Dependency given a bascenev1.DependencyComponent type.
38
39        Optionally, an arbitrary object can be passed as 'config' to
40        influence dependency calculation for the target class.
41        """
42        self.cls: type[T] = cls
43        self.config = config
44        self._hash: int | None = None

Instantiate a Dependency given a bascenev1.DependencyComponent type.

Optionally, an arbitrary object can be passed as 'config' to influence dependency calculation for the target class.

cls: type[~T]
config
def get_hash(self) -> int:
46    def get_hash(self) -> int:
47        """Return the dependency's hash, calculating it if necessary."""
48        from efro.util import make_hash
49
50        if self._hash is None:
51            self._hash = make_hash((self.cls, self.config))
52        return self._hash

Return the dependency's hash, calculating it if necessary.

class DependencyComponent:
 93class DependencyComponent:
 94    """Base class for all classes that can act as or use dependencies."""
 95
 96    _dep_entry: weakref.ref[DependencyEntry]
 97
 98    def __init__(self) -> None:
 99        """Instantiate a DependencyComponent."""
100
101        # For now lets issue a warning if these are instantiated without
102        # a dep-entry; we'll make this an error once we're no longer
103        # seeing warnings.
104        # entry = getattr(self, '_dep_entry', None)
105        # if entry is None:
106        #     print(f'FIXME: INSTANTIATING DEP CLASS {type(self)} DIRECTLY.')
107
108    @classmethod
109    def dep_is_present(cls, config: Any = None) -> bool:
110        """Return whether this component/config is present on this device."""
111        del config  # Unused here.
112        return True
113
114    @classmethod
115    def get_dynamic_deps(cls, config: Any = None) -> list[Dependency]:
116        """Return any dynamically-calculated deps for this component/config.
117
118        Deps declared statically as part of the class do not need to be
119        included here; this is only for additional deps that may vary based
120        on the dep config value. (for instance a map required by a game type)
121        """
122        del config  # Unused here.
123        return []

Base class for all classes that can act as or use dependencies.

DependencyComponent()
 98    def __init__(self) -> None:
 99        """Instantiate a DependencyComponent."""
100
101        # For now lets issue a warning if these are instantiated without
102        # a dep-entry; we'll make this an error once we're no longer
103        # seeing warnings.
104        # entry = getattr(self, '_dep_entry', None)
105        # if entry is None:
106        #     print(f'FIXME: INSTANTIATING DEP CLASS {type(self)} DIRECTLY.')

Instantiate a DependencyComponent.

@classmethod
def dep_is_present(cls, config: Any = None) -> bool:
108    @classmethod
109    def dep_is_present(cls, config: Any = None) -> bool:
110        """Return whether this component/config is present on this device."""
111        del config  # Unused here.
112        return True

Return whether this component/config is present on this device.

@classmethod
def get_dynamic_deps(cls, config: Any = None) -> list[Dependency]:
114    @classmethod
115    def get_dynamic_deps(cls, config: Any = None) -> list[Dependency]:
116        """Return any dynamically-calculated deps for this component/config.
117
118        Deps declared statically as part of the class do not need to be
119        included here; this is only for additional deps that may vary based
120        on the dep config value. (for instance a map required by a game type)
121        """
122        del config  # Unused here.
123        return []

Return any dynamically-calculated deps for this component/config.

Deps declared statically as part of the class do not need to be included here; this is only for additional deps that may vary based on the dep config value. (for instance a map required by a game type)

class DependencySet(typing.Generic[~T]):
169class DependencySet(Generic[T]):
170    """Set of resolved dependencies and their associated data.
171
172    To use DependencyComponents, a set must be created, resolved, and then
173    loaded. The DependencyComponents are only valid while the set remains
174    in existence.
175    """
176
177    def __init__(self, root_dependency: Dependency[T]):
178        # print('DepSet()')
179        self._root_dependency = root_dependency
180        self._resolved = False
181        self._loaded = False
182
183        # Dependency data indexed by hash.
184        self.entries: dict[int, DependencyEntry] = {}
185
186    # def __del__(self) -> None:
187    #     print("~DepSet()")
188
189    def resolve(self) -> None:
190        """Resolve the complete set of required dependencies for this set.
191
192        Raises a bascenev1.DependencyError if dependencies are missing (or
193        other Exception types on other errors).
194        """
195
196        if self._resolved:
197            raise RuntimeError('DependencySet has already been resolved.')
198
199        # print('RESOLVING DEP SET')
200
201        # First, recursively expand out all dependencies.
202        self._resolve(self._root_dependency, 0)
203
204        # Now, if any dependencies are not present, raise an Exception
205        # telling exactly which ones (so hopefully they'll be able to be
206        # downloaded/etc.
207        missing = [
208            Dependency(entry.cls, entry.config)
209            for entry in self.entries.values()
210            if not entry.cls.dep_is_present(entry.config)
211        ]
212        if missing:
213            raise DependencyError(missing)
214
215        self._resolved = True
216        # print('RESOLVE SUCCESS!')
217
218    @property
219    def resolved(self) -> bool:
220        """Whether this set has been successfully resolved."""
221        return self._resolved
222
223    def get_asset_package_ids(self) -> set[str]:
224        """Return the set of asset-package-ids required by this dep-set.
225
226        Must be called on a resolved dep-set.
227        """
228        ids: set[str] = set()
229        if not self._resolved:
230            raise RuntimeError('Must be called on a resolved dep-set.')
231        for entry in self.entries.values():
232            if issubclass(entry.cls, AssetPackage):
233                assert isinstance(entry.config, str)
234                ids.add(entry.config)
235        return ids
236
237    def load(self) -> None:
238        """Instantiate all DependencyComponents in the set.
239
240        Returns a wrapper which can be used to instantiate the root dep.
241        """
242        # NOTE: stuff below here should probably go in a separate 'instantiate'
243        # method or something.
244        if not self._resolved:
245            raise RuntimeError("Can't load an unresolved DependencySet")
246
247        for entry in self.entries.values():
248            # Do a get on everything which will init all payloads
249            # in the proper order recursively.
250            entry.get_component()
251
252        self._loaded = True
253
254    @property
255    def root(self) -> T:
256        """The instantiated root DependencyComponent instance for the set."""
257        if not self._loaded:
258            raise RuntimeError('DependencySet is not loaded.')
259
260        rootdata = self.entries[self._root_dependency.get_hash()].component
261        assert isinstance(rootdata, self._root_dependency.cls)
262        return rootdata
263
264    def _resolve(self, dep: Dependency[T], recursion: int) -> None:
265        # Watch for wacky infinite dep loops.
266        if recursion > 10:
267            raise RecursionError('Max recursion reached')
268
269        hashval = dep.get_hash()
270
271        if hashval in self.entries:
272            # Found an already resolved one; we're done here.
273            return
274
275        # Add our entry before we recurse so we don't repeat add it if
276        # there's a dependency loop.
277        self.entries[hashval] = DependencyEntry(self, dep)
278
279        # Grab all Dependency instances we find in the class.
280        subdeps = [
281            cls
282            for cls in dep.cls.__dict__.values()
283            if isinstance(cls, Dependency)
284        ]
285
286        # ..and add in any dynamic ones it provides.
287        subdeps += dep.cls.get_dynamic_deps(dep.config)
288        for subdep in subdeps:
289            self._resolve(subdep, recursion + 1)

Set of resolved dependencies and their associated data.

To use DependencyComponents, a set must be created, resolved, and then loaded. The DependencyComponents are only valid while the set remains in existence.

DependencySet(root_dependency: Dependency[~T])
177    def __init__(self, root_dependency: Dependency[T]):
178        # print('DepSet()')
179        self._root_dependency = root_dependency
180        self._resolved = False
181        self._loaded = False
182
183        # Dependency data indexed by hash.
184        self.entries: dict[int, DependencyEntry] = {}
entries: dict[int, bascenev1._dependency.DependencyEntry]
def resolve(self) -> None:
189    def resolve(self) -> None:
190        """Resolve the complete set of required dependencies for this set.
191
192        Raises a bascenev1.DependencyError if dependencies are missing (or
193        other Exception types on other errors).
194        """
195
196        if self._resolved:
197            raise RuntimeError('DependencySet has already been resolved.')
198
199        # print('RESOLVING DEP SET')
200
201        # First, recursively expand out all dependencies.
202        self._resolve(self._root_dependency, 0)
203
204        # Now, if any dependencies are not present, raise an Exception
205        # telling exactly which ones (so hopefully they'll be able to be
206        # downloaded/etc.
207        missing = [
208            Dependency(entry.cls, entry.config)
209            for entry in self.entries.values()
210            if not entry.cls.dep_is_present(entry.config)
211        ]
212        if missing:
213            raise DependencyError(missing)
214
215        self._resolved = True
216        # print('RESOLVE SUCCESS!')

Resolve the complete set of required dependencies for this set.

Raises a bascenev1.DependencyError if dependencies are missing (or other Exception types on other errors).

resolved: bool
218    @property
219    def resolved(self) -> bool:
220        """Whether this set has been successfully resolved."""
221        return self._resolved

Whether this set has been successfully resolved.

def get_asset_package_ids(self) -> set[str]:
223    def get_asset_package_ids(self) -> set[str]:
224        """Return the set of asset-package-ids required by this dep-set.
225
226        Must be called on a resolved dep-set.
227        """
228        ids: set[str] = set()
229        if not self._resolved:
230            raise RuntimeError('Must be called on a resolved dep-set.')
231        for entry in self.entries.values():
232            if issubclass(entry.cls, AssetPackage):
233                assert isinstance(entry.config, str)
234                ids.add(entry.config)
235        return ids

Return the set of asset-package-ids required by this dep-set.

Must be called on a resolved dep-set.

def load(self) -> None:
237    def load(self) -> None:
238        """Instantiate all DependencyComponents in the set.
239
240        Returns a wrapper which can be used to instantiate the root dep.
241        """
242        # NOTE: stuff below here should probably go in a separate 'instantiate'
243        # method or something.
244        if not self._resolved:
245            raise RuntimeError("Can't load an unresolved DependencySet")
246
247        for entry in self.entries.values():
248            # Do a get on everything which will init all payloads
249            # in the proper order recursively.
250            entry.get_component()
251
252        self._loaded = True

Instantiate all DependencyComponents in the set.

Returns a wrapper which can be used to instantiate the root dep.

root: ~T
254    @property
255    def root(self) -> T:
256        """The instantiated root DependencyComponent instance for the set."""
257        if not self._loaded:
258            raise RuntimeError('DependencySet is not loaded.')
259
260        rootdata = self.entries[self._root_dependency.get_hash()].component
261        assert isinstance(rootdata, self._root_dependency.cls)
262        return rootdata

The instantiated root DependencyComponent instance for the set.

@dataclass
class DieMessage:
46@dataclass
47class DieMessage:
48    """A message telling an object to die.
49
50    Most bascenev1.Actor-s respond to this.
51    """
52
53    #: If this is set to True, the actor should disappear immediately.
54    #: This is for 'removing' stuff from the game more so than 'killing'
55    #: it. If False, the actor should die a 'normal' death and can take
56    #: its time with lingering corpses, sound effects, etc.
57    immediate: bool = False
58
59    #: The particular reason for death.
60    how: DeathType = DeathType.GENERIC

A message telling an object to die.

Most bascenev1.Actor-s respond to this.

DieMessage( immediate: bool = False, how: DeathType = <DeathType.GENERIC: 'generic'>)
immediate: bool = False
how: DeathType = <DeathType.GENERIC: 'generic'>
def displaytime() -> DisplayTime:
729def displaytime() -> babase.DisplayTime:
730    """Return the current display-time in seconds.
731
732    Display-time is a time value intended to be used for animation and other
733    visual purposes. It will generally increment by a consistent amount each
734    frame. It will pass at an overall similar rate to AppTime, but trades
735    accuracy for smoothness.
736
737    Note that the value returned here is simply a float; it just has a
738    unique type in the type-checker's eyes to help prevent it from being
739    accidentally used with time functionality expecting other time types.
740    """
741    import babase  # pylint: disable=cyclic-import
742
743    return babase.DisplayTime(0.0)

Return the current display-time in seconds.

Display-time is a time value intended to be used for animation and other visual purposes. It will generally increment by a consistent amount each frame. It will pass at an overall similar rate to AppTime, but trades accuracy for smoothness.

Note that the value returned here is simply a float; it just has a unique type in the type-checker's eyes to help prevent it from being accidentally used with time functionality expecting other time types.

DisplayTime = DisplayTime
def displaytimer(time: float, call: Callable[[], Any]) -> None:
746def displaytimer(time: float, call: Callable[[], Any]) -> None:
747    """Schedule a callable object to run based on display-time.
748
749    This function creates a one-off timer which cannot be canceled or
750    modified once created. If you require the ability to do so, or need
751    a repeating timer, use the babase.DisplayTimer class instead.
752
753    Display-time is a time value intended to be used for animation and other
754    visual purposes. It will generally increment by a consistent amount each
755    frame. It will pass at an overall similar rate to AppTime, but trades
756    accuracy for smoothness.
757
758    ##### Arguments
759    ###### time (float)
760    > Length of time in seconds that the timer will wait before firing.
761
762    ###### call (Callable[[], Any])
763    > A callable Python object. Note that the timer will retain a
764    strong reference to the callable for as long as the timer exists, so you
765    may want to look into concepts such as babase.WeakCall if that is not
766    desired.
767
768    ##### Examples
769    Print some stuff through time:
770    >>> babase.screenmessage('hello from now!')
771    >>> babase.displaytimer(1.0, babase.Call(babase.screenmessage,
772    ...                       'hello from the future!'))
773    >>> babase.displaytimer(2.0, babase.Call(babase.screenmessage,
774    ...                       'hello from the future 2!'))
775    """
776    return None

Schedule a callable object to run based on display-time.

This function creates a one-off timer which cannot be canceled or modified once created. If you require the ability to do so, or need a repeating timer, use the babase.DisplayTimer class instead.

Display-time is a time value intended to be used for animation and other visual purposes. It will generally increment by a consistent amount each frame. It will pass at an overall similar rate to AppTime, but trades accuracy for smoothness.

Arguments
time (float)

Length of time in seconds that the timer will wait before firing.

call (Callable[[], Any])

A callable Python object. Note that the timer will retain a strong reference to the callable for as long as the timer exists, so you may want to look into concepts such as babase.WeakCall if that is not desired.

Examples

Print some stuff through time:

>>> babase.screenmessage('hello from now!')
>>> babase.displaytimer(1.0, babase.Call(babase.screenmessage,
...                       'hello from the future!'))
>>> babase.displaytimer(2.0, babase.Call(babase.screenmessage,
...                       'hello from the future 2!'))
class DisplayTimer:
216class DisplayTimer:
217    """Timers are used to run code at later points in time.
218
219    This class encapsulates a timer based on display-time.
220    The underlying timer will be destroyed when this object is no longer
221    referenced. If you do not want to worry about keeping a reference to
222    your timer around, use the babase.displaytimer() function instead to get a
223    one-off timer.
224
225    Display-time is a time value intended to be used for animation and
226    other visual purposes. It will generally increment by a consistent
227    amount each frame. It will pass at an overall similar rate to AppTime,
228    but trades accuracy for smoothness.
229
230    ##### Arguments
231    ###### time
232    > Length of time in seconds that the timer will wait before firing.
233
234    ###### call
235    > A callable Python object. Remember that the timer will retain a
236    strong reference to the callable for as long as it exists, so you
237    may want to look into concepts such as babase.WeakCall if that is not
238    desired.
239
240    ###### repeat
241    > If True, the timer will fire repeatedly, with each successive
242    firing having the same delay as the first.
243
244    ##### Example
245
246    Use a Timer object to print repeatedly for a few seconds:
247    ... def say_it():
248    ...     babase.screenmessage('BADGER!')
249    ... def stop_saying_it():
250    ...     global g_timer
251    ...     g_timer = None
252    ...     babase.screenmessage('MUSHROOM MUSHROOM!')
253    ... # Create our timer; it will run as long as we have the self.t ref.
254    ... g_timer = babase.DisplayTimer(0.3, say_it, repeat=True)
255    ... # Now fire off a one-shot timer to kill it.
256    ... babase.displaytimer(3.89, stop_saying_it)
257    """
258
259    def __init__(
260        self, time: float, call: Callable[[], Any], repeat: bool = False
261    ) -> None:
262        pass

Timers are used to run code at later points in time.

This class encapsulates a timer based on display-time. The underlying timer will be destroyed when this object is no longer referenced. If you do not want to worry about keeping a reference to your timer around, use the babase.displaytimer() function instead to get a one-off timer.

Display-time is a time value intended to be used for animation and other visual purposes. It will generally increment by a consistent amount each frame. It will pass at an overall similar rate to AppTime, but trades accuracy for smoothness.

Arguments
time

Length of time in seconds that the timer will wait before firing.

call

A callable Python object. Remember that the timer will retain a strong reference to the callable for as long as it exists, so you may want to look into concepts such as babase.WeakCall if that is not desired.

repeat

If True, the timer will fire repeatedly, with each successive firing having the same delay as the first.

Example

Use a Timer object to print repeatedly for a few seconds: ... def say_it(): ... babase.screenmessage('BADGER!') ... def stop_saying_it(): ... global g_timer ... g_timer = None ... babase.screenmessage('MUSHROOM MUSHROOM!') ... # Create our timer; it will run as long as we have the self.t ref. ... g_timer = babase.DisplayTimer(0.3, say_it, repeat=True) ... # Now fire off a one-shot timer to kill it. ... babase.displaytimer(3.89, stop_saying_it)

DisplayTimer(time: float, call: Callable[[], Any], repeat: bool = False)
259    def __init__(
260        self, time: float, call: Callable[[], Any], repeat: bool = False
261    ) -> None:
262        pass
@dataclass
class DropMessage:
140@dataclass
141class DropMessage:
142    """Tells an object that it has dropped what it was holding."""

Tells an object that it has dropped what it was holding.

@dataclass
class DroppedMessage:
153@dataclass
154class DroppedMessage:
155    """Tells an object that it has been dropped."""
156
157    node: bascenev1.Node
158    """The bascenev1.Node doing the dropping."""

Tells an object that it has been dropped.

DroppedMessage(node: _bascenev1.Node)
node: _bascenev1.Node

The bascenev1.Node doing the dropping.

class DualTeamSession(bascenev1.MultiTeamSession):
18class DualTeamSession(MultiTeamSession):
19    """bascenev1.Session type for teams mode games."""
20
21    # Base class overrides:
22    use_teams = True
23    use_team_colors = True
24
25    _playlist_selection_var = 'Team Tournament Playlist Selection'
26    _playlist_randomize_var = 'Team Tournament Playlist Randomize'
27    _playlists_var = 'Team Tournament Playlists'
28
29    def __init__(self) -> None:
30        babase.increment_analytics_count('Teams session start')
31        super().__init__()
32
33    @override
34    def _switch_to_score_screen(self, results: bascenev1.GameResults) -> None:
35        # pylint: disable=cyclic-import
36        from bascenev1lib.activity.multiteamvictory import (
37            TeamSeriesVictoryScoreScreenActivity,
38        )
39        from bascenev1lib.activity.dualteamscore import (
40            TeamVictoryScoreScreenActivity,
41        )
42        from bascenev1lib.activity.drawscore import DrawScoreScreenActivity
43
44        winnergroups = results.winnergroups
45
46        # If everyone has the same score, call it a draw.
47        if len(winnergroups) < 2:
48            self.setactivity(_bascenev1.newactivity(DrawScoreScreenActivity))
49        else:
50            winner = winnergroups[0].teams[0]
51            winner.customdata['score'] += 1
52
53            # If a team has won, show final victory screen.
54            if winner.customdata['score'] >= (self._series_length - 1) / 2 + 1:
55                self.setactivity(
56                    _bascenev1.newactivity(
57                        TeamSeriesVictoryScoreScreenActivity,
58                        {'winner': winner},
59                    )
60                )
61            else:
62                self.setactivity(
63                    _bascenev1.newactivity(
64                        TeamVictoryScoreScreenActivity, {'winner': winner}
65                    )
66                )

bascenev1.Session type for teams mode games.

DualTeamSession()
29    def __init__(self) -> None:
30        babase.increment_analytics_count('Teams session start')
31        super().__init__()

Set up playlists & launch a bascenev1.Activity to accept joiners.

use_teams = True

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

def emitfx( position: Sequence[float], velocity: Optional[Sequence[float]] = None, count: int = 10, scale: float = 1.0, spread: float = 1.0, chunk_type: str = 'rock', emit_type: str = 'chunks', tendril_type: str = 'smoke') -> None:
1082def emitfx(
1083    position: Sequence[float],
1084    velocity: Sequence[float] | None = None,
1085    count: int = 10,
1086    scale: float = 1.0,
1087    spread: float = 1.0,
1088    chunk_type: str = 'rock',
1089    emit_type: str = 'chunks',
1090    tendril_type: str = 'smoke',
1091) -> None:
1092    """Emit particles, smoke, etc. into the fx sim layer.
1093
1094    The fx sim layer is a secondary dynamics simulation that runs in
1095    the background and just looks pretty; it does not affect gameplay.
1096    Note that the actual amount emitted may vary depending on graphics
1097    settings, exiting element counts, or other factors.
1098    """
1099    return None

Emit particles, smoke, etc. into the fx sim layer.

The fx sim layer is a secondary dynamics simulation that runs in the background and just looks pretty; it does not affect gameplay. Note that the actual amount emitted may vary depending on graphics settings, exiting element counts, or other factors.

class EmptyPlayer(bascenev1.Player[ForwardRef('bascenev1.EmptyTeam')]):
275class EmptyPlayer(Player['bascenev1.EmptyTeam']):
276    """An empty player for use by Activities that don't need to define one.
277
278    bascenev1.Player and bascenev1.Team are 'Generic' types, and so passing
279    those top level classes as type arguments when defining a
280    bascenev1.Activity reduces type safety. For example,
281    activity.teams[0].player will have type 'Any' in that case. For that
282    reason, it is better to pass EmptyPlayer and EmptyTeam when defining
283    a bascenev1.Activity that does not need custom types of its own.
284
285    Note that EmptyPlayer defines its team type as EmptyTeam and vice versa,
286    so if you want to define your own class for one of them you should do so
287    for both.
288    """

An empty player for use by Activities that don't need to define one.

bascenev1.Player and bascenev1.Team are 'Generic' types, and so passing those top level classes as type arguments when defining a bascenev1.Activity reduces type safety. For example, activity.teams[0].player will have type 'Any' in that case. For that reason, it is better to pass EmptyPlayer and EmptyTeam when defining a bascenev1.Activity that does not need custom types of its own.

Note that EmptyPlayer defines its team type as EmptyTeam and vice versa, so if you want to define your own class for one of them you should do so for both.

194class EmptyTeam(Team['bascenev1.EmptyPlayer']):
195    """An empty player for use by Activities that don't define one.
196
197    bascenev1.Player and bascenev1.Team are 'Generic' types, and so
198    passing those top level classes as type arguments when defining a
199    bascenev1.Activity reduces type safety. For example,
200    activity.teams[0].player will have type 'Any' in that case. For that
201    reason, it is better to pass EmptyPlayer and EmptyTeam when defining
202    a bascenev1.Activity that does not need custom types of its own.
203
204    Note that EmptyPlayer defines its team type as EmptyTeam and vice
205    versa, so if you want to define your own class for one of them you
206    should do so for both.
207    """

An empty player for use by Activities that don't define one.

bascenev1.Player and bascenev1.Team are 'Generic' types, and so passing those top level classes as type arguments when defining a bascenev1.Activity reduces type safety. For example, activity.teams[0].player will have type 'Any' in that case. For that reason, it is better to pass EmptyPlayer and EmptyTeam when defining a bascenev1.Activity that does not need custom types of its own.

Note that EmptyPlayer defines its team type as EmptyTeam and vice versa, so if you want to define your own class for one of them you should do so for both.

def existing(obj: Optional[~ExistableT]) -> Optional[~ExistableT]:
48def existing(obj: ExistableT | None) -> ExistableT | None:
49    """Convert invalid references to None for any babase.Existable object.
50
51    To best support type checking, it is important that invalid references
52    not be passed around and instead get converted to values of None.
53    That way the type checker can properly flag attempts to pass possibly-dead
54    objects (FooType | None) into functions expecting only live ones
55    (FooType), etc. This call can be used on any 'existable' object
56    (one with an exists() method) and will convert it to a None value
57    if it does not exist.
58
59    For more info, see notes on 'existables' here:
60    https://ballistica.net/wiki/Coding-Style-Guide
61    """
62    assert obj is None or hasattr(obj, 'exists'), f'No "exists" attr on {obj}.'
63    return obj if obj is not None and obj.exists() else None

Convert invalid references to None for any babase.Existable object.

To best support type checking, it is important that invalid references not be passed around and instead get converted to values of None. That way the type checker can properly flag attempts to pass possibly-dead objects (FooType | None) into functions expecting only live ones (FooType), etc. This call can be used on any 'existable' object (one with an exists() method) and will convert it to a None value if it does not exist.

For more info, see notes on 'existables' here: https://ballistica.net/wiki/Coding-Style-Guide

def filter_playlist( playlist: list[dict[str, typing.Any]], sessiontype: type[Session], *, add_resolved_type: bool = False, remove_unowned: bool = True, mark_unowned: bool = False, name: str = '?') -> list[dict[str, typing.Any]]:
 22def filter_playlist(
 23    playlist: PlaylistType,
 24    sessiontype: type[Session],
 25    *,
 26    add_resolved_type: bool = False,
 27    remove_unowned: bool = True,
 28    mark_unowned: bool = False,
 29    name: str = '?',
 30) -> PlaylistType:
 31    """Return a filtered version of a playlist.
 32
 33    Strips out or replaces invalid or unowned game types, makes sure all
 34    settings are present, and adds in a 'resolved_type' which is the actual
 35    type.
 36    """
 37    # pylint: disable=too-many-locals
 38    # pylint: disable=too-many-branches
 39    # pylint: disable=too-many-statements
 40    from bascenev1._map import get_filtered_map_name
 41    from bascenev1._gameactivity import GameActivity
 42
 43    assert babase.app.classic is not None
 44
 45    goodlist: list[dict] = []
 46    unowned_maps: Sequence[str]
 47    available_maps: list[str] = list(babase.app.classic.maps.keys())
 48    if (remove_unowned or mark_unowned) and babase.app.classic is not None:
 49        unowned_maps = babase.app.classic.store.get_unowned_maps()
 50        unowned_game_types = babase.app.classic.store.get_unowned_game_types()
 51    else:
 52        unowned_maps = []
 53        unowned_game_types = set()
 54
 55    for entry in copy.deepcopy(playlist):
 56        # 'map' used to be called 'level' here.
 57        if 'level' in entry:
 58            entry['map'] = entry['level']
 59            del entry['level']
 60
 61        # We now stuff map into settings instead of it being its own thing.
 62        if 'map' in entry:
 63            entry['settings']['map'] = entry['map']
 64            del entry['map']
 65
 66        # Update old map names to new ones.
 67        entry['settings']['map'] = get_filtered_map_name(
 68            entry['settings']['map']
 69        )
 70        if remove_unowned and entry['settings']['map'] in unowned_maps:
 71            continue
 72
 73        # Ok, for each game in our list, try to import the module and grab
 74        # the actual game class. add successful ones to our initial list
 75        # to present to the user.
 76        if not isinstance(entry['type'], str):
 77            raise TypeError('invalid entry format')
 78        try:
 79            # Do some type filters for backwards compat.
 80            if entry['type'] in (
 81                'Assault.AssaultGame',
 82                'Happy_Thoughts.HappyThoughtsGame',
 83                'bsAssault.AssaultGame',
 84                'bs_assault.AssaultGame',
 85                'bastd.game.assault.AssaultGame',
 86            ):
 87                entry['type'] = 'bascenev1lib.game.assault.AssaultGame'
 88            if entry['type'] in (
 89                'King_of_the_Hill.KingOfTheHillGame',
 90                'bsKingOfTheHill.KingOfTheHillGame',
 91                'bs_king_of_the_hill.KingOfTheHillGame',
 92                'bastd.game.kingofthehill.KingOfTheHillGame',
 93            ):
 94                entry['type'] = (
 95                    'bascenev1lib.game.kingofthehill.KingOfTheHillGame'
 96                )
 97            if entry['type'] in (
 98                'Capture_the_Flag.CTFGame',
 99                'bsCaptureTheFlag.CTFGame',
100                'bs_capture_the_flag.CTFGame',
101                'bastd.game.capturetheflag.CaptureTheFlagGame',
102            ):
103                entry['type'] = (
104                    'bascenev1lib.game.capturetheflag.CaptureTheFlagGame'
105                )
106            if entry['type'] in (
107                'Death_Match.DeathMatchGame',
108                'bsDeathMatch.DeathMatchGame',
109                'bs_death_match.DeathMatchGame',
110                'bastd.game.deathmatch.DeathMatchGame',
111            ):
112                entry['type'] = 'bascenev1lib.game.deathmatch.DeathMatchGame'
113            if entry['type'] in (
114                'ChosenOne.ChosenOneGame',
115                'bsChosenOne.ChosenOneGame',
116                'bs_chosen_one.ChosenOneGame',
117                'bastd.game.chosenone.ChosenOneGame',
118            ):
119                entry['type'] = 'bascenev1lib.game.chosenone.ChosenOneGame'
120            if entry['type'] in (
121                'Conquest.Conquest',
122                'Conquest.ConquestGame',
123                'bsConquest.ConquestGame',
124                'bs_conquest.ConquestGame',
125                'bastd.game.conquest.ConquestGame',
126            ):
127                entry['type'] = 'bascenev1lib.game.conquest.ConquestGame'
128            if entry['type'] in (
129                'Elimination.EliminationGame',
130                'bsElimination.EliminationGame',
131                'bs_elimination.EliminationGame',
132                'bastd.game.elimination.EliminationGame',
133            ):
134                entry['type'] = 'bascenev1lib.game.elimination.EliminationGame'
135            if entry['type'] in (
136                'Football.FootballGame',
137                'bsFootball.FootballTeamGame',
138                'bs_football.FootballTeamGame',
139                'bastd.game.football.FootballTeamGame',
140            ):
141                entry['type'] = 'bascenev1lib.game.football.FootballTeamGame'
142            if entry['type'] in (
143                'Hockey.HockeyGame',
144                'bsHockey.HockeyGame',
145                'bs_hockey.HockeyGame',
146                'bastd.game.hockey.HockeyGame',
147            ):
148                entry['type'] = 'bascenev1lib.game.hockey.HockeyGame'
149            if entry['type'] in (
150                'Keep_Away.KeepAwayGame',
151                'bsKeepAway.KeepAwayGame',
152                'bs_keep_away.KeepAwayGame',
153                'bastd.game.keepaway.KeepAwayGame',
154            ):
155                entry['type'] = 'bascenev1lib.game.keepaway.KeepAwayGame'
156            if entry['type'] in (
157                'Race.RaceGame',
158                'bsRace.RaceGame',
159                'bs_race.RaceGame',
160                'bastd.game.race.RaceGame',
161            ):
162                entry['type'] = 'bascenev1lib.game.race.RaceGame'
163            if entry['type'] in (
164                'bsEasterEggHunt.EasterEggHuntGame',
165                'bs_easter_egg_hunt.EasterEggHuntGame',
166                'bastd.game.easteregghunt.EasterEggHuntGame',
167            ):
168                entry['type'] = (
169                    'bascenev1lib.game.easteregghunt.EasterEggHuntGame'
170                )
171            if entry['type'] in (
172                'bsMeteorShower.MeteorShowerGame',
173                'bs_meteor_shower.MeteorShowerGame',
174                'bastd.game.meteorshower.MeteorShowerGame',
175            ):
176                entry['type'] = (
177                    'bascenev1lib.game.meteorshower.MeteorShowerGame'
178                )
179            if entry['type'] in (
180                'bsTargetPractice.TargetPracticeGame',
181                'bs_target_practice.TargetPracticeGame',
182                'bastd.game.targetpractice.TargetPracticeGame',
183            ):
184                entry['type'] = (
185                    'bascenev1lib.game.targetpractice.TargetPracticeGame'
186                )
187
188            gameclass = babase.getclass(entry['type'], GameActivity)
189
190            if entry['settings']['map'] not in available_maps:
191                raise babase.MapNotFoundError()
192
193            if remove_unowned and gameclass in unowned_game_types:
194                continue
195            if add_resolved_type:
196                entry['resolved_type'] = gameclass
197            if mark_unowned and entry['settings']['map'] in unowned_maps:
198                entry['is_unowned_map'] = True
199            if mark_unowned and gameclass in unowned_game_types:
200                entry['is_unowned_game'] = True
201
202            # Make sure all settings the game defines are present.
203            neededsettings = gameclass.get_available_settings(sessiontype)
204            for setting in neededsettings:
205                if setting.name not in entry['settings']:
206                    entry['settings'][setting.name] = setting.default
207
208            goodlist.append(entry)
209
210        except babase.MapNotFoundError:
211            logging.warning(
212                'Map \'%s\' not found while scanning playlist \'%s\'.',
213                entry['settings']['map'],
214                name,
215            )
216        except ImportError as exc:
217            logging.warning(
218                'Import failed while scanning playlist \'%s\': %s', name, exc
219            )
220        except Exception:
221            logging.exception('Error in filter_playlist.')
222
223    return goodlist

Return a filtered version of a playlist.

Strips out or replaces invalid or unowned game types, makes sure all settings are present, and adds in a 'resolved_type' which is the actual type.

@dataclass
class FloatChoiceSetting(bascenev1.ChoiceSetting):
65@dataclass
66class FloatChoiceSetting(ChoiceSetting):
67    """A float setting with multiple choices."""
68
69    default: float
70    choices: list[tuple[str, float]]

A float setting with multiple choices.

FloatChoiceSetting(name: str, default: float, choices: list[tuple[str, float]])
default: float
choices: list[tuple[str, float]]
@dataclass
class FloatSetting(bascenev1.Setting):
40@dataclass
41class FloatSetting(Setting):
42    """A floating point game setting."""
43
44    default: float
45    min_value: float = 0.0
46    max_value: float = 9999.0
47    increment: float = 1.0

A floating point game setting.

FloatSetting( name: str, default: float, min_value: float = 0.0, max_value: float = 9999.0, increment: float = 1.0)
default: float
min_value: float = 0.0
max_value: float = 9999.0
increment: float = 1.0
class FreeForAllSession(bascenev1.MultiTeamSession):
 19class FreeForAllSession(MultiTeamSession):
 20    """bascenev1.Session type for free-for-all mode games."""
 21
 22    use_teams = False
 23    use_team_colors = False
 24    _playlist_selection_var = 'Free-for-All Playlist Selection'
 25    _playlist_randomize_var = 'Free-for-All Playlist Randomize'
 26    _playlists_var = 'Free-for-All Playlists'
 27
 28    def get_ffa_point_awards(self) -> dict[int, int]:
 29        """Return the number of points awarded for different rankings.
 30
 31        This is based on the current number of players.
 32        """
 33        point_awards: dict[int, int]
 34        if len(self.sessionplayers) == 1:
 35            point_awards = {}
 36        elif len(self.sessionplayers) == 2:
 37            point_awards = {0: 6}
 38        elif len(self.sessionplayers) == 3:
 39            point_awards = {0: 6, 1: 3}
 40        elif len(self.sessionplayers) == 4:
 41            point_awards = {0: 8, 1: 4, 2: 2}
 42        elif len(self.sessionplayers) == 5:
 43            point_awards = {0: 8, 1: 4, 2: 2}
 44        elif len(self.sessionplayers) == 6:
 45            point_awards = {0: 8, 1: 4, 2: 2}
 46        else:
 47            point_awards = {0: 8, 1: 4, 2: 2, 3: 1}
 48        return point_awards
 49
 50    def __init__(self) -> None:
 51        babase.increment_analytics_count('Free-for-all session start')
 52        super().__init__()
 53
 54    @override
 55    def _switch_to_score_screen(self, results: bascenev1.GameResults) -> None:
 56        # pylint: disable=cyclic-import
 57        from efro.util import asserttype
 58        from bascenev1lib.activity.multiteamvictory import (
 59            TeamSeriesVictoryScoreScreenActivity,
 60        )
 61        from bascenev1lib.activity.freeforallvictory import (
 62            FreeForAllVictoryScoreScreenActivity,
 63        )
 64        from bascenev1lib.activity.drawscore import DrawScoreScreenActivity
 65
 66        winners = results.winnergroups
 67
 68        # If there's multiple players and everyone has the same score,
 69        # call it a draw.
 70        if len(self.sessionplayers) > 1 and len(winners) < 2:
 71            self.setactivity(
 72                _bascenev1.newactivity(
 73                    DrawScoreScreenActivity, {'results': results}
 74                )
 75            )
 76        else:
 77            # Award different point amounts based on number of players.
 78            point_awards = self.get_ffa_point_awards()
 79
 80            for i, winner in enumerate(winners):
 81                for team in winner.teams:
 82                    points = point_awards[i] if i in point_awards else 0
 83                    team.customdata['previous_score'] = team.customdata['score']
 84                    team.customdata['score'] += points
 85
 86            series_winners = [
 87                team
 88                for team in self.sessionteams
 89                if team.customdata['score'] >= self._ffa_series_length
 90            ]
 91            series_winners.sort(
 92                reverse=True,
 93                key=lambda t: asserttype(t.customdata['score'], int),
 94            )
 95            if len(series_winners) == 1 or (
 96                len(series_winners) > 1
 97                and series_winners[0].customdata['score']
 98                != series_winners[1].customdata['score']
 99            ):
100                self.setactivity(
101                    _bascenev1.newactivity(
102                        TeamSeriesVictoryScoreScreenActivity,
103                        {'winner': series_winners[0]},
104                    )
105                )
106            else:
107                self.setactivity(
108                    _bascenev1.newactivity(
109                        FreeForAllVictoryScoreScreenActivity,
110                        {'results': results},
111                    )
112                )

bascenev1.Session type for free-for-all mode games.

FreeForAllSession()
50    def __init__(self) -> None:
51        babase.increment_analytics_count('Free-for-all session start')
52        super().__init__()

Set up playlists & launch a bascenev1.Activity to accept joiners.

use_teams = 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 = False

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.

def get_ffa_point_awards(self) -> dict[int, int]:
28    def get_ffa_point_awards(self) -> dict[int, int]:
29        """Return the number of points awarded for different rankings.
30
31        This is based on the current number of players.
32        """
33        point_awards: dict[int, int]
34        if len(self.sessionplayers) == 1:
35            point_awards = {}
36        elif len(self.sessionplayers) == 2:
37            point_awards = {0: 6}
38        elif len(self.sessionplayers) == 3:
39            point_awards = {0: 6, 1: 3}
40        elif len(self.sessionplayers) == 4:
41            point_awards = {0: 8, 1: 4, 2: 2}
42        elif len(self.sessionplayers) == 5:
43            point_awards = {0: 8, 1: 4, 2: 2}
44        elif len(self.sessionplayers) == 6:
45            point_awards = {0: 8, 1: 4, 2: 2}
46        else:
47            point_awards = {0: 8, 1: 4, 2: 2, 3: 1}
48        return point_awards

Return the number of points awarded for different rankings.

This is based on the current number of players.

@dataclass
class FreezeMessage:
174@dataclass
175class FreezeMessage:
176    """Tells an object to become frozen.
177
178    As seen in the effects of an ice bascenev1.Bomb.
179    """
180
181    time: float = 5.0
182    """The amount of time the object will be frozen."""

Tells an object to become frozen.

As seen in the effects of an ice bascenev1.Bomb.

FreezeMessage(time: float = 5.0)
time: float = 5.0

The amount of time the object will be frozen.

class GameActivity(bascenev1.Activity[~PlayerT, ~TeamT]):
  34class GameActivity(Activity[PlayerT, TeamT]):
  35    """Common base class for all game bascenev1.Activities."""
  36
  37    # pylint: disable=too-many-public-methods
  38
  39    # Tips to be presented to the user at the start of the game.
  40    tips: list[str | bascenev1.GameTip] = []
  41
  42    # Default getname() will return this if not None.
  43    name: str | None = None
  44
  45    # Default get_description() will return this if not None.
  46    description: str | None = None
  47
  48    # Default get_available_settings() will return this if not None.
  49    available_settings: list[bascenev1.Setting] | None = None
  50
  51    # Default getscoreconfig() will return this if not None.
  52    scoreconfig: bascenev1.ScoreConfig | None = None
  53
  54    # Override some defaults.
  55    allow_pausing = True
  56    allow_kick_idle_players = True
  57
  58    # Whether to show points for kills.
  59    show_kill_points = True
  60
  61    # If not None, the music type that should play in on_transition_in()
  62    # (unless overridden by the map).
  63    default_music: bascenev1.MusicType | None = None
  64
  65    @classmethod
  66    def getscoreconfig(cls) -> bascenev1.ScoreConfig:
  67        """Return info about game scoring setup; can be overridden by games."""
  68        return cls.scoreconfig if cls.scoreconfig is not None else ScoreConfig()
  69
  70    @classmethod
  71    def getname(cls) -> str:
  72        """Return a str name for this game type.
  73
  74        This default implementation simply returns the 'name' class attr.
  75        """
  76        return cls.name if cls.name is not None else 'Untitled Game'
  77
  78    @classmethod
  79    def get_display_string(cls, settings: dict | None = None) -> babase.Lstr:
  80        """Return a descriptive name for this game/settings combo.
  81
  82        Subclasses should override getname(); not this.
  83        """
  84        name = babase.Lstr(translate=('gameNames', cls.getname()))
  85
  86        # A few substitutions for 'Epic', 'Solo' etc. modes.
  87        # FIXME: Should provide a way for game types to define filters of
  88        #  their own and should not rely on hard-coded settings names.
  89        if settings is not None:
  90            if 'Solo Mode' in settings and settings['Solo Mode']:
  91                name = babase.Lstr(
  92                    resource='soloNameFilterText', subs=[('${NAME}', name)]
  93                )
  94            if 'Epic Mode' in settings and settings['Epic Mode']:
  95                name = babase.Lstr(
  96                    resource='epicNameFilterText', subs=[('${NAME}', name)]
  97                )
  98
  99        return name
 100
 101    @classmethod
 102    def get_team_display_string(cls, name: str) -> babase.Lstr:
 103        """Given a team name, returns a localized version of it."""
 104        return babase.Lstr(translate=('teamNames', name))
 105
 106    @classmethod
 107    def get_description(cls, sessiontype: type[bascenev1.Session]) -> str:
 108        """Get a str description of this game type.
 109
 110        The default implementation simply returns the 'description' class var.
 111        Classes which want to change their description depending on the session
 112        can override this method.
 113        """
 114        del sessiontype  # Unused arg.
 115        return cls.description if cls.description is not None else ''
 116
 117    @classmethod
 118    def get_description_display_string(
 119        cls, sessiontype: type[bascenev1.Session]
 120    ) -> babase.Lstr:
 121        """Return a translated version of get_description().
 122
 123        Sub-classes should override get_description(); not this.
 124        """
 125        description = cls.get_description(sessiontype)
 126        return babase.Lstr(translate=('gameDescriptions', description))
 127
 128    @classmethod
 129    def get_available_settings(
 130        cls, sessiontype: type[bascenev1.Session]
 131    ) -> list[bascenev1.Setting]:
 132        """Return a list of settings relevant to this game type when
 133        running under the provided session type.
 134        """
 135        del sessiontype  # Unused arg.
 136        return [] if cls.available_settings is None else cls.available_settings
 137
 138    @classmethod
 139    def get_supported_maps(
 140        cls, sessiontype: type[bascenev1.Session]
 141    ) -> list[str]:
 142        """
 143        Called by the default bascenev1.GameActivity.create_settings_ui()
 144        implementation; should return a list of map names valid
 145        for this game-type for the given bascenev1.Session type.
 146        """
 147        del sessiontype  # Unused arg.
 148        assert babase.app.classic is not None
 149        return babase.app.classic.getmaps('melee')
 150
 151    @classmethod
 152    def get_settings_display_string(cls, config: dict[str, Any]) -> babase.Lstr:
 153        """Given a game config dict, return a short description for it.
 154
 155        This is used when viewing game-lists or showing what game
 156        is up next in a series.
 157        """
 158        name = cls.get_display_string(config['settings'])
 159
 160        # In newer configs, map is in settings; it used to be in the
 161        # config root.
 162        if 'map' in config['settings']:
 163            sval = babase.Lstr(
 164                value='${NAME} @ ${MAP}',
 165                subs=[
 166                    ('${NAME}', name),
 167                    (
 168                        '${MAP}',
 169                        _map.get_map_display_string(
 170                            _map.get_filtered_map_name(
 171                                config['settings']['map']
 172                            )
 173                        ),
 174                    ),
 175                ],
 176            )
 177        elif 'map' in config:
 178            sval = babase.Lstr(
 179                value='${NAME} @ ${MAP}',
 180                subs=[
 181                    ('${NAME}', name),
 182                    (
 183                        '${MAP}',
 184                        _map.get_map_display_string(
 185                            _map.get_filtered_map_name(config['map'])
 186                        ),
 187                    ),
 188                ],
 189            )
 190        else:
 191            print('invalid game config - expected map entry under settings')
 192            sval = babase.Lstr(value='???')
 193        return sval
 194
 195    @classmethod
 196    def supports_session_type(
 197        cls, sessiontype: type[bascenev1.Session]
 198    ) -> bool:
 199        """Return whether this game supports the provided Session type."""
 200        from bascenev1._multiteamsession import MultiTeamSession
 201
 202        # By default, games support any versus mode
 203        return issubclass(sessiontype, MultiTeamSession)
 204
 205    def __init__(self, settings: dict):
 206        """Instantiate the Activity."""
 207        super().__init__(settings)
 208
 209        # Holds some flattened info about the player set at the point
 210        # when on_begin() is called.
 211        self.initialplayerinfos: list[bascenev1.PlayerInfo] | None = None
 212
 213        # Go ahead and get our map loading.
 214        self._map_type = _map.get_map_class(self._calc_map_name(settings))
 215
 216        self._spawn_sound = _bascenev1.getsound('spawn')
 217        self._map_type.preload()
 218        self._map: bascenev1.Map | None = None
 219        self._powerup_drop_timer: bascenev1.Timer | None = None
 220        self._tnt_spawners: dict[int, TNTSpawner] | None = None
 221        self._tnt_drop_timer: bascenev1.Timer | None = None
 222        self._game_scoreboard_name_text: bascenev1.Actor | None = None
 223        self._game_scoreboard_description_text: bascenev1.Actor | None = None
 224        self._standard_time_limit_time: int | None = None
 225        self._standard_time_limit_timer: bascenev1.Timer | None = None
 226        self._standard_time_limit_text: bascenev1.NodeActor | None = None
 227        self._standard_time_limit_text_input: bascenev1.NodeActor | None = None
 228        self._tournament_time_limit: int | None = None
 229        self._tournament_time_limit_timer: bascenev1.BaseTimer | None = None
 230        self._tournament_time_limit_title_text: bascenev1.NodeActor | None = (
 231            None
 232        )
 233        self._tournament_time_limit_text: bascenev1.NodeActor | None = None
 234        self._tournament_time_limit_text_input: bascenev1.NodeActor | None = (
 235            None
 236        )
 237        self._zoom_message_times: dict[int, float] = {}
 238
 239    @property
 240    def map(self) -> _map.Map:
 241        """The map being used for this game.
 242
 243        Raises a bascenev1.MapNotFoundError if the map does not currently
 244        exist.
 245        """
 246        if self._map is None:
 247            raise babase.MapNotFoundError
 248        return self._map
 249
 250    def get_instance_display_string(self) -> babase.Lstr:
 251        """Return a name for this particular game instance."""
 252        return self.get_display_string(self.settings_raw)
 253
 254    # noinspection PyUnresolvedReferences
 255    def get_instance_scoreboard_display_string(self) -> babase.Lstr:
 256        """Return a name for this particular game instance.
 257
 258        This name is used above the game scoreboard in the corner
 259        of the screen, so it should be as concise as possible.
 260        """
 261        # If we're in a co-op session, use the level name.
 262        # FIXME: Should clean this up.
 263        try:
 264            from bascenev1._coopsession import CoopSession
 265
 266            if isinstance(self.session, CoopSession):
 267                campaign = self.session.campaign
 268                assert campaign is not None
 269                return campaign.getlevel(
 270                    self.session.campaign_level_name
 271                ).displayname
 272        except Exception:
 273            logging.exception('Error getting campaign level name.')
 274        return self.get_instance_display_string()
 275
 276    def get_instance_description(self) -> str | Sequence:
 277        """Return a description for this game instance, in English.
 278
 279        This is shown in the center of the screen below the game name at the
 280        start of a game. It should start with a capital letter and end with a
 281        period, and can be a bit more verbose than the version returned by
 282        get_instance_description_short().
 283
 284        Note that translation is applied by looking up the specific returned
 285        value as a key, so the number of returned variations should be limited;
 286        ideally just one or two. To include arbitrary values in the
 287        description, you can return a sequence of values in the following
 288        form instead of just a string:
 289
 290        # This will give us something like 'Score 3 goals.' in English
 291        # and can properly translate to 'Anota 3 goles.' in Spanish.
 292        # If we just returned the string 'Score 3 Goals' here, there would
 293        # have to be a translation entry for each specific number. ew.
 294        return ['Score ${ARG1} goals.', self.settings_raw['Score to Win']]
 295
 296        This way the first string can be consistently translated, with any arg
 297        values then substituted into the result. ${ARG1} will be replaced with
 298        the first value, ${ARG2} with the second, etc.
 299        """
 300        return self.get_description(type(self.session))
 301
 302    def get_instance_description_short(self) -> str | Sequence:
 303        """Return a short description for this game instance in English.
 304
 305        This description is used above the game scoreboard in the
 306        corner of the screen, so it should be as concise as possible.
 307        It should be lowercase and should not contain periods or other
 308        punctuation.
 309
 310        Note that translation is applied by looking up the specific returned
 311        value as a key, so the number of returned variations should be limited;
 312        ideally just one or two. To include arbitrary values in the
 313        description, you can return a sequence of values in the following form
 314        instead of just a string:
 315
 316        # This will give us something like 'score 3 goals' in English
 317        # and can properly translate to 'anota 3 goles' in Spanish.
 318        # If we just returned the string 'score 3 goals' here, there would
 319        # have to be a translation entry for each specific number. ew.
 320        return ['score ${ARG1} goals', self.settings_raw['Score to Win']]
 321
 322        This way the first string can be consistently translated, with any arg
 323        values then substituted into the result. ${ARG1} will be replaced
 324        with the first value, ${ARG2} with the second, etc.
 325
 326        """
 327        return ''
 328
 329    @override
 330    def on_transition_in(self) -> None:
 331        super().on_transition_in()
 332
 333        # Make our map.
 334        self._map = self._map_type()
 335
 336        # Give our map a chance to override the music.
 337        # (for happy-thoughts and other such themed maps)
 338        map_music = self._map_type.get_music_type()
 339        music = map_music if map_music is not None else self.default_music
 340
 341        if music is not None:
 342            _music.setmusic(music)
 343
 344    @override
 345    def on_begin(self) -> None:
 346        super().on_begin()
 347
 348        if babase.app.classic is not None:
 349            babase.app.classic.game_begin_analytics()
 350
 351        # We don't do this in on_transition_in because it may depend on
 352        # players/teams which aren't available until now.
 353        _bascenev1.timer(0.001, self._show_scoreboard_info)
 354        _bascenev1.timer(1.0, self._show_info)
 355        _bascenev1.timer(2.5, self._show_tip)
 356
 357        # Store some basic info about players present at start time.
 358        self.initialplayerinfos = [
 359            PlayerInfo(name=p.getname(full=True), character=p.character)
 360            for p in self.players
 361        ]
 362
 363        # Sort this by name so high score lists/etc will be consistent
 364        # regardless of player join order.
 365        self.initialplayerinfos.sort(key=lambda x: x.name)
 366
 367        # If this is a tournament, query info about it such as how much
 368        # time is left.
 369        tournament_id = self.session.tournament_id
 370        if tournament_id is not None:
 371            assert babase.app.plus is not None
 372            babase.app.plus.tournament_query(
 373                args={
 374                    'tournamentIDs': [tournament_id],
 375                    'source': 'in-game time remaining query',
 376                },
 377                callback=babase.WeakCall(self._on_tournament_query_response),
 378            )
 379
 380    def _on_tournament_query_response(
 381        self, data: dict[str, Any] | None
 382    ) -> None:
 383        if data is not None:
 384            data_t = data['t']  # This used to be the whole payload.
 385
 386            # Keep our cached tourney info up to date
 387            assert babase.app.classic is not None
 388            babase.app.classic.accounts.cache_tournament_info(data_t)
 389            self._setup_tournament_time_limit(
 390                max(5, data_t[0]['timeRemaining'])
 391            )
 392
 393    @override
 394    def on_player_join(self, player: PlayerT) -> None:
 395        super().on_player_join(player)
 396
 397        # By default, just spawn a dude.
 398        self.spawn_player(player)
 399
 400    @override
 401    def handlemessage(self, msg: Any) -> Any:
 402        if isinstance(msg, PlayerDiedMessage):
 403            # pylint: disable=cyclic-import
 404            from bascenev1lib.actor.spaz import Spaz
 405
 406            player = msg.getplayer(self.playertype)
 407            killer = msg.getkillerplayer(self.playertype)
 408
 409            # Inform our stats of the demise.
 410            self.stats.player_was_killed(
 411                player, killed=msg.killed, killer=killer
 412            )
 413
 414            # Award the killer points if he's on a different team.
 415            # FIXME: This should not be linked to Spaz actors.
 416            # (should move get_death_points to Actor or make it a message)
 417            if killer and killer.team is not player.team:
 418                assert isinstance(killer.actor, Spaz)
 419                pts, importance = killer.actor.get_death_points(msg.how)
 420                if not self.has_ended():
 421                    self.stats.player_scored(
 422                        killer,
 423                        pts,
 424                        kill=True,
 425                        victim_player=player,
 426                        importance=importance,
 427                        showpoints=self.show_kill_points,
 428                    )
 429        else:
 430            return super().handlemessage(msg)
 431        return None
 432
 433    def _show_scoreboard_info(self) -> None:
 434        """Create the game info display.
 435
 436        This is the thing in the top left corner showing the name
 437        and short description of the game.
 438        """
 439        # pylint: disable=too-many-locals
 440        from bascenev1._freeforallsession import FreeForAllSession
 441        from bascenev1._gameutils import animate
 442        from bascenev1._nodeactor import NodeActor
 443
 444        sb_name = self.get_instance_scoreboard_display_string()
 445
 446        # The description can be either a string or a sequence with args
 447        # to swap in post-translation.
 448        sb_desc_in = self.get_instance_description_short()
 449        sb_desc_l: Sequence
 450        if isinstance(sb_desc_in, str):
 451            sb_desc_l = [sb_desc_in]  # handle simple string case
 452        else:
 453            sb_desc_l = sb_desc_in
 454        if not isinstance(sb_desc_l[0], str):
 455            raise TypeError('Invalid format for instance description.')
 456
 457        is_empty = sb_desc_l[0] == ''
 458        subs = []
 459        for i in range(len(sb_desc_l) - 1):
 460            subs.append(('${ARG' + str(i + 1) + '}', str(sb_desc_l[i + 1])))
 461        translation = babase.Lstr(
 462            translate=('gameDescriptions', sb_desc_l[0]), subs=subs
 463        )
 464        sb_desc = translation
 465        vrmode = babase.app.env.vr
 466        yval = -34 if is_empty else -20
 467        yval -= 16
 468        sbpos = (
 469            (15, yval)
 470            if isinstance(self.session, FreeForAllSession)
 471            else (15, yval)
 472        )
 473        self._game_scoreboard_name_text = NodeActor(
 474            _bascenev1.newnode(
 475                'text',
 476                attrs={
 477                    'text': sb_name,
 478                    'maxwidth': 300,
 479                    'position': sbpos,
 480                    'h_attach': 'left',
 481                    'vr_depth': 10,
 482                    'v_attach': 'top',
 483                    'v_align': 'bottom',
 484                    'color': (1.0, 1.0, 1.0, 1.0),
 485                    'shadow': 1.0 if vrmode else 0.6,
 486                    'flatness': 1.0 if vrmode else 0.5,
 487                    'scale': 1.1,
 488                },
 489            )
 490        )
 491
 492        assert self._game_scoreboard_name_text.node
 493        animate(
 494            self._game_scoreboard_name_text.node, 'opacity', {0: 0.0, 1.0: 1.0}
 495        )
 496
 497        descpos = (
 498            (17, -44 + 10)
 499            if isinstance(self.session, FreeForAllSession)
 500            else (17, -44 + 10)
 501        )
 502        self._game_scoreboard_description_text = NodeActor(
 503            _bascenev1.newnode(
 504                'text',
 505                attrs={
 506                    'text': sb_desc,
 507                    'maxwidth': 480,
 508                    'position': descpos,
 509                    'scale': 0.7,
 510                    'h_attach': 'left',
 511                    'v_attach': 'top',
 512                    'v_align': 'top',
 513                    'shadow': 1.0 if vrmode else 0.7,
 514                    'flatness': 1.0 if vrmode else 0.8,
 515                    'color': (1, 1, 1, 1) if vrmode else (0.9, 0.9, 0.9, 1.0),
 516                },
 517            )
 518        )
 519
 520        assert self._game_scoreboard_description_text.node
 521        animate(
 522            self._game_scoreboard_description_text.node,
 523            'opacity',
 524            {0: 0.0, 1.0: 1.0},
 525        )
 526
 527    def _show_info(self) -> None:
 528        """Show the game description."""
 529        from bascenev1._gameutils import animate
 530        from bascenev1lib.actor.zoomtext import ZoomText
 531
 532        name = self.get_instance_display_string()
 533        ZoomText(
 534            name,
 535            maxwidth=800,
 536            lifespan=2.5,
 537            jitter=2.0,
 538            position=(0, 180),
 539            flash=False,
 540            color=(0.93 * 1.25, 0.9 * 1.25, 1.0 * 1.25),
 541            trailcolor=(0.15, 0.05, 1.0, 0.0),
 542        ).autoretain()
 543        _bascenev1.timer(0.2, _bascenev1.getsound('gong').play)
 544        # _bascenev1.timer(
 545        #     0.2, Call(_bascenev1.playsound, _bascenev1.getsound('gong'))
 546        # )
 547
 548        # The description can be either a string or a sequence with args
 549        # to swap in post-translation.
 550        desc_in = self.get_instance_description()
 551        desc_l: Sequence
 552        if isinstance(desc_in, str):
 553            desc_l = [desc_in]  # handle simple string case
 554        else:
 555            desc_l = desc_in
 556        if not isinstance(desc_l[0], str):
 557            raise TypeError('Invalid format for instance description')
 558        subs = []
 559        for i in range(len(desc_l) - 1):
 560            subs.append(('${ARG' + str(i + 1) + '}', str(desc_l[i + 1])))
 561        translation = babase.Lstr(
 562            translate=('gameDescriptions', desc_l[0]), subs=subs
 563        )
 564
 565        # Do some standard filters (epic mode, etc).
 566        if self.settings_raw.get('Epic Mode', False):
 567            translation = babase.Lstr(
 568                resource='epicDescriptionFilterText',
 569                subs=[('${DESCRIPTION}', translation)],
 570            )
 571        vrmode = babase.app.env.vr
 572        dnode = _bascenev1.newnode(
 573            'text',
 574            attrs={
 575                'v_attach': 'center',
 576                'h_attach': 'center',
 577                'h_align': 'center',
 578                'color': (1, 1, 1, 1),
 579                'shadow': 1.0 if vrmode else 0.5,
 580                'flatness': 1.0 if vrmode else 0.5,
 581                'vr_depth': -30,
 582                'position': (0, 80),
 583                'scale': 1.2,
 584                'maxwidth': 700,
 585                'text': translation,
 586            },
 587        )
 588        cnode = _bascenev1.newnode(
 589            'combine',
 590            owner=dnode,
 591            attrs={'input0': 1.0, 'input1': 1.0, 'input2': 1.0, 'size': 4},
 592        )
 593        cnode.connectattr('output', dnode, 'color')
 594        keys = {0.5: 0, 1.0: 1.0, 2.5: 1.0, 4.0: 0.0}
 595        animate(cnode, 'input3', keys)
 596        _bascenev1.timer(4.0, dnode.delete)
 597
 598    def _show_tip(self) -> None:
 599        # pylint: disable=too-many-locals
 600        from bascenev1._gameutils import animate, GameTip
 601
 602        # If there's any tips left on the list, display one.
 603        if self.tips:
 604            tip = self.tips.pop(random.randrange(len(self.tips)))
 605            tip_title = babase.Lstr(
 606                value='${A}:', subs=[('${A}', babase.Lstr(resource='tipText'))]
 607            )
 608            icon: bascenev1.Texture | None = None
 609            sound: bascenev1.Sound | None = None
 610            if isinstance(tip, GameTip):
 611                icon = tip.icon
 612                sound = tip.sound
 613                tip = tip.text
 614                assert isinstance(tip, str)
 615
 616            # Do a few substitutions.
 617            tip_lstr = babase.Lstr(
 618                translate=('tips', tip),
 619                subs=[
 620                    ('${PICKUP}', babase.charstr(babase.SpecialChar.TOP_BUTTON))
 621                ],
 622            )
 623            base_position = (75, 50)
 624            tip_scale = 0.8
 625            tip_title_scale = 1.2
 626            vrmode = babase.app.env.vr
 627
 628            t_offs = -350.0
 629            tnode = _bascenev1.newnode(
 630                'text',
 631                attrs={
 632                    'text': tip_lstr,
 633                    'scale': tip_scale,
 634                    'maxwidth': 900,
 635                    'position': (base_position[0] + t_offs, base_position[1]),
 636                    'h_align': 'left',
 637                    'vr_depth': 300,
 638                    'shadow': 1.0 if vrmode else 0.5,
 639                    'flatness': 1.0 if vrmode else 0.5,
 640                    'v_align': 'center',
 641                    'v_attach': 'bottom',
 642                },
 643            )
 644            t2pos = (
 645                base_position[0] + t_offs - (20 if icon is None else 82),
 646                base_position[1] + 2,
 647            )
 648            t2node = _bascenev1.newnode(
 649                'text',
 650                owner=tnode,
 651                attrs={
 652                    'text': tip_title,
 653                    'scale': tip_title_scale,
 654                    'position': t2pos,
 655                    'h_align': 'right',
 656                    'vr_depth': 300,
 657                    'shadow': 1.0 if vrmode else 0.5,
 658                    'flatness': 1.0 if vrmode else 0.5,
 659                    'maxwidth': 140,
 660                    'v_align': 'center',
 661                    'v_attach': 'bottom',
 662                },
 663            )
 664            if icon is not None:
 665                ipos = (base_position[0] + t_offs - 40, base_position[1] + 1)
 666                img = _bascenev1.newnode(
 667                    'image',
 668                    attrs={
 669                        'texture': icon,
 670                        'position': ipos,
 671                        'scale': (50, 50),
 672                        'opacity': 1.0,
 673                        'vr_depth': 315,
 674                        'color': (1, 1, 1),
 675                        'absolute_scale': True,
 676                        'attach': 'bottomCenter',
 677                    },
 678                )
 679                animate(img, 'opacity', {0: 0, 1.0: 1, 4.0: 1, 5.0: 0})
 680                _bascenev1.timer(5.0, img.delete)
 681            if sound is not None:
 682                sound.play()
 683
 684            combine = _bascenev1.newnode(
 685                'combine',
 686                owner=tnode,
 687                attrs={'input0': 1.0, 'input1': 0.8, 'input2': 1.0, 'size': 4},
 688            )
 689            combine.connectattr('output', tnode, 'color')
 690            combine.connectattr('output', t2node, 'color')
 691            animate(combine, 'input3', {0: 0, 1.0: 1, 4.0: 1, 5.0: 0})
 692            _bascenev1.timer(5.0, tnode.delete)
 693
 694    @override
 695    def end(
 696        self, results: Any = None, delay: float = 0.0, force: bool = False
 697    ) -> None:
 698        from bascenev1._gameresults import GameResults
 699
 700        # If results is a standard team-game-results, associate it with us
 701        # so it can grab our score prefs.
 702        if isinstance(results, GameResults):
 703            results.set_game(self)
 704
 705        # If we had a standard time-limit that had not expired, stop it so
 706        # it doesnt tick annoyingly.
 707        if (
 708            self._standard_time_limit_time is not None
 709            and self._standard_time_limit_time > 0
 710        ):
 711            self._standard_time_limit_timer = None
 712            self._standard_time_limit_text = None
 713
 714        # Ditto with tournament time limits.
 715        if (
 716            self._tournament_time_limit is not None
 717            and self._tournament_time_limit > 0
 718        ):
 719            self._tournament_time_limit_timer = None
 720            self._tournament_time_limit_text = None
 721            self._tournament_time_limit_title_text = None
 722
 723        super().end(results, delay, force)
 724
 725    def end_game(self) -> None:
 726        """Tell the game to wrap up and call bascenev1.Activity.end().
 727
 728        This method should be overridden by subclasses. A game should always
 729        be prepared to end and deliver results, even if there is no 'winner'
 730        yet; this way things like the standard time-limit
 731        (bascenev1.GameActivity.setup_standard_time_limit()) will work with
 732        the game.
 733        """
 734        print(
 735            'WARNING: default end_game() implementation called;'
 736            ' your game should override this.'
 737        )
 738
 739    def respawn_player(
 740        self, player: PlayerT, respawn_time: float | None = None
 741    ) -> None:
 742        """
 743        Given a bascenev1.Player, sets up a standard respawn timer,
 744        along with the standard counter display, etc.
 745        At the end of the respawn period spawn_player() will
 746        be called if the Player still exists.
 747        An explicit 'respawn_time' can optionally be provided
 748        (in seconds).
 749        """
 750        # pylint: disable=cyclic-import
 751
 752        assert player
 753        if respawn_time is None:
 754            teamsize = len(player.team.players)
 755            if teamsize == 1:
 756                respawn_time = 3.0
 757            elif teamsize == 2:
 758                respawn_time = 5.0
 759            elif teamsize == 3:
 760                respawn_time = 6.0
 761            else:
 762                respawn_time = 7.0
 763
 764        # If this standard setting is present, factor it in.
 765        if 'Respawn Times' in self.settings_raw:
 766            respawn_time *= self.settings_raw['Respawn Times']
 767
 768        # We want whole seconds.
 769        assert respawn_time is not None
 770        respawn_time = round(max(1.0, respawn_time), 0)
 771
 772        if player.actor and not self.has_ended():
 773            from bascenev1lib.actor.respawnicon import RespawnIcon
 774
 775            player.customdata['respawn_timer'] = _bascenev1.Timer(
 776                respawn_time,
 777                babase.WeakCall(self.spawn_player_if_exists, player),
 778            )
 779            player.customdata['respawn_icon'] = RespawnIcon(
 780                player, respawn_time
 781            )
 782
 783    def spawn_player_if_exists(self, player: PlayerT) -> None:
 784        """
 785        A utility method which calls self.spawn_player() *only* if the
 786        bascenev1.Player provided still exists; handy for use in timers
 787        and whatnot.
 788
 789        There is no need to override this; just override spawn_player().
 790        """
 791        if player:
 792            self.spawn_player(player)
 793
 794    def spawn_player(self, player: PlayerT) -> bascenev1.Actor:
 795        """Spawn *something* for the provided bascenev1.Player.
 796
 797        The default implementation simply calls spawn_player_spaz().
 798        """
 799        assert player  # Dead references should never be passed as args.
 800
 801        return self.spawn_player_spaz(player)
 802
 803    def spawn_player_spaz(
 804        self,
 805        player: PlayerT,
 806        position: Sequence[float] = (0, 0, 0),
 807        angle: float | None = None,
 808    ) -> PlayerSpaz:
 809        """Create and wire up a bascenev1.PlayerSpaz for the provided Player."""
 810        # pylint: disable=too-many-locals
 811        # pylint: disable=cyclic-import
 812        from bascenev1._gameutils import animate
 813        from bascenev1._coopsession import CoopSession
 814        from bascenev1lib.actor.playerspaz import PlayerSpaz
 815
 816        name = player.getname()
 817        color = player.color
 818        highlight = player.highlight
 819
 820        playerspaztype = getattr(player, 'playerspaztype', PlayerSpaz)
 821        if not issubclass(playerspaztype, PlayerSpaz):
 822            playerspaztype = PlayerSpaz
 823
 824        light_color = babase.normalized_color(color)
 825        display_color = babase.safecolor(color, target_intensity=0.75)
 826        spaz = playerspaztype(
 827            color=color,
 828            highlight=highlight,
 829            character=player.character,
 830            player=player,
 831        )
 832
 833        player.actor = spaz
 834        assert spaz.node
 835
 836        # If this is co-op and we're on Courtyard or Runaround, add the
 837        # material that allows us to collide with the player-walls.
 838        # FIXME: Need to generalize this.
 839        if isinstance(self.session, CoopSession) and self.map.getname() in [
 840            'Courtyard',
 841            'Tower D',
 842        ]:
 843            mat = self.map.preloaddata['collide_with_wall_material']
 844            assert isinstance(spaz.node.materials, tuple)
 845            assert isinstance(spaz.node.roller_materials, tuple)
 846            spaz.node.materials += (mat,)
 847            spaz.node.roller_materials += (mat,)
 848
 849        spaz.node.name = name
 850        spaz.node.name_color = display_color
 851        spaz.connect_controls_to_player()
 852
 853        # Move to the stand position and add a flash of light.
 854        spaz.handlemessage(
 855            StandMessage(
 856                position, angle if angle is not None else random.uniform(0, 360)
 857            )
 858        )
 859        self._spawn_sound.play(1, position=spaz.node.position)
 860        light = _bascenev1.newnode('light', attrs={'color': light_color})
 861        spaz.node.connectattr('position', light, 'position')
 862        animate(light, 'intensity', {0: 0, 0.25: 1, 0.5: 0})
 863        _bascenev1.timer(0.5, light.delete)
 864        return spaz
 865
 866    def setup_standard_powerup_drops(self, enable_tnt: bool = True) -> None:
 867        """Create standard powerup drops for the current map."""
 868        # pylint: disable=cyclic-import
 869        from bascenev1lib.actor.powerupbox import DEFAULT_POWERUP_INTERVAL
 870
 871        self._powerup_drop_timer = _bascenev1.Timer(
 872            DEFAULT_POWERUP_INTERVAL,
 873            babase.WeakCall(self._standard_drop_powerups),
 874            repeat=True,
 875        )
 876        self._standard_drop_powerups()
 877        if enable_tnt:
 878            self._tnt_spawners = {}
 879            self._setup_standard_tnt_drops()
 880
 881    def _standard_drop_powerup(self, index: int, expire: bool = True) -> None:
 882        # pylint: disable=cyclic-import
 883        from bascenev1lib.actor.powerupbox import PowerupBox, PowerupBoxFactory
 884
 885        PowerupBox(
 886            position=self.map.powerup_spawn_points[index],
 887            poweruptype=PowerupBoxFactory.get().get_random_powerup_type(),
 888            expire=expire,
 889        ).autoretain()
 890
 891    def _standard_drop_powerups(self) -> None:
 892        """Standard powerup drop."""
 893
 894        # Drop one powerup per point.
 895        points = self.map.powerup_spawn_points
 896        for i in range(len(points)):
 897            _bascenev1.timer(
 898                i * 0.4, babase.WeakCall(self._standard_drop_powerup, i)
 899            )
 900
 901    def _setup_standard_tnt_drops(self) -> None:
 902        """Standard tnt drop."""
 903        # pylint: disable=cyclic-import
 904        from bascenev1lib.actor.bomb import TNTSpawner
 905
 906        for i, point in enumerate(self.map.tnt_points):
 907            assert self._tnt_spawners is not None
 908            if self._tnt_spawners.get(i) is None:
 909                self._tnt_spawners[i] = TNTSpawner(point)
 910
 911    def setup_standard_time_limit(self, duration: float) -> None:
 912        """
 913        Create a standard game time-limit given the provided
 914        duration in seconds.
 915        This will be displayed at the top of the screen.
 916        If the time-limit expires, end_game() will be called.
 917        """
 918        from bascenev1._nodeactor import NodeActor
 919
 920        if duration <= 0.0:
 921            return
 922        self._standard_time_limit_time = int(duration)
 923        self._standard_time_limit_timer = _bascenev1.Timer(
 924            1.0, babase.WeakCall(self._standard_time_limit_tick), repeat=True
 925        )
 926        self._standard_time_limit_text = NodeActor(
 927            _bascenev1.newnode(
 928                'text',
 929                attrs={
 930                    'v_attach': 'top',
 931                    'h_attach': 'center',
 932                    'h_align': 'left',
 933                    'color': (1.0, 1.0, 1.0, 0.5),
 934                    'position': (-25, -30),
 935                    'flatness': 1.0,
 936                    'scale': 0.9,
 937                },
 938            )
 939        )
 940        self._standard_time_limit_text_input = NodeActor(
 941            _bascenev1.newnode(
 942                'timedisplay', attrs={'time2': duration * 1000, 'timemin': 0}
 943            )
 944        )
 945        self.globalsnode.connectattr(
 946            'time', self._standard_time_limit_text_input.node, 'time1'
 947        )
 948        assert self._standard_time_limit_text_input.node
 949        assert self._standard_time_limit_text.node
 950        self._standard_time_limit_text_input.node.connectattr(
 951            'output', self._standard_time_limit_text.node, 'text'
 952        )
 953
 954    def _standard_time_limit_tick(self) -> None:
 955        from bascenev1._gameutils import animate
 956
 957        assert self._standard_time_limit_time is not None
 958        self._standard_time_limit_time -= 1
 959        if self._standard_time_limit_time <= 10:
 960            if self._standard_time_limit_time == 10:
 961                assert self._standard_time_limit_text is not None
 962                assert self._standard_time_limit_text.node
 963                self._standard_time_limit_text.node.scale = 1.3
 964                self._standard_time_limit_text.node.position = (-30, -45)
 965                cnode = _bascenev1.newnode(
 966                    'combine',
 967                    owner=self._standard_time_limit_text.node,
 968                    attrs={'size': 4},
 969                )
 970                cnode.connectattr(
 971                    'output', self._standard_time_limit_text.node, 'color'
 972                )
 973                animate(cnode, 'input0', {0: 1, 0.15: 1}, loop=True)
 974                animate(cnode, 'input1', {0: 1, 0.15: 0.5}, loop=True)
 975                animate(cnode, 'input2', {0: 0.1, 0.15: 0.0}, loop=True)
 976                cnode.input3 = 1.0
 977            _bascenev1.getsound('tick').play()
 978        if self._standard_time_limit_time <= 0:
 979            self._standard_time_limit_timer = None
 980            self.end_game()
 981            node = _bascenev1.newnode(
 982                'text',
 983                attrs={
 984                    'v_attach': 'top',
 985                    'h_attach': 'center',
 986                    'h_align': 'center',
 987                    'color': (1, 0.7, 0, 1),
 988                    'position': (0, -90),
 989                    'scale': 1.2,
 990                    'text': babase.Lstr(resource='timeExpiredText'),
 991                },
 992            )
 993            _bascenev1.getsound('refWhistle').play()
 994            animate(node, 'scale', {0.0: 0.0, 0.1: 1.4, 0.15: 1.2})
 995
 996    def _setup_tournament_time_limit(self, duration: float) -> None:
 997        """
 998        Create a tournament game time-limit given the provided
 999        duration in seconds.
1000        This will be displayed at the top of the screen.
1001        If the time-limit expires, end_game() will be called.
1002        """
1003        from bascenev1._nodeactor import NodeActor
1004
1005        if duration <= 0.0:
1006            return
1007        self._tournament_time_limit = int(duration)
1008
1009        # We want this timer to match the server's time as close as possible,
1010        # so lets go with base-time. Theoretically we should do real-time but
1011        # then we have to mess with contexts and whatnot since its currently
1012        # not available in activity contexts. :-/
1013        self._tournament_time_limit_timer = _bascenev1.BaseTimer(
1014            1.0, babase.WeakCall(self._tournament_time_limit_tick), repeat=True
1015        )
1016        self._tournament_time_limit_title_text = NodeActor(
1017            _bascenev1.newnode(
1018                'text',
1019                attrs={
1020                    'v_attach': 'bottom',
1021                    'h_attach': 'left',
1022                    'h_align': 'center',
1023                    'v_align': 'center',
1024                    'vr_depth': 300,
1025                    'maxwidth': 100,
1026                    'color': (1.0, 1.0, 1.0, 0.5),
1027                    'position': (60, 50),
1028                    'flatness': 1.0,
1029                    'scale': 0.5,
1030                    'text': babase.Lstr(resource='tournamentText'),
1031                },
1032            )
1033        )
1034        self._tournament_time_limit_text = NodeActor(
1035            _bascenev1.newnode(
1036                'text',
1037                attrs={
1038                    'v_attach': 'bottom',
1039                    'h_attach': 'left',
1040                    'h_align': 'center',
1041                    'v_align': 'center',
1042                    'vr_depth': 300,
1043                    'maxwidth': 100,
1044                    'color': (1.0, 1.0, 1.0, 0.5),
1045                    'position': (60, 30),
1046                    'flatness': 1.0,
1047                    'scale': 0.9,
1048                },
1049            )
1050        )
1051        self._tournament_time_limit_text_input = NodeActor(
1052            _bascenev1.newnode(
1053                'timedisplay',
1054                attrs={
1055                    'timemin': 0,
1056                    'time2': self._tournament_time_limit * 1000,
1057                },
1058            )
1059        )
1060        assert self._tournament_time_limit_text.node
1061        assert self._tournament_time_limit_text_input.node
1062        self._tournament_time_limit_text_input.node.connectattr(
1063            'output', self._tournament_time_limit_text.node, 'text'
1064        )
1065
1066    def _tournament_time_limit_tick(self) -> None:
1067        from bascenev1._gameutils import animate
1068
1069        assert self._tournament_time_limit is not None
1070        self._tournament_time_limit -= 1
1071        if self._tournament_time_limit <= 10:
1072            if self._tournament_time_limit == 10:
1073                assert self._tournament_time_limit_title_text is not None
1074                assert self._tournament_time_limit_title_text.node
1075                assert self._tournament_time_limit_text is not None
1076                assert self._tournament_time_limit_text.node
1077                self._tournament_time_limit_title_text.node.scale = 1.0
1078                self._tournament_time_limit_text.node.scale = 1.3
1079                self._tournament_time_limit_title_text.node.position = (80, 85)
1080                self._tournament_time_limit_text.node.position = (80, 60)
1081                cnode = _bascenev1.newnode(
1082                    'combine',
1083                    owner=self._tournament_time_limit_text.node,
1084                    attrs={'size': 4},
1085                )
1086                cnode.connectattr(
1087                    'output',
1088                    self._tournament_time_limit_title_text.node,
1089                    'color',
1090                )
1091                cnode.connectattr(
1092                    'output', self._tournament_time_limit_text.node, 'color'
1093                )
1094                animate(cnode, 'input0', {0: 1, 0.15: 1}, loop=True)
1095                animate(cnode, 'input1', {0: 1, 0.15: 0.5}, loop=True)
1096                animate(cnode, 'input2', {0: 0.1, 0.15: 0.0}, loop=True)
1097                cnode.input3 = 1.0
1098            _bascenev1.getsound('tick').play()
1099        if self._tournament_time_limit <= 0:
1100            self._tournament_time_limit_timer = None
1101            self.end_game()
1102            tval = babase.Lstr(
1103                resource='tournamentTimeExpiredText',
1104                fallback_resource='timeExpiredText',
1105            )
1106            node = _bascenev1.newnode(
1107                'text',
1108                attrs={
1109                    'v_attach': 'top',
1110                    'h_attach': 'center',
1111                    'h_align': 'center',
1112                    'color': (1, 0.7, 0, 1),
1113                    'position': (0, -200),
1114                    'scale': 1.6,
1115                    'text': tval,
1116                },
1117            )
1118            _bascenev1.getsound('refWhistle').play()
1119            animate(node, 'scale', {0: 0.0, 0.1: 1.4, 0.15: 1.2})
1120
1121        # Normally we just connect this to time, but since this is a bit of a
1122        # funky setup we just update it manually once per second.
1123        assert self._tournament_time_limit_text_input is not None
1124        assert self._tournament_time_limit_text_input.node
1125        self._tournament_time_limit_text_input.node.time2 = (
1126            self._tournament_time_limit * 1000
1127        )
1128
1129    def show_zoom_message(
1130        self,
1131        message: babase.Lstr,
1132        *,
1133        color: Sequence[float] = (0.9, 0.4, 0.0),
1134        scale: float = 0.8,
1135        duration: float = 2.0,
1136        trail: bool = False,
1137    ) -> None:
1138        """Zooming text used to announce game names and winners."""
1139        # pylint: disable=cyclic-import
1140        from bascenev1lib.actor.zoomtext import ZoomText
1141
1142        # Reserve a spot on the screen (in case we get multiple of these so
1143        # they don't overlap).
1144        i = 0
1145        cur_time = babase.apptime()
1146        while True:
1147            if (
1148                i not in self._zoom_message_times
1149                or self._zoom_message_times[i] < cur_time
1150            ):
1151                self._zoom_message_times[i] = cur_time + duration
1152                break
1153            i += 1
1154        ZoomText(
1155            message,
1156            lifespan=duration,
1157            jitter=2.0,
1158            position=(0, 200 - i * 100),
1159            scale=scale,
1160            maxwidth=800,
1161            trail=trail,
1162            color=color,
1163        ).autoretain()
1164
1165    def _calc_map_name(self, settings: dict) -> str:
1166        map_name: str
1167        if 'map' in settings:
1168            map_name = settings['map']
1169        else:
1170            # If settings doesn't specify a map, pick a random one from the
1171            # list of supported ones.
1172            unowned_maps: list[str] = (
1173                babase.app.classic.store.get_unowned_maps()
1174                if babase.app.classic is not None
1175                else []
1176            )
1177            valid_maps: list[str] = [
1178                m
1179                for m in self.get_supported_maps(type(self.session))
1180                if m not in unowned_maps
1181            ]
1182            if not valid_maps:
1183                _bascenev1.broadcastmessage(
1184                    babase.Lstr(resource='noValidMapsErrorText')
1185                )
1186                raise RuntimeError('No valid maps')
1187            map_name = valid_maps[random.randrange(len(valid_maps))]
1188        return map_name

Common base class for all game bascenev1.Activities.

GameActivity(settings: dict)
205    def __init__(self, settings: dict):
206        """Instantiate the Activity."""
207        super().__init__(settings)
208
209        # Holds some flattened info about the player set at the point
210        # when on_begin() is called.
211        self.initialplayerinfos: list[bascenev1.PlayerInfo] | None = None
212
213        # Go ahead and get our map loading.
214        self._map_type = _map.get_map_class(self._calc_map_name(settings))
215
216        self._spawn_sound = _bascenev1.getsound('spawn')
217        self._map_type.preload()
218        self._map: bascenev1.Map | None = None
219        self._powerup_drop_timer: bascenev1.Timer | None = None
220        self._tnt_spawners: dict[int, TNTSpawner] | None = None
221        self._tnt_drop_timer: bascenev1.Timer | None = None
222        self._game_scoreboard_name_text: bascenev1.Actor | None = None
223        self._game_scoreboard_description_text: bascenev1.Actor | None = None
224        self._standard_time_limit_time: int | None = None
225        self._standard_time_limit_timer: bascenev1.Timer | None = None
226        self._standard_time_limit_text: bascenev1.NodeActor | None = None
227        self._standard_time_limit_text_input: bascenev1.NodeActor | None = None
228        self._tournament_time_limit: int | None = None
229        self._tournament_time_limit_timer: bascenev1.BaseTimer | None = None
230        self._tournament_time_limit_title_text: bascenev1.NodeActor | None = (
231            None
232        )
233        self._tournament_time_limit_text: bascenev1.NodeActor | None = None
234        self._tournament_time_limit_text_input: bascenev1.NodeActor | None = (
235            None
236        )
237        self._zoom_message_times: dict[int, float] = {}

Instantiate the Activity.

tips: list[str | GameTip] = []
name: str | None = None
description: str | None = None
available_settings: list[Setting] | None = None
scoreconfig: ScoreConfig | None = None
allow_pausing = True

Whether game-time should still progress when in menus/etc.

allow_kick_idle_players = True

Whether idle players can potentially be kicked (should not happen in menus/etc).

show_kill_points = True
default_music: MusicType | None = None
@classmethod
def getscoreconfig(cls) -> ScoreConfig:
65    @classmethod
66    def getscoreconfig(cls) -> bascenev1.ScoreConfig:
67        """Return info about game scoring setup; can be overridden by games."""
68        return cls.scoreconfig if cls.scoreconfig is not None else ScoreConfig()

Return info about game scoring setup; can be overridden by games.

@classmethod
def getname(cls) -> str:
70    @classmethod
71    def getname(cls) -> str:
72        """Return a str name for this game type.
73
74        This default implementation simply returns the 'name' class attr.
75        """
76        return cls.name if cls.name is not None else 'Untitled Game'

Return a str name for this game type.

This default implementation simply returns the 'name' class attr.

@classmethod
def get_display_string(cls, settings: dict | None = None) -> Lstr:
78    @classmethod
79    def get_display_string(cls, settings: dict | None = None) -> babase.Lstr:
80        """Return a descriptive name for this game/settings combo.
81
82        Subclasses should override getname(); not this.
83        """
84        name = babase.Lstr(translate=('gameNames', cls.getname()))
85
86        # A few substitutions for 'Epic', 'Solo' etc. modes.
87        # FIXME: Should provide a way for game types to define filters of
88        #  their own and should not rely on hard-coded settings names.
89        if settings is not None:
90            if 'Solo Mode' in settings and settings['Solo Mode']:
91                name = babase.Lstr(
92                    resource='soloNameFilterText', subs=[('${NAME}', name)]
93                )
94            if 'Epic Mode' in settings and settings['Epic Mode']:
95                name = babase.Lstr(
96                    resource='epicNameFilterText', subs=[('${NAME}', name)]
97                )
98
99        return name

Return a descriptive name for this game/settings combo.

Subclasses should override getname(); not this.

@classmethod
def get_team_display_string(cls, name: str) -> Lstr:
101    @classmethod
102    def get_team_display_string(cls, name: str) -> babase.Lstr:
103        """Given a team name, returns a localized version of it."""
104        return babase.Lstr(translate=('teamNames', name))

Given a team name, returns a localized version of it.

@classmethod
def get_description(cls, sessiontype: type[Session]) -> str:
106    @classmethod
107    def get_description(cls, sessiontype: type[bascenev1.Session]) -> str:
108        """Get a str description of this game type.
109
110        The default implementation simply returns the 'description' class var.
111        Classes which want to change their description depending on the session
112        can override this method.
113        """
114        del sessiontype  # Unused arg.
115        return cls.description if cls.description is not None else ''

Get a str description of this game type.

The default implementation simply returns the 'description' class var. Classes which want to change their description depending on the session can override this method.

@classmethod
def get_description_display_string( cls, sessiontype: type[Session]) -> Lstr:
117    @classmethod
118    def get_description_display_string(
119        cls, sessiontype: type[bascenev1.Session]
120    ) -> babase.Lstr:
121        """Return a translated version of get_description().
122
123        Sub-classes should override get_description(); not this.
124        """
125        description = cls.get_description(sessiontype)
126        return babase.Lstr(translate=('gameDescriptions', description))

Return a translated version of get_description().

Sub-classes should override get_description(); not this.

@classmethod
def get_available_settings( cls, sessiontype: type[Session]) -> list[Setting]:
128    @classmethod
129    def get_available_settings(
130        cls, sessiontype: type[bascenev1.Session]
131    ) -> list[bascenev1.Setting]:
132        """Return a list of settings relevant to this game type when
133        running under the provided session type.
134        """
135        del sessiontype  # Unused arg.
136        return [] if cls.available_settings is None else cls.available_settings

Return a list of settings relevant to this game type when running under the provided session type.

@classmethod
def get_supported_maps(cls, sessiontype: type[Session]) -> list[str]:
138    @classmethod
139    def get_supported_maps(
140        cls, sessiontype: type[bascenev1.Session]
141    ) -> list[str]:
142        """
143        Called by the default bascenev1.GameActivity.create_settings_ui()
144        implementation; should return a list of map names valid
145        for this game-type for the given bascenev1.Session type.
146        """
147        del sessiontype  # Unused arg.
148        assert babase.app.classic is not None
149        return babase.app.classic.getmaps('melee')

Called by the default bascenev1.GameActivity.create_settings_ui() implementation; should return a list of map names valid for this game-type for the given bascenev1.Session type.

@classmethod
def get_settings_display_string(cls, config: dict[str, typing.Any]) -> Lstr:
151    @classmethod
152    def get_settings_display_string(cls, config: dict[str, Any]) -> babase.Lstr:
153        """Given a game config dict, return a short description for it.
154
155        This is used when viewing game-lists or showing what game
156        is up next in a series.
157        """
158        name = cls.get_display_string(config['settings'])
159
160        # In newer configs, map is in settings; it used to be in the
161        # config root.
162        if 'map' in config['settings']:
163            sval = babase.Lstr(
164                value='${NAME} @ ${MAP}',
165                subs=[
166                    ('${NAME}', name),
167                    (
168                        '${MAP}',
169                        _map.get_map_display_string(
170                            _map.get_filtered_map_name(
171                                config['settings']['map']
172                            )
173                        ),
174                    ),
175                ],
176            )
177        elif 'map' in config:
178            sval = babase.Lstr(
179                value='${NAME} @ ${MAP}',
180                subs=[
181                    ('${NAME}', name),
182                    (
183                        '${MAP}',
184                        _map.get_map_display_string(
185                            _map.get_filtered_map_name(config['map'])
186                        ),
187                    ),
188                ],
189            )
190        else:
191            print('invalid game config - expected map entry under settings')
192            sval = babase.Lstr(value='???')
193        return sval

Given a game config dict, return a short description for it.

This is used when viewing game-lists or showing what game is up next in a series.

@classmethod
def supports_session_type(cls, sessiontype: type[Session]) -> bool:
195    @classmethod
196    def supports_session_type(
197        cls, sessiontype: type[bascenev1.Session]
198    ) -> bool:
199        """Return whether this game supports the provided Session type."""
200        from bascenev1._multiteamsession import MultiTeamSession
201
202        # By default, games support any versus mode
203        return issubclass(sessiontype, MultiTeamSession)

Return whether this game supports the provided Session type.

initialplayerinfos: list[PlayerInfo] | None
map: Map
239    @property
240    def map(self) -> _map.Map:
241        """The map being used for this game.
242
243        Raises a bascenev1.MapNotFoundError if the map does not currently
244        exist.
245        """
246        if self._map is None:
247            raise babase.MapNotFoundError
248        return self._map

The map being used for this game.

Raises a bascenev1.MapNotFoundError if the map does not currently exist.

def get_instance_display_string(self) -> Lstr:
250    def get_instance_display_string(self) -> babase.Lstr:
251        """Return a name for this particular game instance."""
252        return self.get_display_string(self.settings_raw)

Return a name for this particular game instance.

def get_instance_scoreboard_display_string(self) -> Lstr:
255    def get_instance_scoreboard_display_string(self) -> babase.Lstr:
256        """Return a name for this particular game instance.
257
258        This name is used above the game scoreboard in the corner
259        of the screen, so it should be as concise as possible.
260        """
261        # If we're in a co-op session, use the level name.
262        # FIXME: Should clean this up.
263        try:
264            from bascenev1._coopsession import CoopSession
265
266            if isinstance(self.session, CoopSession):
267                campaign = self.session.campaign
268                assert campaign is not None
269                return campaign.getlevel(
270                    self.session.campaign_level_name
271                ).displayname
272        except Exception:
273            logging.exception('Error getting campaign level name.')
274        return self.get_instance_display_string()

Return a name for this particular game instance.

This name is used above the game scoreboard in the corner of the screen, so it should be as concise as possible.

def get_instance_description(self) -> Union[str, Sequence]:
276    def get_instance_description(self) -> str | Sequence:
277        """Return a description for this game instance, in English.
278
279        This is shown in the center of the screen below the game name at the
280        start of a game. It should start with a capital letter and end with a
281        period, and can be a bit more verbose than the version returned by
282        get_instance_description_short().
283
284        Note that translation is applied by looking up the specific returned
285        value as a key, so the number of returned variations should be limited;
286        ideally just one or two. To include arbitrary values in the
287        description, you can return a sequence of values in the following
288        form instead of just a string:
289
290        # This will give us something like 'Score 3 goals.' in English
291        # and can properly translate to 'Anota 3 goles.' in Spanish.
292        # If we just returned the string 'Score 3 Goals' here, there would
293        # have to be a translation entry for each specific number. ew.
294        return ['Score ${ARG1} goals.', self.settings_raw['Score to Win']]
295
296        This way the first string can be consistently translated, with any arg
297        values then substituted into the result. ${ARG1} will be replaced with
298        the first value, ${ARG2} with the second, etc.
299        """
300        return self.get_description(type(self.session))

Return a description for this game instance, in English.

This is shown in the center of the screen below the game name at the start of a game. It should start with a capital letter and end with a period, and can be a bit more verbose than the version returned by get_instance_description_short().

Note that translation is applied by looking up the specific returned value as a key, so the number of returned variations should be limited; ideally just one or two. To include arbitrary values in the description, you can return a sequence of values in the following form instead of just a string:

This will give us something like 'Score 3 goals.' in English

and can properly translate to 'Anota 3 goles.' in Spanish.

If we just returned the string 'Score 3 Goals' here, there would

have to be a translation entry for each specific number. ew.

return ['Score ${ARG1} goals.', self.settings_raw['Score to Win']]

This way the first string can be consistently translated, with any arg values then substituted into the result. ${ARG1} will be replaced with the first value, ${ARG2} with the second, etc.

def get_instance_description_short(self) -> Union[str, Sequence]:
302    def get_instance_description_short(self) -> str | Sequence:
303        """Return a short description for this game instance in English.
304
305        This description is used above the game scoreboard in the
306        corner of the screen, so it should be as concise as possible.
307        It should be lowercase and should not contain periods or other
308        punctuation.
309
310        Note that translation is applied by looking up the specific returned
311        value as a key, so the number of returned variations should be limited;
312        ideally just one or two. To include arbitrary values in the
313        description, you can return a sequence of values in the following form
314        instead of just a string:
315
316        # This will give us something like 'score 3 goals' in English
317        # and can properly translate to 'anota 3 goles' in Spanish.
318        # If we just returned the string 'score 3 goals' here, there would
319        # have to be a translation entry for each specific number. ew.
320        return ['score ${ARG1} goals', self.settings_raw['Score to Win']]
321
322        This way the first string can be consistently translated, with any arg
323        values then substituted into the result. ${ARG1} will be replaced
324        with the first value, ${ARG2} with the second, etc.
325
326        """
327        return ''

Return a short description for this game instance in English.

This description is used above the game scoreboard in the corner of the screen, so it should be as concise as possible. It should be lowercase and should not contain periods or other punctuation.

Note that translation is applied by looking up the specific returned value as a key, so the number of returned variations should be limited; ideally just one or two. To include arbitrary values in the description, you can return a sequence of values in the following form instead of just a string:

This will give us something like 'score 3 goals' in English

and can properly translate to 'anota 3 goles' in Spanish.

If we just returned the string 'score 3 goals' here, there would

have to be a translation entry for each specific number. ew.

return ['score ${ARG1} goals', self.settings_raw['Score to Win']]

This way the first string can be consistently translated, with any arg values then substituted into the result. ${ARG1} will be replaced with the first value, ${ARG2} with the second, etc.

@override
def on_transition_in(self) -> None:
329    @override
330    def on_transition_in(self) -> None:
331        super().on_transition_in()
332
333        # Make our map.
334        self._map = self._map_type()
335
336        # Give our map a chance to override the music.
337        # (for happy-thoughts and other such themed maps)
338        map_music = self._map_type.get_music_type()
339        music = map_music if map_music is not None else self.default_music
340
341        if music is not None:
342            _music.setmusic(music)

Called when the Activity is first becoming visible.

Upon this call, the Activity should fade in backgrounds, start playing music, etc. It does not yet have access to players or teams, however. They remain owned by the previous Activity up until bascenev1.Activity.on_begin() is called.

@override
def on_begin(self) -> None:
344    @override
345    def on_begin(self) -> None:
346        super().on_begin()
347
348        if babase.app.classic is not None:
349            babase.app.classic.game_begin_analytics()
350
351        # We don't do this in on_transition_in because it may depend on
352        # players/teams which aren't available until now.
353        _bascenev1.timer(0.001, self._show_scoreboard_info)
354        _bascenev1.timer(1.0, self._show_info)
355        _bascenev1.timer(2.5, self._show_tip)
356
357        # Store some basic info about players present at start time.
358        self.initialplayerinfos = [
359            PlayerInfo(name=p.getname(full=True), character=p.character)
360            for p in self.players
361        ]
362
363        # Sort this by name so high score lists/etc will be consistent
364        # regardless of player join order.
365        self.initialplayerinfos.sort(key=lambda x: x.name)
366
367        # If this is a tournament, query info about it such as how much
368        # time is left.
369        tournament_id = self.session.tournament_id
370        if tournament_id is not None:
371            assert babase.app.plus is not None
372            babase.app.plus.tournament_query(
373                args={
374                    'tournamentIDs': [tournament_id],
375                    'source': 'in-game time remaining query',
376                },
377                callback=babase.WeakCall(self._on_tournament_query_response),
378            )

Called once the previous Activity has finished transitioning out.

At this point the activity's initial players and teams are filled in and it should begin its actual game logic.

@override
def on_player_join(self, player: ~PlayerT) -> None:
393    @override
394    def on_player_join(self, player: PlayerT) -> None:
395        super().on_player_join(player)
396
397        # By default, just spawn a dude.
398        self.spawn_player(player)

Called when a new bascenev1.Player has joined the Activity.

(including the initial set of Players)

@override
def handlemessage(self, msg: Any) -> Any:
400    @override
401    def handlemessage(self, msg: Any) -> Any:
402        if isinstance(msg, PlayerDiedMessage):
403            # pylint: disable=cyclic-import
404            from bascenev1lib.actor.spaz import Spaz
405
406            player = msg.getplayer(self.playertype)
407            killer = msg.getkillerplayer(self.playertype)
408
409            # Inform our stats of the demise.
410            self.stats.player_was_killed(
411                player, killed=msg.killed, killer=killer
412            )
413
414            # Award the killer points if he's on a different team.
415            # FIXME: This should not be linked to Spaz actors.
416            # (should move get_death_points to Actor or make it a message)
417            if killer and killer.team is not player.team:
418                assert isinstance(killer.actor, Spaz)
419                pts, importance = killer.actor.get_death_points(msg.how)
420                if not self.has_ended():
421                    self.stats.player_scored(
422                        killer,
423                        pts,
424                        kill=True,
425                        victim_player=player,
426                        importance=importance,
427                        showpoints=self.show_kill_points,
428                    )
429        else:
430            return super().handlemessage(msg)
431        return None

General message handling; can be passed any message object.

@override
def end( self, results: Any = None, delay: float = 0.0, force: bool = False) -> None:
694    @override
695    def end(
696        self, results: Any = None, delay: float = 0.0, force: bool = False
697    ) -> None:
698        from bascenev1._gameresults import GameResults
699
700        # If results is a standard team-game-results, associate it with us
701        # so it can grab our score prefs.
702        if isinstance(results, GameResults):
703            results.set_game(self)
704
705        # If we had a standard time-limit that had not expired, stop it so
706        # it doesnt tick annoyingly.
707        if (
708            self._standard_time_limit_time is not None
709            and self._standard_time_limit_time > 0
710        ):
711            self._standard_time_limit_timer = None
712            self._standard_time_limit_text = None
713
714        # Ditto with tournament time limits.
715        if (
716            self._tournament_time_limit is not None
717            and self._tournament_time_limit > 0
718        ):
719            self._tournament_time_limit_timer = None
720            self._tournament_time_limit_text = None
721            self._tournament_time_limit_title_text = None
722
723        super().end(results, delay, force)

Commences Activity shutdown and delivers results to the Session.

'delay' is the time delay before the Activity actually ends (in seconds). Further calls to end() will be ignored up until this time, unless 'force' is True, in which case the new results will replace the old.

def end_game(self) -> None:
725    def end_game(self) -> None:
726        """Tell the game to wrap up and call bascenev1.Activity.end().
727
728        This method should be overridden by subclasses. A game should always
729        be prepared to end and deliver results, even if there is no 'winner'
730        yet; this way things like the standard time-limit
731        (bascenev1.GameActivity.setup_standard_time_limit()) will work with
732        the game.
733        """
734        print(
735            'WARNING: default end_game() implementation called;'
736            ' your game should override this.'
737        )

Tell the game to wrap up and call bascenev1.Activity.end().

This method should be overridden by subclasses. A game should always be prepared to end and deliver results, even if there is no 'winner' yet; this way things like the standard time-limit (bascenev1.GameActivity.setup_standard_time_limit()) will work with the game.

def respawn_player(self, player: ~PlayerT, respawn_time: float | None = None) -> None:
739    def respawn_player(
740        self, player: PlayerT, respawn_time: float | None = None
741    ) -> None:
742        """
743        Given a bascenev1.Player, sets up a standard respawn timer,
744        along with the standard counter display, etc.
745        At the end of the respawn period spawn_player() will
746        be called if the Player still exists.
747        An explicit 'respawn_time' can optionally be provided
748        (in seconds).
749        """
750        # pylint: disable=cyclic-import
751
752        assert player
753        if respawn_time is None:
754            teamsize = len(player.team.players)
755            if teamsize == 1:
756                respawn_time = 3.0
757            elif teamsize == 2:
758                respawn_time = 5.0
759            elif teamsize == 3:
760                respawn_time = 6.0
761            else:
762                respawn_time = 7.0
763
764        # If this standard setting is present, factor it in.
765        if 'Respawn Times' in self.settings_raw:
766            respawn_time *= self.settings_raw['Respawn Times']
767
768        # We want whole seconds.
769        assert respawn_time is not None
770        respawn_time = round(max(1.0, respawn_time), 0)
771
772        if player.actor and not self.has_ended():
773            from bascenev1lib.actor.respawnicon import RespawnIcon
774
775            player.customdata['respawn_timer'] = _bascenev1.Timer(
776                respawn_time,
777                babase.WeakCall(self.spawn_player_if_exists, player),
778            )
779            player.customdata['respawn_icon'] = RespawnIcon(
780                player, respawn_time
781            )

Given a bascenev1.Player, sets up a standard respawn timer, along with the standard counter display, etc. At the end of the respawn period spawn_player() will be called if the Player still exists. An explicit 'respawn_time' can optionally be provided (in seconds).

def spawn_player_if_exists(self, player: ~PlayerT) -> None:
783    def spawn_player_if_exists(self, player: PlayerT) -> None:
784        """
785        A utility method which calls self.spawn_player() *only* if the
786        bascenev1.Player provided still exists; handy for use in timers
787        and whatnot.
788
789        There is no need to override this; just override spawn_player().
790        """
791        if player:
792            self.spawn_player(player)

A utility method which calls self.spawn_player() only if the bascenev1.Player provided still exists; handy for use in timers and whatnot.

There is no need to override this; just override spawn_player().

def spawn_player(self, player: ~PlayerT) -> Actor:
794    def spawn_player(self, player: PlayerT) -> bascenev1.Actor:
795        """Spawn *something* for the provided bascenev1.Player.
796
797        The default implementation simply calls spawn_player_spaz().
798        """
799        assert player  # Dead references should never be passed as args.
800
801        return self.spawn_player_spaz(player)

Spawn something for the provided bascenev1.Player.

The default implementation simply calls spawn_player_spaz().

def spawn_player_spaz( self, player: ~PlayerT, position: Sequence[float] = (0, 0, 0), angle: float | None = None) -> bascenev1lib.actor.playerspaz.PlayerSpaz:
803    def spawn_player_spaz(
804        self,
805        player: PlayerT,
806        position: Sequence[float] = (0, 0, 0),
807        angle: float | None = None,
808    ) -> PlayerSpaz:
809        """Create and wire up a bascenev1.PlayerSpaz for the provided Player."""
810        # pylint: disable=too-many-locals
811        # pylint: disable=cyclic-import
812        from bascenev1._gameutils import animate
813        from bascenev1._coopsession import CoopSession
814        from bascenev1lib.actor.playerspaz import PlayerSpaz
815
816        name = player.getname()
817        color = player.color
818        highlight = player.highlight
819
820        playerspaztype = getattr(player, 'playerspaztype', PlayerSpaz)
821        if not issubclass(playerspaztype, PlayerSpaz):
822            playerspaztype = PlayerSpaz
823
824        light_color = babase.normalized_color(color)
825        display_color = babase.safecolor(color, target_intensity=0.75)
826        spaz = playerspaztype(
827            color=color,
828            highlight=highlight,
829            character=player.character,
830            player=player,
831        )
832
833        player.actor = spaz
834        assert spaz.node
835
836        # If this is co-op and we're on Courtyard or Runaround, add the
837        # material that allows us to collide with the player-walls.
838        # FIXME: Need to generalize this.
839        if isinstance(self.session, CoopSession) and self.map.getname() in [
840            'Courtyard',
841            'Tower D',
842        ]:
843            mat = self.map.preloaddata['collide_with_wall_material']
844            assert isinstance(spaz.node.materials, tuple)
845            assert isinstance(spaz.node.roller_materials, tuple)
846            spaz.node.materials += (mat,)
847            spaz.node.roller_materials += (mat,)
848
849        spaz.node.name = name
850        spaz.node.name_color = display_color
851        spaz.connect_controls_to_player()
852
853        # Move to the stand position and add a flash of light.
854        spaz.handlemessage(
855            StandMessage(
856                position, angle if angle is not None else random.uniform(0, 360)
857            )
858        )
859        self._spawn_sound.play(1, position=spaz.node.position)
860        light = _bascenev1.newnode('light', attrs={'color': light_color})
861        spaz.node.connectattr('position', light, 'position')
862        animate(light, 'intensity', {0: 0, 0.25: 1, 0.5: 0})
863        _bascenev1.timer(0.5, light.delete)
864        return spaz

Create and wire up a bascenev1.PlayerSpaz for the provided Player.

def setup_standard_powerup_drops(self, enable_tnt: bool = True) -> None:
866    def setup_standard_powerup_drops(self, enable_tnt: bool = True) -> None:
867        """Create standard powerup drops for the current map."""
868        # pylint: disable=cyclic-import
869        from bascenev1lib.actor.powerupbox import DEFAULT_POWERUP_INTERVAL
870
871        self._powerup_drop_timer = _bascenev1.Timer(
872            DEFAULT_POWERUP_INTERVAL,
873            babase.WeakCall(self._standard_drop_powerups),
874            repeat=True,
875        )
876        self._standard_drop_powerups()
877        if enable_tnt:
878            self._tnt_spawners = {}
879            self._setup_standard_tnt_drops()

Create standard powerup drops for the current map.

def setup_standard_time_limit(self, duration: float) -> None:
911    def setup_standard_time_limit(self, duration: float) -> None:
912        """
913        Create a standard game time-limit given the provided
914        duration in seconds.
915        This will be displayed at the top of the screen.
916        If the time-limit expires, end_game() will be called.
917        """
918        from bascenev1._nodeactor import NodeActor
919
920        if duration <= 0.0:
921            return
922        self._standard_time_limit_time = int(duration)
923        self._standard_time_limit_timer = _bascenev1.Timer(
924            1.0, babase.WeakCall(self._standard_time_limit_tick), repeat=True
925        )
926        self._standard_time_limit_text = NodeActor(
927            _bascenev1.newnode(
928                'text',
929                attrs={
930                    'v_attach': 'top',
931                    'h_attach': 'center',
932                    'h_align': 'left',
933                    'color': (1.0, 1.0, 1.0, 0.5),
934                    'position': (-25, -30),
935                    'flatness': 1.0,
936                    'scale': 0.9,
937                },
938            )
939        )
940        self._standard_time_limit_text_input = NodeActor(
941            _bascenev1.newnode(
942                'timedisplay', attrs={'time2': duration * 1000, 'timemin': 0}
943            )
944        )
945        self.globalsnode.connectattr(
946            'time', self._standard_time_limit_text_input.node, 'time1'
947        )
948        assert self._standard_time_limit_text_input.node
949        assert self._standard_time_limit_text.node
950        self._standard_time_limit_text_input.node.connectattr(
951            'output', self._standard_time_limit_text.node, 'text'
952        )

Create a standard game time-limit given the provided duration in seconds. This will be displayed at the top of the screen. If the time-limit expires, end_game() will be called.

def show_zoom_message( self, message: Lstr, *, color: Sequence[float] = (0.9, 0.4, 0.0), scale: float = 0.8, duration: float = 2.0, trail: bool = False) -> None:
1129    def show_zoom_message(
1130        self,
1131        message: babase.Lstr,
1132        *,
1133        color: Sequence[float] = (0.9, 0.4, 0.0),
1134        scale: float = 0.8,
1135        duration: float = 2.0,
1136        trail: bool = False,
1137    ) -> None:
1138        """Zooming text used to announce game names and winners."""
1139        # pylint: disable=cyclic-import
1140        from bascenev1lib.actor.zoomtext import ZoomText
1141
1142        # Reserve a spot on the screen (in case we get multiple of these so
1143        # they don't overlap).
1144        i = 0
1145        cur_time = babase.apptime()
1146        while True:
1147            if (
1148                i not in self._zoom_message_times
1149                or self._zoom_message_times[i] < cur_time
1150            ):
1151                self._zoom_message_times[i] = cur_time + duration
1152                break
1153            i += 1
1154        ZoomText(
1155            message,
1156            lifespan=duration,
1157            jitter=2.0,
1158            position=(0, 200 - i * 100),
1159            scale=scale,
1160            maxwidth=800,
1161            trail=trail,
1162            color=color,
1163        ).autoretain()

Zooming text used to announce game names and winners.

class GameResults:
 31class GameResults:
 32    """
 33    Results for a completed game.
 34
 35    Upon completion, a game should fill one of these out and pass it to its
 36    bascenev1.Activity.end call.
 37    """
 38
 39    def __init__(self) -> None:
 40        self._game_set = False
 41        self._scores: dict[
 42            int, tuple[weakref.ref[bascenev1.SessionTeam], int | None]
 43        ] = {}
 44        self._sessionteams: list[weakref.ref[bascenev1.SessionTeam]] | None = (
 45            None
 46        )
 47        self._playerinfos: list[bascenev1.PlayerInfo] | None = None
 48        self._lower_is_better: bool | None = None
 49        self._score_label: str | None = None
 50        self._none_is_winner: bool | None = None
 51        self._scoretype: bascenev1.ScoreType | None = None
 52
 53    def set_game(self, game: bascenev1.GameActivity) -> None:
 54        """Set the game instance these results are applying to."""
 55        if self._game_set:
 56            raise RuntimeError('Game set twice for GameResults.')
 57        self._game_set = True
 58        self._sessionteams = [
 59            weakref.ref(team.sessionteam) for team in game.teams
 60        ]
 61        scoreconfig = game.getscoreconfig()
 62        self._playerinfos = copy.deepcopy(game.initialplayerinfos)
 63        self._lower_is_better = scoreconfig.lower_is_better
 64        self._score_label = scoreconfig.label
 65        self._none_is_winner = scoreconfig.none_is_winner
 66        self._scoretype = scoreconfig.scoretype
 67
 68    def set_team_score(self, team: bascenev1.Team, score: int | None) -> None:
 69        """Set the score for a given team.
 70
 71        This can be a number or None.
 72        (see the none_is_winner arg in the constructor)
 73        """
 74        assert isinstance(team, Team)
 75        sessionteam = team.sessionteam
 76        self._scores[sessionteam.id] = (weakref.ref(sessionteam), score)
 77
 78    def get_sessionteam_score(
 79        self, sessionteam: bascenev1.SessionTeam
 80    ) -> int | None:
 81        """Return the score for a given bascenev1.SessionTeam."""
 82        assert isinstance(sessionteam, SessionTeam)
 83        for score in list(self._scores.values()):
 84            if score[0]() is sessionteam:
 85                return score[1]
 86
 87        # If we have no score value, assume None.
 88        return None
 89
 90    @property
 91    def sessionteams(self) -> list[bascenev1.SessionTeam]:
 92        """Return all bascenev1.SessionTeams in the results."""
 93        if not self._game_set:
 94            raise RuntimeError("Can't get teams until game is set.")
 95        teams = []
 96        assert self._sessionteams is not None
 97        for team_ref in self._sessionteams:
 98            team = team_ref()
 99            if team is not None:
100                teams.append(team)
101        return teams
102
103    def has_score_for_sessionteam(
104        self, sessionteam: bascenev1.SessionTeam
105    ) -> bool:
106        """Return whether there is a score for a given session-team."""
107        return any(s[0]() is sessionteam for s in self._scores.values())
108
109    def get_sessionteam_score_str(
110        self, sessionteam: bascenev1.SessionTeam
111    ) -> babase.Lstr:
112        """Return the score for the given session-team as an Lstr.
113
114        (properly formatted for the score type.)
115        """
116        from bascenev1._score import ScoreType
117
118        if not self._game_set:
119            raise RuntimeError("Can't get team-score-str until game is set.")
120        for score in list(self._scores.values()):
121            if score[0]() is sessionteam:
122                if score[1] is None:
123                    return babase.Lstr(value='-')
124                if self._scoretype is ScoreType.SECONDS:
125                    return babase.timestring(score[1], centi=False)
126                if self._scoretype is ScoreType.MILLISECONDS:
127                    return babase.timestring(score[1] / 1000.0, centi=True)
128                return babase.Lstr(value=str(score[1]))
129        return babase.Lstr(value='-')
130
131    @property
132    def playerinfos(self) -> list[bascenev1.PlayerInfo]:
133        """Get info about the players represented by the results."""
134        if not self._game_set:
135            raise RuntimeError("Can't get player-info until game is set.")
136        assert self._playerinfos is not None
137        return self._playerinfos
138
139    @property
140    def scoretype(self) -> bascenev1.ScoreType:
141        """The type of score."""
142        if not self._game_set:
143            raise RuntimeError("Can't get score-type until game is set.")
144        assert self._scoretype is not None
145        return self._scoretype
146
147    @property
148    def score_label(self) -> str:
149        """The label associated with scores ('points', etc)."""
150        if not self._game_set:
151            raise RuntimeError("Can't get score-label until game is set.")
152        assert self._score_label is not None
153        return self._score_label
154
155    @property
156    def lower_is_better(self) -> bool:
157        """Whether lower scores are better."""
158        if not self._game_set:
159            raise RuntimeError("Can't get lower-is-better until game is set.")
160        assert self._lower_is_better is not None
161        return self._lower_is_better
162
163    @property
164    def winning_sessionteam(self) -> bascenev1.SessionTeam | None:
165        """The winning SessionTeam if there is exactly one, or else None."""
166        if not self._game_set:
167            raise RuntimeError("Can't get winners until game is set.")
168        winners = self.winnergroups
169        if winners and len(winners[0].teams) == 1:
170            return winners[0].teams[0]
171        return None
172
173    @property
174    def winnergroups(self) -> list[WinnerGroup]:
175        """Get an ordered list of winner groups."""
176        if not self._game_set:
177            raise RuntimeError("Can't get winners until game is set.")
178
179        # Group by best scoring teams.
180        winners: dict[int, list[bascenev1.SessionTeam]] = {}
181        scores = [
182            score
183            for score in self._scores.values()
184            if score[0]() is not None and score[1] is not None
185        ]
186        for score in scores:
187            assert score[1] is not None
188            sval = winners.setdefault(score[1], [])
189            team = score[0]()
190            assert team is not None
191            sval.append(team)
192        results: list[tuple[int | None, list[bascenev1.SessionTeam]]] = list(
193            winners.items()
194        )
195        results.sort(
196            reverse=not self._lower_is_better,
197            key=lambda x: asserttype(x[0], int),
198        )
199
200        # Also group the 'None' scores.
201        none_sessionteams: list[bascenev1.SessionTeam] = []
202        for score in self._scores.values():
203            scoreteam = score[0]()
204            if scoreteam is not None and score[1] is None:
205                none_sessionteams.append(scoreteam)
206
207        # Add the Nones to the list (either as winners or losers
208        # depending on the rules).
209        if none_sessionteams:
210            nones: list[tuple[int | None, list[bascenev1.SessionTeam]]] = [
211                (None, none_sessionteams)
212            ]
213            if self._none_is_winner:
214                results = nones + results
215            else:
216                results = results + nones
217
218        return [WinnerGroup(score, team) for score, team in results]

Results for a completed game.

Upon completion, a game should fill one of these out and pass it to its bascenev1.Activity.end call.

def set_game(self, game: GameActivity) -> None:
53    def set_game(self, game: bascenev1.GameActivity) -> None:
54        """Set the game instance these results are applying to."""
55        if self._game_set:
56            raise RuntimeError('Game set twice for GameResults.')
57        self._game_set = True
58        self._sessionteams = [
59            weakref.ref(team.sessionteam) for team in game.teams
60        ]
61        scoreconfig = game.getscoreconfig()
62        self._playerinfos = copy.deepcopy(game.initialplayerinfos)
63        self._lower_is_better = scoreconfig.lower_is_better
64        self._score_label = scoreconfig.label
65        self._none_is_winner = scoreconfig.none_is_winner
66        self._scoretype = scoreconfig.scoretype

Set the game instance these results are applying to.

def set_team_score(self, team: Team, score: int | None) -> None:
68    def set_team_score(self, team: bascenev1.Team, score: int | None) -> None:
69        """Set the score for a given team.
70
71        This can be a number or None.
72        (see the none_is_winner arg in the constructor)
73        """
74        assert isinstance(team, Team)
75        sessionteam = team.sessionteam
76        self._scores[sessionteam.id] = (weakref.ref(sessionteam), score)

Set the score for a given team.

This can be a number or None. (see the none_is_winner arg in the constructor)

def get_sessionteam_score(self, sessionteam: SessionTeam) -> int | None:
78    def get_sessionteam_score(
79        self, sessionteam: bascenev1.SessionTeam
80    ) -> int | None:
81        """Return the score for a given bascenev1.SessionTeam."""
82        assert isinstance(sessionteam, SessionTeam)
83        for score in list(self._scores.values()):
84            if score[0]() is sessionteam:
85                return score[1]
86
87        # If we have no score value, assume None.
88        return None

Return the score for a given bascenev1.SessionTeam.

sessionteams: list[SessionTeam]
 90    @property
 91    def sessionteams(self) -> list[bascenev1.SessionTeam]:
 92        """Return all bascenev1.SessionTeams in the results."""
 93        if not self._game_set:
 94            raise RuntimeError("Can't get teams until game is set.")
 95        teams = []
 96        assert self._sessionteams is not None
 97        for team_ref in self._sessionteams:
 98            team = team_ref()
 99            if team is not None:
100                teams.append(team)
101        return teams

Return all bascenev1.SessionTeams in the results.

def has_score_for_sessionteam(self, sessionteam: SessionTeam) -> bool:
103    def has_score_for_sessionteam(
104        self, sessionteam: bascenev1.SessionTeam
105    ) -> bool:
106        """Return whether there is a score for a given session-team."""
107        return any(s[0]() is sessionteam for s in self._scores.values())

Return whether there is a score for a given session-team.

def get_sessionteam_score_str(self, sessionteam: SessionTeam) -> Lstr:
109    def get_sessionteam_score_str(
110        self, sessionteam: bascenev1.SessionTeam
111    ) -> babase.Lstr:
112        """Return the score for the given session-team as an Lstr.
113
114        (properly formatted for the score type.)
115        """
116        from bascenev1._score import ScoreType
117
118        if not self._game_set:
119            raise RuntimeError("Can't get team-score-str until game is set.")
120        for score in list(self._scores.values()):
121            if score[0]() is sessionteam:
122                if score[1] is None:
123                    return babase.Lstr(value='-')
124                if self._scoretype is ScoreType.SECONDS:
125                    return babase.timestring(score[1], centi=False)
126                if self._scoretype is ScoreType.MILLISECONDS:
127                    return babase.timestring(score[1] / 1000.0, centi=True)
128                return babase.Lstr(value=str(score[1]))
129        return babase.Lstr(value='-')

Return the score for the given session-team as an Lstr.

(properly formatted for the score type.)

playerinfos: list[PlayerInfo]
131    @property
132    def playerinfos(self) -> list[bascenev1.PlayerInfo]:
133        """Get info about the players represented by the results."""
134        if not self._game_set:
135            raise RuntimeError("Can't get player-info until game is set.")
136        assert self._playerinfos is not None
137        return self._playerinfos

Get info about the players represented by the results.

scoretype: ScoreType
139    @property
140    def scoretype(self) -> bascenev1.ScoreType:
141        """The type of score."""
142        if not self._game_set:
143            raise RuntimeError("Can't get score-type until game is set.")
144        assert self._scoretype is not None
145        return self._scoretype

The type of score.

score_label: str
147    @property
148    def score_label(self) -> str:
149        """The label associated with scores ('points', etc)."""
150        if not self._game_set:
151            raise RuntimeError("Can't get score-label until game is set.")
152        assert self._score_label is not None
153        return self._score_label

The label associated with scores ('points', etc).

lower_is_better: bool
155    @property
156    def lower_is_better(self) -> bool:
157        """Whether lower scores are better."""
158        if not self._game_set:
159            raise RuntimeError("Can't get lower-is-better until game is set.")
160        assert self._lower_is_better is not None
161        return self._lower_is_better

Whether lower scores are better.

winning_sessionteam: SessionTeam | None
163    @property
164    def winning_sessionteam(self) -> bascenev1.SessionTeam | None:
165        """The winning SessionTeam if there is exactly one, or else None."""
166        if not self._game_set:
167            raise RuntimeError("Can't get winners until game is set.")
168        winners = self.winnergroups
169        if winners and len(winners[0].teams) == 1:
170            return winners[0].teams[0]
171        return None

The winning SessionTeam if there is exactly one, or else None.

winnergroups: list[bascenev1._gameresults.WinnerGroup]
173    @property
174    def winnergroups(self) -> list[WinnerGroup]:
175        """Get an ordered list of winner groups."""
176        if not self._game_set:
177            raise RuntimeError("Can't get winners until game is set.")
178
179        # Group by best scoring teams.
180        winners: dict[int, list[bascenev1.SessionTeam]] = {}
181        scores = [
182            score
183            for score in self._scores.values()
184            if score[0]() is not None and score[1] is not None
185        ]
186        for score in scores:
187            assert score[1] is not None
188            sval = winners.setdefault(score[1], [])
189            team = score[0]()
190            assert team is not None
191            sval.append(team)
192        results: list[tuple[int | None, list[bascenev1.SessionTeam]]] = list(
193            winners.items()
194        )
195        results.sort(
196            reverse=not self._lower_is_better,
197            key=lambda x: asserttype(x[0], int),
198        )
199
200        # Also group the 'None' scores.
201        none_sessionteams: list[bascenev1.SessionTeam] = []
202        for score in self._scores.values():
203            scoreteam = score[0]()
204            if scoreteam is not None and score[1] is None:
205                none_sessionteams.append(scoreteam)
206
207        # Add the Nones to the list (either as winners or losers
208        # depending on the rules).
209        if none_sessionteams:
210            nones: list[tuple[int | None, list[bascenev1.SessionTeam]]] = [
211                (None, none_sessionteams)
212            ]
213            if self._none_is_winner:
214                results = nones + results
215            else:
216                results = results + nones
217
218        return [WinnerGroup(score, team) for score, team in results]

Get an ordered list of winner groups.

@dataclass
class GameTip:
33@dataclass
34class GameTip:
35    """Defines a tip presentable to the user at the start of a game."""
36
37    text: str
38    icon: bascenev1.Texture | None = None
39    sound: bascenev1.Sound | None = None

Defines a tip presentable to the user at the start of a game.

GameTip( text: str, icon: _bascenev1.Texture | None = None, sound: _bascenev1.Sound | None = None)
text: str
icon: _bascenev1.Texture | None = None
sound: _bascenev1.Sound | None = None
def get_connection_to_host_info_2() -> HostInfo | None:
1148def get_connection_to_host_info_2() -> bascenev1.HostInfo | None:
1149    """Return info about the host we are currently connected to."""
1150    import bascenev1  # pylint: disable=cyclic-import
1151
1152    return bascenev1.HostInfo('dummyname', -1, 'dummy_addr', -1)

Return info about the host we are currently connected to.

def get_default_free_for_all_playlist() -> list[dict[str, typing.Any]]:
226def get_default_free_for_all_playlist() -> PlaylistType:
227    """Return a default playlist for free-for-all mode."""
228
229    # NOTE: these are currently using old type/map names,
230    # but filtering translates them properly to the new ones.
231    # (is kinda a handy way to ensure filtering is working).
232    # Eventually should update these though.
233    return [
234        {
235            'settings': {
236                'Epic Mode': False,
237                'Kills to Win Per Player': 10,
238                'Respawn Times': 1.0,
239                'Time Limit': 300,
240                'map': 'Doom Shroom',
241            },
242            'type': 'bs_death_match.DeathMatchGame',
243        },
244        {
245            'settings': {
246                'Chosen One Gets Gloves': True,
247                'Chosen One Gets Shield': False,
248                'Chosen One Time': 30,
249                'Epic Mode': 0,
250                'Respawn Times': 1.0,
251                'Time Limit': 300,
252                'map': 'Monkey Face',
253            },
254            'type': 'bs_chosen_one.ChosenOneGame',
255        },
256        {
257            'settings': {
258                'Hold Time': 30,
259                'Respawn Times': 1.0,
260                'Time Limit': 300,
261                'map': 'Zigzag',
262            },
263            'type': 'bs_king_of_the_hill.KingOfTheHillGame',
264        },
265        {
266            'settings': {'Epic Mode': False, 'map': 'Rampage'},
267            'type': 'bs_meteor_shower.MeteorShowerGame',
268        },
269        {
270            'settings': {
271                'Epic Mode': 1,
272                'Lives Per Player': 1,
273                'Respawn Times': 1.0,
274                'Time Limit': 120,
275                'map': 'Tip Top',
276            },
277            'type': 'bs_elimination.EliminationGame',
278        },
279        {
280            'settings': {
281                'Hold Time': 30,
282                'Respawn Times': 1.0,
283                'Time Limit': 300,
284                'map': 'The Pad',
285            },
286            'type': 'bs_keep_away.KeepAwayGame',
287        },
288        {
289            'settings': {
290                'Epic Mode': True,
291                'Kills to Win Per Player': 10,
292                'Respawn Times': 0.25,
293                'Time Limit': 120,
294                'map': 'Rampage',
295            },
296            'type': 'bs_death_match.DeathMatchGame',
297        },
298        {
299            'settings': {
300                'Bomb Spawning': 1000,
301                'Epic Mode': False,
302                'Laps': 3,
303                'Mine Spawn Interval': 4000,
304                'Mine Spawning': 4000,
305                'Time Limit': 300,
306                'map': 'Big G',
307            },
308            'type': 'bs_race.RaceGame',
309        },
310        {
311            'settings': {
312                'Hold Time': 30,
313                'Respawn Times': 1.0,
314                'Time Limit': 300,
315                'map': 'Happy Thoughts',
316            },
317            'type': 'bs_king_of_the_hill.KingOfTheHillGame',
318        },
319        {
320            'settings': {
321                'Enable Impact Bombs': 1,
322                'Enable Triple Bombs': False,
323                'Target Count': 2,
324                'map': 'Doom Shroom',
325            },
326            'type': 'bs_target_practice.TargetPracticeGame',
327        },
328        {
329            'settings': {
330                'Epic Mode': False,
331                'Lives Per Player': 5,
332                'Respawn Times': 1.0,
333                'Time Limit': 300,
334                'map': 'Step Right Up',
335            },
336            'type': 'bs_elimination.EliminationGame',
337        },
338        {
339            'settings': {
340                'Epic Mode': False,
341                'Kills to Win Per Player': 10,
342                'Respawn Times': 1.0,
343                'Time Limit': 300,
344                'map': 'Crag Castle',
345            },
346            'type': 'bs_death_match.DeathMatchGame',
347        },
348        {
349            'map': 'Lake Frigid',
350            'settings': {
351                'Bomb Spawning': 0,
352                'Epic Mode': False,
353                'Laps': 6,
354                'Mine Spawning': 2000,
355                'Time Limit': 300,
356                'map': 'Lake Frigid',
357            },
358            'type': 'bs_race.RaceGame',
359        },
360    ]

Return a default playlist for free-for-all mode.

def get_default_teams_playlist() -> list[dict[str, typing.Any]]:
363def get_default_teams_playlist() -> PlaylistType:
364    """Return a default playlist for teams mode."""
365
366    # NOTE: these are currently using old type/map names,
367    # but filtering translates them properly to the new ones.
368    # (is kinda a handy way to ensure filtering is working).
369    # Eventually should update these though.
370    return [
371        {
372            'settings': {
373                'Epic Mode': False,
374                'Flag Idle Return Time': 30,
375                'Flag Touch Return Time': 0,
376                'Respawn Times': 1.0,
377                'Score to Win': 3,
378                'Time Limit': 600,
379                'map': 'Bridgit',
380            },
381            'type': 'bs_capture_the_flag.CTFGame',
382        },
383        {
384            'settings': {
385                'Epic Mode': False,
386                'Respawn Times': 1.0,
387                'Score to Win': 3,
388                'Time Limit': 600,
389                'map': 'Step Right Up',
390            },
391            'type': 'bs_assault.AssaultGame',
392        },
393        {
394            'settings': {
395                'Balance Total Lives': False,
396                'Epic Mode': False,
397                'Lives Per Player': 3,
398                'Respawn Times': 1.0,
399                'Solo Mode': True,
400                'Time Limit': 600,
401                'map': 'Rampage',
402            },
403            'type': 'bs_elimination.EliminationGame',
404        },
405        {
406            'settings': {
407                'Epic Mode': False,
408                'Kills to Win Per Player': 5,
409                'Respawn Times': 1.0,
410                'Time Limit': 300,
411                'map': 'Roundabout',
412            },
413            'type': 'bs_death_match.DeathMatchGame',
414        },
415        {
416            'settings': {
417                'Respawn Times': 1.0,
418                'Score to Win': 1,
419                'Time Limit': 600,
420                'map': 'Hockey Stadium',
421            },
422            'type': 'bs_hockey.HockeyGame',
423        },
424        {
425            'settings': {
426                'Hold Time': 30,
427                'Respawn Times': 1.0,
428                'Time Limit': 300,
429                'map': 'Monkey Face',
430            },
431            'type': 'bs_keep_away.KeepAwayGame',
432        },
433        {
434            'settings': {
435                'Balance Total Lives': False,
436                'Epic Mode': True,
437                'Lives Per Player': 1,
438                'Respawn Times': 1.0,
439                'Solo Mode': False,
440                'Time Limit': 120,
441                'map': 'Tip Top',
442            },
443            'type': 'bs_elimination.EliminationGame',
444        },
445        {
446            'settings': {
447                'Epic Mode': False,
448                'Respawn Times': 1.0,
449                'Score to Win': 3,
450                'Time Limit': 300,
451                'map': 'Crag Castle',
452            },
453            'type': 'bs_assault.AssaultGame',
454        },
455        {
456            'settings': {
457                'Epic Mode': False,
458                'Kills to Win Per Player': 5,
459                'Respawn Times': 1.0,
460                'Time Limit': 300,
461                'map': 'Doom Shroom',
462            },
463            'type': 'bs_death_match.DeathMatchGame',
464        },
465        {
466            'settings': {'Epic Mode': False, 'map': 'Rampage'},
467            'type': 'bs_meteor_shower.MeteorShowerGame',
468        },
469        {
470            'settings': {
471                'Epic Mode': False,
472                'Flag Idle Return Time': 30,
473                'Flag Touch Return Time': 0,
474                'Respawn Times': 1.0,
475                'Score to Win': 2,
476                'Time Limit': 600,
477                'map': 'Roundabout',
478            },
479            'type': 'bs_capture_the_flag.CTFGame',
480        },
481        {
482            'settings': {
483                'Respawn Times': 1.0,
484                'Score to Win': 21,
485                'Time Limit': 600,
486                'map': 'Football Stadium',
487            },
488            'type': 'bs_football.FootballTeamGame',
489        },
490        {
491            'settings': {
492                'Epic Mode': True,
493                'Respawn Times': 0.25,
494                'Score to Win': 3,
495                'Time Limit': 120,
496                'map': 'Bridgit',
497            },
498            'type': 'bs_assault.AssaultGame',
499        },
500        {
501            'map': 'Doom Shroom',
502            'settings': {
503                'Enable Impact Bombs': 1,
504                'Enable Triple Bombs': False,
505                'Target Count': 2,
506                'map': 'Doom Shroom',
507            },
508            'type': 'bs_target_practice.TargetPracticeGame',
509        },
510        {
511            'settings': {
512                'Hold Time': 30,
513                'Respawn Times': 1.0,
514                'Time Limit': 300,
515                'map': 'Tip Top',
516            },
517            'type': 'bs_king_of_the_hill.KingOfTheHillGame',
518        },
519        {
520            'settings': {
521                'Epic Mode': False,
522                'Respawn Times': 1.0,
523                'Score to Win': 2,
524                'Time Limit': 300,
525                'map': 'Zigzag',
526            },
527            'type': 'bs_assault.AssaultGame',
528        },
529        {
530            'settings': {
531                'Epic Mode': False,
532                'Flag Idle Return Time': 30,
533                'Flag Touch Return Time': 0,
534                'Respawn Times': 1.0,
535                'Score to Win': 3,
536                'Time Limit': 300,
537                'map': 'Happy Thoughts',
538            },
539            'type': 'bs_capture_the_flag.CTFGame',
540        },
541        {
542            'settings': {
543                'Bomb Spawning': 1000,
544                'Epic Mode': True,
545                'Laps': 1,
546                'Mine Spawning': 2000,
547                'Time Limit': 300,
548                'map': 'Big G',
549            },
550            'type': 'bs_race.RaceGame',
551        },
552        {
553            'settings': {
554                'Epic Mode': False,
555                'Kills to Win Per Player': 5,
556                'Respawn Times': 1.0,
557                'Time Limit': 300,
558                'map': 'Monkey Face',
559            },
560            'type': 'bs_death_match.DeathMatchGame',
561        },
562        {
563            'settings': {
564                'Hold Time': 30,
565                'Respawn Times': 1.0,
566                'Time Limit': 300,
567                'map': 'Lake Frigid',
568            },
569            'type': 'bs_keep_away.KeepAwayGame',
570        },
571        {
572            'settings': {
573                'Epic Mode': False,
574                'Flag Idle Return Time': 30,
575                'Flag Touch Return Time': 3,
576                'Respawn Times': 1.0,
577                'Score to Win': 2,
578                'Time Limit': 300,
579                'map': 'Tip Top',
580            },
581            'type': 'bs_capture_the_flag.CTFGame',
582        },
583        {
584            'settings': {
585                'Balance Total Lives': False,
586                'Epic Mode': False,
587                'Lives Per Player': 3,
588                'Respawn Times': 1.0,
589                'Solo Mode': False,
590                'Time Limit': 300,
591                'map': 'Crag Castle',
592            },
593            'type': 'bs_elimination.EliminationGame',
594        },
595        {
596            'settings': {
597                'Epic Mode': True,
598                'Respawn Times': 0.25,
599                'Time Limit': 120,
600                'map': 'Zigzag',
601            },
602            'type': 'bs_conquest.ConquestGame',
603        },
604    ]

Return a default playlist for teams mode.

def get_default_powerup_distribution() -> Sequence[tuple[str, int]]:
46def get_default_powerup_distribution() -> Sequence[tuple[str, int]]:
47    """Standard set of powerups."""
48    return (
49        ('triple_bombs', 3),
50        ('ice_bombs', 3),
51        ('punch', 3),
52        ('impact_bombs', 3),
53        ('land_mines', 2),
54        ('sticky_bombs', 3),
55        ('shield', 2),
56        ('health', 1),
57        ('curse', 1),
58    )

Standard set of powerups.

def get_filtered_map_name(name: str) -> str:
21def get_filtered_map_name(name: str) -> str:
22    """Filter a map name to account for name changes, etc.
23
24    This can be used to support old playlists, etc.
25    """
26    # Some legacy name fallbacks... can remove these eventually.
27    if name in ('AlwaysLand', 'Happy Land'):
28        name = 'Happy Thoughts'
29    if name == 'Hockey Arena':
30        name = 'Hockey Stadium'
31    return name

Filter a map name to account for name changes, etc.

This can be used to support old playlists, etc.

def get_map_class(name: str) -> type[Map]:
39def get_map_class(name: str) -> type[Map]:
40    """Return a map type given a name."""
41    assert babase.app.classic is not None
42    name = get_filtered_map_name(name)
43    try:
44        mapclass: type[Map] = babase.app.classic.maps[name]
45        return mapclass
46    except KeyError:
47        raise babase.NotFoundError(f"Map not found: '{name}'") from None

Return a map type given a name.

def get_map_display_string(name: str) -> Lstr:
34def get_map_display_string(name: str) -> babase.Lstr:
35    """Return a babase.Lstr for displaying a given map's name."""
36    return babase.Lstr(translate=('mapsNames', name))

Return a babase.Lstr for displaying a given map's name.

def get_player_colors() -> list[tuple[float, float, float]]:
37def get_player_colors() -> list[tuple[float, float, float]]:
38    """Return user-selectable player colors."""
39    return PLAYER_COLORS

Return user-selectable player colors.

def get_player_profile_colors( profilename: str | None, profiles: dict[str, dict[str, typing.Any]] | None = None) -> tuple[tuple[float, float, float], tuple[float, float, float]]:
 63def get_player_profile_colors(
 64    profilename: str | None, profiles: dict[str, dict[str, Any]] | None = None
 65) -> tuple[tuple[float, float, float], tuple[float, float, float]]:
 66    """Given a profile, return colors for them."""
 67    appconfig = babase.app.config
 68    if profiles is None:
 69        profiles = appconfig['Player Profiles']
 70
 71    # Special case: when being asked for a random color in kiosk mode,
 72    # always return default purple.
 73    if (babase.app.env.demo or babase.app.env.arcade) and profilename is None:
 74        color = (0.5, 0.4, 1.0)
 75        highlight = (0.4, 0.4, 0.5)
 76    else:
 77        try:
 78            assert profilename is not None
 79            color = profiles[profilename]['color']
 80        except (KeyError, AssertionError):
 81            # Key off name if possible.
 82            if profilename is None:
 83                # First 6 are bright-ish.
 84                color = PLAYER_COLORS[random.randrange(6)]
 85            else:
 86                # First 6 are bright-ish.
 87                color = PLAYER_COLORS[sum(ord(c) for c in profilename) % 6]
 88
 89        try:
 90            assert profilename is not None
 91            highlight = profiles[profilename]['highlight']
 92        except (KeyError, AssertionError):
 93            # Key off name if possible.
 94            if profilename is None:
 95                # Last 2 are grey and white; ignore those or we
 96                # get lots of old-looking players.
 97                highlight = PLAYER_COLORS[
 98                    random.randrange(len(PLAYER_COLORS) - 2)
 99                ]
100            else:
101                highlight = PLAYER_COLORS[
102                    sum(ord(c) + 1 for c in profilename)
103                    % (len(PLAYER_COLORS) - 2)
104                ]
105
106    return color, highlight

Given a profile, return colors for them.

def get_player_profile_icon(profilename: str) -> str:
42def get_player_profile_icon(profilename: str) -> str:
43    """Given a profile name, returns an icon string for it.
44
45    (non-account profiles only)
46    """
47    appconfig = babase.app.config
48    icon: str
49    try:
50        is_global = appconfig['Player Profiles'][profilename]['global']
51    except KeyError:
52        is_global = False
53    if is_global:
54        try:
55            icon = appconfig['Player Profiles'][profilename]['icon']
56        except KeyError:
57            icon = babase.charstr(babase.SpecialChar.LOGO)
58    else:
59        icon = ''
60    return icon

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

(non-account profiles only)

def get_trophy_string(trophy_id: str) -> str:
42def get_trophy_string(trophy_id: str) -> str:
43    """Given a trophy id, returns a string to visualize it."""
44    if trophy_id in TROPHY_CHARS:
45        return babase.charstr(TROPHY_CHARS[trophy_id])
46    return '?'

Given a trophy id, returns a string to visualize it.

def getactivity(doraise: bool = True) -> Activity | None:
1284def getactivity(doraise: bool = True) -> bascenev1.Activity | None:
1285    """Return the current bascenev1.Activity instance.
1286
1287    Note that this is based on context_ref; thus code run in a timer
1288    generated in Activity 'foo' will properly return 'foo' here, even if
1289    another Activity has since been created or is transitioning in.
1290    If there is no current Activity, raises a babase.ActivityNotFoundError.
1291    If doraise is False, None will be returned instead in that case.
1292    """
1293    return None

Return the current bascenev1.Activity instance.

Note that this is based on context_ref; thus code run in a timer generated in Activity 'foo' will properly return 'foo' here, even if another Activity has since been created or is transitioning in. If there is no current Activity, raises a babase.ActivityNotFoundError. If doraise is False, None will be returned instead in that case.

def getcollision() -> Collision:
65def getcollision() -> Collision:
66    """Return the in-progress collision."""
67    return _collision

Return the in-progress collision.

def getcollisionmesh(name: str) -> _bascenev1.CollisionMesh:
1296def getcollisionmesh(name: str) -> bascenev1.CollisionMesh:
1297    """Return a collision-mesh, loading it if necessary.
1298
1299    Collision-meshes are used in physics calculations for such things as
1300    terrain.
1301
1302    Note that this function returns immediately even if the asset has yet
1303    to be loaded. To avoid hitches, instantiate your asset objects in
1304    advance of when you will be using them, allowing time for them to
1305    load in the background if necessary.
1306    """
1307    import bascenev1  # pylint: disable=cyclic-import
1308
1309    return bascenev1.CollisionMesh()

Return a collision-mesh, loading it if necessary.

Collision-meshes are used in physics calculations for such things as terrain.

Note that this function returns immediately even if the asset has yet to be loaded. To avoid hitches, instantiate your asset objects in advance of when you will be using them, allowing time for them to load in the background if necessary.

def getdata(name: str) -> _bascenev1.Data:
1312def getdata(name: str) -> bascenev1.Data:
1313    """Return a data, loading it if necessary.
1314
1315    Note that this function returns immediately even if the asset has yet
1316    to be loaded. To avoid hitches, instantiate your asset objects in
1317    advance of when you will be using them, allowing time for them to
1318    load in the background if necessary.
1319    """
1320    import bascenev1  # pylint: disable=cyclic-import
1321
1322    return bascenev1.Data()

Return a data, loading it if necessary.

Note that this function returns immediately even if the asset has yet to be loaded. To avoid hitches, instantiate your asset objects in advance of when you will be using them, allowing time for them to load in the background if necessary.

def getmesh(name: str) -> _bascenev1.Mesh:
1348def getmesh(name: str) -> bascenev1.Mesh:
1349    """Return a mesh, loading it if necessary.
1350
1351    Note that this function returns immediately even if the asset has yet
1352    to be loaded. To avoid hitches, instantiate your asset objects in
1353    advance of when you will be using them, allowing time for them to
1354     load in the background if necessary.
1355    """
1356    import bascenev1  # pylint: disable=cyclic-import
1357
1358    return bascenev1.Mesh()

Return a mesh, loading it if necessary.

Note that this function returns immediately even if the asset has yet to be loaded. To avoid hitches, instantiate your asset objects in advance of when you will be using them, allowing time for them to load in the background if necessary.

def getnodes() -> list:
1361def getnodes() -> list:
1362    """Return all nodes in the current bascenev1.Context."""
1363    return list()

Return all nodes in the current bascenev1.Context.

def getsession(doraise: bool = True) -> Session | None:
1375def getsession(doraise: bool = True) -> bascenev1.Session | None:
1376    """Returns the current bascenev1.Session instance.
1377    Note that this is based on context_ref; thus code being run in the UI
1378    context will return the UI context_ref here even if a game Session also
1379    exists, etc. If there is no current Session, an Exception is raised, or
1380    if doraise is False then None is returned instead.
1381    """
1382    return None

Returns the current bascenev1.Session instance. Note that this is based on context_ref; thus code being run in the UI context will return the UI context_ref here even if a game Session also exists, etc. If there is no current Session, an Exception is raised, or if doraise is False then None is returned instead.

def getsound(name: str) -> _bascenev1.Sound:
1385def getsound(name: str) -> bascenev1.Sound:
1386    """Return a sound, loading it if necessary.
1387
1388    Note that this function returns immediately even if the asset has yet
1389    to be loaded. To avoid hitches, instantiate your asset objects in
1390    advance of when you will be using them, allowing time for them to
1391    load in the background if necessary.
1392    """
1393    import bascenev1  # pylint: disable=cyclic-import
1394
1395    return bascenev1.Sound()

Return a sound, loading it if necessary.

Note that this function returns immediately even if the asset has yet to be loaded. To avoid hitches, instantiate your asset objects in advance of when you will be using them, allowing time for them to load in the background if necessary.

def gettexture(name: str) -> _bascenev1.Texture:
1398def gettexture(name: str) -> bascenev1.Texture:
1399    """Return a texture, loading it if necessary.
1400
1401    Note that this function returns immediately even if the asset has yet
1402    to be loaded. To avoid hitches, instantiate your asset objects in
1403    advance of when you will be using them, allowing time for them to
1404    load in the background if necessary.
1405    """
1406    import bascenev1  # pylint: disable=cyclic-import
1407
1408    return bascenev1.Texture()

Return a texture, loading it if necessary.

Note that this function returns immediately even if the asset has yet to be loaded. To avoid hitches, instantiate your asset objects in advance of when you will be using them, allowing time for them to load in the background if necessary.

class HitMessage:
198class HitMessage:
199    """Tells an object it has been hit in some way.
200
201    This is used by punches, explosions, etc to convey their effect to a
202    target.
203    """
204
205    def __init__(
206        self,
207        *,
208        srcnode: bascenev1.Node | None = None,
209        pos: Sequence[float] | None = None,
210        velocity: Sequence[float] | None = None,
211        magnitude: float = 1.0,
212        velocity_magnitude: float = 0.0,
213        radius: float = 1.0,
214        source_player: bascenev1.Player | None = None,
215        kick_back: float = 1.0,
216        flat_damage: float | None = None,
217        hit_type: str = 'generic',
218        force_direction: Sequence[float] | None = None,
219        hit_subtype: str = 'default',
220    ):
221        """Instantiate a message with given values."""
222
223        self.srcnode = srcnode
224        self.pos = pos if pos is not None else babase.Vec3()
225        self.velocity = velocity if velocity is not None else babase.Vec3()
226        self.magnitude = magnitude
227        self.velocity_magnitude = velocity_magnitude
228        self.radius = radius
229
230        # We should not be getting passed an invalid ref.
231        assert source_player is None or source_player.exists()
232        self._source_player = source_player
233        self.kick_back = kick_back
234        self.flat_damage = flat_damage
235        self.hit_type = hit_type
236        self.hit_subtype = hit_subtype
237        self.force_direction = (
238            force_direction if force_direction is not None else velocity
239        )
240
241    def get_source_player(self, playertype: type[PlayerT]) -> PlayerT | None:
242        """Return the source-player if one exists and is the provided type."""
243        player: Any = self._source_player
244
245        # We should not be delivering invalid refs.
246        # (we could translate to None here but technically we are changing
247        # the message delivered which seems wrong)
248        assert player is None or player.exists()
249
250        # Return the player *only* if they're the type given.
251        return player if isinstance(player, playertype) else None

Tells an object it has been hit in some way.

This is used by punches, explosions, etc to convey their effect to a target.

HitMessage( *, srcnode: _bascenev1.Node | None = None, pos: Optional[Sequence[float]] = None, velocity: Optional[Sequence[float]] = None, magnitude: float = 1.0, velocity_magnitude: float = 0.0, radius: float = 1.0, source_player: Player | None = None, kick_back: float = 1.0, flat_damage: float | None = None, hit_type: str = 'generic', force_direction: Optional[Sequence[float]] = None, hit_subtype: str = 'default')
205    def __init__(
206        self,
207        *,
208        srcnode: bascenev1.Node | None = None,
209        pos: Sequence[float] | None = None,
210        velocity: Sequence[float] | None = None,
211        magnitude: float = 1.0,
212        velocity_magnitude: float = 0.0,
213        radius: float = 1.0,
214        source_player: bascenev1.Player | None = None,
215        kick_back: float = 1.0,
216        flat_damage: float | None = None,
217        hit_type: str = 'generic',
218        force_direction: Sequence[float] | None = None,
219        hit_subtype: str = 'default',
220    ):
221        """Instantiate a message with given values."""
222
223        self.srcnode = srcnode
224        self.pos = pos if pos is not None else babase.Vec3()
225        self.velocity = velocity if velocity is not None else babase.Vec3()
226        self.magnitude = magnitude
227        self.velocity_magnitude = velocity_magnitude
228        self.radius = radius
229
230        # We should not be getting passed an invalid ref.
231        assert source_player is None or source_player.exists()
232        self._source_player = source_player
233        self.kick_back = kick_back
234        self.flat_damage = flat_damage
235        self.hit_type = hit_type
236        self.hit_subtype = hit_subtype
237        self.force_direction = (
238            force_direction if force_direction is not None else velocity
239        )

Instantiate a message with given values.

srcnode
pos
velocity
magnitude
velocity_magnitude
radius
kick_back
flat_damage
hit_type
hit_subtype
force_direction
def get_source_player(self, playertype: type[~PlayerT]) -> Optional[~PlayerT]:
241    def get_source_player(self, playertype: type[PlayerT]) -> PlayerT | None:
242        """Return the source-player if one exists and is the provided type."""
243        player: Any = self._source_player
244
245        # We should not be delivering invalid refs.
246        # (we could translate to None here but technically we are changing
247        # the message delivered which seems wrong)
248        assert player is None or player.exists()
249
250        # Return the player *only* if they're the type given.
251        return player if isinstance(player, playertype) else None

Return the source-player if one exists and is the provided type.

@dataclass
class HostInfo:
14@dataclass
15class HostInfo:
16    """Info about a host."""
17
18    name: str
19    build_number: int
20
21    # Note this can be None for non-ip hosts such as bluetooth.
22    address: str | None
23
24    # Note this can be None for non-ip hosts such as bluetooth.
25    port: int | None

Info about a host.

HostInfo(name: str, build_number: int, address: str | None, port: int | None)
name: str
build_number: int
address: str | None
port: int | None
@dataclass
class ImpactDamageMessage:
166@dataclass
167class ImpactDamageMessage:
168    """Tells an object that it has been jarred violently."""
169
170    intensity: float
171    """The intensity of the impact."""

Tells an object that it has been jarred violently.

ImpactDamageMessage(intensity: float)
intensity: float

The intensity of the impact.

def init_campaigns() -> None:
102def init_campaigns() -> None:
103    """Fill out initial default Campaigns."""
104    # pylint: disable=cyclic-import
105    from bascenev1._level import Level
106    from bascenev1lib.game.onslaught import OnslaughtGame
107    from bascenev1lib.game.football import FootballCoopGame
108    from bascenev1lib.game.runaround import RunaroundGame
109    from bascenev1lib.game.thelaststand import TheLastStandGame
110    from bascenev1lib.game.race import RaceGame
111    from bascenev1lib.game.targetpractice import TargetPracticeGame
112    from bascenev1lib.game.meteorshower import MeteorShowerGame
113    from bascenev1lib.game.easteregghunt import EasterEggHuntGame
114    from bascenev1lib.game.ninjafight import NinjaFightGame
115
116    # TODO: Campaigns should be load-on-demand; not all imported at launch
117    #  like this.
118
119    # FIXME: Once translations catch up, we can convert these to use the
120    #  generic display-name '${GAME} Training' type stuff.
121    register_campaign(
122        Campaign(
123            'Easy',
124            levels=[
125                Level(
126                    'Onslaught Training',
127                    gametype=OnslaughtGame,
128                    settings={'preset': 'training_easy'},
129                    preview_texture_name='doomShroomPreview',
130                ),
131                Level(
132                    'Rookie Onslaught',
133                    gametype=OnslaughtGame,
134                    settings={'preset': 'rookie_easy'},
135                    preview_texture_name='courtyardPreview',
136                ),
137                Level(
138                    'Rookie Football',
139                    gametype=FootballCoopGame,
140                    settings={'preset': 'rookie_easy'},
141                    preview_texture_name='footballStadiumPreview',
142                ),
143                Level(
144                    'Pro Onslaught',
145                    gametype=OnslaughtGame,
146                    settings={'preset': 'pro_easy'},
147                    preview_texture_name='doomShroomPreview',
148                ),
149                Level(
150                    'Pro Football',
151                    gametype=FootballCoopGame,
152                    settings={'preset': 'pro_easy'},
153                    preview_texture_name='footballStadiumPreview',
154                ),
155                Level(
156                    'Pro Runaround',
157                    gametype=RunaroundGame,
158                    settings={'preset': 'pro_easy'},
159                    preview_texture_name='towerDPreview',
160                ),
161                Level(
162                    'Uber Onslaught',
163                    gametype=OnslaughtGame,
164                    settings={'preset': 'uber_easy'},
165                    preview_texture_name='courtyardPreview',
166                ),
167                Level(
168                    'Uber Football',
169                    gametype=FootballCoopGame,
170                    settings={'preset': 'uber_easy'},
171                    preview_texture_name='footballStadiumPreview',
172                ),
173                Level(
174                    'Uber Runaround',
175                    gametype=RunaroundGame,
176                    settings={'preset': 'uber_easy'},
177                    preview_texture_name='towerDPreview',
178                ),
179            ],
180        )
181    )
182
183    # "hard" mode
184    register_campaign(
185        Campaign(
186            'Default',
187            levels=[
188                Level(
189                    'Onslaught Training',
190                    gametype=OnslaughtGame,
191                    settings={'preset': 'training'},
192                    preview_texture_name='doomShroomPreview',
193                ),
194                Level(
195                    'Rookie Onslaught',
196                    gametype=OnslaughtGame,
197                    settings={'preset': 'rookie'},
198                    preview_texture_name='courtyardPreview',
199                ),
200                Level(
201                    'Rookie Football',
202                    gametype=FootballCoopGame,
203                    settings={'preset': 'rookie'},
204                    preview_texture_name='footballStadiumPreview',
205                ),
206                Level(
207                    'Pro Onslaught',
208                    gametype=OnslaughtGame,
209                    settings={'preset': 'pro'},
210                    preview_texture_name='doomShroomPreview',
211                ),
212                Level(
213                    'Pro Football',
214                    gametype=FootballCoopGame,
215                    settings={'preset': 'pro'},
216                    preview_texture_name='footballStadiumPreview',
217                ),
218                Level(
219                    'Pro Runaround',
220                    gametype=RunaroundGame,
221                    settings={'preset': 'pro'},
222                    preview_texture_name='towerDPreview',
223                ),
224                Level(
225                    'Uber Onslaught',
226                    gametype=OnslaughtGame,
227                    settings={'preset': 'uber'},
228                    preview_texture_name='courtyardPreview',
229                ),
230                Level(
231                    'Uber Football',
232                    gametype=FootballCoopGame,
233                    settings={'preset': 'uber'},
234                    preview_texture_name='footballStadiumPreview',
235                ),
236                Level(
237                    'Uber Runaround',
238                    gametype=RunaroundGame,
239                    settings={'preset': 'uber'},
240                    preview_texture_name='towerDPreview',
241                ),
242                Level(
243                    'The Last Stand',
244                    gametype=TheLastStandGame,
245                    settings={},
246                    preview_texture_name='rampagePreview',
247                ),
248            ],
249        )
250    )
251
252    # challenges: our 'official' random extra co-op levels
253    register_campaign(
254        Campaign(
255            'Challenges',
256            sequential=False,
257            levels=[
258                Level(
259                    'Infinite Onslaught',
260                    gametype=OnslaughtGame,
261                    settings={'preset': 'endless'},
262                    preview_texture_name='doomShroomPreview',
263                ),
264                Level(
265                    'Infinite Runaround',
266                    gametype=RunaroundGame,
267                    settings={'preset': 'endless'},
268                    preview_texture_name='towerDPreview',
269                ),
270                Level(
271                    'Race',
272                    displayname='${GAME}',
273                    gametype=RaceGame,
274                    settings={'map': 'Big G', 'Laps': 3, 'Bomb Spawning': 0},
275                    preview_texture_name='bigGPreview',
276                ),
277                Level(
278                    'Pro Race',
279                    displayname='Pro ${GAME}',
280                    gametype=RaceGame,
281                    settings={'map': 'Big G', 'Laps': 3, 'Bomb Spawning': 1000},
282                    preview_texture_name='bigGPreview',
283                ),
284                Level(
285                    'Lake Frigid Race',
286                    displayname='${GAME}',
287                    gametype=RaceGame,
288                    settings={
289                        'map': 'Lake Frigid',
290                        'Laps': 6,
291                        'Mine Spawning': 2000,
292                        'Bomb Spawning': 0,
293                    },
294                    preview_texture_name='lakeFrigidPreview',
295                ),
296                Level(
297                    'Football',
298                    displayname='${GAME}',
299                    gametype=FootballCoopGame,
300                    settings={'preset': 'tournament'},
301                    preview_texture_name='footballStadiumPreview',
302                ),
303                Level(
304                    'Pro Football',
305                    displayname='Pro ${GAME}',
306                    gametype=FootballCoopGame,
307                    settings={'preset': 'tournament_pro'},
308                    preview_texture_name='footballStadiumPreview',
309                ),
310                Level(
311                    'Runaround',
312                    displayname='${GAME}',
313                    gametype=RunaroundGame,
314                    settings={'preset': 'tournament'},
315                    preview_texture_name='towerDPreview',
316                ),
317                Level(
318                    'Uber Runaround',
319                    displayname='Uber ${GAME}',
320                    gametype=RunaroundGame,
321                    settings={'preset': 'tournament_uber'},
322                    preview_texture_name='towerDPreview',
323                ),
324                Level(
325                    'The Last Stand',
326                    displayname='${GAME}',
327                    gametype=TheLastStandGame,
328                    settings={'preset': 'tournament'},
329                    preview_texture_name='rampagePreview',
330                ),
331                Level(
332                    'Tournament Infinite Onslaught',
333                    displayname='Infinite Onslaught',
334                    gametype=OnslaughtGame,
335                    settings={'preset': 'endless_tournament'},
336                    preview_texture_name='doomShroomPreview',
337                ),
338                Level(
339                    'Tournament Infinite Runaround',
340                    displayname='Infinite Runaround',
341                    gametype=RunaroundGame,
342                    settings={'preset': 'endless_tournament'},
343                    preview_texture_name='towerDPreview',
344                ),
345                Level(
346                    'Target Practice',
347                    displayname='Pro ${GAME}',
348                    gametype=TargetPracticeGame,
349                    settings={},
350                    preview_texture_name='doomShroomPreview',
351                ),
352                Level(
353                    'Target Practice B',
354                    displayname='${GAME}',
355                    gametype=TargetPracticeGame,
356                    settings={
357                        'Target Count': 2,
358                        'Enable Impact Bombs': False,
359                        'Enable Triple Bombs': False,
360                    },
361                    preview_texture_name='doomShroomPreview',
362                ),
363                Level(
364                    'Meteor Shower',
365                    displayname='${GAME}',
366                    gametype=MeteorShowerGame,
367                    settings={},
368                    preview_texture_name='rampagePreview',
369                ),
370                Level(
371                    'Epic Meteor Shower',
372                    displayname='${GAME}',
373                    gametype=MeteorShowerGame,
374                    settings={'Epic Mode': True},
375                    preview_texture_name='rampagePreview',
376                ),
377                Level(
378                    'Easter Egg Hunt',
379                    displayname='${GAME}',
380                    gametype=EasterEggHuntGame,
381                    settings={},
382                    preview_texture_name='towerDPreview',
383                ),
384                Level(
385                    'Pro Easter Egg Hunt',
386                    displayname='Pro ${GAME}',
387                    gametype=EasterEggHuntGame,
388                    settings={'Pro Mode': True},
389                    preview_texture_name='towerDPreview',
390                ),
391                Level(
392                    name='Ninja Fight',  # (unique id not seen by player)
393                    displayname='${GAME}',  # (readable name seen by player)
394                    gametype=NinjaFightGame,
395                    settings={'preset': 'regular'},
396                    preview_texture_name='courtyardPreview',
397                ),
398                Level(
399                    name='Pro Ninja Fight',
400                    displayname='Pro ${GAME}',
401                    gametype=NinjaFightGame,
402                    settings={'preset': 'pro'},
403                    preview_texture_name='courtyardPreview',
404                ),
405            ],
406        )
407    )

Fill out initial default Campaigns.

class InputDevice:
155class InputDevice:
156    """An input-device such as a gamepad, touchscreen, or keyboard."""
157
158    allows_configuring: bool
159    """Whether the input-device can be configured in the app."""
160
161    allows_configuring_in_system_settings: bool
162    """Whether the input-device can be configured in the system.
163       setings app. This can be used to redirect the user to go there
164       if they attempt to configure the device."""
165
166    has_meaningful_button_names: bool
167    """Whether button names returned by this instance match labels
168       on the actual device. (Can be used to determine whether to show
169       them in controls-overlays, etc.)."""
170
171    player: bascenev1.SessionPlayer | None
172    """The player associated with this input device."""
173
174    client_id: int
175    """The numeric client-id this device is associated with.
176       This is only meaningful for remote client inputs; for
177       all local devices this will be -1."""
178
179    name: str
180    """The name of the device."""
181
182    unique_identifier: str
183    """A string that can be used to persistently identify the device,
184       even among other devices of the same type. Used for saving
185       prefs, etc."""
186
187    id: int
188    """The unique numeric id of this device."""
189
190    instance_number: int
191    """The number of this device among devices of the same type."""
192
193    is_controller_app: bool
194    """Whether this input-device represents a locally-connected
195       controller-app."""
196
197    is_remote_client: bool
198    """Whether this input-device represents a remotely-connected
199       client."""
200
201    is_test_input: bool
202    """Whether this input-device is a dummy device for testing."""
203
204    def __bool__(self) -> bool:
205        """Support for bool evaluation."""
206        return bool(True)  # Slight obfuscation.
207
208    def detach_from_player(self) -> None:
209        """Detach the device from any player it is controlling.
210
211        This applies both to local players and remote players.
212        """
213        return None
214
215    def exists(self) -> bool:
216        """Return whether the underlying device for this object is
217        still present.
218        """
219        return bool()
220
221    def get_axis_name(self, axis_id: int) -> str:
222        """Given an axis ID, return the name of the axis on this device.
223
224        Can return an empty string if the value is not meaningful to humans.
225        """
226        return str()
227
228    def get_button_name(self, button_id: int) -> babase.Lstr:
229        """Given a button ID, return a human-readable name for that key/button.
230
231        Can return an empty string if the value is not meaningful to humans.
232        """
233        import babase  # pylint: disable=cyclic-import
234
235        return babase.Lstr(value='')
236
237    def get_default_player_name(self) -> str:
238        """(internal)
239
240        Returns the default player name for this device. (used for the 'random'
241        profile)
242        """
243        return str()
244
245    def get_player_profiles(self) -> dict:
246        """(internal)"""
247        return dict()
248
249    def get_v1_account_name(self, full: bool) -> str:
250        """Returns the account name associated with this device.
251
252        (can be used to get account names for remote players)
253        """
254        return str()
255
256    def is_attached_to_player(self) -> bool:
257        """Return whether this device is controlling a player of some sort.
258
259        This can mean either a local player or a remote player.
260        """
261        return bool()

An input-device such as a gamepad, touchscreen, or keyboard.

allows_configuring: bool

Whether the input-device can be configured in the app.

allows_configuring_in_system_settings: bool

Whether the input-device can be configured in the system. setings app. This can be used to redirect the user to go there if they attempt to configure the device.

has_meaningful_button_names: bool

Whether button names returned by this instance match labels on the actual device. (Can be used to determine whether to show them in controls-overlays, etc.).

player: _bascenev1.SessionPlayer | None

The player associated with this input device.

client_id: int

The numeric client-id this device is associated with. This is only meaningful for remote client inputs; for all local devices this will be -1.

name: str

The name of the device.

unique_identifier: str

A string that can be used to persistently identify the device, even among other devices of the same type. Used for saving prefs, etc.

id: int

The unique numeric id of this device.

instance_number: int

The number of this device among devices of the same type.

is_controller_app: bool

Whether this input-device represents a locally-connected controller-app.

is_remote_client: bool

Whether this input-device represents a remotely-connected client.

is_test_input: bool

Whether this input-device is a dummy device for testing.

def detach_from_player(self) -> None:
208    def detach_from_player(self) -> None:
209        """Detach the device from any player it is controlling.
210
211        This applies both to local players and remote players.
212        """
213        return None

Detach the device from any player it is controlling.

This applies both to local players and remote players.

def exists(self) -> bool:
215    def exists(self) -> bool:
216        """Return whether the underlying device for this object is
217        still present.
218        """
219        return bool()

Return whether the underlying device for this object is still present.

def get_axis_name(self, axis_id: int) -> str:
221    def get_axis_name(self, axis_id: int) -> str:
222        """Given an axis ID, return the name of the axis on this device.
223
224        Can return an empty string if the value is not meaningful to humans.
225        """
226        return str()

Given an axis ID, return the name of the axis on this device.

Can return an empty string if the value is not meaningful to humans.

def get_button_name(self, button_id: int) -> Lstr:
228    def get_button_name(self, button_id: int) -> babase.Lstr:
229        """Given a button ID, return a human-readable name for that key/button.
230
231        Can return an empty string if the value is not meaningful to humans.
232        """
233        import babase  # pylint: disable=cyclic-import
234
235        return babase.Lstr(value='')

Given a button ID, return a human-readable name for that key/button.

Can return an empty string if the value is not meaningful to humans.

def get_v1_account_name(self, full: bool) -> str:
249    def get_v1_account_name(self, full: bool) -> str:
250        """Returns the account name associated with this device.
251
252        (can be used to get account names for remote players)
253        """
254        return str()

Returns the account name associated with this device.

(can be used to get account names for remote players)

def is_attached_to_player(self) -> bool:
256    def is_attached_to_player(self) -> bool:
257        """Return whether this device is controlling a player of some sort.
258
259        This can mean either a local player or a remote player.
260        """
261        return bool()

Return whether this device is controlling a player of some sort.

This can mean either a local player or a remote player.

class InputType(enum.Enum):
 8class InputType(Enum):
 9    """Types of input a controller can send to the game.
10
11    """
12
13    UP_DOWN = 2
14    LEFT_RIGHT = 3
15    JUMP_PRESS = 4
16    JUMP_RELEASE = 5
17    PUNCH_PRESS = 6
18    PUNCH_RELEASE = 7
19    BOMB_PRESS = 8
20    BOMB_RELEASE = 9
21    PICK_UP_PRESS = 10
22    PICK_UP_RELEASE = 11
23    RUN = 12
24    FLY_PRESS = 13
25    FLY_RELEASE = 14
26    START_PRESS = 15
27    START_RELEASE = 16
28    HOLD_POSITION_PRESS = 17
29    HOLD_POSITION_RELEASE = 18
30    LEFT_PRESS = 19
31    LEFT_RELEASE = 20
32    RIGHT_PRESS = 21
33    RIGHT_RELEASE = 22
34    UP_PRESS = 23
35    UP_RELEASE = 24
36    DOWN_PRESS = 25
37    DOWN_RELEASE = 26

Types of input a controller can send to the game.

UP_DOWN = <InputType.UP_DOWN: 2>
LEFT_RIGHT = <InputType.LEFT_RIGHT: 3>
JUMP_PRESS = <InputType.JUMP_PRESS: 4>
JUMP_RELEASE = <InputType.JUMP_RELEASE: 5>
PUNCH_PRESS = <InputType.PUNCH_PRESS: 6>
PUNCH_RELEASE = <InputType.PUNCH_RELEASE: 7>
BOMB_PRESS = <InputType.BOMB_PRESS: 8>
BOMB_RELEASE = <InputType.BOMB_RELEASE: 9>
PICK_UP_PRESS = <InputType.PICK_UP_PRESS: 10>
PICK_UP_RELEASE = <InputType.PICK_UP_RELEASE: 11>
RUN = <InputType.RUN: 12>
FLY_PRESS = <InputType.FLY_PRESS: 13>
FLY_RELEASE = <InputType.FLY_RELEASE: 14>
START_PRESS = <InputType.START_PRESS: 15>
START_RELEASE = <InputType.START_RELEASE: 16>
HOLD_POSITION_PRESS = <InputType.HOLD_POSITION_PRESS: 17>
HOLD_POSITION_RELEASE = <InputType.HOLD_POSITION_RELEASE: 18>
LEFT_PRESS = <InputType.LEFT_PRESS: 19>
LEFT_RELEASE = <InputType.LEFT_RELEASE: 20>
RIGHT_PRESS = <InputType.RIGHT_PRESS: 21>
RIGHT_RELEASE = <InputType.RIGHT_RELEASE: 22>
UP_PRESS = <InputType.UP_PRESS: 23>
UP_RELEASE = <InputType.UP_RELEASE: 24>
DOWN_PRESS = <InputType.DOWN_PRESS: 25>
DOWN_RELEASE = <InputType.DOWN_RELEASE: 26>
@dataclass
class IntChoiceSetting(bascenev1.ChoiceSetting):
57@dataclass
58class IntChoiceSetting(ChoiceSetting):
59    """An int setting with multiple choices."""
60
61    default: int
62    choices: list[tuple[str, int]]

An int setting with multiple choices.

IntChoiceSetting(name: str, default: int, choices: list[tuple[str, int]])
default: int
choices: list[tuple[str, int]]
@dataclass
class IntSetting(bascenev1.Setting):
30@dataclass
31class IntSetting(Setting):
32    """An integer game setting."""
33
34    default: int
35    min_value: int = 0
36    max_value: int = 9999
37    increment: int = 1

An integer game setting.

IntSetting( name: str, default: int, min_value: int = 0, max_value: int = 9999, increment: int = 1)
default: int
min_value: int = 0
max_value: int = 9999
increment: int = 1
def is_point_in_box(pnt: Sequence[float], box: Sequence[float]) -> bool:
38def is_point_in_box(pnt: Sequence[float], box: Sequence[float]) -> bool:
39    """Return whether a given point is within a given box.
40
41    category: General Utility Functions
42
43    For use with standard def boxes (position|rotate|scale).
44    """
45    return (
46        (abs(pnt[0] - box[0]) <= box[6] * 0.5)
47        and (abs(pnt[1] - box[1]) <= box[7] * 0.5)
48        and (abs(pnt[2] - box[2]) <= box[8] * 0.5)
49    )

Return whether a given point is within a given box.

category: General Utility Functions

For use with standard def boxes (position|rotate|scale).

59class JoinActivity(Activity[EmptyPlayer, EmptyTeam]):
60    """Standard activity for waiting for players to join.
61
62    It shows tips and other info and waits for all players to check ready.
63    """
64
65    def __init__(self, settings: dict):
66        super().__init__(settings)
67
68        # This activity is a special 'joiner' activity.
69        # It will get shut down as soon as all players have checked ready.
70        self.is_joining_activity = True
71
72        # Players may be idle waiting for joiners; lets not kick them for it.
73        self.allow_kick_idle_players = False
74
75        # In vr mode we don't want stuff moving around.
76        self.use_fixed_vr_overlay = True
77
78        self._background: bascenev1.Actor | None = None
79        self._tips_text: bascenev1.Actor | None = None
80        self._join_info: JoinInfo | None = None
81
82    @override
83    def on_transition_in(self) -> None:
84        # pylint: disable=cyclic-import
85        from bascenev1lib.actor.tipstext import TipsText
86        from bascenev1lib.actor.background import Background
87
88        super().on_transition_in()
89        self._background = Background(
90            fade_time=0.5, start_faded=True, show_logo=True
91        )
92        self._tips_text = TipsText()
93        setmusic(MusicType.CHAR_SELECT)
94        self._join_info = self.session.lobby.create_join_info()
95        babase.set_analytics_screen('Joining Screen')

Standard activity for waiting for players to join.

It shows tips and other info and waits for all players to check ready.

JoinActivity(settings: dict)
65    def __init__(self, settings: dict):
66        super().__init__(settings)
67
68        # This activity is a special 'joiner' activity.
69        # It will get shut down as soon as all players have checked ready.
70        self.is_joining_activity = True
71
72        # Players may be idle waiting for joiners; lets not kick them for it.
73        self.allow_kick_idle_players = False
74
75        # In vr mode we don't want stuff moving around.
76        self.use_fixed_vr_overlay = True
77
78        self._background: bascenev1.Actor | None = None
79        self._tips_text: bascenev1.Actor | None = None
80        self._join_info: JoinInfo | None = None

Creates an Activity in the current bascenev1.Session.

The activity will not be actually run until bascenev1.Session.setactivity is called. 'settings' should be a dict of key/value pairs specific to the activity.

Activities should preload as much of their media/etc as possible in their constructor, but none of it should actually be used until they are transitioned in.

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.

allow_kick_idle_players = True

Whether idle players can potentially be kicked (should not happen in menus/etc).

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.

@override
def on_transition_in(self) -> None:
82    @override
83    def on_transition_in(self) -> None:
84        # pylint: disable=cyclic-import
85        from bascenev1lib.actor.tipstext import TipsText
86        from bascenev1lib.actor.background import Background
87
88        super().on_transition_in()
89        self._background = Background(
90            fade_time=0.5, start_faded=True, show_logo=True
91        )
92        self._tips_text = TipsText()
93        setmusic(MusicType.CHAR_SELECT)
94        self._join_info = self.session.lobby.create_join_info()
95        babase.set_analytics_screen('Joining Screen')

Called when the Activity is first becoming visible.

Upon this call, the Activity should fade in backgrounds, start playing music, etc. It does not yet have access to players or teams, however. They remain owned by the previous Activity up until bascenev1.Activity.on_begin() is called.

class Level:
 19class Level:
 20    """An entry in a bascenev1.Campaign."""
 21
 22    def __init__(
 23        self,
 24        name: str,
 25        gametype: type[bascenev1.GameActivity],
 26        settings: dict,
 27        preview_texture_name: str,
 28        *,
 29        displayname: str | None = None,
 30    ):
 31        self._name = name
 32        self._gametype = gametype
 33        self._settings = settings
 34        self._preview_texture_name = preview_texture_name
 35        self._displayname = displayname
 36        self._campaign: weakref.ref[bascenev1.Campaign] | None = None
 37        self._index: int | None = None
 38        self._score_version_string: str | None = None
 39
 40    @override
 41    def __repr__(self) -> str:
 42        cls = type(self)
 43        return f"<{cls.__module__}.{cls.__name__} '{self._name}'>"
 44
 45    @property
 46    def name(self) -> str:
 47        """The unique name for this Level."""
 48        return self._name
 49
 50    def get_settings(self) -> dict[str, Any]:
 51        """Returns the settings for this Level."""
 52        settings = copy.deepcopy(self._settings)
 53
 54        # So the game knows what the level is called.
 55        # Hmm; seems hacky; I think we should take this out.
 56        settings['name'] = self._name
 57        return settings
 58
 59    @property
 60    def preview_texture_name(self) -> str:
 61        """The preview texture name for this Level."""
 62        return self._preview_texture_name
 63
 64    # def get_preview_texture(self) -> bauiv1.Texture:
 65    #     """Load/return the preview Texture for this Level."""
 66    #     return _bauiv1.gettexture(self._preview_texture_name)
 67
 68    @property
 69    def displayname(self) -> bascenev1.Lstr:
 70        """The localized name for this Level."""
 71        return babase.Lstr(
 72            translate=(
 73                'coopLevelNames',
 74                (
 75                    self._displayname
 76                    if self._displayname is not None
 77                    else self._name
 78                ),
 79            ),
 80            subs=[
 81                ('${GAME}', self._gametype.get_display_string(self._settings))
 82            ],
 83        )
 84
 85    @property
 86    def gametype(self) -> type[bascenev1.GameActivity]:
 87        """The type of game used for this Level."""
 88        return self._gametype
 89
 90    @property
 91    def campaign(self) -> bascenev1.Campaign | None:
 92        """The baclassic.Campaign this Level is associated with, or None."""
 93        return None if self._campaign is None else self._campaign()
 94
 95    @property
 96    def index(self) -> int:
 97        """The zero-based index of this Level in its baclassic.Campaign.
 98
 99        Access results in a RuntimeError if the Level is  not assigned to a
100        Campaign.
101        """
102        if self._index is None:
103            raise RuntimeError('Level is not part of a Campaign')
104        return self._index
105
106    @property
107    def complete(self) -> bool:
108        """Whether this Level has been completed."""
109        config = self._get_config_dict()
110        val = config.get('Complete', False)
111        assert isinstance(val, bool)
112        return val
113
114    def set_complete(self, val: bool) -> None:
115        """Set whether or not this level is complete."""
116        old_val = self.complete
117        assert isinstance(old_val, bool)
118        assert isinstance(val, bool)
119        if val != old_val:
120            config = self._get_config_dict()
121            config['Complete'] = val
122
123    def get_high_scores(self) -> dict:
124        """Return the current high scores for this Level."""
125        config = self._get_config_dict()
126        high_scores_key = 'High Scores' + self.get_score_version_string()
127        if high_scores_key not in config:
128            return {}
129        return copy.deepcopy(config[high_scores_key])
130
131    def set_high_scores(self, high_scores: dict) -> None:
132        """Set high scores for this level."""
133        config = self._get_config_dict()
134        high_scores_key = 'High Scores' + self.get_score_version_string()
135        config[high_scores_key] = high_scores
136
137    def get_score_version_string(self) -> str:
138        """Return the score version string for this Level.
139
140        If a Level's gameplay changes significantly, its version string
141        can be changed to separate its new high score lists/etc. from the old.
142        """
143        if self._score_version_string is None:
144            scorever = self._gametype.getscoreconfig().version
145            if scorever != '':
146                scorever = ' ' + scorever
147            self._score_version_string = scorever
148        assert self._score_version_string is not None
149        return self._score_version_string
150
151    @property
152    def rating(self) -> float:
153        """The current rating for this Level."""
154        val = self._get_config_dict().get('Rating', 0.0)
155        assert isinstance(val, float)
156        return val
157
158    def set_rating(self, rating: float) -> None:
159        """Set a rating for this Level, replacing the old ONLY IF higher."""
160        old_rating = self.rating
161        config = self._get_config_dict()
162        config['Rating'] = max(old_rating, rating)
163
164    def _get_config_dict(self) -> dict[str, Any]:
165        """Return/create the persistent state dict for this level.
166
167        The referenced dict exists under the game's config dict and
168        can be modified in place."""
169        campaign = self.campaign
170        if campaign is None:
171            raise RuntimeError('Level is not in a campaign.')
172        configdict = campaign.configdict
173        val: dict[str, Any] = configdict.setdefault(
174            self._name, {'Rating': 0.0, 'Complete': False}
175        )
176        assert isinstance(val, dict)
177        return val
178
179    def set_campaign(self, campaign: bascenev1.Campaign, index: int) -> None:
180        """For use by baclassic.Campaign when adding levels to itself.
181
182        (internal)"""
183        self._campaign = weakref.ref(campaign)
184        self._index = index

An entry in a bascenev1.Campaign.

Level( name: str, gametype: type[GameActivity], settings: dict, preview_texture_name: str, *, displayname: str | None = None)
22    def __init__(
23        self,
24        name: str,
25        gametype: type[bascenev1.GameActivity],
26        settings: dict,
27        preview_texture_name: str,
28        *,
29        displayname: str | None = None,
30    ):
31        self._name = name
32        self._gametype = gametype
33        self._settings = settings
34        self._preview_texture_name = preview_texture_name
35        self._displayname = displayname
36        self._campaign: weakref.ref[bascenev1.Campaign] | None = None
37        self._index: int | None = None
38        self._score_version_string: str | None = None
name: str
45    @property
46    def name(self) -> str:
47        """The unique name for this Level."""
48        return self._name

The unique name for this Level.

def get_settings(self) -> dict[str, typing.Any]:
50    def get_settings(self) -> dict[str, Any]:
51        """Returns the settings for this Level."""
52        settings = copy.deepcopy(self._settings)
53
54        # So the game knows what the level is called.
55        # Hmm; seems hacky; I think we should take this out.
56        settings['name'] = self._name
57        return settings

Returns the settings for this Level.

preview_texture_name: str
59    @property
60    def preview_texture_name(self) -> str:
61        """The preview texture name for this Level."""
62        return self._preview_texture_name

The preview texture name for this Level.

displayname: Lstr
68    @property
69    def displayname(self) -> bascenev1.Lstr:
70        """The localized name for this Level."""
71        return babase.Lstr(
72            translate=(
73                'coopLevelNames',
74                (
75                    self._displayname
76                    if self._displayname is not None
77                    else self._name
78                ),
79            ),
80            subs=[
81                ('${GAME}', self._gametype.get_display_string(self._settings))
82            ],
83        )

The localized name for this Level.

gametype: type[GameActivity]
85    @property
86    def gametype(self) -> type[bascenev1.GameActivity]:
87        """The type of game used for this Level."""
88        return self._gametype

The type of game used for this Level.

campaign: Campaign | None
90    @property
91    def campaign(self) -> bascenev1.Campaign | None:
92        """The baclassic.Campaign this Level is associated with, or None."""
93        return None if self._campaign is None else self._campaign()

The baclassic.Campaign this Level is associated with, or None.

index: int
 95    @property
 96    def index(self) -> int:
 97        """The zero-based index of this Level in its baclassic.Campaign.
 98
 99        Access results in a RuntimeError if the Level is  not assigned to a
100        Campaign.
101        """
102        if self._index is None:
103            raise RuntimeError('Level is not part of a Campaign')
104        return self._index

The zero-based index of this Level in its baclassic.Campaign.

Access results in a RuntimeError if the Level is not assigned to a Campaign.

complete: bool
106    @property
107    def complete(self) -> bool:
108        """Whether this Level has been completed."""
109        config = self._get_config_dict()
110        val = config.get('Complete', False)
111        assert isinstance(val, bool)
112        return val

Whether this Level has been completed.

def set_complete(self, val: bool) -> None:
114    def set_complete(self, val: bool) -> None:
115        """Set whether or not this level is complete."""
116        old_val = self.complete
117        assert isinstance(old_val, bool)
118        assert isinstance(val, bool)
119        if val != old_val:
120            config = self._get_config_dict()
121            config['Complete'] = val

Set whether or not this level is complete.

def get_high_scores(self) -> dict:
123    def get_high_scores(self) -> dict:
124        """Return the current high scores for this Level."""
125        config = self._get_config_dict()
126        high_scores_key = 'High Scores' + self.get_score_version_string()
127        if high_scores_key not in config:
128            return {}
129        return copy.deepcopy(config[high_scores_key])

Return the current high scores for this Level.

def set_high_scores(self, high_scores: dict) -> None:
131    def set_high_scores(self, high_scores: dict) -> None:
132        """Set high scores for this level."""
133        config = self._get_config_dict()
134        high_scores_key = 'High Scores' + self.get_score_version_string()
135        config[high_scores_key] = high_scores

Set high scores for this level.

def get_score_version_string(self) -> str:
137    def get_score_version_string(self) -> str:
138        """Return the score version string for this Level.
139
140        If a Level's gameplay changes significantly, its version string
141        can be changed to separate its new high score lists/etc. from the old.
142        """
143        if self._score_version_string is None:
144            scorever = self._gametype.getscoreconfig().version
145            if scorever != '':
146                scorever = ' ' + scorever
147            self._score_version_string = scorever
148        assert self._score_version_string is not None
149        return self._score_version_string

Return the score version string for this Level.

If a Level's gameplay changes significantly, its version string can be changed to separate its new high score lists/etc. from the old.

rating: float
151    @property
152    def rating(self) -> float:
153        """The current rating for this Level."""
154        val = self._get_config_dict().get('Rating', 0.0)
155        assert isinstance(val, float)
156        return val

The current rating for this Level.

def set_rating(self, rating: float) -> None:
158    def set_rating(self, rating: float) -> None:
159        """Set a rating for this Level, replacing the old ONLY IF higher."""
160        old_rating = self.rating
161        config = self._get_config_dict()
162        config['Rating'] = max(old_rating, rating)

Set a rating for this Level, replacing the old ONLY IF higher.

class Lobby:
 940class Lobby:
 941    """Container for baclassic.Choosers."""
 942
 943    def __del__(self) -> None:
 944        # Reset any players that still have a chooser in us.
 945        # (should allow the choosers to die).
 946        sessionplayers = [
 947            c.sessionplayer for c in self.choosers if c.sessionplayer
 948        ]
 949        for sessionplayer in sessionplayers:
 950            sessionplayer.resetinput()
 951
 952    def __init__(self) -> None:
 953        from bascenev1._team import SessionTeam
 954        from bascenev1._coopsession import CoopSession
 955
 956        session = _bascenev1.getsession()
 957        self._use_team_colors = session.use_team_colors
 958        if session.use_teams:
 959            self._sessionteams = [
 960                weakref.ref(team) for team in session.sessionteams
 961            ]
 962        else:
 963            self._dummy_teams = SessionTeam()
 964            self._sessionteams = [weakref.ref(self._dummy_teams)]
 965        v_offset = -150 if isinstance(session, CoopSession) else -50
 966        self.choosers: list[Chooser] = []
 967        self.base_v_offset = v_offset
 968        self.update_positions()
 969        self._next_add_team = 0
 970        self.character_names_local_unlocked: list[str] = []
 971        self._vpos = 0
 972
 973        # Grab available profiles.
 974        self.reload_profiles()
 975
 976        self._join_info_text = None
 977
 978    @property
 979    def next_add_team(self) -> int:
 980        """(internal)"""
 981        return self._next_add_team
 982
 983    @property
 984    def use_team_colors(self) -> bool:
 985        """A bool for whether this lobby is using team colors.
 986
 987        If False, inidividual player colors are used instead.
 988        """
 989        return self._use_team_colors
 990
 991    @property
 992    def sessionteams(self) -> list[bascenev1.SessionTeam]:
 993        """bascenev1.SessionTeams available in this lobby."""
 994        allteams = []
 995        for tref in self._sessionteams:
 996            team = tref()
 997            assert team is not None
 998            allteams.append(team)
 999        return allteams
1000
1001    def get_choosers(self) -> list[Chooser]:
1002        """Return the lobby's current choosers."""
1003        return self.choosers
1004
1005    def create_join_info(self) -> JoinInfo:
1006        """Create a display of on-screen information for joiners.
1007
1008        (how to switch teams, players, etc.)
1009        Intended for use in initial joining-screens.
1010        """
1011        return JoinInfo(self)
1012
1013    def reload_profiles(self) -> None:
1014        """Reload available player profiles."""
1015        # pylint: disable=cyclic-import
1016        from bascenev1lib.actor.spazappearance import get_appearances
1017
1018        assert babase.app.classic is not None
1019
1020        # We may have gained or lost character names if the user
1021        # bought something; reload these too.
1022        self.character_names_local_unlocked = get_appearances()
1023        self.character_names_local_unlocked.sort(key=lambda x: x.lower())
1024
1025        # Do any overall prep we need to such as creating account profile.
1026        babase.app.classic.accounts.ensure_have_account_player_profile()
1027        for chooser in self.choosers:
1028            try:
1029                chooser.reload_profiles()
1030                chooser.update_from_profile()
1031            except Exception:
1032                logging.exception('Error reloading profiles.')
1033
1034    def update_positions(self) -> None:
1035        """Update positions for all choosers."""
1036        self._vpos = -100 + self.base_v_offset
1037        for chooser in self.choosers:
1038            chooser.set_vpos(self._vpos)
1039            chooser.update_position()
1040            self._vpos -= 48
1041
1042    def check_all_ready(self) -> bool:
1043        """Return whether all choosers are marked ready."""
1044        return all(chooser.ready for chooser in self.choosers)
1045
1046    def add_chooser(self, sessionplayer: bascenev1.SessionPlayer) -> None:
1047        """Add a chooser to the lobby for the provided player."""
1048        self.choosers.append(
1049            Chooser(vpos=self._vpos, sessionplayer=sessionplayer, lobby=self)
1050        )
1051        self._next_add_team = (self._next_add_team + 1) % len(
1052            self._sessionteams
1053        )
1054        self._vpos -= 48
1055
1056    def remove_chooser(self, player: bascenev1.SessionPlayer) -> None:
1057        """Remove a single player's chooser; does not kick them.
1058
1059        This is used when a player enters the game and no longer
1060        needs a chooser."""
1061        found = False
1062        chooser = None
1063        for chooser in self.choosers:
1064            if chooser.getplayer() is player:
1065                found = True
1066
1067                # Mark it as dead since there could be more
1068                # change-commands/etc coming in still for it;
1069                # want to avoid duplicate player-adds/etc.
1070                chooser.set_dead(True)
1071                self.choosers.remove(chooser)
1072                break
1073        if not found:
1074            logging.exception('remove_chooser did not find player %s.', player)
1075        elif chooser in self.choosers:
1076            logging.exception('chooser remains after removal for %s.', player)
1077        self.update_positions()
1078
1079    def remove_all_choosers(self) -> None:
1080        """Remove all choosers without kicking players.
1081
1082        This is called after all players check in and enter a game.
1083        """
1084        self.choosers = []
1085        self.update_positions()
1086
1087    def remove_all_choosers_and_kick_players(self) -> None:
1088        """Remove all player choosers and kick attached players."""
1089
1090        # Copy the list; it can change under us otherwise.
1091        for chooser in list(self.choosers):
1092            if chooser.sessionplayer:
1093                chooser.sessionplayer.remove_from_game()
1094        self.remove_all_choosers()

Container for baclassic.Choosers.

choosers: list[Chooser]
base_v_offset
character_names_local_unlocked: list[str]
use_team_colors: bool
983    @property
984    def use_team_colors(self) -> bool:
985        """A bool for whether this lobby is using team colors.
986
987        If False, inidividual player colors are used instead.
988        """
989        return self._use_team_colors

A bool for whether this lobby is using team colors.

If False, inidividual player colors are used instead.

sessionteams: list[SessionTeam]
991    @property
992    def sessionteams(self) -> list[bascenev1.SessionTeam]:
993        """bascenev1.SessionTeams available in this lobby."""
994        allteams = []
995        for tref in self._sessionteams:
996            team = tref()
997            assert team is not None
998            allteams.append(team)
999        return allteams

bascenev1.SessionTeams available in this lobby.

def get_choosers(self) -> list[Chooser]:
1001    def get_choosers(self) -> list[Chooser]:
1002        """Return the lobby's current choosers."""
1003        return self.choosers

Return the lobby's current choosers.

def create_join_info(self) -> bascenev1._lobby.JoinInfo:
1005    def create_join_info(self) -> JoinInfo:
1006        """Create a display of on-screen information for joiners.
1007
1008        (how to switch teams, players, etc.)
1009        Intended for use in initial joining-screens.
1010        """
1011        return JoinInfo(self)

Create a display of on-screen information for joiners.

(how to switch teams, players, etc.) Intended for use in initial joining-screens.

def reload_profiles(self) -> None:
1013    def reload_profiles(self) -> None:
1014        """Reload available player profiles."""
1015        # pylint: disable=cyclic-import
1016        from bascenev1lib.actor.spazappearance import get_appearances
1017
1018        assert babase.app.classic is not None
1019
1020        # We may have gained or lost character names if the user
1021        # bought something; reload these too.
1022        self.character_names_local_unlocked = get_appearances()
1023        self.character_names_local_unlocked.sort(key=lambda x: x.lower())
1024
1025        # Do any overall prep we need to such as creating account profile.
1026        babase.app.classic.accounts.ensure_have_account_player_profile()
1027        for chooser in self.choosers:
1028            try:
1029                chooser.reload_profiles()
1030                chooser.update_from_profile()
1031            except Exception:
1032                logging.exception('Error reloading profiles.')

Reload available player profiles.

def update_positions(self) -> None:
1034    def update_positions(self) -> None:
1035        """Update positions for all choosers."""
1036        self._vpos = -100 + self.base_v_offset
1037        for chooser in self.choosers:
1038            chooser.set_vpos(self._vpos)
1039            chooser.update_position()
1040            self._vpos -= 48

Update positions for all choosers.

def check_all_ready(self) -> bool:
1042    def check_all_ready(self) -> bool:
1043        """Return whether all choosers are marked ready."""
1044        return all(chooser.ready for chooser in self.choosers)

Return whether all choosers are marked ready.

def add_chooser(self, sessionplayer: _bascenev1.SessionPlayer) -> None:
1046    def add_chooser(self, sessionplayer: bascenev1.SessionPlayer) -> None:
1047        """Add a chooser to the lobby for the provided player."""
1048        self.choosers.append(
1049            Chooser(vpos=self._vpos, sessionplayer=sessionplayer, lobby=self)
1050        )
1051        self._next_add_team = (self._next_add_team + 1) % len(
1052            self._sessionteams
1053        )
1054        self._vpos -= 48

Add a chooser to the lobby for the provided player.

def remove_chooser(self, player: _bascenev1.SessionPlayer) -> None:
1056    def remove_chooser(self, player: bascenev1.SessionPlayer) -> None:
1057        """Remove a single player's chooser; does not kick them.
1058
1059        This is used when a player enters the game and no longer
1060        needs a chooser."""
1061        found = False
1062        chooser = None
1063        for chooser in self.choosers:
1064            if chooser.getplayer() is player:
1065                found = True
1066
1067                # Mark it as dead since there could be more
1068                # change-commands/etc coming in still for it;
1069                # want to avoid duplicate player-adds/etc.
1070                chooser.set_dead(True)
1071                self.choosers.remove(chooser)
1072                break
1073        if not found:
1074            logging.exception('remove_chooser did not find player %s.', player)
1075        elif chooser in self.choosers:
1076            logging.exception('chooser remains after removal for %s.', player)
1077        self.update_positions()

Remove a single player's chooser; does not kick them.

This is used when a player enters the game and no longer needs a chooser.

def remove_all_choosers(self) -> None:
1079    def remove_all_choosers(self) -> None:
1080        """Remove all choosers without kicking players.
1081
1082        This is called after all players check in and enter a game.
1083        """
1084        self.choosers = []
1085        self.update_positions()

Remove all choosers without kicking players.

This is called after all players check in and enter a game.

def remove_all_choosers_and_kick_players(self) -> None:
1087    def remove_all_choosers_and_kick_players(self) -> None:
1088        """Remove all player choosers and kick attached players."""
1089
1090        # Copy the list; it can change under us otherwise.
1091        for chooser in list(self.choosers):
1092            if chooser.sessionplayer:
1093                chooser.sessionplayer.remove_from_game()
1094        self.remove_all_choosers()

Remove all player choosers and kick attached players.

def ls_input_devices() -> None:
1442def ls_input_devices() -> None:
1443    """Print debugging info about game objects.
1444
1445    This call only functions in debug builds of the game.
1446    It prints various info about the current object count, etc.
1447    """
1448    return None

Print debugging info about game objects.

This call only functions in debug builds of the game. It prints various info about the current object count, etc.

def ls_objects() -> None:
1451def ls_objects() -> None:
1452    """Log debugging info about C++ level objects.
1453
1454    This call only functions in debug builds of the game.
1455    It prints various info about the current object count, etc.
1456    """
1457    return None

Log debugging info about C++ level objects.

This call only functions in debug builds of the game. It prints various info about the current object count, etc.

class Lstr:
494class Lstr:
495    """Used to define strings in a language-independent way.
496
497    These should be used whenever possible in place of hard-coded
498    strings so that in-game or UI elements show up correctly on all
499    clients in their currently active language.
500
501    To see available resource keys, look at any of the
502    ``bs_language_*.py`` files in the game or the translations pages at
503    `legacy.ballistica.net/translate
504    <https://legacy.ballistica.net/translate>`.
505
506    Examples
507    --------
508
509    **Example 1: Specify a String from a Resource Path**::
510
511        mynode.text = babase.Lstr(resource='audioSettingsWindow.titleText')
512
513    **Example 2: Specify a Translated String via a Category and English Value**
514
515    If a translated value is available, it will be used; otherwise, the
516    English value will be. To see available translation categories, look
517    under the ``translations`` resource section::
518
519        mynode.text = babase.Lstr(translate=('gameDescriptions',
520                                             'Defeat all enemies'))
521
522    **Example 3: Specify a Raw Value with Substitutions**
523
524    Substitutions can be used with ``resource`` and ``translate`` modes
525    as well::
526
527        mynode.text = babase.Lstr(value='${A} / ${B}',
528                                  subs=[('${A}', str(score)),
529                                        ('${B}', str(total))])
530
531    **Example 4: Nesting**
532
533    :class:`~babase.Lstr` instances can be nested. This example would display
534    the resource at ``res_a`` but replace ``${NAME}`` with the value of
535    the resource at ``res_b``::
536
537        mytextnode.text = babase.Lstr(
538            resource='res_a',
539            subs=[('${NAME}', babase.Lstr(resource='res_b'))])
540    """
541
542    # This class is used a lot in UI stuff and doesn't need to be
543    # flexible, so let's optimize its performance a bit.
544    __slots__ = ['args']
545
546    @overload
547    def __init__(
548        self,
549        *,
550        resource: str,
551        fallback_resource: str = '',
552        fallback_value: str = '',
553        subs: Sequence[tuple[str, str | Lstr]] | None = None,
554    ) -> None:
555        """Create an Lstr from a string resource."""
556
557    @overload
558    def __init__(
559        self,
560        *,
561        translate: tuple[str, str],
562        subs: Sequence[tuple[str, str | Lstr]] | None = None,
563    ) -> None:
564        """Create an Lstr by translating a string in a category."""
565
566    @overload
567    def __init__(
568        self,
569        *,
570        value: str,
571        subs: Sequence[tuple[str, str | Lstr]] | None = None,
572    ) -> None:
573        """Create an Lstr from a raw string value."""
574
575    def __init__(self, *args: Any, **keywds: Any) -> None:
576        """Instantiate a Lstr.
577
578        Pass a value for either 'resource', 'translate',
579        or 'value'. (see Lstr help for examples).
580        'subs' can be a sequence of 2-member sequences consisting of values
581        and replacements.
582        'fallback_resource' can be a resource key that will be used if the
583        main one is not present for
584        the current language in place of falling back to the english value
585        ('resource' mode only).
586        'fallback_value' can be a literal string that will be used if neither
587        the resource nor the fallback resource is found ('resource' mode only).
588        """
589        # pylint: disable=too-many-branches
590        if args:
591            raise TypeError('Lstr accepts only keyword arguments')
592
593        # Basically just store the exact args they passed. However if
594        # they passed any Lstr values for subs, replace them with that
595        # Lstr's dict.
596        self.args = keywds
597        our_type = type(self)
598
599        if isinstance(self.args.get('value'), our_type):
600            raise TypeError("'value' must be a regular string; not an Lstr")
601
602        if 'subs' in keywds:
603            subs = keywds.get('subs')
604            subs_filtered = []
605            if subs is not None:
606                for key, value in keywds['subs']:
607                    if isinstance(value, our_type):
608                        subs_filtered.append((key, value.args))
609                    else:
610                        subs_filtered.append((key, value))
611            self.args['subs'] = subs_filtered
612
613        # As of protocol 31 we support compact key names ('t' instead of
614        # 'translate', etc). Convert as needed.
615        if 'translate' in keywds:
616            keywds['t'] = keywds['translate']
617            del keywds['translate']
618        if 'resource' in keywds:
619            keywds['r'] = keywds['resource']
620            del keywds['resource']
621        if 'value' in keywds:
622            keywds['v'] = keywds['value']
623            del keywds['value']
624        if 'fallback' in keywds:
625            from babase import _error
626
627            _error.print_error(
628                'deprecated "fallback" arg passed to Lstr(); use '
629                'either "fallback_resource" or "fallback_value"',
630                once=True,
631            )
632            keywds['f'] = keywds['fallback']
633            del keywds['fallback']
634        if 'fallback_resource' in keywds:
635            keywds['f'] = keywds['fallback_resource']
636            del keywds['fallback_resource']
637        if 'subs' in keywds:
638            keywds['s'] = keywds['subs']
639            del keywds['subs']
640        if 'fallback_value' in keywds:
641            keywds['fv'] = keywds['fallback_value']
642            del keywds['fallback_value']
643
644    def evaluate(self) -> str:
645        """Evaluate the Lstr and returns a flat string in the current language.
646
647        You should avoid doing this as much as possible and instead pass
648        and store Lstr values.
649        """
650        return _babase.evaluate_lstr(self._get_json())
651
652    def is_flat_value(self) -> bool:
653        """Return whether the Lstr is a 'flat' value.
654
655        This is defined as a simple string value incorporating no
656        translations, resources, or substitutions. In this case it may
657        be reasonable to replace it with a raw string value, perform
658        string manipulation on it, etc.
659        """
660        return bool('v' in self.args and not self.args.get('s', []))
661
662    def _get_json(self) -> str:
663        try:
664            return json.dumps(self.args, separators=(',', ':'))
665        except Exception:
666            from babase import _error
667
668            _error.print_exception('_get_json failed for', self.args)
669            return 'JSON_ERR'
670
671    @override
672    def __str__(self) -> str:
673        return '<ba.Lstr: ' + self._get_json() + '>'
674
675    @override
676    def __repr__(self) -> str:
677        return '<ba.Lstr: ' + self._get_json() + '>'
678
679    @staticmethod
680    def from_json(json_string: str) -> babase.Lstr:
681        """Given a json string, returns a babase.Lstr. Does no validation."""
682        lstr = Lstr(value='')
683        lstr.args = json.loads(json_string)
684        return lstr

Used to define strings in a language-independent way.

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 currently active language.

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 <https://legacy.ballistica.net/translate>.

Examples

Example 1: Specify a String from a Resource Path::

mynode.text = babase.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 = babase.Lstr(translate=('gameDescriptions',
                                     'Defeat all enemies'))

Example 3: Specify a Raw Value with Substitutions

Substitutions can be used with resource and translate modes as well::

mynode.text = babase.Lstr(value='${A} / ${B}',
                          subs=[('${A}', str(score)),
                                ('${B}', str(total))])

Example 4: Nesting

~babase.Lstr instances 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 = babase.Lstr(
    resource='res_a',
    subs=[('${NAME}', babase.Lstr(resource='res_b'))])
Lstr(*args: Any, **keywds: Any)
575    def __init__(self, *args: Any, **keywds: Any) -> None:
576        """Instantiate a Lstr.
577
578        Pass a value for either 'resource', 'translate',
579        or 'value'. (see Lstr help for examples).
580        'subs' can be a sequence of 2-member sequences consisting of values
581        and replacements.
582        'fallback_resource' can be a resource key that will be used if the
583        main one is not present for
584        the current language in place of falling back to the english value
585        ('resource' mode only).
586        'fallback_value' can be a literal string that will be used if neither
587        the resource nor the fallback resource is found ('resource' mode only).
588        """
589        # pylint: disable=too-many-branches
590        if args:
591            raise TypeError('Lstr accepts only keyword arguments')
592
593        # Basically just store the exact args they passed. However if
594        # they passed any Lstr values for subs, replace them with that
595        # Lstr's dict.
596        self.args = keywds
597        our_type = type(self)
598
599        if isinstance(self.args.get('value'), our_type):
600            raise TypeError("'value' must be a regular string; not an Lstr")
601
602        if 'subs' in keywds:
603            subs = keywds.get('subs')
604            subs_filtered = []
605            if subs is not None:
606                for key, value in keywds['subs']:
607                    if isinstance(value, our_type):
608                        subs_filtered.append((key, value.args))
609                    else:
610                        subs_filtered.append((key, value))
611            self.args['subs'] = subs_filtered
612
613        # As of protocol 31 we support compact key names ('t' instead of
614        # 'translate', etc). Convert as needed.
615        if 'translate' in keywds:
616            keywds['t'] = keywds['translate']
617            del keywds['translate']
618        if 'resource' in keywds:
619            keywds['r'] = keywds['resource']
620            del keywds['resource']
621        if 'value' in keywds:
622            keywds['v'] = keywds['value']
623            del keywds['value']
624        if 'fallback' in keywds:
625            from babase import _error
626
627            _error.print_error(
628                'deprecated "fallback" arg passed to Lstr(); use '
629                'either "fallback_resource" or "fallback_value"',
630                once=True,
631            )
632            keywds['f'] = keywds['fallback']
633            del keywds['fallback']
634        if 'fallback_resource' in keywds:
635            keywds['f'] = keywds['fallback_resource']
636            del keywds['fallback_resource']
637        if 'subs' in keywds:
638            keywds['s'] = keywds['subs']
639            del keywds['subs']
640        if 'fallback_value' in keywds:
641            keywds['fv'] = keywds['fallback_value']
642            del keywds['fallback_value']

Instantiate a Lstr.

Pass a value for either 'resource', 'translate', or 'value'. (see Lstr help for examples). 'subs' can be a sequence of 2-member sequences consisting of values and replacements. 'fallback_resource' can be a resource key that will be used if the main one is not present for the current language in place of falling back to the english value ('resource' mode only). 'fallback_value' can be a literal string that will be used if neither the resource nor the fallback resource is found ('resource' mode only).

args
def evaluate(self) -> str:
644    def evaluate(self) -> str:
645        """Evaluate the Lstr and returns a flat string in the current language.
646
647        You should avoid doing this as much as possible and instead pass
648        and store Lstr values.
649        """
650        return _babase.evaluate_lstr(self._get_json())

Evaluate the Lstr and returns a flat string in the current language.

You should avoid doing this as much as possible and instead pass and store Lstr values.

def is_flat_value(self) -> bool:
652    def is_flat_value(self) -> bool:
653        """Return whether the Lstr is a 'flat' value.
654
655        This is defined as a simple string value incorporating no
656        translations, resources, or substitutions. In this case it may
657        be reasonable to replace it with a raw string value, perform
658        string manipulation on it, etc.
659        """
660        return bool('v' in self.args and not self.args.get('s', []))

Return whether the Lstr is a 'flat' value.

This is defined as a simple string value incorporating no translations, resources, or substitutions. In this case it may be reasonable to replace it with a raw string value, perform string manipulation on it, etc.

@staticmethod
def from_json(json_string: str) -> Lstr:
679    @staticmethod
680    def from_json(json_string: str) -> babase.Lstr:
681        """Given a json string, returns a babase.Lstr. Does no validation."""
682        lstr = Lstr(value='')
683        lstr.args = json.loads(json_string)
684        return lstr

Given a json string, returns a babase.Lstr. Does no validation.

class Map(bascenev1.Actor):
 50class Map(Actor):
 51    """A game map.
 52
 53    Consists of a collection of terrain nodes, metadata, and other
 54    functionality comprising a game map.
 55    """
 56
 57    defs: Any = None
 58    name = 'Map'
 59    _playtypes: list[str] = []
 60
 61    @classmethod
 62    def preload(cls) -> None:
 63        """Preload map media.
 64
 65        This runs the class's on_preload() method as needed to prep it to run.
 66        Preloading should generally be done in a bascenev1.Activity's
 67        __init__ method. Note that this is a classmethod since it is not
 68        operate on map instances but rather on the class itself before
 69        instances are made
 70        """
 71        activity = _bascenev1.getactivity()
 72        if cls not in activity.preloads:
 73            activity.preloads[cls] = cls.on_preload()
 74
 75    @classmethod
 76    def get_play_types(cls) -> list[str]:
 77        """Return valid play types for this map."""
 78        return []
 79
 80    @classmethod
 81    def get_preview_texture_name(cls) -> str | None:
 82        """Return the name of the preview texture for this map."""
 83        return None
 84
 85    @classmethod
 86    def on_preload(cls) -> Any:
 87        """Called when the map is being preloaded.
 88
 89        It should return any media/data it requires to operate
 90        """
 91        return None
 92
 93    @classmethod
 94    def getname(cls) -> str:
 95        """Return the unique name of this map, in English."""
 96        return cls.name
 97
 98    @classmethod
 99    def get_music_type(cls) -> bascenev1.MusicType | None:
100        """Return a music-type string that should be played on this map.
101
102        If None is returned, default music will be used.
103        """
104        return None
105
106    def __init__(
107        self, vr_overlay_offset: Sequence[float] | None = None
108    ) -> None:
109        """Instantiate a map."""
110        super().__init__()
111
112        # This is expected to always be a bascenev1.Node object
113        # (whether valid or not) should be set to something meaningful
114        # by child classes.
115        self.node: _bascenev1.Node | None = None
116
117        # Make our class' preload-data available to us
118        # (and instruct the user if we weren't preloaded properly).
119        try:
120            self.preloaddata = _bascenev1.getactivity().preloads[type(self)]
121        except Exception as exc:
122            raise babase.NotFoundError(
123                'Preload data not found for '
124                + str(type(self))
125                + '; make sure to call the type\'s preload()'
126                ' staticmethod in the activity constructor'
127            ) from exc
128
129        # Set various globals.
130        gnode = _bascenev1.getactivity().globalsnode
131
132        # Set area-of-interest bounds.
133        aoi_bounds = self.get_def_bound_box('area_of_interest_bounds')
134        if aoi_bounds is None:
135            print('WARNING: no "aoi_bounds" found for map:', self.getname())
136            aoi_bounds = (-1, -1, -1, 1, 1, 1)
137        gnode.area_of_interest_bounds = aoi_bounds
138
139        # Set map bounds.
140        map_bounds = self.get_def_bound_box('map_bounds')
141        if map_bounds is None:
142            print('WARNING: no "map_bounds" found for map:', self.getname())
143            map_bounds = (-30, -10, -30, 30, 100, 30)
144        _bascenev1.set_map_bounds(map_bounds)
145
146        # Set shadow ranges.
147        try:
148            gnode.shadow_range = [
149                self.defs.points[v][1]
150                for v in [
151                    'shadow_lower_bottom',
152                    'shadow_lower_top',
153                    'shadow_upper_bottom',
154                    'shadow_upper_top',
155                ]
156            ]
157        except Exception:
158            pass
159
160        # In vr, set a fixed point in space for the overlay to show up at.
161        # By default we use the bounds center but allow the map to override it.
162        center = (
163            (aoi_bounds[0] + aoi_bounds[3]) * 0.5,
164            (aoi_bounds[1] + aoi_bounds[4]) * 0.5,
165            (aoi_bounds[2] + aoi_bounds[5]) * 0.5,
166        )
167        if vr_overlay_offset is not None:
168            center = (
169                center[0] + vr_overlay_offset[0],
170                center[1] + vr_overlay_offset[1],
171                center[2] + vr_overlay_offset[2],
172            )
173        gnode.vr_overlay_center = center
174        gnode.vr_overlay_center_enabled = True
175
176        self.spawn_points = self.get_def_points('spawn') or [(0, 0, 0, 0, 0, 0)]
177        self.ffa_spawn_points = self.get_def_points('ffa_spawn') or [
178            (0, 0, 0, 0, 0, 0)
179        ]
180        self.spawn_by_flag_points = self.get_def_points('spawn_by_flag') or [
181            (0, 0, 0, 0, 0, 0)
182        ]
183        self.flag_points = self.get_def_points('flag') or [(0, 0, 0)]
184
185        # We just want points.
186        self.flag_points = [p[:3] for p in self.flag_points]
187        self.flag_points_default = self.get_def_point('flag_default') or (
188            0,
189            1,
190            0,
191        )
192        self.powerup_spawn_points = self.get_def_points('powerup_spawn') or [
193            (0, 0, 0)
194        ]
195
196        # We just want points.
197        self.powerup_spawn_points = [p[:3] for p in self.powerup_spawn_points]
198        self.tnt_points = self.get_def_points('tnt') or []
199
200        # We just want points.
201        self.tnt_points = [p[:3] for p in self.tnt_points]
202
203        self.is_hockey = False
204        self.is_flying = False
205
206        # FIXME: this should be part of game; not map.
207        # Let's select random index for first spawn point,
208        # so that no one is offended by the constant spawn on the edge.
209        self._next_ffa_start_index = random.randrange(
210            len(self.ffa_spawn_points)
211        )
212
213    def is_point_near_edge(
214        self, point: babase.Vec3, running: bool = False
215    ) -> bool:
216        """Return whether the provided point is near an edge of the map.
217
218        Simple bot logic uses this call to determine if they
219        are approaching a cliff or wall. If this returns True they will
220        generally not walk/run any farther away from the origin.
221        If 'running' is True, the buffer should be a bit larger.
222        """
223        del point, running  # Unused.
224        return False
225
226    def get_def_bound_box(
227        self, name: str
228    ) -> tuple[float, float, float, float, float, float] | None:
229        """Return a 6 member bounds tuple or None if it is not defined."""
230        try:
231            box = self.defs.boxes[name]
232            return (
233                box[0] - box[6] / 2.0,
234                box[1] - box[7] / 2.0,
235                box[2] - box[8] / 2.0,
236                box[0] + box[6] / 2.0,
237                box[1] + box[7] / 2.0,
238                box[2] + box[8] / 2.0,
239            )
240        except Exception:
241            return None
242
243    def get_def_point(self, name: str) -> Sequence[float] | None:
244        """Return a single defined point or a default value in its absence."""
245        val = self.defs.points.get(name)
246        return (
247            None
248            if val is None
249            else babase.vec3validate(val) if __debug__ else val
250        )
251
252    def get_def_points(self, name: str) -> list[Sequence[float]]:
253        """Return a list of named points.
254
255        Return as many sequential ones are defined (flag1, flag2, flag3), etc.
256        If none are defined, returns an empty list.
257        """
258        point_list = []
259        if self.defs and name + '1' in self.defs.points:
260            i = 1
261            while name + str(i) in self.defs.points:
262                pts = self.defs.points[name + str(i)]
263                if len(pts) == 6:
264                    point_list.append(pts)
265                else:
266                    if len(pts) != 3:
267                        raise ValueError('invalid point')
268                    point_list.append(pts + (0, 0, 0))
269                i += 1
270        return point_list
271
272    def get_start_position(self, team_index: int) -> Sequence[float]:
273        """Return a random starting position for the given team index."""
274        pnt = self.spawn_points[team_index % len(self.spawn_points)]
275        x_range = (-0.5, 0.5) if pnt[3] == 0.0 else (-pnt[3], pnt[3])
276        z_range = (-0.5, 0.5) if pnt[5] == 0.0 else (-pnt[5], pnt[5])
277        pnt = (
278            pnt[0] + random.uniform(*x_range),
279            pnt[1],
280            pnt[2] + random.uniform(*z_range),
281        )
282        return pnt
283
284    def get_ffa_start_position(
285        self, players: Sequence[bascenev1.Player]
286    ) -> Sequence[float]:
287        """Return a random starting position in one of the FFA spawn areas.
288
289        If a list of bascenev1.Player-s is provided; the returned points
290        will be as far from these players as possible.
291        """
292
293        # Get positions for existing players.
294        player_pts = []
295        for player in players:
296            if player.is_alive():
297                player_pts.append(player.position)
298
299        def _getpt() -> Sequence[float]:
300            point = self.ffa_spawn_points[self._next_ffa_start_index]
301            self._next_ffa_start_index = (self._next_ffa_start_index + 1) % len(
302                self.ffa_spawn_points
303            )
304            x_range = (-0.5, 0.5) if point[3] == 0.0 else (-point[3], point[3])
305            z_range = (-0.5, 0.5) if point[5] == 0.0 else (-point[5], point[5])
306            point = (
307                point[0] + random.uniform(*x_range),
308                point[1],
309                point[2] + random.uniform(*z_range),
310            )
311            return point
312
313        if not player_pts:
314            return _getpt()
315
316        # Let's calc several start points and then pick whichever is
317        # farthest from all existing players.
318        farthestpt_dist = -1.0
319        farthestpt = None
320        for _i in range(10):
321            testpt = babase.Vec3(_getpt())
322            closest_player_dist = 9999.0
323            for ppt in player_pts:
324                dist = (ppt - testpt).length()
325                closest_player_dist = min(dist, closest_player_dist)
326            if closest_player_dist > farthestpt_dist:
327                farthestpt_dist = closest_player_dist
328                farthestpt = testpt
329        assert farthestpt is not None
330        return tuple(farthestpt)
331
332    def get_flag_position(
333        self, team_index: int | None = None
334    ) -> Sequence[float]:
335        """Return a flag position on the map for the given team index.
336
337        Pass None to get the default flag point.
338        (used for things such as king-of-the-hill)
339        """
340        if team_index is None:
341            return self.flag_points_default[:3]
342        return self.flag_points[team_index % len(self.flag_points)][:3]
343
344    @override
345    def exists(self) -> bool:
346        return bool(self.node)
347
348    @override
349    def handlemessage(self, msg: Any) -> Any:
350        from bascenev1 import _messages
351
352        if isinstance(msg, _messages.DieMessage):
353            if self.node:
354                self.node.delete()
355        else:
356            return super().handlemessage(msg)
357        return None

A game map.

Consists of a collection of terrain nodes, metadata, and other functionality comprising a game map.

Map(vr_overlay_offset: Optional[Sequence[float]] = None)
106    def __init__(
107        self, vr_overlay_offset: Sequence[float] | None = None
108    ) -> None:
109        """Instantiate a map."""
110        super().__init__()
111
112        # This is expected to always be a bascenev1.Node object
113        # (whether valid or not) should be set to something meaningful
114        # by child classes.
115        self.node: _bascenev1.Node | None = None
116
117        # Make our class' preload-data available to us
118        # (and instruct the user if we weren't preloaded properly).
119        try:
120            self.preloaddata = _bascenev1.getactivity().preloads[type(self)]
121        except Exception as exc:
122            raise babase.NotFoundError(
123                'Preload data not found for '
124                + str(type(self))
125                + '; make sure to call the type\'s preload()'
126                ' staticmethod in the activity constructor'
127            ) from exc
128
129        # Set various globals.
130        gnode = _bascenev1.getactivity().globalsnode
131
132        # Set area-of-interest bounds.
133        aoi_bounds = self.get_def_bound_box('area_of_interest_bounds')
134        if aoi_bounds is None:
135            print('WARNING: no "aoi_bounds" found for map:', self.getname())
136            aoi_bounds = (-1, -1, -1, 1, 1, 1)
137        gnode.area_of_interest_bounds = aoi_bounds
138
139        # Set map bounds.
140        map_bounds = self.get_def_bound_box('map_bounds')
141        if map_bounds is None:
142            print('WARNING: no "map_bounds" found for map:', self.getname())
143            map_bounds = (-30, -10, -30, 30, 100, 30)
144        _bascenev1.set_map_bounds(map_bounds)
145
146        # Set shadow ranges.
147        try:
148            gnode.shadow_range = [
149                self.defs.points[v][1]
150                for v in [
151                    'shadow_lower_bottom',
152                    'shadow_lower_top',
153                    'shadow_upper_bottom',
154                    'shadow_upper_top',
155                ]
156            ]
157        except Exception:
158            pass
159
160        # In vr, set a fixed point in space for the overlay to show up at.
161        # By default we use the bounds center but allow the map to override it.
162        center = (
163            (aoi_bounds[0] + aoi_bounds[3]) * 0.5,
164            (aoi_bounds[1] + aoi_bounds[4]) * 0.5,
165            (aoi_bounds[2] + aoi_bounds[5]) * 0.5,
166        )
167        if vr_overlay_offset is not None:
168            center = (
169                center[0] + vr_overlay_offset[0],
170                center[1] + vr_overlay_offset[1],
171                center[2] + vr_overlay_offset[2],
172            )
173        gnode.vr_overlay_center = center
174        gnode.vr_overlay_center_enabled = True
175
176        self.spawn_points = self.get_def_points('spawn') or [(0, 0, 0, 0, 0, 0)]
177        self.ffa_spawn_points = self.get_def_points('ffa_spawn') or [
178            (0, 0, 0, 0, 0, 0)
179        ]
180        self.spawn_by_flag_points = self.get_def_points('spawn_by_flag') or [
181            (0, 0, 0, 0, 0, 0)
182        ]
183        self.flag_points = self.get_def_points('flag') or [(0, 0, 0)]
184
185        # We just want points.
186        self.flag_points = [p[:3] for p in self.flag_points]
187        self.flag_points_default = self.get_def_point('flag_default') or (
188            0,
189            1,
190            0,
191        )
192        self.powerup_spawn_points = self.get_def_points('powerup_spawn') or [
193            (0, 0, 0)
194        ]
195
196        # We just want points.
197        self.powerup_spawn_points = [p[:3] for p in self.powerup_spawn_points]
198        self.tnt_points = self.get_def_points('tnt') or []
199
200        # We just want points.
201        self.tnt_points = [p[:3] for p in self.tnt_points]
202
203        self.is_hockey = False
204        self.is_flying = False
205
206        # FIXME: this should be part of game; not map.
207        # Let's select random index for first spawn point,
208        # so that no one is offended by the constant spawn on the edge.
209        self._next_ffa_start_index = random.randrange(
210            len(self.ffa_spawn_points)
211        )

Instantiate a map.

defs: Any = None
name = 'Map'
@classmethod
def preload(cls) -> None:
61    @classmethod
62    def preload(cls) -> None:
63        """Preload map media.
64
65        This runs the class's on_preload() method as needed to prep it to run.
66        Preloading should generally be done in a bascenev1.Activity's
67        __init__ method. Note that this is a classmethod since it is not
68        operate on map instances but rather on the class itself before
69        instances are made
70        """
71        activity = _bascenev1.getactivity()
72        if cls not in activity.preloads:
73            activity.preloads[cls] = cls.on_preload()

Preload map media.

This runs the class's on_preload() method as needed to prep it to run. Preloading should generally be done in a bascenev1.Activity's __init__ method. Note that this is a classmethod since it is not operate on map instances but rather on the class itself before instances are made

@classmethod
def get_play_types(cls) -> list[str]:
75    @classmethod
76    def get_play_types(cls) -> list[str]:
77        """Return valid play types for this map."""
78        return []

Return valid play types for this map.

@classmethod
def get_preview_texture_name(cls) -> str | None:
80    @classmethod
81    def get_preview_texture_name(cls) -> str | None:
82        """Return the name of the preview texture for this map."""
83        return None

Return the name of the preview texture for this map.

@classmethod
def on_preload(cls) -> Any:
85    @classmethod
86    def on_preload(cls) -> Any:
87        """Called when the map is being preloaded.
88
89        It should return any media/data it requires to operate
90        """
91        return None

Called when the map is being preloaded.

It should return any media/data it requires to operate

@classmethod
def getname(cls) -> str:
93    @classmethod
94    def getname(cls) -> str:
95        """Return the unique name of this map, in English."""
96        return cls.name

Return the unique name of this map, in English.

@classmethod
def get_music_type(cls) -> MusicType | None:
 98    @classmethod
 99    def get_music_type(cls) -> bascenev1.MusicType | None:
100        """Return a music-type string that should be played on this map.
101
102        If None is returned, default music will be used.
103        """
104        return None

Return a music-type string that should be played on this map.

If None is returned, default music will be used.

node: _bascenev1.Node | None
spawn_points
ffa_spawn_points
spawn_by_flag_points
flag_points
flag_points_default
powerup_spawn_points
tnt_points
is_hockey
is_flying
def is_point_near_edge(self, point: _babase.Vec3, running: bool = False) -> bool:
213    def is_point_near_edge(
214        self, point: babase.Vec3, running: bool = False
215    ) -> bool:
216        """Return whether the provided point is near an edge of the map.
217
218        Simple bot logic uses this call to determine if they
219        are approaching a cliff or wall. If this returns True they will
220        generally not walk/run any farther away from the origin.
221        If 'running' is True, the buffer should be a bit larger.
222        """
223        del point, running  # Unused.
224        return False

Return whether the provided point is near an edge of the map.

Simple bot logic uses this call to determine if they are approaching a cliff or wall. If this returns True they will generally not walk/run any farther away from the origin. If 'running' is True, the buffer should be a bit larger.

def get_def_bound_box( self, name: str) -> tuple[float, float, float, float, float, float] | None:
226    def get_def_bound_box(
227        self, name: str
228    ) -> tuple[float, float, float, float, float, float] | None:
229        """Return a 6 member bounds tuple or None if it is not defined."""
230        try:
231            box = self.defs.boxes[name]
232            return (
233                box[0] - box[6] / 2.0,
234                box[1] - box[7] / 2.0,
235                box[2] - box[8] / 2.0,
236                box[0] + box[6] / 2.0,
237                box[1] + box[7] / 2.0,
238                box[2] + box[8] / 2.0,
239            )
240        except Exception:
241            return None

Return a 6 member bounds tuple or None if it is not defined.

def get_def_point(self, name: str) -> Optional[Sequence[float]]:
243    def get_def_point(self, name: str) -> Sequence[float] | None:
244        """Return a single defined point or a default value in its absence."""
245        val = self.defs.points.get(name)
246        return (
247            None
248            if val is None
249            else babase.vec3validate(val) if __debug__ else val
250        )

Return a single defined point or a default value in its absence.

def get_def_points(self, name: str) -> list[typing.Sequence[float]]:
252    def get_def_points(self, name: str) -> list[Sequence[float]]:
253        """Return a list of named points.
254
255        Return as many sequential ones are defined (flag1, flag2, flag3), etc.
256        If none are defined, returns an empty list.
257        """
258        point_list = []
259        if self.defs and name + '1' in self.defs.points:
260            i = 1
261            while name + str(i) in self.defs.points:
262                pts = self.defs.points[name + str(i)]
263                if len(pts) == 6:
264                    point_list.append(pts)
265                else:
266                    if len(pts) != 3:
267                        raise ValueError('invalid point')
268                    point_list.append(pts + (0, 0, 0))
269                i += 1
270        return point_list

Return a list of named points.

Return as many sequential ones are defined (flag1, flag2, flag3), etc. If none are defined, returns an empty list.

def get_start_position(self, team_index: int) -> Sequence[float]:
272    def get_start_position(self, team_index: int) -> Sequence[float]:
273        """Return a random starting position for the given team index."""
274        pnt = self.spawn_points[team_index % len(self.spawn_points)]
275        x_range = (-0.5, 0.5) if pnt[3] == 0.0 else (-pnt[3], pnt[3])
276        z_range = (-0.5, 0.5) if pnt[5] == 0.0 else (-pnt[5], pnt[5])
277        pnt = (
278            pnt[0] + random.uniform(*x_range),
279            pnt[1],
280            pnt[2] + random.uniform(*z_range),
281        )
282        return pnt

Return a random starting position for the given team index.

def get_ffa_start_position(self, players: Sequence[Player]) -> Sequence[float]:
284    def get_ffa_start_position(
285        self, players: Sequence[bascenev1.Player]
286    ) -> Sequence[float]:
287        """Return a random starting position in one of the FFA spawn areas.
288
289        If a list of bascenev1.Player-s is provided; the returned points
290        will be as far from these players as possible.
291        """
292
293        # Get positions for existing players.
294        player_pts = []
295        for player in players:
296            if player.is_alive():
297                player_pts.append(player.position)
298
299        def _getpt() -> Sequence[float]:
300            point = self.ffa_spawn_points[self._next_ffa_start_index]
301            self._next_ffa_start_index = (self._next_ffa_start_index + 1) % len(
302                self.ffa_spawn_points
303            )
304            x_range = (-0.5, 0.5) if point[3] == 0.0 else (-point[3], point[3])
305            z_range = (-0.5, 0.5) if point[5] == 0.0 else (-point[5], point[5])
306            point = (
307                point[0] + random.uniform(*x_range),
308                point[1],
309                point[2] + random.uniform(*z_range),
310            )
311            return point
312
313        if not player_pts:
314            return _getpt()
315
316        # Let's calc several start points and then pick whichever is
317        # farthest from all existing players.
318        farthestpt_dist = -1.0
319        farthestpt = None
320        for _i in range(10):
321            testpt = babase.Vec3(_getpt())
322            closest_player_dist = 9999.0
323            for ppt in player_pts:
324                dist = (ppt - testpt).length()
325                closest_player_dist = min(dist, closest_player_dist)
326            if closest_player_dist > farthestpt_dist:
327                farthestpt_dist = closest_player_dist
328                farthestpt = testpt
329        assert farthestpt is not None
330        return tuple(farthestpt)

Return a random starting position in one of the FFA spawn areas.

If a list of bascenev1.Player-s is provided; the returned points will be as far from these players as possible.

def get_flag_position(self, team_index: int | None = None) -> Sequence[float]:
332    def get_flag_position(
333        self, team_index: int | None = None
334    ) -> Sequence[float]:
335        """Return a flag position on the map for the given team index.
336
337        Pass None to get the default flag point.
338        (used for things such as king-of-the-hill)
339        """
340        if team_index is None:
341            return self.flag_points_default[:3]
342        return self.flag_points[team_index % len(self.flag_points)][:3]

Return a flag position on the map for the given team index.

Pass None to get the default flag point. (used for things such as king-of-the-hill)

@override
def exists(self) -> bool:
344    @override
345    def exists(self) -> bool:
346        return bool(self.node)

Returns whether the Actor is still present in a meaningful way.

Note that a dying character should still return True here as long as their corpse is visible; this is about presence, not being 'alive' (see bascenev1.Actor.is_alive() for that).

If this returns False, it is assumed the Actor can be completely deleted without affecting the game; this call is often used when pruning lists of Actors, such as with bascenev1.Actor.autoretain()

The default implementation of this method always return True.

Note that the boolean operator for the Actor class calls this method, so a simple "if myactor" test will conveniently do the right thing even if myactor is set to None.

@override
def handlemessage(self, msg: Any) -> Any:
348    @override
349    def handlemessage(self, msg: Any) -> Any:
350        from bascenev1 import _messages
351
352        if isinstance(msg, _messages.DieMessage):
353            if self.node:
354                self.node.delete()
355        else:
356            return super().handlemessage(msg)
357        return None

General message handling; can be passed any message object.

class Material:
264class Material:
265    """An entity applied to game objects to modify collision behavior.
266
267    A material can affect physical characteristics, generate sounds,
268    or trigger callback functions when collisions occur.
269
270    Materials are applied to 'parts', which are groups of one or more
271    rigid bodies created as part of a bascenev1.Node. Nodes can have any
272    number of parts, each with its own set of materials. Generally
273    materials are specified as array attributes on the Node. The `spaz`
274    node, for example, has various attributes such as `materials`,
275    `roller_materials`, and `punch_materials`, which correspond
276    to the various parts it creates.
277
278    Use bascenev1.Material to instantiate a blank material, and then use
279    its :meth:`bascenev1.Material.add_actions()` method to define what the
280    material does.
281    """
282
283    def __init__(self, label: str | None = None) -> None:
284        pass
285
286    label: str
287    """A label for the material; only used for debugging."""
288
289    def add_actions(
290        self, actions: tuple, conditions: tuple | None = None
291    ) -> None:
292        """Add one or more actions to the material, optionally with conditions.
293
294        Conditions
295        ==========
296
297        Conditions are provided as tuples which can be combined to form
298        boolean logic. A single condition might look like:
299
300        ``('condition_name', cond_arg)``
301
302        Or a more complex nested one might look like:
303
304        ``(('condition1', cond_arg), 'or', ('condition2', cond2_arg))``
305
306        The strings ``'and'``, ``'or'``, and ``'xor'`` can chain together
307        two conditions, as seen above.
308
309        Available Conditions
310        --------------------
311        ``('they_have_material', material)``
312          Does the part we're hitting have a given
313          :class:`bascenev1.Material`?
314
315        ``('they_dont_have_material', material)``
316          Does the part we're hitting not have a given
317          :class:`bascenev1.Material`?
318
319        ``('eval_colliding')``
320          Is ``'collide'`` true at this point
321          in material evaluation? (see the ``modify_part_collision`` action)
322
323        ``('eval_not_colliding')``
324          Is ``collide`` false at this point
325          in material evaluation? (see the ``modify_part_collision`` action)
326
327        ``('we_are_younger_than', age)``
328          Is our part younger than ``age`` (in milliseconds)?
329
330        ``('we_are_older_than', age)``
331          Is our part older than ``age`` (in milliseconds)?
332
333        ``('they_are_younger_than', age)``
334          Is the part we're hitting younger than ``age`` (in milliseconds)?
335
336        ``('they_are_older_than', age)``
337          Is the part we're hitting older than ``age`` (in milliseconds)?
338
339        ``('they_are_same_node_as_us')``
340          Does the part we're hitting belong to the same
341          :class:`bascenev1.Node`
342          as us?
343
344        ``('they_are_different_node_than_us')``
345          Does the part we're hitting belong to a different
346          :class:`bascenev1.Node`?
347
348        Actions
349        =======
350
351        In a similar manner, actions are specified as tuples. Multiple
352        actions can be specified by providing a tuple of tuples.
353
354        Available Actions
355        -----------------
356
357        ``('call', when, callable)``
358          Calls the provided callable;
359          ``when`` can be either ``'at_connect'`` or ``'at_disconnect'``.
360          ``'at_connect'`` means to fire when the two parts first come in
361          contact; ``'at_disconnect'`` means to fire once they cease being
362          in contact.
363
364        ``('message', who, when, message_obj)``
365          Sends a message object; ``who`` can be either ``'our_node'`` or
366          ``'their_node'``, ``when`` can be ``'at_connect'`` or
367          ``'at_disconnect'``, and ``message_obj`` is the message object to
368          send. This has the same effect as calling the node's
369          :meth:`bascenev1.Node.handlemessage()` method.
370
371        ``('modify_part_collision', attr, value)``
372          Changes some characteristic of the physical collision that will
373          occur between our part and their part. This change will remain in
374          effect as long as the two parts remain overlapping. This means if
375          you have a part with a material that turns ``'collide'`` off
376          against parts younger than 100ms, and it touches another part that
377          is 50ms old, it will continue to not collide with that part until
378          they separate, even if the 100ms threshold is passed. Options for
379          attr/value are:
380          ``'physical'`` (boolean value; whether a *physical* response will
381          occur at all), ``'friction'`` (float value; how friction-y the
382          physical response will be), ``'collide'`` (boolean value;
383          whether *any* collision will occur at all, including non-physical
384          stuff like callbacks), ``'use_node_collide'``
385          (boolean value; whether to honor modify_node_collision
386          overrides for this collision), ``'stiffness'`` (float value,
387          how springy the physical response is), ``'damping'`` (float
388          value, how damped the physical response is), ``'bounce'`` (float
389          value; how bouncy the physical response is).
390
391        ``('modify_node_collision', attr, value)``
392          Similar to ``modify_part_collision``, but operates at a
393          node-level. Collision attributes set here will remain in effect
394          as long as *anything* from our part's node and their part's node
395          overlap. A key use of this functionality is to prevent new nodes
396          from colliding with each other if they appear overlapped;
397          if ``modify_part_collision`` is used, only the individual
398          parts that were overlapping would avoid contact, but other parts
399          could still contact leaving the two nodes 'tangled up'. Using
400          ``modify_node_collision`` ensures that the nodes must completely
401          separate before they can start colliding. Currently the only attr
402          available here is ``'collide'`` (a boolean value).
403
404        ``('sound', sound, volume)``
405          Plays a :class:`bascenev1.Sound` when a collision occurs, at a
406          given volume, regardless of the collision speed/etc.
407
408        ``('impact_sound', sound, target_impulse, volume)``
409          Plays a sound when a collision occurs, based on the speed of
410          impact. Provide a :class:`bascenev1.Sound`, a target-impulse,
411          and a volume.
412
413        ``('skid_sound', sound, target_impulse, volume)``
414          Plays a sound during a collision when parts are 'scraping'
415          against each other. Provide a :class:`bascenev1.Sound`,
416          a target-impulse, and a volume.
417
418        ``('roll_sound', sound, targetImpulse, volume)``
419          Plays a sound during a collision when parts are 'rolling'
420          against each other.
421          Provide a :class:`bascenev1.Sound`, a target-impulse, and a
422          volume.
423
424        Examples
425        ========
426
427        **Example 1:** Create a material that lets us ignore
428        collisions against any nodes we touch in the first
429        100 ms of our existence; handy for preventing us from
430        exploding outward if we spawn on top of another object::
431
432          m = bascenev1.Material()
433          m.add_actions(
434               conditions=(('we_are_younger_than', 100),
435                           'or', ('they_are_younger_than', 100)),
436               actions=('modify_node_collision', 'collide', False))
437
438        **Example 2:** Send a :class:`bascenev1.DieMessage` to anything we
439        touch, but cause no physical response. This should cause any
440        :class:`bascenev1.Actor` to drop dead::
441
442           m = bascenev1.Material()
443           m.add_actions(
444            actions=(
445              ('modify_part_collision', 'physical', False),
446              ('message', 'their_node', 'at_connect', bascenev1.DieMessage())
447            )
448           )
449
450        **Example 3:** Play some sounds when we're contacting the
451        ground::
452
453          m = bascenev1.Material()
454          m.add_actions(
455            conditions=('they_have_material' shared.footing_material),
456            actions=(
457              ('impact_sound', bascenev1.getsound('metalHit'), 2, 5),
458              ('skid_sound', bascenev1.getsound('metalSkid'), 2, 5)
459            )
460          )
461        """
462        return None

An entity applied to game objects to modify collision behavior.

A material can affect physical characteristics, generate sounds, or trigger callback functions when collisions occur.

Materials are applied to 'parts', which are groups of one or more rigid bodies created as part of a bascenev1.Node. Nodes can have any number of parts, each with its own set of materials. Generally materials are specified as array attributes on the Node. The spaz node, for example, has various attributes such as materials, roller_materials, and punch_materials, which correspond to the various parts it creates.

Use bascenev1.Material to instantiate a blank material, and then use its bascenev1.Material.add_actions()() method to define what the material does.

Material(label: str | None = None)
283    def __init__(self, label: str | None = None) -> None:
284        pass
label: str

A label for the material; only used for debugging.

def add_actions(self, actions: tuple, conditions: tuple | None = None) -> None:
289    def add_actions(
290        self, actions: tuple, conditions: tuple | None = None
291    ) -> None:
292        """Add one or more actions to the material, optionally with conditions.
293
294        Conditions
295        ==========
296
297        Conditions are provided as tuples which can be combined to form
298        boolean logic. A single condition might look like:
299
300        ``('condition_name', cond_arg)``
301
302        Or a more complex nested one might look like:
303
304        ``(('condition1', cond_arg), 'or', ('condition2', cond2_arg))``
305
306        The strings ``'and'``, ``'or'``, and ``'xor'`` can chain together
307        two conditions, as seen above.
308
309        Available Conditions
310        --------------------
311        ``('they_have_material', material)``
312          Does the part we're hitting have a given
313          :class:`bascenev1.Material`?
314
315        ``('they_dont_have_material', material)``
316          Does the part we're hitting not have a given
317          :class:`bascenev1.Material`?
318
319        ``('eval_colliding')``
320          Is ``'collide'`` true at this point
321          in material evaluation? (see the ``modify_part_collision`` action)
322
323        ``('eval_not_colliding')``
324          Is ``collide`` false at this point
325          in material evaluation? (see the ``modify_part_collision`` action)
326
327        ``('we_are_younger_than', age)``
328          Is our part younger than ``age`` (in milliseconds)?
329
330        ``('we_are_older_than', age)``
331          Is our part older than ``age`` (in milliseconds)?
332
333        ``('they_are_younger_than', age)``
334          Is the part we're hitting younger than ``age`` (in milliseconds)?
335
336        ``('they_are_older_than', age)``
337          Is the part we're hitting older than ``age`` (in milliseconds)?
338
339        ``('they_are_same_node_as_us')``
340          Does the part we're hitting belong to the same
341          :class:`bascenev1.Node`
342          as us?
343
344        ``('they_are_different_node_than_us')``
345          Does the part we're hitting belong to a different
346          :class:`bascenev1.Node`?
347
348        Actions
349        =======
350
351        In a similar manner, actions are specified as tuples. Multiple
352        actions can be specified by providing a tuple of tuples.
353
354        Available Actions
355        -----------------
356
357        ``('call', when, callable)``
358          Calls the provided callable;
359          ``when`` can be either ``'at_connect'`` or ``'at_disconnect'``.
360          ``'at_connect'`` means to fire when the two parts first come in
361          contact; ``'at_disconnect'`` means to fire once they cease being
362          in contact.
363
364        ``('message', who, when, message_obj)``
365          Sends a message object; ``who`` can be either ``'our_node'`` or
366          ``'their_node'``, ``when`` can be ``'at_connect'`` or
367          ``'at_disconnect'``, and ``message_obj`` is the message object to
368          send. This has the same effect as calling the node's
369          :meth:`bascenev1.Node.handlemessage()` method.
370
371        ``('modify_part_collision', attr, value)``
372          Changes some characteristic of the physical collision that will
373          occur between our part and their part. This change will remain in
374          effect as long as the two parts remain overlapping. This means if
375          you have a part with a material that turns ``'collide'`` off
376          against parts younger than 100ms, and it touches another part that
377          is 50ms old, it will continue to not collide with that part until
378          they separate, even if the 100ms threshold is passed. Options for
379          attr/value are:
380          ``'physical'`` (boolean value; whether a *physical* response will
381          occur at all), ``'friction'`` (float value; how friction-y the
382          physical response will be), ``'collide'`` (boolean value;
383          whether *any* collision will occur at all, including non-physical
384          stuff like callbacks), ``'use_node_collide'``
385          (boolean value; whether to honor modify_node_collision
386          overrides for this collision), ``'stiffness'`` (float value,
387          how springy the physical response is), ``'damping'`` (float
388          value, how damped the physical response is), ``'bounce'`` (float
389          value; how bouncy the physical response is).
390
391        ``('modify_node_collision', attr, value)``
392          Similar to ``modify_part_collision``, but operates at a
393          node-level. Collision attributes set here will remain in effect
394          as long as *anything* from our part's node and their part's node
395          overlap. A key use of this functionality is to prevent new nodes
396          from colliding with each other if they appear overlapped;
397          if ``modify_part_collision`` is used, only the individual
398          parts that were overlapping would avoid contact, but other parts
399          could still contact leaving the two nodes 'tangled up'. Using
400          ``modify_node_collision`` ensures that the nodes must completely
401          separate before they can start colliding. Currently the only attr
402          available here is ``'collide'`` (a boolean value).
403
404        ``('sound', sound, volume)``
405          Plays a :class:`bascenev1.Sound` when a collision occurs, at a
406          given volume, regardless of the collision speed/etc.
407
408        ``('impact_sound', sound, target_impulse, volume)``
409          Plays a sound when a collision occurs, based on the speed of
410          impact. Provide a :class:`bascenev1.Sound`, a target-impulse,
411          and a volume.
412
413        ``('skid_sound', sound, target_impulse, volume)``
414          Plays a sound during a collision when parts are 'scraping'
415          against each other. Provide a :class:`bascenev1.Sound`,
416          a target-impulse, and a volume.
417
418        ``('roll_sound', sound, targetImpulse, volume)``
419          Plays a sound during a collision when parts are 'rolling'
420          against each other.
421          Provide a :class:`bascenev1.Sound`, a target-impulse, and a
422          volume.
423
424        Examples
425        ========
426
427        **Example 1:** Create a material that lets us ignore
428        collisions against any nodes we touch in the first
429        100 ms of our existence; handy for preventing us from
430        exploding outward if we spawn on top of another object::
431
432          m = bascenev1.Material()
433          m.add_actions(
434               conditions=(('we_are_younger_than', 100),
435                           'or', ('they_are_younger_than', 100)),
436               actions=('modify_node_collision', 'collide', False))
437
438        **Example 2:** Send a :class:`bascenev1.DieMessage` to anything we
439        touch, but cause no physical response. This should cause any
440        :class:`bascenev1.Actor` to drop dead::
441
442           m = bascenev1.Material()
443           m.add_actions(
444            actions=(
445              ('modify_part_collision', 'physical', False),
446              ('message', 'their_node', 'at_connect', bascenev1.DieMessage())
447            )
448           )
449
450        **Example 3:** Play some sounds when we're contacting the
451        ground::
452
453          m = bascenev1.Material()
454          m.add_actions(
455            conditions=('they_have_material' shared.footing_material),
456            actions=(
457              ('impact_sound', bascenev1.getsound('metalHit'), 2, 5),
458              ('skid_sound', bascenev1.getsound('metalSkid'), 2, 5)
459            )
460          )
461        """
462        return None

Add one or more actions to the material, optionally with conditions.

Conditions

Conditions are provided as tuples which can be combined to form boolean logic. A single condition might look like:

('condition_name', cond_arg)

Or a more complex nested one might look like:

(('condition1', cond_arg), 'or', ('condition2', cond2_arg))

The strings 'and', 'or', and 'xor' can chain together two conditions, as seen above.

Available Conditions

('they_have_material', material) Does the part we're hitting have a given bascenev1.Material?

('they_dont_have_material', material) Does the part we're hitting not have a given bascenev1.Material?

('eval_colliding') Is 'collide' true at this point in material evaluation? (see the modify_part_collision action)

('eval_not_colliding') Is collide false at this point in material evaluation? (see the modify_part_collision action)

('we_are_younger_than', age) Is our part younger than age (in milliseconds)?

('we_are_older_than', age) Is our part older than age (in milliseconds)?

('they_are_younger_than', age) Is the part we're hitting younger than age (in milliseconds)?

('they_are_older_than', age) Is the part we're hitting older than age (in milliseconds)?

('they_are_same_node_as_us') Does the part we're hitting belong to the same bascenev1.Node as us?

('they_are_different_node_than_us') Does the part we're hitting belong to a different bascenev1.Node?

Actions

In a similar manner, actions are specified as tuples. Multiple actions can be specified by providing a tuple of tuples.

Available Actions

('call', when, callable) Calls the provided callable; when can be either 'at_connect' or 'at_disconnect'. 'at_connect' means to fire when the two parts first come in contact; 'at_disconnect' means to fire once they cease being in contact.

('message', who, when, message_obj) Sends a message object; who can be either 'our_node' or 'their_node', when can be 'at_connect' or 'at_disconnect', and message_obj is the message object to send. This has the same effect as calling the node's bascenev1.Node.handlemessage()() method.

('modify_part_collision', attr, value) Changes some characteristic of the physical collision that will occur between our part and their part. This change will remain in effect as long as the two parts remain overlapping. This means if you have a part with a material that turns 'collide' off against parts younger than 100ms, and it touches another part that is 50ms old, it will continue to not collide with that part until they separate, even if the 100ms threshold is passed. Options for attr/value are: 'physical' (boolean value; whether a physical response will occur at all), 'friction' (float value; how friction-y the physical response will be), 'collide' (boolean value; whether any collision will occur at all, including non-physical stuff like callbacks), 'use_node_collide' (boolean value; whether to honor modify_node_collision overrides for this collision), 'stiffness' (float value, how springy the physical response is), 'damping' (float value, how damped the physical response is), 'bounce' (float value; how bouncy the physical response is).

('modify_node_collision', attr, value) Similar to modify_part_collision, but operates at a node-level. Collision attributes set here will remain in effect as long as anything from our part's node and their part's node overlap. A key use of this functionality is to prevent new nodes from colliding with each other if they appear overlapped; if modify_part_collision is used, only the individual parts that were overlapping would avoid contact, but other parts could still contact leaving the two nodes 'tangled up'. Using modify_node_collision ensures that the nodes must completely separate before they can start colliding. Currently the only attr available here is 'collide' (a boolean value).

('sound', sound, volume) Plays a bascenev1.Sound when a collision occurs, at a given volume, regardless of the collision speed/etc.

('impact_sound', sound, target_impulse, volume) Plays a sound when a collision occurs, based on the speed of impact. Provide a bascenev1.Sound, a target-impulse, and a volume.

('skid_sound', sound, target_impulse, volume) Plays a sound during a collision when parts are 'scraping' against each other. Provide a bascenev1.Sound, a target-impulse, and a volume.

('roll_sound', sound, targetImpulse, volume) Plays a sound during a collision when parts are 'rolling' against each other. Provide a bascenev1.Sound, a target-impulse, and a volume.

Examples

Example 1: Create a material that lets us ignore collisions against any nodes we touch in the first 100 ms of our existence; handy for preventing us from exploding outward if we spawn on top of another object::

m = bascenev1.Material() m.add_actions( conditions=(('we_are_younger_than', 100), 'or', ('they_are_younger_than', 100)), actions=('modify_node_collision', 'collide', False))

Example 2: Send a bascenev1.DieMessage to anything we touch, but cause no physical response. This should cause any bascenev1.Actor to drop dead::

m = bascenev1.Material() m.add_actions( actions=( ('modify_part_collision', 'physical', False), ('message', 'their_node', 'at_connect', bascenev1.DieMessage()) ) )

Example 3: Play some sounds when we're contacting the ground::

m = bascenev1.Material() m.add_actions( conditions=('they_have_material' shared.footing_material), actions=( ('impact_sound', bascenev1.getsound('metalHit'), 2, 5), ('skid_sound', bascenev1.getsound('metalSkid'), 2, 5) ) )

class Mesh:
465class Mesh:
466    """A reference to a mesh.
467
468    Meshes are used for drawing.
469    Use bascenev1.getmesh() to instantiate one.
470    """
471
472    pass

A reference to a mesh.

Meshes are used for drawing. Use bascenev1.getmesh() to instantiate one.

class MultiTeamSession(bascenev1.Session):
 26class MultiTeamSession(Session):
 27    """Common base for DualTeamSession and FreeForAllSession.
 28
 29    Free-for-all-mode is essentially just teams-mode with each
 30    bascenev1.Player having their own bascenev1.Team, so there is much
 31    overlap in functionality.
 32    """
 33
 34    # These should be overridden.
 35    _playlist_selection_var = 'UNSET Playlist Selection'
 36    _playlist_randomize_var = 'UNSET Playlist Randomize'
 37    _playlists_var = 'UNSET Playlists'
 38
 39    def __init__(self) -> None:
 40        """Set up playlists & launch a bascenev1.Activity to accept joiners."""
 41        # pylint: disable=cyclic-import
 42        from bascenev1 import _playlist
 43        from bascenev1lib.activity.multiteamjoin import MultiTeamJoinActivity
 44
 45        app = babase.app
 46        classic = app.classic
 47        assert classic is not None
 48        cfg = app.config
 49
 50        if self.use_teams:
 51            team_names = cfg.get('Custom Team Names', DEFAULT_TEAM_NAMES)
 52            team_colors = cfg.get('Custom Team Colors', DEFAULT_TEAM_COLORS)
 53        else:
 54            team_names = None
 55            team_colors = None
 56
 57        # print('FIXME: TEAM BASE SESSION WOULD CALC DEPS.')
 58        depsets: Sequence[bascenev1.DependencySet] = []
 59
 60        super().__init__(
 61            depsets,
 62            team_names=team_names,
 63            team_colors=team_colors,
 64            min_players=1,
 65            max_players=self.get_max_players(),
 66        )
 67
 68        self._series_length: int = int(cfg.get('Teams Series Length', 7))
 69        self._ffa_series_length: int = int(cfg.get('FFA Series Length', 24))
 70
 71        show_tutorial = cfg.get('Show Tutorial', True)
 72
 73        # Special case: don't show tutorial while stress testing.
 74        if classic.stress_test_update_timer is not None:
 75            show_tutorial = False
 76
 77        self._tutorial_activity_instance: bascenev1.Activity | None
 78        if show_tutorial:
 79            from bascenev1lib.tutorial import TutorialActivity
 80
 81            tutorial_activity = TutorialActivity
 82
 83            # Get this loading.
 84            self._tutorial_activity_instance = _bascenev1.newactivity(
 85                tutorial_activity
 86            )
 87        else:
 88            self._tutorial_activity_instance = None
 89
 90        self._playlist_name = cfg.get(
 91            self._playlist_selection_var, '__default__'
 92        )
 93        self._playlist_randomize = cfg.get(self._playlist_randomize_var, False)
 94
 95        # Which game activity we're on.
 96        self._game_number = 0
 97
 98        playlists = cfg.get(self._playlists_var, {})
 99
100        if (
101            self._playlist_name != '__default__'
102            and self._playlist_name in playlists
103        ):
104            # Make sure to copy this, as we muck with it in place once we've
105            # got it and we don't want that to affect our config.
106            playlist = copy.deepcopy(playlists[self._playlist_name])
107        else:
108            if self.use_teams:
109                playlist = _playlist.get_default_teams_playlist()
110            else:
111                playlist = _playlist.get_default_free_for_all_playlist()
112
113        # Resolve types and whatnot to get our final playlist.
114        playlist_resolved = _playlist.filter_playlist(
115            playlist,
116            sessiontype=type(self),
117            add_resolved_type=True,
118            name='default teams' if self.use_teams else 'default ffa',
119        )
120
121        if not playlist_resolved:
122            raise RuntimeError('Playlist contains no valid games.')
123
124        self._playlist = ShuffleList(
125            playlist_resolved, shuffle=self._playlist_randomize
126        )
127
128        # Get a game on deck ready to go.
129        self._current_game_spec: dict[str, Any] | None = None
130        self._next_game_spec: dict[str, Any] = self._playlist.pull_next()
131        self._next_game: type[bascenev1.GameActivity] = self._next_game_spec[
132            'resolved_type'
133        ]
134
135        # Go ahead and instantiate the next game we'll
136        # use so it has lots of time to load.
137        self._instantiate_next_game()
138
139        # Start in our custom join screen.
140        self.setactivity(_bascenev1.newactivity(MultiTeamJoinActivity))
141
142    def get_ffa_series_length(self) -> int:
143        """Return free-for-all series length."""
144        return self._ffa_series_length
145
146    def get_series_length(self) -> int:
147        """Return teams series length."""
148        return self._series_length
149
150    def get_next_game_description(self) -> babase.Lstr:
151        """Returns a description of the next game on deck."""
152        # pylint: disable=cyclic-import
153        from bascenev1._gameactivity import GameActivity
154
155        gametype: type[GameActivity] = self._next_game_spec['resolved_type']
156        assert issubclass(gametype, GameActivity)
157        return gametype.get_settings_display_string(self._next_game_spec)
158
159    def get_game_number(self) -> int:
160        """Returns which game in the series is currently being played."""
161        return self._game_number
162
163    @override
164    def on_team_join(self, team: bascenev1.SessionTeam) -> None:
165        team.customdata['previous_score'] = team.customdata['score'] = 0
166
167    def get_max_players(self) -> int:
168        """Return max number of Players allowed to join the game at once."""
169        if self.use_teams:
170            val = babase.app.config.get('Team Game Max Players', 8)
171        else:
172            val = babase.app.config.get('Free-for-All Max Players', 8)
173        assert isinstance(val, int)
174        return val
175
176    def _instantiate_next_game(self) -> None:
177        self._next_game_instance = _bascenev1.newactivity(
178            self._next_game_spec['resolved_type'],
179            self._next_game_spec['settings'],
180        )
181
182    @override
183    def on_activity_end(
184        self, activity: bascenev1.Activity, results: Any
185    ) -> None:
186        # pylint: disable=cyclic-import
187        from bascenev1lib.tutorial import TutorialActivity
188        from bascenev1lib.activity.multiteamvictory import (
189            TeamSeriesVictoryScoreScreenActivity,
190        )
191        from bascenev1._activitytypes import (
192            TransitionActivity,
193            JoinActivity,
194            ScoreScreenActivity,
195        )
196
197        # If we have a tutorial to show, that's the first thing we do no
198        # matter what.
199        if self._tutorial_activity_instance is not None:
200            self.setactivity(self._tutorial_activity_instance)
201            self._tutorial_activity_instance = None
202
203        # If we're leaving the tutorial activity, pop a transition activity
204        # to transition us into a round gracefully (otherwise we'd snap from
205        # one terrain to another instantly).
206        elif isinstance(activity, TutorialActivity):
207            self.setactivity(_bascenev1.newactivity(TransitionActivity))
208
209        # If we're in a between-round activity or a restart-activity, hop
210        # into a round.
211        elif isinstance(
212            activity, (JoinActivity, TransitionActivity, ScoreScreenActivity)
213        ):
214            # If we're coming from a series-end activity, reset scores.
215            if isinstance(activity, TeamSeriesVictoryScoreScreenActivity):
216                self.stats.reset()
217                self._game_number = 0
218                for team in self.sessionteams:
219                    team.customdata['score'] = 0
220
221            # Otherwise just set accum (per-game) scores.
222            else:
223                self.stats.reset_accum()
224
225            next_game = self._next_game_instance
226
227            self._current_game_spec = self._next_game_spec
228            self._next_game_spec = self._playlist.pull_next()
229            self._game_number += 1
230
231            # Instantiate the next now so they have plenty of time to load.
232            self._instantiate_next_game()
233
234            # (Re)register all players and wire stats to our next activity.
235            for player in self.sessionplayers:
236                # ..but only ones who have been placed on a team
237                # (ie: no longer sitting in the lobby).
238                try:
239                    has_team = player.sessionteam is not None
240                except babase.NotFoundError:
241                    has_team = False
242                if has_team:
243                    self.stats.register_sessionplayer(player)
244            self.stats.setactivity(next_game)
245
246            # Now flip the current activity.
247            self.setactivity(next_game)
248
249        # If we're leaving a round, go to the score screen.
250        else:
251            self._switch_to_score_screen(results)
252
253    def _switch_to_score_screen(self, results: Any) -> None:
254        """Switch to a score screen after leaving a round."""
255        del results  # Unused arg.
256        logging.error('This should be overridden.', stack_info=True)
257
258    def announce_game_results(
259        self,
260        activity: bascenev1.GameActivity,
261        results: bascenev1.GameResults,
262        delay: float,
263        announce_winning_team: bool = True,
264    ) -> None:
265        """Show basic game result at the end of a game.
266
267        (before transitioning to a score screen).
268        This will include a zoom-text of 'BLUE WINS'
269        or whatnot, along with a possible audio
270        announcement of the same.
271        """
272        # pylint: disable=cyclic-import
273        from bascenev1._gameutils import cameraflash
274        from bascenev1._freeforallsession import FreeForAllSession
275        from bascenev1._messages import CelebrateMessage
276
277        _bascenev1.timer(delay, _bascenev1.getsound('boxingBell').play)
278
279        if announce_winning_team:
280            winning_sessionteam = results.winning_sessionteam
281            if winning_sessionteam is not None:
282                # Have all players celebrate.
283                celebrate_msg = CelebrateMessage(duration=10.0)
284                assert winning_sessionteam.activityteam is not None
285                for player in winning_sessionteam.activityteam.players:
286                    if player.actor:
287                        player.actor.handlemessage(celebrate_msg)
288                cameraflash()
289
290                # Some languages say "FOO WINS" different for teams vs players.
291                if isinstance(self, FreeForAllSession):
292                    wins_resource = 'winsPlayerText'
293                else:
294                    wins_resource = 'winsTeamText'
295                wins_text = babase.Lstr(
296                    resource=wins_resource,
297                    subs=[('${NAME}', winning_sessionteam.name)],
298                )
299                activity.show_zoom_message(
300                    wins_text,
301                    scale=0.85,
302                    color=babase.normalized_color(winning_sessionteam.color),
303                )

Common base for DualTeamSession and FreeForAllSession.

Free-for-all-mode is essentially just teams-mode with each bascenev1.Player having their own bascenev1.Team, so there is much overlap in functionality.

MultiTeamSession()
 39    def __init__(self) -> None:
 40        """Set up playlists & launch a bascenev1.Activity to accept joiners."""
 41        # pylint: disable=cyclic-import
 42        from bascenev1 import _playlist
 43        from bascenev1lib.activity.multiteamjoin import MultiTeamJoinActivity
 44
 45        app = babase.app
 46        classic = app.classic
 47        assert classic is not None
 48        cfg = app.config
 49
 50        if self.use_teams:
 51            team_names = cfg.get('Custom Team Names', DEFAULT_TEAM_NAMES)
 52            team_colors = cfg.get('Custom Team Colors', DEFAULT_TEAM_COLORS)
 53        else:
 54            team_names = None
 55            team_colors = None
 56
 57        # print('FIXME: TEAM BASE SESSION WOULD CALC DEPS.')
 58        depsets: Sequence[bascenev1.DependencySet] = []
 59
 60        super().__init__(
 61            depsets,
 62            team_names=team_names,
 63            team_colors=team_colors,
 64            min_players=1,
 65            max_players=self.get_max_players(),
 66        )
 67
 68        self._series_length: int = int(cfg.get('Teams Series Length', 7))
 69        self._ffa_series_length: int = int(cfg.get('FFA Series Length', 24))
 70
 71        show_tutorial = cfg.get('Show Tutorial', True)
 72
 73        # Special case: don't show tutorial while stress testing.
 74        if classic.stress_test_update_timer is not None:
 75            show_tutorial = False
 76
 77        self._tutorial_activity_instance: bascenev1.Activity | None
 78        if show_tutorial:
 79            from bascenev1lib.tutorial import TutorialActivity
 80
 81            tutorial_activity = TutorialActivity
 82
 83            # Get this loading.
 84            self._tutorial_activity_instance = _bascenev1.newactivity(
 85                tutorial_activity
 86            )
 87        else:
 88            self._tutorial_activity_instance = None
 89
 90        self._playlist_name = cfg.get(
 91            self._playlist_selection_var, '__default__'
 92        )
 93        self._playlist_randomize = cfg.get(self._playlist_randomize_var, False)
 94
 95        # Which game activity we're on.
 96        self._game_number = 0
 97
 98        playlists = cfg.get(self._playlists_var, {})
 99
100        if (
101            self._playlist_name != '__default__'
102            and self._playlist_name in playlists
103        ):
104            # Make sure to copy this, as we muck with it in place once we've
105            # got it and we don't want that to affect our config.
106            playlist = copy.deepcopy(playlists[self._playlist_name])
107        else:
108            if self.use_teams:
109                playlist = _playlist.get_default_teams_playlist()
110            else:
111                playlist = _playlist.get_default_free_for_all_playlist()
112
113        # Resolve types and whatnot to get our final playlist.
114        playlist_resolved = _playlist.filter_playlist(
115            playlist,
116            sessiontype=type(self),
117            add_resolved_type=True,
118            name='default teams' if self.use_teams else 'default ffa',
119        )
120
121        if not playlist_resolved:
122            raise RuntimeError('Playlist contains no valid games.')
123
124        self._playlist = ShuffleList(
125            playlist_resolved, shuffle=self._playlist_randomize
126        )
127
128        # Get a game on deck ready to go.
129        self._current_game_spec: dict[str, Any] | None = None
130        self._next_game_spec: dict[str, Any] = self._playlist.pull_next()
131        self._next_game: type[bascenev1.GameActivity] = self._next_game_spec[
132            'resolved_type'
133        ]
134
135        # Go ahead and instantiate the next game we'll
136        # use so it has lots of time to load.
137        self._instantiate_next_game()
138
139        # Start in our custom join screen.
140        self.setactivity(_bascenev1.newactivity(MultiTeamJoinActivity))

Set up playlists & launch a bascenev1.Activity to accept joiners.

def get_ffa_series_length(self) -> int:
142    def get_ffa_series_length(self) -> int:
143        """Return free-for-all series length."""
144        return self._ffa_series_length

Return free-for-all series length.

def get_series_length(self) -> int:
146    def get_series_length(self) -> int:
147        """Return teams series length."""
148        return self._series_length

Return teams series length.

def get_next_game_description(self) -> Lstr:
150    def get_next_game_description(self) -> babase.Lstr:
151        """Returns a description of the next game on deck."""
152        # pylint: disable=cyclic-import
153        from bascenev1._gameactivity import GameActivity
154
155        gametype: type[GameActivity] = self._next_game_spec['resolved_type']
156        assert issubclass(gametype, GameActivity)
157        return gametype.get_settings_display_string(self._next_game_spec)

Returns a description of the next game on deck.

def get_game_number(self) -> int:
159    def get_game_number(self) -> int:
160        """Returns which game in the series is currently being played."""
161        return self._game_number

Returns which game in the series is currently being played.

@override
def on_team_join(self, team: SessionTeam) -> None:
163    @override
164    def on_team_join(self, team: bascenev1.SessionTeam) -> None:
165        team.customdata['previous_score'] = team.customdata['score'] = 0

Called when a new bascenev1.Team joins the session.

def get_max_players(self) -> int:
167    def get_max_players(self) -> int:
168        """Return max number of Players allowed to join the game at once."""
169        if self.use_teams:
170            val = babase.app.config.get('Team Game Max Players', 8)
171        else:
172            val = babase.app.config.get('Free-for-All Max Players', 8)
173        assert isinstance(val, int)
174        return val

Return max number of Players allowed to join the game at once.

@override
def on_activity_end(self, activity: Activity, results: Any) -> None:
182    @override
183    def on_activity_end(
184        self, activity: bascenev1.Activity, results: Any
185    ) -> None:
186        # pylint: disable=cyclic-import
187        from bascenev1lib.tutorial import TutorialActivity
188        from bascenev1lib.activity.multiteamvictory import (
189            TeamSeriesVictoryScoreScreenActivity,
190        )
191        from bascenev1._activitytypes import (
192            TransitionActivity,
193            JoinActivity,
194            ScoreScreenActivity,
195        )
196
197        # If we have a tutorial to show, that's the first thing we do no
198        # matter what.
199        if self._tutorial_activity_instance is not None:
200            self.setactivity(self._tutorial_activity_instance)
201            self._tutorial_activity_instance = None
202
203        # If we're leaving the tutorial activity, pop a transition activity
204        # to transition us into a round gracefully (otherwise we'd snap from
205        # one terrain to another instantly).
206        elif isinstance(activity, TutorialActivity):
207            self.setactivity(_bascenev1.newactivity(TransitionActivity))
208
209        # If we're in a between-round activity or a restart-activity, hop
210        # into a round.
211        elif isinstance(
212            activity, (JoinActivity, TransitionActivity, ScoreScreenActivity)
213        ):
214            # If we're coming from a series-end activity, reset scores.
215            if isinstance(activity, TeamSeriesVictoryScoreScreenActivity):
216                self.stats.reset()
217                self._game_number = 0
218                for team in self.sessionteams:
219                    team.customdata['score'] = 0
220
221            # Otherwise just set accum (per-game) scores.
222            else:
223                self.stats.reset_accum()
224
225            next_game = self._next_game_instance
226
227            self._current_game_spec = self._next_game_spec
228            self._next_game_spec = self._playlist.pull_next()
229            self._game_number += 1
230
231            # Instantiate the next now so they have plenty of time to load.
232            self._instantiate_next_game()
233
234            # (Re)register all players and wire stats to our next activity.
235            for player in self.sessionplayers:
236                # ..but only ones who have been placed on a team
237                # (ie: no longer sitting in the lobby).
238                try:
239                    has_team = player.sessionteam is not None
240                except babase.NotFoundError:
241                    has_team = False
242                if has_team:
243                    self.stats.register_sessionplayer(player)
244            self.stats.setactivity(next_game)
245
246            # Now flip the current activity.
247            self.setactivity(next_game)
248
249        # If we're leaving a round, go to the score screen.
250        else:
251            self._switch_to_score_screen(results)

Called when the current bascenev1.Activity has ended.

The bascenev1.Session should look at the results and start another bascenev1.Activity.

def announce_game_results( self, activity: GameActivity, results: GameResults, delay: float, announce_winning_team: bool = True) -> None:
258    def announce_game_results(
259        self,
260        activity: bascenev1.GameActivity,
261        results: bascenev1.GameResults,
262        delay: float,
263        announce_winning_team: bool = True,
264    ) -> None:
265        """Show basic game result at the end of a game.
266
267        (before transitioning to a score screen).
268        This will include a zoom-text of 'BLUE WINS'
269        or whatnot, along with a possible audio
270        announcement of the same.
271        """
272        # pylint: disable=cyclic-import
273        from bascenev1._gameutils import cameraflash
274        from bascenev1._freeforallsession import FreeForAllSession
275        from bascenev1._messages import CelebrateMessage
276
277        _bascenev1.timer(delay, _bascenev1.getsound('boxingBell').play)
278
279        if announce_winning_team:
280            winning_sessionteam = results.winning_sessionteam
281            if winning_sessionteam is not None:
282                # Have all players celebrate.
283                celebrate_msg = CelebrateMessage(duration=10.0)
284                assert winning_sessionteam.activityteam is not None
285                for player in winning_sessionteam.activityteam.players:
286                    if player.actor:
287                        player.actor.handlemessage(celebrate_msg)
288                cameraflash()
289
290                # Some languages say "FOO WINS" different for teams vs players.
291                if isinstance(self, FreeForAllSession):
292                    wins_resource = 'winsPlayerText'
293                else:
294                    wins_resource = 'winsTeamText'
295                wins_text = babase.Lstr(
296                    resource=wins_resource,
297                    subs=[('${NAME}', winning_sessionteam.name)],
298                )
299                activity.show_zoom_message(
300                    wins_text,
301                    scale=0.85,
302                    color=babase.normalized_color(winning_sessionteam.color),
303                )

Show basic game result at the end of a game.

(before transitioning to a score screen). This will include a zoom-text of 'BLUE WINS' or whatnot, along with a possible audio announcement of the same.

class MusicType(enum.Enum):
17class MusicType(Enum):
18    """Types of music available to play in-game.
19
20    These do not correspond to specific pieces of music, but rather to
21    'situations'. The actual music played for each type can be overridden
22    by the game or by the user.
23    """
24
25    MENU = 'Menu'
26    VICTORY = 'Victory'
27    CHAR_SELECT = 'CharSelect'
28    RUN_AWAY = 'RunAway'
29    ONSLAUGHT = 'Onslaught'
30    KEEP_AWAY = 'Keep Away'
31    RACE = 'Race'
32    EPIC_RACE = 'Epic Race'
33    SCORES = 'Scores'
34    GRAND_ROMP = 'GrandRomp'
35    TO_THE_DEATH = 'ToTheDeath'
36    CHOSEN_ONE = 'Chosen One'
37    FORWARD_MARCH = 'ForwardMarch'
38    FLAG_CATCHER = 'FlagCatcher'
39    SURVIVAL = 'Survival'
40    EPIC = 'Epic'
41    SPORTS = 'Sports'
42    HOCKEY = 'Hockey'
43    FOOTBALL = 'Football'
44    FLYING = 'Flying'
45    SCARY = 'Scary'
46    MARCHING = 'Marching'

Types of music available to play in-game.

These do not correspond to specific pieces of music, but rather to 'situations'. The actual music played for each type can be overridden by the game or by the user.

MENU = <MusicType.MENU: 'Menu'>
VICTORY = <MusicType.VICTORY: 'Victory'>
CHAR_SELECT = <MusicType.CHAR_SELECT: 'CharSelect'>
RUN_AWAY = <MusicType.RUN_AWAY: 'RunAway'>
ONSLAUGHT = <MusicType.ONSLAUGHT: 'Onslaught'>
KEEP_AWAY = <MusicType.KEEP_AWAY: 'Keep Away'>
RACE = <MusicType.RACE: 'Race'>
EPIC_RACE = <MusicType.EPIC_RACE: 'Epic Race'>
SCORES = <MusicType.SCORES: 'Scores'>
GRAND_ROMP = <MusicType.GRAND_ROMP: 'GrandRomp'>
TO_THE_DEATH = <MusicType.TO_THE_DEATH: 'ToTheDeath'>
CHOSEN_ONE = <MusicType.CHOSEN_ONE: 'Chosen One'>
FORWARD_MARCH = <MusicType.FORWARD_MARCH: 'ForwardMarch'>
FLAG_CATCHER = <MusicType.FLAG_CATCHER: 'FlagCatcher'>
SURVIVAL = <MusicType.SURVIVAL: 'Survival'>
EPIC = <MusicType.EPIC: 'Epic'>
SPORTS = <MusicType.SPORTS: 'Sports'>
HOCKEY = <MusicType.HOCKEY: 'Hockey'>
FOOTBALL = <MusicType.FOOTBALL: 'Football'>
FLYING = <MusicType.FLYING: 'Flying'>
SCARY = <MusicType.SCARY: 'Scary'>
MARCHING = <MusicType.MARCHING: 'Marching'>
def newactivity( activity_type: type[Activity], settings: dict | None = None) -> Activity:
1472def newactivity(
1473    activity_type: type[bascenev1.Activity], settings: dict | None = None
1474) -> bascenev1.Activity:
1475    """Instantiates a bascenev1.Activity given a type object.
1476
1477    Activities require special setup and thus cannot be directly
1478    instantiated; you must go through this function.
1479    """
1480    import bascenev1  # pylint: disable=cyclic-import
1481
1482    return bascenev1.Activity(settings={})

Instantiates a bascenev1.Activity given a type object.

Activities require special setup and thus cannot be directly instantiated; you must go through this function.

def newnode( type: str, owner: _bascenev1.Node | None = None, attrs: dict | None = None, name: str | None = None, delegate: Any = None) -> _bascenev1.Node:
1486def newnode(
1487    type: str,
1488    owner: bascenev1.Node | None = None,
1489    attrs: dict | None = None,
1490    name: str | None = None,
1491    delegate: Any = None,
1492) -> bascenev1.Node:
1493    """Add a node of the given type to the game.
1494
1495    If a dict is provided for 'attributes', the node's initial attributes
1496    will be set based on them.
1497
1498    'name', if provided, will be stored with the node purely for debugging
1499    purposes. If no name is provided, an automatic one will be generated
1500    such as 'terrain@foo.py:30'.
1501
1502    If 'delegate' is provided, Python messages sent to the node will go to
1503    that object's handlemessage() method. Note that the delegate is stored
1504    as a weak-ref, so the node itself will not keep the object alive.
1505
1506    if 'owner' is provided, the node will be automatically killed when that
1507    object dies. 'owner' can be another node or a bascenev1.Actor
1508    """
1509    import bascenev1  # pylint: disable=cyclic-import
1510
1511    return bascenev1.Node()

Add a node of the given type to the game.

If a dict is provided for 'attributes', the node's initial attributes will be set based on them.

'name', if provided, will be stored with the node purely for debugging purposes. If no name is provided, an automatic one will be generated such as 'terrain@foo.py:30'.

If 'delegate' is provided, Python messages sent to the node will go to that object's handlemessage() method. Note that the delegate is stored as a weak-ref, so the node itself will not keep the object alive.

if 'owner' is provided, the node will be automatically killed when that object dies. 'owner' can be another node or a bascenev1.Actor

class Node:
476class Node:
477    """Reference to a Node; the low level building block of a game.
478
479    At its core, a game is nothing more than a scene of Nodes
480    with attributes getting interconnected or set over time.
481
482    A bascenev1.Node instance should be thought of as a weak-reference
483    to a game node; *not* the node itself. This means a Node's
484    lifecycle is completely independent of how many Python references
485    to it exist. To explicitly add a new node to the game, use
486    bascenev1.newnode(), and to explicitly delete one,
487     use bascenev1.Node.delete().
488    bascenev1.Node.exists() can be used to determine if a Node still points
489    to a live node in the game.
490
491    You can use `ba.Node(None)` to instantiate an invalid
492    Node reference (sometimes used as attr values/etc).
493    """
494
495    # Note attributes:
496    # NOTE: I'm just adding *all* possible node attrs here
497    # now now since we have a single bascenev1.Node type; in the
498    # future I hope to create proper individual classes
499    # corresponding to different node types with correct
500    # attributes per node-type.
501    color: Sequence[float] = (0.0, 0.0, 0.0)
502    size: Sequence[float] = (0.0, 0.0, 0.0)
503    position: Sequence[float] = (0.0, 0.0, 0.0)
504    position_center: Sequence[float] = (0.0, 0.0, 0.0)
505    position_forward: Sequence[float] = (0.0, 0.0, 0.0)
506    punch_position: Sequence[float] = (0.0, 0.0, 0.0)
507    punch_velocity: Sequence[float] = (0.0, 0.0, 0.0)
508    velocity: Sequence[float] = (0.0, 0.0, 0.0)
509    name_color: Sequence[float] = (0.0, 0.0, 0.0)
510    tint_color: Sequence[float] = (0.0, 0.0, 0.0)
511    tint2_color: Sequence[float] = (0.0, 0.0, 0.0)
512    text: babase.Lstr | str = ''
513    texture: bascenev1.Texture | None = None
514    tint_texture: bascenev1.Texture | None = None
515    times: Sequence[int] = (1, 2, 3, 4, 5)
516    values: Sequence[float] = (1.0, 2.0, 3.0, 4.0)
517    offset: float = 0.0
518    input0: float = 0.0
519    input1: float = 0.0
520    input2: float = 0.0
521    input3: float = 0.0
522    flashing: bool = False
523    scale: float | Sequence[float] = 0.0
524    opacity: float = 0.0
525    loop: bool = False
526    time1: int = 0
527    time2: int = 0
528    timemax: int = 0
529    client_only: bool = False
530    materials: Sequence[bascenev1.Material] = ()
531    roller_materials: Sequence[bascenev1.Material] = ()
532    name: str = ''
533    punch_materials: Sequence[bascenev1.Material] = ()
534    pickup_materials: Sequence[bascenev1.Material] = ()
535    extras_material: Sequence[bascenev1.Material] = ()
536    rotate: float = 0.0
537    hold_node: bascenev1.Node | None = None
538    hold_body: int = 0
539    host_only: bool = False
540    premultiplied: bool = False
541    source_player: bascenev1.Player | None = None
542    mesh_opaque: bascenev1.Mesh | None = None
543    mesh_transparent: bascenev1.Mesh | None = None
544    damage_smoothed: float = 0.0
545    gravity_scale: float = 1.0
546    punch_power: float = 0.0
547    punch_momentum_linear: Sequence[float] = (0.0, 0.0, 0.0)
548    punch_momentum_angular: float = 0.0
549    rate: int = 0
550    vr_depth: float = 0.0
551    is_area_of_interest: bool = False
552    jump_pressed: bool = False
553    pickup_pressed: bool = False
554    punch_pressed: bool = False
555    bomb_pressed: bool = False
556    fly_pressed: bool = False
557    hold_position_pressed: bool = False
558    knockout: float = 0.0
559    invincible: bool = False
560    stick_to_owner: bool = False
561    damage: int = 0
562    run: float = 0.0
563    move_up_down: float = 0.0
564    move_left_right: float = 0.0
565    curse_death_time: int = 0
566    boxing_gloves: bool = False
567    hockey: bool = False
568    use_fixed_vr_overlay: bool = False
569    allow_kick_idle_players: bool = False
570    music_continuous: bool = False
571    music_count: int = 0
572    hurt: float = 0.0
573    always_show_health_bar: bool = False
574    mini_billboard_1_texture: bascenev1.Texture | None = None
575    mini_billboard_1_start_time: int = 0
576    mini_billboard_1_end_time: int = 0
577    mini_billboard_2_texture: bascenev1.Texture | None = None
578    mini_billboard_2_start_time: int = 0
579    mini_billboard_2_end_time: int = 0
580    mini_billboard_3_texture: bascenev1.Texture | None = None
581    mini_billboard_3_start_time: int = 0
582    mini_billboard_3_end_time: int = 0
583    boxing_gloves_flashing: bool = False
584    dead: bool = False
585    floor_reflection: bool = False
586    debris_friction: float = 0.0
587    debris_kill_height: float = 0.0
588    vr_near_clip: float = 0.0
589    shadow_ortho: bool = False
590    happy_thoughts_mode: bool = False
591    shadow_offset: Sequence[float] = (0.0, 0.0)
592    paused: bool = False
593    time: int = 0
594    ambient_color: Sequence[float] = (1.0, 1.0, 1.0)
595    camera_mode: str = 'rotate'
596    frozen: bool = False
597    area_of_interest_bounds: Sequence[float] = (-1, -1, -1, 1, 1, 1)
598    shadow_range: Sequence[float] = (0, 0, 0, 0)
599    counter_text: str = ''
600    counter_texture: bascenev1.Texture | None = None
601    shattered: int = 0
602    billboard_texture: bascenev1.Texture | None = None
603    billboard_cross_out: bool = False
604    billboard_opacity: float = 0.0
605    slow_motion: bool = False
606    music: str = ''
607    vr_camera_offset: Sequence[float] = (0.0, 0.0, 0.0)
608    vr_overlay_center: Sequence[float] = (0.0, 0.0, 0.0)
609    vr_overlay_center_enabled: bool = False
610    vignette_outer: Sequence[float] = (0.0, 0.0)
611    vignette_inner: Sequence[float] = (0.0, 0.0)
612    tint: Sequence[float] = (1.0, 1.0, 1.0)
613
614    def __bool__(self) -> bool:
615        """Support for bool evaluation."""
616        return bool(True)  # Slight obfuscation.
617
618    def add_death_action(self, action: Callable[[], None]) -> None:
619        """Add a callable object to be called upon this node's death.
620        Note that these actions are run just after the node dies, not before.
621        """
622        return None
623
624    def connectattr(self, srcattr: str, dstnode: Node, dstattr: str) -> None:
625        """Connect one of this node's attributes to an attribute on another
626        node. This will immediately set the target attribute's value to that
627        of the source attribute, and will continue to do so once per step
628        as long as the two nodes exist. The connection can be severed by
629        setting the target attribute to any value or connecting another
630        node attribute to it.
631
632        ##### Example
633        Create a locator and attach a light to it:
634        >>> light = bascenev1.newnode('light')
635        ... loc = bascenev1.newnode('locator', attrs={'position': (0, 10, 0)})
636        ... loc.connectattr('position', light, 'position')
637        """
638        return None
639
640    def delete(self, ignore_missing: bool = True) -> None:
641        """Delete the node. Ignores already-deleted nodes if `ignore_missing`
642        is True; otherwise a bascenev1.NodeNotFoundError is thrown.
643        """
644        return None
645
646    def exists(self) -> bool:
647        """Returns whether the Node still exists.
648        Most functionality will fail on a nonexistent Node, so it's never a bad
649        idea to check this.
650
651        Note that you can also use the boolean operator for this same
652        functionality, so a statement such as "if mynode" will do
653        the right thing both for Node objects and values of None.
654        """
655        return bool()
656
657    # Show that ur return type varies based on "doraise" value:
658    @overload
659    def getdelegate(
660        self, type: type[_T], doraise: Literal[False] = False
661    ) -> _T | None: ...
662
663    @overload
664    def getdelegate(self, type: type[_T], doraise: Literal[True]) -> _T: ...
665
666    def getdelegate(self, type: Any, doraise: bool = False) -> Any:
667        """Return the node's current delegate object if it matches
668        a certain type.
669
670        If the node has no delegate or it is not an instance of the passed
671        type, then None will be returned. If 'doraise' is True, then an
672        bascenev1.DelegateNotFoundError will be raised instead.
673        """
674        return None
675
676    def getname(self) -> str:
677        """Return the name assigned to a Node; used mainly for debugging"""
678        return str()
679
680    def getnodetype(self) -> str:
681        """Return the type of Node referenced by this object as a string.
682        (Note this is different from the Python type which is always
683         bascenev1.Node)
684        """
685        return str()
686
687    def handlemessage(self, *args: Any) -> None:
688        """General message handling; can be passed any message object.
689
690        All standard message objects are forwarded along to the
691        bascenev1.Node's delegate for handling (generally the bascenev1.Actor
692        that made the node).
693
694        bascenev1.Node-s are unique, however, in that they can be passed a
695        second form of message; 'node-messages'.  These consist of a string
696        type-name as a first argument along with the args specific to that type
697        name as additional arguments.
698        Node-messages communicate directly with the low-level node layer
699        and are delivered simultaneously on all game clients,
700        acting as an alternative to setting node attributes.
701        """
702        return None

Reference to a Node; the low level building block of a game.

At its core, a game is nothing more than a scene of Nodes with attributes getting interconnected or set over time.

A bascenev1.Node instance should be thought of as a weak-reference to a game node; not the node itself. This means a Node's lifecycle is completely independent of how many Python references to it exist. To explicitly add a new node to the game, use bascenev1.newnode(), and to explicitly delete one, use bascenev1.Node.delete(). bascenev1.Node.exists() can be used to determine if a Node still points to a live node in the game.

You can use ba.Node(None) to instantiate an invalid Node reference (sometimes used as attr values/etc).

color: Sequence[float] = (0.0, 0.0, 0.0)
size: Sequence[float] = (0.0, 0.0, 0.0)
position: Sequence[float] = (0.0, 0.0, 0.0)
position_center: Sequence[float] = (0.0, 0.0, 0.0)
position_forward: Sequence[float] = (0.0, 0.0, 0.0)
punch_position: Sequence[float] = (0.0, 0.0, 0.0)
punch_velocity: Sequence[float] = (0.0, 0.0, 0.0)
velocity: Sequence[float] = (0.0, 0.0, 0.0)
name_color: Sequence[float] = (0.0, 0.0, 0.0)
tint_color: Sequence[float] = (0.0, 0.0, 0.0)
tint2_color: Sequence[float] = (0.0, 0.0, 0.0)
text: Lstr | str = ''
texture: _bascenev1.Texture | None = None
tint_texture: _bascenev1.Texture | None = None
times: Sequence[int] = (1, 2, 3, 4, 5)
values: Sequence[float] = (1.0, 2.0, 3.0, 4.0)
offset: float = 0.0
input0: float = 0.0
input1: float = 0.0
input2: float = 0.0
input3: float = 0.0
flashing: bool = False
scale: Union[float, Sequence[float]] = 0.0
opacity: float = 0.0
loop: bool = False
time1: int = 0
time2: int = 0
timemax: int = 0
client_only: bool = False
materials: Sequence[_bascenev1.Material] = ()
roller_materials: Sequence[_bascenev1.Material] = ()
name: str = ''
punch_materials: Sequence[_bascenev1.Material] = ()
pickup_materials: Sequence[_bascenev1.Material] = ()
extras_material: Sequence[_bascenev1.Material] = ()
rotate: float = 0.0
hold_node: _bascenev1.Node | None = None
hold_body: int = 0
host_only: bool = False
premultiplied: bool = False
source_player: Player | None = None
mesh_opaque: _bascenev1.Mesh | None = None
mesh_transparent: _bascenev1.Mesh | None = None
damage_smoothed: float = 0.0
gravity_scale: float = 1.0
punch_power: float = 0.0
punch_momentum_linear: Sequence[float] = (0.0, 0.0, 0.0)
punch_momentum_angular: float = 0.0
rate: int = 0
vr_depth: float = 0.0
is_area_of_interest: bool = False
jump_pressed: bool = False
pickup_pressed: bool = False
punch_pressed: bool = False
bomb_pressed: bool = False
fly_pressed: bool = False
hold_position_pressed: bool = False
knockout: float = 0.0
invincible: bool = False
stick_to_owner: bool = False
damage: int = 0
run: float = 0.0
move_up_down: float = 0.0
move_left_right: float = 0.0
curse_death_time: int = 0
boxing_gloves: bool = False
hockey: bool = False
use_fixed_vr_overlay: bool = False
allow_kick_idle_players: bool = False
music_continuous: bool = False
music_count: int = 0
hurt: float = 0.0
always_show_health_bar: bool = False
mini_billboard_1_texture: _bascenev1.Texture | None = None
mini_billboard_1_start_time: int = 0
mini_billboard_1_end_time: int = 0
mini_billboard_2_texture: _bascenev1.Texture | None = None
mini_billboard_2_start_time: int = 0
mini_billboard_2_end_time: int = 0
mini_billboard_3_texture: _bascenev1.Texture | None = None
mini_billboard_3_start_time: int = 0
mini_billboard_3_end_time: int = 0
boxing_gloves_flashing: bool = False
dead: bool = False
floor_reflection: bool = False
debris_friction: float = 0.0
debris_kill_height: float = 0.0
vr_near_clip: float = 0.0
shadow_ortho: bool = False
happy_thoughts_mode: bool = False
shadow_offset: Sequence[float] = (0.0, 0.0)
paused: bool = False
time: int = 0
ambient_color: Sequence[float] = (1.0, 1.0, 1.0)
camera_mode: str = 'rotate'
frozen: bool = False
area_of_interest_bounds: Sequence[float] = (-1, -1, -1, 1, 1, 1)
shadow_range: Sequence[float] = (0, 0, 0, 0)
counter_text: str = ''
counter_texture: _bascenev1.Texture | None = None
shattered: int = 0
billboard_texture: _bascenev1.Texture | None = None
billboard_cross_out: bool = False
billboard_opacity: float = 0.0
slow_motion: bool = False
music: str = ''
vr_camera_offset: Sequence[float] = (0.0, 0.0, 0.0)
vr_overlay_center: Sequence[float] = (0.0, 0.0, 0.0)
vr_overlay_center_enabled: bool = False
vignette_outer: Sequence[float] = (0.0, 0.0)
vignette_inner: Sequence[float] = (0.0, 0.0)
tint: Sequence[float] = (1.0, 1.0, 1.0)
def add_death_action(self, action: Callable[[], NoneType]) -> None:
618    def add_death_action(self, action: Callable[[], None]) -> None:
619        """Add a callable object to be called upon this node's death.
620        Note that these actions are run just after the node dies, not before.
621        """
622        return None

Add a callable object to be called upon this node's death. Note that these actions are run just after the node dies, not before.

def connectattr(self, srcattr: str, dstnode: _bascenev1.Node, dstattr: str) -> None:
624    def connectattr(self, srcattr: str, dstnode: Node, dstattr: str) -> None:
625        """Connect one of this node's attributes to an attribute on another
626        node. This will immediately set the target attribute's value to that
627        of the source attribute, and will continue to do so once per step
628        as long as the two nodes exist. The connection can be severed by
629        setting the target attribute to any value or connecting another
630        node attribute to it.
631
632        ##### Example
633        Create a locator and attach a light to it:
634        >>> light = bascenev1.newnode('light')
635        ... loc = bascenev1.newnode('locator', attrs={'position': (0, 10, 0)})
636        ... loc.connectattr('position', light, 'position')
637        """
638        return None

Connect one of this node's attributes to an attribute on another node. This will immediately set the target attribute's value to that of the source attribute, and will continue to do so once per step as long as the two nodes exist. The connection can be severed by setting the target attribute to any value or connecting another node attribute to it.

Example

Create a locator and attach a light to it:

>>> light = bascenev1.newnode('light')
... loc = bascenev1.newnode('locator', attrs={'position': (0, 10, 0)})
... loc.connectattr('position', light, 'position')
def delete(self, ignore_missing: bool = True) -> None:
640    def delete(self, ignore_missing: bool = True) -> None:
641        """Delete the node. Ignores already-deleted nodes if `ignore_missing`
642        is True; otherwise a bascenev1.NodeNotFoundError is thrown.
643        """
644        return None

Delete the node. Ignores already-deleted nodes if ignore_missing is True; otherwise a bascenev1.NodeNotFoundError is thrown.

def exists(self) -> bool:
646    def exists(self) -> bool:
647        """Returns whether the Node still exists.
648        Most functionality will fail on a nonexistent Node, so it's never a bad
649        idea to check this.
650
651        Note that you can also use the boolean operator for this same
652        functionality, so a statement such as "if mynode" will do
653        the right thing both for Node objects and values of None.
654        """
655        return bool()

Returns whether the Node still exists. Most functionality will fail on a nonexistent Node, so it's never a bad idea to check this.

Note that you can also use the boolean operator for this same functionality, so a statement such as "if mynode" will do the right thing both for Node objects and values of None.

def getdelegate(self, type: Any, doraise: bool = False) -> Any:
666    def getdelegate(self, type: Any, doraise: bool = False) -> Any:
667        """Return the node's current delegate object if it matches
668        a certain type.
669
670        If the node has no delegate or it is not an instance of the passed
671        type, then None will be returned. If 'doraise' is True, then an
672        bascenev1.DelegateNotFoundError will be raised instead.
673        """
674        return None

Return the node's current delegate object if it matches a certain type.

If the node has no delegate or it is not an instance of the passed type, then None will be returned. If 'doraise' is True, then an bascenev1.DelegateNotFoundError will be raised instead.

def getname(self) -> str:
676    def getname(self) -> str:
677        """Return the name assigned to a Node; used mainly for debugging"""
678        return str()

Return the name assigned to a Node; used mainly for debugging

def getnodetype(self) -> str:
680    def getnodetype(self) -> str:
681        """Return the type of Node referenced by this object as a string.
682        (Note this is different from the Python type which is always
683         bascenev1.Node)
684        """
685        return str()

Return the type of Node referenced by this object as a string. (Note this is different from the Python type which is always bascenev1.Node)

def handlemessage(self, *args: Any) -> None:
687    def handlemessage(self, *args: Any) -> None:
688        """General message handling; can be passed any message object.
689
690        All standard message objects are forwarded along to the
691        bascenev1.Node's delegate for handling (generally the bascenev1.Actor
692        that made the node).
693
694        bascenev1.Node-s are unique, however, in that they can be passed a
695        second form of message; 'node-messages'.  These consist of a string
696        type-name as a first argument along with the args specific to that type
697        name as additional arguments.
698        Node-messages communicate directly with the low-level node layer
699        and are delivered simultaneously on all game clients,
700        acting as an alternative to setting node attributes.
701        """
702        return None

General message handling; can be passed any message object.

All standard message objects are forwarded along to the bascenev1.Node's delegate for handling (generally the bascenev1.Actor that made the node).

bascenev1.Node-s are unique, however, in that they can be passed a second form of message; 'node-messages'. These consist of a string type-name as a first argument along with the args specific to that type name as additional arguments. Node-messages communicate directly with the low-level node layer and are delivered simultaneously on all game clients, acting as an alternative to setting node attributes.

class NodeActor(bascenev1.Actor):
19class NodeActor(Actor):
20    """A simple bascenev1.Actor type that wraps a single bascenev1.Node.
21
22    This Actor will delete its Node when told to die, and it's
23    exists() call will return whether the Node still exists or not.
24    """
25
26    def __init__(self, node: bascenev1.Node):
27        super().__init__()
28        self.node = node
29
30    @override
31    def handlemessage(self, msg: Any) -> Any:
32        if isinstance(msg, DieMessage):
33            if self.node:
34                self.node.delete()
35                return None
36        return super().handlemessage(msg)
37
38    @override
39    def exists(self) -> bool:
40        return bool(self.node)

A simple bascenev1.Actor type that wraps a single bascenev1.Node.

This Actor will delete its Node when told to die, and it's exists() call will return whether the Node still exists or not.

NodeActor(node: _bascenev1.Node)
26    def __init__(self, node: bascenev1.Node):
27        super().__init__()
28        self.node = node

Instantiates an Actor in the current bascenev1.Activity.

node
@override
def handlemessage(self, msg: Any) -> Any:
30    @override
31    def handlemessage(self, msg: Any) -> Any:
32        if isinstance(msg, DieMessage):
33            if self.node:
34                self.node.delete()
35                return None
36        return super().handlemessage(msg)

General message handling; can be passed any message object.

@override
def exists(self) -> bool:
38    @override
39    def exists(self) -> bool:
40        return bool(self.node)

Returns whether the Actor is still present in a meaningful way.

Note that a dying character should still return True here as long as their corpse is visible; this is about presence, not being 'alive' (see bascenev1.Actor.is_alive() for that).

If this returns False, it is assumed the Actor can be completely deleted without affecting the game; this call is often used when pruning lists of Actors, such as with bascenev1.Actor.autoretain()

The default implementation of this method always return True.

Note that the boolean operator for the Actor class calls this method, so a simple "if myactor" test will conveniently do the right thing even if myactor is set to None.

class NodeNotFoundError(bascenev1.NotFoundError):
53class NodeNotFoundError(NotFoundError):
54    """Exception raised when an expected Node does not exist."""

Exception raised when an expected Node does not exist.

def normalized_color(color: Sequence[float]) -> tuple[float, ...]:
52def normalized_color(color: Sequence[float]) -> tuple[float, ...]:
53    """Scale a color so its largest value is 1; useful for coloring lights.
54
55    category: General Utility Functions
56    """
57    color_biased = tuple(max(c, 0.01) for c in color)  # account for black
58    mult = 1.0 / max(color_biased)
59    return tuple(c * mult for c in color_biased)

Scale a color so its largest value is 1; useful for coloring lights.

category: General Utility Functions

class NotFoundError(builtins.Exception):
25class NotFoundError(Exception):
26    """Exception raised when a referenced object does not exist."""

Exception raised when a referenced object does not exist.

@dataclass
class OutOfBoundsMessage:
30@dataclass
31class OutOfBoundsMessage:
32    """A message telling an object that it is out of bounds."""

A message telling an object that it is out of bounds.

@dataclass
class PickedUpMessage:
145@dataclass
146class PickedUpMessage:
147    """Tells an object that it has been picked up by something."""
148
149    node: bascenev1.Node
150    """The bascenev1.Node doing the picking up."""

Tells an object that it has been picked up by something.

PickedUpMessage(node: _bascenev1.Node)
node: _bascenev1.Node

The bascenev1.Node doing the picking up.

@dataclass
class PickUpMessage:
132@dataclass
133class PickUpMessage:
134    """Tells an object that it has picked something up."""
135
136    node: bascenev1.Node
137    """The bascenev1.Node that is getting picked up."""

Tells an object that it has picked something up.

PickUpMessage(node: _bascenev1.Node)
node: _bascenev1.Node

The bascenev1.Node that is getting picked up.

class Player(typing.Generic[~TeamT]):
 42class Player(Generic[TeamT]):
 43    """A player in a specific bascenev1.Activity.
 44
 45    These correspond to bascenev1.SessionPlayer objects, but are associated
 46    with a single bascenev1.Activity instance. This allows activities to
 47    specify their own custom bascenev1.Player types.
 48    """
 49
 50    # These are instance attrs but we define them at the type level so
 51    # their type annotations are introspectable (for docs generation).
 52    character: str
 53
 54    actor: bascenev1.Actor | None
 55    """The bascenev1.Actor associated with the player."""
 56
 57    color: Sequence[float]
 58    highlight: Sequence[float]
 59
 60    _team: TeamT
 61    _sessionplayer: bascenev1.SessionPlayer
 62    _nodeactor: bascenev1.NodeActor | None
 63    _expired: bool
 64    _postinited: bool
 65    _customdata: dict
 66
 67    # NOTE: avoiding having any __init__() here since it seems to not
 68    # get called by default if a dataclass inherits from us.
 69    # This also lets us keep trivial player classes cleaner by skipping
 70    # the super().__init__() line.
 71
 72    def postinit(self, sessionplayer: bascenev1.SessionPlayer) -> None:
 73        """Wire up a newly created player.
 74
 75        (internal)
 76        """
 77        from bascenev1._nodeactor import NodeActor
 78
 79        # Sanity check; if a dataclass is created that inherits from us,
 80        # it will define an equality operator by default which will break
 81        # internal game logic. So complain loudly if we find one.
 82        if type(self).__eq__ is not object.__eq__:
 83            raise RuntimeError(
 84                f'Player class {type(self)} defines an equality'
 85                f' operator (__eq__) which will break internal'
 86                f' logic. Please remove it.\n'
 87                f'For dataclasses you can do "dataclass(eq=False)"'
 88                f' in the class decorator.'
 89            )
 90
 91        self.actor = None
 92        self.character = ''
 93        self._nodeactor: bascenev1.NodeActor | None = None
 94        self._sessionplayer = sessionplayer
 95        self.character = sessionplayer.character
 96        self.color = sessionplayer.color
 97        self.highlight = sessionplayer.highlight
 98        self._team = cast(TeamT, sessionplayer.sessionteam.activityteam)
 99        assert self._team is not None
100        self._customdata = {}
101        self._expired = False
102        self._postinited = True
103        node = _bascenev1.newnode(
104            'player', attrs={'playerID': sessionplayer.id}
105        )
106        self._nodeactor = NodeActor(node)
107        sessionplayer.setnode(node)
108
109    def leave(self) -> None:
110        """Called when the Player leaves a running game.
111
112        (internal)
113        """
114        assert self._postinited
115        assert not self._expired
116        try:
117            # If they still have an actor, kill it.
118            if self.actor:
119                self.actor.handlemessage(DieMessage(how=DeathType.LEFT_GAME))
120            self.actor = None
121        except Exception:
122            logging.exception('Error killing actor on leave for %s.', self)
123        self._nodeactor = None
124        del self._team
125        del self._customdata
126
127    def expire(self) -> None:
128        """Called when the Player is expiring (when its Activity does so).
129
130        (internal)
131        """
132        assert self._postinited
133        assert not self._expired
134        self._expired = True
135
136        try:
137            self.on_expire()
138        except Exception:
139            logging.exception('Error in on_expire for %s.', self)
140
141        self._nodeactor = None
142        self.actor = None
143        del self._team
144        del self._customdata
145
146    def on_expire(self) -> None:
147        """Can be overridden to handle player expiration.
148
149        The player expires when the Activity it is a part of expires.
150        Expired players should no longer run any game logic (which will
151        likely error). They should, however, remove any references to
152        players/teams/games/etc. which could prevent them from being freed.
153        """
154
155    @property
156    def team(self) -> TeamT:
157        """The bascenev1.Team for this player."""
158        assert self._postinited
159        assert not self._expired
160        return self._team
161
162    @property
163    def customdata(self) -> dict:
164        """Arbitrary values associated with the player.
165        Though it is encouraged that most player values be properly defined
166        on the bascenev1.Player subclass, it may be useful for player-agnostic
167        objects to store values here. This dict is cleared when the player
168        leaves or expires so objects stored here will be disposed of at
169        the expected time, unlike the Player instance itself which may
170        continue to be referenced after it is no longer part of the game.
171        """
172        assert self._postinited
173        assert not self._expired
174        return self._customdata
175
176    @property
177    def sessionplayer(self) -> bascenev1.SessionPlayer:
178        """Return the bascenev1.SessionPlayer corresponding to this Player.
179
180        Throws a bascenev1.SessionPlayerNotFoundError if it does not exist.
181        """
182        assert self._postinited
183        if bool(self._sessionplayer):
184            return self._sessionplayer
185        raise babase.SessionPlayerNotFoundError()
186
187    @property
188    def node(self) -> bascenev1.Node:
189        """A bascenev1.Node of type 'player' associated with this Player.
190
191        This node can be used to get a generic player position/etc.
192        """
193        assert self._postinited
194        assert not self._expired
195        assert self._nodeactor
196        return self._nodeactor.node
197
198    @property
199    def position(self) -> babase.Vec3:
200        """The position of the player, as defined by its bascenev1.Actor.
201
202        If the player currently has no actor, raises a
203        babase.ActorNotFoundError.
204        """
205        assert self._postinited
206        assert not self._expired
207        if self.actor is None:
208            raise babase.ActorNotFoundError
209        return babase.Vec3(self.node.position)
210
211    def exists(self) -> bool:
212        """Whether the underlying player still exists.
213
214        This will return False if the underlying bascenev1.SessionPlayer has
215        left the game or if the bascenev1.Activity this player was
216        associated with has ended.
217        Most functionality will fail on a nonexistent player.
218        Note that you can also use the boolean operator for this same
219        functionality, so a statement such as "if player" will do
220        the right thing both for Player objects and values of None.
221        """
222        assert self._postinited
223        return self._sessionplayer.exists() and not self._expired
224
225    def getname(self, full: bool = False, icon: bool = True) -> str:
226        """
227        Returns the player's name. If icon is True, the long version of the
228        name may include an icon.
229        """
230        assert self._postinited
231        assert not self._expired
232        return self._sessionplayer.getname(full=full, icon=icon)
233
234    def is_alive(self) -> bool:
235        """
236        Returns True if the player has a bascenev1.Actor assigned and its
237        is_alive() method return True. False is returned otherwise.
238        """
239        assert self._postinited
240        assert not self._expired
241        return self.actor is not None and self.actor.is_alive()
242
243    def get_icon(self) -> dict[str, Any]:
244        """
245        Returns the character's icon (images, colors, etc contained in a dict)
246        """
247        assert self._postinited
248        assert not self._expired
249        return self._sessionplayer.get_icon()
250
251    def assigninput(
252        self,
253        inputtype: babase.InputType | tuple[babase.InputType, ...],
254        call: Callable,
255    ) -> None:
256        """
257        Set the python callable to be run for one or more types of input.
258        """
259        assert self._postinited
260        assert not self._expired
261        return self._sessionplayer.assigninput(type=inputtype, call=call)
262
263    def resetinput(self) -> None:
264        """
265        Clears out the player's assigned input actions.
266        """
267        assert self._postinited
268        assert not self._expired
269        self._sessionplayer.resetinput()
270
271    def __bool__(self) -> bool:
272        return self.exists()

A player in a specific bascenev1.Activity.

These correspond to bascenev1.SessionPlayer objects, but are associated with a single bascenev1.Activity instance. This allows activities to specify their own custom bascenev1.Player types.

character: str
actor: Actor | None

The bascenev1.Actor associated with the player.

color: Sequence[float]
highlight: Sequence[float]
def on_expire(self) -> None:
146    def on_expire(self) -> None:
147        """Can be overridden to handle player expiration.
148
149        The player expires when the Activity it is a part of expires.
150        Expired players should no longer run any game logic (which will
151        likely error). They should, however, remove any references to
152        players/teams/games/etc. which could prevent them from being freed.
153        """

Can be overridden to handle player expiration.

The player expires when the Activity it is a part of expires. Expired players should no longer run any game logic (which will likely error). They should, however, remove any references to players/teams/games/etc. which could prevent them from being freed.

team: ~TeamT
155    @property
156    def team(self) -> TeamT:
157        """The bascenev1.Team for this player."""
158        assert self._postinited
159        assert not self._expired
160        return self._team

The bascenev1.Team for this player.

customdata: dict
162    @property
163    def customdata(self) -> dict:
164        """Arbitrary values associated with the player.
165        Though it is encouraged that most player values be properly defined
166        on the bascenev1.Player subclass, it may be useful for player-agnostic
167        objects to store values here. This dict is cleared when the player
168        leaves or expires so objects stored here will be disposed of at
169        the expected time, unlike the Player instance itself which may
170        continue to be referenced after it is no longer part of the game.
171        """
172        assert self._postinited
173        assert not self._expired
174        return self._customdata

Arbitrary values associated with the player. Though it is encouraged that most player values be properly defined on the bascenev1.Player subclass, it may be useful for player-agnostic objects to store values here. This dict is cleared when the player leaves or expires so objects stored here will be disposed of at the expected time, unlike the Player instance itself which may continue to be referenced after it is no longer part of the game.

sessionplayer: _bascenev1.SessionPlayer
176    @property
177    def sessionplayer(self) -> bascenev1.SessionPlayer:
178        """Return the bascenev1.SessionPlayer corresponding to this Player.
179
180        Throws a bascenev1.SessionPlayerNotFoundError if it does not exist.
181        """
182        assert self._postinited
183        if bool(self._sessionplayer):
184            return self._sessionplayer
185        raise babase.SessionPlayerNotFoundError()

Return the bascenev1.SessionPlayer corresponding to this Player.

Throws a bascenev1.SessionPlayerNotFoundError if it does not exist.

node: _bascenev1.Node
187    @property
188    def node(self) -> bascenev1.Node:
189        """A bascenev1.Node of type 'player' associated with this Player.
190
191        This node can be used to get a generic player position/etc.
192        """
193        assert self._postinited
194        assert not self._expired
195        assert self._nodeactor
196        return self._nodeactor.node

A bascenev1.Node of type 'player' associated with this Player.

This node can be used to get a generic player position/etc.

position: _babase.Vec3
198    @property
199    def position(self) -> babase.Vec3:
200        """The position of the player, as defined by its bascenev1.Actor.
201
202        If the player currently has no actor, raises a
203        babase.ActorNotFoundError.
204        """
205        assert self._postinited
206        assert not self._expired
207        if self.actor is None:
208            raise babase.ActorNotFoundError
209        return babase.Vec3(self.node.position)

The position of the player, as defined by its bascenev1.Actor.

If the player currently has no actor, raises a babase.ActorNotFoundError.

def exists(self) -> bool:
211    def exists(self) -> bool:
212        """Whether the underlying player still exists.
213
214        This will return False if the underlying bascenev1.SessionPlayer has
215        left the game or if the bascenev1.Activity this player was
216        associated with has ended.
217        Most functionality will fail on a nonexistent player.
218        Note that you can also use the boolean operator for this same
219        functionality, so a statement such as "if player" will do
220        the right thing both for Player objects and values of None.
221        """
222        assert self._postinited
223        return self._sessionplayer.exists() and not self._expired

Whether the underlying player still exists.

This will return False if the underlying bascenev1.SessionPlayer has left the game or if the bascenev1.Activity this player was associated with has ended. Most functionality will fail on a nonexistent player. Note that you can also use the boolean operator for this same functionality, so a statement such as "if player" will do the right thing both for Player objects and values of None.

def getname(self, full: bool = False, icon: bool = True) -> str:
225    def getname(self, full: bool = False, icon: bool = True) -> str:
226        """
227        Returns the player's name. If icon is True, the long version of the
228        name may include an icon.
229        """
230        assert self._postinited
231        assert not self._expired
232        return self._sessionplayer.getname(full=full, icon=icon)

Returns the player's name. If icon is True, the long version of the name may include an icon.

def is_alive(self) -> bool:
234    def is_alive(self) -> bool:
235        """
236        Returns True if the player has a bascenev1.Actor assigned and its
237        is_alive() method return True. False is returned otherwise.
238        """
239        assert self._postinited
240        assert not self._expired
241        return self.actor is not None and self.actor.is_alive()

Returns True if the player has a bascenev1.Actor assigned and its is_alive() method return True. False is returned otherwise.

def get_icon(self) -> dict[str, typing.Any]:
243    def get_icon(self) -> dict[str, Any]:
244        """
245        Returns the character's icon (images, colors, etc contained in a dict)
246        """
247        assert self._postinited
248        assert not self._expired
249        return self._sessionplayer.get_icon()

Returns the character's icon (images, colors, etc contained in a dict)

def assigninput( self, inputtype: InputType | tuple[InputType, ...], call: Callable) -> None:
251    def assigninput(
252        self,
253        inputtype: babase.InputType | tuple[babase.InputType, ...],
254        call: Callable,
255    ) -> None:
256        """
257        Set the python callable to be run for one or more types of input.
258        """
259        assert self._postinited
260        assert not self._expired
261        return self._sessionplayer.assigninput(type=inputtype, call=call)

Set the python callable to be run for one or more types of input.

def resetinput(self) -> None:
263    def resetinput(self) -> None:
264        """
265        Clears out the player's assigned input actions.
266        """
267        assert self._postinited
268        assert not self._expired
269        self._sessionplayer.resetinput()

Clears out the player's assigned input actions.

class PlayerDiedMessage:
 66class PlayerDiedMessage:
 67    """A message saying a bascenev1.Player has died."""
 68
 69    killed: bool
 70    """If True, the player was killed;
 71       If False, they left the game or the round ended."""
 72
 73    how: DeathType
 74    """The particular type of death."""
 75
 76    def __init__(
 77        self,
 78        player: bascenev1.Player,
 79        was_killed: bool,
 80        killerplayer: bascenev1.Player | None,
 81        how: DeathType,
 82    ):
 83        """Instantiate a message with the given values."""
 84
 85        # Invalid refs should never be passed as args.
 86        assert player.exists()
 87        self._player = player
 88
 89        # Invalid refs should never be passed as args.
 90        assert killerplayer is None or killerplayer.exists()
 91        self._killerplayer = killerplayer
 92        self.killed = was_killed
 93        self.how = how
 94
 95    def getkillerplayer(self, playertype: type[PlayerT]) -> PlayerT | None:
 96        """Return the bascenev1.Player responsible for the killing, if any.
 97
 98        Pass the Player type being used by the current game.
 99        """
100        assert isinstance(self._killerplayer, (playertype, type(None)))
101        return self._killerplayer
102
103    def getplayer(self, playertype: type[PlayerT]) -> PlayerT:
104        """Return the bascenev1.Player that died.
105
106        The type of player for the current activity should be passed so that
107        the type-checker properly identifies the returned value as one.
108        """
109        player: Any = self._player
110        assert isinstance(player, playertype)
111
112        # We should never be delivering invalid refs.
113        # (could theoretically happen if someone holds on to us)
114        assert player.exists()
115        return player

A message saying a bascenev1.Player has died.

PlayerDiedMessage( player: Player, was_killed: bool, killerplayer: Player | None, how: DeathType)
76    def __init__(
77        self,
78        player: bascenev1.Player,
79        was_killed: bool,
80        killerplayer: bascenev1.Player | None,
81        how: DeathType,
82    ):
83        """Instantiate a message with the given values."""
84
85        # Invalid refs should never be passed as args.
86        assert player.exists()
87        self._player = player
88
89        # Invalid refs should never be passed as args.
90        assert killerplayer is None or killerplayer.exists()
91        self._killerplayer = killerplayer
92        self.killed = was_killed
93        self.how = how

Instantiate a message with the given values.

killed: bool

If True, the player was killed; If False, they left the game or the round ended.

how: DeathType

The particular type of death.

def getkillerplayer(self, playertype: type[~PlayerT]) -> Optional[~PlayerT]:
 95    def getkillerplayer(self, playertype: type[PlayerT]) -> PlayerT | None:
 96        """Return the bascenev1.Player responsible for the killing, if any.
 97
 98        Pass the Player type being used by the current game.
 99        """
100        assert isinstance(self._killerplayer, (playertype, type(None)))
101        return self._killerplayer

Return the bascenev1.Player responsible for the killing, if any.

Pass the Player type being used by the current game.

def getplayer(self, playertype: type[~PlayerT]) -> ~PlayerT:
103    def getplayer(self, playertype: type[PlayerT]) -> PlayerT:
104        """Return the bascenev1.Player that died.
105
106        The type of player for the current activity should be passed so that
107        the type-checker properly identifies the returned value as one.
108        """
109        player: Any = self._player
110        assert isinstance(player, playertype)
111
112        # We should never be delivering invalid refs.
113        # (could theoretically happen if someone holds on to us)
114        assert player.exists()
115        return player

Return the bascenev1.Player that died.

The type of player for the current activity should be passed so that the type-checker properly identifies the returned value as one.

@dataclass
class PlayerProfilesChangedMessage:
254@dataclass
255class PlayerProfilesChangedMessage:
256    """Signals player profiles may have changed and should be reloaded."""

Signals player profiles may have changed and should be reloaded.

@dataclass
class PlayerInfo:
26@dataclass
27class PlayerInfo:
28    """Holds basic info about a player."""
29
30    name: str
31    character: str

Holds basic info about a player.

PlayerInfo(name: str, character: str)
name: str
character: str
class PlayerNotFoundError(bascenev1.NotFoundError):
29class PlayerNotFoundError(NotFoundError):
30    """Exception raised when an expected player does not exist."""

Exception raised when an expected player does not exist.

class PlayerRecord:
 32class PlayerRecord:
 33    """Stats for an individual player in a bascenev1.Stats object.
 34
 35    This does not necessarily correspond to a bascenev1.Player that is
 36    still present (stats may be retained for players that leave
 37    mid-game)
 38    """
 39
 40    character: str
 41
 42    def __init__(
 43        self,
 44        name: str,
 45        name_full: str,
 46        sessionplayer: bascenev1.SessionPlayer,
 47        stats: bascenev1.Stats,
 48    ):
 49        self.name = name
 50        self.name_full = name_full
 51        self.score = 0
 52        self.accumscore = 0
 53        self.kill_count = 0
 54        self.accum_kill_count = 0
 55        self.killed_count = 0
 56        self.accum_killed_count = 0
 57        self._multi_kill_timer: bascenev1.Timer | None = None
 58        self._multi_kill_count = 0
 59        self._stats = weakref.ref(stats)
 60        self._last_sessionplayer: bascenev1.SessionPlayer | None = None
 61        self._sessionplayer: bascenev1.SessionPlayer | None = None
 62        self._sessionteam: weakref.ref[bascenev1.SessionTeam] | None = None
 63        self.streak = 0
 64        self.associate_with_sessionplayer(sessionplayer)
 65
 66    @property
 67    def team(self) -> bascenev1.SessionTeam:
 68        """The bascenev1.SessionTeam the last associated player was last on.
 69
 70        This can still return a valid result even if the player is gone.
 71        Raises a bascenev1.SessionTeamNotFoundError if the team no longer
 72        exists.
 73        """
 74        assert self._sessionteam is not None
 75        team = self._sessionteam()
 76        if team is None:
 77            raise babase.SessionTeamNotFoundError()
 78        return team
 79
 80    @property
 81    def player(self) -> bascenev1.SessionPlayer:
 82        """Return the instance's associated bascenev1.SessionPlayer.
 83
 84        Raises a bascenev1.SessionPlayerNotFoundError if the player
 85        no longer exists.
 86        """
 87        if not self._sessionplayer:
 88            raise babase.SessionPlayerNotFoundError()
 89        return self._sessionplayer
 90
 91    def getname(self, full: bool = False) -> str:
 92        """Return the player entry's name."""
 93        return self.name_full if full else self.name
 94
 95    def get_icon(self) -> dict[str, Any]:
 96        """Get the icon for this instance's player."""
 97        player = self._last_sessionplayer
 98        assert player is not None
 99        return player.get_icon()
100
101    def cancel_multi_kill_timer(self) -> None:
102        """Cancel any multi-kill timer for this player entry."""
103        self._multi_kill_timer = None
104
105    def getactivity(self) -> bascenev1.Activity | None:
106        """Return the bascenev1.Activity this instance is associated with.
107
108        Returns None if the activity no longer exists."""
109        stats = self._stats()
110        if stats is not None:
111            return stats.getactivity()
112        return None
113
114    def associate_with_sessionplayer(
115        self, sessionplayer: bascenev1.SessionPlayer
116    ) -> None:
117        """Associate this entry with a bascenev1.SessionPlayer."""
118        self._sessionteam = weakref.ref(sessionplayer.sessionteam)
119        self.character = sessionplayer.character
120        self._last_sessionplayer = sessionplayer
121        self._sessionplayer = sessionplayer
122        self.streak = 0
123
124    def _end_multi_kill(self) -> None:
125        self._multi_kill_timer = None
126        self._multi_kill_count = 0
127
128    def get_last_sessionplayer(self) -> bascenev1.SessionPlayer:
129        """Return the last bascenev1.Player we were associated with."""
130        assert self._last_sessionplayer is not None
131        return self._last_sessionplayer
132
133    def submit_kill(self, showpoints: bool = True) -> None:
134        """Submit a kill for this player entry."""
135        # FIXME Clean this up.
136        # pylint: disable=too-many-statements
137
138        self._multi_kill_count += 1
139        stats = self._stats()
140        assert stats
141        if self._multi_kill_count == 1:
142            score = 0
143            name = None
144            delay = 0.0
145            color = (0.0, 0.0, 0.0, 1.0)
146            scale = 1.0
147            sound = None
148        elif self._multi_kill_count == 2:
149            score = 20
150            name = babase.Lstr(resource='twoKillText')
151            color = (0.1, 1.0, 0.0, 1)
152            scale = 1.0
153            delay = 0.0
154            sound = stats.orchestrahitsound1
155        elif self._multi_kill_count == 3:
156            score = 40
157            name = babase.Lstr(resource='threeKillText')
158            color = (1.0, 0.7, 0.0, 1)
159            scale = 1.1
160            delay = 0.3
161            sound = stats.orchestrahitsound2
162        elif self._multi_kill_count == 4:
163            score = 60
164            name = babase.Lstr(resource='fourKillText')
165            color = (1.0, 1.0, 0.0, 1)
166            scale = 1.2
167            delay = 0.6
168            sound = stats.orchestrahitsound3
169        elif self._multi_kill_count == 5:
170            score = 80
171            name = babase.Lstr(resource='fiveKillText')
172            color = (1.0, 0.5, 0.0, 1)
173            scale = 1.3
174            delay = 0.9
175            sound = stats.orchestrahitsound4
176        else:
177            score = 100
178            name = babase.Lstr(
179                resource='multiKillText',
180                subs=[('${COUNT}', str(self._multi_kill_count))],
181            )
182            color = (1.0, 0.5, 0.0, 1)
183            scale = 1.3
184            delay = 1.0
185            sound = stats.orchestrahitsound4
186
187        def _apply(
188            name2: babase.Lstr,
189            score2: int,
190            showpoints2: bool,
191            color2: tuple[float, float, float, float],
192            scale2: float,
193            sound2: bascenev1.Sound | None,
194        ) -> None:
195            # pylint: disable=too-many-positional-arguments
196            from bascenev1lib.actor.popuptext import PopupText
197
198            # Only award this if they're still alive and we can get
199            # a current position for them.
200            our_pos: babase.Vec3 | None = None
201            if self._sessionplayer:
202                if self._sessionplayer.activityplayer is not None:
203                    try:
204                        our_pos = self._sessionplayer.activityplayer.position
205                    except babase.NotFoundError:
206                        pass
207            if our_pos is None:
208                return
209
210            # Jitter position a bit since these often come in clusters.
211            our_pos = babase.Vec3(
212                our_pos[0] + (random.random() - 0.5) * 2.0,
213                our_pos[1] + (random.random() - 0.5) * 2.0,
214                our_pos[2] + (random.random() - 0.5) * 2.0,
215            )
216            activity = self.getactivity()
217            if activity is not None:
218                PopupText(
219                    babase.Lstr(
220                        value=(('+' + str(score2) + ' ') if showpoints2 else '')
221                        + '${N}',
222                        subs=[('${N}', name2)],
223                    ),
224                    color=color2,
225                    scale=scale2,
226                    position=our_pos,
227                ).autoretain()
228            if sound2:
229                sound2.play()
230
231            self.score += score2
232            self.accumscore += score2
233
234            # Inform a running game of the score.
235            if score2 != 0 and activity is not None:
236                activity.handlemessage(PlayerScoredMessage(score=score2))
237
238        if name is not None:
239            _bascenev1.timer(
240                0.3 + delay,
241                babase.Call(
242                    _apply, name, score, showpoints, color, scale, sound
243                ),
244            )
245
246        # Keep the tally rollin'...
247        # set a timer for a bit in the future.
248        self._multi_kill_timer = _bascenev1.Timer(1.0, self._end_multi_kill)

Stats for an individual player in a bascenev1.Stats object.

This does not necessarily correspond to a bascenev1.Player that is still present (stats may be retained for players that leave mid-game)

PlayerRecord( name: str, name_full: str, sessionplayer: _bascenev1.SessionPlayer, stats: Stats)
42    def __init__(
43        self,
44        name: str,
45        name_full: str,
46        sessionplayer: bascenev1.SessionPlayer,
47        stats: bascenev1.Stats,
48    ):
49        self.name = name
50        self.name_full = name_full
51        self.score = 0
52        self.accumscore = 0
53        self.kill_count = 0
54        self.accum_kill_count = 0
55        self.killed_count = 0
56        self.accum_killed_count = 0
57        self._multi_kill_timer: bascenev1.Timer | None = None
58        self._multi_kill_count = 0
59        self._stats = weakref.ref(stats)
60        self._last_sessionplayer: bascenev1.SessionPlayer | None = None
61        self._sessionplayer: bascenev1.SessionPlayer | None = None
62        self._sessionteam: weakref.ref[bascenev1.SessionTeam] | None = None
63        self.streak = 0
64        self.associate_with_sessionplayer(sessionplayer)
character: str
name
name_full
score
accumscore
kill_count
accum_kill_count
killed_count
accum_killed_count
streak
team: SessionTeam
66    @property
67    def team(self) -> bascenev1.SessionTeam:
68        """The bascenev1.SessionTeam the last associated player was last on.
69
70        This can still return a valid result even if the player is gone.
71        Raises a bascenev1.SessionTeamNotFoundError if the team no longer
72        exists.
73        """
74        assert self._sessionteam is not None
75        team = self._sessionteam()
76        if team is None:
77            raise babase.SessionTeamNotFoundError()
78        return team

The bascenev1.SessionTeam the last associated player was last on.

This can still return a valid result even if the player is gone. Raises a bascenev1.SessionTeamNotFoundError if the team no longer exists.

player: _bascenev1.SessionPlayer
80    @property
81    def player(self) -> bascenev1.SessionPlayer:
82        """Return the instance's associated bascenev1.SessionPlayer.
83
84        Raises a bascenev1.SessionPlayerNotFoundError if the player
85        no longer exists.
86        """
87        if not self._sessionplayer:
88            raise babase.SessionPlayerNotFoundError()
89        return self._sessionplayer

Return the instance's associated bascenev1.SessionPlayer.

Raises a bascenev1.SessionPlayerNotFoundError if the player no longer exists.

def getname(self, full: bool = False) -> str:
91    def getname(self, full: bool = False) -> str:
92        """Return the player entry's name."""
93        return self.name_full if full else self.name

Return the player entry's name.

def get_icon(self) -> dict[str, typing.Any]:
95    def get_icon(self) -> dict[str, Any]:
96        """Get the icon for this instance's player."""
97        player = self._last_sessionplayer
98        assert player is not None
99        return player.get_icon()

Get the icon for this instance's player.

def cancel_multi_kill_timer(self) -> None:
101    def cancel_multi_kill_timer(self) -> None:
102        """Cancel any multi-kill timer for this player entry."""
103        self._multi_kill_timer = None

Cancel any multi-kill timer for this player entry.

def getactivity(self) -> Activity | None:
105    def getactivity(self) -> bascenev1.Activity | None:
106        """Return the bascenev1.Activity this instance is associated with.
107
108        Returns None if the activity no longer exists."""
109        stats = self._stats()
110        if stats is not None:
111            return stats.getactivity()
112        return None

Return the bascenev1.Activity this instance is associated with.

Returns None if the activity no longer exists.

def associate_with_sessionplayer(self, sessionplayer: _bascenev1.SessionPlayer) -> None:
114    def associate_with_sessionplayer(
115        self, sessionplayer: bascenev1.SessionPlayer
116    ) -> None:
117        """Associate this entry with a bascenev1.SessionPlayer."""
118        self._sessionteam = weakref.ref(sessionplayer.sessionteam)
119        self.character = sessionplayer.character
120        self._last_sessionplayer = sessionplayer
121        self._sessionplayer = sessionplayer
122        self.streak = 0

Associate this entry with a bascenev1.SessionPlayer.

def get_last_sessionplayer(self) -> _bascenev1.SessionPlayer:
128    def get_last_sessionplayer(self) -> bascenev1.SessionPlayer:
129        """Return the last bascenev1.Player we were associated with."""
130        assert self._last_sessionplayer is not None
131        return self._last_sessionplayer

Return the last bascenev1.Player we were associated with.

def submit_kill(self, showpoints: bool = True) -> None:
133    def submit_kill(self, showpoints: bool = True) -> None:
134        """Submit a kill for this player entry."""
135        # FIXME Clean this up.
136        # pylint: disable=too-many-statements
137
138        self._multi_kill_count += 1
139        stats = self._stats()
140        assert stats
141        if self._multi_kill_count == 1:
142            score = 0
143            name = None
144            delay = 0.0
145            color = (0.0, 0.0, 0.0, 1.0)
146            scale = 1.0
147            sound = None
148        elif self._multi_kill_count == 2:
149            score = 20
150            name = babase.Lstr(resource='twoKillText')
151            color = (0.1, 1.0, 0.0, 1)
152            scale = 1.0
153            delay = 0.0
154            sound = stats.orchestrahitsound1
155        elif self._multi_kill_count == 3:
156            score = 40
157            name = babase.Lstr(resource='threeKillText')
158            color = (1.0, 0.7, 0.0, 1)
159            scale = 1.1
160            delay = 0.3
161            sound = stats.orchestrahitsound2
162        elif self._multi_kill_count == 4:
163            score = 60
164            name = babase.Lstr(resource='fourKillText')
165            color = (1.0, 1.0, 0.0, 1)
166            scale = 1.2
167            delay = 0.6
168            sound = stats.orchestrahitsound3
169        elif self._multi_kill_count == 5:
170            score = 80
171            name = babase.Lstr(resource='fiveKillText')
172            color = (1.0, 0.5, 0.0, 1)
173            scale = 1.3
174            delay = 0.9
175            sound = stats.orchestrahitsound4
176        else:
177            score = 100
178            name = babase.Lstr(
179                resource='multiKillText',
180                subs=[('${COUNT}', str(self._multi_kill_count))],
181            )
182            color = (1.0, 0.5, 0.0, 1)
183            scale = 1.3
184            delay = 1.0
185            sound = stats.orchestrahitsound4
186
187        def _apply(
188            name2: babase.Lstr,
189            score2: int,
190            showpoints2: bool,
191            color2: tuple[float, float, float, float],
192            scale2: float,
193            sound2: bascenev1.Sound | None,
194        ) -> None:
195            # pylint: disable=too-many-positional-arguments
196            from bascenev1lib.actor.popuptext import PopupText
197
198            # Only award this if they're still alive and we can get
199            # a current position for them.
200            our_pos: babase.Vec3 | None = None
201            if self._sessionplayer:
202                if self._sessionplayer.activityplayer is not None:
203                    try:
204                        our_pos = self._sessionplayer.activityplayer.position
205                    except babase.NotFoundError:
206                        pass
207            if our_pos is None:
208                return
209
210            # Jitter position a bit since these often come in clusters.
211            our_pos = babase.Vec3(
212                our_pos[0] + (random.random() - 0.5) * 2.0,
213                our_pos[1] + (random.random() - 0.5) * 2.0,
214                our_pos[2] + (random.random() - 0.5) * 2.0,
215            )
216            activity = self.getactivity()
217            if activity is not None:
218                PopupText(
219                    babase.Lstr(
220                        value=(('+' + str(score2) + ' ') if showpoints2 else '')
221                        + '${N}',
222                        subs=[('${N}', name2)],
223                    ),
224                    color=color2,
225                    scale=scale2,
226                    position=our_pos,
227                ).autoretain()
228            if sound2:
229                sound2.play()
230
231            self.score += score2
232            self.accumscore += score2
233
234            # Inform a running game of the score.
235            if score2 != 0 and activity is not None:
236                activity.handlemessage(PlayerScoredMessage(score=score2))
237
238        if name is not None:
239            _bascenev1.timer(
240                0.3 + delay,
241                babase.Call(
242                    _apply, name, score, showpoints, color, scale, sound
243                ),
244            )
245
246        # Keep the tally rollin'...
247        # set a timer for a bit in the future.
248        self._multi_kill_timer = _bascenev1.Timer(1.0, self._end_multi_kill)

Submit a kill for this player entry.

@dataclass
class PlayerScoredMessage:
24@dataclass
25class PlayerScoredMessage:
26    """Informs something that a bascenev1.Player scored."""
27
28    score: int
29    """The score value."""

Informs something that a bascenev1.Player scored.

PlayerScoredMessage(score: int)
score: int

The score value.

class Plugin:
317class Plugin:
318    """A plugin to alter app behavior in some way.
319
320    Plugins are discoverable by the meta-tag system
321    and the user can select which ones they want to enable.
322    Enabled plugins are then called at specific times as the
323    app is running in order to modify its behavior in some way.
324    """
325
326    def on_app_running(self) -> None:
327        """Called when the app reaches the running state."""
328
329    def on_app_suspend(self) -> None:
330        """Called when the app enters the suspended state."""
331
332    def on_app_unsuspend(self) -> None:
333        """Called when the app exits the suspended state."""
334
335    def on_app_shutdown(self) -> None:
336        """Called when the app is beginning the shutdown process."""
337
338    def on_app_shutdown_complete(self) -> None:
339        """Called when the app has completed the shutdown process."""
340
341    def has_settings_ui(self) -> bool:
342        """Called to ask if we have settings UI we can show."""
343        return False
344
345    def show_settings_ui(self, source_widget: Any | None) -> None:
346        """Called to show our settings UI."""

A plugin to alter app behavior in some way.

Plugins are discoverable by the meta-tag system and the user can select which ones they want to enable. Enabled plugins are then called at specific times as the app is running in order to modify its behavior in some way.

def on_app_running(self) -> None:
326    def on_app_running(self) -> None:
327        """Called when the app reaches the running state."""

Called when the app reaches the running state.

def on_app_suspend(self) -> None:
329    def on_app_suspend(self) -> None:
330        """Called when the app enters the suspended state."""

Called when the app enters the suspended state.

def on_app_unsuspend(self) -> None:
332    def on_app_unsuspend(self) -> None:
333        """Called when the app exits the suspended state."""

Called when the app exits the suspended state.

def on_app_shutdown(self) -> None:
335    def on_app_shutdown(self) -> None:
336        """Called when the app is beginning the shutdown process."""

Called when the app is beginning the shutdown process.

def on_app_shutdown_complete(self) -> None:
338    def on_app_shutdown_complete(self) -> None:
339        """Called when the app has completed the shutdown process."""

Called when the app has completed the shutdown process.

def has_settings_ui(self) -> bool:
341    def has_settings_ui(self) -> bool:
342        """Called to ask if we have settings UI we can show."""
343        return False

Called to ask if we have settings UI we can show.

def show_settings_ui(self, source_widget: typing.Any | None) -> None:
345    def show_settings_ui(self, source_widget: Any | None) -> None:
346        """Called to show our settings UI."""

Called to show our settings UI.

@dataclass
class PowerupAcceptMessage:
37@dataclass
38class PowerupAcceptMessage:
39    """A message informing a bascenev1.Powerup that it was accepted.
40
41    This is generally sent in response to a bascenev1.PowerupMessage to
42    inform the box (or whoever granted it) that it can go away.
43    """

A message informing a bascenev1.Powerup that it was accepted.

This is generally sent in response to a bascenev1.PowerupMessage to inform the box (or whoever granted it) that it can go away.

@dataclass
class PowerupMessage:
17@dataclass
18class PowerupMessage:
19    """A message telling an object to accept a powerup.
20
21    This message is normally received by touching a
22    bascenev1.PowerupBox.
23    """
24
25    poweruptype: str
26    """The type of powerup to be granted (a string).
27       See bascenev1.Powerup.poweruptype for available type values."""
28
29    sourcenode: bascenev1.Node | None = None
30    """The node the powerup game from, or None otherwise.
31       If a powerup is accepted, a bascenev1.PowerupAcceptMessage should be
32       sent back to the sourcenode to inform it of the fact. This will
33       generally cause the powerup box to make a sound and disappear or
34       whatnot."""

A message telling an object to accept a powerup.

This message is normally received by touching a bascenev1.PowerupBox.

PowerupMessage(poweruptype: str, sourcenode: _bascenev1.Node | None = None)
poweruptype: str

The type of powerup to be granted (a string). See bascenev1.Powerup.poweruptype for available type values.

sourcenode: _bascenev1.Node | None = None

The node the powerup game from, or None otherwise. If a powerup is accepted, a bascenev1.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.

def printnodes() -> None:
1522def printnodes() -> None:
1523    """Print various info about existing nodes; useful for debugging."""
1524    return None

Print various info about existing nodes; useful for debugging.

def pushcall( call: Callable, from_other_thread: bool = False, suppress_other_thread_warning: bool = False, other_thread_use_fg_context: bool = False, raw: bool = False) -> None:
1368def pushcall(
1369    call: Callable,
1370    from_other_thread: bool = False,
1371    suppress_other_thread_warning: bool = False,
1372    other_thread_use_fg_context: bool = False,
1373    raw: bool = False,
1374) -> None:
1375    """Push a call to the logic event-loop.
1376
1377    This call expects to be used in the logic thread, and will automatically
1378    save and restore the babase.Context to behave seamlessly.
1379
1380    If you want to push a call from outside of the logic thread,
1381    however, you can pass 'from_other_thread' as True. In this case
1382    the call will always run in the UI context_ref on the logic thread
1383    or whichever context_ref is in the foreground if
1384    other_thread_use_fg_context is True.
1385    Passing raw=True will disable thread checks and context_ref sets/restores.
1386    """
1387    return None

Push a call to the logic event-loop.

This call expects to be used in the logic thread, and will automatically save and restore the babase.Context to behave seamlessly.

If you want to push a call from outside of the logic thread, however, you can pass 'from_other_thread' as True. In this case the call will always run in the UI context_ref on the logic thread or whichever context_ref is in the foreground if other_thread_use_fg_context is True. Passing raw=True will disable thread checks and context_ref sets/restores.

def register_map(maptype: type[Map]) -> None:
360def register_map(maptype: type[Map]) -> None:
361    """Register a map class with the game."""
362    assert babase.app.classic is not None
363    if maptype.name in babase.app.classic.maps:
364        raise RuntimeError(f'Map "{maptype.name}" is already registered.')
365    babase.app.classic.maps[maptype.name] = maptype

Register a map class with the game.

def safecolor( color: Sequence[float], target_intensity: float = 0.6) -> tuple[float, ...]:
1439def safecolor(
1440    color: Sequence[float], target_intensity: float = 0.6
1441) -> tuple[float, ...]:
1442    """Given a color tuple, return a color safe to display as text.
1443
1444    Accepts tuples of length 3 or 4. This will slightly brighten very
1445    dark colors, etc.
1446    """
1447    return (0.0, 0.0, 0.0)

Given a color tuple, return a color safe to display as text.

Accepts tuples of length 3 or 4. This will slightly brighten very dark colors, etc.

def screenmessage( message: str | Lstr, color: Optional[Sequence[float]] = None, log: bool = False) -> None:
1450def screenmessage(
1451    message: str | babase.Lstr,
1452    color: Sequence[float] | None = None,
1453    log: bool = False,
1454) -> None:
1455    """Print a message to the local client's screen, in a given color.
1456
1457    Note that this version of the function is purely for local display.
1458    To broadcast screen messages in network play, look for methods such as
1459    broadcastmessage() provided by the scene-version packages.
1460    """
1461    return None

Print a message to the local client's screen, in a given color.

Note that this version of the function is purely for local display. To broadcast screen messages in network play, look for methods such as broadcastmessage() provided by the scene-version packages.

@dataclass
class ScoreConfig:
25@dataclass
26class ScoreConfig:
27    """Settings for how a game handles scores."""
28
29    label: str = 'Score'
30    """A label show to the user for scores; 'Score', 'Time Survived', etc."""
31
32    scoretype: bascenev1.ScoreType = ScoreType.POINTS
33    """How the score value should be displayed."""
34
35    lower_is_better: bool = False
36    """Whether lower scores are preferable. Higher scores are by default."""
37
38    none_is_winner: bool = False
39    """Whether a value of None is considered better than other scores.
40       By default it is not."""
41
42    version: str = ''
43    """To change high-score lists used by a game without renaming the game,
44       change this. Defaults to an empty string."""

Settings for how a game handles scores.

ScoreConfig( label: str = 'Score', scoretype: ScoreType = <ScoreType.POINTS: 'p'>, lower_is_better: bool = False, none_is_winner: bool = False, version: str = '')
label: str = 'Score'

A label show to the user for scores; 'Score', 'Time Survived', etc.

scoretype: ScoreType = <ScoreType.POINTS: 'p'>

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.

134class ScoreScreenActivity(Activity[EmptyPlayer, EmptyTeam]):
135    """A standard score screen that fades in and shows stuff for a while.
136
137    After a specified delay, player input is assigned to end the activity.
138    """
139
140    transition_time = 0.5
141    inherits_tint = True
142    inherits_vr_camera_offset = True
143    use_fixed_vr_overlay = True
144
145    default_music: MusicType | None = MusicType.SCORES
146
147    def __init__(self, settings: dict):
148        super().__init__(settings)
149        self._birth_time = babase.apptime()
150        self._min_view_time = 5.0
151        self._allow_server_transition = False
152        self._background: bascenev1.Actor | None = None
153        self._tips_text: bascenev1.Actor | None = None
154        self._kicked_off_server_shutdown = False
155        self._kicked_off_server_restart = False
156        self._default_show_tips = True
157        self._custom_continue_message: babase.Lstr | None = None
158        self._server_transitioning: bool | None = None
159
160    @override
161    def on_player_join(self, player: EmptyPlayer) -> None:
162        super().on_player_join(player)
163        time_till_assign = max(
164            0, self._birth_time + self._min_view_time - babase.apptime()
165        )
166
167        # If we're still kicking at the end of our assign-delay, assign this
168        # guy's input to trigger us.
169        _bascenev1.timer(
170            time_till_assign, babase.WeakCall(self._safe_assign, player)
171        )
172
173    @override
174    def on_transition_in(self) -> None:
175        from bascenev1lib.actor.tipstext import TipsText
176        from bascenev1lib.actor.background import Background
177
178        super().on_transition_in()
179        self._background = Background(
180            fade_time=0.5, start_faded=False, show_logo=True
181        )
182        if self._default_show_tips:
183            self._tips_text = TipsText()
184        setmusic(self.default_music)
185
186    @override
187    def on_begin(self) -> None:
188        # pylint: disable=cyclic-import
189        from bascenev1lib.actor.text import Text
190
191        super().on_begin()
192
193        # Pop up a 'press any button to continue' statement after our
194        # min-view-time show a 'press any button to continue..'
195        # thing after a bit.
196        assert babase.app.classic is not None
197        if babase.app.ui_v1.uiscale is babase.UIScale.LARGE:
198            # FIXME: Need a better way to determine whether we've probably
199            #  got a keyboard.
200            sval = babase.Lstr(resource='pressAnyKeyButtonText')
201        else:
202            sval = babase.Lstr(resource='pressAnyButtonText')
203
204        Text(
205            (
206                self._custom_continue_message
207                if self._custom_continue_message is not None
208                else sval
209            ),
210            v_attach=Text.VAttach.BOTTOM,
211            h_align=Text.HAlign.CENTER,
212            flash=True,
213            vr_depth=50,
214            position=(0, 10),
215            scale=0.8,
216            color=(0.5, 0.7, 0.5, 0.5),
217            transition=Text.Transition.IN_BOTTOM_SLOW,
218            transition_delay=self._min_view_time,
219        ).autoretain()
220
221    def _player_press(self) -> None:
222        # If this activity is a good 'end point', ask server-mode just once if
223        # it wants to do anything special like switch sessions or kill the app.
224        if (
225            self._allow_server_transition
226            and babase.app.classic is not None
227            and babase.app.classic.server is not None
228            and self._server_transitioning is None
229        ):
230            self._server_transitioning = (
231                babase.app.classic.server.handle_transition()
232            )
233            assert isinstance(self._server_transitioning, bool)
234
235        # If server-mode is handling this, don't do anything ourself.
236        if self._server_transitioning is True:
237            return
238
239        # Otherwise end the activity normally.
240        self.end()
241
242    def _safe_assign(self, player: EmptyPlayer) -> None:
243        # Just to be extra careful, don't assign if we're transitioning out.
244        # (though theoretically that should be ok).
245        if not self.is_transitioning_out() and player:
246            player.assigninput(
247                (
248                    babase.InputType.JUMP_PRESS,
249                    babase.InputType.PUNCH_PRESS,
250                    babase.InputType.BOMB_PRESS,
251                    babase.InputType.PICK_UP_PRESS,
252                ),
253                self._player_press,
254            )

A standard score screen that fades in and shows stuff for a while.

After a specified delay, player input is assigned to end the activity.

ScoreScreenActivity(settings: dict)
147    def __init__(self, settings: dict):
148        super().__init__(settings)
149        self._birth_time = babase.apptime()
150        self._min_view_time = 5.0
151        self._allow_server_transition = False
152        self._background: bascenev1.Actor | None = None
153        self._tips_text: bascenev1.Actor | None = None
154        self._kicked_off_server_shutdown = False
155        self._kicked_off_server_restart = False
156        self._default_show_tips = True
157        self._custom_continue_message: babase.Lstr | None = None
158        self._server_transitioning: bool | None = None

Creates an Activity in the current bascenev1.Session.

The activity will not be actually run until bascenev1.Session.setactivity is called. 'settings' should be a dict of key/value pairs specific to the activity.

Activities should preload as much of their media/etc as possible in their constructor, but none of it should actually be used until they are transitioned in.

transition_time = 0.5

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.

inherits_tint = True

Set this to true to inherit screen tint/vignette colors from the previous activity (useful to prevent sudden color changes during transitions).

inherits_vr_camera_offset = True

Set this to true to inherit VR camera offsets from the previous activity (useful for preventing sporadic camera movement during transitions).

use_fixed_vr_overlay = True

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.

default_music: MusicType | None = <MusicType.SCORES: 'Scores'>
@override
def on_player_join(self, player: EmptyPlayer) -> None:
160    @override
161    def on_player_join(self, player: EmptyPlayer) -> None:
162        super().on_player_join(player)
163        time_till_assign = max(
164            0, self._birth_time + self._min_view_time - babase.apptime()
165        )
166
167        # If we're still kicking at the end of our assign-delay, assign this
168        # guy's input to trigger us.
169        _bascenev1.timer(
170            time_till_assign, babase.WeakCall(self._safe_assign, player)
171        )

Called when a new bascenev1.Player has joined the Activity.

(including the initial set of Players)

@override
def on_transition_in(self) -> None:
173    @override
174    def on_transition_in(self) -> None:
175        from bascenev1lib.actor.tipstext import TipsText
176        from bascenev1lib.actor.background import Background
177
178        super().on_transition_in()
179        self._background = Background(
180            fade_time=0.5, start_faded=False, show_logo=True
181        )
182        if self._default_show_tips:
183            self._tips_text = TipsText()
184        setmusic(self.default_music)

Called when the Activity is first becoming visible.

Upon this call, the Activity should fade in backgrounds, start playing music, etc. It does not yet have access to players or teams, however. They remain owned by the previous Activity up until bascenev1.Activity.on_begin() is called.

@override
def on_begin(self) -> None:
186    @override
187    def on_begin(self) -> None:
188        # pylint: disable=cyclic-import
189        from bascenev1lib.actor.text import Text
190
191        super().on_begin()
192
193        # Pop up a 'press any button to continue' statement after our
194        # min-view-time show a 'press any button to continue..'
195        # thing after a bit.
196        assert babase.app.classic is not None
197        if babase.app.ui_v1.uiscale is babase.UIScale.LARGE:
198            # FIXME: Need a better way to determine whether we've probably
199            #  got a keyboard.
200            sval = babase.Lstr(resource='pressAnyKeyButtonText')
201        else:
202            sval = babase.Lstr(resource='pressAnyButtonText')
203
204        Text(
205            (
206                self._custom_continue_message
207                if self._custom_continue_message is not None
208                else sval
209            ),
210            v_attach=Text.VAttach.BOTTOM,
211            h_align=Text.HAlign.CENTER,
212            flash=True,
213            vr_depth=50,
214            position=(0, 10),
215            scale=0.8,
216            color=(0.5, 0.7, 0.5, 0.5),
217            transition=Text.Transition.IN_BOTTOM_SLOW,
218            transition_delay=self._min_view_time,
219        ).autoretain()

Called once the previous Activity has finished transitioning out.

At this point the activity's initial players and teams are filled in and it should begin its actual game logic.

@unique
class ScoreType(enum.Enum):
16@unique
17class ScoreType(Enum):
18    """Type of scores."""
19
20    SECONDS = 's'
21    MILLISECONDS = 'ms'
22    POINTS = 'p'

Type of scores.

SECONDS = <ScoreType.SECONDS: 's'>
MILLISECONDS = <ScoreType.MILLISECONDS: 'ms'>
POINTS = <ScoreType.POINTS: 'p'>
class SessionNotFoundError(bascenev1.NotFoundError):
65class SessionNotFoundError(NotFoundError):
66    """Exception raised when an expected session does not exist."""

Exception raised when an expected session does not exist.

def broadcastmessage( message: str | Lstr, color: Optional[Sequence[float]] = None, top: bool = False, image: dict[str, typing.Any] | None = None, log: bool = False, clients: Optional[Sequence[int]] = None, transient: bool = False) -> None:
 998def broadcastmessage(
 999    message: str | babase.Lstr,
1000    color: Sequence[float] | None = None,
1001    top: bool = False,
1002    image: dict[str, Any] | None = None,
1003    log: bool = False,
1004    clients: Sequence[int] | None = None,
1005    transient: bool = False,
1006) -> None:
1007    """Broadcast a screen-message to clients in the current session.
1008
1009    If 'top' is True, the message will go to the top message area.
1010    For 'top' messages, 'image' must be a dict containing 'texture'
1011    and 'tint_texture' textures and 'tint_color' and 'tint2_color'
1012    colors. This defines an icon to display alongside the message.
1013    If 'log' is True, the message will also be submitted to the log.
1014    'clients' can be a list of client-ids the message should be sent
1015    to, or None to specify that everyone should receive it.
1016    If 'transient' is True, the message will not be included in the
1017    game-stream and thus will not show up when viewing replays.
1018    Currently the 'clients' option only works for transient messages.
1019    """
1020    return None

Broadcast a screen-message to clients in the current session.

If 'top' is True, the message will go to the top message area. For 'top' messages, 'image' must be a dict containing 'texture' and 'tint_texture' textures and 'tint_color' and 'tint2_color' colors. This defines an icon to display alongside the message. If 'log' is True, the message will also be submitted to the log. 'clients' can be a list of client-ids the message should be sent to, or None to specify that everyone should receive it. If 'transient' is True, the message will not be included in the game-stream and thus will not show up when viewing replays. Currently the 'clients' option only works for transient messages.

class Session:
 43class Session:
 44    """Defines a high level series of bascenev1.Activity-es.
 45
 46    Examples of sessions are bascenev1.FreeForAllSession,
 47    bascenev1.DualTeamSession, and bascenev1.CoopSession.
 48
 49    A Session is responsible for wrangling and transitioning between various
 50    bascenev1.Activity instances such as mini-games and score-screens, and for
 51    maintaining state between them (players, teams, score tallies, etc).
 52    """
 53
 54    use_teams: bool = False
 55    """Whether this session groups players into an explicit set of
 56       teams. If this is off, a unique team is generated for each
 57       player that joins."""
 58
 59    use_team_colors: bool = True
 60    """Whether players on a team should all adopt the colors of that
 61       team instead of their own profile colors. This only applies if
 62       use_teams is enabled."""
 63
 64    # Note: even though these are instance vars, we annotate and document them
 65    # at the class level so that looks better and nobody get lost while
 66    # reading large __init__
 67
 68    lobby: bascenev1.Lobby
 69    """The baclassic.Lobby instance where new bascenev1.Player-s go to select
 70       a Profile/Team/etc. before being added to games.
 71       Be aware this value may be None if a Session does not allow
 72       any such selection."""
 73
 74    max_players: int
 75    """The maximum number of players allowed in the Session."""
 76
 77    min_players: int
 78    """The minimum number of players who must be present for the Session
 79       to proceed past the initial joining screen"""
 80
 81    sessionplayers: list[bascenev1.SessionPlayer]
 82    """All bascenev1.SessionPlayers in the Session. Most things should use
 83       the list of bascenev1.Player-s in bascenev1.Activity; not this. Some
 84       players, such as those who have not yet selected a character, will
 85       only be found on this list."""
 86
 87    customdata: dict
 88    """A shared dictionary for objects to use as storage on this session.
 89       Ensure that keys here are unique to avoid collisions."""
 90
 91    sessionteams: list[bascenev1.SessionTeam]
 92    """All the bascenev1.SessionTeams in the Session. Most things should
 93       use the list of bascenev1.Team-s in bascenev1.Activity; not this."""
 94
 95    def __init__(
 96        self,
 97        depsets: Sequence[bascenev1.DependencySet],
 98        *,
 99        team_names: Sequence[str] | None = None,
100        team_colors: Sequence[Sequence[float]] | None = None,
101        min_players: int = 1,
102        max_players: int = 8,
103        submit_score: bool = True,
104    ):
105        """Instantiate a session.
106
107        depsets should be a sequence of successfully resolved
108        bascenev1.DependencySet instances; one for each bascenev1.Activity
109        the session may potentially run.
110        """
111        # pylint: disable=too-many-statements
112        # pylint: disable=too-many-locals
113        # pylint: disable=cyclic-import
114        # pylint: disable=too-many-branches
115        from efro.util import empty_weakref
116        from bascenev1._dependency import (
117            Dependency,
118            AssetPackage,
119            DependencyError,
120        )
121        from bascenev1._lobby import Lobby
122        from bascenev1._stats import Stats
123        from bascenev1._gameactivity import GameActivity
124        from bascenev1._activity import Activity
125        from bascenev1._team import SessionTeam
126
127        # First off, resolve all dependency-sets we were passed.
128        # If things are missing, we'll try to gather them into a single
129        # missing-deps exception if possible to give the caller a clean
130        # path to download missing stuff and try again.
131        missing_asset_packages: set[str] = set()
132        for depset in depsets:
133            try:
134                depset.resolve()
135            except DependencyError as exc:
136                # Gather/report missing assets only; barf on anything else.
137                if all(issubclass(d.cls, AssetPackage) for d in exc.deps):
138                    for dep in exc.deps:
139                        assert isinstance(dep.config, str)
140                        missing_asset_packages.add(dep.config)
141                else:
142                    missing_info = [(d.cls, d.config) for d in exc.deps]
143                    raise RuntimeError(
144                        f'Missing non-asset dependencies: {missing_info}'
145                    ) from exc
146
147        # Throw a combined exception if we found anything missing.
148        if missing_asset_packages:
149            raise DependencyError(
150                [
151                    Dependency(AssetPackage, set_id)
152                    for set_id in missing_asset_packages
153                ]
154            )
155
156        # Ok; looks like our dependencies check out.
157        # Now give the engine a list of asset-set-ids to pass along to clients.
158        required_asset_packages: set[str] = set()
159        for depset in depsets:
160            required_asset_packages.update(depset.get_asset_package_ids())
161
162        # print('Would set host-session asset-reqs to:',
163        # required_asset_packages)
164
165        # Init our C++ layer data.
166        self._sessiondata = _bascenev1.register_session(self)
167
168        # Should remove this if possible.
169        self.tournament_id: str | None = None
170
171        self.sessionteams = []
172        self.sessionplayers = []
173        self.min_players = min_players
174        self.max_players = (
175            max_players
176            if _max_players_override is None
177            else _max_players_override
178        )
179        self.submit_score = submit_score
180
181        self.customdata = {}
182        self._in_set_activity = False
183        self._next_team_id = 0
184        self._activity_retained: bascenev1.Activity | None = None
185        self._launch_end_session_activity_time: float | None = None
186        self._activity_end_timer: bascenev1.BaseTimer | None = None
187        self._activity_weak = empty_weakref(Activity)
188        self._next_activity: bascenev1.Activity | None = None
189        self._wants_to_end = False
190        self._ending = False
191        self._activity_should_end_immediately = False
192        self._activity_should_end_immediately_results: (
193            bascenev1.GameResults | None
194        ) = None
195        self._activity_should_end_immediately_delay = 0.0
196
197        # Create static teams if we're using them.
198        if self.use_teams:
199            if team_names is None:
200                raise RuntimeError(
201                    'use_teams is True but team_names not provided.'
202                )
203            if team_colors is None:
204                raise RuntimeError(
205                    'use_teams is True but team_colors not provided.'
206                )
207            if len(team_colors) != len(team_names):
208                raise RuntimeError(
209                    f'Got {len(team_names)} team_names'
210                    f' and {len(team_colors)} team_colors;'
211                    f' these numbers must match.'
212                )
213            for i, color in enumerate(team_colors):
214                team = SessionTeam(
215                    team_id=self._next_team_id,
216                    name=GameActivity.get_team_display_string(team_names[i]),
217                    color=color,
218                )
219                self.sessionteams.append(team)
220                self._next_team_id += 1
221                try:
222                    with self.context:
223                        self.on_team_join(team)
224                except Exception:
225                    logging.exception('Error in on_team_join for %s.', self)
226
227        self.lobby = Lobby()
228        self.stats = Stats()
229
230        # Instantiate our session globals node which will apply its settings.
231        self._sessionglobalsnode = _bascenev1.newnode('sessionglobals')
232
233        # Rejoin cooldown stuff.
234        self._players_on_wait: dict = {}
235        self._player_requested_identifiers: dict = {}
236        self._waitlist_timers: dict = {}
237
238    @property
239    def context(self) -> bascenev1.ContextRef:
240        """A context-ref pointing at this activity."""
241        return self._sessiondata.context()
242
243    @property
244    def sessionglobalsnode(self) -> bascenev1.Node:
245        """The sessionglobals bascenev1.Node for the session."""
246        node = self._sessionglobalsnode
247        if not node:
248            raise babase.NodeNotFoundError()
249        return node
250
251    def should_allow_mid_activity_joins(
252        self, activity: bascenev1.Activity
253    ) -> bool:
254        """Ask ourself if we should allow joins during an Activity.
255
256        Note that for a join to be allowed, both the Session and Activity
257        have to be ok with it (via this function and the
258        Activity.allow_mid_activity_joins property.
259        """
260        del activity  # Unused.
261        return True
262
263    def on_player_request(self, player: bascenev1.SessionPlayer) -> bool:
264        """Called when a new bascenev1.Player wants to join the Session.
265
266        This should return True or False to accept/reject.
267        """
268        # Limit player counts *unless* we're in a stress test.
269        if (
270            babase.app.classic is not None
271            and babase.app.classic.stress_test_update_timer is None
272        ):
273            if len(self.sessionplayers) >= self.max_players >= 0:
274                # Print a rejection message *only* to the client trying to
275                # join (prevents spamming everyone else in the game).
276                _bascenev1.getsound('error').play()
277                _bascenev1.broadcastmessage(
278                    babase.Lstr(
279                        resource='playerLimitReachedText',
280                        subs=[('${COUNT}', str(self.max_players))],
281                    ),
282                    color=(0.8, 0.0, 0.0),
283                    clients=[player.inputdevice.client_id],
284                    transient=True,
285                )
286                return False
287
288        # Rejoin cooldown.
289        identifier = player.get_v1_account_id()
290        if identifier:
291            leave_time = self._players_on_wait.get(identifier)
292            if leave_time:
293                diff = str(
294                    math.ceil(
295                        _g_player_rejoin_cooldown
296                        - babase.apptime()
297                        + leave_time
298                    )
299                )
300                _bascenev1.broadcastmessage(
301                    babase.Lstr(
302                        translate=(
303                            'serverResponses',
304                            'You can join in ${COUNT} seconds.',
305                        ),
306                        subs=[('${COUNT}', diff)],
307                    ),
308                    color=(1, 1, 0),
309                    clients=[player.inputdevice.client_id],
310                    transient=True,
311                )
312                return False
313            self._player_requested_identifiers[player.id] = identifier
314
315        _bascenev1.getsound('dripity').play()
316        return True
317
318    def on_player_leave(self, sessionplayer: bascenev1.SessionPlayer) -> None:
319        """Called when a previously-accepted bascenev1.SessionPlayer leaves."""
320
321        if sessionplayer not in self.sessionplayers:
322            print(
323                'ERROR: Session.on_player_leave called'
324                ' for player not in our list.'
325            )
326            return
327
328        _bascenev1.getsound('playerLeft').play()
329
330        activity = self._activity_weak()
331
332        # Rejoin cooldown.
333        identifier = self._player_requested_identifiers.get(sessionplayer.id)
334        if identifier:
335            self._players_on_wait[identifier] = babase.apptime()
336            with babase.ContextRef.empty():
337                self._waitlist_timers[identifier] = babase.AppTimer(
338                    _g_player_rejoin_cooldown,
339                    babase.Call(self._remove_player_from_waitlist, identifier),
340                )
341
342        if not sessionplayer.in_game:
343            # Ok, the player is still in the lobby; simply remove them.
344            with self.context:
345                try:
346                    self.lobby.remove_chooser(sessionplayer)
347                except Exception:
348                    logging.exception('Error in Lobby.remove_chooser().')
349        else:
350            # Ok, they've already entered the game. Remove them from
351            # teams/activities/etc.
352            sessionteam = sessionplayer.sessionteam
353            assert sessionteam is not None
354
355            _bascenev1.broadcastmessage(
356                babase.Lstr(
357                    resource='playerLeftText',
358                    subs=[('${PLAYER}', sessionplayer.getname(full=True))],
359                )
360            )
361
362            # Remove them from their SessionTeam.
363            if sessionplayer in sessionteam.players:
364                sessionteam.players.remove(sessionplayer)
365            else:
366                print(
367                    'SessionPlayer not found in SessionTeam'
368                    ' in on_player_leave.'
369                )
370
371            # Grab their activity-specific player instance.
372            player = sessionplayer.activityplayer
373            assert isinstance(player, (Player, type(None)))
374
375            # Remove them from any current Activity.
376            if player is not None and activity is not None:
377                if player in activity.players:
378                    activity.remove_player(sessionplayer)
379                else:
380                    print('Player not found in Activity in on_player_leave.')
381
382            # If we're a non-team session, remove their team too.
383            if not self.use_teams:
384                self._remove_player_team(sessionteam, activity)
385
386        # Now remove them from the session list.
387        self.sessionplayers.remove(sessionplayer)
388
389    def _remove_player_team(
390        self,
391        sessionteam: bascenev1.SessionTeam,
392        activity: bascenev1.Activity | None,
393    ) -> None:
394        """Remove the player-specific team in non-teams mode."""
395
396        # They should have been the only one on their team.
397        assert not sessionteam.players
398
399        # Remove their Team from the Activity.
400        if activity is not None:
401            if sessionteam.activityteam in activity.teams:
402                activity.remove_team(sessionteam)
403            else:
404                print('Team not found in Activity in on_player_leave.')
405
406        # And then from the Session.
407        with self.context:
408            if sessionteam in self.sessionteams:
409                try:
410                    self.sessionteams.remove(sessionteam)
411                    self.on_team_leave(sessionteam)
412                except Exception:
413                    logging.exception(
414                        'Error in on_team_leave for Session %s.', self
415                    )
416            else:
417                print('Team no in Session teams in on_player_leave.')
418            try:
419                sessionteam.leave()
420            except Exception:
421                logging.exception(
422                    'Error clearing sessiondata for team %s in session %s.',
423                    sessionteam,
424                    self,
425                )
426
427    def end(self) -> None:
428        """Initiates an end to the session and a return to the main menu.
429
430        Note that this happens asynchronously, allowing the
431        session and its activities to shut down gracefully.
432        """
433        self._wants_to_end = True
434        if self._next_activity is None:
435            self._launch_end_session_activity()
436
437    def _launch_end_session_activity(self) -> None:
438        """(internal)"""
439        from bascenev1._activitytypes import EndSessionActivity
440
441        with self.context:
442            curtime = babase.apptime()
443            if self._ending:
444                # Ignore repeats unless its been a while.
445                assert self._launch_end_session_activity_time is not None
446                since_last = curtime - self._launch_end_session_activity_time
447                if since_last < 30.0:
448                    return
449                logging.error(
450                    '_launch_end_session_activity called twice (since_last=%s)',
451                    since_last,
452                )
453            self._launch_end_session_activity_time = curtime
454            self.setactivity(_bascenev1.newactivity(EndSessionActivity))
455            self._wants_to_end = False
456            self._ending = True  # Prevent further actions.
457
458    def on_team_join(self, team: bascenev1.SessionTeam) -> None:
459        """Called when a new bascenev1.Team joins the session."""
460
461    def on_team_leave(self, team: bascenev1.SessionTeam) -> None:
462        """Called when a bascenev1.Team is leaving the session."""
463
464    def end_activity(
465        self,
466        activity: bascenev1.Activity,
467        results: Any,
468        delay: float,
469        force: bool,
470    ) -> None:
471        """Commence shutdown of a bascenev1.Activity (if not already occurring).
472
473        'delay' is the time delay before the Activity actually ends
474        (in seconds). Further calls to end() will be ignored up until
475        this time, unless 'force' is True, in which case the new results
476        will replace the old.
477        """
478        # Only pay attention if this is coming from our current activity.
479        if activity is not self._activity_retained:
480            return
481
482        # If this activity hasn't begun yet, just set it up to end immediately
483        # once it does.
484        if not activity.has_begun():
485            # activity.set_immediate_end(results, delay, force)
486            if not self._activity_should_end_immediately or force:
487                self._activity_should_end_immediately = True
488                self._activity_should_end_immediately_results = results
489                self._activity_should_end_immediately_delay = delay
490
491        # The activity has already begun; get ready to end it.
492        else:
493            if (not activity.has_ended()) or force:
494                activity.set_has_ended(True)
495
496                # Set a timer to set in motion this activity's demise.
497                self._activity_end_timer = _bascenev1.BaseTimer(
498                    delay,
499                    babase.Call(self._complete_end_activity, activity, results),
500                )
501
502    def handlemessage(self, msg: Any) -> Any:
503        """General message handling; can be passed any message object."""
504        from bascenev1._lobby import PlayerReadyMessage
505        from bascenev1._messages import PlayerProfilesChangedMessage, UNHANDLED
506
507        if isinstance(msg, PlayerReadyMessage):
508            self._on_player_ready(msg.chooser)
509
510        elif isinstance(msg, PlayerProfilesChangedMessage):
511            # If we have a current activity with a lobby, ask it to reload
512            # profiles.
513            with self.context:
514                self.lobby.reload_profiles()
515            return None
516
517        else:
518            return UNHANDLED
519        return None
520
521    class _SetActivityScopedLock:
522        def __init__(self, session: Session) -> None:
523            self._session = session
524            if session._in_set_activity:
525                raise RuntimeError('Session.setactivity() called recursively.')
526            self._session._in_set_activity = True
527
528        def __del__(self) -> None:
529            self._session._in_set_activity = False
530
531    def setactivity(self, activity: bascenev1.Activity) -> None:
532        """Assign a new current bascenev1.Activity for the session.
533
534        Note that this will not change the current context to the new
535        Activity's. Code must be run in the new activity's methods
536        (on_transition_in, etc) to get it. (so you can't do
537        session.setactivity(foo) and then bascenev1.newnode() to add a node
538        to foo)
539        """
540
541        # Make sure we don't get called recursively.
542        _rlock = self._SetActivityScopedLock(self)
543
544        if activity.session is not _bascenev1.getsession():
545            raise RuntimeError("Provided Activity's Session is not current.")
546
547        # Quietly ignore this if the whole session is going down.
548        if self._ending:
549            return
550
551        if activity is self._activity_retained:
552            logging.error('Activity set to already-current activity.')
553            return
554
555        if self._next_activity is not None:
556            raise RuntimeError(
557                'Activity switch already in progress (to '
558                + str(self._next_activity)
559                + ')'
560            )
561
562        prev_activity = self._activity_retained
563        prev_globals = (
564            prev_activity.globalsnode if prev_activity is not None else None
565        )
566
567        # Let the activity do its thing.
568        activity.transition_in(prev_globals)
569
570        self._next_activity = activity
571
572        # If we have a current activity, tell it it's transitioning out;
573        # the next one will become current once this one dies.
574        if prev_activity is not None:
575            prev_activity.transition_out()
576
577            # Setting this to None should free up the old activity to die,
578            # which will call begin_next_activity.
579            # We can still access our old activity through
580            # self._activity_weak() to keep it up to date on player
581            # joins/departures/etc until it dies.
582            self._activity_retained = None
583
584        # There's no existing activity; lets just go ahead with the begin call.
585        else:
586            self.begin_next_activity()
587
588        # We want to call destroy() for the previous activity once it should
589        # tear itself down, clear out any self-refs, etc. After this call
590        # the activity should have no refs left to it and should die (which
591        # will trigger the next activity to run).
592        if prev_activity is not None:
593            with babase.ContextRef.empty():
594                babase.apptimer(
595                    max(0.0, activity.transition_time), prev_activity.expire
596                )
597        self._in_set_activity = False
598
599    def getactivity(self) -> bascenev1.Activity | None:
600        """Return the current foreground activity for this session."""
601        return self._activity_weak()
602
603    def get_custom_menu_entries(self) -> list[dict[str, Any]]:
604        """Subclasses can override this to provide custom menu entries.
605
606        The returned value should be a list of dicts, each containing
607        a 'label' and 'call' entry, with 'label' being the text for
608        the entry and 'call' being the callable to trigger if the entry
609        is pressed.
610        """
611        return []
612
613    def _complete_end_activity(
614        self, activity: bascenev1.Activity, results: Any
615    ) -> None:
616        # Run the subclass callback in the session context.
617        try:
618            with self.context:
619                self.on_activity_end(activity, results)
620        except Exception:
621            logging.error(
622                'Error in on_activity_end() for session %s'
623                ' activity %s with results %s',
624                self,
625                activity,
626                results,
627            )
628
629    def _request_player(self, sessionplayer: bascenev1.SessionPlayer) -> bool:
630        """Called by the native layer when a player wants to join."""
631
632        # If we're ending, allow no new players.
633        if self._ending:
634            return False
635
636        # Ask the bascenev1.Session subclass to approve/deny this request.
637        try:
638            with self.context:
639                result = self.on_player_request(sessionplayer)
640        except Exception:
641            logging.exception('Error in on_player_request for %s.', self)
642            result = False
643
644        # If they said yes, add the player to the lobby.
645        if result:
646            self.sessionplayers.append(sessionplayer)
647            with self.context:
648                try:
649                    self.lobby.add_chooser(sessionplayer)
650                except Exception:
651                    logging.exception('Error in lobby.add_chooser().')
652
653        return result
654
655    def on_activity_end(
656        self, activity: bascenev1.Activity, results: Any
657    ) -> None:
658        """Called when the current bascenev1.Activity has ended.
659
660        The bascenev1.Session should look at the results and start
661        another bascenev1.Activity.
662        """
663
664    def begin_next_activity(self) -> None:
665        """Called once the previous activity has been totally torn down.
666
667        This means we're ready to begin the next one
668        """
669        if self._next_activity is None:
670            # Should this ever happen?
671            logging.error('begin_next_activity() called with no _next_activity')
672            return
673
674        # We store both a weak and a strong ref to the new activity;
675        # the strong is to keep it alive and the weak is so we can access
676        # it even after we've released the strong-ref to allow it to die.
677        self._activity_retained = self._next_activity
678        self._activity_weak = weakref.ref(self._next_activity)
679        self._next_activity = None
680        self._activity_should_end_immediately = False
681
682        # Kick out anyone loitering in the lobby.
683        self.lobby.remove_all_choosers_and_kick_players()
684
685        # Kick off the activity.
686        self._activity_retained.begin(self)
687
688        # If we want to completely end the session, we can now kick that off.
689        if self._wants_to_end:
690            self._launch_end_session_activity()
691        else:
692            # Otherwise, if the activity has already been told to end,
693            # do so now.
694            if self._activity_should_end_immediately:
695                self._activity_retained.end(
696                    self._activity_should_end_immediately_results,
697                    self._activity_should_end_immediately_delay,
698                )
699
700    def _on_player_ready(self, chooser: bascenev1.Chooser) -> None:
701        """Called when a bascenev1.Player has checked themself ready."""
702        lobby = chooser.lobby
703        activity = self._activity_weak()
704
705        # This happens sometimes. That seems like it shouldn't be happening;
706        # when would we have a session and a chooser with players but no
707        # active activity?
708        if activity is None:
709            print('_on_player_ready called with no activity.')
710            return
711
712        # In joining-activities, we wait till all choosers are ready
713        # and then create all players at once.
714        if activity.is_joining_activity:
715            if not lobby.check_all_ready():
716                return
717            choosers = lobby.get_choosers()
718            min_players = self.min_players
719            if len(choosers) >= min_players:
720                for lch in lobby.get_choosers():
721                    self._add_chosen_player(lch)
722                lobby.remove_all_choosers()
723
724                # Get our next activity going.
725                self._complete_end_activity(activity, {})
726            else:
727                _bascenev1.broadcastmessage(
728                    babase.Lstr(
729                        resource='notEnoughPlayersText',
730                        subs=[('${COUNT}', str(min_players))],
731                    ),
732                    color=(1, 1, 0),
733                )
734                _bascenev1.getsound('error').play()
735
736        # Otherwise just add players on the fly.
737        else:
738            self._add_chosen_player(chooser)
739            lobby.remove_chooser(chooser.getplayer())
740
741    def transitioning_out_activity_was_freed(
742        self, can_show_ad_on_death: bool
743    ) -> None:
744        """(internal)"""
745        # pylint: disable=cyclic-import
746
747        # Since things should be generally still right now, it's a good time
748        # to run garbage collection to clear out any circular dependency
749        # loops. We keep this disabled normally to avoid non-deterministic
750        # hitches.
751        babase.garbage_collect()
752
753        assert babase.app.classic is not None
754        with self.context:
755            if can_show_ad_on_death:
756                babase.app.classic.ads.call_after_ad(self.begin_next_activity)
757            else:
758                babase.pushcall(self.begin_next_activity)
759
760    def _add_chosen_player(
761        self, chooser: bascenev1.Chooser
762    ) -> bascenev1.SessionPlayer:
763        from bascenev1._team import SessionTeam
764
765        sessionplayer = chooser.getplayer()
766        assert sessionplayer in self.sessionplayers, (
767            'SessionPlayer not found in session '
768            'player-list after chooser selection.'
769        )
770
771        activity = self._activity_weak()
772        assert activity is not None
773
774        # Reset the player's input here, as it is probably
775        # referencing the chooser which could inadvertently keep it alive.
776        sessionplayer.resetinput()
777
778        # We can pass it to the current activity if it has already begun
779        # (otherwise it'll get passed once begin is called).
780        pass_to_activity = (
781            activity.has_begun() and not activity.is_joining_activity
782        )
783
784        # However, if we're not allowing mid-game joins, don't actually pass;
785        # just announce the arrival and say they'll partake next round.
786        if pass_to_activity:
787            if not (
788                activity.allow_mid_activity_joins
789                and self.should_allow_mid_activity_joins(activity)
790            ):
791                pass_to_activity = False
792                with self.context:
793                    _bascenev1.broadcastmessage(
794                        babase.Lstr(
795                            resource='playerDelayedJoinText',
796                            subs=[
797                                ('${PLAYER}', sessionplayer.getname(full=True))
798                            ],
799                        ),
800                        color=(0, 1, 0),
801                    )
802
803        # If we're a non-team session, each player gets their own team.
804        # (keeps mini-game coding simpler if we can always deal with teams).
805        if self.use_teams:
806            sessionteam = chooser.sessionteam
807        else:
808            our_team_id = self._next_team_id
809            self._next_team_id += 1
810            sessionteam = SessionTeam(
811                team_id=our_team_id,
812                color=chooser.get_color(),
813                name=chooser.getplayer().getname(full=True, icon=False),
814            )
815
816            # Add player's team to the Session.
817            self.sessionteams.append(sessionteam)
818
819            with self.context:
820                try:
821                    self.on_team_join(sessionteam)
822                except Exception:
823                    logging.exception('Error in on_team_join for %s.', self)
824
825            # Add player's team to the Activity.
826            if pass_to_activity:
827                activity.add_team(sessionteam)
828
829        assert sessionplayer not in sessionteam.players
830        sessionteam.players.append(sessionplayer)
831        sessionplayer.setdata(
832            team=sessionteam,
833            character=chooser.get_character_name(),
834            color=chooser.get_color(),
835            highlight=chooser.get_highlight(),
836        )
837
838        self.stats.register_sessionplayer(sessionplayer)
839        if pass_to_activity:
840            activity.add_player(sessionplayer)
841        return sessionplayer
842
843    def _remove_player_from_waitlist(self, identifier: str) -> None:
844        try:
845            self._players_on_wait.pop(identifier)
846        except KeyError:
847            pass

Defines a high level series of bascenev1.Activity-es.

Examples of sessions are bascenev1.FreeForAllSession, bascenev1.DualTeamSession, and bascenev1.CoopSession.

A Session is responsible for wrangling and transitioning between various bascenev1.Activity instances such as mini-games and score-screens, and for maintaining state between them (players, teams, score tallies, etc).

Session( depsets: Sequence[DependencySet], *, team_names: Optional[Sequence[str]] = None, team_colors: Optional[Sequence[Sequence[float]]] = None, min_players: int = 1, max_players: int = 8, submit_score: bool = True)
 95    def __init__(
 96        self,
 97        depsets: Sequence[bascenev1.DependencySet],
 98        *,
 99        team_names: Sequence[str] | None = None,
100        team_colors: Sequence[Sequence[float]] | None = None,
101        min_players: int = 1,
102        max_players: int = 8,
103        submit_score: bool = True,
104    ):
105        """Instantiate a session.
106
107        depsets should be a sequence of successfully resolved
108        bascenev1.DependencySet instances; one for each bascenev1.Activity
109        the session may potentially run.
110        """
111        # pylint: disable=too-many-statements
112        # pylint: disable=too-many-locals
113        # pylint: disable=cyclic-import
114        # pylint: disable=too-many-branches
115        from efro.util import empty_weakref
116        from bascenev1._dependency import (
117            Dependency,
118            AssetPackage,
119            DependencyError,
120        )
121        from bascenev1._lobby import Lobby
122        from bascenev1._stats import Stats
123        from bascenev1._gameactivity import GameActivity
124        from bascenev1._activity import Activity
125        from bascenev1._team import SessionTeam
126
127        # First off, resolve all dependency-sets we were passed.
128        # If things are missing, we'll try to gather them into a single
129        # missing-deps exception if possible to give the caller a clean
130        # path to download missing stuff and try again.
131        missing_asset_packages: set[str] = set()
132        for depset in depsets:
133            try:
134                depset.resolve()
135            except DependencyError as exc:
136                # Gather/report missing assets only; barf on anything else.
137                if all(issubclass(d.cls, AssetPackage) for d in exc.deps):
138                    for dep in exc.deps:
139                        assert isinstance(dep.config, str)
140                        missing_asset_packages.add(dep.config)
141                else:
142                    missing_info = [(d.cls, d.config) for d in exc.deps]
143                    raise RuntimeError(
144                        f'Missing non-asset dependencies: {missing_info}'
145                    ) from exc
146
147        # Throw a combined exception if we found anything missing.
148        if missing_asset_packages:
149            raise DependencyError(
150                [
151                    Dependency(AssetPackage, set_id)
152                    for set_id in missing_asset_packages
153                ]
154            )
155
156        # Ok; looks like our dependencies check out.
157        # Now give the engine a list of asset-set-ids to pass along to clients.
158        required_asset_packages: set[str] = set()
159        for depset in depsets:
160            required_asset_packages.update(depset.get_asset_package_ids())
161
162        # print('Would set host-session asset-reqs to:',
163        # required_asset_packages)
164
165        # Init our C++ layer data.
166        self._sessiondata = _bascenev1.register_session(self)
167
168        # Should remove this if possible.
169        self.tournament_id: str | None = None
170
171        self.sessionteams = []
172        self.sessionplayers = []
173        self.min_players = min_players
174        self.max_players = (
175            max_players
176            if _max_players_override is None
177            else _max_players_override
178        )
179        self.submit_score = submit_score
180
181        self.customdata = {}
182        self._in_set_activity = False
183        self._next_team_id = 0
184        self._activity_retained: bascenev1.Activity | None = None
185        self._launch_end_session_activity_time: float | None = None
186        self._activity_end_timer: bascenev1.BaseTimer | None = None
187        self._activity_weak = empty_weakref(Activity)
188        self._next_activity: bascenev1.Activity | None = None
189        self._wants_to_end = False
190        self._ending = False
191        self._activity_should_end_immediately = False
192        self._activity_should_end_immediately_results: (
193            bascenev1.GameResults | None
194        ) = None
195        self._activity_should_end_immediately_delay = 0.0
196
197        # Create static teams if we're using them.
198        if self.use_teams:
199            if team_names is None:
200                raise RuntimeError(
201                    'use_teams is True but team_names not provided.'
202                )
203            if team_colors is None:
204                raise RuntimeError(
205                    'use_teams is True but team_colors not provided.'
206                )
207            if len(team_colors) != len(team_names):
208                raise RuntimeError(
209                    f'Got {len(team_names)} team_names'
210                    f' and {len(team_colors)} team_colors;'
211                    f' these numbers must match.'
212                )
213            for i, color in enumerate(team_colors):
214                team = SessionTeam(
215                    team_id=self._next_team_id,
216                    name=GameActivity.get_team_display_string(team_names[i]),
217                    color=color,
218                )
219                self.sessionteams.append(team)
220                self._next_team_id += 1
221                try:
222                    with self.context:
223                        self.on_team_join(team)
224                except Exception:
225                    logging.exception('Error in on_team_join for %s.', self)
226
227        self.lobby = Lobby()
228        self.stats = Stats()
229
230        # Instantiate our session globals node which will apply its settings.
231        self._sessionglobalsnode = _bascenev1.newnode('sessionglobals')
232
233        # Rejoin cooldown stuff.
234        self._players_on_wait: dict = {}
235        self._player_requested_identifiers: dict = {}
236        self._waitlist_timers: dict = {}

Instantiate a session.

depsets should be a sequence of successfully resolved bascenev1.DependencySet instances; one for each bascenev1.Activity the session may potentially run.

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.

lobby: Lobby

The baclassic.Lobby instance where new bascenev1.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[_bascenev1.SessionPlayer]

All bascenev1.SessionPlayers in the Session. Most things should use the list of bascenev1.Player-s in bascenev1.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[SessionTeam]

All the bascenev1.SessionTeams in the Session. Most things should use the list of bascenev1.Team-s in bascenev1.Activity; not this.

tournament_id: str | None
submit_score
stats
context: _babase.ContextRef
238    @property
239    def context(self) -> bascenev1.ContextRef:
240        """A context-ref pointing at this activity."""
241        return self._sessiondata.context()

A context-ref pointing at this activity.

sessionglobalsnode: _bascenev1.Node
243    @property
244    def sessionglobalsnode(self) -> bascenev1.Node:
245        """The sessionglobals bascenev1.Node for the session."""
246        node = self._sessionglobalsnode
247        if not node:
248            raise babase.NodeNotFoundError()
249        return node

The sessionglobals bascenev1.Node for the session.

def should_allow_mid_activity_joins(self, activity: Activity) -> bool:
251    def should_allow_mid_activity_joins(
252        self, activity: bascenev1.Activity
253    ) -> bool:
254        """Ask ourself if we should allow joins during an Activity.
255
256        Note that for a join to be allowed, both the Session and Activity
257        have to be ok with it (via this function and the
258        Activity.allow_mid_activity_joins property.
259        """
260        del activity  # Unused.
261        return True

Ask ourself if we should allow joins during an Activity.

Note that for a join to be allowed, both the Session and Activity have to be ok with it (via this function and the Activity.allow_mid_activity_joins property.

def on_player_request(self, player: _bascenev1.SessionPlayer) -> bool:
263    def on_player_request(self, player: bascenev1.SessionPlayer) -> bool:
264        """Called when a new bascenev1.Player wants to join the Session.
265
266        This should return True or False to accept/reject.
267        """
268        # Limit player counts *unless* we're in a stress test.
269        if (
270            babase.app.classic is not None
271            and babase.app.classic.stress_test_update_timer is None
272        ):
273            if len(self.sessionplayers) >= self.max_players >= 0:
274                # Print a rejection message *only* to the client trying to
275                # join (prevents spamming everyone else in the game).
276                _bascenev1.getsound('error').play()
277                _bascenev1.broadcastmessage(
278                    babase.Lstr(
279                        resource='playerLimitReachedText',
280                        subs=[('${COUNT}', str(self.max_players))],
281                    ),
282                    color=(0.8, 0.0, 0.0),
283                    clients=[player.inputdevice.client_id],
284                    transient=True,
285                )
286                return False
287
288        # Rejoin cooldown.
289        identifier = player.get_v1_account_id()
290        if identifier:
291            leave_time = self._players_on_wait.get(identifier)
292            if leave_time:
293                diff = str(
294                    math.ceil(
295                        _g_player_rejoin_cooldown
296                        - babase.apptime()
297                        + leave_time
298                    )
299                )
300                _bascenev1.broadcastmessage(
301                    babase.Lstr(
302                        translate=(
303                            'serverResponses',
304                            'You can join in ${COUNT} seconds.',
305                        ),
306                        subs=[('${COUNT}', diff)],
307                    ),
308                    color=(1, 1, 0),
309                    clients=[player.inputdevice.client_id],
310                    transient=True,
311                )
312                return False
313            self._player_requested_identifiers[player.id] = identifier
314
315        _bascenev1.getsound('dripity').play()
316        return True

Called when a new bascenev1.Player wants to join the Session.

This should return True or False to accept/reject.

def on_player_leave(self, sessionplayer: _bascenev1.SessionPlayer) -> None:
318    def on_player_leave(self, sessionplayer: bascenev1.SessionPlayer) -> None:
319        """Called when a previously-accepted bascenev1.SessionPlayer leaves."""
320
321        if sessionplayer not in self.sessionplayers:
322            print(
323                'ERROR: Session.on_player_leave called'
324                ' for player not in our list.'
325            )
326            return
327
328        _bascenev1.getsound('playerLeft').play()
329
330        activity = self._activity_weak()
331
332        # Rejoin cooldown.
333        identifier = self._player_requested_identifiers.get(sessionplayer.id)
334        if identifier:
335            self._players_on_wait[identifier] = babase.apptime()
336            with babase.ContextRef.empty():
337                self._waitlist_timers[identifier] = babase.AppTimer(
338                    _g_player_rejoin_cooldown,
339                    babase.Call(self._remove_player_from_waitlist, identifier),
340                )
341
342        if not sessionplayer.in_game:
343            # Ok, the player is still in the lobby; simply remove them.
344            with self.context:
345                try:
346                    self.lobby.remove_chooser(sessionplayer)
347                except Exception:
348                    logging.exception('Error in Lobby.remove_chooser().')
349        else:
350            # Ok, they've already entered the game. Remove them from
351            # teams/activities/etc.
352            sessionteam = sessionplayer.sessionteam
353            assert sessionteam is not None
354
355            _bascenev1.broadcastmessage(
356                babase.Lstr(
357                    resource='playerLeftText',
358                    subs=[('${PLAYER}', sessionplayer.getname(full=True))],
359                )
360            )
361
362            # Remove them from their SessionTeam.
363            if sessionplayer in sessionteam.players:
364                sessionteam.players.remove(sessionplayer)
365            else:
366                print(
367                    'SessionPlayer not found in SessionTeam'
368                    ' in on_player_leave.'
369                )
370
371            # Grab their activity-specific player instance.
372            player = sessionplayer.activityplayer
373            assert isinstance(player, (Player, type(None)))
374
375            # Remove them from any current Activity.
376            if player is not None and activity is not None:
377                if player in activity.players:
378                    activity.remove_player(sessionplayer)
379                else:
380                    print('Player not found in Activity in on_player_leave.')
381
382            # If we're a non-team session, remove their team too.
383            if not self.use_teams:
384                self._remove_player_team(sessionteam, activity)
385
386        # Now remove them from the session list.
387        self.sessionplayers.remove(sessionplayer)

Called when a previously-accepted bascenev1.SessionPlayer leaves.

def end(self) -> None:
427    def end(self) -> None:
428        """Initiates an end to the session and a return to the main menu.
429
430        Note that this happens asynchronously, allowing the
431        session and its activities to shut down gracefully.
432        """
433        self._wants_to_end = True
434        if self._next_activity is None:
435            self._launch_end_session_activity()

Initiates an end to the session and a return to the main menu.

Note that this happens asynchronously, allowing the session and its activities to shut down gracefully.

def on_team_join(self, team: SessionTeam) -> None:
458    def on_team_join(self, team: bascenev1.SessionTeam) -> None:
459        """Called when a new bascenev1.Team joins the session."""

Called when a new bascenev1.Team joins the session.

def on_team_leave(self, team: SessionTeam) -> None:
461    def on_team_leave(self, team: bascenev1.SessionTeam) -> None:
462        """Called when a bascenev1.Team is leaving the session."""

Called when a bascenev1.Team is leaving the session.

def end_activity( self, activity: Activity, results: Any, delay: float, force: bool) -> None:
464    def end_activity(
465        self,
466        activity: bascenev1.Activity,
467        results: Any,
468        delay: float,
469        force: bool,
470    ) -> None:
471        """Commence shutdown of a bascenev1.Activity (if not already occurring).
472
473        'delay' is the time delay before the Activity actually ends
474        (in seconds). Further calls to end() will be ignored up until
475        this time, unless 'force' is True, in which case the new results
476        will replace the old.
477        """
478        # Only pay attention if this is coming from our current activity.
479        if activity is not self._activity_retained:
480            return
481
482        # If this activity hasn't begun yet, just set it up to end immediately
483        # once it does.
484        if not activity.has_begun():
485            # activity.set_immediate_end(results, delay, force)
486            if not self._activity_should_end_immediately or force:
487                self._activity_should_end_immediately = True
488                self._activity_should_end_immediately_results = results
489                self._activity_should_end_immediately_delay = delay
490
491        # The activity has already begun; get ready to end it.
492        else:
493            if (not activity.has_ended()) or force:
494                activity.set_has_ended(True)
495
496                # Set a timer to set in motion this activity's demise.
497                self._activity_end_timer = _bascenev1.BaseTimer(
498                    delay,
499                    babase.Call(self._complete_end_activity, activity, results),
500                )

Commence shutdown of a bascenev1.Activity (if not already occurring).

'delay' is the time delay before the Activity actually ends (in seconds). Further calls to end() will be ignored up until this time, unless 'force' is True, in which case the new results will replace the old.

def handlemessage(self, msg: Any) -> Any:
502    def handlemessage(self, msg: Any) -> Any:
503        """General message handling; can be passed any message object."""
504        from bascenev1._lobby import PlayerReadyMessage
505        from bascenev1._messages import PlayerProfilesChangedMessage, UNHANDLED
506
507        if isinstance(msg, PlayerReadyMessage):
508            self._on_player_ready(msg.chooser)
509
510        elif isinstance(msg, PlayerProfilesChangedMessage):
511            # If we have a current activity with a lobby, ask it to reload
512            # profiles.
513            with self.context:
514                self.lobby.reload_profiles()
515            return None
516
517        else:
518            return UNHANDLED
519        return None

General message handling; can be passed any message object.

def setactivity(self, activity: Activity) -> None:
531    def setactivity(self, activity: bascenev1.Activity) -> None:
532        """Assign a new current bascenev1.Activity for the session.
533
534        Note that this will not change the current context to the new
535        Activity's. Code must be run in the new activity's methods
536        (on_transition_in, etc) to get it. (so you can't do
537        session.setactivity(foo) and then bascenev1.newnode() to add a node
538        to foo)
539        """
540
541        # Make sure we don't get called recursively.
542        _rlock = self._SetActivityScopedLock(self)
543
544        if activity.session is not _bascenev1.getsession():
545            raise RuntimeError("Provided Activity's Session is not current.")
546
547        # Quietly ignore this if the whole session is going down.
548        if self._ending:
549            return
550
551        if activity is self._activity_retained:
552            logging.error('Activity set to already-current activity.')
553            return
554
555        if self._next_activity is not None:
556            raise RuntimeError(
557                'Activity switch already in progress (to '
558                + str(self._next_activity)
559                + ')'
560            )
561
562        prev_activity = self._activity_retained
563        prev_globals = (
564            prev_activity.globalsnode if prev_activity is not None else None
565        )
566
567        # Let the activity do its thing.
568        activity.transition_in(prev_globals)
569
570        self._next_activity = activity
571
572        # If we have a current activity, tell it it's transitioning out;
573        # the next one will become current once this one dies.
574        if prev_activity is not None:
575            prev_activity.transition_out()
576
577            # Setting this to None should free up the old activity to die,
578            # which will call begin_next_activity.
579            # We can still access our old activity through
580            # self._activity_weak() to keep it up to date on player
581            # joins/departures/etc until it dies.
582            self._activity_retained = None
583
584        # There's no existing activity; lets just go ahead with the begin call.
585        else:
586            self.begin_next_activity()
587
588        # We want to call destroy() for the previous activity once it should
589        # tear itself down, clear out any self-refs, etc. After this call
590        # the activity should have no refs left to it and should die (which
591        # will trigger the next activity to run).
592        if prev_activity is not None:
593            with babase.ContextRef.empty():
594                babase.apptimer(
595                    max(0.0, activity.transition_time), prev_activity.expire
596                )
597        self._in_set_activity = False

Assign a new current bascenev1.Activity for the session.

Note that this will not change the current context to the new Activity's. Code must be run in the new activity's methods (on_transition_in, etc) to get it. (so you can't do session.setactivity(foo) and then bascenev1.newnode() to add a node to foo)

def getactivity(self) -> Activity | None:
599    def getactivity(self) -> bascenev1.Activity | None:
600        """Return the current foreground activity for this session."""
601        return self._activity_weak()

Return the current foreground activity for this session.

def get_custom_menu_entries(self) -> list[dict[str, typing.Any]]:
603    def get_custom_menu_entries(self) -> list[dict[str, Any]]:
604        """Subclasses can override this to provide custom menu entries.
605
606        The returned value should be a list of dicts, each containing
607        a 'label' and 'call' entry, with 'label' being the text for
608        the entry and 'call' being the callable to trigger if the entry
609        is pressed.
610        """
611        return []

Subclasses can override this to provide custom menu entries.

The returned value should be a list of dicts, each containing a 'label' and 'call' entry, with 'label' being the text for the entry and 'call' being the callable to trigger if the entry is pressed.

def on_activity_end(self, activity: Activity, results: Any) -> None:
655    def on_activity_end(
656        self, activity: bascenev1.Activity, results: Any
657    ) -> None:
658        """Called when the current bascenev1.Activity has ended.
659
660        The bascenev1.Session should look at the results and start
661        another bascenev1.Activity.
662        """

Called when the current bascenev1.Activity has ended.

The bascenev1.Session should look at the results and start another bascenev1.Activity.

def begin_next_activity(self) -> None:
664    def begin_next_activity(self) -> None:
665        """Called once the previous activity has been totally torn down.
666
667        This means we're ready to begin the next one
668        """
669        if self._next_activity is None:
670            # Should this ever happen?
671            logging.error('begin_next_activity() called with no _next_activity')
672            return
673
674        # We store both a weak and a strong ref to the new activity;
675        # the strong is to keep it alive and the weak is so we can access
676        # it even after we've released the strong-ref to allow it to die.
677        self._activity_retained = self._next_activity
678        self._activity_weak = weakref.ref(self._next_activity)
679        self._next_activity = None
680        self._activity_should_end_immediately = False
681
682        # Kick out anyone loitering in the lobby.
683        self.lobby.remove_all_choosers_and_kick_players()
684
685        # Kick off the activity.
686        self._activity_retained.begin(self)
687
688        # If we want to completely end the session, we can now kick that off.
689        if self._wants_to_end:
690            self._launch_end_session_activity()
691        else:
692            # Otherwise, if the activity has already been told to end,
693            # do so now.
694            if self._activity_should_end_immediately:
695                self._activity_retained.end(
696                    self._activity_should_end_immediately_results,
697                    self._activity_should_end_immediately_delay,
698                )

Called once the previous activity has been totally torn down.

This means we're ready to begin the next one

class SessionPlayer:
722class SessionPlayer:
723    """A reference to a player in the :class:`~bascenev1.Session`.
724
725    These are created and managed internally and provided to your
726    :class:`~bascenev1.Session`/:class:`~bascenev1.Activity`
727    instances. Be aware that, like :class:`~bascenev1.Node` objects,
728    :class:`~bascenev1.SessionPlayer` objects are effectively 'weak'
729    references under-the-hood; a player can leave the game at any point.
730    For this reason, you should make judicious use of the
731    :meth:`bascenev1.SessionPlayer.exists()` method (or boolean operator) to
732    ensure that a :class:`SessionPlayer` is still present if retaining
733    references to one for any length of time.
734    """
735
736    id: int
737    """The unique numeric ID of the Player.
738
739       Note that you can also use the boolean operator for this same
740       functionality, so a statement such as "if player" will do
741       the right thing both for Player objects and values of None."""
742
743    in_game: bool
744    """This bool value will be True once the Player has completed
745       any lobby character/team selection."""
746
747    sessionteam: bascenev1.SessionTeam
748    """The bascenev1.SessionTeam this Player is on. If the
749       SessionPlayer is still in its lobby selecting a team/etc.
750       then a bascenev1.SessionTeamNotFoundError will be raised."""
751
752    inputdevice: bascenev1.InputDevice
753    """The input device associated with the player."""
754
755    color: Sequence[float]
756    """The base color for this Player.
757       In team games this will match the bascenev1.SessionTeam's
758       color."""
759
760    highlight: Sequence[float]
761    """A secondary color for this player.
762       This is used for minor highlights and accents
763       to allow a player to stand apart from his teammates
764       who may all share the same team (primary) color."""
765
766    character: str
767    """The character this player has selected in their profile."""
768
769    activityplayer: bascenev1.Player | None
770    """The current game-specific instance for this player."""
771
772    def __bool__(self) -> bool:
773        """Support for bool evaluation."""
774        return bool(True)  # Slight obfuscation.
775
776    def assigninput(
777        self,
778        type: bascenev1.InputType | tuple[bascenev1.InputType, ...],
779        call: Callable,
780    ) -> None:
781        """Set the python callable to be run for one or more types of input."""
782        return None
783
784    def exists(self) -> bool:
785        """Return whether the underlying player is still in the game."""
786        return bool()
787
788    def get_icon(self) -> dict[str, Any]:
789        """Returns the character's icon (images, colors, etc contained
790        in a dict.
791        """
792        return {'foo': 'bar'}
793
794    def get_icon_info(self) -> dict[str, Any]:
795        """(internal)"""
796        return {'foo': 'bar'}
797
798    def get_v1_account_id(self) -> str:
799        """Return the V1 Account ID this player is signed in under, if
800        there is one and it can be determined with relative certainty.
801        Returns None otherwise. Note that this may require an active
802        internet connection (especially for network-connected players)
803        and may return None for a short while after a player initially
804        joins (while verification occurs).
805        """
806        return str()
807
808    def getname(self, full: bool = False, icon: bool = True) -> str:
809        """Returns the player's name. If icon is True, the long version of the
810        name may include an icon.
811        """
812        return str()
813
814    def remove_from_game(self) -> None:
815        """Removes the player from the game."""
816        return None
817
818    def resetinput(self) -> None:
819        """Clears out the player's assigned input actions."""
820        return None
821
822    def set_icon_info(
823        self,
824        texture: str,
825        tint_texture: str,
826        tint_color: Sequence[float],
827        tint2_color: Sequence[float],
828    ) -> None:
829        """(internal)"""
830        return None
831
832    def setactivity(self, activity: bascenev1.Activity | None) -> None:
833        """(internal)"""
834        return None
835
836    def setdata(
837        self,
838        team: bascenev1.SessionTeam,
839        character: str,
840        color: Sequence[float],
841        highlight: Sequence[float],
842    ) -> None:
843        """(internal)"""
844        return None
845
846    def setname(
847        self, name: str, full_name: str | None = None, real: bool = True
848    ) -> None:
849        """Set the player's name to the provided string.
850        A number will automatically be appended if the name is not unique from
851        other players.
852        """
853        return None
854
855    def setnode(self, node: bascenev1.Node | None) -> None:
856        """(internal)"""
857        return None

A reference to a player in the ~bascenev1.Session.

These are created and managed internally and provided to your ~bascenev1.Session/~bascenev1.Activity instances. Be aware that, like ~bascenev1.Node objects, ~bascenev1.SessionPlayer objects are effectively 'weak' references under-the-hood; a player can leave the game at any point. For this reason, you should make judicious use of the bascenev1.SessionPlayer.exists()() method (or boolean operator) to ensure that a SessionPlayer is still present if retaining references to one for any length of time.

id: int

The unique numeric ID of the Player.

Note that you can also use the boolean operator for this same functionality, so a statement such as "if player" will do the right thing both for Player objects and values of None.

in_game: bool

This bool value will be True once the Player has completed any lobby character/team selection.

sessionteam: SessionTeam

The bascenev1.SessionTeam this Player is on. If the SessionPlayer is still in its lobby selecting a team/etc. then a bascenev1.SessionTeamNotFoundError will be raised.

inputdevice: _bascenev1.InputDevice

The input device associated with the player.

color: Sequence[float]

The base color for this Player. In team games this will match the bascenev1.SessionTeam's color.

highlight: Sequence[float]

A secondary color for this player. This is used for minor highlights and accents to allow a player to stand apart from his teammates who may all share the same team (primary) color.

character: str

The character this player has selected in their profile.

activityplayer: Player | None

The current game-specific instance for this player.

def assigninput( self, type: InputType | tuple[InputType, ...], call: Callable) -> None:
776    def assigninput(
777        self,
778        type: bascenev1.InputType | tuple[bascenev1.InputType, ...],
779        call: Callable,
780    ) -> None:
781        """Set the python callable to be run for one or more types of input."""
782        return None

Set the python callable to be run for one or more types of input.

def exists(self) -> bool:
784    def exists(self) -> bool:
785        """Return whether the underlying player is still in the game."""
786        return bool()

Return whether the underlying player is still in the game.

def get_icon(self) -> dict[str, typing.Any]:
788    def get_icon(self) -> dict[str, Any]:
789        """Returns the character's icon (images, colors, etc contained
790        in a dict.
791        """
792        return {'foo': 'bar'}

Returns the character's icon (images, colors, etc contained in a dict.

def get_v1_account_id(self) -> str:
798    def get_v1_account_id(self) -> str:
799        """Return the V1 Account ID this player is signed in under, if
800        there is one and it can be determined with relative certainty.
801        Returns None otherwise. Note that this may require an active
802        internet connection (especially for network-connected players)
803        and may return None for a short while after a player initially
804        joins (while verification occurs).
805        """
806        return str()

Return the V1 Account ID this player is signed in under, if there is one and it can be determined with relative certainty. Returns None otherwise. Note that this may require an active internet connection (especially for network-connected players) and may return None for a short while after a player initially joins (while verification occurs).

def getname(self, full: bool = False, icon: bool = True) -> str:
808    def getname(self, full: bool = False, icon: bool = True) -> str:
809        """Returns the player's name. If icon is True, the long version of the
810        name may include an icon.
811        """
812        return str()

Returns the player's name. If icon is True, the long version of the name may include an icon.

def remove_from_game(self) -> None:
814    def remove_from_game(self) -> None:
815        """Removes the player from the game."""
816        return None

Removes the player from the game.

def resetinput(self) -> None:
818    def resetinput(self) -> None:
819        """Clears out the player's assigned input actions."""
820        return None

Clears out the player's assigned input actions.

def setname(self, name: str, full_name: str | None = None, real: bool = True) -> None:
846    def setname(
847        self, name: str, full_name: str | None = None, real: bool = True
848    ) -> None:
849        """Set the player's name to the provided string.
850        A number will automatically be appended if the name is not unique from
851        other players.
852        """
853        return None

Set the player's name to the provided string. A number will automatically be appended if the name is not unique from other players.

class SessionTeam:
20class SessionTeam:
21    """A team of one or more bascenev1.SessionPlayers.
22
23    Note that a SessionPlayer *always* has a SessionTeam; in some cases,
24    such as free-for-all bascenev1.Sessions, each SessionTeam consists
25    of just one SessionPlayer.
26    """
27
28    # Annotate our attr types at the class level so they're introspectable.
29
30    name: babase.Lstr | str
31    """The team's name."""
32
33    color: tuple[float, ...]  # FIXME: can't we make this fixed len?
34    """The team's color."""
35
36    players: list[bascenev1.SessionPlayer]
37    """The list of bascenev1.SessionPlayer-s on the team."""
38
39    customdata: dict
40    """A dict for use by the current bascenev1.Session for
41       storing data associated with this team.
42       Unlike customdata, this persists for the duration
43       of the session."""
44
45    id: int
46    """The unique numeric id of the team."""
47
48    def __init__(
49        self,
50        team_id: int = 0,
51        name: babase.Lstr | str = '',
52        color: Sequence[float] = (1.0, 1.0, 1.0),
53    ):
54        """Instantiate a bascenev1.SessionTeam.
55
56        In most cases, all teams are provided to you by the bascenev1.Session,
57        bascenev1.Session, so calling this shouldn't be necessary.
58        """
59
60        self.id = team_id
61        self.name = name
62        self.color = tuple(color)
63        self.players = []
64        self.customdata = {}
65        self.activityteam: Team | None = None
66
67    def leave(self) -> None:
68        """(internal)"""
69        self.customdata = {}

A team of one or more bascenev1.SessionPlayers.

Note that a SessionPlayer always has a SessionTeam; in some cases, such as free-for-all bascenev1.Sessions, each SessionTeam consists of just one SessionPlayer.

SessionTeam( team_id: int = 0, name: Lstr | str = '', color: Sequence[float] = (1.0, 1.0, 1.0))
48    def __init__(
49        self,
50        team_id: int = 0,
51        name: babase.Lstr | str = '',
52        color: Sequence[float] = (1.0, 1.0, 1.0),
53    ):
54        """Instantiate a bascenev1.SessionTeam.
55
56        In most cases, all teams are provided to you by the bascenev1.Session,
57        bascenev1.Session, so calling this shouldn't be necessary.
58        """
59
60        self.id = team_id
61        self.name = name
62        self.color = tuple(color)
63        self.players = []
64        self.customdata = {}
65        self.activityteam: Team | None = None

Instantiate a bascenev1.SessionTeam.

In most cases, all teams are provided to you by the bascenev1.Session, bascenev1.Session, so calling this shouldn't be necessary.

name: Lstr | str

The team's name.

color: tuple[float, ...]

The team's color.

players: list[_bascenev1.SessionPlayer]

The list of bascenev1.SessionPlayer-s on the team.

customdata: dict

A dict for use by the current bascenev1.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.

activityteam: Team | None
def set_analytics_screen(screen: str) -> None:
1464def set_analytics_screen(screen: str) -> None:
1465    """Used for analytics to see where in the app players spend their time.
1466
1467    Generally called when opening a new window or entering some UI.
1468    'screen' should be a string description of an app location
1469    ('Main Menu', etc.)
1470    """
1471    return None

Used for analytics to see where in the app players spend their time.

Generally called when opening a new window or entering some UI. 'screen' should be a string description of an app location ('Main Menu', etc.)

def set_player_rejoin_cooldown(cooldown: float) -> None:
31def set_player_rejoin_cooldown(cooldown: float) -> None:
32    """Set the cooldown for individual players rejoining after leaving."""
33    global _g_player_rejoin_cooldown  # pylint: disable=global-statement
34    _g_player_rejoin_cooldown = max(0.0, cooldown)

Set the cooldown for individual players rejoining after leaving.

def set_max_players_override(max_players: int | None) -> None:
37def set_max_players_override(max_players: int | None) -> None:
38    """Set the override for how many players can join a session"""
39    global _max_players_override  # pylint: disable=global-statement
40    _max_players_override = max_players

Set the override for how many players can join a session

def setmusic( musictype: MusicType | None, continuous: bool = False) -> None:
49def setmusic(musictype: MusicType | None, continuous: bool = False) -> None:
50    """Set the app to play (or stop playing) a certain type of music.
51
52    category: **Gameplay Functions**
53
54    This function will handle loading and playing sound assets as necessary,
55    and also supports custom user soundtracks on specific platforms so the
56    user can override particular game music with their own.
57
58    Pass None to stop music.
59
60    if 'continuous' is True and musictype is the same as what is already
61    playing, the playing track will not be restarted.
62    """
63
64    # All we do here now is set a few music attrs on the current globals
65    # node. The foreground globals' current playing music then gets fed to
66    # the do_play_music call in our music controller. This way we can
67    # seamlessly support custom soundtracks in replays/etc since we're being
68    # driven purely by node data.
69    gnode = _bascenev1.getactivity().globalsnode
70    gnode.music_continuous = continuous
71    gnode.music = '' if musictype is None else musictype.value
72    gnode.music_count += 1

Set the app to play (or stop playing) a certain type of music.

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 user can override particular game music with their own.

Pass None to stop music.

if 'continuous' is True and musictype is the same as what is already playing, the playing track will not be restarted.

@dataclass
class Setting:
15@dataclass
16class Setting:
17    """Defines a user-controllable setting for a game or other entity."""
18
19    name: str
20    default: Any

Defines a user-controllable setting for a game or other entity.

Setting(name: str, default: Any)
name: str
default: Any
@dataclass
class ShouldShatterMessage:
161@dataclass
162class ShouldShatterMessage:
163    """Tells an object that it should shatter."""

Tells an object that it should shatter.

def show_damage_count( damage: str, position: Sequence[float], direction: Sequence[float]) -> None:
170def show_damage_count(
171    damage: str, position: Sequence[float], direction: Sequence[float]
172) -> None:
173    """Pop up a damage count at a position in space."""
174    lifespan = 1.0
175    app = babase.app
176
177    # FIXME: Should never vary game elements based on local config.
178    #  (connected clients may have differing configs so they won't
179    #  get the intended results).
180    assert app.classic is not None
181    do_big = app.ui_v1.uiscale is babase.UIScale.SMALL or app.env.vr
182    txtnode = _bascenev1.newnode(
183        'text',
184        attrs={
185            'text': damage,
186            'in_world': True,
187            'h_align': 'center',
188            'flatness': 1.0,
189            'shadow': 1.0 if do_big else 0.7,
190            'color': (1, 0.25, 0.25, 1),
191            'scale': 0.015 if do_big else 0.01,
192        },
193    )
194    # Translate upward.
195    tcombine = _bascenev1.newnode('combine', owner=txtnode, attrs={'size': 3})
196    tcombine.connectattr('output', txtnode, 'position')
197    v_vals = []
198    pval = 0.0
199    vval = 0.07
200    count = 6
201    for i in range(count):
202        v_vals.append((float(i) / count, pval))
203        pval += vval
204        vval *= 0.5
205    p_start = position[0]
206    p_dir = direction[0]
207    animate(
208        tcombine,
209        'input0',
210        {i[0] * lifespan: p_start + p_dir * i[1] for i in v_vals},
211    )
212    p_start = position[1]
213    p_dir = direction[1]
214    animate(
215        tcombine,
216        'input1',
217        {i[0] * lifespan: p_start + p_dir * i[1] for i in v_vals},
218    )
219    p_start = position[2]
220    p_dir = direction[2]
221    animate(
222        tcombine,
223        'input2',
224        {i[0] * lifespan: p_start + p_dir * i[1] for i in v_vals},
225    )
226    animate(txtnode, 'opacity', {0.7 * lifespan: 1.0, lifespan: 0.0})
227    _bascenev1.timer(lifespan, txtnode.delete)

Pop up a damage count at a position in space.

class Sound:
860class Sound:
861    """A reference to a sound.
862
863    Use bascenev1.getsound() to instantiate one.
864    """
865
866    def play(
867        self,
868        volume: float = 1.0,
869        position: Sequence[float] | None = None,
870        host_only: bool = False,
871    ) -> None:
872        """Play the sound a single time.
873
874        If position is not provided, the sound will be at a constant volume
875        everywhere. Position should be a float tuple of size 3.
876        """
877        return None

A reference to a sound.

Use bascenev1.getsound() to instantiate one.

def play( self, volume: float = 1.0, position: Optional[Sequence[float]] = None, host_only: bool = False) -> None:
866    def play(
867        self,
868        volume: float = 1.0,
869        position: Sequence[float] | None = None,
870        host_only: bool = False,
871    ) -> None:
872        """Play the sound a single time.
873
874        If position is not provided, the sound will be at a constant volume
875        everywhere. Position should be a float tuple of size 3.
876        """
877        return None

Play the sound a single time.

If position is not provided, the sound will be at a constant volume everywhere. Position should be a float tuple of size 3.

@dataclass
class StandLocation:
34@dataclass
35class StandLocation:
36    """Describes a point in space and an angle to face."""
37
38    position: babase.Vec3
39    angle: float | None = None

Describes a point in space and an angle to face.

StandLocation(position: _babase.Vec3, angle: float | None = None)
position: _babase.Vec3
angle: float | None = None
@dataclass
class StandMessage:
118@dataclass
119class StandMessage:
120    """A message telling an object to move to a position in space.
121
122    Used when teleporting players to home base, etc.
123    """
124
125    position: Sequence[float] = (0.0, 0.0, 0.0)
126    """Where to move to."""
127
128    angle: float = 0.0
129    """The angle to face (in degrees)"""

A message telling an object to move to a position in space.

Used when teleporting players to home base, etc.

StandMessage(position: Sequence[float] = (0.0, 0.0, 0.0), angle: float = 0.0)
position: Sequence[float] = (0.0, 0.0, 0.0)

Where to move to.

angle: float = 0.0

The angle to face (in degrees)

class Stats:
251class Stats:
252    """Manages scores and statistics for a bascenev1.Session."""
253
254    def __init__(self) -> None:
255        self._activity: weakref.ref[bascenev1.Activity] | None = None
256        self._player_records: dict[str, PlayerRecord] = {}
257        self.orchestrahitsound1: bascenev1.Sound | None = None
258        self.orchestrahitsound2: bascenev1.Sound | None = None
259        self.orchestrahitsound3: bascenev1.Sound | None = None
260        self.orchestrahitsound4: bascenev1.Sound | None = None
261
262    def setactivity(self, activity: bascenev1.Activity | None) -> None:
263        """Set the current activity for this instance."""
264
265        self._activity = None if activity is None else weakref.ref(activity)
266
267        # Load our media into this activity's context.
268        if activity is not None:
269            if activity.expired:
270                logging.exception('Unexpected finalized activity.')
271            else:
272                with activity.context:
273                    self._load_activity_media()
274
275    def getactivity(self) -> bascenev1.Activity | None:
276        """Get the activity associated with this instance.
277
278        May return None.
279        """
280        if self._activity is None:
281            return None
282        return self._activity()
283
284    def _load_activity_media(self) -> None:
285        self.orchestrahitsound1 = _bascenev1.getsound('orchestraHit')
286        self.orchestrahitsound2 = _bascenev1.getsound('orchestraHit2')
287        self.orchestrahitsound3 = _bascenev1.getsound('orchestraHit3')
288        self.orchestrahitsound4 = _bascenev1.getsound('orchestraHit4')
289
290    def reset(self) -> None:
291        """Reset the stats instance completely."""
292
293        # Just to be safe, lets make sure no multi-kill timers are gonna go off
294        # for no-longer-on-the-list players.
295        for p_entry in list(self._player_records.values()):
296            p_entry.cancel_multi_kill_timer()
297        self._player_records = {}
298
299    def reset_accum(self) -> None:
300        """Reset per-sound sub-scores."""
301        for s_player in list(self._player_records.values()):
302            s_player.cancel_multi_kill_timer()
303            s_player.accumscore = 0
304            s_player.accum_kill_count = 0
305            s_player.accum_killed_count = 0
306            s_player.streak = 0
307
308    def register_sessionplayer(self, player: bascenev1.SessionPlayer) -> None:
309        """Register a bascenev1.SessionPlayer with this score-set."""
310        assert player.exists()  # Invalid refs should never be passed to funcs.
311        name = player.getname()
312        if name in self._player_records:
313            # If the player already exists, update his character and such as
314            # it may have changed.
315            self._player_records[name].associate_with_sessionplayer(player)
316        else:
317            name_full = player.getname(full=True)
318            self._player_records[name] = PlayerRecord(
319                name, name_full, player, self
320            )
321
322    def get_records(self) -> dict[str, bascenev1.PlayerRecord]:
323        """Get PlayerRecord corresponding to still-existing players."""
324        records = {}
325
326        # Go through our player records and return ones whose player id still
327        # corresponds to a player with that name.
328        for record_id, record in self._player_records.items():
329            lastplayer = record.get_last_sessionplayer()
330            if lastplayer and lastplayer.getname() == record_id:
331                records[record_id] = record
332        return records
333
334    def player_scored(
335        self,
336        player: bascenev1.Player,
337        base_points: int = 1,
338        *,
339        target: Sequence[float] | None = None,
340        kill: bool = False,
341        victim_player: bascenev1.Player | None = None,
342        scale: float = 1.0,
343        color: Sequence[float] | None = None,
344        title: str | babase.Lstr | None = None,
345        screenmessage: bool = True,
346        display: bool = True,
347        importance: int = 1,
348        showpoints: bool = True,
349        big_message: bool = False,
350    ) -> int:
351        """Register a score for the player.
352
353        Return value is actual score with multipliers and such factored in.
354        """
355        # FIXME: Tidy this up.
356        # pylint: disable=cyclic-import
357        # pylint: disable=too-many-branches
358        # pylint: disable=too-many-locals
359        from bascenev1lib.actor.popuptext import PopupText
360
361        from bascenev1._gameactivity import GameActivity
362
363        del victim_player  # Currently unused.
364        name = player.getname()
365        s_player = self._player_records[name]
366
367        if kill:
368            s_player.submit_kill(showpoints=showpoints)
369
370        display_color: Sequence[float] = (1.0, 1.0, 1.0, 1.0)
371
372        if color is not None:
373            display_color = color
374        elif importance != 1:
375            display_color = (1.0, 1.0, 0.4, 1.0)
376        points = base_points
377
378        # If they want a big announcement, throw a zoom-text up there.
379        if display and big_message:
380            try:
381                assert self._activity is not None
382                activity = self._activity()
383                if isinstance(activity, GameActivity):
384                    name_full = player.getname(full=True, icon=False)
385                    activity.show_zoom_message(
386                        babase.Lstr(
387                            resource='nameScoresText',
388                            subs=[('${NAME}', name_full)],
389                        ),
390                        color=babase.normalized_color(player.team.color),
391                    )
392            except Exception:
393                logging.exception('Error showing big_message.')
394
395        # If we currently have a actor, pop up a score over it.
396        if display and showpoints:
397            our_pos = player.node.position if player.node else None
398            if our_pos is not None:
399                if target is None:
400                    target = our_pos
401
402                # If display-pos is *way* lower than us, raise it up
403                # (so we can still see scores from dudes that fell off cliffs).
404                display_pos = (
405                    target[0],
406                    max(target[1], our_pos[1] - 2.0),
407                    min(target[2], our_pos[2] + 2.0),
408                )
409                activity = self.getactivity()
410                if activity is not None:
411                    if title is not None:
412                        sval = babase.Lstr(
413                            value='+${A} ${B}',
414                            subs=[('${A}', str(points)), ('${B}', title)],
415                        )
416                    else:
417                        sval = babase.Lstr(
418                            value='+${A}', subs=[('${A}', str(points))]
419                        )
420                    PopupText(
421                        sval,
422                        color=display_color,
423                        scale=1.2 * scale,
424                        position=display_pos,
425                    ).autoretain()
426
427        # Tally kills.
428        if kill:
429            s_player.accum_kill_count += 1
430            s_player.kill_count += 1
431
432        # Report non-kill scorings.
433        try:
434            if screenmessage and not kill:
435                _bascenev1.broadcastmessage(
436                    babase.Lstr(
437                        resource='nameScoresText', subs=[('${NAME}', name)]
438                    ),
439                    top=True,
440                    color=player.color,
441                    image=player.get_icon(),
442                )
443        except Exception:
444            logging.exception('Error announcing score.')
445
446        s_player.score += points
447        s_player.accumscore += points
448
449        # Inform a running game of the score.
450        if points != 0:
451            activity = self._activity() if self._activity is not None else None
452            if activity is not None:
453                activity.handlemessage(PlayerScoredMessage(score=points))
454
455        return points
456
457    def player_was_killed(
458        self,
459        player: bascenev1.Player,
460        killed: bool = False,
461        killer: bascenev1.Player | None = None,
462    ) -> None:
463        """Should be called when a player is killed."""
464        name = player.getname()
465        prec = self._player_records[name]
466        prec.streak = 0
467        if killed:
468            prec.accum_killed_count += 1
469            prec.killed_count += 1
470        try:
471            if killed and _bascenev1.getactivity().announce_player_deaths:
472                if killer is player:
473                    _bascenev1.broadcastmessage(
474                        babase.Lstr(
475                            resource='nameSuicideText', subs=[('${NAME}', name)]
476                        ),
477                        top=True,
478                        color=player.color,
479                        image=player.get_icon(),
480                    )
481                elif killer is not None:
482                    if killer.team is player.team:
483                        _bascenev1.broadcastmessage(
484                            babase.Lstr(
485                                resource='nameBetrayedText',
486                                subs=[
487                                    ('${NAME}', killer.getname()),
488                                    ('${VICTIM}', name),
489                                ],
490                            ),
491                            top=True,
492                            color=killer.color,
493                            image=killer.get_icon(),
494                        )
495                    else:
496                        _bascenev1.broadcastmessage(
497                            babase.Lstr(
498                                resource='nameKilledText',
499                                subs=[
500                                    ('${NAME}', killer.getname()),
501                                    ('${VICTIM}', name),
502                                ],
503                            ),
504                            top=True,
505                            color=killer.color,
506                            image=killer.get_icon(),
507                        )
508                else:
509                    _bascenev1.broadcastmessage(
510                        babase.Lstr(
511                            resource='nameDiedText', subs=[('${NAME}', name)]
512                        ),
513                        top=True,
514                        color=player.color,
515                        image=player.get_icon(),
516                    )
517        except Exception:
518            logging.exception('Error announcing kill.')

Manages scores and statistics for a bascenev1.Session.

orchestrahitsound1: _bascenev1.Sound | None
orchestrahitsound2: _bascenev1.Sound | None
orchestrahitsound3: _bascenev1.Sound | None
orchestrahitsound4: _bascenev1.Sound | None
def setactivity(self, activity: Activity | None) -> None:
262    def setactivity(self, activity: bascenev1.Activity | None) -> None:
263        """Set the current activity for this instance."""
264
265        self._activity = None if activity is None else weakref.ref(activity)
266
267        # Load our media into this activity's context.
268        if activity is not None:
269            if activity.expired:
270                logging.exception('Unexpected finalized activity.')
271            else:
272                with activity.context:
273                    self._load_activity_media()

Set the current activity for this instance.

def getactivity(self) -> Activity | None:
275    def getactivity(self) -> bascenev1.Activity | None:
276        """Get the activity associated with this instance.
277
278        May return None.
279        """
280        if self._activity is None:
281            return None
282        return self._activity()

Get the activity associated with this instance.

May return None.

def reset(self) -> None:
290    def reset(self) -> None:
291        """Reset the stats instance completely."""
292
293        # Just to be safe, lets make sure no multi-kill timers are gonna go off
294        # for no-longer-on-the-list players.
295        for p_entry in list(self._player_records.values()):
296            p_entry.cancel_multi_kill_timer()
297        self._player_records = {}

Reset the stats instance completely.

def reset_accum(self) -> None:
299    def reset_accum(self) -> None:
300        """Reset per-sound sub-scores."""
301        for s_player in list(self._player_records.values()):
302            s_player.cancel_multi_kill_timer()
303            s_player.accumscore = 0
304            s_player.accum_kill_count = 0
305            s_player.accum_killed_count = 0
306            s_player.streak = 0

Reset per-sound sub-scores.

def register_sessionplayer(self, player: _bascenev1.SessionPlayer) -> None:
308    def register_sessionplayer(self, player: bascenev1.SessionPlayer) -> None:
309        """Register a bascenev1.SessionPlayer with this score-set."""
310        assert player.exists()  # Invalid refs should never be passed to funcs.
311        name = player.getname()
312        if name in self._player_records:
313            # If the player already exists, update his character and such as
314            # it may have changed.
315            self._player_records[name].associate_with_sessionplayer(player)
316        else:
317            name_full = player.getname(full=True)
318            self._player_records[name] = PlayerRecord(
319                name, name_full, player, self
320            )

Register a bascenev1.SessionPlayer with this score-set.

def get_records(self) -> dict[str, PlayerRecord]:
322    def get_records(self) -> dict[str, bascenev1.PlayerRecord]:
323        """Get PlayerRecord corresponding to still-existing players."""
324        records = {}
325
326        # Go through our player records and return ones whose player id still
327        # corresponds to a player with that name.
328        for record_id, record in self._player_records.items():
329            lastplayer = record.get_last_sessionplayer()
330            if lastplayer and lastplayer.getname() == record_id:
331                records[record_id] = record
332        return records

Get PlayerRecord corresponding to still-existing players.

def player_scored( self, player: Player, base_points: int = 1, *, target: Optional[Sequence[float]] = None, kill: bool = False, victim_player: Player | None = None, scale: float = 1.0, color: Optional[Sequence[float]] = None, title: str | Lstr | None = None, screenmessage: bool = True, display: bool = True, importance: int = 1, showpoints: bool = True, big_message: bool = False) -> int:
334    def player_scored(
335        self,
336        player: bascenev1.Player,
337        base_points: int = 1,
338        *,
339        target: Sequence[float] | None = None,
340        kill: bool = False,
341        victim_player: bascenev1.Player | None = None,
342        scale: float = 1.0,
343        color: Sequence[float] | None = None,
344        title: str | babase.Lstr | None = None,
345        screenmessage: bool = True,
346        display: bool = True,
347        importance: int = 1,
348        showpoints: bool = True,
349        big_message: bool = False,
350    ) -> int:
351        """Register a score for the player.
352
353        Return value is actual score with multipliers and such factored in.
354        """
355        # FIXME: Tidy this up.
356        # pylint: disable=cyclic-import
357        # pylint: disable=too-many-branches
358        # pylint: disable=too-many-locals
359        from bascenev1lib.actor.popuptext import PopupText
360
361        from bascenev1._gameactivity import GameActivity
362
363        del victim_player  # Currently unused.
364        name = player.getname()
365        s_player = self._player_records[name]
366
367        if kill:
368            s_player.submit_kill(showpoints=showpoints)
369
370        display_color: Sequence[float] = (1.0, 1.0, 1.0, 1.0)
371
372        if color is not None:
373            display_color = color
374        elif importance != 1:
375            display_color = (1.0, 1.0, 0.4, 1.0)
376        points = base_points
377
378        # If they want a big announcement, throw a zoom-text up there.
379        if display and big_message:
380            try:
381                assert self._activity is not None
382                activity = self._activity()
383                if isinstance(activity, GameActivity):
384                    name_full = player.getname(full=True, icon=False)
385                    activity.show_zoom_message(
386                        babase.Lstr(
387                            resource='nameScoresText',
388                            subs=[('${NAME}', name_full)],
389                        ),
390                        color=babase.normalized_color(player.team.color),
391                    )
392            except Exception:
393                logging.exception('Error showing big_message.')
394
395        # If we currently have a actor, pop up a score over it.
396        if display and showpoints:
397            our_pos = player.node.position if player.node else None
398            if our_pos is not None:
399                if target is None:
400                    target = our_pos
401
402                # If display-pos is *way* lower than us, raise it up
403                # (so we can still see scores from dudes that fell off cliffs).
404                display_pos = (
405                    target[0],
406                    max(target[1], our_pos[1] - 2.0),
407                    min(target[2], our_pos[2] + 2.0),
408                )
409                activity = self.getactivity()
410                if activity is not None:
411                    if title is not None:
412                        sval = babase.Lstr(
413                            value='+${A} ${B}',
414                            subs=[('${A}', str(points)), ('${B}', title)],
415                        )
416                    else:
417                        sval = babase.Lstr(
418                            value='+${A}', subs=[('${A}', str(points))]
419                        )
420                    PopupText(
421                        sval,
422                        color=display_color,
423                        scale=1.2 * scale,
424                        position=display_pos,
425                    ).autoretain()
426
427        # Tally kills.
428        if kill:
429            s_player.accum_kill_count += 1
430            s_player.kill_count += 1
431
432        # Report non-kill scorings.
433        try:
434            if screenmessage and not kill:
435                _bascenev1.broadcastmessage(
436                    babase.Lstr(
437                        resource='nameScoresText', subs=[('${NAME}', name)]
438                    ),
439                    top=True,
440                    color=player.color,
441                    image=player.get_icon(),
442                )
443        except Exception:
444            logging.exception('Error announcing score.')
445
446        s_player.score += points
447        s_player.accumscore += points
448
449        # Inform a running game of the score.
450        if points != 0:
451            activity = self._activity() if self._activity is not None else None
452            if activity is not None:
453                activity.handlemessage(PlayerScoredMessage(score=points))
454
455        return points

Register a score for the player.

Return value is actual score with multipliers and such factored in.

def player_was_killed( self, player: Player, killed: bool = False, killer: Player | None = None) -> None:
457    def player_was_killed(
458        self,
459        player: bascenev1.Player,
460        killed: bool = False,
461        killer: bascenev1.Player | None = None,
462    ) -> None:
463        """Should be called when a player is killed."""
464        name = player.getname()
465        prec = self._player_records[name]
466        prec.streak = 0
467        if killed:
468            prec.accum_killed_count += 1
469            prec.killed_count += 1
470        try:
471            if killed and _bascenev1.getactivity().announce_player_deaths:
472                if killer is player:
473                    _bascenev1.broadcastmessage(
474                        babase.Lstr(
475                            resource='nameSuicideText', subs=[('${NAME}', name)]
476                        ),
477                        top=True,
478                        color=player.color,
479                        image=player.get_icon(),
480                    )
481                elif killer is not None:
482                    if killer.team is player.team:
483                        _bascenev1.broadcastmessage(
484                            babase.Lstr(
485                                resource='nameBetrayedText',
486                                subs=[
487                                    ('${NAME}', killer.getname()),
488                                    ('${VICTIM}', name),
489                                ],
490                            ),
491                            top=True,
492                            color=killer.color,
493                            image=killer.get_icon(),
494                        )
495                    else:
496                        _bascenev1.broadcastmessage(
497                            babase.Lstr(
498                                resource='nameKilledText',
499                                subs=[
500                                    ('${NAME}', killer.getname()),
501                                    ('${VICTIM}', name),
502                                ],
503                            ),
504                            top=True,
505                            color=killer.color,
506                            image=killer.get_icon(),
507                        )
508                else:
509                    _bascenev1.broadcastmessage(
510                        babase.Lstr(
511                            resource='nameDiedText', subs=[('${NAME}', name)]
512                        ),
513                        top=True,
514                        color=player.color,
515                        image=player.get_icon(),
516                    )
517        except Exception:
518            logging.exception('Error announcing kill.')

Should be called when a player is killed.

def storagename(suffix: str | None = None) -> str:
339def storagename(suffix: str | None = None) -> str:
340    """Generate a unique name for storing class data in shared places.
341
342    This consists of a leading underscore, the module path at the
343    call site with dots replaced by underscores, the containing class's
344    qualified name, and the provided suffix. When storing data in public
345    places such as 'customdata' dicts, this minimizes the chance of
346    collisions with other similarly named classes.
347
348    Note that this will function even if called in the class definition.
349
350    ##### Examples
351    Generate a unique name for storage purposes:
352    >>> class MyThingie:
353    ...     # This will give something like
354    ...     # '_mymodule_submodule_mythingie_data'.
355    ...     _STORENAME = babase.storagename('data')
356    ...
357    ...     # Use that name to store some data in the Activity we were
358    ...     # passed.
359    ...     def __init__(self, activity):
360    ...         activity.customdata[self._STORENAME] = {}
361    """
362    frame = inspect.currentframe()
363    if frame is None:
364        raise RuntimeError('Cannot get current stack frame.')
365    fback = frame.f_back
366
367    # Note: We need to explicitly clear frame here to avoid a ref-loop
368    # that keeps all function-dicts in the stack alive until the next
369    # full GC cycle (the stack frame refers to this function's dict,
370    # which refers to the stack frame).
371    del frame
372
373    if fback is None:
374        raise RuntimeError('Cannot get parent stack frame.')
375    modulepath = fback.f_globals.get('__name__')
376    if modulepath is None:
377        raise RuntimeError('Cannot get parent stack module path.')
378    assert isinstance(modulepath, str)
379    qualname = fback.f_locals.get('__qualname__')
380    if qualname is not None:
381        assert isinstance(qualname, str)
382        fullpath = f'_{modulepath}_{qualname.lower()}'
383    else:
384        fullpath = f'_{modulepath}'
385    if suffix is not None:
386        fullpath = f'{fullpath}_{suffix}'
387    return fullpath.replace('.', '_')

Generate a unique name for storing class data in shared places.

This consists of a leading underscore, the module path at the call site with dots replaced by underscores, the containing class's qualified name, and the provided suffix. When storing data in public places such as 'customdata' dicts, this minimizes the chance of collisions with other similarly named classes.

Note that this will function even if called in the class definition.

Examples

Generate a unique name for storage purposes:

>>> class MyThingie:
...     # This will give something like
...     # '_mymodule_submodule_mythingie_data'.
...     _STORENAME = babase.storagename('data')
...
...     # Use that name to store some data in the Activity we were
...     # passed.
...     def __init__(self, activity):
...         activity.customdata[self._STORENAME] = {}
class Team(typing.Generic[~PlayerT]):
 75class Team(Generic[PlayerT]):
 76    """A team in a specific bascenev1.Activity.
 77
 78    These correspond to bascenev1.SessionTeam objects, but are created
 79    per activity so that the activity can use its own custom team
 80    subclass.
 81    """
 82
 83    # Defining these types at the class level instead of in __init__ so
 84    # that types are introspectable (these are still instance attrs).
 85    players: list[PlayerT]
 86    id: int
 87    name: babase.Lstr | str
 88    color: tuple[float, ...]  # FIXME: can't we make this fixed length?
 89    _sessionteam: weakref.ref[SessionTeam]
 90    _expired: bool
 91    _postinited: bool
 92    _customdata: dict
 93
 94    # NOTE: avoiding having any __init__() here since it seems to not
 95    # get called by default if a dataclass inherits from us.
 96
 97    def postinit(self, sessionteam: SessionTeam) -> None:
 98        """Wire up a newly created SessionTeam.
 99
100        (internal)
101        """
102
103        # Sanity check; if a dataclass is created that inherits from us,
104        # it will define an equality operator by default which will break
105        # internal game logic. So complain loudly if we find one.
106        if type(self).__eq__ is not object.__eq__:
107            raise RuntimeError(
108                f'Team class {type(self)} defines an equality'
109                f' operator (__eq__) which will break internal'
110                f' logic. Please remove it.\n'
111                f'For dataclasses you can do "dataclass(eq=False)"'
112                f' in the class decorator.'
113            )
114
115        self.players = []
116        self._sessionteam = weakref.ref(sessionteam)
117        self.id = sessionteam.id
118        self.name = sessionteam.name
119        self.color = sessionteam.color
120        self._customdata = {}
121        self._expired = False
122        self._postinited = True
123
124    def manual_init(
125        self, team_id: int, name: babase.Lstr | str, color: tuple[float, ...]
126    ) -> None:
127        """Manually init a team for uses such as bots."""
128        self.id = team_id
129        self.name = name
130        self.color = color
131        self._customdata = {}
132        self._expired = False
133        self._postinited = True
134
135    @property
136    def customdata(self) -> dict:
137        """Arbitrary values associated with the team.
138        Though it is encouraged that most player values be properly defined
139        on the bascenev1.Team subclass, it may be useful for player-agnostic
140        objects to store values here. This dict is cleared when the team
141        leaves or expires so objects stored here will be disposed of at
142        the expected time, unlike the Team instance itself which may
143        continue to be referenced after it is no longer part of the game.
144        """
145        assert self._postinited
146        assert not self._expired
147        return self._customdata
148
149    def leave(self) -> None:
150        """Called when the Team leaves a running game.
151
152        (internal)
153        """
154        assert self._postinited
155        assert not self._expired
156        del self._customdata
157        del self.players
158
159    def expire(self) -> None:
160        """Called when the Team is expiring (due to the Activity expiring).
161
162        (internal)
163        """
164        assert self._postinited
165        assert not self._expired
166        self._expired = True
167
168        try:
169            self.on_expire()
170        except Exception:
171            logging.exception('Error in on_expire for %s.', self)
172
173        del self._customdata
174        del self.players
175
176    def on_expire(self) -> None:
177        """Can be overridden to handle team expiration."""
178
179    @property
180    def sessionteam(self) -> SessionTeam:
181        """Return the bascenev1.SessionTeam corresponding to this Team.
182
183        Throws a babase.SessionTeamNotFoundError if there is none.
184        """
185        assert self._postinited
186        if self._sessionteam is not None:
187            sessionteam = self._sessionteam()
188            if sessionteam is not None:
189                return sessionteam
190
191        raise babase.SessionTeamNotFoundError()

A team in a specific bascenev1.Activity.

These correspond to bascenev1.SessionTeam objects, but are created per activity so that the activity can use its own custom team subclass.

players: list[~PlayerT]
id: int
name: Lstr | str
color: tuple[float, ...]
def manual_init( self, team_id: int, name: Lstr | str, color: tuple[float, ...]) -> None:
124    def manual_init(
125        self, team_id: int, name: babase.Lstr | str, color: tuple[float, ...]
126    ) -> None:
127        """Manually init a team for uses such as bots."""
128        self.id = team_id
129        self.name = name
130        self.color = color
131        self._customdata = {}
132        self._expired = False
133        self._postinited = True

Manually init a team for uses such as bots.

customdata: dict
135    @property
136    def customdata(self) -> dict:
137        """Arbitrary values associated with the team.
138        Though it is encouraged that most player values be properly defined
139        on the bascenev1.Team subclass, it may be useful for player-agnostic
140        objects to store values here. This dict is cleared when the team
141        leaves or expires so objects stored here will be disposed of at
142        the expected time, unlike the Team instance itself which may
143        continue to be referenced after it is no longer part of the game.
144        """
145        assert self._postinited
146        assert not self._expired
147        return self._customdata

Arbitrary values associated with the team. Though it is encouraged that most player values be properly defined on the bascenev1.Team subclass, it may be useful for player-agnostic objects to store values here. This dict is cleared when the team leaves or expires so objects stored here will be disposed of at the expected time, unlike the Team instance itself which may continue to be referenced after it is no longer part of the game.

def on_expire(self) -> None:
176    def on_expire(self) -> None:
177        """Can be overridden to handle team expiration."""

Can be overridden to handle team expiration.

sessionteam: SessionTeam
179    @property
180    def sessionteam(self) -> SessionTeam:
181        """Return the bascenev1.SessionTeam corresponding to this Team.
182
183        Throws a babase.SessionTeamNotFoundError if there is none.
184        """
185        assert self._postinited
186        if self._sessionteam is not None:
187            sessionteam = self._sessionteam()
188            if sessionteam is not None:
189                return sessionteam
190
191        raise babase.SessionTeamNotFoundError()

Return the bascenev1.SessionTeam corresponding to this Team.

Throws a babase.SessionTeamNotFoundError if there is none.

class TeamGameActivity(bascenev1.GameActivity[~PlayerT, ~TeamT]):
 30class TeamGameActivity(GameActivity[PlayerT, TeamT]):
 31    """Base class for teams and free-for-all mode games.
 32
 33    (Free-for-all is essentially just a special case where every
 34    bascenev1.Player has their own bascenev1.Team)
 35    """
 36
 37    @override
 38    @classmethod
 39    def supports_session_type(
 40        cls, sessiontype: type[bascenev1.Session]
 41    ) -> bool:
 42        """
 43        Class method override;
 44        returns True for ba.DualTeamSessions and ba.FreeForAllSessions;
 45        False otherwise.
 46        """
 47        return issubclass(sessiontype, DualTeamSession) or issubclass(
 48            sessiontype, FreeForAllSession
 49        )
 50
 51    def __init__(self, settings: dict):
 52        super().__init__(settings)
 53
 54        # By default we don't show kill-points in free-for-all sessions.
 55        # (there's usually some activity-specific score and we don't
 56        # wanna confuse things)
 57        if isinstance(self.session, FreeForAllSession):
 58            self.show_kill_points = False
 59
 60    @override
 61    def on_transition_in(self) -> None:
 62        # pylint: disable=cyclic-import
 63        from bascenev1._coopsession import CoopSession
 64        from bascenev1lib.actor.controlsguide import ControlsGuide
 65
 66        super().on_transition_in()
 67
 68        # On the first game, show the controls UI momentarily.
 69        # (unless we're being run in co-op mode, in which case we leave
 70        # it up to them)
 71        if not isinstance(self.session, CoopSession) and getattr(
 72            self, 'show_controls_guide', True
 73        ):
 74            attrname = '_have_shown_ctrl_help_overlay'
 75            if not getattr(self.session, attrname, False):
 76                delay = 4.0
 77                lifespan = 10.0
 78                if self.slow_motion:
 79                    lifespan *= 0.3
 80                ControlsGuide(
 81                    delay=delay,
 82                    lifespan=lifespan,
 83                    scale=0.8,
 84                    position=(380, 200),
 85                    bright=True,
 86                ).autoretain()
 87                setattr(self.session, attrname, True)
 88
 89    @override
 90    def on_begin(self) -> None:
 91        super().on_begin()
 92        try:
 93            # Award a few (classic) achievements.
 94            if isinstance(self.session, FreeForAllSession):
 95                if len(self.players) >= 2:
 96                    if babase.app.classic is not None:
 97                        babase.app.classic.ach.award_local_achievement(
 98                            'Free Loader'
 99                        )
100            elif isinstance(self.session, DualTeamSession):
101                if len(self.players) >= 4:
102                    if babase.app.classic is not None:
103                        babase.app.classic.ach.award_local_achievement(
104                            'Team Player'
105                        )
106        except Exception:
107            logging.exception('Error in on_begin.')
108
109    @override
110    def spawn_player_spaz(
111        self,
112        player: PlayerT,
113        position: Sequence[float] | None = None,
114        angle: float | None = None,
115    ) -> PlayerSpaz:
116        """
117        Method override; spawns and wires up a standard bascenev1.PlayerSpaz
118        for a bascenev1.Player.
119
120        If position or angle is not supplied, a default will be chosen based
121        on the bascenev1.Player and their bascenev1.Team.
122        """
123        if position is None:
124            # In teams-mode get our team-start-location.
125            if isinstance(self.session, DualTeamSession):
126                position = self.map.get_start_position(player.team.id)
127            else:
128                # Otherwise do free-for-all spawn locations.
129                position = self.map.get_ffa_start_position(self.players)
130
131        return super().spawn_player_spaz(player, position, angle)
132
133    # FIXME: need to unify these arguments with GameActivity.end()
134    def end(  # type: ignore
135        self,
136        results: Any = None,
137        announce_winning_team: bool = True,
138        announce_delay: float = 0.1,
139        force: bool = False,
140    ) -> None:
141        """
142        End the game and announce the single winning team
143        unless 'announce_winning_team' is False.
144        (for results without a single most-important winner).
145        """
146        # pylint: disable=arguments-renamed
147        from bascenev1._coopsession import CoopSession
148        from bascenev1._multiteamsession import MultiTeamSession
149
150        # Announce win (but only for the first finish() call)
151        # (also don't announce in co-op sessions; we leave that up to them).
152        session = self.session
153        if not isinstance(session, CoopSession):
154            do_announce = not self.has_ended()
155            super().end(results, delay=2.0 + announce_delay, force=force)
156
157            # Need to do this *after* end end call so that results is valid.
158            assert isinstance(results, GameResults)
159            if do_announce and isinstance(session, MultiTeamSession):
160                session.announce_game_results(
161                    self,
162                    results,
163                    delay=announce_delay,
164                    announce_winning_team=announce_winning_team,
165                )
166
167        # For co-op we just pass this up the chain with a delay added
168        # (in most cases). Team games expect a delay for the announce
169        # portion in teams/ffa mode so this keeps it consistent.
170        else:
171            # don't want delay on restarts..
172            if (
173                isinstance(results, dict)
174                and 'outcome' in results
175                and results['outcome'] == 'restart'
176            ):
177                delay = 0.0
178            else:
179                delay = 2.0
180                _bascenev1.timer(0.1, _bascenev1.getsound('boxingBell').play)
181            super().end(results, delay=delay, force=force)

Base class for teams and free-for-all mode games.

(Free-for-all is essentially just a special case where every bascenev1.Player has their own bascenev1.Team)

TeamGameActivity(settings: dict)
51    def __init__(self, settings: dict):
52        super().__init__(settings)
53
54        # By default we don't show kill-points in free-for-all sessions.
55        # (there's usually some activity-specific score and we don't
56        # wanna confuse things)
57        if isinstance(self.session, FreeForAllSession):
58            self.show_kill_points = False

Instantiate the Activity.

@override
@classmethod
def supports_session_type(cls, sessiontype: type[Session]) -> bool:
37    @override
38    @classmethod
39    def supports_session_type(
40        cls, sessiontype: type[bascenev1.Session]
41    ) -> bool:
42        """
43        Class method override;
44        returns True for ba.DualTeamSessions and ba.FreeForAllSessions;
45        False otherwise.
46        """
47        return issubclass(sessiontype, DualTeamSession) or issubclass(
48            sessiontype, FreeForAllSession
49        )

Class method override; returns True for ba.DualTeamSessions and ba.FreeForAllSessions; False otherwise.

@override
def on_transition_in(self) -> None:
60    @override
61    def on_transition_in(self) -> None:
62        # pylint: disable=cyclic-import
63        from bascenev1._coopsession import CoopSession
64        from bascenev1lib.actor.controlsguide import ControlsGuide
65
66        super().on_transition_in()
67
68        # On the first game, show the controls UI momentarily.
69        # (unless we're being run in co-op mode, in which case we leave
70        # it up to them)
71        if not isinstance(self.session, CoopSession) and getattr(
72            self, 'show_controls_guide', True
73        ):
74            attrname = '_have_shown_ctrl_help_overlay'
75            if not getattr(self.session, attrname, False):
76                delay = 4.0
77                lifespan = 10.0
78                if self.slow_motion:
79                    lifespan *= 0.3
80                ControlsGuide(
81                    delay=delay,
82                    lifespan=lifespan,
83                    scale=0.8,
84                    position=(380, 200),
85                    bright=True,
86                ).autoretain()
87                setattr(self.session, attrname, True)

Called when the Activity is first becoming visible.

Upon this call, the Activity should fade in backgrounds, start playing music, etc. It does not yet have access to players or teams, however. They remain owned by the previous Activity up until bascenev1.Activity.on_begin() is called.

@override
def on_begin(self) -> None:
 89    @override
 90    def on_begin(self) -> None:
 91        super().on_begin()
 92        try:
 93            # Award a few (classic) achievements.
 94            if isinstance(self.session, FreeForAllSession):
 95                if len(self.players) >= 2:
 96                    if babase.app.classic is not None:
 97                        babase.app.classic.ach.award_local_achievement(
 98                            'Free Loader'
 99                        )
100            elif isinstance(self.session, DualTeamSession):
101                if len(self.players) >= 4:
102                    if babase.app.classic is not None:
103                        babase.app.classic.ach.award_local_achievement(
104                            'Team Player'
105                        )
106        except Exception:
107            logging.exception('Error in on_begin.')

Called once the previous Activity has finished transitioning out.

At this point the activity's initial players and teams are filled in and it should begin its actual game logic.

@override
def spawn_player_spaz( self, player: ~PlayerT, position: Optional[Sequence[float]] = None, angle: float | None = None) -> bascenev1lib.actor.playerspaz.PlayerSpaz:
109    @override
110    def spawn_player_spaz(
111        self,
112        player: PlayerT,
113        position: Sequence[float] | None = None,
114        angle: float | None = None,
115    ) -> PlayerSpaz:
116        """
117        Method override; spawns and wires up a standard bascenev1.PlayerSpaz
118        for a bascenev1.Player.
119
120        If position or angle is not supplied, a default will be chosen based
121        on the bascenev1.Player and their bascenev1.Team.
122        """
123        if position is None:
124            # In teams-mode get our team-start-location.
125            if isinstance(self.session, DualTeamSession):
126                position = self.map.get_start_position(player.team.id)
127            else:
128                # Otherwise do free-for-all spawn locations.
129                position = self.map.get_ffa_start_position(self.players)
130
131        return super().spawn_player_spaz(player, position, angle)

Method override; spawns and wires up a standard bascenev1.PlayerSpaz for a bascenev1.Player.

If position or angle is not supplied, a default will be chosen based on the bascenev1.Player and their bascenev1.Team.

def end( self, results: Any = None, announce_winning_team: bool = True, announce_delay: float = 0.1, force: bool = False) -> None:
134    def end(  # type: ignore
135        self,
136        results: Any = None,
137        announce_winning_team: bool = True,
138        announce_delay: float = 0.1,
139        force: bool = False,
140    ) -> None:
141        """
142        End the game and announce the single winning team
143        unless 'announce_winning_team' is False.
144        (for results without a single most-important winner).
145        """
146        # pylint: disable=arguments-renamed
147        from bascenev1._coopsession import CoopSession
148        from bascenev1._multiteamsession import MultiTeamSession
149
150        # Announce win (but only for the first finish() call)
151        # (also don't announce in co-op sessions; we leave that up to them).
152        session = self.session
153        if not isinstance(session, CoopSession):
154            do_announce = not self.has_ended()
155            super().end(results, delay=2.0 + announce_delay, force=force)
156
157            # Need to do this *after* end end call so that results is valid.
158            assert isinstance(results, GameResults)
159            if do_announce and isinstance(session, MultiTeamSession):
160                session.announce_game_results(
161                    self,
162                    results,
163                    delay=announce_delay,
164                    announce_winning_team=announce_winning_team,
165                )
166
167        # For co-op we just pass this up the chain with a delay added
168        # (in most cases). Team games expect a delay for the announce
169        # portion in teams/ffa mode so this keeps it consistent.
170        else:
171            # don't want delay on restarts..
172            if (
173                isinstance(results, dict)
174                and 'outcome' in results
175                and results['outcome'] == 'restart'
176            ):
177                delay = 0.0
178            else:
179                delay = 2.0
180                _bascenev1.timer(0.1, _bascenev1.getsound('boxingBell').play)
181            super().end(results, delay=delay, force=force)

End the game and announce the single winning team unless 'announce_winning_team' is False. (for results without a single most-important winner).

class Texture:
880class Texture:
881    """A reference to a texture.
882
883    Use bascenev1.gettexture() to instantiate one.
884    """
885
886    pass

A reference to a texture.

Use bascenev1.gettexture() to instantiate one.

@dataclass
class ThawMessage:
185@dataclass
186class ThawMessage:
187    """Tells an object to stop being frozen."""

Tells an object to stop being frozen.

def time() -> Time:
1676def time() -> bascenev1.Time:
1677    """Return the current scene time in seconds.
1678
1679    Scene time maps to local simulation time in bascenev1.Activity or
1680    bascenev1.Session Contexts. This means that it may progress slower
1681    in slow-motion play modes, stop when the game is paused, etc.
1682
1683    Note that the value returned here is simply a float; it just has a
1684    unique type in the type-checker's eyes to help prevent it from being
1685    accidentally used with time functionality expecting other time types.
1686    """
1687    import bascenev1  # pylint: disable=cyclic-import
1688
1689    return bascenev1.Time(0.0)

Return the current scene time in seconds.

Scene time maps to local simulation time in bascenev1.Activity or bascenev1.Session Contexts. This means that it may progress slower in slow-motion play modes, stop when the game is paused, etc.

Note that the value returned here is simply a float; it just has a unique type in the type-checker's eyes to help prevent it from being accidentally used with time functionality expecting other time types.

Time = Time
def timer(time: float, call: Callable[[], Any], repeat: bool = False) -> None:
1693def timer(time: float, call: Callable[[], Any], repeat: bool = False) -> None:
1694    """Schedule a call to run at a later point in time.
1695
1696    This function adds a scene-time timer to the current babase.Context.
1697    This timer cannot be canceled or modified once created. If you
1698     require the ability to do so, use the babase.Timer class instead.
1699
1700    Scene time maps to local simulation time in bascenev1.Activity or
1701    bascenev1.Session Contexts. This means that it may progress slower
1702    in slow-motion play modes, stop when the game is paused, etc.
1703
1704    ##### Arguments
1705    ###### time (float)
1706    > Length of scene time in seconds that the timer will wait
1707    before firing.
1708
1709    ###### call (Callable[[], Any])
1710    > A callable Python object. Note that the timer will retain a
1711    strong reference to the callable for as long as it exists, so you
1712    may want to look into concepts such as babase.WeakCall if that is not
1713    desired.
1714
1715    ###### repeat (bool)
1716    > If True, the timer will fire repeatedly, with each successive
1717    firing having the same delay as the first.
1718
1719    ##### Examples
1720    Print some stuff through time:
1721    >>> import bascenev1 as bs
1722    >>> bs.screenmessage('hello from now!')
1723    >>> bs.timer(1.0, bs.Call(bs.screenmessage, 'hello from the future!'))
1724    >>> bs.timer(2.0, bs.Call(bs.screenmessage,
1725    ...                       'hello from the future 2!'))
1726    """
1727    return None

Schedule a call to run at a later point in time.

This function adds a scene-time timer to the current babase.Context. This timer cannot be canceled or modified once created. If you require the ability to do so, use the babase.Timer class instead.

Scene time maps to local simulation time in bascenev1.Activity or bascenev1.Session Contexts. This means that it may progress slower in slow-motion play modes, stop when the game is paused, etc.

Arguments
time (float)

Length of scene time in seconds that the timer will wait before firing.

call (Callable[[], Any])

A callable Python object. Note that the timer will retain a strong reference to the callable for as long as it exists, so you may want to look into concepts such as babase.WeakCall if that is not desired.

repeat (bool)

If True, the timer will fire repeatedly, with each successive firing having the same delay as the first.

Examples

Print some stuff through time:

>>> import bascenev1 as bs
>>> bs.screenmessage('hello from now!')
>>> bs.timer(1.0, bs.Call(bs.screenmessage, 'hello from the future!'))
>>> bs.timer(2.0, bs.Call(bs.screenmessage,
...                       'hello from the future 2!'))
class Timer:
890class Timer:
891    """Timers are used to run code at later points in time.
892
893    This class encapsulates a scene-time timer in the current
894    bascenev1.Context. The underlying timer will be destroyed when either
895    this object is no longer referenced or when its Context (Activity,
896    etc.) dies. If you do not want to worry about keeping a reference to
897    your timer around,
898    you should use the bs.timer() function instead.
899
900    Scene time maps to local simulation time in bascenev1.Activity or
901    bascenev1.Session Contexts. This means that it may progress slower
902    in slow-motion play modes, stop when the game is paused, etc.
903
904    ###### time
905    > Length of time (in seconds by default) that the timer will wait
906    before firing. Note that the actual delay experienced may vary
907    depending on the timetype. (see below)
908
909    ###### call
910    > A callable Python object. Note that the timer will retain a
911    strong reference to the callable for as long as it exists, so you
912    may want to look into concepts such as babase.WeakCall if that is not
913    desired.
914
915    ###### repeat
916    > If True, the timer will fire repeatedly, with each successive
917    firing having the same delay as the first.
918
919    ##### Example
920
921    Use a Timer object to print repeatedly for a few seconds:
922    >>> import bascenev1 as bs
923    ... def say_it():
924    ...     bs.screenmessage('BADGER!')
925    ... def stop_saying_it():
926    ...     global g_timer
927    ...     g_timer = None
928    ...     bs.screenmessage('MUSHROOM MUSHROOM!')
929    ... # Create our timer; it will run as long as we have the self.t ref.
930    ... g_timer = bs.Timer(0.3, say_it, repeat=True)
931    ... # Now fire off a one-shot timer to kill it.
932    ... bs.timer(3.89, stop_saying_it)
933    """
934
935    def __init__(
936        self, time: float, call: Callable[[], Any], repeat: bool = False
937    ) -> None:
938        pass

Timers are used to run code at later points in time.

This class encapsulates a scene-time timer in the current bascenev1.Context. The underlying timer will be destroyed when either this object is no longer referenced or when its Context (Activity, etc.) dies. If you do not want to worry about keeping a reference to your timer around, you should use the bs.timer() function instead.

Scene time maps to local simulation time in bascenev1.Activity or bascenev1.Session Contexts. This means that it may progress slower in slow-motion play modes, stop when the game is paused, etc.

time

Length of time (in seconds by default) that the timer will wait before firing. Note that the actual delay experienced may vary depending on the timetype. (see below)

call

A callable Python object. Note that the timer will retain a strong reference to the callable for as long as it exists, so you may want to look into concepts such as babase.WeakCall if that is not desired.

repeat

If True, the timer will fire repeatedly, with each successive firing having the same delay as the first.

Example

Use a Timer object to print repeatedly for a few seconds:

>>> import bascenev1 as bs
... def say_it():
...     bs.screenmessage('BADGER!')
... def stop_saying_it():
...     global g_timer
...     g_timer = None
...     bs.screenmessage('MUSHROOM MUSHROOM!')
... # Create our timer; it will run as long as we have the self.t ref.
... g_timer = bs.Timer(0.3, say_it, repeat=True)
... # Now fire off a one-shot timer to kill it.
... bs.timer(3.89, stop_saying_it)
Timer(time: float, call: Callable[[], Any], repeat: bool = False)
935    def __init__(
936        self, time: float, call: Callable[[], Any], repeat: bool = False
937    ) -> None:
938        pass
def timestring(timeval: float | int, centi: bool = True) -> Lstr:
15def timestring(
16    timeval: float | int,
17    centi: bool = True,
18) -> babase.Lstr:
19    """Generate a babase.Lstr for displaying a time value.
20
21    Given a time value, returns a babase.Lstr with:
22    (hours if > 0 ) : minutes : seconds : (centiseconds if centi=True).
23
24    WARNING: the underlying Lstr value is somewhat large so don't use this
25    to rapidly update Node text values for an onscreen timer or you may
26    consume significant network bandwidth.  For that purpose you should
27    use a 'timedisplay' Node and attribute connections.
28
29    """
30    from babase._language import Lstr
31
32    # We take float seconds but operate on int milliseconds internally.
33    timeval = int(1000 * timeval)
34    bits = []
35    subs = []
36    hval = (timeval // 1000) // (60 * 60)
37    if hval != 0:
38        bits.append('${H}')
39        subs.append(
40            (
41                '${H}',
42                Lstr(
43                    resource='timeSuffixHoursText',
44                    subs=[('${COUNT}', str(hval))],
45                ),
46            )
47        )
48    mval = ((timeval // 1000) // 60) % 60
49    if mval != 0:
50        bits.append('${M}')
51        subs.append(
52            (
53                '${M}',
54                Lstr(
55                    resource='timeSuffixMinutesText',
56                    subs=[('${COUNT}', str(mval))],
57                ),
58            )
59        )
60
61    # We add seconds if its non-zero *or* we haven't added anything else.
62    if centi:
63        # pylint: disable=consider-using-f-string
64        sval = timeval / 1000.0 % 60.0
65        if sval >= 0.005 or not bits:
66            bits.append('${S}')
67            subs.append(
68                (
69                    '${S}',
70                    Lstr(
71                        resource='timeSuffixSecondsText',
72                        subs=[('${COUNT}', ('%.2f' % sval))],
73                    ),
74                )
75            )
76    else:
77        sval = timeval // 1000 % 60
78        if sval != 0 or not bits:
79            bits.append('${S}')
80            subs.append(
81                (
82                    '${S}',
83                    Lstr(
84                        resource='timeSuffixSecondsText',
85                        subs=[('${COUNT}', str(sval))],
86                    ),
87                )
88            )
89    return Lstr(value=' '.join(bits), subs=subs)

Generate a babase.Lstr for displaying a time value.

Given a time value, returns a babase.Lstr with: (hours if > 0 ) : minutes : seconds : (centiseconds if centi=True).

WARNING: the underlying Lstr value is somewhat large so don't use this to rapidly update Node text values for an onscreen timer or you may consume significant network bandwidth. For that purpose you should use a 'timedisplay' Node and attribute connections.

class UIScale(enum.Enum):
59class UIScale(Enum):
60    """The overall scale the UI is being rendered for. Note that this is
61    independent of pixel resolution. For example, a phone and a desktop PC
62    might render the game at similar pixel resolutions but the size they
63    display content at will vary significantly.
64
65    'large' is used for devices such as desktop PCs where fine details can
66       be clearly seen. UI elements are generally smaller on the screen
67       and more content can be seen at once.
68
69    'medium' is used for devices such as tablets, TVs, or VR headsets.
70       This mode strikes a balance between clean readability and amount of
71       content visible.
72
73    'small' is used primarily for phones or other small devices where
74       content needs to be presented as large and clear in order to remain
75       readable from an average distance.
76    """
77
78    SMALL = 0
79    MEDIUM = 1
80    LARGE = 2

The overall scale the UI is being rendered for. Note that this is independent of pixel resolution. For example, a phone and a desktop PC might render the game at similar pixel resolutions but the size they display content at will vary significantly.

'large' is used for devices such as desktop PCs where fine details can be clearly seen. UI elements are generally smaller on the screen and more content can be seen at once.

'medium' is used for devices such as tablets, TVs, or VR headsets. This mode strikes a balance between clean readability and amount of content visible.

'small' is used primarily for phones or other small devices where content needs to be presented as large and clear in order to remain readable from an average distance.

SMALL = <UIScale.SMALL: 0>
MEDIUM = <UIScale.MEDIUM: 1>
LARGE = <UIScale.LARGE: 2>
UNHANDLED = <bascenev1._messages._UnhandledType object>
class Vec3(typing.Sequence[float]):
388class Vec3(Sequence[float]):
389    """A vector of 3 floats.
390
391    These can be created the following ways (checked in this order):
392    - with no args, all values are set to 0
393    - with a single numeric arg, all values are set to that value
394    - with a single three-member sequence arg, sequence values are copied
395    - otherwise assumes individual x/y/z args (positional or keywords)
396    """
397
398    x: float
399    """The vector's X component."""
400
401    y: float
402    """The vector's Y component."""
403
404    z: float
405    """The vector's Z component."""
406
407    # pylint: disable=function-redefined
408
409    @overload
410    def __init__(self) -> None:
411        pass
412
413    @overload
414    def __init__(self, value: float):
415        pass
416
417    @overload
418    def __init__(self, values: Sequence[float]):
419        pass
420
421    @overload
422    def __init__(self, x: float, y: float, z: float):
423        pass
424
425    def __init__(self, *args: Any, **kwds: Any):
426        pass
427
428    def __add__(self, other: Vec3) -> Vec3:
429        return self
430
431    def __sub__(self, other: Vec3) -> Vec3:
432        return self
433
434    @overload
435    def __mul__(self, other: float) -> Vec3:
436        return self
437
438    @overload
439    def __mul__(self, other: Sequence[float]) -> Vec3:
440        return self
441
442    def __mul__(self, other: Any) -> Any:
443        return self
444
445    @overload
446    def __rmul__(self, other: float) -> Vec3:
447        return self
448
449    @overload
450    def __rmul__(self, other: Sequence[float]) -> Vec3:
451        return self
452
453    def __rmul__(self, other: Any) -> Any:
454        return self
455
456    # (for index access)
457    @override
458    def __getitem__(self, typeargs: Any) -> Any:
459        return 0.0
460
461    @override
462    def __len__(self) -> int:
463        return 3
464
465    # (for iterator access)
466    @override
467    def __iter__(self) -> Any:
468        return self
469
470    def __next__(self) -> float:
471        return 0.0
472
473    def __neg__(self) -> Vec3:
474        return self
475
476    def __setitem__(self, index: int, val: float) -> None:
477        pass
478
479    def cross(self, other: Vec3) -> Vec3:
480        """Returns the cross product of this vector and another."""
481        return Vec3()
482
483    def dot(self, other: Vec3) -> float:
484        """Returns the dot product of this vector and another."""
485        return float()
486
487    def length(self) -> float:
488        """Returns the length of the vector."""
489        return float()
490
491    def normalized(self) -> Vec3:
492        """Returns a normalized version of the vector."""
493        return Vec3()

A vector of 3 floats.

These can be created the following ways (checked in this order):

  • with no args, all values are set to 0
  • with a single numeric arg, all values are set to that value
  • with a single three-member sequence arg, sequence values are copied
  • otherwise assumes individual x/y/z args (positional or keywords)
Vec3(*args: Any, **kwds: Any)
425    def __init__(self, *args: Any, **kwds: Any):
426        pass
x: float

The vector's X component.

y: float

The vector's Y component.

z: float

The vector's Z component.

def cross(self, other: _babase.Vec3) -> _babase.Vec3:
479    def cross(self, other: Vec3) -> Vec3:
480        """Returns the cross product of this vector and another."""
481        return Vec3()

Returns the cross product of this vector and another.

def dot(self, other: _babase.Vec3) -> float:
483    def dot(self, other: Vec3) -> float:
484        """Returns the dot product of this vector and another."""
485        return float()

Returns the dot product of this vector and another.

def length(self) -> float:
487    def length(self) -> float:
488        """Returns the length of the vector."""
489        return float()

Returns the length of the vector.

def normalized(self) -> _babase.Vec3:
491    def normalized(self) -> Vec3:
492        """Returns a normalized version of the vector."""
493        return Vec3()

Returns a normalized version of the vector.

WeakCall = <class 'babase._general._WeakCall'>