More type safety and simplifying

This commit is contained in:
Eric Froemling 2020-06-02 01:45:12 -07:00
parent ae0c9dde20
commit 24f2792234
33 changed files with 444 additions and 391 deletions

View File

@ -4132,16 +4132,16 @@
"assets/build/windows/x64/python.exe": "https://files.ballistica.net/cache/ba1/25/a7/dc87c1be41605eb6fefd0145144c",
"assets/build/windows/x64/python37.dll": "https://files.ballistica.net/cache/ba1/b9/e4/d912f56e42e9991bcbb4c804cfcb",
"assets/build/windows/x64/pythonw.exe": "https://files.ballistica.net/cache/ba1/6c/bb/b6f52c306aa4e88061510e96cefe",
"build/prefab/linux-server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/63/2c/d64f68f0e4ecbb55f3731ad95530",
"build/prefab/linux-server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/86/cd/c8c9b3e4a265ac816c5d249e4638",
"build/prefab/linux/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/29/92/c312438bf1dbdb599bd020a5c220",
"build/prefab/linux/release/ballisticacore": "https://files.ballistica.net/cache/ba1/68/75/6686aa5b6e0f88e1a40d444fd013",
"build/prefab/mac-server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/c1/bc/23b3f5d0929adf8c497accad24bb",
"build/prefab/mac-server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/81/07/d95b4d6354acf4003f70307ba7cb",
"build/prefab/mac/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/2c/bd/18dff8eda46be2d2edeee0a7ece9",
"build/prefab/mac/release/ballisticacore": "https://files.ballistica.net/cache/ba1/8d/64/a0beab66455ed6be694583fb6d53",
"build/prefab/windows-server/debug/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/a2/ba/1e71e85b38eed56a55f8e0d85821",
"build/prefab/windows-server/release/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/dd/38/d98994fed8732ab774e879b0c8a5",
"build/prefab/windows/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/2c/e0/97ebbdf740a3bfbc5324f510204e",
"build/prefab/windows/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/4e/69/2af7295ec87bcf480e57f41e19a7"
"build/prefab/linux-server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/15/7a/d4e39ad022b8365418ecee4d026b",
"build/prefab/linux-server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/ab/ad/7e371dfb7ed7b10d46f0bf9497d8",
"build/prefab/linux/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/92/28/faa6501d779b381dec7fb9ac19c5",
"build/prefab/linux/release/ballisticacore": "https://files.ballistica.net/cache/ba1/45/57/97d03c6230cfc4d9f0687249f408",
"build/prefab/mac-server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/d3/e1/a3a86c40804f1e724c59ee04f1a2",
"build/prefab/mac-server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/ae/78/7a6522e860506fe1046808ce7b0b",
"build/prefab/mac/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/08/71/09be3ad2f8cf99f885549a7296a0",
"build/prefab/mac/release/ballisticacore": "https://files.ballistica.net/cache/ba1/87/49/3c1d3c8a995df400e46df3470b02",
"build/prefab/windows-server/debug/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/43/d4/48ec85af308fd027d7dd06eaa650",
"build/prefab/windows-server/release/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/cc/1b/303089d3c1f3b7620c632ee864c3",
"build/prefab/windows/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/f2/55/890f1b498ccf5988dee573d5f3ca",
"build/prefab/windows/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/b9/b1/d96d693c3ad52cedd4f517fd9d94"
}

View File

@ -398,6 +398,7 @@
<w>curstate</w>
<w>curtime</w>
<w>curtimestr</w>
<w>customdata</w>
<w>customizebrowser</w>
<w>cutscenes</w>
<w>cval</w>
@ -604,6 +605,7 @@
<w>factoryclass</w>
<w>fallbacks</w>
<w>farthestpt</w>
<w>fback</w>
<w>fbase</w>
<w>fclose</w>
<w>fcmd</w>
@ -874,6 +876,7 @@
<w>icns</w>
<w>iconpicker</w>
<w>iconscale</w>
<w>iconsstorename</w>
<w>ident</w>
<w>idevices</w>
<w>ifeq</w>
@ -910,6 +913,7 @@
<w>infotextcolor</w>
<w>inidividual</w>
<w>initializers</w>
<w>initialplayerinfos</w>
<w>initing</w>
<w>inits</w>
<w>inmobi</w>
@ -1110,6 +1114,8 @@
<w>mapselect</w>
<w>maptype</w>
<w>markupbase</w>
<w>masktex</w>
<w>masktexstorename</w>
<w>mathmodule</w>
<w>mathnode</w>
<w>mathutils</w>
@ -1163,6 +1169,7 @@
<w>moduledir</w>
<w>modulefinder</w>
<w>modulename</w>
<w>modulepath</w>
<w>modutils</w>
<w>moola</w>
<w>mopaque</w>
@ -1204,6 +1211,7 @@
<w>myhome</w>
<w>myinput</w>
<w>mylist</w>
<w>mymodule</w>
<w>mynode</w>
<w>myobj</w>
<w>myprojname</w>
@ -1214,6 +1222,7 @@
<w>mypytype</w>
<w>mysound</w>
<w>mytextnode</w>
<w>mythingie</w>
<w>myweakcall</w>
<w>mywidget</w>
<w>namedarg</w>
@ -1360,6 +1369,7 @@
<w>pkgutil</w>
<w>playercast</w>
<w>playerdata</w>
<w>playerinfos</w>
<w>playerlostspaz</w>
<w>playernode</w>
<w>playerpt</w>
@ -1533,6 +1543,7 @@
<w>qrcode</w>
<w>qrencode</w>
<w>qual</w>
<w>qualname</w>
<w>quoprimime</w>
<w>rando</w>
<w>randommodule</w>
@ -1786,8 +1797,10 @@
<w>steelseries</w>
<w>stickman</w>
<w>storable</w>
<w>storagename</w>
<w>storedhash</w>
<w>storeitemui</w>
<w>storename</w>
<w>strftime</w>
<w>stringprep</w>
<w>stringptr</w>
@ -1913,6 +1926,7 @@
<w>thanvannispen</w>
<w>thelaststand</w>
<w>themself</w>
<w>thingie</w>
<w>threadtype</w>
<w>throwiness</w>
<w>timedisplay</w>
@ -2095,6 +2109,8 @@
<w>willeval</w>
<w>wincfg</w>
<w>wincount</w>
<w>winnergroup</w>
<w>winnergroups</w>
<w>winplt</w>
<w>winprj</w>
<w>winref</w>

View File

@ -34,7 +34,7 @@ NOTE: This file was autogenerated by gendummymodule; do not edit by hand.
"""
# (hash we can use to see if this file is out of date)
# SOURCES_HASH=247084187994045015646460724420234222242
# SOURCES_HASH=214847179904844500339904334878206016957
# I'm sorry Pylint. I know this file saddens you. Be strong.
# pylint: disable=useless-suppression
@ -822,16 +822,11 @@ class SessionPlayer:
This bool value will be True once the Player has completed
any lobby character/team selection.
team: ba.SessionTeam
sessionteam: ba.SessionTeam
The ba.SessionTeam this Player is on. If the SessionPlayer
is still in its lobby selecting a team/etc. then a
ba.SessionTeamNotFoundError will be raised.
sessiondata: Dict
A dict for use by the current ba.Session for
storing data associated with this player.
This persists for the duration of the session.
inputdevice: ba.InputDevice
The input device associated with the player.
@ -853,8 +848,7 @@ class SessionPlayer:
"""
id: int
in_game: bool
team: ba.SessionTeam
sessiondata: Dict
sessionteam: ba.SessionTeam
inputdevice: ba.InputDevice
color: Sequence[float]
highlight: Sequence[float]

View File

@ -73,7 +73,7 @@ from ba._campaign import Campaign
from ba._gameutils import (animate, animate_array, show_damage_count,
timestring, cameraflash)
from ba._general import (WeakCall, Call, existing, Existable,
verify_object_death)
verify_object_death, storagename)
from ba._level import Level
from ba._lobby import Lobby, Chooser
from ba._math import normalized_color, is_point_in_box, vec3validate

View File

@ -215,7 +215,7 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
self.lobby = None
self._stats: Optional[ba.Stats] = None
self._gamedata: Optional[dict] = {}
self._customdata: Optional[dict] = {}
def __del__(self) -> None:
@ -265,15 +265,15 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
"""
@property
def gamedata(self) -> dict:
def customdata(self) -> dict:
"""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.
"""
assert not self._expired
assert isinstance(self._gamedata, dict)
return self._gamedata
assert isinstance(self._customdata, dict)
return self._customdata
@property
def expired(self) -> bool:
@ -574,9 +574,9 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
def add_player(self, sessionplayer: ba.SessionPlayer) -> None:
"""(internal)"""
assert sessionplayer.team is not None
assert sessionplayer.sessionteam is not None
sessionplayer.resetinput()
sessionteam = sessionplayer.team
sessionteam = sessionplayer.sessionteam
assert sessionplayer in sessionteam.players
team = sessionteam.gameteam
assert team is not None
@ -608,7 +608,7 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
player: Any = sessionplayer.gameplayer
assert isinstance(player, self._playertype)
team: Any = sessionplayer.team.gameteam
team: Any = sessionplayer.sessionteam.gameteam
assert isinstance(team, self._teamtype)
assert player in team.players
@ -795,9 +795,9 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
print_exception(f'Error in Activity on_expire() for {self}.')
try:
self._gamedata = None
self._customdata = None
except Exception:
print_exception(f'Error clearing gamedata for {self}.')
print_exception(f'Error clearing customdata for {self}.')
# Don't want to be holding any delay-delete refs at this point.
self._prune_delay_deletes()

View File

@ -27,8 +27,8 @@ import _ba
from ba._activity import Activity
from ba._music import setmusic, MusicType
# False positive due to our class_generics_filter custom pylint filter.
from ba._player import Player # pylint: disable=W0611
from ba._team import Team # pylint: disable=W0611
from ba import _player
from ba import _team
if TYPE_CHECKING:
from typing import Any, Dict, Optional
@ -36,6 +36,17 @@ if TYPE_CHECKING:
from ba._lobby import JoinInfo
# Even though we don't need custom Player/Team types, define empty ones
# so we don't have ba.Player[Any] and ba.Team[Any] as our types which
# reduces type safety.
class Player(_player.Player['Team']):
"""Our player type for this game."""
class Team(_team.Team[Player]):
"""Our team type for this game."""
class EndSessionActivity(Activity[Player, Team]):
"""Special ba.Activity to fade out and end the current ba.Session."""
@ -158,7 +169,7 @@ class ScoreScreenActivity(Activity[Player, Team]):
self._custom_continue_message: Optional[ba.Lstr] = None
self._server_transitioning: Optional[bool] = None
def on_player_join(self, player: ba.Player) -> None:
def on_player_join(self, player: Player) -> None:
from ba import _general
super().on_player_join(player)
time_till_assign = max(
@ -224,7 +235,7 @@ class ScoreScreenActivity(Activity[Player, Team]):
# Otherwise end the activity normally.
self.end()
def _safe_assign(self, player: ba.Player) -> None:
def _safe_assign(self, player: Player) -> None:
# Just to be extra careful, don't assign if we're transitioning out.
# (though theoretically that would be ok).

View File

@ -341,37 +341,35 @@ class CoopSession(Session):
self.setactivity(_ba.new_activity(TransitionActivity))
else:
player_info: List[ba.PlayerInfo]
playerinfos: List[ba.PlayerInfo]
# Generic team games.
if isinstance(results, TeamGameResults):
player_info = results.get_player_info()
score = results.get_team_score(results.get_teams()[0])
playerinfos = results.playerinfos
score = results.get_team_score(results.sessionteams[0])
fail_message = None
score_order = ('decreasing' if results.get_lower_is_better()
else 'increasing')
if results.get_score_type() in (ScoreType.SECONDS,
ScoreType.MILLISECONDS):
score_type = 'time'
score_order = ('decreasing'
if results.lower_is_better else 'increasing')
if results.scoretype in (ScoreType.SECONDS,
ScoreType.MILLISECONDS):
scoretype = 'time'
# ScoreScreen wants hundredths of a second.
if score is not None:
if results.get_score_type() is ScoreType.SECONDS:
if results.scoretype is ScoreType.SECONDS:
score *= 100
elif (results.get_score_type() is
ScoreType.MILLISECONDS):
elif results.scoretype is ScoreType.MILLISECONDS:
score //= 10
else:
raise RuntimeError('FIXME')
else:
if results.get_score_type() is not ScoreType.POINTS:
print(f'Unknown ScoreType:'
f' "{results.get_score_type()}"')
score_type = 'points'
if results.scoretype is not ScoreType.POINTS:
print(f'Unknown ScoreType:' f' "{results.scoretype}"')
scoretype = 'points'
# Old coop-game-specific results; should migrate away from these.
else:
player_info = results.get('player_info')
playerinfos = results.get('playerinfos')
score = results['score'] if 'score' in results else None
fail_message = (results['fail_message']
if 'fail_message' in results else None)
@ -380,12 +378,12 @@ class CoopSession(Session):
activity_score_type = (activity.get_score_type() if isinstance(
activity, CoopGameActivity) else None)
assert activity_score_type is not None
score_type = activity_score_type
scoretype = activity_score_type
# Validate types.
if player_info is not None:
assert isinstance(player_info, list)
assert (isinstance(i, PlayerInfo) for i in player_info)
if playerinfos is not None:
assert isinstance(playerinfos, list)
assert (isinstance(i, PlayerInfo) for i in playerinfos)
# Looks like we were in a round - check the outcome and
# go from there.
@ -397,11 +395,11 @@ class CoopSession(Session):
self.setactivity(
_ba.new_activity(
CoopScoreScreen, {
'player_info': player_info,
'playerinfos': playerinfos,
'score': score,
'fail_message': fail_message,
'score_order': score_order,
'score_type': score_type,
'score_type': scoretype,
'outcome': outcome,
'campaign': self.campaign,
'level': self.campaign_level_name

View File

@ -52,18 +52,17 @@ class DualTeamSession(MultiTeamSession):
TeamVictoryScoreScreenActivity)
from bastd.activity.multiteamvictory import (
TeamSeriesVictoryScoreScreenActivity)
winners = results.get_winners()
winners = results.winnergroups
# If everyone has the same score, call it a draw.
if len(winners) < 2:
self.setactivity(_ba.new_activity(DrawScoreScreenActivity))
else:
winner = winners[0].teams[0]
winner.sessiondata['score'] += 1
winner.customdata['score'] += 1
# If a team has won, show final victory screen.
if winner.sessiondata['score'] >= (self._series_length -
1) / 2 + 1:
if winner.customdata['score'] >= (self._series_length - 1) / 2 + 1:
self.setactivity(
_ba.new_activity(TeamSeriesVictoryScoreScreenActivity,
{'winner': winner}))

View File

@ -76,7 +76,7 @@ class FreeForAllSession(MultiTeamSession):
TeamSeriesVictoryScoreScreenActivity)
from bastd.activity.freeforallvictory import (
FreeForAllVictoryScoreScreenActivity)
winners = results.get_winners()
winners = results.winnergroups
# If there's multiple players and everyone has the same score,
# call it a draw.
@ -91,20 +91,20 @@ class FreeForAllSession(MultiTeamSession):
for i, winner in enumerate(winners):
for team in winner.teams:
points = (point_awards[i] if i in point_awards else 0)
team.sessiondata['previous_score'] = (
team.sessiondata['score'])
team.sessiondata['score'] += points
team.customdata['previous_score'] = (
team.customdata['score'])
team.customdata['score'] += points
series_winners = [
team for team in self.teams
if team.sessiondata['score'] >= self._ffa_series_length
if team.customdata['score'] >= self._ffa_series_length
]
series_winners.sort(reverse=True,
key=lambda tm: (tm.sessiondata['score']))
key=lambda tm: (tm.customdata['score']))
if (len(series_winners) == 1
or (len(series_winners) > 1
and series_winners[0].sessiondata['score'] !=
series_winners[1].sessiondata['score'])):
and series_winners[0].customdata['score'] !=
series_winners[1].customdata['score'])):
self.setactivity(
_ba.new_activity(TeamSeriesVictoryScoreScreenActivity,
{'winner': series_winners[0]}))

View File

@ -284,7 +284,7 @@ class GameActivity(Activity[PlayerType, TeamType]):
# Holds some flattened info about the player set at the point
# when on_begin() is called.
self.initial_player_info: Optional[List[ba.PlayerInfo]] = None
self.initialplayerinfos: Optional[List[ba.PlayerInfo]] = None
# Go ahead and get our map loading.
self._map_type = _map.get_map_class(self._calc_map_name(settings))
@ -511,14 +511,14 @@ class GameActivity(Activity[PlayerType, TeamType]):
_ba.timer(2.5, self._show_tip)
# Store some basic info about players present at start time.
self.initial_player_info = [
self.initialplayerinfos = [
PlayerInfo(name=p.getname(full=True), character=p.character)
for p in self.players
]
# Sort this by name so high score lists/etc will be consistent
# regardless of player join order.
self.initial_player_info.sort(key=lambda x: x.name)
self.initialplayerinfos.sort(key=lambda x: x.name)
# If this is a tournament, query info about it such as how much
# time is left.
@ -890,9 +890,10 @@ class GameActivity(Activity[PlayerType, TeamType]):
if player.actor and not self.has_ended():
from bastd.actor.respawnicon import RespawnIcon
player.gamedata['respawn_timer'] = _ba.Timer(
player.customdata['respawn_timer'] = _ba.Timer(
respawn_time, WeakCall(self.spawn_player_if_exists, player))
player.gamedata['respawn_icon'] = RespawnIcon(player, respawn_time)
player.customdata['respawn_icon'] = RespawnIcon(
player, respawn_time)
def spawn_player_if_exists(self, player: PlayerType) -> None:
"""

View File

@ -26,8 +26,6 @@ import weakref
from dataclasses import dataclass
from typing import TYPE_CHECKING
from ba._team import Team
if TYPE_CHECKING:
from weakref import ReferenceType
from typing import Sequence, Tuple, Any, Optional, Dict, List, Union
@ -56,56 +54,54 @@ class TeamGameResults:
self._game_set = False
self._scores: Dict[int, Tuple[ReferenceType[ba.SessionTeam],
Optional[int]]] = {}
self._teams: Optional[List[ReferenceType[ba.SessionTeam]]] = None
self._player_info: Optional[List[ba.PlayerInfo]] = None
self._sessionteams: Optional[List[ReferenceType[
ba.SessionTeam]]] = None
self._playerinfos: Optional[List[ba.PlayerInfo]] = None
self._lower_is_better: Optional[bool] = None
self._score_label: Optional[str] = None
self._none_is_winner: Optional[bool] = None
self._score_type: Optional[ba.ScoreType] = None
self._scoretype: Optional[ba.ScoreType] = None
def set_game(self, game: ba.GameActivity) -> None:
"""Set the game instance these results are applying to."""
if self._game_set:
raise RuntimeError('Game set twice for TeamGameResults.')
self._game_set = True
self._teams = [weakref.ref(team) for team in game.teams]
self._sessionteams = [weakref.ref(team) for team in game.teams]
score_info = game.get_score_info()
self._player_info = copy.deepcopy(game.initial_player_info)
self._playerinfos = copy.deepcopy(game.initialplayerinfos)
self._lower_is_better = score_info.lower_is_better
self._score_label = score_info.label
self._none_is_winner = score_info.none_is_winner
self._score_type = score_info.scoretype
self._scoretype = score_info.scoretype
def set_team_score(self, team: Union[ba.SessionTeam, ba.Team],
score: Optional[int]) -> None:
def set_team_score(self, team: ba.Team, score: Optional[int]) -> None:
"""Set the score for a given ba.Team.
This can be a number or None.
(see the none_is_winner arg in the constructor)
"""
if isinstance(team, Team):
team = team.sessionteam
self._scores[team.id] = (weakref.ref(team), score)
sessionteam = team.sessionteam
self._scores[sessionteam.id] = (weakref.ref(sessionteam), score)
def get_team_score(self, team: Union[ba.SessionTeam,
ba.Team]) -> Optional[int]:
"""Return the score for a given team."""
if isinstance(team, Team):
team = team.sessionteam
def get_team_score(self,
sessionteam: Union[ba.SessionTeam]) -> Optional[int]:
"""Return the score for a given ba.SessionTeam."""
for score in list(self._scores.values()):
if score[0]() is team:
if score[0]() is sessionteam:
return score[1]
# If we have no score value, assume None.
return None
def get_teams(self) -> List[ba.SessionTeam]:
@property
def sessionteams(self) -> List[ba.SessionTeam]:
"""Return all ba.SessionTeams in the results."""
if not self._game_set:
raise RuntimeError("Can't get teams until game is set.")
teams = []
assert self._teams is not None
for team_ref in self._teams:
assert self._sessionteams is not None
for team_ref in self._sessionteams:
team = team_ref()
if team is not None:
teams.append(team)
@ -130,55 +126,61 @@ class TeamGameResults:
if score[0]() is team.sessionteam:
if score[1] is None:
return Lstr(value='-')
if self._score_type is ScoreType.SECONDS:
if self._scoretype is ScoreType.SECONDS:
return timestring(score[1] * 1000,
centi=False,
timeformat=TimeFormat.MILLISECONDS)
if self._score_type is ScoreType.MILLISECONDS:
if self._scoretype is ScoreType.MILLISECONDS:
return timestring(score[1],
centi=True,
timeformat=TimeFormat.MILLISECONDS)
return Lstr(value=str(score[1]))
return Lstr(value='-')
def get_player_info(self) -> List[ba.PlayerInfo]:
@property
def playerinfos(self) -> List[ba.PlayerInfo]:
"""Get info about the players represented by the results."""
if not self._game_set:
raise RuntimeError("Can't get player-info until game is set.")
assert self._player_info is not None
return self._player_info
assert self._playerinfos is not None
return self._playerinfos
def get_score_type(self) -> ba.ScoreType:
"""Get the type of score."""
@property
def scoretype(self) -> ba.ScoreType:
"""The type of score."""
if not self._game_set:
raise RuntimeError("Can't get score-type until game is set.")
assert self._score_type is not None
return self._score_type
assert self._scoretype is not None
return self._scoretype
def get_score_name(self) -> str:
"""Get the name associated with scores ('points', etc)."""
@property
def score_label(self) -> str:
"""The label associated with scores ('points', etc)."""
if not self._game_set:
raise RuntimeError("Can't get score-name until game is set.")
raise RuntimeError("Can't get score-label until game is set.")
assert self._score_label is not None
return self._score_label
def get_lower_is_better(self) -> bool:
"""Return whether lower scores are better."""
@property
def lower_is_better(self) -> bool:
"""Whether lower scores are better."""
if not self._game_set:
raise RuntimeError("Can't get lower-is-better until game is set.")
assert self._lower_is_better is not None
return self._lower_is_better
def get_winning_team(self) -> Optional[ba.SessionTeam]:
"""Get the winning ba.Team if there is exactly one; None otherwise."""
@property
def winning_team(self) -> Optional[ba.SessionTeam]:
"""The winning ba.SessionTeam if there is exactly one, or else None."""
if not self._game_set:
raise RuntimeError("Can't get winners until game is set.")
winners = self.get_winners()
winners = self.winnergroups
if winners and len(winners[0].teams) == 1:
return winners[0].teams[0]
return None
def get_winners(self) -> List[WinnerGroup]:
@property
def winnergroups(self) -> List[WinnerGroup]:
"""Get an ordered list of winner groups."""
if not self._game_set:
raise RuntimeError("Can't get winners until game is set.")
@ -200,17 +202,18 @@ class TeamGameResults:
results.sort(reverse=not self._lower_is_better, key=lambda x: x[0])
# Also group the 'None' scores.
none_teams: List[ba.SessionTeam] = []
none_sessionteams: List[ba.SessionTeam] = []
for score in self._scores.values():
scoreteam = score[0]()
if scoreteam is not None and score[1] is None:
none_teams.append(scoreteam)
none_sessionteams.append(scoreteam)
# Add the Nones to the list (either as winners or losers
# depending on the rules).
if none_teams:
nones: List[Tuple[Optional[int],
List[ba.SessionTeam]]] = [(None, none_teams)]
if none_sessionteams:
nones: List[Tuple[Optional[int], List[ba.SessionTeam]]] = [
(None, none_sessionteams)
]
if self._none_is_winner:
results = nones + results
else:

View File

@ -25,6 +25,7 @@ import gc
import types
import weakref
import random
import inspect
from typing import TYPE_CHECKING, TypeVar
from typing_extensions import Protocol
@ -332,7 +333,45 @@ def _verify_object_death(wref: ReferenceType) -> None:
print(f'{Clr.YLW}Active References:{Clr.RST}')
i = 1
for ref in refs:
# if isinstance(ref, types.FrameType):
# continue
print(f'{Clr.YLW} reference {i}:{Clr.BLU} {ref}{Clr.RST}')
i += 1
def storagename(basename: str) -> str:
"""Generate a (hopefully) unique name for storing things in public places.
Category: General Utility Functions
This consists of a leading underscore, the module path at the
call site with dots replaced by underscores, the class name, and
the provided suffix. When storing data in public places such as
'customdata' dicts, this minimizes the chance of collisions if a
module or class is duplicated or renamed.
# Example: generate a unique name for storage purposes:
class MyThingie:
# This will give something like '_mymodule_submodule_mythingie_data'.
_STORENAME = ba.storagename('data')
def __init__(self, activity):
# Store some data in the Activity we were passed
activity.customdata[self._STORENAME] = {}
"""
frame = inspect.currentframe()
if frame is None:
raise RuntimeError('Cannot get current stack frame.')
fback = frame.f_back
if fback is None:
raise RuntimeError('Cannot get parent stack frame.')
modulepath = fback.f_globals.get('__name__')
if modulepath is None:
raise RuntimeError('Cannot get parent stack module path.')
assert isinstance(modulepath, str)
qualname = fback.f_locals.get('__qualname__')
if qualname is not None:
assert isinstance(qualname, str)
fullpath = f'_{modulepath}_{qualname.lower()}_{basename}'
else:
fullpath = f'_{modulepath}_{basename}'
return fullpath.replace('.', '_')

View File

@ -156,7 +156,7 @@ class MultiTeamSession(Session):
return self._game_number
def on_team_join(self, team: ba.SessionTeam) -> None:
team.sessiondata['previous_score'] = team.sessiondata['score'] = 0
team.customdata['previous_score'] = team.customdata['score'] = 0
def get_max_players(self) -> int:
"""Return max number of ba.Players allowed to join the game at once."""
@ -174,7 +174,8 @@ class MultiTeamSession(Session):
from bastd.tutorial import TutorialActivity
from bastd.activity.multiteamvictory import (
TeamSeriesVictoryScoreScreenActivity)
from ba import _activitytypes
from ba._activitytypes import (TransitionActivity, JoinActivity,
ScoreScreenActivity)
# If we have a tutorial to show, that's the first thing we do no
# matter what.
@ -186,22 +187,20 @@ class MultiTeamSession(Session):
# to transition us into a round gracefully (otherwise we'd snap from
# one terrain to another instantly).
elif isinstance(activity, TutorialActivity):
self.setactivity(
_ba.new_activity(_activitytypes.TransitionActivity))
self.setactivity(_ba.new_activity(TransitionActivity))
# If we're in a between-round activity or a restart-activity, hop
# into a round.
elif isinstance(
activity,
(_activitytypes.JoinActivity, _activitytypes.TransitionActivity,
_activitytypes.ScoreScreenActivity)):
(JoinActivity, TransitionActivity, ScoreScreenActivity)):
# If we're coming from a series-end activity, reset scores.
if isinstance(activity, TeamSeriesVictoryScoreScreenActivity):
self.stats.reset()
self._game_number = 0
for team in self.teams:
team.sessiondata['score'] = 0
team.customdata['score'] = 0
# Otherwise just set accum (per-game) scores.
else:
@ -221,7 +220,7 @@ class MultiTeamSession(Session):
# ..but only ones who have been placed on a team
# (ie: no longer sitting in the lobby).
try:
has_team = (player.team is not None)
has_team = (player.sessionteam is not None)
except NotFoundError:
has_team = False
if has_team:
@ -263,7 +262,7 @@ class MultiTeamSession(Session):
_ba.timer(delay, Call(_ba.playsound, _ba.getsound('boxingBell')))
if announce_winning_team:
winning_team = results.get_winning_team()
winning_team = results.winning_team
if winning_team is not None:
# Have all players celebrate.
celebrate_msg = CelebrateMessage(duration=10.0)

View File

@ -23,7 +23,7 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import TYPE_CHECKING, TypeVar, Generic
from typing import TYPE_CHECKING, TypeVar, Generic, cast
import _ba
from ba._error import SessionPlayerNotFoundError, print_exception
@ -85,8 +85,7 @@ class Player(Generic[TeamType]):
_nodeactor: Optional[ba.NodeActor]
_expired: bool
_postinited: bool
sessiondata: dict
_gamedata: dict
_customdata: dict
# NOTE: avoiding having any __init__() here since it seems to not
# get called by default if a dataclass inherits from us.
@ -118,10 +117,9 @@ class Player(Generic[TeamType]):
self.character = sessionplayer.character
self.color = sessionplayer.color
self.highlight = sessionplayer.highlight
self._team = sessionplayer.team.gameteam # type: ignore
self._team = cast(TeamType, sessionplayer.sessionteam.gameteam)
assert self._team is not None
self.sessiondata = sessionplayer.sessiondata
self._gamedata = {}
self._customdata = {}
self._expired = False
self._postinited = True
node = _ba.newnode('player', attrs={'playerID': sessionplayer.id})
@ -144,7 +142,7 @@ class Player(Generic[TeamType]):
print_exception(f'Error killing actor on leave for {self}')
self._nodeactor = None
del self._team
del self._gamedata
del self._customdata
def expire(self) -> None:
"""Called when the Player is expiring (when its Activity does so).
@ -163,7 +161,7 @@ class Player(Generic[TeamType]):
self._nodeactor = None
self.actor = None
del self._team
del self._gamedata
del self._customdata
def on_expire(self) -> None:
"""Can be overridden to handle player expiration.
@ -182,7 +180,7 @@ class Player(Generic[TeamType]):
return self._team
@property
def gamedata(self) -> dict:
def customdata(self) -> dict:
"""Arbitrary values associated with the player.
Though it is encouraged that most player values be properly defined
on the ba.Player subclass, it may be useful for player-agnostic
@ -193,7 +191,7 @@ class Player(Generic[TeamType]):
"""
assert self._postinited
assert not self._expired
return self._gamedata
return self._customdata
@property
def sessionplayer(self) -> ba.SessionPlayer:

View File

@ -259,7 +259,7 @@ class Session:
else:
# Ok, they've already entered the game. Remove them from
# teams/activities/etc.
sessionteam = sessionplayer.team
sessionteam = sessionplayer.sessionteam
assert sessionteam is not None
_ba.screenmessage(
@ -317,7 +317,7 @@ class Session:
else:
print('Team no in Session teams in on_player_leave.')
try:
sessionteam.reset_sessiondata()
sessionteam.leave()
except Exception:
print_exception(f'Error clearing sessiondata'
f' for team {sessionteam} in session {self}.')

View File

@ -74,7 +74,7 @@ class PlayerRecord:
self._multi_kill_timer: Optional[ba.Timer] = None
self._multi_kill_count = 0
self._stats = weakref.ref(stats)
self._last_player: Optional[ba.SessionPlayer] = None
self._last_sessionplayer: Optional[ba.SessionPlayer] = None
self._player: Optional[ba.SessionPlayer] = None
self._team: Optional[ReferenceType[ba.SessionTeam]] = None
self.streak = 0
@ -110,7 +110,7 @@ class PlayerRecord:
def get_icon(self) -> Dict[str, Any]:
"""Get the icon for this instance's player."""
player = self._last_player
player = self._last_sessionplayer
assert player is not None
return player.get_icon()
@ -127,12 +127,12 @@ class PlayerRecord:
return stats.getactivity()
return None
def associate_with_player(self, player: ba.SessionPlayer) -> None:
"""Associate this entry with a ba.Player."""
self._team = weakref.ref(player.team)
self.character = player.character
self._last_player = player
self._player = player
def associate_with_player(self, sessionplayer: ba.SessionPlayer) -> None:
"""Associate this entry with a ba.SessionPlayer."""
self._team = weakref.ref(sessionplayer.sessionteam)
self.character = sessionplayer.character
self._last_sessionplayer = sessionplayer
self._player = sessionplayer
self.streak = 0
def _end_multi_kill(self) -> None:
@ -141,8 +141,8 @@ class PlayerRecord:
def get_last_player(self) -> ba.SessionPlayer:
"""Return the last ba.Player we were associated with."""
assert self._last_player is not None
return self._last_player
assert self._last_sessionplayer is not None
return self._last_sessionplayer
def submit_kill(self, showpoints: bool = True) -> None:
"""Submit a kill for this player entry."""
@ -307,15 +307,14 @@ class Stats:
def register_player(self, player: ba.SessionPlayer) -> None:
"""Register a player with this score-set."""
assert player.exists() # Invalid refs should never be passed to funcs.
name = player.getname()
name_full = player.getname(full=True)
try:
if name in self._player_records:
# If the player already exists, update his character and such as
# it may have changed.
self._player_records[name].associate_with_player(player)
except Exception:
# FIXME: Shouldn't use top level Exception catch for logic.
# Should only have this as a fallback and always log it.
else:
name_full = player.getname(full=True)
self._player_records[name] = PlayerRecord(name, name_full, player,
self)
@ -331,11 +330,6 @@ class Stats:
records[record_id] = record
return records
def player_got_hit(self, player: ba.SessionPlayer) -> None:
"""Call this when a player got hit."""
s_player = self._player_records[player.getname()]
s_player.streak = 0
def player_scored(self,
player: ba.Player,
base_points: int = 1,

View File

@ -56,15 +56,10 @@ class SessionTeam:
players
The list of ba.SessionPlayers on the team.
gamedata
A dict for use by the current ba.Activity
for storing data associated with this team.
This gets cleared for each new ba.Activity.
sessiondata
customdata
A dict for use by the current ba.Session for
storing data associated with this team.
Unlike gamedata, this persists for the duration
Unlike customdata, this persists for the duration
of the session.
"""
@ -72,8 +67,7 @@ class SessionTeam:
name: Union[ba.Lstr, str]
color: Tuple[float, ...] # FIXME: can't we make this fixed len?
players: List[ba.SessionPlayer]
gamedata: Dict
sessiondata: Dict
customdata: dict
id: int
def __init__(self,
@ -90,12 +84,12 @@ class SessionTeam:
self.name = name
self.color = tuple(color)
self.players = []
self.sessiondata = {}
self.customdata = {}
self.gameteam: Optional[Team] = None
def reset_sessiondata(self) -> None:
def leave(self) -> None:
"""(internal)"""
self.sessiondata = {}
self.customdata = {}
PlayerType = TypeVar('PlayerType', bound='ba.Player')
@ -119,10 +113,7 @@ class Team(Generic[PlayerType]):
_sessionteam: ReferenceType[SessionTeam]
_expired: bool
_postinited: bool
_gamedata: dict
# TODO: kill these.
sessiondata: dict
_customdata: dict
# NOTE: avoiding having any __init__() here since it seems to not
# get called by default if a dataclass inherits from us.
@ -149,8 +140,7 @@ class Team(Generic[PlayerType]):
self.id = sessionteam.id
self.name = sessionteam.name
self.color = sessionteam.color
self.sessiondata = sessionteam.sessiondata
self._gamedata = {}
self._customdata = {}
self._expired = False
self._postinited = True
@ -160,13 +150,12 @@ class Team(Generic[PlayerType]):
self.id = team_id
self.name = name
self.color = color
self._gamedata = {}
self.sessiondata = {}
self._customdata = {}
self._expired = False
self._postinited = True
@property
def gamedata(self) -> dict:
def customdata(self) -> dict:
"""Arbitrary values associated with the team.
Though it is encouraged that most player values be properly defined
on the ba.Team subclass, it may be useful for player-agnostic
@ -177,7 +166,7 @@ class Team(Generic[PlayerType]):
"""
assert self._postinited
assert not self._expired
return self._gamedata
return self._customdata
def leave(self) -> None:
"""Called when the Team leaves a running game.
@ -186,7 +175,7 @@ class Team(Generic[PlayerType]):
"""
assert self._postinited
assert not self._expired
del self._gamedata
del self._customdata
del self.players
def expire(self) -> None:
@ -203,7 +192,7 @@ class Team(Generic[PlayerType]):
except Exception:
print_exception(f'Error in on_expire for {self}.')
del self._gamedata
del self._customdata
del self.players
def on_expire(self) -> None:

View File

@ -137,9 +137,9 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]):
self._tournament_time_remaining_text: Optional[Text] = None
self._tournament_time_remaining_text_timer: Optional[ba.Timer] = None
self._player_info: List[ba.PlayerInfo] = settings['player_info']
assert isinstance(self._player_info, list)
assert (isinstance(i, ba.PlayerInfo) for i in self._player_info)
self._playerinfos: List[ba.PlayerInfo] = settings['playerinfos']
assert isinstance(self._playerinfos, list)
assert (isinstance(i, ba.PlayerInfo) for i in self._playerinfos)
self._score: Optional[int] = settings['score']
assert isinstance(self._score, (int, type(None)))
@ -174,7 +174,7 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]):
self._game_name_str = self._campaign.name + ':' + self._level_name
self._game_config_str = str(len(
self._player_info)) + 'p' + self._campaign.get_level(
self._playerinfos)) + 'p' + self._campaign.get_level(
self._level_name).get_score_version_string().replace(' ', '_')
# If game-center/etc scores are available we show our friends'
@ -580,12 +580,12 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]):
ba.timer(5.2, ba.Call(ba.playsound, self._dingsound))
offs_x = -195
if len(self._player_info) > 1:
if len(self._playerinfos) > 1:
pstr = ba.Lstr(value='- ${A} -',
subs=[('${A}',
ba.Lstr(resource='multiPlayerCountText',
subs=[('${COUNT}',
str(len(self._player_info)))
str(len(self._playerinfos)))
]))])
else:
pstr = ba.Lstr(value='- ${A} -',
@ -636,7 +636,7 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]):
ba.pushcall(ba.WeakCall(self._show_fail))
self._name_str = name_str = ', '.join(
[p.name for p in self._player_info])
[p.name for p in self._playerinfos])
if self._show_friend_scores:
self._friends_loading_status = Text(
@ -662,10 +662,10 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]):
our_high_scores_all = self._campaign.get_level(
self._level_name).get_high_scores()
try:
our_high_scores = our_high_scores_all[str(len(self._player_info)) +
our_high_scores = our_high_scores_all[str(len(self._playerinfos)) +
' Player']
except Exception:
our_high_scores = our_high_scores_all[str(len(self._player_info)) +
our_high_scores = our_high_scores_all[str(len(self._playerinfos)) +
' Player'] = []
if self._score is not None:
@ -674,7 +674,7 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]):
'players': [{
'name': p.name,
'character': p.character
} for p in self._player_info]
} for p in self._playerinfos]
}
]
our_high_scores.append(our_score)
@ -789,7 +789,7 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]):
v_offs_extra = 20
v_offs_names = 0
scale = 1.0
p_count = len(self._player_info)
p_count = len(self._playerinfos)
h_offs_extra -= 75
if p_count > 1:
h_offs_extra -= 20
@ -841,7 +841,7 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]):
position=(ts_h_offs + 35 + h_offs_extra,
v_offs_extra + ts_height / 2 + -ts_height *
(i + 1) / 10 + v_offs_names + v_offs + 11.0),
maxwidth=80.0 + 100.0 * len(self._player_info),
maxwidth=80.0 + 100.0 * len(self._playerinfos),
v_align=Text.VAlign.CENTER,
color=color1,
flash=flash,
@ -1086,7 +1086,7 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]):
h_offs_extra = 0
v_offs_names = 0
scale = 1.0
p_count = len(self._player_info)
p_count = len(self._playerinfos)
if p_count > 1:
h_offs_extra -= 40
if self._score_type != 'points':
@ -1146,7 +1146,7 @@ class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]):
position=(ts_h_offs + 35 + h_offs_extra,
ts_height / 2 + -ts_height * (i + 1) / 10 +
v_offs_names + v_offs + 11.0),
maxwidth=80.0 + 100.0 * len(self._player_info),
maxwidth=80.0 + 100.0 * len(self._playerinfos),
v_align=Text.VAlign.CENTER,
color=color1,
flash=flash,

View File

@ -103,7 +103,7 @@ class TeamVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
def _show_team_name(self, pos_v: float, team: ba.SessionTeam,
kill_delay: float, shiftdelay: float) -> None:
del kill_delay # unused arg
del kill_delay # Unused arg.
ZoomText(ba.Lstr(value='${A}:', subs=[('${A}', team.name)]),
position=(100, pos_v),
shiftposition=(-150, pos_v),
@ -115,9 +115,9 @@ class TeamVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
color=team.color,
jitter=1.0).autoretain()
def _show_team_old_score(self, pos_v: float, team: ba.SessionTeam,
def _show_team_old_score(self, pos_v: float, sessionteam: ba.SessionTeam,
shiftdelay: float) -> None:
ZoomText(str(team.sessiondata['score'] - 1),
ZoomText(str(sessionteam.customdata['score'] - 1),
position=(150, pos_v),
maxwidth=100,
color=(0.6, 0.6, 0.7),
@ -129,11 +129,11 @@ class TeamVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
h_align='left',
jitter=1.0).autoretain()
def _show_team_score(self, pos_v: float, team: ba.SessionTeam,
def _show_team_score(self, pos_v: float, sessionteam: ba.SessionTeam,
scored: bool, kill_delay: float,
shiftdelay: float) -> None:
del kill_delay # unused arg
ZoomText(str(team.sessiondata['score']),
del kill_delay # Unused arg.
ZoomText(str(sessionteam.customdata['score']),
position=(150, pos_v),
maxwidth=100,
color=(1.0, 0.9, 0.5) if scored else (0.6, 0.6, 0.7),

View File

@ -28,7 +28,7 @@ import ba
from bastd.activity.multiteamscore import MultiTeamScoreScreenActivity
if TYPE_CHECKING:
from typing import Any, Dict, Optional, Set
from typing import Any, Dict, Optional, Set, Tuple
class FreeForAllVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
@ -60,13 +60,17 @@ class FreeForAllVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
player_order_prev = list(self.players)
player_order_prev.sort(
reverse=True,
key=lambda p:
(p.team.sessiondata['previous_score'], p.getname(full=True)))
key=lambda p: (
p.team.sessionteam.customdata['previous_score'],
p.getname(full=True),
))
player_order = list(self.players)
player_order.sort(reverse=True,
key=lambda p:
(p.team.sessiondata['score'], p.team.sessiondata[
'score'], p.getname(full=True)))
key=lambda p: (
p.team.sessionteam.customdata['score'],
p.team.sessionteam.customdata['score'],
p.getname(full=True),
))
v_offs = -74.0 + spacing * len(player_order_prev) * 0.5
delay1 = 1.3 + 0.1
@ -202,8 +206,9 @@ class FreeForAllVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
transtime2: ts_h_offs - (95.0 + slide_amt) * scale
}))
s_txt = _scoretxt(str(player.team.sessiondata['previous_score']),
80, 0, False, 0, 1.0)
s_txt = _scoretxt(
str(player.team.sessionteam.customdata['previous_score']), 80,
0, False, 0, 1.0)
ba.timer(
tdelay + delay2,
ba.WeakCall(
@ -219,8 +224,9 @@ class FreeForAllVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
transtime2: ts_h_offs + (80.0 - slide_amt) * scale
}))
score_change = (player.team.sessiondata['score'] -
player.team.sessiondata['previous_score'])
score_change = (
player.team.sessionteam.customdata['score'] -
player.team.sessionteam.customdata['previous_score'])
if score_change > 0:
xval = 113
yval = 3.0
@ -257,12 +263,11 @@ class FreeForAllVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
tdelay + delay1,
ba.Call(_safesetattr, s_txt.node, 'color', (1, 1, 1, 1)))
for j in range(score_change):
ba.timer(
(tdelay + delay1 + 0.15 * j),
ba.Call(
_safesetattr, s_txt.node, 'text',
str(player.team.sessiondata['previous_score'] + j +
1)))
ba.timer((tdelay + delay1 + 0.15 * j),
ba.Call(
_safesetattr, s_txt.node, 'text',
str(player.team.sessionteam.
customdata['previous_score'] + j + 1)))
tfin = tdelay + delay1 + 0.15 * j
if tfin not in sound_times:
sound_times.add(tfin)

View File

@ -121,8 +121,8 @@ class MultiTeamScoreScreenActivity(ScoreScreenActivity):
# Results is already sorted; just convert it into a list of
# score-set-entries.
for winner in results.get_winners():
for team in winner.teams:
for winnergroup in results.winnergroups:
for team in winnergroup.teams:
if len(team.players) == 1:
player_entry = _get_player_score_set_entry(
team.players[0])
@ -172,8 +172,8 @@ class MultiTeamScoreScreenActivity(ScoreScreenActivity):
_txt(180, 4, ba.Lstr(resource='killsText'))
_txt(280, 4, ba.Lstr(resource='deathsText'), maxwidth=100)
score_name = 'Score' if results is None else results.get_score_name()
translated = ba.Lstr(translate=('scoreNames', score_name))
score_label = 'Score' if results is None else results.score_label
translated = ba.Lstr(translate=('scoreNames', score_label))
_txt(390, 0, translated)

View File

@ -79,7 +79,7 @@ class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
for _pkey, prec in self.stats.get_records().items():
if prec.player.in_game:
player_entries.append(
(prec.player.team.sessiondata['score'],
(prec.player.sessionteam.customdata['score'],
prec.getname(full=True), prec))
player_entries.sort(reverse=True, key=lambda x: x[0])
else:
@ -145,8 +145,8 @@ class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
win_score = (session.get_series_length() - 1) // 2 + 1
lose_score = 0
for team in self.teams:
if team.sessiondata['score'] != win_score:
lose_score = team.sessiondata['score']
if team.sessionteam.customdata['score'] != win_score:
lose_score = team.sessionteam.customdata['score']
if not self._is_ffa:
Text(ba.Lstr(resource='gamesToText',
@ -309,7 +309,7 @@ class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
for _score, name, prec in player_entries:
tdelay -= 4 * t_incr
v_offs -= 40
Text(str(prec.team.sessiondata['score'])
Text(str(prec.team.customdata['score'])
if self._is_ffa else str(prec.score),
color=(0.5, 0.5, 0.5, 1.0),
position=(ts_h_offs + 230, ts_height / 2 + v_offs),

View File

@ -39,6 +39,9 @@ class RespawnIcon:
This is used to indicate that a ba.Player is waiting to respawn.
"""
_MASKTEXSTORENAME = ba.storagename('masktex')
_ICONSSTORENAME = ba.storagename('icons')
def __init__(self, player: ba.Player, respawn_time: float):
"""Instantiate with a ba.Player and respawn_time (in seconds)."""
self._visible = True
@ -46,10 +49,10 @@ class RespawnIcon:
on_right, offs_extra, respawn_icons = self._get_context(player)
# Cache our mask tex on the team for easy access.
mask_tex = player.team.gamedata.get('_spaz_respawn_icons_mask_tex')
mask_tex = player.team.customdata.get(self._MASKTEXSTORENAME)
if mask_tex is None:
mask_tex = ba.gettexture('characterIconMask')
player.team.gamedata['_spaz_respawn_icons_mask_tex'] = mask_tex
player.team.customdata[self._MASKTEXSTORENAME] = mask_tex
assert isinstance(mask_tex, ba.Texture)
# Now find the first unused slot and use that.
@ -139,9 +142,9 @@ class RespawnIcon:
on_right = player.team.id % 2 == 1
# Store a list of icons in the team.
icons = player.team.gamedata.get('_spaz_respawn_icons')
icons = player.team.customdata.get(self._ICONSSTORENAME)
if icons is None:
player.team.gamedata['_spaz_respawn_icons'] = icons = {}
player.team.customdata[self._ICONSSTORENAME] = icons = {}
assert isinstance(icons, dict)
offs_extra = -20
@ -149,9 +152,9 @@ class RespawnIcon:
on_right = False
# Store a list of icons in the activity.
icons = activity.gamedata.get('_spaz_respawn_icons')
icons = activity.customdata.get(self._ICONSSTORENAME)
if icons is None:
activity.gamedata['_spaz_respawn_icons'] = icons = {}
activity.customdata[self._ICONSSTORENAME] = icons = {}
assert isinstance(icons, dict)
if isinstance(activity.session, ba.FreeForAllSession):

View File

@ -343,6 +343,8 @@ class Scoreboard:
category: Gameplay Classes
"""
_STORENAME = ba.storagename('entry')
def __init__(self, label: ba.Lstr = None, score_split: float = 0.7):
"""Instantiate a scoreboard.
@ -382,8 +384,8 @@ class Scoreboard:
# Create a proxy in the team which will kill
# our entry when it dies (for convenience)
assert '_scoreboard_entry' not in team.gamedata
team.gamedata['_scoreboard_entry'] = _EntryProxy(self, team)
assert self._STORENAME not in team.customdata
team.customdata[self._STORENAME] = _EntryProxy(self, team)
# Now set the entry.
self._entries[team.id].set_value(score=score,

View File

@ -61,23 +61,25 @@ class ConquestFlag(Flag):
class Player(ba.Player['Team']):
"""Our player type for this game."""
# FIXME: We shouldn't be using customdata here
# (but need to update respawn funcs accordingly first).
@property
def respawn_timer(self) -> Optional[ba.Timer]:
"""Type safe access to standard respawn timer."""
return self.gamedata.get('respawn_timer', None)
return self.customdata.get('respawn_timer', None)
@respawn_timer.setter
def respawn_timer(self, value: Optional[ba.Timer]) -> None:
self.gamedata['respawn_timer'] = value
self.customdata['respawn_timer'] = value
@property
def respawn_icon(self) -> Optional[RespawnIcon]:
"""Type safe access to standard respawn icon."""
return self.gamedata.get('respawn_icon', None)
return self.customdata.get('respawn_icon', None)
@respawn_icon.setter
def respawn_icon(self, value: Optional[RespawnIcon]) -> None:
self.gamedata['respawn_icon'] = value
self.customdata['respawn_icon'] = value
class Team(ba.Team[Player]):

View File

@ -203,8 +203,8 @@ class EasterEggHuntGame(ba.TeamGameActivity[Player, Team]):
# Respawn them shortly.
player = msg.getplayer(Player)
assert self.initial_player_info is not None
respawn_time = 2.0 + len(self.initial_player_info) * 1.0
assert self.initialplayerinfos is not None
respawn_time = 2.0 + len(self.initialplayerinfos) * 1.0
player.respawn_timer = ba.Timer(
respawn_time, ba.Call(self.spawn_player_if_exists, player))
player.respawn_icon = RespawnIcon(player, respawn_time)

View File

@ -441,7 +441,7 @@ class FootballCoopGame(ba.CoopGameActivity[Player, Team]):
if ba.app.kiosk_mode:
controlsguide.ControlsGuide(delay=3.0, lifespan=10.0,
bright=True).autoretain()
assert self.initial_player_info is not None
assert self.initialplayerinfos is not None
abot: Type[SpazBot]
bbot: Type[SpazBot]
cbot: Type[SpazBot]
@ -450,36 +450,36 @@ class FootballCoopGame(ba.CoopGameActivity[Player, Team]):
self._have_tnt = False
abot = (BrawlerBotLite
if self._preset == 'rookie_easy' else BrawlerBot)
self._bot_types_initial = [abot] * len(self.initial_player_info)
self._bot_types_initial = [abot] * len(self.initialplayerinfos)
bbot = (BomberBotLite
if self._preset == 'rookie_easy' else BomberBot)
self._bot_types_7 = (
[bbot] * (1 if len(self.initial_player_info) < 3 else 2))
[bbot] * (1 if len(self.initialplayerinfos) < 3 else 2))
cbot = (BomberBot if self._preset == 'rookie_easy' else TriggerBot)
self._bot_types_14 = (
[cbot] * (1 if len(self.initial_player_info) < 3 else 2))
[cbot] * (1 if len(self.initialplayerinfos) < 3 else 2))
elif self._preset == 'tournament':
self._exclude_powerups = []
self._have_tnt = True
self._bot_types_initial = (
[BrawlerBot] * (1 if len(self.initial_player_info) < 2 else 2))
[BrawlerBot] * (1 if len(self.initialplayerinfos) < 2 else 2))
self._bot_types_7 = (
[TriggerBot] * (1 if len(self.initial_player_info) < 3 else 2))
[TriggerBot] * (1 if len(self.initialplayerinfos) < 3 else 2))
self._bot_types_14 = (
[ChargerBot] * (1 if len(self.initial_player_info) < 4 else 2))
[ChargerBot] * (1 if len(self.initialplayerinfos) < 4 else 2))
elif self._preset in ['pro', 'pro_easy', 'tournament_pro']:
self._exclude_powerups = ['curse']
self._have_tnt = True
self._bot_types_initial = [ChargerBot] * len(
self.initial_player_info)
self.initialplayerinfos)
abot = (BrawlerBot if self._preset == 'pro' else BrawlerBotLite)
typed_bot_list: List[Type[SpazBot]] = []
self._bot_types_7 = (
typed_bot_list + [abot] + [BomberBot] *
(1 if len(self.initial_player_info) < 3 else 2))
(1 if len(self.initialplayerinfos) < 3 else 2))
bbot = (TriggerBotPro if self._preset == 'pro' else TriggerBot)
self._bot_types_14 = (
[bbot] * (1 if len(self.initial_player_info) < 3 else 2))
[bbot] * (1 if len(self.initialplayerinfos) < 3 else 2))
elif self._preset in ['uber', 'uber_easy']:
self._exclude_powerups = []
self._have_tnt = True
@ -487,12 +487,11 @@ class FootballCoopGame(ba.CoopGameActivity[Player, Team]):
bbot = (TriggerBotPro if self._preset == 'uber' else TriggerBot)
typed_bot_list_2: List[Type[SpazBot]] = []
self._bot_types_initial = (typed_bot_list_2 + [StickyBot] +
[abot] * len(self.initial_player_info))
[abot] * len(self.initialplayerinfos))
self._bot_types_7 = (
[bbot] * (1 if len(self.initial_player_info) < 3 else 2))
[bbot] * (1 if len(self.initialplayerinfos) < 3 else 2))
self._bot_types_14 = (
[ExplodeyBot] *
(1 if len(self.initial_player_info) < 3 else 2))
[ExplodeyBot] * (1 if len(self.initialplayerinfos) < 3 else 2))
else:
raise Exception()
@ -794,7 +793,7 @@ class FootballCoopGame(ba.CoopGameActivity[Player, Team]):
'outcome': outcome,
'score': scoreval,
'score_order': 'decreasing',
'player_info': self.initial_player_info
'playerinfos': self.initialplayerinfos
})
def handlemessage(self, msg: Any) -> Any:
@ -805,8 +804,8 @@ class FootballCoopGame(ba.CoopGameActivity[Player, Team]):
# Respawn them shortly.
player = msg.getplayer(Player)
assert self.initial_player_info is not None
respawn_time = 2.0 + len(self.initial_player_info) * 1.0
assert self.initialplayerinfos is not None
respawn_time = 2.0 + len(self.initialplayerinfos) * 1.0
player.respawn_timer = ba.Timer(
respawn_time, ba.Call(self.spawn_player_if_exists, player))
player.respawn_icon = RespawnIcon(player, respawn_time)

View File

@ -154,7 +154,6 @@ class KingOfTheHillGame(ba.TeamGameActivity[Player, Team]):
ba.timer(1.0, self._tick, repeat=True)
self._flag_state = FlagState.NEW
Flag.project_stand(self._flag_pos)
self._flag = Flag(position=self._flag_pos,
touchable=False,
color=(1, 1, 1))

View File

@ -113,12 +113,12 @@ class NinjaFightGame(ba.TeamGameActivity[Player, Team]):
ChargerBot, pos=(-5, 3, -2), spawn_time=3.0))
# Add some extras for multiplayer or pro mode.
assert self.initial_player_info is not None
if len(self.initial_player_info) > 2 or is_pro:
assert self.initialplayerinfos is not None
if len(self.initialplayerinfos) > 2 or is_pro:
ba.timer(
5.0, lambda: self._bots.spawn_bot(
ChargerBot, pos=(0, 3, -5), spawn_time=3.0))
if len(self.initial_player_info) > 3 or is_pro:
if len(self.initialplayerinfos) > 3 or is_pro:
ba.timer(
6.0, lambda: self._bots.spawn_bot(
ChargerBot, pos=(0, 3, 1), spawn_time=3.0))

View File

@ -757,7 +757,7 @@ class OnslaughtGame(ba.CoopGameActivity[Player, Team]):
'outcome': outcome,
'score': score,
'fail_message': fail_message,
'player_info': self.initial_player_info
'playerinfos': self.initialplayerinfos
},
delay=delay)
@ -844,10 +844,10 @@ class OnslaughtGame(ba.CoopGameActivity[Player, Team]):
for player in self.players:
try:
if player.is_alive():
assert self.initial_player_info is not None
assert self.initialplayerinfos is not None
self.stats.player_scored(
player,
int(100 / len(self.initial_player_info)),
int(100 / len(self.initialplayerinfos)),
scale=1.4,
color=(0.6, 0.6, 1.0, 1.0),
title=ba.Lstr(resource='completionBonusText'),

View File

@ -563,7 +563,7 @@ class RunaroundGame(ba.CoopGameActivity[Player, Team]):
'outcome': outcome,
'score': score,
'fail_message': fail_message,
'player_info': self.initial_player_info
'playerinfos': self.initialplayerinfos
})
def _on_got_scores_to_beat(self, scores: List[Dict[str, Any]]) -> None:
@ -1118,8 +1118,8 @@ class RunaroundGame(ba.CoopGameActivity[Player, Team]):
# Respawn them shortly.
player = msg.getplayer(Player)
assert self.initial_player_info is not None
respawn_time = 2.0 + len(self.initial_player_info) * 1.0
assert self.initialplayerinfos is not None
respawn_time = 2.0 + len(self.initialplayerinfos) * 1.0
player.respawn_timer = ba.Timer(
respawn_time, ba.Call(self.spawn_player_if_exists, player))
player.respawn_icon = RespawnIcon(player, respawn_time)

View File

@ -196,7 +196,7 @@ class TheLastStandGame(ba.CoopGameActivity[Player, Team]):
results={
'outcome': outcome,
'score': self._score,
'player_info': self.initial_player_info
'playerinfos': self.initialplayerinfos
})
def _update_bots(self) -> None:

View File

@ -1,5 +1,5 @@
<!-- THIS FILE IS AUTO GENERATED; DO NOT EDIT BY HAND -->
<h4><em>last updated on 2020-06-01 for Ballistica version 1.5.0 build 20039</em></h4>
<h4><em>last updated on 2020-06-02 for Ballistica version 1.5.0 build 20041</em></h4>
<p>This page documents the Python classes and functions in the 'ba' module,
which are the ones most relevant to modding in Ballistica. If you come across something you feel should be included here or could be better explained, please <a href="mailto:support@froemling.net">let me know</a>. Happy modding!</p>
<hr>
@ -92,6 +92,7 @@
<li><a href="#function_ba_screenmessage">ba.screenmessage()</a></li>
<li><a href="#function_ba_set_analytics_screen">ba.set_analytics_screen()</a></li>
<li><a href="#function_ba_setlanguage">ba.setlanguage()</a></li>
<li><a href="#function_ba_storagename">ba.storagename()</a></li>
<li><a href="#function_ba_time">ba.time()</a></li>
<li><a href="#function_ba_timer">ba.timer()</a></li>
<li><a href="#function_ba_timestring">ba.timestring()</a></li>
@ -343,8 +344,16 @@ actually award achievements.</p>
can overlap during transitions.</p>
<h3>Attributes:</h3>
<h5><a href="#attr_ba_Activity__expired">expired</a>, <a href="#attr_ba_Activity__gamedata">gamedata</a>, <a href="#attr_ba_Activity__globalsnode">globalsnode</a>, <a href="#attr_ba_Activity__players">players</a>, <a href="#attr_ba_Activity__playertype">playertype</a>, <a href="#attr_ba_Activity__session">session</a>, <a href="#attr_ba_Activity__settings_raw">settings_raw</a>, <a href="#attr_ba_Activity__stats">stats</a>, <a href="#attr_ba_Activity__teams">teams</a>, <a href="#attr_ba_Activity__teamtype">teamtype</a></h5>
<h5><a href="#attr_ba_Activity__customdata">customdata</a>, <a href="#attr_ba_Activity__expired">expired</a>, <a href="#attr_ba_Activity__globalsnode">globalsnode</a>, <a href="#attr_ba_Activity__players">players</a>, <a href="#attr_ba_Activity__playertype">playertype</a>, <a href="#attr_ba_Activity__session">session</a>, <a href="#attr_ba_Activity__settings_raw">settings_raw</a>, <a href="#attr_ba_Activity__stats">stats</a>, <a href="#attr_ba_Activity__teams">teams</a>, <a href="#attr_ba_Activity__teamtype">teamtype</a></h5>
<dl>
<dt><h4><a name="attr_ba_Activity__customdata">customdata</a></h4></dt><dd>
<p><span>dict</span></p>
<p>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.</p>
</dd>
<dt><h4><a name="attr_ba_Activity__expired">expired</a></h4></dt><dd>
<p><span>bool</span></p>
<p>Whether the activity is expired.</p>
@ -353,14 +362,6 @@ actually award achievements.</p>
At this point no new nodes, timers, etc should be made,
run, etc, and the activity should be considered a 'zombie'.</p>
</dd>
<dt><h4><a name="attr_ba_Activity__gamedata">gamedata</a></h4></dt><dd>
<p><span>dict</span></p>
<p>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.</p>
</dd>
<dt><h4><a name="attr_ba_Activity__globalsnode">globalsnode</a></h4></dt><dd>
<p><span><a href="#class_ba_Node">ba.Node</a></span></p>
@ -1552,8 +1553,16 @@ start_long_action(callback_when_done=<a href="#class_ba_ContextCall">ba.ContextC
<h3>Attributes Inherited:</h3>
<h5><a href="#attr_ba_Activity__players">players</a>, <a href="#attr_ba_Activity__settings_raw">settings_raw</a>, <a href="#attr_ba_Activity__teams">teams</a></h5>
<h3>Attributes Defined Here:</h3>
<h5><a href="#attr_ba_CoopGameActivity__expired">expired</a>, <a href="#attr_ba_CoopGameActivity__gamedata">gamedata</a>, <a href="#attr_ba_CoopGameActivity__globalsnode">globalsnode</a>, <a href="#attr_ba_CoopGameActivity__map">map</a>, <a href="#attr_ba_CoopGameActivity__playertype">playertype</a>, <a href="#attr_ba_CoopGameActivity__session">session</a>, <a href="#attr_ba_CoopGameActivity__stats">stats</a>, <a href="#attr_ba_CoopGameActivity__teamtype">teamtype</a></h5>
<h5><a href="#attr_ba_CoopGameActivity__customdata">customdata</a>, <a href="#attr_ba_CoopGameActivity__expired">expired</a>, <a href="#attr_ba_CoopGameActivity__globalsnode">globalsnode</a>, <a href="#attr_ba_CoopGameActivity__map">map</a>, <a href="#attr_ba_CoopGameActivity__playertype">playertype</a>, <a href="#attr_ba_CoopGameActivity__session">session</a>, <a href="#attr_ba_CoopGameActivity__stats">stats</a>, <a href="#attr_ba_CoopGameActivity__teamtype">teamtype</a></h5>
<dl>
<dt><h4><a name="attr_ba_CoopGameActivity__customdata">customdata</a></h4></dt><dd>
<p><span>dict</span></p>
<p>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.</p>
</dd>
<dt><h4><a name="attr_ba_CoopGameActivity__expired">expired</a></h4></dt><dd>
<p><span>bool</span></p>
<p>Whether the activity is expired.</p>
@ -1562,14 +1571,6 @@ start_long_action(callback_when_done=<a href="#class_ba_ContextCall">ba.ContextC
At this point no new nodes, timers, etc should be made,
run, etc, and the activity should be considered a 'zombie'.</p>
</dd>
<dt><h4><a name="attr_ba_CoopGameActivity__gamedata">gamedata</a></h4></dt><dd>
<p><span>dict</span></p>
<p>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.</p>
</dd>
<dt><h4><a name="attr_ba_CoopGameActivity__globalsnode">globalsnode</a></h4></dt><dd>
<p><span><a href="#class_ba_Node">ba.Node</a></span></p>
@ -2145,8 +2146,16 @@ its time with lingering corpses, sound effects, etc.</p>
<h3>Attributes Inherited:</h3>
<h5><a href="#attr_ba_Activity__players">players</a>, <a href="#attr_ba_Activity__settings_raw">settings_raw</a>, <a href="#attr_ba_Activity__teams">teams</a></h5>
<h3>Attributes Defined Here:</h3>
<h5><a href="#attr_ba_GameActivity__expired">expired</a>, <a href="#attr_ba_GameActivity__gamedata">gamedata</a>, <a href="#attr_ba_GameActivity__globalsnode">globalsnode</a>, <a href="#attr_ba_GameActivity__map">map</a>, <a href="#attr_ba_GameActivity__playertype">playertype</a>, <a href="#attr_ba_GameActivity__session">session</a>, <a href="#attr_ba_GameActivity__stats">stats</a>, <a href="#attr_ba_GameActivity__teamtype">teamtype</a></h5>
<h5><a href="#attr_ba_GameActivity__customdata">customdata</a>, <a href="#attr_ba_GameActivity__expired">expired</a>, <a href="#attr_ba_GameActivity__globalsnode">globalsnode</a>, <a href="#attr_ba_GameActivity__map">map</a>, <a href="#attr_ba_GameActivity__playertype">playertype</a>, <a href="#attr_ba_GameActivity__session">session</a>, <a href="#attr_ba_GameActivity__stats">stats</a>, <a href="#attr_ba_GameActivity__teamtype">teamtype</a></h5>
<dl>
<dt><h4><a name="attr_ba_GameActivity__customdata">customdata</a></h4></dt><dd>
<p><span>dict</span></p>
<p>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.</p>
</dd>
<dt><h4><a name="attr_ba_GameActivity__expired">expired</a></h4></dt><dd>
<p><span>bool</span></p>
<p>Whether the activity is expired.</p>
@ -2155,14 +2164,6 @@ its time with lingering corpses, sound effects, etc.</p>
At this point no new nodes, timers, etc should be made,
run, etc, and the activity should be considered a 'zombie'.</p>
</dd>
<dt><h4><a name="attr_ba_GameActivity__gamedata">gamedata</a></h4></dt><dd>
<p><span>dict</span></p>
<p>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.</p>
</dd>
<dt><h4><a name="attr_ba_GameActivity__globalsnode">globalsnode</a></h4></dt><dd>
<p><span><a href="#class_ba_Node">ba.Node</a></span></p>
@ -3871,14 +3872,14 @@ even if myactor is set to None.</p>
own custom <a href="#class_ba_Player">ba.Player</a> types.</p>
<h3>Attributes:</h3>
<h5><a href="#attr_ba_Player__actor">actor</a>, <a href="#attr_ba_Player__gamedata">gamedata</a>, <a href="#attr_ba_Player__node">node</a>, <a href="#attr_ba_Player__position">position</a>, <a href="#attr_ba_Player__sessionplayer">sessionplayer</a>, <a href="#attr_ba_Player__team">team</a></h5>
<h5><a href="#attr_ba_Player__actor">actor</a>, <a href="#attr_ba_Player__customdata">customdata</a>, <a href="#attr_ba_Player__node">node</a>, <a href="#attr_ba_Player__position">position</a>, <a href="#attr_ba_Player__sessionplayer">sessionplayer</a>, <a href="#attr_ba_Player__team">team</a></h5>
<dl>
<dt><h4><a name="attr_ba_Player__actor">actor</a></h4></dt><dd>
<p><span>Optional[<a href="#class_ba_Actor">ba.Actor</a>]</span></p>
<p>The <a href="#class_ba_Actor">ba.Actor</a> associated with the player.</p>
</dd>
<dt><h4><a name="attr_ba_Player__gamedata">gamedata</a></h4></dt><dd>
<dt><h4><a name="attr_ba_Player__customdata">customdata</a></h4></dt><dd>
<p><span>dict</span></p>
<p>Arbitrary values associated with the player.
Though it is encouraged that most player values be properly defined
@ -4111,9 +4112,9 @@ the type-checker properly identifies the returned value as one.</p>
</dd>
<dt><h4><a name="method_ba_PlayerRecord__associate_with_player">associate_with_player()</a></dt></h4><dd>
<p><span>associate_with_player(self, player: <a href="#class_ba_SessionPlayer">ba.SessionPlayer</a>) -&gt; None</span></p>
<p><span>associate_with_player(self, sessionplayer: <a href="#class_ba_SessionPlayer">ba.SessionPlayer</a>) -&gt; None</span></p>
<p>Associate this entry with a <a href="#class_ba_Player">ba.Player</a>.</p>
<p>Associate this entry with a <a href="#class_ba_SessionPlayer">ba.SessionPlayer</a>.</p>
</dd>
<dt><h4><a name="method_ba_PlayerRecord__cancel_multi_kill_timer">cancel_multi_kill_timer()</a></dt></h4><dd>
@ -4556,7 +4557,7 @@ that a SessionPlayer is still present if retaining references to one
for any length of time.</p>
<h3>Attributes:</h3>
<h5><a href="#attr_ba_SessionPlayer__character">character</a>, <a href="#attr_ba_SessionPlayer__color">color</a>, <a href="#attr_ba_SessionPlayer__gameplayer">gameplayer</a>, <a href="#attr_ba_SessionPlayer__highlight">highlight</a>, <a href="#attr_ba_SessionPlayer__id">id</a>, <a href="#attr_ba_SessionPlayer__in_game">in_game</a>, <a href="#attr_ba_SessionPlayer__inputdevice">inputdevice</a>, <a href="#attr_ba_SessionPlayer__sessiondata">sessiondata</a>, <a href="#attr_ba_SessionPlayer__team">team</a></h5>
<h5><a href="#attr_ba_SessionPlayer__character">character</a>, <a href="#attr_ba_SessionPlayer__color">color</a>, <a href="#attr_ba_SessionPlayer__gameplayer">gameplayer</a>, <a href="#attr_ba_SessionPlayer__highlight">highlight</a>, <a href="#attr_ba_SessionPlayer__id">id</a>, <a href="#attr_ba_SessionPlayer__in_game">in_game</a>, <a href="#attr_ba_SessionPlayer__inputdevice">inputdevice</a>, <a href="#attr_ba_SessionPlayer__sessionteam">sessionteam</a></h5>
<dl>
<dt><h4><a name="attr_ba_SessionPlayer__character">character</a></h4></dt><dd>
<p><span> str</span></p>
@ -4602,14 +4603,7 @@ any lobby character/team selection.</p>
<p>The input device associated with the player.</p>
</dd>
<dt><h4><a name="attr_ba_SessionPlayer__sessiondata">sessiondata</a></h4></dt><dd>
<p><span> Dict</span></p>
<p>A dict for use by the current <a href="#class_ba_Session">ba.Session</a> for
storing data associated with this player.
This persists for the duration of the session.</p>
</dd>
<dt><h4><a name="attr_ba_SessionPlayer__team">team</a></h4></dt><dd>
<dt><h4><a name="attr_ba_SessionPlayer__sessionteam">sessionteam</a></h4></dt><dd>
<p><span> <a href="#class_ba_SessionTeam">ba.SessionTeam</a></span></p>
<p>The <a href="#class_ba_SessionTeam">ba.SessionTeam</a> this Player is on. If the SessionPlayer
is still in its lobby selecting a team/etc. then a
@ -4708,18 +4702,19 @@ other players.</p>
each SessionTeam consists of just one SessionPlayer.</p>
<h3>Attributes:</h3>
<h5><a href="#attr_ba_SessionTeam__color">color</a>, <a href="#attr_ba_SessionTeam__gamedata">gamedata</a>, <a href="#attr_ba_SessionTeam__id">id</a>, <a href="#attr_ba_SessionTeam__name">name</a>, <a href="#attr_ba_SessionTeam__players">players</a>, <a href="#attr_ba_SessionTeam__sessiondata">sessiondata</a></h5>
<h5><a href="#attr_ba_SessionTeam__color">color</a>, <a href="#attr_ba_SessionTeam__customdata">customdata</a>, <a href="#attr_ba_SessionTeam__id">id</a>, <a href="#attr_ba_SessionTeam__name">name</a>, <a href="#attr_ba_SessionTeam__players">players</a></h5>
<dl>
<dt><h4><a name="attr_ba_SessionTeam__color">color</a></h4></dt><dd>
<p><span>Tuple[float, ...]</span></p>
<p>The team's color.</p>
</dd>
<dt><h4><a name="attr_ba_SessionTeam__gamedata">gamedata</a></h4></dt><dd>
<p><span>Dict</span></p>
<p>A dict for use by the current <a href="#class_ba_Activity">ba.Activity</a>
for storing data associated with this team.
This gets cleared for each new <a href="#class_ba_Activity">ba.Activity</a>.</p>
<dt><h4><a name="attr_ba_SessionTeam__customdata">customdata</a></h4></dt><dd>
<p><span>dict</span></p>
<p>A dict for use by the current <a href="#class_ba_Session">ba.Session</a> for
storing data associated with this team.
Unlike customdata, this persists for the duration
of the session.</p>
</dd>
<dt><h4><a name="attr_ba_SessionTeam__id">id</a></h4></dt><dd>
@ -4736,14 +4731,6 @@ This gets cleared for each new <a href="#class_ba_Activity">ba.Activity</a>.</p>
<p><span>List[<a href="#class_ba_SessionPlayer">ba.SessionPlayer</a>]</span></p>
<p>The list of <a href="#class_ba_SessionPlayer">ba.SessionPlayers</a> on the team.</p>
</dd>
<dt><h4><a name="attr_ba_SessionTeam__sessiondata">sessiondata</a></h4></dt><dd>
<p><span>Dict</span></p>
<p>A dict for use by the current <a href="#class_ba_Session">ba.Session</a> for
storing data associated with this team.
Unlike gamedata, this persists for the duration
of the session.</p>
</dd>
</dl>
<h3>Methods:</h3>
@ -4952,7 +4939,7 @@ of the session.</p>
</p>
<h3>Methods:</h3>
<h5><a href="#method_ba_Stats____init__">&lt;constructor&gt;</a>, <a href="#method_ba_Stats__get_records">get_records()</a>, <a href="#method_ba_Stats__getactivity">getactivity()</a>, <a href="#method_ba_Stats__player_got_hit">player_got_hit()</a>, <a href="#method_ba_Stats__player_scored">player_scored()</a>, <a href="#method_ba_Stats__player_was_killed">player_was_killed()</a>, <a href="#method_ba_Stats__register_player">register_player()</a>, <a href="#method_ba_Stats__reset">reset()</a>, <a href="#method_ba_Stats__reset_accum">reset_accum()</a>, <a href="#method_ba_Stats__setactivity">setactivity()</a></h5>
<h5><a href="#method_ba_Stats____init__">&lt;constructor&gt;</a>, <a href="#method_ba_Stats__get_records">get_records()</a>, <a href="#method_ba_Stats__getactivity">getactivity()</a>, <a href="#method_ba_Stats__player_scored">player_scored()</a>, <a href="#method_ba_Stats__player_was_killed">player_was_killed()</a>, <a href="#method_ba_Stats__register_player">register_player()</a>, <a href="#method_ba_Stats__reset">reset()</a>, <a href="#method_ba_Stats__reset_accum">reset_accum()</a>, <a href="#method_ba_Stats__setactivity">setactivity()</a></h5>
<dl>
<dt><h4><a name="method_ba_Stats____init__">&lt;constructor&gt;</a></dt></h4><dd>
<p><span>ba.Stats()</span></p>
@ -4971,12 +4958,6 @@ of the session.</p>
<p>May return None.</p>
</dd>
<dt><h4><a name="method_ba_Stats__player_got_hit">player_got_hit()</a></dt></h4><dd>
<p><span>player_got_hit(self, player: <a href="#class_ba_SessionPlayer">ba.SessionPlayer</a>) -&gt; None</span></p>
<p>Call this when a player got hit.</p>
</dd>
<dt><h4><a name="method_ba_Stats__player_scored">player_scored()</a></dt></h4><dd>
<p><span>player_scored(self, player: <a href="#class_ba_Player">ba.Player</a>, base_points: int = 1, target: Sequence[float] = None, kill: bool = False, victim_player: <a href="#class_ba_Player">ba.Player</a> = None, scale: float = 1.0, color: Sequence[float] = None, title: Union[str, <a href="#class_ba_Lstr">ba.Lstr</a>] = None, screenmessage: bool = True, display: bool = True, importance: int = 1, showpoints: bool = True, big_message: bool = False) -&gt; int</span></p>
@ -5029,9 +5010,9 @@ of the session.</p>
</p>
<h3>Attributes:</h3>
<h5><a href="#attr_ba_Team__gamedata">gamedata</a>, <a href="#attr_ba_Team__sessionteam">sessionteam</a></h5>
<h5><a href="#attr_ba_Team__customdata">customdata</a>, <a href="#attr_ba_Team__sessionteam">sessionteam</a></h5>
<dl>
<dt><h4><a name="attr_ba_Team__gamedata">gamedata</a></h4></dt><dd>
<dt><h4><a name="attr_ba_Team__customdata">customdata</a></h4></dt><dd>
<p><span>dict</span></p>
<p>Arbitrary values associated with the team.
Though it is encouraged that most player values be properly defined
@ -5080,8 +5061,16 @@ of the session.</p>
<h3>Attributes Inherited:</h3>
<h5><a href="#attr_ba_Activity__players">players</a>, <a href="#attr_ba_Activity__settings_raw">settings_raw</a>, <a href="#attr_ba_Activity__teams">teams</a></h5>
<h3>Attributes Defined Here:</h3>
<h5><a href="#attr_ba_TeamGameActivity__expired">expired</a>, <a href="#attr_ba_TeamGameActivity__gamedata">gamedata</a>, <a href="#attr_ba_TeamGameActivity__globalsnode">globalsnode</a>, <a href="#attr_ba_TeamGameActivity__map">map</a>, <a href="#attr_ba_TeamGameActivity__playertype">playertype</a>, <a href="#attr_ba_TeamGameActivity__session">session</a>, <a href="#attr_ba_TeamGameActivity__stats">stats</a>, <a href="#attr_ba_TeamGameActivity__teamtype">teamtype</a></h5>
<h5><a href="#attr_ba_TeamGameActivity__customdata">customdata</a>, <a href="#attr_ba_TeamGameActivity__expired">expired</a>, <a href="#attr_ba_TeamGameActivity__globalsnode">globalsnode</a>, <a href="#attr_ba_TeamGameActivity__map">map</a>, <a href="#attr_ba_TeamGameActivity__playertype">playertype</a>, <a href="#attr_ba_TeamGameActivity__session">session</a>, <a href="#attr_ba_TeamGameActivity__stats">stats</a>, <a href="#attr_ba_TeamGameActivity__teamtype">teamtype</a></h5>
<dl>
<dt><h4><a name="attr_ba_TeamGameActivity__customdata">customdata</a></h4></dt><dd>
<p><span>dict</span></p>
<p>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.</p>
</dd>
<dt><h4><a name="attr_ba_TeamGameActivity__expired">expired</a></h4></dt><dd>
<p><span>bool</span></p>
<p>Whether the activity is expired.</p>
@ -5090,14 +5079,6 @@ of the session.</p>
At this point no new nodes, timers, etc should be made,
run, etc, and the activity should be considered a 'zombie'.</p>
</dd>
<dt><h4><a name="attr_ba_TeamGameActivity__gamedata">gamedata</a></h4></dt><dd>
<p><span>dict</span></p>
<p>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.</p>
</dd>
<dt><h4><a name="attr_ba_TeamGameActivity__globalsnode">globalsnode</a></h4></dt><dd>
<p><span><a href="#class_ba_Node">ba.Node</a></span></p>
@ -5208,43 +5189,58 @@ Results for a completed <a href="#class_ba_TeamGameActivity">ba.TeamGameActivity
<p>Upon completion, a game should fill one of these out and pass it to its
<a href="#method_ba_Activity__end">ba.Activity.end</a>() call.</p>
<h3>Attributes:</h3>
<h5><a href="#attr_ba_TeamGameResults__lower_is_better">lower_is_better</a>, <a href="#attr_ba_TeamGameResults__playerinfos">playerinfos</a>, <a href="#attr_ba_TeamGameResults__score_label">score_label</a>, <a href="#attr_ba_TeamGameResults__scoretype">scoretype</a>, <a href="#attr_ba_TeamGameResults__sessionteams">sessionteams</a>, <a href="#attr_ba_TeamGameResults__winnergroups">winnergroups</a>, <a href="#attr_ba_TeamGameResults__winning_team">winning_team</a></h5>
<dl>
<dt><h4><a name="attr_ba_TeamGameResults__lower_is_better">lower_is_better</a></h4></dt><dd>
<p><span>bool</span></p>
<p>Whether lower scores are better.</p>
</dd>
<dt><h4><a name="attr_ba_TeamGameResults__playerinfos">playerinfos</a></h4></dt><dd>
<p><span>List[<a href="#class_ba_PlayerInfo">ba.PlayerInfo</a>]</span></p>
<p>Get info about the players represented by the results.</p>
</dd>
<dt><h4><a name="attr_ba_TeamGameResults__score_label">score_label</a></h4></dt><dd>
<p><span>str</span></p>
<p>The label associated with scores ('points', etc).</p>
</dd>
<dt><h4><a name="attr_ba_TeamGameResults__scoretype">scoretype</a></h4></dt><dd>
<p><span><a href="#class_ba_ScoreType">ba.ScoreType</a></span></p>
<p>The type of score.</p>
</dd>
<dt><h4><a name="attr_ba_TeamGameResults__sessionteams">sessionteams</a></h4></dt><dd>
<p><span>List[<a href="#class_ba_SessionTeam">ba.SessionTeam</a>]</span></p>
<p>Return all <a href="#class_ba_SessionTeam">ba.SessionTeams</a> in the results.</p>
</dd>
<dt><h4><a name="attr_ba_TeamGameResults__winnergroups">winnergroups</a></h4></dt><dd>
<p><span>List[WinnerGroup]</span></p>
<p>Get an ordered list of winner groups.</p>
</dd>
<dt><h4><a name="attr_ba_TeamGameResults__winning_team">winning_team</a></h4></dt><dd>
<p><span>Optional[<a href="#class_ba_SessionTeam">ba.SessionTeam</a>]</span></p>
<p>The winning <a href="#class_ba_SessionTeam">ba.SessionTeam</a> if there is exactly one, or else None.</p>
</dd>
</dl>
<h3>Methods:</h3>
<h5><a href="#method_ba_TeamGameResults____init__">&lt;constructor&gt;</a>, <a href="#method_ba_TeamGameResults__get_lower_is_better">get_lower_is_better()</a>, <a href="#method_ba_TeamGameResults__get_player_info">get_player_info()</a>, <a href="#method_ba_TeamGameResults__get_score_name">get_score_name()</a>, <a href="#method_ba_TeamGameResults__get_score_type">get_score_type()</a>, <a href="#method_ba_TeamGameResults__get_team_score">get_team_score()</a>, <a href="#method_ba_TeamGameResults__get_team_score_str">get_team_score_str()</a>, <a href="#method_ba_TeamGameResults__get_teams">get_teams()</a>, <a href="#method_ba_TeamGameResults__get_winners">get_winners()</a>, <a href="#method_ba_TeamGameResults__get_winning_team">get_winning_team()</a>, <a href="#method_ba_TeamGameResults__has_score_for_team">has_score_for_team()</a>, <a href="#method_ba_TeamGameResults__set_game">set_game()</a>, <a href="#method_ba_TeamGameResults__set_team_score">set_team_score()</a></h5>
<h5><a href="#method_ba_TeamGameResults____init__">&lt;constructor&gt;</a>, <a href="#method_ba_TeamGameResults__get_team_score">get_team_score()</a>, <a href="#method_ba_TeamGameResults__get_team_score_str">get_team_score_str()</a>, <a href="#method_ba_TeamGameResults__has_score_for_team">has_score_for_team()</a>, <a href="#method_ba_TeamGameResults__set_game">set_game()</a>, <a href="#method_ba_TeamGameResults__set_team_score">set_team_score()</a></h5>
<dl>
<dt><h4><a name="method_ba_TeamGameResults____init__">&lt;constructor&gt;</a></dt></h4><dd>
<p><span>ba.TeamGameResults()</span></p>
<p>Instantiate a results instance.</p>
</dd>
<dt><h4><a name="method_ba_TeamGameResults__get_lower_is_better">get_lower_is_better()</a></dt></h4><dd>
<p><span>get_lower_is_better(self) -&gt; bool</span></p>
<p>Return whether lower scores are better.</p>
</dd>
<dt><h4><a name="method_ba_TeamGameResults__get_player_info">get_player_info()</a></dt></h4><dd>
<p><span>get_player_info(self) -&gt; List[<a href="#class_ba_PlayerInfo">ba.PlayerInfo</a>]</span></p>
<p>Get info about the players represented by the results.</p>
</dd>
<dt><h4><a name="method_ba_TeamGameResults__get_score_name">get_score_name()</a></dt></h4><dd>
<p><span>get_score_name(self) -&gt; str</span></p>
<p>Get the name associated with scores ('points', etc).</p>
</dd>
<dt><h4><a name="method_ba_TeamGameResults__get_score_type">get_score_type()</a></dt></h4><dd>
<p><span>get_score_type(self) -&gt; <a href="#class_ba_ScoreType">ba.ScoreType</a></span></p>
<p>Get the type of score.</p>
</dd>
<dt><h4><a name="method_ba_TeamGameResults__get_team_score">get_team_score()</a></dt></h4><dd>
<p><span>get_team_score(self, team: Union[<a href="#class_ba_SessionTeam">ba.SessionTeam</a>, <a href="#class_ba_Team">ba.Team</a>]) -&gt; Optional[int]</span></p>
<p><span>get_team_score(self, sessionteam: Union[<a href="#class_ba_SessionTeam">ba.SessionTeam</a>]) -&gt; Optional[int]</span></p>
<p>Return the score for a given team.</p>
<p>Return the score for a given <a href="#class_ba_SessionTeam">ba.SessionTeam</a>.</p>
</dd>
<dt><h4><a name="method_ba_TeamGameResults__get_team_score_str">get_team_score_str()</a></dt></h4><dd>
@ -5254,24 +5250,6 @@ Results for a completed <a href="#class_ba_TeamGameActivity">ba.TeamGameActivity
<p>(properly formatted for the score type.)</p>
</dd>
<dt><h4><a name="method_ba_TeamGameResults__get_teams">get_teams()</a></dt></h4><dd>
<p><span>get_teams(self) -&gt; List[<a href="#class_ba_SessionTeam">ba.SessionTeam</a>]</span></p>
<p>Return all <a href="#class_ba_SessionTeam">ba.SessionTeams</a> in the results.</p>
</dd>
<dt><h4><a name="method_ba_TeamGameResults__get_winners">get_winners()</a></dt></h4><dd>
<p><span>get_winners(self) -&gt; List[WinnerGroup]</span></p>
<p>Get an ordered list of winner groups.</p>
</dd>
<dt><h4><a name="method_ba_TeamGameResults__get_winning_team">get_winning_team()</a></dt></h4><dd>
<p><span>get_winning_team(self) -&gt; Optional[<a href="#class_ba_SessionTeam">ba.SessionTeam</a>]</span></p>
<p>Get the winning <a href="#class_ba_Team">ba.Team</a> if there is exactly one; None otherwise.</p>
</dd>
<dt><h4><a name="method_ba_TeamGameResults__has_score_for_team">has_score_for_team()</a></dt></h4><dd>
<p><span>has_score_for_team(self, sessionteam: <a href="#class_ba_SessionTeam">ba.SessionTeam</a>) -&gt; bool</span></p>
@ -5286,7 +5264,7 @@ Results for a completed <a href="#class_ba_TeamGameActivity">ba.TeamGameActivity
</dd>
<dt><h4><a name="method_ba_TeamGameResults__set_team_score">set_team_score()</a></dt></h4><dd>
<p><span>set_team_score(self, team: Union[<a href="#class_ba_SessionTeam">ba.SessionTeam</a>, <a href="#class_ba_Team">ba.Team</a>], score: Optional[int]) -&gt; None</span></p>
<p><span>set_team_score(self, team: <a href="#class_ba_Team">ba.Team</a>, score: Optional[int]) -&gt; None</span></p>
<p>Set the score for a given <a href="#class_ba_Team">ba.Team</a>.</p>
@ -6422,6 +6400,30 @@ playing, the playing track will not be restarted.</p>
<p>Category: <a href="#function_category_Gameplay_Functions">Gameplay Functions</a></p>
<hr>
<h2><strong><a name="function_ba_storagename">ba.storagename()</a></strong></h3>
<p><span>storagename(basename: str) -&gt; str</span></p>
<p>Generate a (hopefully) unique name for storing things in public places.</p>
<p>Category: <a href="#function_category_General_Utility_Functions">General Utility Functions</a></p>
<p>This consists of a leading underscore, the module path at the
call site with dots replaced by underscores, the class name, and
the provided suffix. When storing data in public places such as
'customdata' dicts, this minimizes the chance of collisions if a
module or class is duplicated or renamed.</p>
<pre><span><em><small># Example: generate a unique name for storage purposes:</small></em></span>
class MyThingie:</pre>
<pre><span><em><small> # This will give something like '_mymodule_submodule_mythingie_data'.</small></em></span>
_STORENAME = <a href="#function_ba_storagename">ba.storagename</a>('data')</pre>
<p> def __init__(self, activity):
# Store some data in the Activity we were passed
activity.customdata[self._STORENAME] = {}</p>
<hr>
<h2><strong><a name="function_ba_textwidget">ba.textwidget()</a></strong></h3>
<p><span>textwidget(edit: Widget = None, parent: Widget = None,