Added per-game team and player types

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

View File

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

View File

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

View File

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

View File

@ -34,7 +34,7 @@ NOTE: This file was autogenerated by gendummymodule; do not edit by hand.
""" """
# (hash we can use to see if this file is out of date) # (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. # I'm sorry Pylint. I know this file saddens you. Be strong.
# pylint: disable=useless-suppression # pylint: disable=useless-suppression
@ -259,7 +259,7 @@ class InputDevice:
allows_configuring: bool allows_configuring: bool
Whether the input-device can be configured. Whether the input-device can be configured.
player: Optional[ba.Player] player: Optional[ba.SessionPlayer]
The player associated with this input device. The player associated with this input device.
client_id: int client_id: int
@ -292,7 +292,7 @@ class InputDevice:
""" """
exists: bool exists: bool
allows_configuring: bool allows_configuring: bool
player: Optional[ba.Player] player: Optional[ba.SessionPlayer]
client_id: int client_id: int
name: str name: str
unique_identifier: str unique_identifier: str
@ -648,6 +648,14 @@ class Node:
billboard_texture: Optional[ba.Texture] = None billboard_texture: Optional[ba.Texture] = None
billboard_cross_out: bool = False billboard_cross_out: bool = False
billboard_opacity: float = 0.0 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: def add_death_action(self, action: Callable[[], None]) -> None:
"""add_death_action(action: Callable[[], None]) -> None """add_death_action(action: Callable[[], None]) -> None
@ -737,31 +745,36 @@ class Node:
return None return None
class Player: class SessionData:
"""A reference to a player in the game. """(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 Category: Gameplay Classes
These are created and managed internally and These are created and managed internally and
provided to your Session/Activity instances. 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 references under-the-hood; a player can leave the game at
any point. For this reason, you should make judicious use of the any point. For this reason, you should make judicious use of the
ba.Player.exists attribute (or boolean operator) to ensure that a ba.SessionPlayer.exists attribute (or boolean operator) to ensure
Player is still present if retaining references to one for any that a SessionPlayer is still present if retaining references to one
length of time. for any length of time.
Attributes: Attributes:
actor: Optional[ba.Actor] id: int
The current ba.Actor associated with this Player. The unique numeric ID of the 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.
exists: bool exists: bool
Whether the player still exists. Whether the player still exists.
@ -775,10 +788,10 @@ class Player:
This bool value will be True once the Player has completed This bool value will be True once the Player has completed
any lobby character/team selection. any lobby character/team selection.
team: ba.Team team: ba.SessionTeam
The ba.Team this Player is on. If the Player is The ba.SessionTeam this Player is on. If the SessionPlayer
still in its lobby selecting a team/etc. then a is still in its lobby selecting a team/etc. then a
ba.TeamNotFoundError will be raised. ba.SessionTeamNotFoundError will be raised.
sessiondata: Dict sessiondata: Dict
A dict for use by the current ba.Session for A dict for use by the current ba.Session for
@ -792,7 +805,7 @@ class Player:
color: Sequence[float] color: Sequence[float]
The base color for this Player. 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] highlight: Sequence[float]
A secondary color for this player. A secondary color for this player.
@ -802,17 +815,20 @@ class Player:
character: str character: str
The character this player has selected in their profile. 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] id: int
node: Optional[ba.Node]
exists: bool exists: bool
in_game: bool in_game: bool
team: ba.Team team: ba.SessionTeam
sessiondata: Dict sessiondata: Dict
gamedata: Dict gamedata: Dict
color: Sequence[float] color: Sequence[float]
highlight: Sequence[float] highlight: Sequence[float]
character: str character: str
gameplayer: Optional[ba.Player]
def assign_input_call(self, type: Union[str, Tuple[str, ...]], def assign_input_call(self, type: Union[str, Tuple[str, ...]],
call: Callable) -> None: call: Callable) -> None:
@ -855,13 +871,6 @@ class Player:
""" """
return {'foo': 'bar'} 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: def get_input_device(self) -> ba.InputDevice:
"""get_input_device() -> ba.InputDevice """get_input_device() -> ba.InputDevice
@ -878,14 +887,6 @@ class Player:
""" """
return str() 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: def remove_from_game(self) -> None:
"""remove_from_game() -> None """remove_from_game() -> None
@ -914,17 +915,10 @@ class Player:
""" """
return None return None
def set_actor(self, actor: Optional[ba.Actor]) -> None: def set_data(self, team: ba.SessionTeam, character: str,
"""set_actor(actor: Optional[ba.Actor]) -> None color: Sequence[float], highlight: Sequence[float]) -> None:
"""set_data(team: ba.SessionTeam, character: str,
Set the player's associated ba.Actor. color: Sequence[float], highlight: Sequence[float]) -> None
"""
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
(internal) (internal)
""" """
@ -961,18 +955,6 @@ class Player:
return None 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: class Sound:
"""A reference to a sound. """A reference to a sound.
@ -2011,7 +1993,7 @@ def get_foreground_host_activity() -> Optional[ba.Activity]:
is none. is none.
""" """
import ba # pylint: disable=cyclic-import import ba # pylint: disable=cyclic-import
return ba.Activity({}) return ba.Activity(settings={})
def get_foreground_host_session() -> Optional[ba.Session]: 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. False then None is returned instead.
""" """
import ba # pylint: disable=cyclic-import import ba # pylint: disable=cyclic-import
return ba.Activity({}) return ba.Activity(settings={})
def getcollidemodel(name: str) -> ba.CollideModel: 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. instantiated; You must go through this function.
""" """
import ba # pylint: disable=cyclic-import import ba # pylint: disable=cyclic-import
return ba.Activity({}) return ba.Activity(settings={})
def new_host_session(sessiontype: Type[ba.Session], def new_host_session(sessiontype: Type[ba.Session],

View File

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

View File

@ -22,10 +22,13 @@
from __future__ import annotations from __future__ import annotations
import weakref 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 from ba._dependency import DependencyComponent
import _ba
if TYPE_CHECKING: if TYPE_CHECKING:
from weakref import ReferenceType from weakref import ReferenceType
@ -33,8 +36,11 @@ if TYPE_CHECKING:
import ba import ba
from bastd.actor.respawnicon import RespawnIcon 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. """Units of execution wrangled by a ba.Session.
Category: Gameplay Classes Category: Gameplay Classes
@ -66,13 +72,73 @@ class Activity(DependencyComponent):
# pylint: disable=too-many-public-methods # 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] settings_raw: Dict[str, Any]
teams: List[ba.Team] teams: List[TeamType]
players: List[ba.Player] 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]): 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() The activity will not be actually run until ba.Session.set_activity()
is called. 'settings' should be a dict of key/value pairs specific is called. 'settings' should be a dict of key/value pairs specific
@ -84,14 +150,20 @@ class Activity(DependencyComponent):
""" """
super().__init__() 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.sharedobjs: Dict[str, Any] = {}
self.paused_text: Optional[ba.Actor] = None self.paused_text: Optional[ba.Actor] = None
self.spaz_respawn_icons_right: Dict[int, RespawnIcon] self.spaz_respawn_icons_right: Dict[int, RespawnIcon]
# Create our internal engine data.
self._activity_data = _ba.register_activity(self)
session = _ba.getsession() session = _ba.getsession()
if session is None: if session is None:
raise Exception('No current session') raise Exception('No current session')
@ -105,8 +177,9 @@ class Activity(DependencyComponent):
if _ba.getactivity(doraise=False) is not self: if _ba.getactivity(doraise=False) is not self:
raise Exception('invalid context state') raise Exception('invalid context state')
# Should perhaps kill this; activities should validate/store whatever # Hopefully can eventually kill this; activities should
# settings they need at init time (in a more type-safe way). # validate/store whatever settings they need at init time
# (in a more type-safe way).
self.settings_raw = settings self.settings_raw = settings
self._has_transitioned_in = False self._has_transitioned_in = False
@ -122,66 +195,6 @@ class Activity(DependencyComponent):
self._activity_death_check_timer: Optional[ba.Timer] = None self._activity_death_check_timer: Optional[ba.Timer] = None
self._expired = False 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 # This gets set once another activity has begun transitioning in but
# before this one is killed. The on_transition_out() method is also # before this one is killed. The on_transition_out() method is also
# called at this time. Make sure to not assign player inputs, # called at this time. Make sure to not assign player inputs,
@ -194,48 +207,16 @@ class Activity(DependencyComponent):
# is dying. # is dying.
self._actor_refs: List[ba.Actor] = [] self._actor_refs: List[ba.Actor] = []
self._actor_weak_refs: List[ReferenceType[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. # This stuff gets filled in just before on_begin() is called.
self.teams = [] self.teams = []
self.players = [] self.players = []
self.lobby = None
self._stats: Optional[ba.Stats] = 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: def __del__(self) -> None:
from ba._apputils import garbage_collect, call_after_ad from ba._apputils import garbage_collect, call_after_ad
# If the activity has been run then we should have already cleaned # If the activity has been run then we should have already cleaned
@ -259,6 +240,47 @@ class Activity(DependencyComponent):
else: else:
_ba.pushcall(session.begin_next_activity) _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: def set_has_ended(self, val: bool) -> None:
"""(internal)""" """(internal)"""
self._has_ended = val self._has_ended = val
@ -277,18 +299,11 @@ class Activity(DependencyComponent):
self._should_end_immediately_results = results self._should_end_immediately_results = results
self._should_end_immediately_delay = delay 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? (internal)
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:
from ba._general import Call from ba._general import Call
from ba._enums import TimeType from ba._enums import TimeType
@ -313,7 +328,8 @@ class Activity(DependencyComponent):
with _ba.Context('empty'): with _ba.Context('empty'):
self._expire() self._expire()
else: else:
raise Exception('_destroy() called multiple times') raise RuntimeError(f'destroy() called when'
f' already expired for {self}')
@classmethod @classmethod
def _check_activity_death(cls, activity_ref: ReferenceType[Activity], def _check_activity_death(cls, activity_ref: ReferenceType[Activity],
@ -351,54 +367,54 @@ class Activity(DependencyComponent):
_ba.quit() _ba.quit()
except Exception: except Exception:
from ba import _error print_exception('exception on _check_activity_death:')
_error.print_exception('exception on _check_activity_death:')
def _expire(self) -> None: 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 self._expired = True
# Do some default cleanup.
try: try:
try: self.on_expire()
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)
except Exception: 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 # 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 # our activity might not go down if we don't. This will kill all
@ -407,13 +423,13 @@ class Activity(DependencyComponent):
try: try:
self._activity_data.destroy() self._activity_data.destroy()
except Exception: except Exception:
_error.print_exception( print_exception(
'Exception during ba.Activity._expire() destroying data:') '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_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._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: def retain_actor(self, actor: ba.Actor) -> None:
"""Add a strong-reference to a ba.Actor to this Activity. """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. is a convenient way to access this same functionality.
""" """
from ba import _actor as bsactor from ba import _actor as bsactor
from ba import _error
if not isinstance(actor, bsactor.Actor): if not isinstance(actor, bsactor.Actor):
raise Exception('non-actor passed to _retain_actor') raise Exception('non-actor passed to _retain_actor')
if (self.has_transitioned_in() if (self.has_transitioned_in()
and _ba.time() - self._last_dead_object_prune_time > 10.0): and _ba.time() - self._last_prune_dead_actors_time > 10.0):
_error.print_error('it looks like nodes/actors are not' print_error('it looks like nodes/actors are not'
' being pruned in your activity;' ' being pruned in your activity;'
' did you call Activity.on_transition_in()' ' did you call Activity.on_transition_in()'
' from your subclass?; ' + str(self) + ' from your subclass?; ' + str(self) + ' (loc. a)')
' (loc. a)')
self._actor_refs.append(actor) self._actor_refs.append(actor)
def add_actor_weak_ref(self, actor: ba.Actor) -> None: def add_actor_weak_ref(self, actor: ba.Actor) -> None:
@ -441,16 +455,14 @@ class Activity(DependencyComponent):
(called by the ba.Actor base class) (called by the ba.Actor base class)
""" """
from ba import _actor as bsactor from ba import _actor as bsactor
from ba import _error
if not isinstance(actor, bsactor.Actor): if not isinstance(actor, bsactor.Actor):
raise Exception('non-actor passed to _add_actor_weak_ref') raise Exception('non-actor passed to _add_actor_weak_ref')
if (self.has_transitioned_in() if (self.has_transitioned_in()
and _ba.time() - self._last_dead_object_prune_time > 10.0): and _ba.time() - self._last_prune_dead_actors_time > 10.0):
_error.print_error('it looks like nodes/actors are ' print_error('it looks like nodes/actors are '
'not being pruned in your activity;' 'not being pruned in your activity;'
' did you call Activity.on_transition_in()' ' did you call Activity.on_transition_in()'
' from your subclass?; ' + str(self) + ' from your subclass?; ' + str(self) + ' (loc. b)')
' (loc. b)')
self._actor_weak_refs.append(weakref.ref(actor)) self._actor_weak_refs.append(weakref.ref(actor))
@property @property
@ -465,48 +477,38 @@ class Activity(DependencyComponent):
raise SessionNotFoundError() raise SessionNotFoundError()
return session 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. """Called when a new ba.Player has joined the Activity.
(including the initial set of Players) (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.""" """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. """Called when a new ba.Team joins the Activity.
(including the initial set of Teams) (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.""" """Called when a ba.Team leaves the Activity."""
def on_transition_in(self) -> None: def on_transition_in(self) -> None:
"""Called when the Activity is first becoming visible. """Called when the Activity is first becoming visible.
Upon this call, the Activity should fade in backgrounds, Upon this call, the Activity should fade in backgrounds,
start playing music, etc. It does not yet have access to ba.Players start playing music, etc. It does not yet have access to players
or ba.Teams, however. They remain owned by the previous Activity or teams, however. They remain owned by the previous Activity
up until ba.Activity.on_begin() is called. up until ba.Activity.on_begin() is called.
""" """
from ba._general import WeakCall
self._called_activity_on_transition_in = True 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: def on_transition_out(self) -> None:
"""Called when your activity begins transitioning out. """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. called.
""" """
@ -552,110 +554,223 @@ class Activity(DependencyComponent):
"""Return whether on_transition_out() has been called.""" """Return whether on_transition_out() has been called."""
return self._transitioning_out return self._transitioning_out
def start_transition_in(self) -> None: def transition_in(self, prev_globals: Optional[ba.Node]) -> None:
"""Called by Session to kick of transition-in. """Called by Session to kick off transition-in.
(internal) (internal)
""" """
from ba._general import WeakCall
from ba._gameutils import sharedobj
assert not self._has_transitioned_in assert not self._has_transitioned_in
self._has_transitioned_in = True self._has_transitioned_in = True
self.on_transition_in()
def create_player_node(self, player: ba.Player) -> ba.Node: # Set up the globals node based on our settings.
"""Create the 'player' node associated with the provided ba.Player."""
from ba._nodeactor import NodeActor
with _ba.Context(self): with _ba.Context(self):
node = _ba.newnode('player', attrs={'playerID': player.get_id()}) # Now that it's going to be front and center,
# FIXME: Should add a dedicated slot for this on ba.Player # set some global values based on what the activity wants.
# instead of cluttering up their gamedata dict. glb = sharedobj('globals')
player.gamedata['_playernode'] = NodeActor(node) glb.use_fixed_vr_overlay = self.use_fixed_vr_overlay
return node glb.allow_kick_idle_players = self.allow_kick_idle_players
if self.inherits_slow_motion and prev_globals is not None:
def begin(self, session: ba.Session) -> None: glb.slow_motion = prev_globals.slow_motion
"""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)
else: else:
_error.print_error( glb.slow_motion = self.slow_motion
'got nonexistent player in Activity._begin()') 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. # If they want to inherit tint from the previous self.
for team in session.teams: if self.inherits_tint and prev_globals is not None:
if team in self.teams: glb.tint = prev_globals.tint
raise Exception('Duplicate Team Entry') 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) self.teams.append(team)
try: try:
with _ba.Context(self): self.on_team_join(team)
self.on_team_join(team)
except Exception: 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, def remove_team(self, sessionteam: ba.SessionTeam) -> None:
# and send player-joined messages for each. """(internal)"""
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)
# 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): with _ba.Context(self):
# And finally tell the game to start. # Make a decent attempt to persevere if user code breaks.
self._has_begun = True try:
self.on_begin() 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 def _sanity_check_begin_call(self) -> None:
# at some point. # Make sure ba.Activity.on_transition_in() got called at some point.
if not self._called_activity_on_transition_in: if not self._called_activity_on_transition_in:
_error.print_error( print_error(
'ba.Activity.on_transition_in() never got called for ' + 'ba.Activity.on_transition_in() never got called for ' +
str(self) + '; did you forget to call it' str(self) + '; did you forget to call it'
' in your on_transition_in override?') ' in your on_transition_in override?')
# Make sure that ba.Activity.on_begin() got called at some point. # Make sure that ba.Activity.on_begin() got called at some point.
if not self._called_activity_on_begin: if not self._called_activity_on_begin:
_error.print_error( print_error(
'ba.Activity.on_begin() never got called for ' + str(self) + 'ba.Activity.on_begin() never got called for ' + str(self) +
'; did you forget to call it in your on_begin override?') '; 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 def begin(self, session: ba.Session) -> None:
# that going now. """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: if session.wants_to_end:
session.launch_end_session_activity() session.launch_end_session_activity()
else: else:
@ -663,3 +778,30 @@ class Activity(DependencyComponent):
if self._should_end_immediately: if self._should_end_immediately:
self.end(self._should_end_immediately_results, self.end(self._should_end_immediately_results,
self._should_end_immediately_delay) self._should_end_immediately_delay)
# noinspection PyUnresolvedReferences
def _setup_player_and_team_types(self) -> None:
"""Pull player and team types from our typing.Generic params."""
# TODO: There are proper calls for pulling these in Python 3.8;
# should update this code when we adopt that.
# NOTE: If we get Any as PlayerType or TeamType (generally due
# to no generic params being passed) we automatically use the
# base class types, but also warn the user since this will mean
# less type safety for that class. (its better to pass the base
# types explicitly vs. having them be Any)
if not TYPE_CHECKING:
self._playertype = type(self).__orig_bases__[-1].__args__[0]
if not isinstance(self._playertype, type):
self._playertype = Player
print(f'ERROR: {type(self)} was not passed a Player'
f' type argument; please explicitly pass ba.Player'
f' if you do not want to override it.')
self._teamtype = type(self).__orig_bases__[-1].__args__[1]
if not isinstance(self._teamtype, type):
self._teamtype = Team
print(f'ERROR: {type(self)} was not passed a Team'
f' type argument; please explicitly pass ba.Team'
f' if you do not want to override it.')
assert issubclass(self._playertype, Player)
assert issubclass(self._teamtype, Team)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -31,16 +31,6 @@ if TYPE_CHECKING:
import ba 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): class DependencyError(Exception):
"""Exception raised when one or more ba.Dependency items are missing. """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): class TeamNotFoundError(NotFoundError):
"""Exception raised when an expected ba.Team does not exist. """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): class NodeNotFoundError(NotFoundError):
"""Exception raised when an expected ba.Node does not exist. """Exception raised when an expected ba.Node does not exist.

View File

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

View File

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

View File

@ -29,7 +29,7 @@ import _ba
if TYPE_CHECKING: if TYPE_CHECKING:
from typing import Any, Type 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') T = TypeVar('T')

View File

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

View File

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

View File

@ -33,6 +33,16 @@ if TYPE_CHECKING:
import ba 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 @dataclass
class OutOfBoundsMessage: class OutOfBoundsMessage:
"""A message telling an object that it is out of bounds. """A message telling an object that it is out of bounds.

View File

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

View File

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

View File

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

View File

@ -25,11 +25,12 @@ import weakref
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
import _ba import _ba
from ba._error import print_error, print_exception
from ba._lang import Lstr
from ba._player import Player
if TYPE_CHECKING: if TYPE_CHECKING:
from weakref import ReferenceType
from typing import Sequence, List, Dict, Any, Optional, Set from typing import Sequence, List, Dict, Any, Optional, Set
import ba import ba
@ -81,8 +82,8 @@ class Session:
lobby: ba.Lobby lobby: ba.Lobby
max_players: int max_players: int
min_players: int min_players: int
players: List[ba.Player] players: List[ba.SessionPlayer]
teams: List[ba.Team] teams: List[ba.SessionTeam]
def __init__(self, def __init__(self,
depsets: Sequence[ba.DependencySet], depsets: Sequence[ba.DependencySet],
@ -104,9 +105,11 @@ class Session:
from ba._stats import Stats from ba._stats import Stats
from ba._gameutils import sharedobj from ba._gameutils import sharedobj
from ba._gameactivity import GameActivity 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._error import DependencyError
from ba._dependency import Dependency, AssetPackage from ba._dependency import Dependency, AssetPackage
from efro.util import empty_weakref
# First off, resolve all dependency-sets we were passed. # First off, resolve all dependency-sets we were passed.
# If things are missing, we'll try to gather them into a single # 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:', # print('Would set host-session asset-reqs to:',
# required_asset_packages) # 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._sessiondata = _ba.register_session(self)
self.tournament_id: Optional[str] = None self.tournament_id: Optional[str] = None
# FIXME: This stuff shouldn't be here.
self.sharedobjs: Dict[str, Any] = {} 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.have_shown_controls_help_overlay = False
self.campaign = None self.campaign = None
# FIXME: Should be able to kill this I think.
self.campaign_state: Dict[str, str] = {} self.campaign_state: Dict[str, str] = {}
self._use_teams = (team_names is not None) self._use_teams = (team_names is not None)
@ -171,13 +165,7 @@ class Session:
self._activity_retained: Optional[ba.Activity] = None self._activity_retained: Optional[ba.Activity] = None
self.launch_end_session_activity_time: Optional[float] = None self.launch_end_session_activity_time: Optional[float] = None
self._activity_end_timer: Optional[ba.Timer] = None self._activity_end_timer: Optional[ba.Timer] = None
self._activity_weak = empty_weakref(Activity)
# 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
if self._activity_weak() is not None: if self._activity_weak() is not None:
raise Exception('Error creating empty activity weak ref.') raise Exception('Error creating empty activity weak ref.')
@ -192,10 +180,10 @@ class Session:
assert team_names is not None assert team_names is not None
assert team_colors is not None assert team_colors is not None
for i, color in enumerate(team_colors): for i, color in enumerate(team_colors):
team = Team(team_id=self._next_team_id, team = SessionTeam(team_id=self._next_team_id,
name=GameActivity.get_team_display_string( name=GameActivity.get_team_display_string(
team_names[i]), team_names[i]),
color=color) color=color)
self.teams.append(team) self.teams.append(team)
self._next_team_id += 1 self._next_team_id += 1
@ -203,15 +191,12 @@ class Session:
with _ba.Context(self): with _ba.Context(self):
self.on_team_join(team) self.on_team_join(team)
except Exception: except Exception:
from ba import _error print_exception(f'Error in on_team_join for {self}.')
_error.print_exception(
f'Error in on_team_join for {self}.')
self.lobby = Lobby() self.lobby = Lobby()
self.stats = Stats() self.stats = Stats()
# Instantiate our session globals node # Instantiate our session globals node which will apply its settings.
# (so it can apply default settings).
sharedobj('globals') sharedobj('globals')
@property @property
@ -224,12 +209,11 @@ class Session:
"""(internal)""" """(internal)"""
return self._use_team_colors 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. """Called when a new ba.Player wants to join the Session.
This should return True or False to accept/reject. This should return True or False to accept/reject.
""" """
from ba._lang import Lstr
# Limit player counts *unless* we're in a stress test. # Limit player counts *unless* we're in a stress test.
if _ba.app.stress_test_reset_timer is None: if _ba.app.stress_test_reset_timer is None:
@ -250,144 +234,88 @@ class Session:
_ba.playsound(_ba.getsound('dripity')) _ba.playsound(_ba.getsound('dripity'))
return True return True
def on_player_leave(self, player: ba.Player) -> None: def 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."""
# pylint: disable=too-many-statements
# pylint: disable=too-many-branches # 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 sessionplayer not in self.players:
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:
print('ERROR: Session.on_player_leave called' print('ERROR: Session.on_player_leave called'
' for player not in our list.') ' 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: def end(self) -> None:
"""Initiates an end to the session and a return to the main menu. """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: def launch_end_session_activity(self) -> None:
"""(internal)""" """(internal)"""
from ba import _error
from ba._activitytypes import EndSessionActivity from ba._activitytypes import EndSessionActivity
from ba._enums import TimeType from ba._enums import TimeType
with _ba.Context(self): with _ba.Context(self):
@ -412,18 +339,18 @@ class Session:
since_last = (curtime - self.launch_end_session_activity_time) since_last = (curtime - self.launch_end_session_activity_time)
if since_last < 30.0: if since_last < 30.0:
return return
_error.print_error( print_error(
'launch_end_session_activity called twice (since_last=' + 'launch_end_session_activity called twice (since_last=' +
str(since_last) + ')') str(since_last) + ')')
self.launch_end_session_activity_time = curtime self.launch_end_session_activity_time = curtime
self.set_activity(_ba.new_activity(EndSessionActivity)) self.set_activity(_ba.new_activity(EndSessionActivity))
self.wants_to_end = False 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.""" """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.""" """Called when a ba.Team is leaving the session."""
def _complete_end_activity(self, activity: ba.Activity, def _complete_end_activity(self, activity: ba.Activity,
@ -433,10 +360,8 @@ class Session:
with _ba.Context(self): with _ba.Context(self):
self.on_activity_end(activity, results) self.on_activity_end(activity, results)
except Exception: except Exception:
from ba import _error print_exception('exception in on_activity_end() for session', self,
_error.print_exception( 'activity', activity, 'with results', results)
'exception in on_activity_end() for session', self, 'activity',
activity, 'with results', results)
def end_activity(self, activity: ba.Activity, results: Any, delay: float, def end_activity(self, activity: ba.Activity, results: Any, delay: float,
force: bool) -> None: force: bool) -> None:
@ -473,8 +398,7 @@ class Session:
def handlemessage(self, msg: Any) -> Any: def handlemessage(self, msg: Any) -> Any:
"""General message handling; can be passed any message object.""" """General message handling; can be passed any message object."""
from ba._lobby import PlayerReadyMessage from ba._lobby import PlayerReadyMessage
from ba._error import UNHANDLED from ba._messages import PlayerProfilesChangedMessage, UNHANDLED
from ba._messages import PlayerProfilesChangedMessage
if isinstance(msg, PlayerReadyMessage): if isinstance(msg, PlayerReadyMessage):
self._on_player_ready(msg.chooser) self._on_player_ready(msg.chooser)
return None return None
@ -496,84 +420,46 @@ class Session:
(on_transition_in, etc) to get it. (so you can't do (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) 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._gameutils import sharedobj
from ba._enums import TimeType from ba._enums import TimeType
# Sanity test: make sure this doesn't get called recursively. # Sanity test: make sure this doesn't get called recursively.
if self._in_set_activity: if self._in_set_activity:
raise Exception( raise RuntimeError(
'Session.set_activity() cannot be called recursively.') 'Session.set_activity() cannot be called recursively.')
self._in_set_activity = True
if activity.session is not _ba.getsession(): 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. # Quietly ignore this if the whole session is going down.
if self._ending: if self._ending:
return return
if activity is self._activity_retained: if activity is self._activity_retained:
_error.print_error('activity set to already-current activity') print_error('activity set to already-current activity')
return return
if self._next_activity is not None: if self._next_activity is not None:
raise Exception('Activity switch already in progress (to ' + raise RuntimeError('Activity switch already in progress (to ' +
str(self._next_activity) + ')') str(self._next_activity) + ')')
self._in_set_activity = True
prev_activity = self._activity_retained prev_activity = self._activity_retained
if prev_activity is not None: if prev_activity is not None:
with _ba.Context(prev_activity): with _ba.Context(prev_activity):
gprev = sharedobj('globals') prev_globals = sharedobj('globals')
else: else:
gprev = None prev_globals = None
with _ba.Context(activity): # Let the activity do its thing.
activity.transition_in(prev_globals)
# 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()
self._next_activity = activity self._next_activity = activity
# If we have a current activity, tell it it's transitioning out; # If we have a current activity, tell it it's transitioning out;
# the next one will become current once this one dies. # the next one will become current once this one dies.
if prev_activity is not None: if prev_activity is not None:
# pylint: disable=protected-access prev_activity.transition_out()
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()
# Setting this to None should free up the old activity to die, # Setting this to None should free up the old activity to die,
# which will call begin_next_activity. # which will call begin_next_activity.
@ -586,35 +472,15 @@ class Session:
else: else:
self.begin_next_activity() self.begin_next_activity()
# Tell the C layer that this new activity is now 'foregrounded'. # We want to call destroy() for the previous activity once it should
# This means that its globals node controls global stuff and stuff # tear itself down, clear out any self-refs, etc. After this call
# like console operations, keyboard shortcuts, etc will run in it. # the activity should have no refs left to it and should die (which
# pylint: disable=protected-access # will trigger the next activity to run).
# 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).
if prev_activity is not None: if prev_activity is not None:
if activity.transition_time > 0.0: with _ba.Context('ui'):
# FIXME: We should tweak the activity to not allow _ba.timer(max(0.0, activity.transition_time),
# node-creation/etc when we call _destroy (or after). prev_activity.destroy,
with _ba.Context('ui'): timetype=TimeType.REAL)
# 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
self._in_set_activity = False self._in_set_activity = False
def getactivity(self) -> Optional[ba.Activity]: def getactivity(self) -> Optional[ba.Activity]:
@ -631,34 +497,29 @@ class Session:
""" """
return [] 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 we're ending, allow no new players.
if self._ending: if self._ending:
return False return False
# Ask the user. # Ask the session subclass to approve/deny this request.
try: try:
with _ba.Context(self): with _ba.Context(self):
result = self.on_player_request(player) result = self.on_player_request(sessionplayer)
except Exception: except Exception:
from ba import _error print_exception('error in on_player_request call for', self)
_error.print_exception('error in on_player_request call for', self)
result = False result = False
# If the user said yes, add the player to the session list. # If the user said yes, add the player to the session list.
if result: if result:
self.players.append(player) self.players.append(sessionplayer)
# 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.
with _ba.Context(self): with _ba.Context(self):
try: try:
self.lobby.add_chooser(player) self.lobby.add_chooser(sessionplayer)
except Exception: except Exception:
from ba import _error print_exception('exception in lobby.add_chooser()')
_error.print_exception('exception in lobby.add_chooser()')
return result return result
@ -683,17 +544,12 @@ class Session:
self._activity_weak = weakref.ref(self._next_activity) self._activity_weak = weakref.ref(self._next_activity)
self._next_activity = None self._next_activity = None
# Lets kick out any players sitting in the lobby since # Kick out anyone loitering in the lobby.
# new activities such as score screens could cover them up;
# better to have them rejoin.
self.lobby.remove_all_choosers_and_kick_players() self.lobby.remove_all_choosers_and_kick_players()
activity = self._activity_weak() self._activity_retained.begin(self)
assert activity is not None
activity.begin(self)
def _on_player_ready(self, chooser: ba.Chooser) -> None: def _on_player_ready(self, chooser: ba.Chooser) -> None:
"""Called when a ba.Player has checked themself ready.""" """Called when a ba.Player has checked themself ready."""
from ba._lang import Lstr
lobby = chooser.lobby lobby = chooser.lobby
activity = self._activity_weak() activity = self._activity_weak()
@ -711,10 +567,11 @@ class Session:
# Get our next activity going. # Get our next activity going.
self._complete_end_activity(activity, {}) self._complete_end_activity(activity, {})
else: else:
_ba.screenmessage(Lstr(resource='notEnoughPlayersText', _ba.screenmessage(
subs=[('${COUNT}', str(min_players)) Lstr(resource='notEnoughPlayersText',
]), subs=[('${COUNT}', str(min_players))]),
color=(1, 1, 0)) color=(1, 1, 0),
)
_ba.playsound(_ba.getsound('error')) _ba.playsound(_ba.getsound('error'))
else: else:
return return
@ -724,24 +581,19 @@ class Session:
self._add_chosen_player(chooser) self._add_chosen_player(chooser)
lobby.remove_chooser(chooser.getplayer()) lobby.remove_chooser(chooser.getplayer())
def _add_chosen_player(self, chooser: ba.Chooser) -> ba.Player: def _add_chosen_player(self, chooser: ba.Chooser) -> ba.SessionPlayer:
# pylint: disable=too-many-statements from ba._team import SessionTeam
# pylint: disable=too-many-branches sessionplayer = chooser.getplayer()
from ba import _error assert sessionplayer in self.players, (
from ba._lang import Lstr 'SessionPlayer not found in session '
from ba._team import Team 'player-list after chooser selection.')
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')
activity = self._activity_weak() activity = self._activity_weak()
assert activity is not None 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. # 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 # Pass it to the current activity if it has already begun
# (otherwise it'll get passed once begin is called). # (otherwise it'll get passed once begin is called).
@ -749,74 +601,51 @@ class Session:
and not activity.is_joining_activity) and not activity.is_joining_activity)
# If we're not allowing mid-game joins, don't pass; just announce # 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 pass_to_activity:
if not self._allow_mid_activity_joins: if not self._allow_mid_activity_joins:
pass_to_activity = False pass_to_activity = False
with _ba.Context(self): with _ba.Context(self):
_ba.screenmessage(Lstr(resource='playerDelayedJoinText', _ba.screenmessage(
subs=[('${PLAYER}', Lstr(resource='playerDelayedJoinText',
player.get_name(full=True)) subs=[('${PLAYER}',
]), sessionplayer.get_name(full=True))]),
color=(0, 1, 0)) 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). # (keeps mini-game coding simpler if we can always deal with teams).
if self._use_teams: if self._use_teams:
team = chooser.get_team() sessionteam = chooser.get_team()
else: else:
our_team_id = self._next_team_id 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 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: try:
with _ba.Context(self): with _ba.Context(self):
self.on_team_join(team) self.on_team_join(sessionteam)
except Exception: 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 pass_to_activity:
if team in activity.teams: activity.add_team(sessionteam)
_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}')
player.set_data(team=team, assert sessionplayer not in sessionteam.players
character=chooser.get_character_name(), sessionteam.players.append(sessionplayer)
color=chooser.get_color(), sessionplayer.set_data(team=sessionteam,
highlight=chooser.get_highlight()) 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 pass_to_activity:
if isinstance(self, _freeforallsession.FreeForAllSession): activity.add_player(sessionplayer)
if player.team.players: return sessionplayer
_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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -142,7 +142,7 @@ class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
h_align=Text.HAlign.CENTER, h_align=Text.HAlign.CENTER,
transition_delay=t_incr * 4).autoretain() 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 lose_score = 0
for team in self.teams: for team in self.teams:
if team.sessiondata['score'] != win_score: if team.sessiondata['score'] != win_score:
@ -344,7 +344,7 @@ class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
if not self.is_transitioning_out(): if not self.is_transitioning_out():
ba.setmusic(ba.MusicType.VICTORY) 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.image import Image
from bastd.actor.zoomtext import ZoomText from bastd.actor.zoomtext import ZoomText
if not self._is_ffa: if not self._is_ffa:

View File

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

View File

@ -136,7 +136,7 @@ class RespawnIcon:
"""Return info on where we should be shown and stored.""" """Return info on where we should be shown and stored."""
activity = ba.getactivity() activity = ba.getactivity()
if isinstance(ba.getsession(), ba.DualTeamSession): 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. # Store a list of icons in the team.
try: try:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -38,7 +38,7 @@ if TYPE_CHECKING:
# ba_meta export game # 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 A co-op game where you try to defeat a group
of Ninjas as fast as possible of Ninjas as fast as possible
@ -148,7 +148,7 @@ class NinjaFightGame(ba.TeamGameActivity):
# A player has died. # A player has died.
if isinstance(msg, playerspaz.PlayerSpazDeathMessage): if isinstance(msg, playerspaz.PlayerSpazDeathMessage):
super().handlemessage(msg) # do standard stuff 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. # A spaz-bot has died.
elif isinstance(msg, spazbot.SpazBotDeathMessage): elif isinstance(msg, spazbot.SpazBotDeathMessage):

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -31,22 +31,19 @@ from pathlib import Path
from threading import Lock, Thread, current_thread from threading import Lock, Thread, current_thread
from typing import TYPE_CHECKING 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 bacommon.servermanager import ServerConfig, StartServerModeCommand
from efro.dataclasses import dataclass_assign, dataclass_validate from efro.dataclasses import dataclass_assign, dataclass_validate
from efro.error import CleanError from efro.error import CleanError
from efro.terminal import Clr 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: if TYPE_CHECKING:
from typing import Optional, List, Dict, Union, Tuple from typing import Optional, List, Dict, Union, Tuple
from types import FrameType 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 # Not sure how much versioning we'll do with this, but this will get
# printed at startup in case we need it. # printed at startup in case we need it.
VERSION_STR = '1.0' VERSION_STR = '1.0.1'
class ServerManagerApp: class ServerManagerApp:
@ -432,6 +429,10 @@ class ServerManagerApp:
def main() -> None: def main() -> None:
"""Run a BallisticaCore server manager in interactive mode.""" """Run a BallisticaCore server manager in interactive mode."""
try: 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() ServerManagerApp().run_interactive()
except CleanError as exc: except CleanError as exc:
# For clean errors, do a simple print and fail; no tracebacks/etc. # For clean errors, do a simple print and fail; no tracebacks/etc.

View File

@ -3,6 +3,21 @@ mypy_path = __EFRO_PROJECT_ROOT__/tools:__EFRO_PROJECT_ROOT__/assets/src/ba_data
__EFRO_MYPY_STANDARD_SETTINGS__ __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.*] [mypy-pylint.*]
ignore_missing_imports = True ignore_missing_imports = True

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -237,7 +237,7 @@ class App:
with open(self._state_data_path, 'r') as infile: with open(self._state_data_path, 'r') as infile:
self._state = StateData(**json.loads(infile.read())) self._state = StateData(**json.loads(infile.read()))
except Exception: 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}') f' resetting to defaults.{Clr.RST}')
def _save_state(self) -> None: def _save_state(self) -> None:
@ -285,7 +285,7 @@ class App:
return response return response
def _upload_file(self, filename: str, call: str, args: Dict) -> None: 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: with tempfile.TemporaryDirectory() as tempdir:
srcpath = Path(filename) srcpath = Path(filename)
gzpath = Path(tempdir, 'file.gz') gzpath = Path(tempdir, 'file.gz')

View File

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

View File

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

View File

@ -27,7 +27,9 @@ import inspect
import logging import logging
from collections import abc from collections import abc
from enum import Enum 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._base import DataHandler, BaseField
from efro.entity.util import compound_eq from efro.entity.util import compound_eq

View File

@ -24,17 +24,24 @@ from __future__ import annotations
import datetime import datetime
import time import time
import weakref
from typing import TYPE_CHECKING, cast, TypeVar, Generic from typing import TYPE_CHECKING, cast, TypeVar, Generic
if TYPE_CHECKING: if TYPE_CHECKING:
import asyncio 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') TVAL = TypeVar('TVAL')
TARG = TypeVar('TARG') TARG = TypeVar('TARG')
TRET = TypeVar('TRET') TRET = TypeVar('TRET')
class _EmptyObj:
pass
def utc_now() -> datetime.datetime: def utc_now() -> datetime.datetime:
"""Get offset-aware current utc time. """Get offset-aware current utc time.
@ -46,6 +53,15 @@ def utc_now() -> datetime.datetime:
return datetime.datetime.now(datetime.timezone.utc) 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: def data_size_str(bytecount: int) -> str:
"""Given a size in bytes, returns a short human readable string. """Given a size in bytes, returns a short human readable string.

View File

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

View File

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

View File

@ -28,7 +28,7 @@ import astroid
if TYPE_CHECKING: if TYPE_CHECKING:
from astroid import node_classes as nc from astroid import node_classes as nc
from typing import Set, Dict, Any from typing import Set, Dict, Any, List
VERBOSE = False VERBOSE = False
@ -199,9 +199,49 @@ def var_annotations_filter(node: nc.NodeNG) -> nc.NodeNG:
return node 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: def register_plugins(manager: astroid.Manager) -> None:
"""Apply our transforms to a given astroid manager object.""" """Apply our transforms to a given astroid manager object."""
# Hmm; is this still necessary?
if VERBOSE: if VERBOSE:
manager.register_failed_import_hook(failed_import_hook) 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. # check code as if it doesn't exist at all.
manager.register_transform(astroid.If, ignore_type_check_filter) 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) manager.register_transform(astroid.Call, ignore_reveal_type_call)
# Annotations on variables within a function are defer-eval'ed # We make use of 'from __future__ import annotations' which causes Python
# in some cases, so lets replace them with simple strings in those # to receive annotations as strings, and also 'if TYPE_CHECKING:' blocks,
# cases to avoid type complaints. # which lets us do imports and whatnot that are limited to type-checking.
# (mypy will still properly alert us to type errors for them) # Let's make Pylint understand these.
manager.register_transform(astroid.AnnAssign, var_annotations_filter) manager.register_transform(astroid.AnnAssign, var_annotations_filter)
manager.register_transform(astroid.FunctionDef, func_annotations_filter) manager.register_transform(astroid.FunctionDef, func_annotations_filter)
manager.register_transform(astroid.AsyncFunctionDef, manager.register_transform(astroid.AsyncFunctionDef,
func_annotations_filter) 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) register_plugins(astroid.MANAGER)

View File

@ -54,7 +54,7 @@ def snippets_main(globs: Dict[str, Any]) -> None:
show_help = False show_help = False
retval = 0 retval = 0
if len(sys.argv) < 2: 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 show_help = True
retval = 255 retval = 255
else: else:
@ -67,13 +67,17 @@ def snippets_main(globs: Dict[str, Any]) -> None:
else: else:
docs = _trim_docstring( docs = _trim_docstring(
getattr(funcs[sys.argv[2]], '__doc__', '<no docs>')) getattr(funcs[sys.argv[2]], '__doc__', '<no docs>'))
print('\nsnippets ' + sys.argv[2] + ':\n' + docs + '\n') print(f'\n{Clr.MAG}{Clr.BLD}snippets {sys.argv[2]}:{Clr.RST}\n'
f'{Clr.MAG}{docs}{Clr.RST}\n')
elif sys.argv[1] in funcs: elif sys.argv[1] in funcs:
try: try:
funcs[sys.argv[1]]() funcs[sys.argv[1]]()
except KeyboardInterrupt as exc:
print(f'{Clr.RED}{exc}{Clr.RST}')
sys.exit(1)
except CleanError as exc: except CleanError as exc:
exc.pretty_print() exc.pretty_print()
sys.exit(-1) sys.exit(1)
else: else:
print('Unknown snippets command: "' + sys.argv[1] + '"', print('Unknown snippets command: "' + sys.argv[1] + '"',
file=sys.stderr) file=sys.stderr)
@ -82,11 +86,12 @@ def snippets_main(globs: Dict[str, Any]) -> None:
if show_help: if show_help:
print('Snippets contains project related commands too small' print('Snippets contains project related commands too small'
' to warrant full scripts.') ' to warrant full scripts.')
print("Run 'snippets help <COMMAND>' for full command documentation.") print(f"Run {Clr.MAG}'snippets help {Clr.BLD}<COMMAND>'"
f'{Clr.RST} for full command documentation.')
print('Available commands:') print('Available commands:')
for func, obj in sorted(funcs.items()): for func, obj in sorted(funcs.items()):
doc = getattr(obj, '__doc__', '').splitlines()[0].strip() 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) sys.exit(retval)
@ -194,16 +199,16 @@ def check_clean_safety() -> None:
def formatcode() -> None: def formatcode() -> None:
"""Run clang-format on all of our source code (multithreaded).""" """Run clang-format on all of our source code (multithreaded)."""
from efrotools import code import efrotools.code
full = '-full' in sys.argv full = '-full' in sys.argv
code.formatcode(PROJROOT, full) efrotools.code.formatcode(PROJROOT, full)
def formatscripts() -> None: def formatscripts() -> None:
"""Run yapf on all our scripts (multithreaded).""" """Run yapf on all our scripts (multithreaded)."""
from efrotools import code import efrotools.code
full = '-full' in sys.argv full = '-full' in sys.argv
code.formatscripts(PROJROOT, full) efrotools.code.formatscripts(PROJROOT, full)
def formatmakefile() -> None: def formatmakefile() -> None:
@ -222,9 +227,9 @@ def formatmakefile() -> None:
def cpplint() -> None: def cpplint() -> None:
"""Run lint-checking on all code deemed lint-able.""" """Run lint-checking on all code deemed lint-able."""
from efrotools import code import efrotools.code
full = '-full' in sys.argv full = '-full' in sys.argv
code.cpplint(PROJROOT, full) efrotools.code.cpplint(PROJROOT, full)
def scriptfiles() -> None: def scriptfiles() -> None:
@ -232,8 +237,8 @@ def scriptfiles() -> None:
Pass -lines to use newlines as separators. The default is spaces. Pass -lines to use newlines as separators. The default is spaces.
""" """
from efrotools import code import efrotools.code
paths = code.get_script_filenames(projroot=PROJROOT) paths = efrotools.code.get_script_filenames(projroot=PROJROOT)
assert not any(' ' in path for path in paths) assert not any(' ' in path for path in paths)
if '-lines' in sys.argv: if '-lines' in sys.argv:
print('\n'.join(paths)) print('\n'.join(paths))
@ -243,101 +248,102 @@ def scriptfiles() -> None:
def pylint() -> None: def pylint() -> None:
"""Run pylint checks on our scripts.""" """Run pylint checks on our scripts."""
from efrotools import code from efro.error import CleanError
import efrotools.code
full = ('-full' in sys.argv) full = ('-full' in sys.argv)
fast = ('-fast' 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: def mypy() -> None:
"""Run mypy checks on our scripts.""" """Run mypy checks on our scripts."""
from efrotools import code import efrotools.code
full = ('-full' in sys.argv) 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: def dmypy() -> None:
"""Run mypy checks on our scripts using the mypy daemon.""" """Run mypy checks on our scripts using the mypy daemon."""
from efrotools import code import efrotools.code
code.dmypy(PROJROOT) efrotools.code.dmypy(PROJROOT)
def pycharm() -> None: def pycharm() -> None:
"""Run PyCharm checks on our scripts.""" """Run PyCharm checks on our scripts."""
from efrotools import code import efrotools.code
full = '-full' in sys.argv full = '-full' in sys.argv
verbose = '-v' in sys.argv verbose = '-v' in sys.argv
code.pycharm(PROJROOT, full, verbose) efrotools.code.pycharm(PROJROOT, full, verbose)
def clioncode() -> None: def clioncode() -> None:
"""Run CLion checks on our code.""" """Run CLion checks on our code."""
from efrotools import code import efrotools.code
full = '-full' in sys.argv full = '-full' in sys.argv
verbose = '-v' in sys.argv verbose = '-v' in sys.argv
code.clioncode(PROJROOT, full, verbose) efrotools.code.clioncode(PROJROOT, full, verbose)
def androidstudiocode() -> None: def androidstudiocode() -> None:
"""Run Android Studio checks on our code.""" """Run Android Studio checks on our code."""
from efrotools import code import efrotools.code
full = '-full' in sys.argv full = '-full' in sys.argv
verbose = '-v' in sys.argv verbose = '-v' in sys.argv
code.androidstudiocode(PROJROOT, full, verbose) efrotools.code.androidstudiocode(PROJROOT, full, verbose)
def tool_config_install() -> None: def tool_config_install() -> None:
"""Install a tool config file (with some filtering).""" """Install a tool config file (with some filtering)."""
from efrotools import get_config from efro.terminal import Clr
import textwrap
if len(sys.argv) != 4: if len(sys.argv) != 4:
raise Exception('expected 2 args') raise Exception('expected 2 args')
src = Path(sys.argv[2]) src = Path(sys.argv[2])
dst = Path(sys.argv[3]) dst = Path(sys.argv[3])
print(f'Creating tool config: {Clr.BLD}{dst}{Clr.RST}')
with src.open() as infile: with src.open() as infile:
cfg = infile.read() cfg = infile.read()
# Do a bit of filtering. # Rome substitutions, etc.
cfg = _filter_tool_config(cfg)
# 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)
# Add an auto-generated notice. # Add an auto-generated notice.
comment = None comment = None
@ -356,6 +362,57 @@ def tool_config_install() -> None:
outfile.write(cfg) 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: def sync_all() -> None:
"""Runs full syncs between all efrotools projects. """Runs full syncs between all efrotools projects.
@ -368,7 +425,7 @@ def sync_all() -> None:
import concurrent.futures import concurrent.futures
from efro.error import CleanError from efro.error import CleanError
from efro.terminal import Clr 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') projects_str = os.environ.get('EFROTOOLS_SYNC_PROJECTS')
if projects_str is None: if projects_str is None:
raise CleanError('EFROTOOL_SYNC_PROJECTS is not defined.') raise CleanError('EFROTOOL_SYNC_PROJECTS is not defined.')
@ -399,17 +456,17 @@ def sync_all() -> None:
# Real mode # Real mode
for i in range(2): for i in range(2):
if i == 0: 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)' + ' (ensures all changes at dsts are pushed to src)' +
Clr.RST) Clr.RST)
else: 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) ' (ensures latest src is pulled to all dsts)' + Clr.RST)
for project in projects_str.split(':'): for project in projects_str.split(':'):
cmd = f'cd "{project}" && make sync-full' cmd = f'cd "{project}" && make sync-full'
print(cmd) print(cmd)
subprocess.run(cmd, shell=True, check=True) 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: def sync() -> None:
@ -530,5 +587,26 @@ def makefile_target_list() -> None:
continue continue
print('\n' + entry.title + '\n' + '-' * len(entry.title)) print('\n' + entry.title + '\n' + '-' * len(entry.title))
elif entry.kind == 'target': elif entry.kind == 'target':
print(Clr.SMAG + entry.title + Clr.SBLU + print(Clr.MAG + entry.title + Clr.BLU +
_docstr(lines, entry.line) + Clr.RST) _docstr(lines, entry.line) + Clr.RST)
def echo() -> None:
"""Echo with support for efro.terminal.Clr args (RED, GRN, BLU, etc).
Prints a Clr.RST at the end so that can be omitted.
"""
from efro.terminal import Clr
clrnames = {n for n in dir(Clr) if n.isupper() and not n.startswith('_')}
first = True
out: List[str] = []
for arg in sys.argv[2:]:
if arg in clrnames:
out.append(getattr(Clr, arg))
else:
if not first:
out.append(' ')
first = False
out.append(arg)
out.append(Clr.RST)
print(''.join(out))

View File

@ -40,9 +40,10 @@ from typing import TYPE_CHECKING
# noinspection PyUnresolvedReferences # noinspection PyUnresolvedReferences
from efrotools.snippets import ( from efrotools.snippets import (
PROJROOT, snippets_main, formatcode, formatscripts, formatmakefile, PROJROOT, snippets_main, formatcode, formatscripts, formatmakefile,
cpplint, pylint, mypy, dmypy, tool_config_install, sync, sync_all, cpplint, pylint, runpylint, mypy, runmypy, dmypy, tool_config_install,
scriptfiles, pycharm, clioncode, androidstudiocode, makefile_target_list, sync, sync_all, scriptfiles, pycharm, clioncode, androidstudiocode,
spelling, spelling_all, compile_python_files, pytest) makefile_target_list, spelling, spelling_all, compile_python_files, pytest,
echo)
# pylint: enable=unused-import # pylint: enable=unused-import
if TYPE_CHECKING: if TYPE_CHECKING:
@ -259,7 +260,7 @@ def clean_orphaned_assets() -> None:
# Operate from dist root.. # Operate from dist root..
os.chdir(PROJROOT) 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: with open('assets/.asset_manifest_public.json') as infile:
manifest = set(json.loads(infile.read())) manifest = set(json.loads(infile.read()))
with open('assets/.asset_manifest_private.json') as infile: with open('assets/.asset_manifest_private.json') as infile:
@ -303,7 +304,8 @@ def py_examine() -> None:
sys.path.append(scriptsdir) sys.path.append(scriptsdir)
if toolsdir not in sys.path: if toolsdir not in sys.path:
sys.path.append(toolsdir) 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: def push_ipa() -> None:
@ -553,6 +555,9 @@ def lazy_increment_build() -> None:
"""Increment build number only if C++ sources have changed. """Increment build number only if C++ sources have changed.
This is convenient to place in automatic commit/push scripts. 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 os
import subprocess import subprocess

View File

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