From 1ccf33e41dc72835aa4520739b8ab40829c0e0d8 Mon Sep 17 00:00:00 2001 From: Eric Froemling Date: Mon, 18 May 2020 01:47:52 -0700 Subject: [PATCH] Added per-game team and player types --- .idea/dictionaries/ericf.xml | 19 + CHANGELOG.md | 3 + Makefile | 39 +- assets/src/ba_data/python/_ba.py | 116 ++- assets/src/ba_data/python/ba/__init__.py | 21 +- assets/src/ba_data/python/ba/_activity.py | 692 +++++++++------ .../src/ba_data/python/ba/_activitytypes.py | 11 +- assets/src/ba_data/python/ba/_actor.py | 34 +- assets/src/ba_data/python/ba/_benchmark.py | 2 +- assets/src/ba_data/python/ba/_coopgame.py | 9 +- assets/src/ba_data/python/ba/_coopsession.py | 8 +- assets/src/ba_data/python/ba/_error.py | 24 +- assets/src/ba_data/python/ba/_gameactivity.py | 25 +- assets/src/ba_data/python/ba/_gameresults.py | 45 +- assets/src/ba_data/python/ba/_general.py | 2 +- assets/src/ba_data/python/ba/_hooks.py | 12 +- assets/src/ba_data/python/ba/_lobby.py | 49 +- assets/src/ba_data/python/ba/_messages.py | 10 + .../ba_data/python/ba/_multiteamsession.py | 12 +- assets/src/ba_data/python/ba/_netutils.py | 2 +- assets/src/ba_data/python/ba/_player.py | 183 +++- assets/src/ba_data/python/ba/_session.py | 519 ++++------- assets/src/ba_data/python/ba/_stats.py | 71 +- assets/src/ba_data/python/ba/_team.py | 99 ++- assets/src/ba_data/python/ba/_teamgame.py | 14 +- .../python/bastd/activity/coopscore.py | 6 +- .../python/bastd/activity/dualteamscore.py | 13 +- .../python/bastd/activity/multiteamscore.py | 12 +- .../python/bastd/activity/multiteamvictory.py | 4 +- .../ba_data/python/bastd/actor/playerspaz.py | 51 +- .../ba_data/python/bastd/actor/respawnicon.py | 2 +- .../ba_data/python/bastd/actor/scoreboard.py | 28 +- .../src/ba_data/python/bastd/actor/spazbot.py | 2 +- .../src/ba_data/python/bastd/game/assault.py | 70 +- .../python/bastd/game/capturetheflag.py | 275 +++--- .../ba_data/python/bastd/game/chosenone.py | 4 +- .../src/ba_data/python/bastd/game/conquest.py | 4 +- .../ba_data/python/bastd/game/deathmatch.py | 4 +- .../python/bastd/game/easteregghunt.py | 4 +- .../ba_data/python/bastd/game/elimination.py | 11 +- .../src/ba_data/python/bastd/game/football.py | 22 +- .../src/ba_data/python/bastd/game/hockey.py | 26 +- .../src/ba_data/python/bastd/game/keepaway.py | 4 +- .../python/bastd/game/kingofthehill.py | 4 +- .../ba_data/python/bastd/game/meteorshower.py | 59 +- .../ba_data/python/bastd/game/ninjafight.py | 4 +- .../ba_data/python/bastd/game/onslaught.py | 4 +- assets/src/ba_data/python/bastd/game/race.py | 4 +- .../ba_data/python/bastd/game/runaround.py | 6 +- .../python/bastd/game/targetpractice.py | 4 +- .../ba_data/python/bastd/game/thelaststand.py | 4 +- assets/src/ba_data/python/bastd/mainmenu.py | 10 +- assets/src/ba_data/python/bastd/tutorial.py | 2 +- assets/src/server/ballisticacore_server.py | 25 +- config/toolconfigsrc/mypy.ini | 15 + config/toolconfigsrc/pylintrc | 1 + docs/ba_module.md | 816 +++++++++++------- tools/bacloud | 4 +- tools/batools/build.py | 6 +- tools/efro/entity/_field.py | 6 +- tools/efro/entity/_value.py | 4 +- tools/efro/util.py | 18 +- tools/efrotools/__init__.py | 4 +- tools/efrotools/code.py | 173 ++-- tools/efrotools/pylintplugins.py | 64 +- tools/efrotools/snippets.py | 224 +++-- tools/snippets | 15 +- tools/update_project | 53 +- 68 files changed, 2383 insertions(+), 1709 deletions(-) diff --git a/.idea/dictionaries/ericf.xml b/.idea/dictionaries/ericf.xml index 4e45609f..78083559 100644 --- a/.idea/dictionaries/ericf.xml +++ b/.idea/dictionaries/ericf.xml @@ -319,6 +319,7 @@ cnode codecsmodule codefilenames + codefiles codehash codeop collapsable @@ -727,8 +728,10 @@ gamepads gamepadselect gameplay + gameplayer gameport gameresults + gameteam gametype gametypes gameutils @@ -762,6 +765,7 @@ getsession getsockname getsound + getspaz getstarttime gettext gettexture @@ -893,6 +897,7 @@ inputfiles inputhash inputnode + inputtype inpututils inspectdir insta @@ -1319,6 +1324,7 @@ pipname pkey pkgutil + playercast playerdata playerlostspaz playernode @@ -1326,6 +1332,8 @@ playerpts playerrec playerspaz + playerteamdata + playertype playerval playlistui playmode @@ -1358,6 +1366,7 @@ positionadjusted posixpath posixsubprocess + postinit poststr powerdown powersgiven @@ -1368,6 +1377,7 @@ poweruptype powervr ppos + pproxy pragmas prch prec @@ -1547,6 +1557,7 @@ runmypy runonly runpy + runpylint runswindows rval safecolor @@ -1610,6 +1621,8 @@ sessiondata sessionglobals sessionname + sessionplayer + sessionteam sessiontype setalpha setbuild @@ -1685,6 +1698,7 @@ srcnode srcpath srcpathfull + srcplayer srcpy srcpydata srcstr @@ -1720,6 +1734,7 @@ strftime stringprep stringptr + strippable strobing strptime strt @@ -1785,11 +1800,13 @@ tdelay tdval teambasesession + teamdata teamgame teamnamescolors teamsize teamsscorescreen teamssession + teamtype teeeeeeeesssssttttdddddddd teehee teleporting @@ -1867,11 +1884,13 @@ toplevel totaldudes totalpts + totype touchpad tournamententry tournamentscores tplayer tpos + tproxy tracebacks tracemalloc tradeoff diff --git a/CHANGELOG.md b/CHANGELOG.md index b0c46787..424485c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/Makefile b/Makefile index 99a72509..28aca96d 100644 --- a/Makefile +++ b/Makefile @@ -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} diff --git a/assets/src/ba_data/python/_ba.py b/assets/src/ba_data/python/_ba.py index e73d0521..e9207771 100644 --- a/assets/src/ba_data/python/_ba.py +++ b/assets/src/ba_data/python/_ba.py @@ -34,7 +34,7 @@ NOTE: This file was autogenerated by gendummymodule; do not edit by hand. """ # (hash we can use to see if this file is out of date) -# SOURCES_HASH=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], diff --git a/assets/src/ba_data/python/ba/__init__.py b/assets/src/ba_data/python/ba/__init__.py index 0277bf92..1c5d157c 100644 --- a/assets/src/ba_data/python/ba/__init__.py +++ b/assets/src/ba_data/python/ba/__init__.py @@ -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, diff --git a/assets/src/ba_data/python/ba/_activity.py b/assets/src/ba_data/python/ba/_activity.py index 904fe28e..4098788f 100644 --- a/assets/src/ba_data/python/ba/_activity.py +++ b/assets/src/ba_data/python/ba/_activity.py @@ -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) diff --git a/assets/src/ba_data/python/ba/_activitytypes.py b/assets/src/ba_data/python/ba/_activitytypes.py index 9e53b6a6..20986457 100644 --- a/assets/src/ba_data/python/ba/_activitytypes.py +++ b/assets/src/ba_data/python/ba/_activitytypes.py @@ -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. diff --git a/assets/src/ba_data/python/ba/_actor.py b/assets/src/ba_data/python/ba/_actor.py index d45ea744..cfffb64b 100644 --- a/assets/src/ba_data/python/ba/_actor.py +++ b/assets/src/ba_data/python/ba/_actor.py @@ -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 diff --git a/assets/src/ba_data/python/ba/_benchmark.py b/assets/src/ba_data/python/ba/_benchmark.py index e31ee809..ac9fb011 100644 --- a/assets/src/ba_data/python/ba/_benchmark.py +++ b/assets/src/ba_data/python/ba/_benchmark.py @@ -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') diff --git a/assets/src/ba_data/python/ba/_coopgame.py b/assets/src/ba_data/python/ba/_coopgame.py index bcad453d..89421b82 100644 --- a/assets/src/ba_data/python/ba/_coopgame.py +++ b/assets/src/ba_data/python/ba/_coopgame.py @@ -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.""" diff --git a/assets/src/ba_data/python/ba/_coopsession.py b/assets/src/ba_data/python/ba/_coopsession.py index bcaa1ceb..b6c008ec 100644 --- a/assets/src/ba_data/python/ba/_coopsession.py +++ b/assets/src/ba_data/python/ba/_coopsession.py @@ -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) diff --git a/assets/src/ba_data/python/ba/_error.py b/assets/src/ba_data/python/ba/_error.py index 5beb6936..2f54e07b 100644 --- a/assets/src/ba_data/python/ba/_error.py +++ b/assets/src/ba_data/python/ba/_error.py @@ -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. diff --git a/assets/src/ba_data/python/ba/_gameactivity.py b/assets/src/ba_data/python/ba/_gameactivity.py index b3131c4e..869d2eb2 100644 --- a/assets/src/ba_data/python/ba/_gameactivity.py +++ b/assets/src/ba_data/python/ba/_gameactivity.py @@ -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.""" diff --git a/assets/src/ba_data/python/ba/_gameresults.py b/assets/src/ba_data/python/ba/_gameresults.py index d4fe7147..9376cf69 100644 --- a/assets/src/ba_data/python/ba/_gameresults.py +++ b/assets/src/ba_data/python/ba/_gameresults.py @@ -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: diff --git a/assets/src/ba_data/python/ba/_general.py b/assets/src/ba_data/python/ba/_general.py index 73945d5d..ed61f77d 100644 --- a/assets/src/ba_data/python/ba/_general.py +++ b/assets/src/ba_data/python/ba/_general.py @@ -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') diff --git a/assets/src/ba_data/python/ba/_hooks.py b/assets/src/ba_data/python/ba/_hooks.py index fdafedd7..7c1d2c3c 100644 --- a/assets/src/ba_data/python/ba/_hooks.py +++ b/assets/src/ba_data/python/ba/_hooks.py @@ -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'] + } diff --git a/assets/src/ba_data/python/ba/_lobby.py b/assets/src/ba_data/python/ba/_lobby.py index adf525a2..198338e7 100644 --- a/assets/src/ba_data/python/ba/_lobby.py +++ b/assets/src/ba_data/python/ba/_lobby.py @@ -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 diff --git a/assets/src/ba_data/python/ba/_messages.py b/assets/src/ba_data/python/ba/_messages.py index 9ac55859..eb0fda5a 100644 --- a/assets/src/ba_data/python/ba/_messages.py +++ b/assets/src/ba_data/python/ba/_messages.py @@ -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. diff --git a/assets/src/ba_data/python/ba/_multiteamsession.py b/assets/src/ba_data/python/ba/_multiteamsession.py index e94815f2..84097208 100644 --- a/assets/src/ba_data/python/ba/_multiteamsession.py +++ b/assets/src/ba_data/python/ba/_multiteamsession.py @@ -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() diff --git a/assets/src/ba_data/python/ba/_netutils.py b/assets/src/ba_data/python/ba/_netutils.py index 1f290972..afeffd15 100644 --- a/assets/src/ba_data/python/ba/_netutils.py +++ b/assets/src/ba_data/python/ba/_netutils.py @@ -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, diff --git a/assets/src/ba_data/python/ba/_player.py b/assets/src/ba_data/python/ba/_player.py index 9d3100d1..2779717c 100644 --- a/assets/src/ba_data/python/ba/_player.py +++ b/assets/src/ba_data/python/ba/_player.py @@ -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 diff --git a/assets/src/ba_data/python/ba/_session.py b/assets/src/ba_data/python/ba/_session.py index 1a5c4a7d..dbb47fd8 100644 --- a/assets/src/ba_data/python/ba/_session.py +++ b/assets/src/ba_data/python/ba/_session.py @@ -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 diff --git a/assets/src/ba_data/python/ba/_stats.py b/assets/src/ba_data/python/ba/_stats.py index d62ac24f..7cfad6e9 100644 --- a/assets/src/ba_data/python/ba/_stats.py +++ b/assets/src/ba_data/python/ba/_stats.py @@ -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') diff --git a/assets/src/ba_data/python/ba/_team.py b/assets/src/ba_data/python/ba/_team.py index 31e2e9d1..7c30d959 100644 --- a/assets/src/ba_data/python/ba/_team.py +++ b/assets/src/ba_data/python/ba/_team.py @@ -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() diff --git a/assets/src/ba_data/python/ba/_teamgame.py b/assets/src/ba_data/python/ba/_teamgame.py index 7c13f6ab..ceb9b330 100644 --- a/assets/src/ba_data/python/ba/_teamgame.py +++ b/assets/src/ba_data/python/ba/_teamgame.py @@ -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) diff --git a/assets/src/ba_data/python/bastd/activity/coopscore.py b/assets/src/ba_data/python/bastd/activity/coopscore.py index 23604e2b..20e20d8e 100644 --- a/assets/src/ba_data/python/bastd/activity/coopscore.py +++ b/assets/src/ba_data/python/bastd/activity/coopscore.py @@ -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. diff --git a/assets/src/ba_data/python/bastd/activity/dualteamscore.py b/assets/src/ba_data/python/bastd/activity/dualteamscore.py index 408495cc..6fcb0305 100644 --- a/assets/src/ba_data/python/bastd/activity/dualteamscore.py +++ b/assets/src/ba_data/python/bastd/activity/dualteamscore.py @@ -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), diff --git a/assets/src/ba_data/python/bastd/activity/multiteamscore.py b/assets/src/ba_data/python/bastd/activity/multiteamscore.py index 1f5a9a47..b58d43dd 100644 --- a/assets/src/ba_data/python/bastd/activity/multiteamscore.py +++ b/assets/src/ba_data/python/bastd/activity/multiteamscore.py @@ -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 diff --git a/assets/src/ba_data/python/bastd/activity/multiteamvictory.py b/assets/src/ba_data/python/bastd/activity/multiteamvictory.py index 3a6aeaae..e4146da3 100644 --- a/assets/src/ba_data/python/bastd/activity/multiteamvictory.py +++ b/assets/src/ba_data/python/bastd/activity/multiteamvictory.py @@ -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: diff --git a/assets/src/ba_data/python/bastd/actor/playerspaz.py b/assets/src/ba_data/python/bastd/actor/playerspaz.py index 82a1c0b7..f8ca79f1 100644 --- a/assets/src/ba_data/python/bastd/actor/playerspaz.py +++ b/assets/src/ba_data/python/bastd/actor/playerspaz.py @@ -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. diff --git a/assets/src/ba_data/python/bastd/actor/respawnicon.py b/assets/src/ba_data/python/bastd/actor/respawnicon.py index a6d91b74..44e41e0b 100644 --- a/assets/src/ba_data/python/bastd/actor/respawnicon.py +++ b/assets/src/ba_data/python/bastd/actor/respawnicon.py @@ -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: diff --git a/assets/src/ba_data/python/bastd/actor/scoreboard.py b/assets/src/ba_data/python/bastd/actor/scoreboard.py index c229ca55..52ff65c8 100644 --- a/assets/src/ba_data/python/bastd/actor/scoreboard.py +++ b/assets/src/ba_data/python/bastd/actor/scoreboard.py @@ -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: diff --git a/assets/src/ba_data/python/bastd/actor/spazbot.py b/assets/src/ba_data/python/bastd/actor/spazbot.py index 1b5e4b8c..e62d0b97 100644 --- a/assets/src/ba_data/python/bastd/actor/spazbot.py +++ b/assets/src/ba_data/python/bastd/actor/spazbot.py @@ -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)): diff --git a/assets/src/ba_data/python/bastd/game/assault.py b/assets/src/ba_data/python/bastd/game/assault.py index 0c8aec67..092c9495 100644 --- a/assets/src/ba_data/python/bastd/game/assault.py +++ b/assets/src/ba_data/python/bastd/game/assault.py @@ -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, diff --git a/assets/src/ba_data/python/bastd/game/capturetheflag.py b/assets/src/ba_data/python/bastd/game/capturetheflag.py index 35e4a20a..09ce7be6 100644 --- a/assets/src/ba_data/python/bastd/game/capturetheflag.py +++ b/assets/src/ba_data/python/bastd/game/capturetheflag.py @@ -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)) diff --git a/assets/src/ba_data/python/bastd/game/chosenone.py b/assets/src/ba_data/python/bastd/game/chosenone.py index 47247396..65b41a4c 100644 --- a/assets/src/ba_data/python/bastd/game/chosenone.py +++ b/assets/src/ba_data/python/bastd/game/chosenone.py @@ -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 ( diff --git a/assets/src/ba_data/python/bastd/game/conquest.py b/assets/src/ba_data/python/bastd/game/conquest.py index d3ac9983..2ffac1a0 100644 --- a/assets/src/ba_data/python/bastd/game/conquest.py +++ b/assets/src/ba_data/python/bastd/game/conquest.py @@ -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: diff --git a/assets/src/ba_data/python/bastd/game/deathmatch.py b/assets/src/ba_data/python/bastd/game/deathmatch.py index b4798d82..3b5237c2 100644 --- a/assets/src/ba_data/python/bastd/game/deathmatch.py +++ b/assets/src/ba_data/python/bastd/game/deathmatch.py @@ -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 diff --git a/assets/src/ba_data/python/bastd/game/easteregghunt.py b/assets/src/ba_data/python/bastd/game/easteregghunt.py index 72259d2e..eebdef24 100644 --- a/assets/src/ba_data/python/bastd/game/easteregghunt.py +++ b/assets/src/ba_data/python/bastd/game/easteregghunt.py @@ -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) diff --git a/assets/src/ba_data/python/bastd/game/elimination.py b/assets/src/ba_data/python/bastd/game/elimination.py index 6dfe3006..047db791 100644 --- a/assets/src/ba_data/python/bastd/game/elimination.py +++ b/assets/src/ba_data/python/bastd/game/elimination.py @@ -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: diff --git a/assets/src/ba_data/python/bastd/game/football.py b/assets/src/ba_data/python/bastd/game/football.py index 96e4bba9..f5e630ca 100644 --- a/assets/src/ba_data/python/bastd/game/football.py +++ b/assets/src/ba_data/python/bastd/game/football.py @@ -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) diff --git a/assets/src/ba_data/python/bastd/game/hockey.py b/assets/src/ba_data/python/bastd/game/hockey.py index b530cfc8..daa2b354 100644 --- a/assets/src/ba_data/python/bastd/game/hockey.py +++ b/assets/src/ba_data/python/bastd/game/hockey.py @@ -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): diff --git a/assets/src/ba_data/python/bastd/game/keepaway.py b/assets/src/ba_data/python/bastd/game/keepaway.py index 2b17c606..2a8839e9 100644 --- a/assets/src/ba_data/python/bastd/game/keepaway.py +++ b/assets/src/ba_data/python/bastd/game/keepaway.py @@ -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( diff --git a/assets/src/ba_data/python/bastd/game/kingofthehill.py b/assets/src/ba_data/python/bastd/game/kingofthehill.py index 024589ba..3d0f5d8a 100644 --- a/assets/src/ba_data/python/bastd/game/kingofthehill.py +++ b/assets/src/ba_data/python/bastd/game/kingofthehill.py @@ -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) diff --git a/assets/src/ba_data/python/bastd/game/meteorshower.py b/assets/src/ba_data/python/bastd/game/meteorshower.py index c757cbc4..e87e6249 100644 --- a/assets/src/ba_data/python/bastd/game/meteorshower.py +++ b/assets/src/ba_data/python/bastd/game/meteorshower.py @@ -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)) diff --git a/assets/src/ba_data/python/bastd/game/ninjafight.py b/assets/src/ba_data/python/bastd/game/ninjafight.py index 2cf667b9..24a262b9 100644 --- a/assets/src/ba_data/python/bastd/game/ninjafight.py +++ b/assets/src/ba_data/python/bastd/game/ninjafight.py @@ -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): diff --git a/assets/src/ba_data/python/bastd/game/onslaught.py b/assets/src/ba_data/python/bastd/game/onslaught.py index 50648987..88d62efa 100644 --- a/assets/src/ba_data/python/bastd/game/onslaught.py +++ b/assets/src/ba_data/python/bastd/game/onslaught.py @@ -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 diff --git a/assets/src/ba_data/python/bastd/game/race.py b/assets/src/ba_data/python/bastd/game/race.py index 2fac27bb..89eb04aa 100644 --- a/assets/src/ba_data/python/bastd/game/race.py +++ b/assets/src/ba_data/python/bastd/game/race.py @@ -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 diff --git a/assets/src/ba_data/python/bastd/game/runaround.py b/assets/src/ba_data/python/bastd/game/runaround.py index 0ea9dc1c..5c6e2f06 100644 --- a/assets/src/ba_data/python/bastd/game/runaround.py +++ b/assets/src/ba_data/python/bastd/game/runaround.py @@ -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') diff --git a/assets/src/ba_data/python/bastd/game/targetpractice.py b/assets/src/ba_data/python/bastd/game/targetpractice.py index 61c2e0c0..ad3d1fce 100644 --- a/assets/src/ba_data/python/bastd/game/targetpractice.py +++ b/assets/src/ba_data/python/bastd/game/targetpractice.py @@ -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): diff --git a/assets/src/ba_data/python/bastd/game/thelaststand.py b/assets/src/ba_data/python/bastd/game/thelaststand.py index ff6a5b94..a59fbbec 100644 --- a/assets/src/ba_data/python/bastd/game/thelaststand.py +++ b/assets/src/ba_data/python/bastd/game/thelaststand.py @@ -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.') diff --git a/assets/src/ba_data/python/bastd/mainmenu.py b/assets/src/ba_data/python/bastd/mainmenu.py index ff81f237..ad071ec1 100644 --- a/assets/src/ba_data/python/bastd/mainmenu.py +++ b/assets/src/ba_data/python/bastd/mainmenu.py @@ -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 diff --git a/assets/src/ba_data/python/bastd/tutorial.py b/assets/src/ba_data/python/bastd/tutorial.py index ff912995..0aca2a2f 100644 --- a/assets/src/ba_data/python/bastd/tutorial.py +++ b/assets/src/ba_data/python/bastd/tutorial.py @@ -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 diff --git a/assets/src/server/ballisticacore_server.py b/assets/src/server/ballisticacore_server.py index 926043b5..969d5884 100755 --- a/assets/src/server/ballisticacore_server.py +++ b/assets/src/server/ballisticacore_server.py @@ -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. diff --git a/config/toolconfigsrc/mypy.ini b/config/toolconfigsrc/mypy.ini index 299b6668..249b3f67 100644 --- a/config/toolconfigsrc/mypy.ini +++ b/config/toolconfigsrc/mypy.ini @@ -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 diff --git a/config/toolconfigsrc/pylintrc b/config/toolconfigsrc/pylintrc index 8d17ef52..89e19ab4 100644 --- a/config/toolconfigsrc/pylintrc +++ b/config/toolconfigsrc/pylintrc @@ -79,6 +79,7 @@ good-names=i, v2, ex, Run, + id, T, S, U, diff --git a/docs/ba_module.md b/docs/ba_module.md index 992a54ea..9e6cd0d2 100644 --- a/docs/ba_module.md +++ b/docs/ba_module.md @@ -1,5 +1,5 @@ -

last updated on 2020-05-07 for Ballistica version 1.5.0 build 20018

+

last updated on 2020-05-18 for Ballistica version 1.5.0 build 20021

This page documents the Python classes and functions in the 'ba' module, which are the ones most relevant to modding in Ballistica. If you come across something you feel should be included here or could be better explained, please let me know. Happy modding!


@@ -19,14 +19,12 @@
  • ba.Map
  • ba.NodeActor
  • -
  • ba.BasePlayerData
  • ba.Chooser
  • ba.InputDevice
  • ba.Level
  • ba.Lobby
  • ba.Material
  • ba.Node
  • -
  • ba.Player
  • ba.PlayerRecord
  • ba.ScoreInfo
  • ba.Session
  • @@ -38,8 +36,9 @@
  • ba.FreeForAllSession
  • +
  • ba.SessionPlayer
  • +
  • ba.SessionTeam
  • ba.Stats
  • -
  • ba.Team
  • ba.TeamGameResults
  • Gameplay Functions

    @@ -54,6 +53,8 @@
  • ba.getnodes()
  • ba.getsession()
  • ba.newnode()
  • +
  • ba.playercast()
  • +
  • ba.playercast_o()
  • ba.playsound()
  • ba.printnodes()
  • ba.setmusic()
  • @@ -189,10 +190,17 @@
  • ba.NodeNotFoundError
  • ba.PlayerNotFoundError
  • ba.SessionNotFoundError
  • +
  • ba.SessionPlayerNotFoundError
  • +
  • ba.SessionTeamNotFoundError
  • ba.TeamNotFoundError
  • ba.WidgetNotFoundError
  • +

    Misc Classes

    +

    ba.Achievement

    <top level class> @@ -313,7 +321,7 @@ actually award achievements.


    ba.Activity

    -

    inherits from: ba.DependencyComponent

    +

    inherits from: ba.DependencyComponent, typing.Generic

    Units of execution wrangled by a ba.Session.

    Category: Gameplay Classes

    @@ -323,14 +331,28 @@ actually award achievements.

    can overlap during transitions.

    Attributes:

    -
    players, session, settings_raw, stats, teams
    +
    expired, players, playertype, session, settings_raw, stats, teams, teamtype
    +

    expired

    +

    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'.

    + +

    players

    -

    List[ba.Player]

    +

    List[PlayerType]

    The list of ba.Players in the Activity. This gets populated just before on_begin() is called and is updated automatically as players join or leave the game.

    +
    +

    playertype

    +

    Type[PlayerType]

    +

    The type of ba.Player this Activity is using.

    +

    session

    ba.Session

    @@ -355,24 +377,29 @@ passed to the Activity __init__ call.

    teams

    -

    List[ba.Team]

    +

    List[TeamType]

    The list of ba.Teams in the Activity. This gets populated just before before on_begin() is called and is updated automatically as players join or leave the game. (at least in free-for-all mode where every player gets their own team; in teams mode there are always 2 teams regardless of the player count).

    +
    +

    teamtype

    +

    Type[TeamType]

    +

    The type of ba.Team this Activity is using.

    +

    Methods Inherited:

    dep_is_present(), get_dynamic_deps()

    Methods Defined or Overridden:

    -
    <constructor>, add_actor_weak_ref(), create_player_node(), end(), handlemessage(), has_begun(), has_ended(), has_transitioned_in(), is_expired(), is_transitioning_out(), on_begin(), on_expire(), on_player_join(), on_player_leave(), on_team_join(), on_team_leave(), on_transition_in(), on_transition_out(), retain_actor()
    +
    <constructor>, add_actor_weak_ref(), create_player(), create_team(), end(), handlemessage(), has_begun(), has_ended(), has_transitioned_in(), is_transitioning_out(), on_begin(), on_expire(), on_player_join(), on_player_leave(), on_team_join(), on_team_leave(), on_transition_in(), on_transition_out(), retain_actor(), transition_out()

    <constructor>

    ba.Activity(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 @@ -391,10 +418,28 @@ are transitioned in.

    (called by the ba.Actor base class)

    -

    create_player_node()

    -

    create_player_node(self, player: ba.Player) -> ba.Node

    +

    create_player()

    +

    create_player(self, sessionplayer: ba.SessionPlayer) -> PlayerType

    -

    Create the 'player' node associated with the provided ba.Player.

    +

    Create the Player instance for this Activity.

    + +

    Subclasses can override this if the activity's player class +requires a custom constructor; otherwise it will be called with +no args. Note that the player object should not be used at this +point as it is not yet fully wired up; wait for on_player_join() +for that.

    + +
    +

    create_team()

    +

    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.

    end()

    @@ -431,16 +476,6 @@ will replace the old.

    Return whether on_transition_in() has been called.

    -
    -

    is_expired()

    -

    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'.

    -

    is_transitioning_out()

    is_transitioning_out(self) -> bool

    @@ -470,7 +505,7 @@ can begin.

    on_player_join()

    -

    on_player_join(self, player: ba.Player) -> None

    +

    on_player_join(self, player: PlayerType) -> None

    Called when a new ba.Player has joined the Activity.

    @@ -478,13 +513,13 @@ can begin.

    on_player_leave()

    -

    on_player_leave(self, player: ba.Player) -> None

    +

    on_player_leave(self, player: PlayerType) -> None

    Called when a ba.Player is leaving the Activity.

    on_team_join()

    -

    on_team_join(self, team: ba.Team) -> None

    +

    on_team_join(self, team: TeamType) -> None

    Called when a new ba.Team joins the Activity.

    @@ -492,7 +527,7 @@ can begin.

    on_team_leave()

    -

    on_team_leave(self, team: ba.Team) -> None

    +

    on_team_leave(self, team: TeamType) -> None

    Called when a ba.Team leaves the Activity.

    @@ -503,8 +538,8 @@ can begin.

    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.

    @@ -513,7 +548,7 @@ up until ba.Activity.on_begin() is c

    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.

    @@ -526,6 +561,12 @@ called.

    returns False for the Actor. The ba.Actor.autoretain() method is a convenient way to access this same functionality.

    + +

    transition_out()

    +

    transition_out(self) -> None

    + +

    Called by the Session to start us transitioning out.

    +

    @@ -588,6 +629,7 @@ is a convenient way to access this same functionality.

    Attributes:

    +
    activity, expired

    activity

    ba.Activity

    @@ -595,10 +637,17 @@ is a convenient way to access this same functionality.

    Raises a ba.ActivityNotFoundError if the Activity no longer exists.

    +
    +

    expired

    +

    bool

    +

    Whether the Actor is expired.

    + +

    (see ba.Actor.on_expire())

    +

    Methods:

    -
    <constructor>, autoretain(), exists(), getactivity(), handlemessage(), is_alive(), is_expired(), on_expire()
    +
    <constructor>, autoretain(), exists(), getactivity(), handlemessage(), is_alive(), on_expire()

    <constructor>

    ba.Actor()

    @@ -665,14 +714,6 @@ It is not a requirement for Actors to be able to die; just that they report whether they are Alive or not.

    -
    -

    is_expired()

    -

    is_expired(self) -> bool

    - -

    Returns whether the Actor is expired.

    - -

    (see ba.Actor.on_expire())

    -

    on_expire()

    on_expire(self) -> None

    @@ -684,7 +725,7 @@ or other references which have the potential of keeping the ba.Activity alive inadvertently (Activities can not exit cleanly while any Python references to them remain.)

    -

    Once an actor is expired (see ba.Actor.is_expired()) it should no +

    Once an actor is expired (see ba.Actor.is_expired()) it should no longer perform any game-affecting operations (creating, modifying, or deleting nodes, media, timers, etc.) Attempts to do so will likely result in errors.

    @@ -1086,31 +1127,6 @@ when done.

    Behavior is similar to ba.gettexture()

    -
    -
    -
    -

    ba.BasePlayerData

    -

    <top level class> -

    -

    Base class for custom player data.

    - -

    Category: Gameplay Classes

    - -

    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. -

    - -

    Methods:

    -
    -

    get()

    -
    <class method>
    -

    get(player: ba.Player) -> T

    - -

    Return the custom player data associated with a player.

    - -

    If one does not exist, it will be created.

    -

    @@ -1261,7 +1277,7 @@ mycall()

    player

    -

    ba.Player

    +

    ba.SessionPlayer

    The ba.Player associated with this chooser.

    @@ -1275,7 +1291,7 @@ mycall()
    <constructor>, get_character_name(), get_color(), get_highlight(), get_lobby(), get_team(), getplayer(), handlemessage(), reload_profiles(), update_from_player_profiles(), update_position()

    <constructor>

    -

    ba.Chooser(vpos: float, player: _ba.Player, lobby: "Lobby")

    +

    ba.Chooser(vpos: float, player: _ba.SessionPlayer, lobby: "Lobby")

    get_character_name()

    @@ -1303,13 +1319,13 @@ mycall()

    get_team()

    -

    get_team(self) -> ba.Team

    +

    get_team(self) -> ba.SessionTeam

    Return this chooser's selected ba.Team.

    getplayer()

    -

    getplayer(self) -> ba.Player

    +

    getplayer(self) -> ba.SessionPlayer

    Return the player associated with this chooser.

    @@ -1450,7 +1466,7 @@ start_long_action(callback_when_done=ba.ContextC

    ba.CoopGameActivity

    -

    inherits from: ba.GameActivity, ba.Activity, ba.DependencyComponent

    +

    inherits from: ba.GameActivity, ba.Activity, ba.DependencyComponent, typing.Generic

    Base class for cooperative-mode games.

    Category: Gameplay Classes @@ -1459,14 +1475,28 @@ start_long_action(callback_when_done=ba.ContextC

    Attributes Inherited:

    players, settings_raw, teams

    Attributes Defined Here:

    -
    map, session, stats
    +
    expired, map, playertype, session, stats, teamtype
    +

    expired

    +

    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'.

    + +

    map

    ba.Map

    The map being used for this game.

    Raises a ba.NotFoundError if the map does not currently exist.

    +
    +

    playertype

    +

    Type[PlayerType]

    +

    The type of ba.Player this Activity is using.

    +

    session

    ba.Session

    @@ -1481,10 +1511,15 @@ start_long_action(callback_when_done=ba.ContextC

    If access is attempted before or after, raises a ba.NotFoundError.

    +
    +

    teamtype

    +

    Type[TeamType]

    +

    The type of ba.Team this Activity is using.

    +

    Methods Inherited:

    -
    add_actor_weak_ref(), begin(), continue_or_end_game(), create_config_ui(), create_player_node(), dep_is_present(), end(), end_game(), get_config_display_string(), get_description(), get_description_display_string(), get_display_string(), get_dynamic_deps(), get_instance_description(), get_instance_display_string(), get_instance_scoreboard_description(), get_instance_scoreboard_display_string(), get_name(), get_score_info(), get_settings(), get_supported_maps(), get_team_display_string(), handlemessage(), has_begun(), has_ended(), has_transitioned_in(), is_expired(), is_transitioning_out(), is_waiting_for_continue(), on_continue(), on_expire(), on_player_join(), on_player_leave(), on_team_join(), on_team_leave(), on_transition_in(), on_transition_out(), project_flag_stand(), respawn_player(), retain_actor(), set_has_ended(), set_immediate_end(), setup_standard_powerup_drops(), setup_standard_time_limit(), show_info(), show_scoreboard_info(), show_zoom_message(), spawn_player(), spawn_player_if_exists(), start_transition_in()
    +
    add_actor_weak_ref(), add_player(), add_team(), begin(), continue_or_end_game(), create_config_ui(), create_player(), create_team(), dep_is_present(), destroy(), end(), end_game(), get_config_display_string(), get_description(), get_description_display_string(), get_display_string(), get_dynamic_deps(), get_instance_description(), get_instance_display_string(), get_instance_scoreboard_description(), get_instance_scoreboard_display_string(), get_name(), get_score_info(), get_settings(), get_supported_maps(), get_team_display_string(), handlemessage(), has_begun(), has_ended(), has_transitioned_in(), is_transitioning_out(), is_waiting_for_continue(), on_continue(), on_expire(), on_player_join(), on_player_leave(), on_team_join(), on_team_leave(), on_transition_in(), on_transition_out(), project_flag_stand(), remove_player(), remove_team(), respawn_player(), retain_actor(), set_has_ended(), set_immediate_end(), setup_standard_powerup_drops(), setup_standard_time_limit(), show_info(), show_scoreboard_info(), show_zoom_message(), spawn_player(), spawn_player_if_exists(), transition_in(), transition_out()

    Methods Defined or Overridden:

    <constructor>, celebrate(), fade_to_red(), get_score_type(), on_begin(), setup_low_life_warning_sound(), spawn_player_spaz(), supports_session_type()
    @@ -1532,7 +1567,7 @@ and it should begin its actual game logic.

    spawn_player_spaz()

    -

    spawn_player_spaz(self, player: ba.Player, position: Sequence[float] = (0.0, 0.0, 0.0), angle: float = None) -> PlayerSpaz

    +

    spawn_player_spaz(self, player: PlayerType, position: Sequence[float] = (0.0, 0.0, 0.0), angle: float = None) -> PlayerSpaz

    Spawn and wire up a standard player spaz.

    @@ -1596,9 +1631,9 @@ is pressed.

    on_player_leave()

    -

    on_player_leave(self, player: ba.Player) -> None

    +

    on_player_leave(self, sessionplayer: ba.SessionPlayer) -> None

    -

    Called when a previously-accepted ba.Player leaves the session.

    +

    Called when a previously-accepted ba.SessionPlayer leaves.

    restart()

    @@ -1953,7 +1988,7 @@ its time with lingering corpses, sound effects, etc.


    ba.GameActivity

    -

    inherits from: ba.Activity, ba.DependencyComponent

    +

    inherits from: ba.Activity, ba.DependencyComponent, typing.Generic

    Common base class for all game ba.Activities.

    Category: Gameplay Classes @@ -1962,14 +1997,28 @@ its time with lingering corpses, sound effects, etc.

    Attributes Inherited:

    players, settings_raw, teams

    Attributes Defined Here:

    -
    map, session, stats
    +
    expired, map, playertype, session, stats, teamtype
    +

    expired

    +

    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'.

    + +

    map

    ba.Map

    The map being used for this game.

    Raises a ba.NotFoundError if the map does not currently exist.

    +
    +

    playertype

    +

    Type[PlayerType]

    +

    The type of ba.Player this Activity is using.

    +

    session

    ba.Session

    @@ -1984,10 +2033,15 @@ its time with lingering corpses, sound effects, etc.

    If access is attempted before or after, raises a ba.NotFoundError.

    +
    +

    teamtype

    +

    Type[TeamType]

    +

    The type of ba.Team this Activity is using.

    +

    Methods Inherited:

    -
    add_actor_weak_ref(), begin(), create_player_node(), dep_is_present(), get_dynamic_deps(), has_begun(), has_ended(), has_transitioned_in(), is_expired(), is_transitioning_out(), on_expire(), on_team_join(), on_team_leave(), on_transition_out(), retain_actor(), set_has_ended(), set_immediate_end(), start_transition_in()
    +
    add_actor_weak_ref(), add_player(), add_team(), begin(), create_player(), create_team(), dep_is_present(), destroy(), get_dynamic_deps(), has_begun(), has_ended(), has_transitioned_in(), is_transitioning_out(), on_expire(), on_team_join(), on_team_leave(), on_transition_out(), remove_player(), remove_team(), retain_actor(), set_has_ended(), set_immediate_end(), transition_in(), transition_out()

    Methods Defined or Overridden:

    <constructor>, continue_or_end_game(), create_config_ui(), end(), end_game(), get_config_display_string(), get_description(), get_description_display_string(), get_display_string(), get_instance_description(), get_instance_display_string(), get_instance_scoreboard_description(), get_instance_scoreboard_display_string(), get_name(), get_score_info(), get_settings(), get_supported_maps(), get_team_display_string(), handlemessage(), is_waiting_for_continue(), on_begin(), on_continue(), on_player_join(), on_player_leave(), on_transition_in(), project_flag_stand(), respawn_player(), setup_standard_powerup_drops(), setup_standard_time_limit(), show_info(), show_scoreboard_info(), show_zoom_message(), spawn_player(), spawn_player_if_exists(), spawn_player_spaz(), supports_session_type()
    @@ -2274,7 +2328,7 @@ whatever is relevant to keep the game going.

    on_player_join()

    -

    on_player_join(self, player: ba.Player) -> None

    +

    on_player_join(self, player: PlayerType) -> None

    Called when a new ba.Player has joined the Activity.

    @@ -2282,7 +2336,7 @@ whatever is relevant to keep the game going.

    on_player_leave()

    -

    on_player_leave(self, player: ba.Player) -> None

    +

    on_player_leave(self, player: PlayerType) -> None

    Called when a ba.Player is leaving the Activity.

    @@ -2293,8 +2347,8 @@ whatever is relevant to keep the game going.

    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.

    @@ -2308,7 +2362,7 @@ movable flag originated from.

    respawn_player()

    -

    respawn_player(self, player: ba.Player, respawn_time: Optional[float] = None) -> None

    +

    respawn_player(self, player: PlayerType, respawn_time: Optional[float] = None) -> None

    Given a ba.Player, sets up a standard respawn timer, along with the standard counter display, etc. @@ -2355,7 +2409,7 @@ and short description of the game.

    spawn_player()

    -

    spawn_player(self, player: ba.Player) -> ba.Actor

    +

    spawn_player(self, player: PlayerType) -> ba.Actor

    Spawn *something* for the provided ba.Player.

    @@ -2363,7 +2417,7 @@ and short description of the game.

    spawn_player_if_exists()

    -

    spawn_player_if_exists(self, player: ba.Player) -> None

    +

    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.

    @@ -2372,7 +2426,7 @@ and short description of the game.

    spawn_player_spaz()

    -

    spawn_player_spaz(self, player: ba.Player, position: Sequence[float] = (0, 0, 0), angle: float = None) -> PlayerSpaz

    +

    spawn_player_spaz(self, player: PlayerType, position: Sequence[float] = (0, 0, 0), angle: float = None) -> PlayerSpaz

    Create and wire up a ba.PlayerSpaz for the provided ba.Player.

    @@ -2485,7 +2539,7 @@ client.

    player

    -

    Optional[ba.Player]

    +

    Optional[ba.SessionPlayer]

    The player associated with this input device.

    @@ -2656,7 +2710,7 @@ can be changed to separate its new high score lists/etc. from the old.

    teams, use_team_colors

    teams

    -

    List[ba.Team]

    +

    List[ba.SessionTeam]

    Teams available in this lobby.

    @@ -2676,7 +2730,7 @@ can be changed to separate its new high score lists/etc. from the old.

    add_chooser()

    -

    add_chooser(self, player: ba.Player) -> None

    +

    add_chooser(self, player: ba.SessionPlayer) -> None

    Add a chooser to the lobby for the provided player.

    @@ -2723,7 +2777,7 @@ Intended for use in initial joining-screens.

    remove_chooser()

    -

    remove_chooser(self, player: ba.Player) -> None

    +

    remove_chooser(self, player: ba.SessionPlayer) -> None

    Remove a single player's chooser; does not kick him.

    @@ -2826,6 +2880,7 @@ etc.

    Attributes:

    +
    activity, expired

    activity

    ba.Activity

    @@ -2833,10 +2888,17 @@ etc.

    Raises a ba.ActivityNotFoundError if the Activity no longer exists.

    +
    +

    expired

    +

    bool

    +

    Whether the Actor is expired.

    + +

    (see ba.Actor.on_expire())

    +

    Methods Inherited:

    -
    autoretain(), getactivity(), is_alive(), is_expired(), on_expire()
    +
    autoretain(), getactivity(), is_alive(), on_expire()

    Methods Defined or Overridden:

    <constructor>, exists(), get_def_bound_box(), get_def_point(), get_def_points(), get_ffa_start_position(), get_flag_position(), get_music_type(), get_name(), get_play_types(), get_preview_texture_name(), get_start_position(), handlemessage(), is_point_near_edge(), on_preload(), preload()
    @@ -3237,7 +3299,7 @@ another ba.Activity.

    on_team_join()

    -

    on_team_join(self, team: ba.Team) -> None

    +

    on_team_join(self, team: ba.SessionTeam) -> None

    Called when a new ba.Team joins the session.

    @@ -3493,6 +3555,7 @@ acting as an alternative to setting node attributes.

    Attributes:

    +
    activity, expired

    activity

    ba.Activity

    @@ -3500,10 +3563,17 @@ acting as an alternative to setting node attributes.

    Raises a ba.ActivityNotFoundError if the Activity no longer exists.

    +
    +

    expired

    +

    bool

    +

    Whether the Actor is expired.

    + +

    (see ba.Actor.on_expire())

    +

    Methods Inherited:

    -
    autoretain(), getactivity(), is_alive(), is_expired(), on_expire()
    +
    autoretain(), getactivity(), is_alive(), on_expire()

    Methods Defined or Overridden:

    <constructor>, exists(), handlemessage()
    @@ -3636,101 +3706,45 @@ even if myactor is set to None.


    ba.Player

    -

    <top level class> -

    -

    A reference to a player in the game.

    - -

    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' -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.

    +

    inherits from: typing.Generic

    +

    Testing.

    Attributes:

    -
    actor, character, color, exists, gamedata, highlight, in_game, node, sessiondata, team
    +
    exists, node, sessionplayer
    -

    actor

    -

    Optional[ba.Actor]

    -

    The current ba.Actor associated with this Player. -This may be None

    - -
    -

    character

    -

    str

    -

    The character this player has selected in their profile.

    - -
    -

    color

    -

    Sequence[float]

    -

    The base color for this Player. -In team games this will match the ba.Team's color.

    - -

    exists

    -

    bool

    -

    Whether the player still exists. -Most functionality will fail on a nonexistent player.

    +

    bool

    +

    Whether the player still exists.

    -

    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.

    - -
    -

    gamedata

    -

    Dict

    -

    A dict for use by the current ba.Activity for -storing data associated with this Player. -This gets cleared for each new ba.Activity.

    - -
    -

    highlight

    -

    Sequence[float]

    -

    A secondary color for this player. -This is used for minor highlights and accents -to allow a player to stand apart from his teammates -who may all share the same team (primary) color.

    - -
    -

    in_game

    -

    bool

    -

    This bool value will be True once the Player has completed -any lobby character/team selection.

    +

    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.

    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.

    +

    ba.Node

    +

    A ba.Node of type 'player' associated with this Player.

    + +

    This node can be used to get a generic player position/etc.

    -

    sessiondata

    -

    Dict

    -

    A dict for use by the current ba.Session for -storing data associated with this player. -This persists for the duration of the session.

    +

    sessionplayer

    +

    ba.SessionPlayer

    +

    Return the ba.SessionPlayer corresponding to this Player.

    -
    -

    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.

    +

    Throws a ba.SessionPlayerNotFoundError if it does not exist.

    Methods:

    -
    assign_input_call(), get_account_id(), get_icon(), get_id(), get_input_device(), get_name(), is_alive(), remove_from_game(), reset_input(), set_actor(), set_name()
    +
    assign_input_call(), get_icon(), get_name(), is_alive(), reset_input(), set_actor()

    assign_input_call()

    -

    assign_input_call(type: Union[str, Tuple[str, ...]], - call: Callable) -> None

    +

    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', @@ -3740,76 +3754,48 @@ Valid type values are: 'jumpPress', 'jumpRelease', 'punchPress', 'rightRelease', 'run', 'flyPress', 'flyRelease', 'startPress', 'startRelease'

    -
    -

    get_account_id()

    -

    get_account_id() -> str

    - -

    Return the Account ID this player is signed in under, if -there is one and it can be determined with relative certainty. -Returns None otherwise. Note that this may require an active -internet connection (especially for network-connected players) -and may return None for a short while after a player initially -joins (while verification occurs).

    -

    get_icon()

    -

    get_icon() -> Dict[str, Any]

    +

    get_icon(self) -> Dict[str, Any]

    + +

    get_icon() -> Dict[str, Any]

    Returns the character's icon (images, colors, etc contained in a dict)

    -
    -

    get_id()

    -

    get_id() -> int

    - -

    Returns the unique numeric player ID for this player.

    - -
    -

    get_input_device()

    -

    get_input_device() -> ba.InputDevice

    - -

    Returns the player's input device.

    -

    get_name()

    -

    get_name(full: bool = False, icon: bool = True) -> str

    +

    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.

    is_alive()

    -

    is_alive() -> bool

    +

    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.

    -
    -

    remove_from_game()

    -

    remove_from_game() -> None

    - -

    Removes the player from the game.

    -

    reset_input()

    -

    reset_input() -> None

    +

    reset_input(self) -> None

    + +

    reset_input() -> None

    Clears out the player's assigned input actions.

    set_actor()

    -

    set_actor(actor: Optional[ba.Actor]) -> None

    +

    set_actor(self, actor: Optional[ba.Actor]) -> None

    + +

    set_actor(actor: Optional[ba.Actor]) -> None

    Set the player's associated ba.Actor.

    -
    -

    set_name()

    -

    set_name(name: str, full_name: str = None, real: bool = True) - -> None

    - -

    Set the player's name to the provided string. -A number will automatically be appended if the name is not unique from -other players.

    -

    @@ -3839,18 +3825,19 @@ other players.

    player, team

    player

    -

    ba.Player

    -

    Return the instance's associated ba.Player.

    +

    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.

    team

    -

    ba.Team

    -

    The ba.Team the last associated player was last on.

    +

    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.

    @@ -3858,11 +3845,11 @@ other players.

    <constructor>, associate_with_player(), cancel_multi_kill_timer(), get_icon(), get_last_player(), get_name(), getactivity(), submit_kill()

    <constructor>

    -

    ba.PlayerRecord(name: str, name_full: str, player: ba.Player, stats: ba.Stats)

    +

    ba.PlayerRecord(name: str, name_full: str, player: ba.SessionPlayer, stats: ba.Stats)

    associate_with_player()

    -

    associate_with_player(self, player: ba.Player) -> None

    +

    associate_with_player(self, player: ba.SessionPlayer) -> None

    Associate this entry with a ba.Player.

    @@ -3880,7 +3867,7 @@ other players.

    get_last_player()

    -

    get_last_player(self) -> ba.Player

    +

    get_last_player(self) -> ba.SessionPlayer

    Return the last ba.Player we were associated with.

    @@ -4137,14 +4124,14 @@ to proceed past the initial joining screen.

    players

    -

    List[ba.Player]

    +

    List[ba.SessionPlayer]

    All ba.Players in the Session. Most things should use the player list in ba.Activity; not this. Some players, such as those who have not yet selected a character, will only appear on this list.

    teams

    -

    List[ba.Team]

    +

    List[ba.SessionTeam]

    All the ba.Teams in the Session. Most things should use the team list in ba.Activity; not this.

    @@ -4223,13 +4210,13 @@ another ba.Activity.

    on_player_leave()

    -

    on_player_leave(self, player: ba.Player) -> None

    +

    on_player_leave(self, sessionplayer: ba.SessionPlayer) -> None

    -

    Called when a previously-accepted ba.Player leaves the session.

    +

    Called when a previously-accepted ba.SessionPlayer leaves.

    on_player_request()

    -

    on_player_request(self, player: ba.Player) -> bool

    +

    on_player_request(self, player: ba.SessionPlayer) -> bool

    Called when a new ba.Player wants to join the Session.

    @@ -4237,13 +4224,13 @@ another ba.Activity.

    on_team_join()

    -

    on_team_join(self, team: ba.Team) -> None

    +

    on_team_join(self, team: ba.SessionTeam) -> None

    Called when a new ba.Team joins the session.

    on_team_leave()

    -

    on_team_leave(self, team: ba.Team) -> None

    +

    on_team_leave(self, team: ba.SessionTeam) -> None

    Called when a ba.Team is leaving the session.

    @@ -4268,6 +4255,244 @@ session.set_activity(foo) and then ba.newnode

    Category: Exception Classes

    +

    Methods:

    +

    <all methods inherited from ba.NotFoundError>

    +
    +

    ba.SessionPlayer

    +

    <top level class> +

    +

    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.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.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:

    +
    character, color, exists, gamedata, gameplayer, highlight, id, in_game, sessiondata, team
    +
    +

    character

    +

    str

    +

    The character this player has selected in their profile.

    + +
    +

    color

    +

    Sequence[float]

    +

    The base color for this Player. +In team games this will match the ba.SessionTeam's color.

    + +
    +

    exists

    +

    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.

    + +
    +

    gamedata

    +

    Dict

    +

    A dict for use by the current ba.Activity for +storing data associated with this Player. +This gets cleared for each new ba.Activity.

    + +
    +

    gameplayer

    +

    Optional[ba.Player]

    +

    The current game-specific instance for this player.

    + +
    +

    highlight

    +

    Sequence[float]

    +

    A secondary color for this player. +This is used for minor highlights and accents +to allow a player to stand apart from his teammates +who may all share the same team (primary) color.

    + +
    +

    id

    +

    int

    +

    The unique numeric ID of the Player.

    + +
    +

    in_game

    +

    bool

    +

    This bool value will be True once the Player has completed +any lobby character/team selection.

    + +
    +

    sessiondata

    +

    Dict

    +

    A dict for use by the current ba.Session for +storing data associated with this player. +This persists for the duration of the session.

    + +
    +

    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.

    + +
    +
    +

    Methods:

    +
    assign_input_call(), get_account_id(), get_icon(), get_input_device(), get_name(), remove_from_game(), reset_input(), set_name()
    +
    +

    assign_input_call()

    +

    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'

    + +
    +

    get_account_id()

    +

    get_account_id() -> str

    + +

    Return the Account ID this player is signed in under, if +there is one and it can be determined with relative certainty. +Returns None otherwise. Note that this may require an active +internet connection (especially for network-connected players) +and may return None for a short while after a player initially +joins (while verification occurs).

    + +
    +

    get_icon()

    +

    get_icon() -> Dict[str, Any]

    + +

    Returns the character's icon (images, colors, etc contained in a dict)

    + +
    +

    get_input_device()

    +

    get_input_device() -> ba.InputDevice

    + +

    Returns the player's input device.

    + +
    +

    get_name()

    +

    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.

    + +
    +

    remove_from_game()

    +

    remove_from_game() -> None

    + +

    Removes the player from the game.

    + +
    +

    reset_input()

    +

    reset_input() -> None

    + +

    Clears out the player's assigned input actions.

    + +
    +

    set_name()

    +

    set_name(name: str, full_name: str = None, real: bool = True) + -> None

    + +

    Set the player's name to the provided string. +A number will automatically be appended if the name is not unique from +other players.

    + +
    +
    +
    +

    ba.SessionPlayerNotFoundError

    +

    inherits from: ba.NotFoundError, Exception, BaseException

    +

    Exception raised when an expected ba.SessionPlayer does not exist.

    + +

    Category: Exception Classes +

    + +

    Methods:

    +

    <all methods inherited from ba.NotFoundError>

    +
    +

    ba.SessionTeam

    +

    <top level class> +

    +

    A team of one or more ba.SessionPlayers.

    + +

    Category: Gameplay Classes

    + +

    Note that a player *always* has a team; + in some cases, such as free-for-all ba.Sessions, + each team consists of just one ba.Player.

    + +

    Attributes:

    +
    color, gamedata, id, name, players, sessiondata
    +
    +

    color

    +

    Tuple[float, ...]

    +

    The team's color.

    + +
    +

    gamedata

    +

    Dict

    +

    A dict for use by the current ba.Activity +for storing data associated with this team. +This gets cleared for each new ba.Activity.

    + +
    +

    id

    +

    int

    +

    The unique numeric id of the team.

    + +
    +

    name

    +

    Union[ba.Lstr, str]

    +

    The team's name.

    + +
    +

    players

    +

    List[ba.SessionPlayer]

    +

    The list of ba.SessionPlayers on the team.

    + +
    +

    sessiondata

    +

    Dict

    +

    A dict for use by the current ba.Session for +storing data associated with this team. +Unlike gamedata, this persists for the duration +of the session.

    + +
    +
    +

    Methods:

    +
    +

    <constructor>

    +

    ba.SessionTeam(team_id: 'int' = 0, name: 'Union[ba.Lstr, str]' = '', color: 'Sequence[float]' = (1.0, 1.0, 1.0))

    + +

    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.

    + +
    +
    +
    +

    ba.SessionTeamNotFoundError

    +

    inherits from: ba.NotFoundError, Exception, BaseException

    +

    Exception raised when an expected ba.SessionTeam does not exist.

    + +

    Category: Exception Classes +

    +

    Methods:

    <all methods inherited from ba.NotFoundError>


    @@ -4459,7 +4684,7 @@ session.set_activity(foo) and then ba.newnode

    player_got_hit()

    -

    player_got_hit(self, player: ba.Player) -> None

    +

    player_got_hit(self, player: ba.SessionPlayer) -> None

    Call this when a player got hit.

    @@ -4479,7 +4704,7 @@ session.set_activity(foo) and then ba.newnode

    register_player()

    -

    register_player(self, player: ba.Player) -> None

    +

    register_player(self, player: ba.SessionPlayer) -> None

    Register a player with this score-set.

    @@ -4505,72 +4730,22 @@ session.set_activity(foo) and then ba.newnode

    ba.Team

    -

    <top level class> -

    -

    A team of one or more ba.Players.

    - -

    Category: Gameplay Classes

    - -

    Note that a player *always* has a team; - in some cases, such as free-for-all ba.Sessions, - each team consists of just one ba.Player.

    +

    inherits from: typing.Generic

    +

    Testing.

    Attributes:

    -
    color, gamedata, name, players, sessiondata
    -

    color

    -

    Tuple[float, ...]

    -

    The team's color.

    +

    sessionteam

    +

    SessionTeam

    +

    Return the ba.SessionTeam corresponding to this Team.

    -
    -

    gamedata

    -

    Dict

    -

    A dict for use by the current ba.Activity -for storing data associated with this team. -This gets cleared for each new ba.Activity.

    - -
    -

    name

    -

    Union[ba.Lstr, str]

    -

    The team's name.

    - -
    -

    players

    -

    List[ba.Player]

    -

    The list of ba.Players on the team.

    - -
    -

    sessiondata

    -

    Dict

    -

    A dict for use by the current ba.Session for -storing data associated with this team. -Unlike gamedata, this persists for the duration -of the session.

    - -
    -
    -

    Methods:

    -
    <constructor>, get_id()
    -
    -

    <constructor>

    -

    ba.Team(team_id: 'int' = 0, name: 'Union[ba.Lstr, str]' = '', color: 'Sequence[float]' = (1.0, 1.0, 1.0))

    - -

    Instantiate a ba.Team.

    - -

    In most cases, all teams are provided to you by the ba.Session, -ba.Session, so calling this shouldn't be necessary.

    - -
    -

    get_id()

    -

    get_id(self) -> int

    - -

    Returns the numeric team ID.

    +

    Throws a ba.SessionTeamNotFoundError if there is none.


    ba.TeamGameActivity

    -

    inherits from: ba.GameActivity, ba.Activity, ba.DependencyComponent

    +

    inherits from: ba.GameActivity, ba.Activity, ba.DependencyComponent, typing.Generic

    Base class for teams and free-for-all mode games.

    Category: Gameplay Classes

    @@ -4582,14 +4757,28 @@ of the session.

    Attributes Inherited:

    players, settings_raw, teams

    Attributes Defined Here:

    -
    map, session, stats
    +
    expired, map, playertype, session, stats, teamtype
    +

    expired

    +

    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'.

    + +

    map

    ba.Map

    The map being used for this game.

    Raises a ba.NotFoundError if the map does not currently exist.

    +
    +

    playertype

    +

    Type[PlayerType]

    +

    The type of ba.Player this Activity is using.

    +

    session

    ba.Session

    @@ -4604,10 +4793,15 @@ of the session.

    If access is attempted before or after, raises a ba.NotFoundError.

    +
    +

    teamtype

    +

    Type[TeamType]

    +

    The type of ba.Team this Activity is using.

    +

    Methods Inherited:

    -
    add_actor_weak_ref(), begin(), continue_or_end_game(), create_config_ui(), create_player_node(), dep_is_present(), end_game(), get_config_display_string(), get_description(), get_description_display_string(), get_display_string(), get_dynamic_deps(), get_instance_description(), get_instance_display_string(), get_instance_scoreboard_description(), get_instance_scoreboard_display_string(), get_name(), get_score_info(), get_settings(), get_supported_maps(), get_team_display_string(), handlemessage(), has_begun(), has_ended(), has_transitioned_in(), is_expired(), is_transitioning_out(), is_waiting_for_continue(), on_continue(), on_expire(), on_player_join(), on_player_leave(), on_team_join(), on_team_leave(), on_transition_out(), project_flag_stand(), respawn_player(), retain_actor(), set_has_ended(), set_immediate_end(), setup_standard_powerup_drops(), setup_standard_time_limit(), show_info(), show_scoreboard_info(), show_zoom_message(), spawn_player(), spawn_player_if_exists(), start_transition_in()
    +
    add_actor_weak_ref(), add_player(), add_team(), begin(), continue_or_end_game(), create_config_ui(), create_player(), create_team(), dep_is_present(), destroy(), end_game(), get_config_display_string(), get_description(), get_description_display_string(), get_display_string(), get_dynamic_deps(), get_instance_description(), get_instance_display_string(), get_instance_scoreboard_description(), get_instance_scoreboard_display_string(), get_name(), get_score_info(), get_settings(), get_supported_maps(), get_team_display_string(), handlemessage(), has_begun(), has_ended(), has_transitioned_in(), is_transitioning_out(), is_waiting_for_continue(), on_continue(), on_expire(), on_player_join(), on_player_leave(), on_team_join(), on_team_leave(), on_transition_out(), project_flag_stand(), remove_player(), remove_team(), respawn_player(), retain_actor(), set_has_ended(), set_immediate_end(), setup_standard_powerup_drops(), setup_standard_time_limit(), show_info(), show_scoreboard_info(), show_zoom_message(), spawn_player(), spawn_player_if_exists(), transition_in(), transition_out()

    Methods Defined or Overridden:

    <constructor>, end(), on_begin(), on_transition_in(), spawn_player_spaz(), supports_session_type()
    @@ -4640,13 +4834,13 @@ and it should begin its actual game logic.

    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.

    spawn_player_spaz()

    -

    spawn_player_spaz(self, player: ba.Player, position: Sequence[float] = None, angle: float = None) -> PlayerSpaz

    +

    spawn_player_spaz(self, player: PlayerType, position: Sequence[float] = None, angle: float = None) -> PlayerSpaz

    Method override; spawns and wires up a standard ba.PlayerSpaz for a ba.Player.

    @@ -4711,7 +4905,7 @@ Results for a completed ba.TeamGameActivity

    get_team_score()

    -

    get_team_score(self, team: ba.Team) -> Optional[int]

    +

    get_team_score(self, team: Union[ba.SessionTeam, ba.Team]) -> Optional[int]

    Return the score for a given team.

    @@ -4725,9 +4919,9 @@ Results for a completed ba.TeamGameActivity

    get_teams()

    -

    get_teams(self) -> List[ba.Team]

    +

    get_teams(self) -> List[ba.SessionTeam]

    -

    Return all ba.Teams in the results.

    +

    Return all ba.SessionTeams in the results.

    get_winners()

    @@ -4737,13 +4931,13 @@ Results for a completed ba.TeamGameActivity

    get_winning_team()

    -

    get_winning_team(self) -> Optional[ba.Team]

    +

    get_winning_team(self) -> Optional[ba.SessionTeam]

    Get the winning ba.Team if there is exactly one; None otherwise.

    has_score_for_team()

    -

    has_score_for_team(self, team: ba.Team) -> bool

    +

    has_score_for_team(self, sessionteam: ba.SessionTeam) -> bool

    Return whether there is a score for a given team.

    @@ -4755,7 +4949,7 @@ Results for a completed ba.TeamGameActivity

    set_team_score()

    -

    set_team_score(self, team: ba.Team, score: int) -> None

    +

    set_team_score(self, team: Union[ba.SessionTeam, ba.Team], score: int) -> None

    Set the score for a given ba.Team.

    @@ -5680,6 +5874,28 @@ object dies. 'owner' can be another node or a ba.Actor

    Open the provided url in a web-browser, or display the URL string in a window if that isn't possible.

    +
    +

    ba.playercast()

    +

    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.

    + +
    +

    ba.playercast_o()

    +

    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

    +

    ba.playsound()

    playsound(sound: Sound, volume: float = 1.0, diff --git a/tools/bacloud b/tools/bacloud index 65efdff2..51c3b050 100755 --- a/tools/bacloud +++ b/tools/bacloud @@ -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') diff --git a/tools/batools/build.py b/tools/batools/build.py index cf762d60..db1c366e 100644 --- a/tools/batools/build.py +++ b/tools/batools/build.py @@ -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]: diff --git a/tools/efro/entity/_field.py b/tools/efro/entity/_field.py index 4a0a4714..f540141b 100644 --- a/tools/efro/entity/_field.py +++ b/tools/efro/entity/_field.py @@ -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 diff --git a/tools/efro/entity/_value.py b/tools/efro/entity/_value.py index 30e8abc3..44bf4ec9 100644 --- a/tools/efro/entity/_value.py +++ b/tools/efro/entity/_value.py @@ -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 diff --git a/tools/efro/util.py b/tools/efro/util.py index a236db37..fc74c96f 100644 --- a/tools/efro/util.py +++ b/tools/efro/util.py @@ -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. diff --git a/tools/efrotools/__init__.py b/tools/efrotools/__init__.py index fc70e5ee..cbb2cf98 100644 --- a/tools/efrotools/__init__.py +++ b/tools/efrotools/__init__.py @@ -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() diff --git a/tools/efrotools/code.py b/tools/efrotools/code.py index 3b18a02d..4d921ab2 100644 --- a/tools/efrotools/code.py +++ b/tools/efrotools/code.py @@ -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: diff --git a/tools/efrotools/pylintplugins.py b/tools/efrotools/pylintplugins.py index d897ff38..3db6760b 100644 --- a/tools/efrotools/pylintplugins.py +++ b/tools/efrotools/pylintplugins.py @@ -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) diff --git a/tools/efrotools/snippets.py b/tools/efrotools/snippets.py index 6fd7f7d7..d2f206ba 100644 --- a/tools/efrotools/snippets.py +++ b/tools/efrotools/snippets.py @@ -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__', '')) - 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 ' for full command documentation.") + print(f"Run {Clr.MAG}'snippets help {Clr.BLD}'" + 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)) diff --git a/tools/snippets b/tools/snippets index d270f01e..b5c0405e 100755 --- a/tools/snippets +++ b/tools/snippets @@ -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 diff --git a/tools/update_project b/tools/update_project index a66fb333..72e82c7f 100755 --- a/tools/update_project +++ b/tools/update_project @@ -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)