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()
@@ -1644,7 +1645,7 @@ and it should begin its actual game logic.
Attributes Inherited:
Methods Inherited:
-
+
Methods Defined or Overridden:
@@ -1987,7 +1988,7 @@ its time with lingering corpses, sound effects, etc.
Attributes Inherited:
Methods Inherited:
-
+
Methods Defined or Overridden:
-
@@ -2025,7 +2026,7 @@ its time with lingering corpses, sound effects, etc.
Attributes Inherited:
Methods Inherited:
-
+
Methods Defined or Overridden:
@@ -3329,7 +3330,7 @@ Use ba.getmodel() to instantiate one.
Attributes Inherited:
Methods Inherited:
-
+
Methods Defined or Overridden:
@@ -6522,6 +6523,16 @@ so this can be used to disambiguate 'Any' types).
Generally this should be used in 'if __debug__' or assert clauses
to keep runtime overhead minimal.
+
+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.
+
widget(edit: ba.Widget = None, up_widget: ba.Widget = None,
diff --git a/tools/efrotools/filecache.py b/tools/efrotools/filecache.py
index 6d1fa12b..4dcf9634 100644
--- a/tools/efrotools/filecache.py
+++ b/tools/efrotools/filecache.py
@@ -26,6 +26,7 @@ import json
import os
from typing import TYPE_CHECKING
+from efro.terminal import Clr
from efrotools import get_files_hash
if TYPE_CHECKING:
@@ -104,8 +105,8 @@ class FileCache:
# if anything has been modified, don't write.
for fname, mtime in self.mtimes.items():
if os.path.getmtime(fname) != mtime:
- print('File changed during run: "' + fname + '";' +
- ' cache not updated.')
+ print(f'{Clr.YLW}File changed during run:'
+ f' "{fname}"; cache not updated.{Clr.RST}')
return
out = json.dumps(self.entries)
with open(self._path, 'w') as outfile: