From 08ea64bdc52fcbc726e4df72dee015480129c9b2 Mon Sep 17 00:00:00 2001 From: Eric Froemling Date: Wed, 27 May 2020 17:43:41 -0700 Subject: [PATCH] More modernizing --- .efrocachemap | 24 +++---- assets/src/ba_data/python/_ba.py | 2 +- assets/src/ba_data/python/ba/__init__.py | 3 +- assets/src/ba_data/python/ba/_activity.py | 62 +++++++++++------- assets/src/ba_data/python/ba/_general.py | 63 ++++++++++++++++--- assets/src/ba_data/python/ba/_profile.py | 18 +++--- assets/src/ba_data/python/ba/_session.py | 15 +++++ .../ba_data/python/bastd/actor/respawnicon.py | 29 ++++----- .../ba_data/python/bastd/actor/scoreboard.py | 23 ++++--- .../src/ba_data/python/bastd/actor/spazbot.py | 12 ++-- .../python/bastd/game/easteregghunt.py | 4 +- .../src/ba_data/python/bastd/game/football.py | 2 +- .../src/ba_data/python/bastd/game/hockey.py | 35 ++++++----- .../ba_data/python/bastd/game/onslaught.py | 16 ++--- assets/src/ba_data/python/bastd/game/race.py | 13 ++-- .../ba_data/python/bastd/game/runaround.py | 21 ++++--- .../ba_data/python/bastd/game/thelaststand.py | 6 +- assets/src/ba_data/python/bastd/tutorial.py | 35 ++++++++--- docs/ba_module.md | 21 +++++-- tools/efrotools/filecache.py | 5 +- 20 files changed, 261 insertions(+), 148 deletions(-) diff --git a/.efrocachemap b/.efrocachemap index 9e595257..4179fd4f 100644 --- a/.efrocachemap +++ b/.efrocachemap @@ -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/ba/35/3b6bc5c5609b1dd37bd65c39df45", - "build/prefab/linux-server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/a6/bc/c2c7231dc6bf085eda15d6198554", - "build/prefab/linux/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/6b/fd/020ed9bb0e8c8a18b2d793fee8bd", - "build/prefab/linux/release/ballisticacore": "https://files.ballistica.net/cache/ba1/43/0b/78c8bacb215abaf50dcb3284eef7", - "build/prefab/mac-server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/f8/e1/e0dc64b5c00661cce19530c0e836", - "build/prefab/mac-server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/e6/74/73a514993d626a6bc75717d185ef", - "build/prefab/mac/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/f0/7d/dbd2624759a1fdce2a20d53cab1a", - "build/prefab/mac/release/ballisticacore": "https://files.ballistica.net/cache/ba1/1c/01/4833ec215cc6c53f7e4ebf850608", - "build/prefab/windows-server/debug/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/b1/57/b72500d2a568df5afa36556f89dd", - "build/prefab/windows-server/release/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/48/ea/c83f97f44703b16eeec794d29da6", - "build/prefab/windows/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/a3/16/284b9953c7ef4a841ff907079cbd", - "build/prefab/windows/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/12/43/d0513cf8f8dac0712cbf42d4b94b" + "build/prefab/linux-server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/ca/db/9c7cfd4e4f4a1f7a7adc980bca42", + "build/prefab/linux-server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/4e/0b/231e38ff29d932df7552050891c5", + "build/prefab/linux/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/f2/56/bb316ec28ee98ece5c0c3a04b77f", + "build/prefab/linux/release/ballisticacore": "https://files.ballistica.net/cache/ba1/ef/92/d787c99db6cc85f70b7131ff2c0c", + "build/prefab/mac-server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/97/b9/9c6c3c90f10d319250a9f3d287b3", + "build/prefab/mac-server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/f0/2a/60bdf1c4d4e13bdbb5f4df121e3e", + "build/prefab/mac/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/66/25/79ea606983dc91ac0cd79c1e7da6", + "build/prefab/mac/release/ballisticacore": "https://files.ballistica.net/cache/ba1/70/c6/0ab2cdf222ffcadade37dd3b8462", + "build/prefab/windows-server/debug/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/3c/65/450a67dab189c0832b6bf28a9e9c", + "build/prefab/windows-server/release/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/7e/a9/e1ab6defb8bcf536dff46d0c62b2", + "build/prefab/windows/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/ab/fc/d00336dae2b1c7323b31518b52aa", + "build/prefab/windows/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/ed/98/dbea1af1da83bfa1a3283175b234" } \ No newline at end of file diff --git a/assets/src/ba_data/python/_ba.py b/assets/src/ba_data/python/_ba.py index 3e0e0d1f..9df9b873 100644 --- a/assets/src/ba_data/python/_ba.py +++ b/assets/src/ba_data/python/_ba.py @@ -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=223083205204988067566025188831386474803 +# SOURCES_HASH=265401783818737452594582363319036908124 # I'm sorry Pylint. I know this file saddens you. Be strong. # pylint: disable=useless-suppression diff --git a/assets/src/ba_data/python/ba/__init__.py b/assets/src/ba_data/python/ba/__init__.py index 9e808242..12225c01 100644 --- a/assets/src/ba_data/python/ba/__init__.py +++ b/assets/src/ba_data/python/ba/__init__.py @@ -73,7 +73,8 @@ from ba._apputils import is_browser_likely_available from ba._campaign import Campaign from ba._gameutils import (animate, animate_array, show_damage_count, sharedobj, timestring, cameraflash) -from ba._general import WeakCall, Call, existing, Existable +from ba._general import (WeakCall, Call, existing, Existable, + verify_object_death) from ba._level import Level from ba._lobby import Lobby, Chooser from ba._math import normalized_color, is_point_in_box, vec3validate diff --git a/assets/src/ba_data/python/ba/_activity.py b/assets/src/ba_data/python/ba/_activity.py index c372069d..241d1d70 100644 --- a/assets/src/ba_data/python/ba/_activity.py +++ b/assets/src/ba_data/python/ba/_activity.py @@ -28,6 +28,7 @@ from ba._team import Team from ba._player import Player from ba._error import print_exception, print_error, SessionTeamNotFoundError from ba._dependency import DependencyComponent +from ba._general import Call, verify_object_death import _ba if TYPE_CHECKING: @@ -217,7 +218,6 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]): self._stats: Optional[ba.Stats] = None def __del__(self) -> None: - from ba._apputils import garbage_collect, call_after_ad # If the activity has been run then we should have already cleaned # it up, but we still need to run expire calls for un-run activities. @@ -225,20 +225,13 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]): with _ba.Context('empty'): self._expire() - # Since we're mostly between activities at this point, lets run a cycle - # of garbage collection; hopefully it won't cause hitches here. - garbage_collect(session_end=False) - - # Now that our object is officially gonna be dead, tell the session it - # can fire up the next activity. + # Inform our owner that we're officially kicking the bucket. if self._transitioning_out: session = self._session() if session is not None: - with _ba.Context(session): - if self.can_show_ad_on_death: - call_after_ad(session.begin_next_activity) - else: - _ba.pushcall(session.begin_next_activity) + _ba.pushcall( + Call(session.transitioning_out_activity_was_freed, + self.can_show_ad_on_death)) @property def stats(self) -> ba.Stats: @@ -304,7 +297,6 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]): (internal) """ - from ba._general import Call from ba._enums import TimeType # Create a real-timer that watches a weak-ref of this activity @@ -628,6 +620,10 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]): self.players.remove(player) assert player not in self.players + # This should allow our ba.Player instance to die. + # Complain if that doesn't happen. + # verify_object_death(player) + with _ba.Context(self): # Make a decent attempt to persevere if user code breaks. try: @@ -670,6 +666,10 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]): self.teams.remove(team) assert team not in self.teams + # This should allow our ba.Team instance to die. Complain + # if that doesn't happen. + # verify_object_death(team) + with _ba.Context(self): # Make a decent attempt to persevere if user code breaks. try: @@ -680,6 +680,7 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]): sessionteam.reset_gamedata() except Exception: print_exception(f'Error in reset_gamedata for {self}') + sessionteam.gameteam = None def _sanity_check_begin_call(self) -> None: @@ -777,32 +778,45 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]): for actor_ref in self._actor_weak_refs: actor = actor_ref() if actor is not None: + verify_object_death(actor) try: actor.on_expire() except Exception: - print_exception(f'Error expiring Actor {actor_ref()}') + print_exception(f'Error in Actor.on_expire()' + f' for {actor_ref()}') # Reset all Players. # (releases any attached actors, clears game-data, etc) for player in self.players: - if player: - try: - sessionplayer = player.sessionplayer - player.reset() - sessionplayer.set_node(None) - sessionplayer.set_activity(None) - sessionplayer.gameplayer = None - sessionplayer.reset() - except Exception: - print_exception(f'Error resetting Player {player}') + try: + # This should allow our ba.Player instance to die. + # Complain if that doesn't happen. + # verify_object_death(player) + sessionplayer = player.sessionplayer + player.reset() + sessionplayer.set_node(None) + sessionplayer.set_activity(None) + + sessionplayer.gameplayer = None + sessionplayer.reset() + except Exception: + print_exception(f'Error resetting Player {player}') # Ditto with Teams. for team in self.teams: try: sessionteam = team.sessionteam + + # This should allow our ba.Team instance to die. + # Complain if that doesn't happen. + # verify_object_death(sessionteam.gameteam) sessionteam.gameteam = None sessionteam.reset_gamedata() except SessionTeamNotFoundError: + # It is expected that Team objects may last longer than + # the SessionTeam they came from (game objects may hold + # team references past the point at which the underlying + # player/team leaves) pass except Exception: print_exception(f'Error resetting Team {team}') diff --git a/assets/src/ba_data/python/ba/_general.py b/assets/src/ba_data/python/ba/_general.py index 46f6be9b..5ff0acd2 100644 --- a/assets/src/ba_data/python/ba/_general.py +++ b/assets/src/ba_data/python/ba/_general.py @@ -21,16 +21,22 @@ """Utility snippets applying to generic Python code.""" from __future__ import annotations +import gc import types import weakref +import random from typing import TYPE_CHECKING, TypeVar from typing_extensions import Protocol +from efro.terminal import Clr +from ba._error import print_error, print_exception +from ba._enums import TimeType import _ba if TYPE_CHECKING: from typing import Any, Type, Optional from efro.call import Call as Call # 'as Call' so we re-export. + from weakref import ReferenceType class Existable(Protocol): @@ -100,22 +106,18 @@ def json_prep(data: Any) -> Any: if isinstance(data, list): return [json_prep(element) for element in data] if isinstance(data, tuple): - from ba import _error - _error.print_error('json_prep encountered tuple', once=True) + print_error('json_prep encountered tuple', once=True) return [json_prep(element) for element in data] if isinstance(data, bytes): try: return data.decode(errors='ignore') except Exception: from ba import _error - _error.print_error('json_prep encountered utf-8 decode error', - once=True) + print_error('json_prep encountered utf-8 decode error', once=True) return data.decode(errors='ignore') if not isinstance(data, (str, float, bool, type(None), int)): - from ba import _error - _error.print_error('got unsupported type in json_prep:' + - str(type(data)), - once=True) + print_error('got unsupported type in json_prep:' + str(type(data)), + once=True) return data @@ -135,7 +137,6 @@ def utf8_all(data: Any) -> Any: def print_refs(obj: Any) -> None: """Print a list of known live references to an object.""" - import gc # Hmmm; I just noticed that calling this on an object # seems to keep it alive. Should figure out why. @@ -291,3 +292,47 @@ class WeakMethod: def __str__(self) -> str: return '' + + +def verify_object_death(obj: object) -> None: + """Warn if an object does not get freed within a short period. + + Category: General Utility Functions + + This can be handy to detect and prevent memory/resource leaks. + """ + try: + ref = weakref.ref(obj) + except Exception: + print_exception('Unable to create weak-ref in verify_object_death') + + # Use a slight range for our checks so they don't all land at once + # if we queue a lot of them. + delay = random.uniform(2.0, 5.5) + with _ba.Context('ui'): + _ba.timer(delay, + lambda: _verify_object_death(ref), + timetype=TimeType.REAL) + + +def _verify_object_death(wref: ReferenceType) -> None: + obj = wref() + if obj is None: + return + + try: + name = type(obj).__name__ + except Exception: + print(f'Note: unable to get type name for {obj}') + name = 'object' + + print(f'{Clr.RED}Error: {name} not dying' + f' when expected to: {Clr.BLD}{obj}{Clr.RST}') + refs = list(gc.get_referrers(obj)) + 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 diff --git a/assets/src/ba_data/python/ba/_profile.py b/assets/src/ba_data/python/ba/_profile.py index e29384c6..86fa1c56 100644 --- a/assets/src/ba_data/python/ba/_profile.py +++ b/assets/src/ba_data/python/ba/_profile.py @@ -35,7 +35,7 @@ PLAYER_COLORS = [(1, 0.15, 0.15), (0.2, 1, 0.2), (0.1, 0.1, 1), (0.2, 1, 1), (0.5, 0.25, 1.0), (1, 1, 0), (1, 0.5, 0), (1, 0.3, 0.5), (0.1, 0.1, 0.5), (0.4, 0.2, 0.1), (0.1, 0.35, 0.1), (1, 0.8, 0.5), (0.4, 0.05, 0.05), (0.13, 0.13, 0.13), - (0.5, 0.5, 0.5), (1, 1, 1)] # yapf: disable + (0.5, 0.5, 0.5), (1, 1, 1)] def get_player_colors() -> List[Tuple[float, float, float]]: @@ -75,8 +75,8 @@ def get_player_profile_colors( if profiles is None: profiles = bs_config['Player Profiles'] - # special case - when being asked for a random color in kiosk mode, - # always return default purple + # Special case: when being asked for a random color in kiosk mode, + # always return default purple. if _ba.app.kiosk_mode and profilename is None: color = (0.5, 0.4, 1.0) highlight = (0.4, 0.4, 0.5) @@ -85,22 +85,22 @@ def get_player_profile_colors( assert profilename is not None color = profiles[profilename]['color'] except (KeyError, AssertionError): - # key off name if possible + # Key off name if possible. if profilename is None: - # first 6 are bright-ish + # First 6 are bright-ish. color = PLAYER_COLORS[random.randrange(6)] else: - # first 6 are bright-ish + # First 6 are bright-ish. color = PLAYER_COLORS[sum([ord(c) for c in profilename]) % 6] try: assert profilename is not None highlight = profiles[profilename]['highlight'] except (KeyError, AssertionError): - # key off name if possible + # Key off name if possible. if profilename is None: - # last 2 are grey and white; ignore those or we - # get lots of old-looking players + # Last 2 are grey and white; ignore those or we + # get lots of old-looking players. highlight = PLAYER_COLORS[random.randrange( len(PLAYER_COLORS) - 2)] else: diff --git a/assets/src/ba_data/python/ba/_session.py b/assets/src/ba_data/python/ba/_session.py index cf9fb06a..c84e42ea 100644 --- a/assets/src/ba_data/python/ba/_session.py +++ b/assets/src/ba_data/python/ba/_session.py @@ -582,6 +582,21 @@ class Session: self._add_chosen_player(chooser) lobby.remove_chooser(chooser.getplayer()) + def transitioning_out_activity_was_freed( + self, can_show_ad_on_death: bool) -> None: + """(internal)""" + from ba._apputils import garbage_collect, call_after_ad + + # Since we're mostly between activities at this point, lets run a cycle + # of garbage collection; hopefully it won't cause hitches here. + garbage_collect(session_end=False) + + with _ba.Context(self): + if can_show_ad_on_death: + call_after_ad(self.begin_next_activity) + else: + _ba.pushcall(self.begin_next_activity) + def _add_chosen_player(self, chooser: ba.Chooser) -> ba.SessionPlayer: from ba._team import SessionTeam sessionplayer = chooser.getplayer() diff --git a/assets/src/ba_data/python/bastd/actor/respawnicon.py b/assets/src/ba_data/python/bastd/actor/respawnicon.py index 44e41e0b..b2c7adba 100644 --- a/assets/src/ba_data/python/bastd/actor/respawnicon.py +++ b/assets/src/ba_data/python/bastd/actor/respawnicon.py @@ -40,18 +40,17 @@ class RespawnIcon: """ def __init__(self, player: ba.Player, respawn_time: float): - """ - Instantiate with a given ba.Player and respawn_time (in seconds) - """ + """Instantiate with a ba.Player and respawn_time (in seconds).""" self._visible = True on_right, offs_extra, respawn_icons = self._get_context(player) - try: - mask_tex = (player.team.gamedata['_spaz_respawn_icons_mask_tex']) - except Exception: - mask_tex = player.team.gamedata['_spaz_respawn_icons_mask_tex'] = ( - ba.gettexture('characterIconMask')) + # Cache our mask tex on the team for easy access. + mask_tex = getattr(player.team, '_spaz_respawn_icons_mask_tex', None) + if mask_tex is None: + mask_tex = ba.gettexture('characterIconMask') + setattr(player.team, '_spaz_respawn_icons_mask_tex', mask_tex) + assert isinstance(mask_tex, ba.Texture) # Now find the first unused slot and use that. index = 0 @@ -139,12 +138,14 @@ class RespawnIcon: on_right = player.team.id % 2 == 1 # Store a list of icons in the team. - try: - respawn_icons = ( - player.team.gamedata['_spaz_respawn_icons_right']) - except Exception: - respawn_icons = ( - player.team.gamedata['_spaz_respawn_icons_right']) = {} + respawn_icons = getattr(player.team, '_spaz_respawn_icons_right', + None) + if respawn_icons is None: + respawn_icons = {} + setattr(player.team, '_spaz_respawn_icons_right', + respawn_icons) + assert isinstance(respawn_icons, dict) + offs_extra = -20 else: on_right = False diff --git a/assets/src/ba_data/python/bastd/actor/scoreboard.py b/assets/src/ba_data/python/bastd/actor/scoreboard.py index 6b3c2374..7ed874fc 100644 --- a/assets/src/ba_data/python/bastd/actor/scoreboard.py +++ b/assets/src/ba_data/python/bastd/actor/scoreboard.py @@ -200,7 +200,8 @@ class _Entry: def set_position(self, position: Sequence[float]) -> None: """Set the entry's position.""" - # abort if we've been killed + + # Abort if we've been killed if not self._backing.node: return self._pos = tuple(position) @@ -315,13 +316,15 @@ class _EntryProxy: def __init__(self, scoreboard: Scoreboard, team: ba.Team): self._scoreboard = weakref.ref(scoreboard) - # have to store ID here instead of a weak-ref since the team will be - # dead when we die and need to remove it + + # Have to store ID here instead of a weak-ref since the team will be + # dead when we die and need to remove it. self._team_id = team.id def __del__(self) -> None: scoreboard = self._scoreboard() - # remove our team from the scoreboard if its still around + + # Remove our team from the scoreboard if its still around. if scoreboard is not None: scoreboard.remove_team(self._team_id) @@ -343,7 +346,7 @@ class Scoreboard: self._label = label self.score_split = score_split - # for free-for-all we go simpler since we have one per player + # For free-for-all we go simpler since we have one per player. self._pos: Sequence[float] if isinstance(ba.getsession(), ba.FreeForAllSession): self._do_cover = False @@ -368,11 +371,11 @@ class Scoreboard: """Update the score-board display for the given ba.Team.""" if not team.id in self._entries: self._add_team(team) - # create a proxy in the team which will kill + + # Create a proxy in the team which will kill # our entry when it dies (for convenience) - if '_scoreboard_entry' in team.gamedata: - raise Exception('existing _EntryProxy found') - team.gamedata['_scoreboard_entry'] = _EntryProxy(self, team) + assert not hasattr(team, '_scoreboard_entry') + setattr(team, '_scoreboard_entry', _EntryProxy(self, team)) # Now set the entry. self._entries[team.id].set_value(score=score, @@ -383,7 +386,7 @@ class Scoreboard: def _add_team(self, team: ba.Team) -> None: if team.id in self._entries: - raise Exception('Duplicate team add') + raise RuntimeError('Duplicate team add') self._entries[team.id] = _Entry(self, team, do_cover=self._do_cover, diff --git a/assets/src/ba_data/python/bastd/actor/spazbot.py b/assets/src/ba_data/python/bastd/actor/spazbot.py index d86cc1b2..984d209d 100644 --- a/assets/src/ba_data/python/bastd/actor/spazbot.py +++ b/assets/src/ba_data/python/bastd/actor/spazbot.py @@ -49,16 +49,16 @@ class SpazBotPunchedMessage: Attributes: - badguy + spazbot The ba.SpazBot that got punched. damage How much damage was done to the ba.SpazBot. """ - def __init__(self, badguy: SpazBot, damage: int): + def __init__(self, spazbot: SpazBot, damage: int): """Instantiate a message with the given values.""" - self.badguy = badguy + self.spazbot = spazbot self.damage = damage @@ -69,7 +69,7 @@ class SpazBotDiedMessage: Attributes: - badguy + spazbot The ba.SpazBot that was killed. killerplayer @@ -79,10 +79,10 @@ class SpazBotDiedMessage: The particular type of death. """ - def __init__(self, badguy: SpazBot, killerplayer: Optional[ba.Player], + def __init__(self, spazbot: SpazBot, killerplayer: Optional[ba.Player], how: ba.DeathType): """Instantiate with given values.""" - self.badguy = badguy + self.spazbot = spazbot self.killerplayer = killerplayer self.how = how diff --git a/assets/src/ba_data/python/bastd/game/easteregghunt.py b/assets/src/ba_data/python/bastd/game/easteregghunt.py index ae0c0f48..503d4196 100644 --- a/assets/src/ba_data/python/bastd/game/easteregghunt.py +++ b/assets/src/ba_data/python/bastd/game/easteregghunt.py @@ -216,8 +216,8 @@ class EasterEggHuntGame(ba.TeamGameActivity[Player, Team]): # Whenever our evil bunny dies, respawn him and spew some eggs. elif isinstance(msg, SpazBotDiedMessage): self._spawn_evil_bunny() - assert msg.badguy.node - pos = msg.badguy.node.position + assert msg.spazbot.node + pos = msg.spazbot.node.position for _i in range(6): spread = 0.4 self._eggs.append( diff --git a/assets/src/ba_data/python/bastd/game/football.py b/assets/src/ba_data/python/bastd/game/football.py index c428a64d..5ded7ace 100644 --- a/assets/src/ba_data/python/bastd/game/football.py +++ b/assets/src/ba_data/python/bastd/game/football.py @@ -816,7 +816,7 @@ class FootballCoopGame(ba.CoopGameActivity[Player, Team]): elif isinstance(msg, SpazBotDiedMessage): # Every time a bad guy dies, spawn a new one. - ba.timer(3.0, ba.Call(self._spawn_bot, (type(msg.badguy)))) + ba.timer(3.0, ba.Call(self._spawn_bot, (type(msg.spazbot)))) elif isinstance(msg, SpazBotPunchedMessage): if self._preset in ['rookie', 'rookie_easy']: diff --git a/assets/src/ba_data/python/bastd/game/hockey.py b/assets/src/ba_data/python/bastd/game/hockey.py index 26af046c..bfb6e196 100644 --- a/assets/src/ba_data/python/bastd/game/hockey.py +++ b/assets/src/ba_data/python/bastd/game/hockey.py @@ -36,7 +36,7 @@ if TYPE_CHECKING: from typing import Any, Sequence, Dict, Type, List, Optional, Union -class PuckDeathMessage: +class PuckDiedMessage: """Inform something that a puck has died.""" def __init__(self, puck: Puck): @@ -78,7 +78,7 @@ class Puck(ba.Actor): self.node.delete() activity = self._activity() if activity and not msg.immediate: - activity.handlemessage(PuckDeathMessage(self)) + activity.handlemessage(PuckDiedMessage(self)) # If we go out of bounds, move back to where we started. elif isinstance(msg, ba.OutOfBoundsMessage): @@ -113,6 +113,9 @@ class Player(ba.Player['Team']): class Team(ba.Team[Player]): """Our team type for this game.""" + def __init__(self) -> None: + self.score = 0 + # ba_meta export game class HockeyGame(ba.TeamGameActivity[Player, Team]): @@ -196,26 +199,28 @@ class HockeyGame(ba.TeamGameActivity[Player, Team]): self._puck_spawn_pos: Optional[Sequence[float]] = None self._score_regions: Optional[List[ba.NodeActor]] = None self._puck: Optional[Puck] = None + self._score_to_win = int(settings['Score to Win']) + self._time_limit = float(settings['Time Limit']) def get_instance_description(self) -> Union[str, Sequence]: - if self.settings_raw['Score to Win'] == 1: + if self._score_to_win == 1: return 'Score a goal.' - return 'Score ${ARG1} goals.', self.settings_raw['Score to Win'] + return 'Score ${ARG1} goals.', self._score_to_win def get_instance_description_short(self) -> Union[str, Sequence]: - if self.settings_raw['Score to Win'] == 1: + if self._score_to_win == 1: return 'score a goal' - return 'score ${ARG1} goals', self.settings_raw['Score to Win'] + return 'score ${ARG1} goals', self._score_to_win def on_begin(self) -> None: super().on_begin() - self.setup_standard_time_limit(self.settings_raw['Time Limit']) + self.setup_standard_time_limit(self._time_limit) self.setup_standard_powerup_drops() self._puck_spawn_pos = self.map.get_flag_position(None) self._spawn_puck() - # set up the two score regions + # Set up the two score regions. defs = self.map.defs self._score_regions = [] self._score_regions.append( @@ -240,7 +245,6 @@ class HockeyGame(ba.TeamGameActivity[Player, Team]): ba.playsound(self._chant_sound) def on_team_join(self, team: Team) -> None: - team.gamedata['score'] = 0 self._update_scoreboard() def _handle_puck_player_collide(self) -> None: @@ -274,7 +278,7 @@ class HockeyGame(ba.TeamGameActivity[Player, Team]): for team in self.teams: if team.id == index: scoring_team = team - team.gamedata['score'] += 1 + team.score += 1 # Tell all players to celebrate. for player in team.players: @@ -291,7 +295,7 @@ class HockeyGame(ba.TeamGameActivity[Player, Team]): big_message=True) # End game if we won. - if team.gamedata['score'] >= self.settings_raw['Score to Win']: + if team.score >= self._score_to_win: self.end_game() ba.playsound(self._foghorn_sound) @@ -317,14 +321,13 @@ class HockeyGame(ba.TeamGameActivity[Player, Team]): def end_game(self) -> None: results = ba.TeamGameResults() for team in self.teams: - results.set_team_score(team, team.gamedata['score']) + results.set_team_score(team, team.score) self.end(results=results) def _update_scoreboard(self) -> None: - winscore = self.settings_raw['Score to Win'] + winscore = self._score_to_win for team in self.teams: - self._scoreboard.set_team_value(team, team.gamedata['score'], - winscore) + self._scoreboard.set_team_value(team, team.score, winscore) def handlemessage(self, msg: Any) -> Any: @@ -335,7 +338,7 @@ class HockeyGame(ba.TeamGameActivity[Player, Team]): self.respawn_player(msg.getplayer(Player)) # Respawn dead pucks. - elif isinstance(msg, PuckDeathMessage): + elif isinstance(msg, PuckDiedMessage): if not self.has_ended(): ba.timer(3.0, self._spawn_puck) else: diff --git a/assets/src/ba_data/python/bastd/game/onslaught.py b/assets/src/ba_data/python/bastd/game/onslaught.py index dd7bf670..2ba9846d 100644 --- a/assets/src/ba_data/python/bastd/game/onslaught.py +++ b/assets/src/ba_data/python/bastd/game/onslaught.py @@ -1220,13 +1220,13 @@ class OnslaughtGame(ba.CoopGameActivity[Player, Team]): ba.timer(0.1, self._checkroundover) elif isinstance(msg, SpazBotDiedMessage): - pts, importance = msg.badguy.get_death_points(msg.how) + pts, importance = msg.spazbot.get_death_points(msg.how) if msg.killerplayer is not None: self._handle_kill_achievements(msg) target: Optional[Sequence[float]] try: - assert msg.badguy.node - target = msg.badguy.node.position + assert msg.spazbot.node + target = msg.spazbot.node.position except Exception: ba.print_exception() target = None @@ -1265,13 +1265,13 @@ class OnslaughtGame(ba.CoopGameActivity[Player, Team]): def _handle_uber_kill_achievements(self, msg: SpazBotDiedMessage) -> None: # Uber mine achievement: - if msg.badguy.last_attacked_type == ('explosion', 'land_mine'): + if msg.spazbot.last_attacked_type == ('explosion', 'land_mine'): self._land_mine_kills += 1 if self._land_mine_kills >= 6: self._award_achievement('Gold Miner') # Uber tnt achievement: - if msg.badguy.last_attacked_type == ('explosion', 'tnt'): + if msg.spazbot.last_attacked_type == ('explosion', 'tnt'): self._tnt_kills += 1 if self._tnt_kills >= 6: ba.timer(0.5, ba.WeakCall(self._award_achievement, @@ -1280,7 +1280,7 @@ class OnslaughtGame(ba.CoopGameActivity[Player, Team]): def _handle_pro_kill_achievements(self, msg: SpazBotDiedMessage) -> None: # TNT achievement: - if msg.badguy.last_attacked_type == ('explosion', 'tnt'): + if msg.spazbot.last_attacked_type == ('explosion', 'tnt'): self._tnt_kills += 1 if self._tnt_kills >= 3: ba.timer( @@ -1291,7 +1291,7 @@ class OnslaughtGame(ba.CoopGameActivity[Player, Team]): def _handle_rookie_kill_achievements(self, msg: SpazBotDiedMessage) -> None: # Land-mine achievement: - if msg.badguy.last_attacked_type == ('explosion', 'land_mine'): + if msg.spazbot.last_attacked_type == ('explosion', 'land_mine'): self._land_mine_kills += 1 if self._land_mine_kills >= 3: self._award_achievement('Mine Games') @@ -1299,7 +1299,7 @@ class OnslaughtGame(ba.CoopGameActivity[Player, Team]): def _handle_training_kill_achievements(self, msg: SpazBotDiedMessage) -> None: # Toss-off-map achievement: - if msg.badguy.last_attacked_type == ('picked_up', 'default'): + if msg.spazbot.last_attacked_type == ('picked_up', 'default'): self._throw_off_kills += 1 if self._throw_off_kills >= 3: self._award_achievement('Off You Go Then') diff --git a/assets/src/ba_data/python/bastd/game/race.py b/assets/src/ba_data/python/bastd/game/race.py index df6de7bf..41485b2e 100644 --- a/assets/src/ba_data/python/bastd/game/race.py +++ b/assets/src/ba_data/python/bastd/game/race.py @@ -180,18 +180,18 @@ class RaceGame(ba.TeamGameActivity[Player, Team]): def get_instance_description(self) -> Union[str, Sequence]: if (isinstance(self.session, ba.DualTeamSession) - and self.settings_raw.get('Entire Team Must Finish', False)): + and self._entire_team_must_finish): t_str = ' Your entire team has to finish.' else: t_str = '' - if self.settings_raw['Laps'] > 1: - return 'Run ${ARG1} laps.' + t_str, self.settings_raw['Laps'] + if self._laps > 1: + return 'Run ${ARG1} laps.' + t_str, self._laps return 'Run 1 lap.' + t_str def get_instance_description_short(self) -> Union[str, Sequence]: - if self.settings_raw['Laps'] > 1: - return 'run ${ARG1} laps', self.settings_raw['Laps'] + if self._laps > 1: + return 'run ${ARG1} laps', self._laps return 'run 1 lap' def on_transition_in(self) -> None: @@ -715,8 +715,7 @@ class RaceGame(ba.TeamGameActivity[Player, Team]): def handlemessage(self, msg: Any) -> Any: if isinstance(msg, ba.PlayerDiedMessage): - # Augment default behavior. - super().handlemessage(msg) + super().handlemessage(msg) # Augment default behavior. player = msg.getplayer(Player) if not player.finished: self.respawn_player(player, respawn_time=1) diff --git a/assets/src/ba_data/python/bastd/game/runaround.py b/assets/src/ba_data/python/bastd/game/runaround.py index d5d0637c..f134816a 100644 --- a/assets/src/ba_data/python/bastd/game/runaround.py +++ b/assets/src/ba_data/python/bastd/game/runaround.py @@ -47,6 +47,10 @@ if TYPE_CHECKING: class Player(ba.Player['Team']): """Our player type for this game.""" + def __init__(self) -> None: + self.respawn_timer: Optional[ba.Timer] = None + self.respawn_icon: Optional[RespawnIcon] = None + class Team(ba.Team[Player]): """Our team type for this game.""" @@ -1113,20 +1117,20 @@ class RunaroundGame(ba.CoopGameActivity[Player, Team]): # Respawn them shortly. assert self.initial_player_info is not None respawn_time = 2.0 + len(self.initial_player_info) * 1.0 - player.gamedata['respawn_timer'] = ba.Timer( + player.respawn_timer = ba.Timer( respawn_time, ba.Call(self.spawn_player_if_exists, player)) - player.gamedata['respawn_icon'] = RespawnIcon(player, respawn_time) + player.respawn_icon = RespawnIcon(player, respawn_time) elif isinstance(msg, SpazBotDiedMessage): if msg.how is ba.DeathType.REACHED_GOAL: - return - pts, importance = msg.badguy.get_death_points(msg.how) + return None + pts, importance = msg.spazbot.get_death_points(msg.how) if msg.killerplayer is not None: target: Optional[Sequence[float]] try: - assert msg.badguy is not None - assert msg.badguy.node - target = msg.badguy.node.position + assert msg.spazbot is not None + assert msg.spazbot.node + target = msg.spazbot.node.position except Exception: ba.print_exception() target = None @@ -1151,7 +1155,8 @@ class RunaroundGame(ba.CoopGameActivity[Player, Team]): self._update_scores() else: - super().handlemessage(msg) + return super().handlemessage(msg) + return None def _get_bot_speed(self, bot_type: Type[SpazBot]) -> float: speed = self._bot_speed_map.get(bot_type) diff --git a/assets/src/ba_data/python/bastd/game/thelaststand.py b/assets/src/ba_data/python/bastd/game/thelaststand.py index 6840f909..ae9a1a9a 100644 --- a/assets/src/ba_data/python/bastd/game/thelaststand.py +++ b/assets/src/ba_data/python/bastd/game/thelaststand.py @@ -272,11 +272,11 @@ class TheLastStandGame(ba.CoopGameActivity[Player, Team]): self._update_scores() elif isinstance(msg, SpazBotDiedMessage): - pts, importance = msg.badguy.get_death_points(msg.how) + pts, importance = msg.spazbot.get_death_points(msg.how) target: Optional[Sequence[float]] if msg.killerplayer: - assert msg.badguy.node - target = msg.badguy.node.position + assert msg.spazbot.node + target = msg.spazbot.node.position self.stats.player_scored(msg.killerplayer, pts, target=target, diff --git a/assets/src/ba_data/python/bastd/tutorial.py b/assets/src/ba_data/python/bastd/tutorial.py index 0aca2a2f..2d9f3ffa 100644 --- a/assets/src/ba_data/python/bastd/tutorial.py +++ b/assets/src/ba_data/python/bastd/tutorial.py @@ -28,8 +28,8 @@ # pylint: disable=missing-function-docstring, missing-class-docstring # pylint: disable=invalid-name # pylint: disable=too-many-locals -# pylint: disable=unused-variable # pylint: disable=unused-argument +# pylint: disable=unused-variable from __future__ import annotations @@ -181,7 +181,21 @@ class ButtonRelease: timeformat=ba.TimeFormat.MILLISECONDS) -class TutorialActivity(ba.Activity[ba.Player, ba.Team]): +class Player(ba.Player['Team']): + """Our player type for this game.""" + + def __init__(self) -> None: + self.pressed = False + + +class Team(ba.Team[Player]): + """Our team type for this game.""" + + def __init__(self) -> None: + pass + + +class TutorialActivity(ba.Activity[Player, Team]): def __init__(self, settings: Dict[str, Any] = None): from bastd.maps import Rampage @@ -462,6 +476,7 @@ class TutorialActivity(ba.Activity[ba.Player, ba.Team]): n.opacity = 0.0 a.set_stick_image_position(0, 0) + # Can be used for debugging. class SetSpeed: def __init__(self, speed: int): @@ -2330,7 +2345,7 @@ class TutorialActivity(ba.Activity[ba.Player, ba.Team]): ba.WeakCall(self._read_entries)) def _update_skip_votes(self) -> None: - count = sum(1 for player in self.players if player.gamedata['pressed']) + count = sum(1 for player in self.players if player.pressed) assert self._skip_count_text self._skip_count_text.text = ba.Lstr( resource=self._r + '.skipVoteCountText', @@ -2349,7 +2364,7 @@ class TutorialActivity(ba.Activity[ba.Player, ba.Team]): self._skip_text.text = '' self.end() - def _player_pressed_button(self, player: ba.Player) -> None: + def _player_pressed_button(self, player: Player) -> None: # Special case: if there's only one player, we give them a # warning on their first press (some players were thinking the @@ -2363,7 +2378,7 @@ class TutorialActivity(ba.Activity[ba.Player, ba.Team]): self._skip_text.scale = 1.3 incr = 50 t = incr - for i in range(6): + for _i in range(6): ba.timer(t, ba.Call(setattr, self._skip_text, 'color', (1, 0.5, 0.1)), @@ -2376,7 +2391,7 @@ class TutorialActivity(ba.Activity[ba.Player, ba.Team]): ba.timer(6.0, ba.WeakCall(self._revert_confirm)) return - player.gamedata['pressed'] = True + player.pressed = True # test... if not all(self.players): @@ -2393,15 +2408,15 @@ class TutorialActivity(ba.Activity[ba.Player, ba.Team]): self._skip_text.color = (1, 1, 1) self._issued_warning = False - def on_player_join(self, player: ba.Player) -> None: + def on_player_join(self, player: Player) -> None: super().on_player_join(player) - player.gamedata['pressed'] = False - # we just wanna know if this player presses anything.. + + # We just wanna know if this player presses anything. player.assign_input_call( ('jumpPress', 'punchPress', 'bombPress', 'pickUpPress'), ba.Call(self._player_pressed_button, player)) - def on_player_leave(self, player: ba.Player) -> None: + def on_player_leave(self, player: Player) -> None: if not all(self.players): ba.print_error('Nonexistent player in on_player_leave: ' + str([str(p) for p in self.players]) + ': we are ' + diff --git a/docs/ba_module.md b/docs/ba_module.md index d6b1b011..7ad463ee 100644 --- a/docs/ba_module.md +++ b/docs/ba_module.md @@ -1,5 +1,5 @@ -

last updated on 2020-05-25 for Ballistica version 1.5.0 build 20029

+

last updated on 2020-05-27 for Ballistica version 1.5.0 build 20030

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 let me know. Happy modding!


@@ -97,6 +97,7 @@
  • ba.timer()
  • ba.timestring()
  • ba.vec3validate()
  • +
  • ba.verify_object_death()
  • Asset Classes