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

Units of execution wrangled by a Session.

Category: Gameplay Classes

Examples of Activities include games, score-screens, cutscenes, etc. A 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 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 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
205    @property
206    def context(self) -> bascenev1.ContextRef:
207        """A context-ref pointing at this activity."""
208        return self._activity_data.context()

A context-ref pointing at this activity.

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

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

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

The stats instance accessible while the activity is running.

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

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

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
241    @property
242    def customdata(self) -> dict:
243        """Entities needing to store simple data with an activity can put it
244        here. This dict will be deleted when the activity expires, so contained
245        objects generally do not need to worry about handling expired
246        activities.
247        """
248        assert not self._expired
249        assert isinstance(self._customdata, dict)
250        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
252    @property
253    def expired(self) -> bool:
254        """Whether the activity is expired.
255
256        An activity is set as expired when shutting down.
257        At this point no new nodes, timers, etc should be made,
258        run, etc, and the activity should be considered a 'zombie'.
259        """
260        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]
262    @property
263    def playertype(self) -> type[PlayerT]:
264        """The type of bascenev1.Player this Activity is using."""
265        return self._playertype

The type of Player this Activity is using.

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

The type of Team this Activity is using.

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

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

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

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

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

(called by the Actor base class)

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

The Session this Activity belongs to.

Raises a babase.SessionNotFoundError if the Session no longer exists.

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

Called when a new Player has joined the Activity.

(including the initial set of Players)

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

Called when a Player is leaving the Activity.

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

Called when a new Team joins the Activity.

(including the initial set of Teams)

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

Called when a Team leaves the Activity.

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

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 Activity.on_begin() is called.

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

Called when your activity begins transitioning out.

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

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

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:
383    def handlemessage(self, msg: Any) -> Any:
384        """General message handling; can be passed any message object."""
385        del msg  # Unused arg.
386        return UNHANDLED

General message handling; can be passed any message object.

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

Return whether Activity.on_transition_in() has run.

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

Return whether Activity.on_begin() has run.

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

Return whether the activity has commenced ending.

def is_transitioning_out(self) -> bool:
400    def is_transitioning_out(self) -> bool:
401        """Return whether bascenev1.Activity.on_transition_out() has run."""
402        return self._transitioning_out

Return whether Activity.on_transition_out() has run.

def transition_out(self) -> None:
462    def transition_out(self) -> None:
463        """Called by the Session to start us transitioning out."""
464        assert not self._transitioning_out
465        self._transitioning_out = True
466        with self.context:
467            try:
468                self.on_transition_out()
469            except Exception:
470                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:
500    def end(
501        self, results: Any = None, delay: float = 0.0, force: bool = False
502    ) -> None:
503        """Commences Activity shutdown and delivers results to the Session.
504
505        'delay' is the time delay before the Activity actually ends
506        (in seconds). Further calls to end() will be ignored up until
507        this time, unless 'force' is True, in which case the new results
508        will replace the old.
509        """
510
511        # Ask the session to end us.
512        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:
514    def create_player(self, sessionplayer: bascenev1.SessionPlayer) -> PlayerT:
515        """Create the Player instance for this Activity.
516
517        Subclasses can override this if the activity's player class
518        requires a custom constructor; otherwise it will be called with
519        no args. Note that the player object should not be used at this
520        point as it is not yet fully wired up; wait for
521        bascenev1.Activity.on_player_join() for that.
522        """
523        del sessionplayer  # Unused.
524        player = self._playertype()
525        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 Activity.on_player_join() for that.

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

High level logical entities in a Activity.

Category: Gameplay Classes

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 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 Actor.autoretain() method if you want an Actor to stick around until explicitly killed regardless of references.

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

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

Instantiates an Actor in the current Activity.

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

General message handling; can be passed any message object.

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

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

This keeps the Actor in existence by storing a reference to it with the Activity it was created in. The reference is lazily released once 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 Actor from dying. For convenience, this method returns the Actor it is called with, enabling chained statements such as: myflag = bascenev1.Flag().autoretain()

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

Called for remaining Actors when their activity dies.

Actors can use this opportunity to clear callbacks or other references which have the potential of keeping the 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
144    @property
145    def expired(self) -> bool:
146        """Whether the Actor is expired.
147
148        (see bascenev1.Actor.on_expire())
149        """
150        activity = self.getactivity(doraise=False)
151        return True if activity is None else activity.expired

Whether the Actor is expired.

(see Actor.on_expire())

def exists(self) -> bool:
153    def exists(self) -> bool:
154        """Returns whether the Actor is still present in a meaningful way.
155
156        Note that a dying character should still return True here as long as
157        their corpse is visible; this is about presence, not being 'alive'
158        (see bascenev1.Actor.is_alive() for that).
159
160        If this returns False, it is assumed the Actor can be completely
161        deleted without affecting the game; this call is often used
162        when pruning lists of Actors, such as with bascenev1.Actor.autoretain()
163
164        The default implementation of this method always return True.
165
166        Note that the boolean operator for the Actor class calls this method,
167        so a simple "if myactor" test will conveniently do the right thing
168        even if myactor is set to None.
169        """
170        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 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 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:
176    def is_alive(self) -> bool:
177        """Returns whether the Actor is 'alive'.
178
179        What this means is up to the Actor.
180        It is not a requirement for Actors to be able to die;
181        just that they report whether they consider themselves
182        to be alive or not. In cases where dead/alive is
183        irrelevant, True should be returned.
184        """
185        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
187    @property
188    def activity(self) -> bascenev1.Activity:
189        """The Activity this Actor was created in.
190
191        Raises a bascenev1.ActivityNotFoundError if the Activity no longer
192        exists.
193        """
194        activity = self._activity()
195        if activity is None:
196            raise babase.ActivityNotFoundError()
197        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:
211    def getactivity(self, doraise: bool = True) -> bascenev1.Activity | None:
212        """Return the bascenev1.Activity this Actor is associated with.
213
214        If the Activity no longer exists, raises a
215        bascenev1.ActivityNotFoundError or returns None depending on whether
216        'doraise' is True.
217        """
218        activity = self._activity()
219        if activity is None and doraise:
220            raise babase.ActivityNotFoundError()
221        return activity

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

Animate values on a target Node.

Category: Gameplay Functions

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

Animate an array of values on a target Node.

Category: Gameplay Functions

Like bs.animate, but operates on array attributes.

app = <babase.App object>
class AppIntent:
13class AppIntent:
14    """A high level directive given to the app.
15
16    Category: **App Classes**
17    """

A high level directive given to the app.

Category: App Classes

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

Tells the app to simply run in its default mode.

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

Tells the app to exec some Python code.

AppIntentExec(code: str)
27    def __init__(self, code: str):
28        self.code = code
code
class AppMode:
14class AppMode:
15    """A high level mode for the app.
16
17    Category: **App Classes**
18
19    """
20
21    @classmethod
22    def get_app_experience(cls) -> AppExperience:
23        """Return the overall experience provided by this mode."""
24        raise NotImplementedError('AppMode subclasses must override this.')
25
26    @classmethod
27    def can_handle_intent(cls, intent: AppIntent) -> bool:
28        """Return whether this mode can handle the provided intent.
29
30        For this to return True, the AppMode must claim to support the
31        provided intent (via its _supports_intent() method) AND the
32        AppExperience associated with the AppMode must be supported by
33        the current app and runtime environment.
34        """
35        # TODO: check AppExperience.
36        return cls._supports_intent(intent)
37
38    @classmethod
39    def _supports_intent(cls, intent: AppIntent) -> bool:
40        """Return whether our mode can handle the provided intent.
41
42        AppModes should override this to define what they can handle.
43        Note that AppExperience does not have to be considered here; that
44        is handled automatically by the can_handle_intent() call."""
45        raise NotImplementedError('AppMode subclasses must override this.')
46
47    def handle_intent(self, intent: AppIntent) -> None:
48        """Handle an intent."""
49        raise NotImplementedError('AppMode subclasses must override this.')
50
51    def on_activate(self) -> None:
52        """Called when the mode is being activated."""
53
54    def on_deactivate(self) -> None:
55        """Called when the mode is being deactivated."""
56
57    def on_app_active_changed(self) -> None:
58        """Called when babase.app.active changes.
59
60        The app-mode may want to take action such as pausing a running
61        game in such cases.
62        """

A high level mode for the app.

Category: App Classes

@classmethod
def get_app_experience(cls) -> bacommon.app.AppExperience:
21    @classmethod
22    def get_app_experience(cls) -> AppExperience:
23        """Return the overall experience provided by this mode."""
24        raise NotImplementedError('AppMode subclasses must override this.')

Return the overall experience provided by this mode.

@classmethod
def can_handle_intent(cls, intent: AppIntent) -> bool:
26    @classmethod
27    def can_handle_intent(cls, intent: AppIntent) -> bool:
28        """Return whether this mode can handle the provided intent.
29
30        For this to return True, the AppMode must claim to support the
31        provided intent (via its _supports_intent() method) AND the
32        AppExperience associated with the AppMode must be supported by
33        the current app and runtime environment.
34        """
35        # TODO: check AppExperience.
36        return cls._supports_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 _supports_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:
47    def handle_intent(self, intent: AppIntent) -> None:
48        """Handle an intent."""
49        raise NotImplementedError('AppMode subclasses must override this.')

Handle an intent.

def on_activate(self) -> None:
51    def on_activate(self) -> None:
52        """Called when the mode is being activated."""

Called when the mode is being activated.

def on_deactivate(self) -> None:
54    def on_deactivate(self) -> None:
55        """Called when the mode is being deactivated."""

Called when the mode is being deactivated.

def on_app_active_changed(self) -> None:
57    def on_app_active_changed(self) -> None:
58        """Called when babase.app.active changes.
59
60        The app-mode may want to take action such as pausing a running
61        game in such cases.
62        """

Called when babase.app.active changes.

The app-mode may want to take action such as pausing a running game in such cases.

AppTime = AppTime
def apptime() -> AppTime:
552def apptime() -> babase.AppTime:
553    """Return the current app-time in seconds.
554
555    Category: **General Utility Functions**
556
557    App-time is a monotonic time value; it starts at 0.0 when the app
558    launches and will never jump by large amounts or go backwards, even if
559    the system time changes. Its progression will pause when the app is in
560    a suspended state.
561
562    Note that the AppTime returned here is simply float; it just has a
563    unique type in the type-checker's eyes to help prevent it from being
564    accidentally used with time functionality expecting other time types.
565    """
566    import babase  # pylint: disable=cyclic-import
567
568    return babase.AppTime(0.0)

Return the current app-time in seconds.

Category: General Utility Functions

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:
571def apptimer(time: float, call: Callable[[], Any]) -> None:
572    """Schedule a callable object to run based on app-time.
573
574    Category: **General Utility Functions**
575
576    This function creates a one-off timer which cannot be canceled or
577    modified once created. If you require the ability to do so, or need
578    a repeating timer, use the babase.AppTimer class instead.
579
580    ##### Arguments
581    ###### time (float)
582    > Length of time in seconds that the timer will wait before firing.
583
584    ###### call (Callable[[], Any])
585    > A callable Python object. Note that the timer will retain a
586    strong reference to the callable for as long as the timer exists, so you
587    may want to look into concepts such as babase.WeakCall if that is not
588    desired.
589
590    ##### Examples
591    Print some stuff through time:
592    >>> babase.screenmessage('hello from now!')
593    >>> babase.apptimer(1.0, babase.Call(babase.screenmessage,
594                              'hello from the future!'))
595    >>> babase.apptimer(2.0, babase.Call(babase.screenmessage,
596    ...                       'hello from the future 2!'))
597    """
598    return None

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

Category: General Utility Functions

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 AppTimer class instead.

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 WeakCall if that is not desired.

Examples

Print some stuff through time:

>>> screenmessage('hello from now!')
>>> apptimer(1.0, Call(screenmessage,
                          'hello from the future!'))
>>> apptimer(2.0, Call(screenmessage,
...                       'hello from the future 2!'))
class AppTimer:
53class AppTimer:
54    """Timers are used to run code at later points in time.
55
56    Category: **General Utility Classes**
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.

Category: General Utility Classes

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 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 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(): ... screenmessage('BADGER!') ... def stop_saying_it(): ... global g_timer ... g_timer = None ... screenmessage('MUSHROOM MUSHROOM!') ... # Create our timer; it will run as long as we have the self.t ref. ... g_timer = AppTimer(0.3, say_it, repeat=True) ... # Now fire off a one-shot timer to kill it. ... 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):
299class AssetPackage(DependencyComponent):
300    """bascenev1.DependencyComponent representing a bundled package of assets.
301
302    Category: **Asset Classes**
303    """
304
305    def __init__(self) -> None:
306        super().__init__()
307
308        # This is used internally by the get_package_xxx calls.
309        self.context = babase.ContextRef()
310
311        entry = self._dep_entry()
312        assert entry is not None
313        assert isinstance(entry.config, str)
314        self.package_id = entry.config
315        print(f'LOADING ASSET PACKAGE {self.package_id}')
316
317    @override
318    @classmethod
319    def dep_is_present(cls, config: Any = None) -> bool:
320        assert isinstance(config, str)
321
322        # Temp: hard-coding for a single asset-package at the moment.
323        if config == 'stdassets@1':
324            return True
325        return False
326
327    def gettexture(self, name: str) -> bascenev1.Texture:
328        """Load a named bascenev1.Texture from the AssetPackage.
329
330        Behavior is similar to bascenev1.gettexture()
331        """
332        return _bascenev1.get_package_texture(self, name)
333
334    def getmesh(self, name: str) -> bascenev1.Mesh:
335        """Load a named bascenev1.Mesh from the AssetPackage.
336
337        Behavior is similar to bascenev1.getmesh()
338        """
339        return _bascenev1.get_package_mesh(self, name)
340
341    def getcollisionmesh(self, name: str) -> bascenev1.CollisionMesh:
342        """Load a named bascenev1.CollisionMesh from the AssetPackage.
343
344        Behavior is similar to bascenev1.getcollisionmesh()
345        """
346        return _bascenev1.get_package_collision_mesh(self, name)
347
348    def getsound(self, name: str) -> bascenev1.Sound:
349        """Load a named bascenev1.Sound from the AssetPackage.
350
351        Behavior is similar to bascenev1.getsound()
352        """
353        return _bascenev1.get_package_sound(self, name)
354
355    def getdata(self, name: str) -> bascenev1.Data:
356        """Load a named bascenev1.Data from the AssetPackage.
357
358        Behavior is similar to bascenev1.getdata()
359        """
360        return _bascenev1.get_package_data(self, name)

DependencyComponent representing a bundled package of assets.

Category: Asset Classes

AssetPackage()
305    def __init__(self) -> None:
306        super().__init__()
307
308        # This is used internally by the get_package_xxx calls.
309        self.context = babase.ContextRef()
310
311        entry = self._dep_entry()
312        assert entry is not None
313        assert isinstance(entry.config, str)
314        self.package_id = entry.config
315        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:
317    @override
318    @classmethod
319    def dep_is_present(cls, config: Any = None) -> bool:
320        assert isinstance(config, str)
321
322        # Temp: hard-coding for a single asset-package at the moment.
323        if config == 'stdassets@1':
324            return True
325        return False

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

def gettexture(self, name: str) -> _bascenev1.Texture:
327    def gettexture(self, name: str) -> bascenev1.Texture:
328        """Load a named bascenev1.Texture from the AssetPackage.
329
330        Behavior is similar to bascenev1.gettexture()
331        """
332        return _bascenev1.get_package_texture(self, name)

Load a named Texture from the AssetPackage.

Behavior is similar to gettexture()

def getmesh(self, name: str) -> _bascenev1.Mesh:
334    def getmesh(self, name: str) -> bascenev1.Mesh:
335        """Load a named bascenev1.Mesh from the AssetPackage.
336
337        Behavior is similar to bascenev1.getmesh()
338        """
339        return _bascenev1.get_package_mesh(self, name)

Load a named Mesh from the AssetPackage.

Behavior is similar to getmesh()

def getcollisionmesh(self, name: str) -> _bascenev1.CollisionMesh:
341    def getcollisionmesh(self, name: str) -> bascenev1.CollisionMesh:
342        """Load a named bascenev1.CollisionMesh from the AssetPackage.
343
344        Behavior is similar to bascenev1.getcollisionmesh()
345        """
346        return _bascenev1.get_package_collision_mesh(self, name)

Load a named CollisionMesh from the AssetPackage.

Behavior is similar to getcollisionmesh()

def getsound(self, name: str) -> _bascenev1.Sound:
348    def getsound(self, name: str) -> bascenev1.Sound:
349        """Load a named bascenev1.Sound from the AssetPackage.
350
351        Behavior is similar to bascenev1.getsound()
352        """
353        return _bascenev1.get_package_sound(self, name)

Load a named Sound from the AssetPackage.

Behavior is similar to getsound()

def getdata(self, name: str) -> _bascenev1.Data:
355    def getdata(self, name: str) -> bascenev1.Data:
356        """Load a named bascenev1.Data from the AssetPackage.
357
358        Behavior is similar to bascenev1.getdata()
359        """
360        return _bascenev1.get_package_data(self, name)

Load a named Data from the AssetPackage.

Behavior is similar to getdata()

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

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

Category: General Utility Functions

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

Category: General Utility Functions

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 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 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:
 80class BaseTimer:
 81    """Timers are used to run code at later points in time.
 82
 83    Category: **General Utility Classes**
 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.

Category: General Utility Classes

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 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 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):
26@dataclass
27class BoolSetting(Setting):
28    """A boolean game setting.
29
30    Category: Settings Classes
31    """
32
33    default: bool

A boolean game setting.

Category: Settings Classes

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

Create a strobing camera flash effect.

Category: Gameplay Functions

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

def camerashake(intensity: float = 1.0) -> None:
1028def camerashake(intensity: float = 1.0) -> None:
1029    """Shake the camera.
1030
1031    Category: **Gameplay Functions**
1032
1033    Note that some cameras and/or platforms (such as VR) may not display
1034    camera-shake, so do not rely on this always being visible to the
1035    player as a gameplay cue.
1036    """
1037    return None

Shake the camera.

Category: Gameplay Functions

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

Represents a unique set or series of Level-s.

Category: App Classes

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

The name of the Campaign.

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

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

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

Adds a Level to the Campaign.

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

The list of Level-s in the Campaign.

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

Return a contained Level by name.

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

Reset state for the Campaign.

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

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

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

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

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

Return the live config dict for this campaign.

@dataclass
class CelebrateMessage:
223@dataclass
224class CelebrateMessage:
225    """Tells an object to celebrate.
226
227    Category: **Message Classes**
228    """
229
230    duration: float = 10.0
231    """Amount of time to celebrate in seconds."""

Tells an object to celebrate.

Category: Message Classes

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

Amount of time to celebrate in seconds.

@dataclass
class ChoiceSetting(bascenev1.Setting):
62@dataclass
63class ChoiceSetting(Setting):
64    """A setting with multiple choices.
65
66    Category: Settings Classes
67    """
68
69    choices: list[tuple[str, Any]]

A setting with multiple choices.

Category: Settings Classes

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

A character/team selector for a Player.

Category: Gameplay Classes

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

The SessionPlayer associated with this chooser.

ready: bool
371    @property
372    def ready(self) -> bool:
373        """Whether this chooser is checked in as ready."""
374        return self._ready

Whether this chooser is checked in as ready.

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

Return this chooser's currently selected SessionTeam.

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

The chooser's Lobby.

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

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

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