bascenev1
Ballistica scene api version 1. Basically all gameplay related code.
1# Released under the MIT License. See LICENSE for details. 2# 3"""Ballistica scene api version 1. Basically all gameplay related code.""" 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 app, 22 AppIntent, 23 AppIntentDefault, 24 AppIntentExec, 25 AppMode, 26 apptime, 27 AppTime, 28 apptimer, 29 AppTimer, 30 Call, 31 ContextError, 32 ContextRef, 33 displaytime, 34 DisplayTime, 35 displaytimer, 36 DisplayTimer, 37 existing, 38 fade_screen, 39 get_remote_app_name, 40 increment_analytics_count, 41 InputType, 42 is_point_in_box, 43 lock_all_input, 44 Lstr, 45 NodeNotFoundError, 46 normalized_color, 47 NotFoundError, 48 PlayerNotFoundError, 49 Plugin, 50 pushcall, 51 safecolor, 52 screenmessage, 53 set_analytics_screen, 54 storagename, 55 timestring, 56 UIScale, 57 unlock_all_input, 58 Vec3, 59 WeakCall, 60) 61 62from _bascenev1 import ( 63 ActivityData, 64 basetime, 65 basetimer, 66 BaseTimer, 67 camerashake, 68 capture_gamepad_input, 69 capture_keyboard_input, 70 chatmessage, 71 client_info_query_response, 72 CollisionMesh, 73 connect_to_party, 74 Data, 75 disconnect_client, 76 disconnect_from_host, 77 emitfx, 78 end_host_scanning, 79 get_chat_messages, 80 get_connection_to_host_info, 81 get_foreground_host_activity, 82 get_foreground_host_session, 83 get_game_port, 84 get_game_roster, 85 get_local_active_input_devices_count, 86 get_public_party_enabled, 87 get_public_party_max_size, 88 get_random_names, 89 get_replay_speed_exponent, 90 get_ui_input_device, 91 getactivity, 92 getcollisionmesh, 93 getdata, 94 getinputdevice, 95 getmesh, 96 getnodes, 97 getsession, 98 getsound, 99 gettexture, 100 have_connected_clients, 101 have_touchscreen_input, 102 host_scan_cycle, 103 InputDevice, 104 is_in_replay, 105 ls_input_devices, 106 ls_objects, 107 Material, 108 Mesh, 109 new_host_session, 110 new_replay_session, 111 newactivity, 112 newnode, 113 Node, 114 printnodes, 115 protocol_version, 116 release_gamepad_input, 117 release_keyboard_input, 118 reset_random_player_names, 119 broadcastmessage, 120 SessionData, 121 SessionPlayer, 122 set_admins, 123 set_authenticate_clients, 124 set_debug_speed_exponent, 125 set_enable_default_kick_voting, 126 set_internal_music, 127 set_map_bounds, 128 set_master_server_source, 129 set_public_party_enabled, 130 set_public_party_max_size, 131 set_public_party_name, 132 set_public_party_queue_enabled, 133 set_public_party_stats_url, 134 set_replay_speed_exponent, 135 set_touchscreen_editing, 136 Sound, 137 Texture, 138 time, 139 timer, 140 Timer, 141) 142from bascenev1._activity import Activity 143from bascenev1._activitytypes import JoinActivity, ScoreScreenActivity 144from bascenev1._actor import Actor 145from bascenev1._appmode import SceneV1AppMode 146from bascenev1._campaign import init_campaigns, Campaign 147from bascenev1._collision import Collision, getcollision 148from bascenev1._coopgame import CoopGameActivity 149from bascenev1._coopsession import CoopSession 150from bascenev1._debug import print_live_object_warnings 151from bascenev1._dependency import ( 152 Dependency, 153 DependencyComponent, 154 DependencySet, 155 AssetPackage, 156) 157from bascenev1._dualteamsession import DualTeamSession 158from bascenev1._freeforallsession import FreeForAllSession 159from bascenev1._gameactivity import GameActivity 160from bascenev1._gameresults import GameResults 161from bascenev1._gameutils import ( 162 animate, 163 animate_array, 164 BaseTime, 165 cameraflash, 166 GameTip, 167 get_trophy_string, 168 show_damage_count, 169 Time, 170) 171from bascenev1._level import Level 172from bascenev1._lobby import Lobby, Chooser 173from bascenev1._map import ( 174 get_filtered_map_name, 175 get_map_class, 176 get_map_display_string, 177 Map, 178 register_map, 179) 180from bascenev1._messages import ( 181 CelebrateMessage, 182 DeathType, 183 DieMessage, 184 DropMessage, 185 DroppedMessage, 186 FreezeMessage, 187 HitMessage, 188 ImpactDamageMessage, 189 OutOfBoundsMessage, 190 PickedUpMessage, 191 PickUpMessage, 192 PlayerDiedMessage, 193 PlayerProfilesChangedMessage, 194 ShouldShatterMessage, 195 StandMessage, 196 ThawMessage, 197 UNHANDLED, 198) 199from bascenev1._multiteamsession import ( 200 MultiTeamSession, 201 DEFAULT_TEAM_COLORS, 202 DEFAULT_TEAM_NAMES, 203) 204from bascenev1._music import MusicType, setmusic 205from bascenev1._nodeactor import NodeActor 206from bascenev1._powerup import get_default_powerup_distribution 207from bascenev1._profile import ( 208 get_player_colors, 209 get_player_profile_icon, 210 get_player_profile_colors, 211) 212from bascenev1._player import PlayerInfo, Player, EmptyPlayer, StandLocation 213from bascenev1._playlist import ( 214 get_default_free_for_all_playlist, 215 get_default_teams_playlist, 216 filter_playlist, 217) 218from bascenev1._powerup import PowerupMessage, PowerupAcceptMessage 219from bascenev1._score import ScoreType, ScoreConfig 220from bascenev1._settings import ( 221 BoolSetting, 222 ChoiceSetting, 223 FloatChoiceSetting, 224 FloatSetting, 225 IntChoiceSetting, 226 IntSetting, 227 Setting, 228) 229from bascenev1._session import Session 230from bascenev1._stats import PlayerScoredMessage, PlayerRecord, Stats 231from bascenev1._team import SessionTeam, Team, EmptyTeam 232from bascenev1._teamgame import TeamGameActivity 233 234__all__ = [ 235 'Activity', 236 'ActivityData', 237 'Actor', 238 'animate', 239 'animate_array', 240 'app', 241 'AppIntent', 242 'AppIntentDefault', 243 'AppIntentExec', 244 'AppMode', 245 'AppTime', 246 'apptime', 247 'apptimer', 248 'AppTimer', 249 'AssetPackage', 250 'basetime', 251 'BaseTime', 252 'basetimer', 253 'BaseTimer', 254 'BoolSetting', 255 'Call', 256 'cameraflash', 257 'camerashake', 258 'Campaign', 259 'capture_gamepad_input', 260 'capture_keyboard_input', 261 'CelebrateMessage', 262 'chatmessage', 263 'ChoiceSetting', 264 'Chooser', 265 'client_info_query_response', 266 'Collision', 267 'CollisionMesh', 268 'connect_to_party', 269 'ContextError', 270 'ContextRef', 271 'CoopGameActivity', 272 'CoopSession', 273 'Data', 274 'DeathType', 275 'DEFAULT_TEAM_COLORS', 276 'DEFAULT_TEAM_NAMES', 277 'Dependency', 278 'DependencyComponent', 279 'DependencySet', 280 'DieMessage', 281 'disconnect_client', 282 'disconnect_from_host', 283 'displaytime', 284 'DisplayTime', 285 'displaytimer', 286 'DisplayTimer', 287 'DropMessage', 288 'DroppedMessage', 289 'DualTeamSession', 290 'emitfx', 291 'EmptyPlayer', 292 'EmptyTeam', 293 'end_host_scanning', 294 'existing', 295 'fade_screen', 296 'filter_playlist', 297 'FloatChoiceSetting', 298 'FloatSetting', 299 'FreeForAllSession', 300 'FreezeMessage', 301 'GameActivity', 302 'GameResults', 303 'GameTip', 304 'get_chat_messages', 305 'get_connection_to_host_info', 306 'get_default_free_for_all_playlist', 307 'get_default_teams_playlist', 308 'get_default_powerup_distribution', 309 'get_filtered_map_name', 310 'get_foreground_host_activity', 311 'get_foreground_host_session', 312 'get_game_port', 313 'get_game_roster', 314 'get_game_roster', 315 'get_local_active_input_devices_count', 316 'get_map_class', 317 'get_map_display_string', 318 'get_player_colors', 319 'get_player_profile_colors', 320 'get_player_profile_icon', 321 'get_public_party_enabled', 322 'get_public_party_max_size', 323 'get_random_names', 324 'get_remote_app_name', 325 'get_replay_speed_exponent', 326 'get_trophy_string', 327 'get_ui_input_device', 328 'getactivity', 329 'getcollision', 330 'getcollisionmesh', 331 'getdata', 332 'getinputdevice', 333 'getmesh', 334 'getnodes', 335 'getsession', 336 'getsound', 337 'gettexture', 338 'have_connected_clients', 339 'have_touchscreen_input', 340 'HitMessage', 341 'host_scan_cycle', 342 'ImpactDamageMessage', 343 'increment_analytics_count', 344 'init_campaigns', 345 'InputDevice', 346 'InputType', 347 'IntChoiceSetting', 348 'IntSetting', 349 'is_in_replay', 350 'is_point_in_box', 351 'JoinActivity', 352 'Level', 353 'Lobby', 354 'lock_all_input', 355 'ls_input_devices', 356 'ls_objects', 357 'Lstr', 358 'Map', 359 'Material', 360 'Mesh', 361 'MultiTeamSession', 362 'MusicType', 363 'new_host_session', 364 'new_replay_session', 365 'newactivity', 366 'newnode', 367 'Node', 368 'NodeActor', 369 'NodeNotFoundError', 370 'normalized_color', 371 'NotFoundError', 372 'OutOfBoundsMessage', 373 'PickedUpMessage', 374 'PickUpMessage', 375 'Player', 376 'PlayerDiedMessage', 377 'PlayerProfilesChangedMessage', 378 'PlayerInfo', 379 'PlayerNotFoundError', 380 'PlayerRecord', 381 'PlayerScoredMessage', 382 'Plugin', 383 'PowerupAcceptMessage', 384 'PowerupMessage', 385 'print_live_object_warnings', 386 'printnodes', 387 'protocol_version', 388 'pushcall', 389 'register_map', 390 'release_gamepad_input', 391 'release_keyboard_input', 392 'reset_random_player_names', 393 'safecolor', 394 'screenmessage', 395 'SceneV1AppMode', 396 'ScoreConfig', 397 'ScoreScreenActivity', 398 'ScoreType', 399 'broadcastmessage', 400 'Session', 401 'SessionData', 402 'SessionPlayer', 403 'SessionTeam', 404 'set_admins', 405 'set_analytics_screen', 406 'set_authenticate_clients', 407 'set_debug_speed_exponent', 408 'set_debug_speed_exponent', 409 'set_enable_default_kick_voting', 410 'set_internal_music', 411 'set_map_bounds', 412 'set_master_server_source', 413 'set_public_party_enabled', 414 'set_public_party_max_size', 415 'set_public_party_name', 416 'set_public_party_queue_enabled', 417 'set_public_party_stats_url', 418 'set_replay_speed_exponent', 419 'set_touchscreen_editing', 420 'setmusic', 421 'Setting', 422 'ShouldShatterMessage', 423 'show_damage_count', 424 'Sound', 425 'StandLocation', 426 'StandMessage', 427 'Stats', 428 'storagename', 429 'Team', 430 'TeamGameActivity', 431 'Texture', 432 'ThawMessage', 433 'time', 434 'Time', 435 'timer', 436 'Timer', 437 'timestring', 438 'UIScale', 439 'UNHANDLED', 440 'unlock_all_input', 441 'Vec3', 442 'WeakCall', 443] 444 445# We want stuff here to show up as bascenev1.Foo instead of 446# bascenev1._submodule.Foo. 447set_canonical_module_names(globals()) 448 449# Sanity check: we want to keep ballistica's dependencies and 450# bootstrapping order clearly defined; let's check a few particular 451# modules to make sure they never directly or indirectly import us 452# before their own execs complete. 453if __debug__: 454 for _mdl in 'babase', '_babase': 455 if not hasattr(__import__(_mdl), '_REACHED_END_OF_MODULE'): 456 logging.warning( 457 '%s was imported before %s finished importing;' 458 ' should not happen.', 459 __name__, 460 _mdl, 461 )
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 bascenev1.Session.
Category: Gameplay Classes
Examples of Activities include games, score-screens, cutscenes, etc. A bascenev1.Session has one 'current' Activity at any time, though their existence can overlap during transitions.
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.
The list of bascenev1.Team-s in the Activity. This gets populated just before on_begin() is called and is updated automatically as players join or leave the game. (at least in free-for-all mode where every player gets their own team; in teams mode there are always 2 teams regardless of the player count).
The list of bascenev1.Player-s in the Activity. This gets populated just before on_begin() is called and is updated automatically as players join or leave the game.
Whether to print every time a player dies. This can be pertinent in games such as Death-Match but can be annoying in games where it doesn't matter.
Joining activities are for waiting for initial player joins. They are treated slightly differently than regular activities, mainly in that all players are passed to the activity at once instead of as each joins.
Whether idle players can potentially be kicked (should not happen in menus/etc).
In vr mode, this determines whether overlay nodes (text, images, etc) are created at a fixed position in space or one that moves based on the current map. Generally this should be on for games and off for transitions/score-screens/etc. that persist between maps.
Set this to True to inherit slow motion setting from previous activity (useful for transitions to avoid hitches).
Set this to True to keep playing the music from the previous activity (without even restarting it).
Set this to true to inherit VR camera offsets from the previous activity (useful for preventing sporadic camera movement during transitions).
Set this to true to inherit (non-fixed) VR overlay positioning from the previous activity (useful for prevent sporadic overlay jostling during transitions).
Set this to true to inherit screen tint/vignette colors from the previous activity (useful to prevent sudden color changes during transitions).
Whether players should be allowed to join in the middle of this activity. Note that Sessions may not allow mid-activity-joins even if the activity says its ok.
If the activity fades or transitions in, it should set the length of time here so that previous activities will be kept alive for that long (avoiding 'holes' in the screen) This value is given in real-time seconds.
Is it ok to show an ad after this activity ends before showing the next activity?
The 'globals' bascenev1.Node for the activity. This contains various global controls and values.
The stats instance accessible while the activity is running.
If access is attempted before or after, raises a bascenev1.NotFoundError.
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.
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.
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'.
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 bascenev1.Actor to this Activity.
The reference will be lazily released once bascenev1.Actor.exists() returns False for the Actor. The bascenev1.Actor.autoretain() method is a convenient way to access this same functionality.
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 bascenev1.Actor to the bascenev1.Activity.
(called by the bascenev1.Actor base class)
The bascenev1.Session this bascenev1.Activity belongs to.
Raises a babase.SessionNotFoundError if the Session no longer exists.
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 bascenev1.Player has joined the Activity.
(including the initial set of Players)
348 def on_player_leave(self, player: PlayerT) -> None: 349 """Called when a bascenev1.Player is leaving the Activity."""
Called when a bascenev1.Player is leaving the Activity.
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 bascenev1.Team joins the Activity.
(including the initial set of Teams)
357 def on_team_leave(self, team: TeamT) -> None: 358 """Called when a bascenev1.Team leaves the Activity."""
Called when a bascenev1.Team leaves the Activity.
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 bascenev1.Activity.on_begin() is called.
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 bascenev1.Activity.end() has not been called.
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.
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.
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 bascenev1.Activity.on_transition_in() has run.
392 def has_begun(self) -> bool: 393 """Return whether bascenev1.Activity.on_begin() has run.""" 394 return self._has_begun
Return whether bascenev1.Activity.on_begin() has run.
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.
400 def is_transitioning_out(self) -> bool: 401 """Return whether bascenev1.Activity.on_transition_out() has run.""" 402 return self._transitioning_out
Return whether bascenev1.Activity.on_transition_out() has run.
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.
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.
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 bascenev1.Activity.on_player_join() for that.
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.
Inherited Members
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(self, doraise: Literal[True] = True) -> bascenev1.Activity: 203 ... 204 205 @overload 206 def getactivity(self, doraise: Literal[False]) -> bascenev1.Activity | None: 207 ... 208 209 def getactivity(self, doraise: bool = True) -> bascenev1.Activity | None: 210 """Return the bascenev1.Activity this Actor is associated with. 211 212 If the Activity no longer exists, raises a 213 bascenev1.ActivityNotFoundError or returns None depending on whether 214 'doraise' is True. 215 """ 216 activity = self._activity() 217 if activity is None and doraise: 218 raise babase.ActivityNotFoundError() 219 return activity
High level logical entities in a bascenev1.Activity.
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 bascenev1.Node, which is always explicitly created and destroyed and doesn't care how many Python references to it exist.
Note, however, that you can use the bascenev1.Actor.autoretain() method if you want an Actor to stick around until explicitly killed regardless of references.
Another key feature of bascenev1.Actor is its bascenev1.Actor.handlemessage() method, which takes a single arbitrary object as an argument. This provides a safe way to communicate between bascenev1.Actor, bascenev1.Activity, bascenev1.Session, and any other class providing a handlemessage() method. The most universally handled message type for Actors is the bascenev1.DieMessage.
Another way to kill the flag from the example above: We can safely call this on any type with a 'handlemessage' method (though its not guaranteed to always have a meaningful effect). In this case the Actor instance will still be around, but its bascenev1.Actor.exists() and bascenev1.Actor.is_alive() methods will both return False.
>>> self.flag.handlemessage(bascenev1.DieMessage())
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 bascenev1.Activity.
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.
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 bascenev1.Actor in existence by storing a reference to it with the bascenev1.Activity it was created in. The reference is lazily released once bascenev1.Actor.exists() returns False for it or when the Activity is set as expired. This can be a convenient alternative to storing references explicitly just to keep a bascenev1.Actor from dying. For convenience, this method returns the bascenev1.Actor it is called with, enabling chained statements such as: myflag = bascenev1.Flag().autoretain()
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 bascenev1.Actor
s when their activity dies.
Actors can use this opportunity to clear callbacks or other references which have the potential of keeping the bascenev1.Activity alive inadvertently (Activities can not exit cleanly while any Python references to them remain.)
Once an actor is expired (see bascenev1.Actor.is_expired()) it should no longer perform any game-affecting operations (creating, modifying, or deleting nodes, media, timers, etc.) Attempts to do so will likely result in errors.
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 bascenev1.Actor.is_alive() for that).
If this returns False, it is assumed the Actor can be completely deleted without affecting the game; this call is often used when pruning lists of Actors, such as with bascenev1.Actor.autoretain()
The default implementation of this method always return True.
Note that the boolean operator for the Actor class calls this method, so a simple "if myactor" test will conveniently do the right thing even if myactor is set to None.
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.
The Activity this Actor was created in.
Raises a bascenev1.ActivityNotFoundError if the Activity no longer exists.
209 def getactivity(self, doraise: bool = True) -> bascenev1.Activity | None: 210 """Return the bascenev1.Activity this Actor is associated with. 211 212 If the Activity no longer exists, raises a 213 bascenev1.ActivityNotFoundError or returns None depending on whether 214 'doraise' is True. 215 """ 216 activity = self._activity() 217 if activity is None and doraise: 218 raise babase.ActivityNotFoundError() 219 return activity
Return the bascenev1.Activity this Actor is associated with.
If the Activity no longer exists, raises a bascenev1.ActivityNotFoundError or returns None depending on whether 'doraise' is True.
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 bascenev1.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.
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 bascenev1.Node.
Category: Gameplay Functions
Like bs.animate, but operates on array attributes.
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
Tells the app to simply run in its default mode.
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.
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 return cls._supports_intent(intent) 36 37 @classmethod 38 def _supports_intent(cls, intent: AppIntent) -> bool: 39 """Return whether our mode can handle the provided intent. 40 41 AppModes should override this to define what they can handle. 42 Note that AppExperience does not have to be considered here; that 43 is handled automatically by the can_handle_intent() call.""" 44 raise NotImplementedError('AppMode subclasses must override this.') 45 46 def handle_intent(self, intent: AppIntent) -> None: 47 """Handle an intent.""" 48 raise NotImplementedError('AppMode subclasses must override this.') 49 50 def on_activate(self) -> None: 51 """Called when the mode is being activated.""" 52 53 def on_deactivate(self) -> None: 54 """Called when the mode is being deactivated."""
A high level mode for the app.
Category: App Classes
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.
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 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.
46 def handle_intent(self, intent: AppIntent) -> None: 47 """Handle an intent.""" 48 raise NotImplementedError('AppMode subclasses must override this.')
Handle an intent.
539def apptime() -> babase.AppTime: 540 """Return the current app-time in seconds. 541 542 Category: **General Utility Functions** 543 544 App-time is a monotonic time value; it starts at 0.0 when the app 545 launches and will never jump by large amounts or go backwards, even if 546 the system time changes. Its progression will pause when the app is in 547 a suspended state. 548 549 Note that the AppTime returned here is simply float; it just has a 550 unique type in the type-checker's eyes to help prevent it from being 551 accidentally used with time functionality expecting other time types. 552 """ 553 import babase # pylint: disable=cyclic-import 554 555 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.
558def apptimer(time: float, call: Callable[[], Any]) -> None: 559 """Schedule a callable object to run based on app-time. 560 561 Category: **General Utility Functions** 562 563 This function creates a one-off timer which cannot be canceled or 564 modified once created. If you require the ability to do so, or need 565 a repeating timer, use the babase.AppTimer class instead. 566 567 ##### Arguments 568 ###### time (float) 569 > Length of time in seconds that the timer will wait before firing. 570 571 ###### call (Callable[[], Any]) 572 > A callable Python object. Note that the timer will retain a 573 strong reference to the callable for as long as the timer exists, so you 574 may want to look into concepts such as babase.WeakCall if that is not 575 desired. 576 577 ##### Examples 578 Print some stuff through time: 579 >>> babase.screenmessage('hello from now!') 580 >>> babase.apptimer(1.0, babase.Call(babase.screenmessage, 581 'hello from the future!')) 582 >>> babase.apptimer(2.0, babase.Call(babase.screenmessage, 583 ... 'hello from the future 2!')) 584 """ 585 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 babase.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 babase.WeakCall if that is not desired.
Examples
Print some stuff through time:
>>> babase.screenmessage('hello from now!')
>>> babase.apptimer(1.0, babase.Call(babase.screenmessage,
'hello from the future!'))
>>> babase.apptimer(2.0, babase.Call(babase.screenmessage,
... 'hello from the future 2!'))
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 babase.apptimer() function instead to get a one-off timer.
Arguments
time
Length of time in seconds that the timer will wait before firing.
call
A callable Python object. Remember that the timer will retain a strong reference to the callable for as long as it exists, so you may want to look into concepts such as babase.WeakCall if that is not desired.
repeat
If True, the timer will fire repeatedly, with each successive firing having the same delay as the first.
Example
Use a Timer object to print repeatedly for a few seconds: ... def say_it(): ... babase.screenmessage('BADGER!') ... def stop_saying_it(): ... global g_timer ... g_timer = None ... babase.screenmessage('MUSHROOM MUSHROOM!') ... # Create our timer; it will run as long as we have the self.t ref. ... g_timer = babase.AppTimer(0.3, say_it, repeat=True) ... # Now fire off a one-shot timer to kill it. ... babase.apptimer(3.89, stop_saying_it)
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 @classmethod 318 def dep_is_present(cls, config: Any = None) -> bool: 319 assert isinstance(config, str) 320 321 # Temp: hard-coding for a single asset-package at the moment. 322 if config == 'stdassets@1': 323 return True 324 return False 325 326 def gettexture(self, name: str) -> bascenev1.Texture: 327 """Load a named bascenev1.Texture from the AssetPackage. 328 329 Behavior is similar to bascenev1.gettexture() 330 """ 331 return _bascenev1.get_package_texture(self, name) 332 333 def getmesh(self, name: str) -> bascenev1.Mesh: 334 """Load a named bascenev1.Mesh from the AssetPackage. 335 336 Behavior is similar to bascenev1.getmesh() 337 """ 338 return _bascenev1.get_package_mesh(self, name) 339 340 def getcollisionmesh(self, name: str) -> bascenev1.CollisionMesh: 341 """Load a named bascenev1.CollisionMesh from the AssetPackage. 342 343 Behavior is similar to bascenev1.getcollisionmesh() 344 """ 345 return _bascenev1.get_package_collision_mesh(self, name) 346 347 def getsound(self, name: str) -> bascenev1.Sound: 348 """Load a named bascenev1.Sound from the AssetPackage. 349 350 Behavior is similar to bascenev1.getsound() 351 """ 352 return _bascenev1.get_package_sound(self, name) 353 354 def getdata(self, name: str) -> bascenev1.Data: 355 """Load a named bascenev1.Data from the AssetPackage. 356 357 Behavior is similar to bascenev1.getdata() 358 """ 359 return _bascenev1.get_package_data(self, name)
bascenev1.DependencyComponent representing a bundled package of assets.
Category: Asset Classes
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.
317 @classmethod 318 def dep_is_present(cls, config: Any = None) -> bool: 319 assert isinstance(config, str) 320 321 # Temp: hard-coding for a single asset-package at the moment. 322 if config == 'stdassets@1': 323 return True 324 return False
Return whether this component/config is present on this device.
326 def gettexture(self, name: str) -> bascenev1.Texture: 327 """Load a named bascenev1.Texture from the AssetPackage. 328 329 Behavior is similar to bascenev1.gettexture() 330 """ 331 return _bascenev1.get_package_texture(self, name)
Load a named bascenev1.Texture from the AssetPackage.
Behavior is similar to bascenev1.gettexture()
333 def getmesh(self, name: str) -> bascenev1.Mesh: 334 """Load a named bascenev1.Mesh from the AssetPackage. 335 336 Behavior is similar to bascenev1.getmesh() 337 """ 338 return _bascenev1.get_package_mesh(self, name)
Load a named bascenev1.Mesh from the AssetPackage.
Behavior is similar to bascenev1.getmesh()
340 def getcollisionmesh(self, name: str) -> bascenev1.CollisionMesh: 341 """Load a named bascenev1.CollisionMesh from the AssetPackage. 342 343 Behavior is similar to bascenev1.getcollisionmesh() 344 """ 345 return _bascenev1.get_package_collision_mesh(self, name)
Load a named bascenev1.CollisionMesh from the AssetPackage.
Behavior is similar to bascenev1.getcollisionmesh()
347 def getsound(self, name: str) -> bascenev1.Sound: 348 """Load a named bascenev1.Sound from the AssetPackage. 349 350 Behavior is similar to bascenev1.getsound() 351 """ 352 return _bascenev1.get_package_sound(self, name)
Load a named bascenev1.Sound from the AssetPackage.
Behavior is similar to bascenev1.getsound()
354 def getdata(self, name: str) -> bascenev1.Data: 355 """Load a named bascenev1.Data from the AssetPackage. 356 357 Behavior is similar to bascenev1.getdata() 358 """ 359 return _bascenev1.get_package_data(self, name)
Load a named bascenev1.Data from the AssetPackage.
Behavior is similar to bascenev1.getdata()
Inherited Members
934def basetime() -> bascenev1.BaseTime: 935 """Return the base-time in seconds for the current scene-v1 context. 936 937 Category: **General Utility Functions** 938 939 Base-time is a time value that progresses at a constant rate for a scene, 940 even when the scene is sped up, slowed down, or paused. It may, however, 941 speed up or slow down due to replay speed adjustments or may slow down 942 if the cpu is overloaded. 943 Note that the value returned here is simply a float; it just has a 944 unique type in the type-checker's eyes to help prevent it from being 945 accidentally used with time functionality expecting other time types. 946 """ 947 import bascenev1 # pylint: disable=cyclic-import 948 949 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.
954def basetimer( 955 time: float, call: Callable[[], Any], repeat: bool = False 956) -> None: 957 """Schedule a call to run at a later point in scene base-time. 958 Base-time is a value that progresses at a constant rate for a scene, 959 even when the scene is sped up, slowed down, or paused. It may, 960 however, speed up or slow down due to replay speed adjustments or may 961 slow down if the cpu is overloaded. 962 963 Category: **General Utility Functions** 964 965 This function adds a timer to the current scene context. 966 This timer cannot be canceled or modified once created. If you 967 require the ability to do so, use the bascenev1.BaseTimer class 968 instead. 969 970 ##### Arguments 971 ###### time (float) 972 > Length of time in seconds that the timer will wait before firing. 973 974 ###### call (Callable[[], Any]) 975 > A callable Python object. Remember that the timer will retain a 976 strong reference to the callable for the duration of the timer, so you 977 may want to look into concepts such as babase.WeakCall if that is not 978 desired. 979 980 ###### repeat (bool) 981 > If True, the timer will fire repeatedly, with each successive 982 firing having the same delay as the first. 983 984 ##### Examples 985 Print some stuff through time: 986 >>> import bascenev1 as bs 987 >>> bs.screenmessage('hello from now!') 988 >>> bs.basetimer(1.0, bs.Call(bs.screenmessage, 'hello from the future!')) 989 >>> bs.basetimer(2.0, bs.Call(bs.screenmessage, 990 ... 'hello from the future 2!')) 991 """ 992 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 bascenev1.BaseTimer class instead.
Arguments
time (float)
Length of time in seconds that the timer will wait before firing.
call (Callable[[], Any])
A callable Python object. Remember that the timer will retain a strong reference to the callable for the duration of the timer, so you may want to look into concepts such as babase.WeakCall if that is not desired.
repeat (bool)
If True, the timer will fire repeatedly, with each successive firing having the same delay as the first.
Examples
Print some stuff through time:
>>> import bascenev1 as bs
>>> bs.screenmessage('hello from now!')
>>> bs.basetimer(1.0, bs.Call(bs.screenmessage, 'hello from the future!'))
>>> bs.basetimer(2.0, bs.Call(bs.screenmessage,
... 'hello from the future 2!'))
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 bascenev1.basetimer() function instead.
time (float)
Length of time in seconds that the timer will wait before firing.
call (Callable[[], Any])
A callable Python object. Remember that the timer will retain a strong reference to the callable for as long as it exists, so you may want to look into concepts such as babase.WeakCall if that is not desired.
repeat (bool)
If True, the timer will fire repeatedly, with each successive firing having the same delay as the first.
Example
Use a BaseTimer object to print repeatedly for a few seconds:
>>> import bascenev1 as bs
... def say_it():
... bs.screenmessage('BADGER!')
... def stop_saying_it():
... global g_timer
... g_timer = None
... bs.screenmessage('MUSHROOM MUSHROOM!')
... # Create our timer; it will run as long as we have the self.t ref.
... g_timer = bs.BaseTimer(0.3, say_it, repeat=True)
... # Now fire off a one-shot timer to kill it.
... bs.basetimer(3.89, stop_saying_it)
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
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.
1022def camerashake(intensity: float = 1.0) -> None: 1023 """Shake the camera. 1024 1025 Category: **Gameplay Functions** 1026 1027 Note that some cameras and/or platforms (such as VR) may not display 1028 camera-shake, so do not rely on this always being visible to the 1029 player as a gameplay cue. 1030 """ 1031 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.
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 return self.configdict.get('Selection', self._levels[0].name) 92 93 @property 94 def configdict(self) -> dict[str, Any]: 95 """Return the live config dict for this campaign.""" 96 val: dict[str, Any] = babase.app.config.setdefault( 97 'Campaigns', {} 98 ).setdefault(self._name, {}) 99 assert isinstance(val, dict) 100 return val
Represents a unique set or series of baclassic.Level-s.
Category: App Classes
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 baclassic.Level to the Campaign.
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 baclassic.Level by name.
78 def reset(self) -> None: 79 """Reset state for the Campaign.""" 80 babase.app.config.setdefault('Campaigns', {})[self._name] = {}
Reset state for the Campaign.
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).
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
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
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.name.startswith('TestInput') 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 bascenev1.Player.
Category: Gameplay Classes
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)
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.
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()
Set character/colors based on the current profile.
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.name.startswith('TestInput') 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]
Reload all player profiles.
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 )
Update this chooser's position.
526 def get_character_name(self) -> str: 527 """Return the selected character name.""" 528 return self._character_names[self._character_index]
Return the selected character name.
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