Added per-game team and player types

This commit is contained in:
Eric Froemling 2020-05-18 01:47:52 -07:00
parent b1d7d96386
commit 1ccf33e41d
68 changed files with 2383 additions and 1709 deletions

View File

@ -319,6 +319,7 @@
<w>cnode</w>
<w>codecsmodule</w>
<w>codefilenames</w>
<w>codefiles</w>
<w>codehash</w>
<w>codeop</w>
<w>collapsable</w>
@ -727,8 +728,10 @@
<w>gamepads</w>
<w>gamepadselect</w>
<w>gameplay</w>
<w>gameplayer</w>
<w>gameport</w>
<w>gameresults</w>
<w>gameteam</w>
<w>gametype</w>
<w>gametypes</w>
<w>gameutils</w>
@ -762,6 +765,7 @@
<w>getsession</w>
<w>getsockname</w>
<w>getsound</w>
<w>getspaz</w>
<w>getstarttime</w>
<w>gettext</w>
<w>gettexture</w>
@ -893,6 +897,7 @@
<w>inputfiles</w>
<w>inputhash</w>
<w>inputnode</w>
<w>inputtype</w>
<w>inpututils</w>
<w>inspectdir</w>
<w>insta</w>
@ -1319,6 +1324,7 @@
<w>pipname</w>
<w>pkey</w>
<w>pkgutil</w>
<w>playercast</w>
<w>playerdata</w>
<w>playerlostspaz</w>
<w>playernode</w>
@ -1326,6 +1332,8 @@
<w>playerpts</w>
<w>playerrec</w>
<w>playerspaz</w>
<w>playerteamdata</w>
<w>playertype</w>
<w>playerval</w>
<w>playlistui</w>
<w>playmode</w>
@ -1358,6 +1366,7 @@
<w>positionadjusted</w>
<w>posixpath</w>
<w>posixsubprocess</w>
<w>postinit</w>
<w>poststr</w>
<w>powerdown</w>
<w>powersgiven</w>
@ -1368,6 +1377,7 @@
<w>poweruptype</w>
<w>powervr</w>
<w>ppos</w>
<w>pproxy</w>
<w>pragmas</w>
<w>prch</w>
<w>prec</w>
@ -1547,6 +1557,7 @@
<w>runmypy</w>
<w>runonly</w>
<w>runpy</w>
<w>runpylint</w>
<w>runswindows</w>
<w>rval</w>
<w>safecolor</w>
@ -1610,6 +1621,8 @@
<w>sessiondata</w>
<w>sessionglobals</w>
<w>sessionname</w>
<w>sessionplayer</w>
<w>sessionteam</w>
<w>sessiontype</w>
<w>setalpha</w>
<w>setbuild</w>
@ -1685,6 +1698,7 @@
<w>srcnode</w>
<w>srcpath</w>
<w>srcpathfull</w>
<w>srcplayer</w>
<w>srcpy</w>
<w>srcpydata</w>
<w>srcstr</w>
@ -1720,6 +1734,7 @@
<w>strftime</w>
<w>stringprep</w>
<w>stringptr</w>
<w>strippable</w>
<w>strobing</w>
<w>strptime</w>
<w>strt</w>
@ -1785,11 +1800,13 @@
<w>tdelay</w>
<w>tdval</w>
<w>teambasesession</w>
<w>teamdata</w>
<w>teamgame</w>
<w>teamnamescolors</w>
<w>teamsize</w>
<w>teamsscorescreen</w>
<w>teamssession</w>
<w>teamtype</w>
<w>teeeeeeeesssssttttdddddddd</w>
<w>teehee</w>
<w>teleporting</w>
@ -1867,11 +1884,13 @@
<w>toplevel</w>
<w>totaldudes</w>
<w>totalpts</w>
<w>totype</w>
<w>touchpad</w>
<w>tournamententry</w>
<w>tournamentscores</w>
<w>tplayer</w>
<w>tpos</w>
<w>tproxy</w>
<w>tracebacks</w>
<w>tracemalloc</w>
<w>tradeoff</w>

View File

@ -50,6 +50,9 @@
- The bs.Vector class is no more; in its place is a shiny new ba.Vec3 which is implemented internally in C++ so its nice and speedy. Will probably update certain things like vector node attrs to support this class in the future since it makes vector math nice and convenient.
- Ok you get the point..
### 1.4.155 (14377)
- Added protection against a repeated-input attack in lobbies.
### 1.4.151 (14371)
- Added Chinese-Traditional language and improved translations for others.

View File

@ -25,7 +25,7 @@
# Targets in this top level Makefile do not expect -jX to be passed to them
# and generally handle spawning an appropriate number of child jobs themselves.
# Prefix used for output of docs/changelogs/etc targets for use in webpages.
# Prefix used for output of docs/changelogs/etc. targets for use in webpages.
DOCPREFIX = "ballisticacore_"
@ -475,12 +475,12 @@ update-check: prereqs
# Run formatting on all files in the project considered 'dirty'.
format:
@${MAKE} -j3 format-code format-scripts format-makefile
@echo Formatting complete!
@tools/snippets echo GRN Formatting complete!
# Same but always formats; ignores dirty state.
format-full:
@${MAKE} -j3 format-code-full format-scripts-full format-makefile
@echo Formatting complete!
@tools/snippets echo GRN Formatting complete!
# Run formatting for compiled code sources (.cc, .h, etc.).
format-code: prereqs
@ -515,22 +515,22 @@ format-makefile: prereqs
# Run all project checks. (static analysis)
check: update-check
@${MAKE} -j3 cpplint pylint mypy
@echo ALL CHECKS PASSED!
@tools/snippets echo GRN ALL CHECKS PASSED!
# Same as check but no caching (all files are checked).
check-full: update-check
@${MAKE} -j3 cpplint-full pylint-full mypy-full
@echo ALL CHECKS PASSED!
@tools/snippets echo GRN ALL CHECKS PASSED!
# Same as 'check' plus optional/slow extra checks.
check2: update-check
@${MAKE} -j4 cpplint pylint mypy pycharm
@echo ALL CHECKS PASSED!
@tools/snippets echo GRN ALL CHECKS PASSED!
# Same as check2 but no caching (all files are checked).
check2-full: update-check
@${MAKE} -j4 cpplint-full pylint-full mypy-full pycharm-full
@echo ALL CHECKS PASSED!
@tools/snippets echo GRN ALL CHECKS PASSED!
# Run Cpplint checks on all C/C++ code.
cpplint: prereqs
@ -586,6 +586,7 @@ pycharm-full: prereqs
# Run all tests. (live execution verification)
test: prereqs
@tools/snippets echo BLU Running all tests...
@tools/snippets pytest -v tests
# Run tests with any caching disabled.
@ -615,28 +616,28 @@ preflight:
@${MAKE} format
@${MAKE} update
@${MAKE} -j4 cpplint pylint mypy test
@echo PREFLIGHT SUCCESSFUL!
@tools/snippets echo SGRN BLD PREFLIGHT SUCCESSFUL!
# Same as 'preflight' without caching (all files are visited).
preflight-full:
@${MAKE} format-full
@${MAKE} update
@${MAKE} -j4 cpplint-full pylint-full mypy-full test-full
@echo PREFLIGHT SUCCESSFUL!
@tools/snippets echo SGRN BLD PREFLIGHT SUCCESSFUL!
# Same as 'preflight' plus optional/slow extra checks.
preflight2:
@${MAKE} format
@${MAKE} update
@${MAKE} -j5 cpplint pylint mypy pycharm test
@echo PREFLIGHT SUCCESSFUL!
@tools/snippets echo SGRN BLD PREFLIGHT SUCCESSFUL!
# Same as 'preflight2' but without caching (all files visited).
preflight2-full:
@${MAKE} format-full
@${MAKE} update
@${MAKE} -j5 cpplint-full pylint-full mypy-full pycharm-full test-full
@echo PREFLIGHT SUCCESSFUL!
@tools/snippets echo SGRN BLD PREFLIGHT SUCCESSFUL!
# Tell make which of these targets don't represent files.
.PHONY: preflight preflight-full preflight2 preflight2-full
@ -675,28 +676,28 @@ TOOL_CFG_SRC = tools/efrotools/snippets.py config/config.json
ENV_SRC = tools/snippets tools/batools/build.py
.clang-format: config/toolconfigsrc/clang-format ${TOOL_CFG_SRC}
${TOOL_CFG_INST} $< $@
@${TOOL_CFG_INST} $< $@
.style.yapf: config/toolconfigsrc/style.yapf ${TOOL_CFG_SRC}
${TOOL_CFG_INST} $< $@
@${TOOL_CFG_INST} $< $@
.pylintrc: config/toolconfigsrc/pylintrc ${TOOL_CFG_SRC}
${TOOL_CFG_INST} $< $@
@${TOOL_CFG_INST} $< $@
.projectile: config/toolconfigsrc/projectile ${TOOL_CFG_SRC}
${TOOL_CFG_INST} $< $@
@${TOOL_CFG_INST} $< $@
.editorconfig: config/toolconfigsrc/editorconfig ${TOOL_CFG_SRC}
${TOOL_CFG_INST} $< $@
@${TOOL_CFG_INST} $< $@
.dir-locals.el: config/toolconfigsrc/dir-locals.el ${TOOL_CFG_SRC}
${TOOL_CFG_INST} $< $@
@${TOOL_CFG_INST} $< $@
.mypy.ini: config/toolconfigsrc/mypy.ini ${TOOL_CFG_SRC}
${TOOL_CFG_INST} $< $@
@${TOOL_CFG_INST} $< $@
.pycheckers: config/toolconfigsrc/pycheckers ${TOOL_CFG_SRC}
${TOOL_CFG_INST} $< $@
@${TOOL_CFG_INST} $< $@
# Include anything as sources here that should require
.cache/checkenv: ${ENV_SRC}

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=233656876767477563471907026700832716822
# SOURCES_HASH=266649817838802754126771358652920545389
# I'm sorry Pylint. I know this file saddens you. Be strong.
# pylint: disable=useless-suppression
@ -259,7 +259,7 @@ class InputDevice:
allows_configuring: bool
Whether the input-device can be configured.
player: Optional[ba.Player]
player: Optional[ba.SessionPlayer]
The player associated with this input device.
client_id: int
@ -292,7 +292,7 @@ class InputDevice:
"""
exists: bool
allows_configuring: bool
player: Optional[ba.Player]
player: Optional[ba.SessionPlayer]
client_id: int
name: str
unique_identifier: str
@ -648,6 +648,14 @@ class Node:
billboard_texture: Optional[ba.Texture] = None
billboard_cross_out: bool = False
billboard_opacity: float = 0.0
slow_motion: bool = False
music: str = ''
vr_camera_offset: Sequence[float] = (0.0, 0.0, 0.0)
vr_overlay_center: Sequence[float] = (0.0, 0.0, 0.0)
vr_overlay_center_enabled: bool = False
vignette_outer: Sequence[float] = (0.0, 0.0)
vignette_inner: Sequence[float] = (0.0, 0.0)
tint: Sequence[float] = (1.0, 1.0, 1.0)
def add_death_action(self, action: Callable[[], None]) -> None:
"""add_death_action(action: Callable[[], None]) -> None
@ -737,31 +745,36 @@ class Node:
return None
class Player:
"""A reference to a player in the game.
class SessionData:
"""(internal)"""
def exists(self) -> bool:
"""exists() -> bool
Returns whether the SessionData still exists.
Most functionality will fail on a nonexistent instance.
"""
return bool()
class SessionPlayer:
"""A reference to a player in the ba.Session.
Category: Gameplay Classes
These are created and managed internally and
provided to your Session/Activity instances.
Be aware that, like ba.Nodes, ba.Player objects are 'weak'
Be aware that, like ba.Nodes, ba.SessionPlayer objects are 'weak'
references under-the-hood; a player can leave the game at
any point. For this reason, you should make judicious use of the
ba.Player.exists attribute (or boolean operator) to ensure that a
Player is still present if retaining references to one for any
length of time.
ba.SessionPlayer.exists attribute (or boolean operator) to ensure
that a SessionPlayer is still present if retaining references to one
for any length of time.
Attributes:
actor: Optional[ba.Actor]
The current ba.Actor associated with this Player.
This may be None
node: Optional[ba.Node]
A ba.Node of type 'player' associated with this Player.
This Node exists in the currently active game and can be used
to get a generic player position/etc.
This will be None if the Player is not in a game.
id: int
The unique numeric ID of the Player.
exists: bool
Whether the player still exists.
@ -775,10 +788,10 @@ class Player:
This bool value will be True once the Player has completed
any lobby character/team selection.
team: ba.Team
The ba.Team this Player is on. If the Player is
still in its lobby selecting a team/etc. then a
ba.TeamNotFoundError will be raised.
team: 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
@ -792,7 +805,7 @@ class Player:
color: Sequence[float]
The base color for this Player.
In team games this will match the ba.Team's color.
In team games this will match the ba.SessionTeam's color.
highlight: Sequence[float]
A secondary color for this player.
@ -802,17 +815,20 @@ class Player:
character: str
The character this player has selected in their profile.
gameplayer: Optional[ba.Player]
The current game-specific instance for this player.
"""
actor: Optional[ba.Actor]
node: Optional[ba.Node]
id: int
exists: bool
in_game: bool
team: ba.Team
team: ba.SessionTeam
sessiondata: Dict
gamedata: Dict
color: Sequence[float]
highlight: Sequence[float]
character: str
gameplayer: Optional[ba.Player]
def assign_input_call(self, type: Union[str, Tuple[str, ...]],
call: Callable) -> None:
@ -855,13 +871,6 @@ class Player:
"""
return {'foo': 'bar'}
def get_id(self) -> int:
"""get_id() -> int
Returns the unique numeric player ID for this player.
"""
return int()
def get_input_device(self) -> ba.InputDevice:
"""get_input_device() -> ba.InputDevice
@ -878,14 +887,6 @@ class Player:
"""
return str()
def is_alive(self) -> bool:
"""is_alive() -> bool
Returns True if the player has a ba.Actor assigned and its
is_alive() method return True. False is returned otherwise.
"""
return bool()
def remove_from_game(self) -> None:
"""remove_from_game() -> None
@ -914,17 +915,10 @@ class Player:
"""
return None
def set_actor(self, actor: Optional[ba.Actor]) -> None:
"""set_actor(actor: Optional[ba.Actor]) -> None
Set the player's associated ba.Actor.
"""
return None
def set_data(self, team: ba.Team, character: str, color: Sequence[float],
highlight: Sequence[float]) -> None:
"""set_data(team: ba.Team, character: str, color: Sequence[float],
highlight: Sequence[float]) -> None
def set_data(self, team: ba.SessionTeam, character: str,
color: Sequence[float], highlight: Sequence[float]) -> None:
"""set_data(team: ba.SessionTeam, character: str,
color: Sequence[float], highlight: Sequence[float]) -> None
(internal)
"""
@ -961,18 +955,6 @@ class Player:
return None
class SessionData:
"""(internal)"""
def exists(self) -> bool:
"""exists() -> bool
Returns whether the SessionData still exists.
Most functionality will fail on a nonexistent instance.
"""
return bool()
class Sound:
"""A reference to a sound.
@ -2011,7 +1993,7 @@ def get_foreground_host_activity() -> Optional[ba.Activity]:
is none.
"""
import ba # pylint: disable=cyclic-import
return ba.Activity({})
return ba.Activity(settings={})
def get_foreground_host_session() -> Optional[ba.Session]:
@ -2348,7 +2330,7 @@ def getactivity(doraise: bool = True) -> ba.Activity:
False then None is returned instead.
"""
import ba # pylint: disable=cyclic-import
return ba.Activity({})
return ba.Activity(settings={})
def getcollidemodel(name: str) -> ba.CollideModel:
@ -2905,7 +2887,7 @@ def new_activity(activity_type: Type[ba.Activity],
instantiated; You must go through this function.
"""
import ba # pylint: disable=cyclic-import
return ba.Activity({})
return ba.Activity(settings={})
def new_host_session(sessiontype: Type[ba.Session],

View File

@ -29,8 +29,8 @@ In some specific cases you may need to pull in individual submodules instead.
# pylint: disable=redefined-builtin
from _ba import (CollideModel, Context, ContextCall, Data, InputDevice,
Material, Model, Node, Player, Sound, Texture, Timer, Vec3,
Widget, buttonwidget, camerashake, checkboxwidget,
Material, Model, Node, SessionPlayer, Sound, Texture, Timer,
Vec3, Widget, buttonwidget, camerashake, checkboxwidget,
columnwidget, containerwidget, do_once, emitfx,
get_collision_info, getactivity, getcollidemodel, getmodel,
getnodes, getsession, getsound, gettexture, hscrollwidget,
@ -40,7 +40,7 @@ from _ba import (CollideModel, Context, ContextCall, Data, InputDevice,
charstr, textwidget, time, timer, open_url, widget)
from ba._activity import Activity
from ba._actor import Actor
from ba._player import BasePlayerData
from ba._player import Player, playercast, playercast_o
from ba._nodeactor import NodeActor
from ba._app import App
from ba._coopgame import CoopGameActivity
@ -48,11 +48,12 @@ from ba._coopsession import CoopSession
from ba._dependency import (Dependency, DependencyComponent, DependencySet,
AssetPackage)
from ba._enums import TimeType, Permission, TimeFormat, SpecialChar
from ba._error import (UNHANDLED, print_exception, print_error, NotFoundError,
PlayerNotFoundError, NodeNotFoundError,
ActorNotFoundError, InputDeviceNotFoundError,
WidgetNotFoundError, ActivityNotFoundError,
TeamNotFoundError, SessionNotFoundError,
from ba._error import (print_exception, print_error, NotFoundError,
PlayerNotFoundError, SessionPlayerNotFoundError,
NodeNotFoundError, ActorNotFoundError,
InputDeviceNotFoundError, WidgetNotFoundError,
ActivityNotFoundError, TeamNotFoundError,
SessionTeamNotFoundError, SessionNotFoundError,
DependencyError)
from ba._freeforallsession import FreeForAllSession
from ba._gameactivity import GameActivity
@ -63,7 +64,7 @@ from ba._session import Session
from ba._servermode import ServerController
from ba._score import ScoreType, ScoreInfo
from ba._stats import PlayerScoredMessage, PlayerRecord, Stats
from ba._team import Team
from ba._team import SessionTeam, Team
from ba._teamgame import TeamGameActivity
from ba._dualteamsession import DualTeamSession
from ba._achievement import Achievement
@ -77,7 +78,7 @@ from ba._general import WeakCall, Call
from ba._level import Level
from ba._lobby import Lobby, Chooser
from ba._math import normalized_color, is_point_in_box, vec3validate
from ba._messages import (OutOfBoundsMessage, DeathType, DieMessage,
from ba._messages import (UNHANDLED, OutOfBoundsMessage, DeathType, DieMessage,
StandMessage, PickUpMessage, DropMessage,
PickedUpMessage, DroppedMessage,
ShouldShatterMessage, ImpactDamageMessage,

View File

@ -22,10 +22,13 @@
from __future__ import annotations
import weakref
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Generic, TypeVar
import _ba
from ba._team import Team
from ba._player import Player
from ba._error import print_exception, print_error, SessionTeamNotFoundError
from ba._dependency import DependencyComponent
import _ba
if TYPE_CHECKING:
from weakref import ReferenceType
@ -33,8 +36,11 @@ if TYPE_CHECKING:
import ba
from bastd.actor.respawnicon import RespawnIcon
PlayerType = TypeVar('PlayerType', bound=Player)
TeamType = TypeVar('TeamType', bound=Team)
class Activity(DependencyComponent):
class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
"""Units of execution wrangled by a ba.Session.
Category: Gameplay Classes
@ -66,13 +72,73 @@ class Activity(DependencyComponent):
# pylint: disable=too-many-public-methods
# Annotating attr types at the class level lets us introspect them.
# Annotating attr types at the class level lets us introspect at runtime.
settings_raw: Dict[str, Any]
teams: List[ba.Team]
players: List[ba.Player]
teams: List[TeamType]
players: List[PlayerType]
# Whether to print every time a player dies. This can be pertinent
# in games such as Death-Match but can be annoying in games where it
# doesn't matter.
announce_player_deaths = False
# Joining activities are for waiting for initial player joins.
# They are treated slightly differently than regular activities,
# mainly in that all players are passed to the activity at once
# instead of as each joins.
is_joining_activity = False
# Whether game-time should still progress when in menus/etc.
allow_pausing = False
# Whether idle players can potentially be kicked (should not happen in
# menus/etc).
allow_kick_idle_players = True
# In vr mode, this determines whether overlay nodes (text, images, etc)
# are created at a fixed position in space or one that moves based on
# the current map. Generally this should be on for games and off for
# transitions/score-screens/etc. that persist between maps.
use_fixed_vr_overlay = False
# If True, runs in slow motion and turns down sound pitch.
slow_motion = False
# Set this to True to inherit slow motion setting from previous
# activity (useful for transitions to avoid hitches).
inherits_slow_motion = False
# Set this to True to keep playing the music from the previous activity
# (without even restarting it).
inherits_music = False
# Set this to true to inherit VR camera offsets from the previous
# activity (useful for preventing sporadic camera movement
# during transitions).
inherits_camera_vr_offset = False
# Set this to true to inherit (non-fixed) VR overlay positioning from
# the previous activity (useful for prevent sporadic overlay jostling
# during transitions).
inherits_vr_overlay_center = False
# Set this to true to inherit screen tint/vignette colors from the
# previous activity (useful to prevent sudden color changes during
# transitions).
inherits_tint = False
# If the activity fades or transitions in, it should set the length of
# time here so that previous activities will be kept alive for that
# long (avoiding 'holes' in the screen)
# This value is given in real-time seconds.
transition_time = 0.0
# Is it ok to show an ad after this activity ends before showing
# the next activity?
can_show_ad_on_death = False
def __init__(self, settings: Dict[str, Any]):
"""Creates an activity in the current ba.Session.
"""Creates an Activity in the current ba.Session.
The activity will not be actually run until ba.Session.set_activity()
is called. 'settings' should be a dict of key/value pairs specific
@ -84,14 +150,20 @@ class Activity(DependencyComponent):
"""
super().__init__()
# FIXME: Relocate this stuff.
# Create our internal engine data.
self._activity_data = _ba.register_activity(self)
# Player/Team types should have been specified as type args;
# grab those.
self._playertype: Type[PlayerType]
self._teamtype: Type[TeamType]
self._setup_player_and_team_types()
# FIXME: Relocate or remove the need for this stuff.
self.sharedobjs: Dict[str, Any] = {}
self.paused_text: Optional[ba.Actor] = None
self.spaz_respawn_icons_right: Dict[int, RespawnIcon]
# Create our internal engine data.
self._activity_data = _ba.register_activity(self)
session = _ba.getsession()
if session is None:
raise Exception('No current session')
@ -105,8 +177,9 @@ class Activity(DependencyComponent):
if _ba.getactivity(doraise=False) is not self:
raise Exception('invalid context state')
# Should perhaps kill this; activities should validate/store whatever
# settings they need at init time (in a more type-safe way).
# Hopefully can eventually kill this; activities should
# validate/store whatever settings they need at init time
# (in a more type-safe way).
self.settings_raw = settings
self._has_transitioned_in = False
@ -122,66 +195,6 @@ class Activity(DependencyComponent):
self._activity_death_check_timer: Optional[ba.Timer] = None
self._expired = False
# Whether to print every time a player dies. This can be pertinent
# in games such as Death-Match but can be annoying in games where it
# doesn't matter.
self.announce_player_deaths = False
# Joining activities are for waiting for initial player joins.
# They are treated slightly differently than regular activities,
# mainly in that all players are passed to the activity at once
# instead of as each joins.
self.is_joining_activity = False
# Whether game-time should still progress when in menus/etc.
self.allow_pausing = False
# Whether idle players can potentially be kicked (should not happen in
# menus/etc).
self.allow_kick_idle_players = True
# In vr mode, this determines whether overlay nodes (text, images, etc)
# are created at a fixed position in space or one that moves based on
# the current map. Generally this should be on for games and off for
# transitions/score-screens/etc. that persist between maps.
self.use_fixed_vr_overlay = False
# If True, runs in slow motion and turns down sound pitch.
self.slow_motion = False
# Set this to True to inherit slow motion setting from previous
# activity (useful for transitions to avoid hitches).
self.inherits_slow_motion = False
# Set this to True to keep playing the music from the previous activity
# (without even restarting it).
self.inherits_music = False
# Set this to true to inherit VR camera offsets from the previous
# activity (useful for preventing sporadic camera movement
# during transitions).
self.inherits_camera_vr_offset = False
# Set this to true to inherit (non-fixed) VR overlay positioning from
# the previous activity (useful for prevent sporadic overlay jostling
# during transitions).
self.inherits_vr_overlay_center = False
# Set this to true to inherit screen tint/vignette colors from the
# previous activity (useful to prevent sudden color changes during
# transitions).
self.inherits_tint = False
# If the activity fades or transitions in, it should set the length of
# time here so that previous activities will be kept alive for that
# long (avoiding 'holes' in the screen)
# This value is given in real-time seconds.
self.transition_time = 0.0
# Is it ok to show an ad after this activity ends before showing
# the next activity?
self.can_show_ad_on_death = False
# This gets set once another activity has begun transitioning in but
# before this one is killed. The on_transition_out() method is also
# called at this time. Make sure to not assign player inputs,
@ -194,48 +207,16 @@ class Activity(DependencyComponent):
# is dying.
self._actor_refs: List[ba.Actor] = []
self._actor_weak_refs: List[ReferenceType[ba.Actor]] = []
self._last_dead_object_prune_time = _ba.time()
self._last_prune_dead_actors_time = _ba.time()
self._prune_dead_actors_timer: Optional[ba.Timer] = None
# This stuff gets filled in just before on_begin() is called.
self.teams = []
self.players = []
self.lobby = None
self._stats: Optional[ba.Stats] = None
self.lobby = None
self._prune_dead_objects_timer: Optional[ba.Timer] = None
@property
def stats(self) -> ba.Stats:
"""The stats instance accessible while the activity is running.
If access is attempted before or after, raises a ba.NotFoundError.
"""
if self._stats is None:
from ba._error import NotFoundError
raise NotFoundError()
return self._stats
def on_expire(self) -> None:
"""Called when your activity is being expired.
If your activity has created anything explicitly that may be retaining
a strong reference to the activity and preventing it from dying, you
should clear that out here. From this point on your activity's sole
purpose in life is to hit zero references and die so the next activity
can begin.
"""
def is_expired(self) -> bool:
"""Return whether the activity is expired.
An activity is set as expired when shutting down.
At this point no new nodes, timers, etc should be made,
run, etc, and the activity should be considered a 'zombie'.
"""
return self._expired
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
@ -259,6 +240,47 @@ class Activity(DependencyComponent):
else:
_ba.pushcall(session.begin_next_activity)
@property
def stats(self) -> ba.Stats:
"""The stats instance accessible while the activity is running.
If access is attempted before or after, raises a ba.NotFoundError.
"""
if self._stats is None:
from ba._error import NotFoundError
raise NotFoundError()
return self._stats
def on_expire(self) -> None:
"""Called when your activity is being expired.
If your activity has created anything explicitly that may be retaining
a strong reference to the activity and preventing it from dying, you
should clear that out here. From this point on your activity's sole
purpose in life is to hit zero references and die so the next activity
can begin.
"""
@property
def expired(self) -> bool:
"""Whether the activity is expired.
An activity is set as expired when shutting down.
At this point no new nodes, timers, etc should be made,
run, etc, and the activity should be considered a 'zombie'.
"""
return self._expired
@property
def playertype(self) -> Type[PlayerType]:
"""The type of ba.Player this Activity is using."""
return self._playertype
@property
def teamtype(self) -> Type[TeamType]:
"""The type of ba.Team this Activity is using."""
return self._teamtype
def set_has_ended(self, val: bool) -> None:
"""(internal)"""
self._has_ended = val
@ -277,18 +299,11 @@ class Activity(DependencyComponent):
self._should_end_immediately_results = results
self._should_end_immediately_delay = delay
def _get_player_icon(self, player: ba.Player) -> Dict[str, Any]:
def destroy(self) -> None:
"""Begin the process of tearing down the activity.
# Do we want to cache these somehow?
info = player.get_icon_info()
return {
'texture': _ba.gettexture(info['texture']),
'tint_texture': _ba.gettexture(info['tint_texture']),
'tint_color': info['tint_color'],
'tint2_color': info['tint2_color']
}
def _destroy(self) -> None:
(internal)
"""
from ba._general import Call
from ba._enums import TimeType
@ -313,7 +328,8 @@ class Activity(DependencyComponent):
with _ba.Context('empty'):
self._expire()
else:
raise Exception('_destroy() called multiple times')
raise RuntimeError(f'destroy() called when'
f' already expired for {self}')
@classmethod
def _check_activity_death(cls, activity_ref: ReferenceType[Activity],
@ -351,54 +367,54 @@ class Activity(DependencyComponent):
_ba.quit()
except Exception:
from ba import _error
_error.print_exception('exception on _check_activity_death:')
print_exception('exception on _check_activity_death:')
def _expire(self) -> None:
from ba import _error
"""Put the activity in a state where it can be garbage-collected.
This involves clearing anything that might be holding a reference
to it, etc.
"""
assert not self._expired
self._expired = True
# Do some default cleanup.
try:
try:
self.on_expire()
except Exception:
_error.print_exception('Error in activity on_expire()', self)
# Send finalize notices to all remaining actors.
for actor_ref in self._actor_weak_refs:
try:
actor = actor_ref()
if actor is not None:
actor.on_expire()
except Exception:
_error.print_exception(
'Exception on ba.Activity._expire()'
' in actor on_expire():', actor_ref())
# Reset all players.
# (releases any attached actors, clears game-data, etc)
for player in self.players:
if player:
try:
player.reset()
player.set_activity(None)
except Exception:
_error.print_exception(
'Exception on ba.Activity._expire()'
' resetting player:', player)
# Ditto with teams.
for team in self.teams:
try:
team.reset()
except Exception:
_error.print_exception(
'Exception on ba.Activity._expire() resetting team:',
team)
self.on_expire()
except Exception:
_error.print_exception('Exception during ba.Activity._expire():')
print_exception(f'Error in Activity on_expire() for {self}')
# Send expire notices to all remaining actors.
for actor_ref in self._actor_weak_refs:
try:
actor = actor_ref()
if actor is not None:
actor.on_expire()
except Exception:
print_exception(f'Error expiring Actor {actor_ref()}')
# Reset all Players.
# (releases any attached actors, clears game-data, etc)
for player in self.players:
if player:
try:
sessionplayer = player.sessionplayer
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
sessionteam.gameteam = None
sessionteam.reset_gamedata()
except SessionTeamNotFoundError:
pass
except Exception:
print_exception(f'Error resetting Team {team}')
# Regardless of what happened here, we want to destroy our data, as
# our activity might not go down if we don't. This will kill all
@ -407,13 +423,13 @@ class Activity(DependencyComponent):
try:
self._activity_data.destroy()
except Exception:
_error.print_exception(
print_exception(
'Exception during ba.Activity._expire() destroying data:')
def _prune_dead_objects(self) -> None:
def _prune_dead_actors(self) -> None:
self._actor_refs = [a for a in self._actor_refs if a]
self._actor_weak_refs = [a for a in self._actor_weak_refs if a()]
self._last_dead_object_prune_time = _ba.time()
self._last_prune_dead_actors_time = _ba.time()
def retain_actor(self, actor: ba.Actor) -> None:
"""Add a strong-reference to a ba.Actor to this Activity.
@ -423,16 +439,14 @@ class Activity(DependencyComponent):
is a convenient way to access this same functionality.
"""
from ba import _actor as bsactor
from ba import _error
if not isinstance(actor, bsactor.Actor):
raise Exception('non-actor passed to _retain_actor')
if (self.has_transitioned_in()
and _ba.time() - self._last_dead_object_prune_time > 10.0):
_error.print_error('it looks like nodes/actors are not'
' being pruned in your activity;'
' did you call Activity.on_transition_in()'
' from your subclass?; ' + str(self) +
' (loc. a)')
and _ba.time() - self._last_prune_dead_actors_time > 10.0):
print_error('it looks like nodes/actors are not'
' being pruned in your activity;'
' did you call Activity.on_transition_in()'
' from your subclass?; ' + str(self) + ' (loc. a)')
self._actor_refs.append(actor)
def add_actor_weak_ref(self, actor: ba.Actor) -> None:
@ -441,16 +455,14 @@ class Activity(DependencyComponent):
(called by the ba.Actor base class)
"""
from ba import _actor as bsactor
from ba import _error
if not isinstance(actor, bsactor.Actor):
raise Exception('non-actor passed to _add_actor_weak_ref')
if (self.has_transitioned_in()
and _ba.time() - self._last_dead_object_prune_time > 10.0):
_error.print_error('it looks like nodes/actors are '
'not being pruned in your activity;'
' did you call Activity.on_transition_in()'
' from your subclass?; ' + str(self) +
' (loc. b)')
and _ba.time() - self._last_prune_dead_actors_time > 10.0):
print_error('it looks like nodes/actors are '
'not being pruned in your activity;'
' did you call Activity.on_transition_in()'
' from your subclass?; ' + str(self) + ' (loc. b)')
self._actor_weak_refs.append(weakref.ref(actor))
@property
@ -465,48 +477,38 @@ class Activity(DependencyComponent):
raise SessionNotFoundError()
return session
def on_player_join(self, player: ba.Player) -> None:
def on_player_join(self, player: PlayerType) -> None:
"""Called when a new ba.Player has joined the Activity.
(including the initial set of Players)
"""
def on_player_leave(self, player: ba.Player) -> None:
def on_player_leave(self, player: PlayerType) -> None:
"""Called when a ba.Player is leaving the Activity."""
def on_team_join(self, team: ba.Team) -> None:
def on_team_join(self, team: TeamType) -> None:
"""Called when a new ba.Team joins the Activity.
(including the initial set of Teams)
"""
def on_team_leave(self, team: ba.Team) -> None:
def on_team_leave(self, team: TeamType) -> None:
"""Called when a ba.Team leaves the Activity."""
def on_transition_in(self) -> None:
"""Called when the Activity is first becoming visible.
Upon this call, the Activity should fade in backgrounds,
start playing music, etc. It does not yet have access to ba.Players
or ba.Teams, however. They remain owned by the previous Activity
start playing music, etc. It does not yet have access to players
or teams, however. They remain owned by the previous Activity
up until ba.Activity.on_begin() is called.
"""
from ba._general import WeakCall
self._called_activity_on_transition_in = True
# Start pruning our transient actors periodically.
self._prune_dead_objects_timer = _ba.Timer(
5.17, WeakCall(self._prune_dead_objects), repeat=True)
self._prune_dead_objects()
# Also start our low-level scene-graph running.
self._activity_data.start()
def on_transition_out(self) -> None:
"""Called when your activity begins transitioning out.
Note that this may happen at any time even if finish() has not been
Note that this may happen at any time even if end() has not been
called.
"""
@ -552,110 +554,223 @@ class Activity(DependencyComponent):
"""Return whether on_transition_out() has been called."""
return self._transitioning_out
def start_transition_in(self) -> None:
"""Called by Session to kick of transition-in.
def transition_in(self, prev_globals: Optional[ba.Node]) -> None:
"""Called by Session to kick off transition-in.
(internal)
"""
from ba._general import WeakCall
from ba._gameutils import sharedobj
assert not self._has_transitioned_in
self._has_transitioned_in = True
self.on_transition_in()
def create_player_node(self, player: ba.Player) -> ba.Node:
"""Create the 'player' node associated with the provided ba.Player."""
from ba._nodeactor import NodeActor
# Set up the globals node based on our settings.
with _ba.Context(self):
node = _ba.newnode('player', attrs={'playerID': player.get_id()})
# FIXME: Should add a dedicated slot for this on ba.Player
# instead of cluttering up their gamedata dict.
player.gamedata['_playernode'] = NodeActor(node)
return node
def begin(self, session: ba.Session) -> None:
"""Begin the activity. (should only be called by Session).
(internal)"""
# pylint: disable=too-many-branches
from ba import _error
if self._has_begun:
_error.print_error("_begin called twice; this shouldn't happen")
return
self._stats = session.stats
# Operate on the subset of session players who have passed team/char
# selection.
players = []
chooser_players = []
for player in session.players:
assert player # should we ever have invalid players?..
if player:
try:
team: Optional[ba.Team] = player.team
except _error.TeamNotFoundError:
team = None
if team is not None:
player.reset_input()
players.append(player)
else:
# Simply ignore players sitting in the lobby.
# (though this technically shouldn't happen anymore since
# choosers now get cleared when starting new activities.)
print('unexpected: got no-team player in _begin')
chooser_players.append(player)
# Now that it's going to be front and center,
# set some global values based on what the activity wants.
glb = sharedobj('globals')
glb.use_fixed_vr_overlay = self.use_fixed_vr_overlay
glb.allow_kick_idle_players = self.allow_kick_idle_players
if self.inherits_slow_motion and prev_globals is not None:
glb.slow_motion = prev_globals.slow_motion
else:
_error.print_error(
'got nonexistent player in Activity._begin()')
glb.slow_motion = self.slow_motion
if self.inherits_music and prev_globals is not None:
glb.music_continuous = True # Prevent restarting same music.
glb.music = prev_globals.music
glb.music_count += 1
if self.inherits_camera_vr_offset and prev_globals is not None:
glb.vr_camera_offset = prev_globals.vr_camera_offset
if self.inherits_vr_overlay_center and prev_globals is not None:
glb.vr_overlay_center = prev_globals.vr_overlay_center
glb.vr_overlay_center_enabled = (
prev_globals.vr_overlay_center_enabled)
# Add teams in one by one and send team-joined messages for each.
for team in session.teams:
if team in self.teams:
raise Exception('Duplicate Team Entry')
# If they want to inherit tint from the previous self.
if self.inherits_tint and prev_globals is not None:
glb.tint = prev_globals.tint
glb.vignette_outer = prev_globals.vignette_outer
glb.vignette_inner = prev_globals.vignette_inner
# Start pruning our transient actors periodically.
self._prune_dead_actors_timer = _ba.Timer(
5.17, WeakCall(self._prune_dead_actors), repeat=True)
self._prune_dead_actors()
# Also start our low-level scene running.
self._activity_data.start()
try:
self.on_transition_in()
except Exception:
print_exception('Error in on_transition_in for', self)
# Tell the C++ layer that this activity is the main one, so it uses
# settings from our globals, directs various events to us, etc.
self._activity_data.make_foreground()
def transition_out(self) -> None:
"""Called by the Session to start us transitioning out."""
assert not self._transitioning_out
self._transitioning_out = True
with _ba.Context(self):
try:
self.on_transition_out()
except Exception:
print_exception('Error in on_transition_out for', self)
def create_player(self, sessionplayer: ba.SessionPlayer) -> PlayerType:
"""Create the Player instance for this Activity.
Subclasses can override this if the activity's player class
requires a custom constructor; otherwise it will be called with
no args. Note that the player object should not be used at this
point as it is not yet fully wired up; wait for on_player_join()
for that.
"""
del sessionplayer # Unused
player = self._playertype()
return player
def create_team(self, sessionteam: ba.SessionTeam) -> TeamType:
"""Create the Team instance for this Activity.
Subclasses can override this if the activity's team class
requires a custom constructor; otherwise it will be called with
no args. Note that the team object should not be used at this
point as it is not yet fully wired up; wait for on_team_join()
for that.
"""
del sessionteam # Unused.
team = self._teamtype()
return team
def add_player(self, sessionplayer: ba.SessionPlayer) -> None:
"""(internal)"""
assert sessionplayer.team is not None
sessionplayer.reset_input()
sessionteam = sessionplayer.team
assert sessionplayer in sessionteam.players
team = sessionteam.gameteam
assert team is not None
sessionplayer.set_activity(self)
with _ba.Context(self):
sessionplayer.gameplayer = player = self.create_player(
sessionplayer)
player.postinit(sessionplayer)
team.players.append(player)
self.players.append(player)
try:
self.on_player_join(player)
except Exception:
print_exception('Error in on_player_join for', self)
def remove_player(self, sessionplayer: ba.SessionPlayer) -> None:
"""(internal)"""
# This should only be called on unexpired activities
# the player has been added to.
assert not self.expired
player = sessionplayer.gameplayer
assert isinstance(player, self._playertype)
assert player in self.players
self.players.remove(player)
with _ba.Context(self):
# Make a decent attempt to persevere if user code breaks.
try:
self.on_player_leave(player)
except Exception:
print_exception(f'Error in on_player_leave for {self}')
try:
sessionplayer.reset()
sessionplayer.set_node(None)
sessionplayer.set_activity(None)
except Exception:
print_exception(f'Error resetting player for {self}')
def add_team(self, sessionteam: ba.SessionTeam) -> None:
"""(internal)"""
assert not self.expired
with _ba.Context(self):
sessionteam.gameteam = team = self.create_team(sessionteam)
team.postinit(sessionteam)
self.teams.append(team)
try:
with _ba.Context(self):
self.on_team_join(team)
self.on_team_join(team)
except Exception:
_error.print_exception('Error in on_team_join for', self)
print_exception(f'Error in on_team_join for {self}')
# Now add each player to the activity and to its team's list,
# and send player-joined messages for each.
for player in players:
self.players.append(player)
player.team.players.append(player)
player.set_activity(self)
pnode = self.create_player_node(player)
player.set_node(pnode)
try:
with _ba.Context(self):
self.on_player_join(player)
except Exception:
_error.print_exception('Error in on_player_join for', self)
def remove_team(self, sessionteam: ba.SessionTeam) -> None:
"""(internal)"""
# This should only be called on unexpired activities the team has
# been added to.
assert not self.expired
assert sessionteam.gameteam is not None
assert sessionteam.gameteam in self.teams
team = sessionteam.gameteam
assert isinstance(team, self._teamtype)
self.teams.remove(team)
with _ba.Context(self):
# And finally tell the game to start.
self._has_begun = True
self.on_begin()
# Make a decent attempt to persevere if user code breaks.
try:
self.on_team_leave(team)
except Exception:
print_exception(f'Error in on_team_leave for {self}')
try:
sessionteam.reset_gamedata()
except Exception:
print_exception(f'Error in reset_gamedata for {self}')
sessionteam.gameteam = None
# Make sure that ba.Activity.on_transition_in() got called
# at some point.
def _sanity_check_begin_call(self) -> None:
# Make sure ba.Activity.on_transition_in() got called at some point.
if not self._called_activity_on_transition_in:
_error.print_error(
print_error(
'ba.Activity.on_transition_in() never got called for ' +
str(self) + '; did you forget to call it'
' in your on_transition_in override?')
# Make sure that ba.Activity.on_begin() got called at some point.
if not self._called_activity_on_begin:
_error.print_error(
print_error(
'ba.Activity.on_begin() never got called for ' + str(self) +
'; did you forget to call it in your on_begin override?')
# If the whole session wants to die and was waiting on us, can get
# that going now.
def begin(self, session: ba.Session) -> None:
"""Begin the activity.
(internal)
"""
# Is this ever still happening?...
if self._has_begun:
print_error("_begin called twice; this shouldn't happen")
return
# Inherit stats from the session.
self._stats = session.stats
# Add session's teams in.
for team in session.teams:
self.add_team(team)
# Add session's players in.
for player in session.players:
self.add_player(player)
# And finally tell the game to start.
with _ba.Context(self):
self._has_begun = True
self.on_begin()
self._sanity_check_begin_call()
# If the whole session wants to die and was waiting on us,
# can kick off that process now.
if session.wants_to_end:
session.launch_end_session_activity()
else:
@ -663,3 +778,30 @@ class Activity(DependencyComponent):
if self._should_end_immediately:
self.end(self._should_end_immediately_results,
self._should_end_immediately_delay)
# noinspection PyUnresolvedReferences
def _setup_player_and_team_types(self) -> None:
"""Pull player and team types from our typing.Generic params."""
# TODO: There are proper calls for pulling these in Python 3.8;
# should update this code when we adopt that.
# NOTE: If we get Any as PlayerType or TeamType (generally due
# to no generic params being passed) we automatically use the
# base class types, but also warn the user since this will mean
# less type safety for that class. (its better to pass the base
# types explicitly vs. having them be Any)
if not TYPE_CHECKING:
self._playertype = type(self).__orig_bases__[-1].__args__[0]
if not isinstance(self._playertype, type):
self._playertype = Player
print(f'ERROR: {type(self)} was not passed a Player'
f' type argument; please explicitly pass ba.Player'
f' if you do not want to override it.')
self._teamtype = type(self).__orig_bases__[-1].__args__[1]
if not isinstance(self._teamtype, type):
self._teamtype = Team
print(f'ERROR: {type(self)} was not passed a Team'
f' type argument; please explicitly pass ba.Team'
f' if you do not want to override it.')
assert issubclass(self._playertype, Player)
assert issubclass(self._teamtype, Team)

View File

@ -26,6 +26,9 @@ from typing import TYPE_CHECKING
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
if TYPE_CHECKING:
from typing import Any, Dict, Optional
@ -33,7 +36,7 @@ if TYPE_CHECKING:
from ba._lobby import JoinInfo
class EndSessionActivity(Activity):
class EndSessionActivity(Activity[Player, Team]):
"""Special ba.Activity to fade out and end the current ba.Session."""
def __init__(self, settings: Dict[str, Any]):
@ -61,7 +64,7 @@ class EndSessionActivity(Activity):
call_after_ad(Call(_ba.new_host_session, MainMenuSession))
class JoinActivity(Activity):
class JoinActivity(Activity[Player, Team]):
"""Standard activity for waiting for players to join.
It shows tips and other info and waits for all players to check ready.
@ -98,7 +101,7 @@ class JoinActivity(Activity):
_ba.set_analytics_screen('Joining Screen')
class TransitionActivity(Activity):
class TransitionActivity(Activity[Player, Team]):
"""A simple overlay fade out/in.
Useful as a bare minimum transition between two level based activities.
@ -131,7 +134,7 @@ class TransitionActivity(Activity):
_ba.timer(0.1, self.end)
class ScoreScreenActivity(Activity):
class ScoreScreenActivity(Activity[Player, Team]):
"""A standard score screen that fades in and shows stuff for a while.
After a specified delay, player input is assigned to end the activity.

View File

@ -25,8 +25,8 @@ from __future__ import annotations
import weakref
from typing import TYPE_CHECKING, TypeVar
from ba._messages import DieMessage, DeathType, OutOfBoundsMessage
from ba import _error
from ba._messages import DieMessage, DeathType, OutOfBoundsMessage, UNHANDLED
from ba._error import print_error, print_exception, ActivityNotFoundError
import _ba
if TYPE_CHECKING:
@ -97,10 +97,10 @@ class Actor:
# Non-expired Actors send themselves a DieMessage when going down.
# That way we can treat DieMessage handling as the single
# point-of-action for death.
if not self.is_expired():
if not self.expired:
self.handlemessage(DieMessage())
except Exception:
_error.print_exception('exception in ba.Actor.__del__() for', self)
print_exception('exception in ba.Actor.__del__() for', self)
def handlemessage(self, msg: Any) -> Any:
"""General message handling; can be passed any message object."""
@ -111,7 +111,7 @@ class Actor:
if isinstance(msg, OutOfBoundsMessage):
return self.handlemessage(DieMessage(how=DeathType.OUT_OF_BOUNDS))
return _error.UNHANDLED
return UNHANDLED
def autoretain(self: T) -> T:
"""Keep this Actor alive without needing to hold a reference to it.
@ -126,7 +126,7 @@ class Actor:
"""
activity = self._activity()
if activity is None:
raise _error.ActivityNotFoundError()
raise ActivityNotFoundError()
activity.retain_actor(self)
return self
@ -144,13 +144,14 @@ class Actor:
likely result in errors.
"""
def is_expired(self) -> bool:
"""Returns whether the Actor is expired.
@property
def expired(self) -> bool:
"""Whether the Actor is expired.
(see ba.Actor.on_expire())
"""
activity = self.getactivity(doraise=False)
return True if activity is None else activity.is_expired()
return True if activity is None else activity.expired
def exists(self) -> bool:
"""Returns whether the Actor is still present in a meaningful way.
@ -195,13 +196,12 @@ class Actor:
avoided.
"""
if not __debug__:
_error.print_error('This should only be called in __debug__ mode.',
once=True)
print_error('This should only be called in __debug__ mode.',
once=True)
if not getattr(self, '_root_actor_init_called', False):
_error.print_error('Root Actor __init__() not called.')
if self.is_expired():
_error.print_error(
f'handlemessage() called on expired actor: {self}')
print_error('Root Actor __init__() not called.')
if self.expired:
print_error(f'handlemessage() called on expired actor: {self}')
@property
def activity(self) -> ba.Activity:
@ -211,7 +211,7 @@ class Actor:
"""
activity = self._activity()
if activity is None:
raise _error.ActivityNotFoundError()
raise ActivityNotFoundError()
return activity
def getactivity(self, doraise: bool = True) -> Optional[ba.Activity]:
@ -222,5 +222,5 @@ class Actor:
"""
activity = self._activity()
if activity is None and doraise:
raise _error.ActivityNotFoundError()
raise ActivityNotFoundError()
return activity

View File

@ -62,7 +62,7 @@ def run_cpu_benchmark() -> None:
cfg['Graphics Quality'] = self._old_quality
cfg.apply()
def on_player_request(self, player: ba.Player) -> bool:
def on_player_request(self, player: ba.SessionPlayer) -> bool:
return False
_ba.new_host_session(BenchmarkSession, benchmark_type='cpu')

View File

@ -21,7 +21,7 @@
"""Functionality related to co-op games."""
from __future__ import annotations
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, TypeVar
import _ba
from ba._gameactivity import GameActivity
@ -31,8 +31,11 @@ if TYPE_CHECKING:
from bastd.actor.playerspaz import PlayerSpaz
import ba
PlayerType = TypeVar('PlayerType', bound='ba.Player')
TeamType = TypeVar('TeamType', bound='ba.Team')
class CoopGameActivity(GameActivity):
class CoopGameActivity(GameActivity[PlayerType, TeamType]):
"""Base class for cooperative-mode games.
Category: Gameplay Classes
@ -187,7 +190,7 @@ class CoopGameActivity(GameActivity):
vval -= 55
def spawn_player_spaz(self,
player: ba.Player,
player: PlayerType,
position: Sequence[float] = (0.0, 0.0, 0.0),
angle: float = None) -> PlayerSpaz:
"""Spawn and wire up a standard player spaz."""

View File

@ -173,9 +173,9 @@ class CoopSession(Session):
def get_custom_menu_entries(self) -> List[Dict[str, Any]]:
return self._custom_menu_ui
def on_player_leave(self, player: ba.Player) -> None:
def on_player_leave(self, sessionplayer: ba.SessionPlayer) -> None:
from ba._general import WeakCall
super().on_player_leave(player)
super().on_player_leave(sessionplayer)
# If all our players leave we wanna quit out of the session.
_ba.timer(2.0, WeakCall(self._end_session_if_empty))
@ -213,7 +213,7 @@ class CoopSession(Session):
from bastd.ui.tournamententry import TournamentEntryWindow
from ba._gameactivity import GameActivity
activity = self.getactivity()
if activity is not None and not activity.is_expired():
if activity is not None and not activity.expired:
assert self.tournament_id is not None
assert isinstance(activity, GameActivity)
TournamentEntryWindow(tournament_id=self.tournament_id,
@ -235,7 +235,7 @@ class CoopSession(Session):
# This method may get called from the UI context so make sure we
# explicitly run in the activity's context.
activity = self.getactivity()
if activity is not None and not activity.is_expired():
if activity is not None and not activity.expired:
activity.can_show_ad_on_death = True
with _ba.Context(activity):
activity.end(results={'outcome': 'restart'}, force=True)

View File

@ -31,16 +31,6 @@ if TYPE_CHECKING:
import ba
class _UnhandledType:
pass
# A special value that should be returned from handlemessage()
# functions for unhandled message types. This may result
# in fallback message types being attempted/etc.
UNHANDLED = _UnhandledType()
class DependencyError(Exception):
"""Exception raised when one or more ba.Dependency items are missing.
@ -73,6 +63,13 @@ class PlayerNotFoundError(NotFoundError):
"""
class SessionPlayerNotFoundError(NotFoundError):
"""Exception raised when an expected ba.SessionPlayer does not exist.
category: Exception Classes
"""
class TeamNotFoundError(NotFoundError):
"""Exception raised when an expected ba.Team does not exist.
@ -80,6 +77,13 @@ class TeamNotFoundError(NotFoundError):
"""
class SessionTeamNotFoundError(NotFoundError):
"""Exception raised when an expected ba.SessionTeam does not exist.
category: Exception Classes
"""
class NodeNotFoundError(NotFoundError):
"""Exception raised when an expected ba.Node does not exist.

View File

@ -24,12 +24,12 @@
from __future__ import annotations
import random
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, TypeVar
import _ba
from ba._activity import Activity
from ba._score import ScoreInfo
from ba._lang import Lstr
import _ba
if TYPE_CHECKING:
from typing import (List, Optional, Dict, Type, Any, Callable, Sequence,
@ -38,8 +38,11 @@ if TYPE_CHECKING:
from bastd.actor.bomb import TNTSpawner
import ba
PlayerType = TypeVar('PlayerType', bound='ba.Player')
TeamType = TypeVar('TeamType', bound='ba.Team')
class GameActivity(Activity):
class GameActivity(Activity[PlayerType, TeamType]):
"""Common base class for all game ba.Activities.
category: Gameplay Classes
@ -606,13 +609,13 @@ class GameActivity(Activity):
self._setup_tournament_time_limit(
max(5, data_t[0]['timeRemaining']))
def on_player_join(self, player: ba.Player) -> None:
def on_player_join(self, player: PlayerType) -> None:
super().on_player_join(player)
# By default, just spawn a dude.
self.spawn_player(player)
def on_player_leave(self, player: ba.Player) -> None:
def on_player_leave(self, player: PlayerType) -> None:
from ba._general import Call
from ba._messages import DieMessage, DeathType
@ -632,7 +635,7 @@ class GameActivity(Activity):
from bastd.actor.playerspaz import PlayerSpazDeathMessage
if isinstance(msg, PlayerSpazDeathMessage):
player = msg.spaz.player
player = msg.getspaz(self).player
killer = msg.killerplayer
# Inform our score-set of the demise.
@ -642,7 +645,7 @@ class GameActivity(Activity):
# Award the killer points if he's on a different team.
if killer and killer.team is not player.team:
pts, importance = msg.spaz.get_death_points(msg.how)
pts, importance = msg.getspaz(self).get_death_points(msg.how)
if not self.has_ended():
self.stats.player_scored(killer,
pts,
@ -928,7 +931,7 @@ class GameActivity(Activity):
print('WARNING: default end_game() implementation called;'
' your game should override this.')
def spawn_player_if_exists(self, player: ba.Player) -> None:
def spawn_player_if_exists(self, player: PlayerType) -> None:
"""
A utility method which calls self.spawn_player() *only* if the
ba.Player provided still exists; handy for use in timers and whatnot.
@ -938,7 +941,7 @@ class GameActivity(Activity):
if player:
self.spawn_player(player)
def spawn_player(self, player: ba.Player) -> ba.Actor:
def spawn_player(self, player: PlayerType) -> ba.Actor:
"""Spawn *something* for the provided ba.Player.
The default implementation simply calls spawn_player_spaz().
@ -949,7 +952,7 @@ class GameActivity(Activity):
return self.spawn_player_spaz(player)
def respawn_player(self,
player: ba.Player,
player: PlayerType,
respawn_time: Optional[float] = None) -> None:
"""
Given a ba.Player, sets up a standard respawn timer,
@ -989,7 +992,7 @@ class GameActivity(Activity):
player.gamedata['respawn_icon'] = RespawnIcon(player, respawn_time)
def spawn_player_spaz(self,
player: ba.Player,
player: PlayerType,
position: Sequence[float] = (0, 0, 0),
angle: float = None) -> PlayerSpaz:
"""Create and wire up a ba.PlayerSpaz for the provided ba.Player."""

View File

@ -26,9 +26,11 @@ 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
from typing import Sequence, Tuple, Any, Optional, Dict, List, Union
import ba
@ -36,7 +38,7 @@ if TYPE_CHECKING:
class WinnerGroup:
"""Entry for a winning team or teams calculated by game-results."""
score: Optional[int]
teams: Sequence[ba.Team]
teams: Sequence[ba.SessionTeam]
class TeamGameResults:
@ -52,9 +54,9 @@ class TeamGameResults:
def __init__(self) -> None:
"""Instantiate a results instance."""
self._game_set = False
self._scores: Dict[int, Tuple[ReferenceType[ba.Team],
self._scores: Dict[int, Tuple[ReferenceType[ba.SessionTeam],
Optional[int]]] = {}
self._teams: Optional[List[ReferenceType[ba.Team]]] = None
self._teams: Optional[List[ReferenceType[ba.SessionTeam]]] = None
self._player_info: Optional[List[Dict[str, Any]]] = None
self._lower_is_better: Optional[bool] = None
self._score_label: Optional[str] = None
@ -74,16 +76,22 @@ class TeamGameResults:
self._none_is_winner = score_info.none_is_winner
self._score_type = score_info.scoretype
def set_team_score(self, team: ba.Team, score: int) -> None:
def set_team_score(self, team: Union[ba.SessionTeam, ba.Team],
score: 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)
"""
self._scores[team.get_id()] = (weakref.ref(team), score)
if isinstance(team, Team):
team = team.sessionteam
self._scores[team.id] = (weakref.ref(team), score)
def get_team_score(self, team: ba.Team) -> Optional[int]:
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
for score in list(self._scores.values()):
if score[0]() is team:
return score[1]
@ -91,8 +99,8 @@ class TeamGameResults:
# If we have no score value, assume None.
return None
def get_teams(self) -> List[ba.Team]:
"""Return all ba.Teams in the results."""
def get_teams(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 = []
@ -103,12 +111,9 @@ class TeamGameResults:
teams.append(team)
return teams
def has_score_for_team(self, team: ba.Team) -> bool:
def has_score_for_team(self, sessionteam: ba.SessionTeam) -> bool:
"""Return whether there is a score for a given team."""
for score in list(self._scores.values()):
if score[0]() is team:
return True
return False
return any(s[0]() is sessionteam for s in self._scores.values())
def get_team_score_str(self, team: ba.Team) -> ba.Lstr:
"""Return the score for the given ba.Team as an Lstr.
@ -122,7 +127,7 @@ class TeamGameResults:
if not self._game_set:
raise RuntimeError("Can't get team-score-str until game is set.")
for score in list(self._scores.values()):
if score[0]() is team:
if score[0]() is team.sessionteam:
if score[1] is None:
return Lstr(value='-')
if self._score_type is ScoreType.SECONDS:
@ -164,7 +169,7 @@ class TeamGameResults:
assert self._lower_is_better is not None
return self._lower_is_better
def get_winning_team(self) -> Optional[ba.Team]:
def get_winning_team(self) -> Optional[ba.SessionTeam]:
"""Get the winning ba.Team if there is exactly one; None otherwise."""
if not self._game_set:
raise RuntimeError("Can't get winners until game is set.")
@ -179,7 +184,7 @@ class TeamGameResults:
raise RuntimeError("Can't get winners until game is set.")
# Group by best scoring teams.
winners: Dict[int, List[ba.Team]] = {}
winners: Dict[int, List[ba.SessionTeam]] = {}
scores = [
score for score in self._scores.values()
if score[0]() is not None and score[1] is not None
@ -191,11 +196,11 @@ class TeamGameResults:
assert team is not None
sval.append(team)
results: List[Tuple[Optional[int],
List[ba.Team]]] = list(winners.items())
List[ba.SessionTeam]]] = list(winners.items())
results.sort(reverse=not self._lower_is_better, key=lambda x: x[0])
# Also group the 'None' scores.
none_teams: List[ba.Team] = []
none_teams: List[ba.SessionTeam] = []
for score in self._scores.values():
scoreteam = score[0]()
if scoreteam is not None and score[1] is None:
@ -205,7 +210,7 @@ class TeamGameResults:
# depending on the rules).
if none_teams:
nones: List[Tuple[Optional[int],
List[ba.Team]]] = [(None, none_teams)]
List[ba.SessionTeam]]] = [(None, none_teams)]
if self._none_is_winner:
results = nones + results
else:

View File

@ -29,7 +29,7 @@ import _ba
if TYPE_CHECKING:
from typing import Any, Type
from efro.call import Call
from efro.call import Call as Call # 'as Call' so we re-export.
T = TypeVar('T')

View File

@ -36,7 +36,7 @@ from typing import TYPE_CHECKING
import _ba
if TYPE_CHECKING:
from typing import List, Sequence, Optional
from typing import List, Sequence, Optional, Dict, Any
import ba
@ -346,3 +346,13 @@ def local_chat_message(msg: str) -> None:
def handle_remote_achievement_list(completed_achievements: List[str]) -> None:
from ba import _achievement
_achievement.set_completed_achievements(completed_achievements)
def get_player_icon(sessionplayer: ba.SessionPlayer) -> Dict[str, Any]:
info = sessionplayer.get_icon_info()
return {
'texture': _ba.gettexture(info['texture']),
'tint_texture': _ba.gettexture(info['tint_texture']),
'tint_color': info['tint_color'],
'tint2_color': info['tint2_color']
}

View File

@ -36,6 +36,7 @@ if TYPE_CHECKING:
MAX_QUICK_CHANGE_COUNT = 30
QUICK_CHANGE_INTERVAL = 0.05
QUICK_CHANGE_RESET_INTERVAL = 1.0
# Hmm should we move this to actors?..
@ -147,7 +148,7 @@ class Chooser:
if self._text_node:
self._text_node.delete()
def __init__(self, vpos: float, player: _ba.Player,
def __init__(self, vpos: float, player: _ba.SessionPlayer,
lobby: 'Lobby') -> None:
# FIXME: Tidy up around here.
# pylint: disable=too-many-branches
@ -326,7 +327,7 @@ class Chooser:
self._inited = True
@property
def player(self) -> ba.Player:
def player(self) -> ba.SessionPlayer:
"""The ba.Player associated with this chooser."""
return self._player
@ -343,7 +344,7 @@ class Chooser:
"""(internal)"""
self._dead = val
def get_team(self) -> ba.Team:
def get_team(self) -> ba.SessionTeam:
"""Return this chooser's selected ba.Team."""
return self.lobby.teams[self._selected_team_index]
@ -641,18 +642,17 @@ class Chooser:
# choosers that have been marked as ready.
team_player_counts = {}
for team in teams:
team_player_counts[team.get_id()] = (len(team.players))
team_player_counts[team.id] = (len(team.players))
for chooser in lobby.choosers:
if chooser.ready:
team_player_counts[
chooser.get_team().get_id()] += 1
team_player_counts[chooser.get_team().id] += 1
largest_team_size = max(team_player_counts.values())
smallest_team_size = (min(team_player_counts.values()))
# Force switch if we're on the biggest team
# and there's a smaller one available.
if (largest_team_size != smallest_team_size
and team_player_counts[self.get_team().get_id()] >=
and team_player_counts[self.get_team().id] >=
largest_team_size):
force_team_switch = True
@ -664,17 +664,24 @@ class Chooser:
_ba.playsound(self._punchsound)
self._set_ready(ready)
def handlemessage(self, msg: Any) -> Any:
"""Standard generic message handler."""
if isinstance(msg, ChangeMessage):
now = _ba.time()
count = self.last_change[1] + 1
if (now - self.last_change[0] < QUICK_CHANGE_INTERVAL
and count > MAX_QUICK_CHANGE_COUNT):
# Hmm maybe we should notify client?
# TODO: should handle this at the engine layer so this is unnecessary.
def _handle_repeat_message_attack(self) -> None:
now = _ba.time()
count = self.last_change[1]
if now - self.last_change[0] < QUICK_CHANGE_INTERVAL:
count += 1
if count > MAX_QUICK_CHANGE_COUNT:
_ba.disconnect_client(
self._player.get_input_device().client_id)
self.last_change = (now, count)
elif now - self.last_change[0] > QUICK_CHANGE_RESET_INTERVAL:
count = 0
self.last_change = (now, count)
def handlemessage(self, msg: Any) -> Any:
"""Standard generic message handler."""
if isinstance(msg, ChangeMessage):
self._handle_repeat_message_attack()
# If we've been removed from the lobby, ignore this stuff.
if self._dead:
@ -806,7 +813,7 @@ class Chooser:
highlight[(max_index + 2) % 3] += diff * 0.2
return highlight
def getplayer(self) -> ba.Player:
def getplayer(self) -> ba.SessionPlayer:
"""Return the player associated with this chooser."""
return self._player
@ -890,7 +897,7 @@ class Lobby:
if teams is not None:
self._teams = [weakref.ref(team) for team in teams]
else:
self._dummy_teams = bs_team.Team()
self._dummy_teams = bs_team.SessionTeam()
self._teams = [weakref.ref(self._dummy_teams)]
v_offset = (-150
if isinstance(session, _coopsession.CoopSession) else -50)
@ -920,7 +927,7 @@ class Lobby:
return self._use_team_colors
@property
def teams(self) -> List[ba.Team]:
def teams(self) -> List[ba.SessionTeam]:
"""Teams available in this lobby."""
allteams = []
for tref in self._teams:
@ -974,14 +981,14 @@ class Lobby:
"""Return whether all choosers are marked ready."""
return all(chooser.ready for chooser in self.choosers)
def add_chooser(self, player: ba.Player) -> None:
def add_chooser(self, player: ba.SessionPlayer) -> None:
"""Add a chooser to the lobby for the provided player."""
self.choosers.append(
Chooser(vpos=self._vpos, player=player, lobby=self))
self._next_add_team = (self._next_add_team + 1) % len(self._teams)
self._vpos -= 48
def remove_chooser(self, player: ba.Player) -> None:
def remove_chooser(self, player: ba.SessionPlayer) -> None:
"""Remove a single player's chooser; does not kick him.
This is used when a player enters the game and no longer

View File

@ -33,6 +33,16 @@ if TYPE_CHECKING:
import ba
class _UnhandledType:
pass
# A special value that should be returned from handlemessage()
# functions for unhandled message types. This may result
# in fallback message types being attempted/etc.
UNHANDLED = _UnhandledType()
@dataclass
class OutOfBoundsMessage:
"""A message telling an object that it is out of bounds.

View File

@ -27,6 +27,7 @@ from typing import TYPE_CHECKING
import _ba
from ba._session import Session
from ba._error import NotFoundError, print_error
if TYPE_CHECKING:
from typing import Optional, Any, Dict, List, Type, Sequence
@ -155,7 +156,7 @@ class MultiTeamSession(Session):
"""Returns which game in the series is currently being played."""
return self._game_number
def on_team_join(self, team: ba.Team) -> None:
def on_team_join(self, team: ba.SessionTeam) -> None:
team.sessiondata['previous_score'] = team.sessiondata['score'] = 0
def get_max_players(self) -> int:
@ -171,7 +172,6 @@ class MultiTeamSession(Session):
def on_activity_end(self, activity: ba.Activity, results: Any) -> None:
# pylint: disable=cyclic-import
from ba import _error
from bastd.tutorial import TutorialActivity
from bastd.activity.multiteamvictory import (
TeamSeriesVictoryScoreScreenActivity)
@ -223,7 +223,7 @@ class MultiTeamSession(Session):
# (ie: no longer sitting in the lobby).
try:
has_team = (player.team is not None)
except _error.TeamNotFoundError:
except NotFoundError:
has_team = False
if has_team:
self.stats.register_player(player)
@ -238,9 +238,8 @@ class MultiTeamSession(Session):
def _switch_to_score_screen(self, results: Any) -> None:
"""Switch to a score screen after leaving a round."""
from ba import _error
del results # Unused arg.
_error.print_error('this should be overridden')
print_error('this should be overridden')
def announce_game_results(self,
activity: ba.GameActivity,
@ -269,7 +268,8 @@ class MultiTeamSession(Session):
if winning_team is not None:
# Have all players celebrate.
celebrate_msg = CelebrateMessage(duration=10.0)
for player in winning_team.players:
assert winning_team.gameteam is not None
for player in winning_team.gameteam.players:
if player.actor:
player.actor.handlemessage(celebrate_msg)
cameraflash()

View File

@ -94,7 +94,7 @@ class ServerCallThread(threading.Thread):
# this check manually?
if self._activity is not None:
activity = self._activity()
if activity is None or activity.is_expired():
if activity is None or activity.expired:
return
# Technically we could do the same check for session contexts,

View File

@ -20,35 +20,178 @@
# -----------------------------------------------------------------------------
"""Player related functionality."""
from __future__ import annotations
from typing import TYPE_CHECKING, TypeVar
from typing import TYPE_CHECKING, TypeVar, Generic
if TYPE_CHECKING:
from typing import Type
from typing import (Type, Optional, Sequence, Dict, Any, Union, Tuple,
Callable)
import ba
T = TypeVar('T')
TeamType = TypeVar('TeamType', bound='ba.Team')
class BasePlayerData:
"""Base class for custom player data.
class Player(Generic[TeamType]):
"""Testing."""
Category: Gameplay Classes
# Defining these types at the class level instead of in __init__ so
# that types are introspectable (these are still instance attrs).
team: TeamType
character: str
actor: Optional[ba.Actor]
color: Sequence[float]
highlight: Sequence[float]
_sessionplayer: ba.SessionPlayer
_nodeactor: Optional[ba.NodeActor]
A convenience class that can be used as a base class for custom
per-game player data. It simply provides the ability to easily fetch
an instance of itself for a given ba.Player.
"""
# Should aim to kill this eventually (at least gamedata).
# Game-specific data can be tacked on to the per-game player class.
sessiondata: Dict
gamedata: Dict
@classmethod
def get(cls: Type[T], player: ba.Player) -> T:
"""Return the custom player data associated with a player.
# NOTE: avoiding having any __init__() here since it seems to not
# get called by default if a dataclass inherits from us.
If one does not exist, it will be created.
def postinit(self, sessionplayer: ba.SessionPlayer) -> None:
"""Wire up a newly created player.
(internal)
"""
from ba._nodeactor import NodeActor
import _ba
self.actor = None
self.character = ''
self._nodeactor: Optional[ba.NodeActor] = None
self._sessionplayer = sessionplayer
self.character = sessionplayer.character
self.color = sessionplayer.color
self.highlight = sessionplayer.highlight
self.team = sessionplayer.team.gameteam # type: ignore
assert self.team is not None
self.sessiondata = sessionplayer.sessiondata
self.gamedata = sessionplayer.gamedata
# Store/return an instance of ourself in the player's per-game dict.
data = player.gamedata.get('playerdata')
if data is None:
player.gamedata['playerdata'] = data = cls()
assert isinstance(data, cls)
return data
# Create our player node in the current activity.
node = _ba.newnode('player', attrs={'playerID': sessionplayer.id})
self._nodeactor = NodeActor(node)
sessionplayer.set_node(node)
@property
def sessionplayer(self) -> ba.SessionPlayer:
"""Return the ba.SessionPlayer corresponding to this Player.
Throws a ba.SessionPlayerNotFoundError if it does not exist.
"""
if bool(self._sessionplayer):
return self._sessionplayer
from ba import _error
raise _error.SessionPlayerNotFoundError()
@property
def node(self) -> ba.Node:
"""A ba.Node of type 'player' associated with this Player.
This node can be used to get a generic player position/etc.
"""
if not self._nodeactor:
from ba import _error
raise _error.NodeNotFoundError
return self._nodeactor.node
@property
def exists(self) -> bool:
"""Whether the player still exists.
Most functionality will fail on a nonexistent player.
Note that you can also use the boolean operator for this same
functionality, so a statement such as "if player" will do
the right thing both for Player objects and values of None.
"""
return bool(self._sessionplayer)
def get_name(self, full: bool = False, icon: bool = True) -> str:
"""get_name(full: bool = False, icon: bool = True) -> str
Returns the player's name. If icon is True, the long version of the
name may include an icon.
"""
return self._sessionplayer.get_name(full=full, icon=icon)
def set_actor(self, actor: Optional[ba.Actor]) -> None:
"""set_actor(actor: Optional[ba.Actor]) -> None
Set the player's associated ba.Actor.
"""
self.actor = actor
def is_alive(self) -> bool:
"""is_alive() -> bool
Returns True if the player has a ba.Actor assigned and its
is_alive() method return True. False is returned otherwise.
"""
return self.actor is not None and self.actor.is_alive()
def get_icon(self) -> Dict[str, Any]:
"""get_icon() -> Dict[str, Any]
Returns the character's icon (images, colors, etc contained in a dict)
"""
return self._sessionplayer.get_icon()
def assign_input_call(self, inputtype: Union[str, Tuple[str, ...]],
call: Callable) -> None:
"""assign_input_call(type: Union[str, Tuple[str, ...]],
call: Callable) -> None
Set the python callable to be run for one or more types of input.
Valid type values are: 'jumpPress', 'jumpRelease', 'punchPress',
'punchRelease','bombPress', 'bombRelease', 'pickUpPress',
'pickUpRelease', 'upDown','leftRight','upPress', 'upRelease',
'downPress', 'downRelease', 'leftPress','leftRelease','rightPress',
'rightRelease', 'run', 'flyPress', 'flyRelease', 'startPress',
'startRelease'
"""
return self._sessionplayer.assign_input_call(type=inputtype, call=call)
def reset_input(self) -> None:
"""reset_input() -> None
Clears out the player's assigned input actions.
"""
self._sessionplayer.reset_input()
def __bool__(self) -> bool:
return bool(self._sessionplayer)
PlayerType = TypeVar('PlayerType', bound='ba.Player')
def playercast(totype: Type[PlayerType], player: ba.Player) -> PlayerType:
"""Cast a ba.Player to a specific ba.Player subclass.
Category: Gameplay Functions
When writing type-checked code, sometimes code will deal with raw
ba.Player objects which need to be cast back to the game's actual
player type so that access can be properly type-checked. This function
is a safe way to do so. It ensures that Optional values are not cast
into Non-Optional, etc.
"""
assert isinstance(player, totype)
return player
# NOTE: ideally we should have a single playercast() call and use overloads
# for the optional variety, but that currently seems to not be working.
# See: https://github.com/python/mypy/issues/8800
def playercast_o(totype: Type[PlayerType],
player: Optional[ba.Player]) -> Optional[PlayerType]:
"""A variant of ba.playercast() for use with optional ba.Player values.
Category: Gameplay Functions
"""
# noinspection PyTypeHints
assert isinstance(player, (totype, type(None)))
return player

View File

@ -25,11 +25,12 @@ import weakref
from typing import TYPE_CHECKING
import _ba
from ba._error import print_error, print_exception
from ba._lang import Lstr
from ba._player import Player
if TYPE_CHECKING:
from weakref import ReferenceType
from typing import Sequence, List, Dict, Any, Optional, Set
import ba
@ -81,8 +82,8 @@ class Session:
lobby: ba.Lobby
max_players: int
min_players: int
players: List[ba.Player]
teams: List[ba.Team]
players: List[ba.SessionPlayer]
teams: List[ba.SessionTeam]
def __init__(self,
depsets: Sequence[ba.DependencySet],
@ -104,9 +105,11 @@ class Session:
from ba._stats import Stats
from ba._gameutils import sharedobj
from ba._gameactivity import GameActivity
from ba._team import Team
from ba._activity import Activity
from ba._team import SessionTeam
from ba._error import DependencyError
from ba._dependency import Dependency, AssetPackage
from efro.util import empty_weakref
# First off, resolve all dependency-sets we were passed.
# If things are missing, we'll try to gather them into a single
@ -143,21 +146,12 @@ class Session:
# print('Would set host-session asset-reqs to:',
# required_asset_packages)
# First thing, wire up our internal engine data.
# Stuff in this section should be removed from this class if possible.
self._sessiondata = _ba.register_session(self)
self.tournament_id: Optional[str] = None
# FIXME: This stuff shouldn't be here.
self.sharedobjs: Dict[str, Any] = {}
# TeamGameActivity uses this to display a help overlay on the first
# activity only.
self.have_shown_controls_help_overlay = False
self.campaign = None
# FIXME: Should be able to kill this I think.
self.campaign_state: Dict[str, str] = {}
self._use_teams = (team_names is not None)
@ -171,13 +165,7 @@ class Session:
self._activity_retained: Optional[ba.Activity] = None
self.launch_end_session_activity_time: Optional[float] = None
self._activity_end_timer: Optional[ba.Timer] = None
# Hacky way to create empty weak ref; must be a better way.
class _EmptyObj:
pass
self._activity_weak: ReferenceType[ba.Activity]
self._activity_weak = weakref.ref(_EmptyObj()) # type: ignore
self._activity_weak = empty_weakref(Activity)
if self._activity_weak() is not None:
raise Exception('Error creating empty activity weak ref.')
@ -192,10 +180,10 @@ class Session:
assert team_names is not None
assert team_colors is not None
for i, color in enumerate(team_colors):
team = Team(team_id=self._next_team_id,
name=GameActivity.get_team_display_string(
team_names[i]),
color=color)
team = SessionTeam(team_id=self._next_team_id,
name=GameActivity.get_team_display_string(
team_names[i]),
color=color)
self.teams.append(team)
self._next_team_id += 1
@ -203,15 +191,12 @@ class Session:
with _ba.Context(self):
self.on_team_join(team)
except Exception:
from ba import _error
_error.print_exception(
f'Error in on_team_join for {self}.')
print_exception(f'Error in on_team_join for {self}.')
self.lobby = Lobby()
self.stats = Stats()
# Instantiate our session globals node
# (so it can apply default settings).
# Instantiate our session globals node which will apply its settings.
sharedobj('globals')
@property
@ -224,12 +209,11 @@ class Session:
"""(internal)"""
return self._use_team_colors
def on_player_request(self, player: ba.Player) -> bool:
def on_player_request(self, player: ba.SessionPlayer) -> bool:
"""Called when a new ba.Player wants to join the Session.
This should return True or False to accept/reject.
"""
from ba._lang import Lstr
# Limit player counts *unless* we're in a stress test.
if _ba.app.stress_test_reset_timer is None:
@ -250,144 +234,88 @@ class Session:
_ba.playsound(_ba.getsound('dripity'))
return True
def on_player_leave(self, player: ba.Player) -> None:
"""Called when a previously-accepted ba.Player leaves the session."""
# pylint: disable=too-many-statements
def on_player_leave(self, sessionplayer: ba.SessionPlayer) -> None:
"""Called when a previously-accepted ba.SessionPlayer leaves."""
# pylint: disable=too-many-branches
# pylint: disable=cyclic-import
from ba._freeforallsession import FreeForAllSession
from ba._lang import Lstr
from ba import _error
# Remove them from the game rosters.
if player in self.players:
_ba.playsound(_ba.getsound('playerLeft'))
team: Optional[ba.Team]
# The player will have no team if they are still in the lobby.
try:
team = player.team
except _error.TeamNotFoundError:
team = None
activity = self._activity_weak()
# If he had no team, he's in the lobby.
# If we have a current activity with a lobby, ask them to
# remove him.
if team is None:
with _ba.Context(self):
try:
self.lobby.remove_chooser(player)
except Exception:
_error.print_exception(
'Error in Lobby.remove_chooser()')
# *If* they were actually in the game, announce their departure.
if team is not None:
_ba.screenmessage(
Lstr(resource='playerLeftText',
subs=[('${PLAYER}', player.get_name(full=True))]))
# Remove him from his team and session lists.
# (he may not be on the team list since player are re-added to
# team lists every activity)
if team is not None and player in team.players:
# Testing; can remove this eventually.
if isinstance(self, FreeForAllSession):
if len(team.players) != 1:
_error.print_error('expected 1 player in FFA team')
team.players.remove(player)
# Remove player from any current activity.
if activity is not None and player in activity.players:
activity.players.remove(player)
# Run the activity callback unless its been expired.
if not activity.is_expired():
try:
with _ba.Context(activity):
activity.on_player_leave(player)
except Exception:
_error.print_exception(
'exception in on_player_leave for activity',
activity)
else:
_error.print_error('expired activity in on_player_leave;'
" shouldn't happen")
player.set_activity(None)
player.set_node(None)
# Reset the player; this will remove its actor-ref and clear
# its calls/etc
try:
with _ba.Context(activity):
player.reset()
except Exception:
_error.print_exception(
'exception in player.reset in'
' on_player_leave for player', player)
# If we're a non-team session, remove the player's team completely.
if not self._use_teams and team is not None:
# If the team's in an activity, call its on_team_leave
# callback.
if activity is not None and team in activity.teams:
activity.teams.remove(team)
if not activity.is_expired():
try:
with _ba.Context(activity):
activity.on_team_leave(team)
except Exception:
_error.print_exception(
'exception in on_team_leave for activity',
activity)
else:
_error.print_error(
'expired activity in on_player_leave p2'
"; shouldn't happen")
# Clear the team's game-data (so dying stuff will
# have proper context).
try:
with _ba.Context(activity):
team.reset_gamedata()
except Exception:
_error.print_exception(
'exception clearing gamedata for team:', team,
'for player:', player, 'in activity:', activity)
# Remove the team from the session.
self.teams.remove(team)
try:
with _ba.Context(self):
self.on_team_leave(team)
except Exception:
_error.print_exception(
'exception in on_team_leave for session', self)
# Clear the team's session-data (so dying stuff will
# have proper context).
try:
with _ba.Context(self):
team.reset_sessiondata()
except Exception:
_error.print_exception(
'exception clearing sessiondata for team:', team,
'in session:', self)
# Now remove them from the session list.
self.players.remove(player)
else:
if sessionplayer not in self.players:
print('ERROR: Session.on_player_leave called'
' for player not in our list.')
return
_ba.playsound(_ba.getsound('playerLeft'))
activity = self._activity_weak()
if not sessionplayer.in_game:
# Ok, the player's still in the lobby. Simply remove them from it.
with _ba.Context(self):
try:
self.lobby.remove_chooser(sessionplayer)
except Exception:
print_exception('Error in Lobby.remove_chooser().')
else:
# Ok, they've already entered the game. Remove them from
# teams/activities/etc.
sessionteam = sessionplayer.team
assert sessionteam is not None
assert sessionplayer in sessionteam.players
_ba.screenmessage(
Lstr(resource='playerLeftText',
subs=[('${PLAYER}', sessionplayer.get_name(full=True))]))
# Remove them from their SessionTeam.
if sessionplayer in sessionteam.players:
sessionteam.players.remove(sessionplayer)
else:
print('SessionPlayer not found in SessionTeam'
' in on_player_leave.')
# Grab their activity-specific player instance.
player = sessionplayer.gameplayer
assert isinstance(player, (Player, type(None)))
# Remove them from any current Activity.
if activity is not None:
if player in activity.players:
activity.remove_player(sessionplayer)
else:
print('Player not found in Activity in on_player_leave.')
# If we're a non-team session, remove their team too.
if not self._use_teams:
# They should have been the only one on their team.
assert not sessionteam.players
# Remove their Team from the Activity.
if activity is not None:
if sessionteam.gameteam in activity.teams:
activity.remove_team(sessionteam)
else:
print('Team not found in Activity in on_player_leave.')
# And then from the Session.
with _ba.Context(self):
if sessionteam in self.teams:
try:
self.teams.remove(sessionteam)
self.on_team_leave(sessionteam)
except Exception:
print_exception(
f'Error in on_team_leave for Session {self}.')
else:
print('Team no in Session teams in on_player_leave.')
try:
sessionteam.reset_sessiondata()
except Exception:
print_exception(
f'Error clearing sessiondata'
f' for team {sessionteam} in session {self}.')
# Now remove them from the session list.
self.players.remove(sessionplayer)
def end(self) -> None:
"""Initiates an end to the session and a return to the main menu.
@ -401,7 +329,6 @@ class Session:
def launch_end_session_activity(self) -> None:
"""(internal)"""
from ba import _error
from ba._activitytypes import EndSessionActivity
from ba._enums import TimeType
with _ba.Context(self):
@ -412,18 +339,18 @@ class Session:
since_last = (curtime - self.launch_end_session_activity_time)
if since_last < 30.0:
return
_error.print_error(
print_error(
'launch_end_session_activity called twice (since_last=' +
str(since_last) + ')')
self.launch_end_session_activity_time = curtime
self.set_activity(_ba.new_activity(EndSessionActivity))
self.wants_to_end = False
self._ending = True # Prevent further activity-mucking.
self._ending = True # Prevent further actions.
def on_team_join(self, team: ba.Team) -> None:
def on_team_join(self, team: ba.SessionTeam) -> None:
"""Called when a new ba.Team joins the session."""
def on_team_leave(self, team: ba.Team) -> None:
def on_team_leave(self, team: ba.SessionTeam) -> None:
"""Called when a ba.Team is leaving the session."""
def _complete_end_activity(self, activity: ba.Activity,
@ -433,10 +360,8 @@ class Session:
with _ba.Context(self):
self.on_activity_end(activity, results)
except Exception:
from ba import _error
_error.print_exception(
'exception in on_activity_end() for session', self, 'activity',
activity, 'with results', results)
print_exception('exception in on_activity_end() for session', self,
'activity', activity, 'with results', results)
def end_activity(self, activity: ba.Activity, results: Any, delay: float,
force: bool) -> None:
@ -473,8 +398,7 @@ class Session:
def handlemessage(self, msg: Any) -> Any:
"""General message handling; can be passed any message object."""
from ba._lobby import PlayerReadyMessage
from ba._error import UNHANDLED
from ba._messages import PlayerProfilesChangedMessage
from ba._messages import PlayerProfilesChangedMessage, UNHANDLED
if isinstance(msg, PlayerReadyMessage):
self._on_player_ready(msg.chooser)
return None
@ -496,84 +420,46 @@ class Session:
(on_transition_in, etc) to get it. (so you can't do
session.set_activity(foo) and then ba.newnode() to add a node to foo)
"""
# pylint: disable=too-many-statements
# pylint: disable=too-many-branches
from ba import _error
from ba._gameutils import sharedobj
from ba._enums import TimeType
# Sanity test: make sure this doesn't get called recursively.
if self._in_set_activity:
raise Exception(
raise RuntimeError(
'Session.set_activity() cannot be called recursively.')
self._in_set_activity = True
if activity.session is not _ba.getsession():
raise Exception("Provided Activity's Session is not current.")
raise RuntimeError("Provided Activity's Session is not current.")
# Quietly ignore this if the whole session is going down.
if self._ending:
return
if activity is self._activity_retained:
_error.print_error('activity set to already-current activity')
print_error('activity set to already-current activity')
return
if self._next_activity is not None:
raise Exception('Activity switch already in progress (to ' +
str(self._next_activity) + ')')
self._in_set_activity = True
raise RuntimeError('Activity switch already in progress (to ' +
str(self._next_activity) + ')')
prev_activity = self._activity_retained
if prev_activity is not None:
with _ba.Context(prev_activity):
gprev = sharedobj('globals')
prev_globals = sharedobj('globals')
else:
gprev = None
prev_globals = None
with _ba.Context(activity):
# Now that it's going to be front and center,
# set some global values based on what the activity wants.
glb = sharedobj('globals')
glb.use_fixed_vr_overlay = activity.use_fixed_vr_overlay
glb.allow_kick_idle_players = activity.allow_kick_idle_players
if activity.inherits_slow_motion and gprev is not None:
glb.slow_motion = gprev.slow_motion
else:
glb.slow_motion = activity.slow_motion
if activity.inherits_music and gprev is not None:
glb.music_continuous = True # Prevent restarting same music.
glb.music = gprev.music
glb.music_count += 1
if activity.inherits_camera_vr_offset and gprev is not None:
glb.vr_camera_offset = gprev.vr_camera_offset
if activity.inherits_vr_overlay_center and gprev is not None:
glb.vr_overlay_center = gprev.vr_overlay_center
glb.vr_overlay_center_enabled = gprev.vr_overlay_center_enabled
# If they want to inherit tint from the previous activity.
if activity.inherits_tint and gprev is not None:
glb.tint = gprev.tint
glb.vignette_outer = gprev.vignette_outer
glb.vignette_inner = gprev.vignette_inner
# Let the activity do its thing.
activity.start_transition_in()
# Let the activity do its thing.
activity.transition_in(prev_globals)
self._next_activity = activity
# If we have a current activity, tell it it's transitioning out;
# the next one will become current once this one dies.
if prev_activity is not None:
# pylint: disable=protected-access
prev_activity._transitioning_out = True
# pylint: enable=protected-access
# Activity will be None until the next one begins.
with _ba.Context(prev_activity):
prev_activity.on_transition_out()
prev_activity.transition_out()
# Setting this to None should free up the old activity to die,
# which will call begin_next_activity.
@ -586,35 +472,15 @@ class Session:
else:
self.begin_next_activity()
# Tell the C layer that this new activity is now 'foregrounded'.
# This means that its globals node controls global stuff and stuff
# like console operations, keyboard shortcuts, etc will run in it.
# pylint: disable=protected-access
# noinspection PyProtectedMember
activity._activity_data.make_foreground()
# pylint: enable=protected-access
# We want to call _destroy() for the previous activity once it should
# tear itself down, clear out any self-refs, etc. If the new activity
# has a transition-time, set it up to be called after that passes;
# otherwise call it immediately. After this call the activity should
# have no refs left to it and should die (which will trigger the next
# activity to run).
# We want to call destroy() for the previous activity once it should
# tear itself down, clear out any self-refs, etc. After this call
# the activity should have no refs left to it and should die (which
# will trigger the next activity to run).
if prev_activity is not None:
if activity.transition_time > 0.0:
# FIXME: We should tweak the activity to not allow
# node-creation/etc when we call _destroy (or after).
with _ba.Context('ui'):
# pylint: disable=protected-access
# noinspection PyProtectedMember
_ba.timer(activity.transition_time,
prev_activity._destroy,
timetype=TimeType.REAL)
# Just run immediately.
else:
# noinspection PyProtectedMember
prev_activity._destroy() # pylint: disable=protected-access
with _ba.Context('ui'):
_ba.timer(max(0.0, activity.transition_time),
prev_activity.destroy,
timetype=TimeType.REAL)
self._in_set_activity = False
def getactivity(self) -> Optional[ba.Activity]:
@ -631,34 +497,29 @@ class Session:
"""
return []
def _request_player(self, player: ba.Player) -> bool:
def _request_player(self, sessionplayer: ba.SessionPlayer) -> bool:
"""Called by the C++ layer when players want to join."""
# If we're ending, allow no new players.
if self._ending:
return False
# Ask the user.
# Ask the session subclass to approve/deny this request.
try:
with _ba.Context(self):
result = self.on_player_request(player)
result = self.on_player_request(sessionplayer)
except Exception:
from ba import _error
_error.print_exception('error in on_player_request call for', self)
print_exception('error in on_player_request call for', self)
result = False
# If the user said yes, add the player to the session list.
if result:
self.players.append(player)
# If we have a current activity with a lobby,
# ask it to bring up a chooser for this player.
# otherwise they'll have to wait around for the next activity.
self.players.append(sessionplayer)
with _ba.Context(self):
try:
self.lobby.add_chooser(player)
self.lobby.add_chooser(sessionplayer)
except Exception:
from ba import _error
_error.print_exception('exception in lobby.add_chooser()')
print_exception('exception in lobby.add_chooser()')
return result
@ -683,17 +544,12 @@ class Session:
self._activity_weak = weakref.ref(self._next_activity)
self._next_activity = None
# Lets kick out any players sitting in the lobby since
# new activities such as score screens could cover them up;
# better to have them rejoin.
# Kick out anyone loitering in the lobby.
self.lobby.remove_all_choosers_and_kick_players()
activity = self._activity_weak()
assert activity is not None
activity.begin(self)
self._activity_retained.begin(self)
def _on_player_ready(self, chooser: ba.Chooser) -> None:
"""Called when a ba.Player has checked themself ready."""
from ba._lang import Lstr
lobby = chooser.lobby
activity = self._activity_weak()
@ -711,10 +567,11 @@ class Session:
# Get our next activity going.
self._complete_end_activity(activity, {})
else:
_ba.screenmessage(Lstr(resource='notEnoughPlayersText',
subs=[('${COUNT}', str(min_players))
]),
color=(1, 1, 0))
_ba.screenmessage(
Lstr(resource='notEnoughPlayersText',
subs=[('${COUNT}', str(min_players))]),
color=(1, 1, 0),
)
_ba.playsound(_ba.getsound('error'))
else:
return
@ -724,24 +581,19 @@ class Session:
self._add_chosen_player(chooser)
lobby.remove_chooser(chooser.getplayer())
def _add_chosen_player(self, chooser: ba.Chooser) -> ba.Player:
# pylint: disable=too-many-statements
# pylint: disable=too-many-branches
from ba import _error
from ba._lang import Lstr
from ba._team import Team
from ba import _freeforallsession
player = chooser.getplayer()
if player not in self.players:
_error.print_error('player not found in session '
'player-list after chooser selection')
def _add_chosen_player(self, chooser: ba.Chooser) -> ba.SessionPlayer:
from ba._team import SessionTeam
sessionplayer = chooser.getplayer()
assert sessionplayer in self.players, (
'SessionPlayer not found in session '
'player-list after chooser selection.')
activity = self._activity_weak()
assert activity is not None
# We need to reset the player's input here, as it is currently
# Reset the player's input here, as it is probably
# referencing the chooser which could inadvertently keep it alive.
player.reset_input()
sessionplayer.reset_input()
# Pass it to the current activity if it has already begun
# (otherwise it'll get passed once begin is called).
@ -749,74 +601,51 @@ class Session:
and not activity.is_joining_activity)
# If we're not allowing mid-game joins, don't pass; just announce
# the arrival.
# the arrival and say they'll partake next round.
if pass_to_activity:
if not self._allow_mid_activity_joins:
pass_to_activity = False
with _ba.Context(self):
_ba.screenmessage(Lstr(resource='playerDelayedJoinText',
subs=[('${PLAYER}',
player.get_name(full=True))
]),
color=(0, 1, 0))
_ba.screenmessage(
Lstr(resource='playerDelayedJoinText',
subs=[('${PLAYER}',
sessionplayer.get_name(full=True))]),
color=(0, 1, 0),
)
# If we're a non-team game, each player gets their own team
# If we're a non-team session, each player gets their own team.
# (keeps mini-game coding simpler if we can always deal with teams).
if self._use_teams:
team = chooser.get_team()
sessionteam = chooser.get_team()
else:
our_team_id = self._next_team_id
team = Team(team_id=our_team_id,
name=chooser.getplayer().get_name(full=True,
icon=False),
color=chooser.get_color())
self.teams.append(team)
self._next_team_id += 1
sessionteam = SessionTeam(
team_id=our_team_id,
color=chooser.get_color(),
name=chooser.getplayer().get_name(full=True, icon=False),
)
# Add player's team to the Session.
self.teams.append(sessionteam)
try:
with _ba.Context(self):
self.on_team_join(team)
self.on_team_join(sessionteam)
except Exception:
_error.print_exception(f'exception in on_team_join for {self}')
print_exception(f'exception in on_team_join for {self}')
# Add player's team to the Activity.
if pass_to_activity:
if team in activity.teams:
_error.print_error(
'Duplicate team ID in ba.Session._add_chosen_player')
activity.teams.append(team)
try:
with _ba.Context(activity):
activity.on_team_join(team)
except Exception:
_error.print_exception(
f'ERROR: exception in on_team_join for {activity}')
activity.add_team(sessionteam)
player.set_data(team=team,
character=chooser.get_character_name(),
color=chooser.get_color(),
highlight=chooser.get_highlight())
assert sessionplayer not in sessionteam.players
sessionteam.players.append(sessionplayer)
sessionplayer.set_data(team=sessionteam,
character=chooser.get_character_name(),
color=chooser.get_color(),
highlight=chooser.get_highlight())
self.stats.register_player(player)
self.stats.register_player(sessionplayer)
if pass_to_activity:
if isinstance(self, _freeforallsession.FreeForAllSession):
if player.team.players:
_error.print_error('expected 0 players in FFA team')
# Don't actually add the player to their team list if we're not
# in an activity. (players get (re)added to their team lists
# when the activity begins).
player.team.players.append(player)
if player in activity.players:
_error.print_exception(
f'Dup player in ba.Session._add_chosen_player: {player}')
else:
activity.players.append(player)
player.set_activity(activity)
pnode = activity.create_player_node(player)
player.set_node(pnode)
try:
with _ba.Context(activity):
activity.on_player_join(player)
except Exception:
_error.print_exception(
f'Error on on_player_join for {activity}')
return player
activity.add_player(sessionplayer)
return sessionplayer

View File

@ -19,7 +19,6 @@
# SOFTWARE.
# -----------------------------------------------------------------------------
"""Functionality related to scores and statistics."""
from __future__ import annotations
import random
@ -28,6 +27,8 @@ from typing import TYPE_CHECKING
from dataclasses import dataclass
import _ba
from ba._error import (print_exception, print_error, SessionTeamNotFoundError,
SessionPlayerNotFoundError)
if TYPE_CHECKING:
import ba
@ -37,12 +38,11 @@ if TYPE_CHECKING:
@dataclass
class PlayerScoredMessage:
# noinspection PyUnresolvedReferences
"""Informs something that a ba.Player scored.
Category: Message Classes
Attributes:
Attrs:
score
The score value.
@ -61,7 +61,7 @@ class PlayerRecord:
"""
character: str
def __init__(self, name: str, name_full: str, player: ba.Player,
def __init__(self, name: str, name_full: str, player: ba.SessionPlayer,
stats: ba.Stats):
self.name = name
self.name_full = name_full
@ -74,34 +74,34 @@ 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.Player] = None
self._player: Optional[ba.Player] = None
self._team: Optional[ReferenceType[ba.Team]] = None
self._last_player: Optional[ba.SessionPlayer] = None
self._player: Optional[ba.SessionPlayer] = None
self._team: Optional[ReferenceType[ba.SessionTeam]] = None
self.streak = 0
self.associate_with_player(player)
@property
def team(self) -> ba.Team:
"""The ba.Team the last associated player was last on.
def team(self) -> ba.SessionTeam:
"""The ba.SessionTeam the last associated player was last on.
This can still return a valid result even if the player is gone.
Raises a ba.TeamNotFoundError if the team no longer exists.
Raises a ba.SessionTeamNotFoundError if the team no longer exists.
"""
assert self._team is not None
team = self._team()
if team is None:
from ba._error import TeamNotFoundError
raise TeamNotFoundError()
raise SessionTeamNotFoundError()
return team
@property
def player(self) -> ba.Player:
"""Return the instance's associated ba.Player.
def player(self) -> ba.SessionPlayer:
"""Return the instance's associated ba.SessionPlayer.
Raises a ba.PlayerNotFoundError if the player no longer exists."""
Raises a ba.SessionPlayerNotFoundError if the player
no longer exists.
"""
if not self._player:
from ba._error import PlayerNotFoundError
raise PlayerNotFoundError()
raise SessionPlayerNotFoundError()
return self._player
def get_name(self, full: bool = False) -> str:
@ -127,7 +127,7 @@ class PlayerRecord:
return stats.getactivity()
return None
def associate_with_player(self, player: ba.Player) -> 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
@ -139,7 +139,7 @@ class PlayerRecord:
self._multi_kill_timer = None
self._multi_kill_count = 0
def get_last_player(self) -> ba.Player:
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
@ -203,10 +203,13 @@ class PlayerRecord:
from bastd.actor.popuptext import PopupText
# Only award this if they're still alive and we can get
# their pos.
if self._player is not None and self._player.node:
our_pos = self._player.node.position
else:
# a current position for them.
our_pos: Optional[Sequence[float]] = None
if self._player is not None:
if self._player.gameplayer is not None:
if self._player.gameplayer.node:
our_pos = self._player.gameplayer.node.position
if our_pos is None:
return
# Jitter position a bit since these often come in clusters.
@ -263,9 +266,8 @@ class Stats:
# Load our media into this activity's context.
if activity is not None:
if activity.is_expired():
from ba import _error
_error.print_error('unexpected finalized activity')
if activity.expired:
print_error('unexpected finalized activity')
else:
with _ba.Context(activity):
self._load_activity_media()
@ -303,7 +305,7 @@ class Stats:
s_player.accum_killed_count = 0
s_player.streak = 0
def register_player(self, player: ba.Player) -> None:
def register_player(self, player: ba.SessionPlayer) -> None:
"""Register a player with this score-set."""
name = player.get_name()
name_full = player.get_name(full=True)
@ -329,7 +331,7 @@ class Stats:
records[record_id] = record
return records
def player_got_hit(self, player: ba.Player) -> None:
def player_got_hit(self, player: ba.SessionPlayer) -> None:
"""Call this when a player got hit."""
s_player = self._player_records[player.get_name()]
s_player.streak = 0
@ -388,8 +390,7 @@ class Stats:
subs=[('${NAME}', name_full)]),
color=_math.normalized_color(player.team.color))
except Exception:
from ba import _error
_error.print_exception('error showing big_message')
print_exception('error showing big_message')
# If we currently have a actor, pop up a score over it.
if display and showpoints:
@ -430,8 +431,7 @@ class Stats:
color=player.color,
image=player.get_icon())
except Exception:
from ba import _error
_error.print_exception('error announcing score')
print_exception('error announcing score')
s_player.score += points
s_player.accumscore += points
@ -458,14 +458,14 @@ class Stats:
prec.killed_count += 1
try:
if killed and _ba.getactivity().announce_player_deaths:
if killer == player:
if killer is player:
_ba.screenmessage(Lstr(resource='nameSuicideText',
subs=[('${NAME}', name)]),
top=True,
color=player.color,
image=player.get_icon())
elif killer is not None:
if killer.team == player.team:
if killer.team is player.team:
_ba.screenmessage(Lstr(resource='nameBetrayedText',
subs=[('${NAME}',
killer.get_name()),
@ -488,5 +488,4 @@ class Stats:
color=player.color,
image=player.get_icon())
except Exception:
from ba import _error
_error.print_exception('error announcing kill')
print_exception('error announcing kill')

View File

@ -21,15 +21,17 @@
"""Defines Team class."""
from __future__ import annotations
from typing import TYPE_CHECKING
import weakref
from typing import TYPE_CHECKING, TypeVar, Generic
if TYPE_CHECKING:
from typing import Dict, List, Sequence, Any, Tuple, Union
from weakref import ReferenceType
from typing import Dict, List, Sequence, Tuple, Union, Optional
import ba
class Team:
"""A team of one or more ba.Players.
class SessionTeam:
"""A team of one or more ba.SessionPlayers.
Category: Gameplay Classes
@ -42,11 +44,14 @@ class Team:
name
The team's name.
id
The unique numeric id of the team.
color
The team's color.
players
The list of ba.Players on the team.
The list of ba.SessionPlayers on the team.
gamedata
A dict for use by the current ba.Activity
@ -62,56 +67,82 @@ class Team:
# Annotate our attr types at the class level so they're introspectable.
name: Union[ba.Lstr, str]
color: Tuple[float, ...]
players: List[ba.Player]
color: Tuple[float, ...] # FIXME: can't we make this fixed len?
players: List[ba.SessionPlayer]
gamedata: Dict
sessiondata: Dict
id: int
def __init__(self,
team_id: int = 0,
name: Union[ba.Lstr, str] = '',
color: Sequence[float] = (1.0, 1.0, 1.0)):
"""Instantiate a ba.Team.
"""Instantiate a ba.SessionTeam.
In most cases, all teams are provided to you by the ba.Session,
ba.Session, so calling this shouldn't be necessary.
"""
# TODO: Once we spin off team copies for each activity, we don't
# need to bother with trying to lock things down, since it won't
# matter at that point if the activity mucks with them.
# Temporarily allow us to set our own attrs
# (keeps pylint happier than using __setattr__ explicitly for all).
object.__setattr__(self, '_locked', False)
self._team_id: int = team_id
self.id = team_id
self.name = name
self.color = tuple(color)
self.players = []
self.gamedata = {}
self.sessiondata = {}
# Now prevent further attr sets.
self._locked = True
def get_id(self) -> int:
"""Returns the numeric team ID."""
return self._team_id
def reset(self) -> None:
"""(internal)"""
self.reset_gamedata()
object.__setattr__(self, 'players', [])
self.gameteam: Optional[Team] = None
def reset_gamedata(self) -> None:
"""(internal)"""
object.__setattr__(self, 'gamedata', {})
self.gamedata = {}
def reset_sessiondata(self) -> None:
"""(internal)"""
object.__setattr__(self, 'sessiondata', {})
self.sessiondata = {}
def __setattr__(self, name: str, value: Any) -> None:
if self._locked:
raise Exception("can't set attrs on ba.Team objects")
object.__setattr__(self, name, value)
PlayerType = TypeVar('PlayerType', bound='ba.Player')
class Team(Generic[PlayerType]):
"""Testing."""
# Defining these types at the class level instead of in __init__ so
# that types are introspectable (these are still instance attrs).
players: List[PlayerType]
id: int
name: Union[ba.Lstr, str]
color: Tuple[float, ...] # FIXME: can't we make this fixed len?
_sessionteam: ReferenceType[SessionTeam]
# TODO: kill these.
gamedata: Dict
sessiondata: Dict
# NOTE: avoiding having any __init__() here since it seems to not
# get called by default if a dataclass inherits from us.
def postinit(self, sessionteam: SessionTeam) -> None:
"""Wire up a newly created SessionTeam.
(internal)
"""
self.players = []
self._sessionteam = weakref.ref(sessionteam)
self.id = sessionteam.id
self.name = sessionteam.name
self.color = sessionteam.color
self.gamedata = sessionteam.gamedata
self.sessiondata = sessionteam.sessiondata
@property
def sessionteam(self) -> SessionTeam:
"""Return the ba.SessionTeam corresponding to this Team.
Throws a ba.SessionTeamNotFoundError if there is none.
"""
if self._sessionteam is not None:
sessionteam = self._sessionteam()
if sessionteam is not None:
return sessionteam
from ba import _error
raise _error.SessionTeamNotFoundError()

View File

@ -22,21 +22,24 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, TypeVar
import _ba
from ba._freeforallsession import FreeForAllSession
from ba._gameactivity import GameActivity
from ba._gameresults import TeamGameResults
from ba._dualteamsession import DualTeamSession
import _ba
if TYPE_CHECKING:
from typing import Any, Dict, Type, Sequence
from bastd.actor.playerspaz import PlayerSpaz
import ba
PlayerType = TypeVar('PlayerType', bound='ba.Player')
TeamType = TypeVar('TeamType', bound='ba.Team')
class TeamGameActivity(GameActivity):
class TeamGameActivity(GameActivity[PlayerType, TeamType]):
"""Base class for teams and free-for-all mode games.
Category: Gameplay Classes
@ -56,6 +59,7 @@ class TeamGameActivity(GameActivity):
or issubclass(sessiontype, FreeForAllSession))
def __init__(self, settings: Dict[str, Any]):
super().__init__(settings)
# By default we don't show kill-points in free-for-all.
@ -104,7 +108,7 @@ class TeamGameActivity(GameActivity):
_error.print_exception()
def spawn_player_spaz(self,
player: ba.Player,
player: PlayerType,
position: Sequence[float] = None,
angle: float = None) -> PlayerSpaz:
"""
@ -117,7 +121,7 @@ class TeamGameActivity(GameActivity):
if position is None:
# In teams-mode get our team-start-location.
if isinstance(self.session, DualTeamSession):
position = (self.map.get_start_position(player.team.get_id()))
position = (self.map.get_start_position(player.team.id))
else:
# Otherwise do free-for-all spawn locations.
position = self.map.get_ffa_start_position(self.players)

View File

@ -38,12 +38,12 @@ if TYPE_CHECKING:
from bastd.ui.league.rankbutton import LeagueRankButton
class CoopScoreScreen(ba.Activity):
class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]):
"""Score screen showing the results of a cooperative game."""
def __init__(self, settings: Dict[str, Any]):
# pylint: disable=too-many-statements
super().__init__(settings=settings)
super().__init__(settings)
# Keep prev activity alive while we fade in
self.transition_time = 0.5
@ -1008,7 +1008,7 @@ class CoopScoreScreen(ba.Activity):
# We need to manually run this in the context of our activity
# and only if we aren't shutting down.
# (really should make the submit_score call handle that stuff itself)
if self.is_expired():
if self.expired:
return
with ba.Context(self):
# Delay a bit if results come in too fast.

View File

@ -73,7 +73,7 @@ class TeamVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
scale=0.25,
color=(0.5, 0.5, 0.5, 1.0),
jitter=3.0).autoretain()
for team in self.teams:
for team in self.session.teams:
ba.timer(
i * 0.15 + 0.15,
ba.WeakCall(self._show_team_name, vval - i * height, team,
@ -99,8 +99,8 @@ class TeamVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
i += 1
self.show_player_scores()
def _show_team_name(self, pos_v: float, team: ba.Team, kill_delay: float,
shiftdelay: float) -> None:
def _show_team_name(self, pos_v: float, team: ba.SessionTeam,
kill_delay: float, shiftdelay: float) -> None:
del kill_delay # unused arg
ZoomText(ba.Lstr(value='${A}:', subs=[('${A}', team.name)]),
position=(100, pos_v),
@ -113,7 +113,7 @@ class TeamVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
color=team.color,
jitter=1.0).autoretain()
def _show_team_old_score(self, pos_v: float, team: ba.Team,
def _show_team_old_score(self, pos_v: float, team: ba.SessionTeam,
shiftdelay: float) -> None:
ZoomText(str(team.sessiondata['score'] - 1),
position=(150, pos_v),
@ -127,8 +127,9 @@ class TeamVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
h_align='left',
jitter=1.0).autoretain()
def _show_team_score(self, pos_v: float, team: ba.Team, scored: bool,
kill_delay: float, shiftdelay: float) -> None:
def _show_team_score(self, pos_v: float, team: ba.SessionTeam,
scored: bool, kill_delay: float,
shiftdelay: float) -> None:
del kill_delay # unused arg
ZoomText(str(team.sessiondata['score']),
position=(150, pos_v),

View File

@ -19,13 +19,14 @@
# SOFTWARE.
# -----------------------------------------------------------------------------
"""Functionality related to teams mode score screen."""
from __future__ import annotations
from typing import TYPE_CHECKING
import ba
from ba.internal import ScoreScreenActivity
from bastd.actor.text import Text
from bastd.actor.image import Image
if TYPE_CHECKING:
from typing import Any, Dict, Optional, Union
@ -42,7 +43,6 @@ class MultiTeamScoreScreenActivity(ScoreScreenActivity):
self._show_up_next: bool = True
def on_begin(self) -> None:
from bastd.actor.text import Text
super().on_begin()
session = self.session
if self._show_up_next and isinstance(session, ba.MultiTeamSession):
@ -77,8 +77,6 @@ class MultiTeamScoreScreenActivity(ScoreScreenActivity):
"""Show scores for individual players."""
# pylint: disable=too-many-locals
# pylint: disable=too-many-statements
from bastd.actor.text import Text
from bastd.actor.image import Image
ts_v_offset = 150.0 + y_offset
ts_h_offs = 80.0 + x_offset
@ -90,6 +88,7 @@ class MultiTeamScoreScreenActivity(ScoreScreenActivity):
def _get_prec_score(p_rec: ba.PlayerRecord) -> Optional[int]:
if is_free_for_all and results is not None:
assert isinstance(results, ba.TeamGameResults)
assert p_rec.team.gameteam is not None
val = results.get_team_score(p_rec.team)
return val
return p_rec.accumscore
@ -97,7 +96,8 @@ class MultiTeamScoreScreenActivity(ScoreScreenActivity):
def _get_prec_score_str(p_rec: ba.PlayerRecord) -> Union[str, ba.Lstr]:
if is_free_for_all and results is not None:
assert isinstance(results, ba.TeamGameResults)
val = results.get_team_score_str(p_rec.team)
assert p_rec.team.gameteam is not None
val = results.get_team_score_str(p_rec.team.gameteam)
assert val is not None
return val
return str(p_rec.accumscore)
@ -113,7 +113,7 @@ class MultiTeamScoreScreenActivity(ScoreScreenActivity):
valid_players = list(self.stats.get_records().items())
def _get_player_score_set_entry(
player: ba.Player) -> Optional[ba.PlayerRecord]:
player: ba.SessionPlayer) -> Optional[ba.PlayerRecord]:
for p_rec in valid_players:
# PyCharm incorrectly thinks valid_players is a List[str]
# noinspection PyUnresolvedReferences

View File

@ -142,7 +142,7 @@ class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
h_align=Text.HAlign.CENTER,
transition_delay=t_incr * 4).autoretain()
win_score = (session.get_series_length() - 1) / 2 + 1
win_score = (session.get_series_length() - 1) // 2 + 1
lose_score = 0
for team in self.teams:
if team.sessiondata['score'] != win_score:
@ -344,7 +344,7 @@ class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
if not self.is_transitioning_out():
ba.setmusic(ba.MusicType.VICTORY)
def _show_winner(self, team: ba.Team) -> None:
def _show_winner(self, team: ba.SessionTeam) -> None:
from bastd.actor.image import Image
from bastd.actor.zoomtext import ZoomText
if not self._is_ffa:

View File

@ -22,13 +22,16 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Generic, TypeVar
import ba
from bastd.actor.spaz import Spaz
if TYPE_CHECKING:
from typing import Any, Optional, Sequence, Tuple
from typing import Any, Sequence, Tuple, Optional
PlayerType = TypeVar('PlayerType', bound=ba.Player)
TeamType = TypeVar('TeamType', bound=ba.Team)
class PlayerSpazDeathMessage:
@ -38,9 +41,6 @@ class PlayerSpazDeathMessage:
Attributes:
spaz
The ba.PlayerSpaz that died.
killed
If True, the spaz was killed;
If False, they left the game or the round ended.
@ -55,11 +55,22 @@ class PlayerSpazDeathMessage:
def __init__(self, spaz: PlayerSpaz, was_killed: bool,
killerplayer: Optional[ba.Player], how: ba.DeathType):
"""Instantiate a message with the given values."""
self.spaz = spaz
self._spaz = spaz
self.killed = was_killed
self.killerplayer = killerplayer
self.how = how
def getspaz(
self, activity: ba.Activity[PlayerType,
TeamType]) -> PlayerSpaz[PlayerType]:
"""Return the spaz that died.
The current activity is required as an argument so the exact type of
PlayerSpaz can be determined by the type checker.
"""
del activity # Unused
return self._spaz
class PlayerSpazHurtMessage:
"""A message saying a ba.PlayerSpaz was hurt.
@ -77,7 +88,7 @@ class PlayerSpazHurtMessage:
self.spaz = spaz
class PlayerSpaz(Spaz):
class PlayerSpaz(Spaz, Generic[PlayerType]):
"""A ba.Spaz subclass meant to be controlled by a ba.Player.
category: Gameplay Classes
@ -91,10 +102,10 @@ class PlayerSpaz(Spaz):
"""
def __init__(self,
player: PlayerType,
color: Sequence[float] = (1.0, 1.0, 1.0),
highlight: Sequence[float] = (0.5, 0.5, 0.5),
character: str = 'Spaz',
player: ba.Player = None,
powerups_expire: bool = True):
"""Create a spaz for the provided ba.Player.
@ -108,12 +119,13 @@ class PlayerSpaz(Spaz):
source_player=player,
start_invincible=True,
powerups_expire=powerups_expire)
self.last_player_attacked_by: Optional[ba.Player] = None
self.last_player_attacked_by: Optional[PlayerType] = None
self.last_attacked_time = 0.0
self.last_attacked_type: Optional[Tuple[str, str]] = None
self.held_count = 0
self.last_player_held_by: Optional[ba.Player] = None
self.last_player_held_by: Optional[PlayerType] = None
self._player = player
self.playertype = type(player)
# Grab the node for this player and wire it to follow our spaz
# (so players' controllers know where to draw their guides, etc).
@ -123,7 +135,7 @@ class PlayerSpaz(Spaz):
self.node.connectattr('torso_position', player.node, 'position')
@property
def player(self) -> ba.Player:
def player(self) -> PlayerType:
"""The ba.Player associated with this Spaz.
If the player no longer exists, raises an ba.PlayerNotFoundError.
@ -132,7 +144,7 @@ class PlayerSpaz(Spaz):
raise ba.PlayerNotFoundError()
return self._player
def getplayer(self) -> Optional[ba.Player]:
def getplayer(self) -> Optional[PlayerType]:
"""Get the ba.Player associated with this Spaz.
Note that this may return None if the player has left.
@ -226,7 +238,8 @@ class PlayerSpaz(Spaz):
if isinstance(msg, ba.PickedUpMessage):
super().handlemessage(msg) # Augment standard behavior.
self.held_count += 1
picked_up_by = msg.node.source_player
picked_up_by = ba.playercast_o(self.playertype,
msg.node.source_player)
if picked_up_by:
self.last_player_held_by = picked_up_by
elif isinstance(msg, ba.DroppedMessage):
@ -237,11 +250,12 @@ class PlayerSpaz(Spaz):
# Let's count someone dropping us as an attack.
try:
picked_up_by = msg.node.source_player
picked_up_by_2 = ba.playercast_o(self.playertype,
msg.node.source_player)
except Exception:
picked_up_by = None
if picked_up_by:
self.last_player_attacked_by = picked_up_by
picked_up_by_2 = None
if picked_up_by_2:
self.last_player_attacked_by = picked_up_by_2
self.last_attacked_time = ba.time()
self.last_attacked_type = ('picked_up', 'default')
elif isinstance(msg, ba.DieMessage):
@ -296,7 +310,8 @@ class PlayerSpaz(Spaz):
# Keep track of the player who last hit us for point rewarding.
elif isinstance(msg, ba.HitMessage):
if msg.source_player:
self.last_player_attacked_by = msg.source_player
srcplayer = ba.playercast_o(self.playertype, msg.source_player)
self.last_player_attacked_by = srcplayer
self.last_attacked_time = ba.time()
self.last_attacked_type = (msg.hit_type, msg.hit_subtype)
super().handlemessage(msg) # Augment standard behavior.

View File

@ -136,7 +136,7 @@ class RespawnIcon:
"""Return info on where we should be shown and stored."""
activity = ba.getactivity()
if isinstance(ba.getsession(), ba.DualTeamSession):
on_right = player.team.get_id() % 2 == 1
on_right = player.team.id % 2 == 1
# Store a list of icons in the team.
try:

View File

@ -317,7 +317,7 @@ class _EntryProxy:
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
self._team_id = team.get_id()
self._team_id = team.id
def __del__(self) -> None:
scoreboard = self._scoreboard()
@ -366,7 +366,7 @@ class Scoreboard:
flash: bool = True,
show_value: bool = True) -> None:
"""Update the score-board display for the given ba.Team."""
if not team.get_id() in self._entries:
if not team.id in self._entries:
self._add_team(team)
# create a proxy in the team which will kill
# our entry when it dies (for convenience)
@ -374,21 +374,21 @@ class Scoreboard:
raise Exception('existing _EntryProxy found')
team.gamedata['_scoreboard_entry'] = _EntryProxy(self, team)
# now set the entry..
self._entries[team.get_id()].set_value(score=score,
max_score=max_score,
countdown=countdown,
flash=flash,
show_value=show_value)
self._entries[team.id].set_value(score=score,
max_score=max_score,
countdown=countdown,
flash=flash,
show_value=show_value)
def _add_team(self, team: ba.Team) -> None:
if team.get_id() in self._entries:
if team.id in self._entries:
raise Exception('Duplicate team add')
self._entries[team.get_id()] = _Entry(self,
team,
do_cover=self._do_cover,
scale=self._scale,
label=self._label,
flash_length=self._flash_length)
self._entries[team.id] = _Entry(self,
team,
do_cover=self._do_cover,
scale=self._scale,
label=self._label,
flash_length=self._flash_length)
self._update_teams()
def remove_team(self, team_id: int) -> None:

View File

@ -998,7 +998,7 @@ class BotSet:
# Don't do this if the activity is shutting down or dead.
activity: Optional[ba.Activity] = ba.getactivity(doraise=False)
if activity is None or activity.is_expired():
if activity is None or activity.expired:
return
for i in range(len(self._bot_lists)):

View File

@ -29,14 +29,14 @@ import random
from typing import TYPE_CHECKING
import ba
from bastd.actor import playerspaz
from bastd.actor.playerspaz import PlayerSpaz, PlayerSpazDeathMessage
if TYPE_CHECKING:
from typing import Any, Type, List, Dict, Tuple, Sequence, Union
# ba_meta export game
class AssaultGame(ba.TeamGameActivity):
class AssaultGame(ba.TeamGameActivity[ba.Player, ba.Team]):
"""Game where you score by touching the other team's flag."""
@classmethod
@ -109,7 +109,7 @@ class AssaultGame(ba.TeamGameActivity):
self.setup_standard_time_limit(self.settings_raw['Time Limit'])
self.setup_standard_powerup_drops()
for team in self.teams:
mat = self._base_region_materials[team.get_id()] = ba.Material()
mat = self._base_region_materials[team.id] = ba.Material()
mat.add_actions(conditions=('they_have_material',
ba.sharedobj('player_material')),
actions=(('modify_part_collision', 'collide',
@ -121,8 +121,7 @@ class AssaultGame(ba.TeamGameActivity):
# Create a score region and flag for each team.
for team in self.teams:
team.gamedata['base_pos'] = self.map.get_flag_position(
team.get_id())
team.gamedata['base_pos'] = self.map.get_flag_position(team.id)
ba.newnode('light',
attrs={
@ -139,20 +138,20 @@ class AssaultGame(ba.TeamGameActivity):
position=team.gamedata['base_pos'],
color=team.color)
basepos = team.gamedata['base_pos']
ba.newnode(
'region',
owner=team.gamedata['flag'].node,
attrs={
'position': (basepos[0], basepos[1] + 0.75, basepos[2]),
'scale': (0.5, 0.5, 0.5),
'type': 'sphere',
'materials': [self._base_region_materials[team.get_id()]]
})
ba.newnode('region',
owner=team.gamedata['flag'].node,
attrs={
'position':
(basepos[0], basepos[1] + 0.75, basepos[2]),
'scale': (0.5, 0.5, 0.5),
'type': 'sphere',
'materials': [self._base_region_materials[team.id]]
})
def handlemessage(self, msg: Any) -> Any:
if isinstance(msg, playerspaz.PlayerSpazDeathMessage):
if isinstance(msg, PlayerSpazDeathMessage):
super().handlemessage(msg) # Augment standard.
self.respawn_player(msg.spaz.player)
self.respawn_player(msg.getspaz(self).player)
else:
super().handlemessage(msg)
@ -173,14 +172,15 @@ class AssaultGame(ba.TeamGameActivity):
cnode = ba.get_collision_info('opposing_node')
assert isinstance(cnode, ba.Node)
actor = cnode.getdelegate()
if not isinstance(actor, playerspaz.PlayerSpaz):
if not isinstance(actor, PlayerSpaz):
return
player = actor.getplayer()
if not player or not player.is_alive():
if not player or not player.actor:
return
# If its another team's player, they scored.
player_team = player.team
player_team: ba.Team[ba.Player] = player.team
if player_team is not team:
# Prevent multiple simultaneous scores.
@ -194,24 +194,22 @@ class AssaultGame(ba.TeamGameActivity):
# and add flashes of light so its noticeable.
for player in player_team.players:
if player.is_alive():
if player.node:
pos = player.node.position
light = ba.newnode('light',
attrs={
'position': pos,
'color': player_team.color,
'height_attenuated': False,
'radius': 0.4
})
ba.timer(0.5, light.delete)
ba.animate(light, 'intensity', {
0: 0,
0.1: 1.0,
0.5: 0
})
pos = player.node.position
light = ba.newnode('light',
attrs={
'position': pos,
'color': player_team.color,
'height_attenuated': False,
'radius': 0.4
})
ba.timer(0.5, light.delete)
ba.animate(light, 'intensity', {
0: 0,
0.1: 1.0,
0.5: 0
})
new_pos = (self.map.get_start_position(
player_team.get_id()))
new_pos = (self.map.get_start_position(player_team.id))
light = ba.newnode('light',
attrs={
'position': new_pos,

View File

@ -25,11 +25,13 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import TYPE_CHECKING
import ba
from bastd.actor import flag as stdflag
from bastd.actor.playerspaz import PlayerSpaz, PlayerSpazDeathMessage
from bastd.actor.scoreboard import Scoreboard
if TYPE_CHECKING:
from typing import Any, Type, List, Dict, Tuple, Sequence, Union, Optional
@ -38,9 +40,10 @@ if TYPE_CHECKING:
class CTFFlag(stdflag.Flag):
"""Special flag type for ctf games."""
def __init__(self, team: ba.Team):
super().__init__(materials=[team.gamedata['flagmaterial']],
position=team.gamedata['base_pos'],
def __init__(self, team: Team):
assert team.flagmaterial is not None
super().__init__(materials=[team.flagmaterial],
position=team.base_pos,
color=team.color)
self._team = team
self.held_count = 0
@ -52,7 +55,7 @@ class CTFFlag(stdflag.Flag):
'h_align': 'center'
})
self.reset_return_times()
self.last_player_to_hold: Optional[ba.Player] = None
self.last_player_to_hold: Optional[Player] = None
self.time_out_respawn_time: Optional[int] = None
self.touch_return_time: Optional[float] = None
@ -64,7 +67,7 @@ class CTFFlag(stdflag.Flag):
self.activity.settings_raw['Flag Touch Return Time'])
@property
def team(self) -> ba.Team:
def team(self) -> Team:
"""return the flag's team."""
return self._team
@ -77,8 +80,33 @@ class CTFFlag(stdflag.Flag):
return delegate if isinstance(delegate, CTFFlag) else None
@dataclass
class Player(ba.Player['Team']):
"""Our player type for this game."""
touching_own_flag: int = 0
@dataclass
class Team(ba.Team[Player]):
"""Our team type for this game."""
base_pos: Sequence[float]
base_region_material: ba.Material
base_region: ba.Node
spaz_material_no_flag_physical: ba.Material
spaz_material_no_flag_collide: ba.Material
flagmaterial: ba.Material
score: int = 0
flag_return_touches: int = 0
home_flag_at_base: bool = True
touch_return_timer: Optional[ba.Timer] = None
enemy_flag_at_base: bool = False
flag: Optional[CTFFlag] = None
last_flag_leave_time: Optional[float] = None
touch_return_timer_ticking: Optional[ba.NodeActor] = None
# ba_meta export game
class CaptureTheFlagGame(ba.TeamGameActivity):
class CaptureTheFlagGame(ba.TeamGameActivity[Player, Team]):
"""Game of stealing other team's flag and returning it to your base."""
@classmethod
@ -102,24 +130,37 @@ class CaptureTheFlagGame(ba.TeamGameActivity):
cls,
sessiontype: Type[ba.Session]) -> List[Tuple[str, Dict[str, Any]]]:
return [
('Score to Win', {'min_value': 1, 'default': 3}),
('Score to Win', {
'min_value': 1,
'default': 3
}),
('Flag Touch Return Time', {
'min_value': 0, 'default': 0, 'increment': 1}),
'min_value': 0,
'default': 0,
'increment': 1
}),
('Flag Idle Return Time', {
'min_value': 5, 'default': 30, 'increment': 5}),
'min_value': 5,
'default': 30,
'increment': 5
}),
('Time Limit', {
'choices': [('None', 0), ('1 Minute', 60),
('2 Minutes', 120), ('5 Minutes', 300),
('10 Minutes', 600), ('20 Minutes', 1200)],
'default': 0}),
'choices': [('None', 0), ('1 Minute', 60), ('2 Minutes', 120),
('5 Minutes', 300), ('10 Minutes', 600),
('20 Minutes', 1200)],
'default': 0
}),
('Respawn Times', {
'choices': [('Shorter', 0.25), ('Short', 0.5), ('Normal', 1.0),
('Long', 2.0), ('Longer', 4.0)],
'default': 1.0}),
('Epic Mode', {'default': False})] # yapf: disable
'default': 1.0
}),
('Epic Mode', {
'default': False
}),
]
def __init__(self, settings: Dict[str, Any]):
from bastd.actor.scoreboard import Scoreboard
super().__init__(settings)
self._scoreboard = Scoreboard()
if self.settings_raw['Epic Mode']:
@ -149,29 +190,24 @@ class CaptureTheFlagGame(ba.TeamGameActivity):
ba.MusicType.FLAG_CATCHER)
super().on_transition_in()
def on_team_join(self, team: ba.Team) -> None:
team.gamedata['score'] = 0
team.gamedata['flag_return_touches'] = 0
team.gamedata['home_flag_at_base'] = True
team.gamedata['touch_return_timer'] = None
team.gamedata['enemy_flag_at_base'] = False
team.gamedata['base_pos'] = (self.map.get_flag_position(team.get_id()))
def create_team(self, sessionteam: ba.SessionTeam) -> Team:
self.project_flag_stand(team.gamedata['base_pos'])
base_pos = self.map.get_flag_position(sessionteam.id)
self.project_flag_stand(base_pos)
ba.newnode('light',
attrs={
'position': team.gamedata['base_pos'],
'position': base_pos,
'intensity': 0.6,
'height_attenuated': False,
'volume_intensity_scale': 0.1,
'radius': 0.1,
'color': team.color
'color': sessionteam.color
})
base_region_mat = team.gamedata['base_region_material'] = ba.Material()
pos = team.gamedata['base_pos']
team.gamedata['base_region'] = ba.newnode(
base_region_mat = ba.Material()
pos = base_pos
base_region = ba.newnode(
'region',
attrs={
'position': (pos[0], pos[1] + 0.75, pos[2]),
@ -180,22 +216,28 @@ class CaptureTheFlagGame(ba.TeamGameActivity):
'materials': [base_region_mat, self._all_bases_material]
})
# create some materials for this team
spaz_mat_no_flag_physical = team.gamedata[
'spaz_material_no_flag_physical'] = ba.Material()
spaz_mat_no_flag_collide = team.gamedata[
'spaz_material_no_flag_collide'] = ba.Material()
flagmat = team.gamedata['flagmaterial'] = ba.Material()
spaz_mat_no_flag_physical = ba.Material()
spaz_mat_no_flag_collide = ba.Material()
flagmat = ba.Material()
team = Team(base_pos=base_pos,
base_region_material=base_region_mat,
base_region=base_region,
spaz_material_no_flag_physical=spaz_mat_no_flag_physical,
spaz_material_no_flag_collide=spaz_mat_no_flag_collide,
flagmaterial=flagmat)
# Some parts of our spazzes don't collide physically with our
# flags but generate callbacks.
spaz_mat_no_flag_physical.add_actions(
conditions=('they_have_material', flagmat),
actions=(('modify_part_collision', 'physical',
False), ('call', 'at_connect',
lambda: self._handle_hit_own_flag(team, 1)),
('call', 'at_disconnect',
lambda: self._handle_hit_own_flag(team, 0))))
actions=(
('modify_part_collision', 'physical', False),
('call', 'at_connect',
lambda: self._handle_hit_own_flag(team, 1)),
('call', 'at_disconnect',
lambda: self._handle_hit_own_flag(team, 0)),
))
# Other parts of our spazzes don't collide with our flags at all.
spaz_mat_no_flag_collide.add_actions(conditions=('they_have_material',
@ -214,6 +256,9 @@ class CaptureTheFlagGame(ba.TeamGameActivity):
('call', 'at_disconnect',
lambda: self._handle_flag_left_base(team))))
return team
def on_team_join(self, team: Team) -> None:
self._spawn_flag_for_team(team)
self._update_scoreboard()
@ -223,14 +268,14 @@ class CaptureTheFlagGame(ba.TeamGameActivity):
self.setup_standard_powerup_drops()
ba.timer(1.0, call=self._tick, repeat=True)
def _spawn_flag_for_team(self, team: ba.Team) -> None:
flag = team.gamedata['flag'] = CTFFlag(team)
team.gamedata['flag_return_touches'] = 0
def _spawn_flag_for_team(self, team: Team) -> None:
team.flag = CTFFlag(team)
team.flag_return_touches = 0
self._flash_base(team, length=1.0)
assert flag.node
ba.playsound(self._swipsound, position=flag.node.position)
assert team.flag.node
ba.playsound(self._swipsound, position=team.flag.node.position)
def _handle_flag_entered_base(self, team: ba.Team) -> None:
def _handle_flag_entered_base(self, team: Team) -> None:
node = ba.get_collision_info('opposing_node')
assert isinstance(node, (ba.Node, type(None)))
flag = CTFFlag.from_node(node)
@ -239,14 +284,14 @@ class CaptureTheFlagGame(ba.TeamGameActivity):
return
if flag.team is team:
team.gamedata['home_flag_at_base'] = True
team.home_flag_at_base = True
# If the enemy flag is already here, score!
if team.gamedata['enemy_flag_at_base']:
if team.enemy_flag_at_base:
self._score(team)
else:
team.gamedata['enemy_flag_at_base'] = True
if team.gamedata['home_flag_at_base']:
team.enemy_flag_at_base = True
if team.home_flag_at_base:
# Award points to whoever was carrying the enemy flag.
player = flag.last_player_to_hold
if player and player.team is team:
@ -262,7 +307,7 @@ class CaptureTheFlagGame(ba.TeamGameActivity):
curtime = ba.time(ba.TimeType.BASE)
if curtime - self._last_home_flag_notice_print_time > 5.0:
self._last_home_flag_notice_print_time = curtime
bpos = team.gamedata['base_pos']
bpos = team.base_pos
tval = ba.Lstr(resource='ownFlagAtYourBaseWarning')
tnode = ba.newnode(
'text',
@ -286,10 +331,10 @@ class CaptureTheFlagGame(ba.TeamGameActivity):
# If either flag is away from base and not being held, tick down its
# respawn timer.
for team in self.teams:
flag = team.gamedata['flag']
flag = team.flag
assert flag is not None
if (not team.gamedata['home_flag_at_base']
and flag.held_count == 0):
if not team.home_flag_at_base and flag.held_count == 0:
time_out_counting_down = True
if flag.time_out_respawn_time is None:
flag.reset_return_times()
@ -307,16 +352,16 @@ class CaptureTheFlagGame(ba.TeamGameActivity):
# If there's no self-touches on this flag, set its text
# to show its auto-return counter. (if there's self-touches
# its showing that time).
if team.gamedata['flag_return_touches'] == 0:
flag.counter.text = (str(flag.time_out_respawn_time) if
(time_out_counting_down
and flag.time_out_respawn_time <= 10)
else '')
if team.flag_return_touches == 0:
flag.counter.text = (str(flag.time_out_respawn_time) if (
time_out_counting_down
and flag.time_out_respawn_time is not None
and flag.time_out_respawn_time <= 10) else '')
flag.counter.color = (1, 1, 1, 0.5)
flag.counter.scale = 0.014
def _score(self, team: ba.Team) -> None:
team.gamedata['score'] += 1
def _score(self, team: Team) -> None:
team.score += 1
ba.playsound(self._score_sound)
self._flash_base(team)
self._update_scoreboard()
@ -328,58 +373,57 @@ class CaptureTheFlagGame(ba.TeamGameActivity):
# Reset all flags/state.
for reset_team in self.teams:
if not reset_team.gamedata['home_flag_at_base']:
reset_team.gamedata['flag'].handlemessage(ba.DieMessage())
reset_team.gamedata['enemy_flag_at_base'] = False
if team.gamedata['score'] >= self.settings_raw['Score to Win']:
if not reset_team.home_flag_at_base:
assert reset_team.flag is not None
reset_team.flag.handlemessage(ba.DieMessage())
reset_team.enemy_flag_at_base = False
if team.score >= self.settings_raw['Score to Win']:
self.end_game()
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, announce_delay=0.8)
def _handle_flag_left_base(self, team: ba.Team) -> None:
def _handle_flag_left_base(self, team: Team) -> None:
cur_time = ba.time()
op_node = ba.get_collision_info('opposing_node')
assert isinstance(op_node, (ba.Node, type(None)))
flag = CTFFlag.from_node(op_node)
if not flag:
return
if flag.team is team:
# Check times here to prevent too much flashing.
if ('last_flag_leave_time' not in team.gamedata
or cur_time - team.gamedata['last_flag_leave_time'] > 3.0):
ba.playsound(self._alarmsound,
position=team.gamedata['base_pos'])
if (team.last_flag_leave_time is None
or cur_time - team.last_flag_leave_time > 3.0):
ba.playsound(self._alarmsound, position=team.base_pos)
self._flash_base(team)
team.gamedata['last_flag_leave_time'] = cur_time
team.gamedata['home_flag_at_base'] = False
team.last_flag_leave_time = cur_time
team.home_flag_at_base = False
else:
team.gamedata['enemy_flag_at_base'] = False
def _touch_return_update(self, team: ba.Team) -> None:
team.enemy_flag_at_base = False
def _touch_return_update(self, team: Team) -> None:
# Count down only while its away from base and not being held.
if (team.gamedata['home_flag_at_base']
or team.gamedata['flag'].held_count > 0):
team.gamedata['touch_return_timer_ticking'] = None
assert team.flag is not None
if team.home_flag_at_base or team.flag.held_count > 0:
team.touch_return_timer_ticking = None
return # No need to return when its at home.
if team.gamedata['touch_return_timer_ticking'] is None:
team.gamedata['touch_return_timer_ticking'] = ba.NodeActor(
if team.touch_return_timer_ticking is None:
team.touch_return_timer_ticking = ba.NodeActor(
ba.newnode('sound',
attrs={
'sound': self._ticking_sound,
'positional': False,
'loop': True
}))
flag = team.gamedata['flag']
flag = team.flag
assert flag.touch_return_time is not None
flag.touch_return_time -= 0.1
if flag.counter:
flag.counter.text = '%.1f' % flag.touch_return_time
flag.counter.text = f'{flag.touch_return_time:.1f}'
flag.counter.color = (1, 1, 0, 1)
flag.counter.scale = 0.02
@ -387,9 +431,9 @@ class CaptureTheFlagGame(ba.TeamGameActivity):
self._award_players_touching_own_flag(team)
flag.handlemessage(ba.DieMessage())
def _award_players_touching_own_flag(self, team: ba.Team) -> None:
def _award_players_touching_own_flag(self, team: Team) -> None:
for player in team.players:
if player.gamedata['touching_own_flag'] > 0:
if player.touching_own_flag > 0:
return_score = 10 + 5 * int(
self.settings_raw['Flag Touch Return Time'])
self.stats.player_scored(player,
@ -397,7 +441,7 @@ class CaptureTheFlagGame(ba.TeamGameActivity):
screenmessage=False)
@staticmethod
def _player_from_node(node: Optional[ba.Node]) -> Optional[ba.Player]:
def _player_from_node(node: Optional[ba.Node]) -> Optional[Player]:
"""Return a player if given a node that is part of one's actor."""
if not node:
return None
@ -406,7 +450,7 @@ class CaptureTheFlagGame(ba.TeamGameActivity):
return None
return delegate.getplayer()
def _handle_hit_own_flag(self, team: ba.Team, val: int) -> None:
def _handle_hit_own_flag(self, team: Team, val: int) -> None:
"""
keep track of when each player is touching their
own flag so we can award points when returned
@ -415,13 +459,13 @@ class CaptureTheFlagGame(ba.TeamGameActivity):
assert isinstance(srcnode, (ba.Node, type(None)))
player = self._player_from_node(srcnode)
if player:
player.gamedata['touching_own_flag'] += (1 if val else -1)
player.touching_own_flag += (1 if val else -1)
# If return-time is zero, just kill it immediately.. otherwise keep
# track of touches and count down.
if float(self.settings_raw['Flag Touch Return Time']) <= 0.0:
if (not team.gamedata['home_flag_at_base']
and team.gamedata['flag'].held_count == 0):
assert team.flag is not None
if not team.home_flag_at_base and team.flag.held_count == 0:
# Use a node message to kill the flag instead of just killing
# our team's. (avoids redundantly killing new flags if
@ -434,26 +478,26 @@ class CaptureTheFlagGame(ba.TeamGameActivity):
# Takes a non-zero amount of time to return.
else:
if val:
team.gamedata['flag_return_touches'] += 1
if team.gamedata['flag_return_touches'] == 1:
team.gamedata['touch_return_timer'] = ba.Timer(
team.flag_return_touches += 1
if team.flag_return_touches == 1:
team.touch_return_timer = ba.Timer(
0.1,
call=ba.Call(self._touch_return_update, team),
repeat=True)
team.gamedata['touch_return_timer_ticking'] = None
team.touch_return_timer_ticking = None
else:
team.gamedata['flag_return_touches'] -= 1
if team.gamedata['flag_return_touches'] == 0:
team.gamedata['touch_return_timer'] = None
team.gamedata['touch_return_timer_ticking'] = None
if team.gamedata['flag_return_touches'] < 0:
team.flag_return_touches -= 1
if team.flag_return_touches == 0:
team.touch_return_timer = None
team.touch_return_timer_ticking = None
if team.flag_return_touches < 0:
ba.print_error(
"CTF: flag_return_touches < 0; this shouldn't happen.")
def _flash_base(self, team: ba.Team, length: float = 2.0) -> None:
def _flash_base(self, team: Team, length: float = 2.0) -> None:
light = ba.newnode('light',
attrs={
'position': team.gamedata['base_pos'],
'position': team.base_pos,
'height_attenuated': False,
'radius': 0.3,
'color': team.color
@ -461,22 +505,21 @@ class CaptureTheFlagGame(ba.TeamGameActivity):
ba.animate(light, 'intensity', {0.0: 0, 0.25: 2.0, 0.5: 0}, loop=True)
ba.timer(length, light.delete)
def spawn_player_spaz(self, *args: Any, **keywds: Any) -> Any:
def spawn_player_spaz(self,
player: Player,
position: Sequence[float] = None,
angle: float = None) -> PlayerSpaz:
"""Intercept new spazzes and add our team material for them."""
# (chill pylint; we're passing our exact args to parent call)
# pylint: disable=signature-differs
spaz = super().spawn_player_spaz(*args, **keywds)
spaz = super().spawn_player_spaz(player, position, angle)
player = spaz.player
player.gamedata['touching_own_flag'] = 0
# Ignore false alarm for gamedata member.
no_physical_mats = [
player.team.gamedata['spaz_material_no_flag_physical']
team: Team = player.team
player.touching_own_flag = 0
no_physical_mats: List[ba.Material] = [
team.spaz_material_no_flag_physical
]
no_collide_mats = [
player.team.gamedata['spaz_material_no_flag_collide']
no_collide_mats: List[ba.Material] = [
team.spaz_material_no_flag_collide
]
# pylint: enable=arguments-differ
# Our normal parts should still collide; just not physically
# (so we can calc restores).
@ -496,14 +539,14 @@ class CaptureTheFlagGame(ba.TeamGameActivity):
def _update_scoreboard(self) -> None:
for team in self.teams:
self._scoreboard.set_team_value(team, team.gamedata['score'],
self._scoreboard.set_team_value(team, team.score,
self.settings_raw['Score to Win'])
def handlemessage(self, msg: Any) -> Any:
if isinstance(msg, PlayerSpazDeathMessage):
# Augment standard behavior.
super().handlemessage(msg)
self.respawn_player(msg.spaz.player)
self.respawn_player(msg.getspaz(self).player)
elif isinstance(msg, stdflag.FlagDeathMessage):
assert isinstance(msg.flag, CTFFlag)
ba.timer(0.1, ba.Call(self._spawn_flag_for_team, msg.flag.team))

View File

@ -37,7 +37,7 @@ if TYPE_CHECKING:
# ba_meta export game
class ChosenOneGame(ba.TeamGameActivity):
class ChosenOneGame(ba.TeamGameActivity[ba.Player, ba.Team]):
"""
Game involving trying to remain the one 'chosen one'
for a set length of time while everyone else tries to
@ -327,7 +327,7 @@ class ChosenOneGame(ba.TeamGameActivity):
if isinstance(msg, playerspaz.PlayerSpazDeathMessage):
# Augment standard behavior.
super().handlemessage(msg)
player = msg.spaz.player
player = msg.getspaz(self).player
if player is self._get_chosen_one_player():
killerplayer = msg.killerplayer
self._set_chosen_one_player(None if (

View File

@ -57,7 +57,7 @@ class ConquestFlag(Flag):
# ba_meta export game
class ConquestGame(ba.TeamGameActivity):
class ConquestGame(ba.TeamGameActivity[ba.Player, ba.Team]):
"""A game where teams try to claim all flags on the map."""
@classmethod
@ -254,7 +254,7 @@ class ConquestGame(ba.TeamGameActivity):
super().handlemessage(msg)
# Respawn only if this team has a flag.
player = msg.spaz.player
player = msg.getspaz(self).player
if player.team.gamedata['flags_held'] > 0:
self.respawn_player(player)
else:

View File

@ -35,7 +35,7 @@ if TYPE_CHECKING:
# ba_meta export game
class DeathMatchGame(ba.TeamGameActivity):
class DeathMatchGame(ba.TeamGameActivity[ba.Player, ba.Team]):
"""A game type based on acquiring kills."""
@classmethod
@ -145,7 +145,7 @@ class DeathMatchGame(ba.TeamGameActivity):
# Augment standard behavior.
super().handlemessage(msg)
player = msg.spaz.player
player = msg.getspaz(self).player
self.respawn_player(player)
killer = msg.killerplayer

View File

@ -39,7 +39,7 @@ if TYPE_CHECKING:
# ba_meta export game
class EasterEggHuntGame(ba.TeamGameActivity):
class EasterEggHuntGame(ba.TeamGameActivity[ba.Player, ba.Team]):
"""A game where score is based on collecting eggs."""
@classmethod
@ -212,7 +212,7 @@ class EasterEggHuntGame(ba.TeamGameActivity):
# Augment standard behavior.
super().handlemessage(msg)
player = msg.spaz.getplayer()
player = msg.getspaz(self).getplayer()
if not player:
return
self.stats.player_was_killed(player)

View File

@ -164,7 +164,7 @@ class Icon(ba.Actor):
# ba_meta export game
class EliminationGame(ba.TeamGameActivity):
class EliminationGame(ba.TeamGameActivity[ba.Player, ba.Team]):
"""Game type where last player(s) left alive win."""
@classmethod
@ -330,7 +330,7 @@ class EliminationGame(ba.TeamGameActivity):
# Now for each team, cycle through our available players
# adding icons.
for team in self.teams:
if team.get_id() == 0:
if team.id == 0:
xval = -60
x_offs = -78
else:
@ -362,7 +362,7 @@ class EliminationGame(ba.TeamGameActivity):
# Non-solo mode.
else:
for team in self.teams:
if team.get_id() == 0:
if team.id == 0:
xval = -50
x_offs = -85
else:
@ -395,8 +395,7 @@ class EliminationGame(ba.TeamGameActivity):
player_pos = ba.Vec3(living_player_pos)
points: List[Tuple[float, ba.Vec3]] = []
for team in self.teams:
start_pos = ba.Vec3(
self.map.get_start_position(team.get_id()))
start_pos = ba.Vec3(self.map.get_start_position(team.id))
points.append(
((start_pos - player_pos).length(), start_pos))
# Hmm.. we need to sorting vectors too?
@ -492,7 +491,7 @@ class EliminationGame(ba.TeamGameActivity):
# Augment standard behavior.
super().handlemessage(msg)
player = msg.spaz.player
player = msg.getspaz(self).player
player.gamedata['lives'] -= 1
if player.gamedata['lives'] < 0:

View File

@ -67,7 +67,7 @@ class FootballFlag(stdflag.Flag):
# ba_meta export game
class FootballTeamGame(ba.TeamGameActivity):
class FootballTeamGame(ba.TeamGameActivity[ba.Player, ba.Team]):
"""Football game for teams mode."""
@classmethod
@ -204,7 +204,7 @@ class FootballTeamGame(ba.TeamGameActivity):
if region == self._score_regions[i].node:
break
for team in self.teams:
if team.get_id() == i:
if team.id == i:
team.gamedata['score'] += 7
# Tell all players to celebrate.
@ -274,7 +274,7 @@ class FootballTeamGame(ba.TeamGameActivity):
elif isinstance(msg, playerspaz.PlayerSpazDeathMessage):
# Augment standard behavior.
super().handlemessage(msg)
self.respawn_player(msg.spaz.player)
self.respawn_player(msg.getspaz(self).player)
# Respawn dead flags.
elif isinstance(msg, stdflag.FlagDeathMessage):
@ -320,7 +320,7 @@ class FootballTeamGame(ba.TeamGameActivity):
self._flag = FootballFlag(position=self._flag_spawn_pos)
class FootballCoopGame(ba.CoopGameActivity):
class FootballCoopGame(ba.CoopGameActivity[ba.Player, ba.Team]):
"""
Co-op variant of football
"""
@ -508,7 +508,11 @@ class FootballCoopGame(ba.CoopGameActivity):
# Make a bogus team for our bots.
bad_team_name = self.get_team_display_string('Bad Guys')
self._bot_team = ba.Team(1, bad_team_name, (0.5, 0.4, 0.4))
# self._bot_team = ba.Team(1, bad_team_name, (0.5, 0.4, 0.4))
self._bot_team = ba.Team()
self._bot_team.id = 1
self._bot_team.name = bad_team_name
self._bot_team.color = (0.5, 0.4, 0.4)
for team in [self.teams[0], self._bot_team]:
team.gamedata['score'] = 0
@ -562,7 +566,7 @@ class FootballCoopGame(ba.CoopGameActivity):
spaz_type: Type[spazbot.SpazBot],
immediate: bool = False) -> None:
assert self._bot_team is not None
pos = self.map.get_start_position(self._bot_team.get_id())
pos = self.map.get_start_position(self._bot_team.id)
self._bots.spawn_bot(spaz_type,
pos=pos,
spawn_time=0.001 if immediate else 3.0,
@ -668,7 +672,7 @@ class FootballCoopGame(ba.CoopGameActivity):
for team in [self.teams[0], self._bot_team]:
assert team is not None
if team.get_id() == i:
if team.id == i:
team.gamedata['score'] += 7
# Tell all players (or bots) to celebrate.
@ -808,7 +812,7 @@ class FootballCoopGame(ba.CoopGameActivity):
from bastd.actor import respawnicon
# Respawn dead players.
player = msg.spaz.player
player = msg.getspaz(self).player
self.stats.player_was_killed(player)
assert self.initial_player_info is not None
respawn_time = 2.0 + len(self.initial_player_info) * 1.0
@ -871,7 +875,7 @@ class FootballCoopGame(ba.CoopGameActivity):
def spawn_player(self, player: ba.Player) -> ba.Actor:
spaz = self.spawn_player_spaz(player,
position=self.map.get_start_position(
player.team.get_id()))
player.team.id))
if self._preset in ['rookie_easy', 'pro_easy', 'uber_easy']:
spaz.impact_scale = 0.25
spaz.add_dropped_bomb_callback(self._handle_player_dropped_bomb)

View File

@ -100,14 +100,13 @@ class Puck(ba.Actor):
if activity:
if msg.source_player in activity.players:
self.last_players_to_touch[
msg.source_player.team.get_id(
)] = msg.source_player
msg.source_player.team.id] = msg.source_player
else:
super().handlemessage(msg)
# ba_meta export game
class HockeyGame(ba.TeamGameActivity):
class HockeyGame(ba.TeamGameActivity[ba.Player, ba.Team]):
"""Ice hockey game."""
@classmethod
@ -255,8 +254,10 @@ class HockeyGame(ba.TeamGameActivity):
player = playernode.getdelegate().getplayer()
except Exception:
player = puck = None
assert isinstance(player, ba.Player)
assert isinstance(puck, Puck)
if player and puck:
puck.last_players_to_touch[player.team.get_id()] = player
puck.last_players_to_touch[player.team.id] = player
def _kill_puck(self) -> None:
self._puck = None
@ -279,7 +280,7 @@ class HockeyGame(ba.TeamGameActivity):
break
for team in self.teams:
if team.get_id() == index:
if team.id == index:
scoring_team = team
team.gamedata['score'] += 1
@ -290,13 +291,12 @@ class HockeyGame(ba.TeamGameActivity):
# If we've got the player from the scoring team that last
# touched us, give them points.
if (scoring_team.get_id() in self._puck.last_players_to_touch
and self._puck.last_players_to_touch[
scoring_team.get_id()]):
self.stats.player_scored(self._puck.last_players_to_touch[
scoring_team.get_id()],
100,
big_message=True)
if (scoring_team.id in self._puck.last_players_to_touch
and self._puck.last_players_to_touch[scoring_team.id]):
self.stats.player_scored(
self._puck.last_players_to_touch[scoring_team.id],
100,
big_message=True)
# End game if we won.
if team.gamedata['score'] >= self.settings_raw['Score to Win']:
@ -341,7 +341,7 @@ class HockeyGame(ba.TeamGameActivity):
if isinstance(msg, playerspaz.PlayerSpazDeathMessage):
# Augment standard behavior...
super().handlemessage(msg)
self.respawn_player(msg.spaz.player)
self.respawn_player(msg.getspaz(self).player)
# Respawn dead pucks.
elif isinstance(msg, PuckDeathMessage):

View File

@ -37,7 +37,7 @@ if TYPE_CHECKING:
# ba_meta export game
class KeepAwayGame(ba.TeamGameActivity):
class KeepAwayGame(ba.TeamGameActivity[ba.Player, ba.Team]):
"""Game where you try to keep the flag away from your enemies."""
FLAG_NEW = 0
@ -271,7 +271,7 @@ class KeepAwayGame(ba.TeamGameActivity):
if isinstance(msg, playerspaz.PlayerSpazDeathMessage):
# Augment standard behavior.
super().handlemessage(msg)
self.respawn_player(msg.spaz.player)
self.respawn_player(msg.getspaz(self).player)
elif isinstance(msg, stdflag.FlagDeathMessage):
self._spawn_flag()
elif isinstance(

View File

@ -39,7 +39,7 @@ if TYPE_CHECKING:
# ba_meta export game
class KingOfTheHillGame(ba.TeamGameActivity):
class KingOfTheHillGame(ba.TeamGameActivity[ba.Player, ba.Team]):
"""Game where a team wins by holding a 'hill' for a set amount of time."""
FLAG_NEW = 0
@ -281,7 +281,7 @@ class KingOfTheHillGame(ba.TeamGameActivity):
super().handlemessage(msg) # Augment default.
# No longer can count as at_flag once dead.
player = msg.spaz.player
player = msg.getspaz(self).player
player.gamedata['at_flag'] = 0
self._update_flag_state()
self.respawn_player(player)

View File

@ -26,26 +26,31 @@
from __future__ import annotations
import random
from dataclasses import dataclass
from typing import TYPE_CHECKING
import ba
from bastd.actor.bomb import Bomb
from bastd.actor.playerspaz import PlayerSpazDeathMessage
from bastd.actor.onscreentimer import OnScreenTimer
if TYPE_CHECKING:
from typing import Any, Tuple, Sequence, Optional, List, Dict, Type, Type
from bastd.actor.onscreentimer import OnScreenTimer
@dataclass
class PlayerData(ba.BasePlayerData):
"""Data we store per player."""
death_time: Optional[float] = None
class Player(ba.Player['Team']):
"""Our player type for this game."""
def __init__(self) -> None:
super().__init__()
self.death_time: Optional[float] = None
class Team(ba.Team[Player]):
"""Our team type for this game."""
# ba_meta export game
class MeteorShowerGame(ba.TeamGameActivity):
class MeteorShowerGame(ba.TeamGameActivity[Player, Team]):
"""Minigame involving dodging falling bombs."""
@classmethod
@ -80,6 +85,9 @@ class MeteorShowerGame(ba.TeamGameActivity):
or issubclass(sessiontype, ba.FreeForAllSession)
or issubclass(sessiontype, ba.CoopSession))
# Print messages when players die (since its meaningful in this game).
announce_player_deaths = True
def __init__(self, settings: Dict[str, Any]):
super().__init__(settings)
@ -94,12 +102,7 @@ class MeteorShowerGame(ba.TeamGameActivity):
if self._epic_mode:
self.slow_motion = True
# Print messages when players die (since its meaningful in this game).
self.announce_player_deaths = True
def on_begin(self) -> None:
from bastd.actor.onscreentimer import OnScreenTimer
super().on_begin()
# Drop a wave every few seconds.. and every so often drop the time
@ -122,22 +125,23 @@ class MeteorShowerGame(ba.TeamGameActivity):
# Check for immediate end (if we've only got 1 player, etc).
ba.timer(5.0, self._check_end_game)
def on_player_join(self, player: ba.Player) -> None:
def on_player_join(self, player: Player) -> None:
# Don't allow joining after we start
# (would enable leave/rejoin tomfoolery).
if self.has_begun():
ba.screenmessage(ba.Lstr(resource='playerDelayedJoinText',
subs=[('${PLAYER}',
player.get_name(full=True))]),
color=(0, 1, 0))
ba.screenmessage(
ba.Lstr(resource='playerDelayedJoinText',
subs=[('${PLAYER}', player.get_name(full=True))]),
color=(0, 1, 0),
)
# For score purposes, mark them as having died right as the
# game started.
assert self._timer is not None
PlayerData.get(player).death_time = self._timer.getstarttime()
player.death_time = self._timer.getstarttime()
return
self.spawn_player(player)
def on_player_leave(self, player: ba.Player) -> None:
def on_player_leave(self, player: Player) -> None:
# Augment default behavior.
super().on_player_leave(player)
@ -145,7 +149,7 @@ class MeteorShowerGame(ba.TeamGameActivity):
self._check_end_game()
# overriding the default character spawning..
def spawn_player(self, player: ba.Player) -> ba.Actor:
def spawn_player(self, player: Player) -> ba.Actor:
spaz = self.spawn_player_spaz(player)
# Let's reconnect this player's controls to this
@ -168,7 +172,8 @@ class MeteorShowerGame(ba.TeamGameActivity):
curtime = ba.time()
# Record the player's moment of death.
PlayerData.get(msg.spaz.player).death_time = curtime
# assert isinstance(msg.spaz.player
msg.getspaz(self).player.death_time = curtime
# In co-op mode, end the game the instant everyone dies
# (more accurate looking).
@ -250,19 +255,18 @@ class MeteorShowerGame(ba.TeamGameActivity):
# (these per-player scores are only meaningful in team-games)
for team in self.teams:
for player in team.players:
playerdata = PlayerData.get(player)
survived = False
# Throw an extra fudge factor in so teams that
# didn't die come out ahead of teams that did.
if playerdata.death_time is None:
if player.death_time is None:
survived = True
playerdata.death_time = cur_time + 1
player.death_time = cur_time + 1
# Award a per-player score depending on how many seconds
# they lasted (per-player scores only affect teams mode;
# everywhere else just looks at the per-team score).
score = int(playerdata.death_time - self._timer.getstarttime())
score = int(player.death_time - self._timer.getstarttime())
if survived:
score += 50 # A bit extra for survivors.
self.stats.player_scored(player, score, screenmessage=False)
@ -284,10 +288,9 @@ class MeteorShowerGame(ba.TeamGameActivity):
# that team.
longest_life = 0.0
for player in team.players:
playerdata = PlayerData.get(player)
assert playerdata.death_time is not None
assert player.death_time is not None
longest_life = max(longest_life,
playerdata.death_time - start_time)
player.death_time - start_time)
# Submit the score value in milliseconds.
results.set_team_score(team, int(1000.0 * longest_life))

View File

@ -38,7 +38,7 @@ if TYPE_CHECKING:
# ba_meta export game
class NinjaFightGame(ba.TeamGameActivity):
class NinjaFightGame(ba.TeamGameActivity[ba.Player, ba.Team]):
"""
A co-op game where you try to defeat a group
of Ninjas as fast as possible
@ -148,7 +148,7 @@ class NinjaFightGame(ba.TeamGameActivity):
# A player has died.
if isinstance(msg, playerspaz.PlayerSpazDeathMessage):
super().handlemessage(msg) # do standard stuff
self.respawn_player(msg.spaz.player) # kick off a respawn
self.respawn_player(msg.getspaz(self).player) # kick off a respawn
# A spaz-bot has died.
elif isinstance(msg, spazbot.SpazBotDeathMessage):

View File

@ -38,7 +38,7 @@ if TYPE_CHECKING:
from bastd.actor.scoreboard import Scoreboard
class OnslaughtGame(ba.CoopGameActivity):
class OnslaughtGame(ba.CoopGameActivity[ba.Player, ba.Team]):
"""Co-op game where players try to survive attacking waves of enemies."""
tips: List[Union[str, Dict[str, Any]]] = [
@ -1163,7 +1163,7 @@ class OnslaughtGame(ba.CoopGameActivity):
elif isinstance(msg, playerspaz.PlayerSpazDeathMessage):
super().handlemessage(msg) # Augment standard behavior.
player = msg.spaz.getplayer()
player = msg.getspaz(self).getplayer()
assert player is not None
self._a_player_has_been_hurt = True

View File

@ -67,7 +67,7 @@ class RaceRegion(ba.Actor):
# ba_meta export game
class RaceGame(ba.TeamGameActivity):
class RaceGame(ba.TeamGameActivity[ba.Player, ba.Team]):
"""Game of racing around a track."""
@classmethod
@ -733,7 +733,7 @@ class RaceGame(ba.TeamGameActivity):
if isinstance(msg, PlayerSpazDeathMessage):
# Augment default behavior.
super().handlemessage(msg)
player = msg.spaz.getplayer()
player = msg.getspaz(self).getplayer()
if not player:
ba.print_error('got no player in PlayerSpazDeathMessage')
return

View File

@ -38,7 +38,7 @@ if TYPE_CHECKING:
from typing import Type, Any, List, Dict, Tuple, Sequence, Optional
class RunaroundGame(ba.CoopGameActivity):
class RunaroundGame(ba.CoopGameActivity[ba.Player, ba.Team]):
"""Game involving trying to bomb bots as they walk through the map."""
tips = [
@ -48,7 +48,7 @@ class RunaroundGame(ba.CoopGameActivity):
]
# How fast our various bot types walk.
_bot_speed_map = {
_bot_speed_map: Dict[Type[spazbot.SpazBot], float] = {
spazbot.BomberBot: 0.48,
spazbot.BomberBotPro: 0.48,
spazbot.BomberBotProShielded: 0.48,
@ -1127,7 +1127,7 @@ class RunaroundGame(ba.CoopGameActivity):
elif isinstance(msg, playerspaz.PlayerSpazDeathMessage):
from bastd.actor import respawnicon
self._a_player_has_been_killed = True
player = msg.spaz.getplayer()
player = msg.getspaz(self).getplayer()
if player is None:
ba.print_error('FIXME: getplayer() should no'
' longer ever be returning None')

View File

@ -38,7 +38,7 @@ if TYPE_CHECKING:
# ba_meta export game
class TargetPracticeGame(ba.TeamGameActivity):
class TargetPracticeGame(ba.TeamGameActivity[ba.Player, ba.Team]):
"""Game where players try to hit targets with bombs."""
@classmethod
@ -187,7 +187,7 @@ class TargetPracticeGame(ba.TeamGameActivity):
# When players die, respawn them.
if isinstance(msg, playerspaz.PlayerSpazDeathMessage):
super().handlemessage(msg) # Do standard stuff.
player = msg.spaz.getplayer()
player = msg.getspaz(self).getplayer()
assert player is not None
self.respawn_player(player) # Kick off a respawn.
elif isinstance(msg, Target.TargetHitMessage):

View File

@ -35,7 +35,7 @@ if TYPE_CHECKING:
from bastd.actor.scoreboard import Scoreboard
class TheLastStandGame(ba.CoopGameActivity):
class TheLastStandGame(ba.CoopGameActivity[ba.Player, ba.Team]):
"""Slow motion how-long-can-you-last game."""
tips = [
@ -257,7 +257,7 @@ class TheLastStandGame(ba.CoopGameActivity):
def handlemessage(self, msg: Any) -> Any:
if isinstance(msg, playerspaz.PlayerSpazDeathMessage):
player = msg.spaz.getplayer()
player = msg.getspaz(self).getplayer()
if player is None:
ba.print_error('FIXME: getplayer() should no longer '
'ever be returning None.')

View File

@ -27,9 +27,9 @@ import time
import weakref
from typing import TYPE_CHECKING
import _ba
import ba
from bastd.actor import spaz
import _ba
if TYPE_CHECKING:
from typing import Any, List, Optional
@ -43,7 +43,7 @@ if TYPE_CHECKING:
# noinspection PyAttributeOutsideInit
class MainMenuActivity(ba.Activity):
class MainMenuActivity(ba.Activity[ba.Player, ba.Team]):
"""Activity showing the rotating main menu bg stuff."""
_stdassets = ba.Dependency(ba.AssetPackage, 'stdassets@1')
@ -358,7 +358,7 @@ class MainMenuActivity(ba.Activity):
# need to make nodes and stuff.. should fix the serverget
# call so it.
activity = self._activity()
if activity is None or activity.is_expired():
if activity is None or activity.expired:
return
with ba.Context(activity):
@ -830,7 +830,7 @@ class MainMenuActivity(ba.Activity):
def _start_preloads(self) -> None:
# FIXME: The func that calls us back doesn't save/restore state
# or check for a dead activity so we have to do that ourself.
if self.is_expired():
if self.expired:
return
with ba.Context(self):
_preload1()
@ -929,6 +929,6 @@ class MainMenuSession(ba.Session):
# Any ending activity leads us into the main menu one.
self.set_activity(ba.new_activity(MainMenuActivity))
def on_player_request(self, player: ba.Player) -> bool:
def on_player_request(self, player: ba.SessionPlayer) -> bool:
# Reject all player requests.
return False

View File

@ -181,7 +181,7 @@ class ButtonRelease:
timeformat=ba.TimeFormat.MILLISECONDS)
class TutorialActivity(ba.Activity):
class TutorialActivity(ba.Activity[ba.Player, ba.Team]):
def __init__(self, settings: Dict[str, Any] = None):
from bastd.maps import Rampage

View File

@ -31,22 +31,19 @@ from pathlib import Path
from threading import Lock, Thread, current_thread
from typing import TYPE_CHECKING
# We make use of the bacommon and efro packages as well as site-packages
# included with our bundled Ballistica dist, so we need to add those paths
# before we import them.
sys.path += [
str(Path(Path(__file__).parent, 'dist', 'ba_data', 'python')),
str(Path(Path(__file__).parent, 'dist', 'ba_data', 'python-site-packages'))
]
from bacommon.servermanager import ServerConfig, StartServerModeCommand
from efro.dataclasses import dataclass_assign, dataclass_validate
from efro.error import CleanError
from efro.terminal import Clr
# We change our working directory according to file's path
# so that the script can be properly executed from anywhere
os.chdir(os.path.abspath(os.path.dirname(__file__)))
# We make use of the bacommon and efro packages as well as site-packages
# included with our bundled Ballistica dist.
sys.path += [
str(Path(os.getcwd(), 'dist', 'ba_data', 'python')),
str(Path(os.getcwd(), 'dist', 'ba_data', 'python-site-packages'))
]
if TYPE_CHECKING:
from typing import Optional, List, Dict, Union, Tuple
from types import FrameType
@ -54,7 +51,7 @@ if TYPE_CHECKING:
# Not sure how much versioning we'll do with this, but this will get
# printed at startup in case we need it.
VERSION_STR = '1.0'
VERSION_STR = '1.0.1'
class ServerManagerApp:
@ -432,6 +429,10 @@ class ServerManagerApp:
def main() -> None:
"""Run a BallisticaCore server manager in interactive mode."""
try:
# Change our working directory according to file's path
# so that this script can be run from anywhere.
os.chdir(os.path.abspath(os.path.dirname(__file__)))
ServerManagerApp().run_interactive()
except CleanError as exc:
# For clean errors, do a simple print and fail; no tracebacks/etc.

View File

@ -3,6 +3,21 @@ mypy_path = __EFRO_PROJECT_ROOT__/tools:__EFRO_PROJECT_ROOT__/assets/src/ba_data
__EFRO_MYPY_STANDARD_SETTINGS__
# We have mypy alert us if we use any vars that have been imported
# by other modules; we want to import everything directly from its
# source. However there are some modules that are explicitly exist
# to reexport things, so let's let those pass.
# (we could also set __all__ in those modules, but that's a lot of
# repeating ourself)
[mypy-ba]
no_implicit_reexport = False
[mypy-efro.entity]
no_implicit_reexport = False
[mypy-ba.internal]
no_implicit_reexport = False
[mypy-ba.deprecated]
no_implicit_reexport = False
[mypy-pylint.*]
ignore_missing_imports = True

View File

@ -79,6 +79,7 @@ good-names=i,
v2,
ex,
Run,
id,
T,
S,
U,

File diff suppressed because it is too large Load Diff

View File

@ -237,7 +237,7 @@ class App:
with open(self._state_data_path, 'r') as infile:
self._state = StateData(**json.loads(infile.read()))
except Exception:
print(f'{Clr.SRED}Error loading {TOOL_NAME} data;'
print(f'{Clr.RED}Error loading {TOOL_NAME} data;'
f' resetting to defaults.{Clr.RST}')
def _save_state(self) -> None:
@ -285,7 +285,7 @@ class App:
return response
def _upload_file(self, filename: str, call: str, args: Dict) -> None:
print(f'{Clr.SBLU}Uploading {filename}{Clr.RST}', flush=True)
print(f'{Clr.BLU}Uploading {filename}{Clr.RST}', flush=True)
with tempfile.TemporaryDirectory() as tempdir:
srcpath = Path(filename)
gzpath = Path(tempdir, 'file.gz')

View File

@ -112,7 +112,7 @@ def _lazybuild_check_paths(inpaths: List[str], category: SourceCategory,
# Now see this path is newer than our target..
if mtime is None or os.path.getmtime(path) >= mtime:
print(f'{Clr.SMAG}Build of {tnamepretty} triggered by'
f' {path}{Clr.RST}')
f" '{path}'{Clr.RST}")
return True
return False
@ -483,7 +483,7 @@ def _vstr(nums: Sequence[int]) -> str:
def checkenv() -> None:
"""Check for tools necessary to build and run the app."""
from efrotools import PYTHON_BIN
print('Checking environment...', flush=True)
print(f'{Clr.BLD}Checking environment...{Clr.RST}', flush=True)
# Make sure they've got curl.
if subprocess.run(['which', 'curl'], check=False,
@ -539,7 +539,7 @@ def checkenv() -> None:
'Alternately, "tools/snippets install_pip_reqs"'
' will update all pip requirements.')
print('Environment ok.', flush=True)
print(f'{Clr.BLD}Environment ok.{Clr.RST}', flush=True)
def get_pip_reqs() -> List[str]:

View File

@ -26,9 +26,9 @@ import copy
import logging
from typing import TYPE_CHECKING, Generic, TypeVar, overload
from efro.entity._support import (BaseField, BoundCompoundValue,
BoundListField, BoundDictField,
BoundCompoundListField,
from efro.entity._base import BaseField
from efro.entity._support import (BoundCompoundValue, BoundListField,
BoundDictField, BoundCompoundListField,
BoundCompoundDictField)
from efro.entity.util import have_matching_fields

View File

@ -27,7 +27,9 @@ import inspect
import logging
from collections import abc
from enum import Enum
from typing import TYPE_CHECKING, TypeVar, Tuple, Optional, Generic
from typing import TYPE_CHECKING, TypeVar, Generic
# Our Pylint class_generics_filter gives us a false-positive unused-import.
from typing import Tuple, Optional # pylint: disable=W0611
from efro.entity._base import DataHandler, BaseField
from efro.entity.util import compound_eq

View File

@ -24,17 +24,24 @@ from __future__ import annotations
import datetime
import time
import weakref
from typing import TYPE_CHECKING, cast, TypeVar, Generic
if TYPE_CHECKING:
import asyncio
from typing import Any, Dict, Callable, Optional
from weakref import ReferenceType
from typing import Any, Dict, Callable, Optional, Type
T = TypeVar('T')
TVAL = TypeVar('TVAL')
TARG = TypeVar('TARG')
TRET = TypeVar('TRET')
class _EmptyObj:
pass
def utc_now() -> datetime.datetime:
"""Get offset-aware current utc time.
@ -46,6 +53,15 @@ def utc_now() -> datetime.datetime:
return datetime.datetime.now(datetime.timezone.utc)
def empty_weakref(objtype: Type[T]) -> ReferenceType[T]:
"""Return an invalidated weak-reference for the specified type."""
# At runtime, all weakrefs are the same; our type arg is just
# for the static type checker.
del objtype # Unused.
# Just create an object and let it die. Is there a cleaner way to do this?
return weakref.ref(_EmptyObj()) # type: ignore
def data_size_str(bytecount: int) -> str:
"""Given a size in bytes, returns a short human readable string.

View File

@ -187,7 +187,7 @@ def _py_symbol_at_column(line: str, col: int) -> str:
return line[start:end]
def py_examine(filename: Path, line: int, column: int,
def py_examine(projroot: Path, filename: Path, line: int, column: int,
selection: Optional[str], operation: str) -> None:
"""Given file position info, performs some code inspection."""
# pylint: disable=too-many-locals
@ -242,7 +242,7 @@ def py_examine(filename: Path, line: int, column: int,
with tmppath.open('w') as outfile:
outfile.write('\n'.join(flines))
try:
code.runmypy([str(tmppath)], check=False)
code.runmypy(projroot, [str(tmppath)], check=False)
except Exception as exc:
print('error running mypy:', exc)
tmppath.unlink()

View File

@ -38,8 +38,8 @@ def formatcode(projroot: Path, full: bool) -> None:
"""Run clang-format on all of our source code (multithreaded)."""
import time
import concurrent.futures
from efrotools import get_files_hash
from multiprocessing import cpu_count
from efrotools import get_files_hash
os.chdir(projroot)
cachepath = Path(projroot, 'config/.cache-formatcode')
if full and cachepath.exists():
@ -66,13 +66,8 @@ def formatcode(projroot: Path, full: bool) -> None:
sys.stdout.flush()
return {'f': filename, 't': duration}
# NOTE: using fewer workers than we have logical procs for now;
# we're bottlenecked by one or two long running instances
# so it actually helps to lighten the load around them.
# may want to revisit later when we have everything chopped up
# better
with concurrent.futures.ThreadPoolExecutor(max_workers=cpu_count() //
2) as executor:
with concurrent.futures.ThreadPoolExecutor(
max_workers=cpu_count()) as executor:
# Converting this to a list will propagate any errors.
list(executor.map(format_file, dirtyfiles))
@ -88,8 +83,9 @@ def formatcode(projroot: Path, full: bool) -> None:
def cpplint(projroot: Path, full: bool) -> None:
"""Run lint-checking on all code deemed lint-able."""
from concurrent.futures import ThreadPoolExecutor
from efrotools import get_config
from multiprocessing import cpu_count
from efrotools import get_config
from efro.terminal import Clr
os.chdir(projroot)
filenames = get_code_filenames(projroot)
@ -115,21 +111,24 @@ def cpplint(projroot: Path, full: bool) -> None:
dirtyfiles = cache.get_dirty_files()
if dirtyfiles:
print(f'CppLint checking {len(dirtyfiles)} file(s)...')
print(f'{Clr.BLU}CppLint checking'
f' {len(dirtyfiles)} file(s)...{Clr.RST}')
def lint_file(filename: str) -> None:
result = subprocess.call(['cpplint', '--root=src', filename])
if result != 0:
raise Exception(f'Linting failed for {filename}')
with ThreadPoolExecutor(max_workers=cpu_count() // 2) as executor:
with ThreadPoolExecutor(max_workers=cpu_count()) as executor:
# Converting this to a list will propagate any errors.
list(executor.map(lint_file, dirtyfiles))
if dirtyfiles:
cache.mark_clean(filenames)
cache.write()
print(f'CppLint: all {len(filenames)} files are passing.', flush=True)
print(
f'{Clr.GRN}CppLint: all {len(filenames)} files are passing.{Clr.RST}',
flush=True)
def get_code_filenames(projroot: Path) -> List[str]:
@ -153,8 +152,8 @@ def formatscripts(projroot: Path, full: bool) -> None:
"""Runs yapf on all our scripts (multithreaded)."""
import time
from concurrent.futures import ThreadPoolExecutor
from efrotools import get_files_hash
from multiprocessing import cpu_count
from efrotools import get_files_hash
os.chdir(projroot)
cachepath = Path(projroot, 'config/.cache-formatscripts')
if full and cachepath.exists():
@ -178,12 +177,7 @@ def formatscripts(projroot: Path, full: bool) -> None:
print(f'Formatted {filename} in {duration:.2f} seconds.')
sys.stdout.flush()
# NOTE: using fewer workers than we have logical procs for now;
# we're bottlenecked by one or two long running instances
# so it actually helps to lighten the load around them.
# may want to revisit later when we have everything chopped up
# better
with ThreadPoolExecutor(max_workers=cpu_count() // 2) as executor:
with ThreadPoolExecutor(max_workers=cpu_count()) as executor:
# Convert the futures to a list to propagate any errors even
# though there are no return values we use.
list(executor.map(format_file, dirtyfiles))
@ -235,9 +229,27 @@ def get_script_filenames(projroot: Path) -> List[str]:
return sorted(list(f for f in filenames if 'flycheck_' not in f))
def runpylint(projroot: Path, filenames: List[str]) -> None:
"""Run Pylint explicitly on files."""
pylintrc = Path(projroot, '.pylintrc')
if not os.path.isfile(pylintrc):
raise Exception('pylintrc not found where expected')
# Technically we could just run pylint standalone via command line here,
# but let's go ahead and run it inline so we're consistent with our cached
# full-project version.
_run_pylint(projroot,
pylintrc,
cache=None,
dirtyfiles=filenames,
allfiles=None)
def pylint(projroot: Path, full: bool, fast: bool) -> None:
"""Run lint-checking on all scripts deemed lint-able."""
"""Run Pylint on all scripts in our project (with smart dep tracking)."""
from efrotools import get_files_hash
from efro.terminal import Clr
pylintrc = Path(projroot, '.pylintrc')
if not os.path.isfile(pylintrc):
raise Exception('pylintrc not found where expected')
@ -270,22 +282,17 @@ def pylint(projroot: Path, full: bool, fast: bool) -> None:
dirtyfiles.sort(reverse=True, key=lambda f: os.stat(f).st_mtime)
if dirtyfiles:
print(f'Pylint checking {len(dirtyfiles)} file(s)...', flush=True)
print(
f'{Clr.BLU}Pylint checking {len(dirtyfiles)} file(s)...{Clr.RST}',
flush=True)
try:
_run_script_lint(projroot, pylintrc, cache, dirtyfiles, filenames)
except Exception:
# Note: even if we fail here, we still want to
_run_pylint(projroot, pylintrc, cache, dirtyfiles, filenames)
finally:
# No matter what happens, we still want to
# update our disk cache (since some lints may have passed).
print('Pylint failed.', flush=True)
# Hmm; this can be handy sometimes; perhaps should add an env
# var to control it?
if bool(False):
import traceback
traceback.print_exc()
cache.write()
sys.exit(255)
print(f'Pylint: all {len(filenames)} files are passing.', flush=True)
print(f'{Clr.GRN}Pylint: all {len(filenames)} files are passing.{Clr.RST}',
flush=True)
cache.write()
@ -349,32 +356,39 @@ def _dirty_dep_check(fname: str, filestates: Dict[str, bool], cache: FileCache,
return dirty
def _run_script_lint(projroot: Path, pylintrc: Union[Path, str],
cache: FileCache, dirtyfiles: List[str],
allfiles: List[str]) -> Dict[str, Any]:
def _run_pylint(projroot: Path, pylintrc: Union[Path, str],
cache: Optional[FileCache], dirtyfiles: List[str],
allfiles: Optional[List[str]]) -> Dict[str, Any]:
import time
from pylint import lint
from efro.error import CleanError
from efro.terminal import Clr
start_time = time.time()
args = ['--rcfile', str(pylintrc)]
args = ['--rcfile', str(pylintrc), '--output-format=colorized']
args += dirtyfiles
name = f'{len(dirtyfiles)} file(s)'
run = lint.Run(args, do_exit=False)
result = _apply_pylint_run_to_cache(projroot, run, dirtyfiles, allfiles,
cache)
if result != 0:
raise Exception(f'Linting failed for {result} file(s).')
if cache is not None:
assert allfiles is not None
result = _apply_pylint_run_to_cache(projroot, run, dirtyfiles,
allfiles, cache)
if result != 0:
raise CleanError(f'Linting failed for {result} file(s).')
# Sanity check: when the linter fails we should always be failing too.
# If not, it means we're probably missing something and incorrectly
# marking a failed file as clean.
if run.linter.msg_status != 0 and result == 0:
raise RuntimeError('Pylint linter returned non-zero result'
' but we did not; this is probably a bug.')
else:
if run.linter.msg_status != 0:
raise CleanError('Pylint failed.')
# Sanity check: when the linter fails we should always be failing too.
# If not, it means we're probably missing something and incorrectly
# marking a failed file as clean.
if run.linter.msg_status != 0 and result == 0:
raise Exception('linter returned non-zero result but we did not;'
' this is probably a bug.')
# result = run.linter.msg_status
# we can run
duration = time.time() - start_time
print(f'Pylint passed for {name} in {duration:.1f} seconds.')
print(f'{Clr.GRN}Pylint passed for {name}'
f' in {duration:.1f} seconds.{Clr.RST}')
sys.stdout.flush()
return {'f': dirtyfiles, 't': duration}
@ -415,7 +429,7 @@ def _apply_pylint_run_to_cache(projroot: Path, run: Any, dirtyfiles: List[str],
# Update dependencies for what we just ran.
# A run leaves us with a map of modules to a list of the modules that
# imports them. We want the opposite though: for each of our modules
# imports them. We want the opposite though: for each of our modules
# we want a list of the modules it imports.
reversedeps = {}
@ -453,10 +467,8 @@ def _apply_pylint_run_to_cache(projroot: Path, run: Any, dirtyfiles: List[str],
for fname in dirtyfiles:
fmod = paths_to_names[fname]
if fmod not in deps:
# Since this code is a bit flaky, lets always announce when
# we come up empty and keep a whitelist of expected values to
# ignore.
# Since this code is a bit flaky, lets always announce when we
# come up empty and keep a whitelist of expected values to ignore.
no_deps_modules.add(fmod)
depsval: List[str] = []
else:
@ -512,14 +524,16 @@ def _filter_module_name(mpath: str) -> str:
return mpath[:-9] if mpath.endswith('.__init__') else mpath
def runmypy(filenames: List[str],
def runmypy(projroot: Path,
filenames: List[str],
full: bool = False,
check: bool = True) -> None:
"""Run MyPy on provided filenames."""
from efrotools import PYTHON_BIN
args = [
PYTHON_BIN, '-m', 'mypy', '--pretty', '--no-error-summary',
'--config-file', '.mypy.ini'
'--config-file',
str(Path(projroot, '.mypy.ini'))
] + filenames
if full:
args.insert(args.index('mypy') + 1, '--no-incremental')
@ -529,22 +543,26 @@ def runmypy(filenames: List[str],
def mypy(projroot: Path, full: bool) -> None:
"""Type check all of our scripts using mypy."""
import time
from efro.terminal import Clr
from efro.error import CleanError
filenames = get_script_filenames(projroot)
print('Running Mypy ' + ('(full)' if full else '(incremental)') + '...',
flush=True)
desc = '(full)' if full else '(incremental)'
print(f'{Clr.BLU}Running Mypy {desc}...{Clr.RST}', flush=True)
starttime = time.time()
try:
runmypy(filenames, full)
runmypy(projroot, filenames, full)
except Exception:
print('Mypy: fail.')
sys.exit(255)
raise CleanError('Mypy: fail.')
duration = time.time() - starttime
print(f'Mypy passed in {duration:.1f} seconds.', flush=True)
print(f'{Clr.GRN}Mypy passed in {duration:.1f} seconds.{Clr.RST}',
flush=True)
def dmypy(projroot: Path) -> None:
"""Type check all of our scripts using mypy in daemon mode."""
import time
from efro.terminal import Clr
from efro.error import CleanError
filenames = get_script_filenames(projroot)
# Special case; explicitly kill the daemon.
@ -561,10 +579,10 @@ def dmypy(projroot: Path) -> None:
] + filenames
subprocess.run(args, check=True)
except Exception:
print('Mypy: fail.')
sys.exit(255)
raise CleanError('Mypy daemon: fail.')
duration = time.time() - starttime
print(f'Mypy daemon passed in {duration:.1f} seconds.', flush=True)
print(f'{Clr.GRN}Mypy daemon passed in {duration:.1f} seconds.{Clr.RST}',
flush=True)
def _parse_idea_results(path: Path) -> int:
@ -615,8 +633,13 @@ def _run_idea_inspections(projroot: Path,
import tempfile
import time
import datetime
from efro.error import CleanError
from efro.terminal import Clr
start_time = time.time()
print(f'{displayname} checking', len(scripts), 'file(s)...', flush=True)
print(
f'{Clr.BLU}{displayname} checking'
f' {len(scripts)} file(s)...{Clr.RST}',
flush=True)
tmpdir = tempfile.TemporaryDirectory()
iprof = Path(projroot, '.idea/inspectionProfiles/Default.xml')
if not iprof.exists():
@ -659,13 +682,12 @@ def _run_idea_inspections(projroot: Path,
for fname in files:
total_errors += _parse_idea_results(Path(tmpdir.name, fname))
if total_errors > 0:
raise RuntimeError(
f'{displayname} inspection found {total_errors} error(s).')
raise CleanError(f'{Clr.SRED}{displayname} inspection'
f' found {total_errors} error(s).{Clr.RST}')
duration = time.time() - start_time
print(
f'{displayname} passed for {len(scripts)} files'
f' in {duration:.1f} seconds.',
f'{Clr.GRN}{displayname} passed for {len(scripts)} files'
f' in {duration:.1f} seconds.{Clr.RST}',
flush=True)
@ -680,6 +702,7 @@ def _run_idea_inspections_cached(cachepath: Path,
# pylint: disable=too-many-locals
import hashlib
import json
from efro.terminal import Clr
md5 = hashlib.md5()
# Let's calc a single hash from the contents of all script files and only
@ -717,8 +740,10 @@ def _run_idea_inspections_cached(cachepath: Path,
inspectdir=inspectdir)
with open(cachepath, 'w') as outfile:
outfile.write(json.dumps({'hash': current_hash}))
print(f'{displayname}: all {len(filenames)} files are passing.',
flush=True)
print(
f'{Clr.GRN}{displayname}: all {len(filenames)}'
f' files are passing.{Clr.RST}',
flush=True)
def pycharm(projroot: Path, full: bool, verbose: bool) -> None:

View File

@ -28,7 +28,7 @@ import astroid
if TYPE_CHECKING:
from astroid import node_classes as nc
from typing import Set, Dict, Any
from typing import Set, Dict, Any, List
VERBOSE = False
@ -199,9 +199,49 @@ def var_annotations_filter(node: nc.NodeNG) -> nc.NodeNG:
return node
# Stripping subscripts on some generics seems to cause
# more harm than good, so we leave some intact.
ALLOWED_GENERICS = {'Sequence'}
def _is_strippable_subscript(node: nc.NodeNG) -> bool:
if isinstance(node, astroid.Subscript):
# We can strip if its not in our allowed list.
if not (isinstance(node.value, astroid.Name)
and node.value.name in ALLOWED_GENERICS):
return True
return False
def class_generics_filter(node: nc.NodeNG) -> nc.NodeNG:
"""Filter generics subscripts out of class declarations."""
# First, quick-out if nothing here should be filtered.
found = False
for base in node.bases:
if _is_strippable_subscript(base):
found = True
if not found:
return node
# Now strip subscripts from base classes.
new_bases: List[nc.NodeNG] = []
for base in node.bases:
if _is_strippable_subscript(base):
new_bases.append(base.value)
base.value.parent = node
else:
new_bases.append(base)
node.bases = new_bases
return node
def register_plugins(manager: astroid.Manager) -> None:
"""Apply our transforms to a given astroid manager object."""
# Hmm; is this still necessary?
if VERBOSE:
manager.register_failed_import_hook(failed_import_hook)
@ -210,16 +250,30 @@ def register_plugins(manager: astroid.Manager) -> None:
# check code as if it doesn't exist at all.
manager.register_transform(astroid.If, ignore_type_check_filter)
# We use 'reveal_type()' quite often, which tells mypy to print
# the type of an expression. Let's ignore it in Pylint's eyes so
# we don't see an ugly error there.
manager.register_transform(astroid.Call, ignore_reveal_type_call)
# Annotations on variables within a function are defer-eval'ed
# in some cases, so lets replace them with simple strings in those
# cases to avoid type complaints.
# (mypy will still properly alert us to type errors for them)
# We make use of 'from __future__ import annotations' which causes Python
# to receive annotations as strings, and also 'if TYPE_CHECKING:' blocks,
# which lets us do imports and whatnot that are limited to type-checking.
# Let's make Pylint understand these.
manager.register_transform(astroid.AnnAssign, var_annotations_filter)
manager.register_transform(astroid.FunctionDef, func_annotations_filter)
manager.register_transform(astroid.AsyncFunctionDef,
func_annotations_filter)
# Pylint doesn't seem to support Generics much right now, and it seems
# to lead to some buggy behavior and slowdowns. So let's filter them
# out. So instead of this:
# class MyClass(MyType[T]):
# Pylint will see this:
# class MyClass(MyType):
# I've opened a github issue related to the problems I was hitting,
# so we can revisit the need for this if that gets resolved.
# https://github.com/PyCQA/pylint/issues/3605
manager.register_transform(astroid.ClassDef, class_generics_filter)
register_plugins(astroid.MANAGER)

View File

@ -54,7 +54,7 @@ def snippets_main(globs: Dict[str, Any]) -> None:
show_help = False
retval = 0
if len(sys.argv) < 2:
print(f'{Clr.SRED}ERROR: command expected.{Clr.RST}')
print(f'{Clr.RED}ERROR: command expected.{Clr.RST}')
show_help = True
retval = 255
else:
@ -67,13 +67,17 @@ def snippets_main(globs: Dict[str, Any]) -> None:
else:
docs = _trim_docstring(
getattr(funcs[sys.argv[2]], '__doc__', '<no docs>'))
print('\nsnippets ' + sys.argv[2] + ':\n' + docs + '\n')
print(f'\n{Clr.MAG}{Clr.BLD}snippets {sys.argv[2]}:{Clr.RST}\n'
f'{Clr.MAG}{docs}{Clr.RST}\n')
elif sys.argv[1] in funcs:
try:
funcs[sys.argv[1]]()
except KeyboardInterrupt as exc:
print(f'{Clr.RED}{exc}{Clr.RST}')
sys.exit(1)
except CleanError as exc:
exc.pretty_print()
sys.exit(-1)
sys.exit(1)
else:
print('Unknown snippets command: "' + sys.argv[1] + '"',
file=sys.stderr)
@ -82,11 +86,12 @@ def snippets_main(globs: Dict[str, Any]) -> None:
if show_help:
print('Snippets contains project related commands too small'
' to warrant full scripts.')
print("Run 'snippets help <COMMAND>' for full command documentation.")
print(f"Run {Clr.MAG}'snippets help {Clr.BLD}<COMMAND>'"
f'{Clr.RST} for full command documentation.')
print('Available commands:')
for func, obj in sorted(funcs.items()):
doc = getattr(obj, '__doc__', '').splitlines()[0].strip()
print(f'{Clr.SMAG}{func}{Clr.SBLU} - {doc}{Clr.RST}')
print(f'{Clr.MAG}{func}{Clr.BLU} - {doc}{Clr.RST}')
sys.exit(retval)
@ -194,16 +199,16 @@ def check_clean_safety() -> None:
def formatcode() -> None:
"""Run clang-format on all of our source code (multithreaded)."""
from efrotools import code
import efrotools.code
full = '-full' in sys.argv
code.formatcode(PROJROOT, full)
efrotools.code.formatcode(PROJROOT, full)
def formatscripts() -> None:
"""Run yapf on all our scripts (multithreaded)."""
from efrotools import code
import efrotools.code
full = '-full' in sys.argv
code.formatscripts(PROJROOT, full)
efrotools.code.formatscripts(PROJROOT, full)
def formatmakefile() -> None:
@ -222,9 +227,9 @@ def formatmakefile() -> None:
def cpplint() -> None:
"""Run lint-checking on all code deemed lint-able."""
from efrotools import code
import efrotools.code
full = '-full' in sys.argv
code.cpplint(PROJROOT, full)
efrotools.code.cpplint(PROJROOT, full)
def scriptfiles() -> None:
@ -232,8 +237,8 @@ def scriptfiles() -> None:
Pass -lines to use newlines as separators. The default is spaces.
"""
from efrotools import code
paths = code.get_script_filenames(projroot=PROJROOT)
import efrotools.code
paths = efrotools.code.get_script_filenames(projroot=PROJROOT)
assert not any(' ' in path for path in paths)
if '-lines' in sys.argv:
print('\n'.join(paths))
@ -243,101 +248,102 @@ def scriptfiles() -> None:
def pylint() -> None:
"""Run pylint checks on our scripts."""
from efrotools import code
from efro.error import CleanError
import efrotools.code
full = ('-full' in sys.argv)
fast = ('-fast' in sys.argv)
code.pylint(PROJROOT, full, fast)
try:
efrotools.code.pylint(PROJROOT, full, fast)
except Exception:
raise CleanError('Pylint failed.')
def runpylint() -> None:
"""Run pylint checks on provided filenames."""
import os
from efro.terminal import Clr
from efro.error import CleanError
import efrotools.code
if len(sys.argv) < 3:
raise CleanError('Expected at least 1 filename arg.')
filenames = sys.argv[2:]
try:
efrotools.code.runpylint(PROJROOT, filenames)
print(f'{Clr.GRN}Pylint Passed.{Clr.RST}')
except Exception:
if os.environ.get('VERBOSE') == '1':
import traceback
traceback.print_exc()
raise CleanError('Pylint Failed.')
def mypy() -> None:
"""Run mypy checks on our scripts."""
from efrotools import code
import efrotools.code
full = ('-full' in sys.argv)
code.mypy(PROJROOT, full)
efrotools.code.mypy(PROJROOT, full)
def runmypy() -> None:
"""Run mypy checks on provided filenames."""
from efro.terminal import Clr
from efro.error import CleanError
import efrotools.code
if len(sys.argv) < 3:
raise CleanError('Expected at least 1 filename arg.')
filenames = sys.argv[2:]
try:
efrotools.code.runmypy(PROJROOT, filenames)
print(f'{Clr.GRN}Mypy Passed.{Clr.RST}')
except Exception:
raise CleanError('Mypy Failed.')
def dmypy() -> None:
"""Run mypy checks on our scripts using the mypy daemon."""
from efrotools import code
code.dmypy(PROJROOT)
import efrotools.code
efrotools.code.dmypy(PROJROOT)
def pycharm() -> None:
"""Run PyCharm checks on our scripts."""
from efrotools import code
import efrotools.code
full = '-full' in sys.argv
verbose = '-v' in sys.argv
code.pycharm(PROJROOT, full, verbose)
efrotools.code.pycharm(PROJROOT, full, verbose)
def clioncode() -> None:
"""Run CLion checks on our code."""
from efrotools import code
import efrotools.code
full = '-full' in sys.argv
verbose = '-v' in sys.argv
code.clioncode(PROJROOT, full, verbose)
efrotools.code.clioncode(PROJROOT, full, verbose)
def androidstudiocode() -> None:
"""Run Android Studio checks on our code."""
from efrotools import code
import efrotools.code
full = '-full' in sys.argv
verbose = '-v' in sys.argv
code.androidstudiocode(PROJROOT, full, verbose)
efrotools.code.androidstudiocode(PROJROOT, full, verbose)
def tool_config_install() -> None:
"""Install a tool config file (with some filtering)."""
from efrotools import get_config
import textwrap
from efro.terminal import Clr
if len(sys.argv) != 4:
raise Exception('expected 2 args')
src = Path(sys.argv[2])
dst = Path(sys.argv[3])
print(f'Creating tool config: {Clr.BLD}{dst}{Clr.RST}')
with src.open() as infile:
cfg = infile.read()
# Do a bit of filtering.
# Stick project-root wherever they want.
cfg = cfg.replace('__EFRO_PROJECT_ROOT__', str(PROJROOT))
# Short project name.
short_names = {'ballistica-internal': 'ba-int', 'ballistica': 'ba'}
shortname = short_names.get(PROJROOT.name, PROJROOT.name)
cfg = cfg.replace('__EFRO_PROJECT_SHORTNAME__', shortname)
stdsettings = textwrap.dedent("""
# We don't want all of our plain scripts complaining
# about __main__ being redefined.
scripts_are_modules = True
# Try to be as strict as we can about using types everywhere.
warn_unused_ignores = True
warn_return_any = True
warn_redundant_casts = True
warn_unreachable=True
disallow_incomplete_defs = True
disallow_untyped_defs = True
disallow_untyped_decorators = True
disallow_untyped_calls = True
disallow_any_unimported = True
strict_equality = True
""").strip()
cfg = cfg.replace('__EFRO_MYPY_STANDARD_SETTINGS__', stdsettings)
# Gen a pylint init to set up our python paths:
pylint_init_tag = '__EFRO_PYLINT_INIT__'
if pylint_init_tag in cfg:
pypaths = get_config(PROJROOT).get('python_paths')
if pypaths is None:
raise RuntimeError('python_paths not set in project config')
cstr = "init-hook='import sys;"
for path in pypaths:
cstr += f" sys.path.append('{PROJROOT}/{path}');"
cstr += "'"
cfg = cfg.replace(pylint_init_tag, cstr)
# Rome substitutions, etc.
cfg = _filter_tool_config(cfg)
# Add an auto-generated notice.
comment = None
@ -356,6 +362,57 @@ def tool_config_install() -> None:
outfile.write(cfg)
def _filter_tool_config(cfg: str) -> str:
import textwrap
from efrotools import get_config
# Stick project-root wherever they want.
cfg = cfg.replace('__EFRO_PROJECT_ROOT__', str(PROJROOT))
# Short project name.
short_names = {'ballistica-internal': 'ba-int', 'ballistica': 'ba'}
shortname = short_names.get(PROJROOT.name, PROJROOT.name)
cfg = cfg.replace('__EFRO_PROJECT_SHORTNAME__', shortname)
mypy_standard_settings = textwrap.dedent("""
# We don't want all of our plain scripts complaining
# about __main__ being redefined.
scripts_are_modules = True
# Try to be as strict as we can about using types everywhere.
warn_unused_ignores = True
warn_return_any = True
warn_redundant_casts = True
warn_unreachable = True
warn_unused_configs = True
disallow_incomplete_defs = True
disallow_untyped_defs = True
disallow_untyped_decorators = True
disallow_untyped_calls = True
disallow_any_unimported = True
disallow_subclassing_any = True
strict_equality = True
local_partial_types = True
no_implicit_reexport = True
""").strip()
cfg = cfg.replace('__EFRO_MYPY_STANDARD_SETTINGS__',
mypy_standard_settings)
# Gen a pylint init to set up our python paths:
pylint_init_tag = '__EFRO_PYLINT_INIT__'
if pylint_init_tag in cfg:
pypaths = get_config(PROJROOT).get('python_paths')
if pypaths is None:
raise RuntimeError('python_paths not set in project config')
cstr = "init-hook='import sys;"
for path in pypaths:
cstr += f" sys.path.append('{PROJROOT}/{path}');"
cstr += "'"
cfg = cfg.replace(pylint_init_tag, cstr)
return cfg
def sync_all() -> None:
"""Runs full syncs between all efrotools projects.
@ -368,7 +425,7 @@ def sync_all() -> None:
import concurrent.futures
from efro.error import CleanError
from efro.terminal import Clr
print(f'{Clr.SBLU}Updating formatting for all projects...{Clr.RST}')
print(f'{Clr.BLU}Updating formatting for all projects...{Clr.RST}')
projects_str = os.environ.get('EFROTOOLS_SYNC_PROJECTS')
if projects_str is None:
raise CleanError('EFROTOOL_SYNC_PROJECTS is not defined.')
@ -399,17 +456,17 @@ def sync_all() -> None:
# Real mode
for i in range(2):
if i == 0:
print(Clr.SBLU + 'Running sync pass 1:'
print(Clr.BLU + 'Running sync pass 1:'
' (ensures all changes at dsts are pushed to src)' +
Clr.RST)
else:
print(Clr.SBLU + 'Running sync pass 2:'
print(Clr.BLU + 'Running sync pass 2:'
' (ensures latest src is pulled to all dsts)' + Clr.RST)
for project in projects_str.split(':'):
cmd = f'cd "{project}" && make sync-full'
print(cmd)
subprocess.run(cmd, shell=True, check=True)
print(Clr.SBLU + 'Sync-all successful!' + Clr.RST)
print(Clr.BLU + 'Sync-all successful!' + Clr.RST)
def sync() -> None:
@ -530,5 +587,26 @@ def makefile_target_list() -> None:
continue
print('\n' + entry.title + '\n' + '-' * len(entry.title))
elif entry.kind == 'target':
print(Clr.SMAG + entry.title + Clr.SBLU +
print(Clr.MAG + entry.title + Clr.BLU +
_docstr(lines, entry.line) + Clr.RST)
def echo() -> None:
"""Echo with support for efro.terminal.Clr args (RED, GRN, BLU, etc).
Prints a Clr.RST at the end so that can be omitted.
"""
from efro.terminal import Clr
clrnames = {n for n in dir(Clr) if n.isupper() and not n.startswith('_')}
first = True
out: List[str] = []
for arg in sys.argv[2:]:
if arg in clrnames:
out.append(getattr(Clr, arg))
else:
if not first:
out.append(' ')
first = False
out.append(arg)
out.append(Clr.RST)
print(''.join(out))

View File

@ -40,9 +40,10 @@ from typing import TYPE_CHECKING
# noinspection PyUnresolvedReferences
from efrotools.snippets import (
PROJROOT, snippets_main, formatcode, formatscripts, formatmakefile,
cpplint, pylint, mypy, dmypy, tool_config_install, sync, sync_all,
scriptfiles, pycharm, clioncode, androidstudiocode, makefile_target_list,
spelling, spelling_all, compile_python_files, pytest)
cpplint, pylint, runpylint, mypy, runmypy, dmypy, tool_config_install,
sync, sync_all, scriptfiles, pycharm, clioncode, androidstudiocode,
makefile_target_list, spelling, spelling_all, compile_python_files, pytest,
echo)
# pylint: enable=unused-import
if TYPE_CHECKING:
@ -259,7 +260,7 @@ def clean_orphaned_assets() -> None:
# Operate from dist root..
os.chdir(PROJROOT)
# Our manifest is split into 2 files (public and non-public)
# Our manifest is split into 2 files (public and private)
with open('assets/.asset_manifest_public.json') as infile:
manifest = set(json.loads(infile.read()))
with open('assets/.asset_manifest_private.json') as infile:
@ -303,7 +304,8 @@ def py_examine() -> None:
sys.path.append(scriptsdir)
if toolsdir not in sys.path:
sys.path.append(toolsdir)
efrotools.py_examine(filename, line, column, selection, operation)
efrotools.py_examine(PROJROOT, filename, line, column, selection,
operation)
def push_ipa() -> None:
@ -553,6 +555,9 @@ def lazy_increment_build() -> None:
"""Increment build number only if C++ sources have changed.
This is convenient to place in automatic commit/push scripts.
It could make sense to auto update build number when scripts/assets
change too, but a build number change requires rebuilding all binaries
so I'll leave that as an explicit choice to save work.
"""
import os
import subprocess

View File

@ -133,9 +133,9 @@ class App:
self._update_docs_md()
if self._check:
print('Check-Builds: Everything up to date.')
print(f'{Clr.BLU}Check-Builds: Everything up to date.{Clr.RST}')
else:
print('Update-Builds: SUCCESS!')
print(f'{Clr.GRN}Update-Project: SUCCESS!{Clr.RST}')
def _update_dummy_module(self) -> None:
# Update our dummy _ba module.
@ -144,8 +144,8 @@ class App:
# been updated.
if os.path.exists('tools/gendummymodule.py'):
if os.system('tools/gendummymodule.py' + self._checkarg) != 0:
print(Clr.SRED + 'Error checking/updating dummy module' +
Clr.RST)
print(
f'{Clr.RED}Error checking/updating dummy module{Clr.RST}')
sys.exit(255)
def _update_docs_md(self) -> None:
@ -157,8 +157,8 @@ class App:
if os.path.exists('tools/gendocs.py'):
if os.system('tools/snippets update_docs_md' +
self._checkarg) != 0:
print(Clr.SRED + 'Error checking/updating docs markdown.' +
Clr.RST)
print(f'{Clr.RED}Error checking/updating'
f' docs markdown.{Clr.RST}')
sys.exit(255)
def _update_compile_commands_file(self) -> None:
@ -166,7 +166,7 @@ class App:
# our cmake stuff. Do this at end so cmake changes already happened.
if not self._check and os.path.exists('ballisticacore-cmake'):
if os.system('make .irony/compile_commands.json') != 0:
print(Clr.SRED + 'Error updating compile-commands.' + Clr.RST)
print(f'{Clr.RED}Error updating compile-commands.{Clr.RST}')
sys.exit(255)
def _apply_file_changes(self) -> None:
@ -184,11 +184,11 @@ class App:
unchanged_project_count += 1
else:
if self._check:
print(f'{Clr.SRED}ERROR: found out-of-date'
print(f'{Clr.RED}ERROR: found out-of-date'
f' project file: {fname}{Clr.RST}')
sys.exit(255)
print(f'{Clr.SBLU}Writing project file: {fname}{Clr.RST}')
print(f'{Clr.BLU}Writing project file: {fname}{Clr.RST}')
with open(fname, 'w') as outfile:
outfile.write(fcode)
if unchanged_project_count > 0:
@ -211,16 +211,16 @@ class App:
# If there are any manual-only entries, list then and bail.
# (Don't wanna allow auto-apply unless it fixes everything)
if manual_changes:
print(f'{Clr.SRED}Found erroneous lines '
print(f'{Clr.RED}Found erroneous lines '
f'requiring manual correction:{Clr.RST}')
for change in manual_changes:
print(
f'{Clr.SRED}{change[0]}:{change[1].line_number + 1}:'
f'{Clr.RED}{change[0]}:{change[1].line_number + 1}:'
f' Expected line to be:\n {change[1].expected}{Clr.RST}')
# Make a note on copyright lines that this can be disabled.
if 'Copyright' in change[1].expected:
print(f'{Clr.SRED}NOTE: You can disable copyright'
print(f'{Clr.RED}NOTE: You can disable copyright'
f' checks by adding "copyright_checks": false\n'
f'to the root dict in config/localconfig.json.\n'
f'see https://github.com/efroemling/ballistica/wiki'
@ -232,22 +232,21 @@ class App:
if auto_changes:
if not self._fix:
for i, change in enumerate(auto_changes):
print(f'{Clr.SRED}#{i}: {change[0]}:{Clr.RST}')
print(f'{Clr.RED}#{i}: {change[0]}:{Clr.RST}')
print(
f'{Clr.SRED} Expected "{change[1].expected}"{Clr.RST}'
)
f'{Clr.RED} Expected "{change[1].expected}"{Clr.RST}')
with open(change[0]) as infile:
lines = infile.read().splitlines()
line = lines[change[1].line_number]
print(f'{Clr.SRED} Found "{line}"{Clr.RST}')
print(Clr.SRED +
print(f'{Clr.RED} Found "{line}"{Clr.RST}')
print(Clr.RED +
f'All {len(auto_changes)} errors are auto-fixable;'
' run tools/update_project --fix to apply corrections.' +
Clr.RST)
sys.exit(255)
else:
for i, change in enumerate(auto_changes):
print(f'{Clr.SBLU}Correcting file: {change[0]}{Clr.RST}')
print(f'{Clr.BLU}Correcting file: {change[0]}{Clr.RST}')
with open(change[0]) as infile:
lines = infile.read().splitlines()
lines[change[1].line_number] = change[1].expected
@ -269,7 +268,7 @@ class App:
# Could just ignore these but it probably means I intended
# to save something and forgot.
if '/.#' in fsrc:
print(f'{Clr.SRED}'
print(f'{Clr.RED}'
f'ERROR: Found an unsaved emacs file: "{fsrc}"'
f'{Clr.RST}')
sys.exit(255)
@ -393,7 +392,7 @@ class App:
'tools/devtool', 'tools/version_utils', 'tools/vmshell'
]:
if not contents.startswith('#!/usr/bin/env python3.7'):
print(f'{Clr.SRED}Incorrect shebang (first line) for '
print(f'{Clr.RED}Incorrect shebang (first line) for '
f'{fname}.{Clr.RST}')
sys.exit(255)
else:
@ -482,7 +481,7 @@ class App:
if ('__pycache__' not in root
and os.path.basename(root) != '.vscode'):
if '__init__.py' not in files:
print(Clr.SRED +
print(Clr.RED +
'Error: no __init__.py in package dir: ' + root +
Clr.RST)
sys.exit(255)
@ -630,14 +629,14 @@ class App:
# Make sure none of our sync targets have been mucked with since
# their last sync.
if os.system('tools/snippets sync check') != 0:
print(Clr.SRED + 'Sync check failed; you may need to run "sync".' +
print(Clr.RED + 'Sync check failed; you may need to run "sync".' +
Clr.RST)
sys.exit(255)
def _update_assets_makefile(self) -> None:
if os.path.exists('tools/update_assets_makefile'):
if os.system('tools/update_assets_makefile' + self._checkarg) != 0:
print(Clr.SRED + 'Error checking/updating assets Makefile' +
print(Clr.RED + 'Error checking/updating assets Makefile' +
Clr.RST)
sys.exit(255)
@ -645,7 +644,7 @@ class App:
if os.path.exists('tools/update_generated_code_makefile'):
if os.system('tools/update_generated_code_makefile' +
self._checkarg) != 0:
print(Clr.SRED +
print(Clr.RED +
'Error checking/updating generated-code Makefile' +
Clr.RST)
sys.exit(255)
@ -654,7 +653,7 @@ class App:
if os.path.exists('tools/update_resources_makefile'):
if os.system('tools/update_resources_makefile' +
self._checkarg) != 0:
print(Clr.SRED + 'Error checking/updating resources Makefile' +
print(Clr.RED + 'Error checking/updating resources Makefile' +
Clr.RST)
sys.exit(255)
@ -662,8 +661,8 @@ class App:
if os.path.exists('tools/update_python_enums_module'):
if os.system('tools/update_python_enums_module' +
self._checkarg) != 0:
print(Clr.SRED +
'Error checking/updating python enums module' + Clr.RST)
print(Clr.RED + 'Error checking/updating python enums module' +
Clr.RST)
sys.exit(255)