mirror of
https://github.com/RYDE-WORK/ballistica.git
synced 2026-01-19 21:37:57 +08:00
Added per-game team and player types
This commit is contained in:
parent
b1d7d96386
commit
1ccf33e41d
19
.idea/dictionaries/ericf.xml
generated
19
.idea/dictionaries/ericf.xml
generated
@ -319,6 +319,7 @@
|
||||
<w>cnode</w>
|
||||
<w>codecsmodule</w>
|
||||
<w>codefilenames</w>
|
||||
<w>codefiles</w>
|
||||
<w>codehash</w>
|
||||
<w>codeop</w>
|
||||
<w>collapsable</w>
|
||||
@ -727,8 +728,10 @@
|
||||
<w>gamepads</w>
|
||||
<w>gamepadselect</w>
|
||||
<w>gameplay</w>
|
||||
<w>gameplayer</w>
|
||||
<w>gameport</w>
|
||||
<w>gameresults</w>
|
||||
<w>gameteam</w>
|
||||
<w>gametype</w>
|
||||
<w>gametypes</w>
|
||||
<w>gameutils</w>
|
||||
@ -762,6 +765,7 @@
|
||||
<w>getsession</w>
|
||||
<w>getsockname</w>
|
||||
<w>getsound</w>
|
||||
<w>getspaz</w>
|
||||
<w>getstarttime</w>
|
||||
<w>gettext</w>
|
||||
<w>gettexture</w>
|
||||
@ -893,6 +897,7 @@
|
||||
<w>inputfiles</w>
|
||||
<w>inputhash</w>
|
||||
<w>inputnode</w>
|
||||
<w>inputtype</w>
|
||||
<w>inpututils</w>
|
||||
<w>inspectdir</w>
|
||||
<w>insta</w>
|
||||
@ -1319,6 +1324,7 @@
|
||||
<w>pipname</w>
|
||||
<w>pkey</w>
|
||||
<w>pkgutil</w>
|
||||
<w>playercast</w>
|
||||
<w>playerdata</w>
|
||||
<w>playerlostspaz</w>
|
||||
<w>playernode</w>
|
||||
@ -1326,6 +1332,8 @@
|
||||
<w>playerpts</w>
|
||||
<w>playerrec</w>
|
||||
<w>playerspaz</w>
|
||||
<w>playerteamdata</w>
|
||||
<w>playertype</w>
|
||||
<w>playerval</w>
|
||||
<w>playlistui</w>
|
||||
<w>playmode</w>
|
||||
@ -1358,6 +1366,7 @@
|
||||
<w>positionadjusted</w>
|
||||
<w>posixpath</w>
|
||||
<w>posixsubprocess</w>
|
||||
<w>postinit</w>
|
||||
<w>poststr</w>
|
||||
<w>powerdown</w>
|
||||
<w>powersgiven</w>
|
||||
@ -1368,6 +1377,7 @@
|
||||
<w>poweruptype</w>
|
||||
<w>powervr</w>
|
||||
<w>ppos</w>
|
||||
<w>pproxy</w>
|
||||
<w>pragmas</w>
|
||||
<w>prch</w>
|
||||
<w>prec</w>
|
||||
@ -1547,6 +1557,7 @@
|
||||
<w>runmypy</w>
|
||||
<w>runonly</w>
|
||||
<w>runpy</w>
|
||||
<w>runpylint</w>
|
||||
<w>runswindows</w>
|
||||
<w>rval</w>
|
||||
<w>safecolor</w>
|
||||
@ -1610,6 +1621,8 @@
|
||||
<w>sessiondata</w>
|
||||
<w>sessionglobals</w>
|
||||
<w>sessionname</w>
|
||||
<w>sessionplayer</w>
|
||||
<w>sessionteam</w>
|
||||
<w>sessiontype</w>
|
||||
<w>setalpha</w>
|
||||
<w>setbuild</w>
|
||||
@ -1685,6 +1698,7 @@
|
||||
<w>srcnode</w>
|
||||
<w>srcpath</w>
|
||||
<w>srcpathfull</w>
|
||||
<w>srcplayer</w>
|
||||
<w>srcpy</w>
|
||||
<w>srcpydata</w>
|
||||
<w>srcstr</w>
|
||||
@ -1720,6 +1734,7 @@
|
||||
<w>strftime</w>
|
||||
<w>stringprep</w>
|
||||
<w>stringptr</w>
|
||||
<w>strippable</w>
|
||||
<w>strobing</w>
|
||||
<w>strptime</w>
|
||||
<w>strt</w>
|
||||
@ -1785,11 +1800,13 @@
|
||||
<w>tdelay</w>
|
||||
<w>tdval</w>
|
||||
<w>teambasesession</w>
|
||||
<w>teamdata</w>
|
||||
<w>teamgame</w>
|
||||
<w>teamnamescolors</w>
|
||||
<w>teamsize</w>
|
||||
<w>teamsscorescreen</w>
|
||||
<w>teamssession</w>
|
||||
<w>teamtype</w>
|
||||
<w>teeeeeeeesssssttttdddddddd</w>
|
||||
<w>teehee</w>
|
||||
<w>teleporting</w>
|
||||
@ -1867,11 +1884,13 @@
|
||||
<w>toplevel</w>
|
||||
<w>totaldudes</w>
|
||||
<w>totalpts</w>
|
||||
<w>totype</w>
|
||||
<w>touchpad</w>
|
||||
<w>tournamententry</w>
|
||||
<w>tournamentscores</w>
|
||||
<w>tplayer</w>
|
||||
<w>tpos</w>
|
||||
<w>tproxy</w>
|
||||
<w>tracebacks</w>
|
||||
<w>tracemalloc</w>
|
||||
<w>tradeoff</w>
|
||||
|
||||
@ -50,6 +50,9 @@
|
||||
- The bs.Vector class is no more; in its place is a shiny new ba.Vec3 which is implemented internally in C++ so its nice and speedy. Will probably update certain things like vector node attrs to support this class in the future since it makes vector math nice and convenient.
|
||||
- Ok you get the point..
|
||||
|
||||
### 1.4.155 (14377)
|
||||
- Added protection against a repeated-input attack in lobbies.
|
||||
|
||||
### 1.4.151 (14371)
|
||||
- Added Chinese-Traditional language and improved translations for others.
|
||||
|
||||
|
||||
39
Makefile
39
Makefile
@ -25,7 +25,7 @@
|
||||
# Targets in this top level Makefile do not expect -jX to be passed to them
|
||||
# and generally handle spawning an appropriate number of child jobs themselves.
|
||||
|
||||
# Prefix used for output of docs/changelogs/etc targets for use in webpages.
|
||||
# Prefix used for output of docs/changelogs/etc. targets for use in webpages.
|
||||
DOCPREFIX = "ballisticacore_"
|
||||
|
||||
|
||||
@ -475,12 +475,12 @@ update-check: prereqs
|
||||
# Run formatting on all files in the project considered 'dirty'.
|
||||
format:
|
||||
@${MAKE} -j3 format-code format-scripts format-makefile
|
||||
@echo Formatting complete!
|
||||
@tools/snippets echo GRN Formatting complete!
|
||||
|
||||
# Same but always formats; ignores dirty state.
|
||||
format-full:
|
||||
@${MAKE} -j3 format-code-full format-scripts-full format-makefile
|
||||
@echo Formatting complete!
|
||||
@tools/snippets echo GRN Formatting complete!
|
||||
|
||||
# Run formatting for compiled code sources (.cc, .h, etc.).
|
||||
format-code: prereqs
|
||||
@ -515,22 +515,22 @@ format-makefile: prereqs
|
||||
# Run all project checks. (static analysis)
|
||||
check: update-check
|
||||
@${MAKE} -j3 cpplint pylint mypy
|
||||
@echo ALL CHECKS PASSED!
|
||||
@tools/snippets echo GRN ALL CHECKS PASSED!
|
||||
|
||||
# Same as check but no caching (all files are checked).
|
||||
check-full: update-check
|
||||
@${MAKE} -j3 cpplint-full pylint-full mypy-full
|
||||
@echo ALL CHECKS PASSED!
|
||||
@tools/snippets echo GRN ALL CHECKS PASSED!
|
||||
|
||||
# Same as 'check' plus optional/slow extra checks.
|
||||
check2: update-check
|
||||
@${MAKE} -j4 cpplint pylint mypy pycharm
|
||||
@echo ALL CHECKS PASSED!
|
||||
@tools/snippets echo GRN ALL CHECKS PASSED!
|
||||
|
||||
# Same as check2 but no caching (all files are checked).
|
||||
check2-full: update-check
|
||||
@${MAKE} -j4 cpplint-full pylint-full mypy-full pycharm-full
|
||||
@echo ALL CHECKS PASSED!
|
||||
@tools/snippets echo GRN ALL CHECKS PASSED!
|
||||
|
||||
# Run Cpplint checks on all C/C++ code.
|
||||
cpplint: prereqs
|
||||
@ -586,6 +586,7 @@ pycharm-full: prereqs
|
||||
|
||||
# Run all tests. (live execution verification)
|
||||
test: prereqs
|
||||
@tools/snippets echo BLU Running all tests...
|
||||
@tools/snippets pytest -v tests
|
||||
|
||||
# Run tests with any caching disabled.
|
||||
@ -615,28 +616,28 @@ preflight:
|
||||
@${MAKE} format
|
||||
@${MAKE} update
|
||||
@${MAKE} -j4 cpplint pylint mypy test
|
||||
@echo PREFLIGHT SUCCESSFUL!
|
||||
@tools/snippets echo SGRN BLD PREFLIGHT SUCCESSFUL!
|
||||
|
||||
# Same as 'preflight' without caching (all files are visited).
|
||||
preflight-full:
|
||||
@${MAKE} format-full
|
||||
@${MAKE} update
|
||||
@${MAKE} -j4 cpplint-full pylint-full mypy-full test-full
|
||||
@echo PREFLIGHT SUCCESSFUL!
|
||||
@tools/snippets echo SGRN BLD PREFLIGHT SUCCESSFUL!
|
||||
|
||||
# Same as 'preflight' plus optional/slow extra checks.
|
||||
preflight2:
|
||||
@${MAKE} format
|
||||
@${MAKE} update
|
||||
@${MAKE} -j5 cpplint pylint mypy pycharm test
|
||||
@echo PREFLIGHT SUCCESSFUL!
|
||||
@tools/snippets echo SGRN BLD PREFLIGHT SUCCESSFUL!
|
||||
|
||||
# Same as 'preflight2' but without caching (all files visited).
|
||||
preflight2-full:
|
||||
@${MAKE} format-full
|
||||
@${MAKE} update
|
||||
@${MAKE} -j5 cpplint-full pylint-full mypy-full pycharm-full test-full
|
||||
@echo PREFLIGHT SUCCESSFUL!
|
||||
@tools/snippets echo SGRN BLD PREFLIGHT SUCCESSFUL!
|
||||
|
||||
# Tell make which of these targets don't represent files.
|
||||
.PHONY: preflight preflight-full preflight2 preflight2-full
|
||||
@ -675,28 +676,28 @@ TOOL_CFG_SRC = tools/efrotools/snippets.py config/config.json
|
||||
ENV_SRC = tools/snippets tools/batools/build.py
|
||||
|
||||
.clang-format: config/toolconfigsrc/clang-format ${TOOL_CFG_SRC}
|
||||
${TOOL_CFG_INST} $< $@
|
||||
@${TOOL_CFG_INST} $< $@
|
||||
|
||||
.style.yapf: config/toolconfigsrc/style.yapf ${TOOL_CFG_SRC}
|
||||
${TOOL_CFG_INST} $< $@
|
||||
@${TOOL_CFG_INST} $< $@
|
||||
|
||||
.pylintrc: config/toolconfigsrc/pylintrc ${TOOL_CFG_SRC}
|
||||
${TOOL_CFG_INST} $< $@
|
||||
@${TOOL_CFG_INST} $< $@
|
||||
|
||||
.projectile: config/toolconfigsrc/projectile ${TOOL_CFG_SRC}
|
||||
${TOOL_CFG_INST} $< $@
|
||||
@${TOOL_CFG_INST} $< $@
|
||||
|
||||
.editorconfig: config/toolconfigsrc/editorconfig ${TOOL_CFG_SRC}
|
||||
${TOOL_CFG_INST} $< $@
|
||||
@${TOOL_CFG_INST} $< $@
|
||||
|
||||
.dir-locals.el: config/toolconfigsrc/dir-locals.el ${TOOL_CFG_SRC}
|
||||
${TOOL_CFG_INST} $< $@
|
||||
@${TOOL_CFG_INST} $< $@
|
||||
|
||||
.mypy.ini: config/toolconfigsrc/mypy.ini ${TOOL_CFG_SRC}
|
||||
${TOOL_CFG_INST} $< $@
|
||||
@${TOOL_CFG_INST} $< $@
|
||||
|
||||
.pycheckers: config/toolconfigsrc/pycheckers ${TOOL_CFG_SRC}
|
||||
${TOOL_CFG_INST} $< $@
|
||||
@${TOOL_CFG_INST} $< $@
|
||||
|
||||
# Include anything as sources here that should require
|
||||
.cache/checkenv: ${ENV_SRC}
|
||||
|
||||
@ -34,7 +34,7 @@ NOTE: This file was autogenerated by gendummymodule; do not edit by hand.
|
||||
"""
|
||||
|
||||
# (hash we can use to see if this file is out of date)
|
||||
# SOURCES_HASH=233656876767477563471907026700832716822
|
||||
# SOURCES_HASH=266649817838802754126771358652920545389
|
||||
|
||||
# I'm sorry Pylint. I know this file saddens you. Be strong.
|
||||
# pylint: disable=useless-suppression
|
||||
@ -259,7 +259,7 @@ class InputDevice:
|
||||
allows_configuring: bool
|
||||
Whether the input-device can be configured.
|
||||
|
||||
player: Optional[ba.Player]
|
||||
player: Optional[ba.SessionPlayer]
|
||||
The player associated with this input device.
|
||||
|
||||
client_id: int
|
||||
@ -292,7 +292,7 @@ class InputDevice:
|
||||
"""
|
||||
exists: bool
|
||||
allows_configuring: bool
|
||||
player: Optional[ba.Player]
|
||||
player: Optional[ba.SessionPlayer]
|
||||
client_id: int
|
||||
name: str
|
||||
unique_identifier: str
|
||||
@ -648,6 +648,14 @@ class Node:
|
||||
billboard_texture: Optional[ba.Texture] = None
|
||||
billboard_cross_out: bool = False
|
||||
billboard_opacity: float = 0.0
|
||||
slow_motion: bool = False
|
||||
music: str = ''
|
||||
vr_camera_offset: Sequence[float] = (0.0, 0.0, 0.0)
|
||||
vr_overlay_center: Sequence[float] = (0.0, 0.0, 0.0)
|
||||
vr_overlay_center_enabled: bool = False
|
||||
vignette_outer: Sequence[float] = (0.0, 0.0)
|
||||
vignette_inner: Sequence[float] = (0.0, 0.0)
|
||||
tint: Sequence[float] = (1.0, 1.0, 1.0)
|
||||
|
||||
def add_death_action(self, action: Callable[[], None]) -> None:
|
||||
"""add_death_action(action: Callable[[], None]) -> None
|
||||
@ -737,31 +745,36 @@ class Node:
|
||||
return None
|
||||
|
||||
|
||||
class Player:
|
||||
"""A reference to a player in the game.
|
||||
class SessionData:
|
||||
"""(internal)"""
|
||||
|
||||
def exists(self) -> bool:
|
||||
"""exists() -> bool
|
||||
|
||||
Returns whether the SessionData still exists.
|
||||
Most functionality will fail on a nonexistent instance.
|
||||
"""
|
||||
return bool()
|
||||
|
||||
|
||||
class SessionPlayer:
|
||||
"""A reference to a player in the ba.Session.
|
||||
|
||||
Category: Gameplay Classes
|
||||
|
||||
These are created and managed internally and
|
||||
provided to your Session/Activity instances.
|
||||
Be aware that, like ba.Nodes, ba.Player objects are 'weak'
|
||||
Be aware that, like ba.Nodes, ba.SessionPlayer objects are 'weak'
|
||||
references under-the-hood; a player can leave the game at
|
||||
any point. For this reason, you should make judicious use of the
|
||||
ba.Player.exists attribute (or boolean operator) to ensure that a
|
||||
Player is still present if retaining references to one for any
|
||||
length of time.
|
||||
ba.SessionPlayer.exists attribute (or boolean operator) to ensure
|
||||
that a SessionPlayer is still present if retaining references to one
|
||||
for any length of time.
|
||||
|
||||
Attributes:
|
||||
|
||||
actor: Optional[ba.Actor]
|
||||
The current ba.Actor associated with this Player.
|
||||
This may be None
|
||||
|
||||
node: Optional[ba.Node]
|
||||
A ba.Node of type 'player' associated with this Player.
|
||||
This Node exists in the currently active game and can be used
|
||||
to get a generic player position/etc.
|
||||
This will be None if the Player is not in a game.
|
||||
id: int
|
||||
The unique numeric ID of the Player.
|
||||
|
||||
exists: bool
|
||||
Whether the player still exists.
|
||||
@ -775,10 +788,10 @@ class Player:
|
||||
This bool value will be True once the Player has completed
|
||||
any lobby character/team selection.
|
||||
|
||||
team: ba.Team
|
||||
The ba.Team this Player is on. If the Player is
|
||||
still in its lobby selecting a team/etc. then a
|
||||
ba.TeamNotFoundError will be raised.
|
||||
team: ba.SessionTeam
|
||||
The ba.SessionTeam this Player is on. If the SessionPlayer
|
||||
is still in its lobby selecting a team/etc. then a
|
||||
ba.SessionTeamNotFoundError will be raised.
|
||||
|
||||
sessiondata: Dict
|
||||
A dict for use by the current ba.Session for
|
||||
@ -792,7 +805,7 @@ class Player:
|
||||
|
||||
color: Sequence[float]
|
||||
The base color for this Player.
|
||||
In team games this will match the ba.Team's color.
|
||||
In team games this will match the ba.SessionTeam's color.
|
||||
|
||||
highlight: Sequence[float]
|
||||
A secondary color for this player.
|
||||
@ -802,17 +815,20 @@ class Player:
|
||||
|
||||
character: str
|
||||
The character this player has selected in their profile.
|
||||
|
||||
gameplayer: Optional[ba.Player]
|
||||
The current game-specific instance for this player.
|
||||
"""
|
||||
actor: Optional[ba.Actor]
|
||||
node: Optional[ba.Node]
|
||||
id: int
|
||||
exists: bool
|
||||
in_game: bool
|
||||
team: ba.Team
|
||||
team: ba.SessionTeam
|
||||
sessiondata: Dict
|
||||
gamedata: Dict
|
||||
color: Sequence[float]
|
||||
highlight: Sequence[float]
|
||||
character: str
|
||||
gameplayer: Optional[ba.Player]
|
||||
|
||||
def assign_input_call(self, type: Union[str, Tuple[str, ...]],
|
||||
call: Callable) -> None:
|
||||
@ -855,13 +871,6 @@ class Player:
|
||||
"""
|
||||
return {'foo': 'bar'}
|
||||
|
||||
def get_id(self) -> int:
|
||||
"""get_id() -> int
|
||||
|
||||
Returns the unique numeric player ID for this player.
|
||||
"""
|
||||
return int()
|
||||
|
||||
def get_input_device(self) -> ba.InputDevice:
|
||||
"""get_input_device() -> ba.InputDevice
|
||||
|
||||
@ -878,14 +887,6 @@ class Player:
|
||||
"""
|
||||
return str()
|
||||
|
||||
def is_alive(self) -> bool:
|
||||
"""is_alive() -> bool
|
||||
|
||||
Returns True if the player has a ba.Actor assigned and its
|
||||
is_alive() method return True. False is returned otherwise.
|
||||
"""
|
||||
return bool()
|
||||
|
||||
def remove_from_game(self) -> None:
|
||||
"""remove_from_game() -> None
|
||||
|
||||
@ -914,17 +915,10 @@ class Player:
|
||||
"""
|
||||
return None
|
||||
|
||||
def set_actor(self, actor: Optional[ba.Actor]) -> None:
|
||||
"""set_actor(actor: Optional[ba.Actor]) -> None
|
||||
|
||||
Set the player's associated ba.Actor.
|
||||
"""
|
||||
return None
|
||||
|
||||
def set_data(self, team: ba.Team, character: str, color: Sequence[float],
|
||||
highlight: Sequence[float]) -> None:
|
||||
"""set_data(team: ba.Team, character: str, color: Sequence[float],
|
||||
highlight: Sequence[float]) -> None
|
||||
def set_data(self, team: ba.SessionTeam, character: str,
|
||||
color: Sequence[float], highlight: Sequence[float]) -> None:
|
||||
"""set_data(team: ba.SessionTeam, character: str,
|
||||
color: Sequence[float], highlight: Sequence[float]) -> None
|
||||
|
||||
(internal)
|
||||
"""
|
||||
@ -961,18 +955,6 @@ class Player:
|
||||
return None
|
||||
|
||||
|
||||
class SessionData:
|
||||
"""(internal)"""
|
||||
|
||||
def exists(self) -> bool:
|
||||
"""exists() -> bool
|
||||
|
||||
Returns whether the SessionData still exists.
|
||||
Most functionality will fail on a nonexistent instance.
|
||||
"""
|
||||
return bool()
|
||||
|
||||
|
||||
class Sound:
|
||||
"""A reference to a sound.
|
||||
|
||||
@ -2011,7 +1993,7 @@ def get_foreground_host_activity() -> Optional[ba.Activity]:
|
||||
is none.
|
||||
"""
|
||||
import ba # pylint: disable=cyclic-import
|
||||
return ba.Activity({})
|
||||
return ba.Activity(settings={})
|
||||
|
||||
|
||||
def get_foreground_host_session() -> Optional[ba.Session]:
|
||||
@ -2348,7 +2330,7 @@ def getactivity(doraise: bool = True) -> ba.Activity:
|
||||
False then None is returned instead.
|
||||
"""
|
||||
import ba # pylint: disable=cyclic-import
|
||||
return ba.Activity({})
|
||||
return ba.Activity(settings={})
|
||||
|
||||
|
||||
def getcollidemodel(name: str) -> ba.CollideModel:
|
||||
@ -2905,7 +2887,7 @@ def new_activity(activity_type: Type[ba.Activity],
|
||||
instantiated; You must go through this function.
|
||||
"""
|
||||
import ba # pylint: disable=cyclic-import
|
||||
return ba.Activity({})
|
||||
return ba.Activity(settings={})
|
||||
|
||||
|
||||
def new_host_session(sessiontype: Type[ba.Session],
|
||||
|
||||
@ -29,8 +29,8 @@ In some specific cases you may need to pull in individual submodules instead.
|
||||
# pylint: disable=redefined-builtin
|
||||
|
||||
from _ba import (CollideModel, Context, ContextCall, Data, InputDevice,
|
||||
Material, Model, Node, Player, Sound, Texture, Timer, Vec3,
|
||||
Widget, buttonwidget, camerashake, checkboxwidget,
|
||||
Material, Model, Node, SessionPlayer, Sound, Texture, Timer,
|
||||
Vec3, Widget, buttonwidget, camerashake, checkboxwidget,
|
||||
columnwidget, containerwidget, do_once, emitfx,
|
||||
get_collision_info, getactivity, getcollidemodel, getmodel,
|
||||
getnodes, getsession, getsound, gettexture, hscrollwidget,
|
||||
@ -40,7 +40,7 @@ from _ba import (CollideModel, Context, ContextCall, Data, InputDevice,
|
||||
charstr, textwidget, time, timer, open_url, widget)
|
||||
from ba._activity import Activity
|
||||
from ba._actor import Actor
|
||||
from ba._player import BasePlayerData
|
||||
from ba._player import Player, playercast, playercast_o
|
||||
from ba._nodeactor import NodeActor
|
||||
from ba._app import App
|
||||
from ba._coopgame import CoopGameActivity
|
||||
@ -48,11 +48,12 @@ from ba._coopsession import CoopSession
|
||||
from ba._dependency import (Dependency, DependencyComponent, DependencySet,
|
||||
AssetPackage)
|
||||
from ba._enums import TimeType, Permission, TimeFormat, SpecialChar
|
||||
from ba._error import (UNHANDLED, print_exception, print_error, NotFoundError,
|
||||
PlayerNotFoundError, NodeNotFoundError,
|
||||
ActorNotFoundError, InputDeviceNotFoundError,
|
||||
WidgetNotFoundError, ActivityNotFoundError,
|
||||
TeamNotFoundError, SessionNotFoundError,
|
||||
from ba._error import (print_exception, print_error, NotFoundError,
|
||||
PlayerNotFoundError, SessionPlayerNotFoundError,
|
||||
NodeNotFoundError, ActorNotFoundError,
|
||||
InputDeviceNotFoundError, WidgetNotFoundError,
|
||||
ActivityNotFoundError, TeamNotFoundError,
|
||||
SessionTeamNotFoundError, SessionNotFoundError,
|
||||
DependencyError)
|
||||
from ba._freeforallsession import FreeForAllSession
|
||||
from ba._gameactivity import GameActivity
|
||||
@ -63,7 +64,7 @@ from ba._session import Session
|
||||
from ba._servermode import ServerController
|
||||
from ba._score import ScoreType, ScoreInfo
|
||||
from ba._stats import PlayerScoredMessage, PlayerRecord, Stats
|
||||
from ba._team import Team
|
||||
from ba._team import SessionTeam, Team
|
||||
from ba._teamgame import TeamGameActivity
|
||||
from ba._dualteamsession import DualTeamSession
|
||||
from ba._achievement import Achievement
|
||||
@ -77,7 +78,7 @@ from ba._general import WeakCall, Call
|
||||
from ba._level import Level
|
||||
from ba._lobby import Lobby, Chooser
|
||||
from ba._math import normalized_color, is_point_in_box, vec3validate
|
||||
from ba._messages import (OutOfBoundsMessage, DeathType, DieMessage,
|
||||
from ba._messages import (UNHANDLED, OutOfBoundsMessage, DeathType, DieMessage,
|
||||
StandMessage, PickUpMessage, DropMessage,
|
||||
PickedUpMessage, DroppedMessage,
|
||||
ShouldShatterMessage, ImpactDamageMessage,
|
||||
|
||||
@ -22,10 +22,13 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import weakref
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Generic, TypeVar
|
||||
|
||||
import _ba
|
||||
from ba._team import Team
|
||||
from ba._player import Player
|
||||
from ba._error import print_exception, print_error, SessionTeamNotFoundError
|
||||
from ba._dependency import DependencyComponent
|
||||
import _ba
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from weakref import ReferenceType
|
||||
@ -33,8 +36,11 @@ if TYPE_CHECKING:
|
||||
import ba
|
||||
from bastd.actor.respawnicon import RespawnIcon
|
||||
|
||||
PlayerType = TypeVar('PlayerType', bound=Player)
|
||||
TeamType = TypeVar('TeamType', bound=Team)
|
||||
|
||||
class Activity(DependencyComponent):
|
||||
|
||||
class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
|
||||
"""Units of execution wrangled by a ba.Session.
|
||||
|
||||
Category: Gameplay Classes
|
||||
@ -66,13 +72,73 @@ class Activity(DependencyComponent):
|
||||
|
||||
# pylint: disable=too-many-public-methods
|
||||
|
||||
# Annotating attr types at the class level lets us introspect them.
|
||||
# Annotating attr types at the class level lets us introspect at runtime.
|
||||
settings_raw: Dict[str, Any]
|
||||
teams: List[ba.Team]
|
||||
players: List[ba.Player]
|
||||
teams: List[TeamType]
|
||||
players: List[PlayerType]
|
||||
|
||||
# Whether to print every time a player dies. This can be pertinent
|
||||
# in games such as Death-Match but can be annoying in games where it
|
||||
# doesn't matter.
|
||||
announce_player_deaths = False
|
||||
|
||||
# Joining activities are for waiting for initial player joins.
|
||||
# They are treated slightly differently than regular activities,
|
||||
# mainly in that all players are passed to the activity at once
|
||||
# instead of as each joins.
|
||||
is_joining_activity = False
|
||||
|
||||
# Whether game-time should still progress when in menus/etc.
|
||||
allow_pausing = False
|
||||
|
||||
# Whether idle players can potentially be kicked (should not happen in
|
||||
# menus/etc).
|
||||
allow_kick_idle_players = True
|
||||
|
||||
# In vr mode, this determines whether overlay nodes (text, images, etc)
|
||||
# are created at a fixed position in space or one that moves based on
|
||||
# the current map. Generally this should be on for games and off for
|
||||
# transitions/score-screens/etc. that persist between maps.
|
||||
use_fixed_vr_overlay = False
|
||||
|
||||
# If True, runs in slow motion and turns down sound pitch.
|
||||
slow_motion = False
|
||||
|
||||
# Set this to True to inherit slow motion setting from previous
|
||||
# activity (useful for transitions to avoid hitches).
|
||||
inherits_slow_motion = False
|
||||
|
||||
# Set this to True to keep playing the music from the previous activity
|
||||
# (without even restarting it).
|
||||
inherits_music = False
|
||||
|
||||
# Set this to true to inherit VR camera offsets from the previous
|
||||
# activity (useful for preventing sporadic camera movement
|
||||
# during transitions).
|
||||
inherits_camera_vr_offset = False
|
||||
|
||||
# Set this to true to inherit (non-fixed) VR overlay positioning from
|
||||
# the previous activity (useful for prevent sporadic overlay jostling
|
||||
# during transitions).
|
||||
inherits_vr_overlay_center = False
|
||||
|
||||
# Set this to true to inherit screen tint/vignette colors from the
|
||||
# previous activity (useful to prevent sudden color changes during
|
||||
# transitions).
|
||||
inherits_tint = False
|
||||
|
||||
# If the activity fades or transitions in, it should set the length of
|
||||
# time here so that previous activities will be kept alive for that
|
||||
# long (avoiding 'holes' in the screen)
|
||||
# This value is given in real-time seconds.
|
||||
transition_time = 0.0
|
||||
|
||||
# Is it ok to show an ad after this activity ends before showing
|
||||
# the next activity?
|
||||
can_show_ad_on_death = False
|
||||
|
||||
def __init__(self, settings: Dict[str, Any]):
|
||||
"""Creates an activity in the current ba.Session.
|
||||
"""Creates an Activity in the current ba.Session.
|
||||
|
||||
The activity will not be actually run until ba.Session.set_activity()
|
||||
is called. 'settings' should be a dict of key/value pairs specific
|
||||
@ -84,14 +150,20 @@ class Activity(DependencyComponent):
|
||||
"""
|
||||
super().__init__()
|
||||
|
||||
# FIXME: Relocate this stuff.
|
||||
# Create our internal engine data.
|
||||
self._activity_data = _ba.register_activity(self)
|
||||
|
||||
# Player/Team types should have been specified as type args;
|
||||
# grab those.
|
||||
self._playertype: Type[PlayerType]
|
||||
self._teamtype: Type[TeamType]
|
||||
self._setup_player_and_team_types()
|
||||
|
||||
# FIXME: Relocate or remove the need for this stuff.
|
||||
self.sharedobjs: Dict[str, Any] = {}
|
||||
self.paused_text: Optional[ba.Actor] = None
|
||||
self.spaz_respawn_icons_right: Dict[int, RespawnIcon]
|
||||
|
||||
# Create our internal engine data.
|
||||
self._activity_data = _ba.register_activity(self)
|
||||
|
||||
session = _ba.getsession()
|
||||
if session is None:
|
||||
raise Exception('No current session')
|
||||
@ -105,8 +177,9 @@ class Activity(DependencyComponent):
|
||||
if _ba.getactivity(doraise=False) is not self:
|
||||
raise Exception('invalid context state')
|
||||
|
||||
# Should perhaps kill this; activities should validate/store whatever
|
||||
# settings they need at init time (in a more type-safe way).
|
||||
# Hopefully can eventually kill this; activities should
|
||||
# validate/store whatever settings they need at init time
|
||||
# (in a more type-safe way).
|
||||
self.settings_raw = settings
|
||||
|
||||
self._has_transitioned_in = False
|
||||
@ -122,66 +195,6 @@ class Activity(DependencyComponent):
|
||||
self._activity_death_check_timer: Optional[ba.Timer] = None
|
||||
self._expired = False
|
||||
|
||||
# Whether to print every time a player dies. This can be pertinent
|
||||
# in games such as Death-Match but can be annoying in games where it
|
||||
# doesn't matter.
|
||||
self.announce_player_deaths = False
|
||||
|
||||
# Joining activities are for waiting for initial player joins.
|
||||
# They are treated slightly differently than regular activities,
|
||||
# mainly in that all players are passed to the activity at once
|
||||
# instead of as each joins.
|
||||
self.is_joining_activity = False
|
||||
|
||||
# Whether game-time should still progress when in menus/etc.
|
||||
self.allow_pausing = False
|
||||
|
||||
# Whether idle players can potentially be kicked (should not happen in
|
||||
# menus/etc).
|
||||
self.allow_kick_idle_players = True
|
||||
|
||||
# In vr mode, this determines whether overlay nodes (text, images, etc)
|
||||
# are created at a fixed position in space or one that moves based on
|
||||
# the current map. Generally this should be on for games and off for
|
||||
# transitions/score-screens/etc. that persist between maps.
|
||||
self.use_fixed_vr_overlay = False
|
||||
|
||||
# If True, runs in slow motion and turns down sound pitch.
|
||||
self.slow_motion = False
|
||||
|
||||
# Set this to True to inherit slow motion setting from previous
|
||||
# activity (useful for transitions to avoid hitches).
|
||||
self.inherits_slow_motion = False
|
||||
|
||||
# Set this to True to keep playing the music from the previous activity
|
||||
# (without even restarting it).
|
||||
self.inherits_music = False
|
||||
|
||||
# Set this to true to inherit VR camera offsets from the previous
|
||||
# activity (useful for preventing sporadic camera movement
|
||||
# during transitions).
|
||||
self.inherits_camera_vr_offset = False
|
||||
|
||||
# Set this to true to inherit (non-fixed) VR overlay positioning from
|
||||
# the previous activity (useful for prevent sporadic overlay jostling
|
||||
# during transitions).
|
||||
self.inherits_vr_overlay_center = False
|
||||
|
||||
# Set this to true to inherit screen tint/vignette colors from the
|
||||
# previous activity (useful to prevent sudden color changes during
|
||||
# transitions).
|
||||
self.inherits_tint = False
|
||||
|
||||
# If the activity fades or transitions in, it should set the length of
|
||||
# time here so that previous activities will be kept alive for that
|
||||
# long (avoiding 'holes' in the screen)
|
||||
# This value is given in real-time seconds.
|
||||
self.transition_time = 0.0
|
||||
|
||||
# Is it ok to show an ad after this activity ends before showing
|
||||
# the next activity?
|
||||
self.can_show_ad_on_death = False
|
||||
|
||||
# This gets set once another activity has begun transitioning in but
|
||||
# before this one is killed. The on_transition_out() method is also
|
||||
# called at this time. Make sure to not assign player inputs,
|
||||
@ -194,48 +207,16 @@ class Activity(DependencyComponent):
|
||||
# is dying.
|
||||
self._actor_refs: List[ba.Actor] = []
|
||||
self._actor_weak_refs: List[ReferenceType[ba.Actor]] = []
|
||||
self._last_dead_object_prune_time = _ba.time()
|
||||
self._last_prune_dead_actors_time = _ba.time()
|
||||
self._prune_dead_actors_timer: Optional[ba.Timer] = None
|
||||
|
||||
# This stuff gets filled in just before on_begin() is called.
|
||||
self.teams = []
|
||||
self.players = []
|
||||
self.lobby = None
|
||||
self._stats: Optional[ba.Stats] = None
|
||||
|
||||
self.lobby = None
|
||||
self._prune_dead_objects_timer: Optional[ba.Timer] = None
|
||||
|
||||
@property
|
||||
def stats(self) -> ba.Stats:
|
||||
"""The stats instance accessible while the activity is running.
|
||||
|
||||
If access is attempted before or after, raises a ba.NotFoundError.
|
||||
"""
|
||||
if self._stats is None:
|
||||
from ba._error import NotFoundError
|
||||
raise NotFoundError()
|
||||
return self._stats
|
||||
|
||||
def on_expire(self) -> None:
|
||||
"""Called when your activity is being expired.
|
||||
|
||||
If your activity has created anything explicitly that may be retaining
|
||||
a strong reference to the activity and preventing it from dying, you
|
||||
should clear that out here. From this point on your activity's sole
|
||||
purpose in life is to hit zero references and die so the next activity
|
||||
can begin.
|
||||
"""
|
||||
|
||||
def is_expired(self) -> bool:
|
||||
"""Return whether the activity is expired.
|
||||
|
||||
An activity is set as expired when shutting down.
|
||||
At this point no new nodes, timers, etc should be made,
|
||||
run, etc, and the activity should be considered a 'zombie'.
|
||||
"""
|
||||
return self._expired
|
||||
|
||||
def __del__(self) -> None:
|
||||
|
||||
from ba._apputils import garbage_collect, call_after_ad
|
||||
|
||||
# If the activity has been run then we should have already cleaned
|
||||
@ -259,6 +240,47 @@ class Activity(DependencyComponent):
|
||||
else:
|
||||
_ba.pushcall(session.begin_next_activity)
|
||||
|
||||
@property
|
||||
def stats(self) -> ba.Stats:
|
||||
"""The stats instance accessible while the activity is running.
|
||||
|
||||
If access is attempted before or after, raises a ba.NotFoundError.
|
||||
"""
|
||||
if self._stats is None:
|
||||
from ba._error import NotFoundError
|
||||
raise NotFoundError()
|
||||
return self._stats
|
||||
|
||||
def on_expire(self) -> None:
|
||||
"""Called when your activity is being expired.
|
||||
|
||||
If your activity has created anything explicitly that may be retaining
|
||||
a strong reference to the activity and preventing it from dying, you
|
||||
should clear that out here. From this point on your activity's sole
|
||||
purpose in life is to hit zero references and die so the next activity
|
||||
can begin.
|
||||
"""
|
||||
|
||||
@property
|
||||
def expired(self) -> bool:
|
||||
"""Whether the activity is expired.
|
||||
|
||||
An activity is set as expired when shutting down.
|
||||
At this point no new nodes, timers, etc should be made,
|
||||
run, etc, and the activity should be considered a 'zombie'.
|
||||
"""
|
||||
return self._expired
|
||||
|
||||
@property
|
||||
def playertype(self) -> Type[PlayerType]:
|
||||
"""The type of ba.Player this Activity is using."""
|
||||
return self._playertype
|
||||
|
||||
@property
|
||||
def teamtype(self) -> Type[TeamType]:
|
||||
"""The type of ba.Team this Activity is using."""
|
||||
return self._teamtype
|
||||
|
||||
def set_has_ended(self, val: bool) -> None:
|
||||
"""(internal)"""
|
||||
self._has_ended = val
|
||||
@ -277,18 +299,11 @@ class Activity(DependencyComponent):
|
||||
self._should_end_immediately_results = results
|
||||
self._should_end_immediately_delay = delay
|
||||
|
||||
def _get_player_icon(self, player: ba.Player) -> Dict[str, Any]:
|
||||
def destroy(self) -> None:
|
||||
"""Begin the process of tearing down the activity.
|
||||
|
||||
# Do we want to cache these somehow?
|
||||
info = player.get_icon_info()
|
||||
return {
|
||||
'texture': _ba.gettexture(info['texture']),
|
||||
'tint_texture': _ba.gettexture(info['tint_texture']),
|
||||
'tint_color': info['tint_color'],
|
||||
'tint2_color': info['tint2_color']
|
||||
}
|
||||
|
||||
def _destroy(self) -> None:
|
||||
(internal)
|
||||
"""
|
||||
from ba._general import Call
|
||||
from ba._enums import TimeType
|
||||
|
||||
@ -313,7 +328,8 @@ class Activity(DependencyComponent):
|
||||
with _ba.Context('empty'):
|
||||
self._expire()
|
||||
else:
|
||||
raise Exception('_destroy() called multiple times')
|
||||
raise RuntimeError(f'destroy() called when'
|
||||
f' already expired for {self}')
|
||||
|
||||
@classmethod
|
||||
def _check_activity_death(cls, activity_ref: ReferenceType[Activity],
|
||||
@ -351,54 +367,54 @@ class Activity(DependencyComponent):
|
||||
_ba.quit()
|
||||
|
||||
except Exception:
|
||||
from ba import _error
|
||||
_error.print_exception('exception on _check_activity_death:')
|
||||
print_exception('exception on _check_activity_death:')
|
||||
|
||||
def _expire(self) -> None:
|
||||
from ba import _error
|
||||
"""Put the activity in a state where it can be garbage-collected.
|
||||
|
||||
This involves clearing anything that might be holding a reference
|
||||
to it, etc.
|
||||
"""
|
||||
assert not self._expired
|
||||
self._expired = True
|
||||
|
||||
# Do some default cleanup.
|
||||
try:
|
||||
try:
|
||||
self.on_expire()
|
||||
except Exception:
|
||||
_error.print_exception('Error in activity on_expire()', self)
|
||||
|
||||
# Send finalize notices to all remaining actors.
|
||||
for actor_ref in self._actor_weak_refs:
|
||||
try:
|
||||
actor = actor_ref()
|
||||
if actor is not None:
|
||||
actor.on_expire()
|
||||
except Exception:
|
||||
_error.print_exception(
|
||||
'Exception on ba.Activity._expire()'
|
||||
' in actor on_expire():', actor_ref())
|
||||
|
||||
# Reset all players.
|
||||
# (releases any attached actors, clears game-data, etc)
|
||||
for player in self.players:
|
||||
if player:
|
||||
try:
|
||||
player.reset()
|
||||
player.set_activity(None)
|
||||
except Exception:
|
||||
_error.print_exception(
|
||||
'Exception on ba.Activity._expire()'
|
||||
' resetting player:', player)
|
||||
|
||||
# Ditto with teams.
|
||||
for team in self.teams:
|
||||
try:
|
||||
team.reset()
|
||||
except Exception:
|
||||
_error.print_exception(
|
||||
'Exception on ba.Activity._expire() resetting team:',
|
||||
team)
|
||||
|
||||
self.on_expire()
|
||||
except Exception:
|
||||
_error.print_exception('Exception during ba.Activity._expire():')
|
||||
print_exception(f'Error in Activity on_expire() for {self}')
|
||||
|
||||
# Send expire notices to all remaining actors.
|
||||
for actor_ref in self._actor_weak_refs:
|
||||
try:
|
||||
actor = actor_ref()
|
||||
if actor is not None:
|
||||
actor.on_expire()
|
||||
except Exception:
|
||||
print_exception(f'Error expiring Actor {actor_ref()}')
|
||||
|
||||
# Reset all Players.
|
||||
# (releases any attached actors, clears game-data, etc)
|
||||
for player in self.players:
|
||||
if player:
|
||||
try:
|
||||
sessionplayer = player.sessionplayer
|
||||
sessionplayer.set_node(None)
|
||||
sessionplayer.set_activity(None)
|
||||
sessionplayer.gameplayer = None
|
||||
sessionplayer.reset()
|
||||
except Exception:
|
||||
print_exception(f'Error resetting Player {player}')
|
||||
|
||||
# Ditto with Teams.
|
||||
for team in self.teams:
|
||||
try:
|
||||
sessionteam = team.sessionteam
|
||||
sessionteam.gameteam = None
|
||||
sessionteam.reset_gamedata()
|
||||
except SessionTeamNotFoundError:
|
||||
pass
|
||||
except Exception:
|
||||
print_exception(f'Error resetting Team {team}')
|
||||
|
||||
# Regardless of what happened here, we want to destroy our data, as
|
||||
# our activity might not go down if we don't. This will kill all
|
||||
@ -407,13 +423,13 @@ class Activity(DependencyComponent):
|
||||
try:
|
||||
self._activity_data.destroy()
|
||||
except Exception:
|
||||
_error.print_exception(
|
||||
print_exception(
|
||||
'Exception during ba.Activity._expire() destroying data:')
|
||||
|
||||
def _prune_dead_objects(self) -> None:
|
||||
def _prune_dead_actors(self) -> None:
|
||||
self._actor_refs = [a for a in self._actor_refs if a]
|
||||
self._actor_weak_refs = [a for a in self._actor_weak_refs if a()]
|
||||
self._last_dead_object_prune_time = _ba.time()
|
||||
self._last_prune_dead_actors_time = _ba.time()
|
||||
|
||||
def retain_actor(self, actor: ba.Actor) -> None:
|
||||
"""Add a strong-reference to a ba.Actor to this Activity.
|
||||
@ -423,16 +439,14 @@ class Activity(DependencyComponent):
|
||||
is a convenient way to access this same functionality.
|
||||
"""
|
||||
from ba import _actor as bsactor
|
||||
from ba import _error
|
||||
if not isinstance(actor, bsactor.Actor):
|
||||
raise Exception('non-actor passed to _retain_actor')
|
||||
if (self.has_transitioned_in()
|
||||
and _ba.time() - self._last_dead_object_prune_time > 10.0):
|
||||
_error.print_error('it looks like nodes/actors are not'
|
||||
' being pruned in your activity;'
|
||||
' did you call Activity.on_transition_in()'
|
||||
' from your subclass?; ' + str(self) +
|
||||
' (loc. a)')
|
||||
and _ba.time() - self._last_prune_dead_actors_time > 10.0):
|
||||
print_error('it looks like nodes/actors are not'
|
||||
' being pruned in your activity;'
|
||||
' did you call Activity.on_transition_in()'
|
||||
' from your subclass?; ' + str(self) + ' (loc. a)')
|
||||
self._actor_refs.append(actor)
|
||||
|
||||
def add_actor_weak_ref(self, actor: ba.Actor) -> None:
|
||||
@ -441,16 +455,14 @@ class Activity(DependencyComponent):
|
||||
(called by the ba.Actor base class)
|
||||
"""
|
||||
from ba import _actor as bsactor
|
||||
from ba import _error
|
||||
if not isinstance(actor, bsactor.Actor):
|
||||
raise Exception('non-actor passed to _add_actor_weak_ref')
|
||||
if (self.has_transitioned_in()
|
||||
and _ba.time() - self._last_dead_object_prune_time > 10.0):
|
||||
_error.print_error('it looks like nodes/actors are '
|
||||
'not being pruned in your activity;'
|
||||
' did you call Activity.on_transition_in()'
|
||||
' from your subclass?; ' + str(self) +
|
||||
' (loc. b)')
|
||||
and _ba.time() - self._last_prune_dead_actors_time > 10.0):
|
||||
print_error('it looks like nodes/actors are '
|
||||
'not being pruned in your activity;'
|
||||
' did you call Activity.on_transition_in()'
|
||||
' from your subclass?; ' + str(self) + ' (loc. b)')
|
||||
self._actor_weak_refs.append(weakref.ref(actor))
|
||||
|
||||
@property
|
||||
@ -465,48 +477,38 @@ class Activity(DependencyComponent):
|
||||
raise SessionNotFoundError()
|
||||
return session
|
||||
|
||||
def on_player_join(self, player: ba.Player) -> None:
|
||||
def on_player_join(self, player: PlayerType) -> None:
|
||||
"""Called when a new ba.Player has joined the Activity.
|
||||
|
||||
(including the initial set of Players)
|
||||
"""
|
||||
|
||||
def on_player_leave(self, player: ba.Player) -> None:
|
||||
def on_player_leave(self, player: PlayerType) -> None:
|
||||
"""Called when a ba.Player is leaving the Activity."""
|
||||
|
||||
def on_team_join(self, team: ba.Team) -> None:
|
||||
def on_team_join(self, team: TeamType) -> None:
|
||||
"""Called when a new ba.Team joins the Activity.
|
||||
|
||||
(including the initial set of Teams)
|
||||
"""
|
||||
|
||||
def on_team_leave(self, team: ba.Team) -> None:
|
||||
def on_team_leave(self, team: TeamType) -> None:
|
||||
"""Called when a ba.Team leaves the Activity."""
|
||||
|
||||
def on_transition_in(self) -> None:
|
||||
"""Called when the Activity is first becoming visible.
|
||||
|
||||
Upon this call, the Activity should fade in backgrounds,
|
||||
start playing music, etc. It does not yet have access to ba.Players
|
||||
or ba.Teams, however. They remain owned by the previous Activity
|
||||
start playing music, etc. It does not yet have access to players
|
||||
or teams, however. They remain owned by the previous Activity
|
||||
up until ba.Activity.on_begin() is called.
|
||||
"""
|
||||
from ba._general import WeakCall
|
||||
|
||||
self._called_activity_on_transition_in = True
|
||||
|
||||
# Start pruning our transient actors periodically.
|
||||
self._prune_dead_objects_timer = _ba.Timer(
|
||||
5.17, WeakCall(self._prune_dead_objects), repeat=True)
|
||||
self._prune_dead_objects()
|
||||
|
||||
# Also start our low-level scene-graph running.
|
||||
self._activity_data.start()
|
||||
|
||||
def on_transition_out(self) -> None:
|
||||
"""Called when your activity begins transitioning out.
|
||||
|
||||
Note that this may happen at any time even if finish() has not been
|
||||
Note that this may happen at any time even if end() has not been
|
||||
called.
|
||||
"""
|
||||
|
||||
@ -552,110 +554,223 @@ class Activity(DependencyComponent):
|
||||
"""Return whether on_transition_out() has been called."""
|
||||
return self._transitioning_out
|
||||
|
||||
def start_transition_in(self) -> None:
|
||||
"""Called by Session to kick of transition-in.
|
||||
def transition_in(self, prev_globals: Optional[ba.Node]) -> None:
|
||||
"""Called by Session to kick off transition-in.
|
||||
|
||||
(internal)
|
||||
"""
|
||||
from ba._general import WeakCall
|
||||
from ba._gameutils import sharedobj
|
||||
assert not self._has_transitioned_in
|
||||
self._has_transitioned_in = True
|
||||
self.on_transition_in()
|
||||
|
||||
def create_player_node(self, player: ba.Player) -> ba.Node:
|
||||
"""Create the 'player' node associated with the provided ba.Player."""
|
||||
from ba._nodeactor import NodeActor
|
||||
# Set up the globals node based on our settings.
|
||||
with _ba.Context(self):
|
||||
node = _ba.newnode('player', attrs={'playerID': player.get_id()})
|
||||
# FIXME: Should add a dedicated slot for this on ba.Player
|
||||
# instead of cluttering up their gamedata dict.
|
||||
player.gamedata['_playernode'] = NodeActor(node)
|
||||
return node
|
||||
|
||||
def begin(self, session: ba.Session) -> None:
|
||||
"""Begin the activity. (should only be called by Session).
|
||||
|
||||
(internal)"""
|
||||
|
||||
# pylint: disable=too-many-branches
|
||||
from ba import _error
|
||||
|
||||
if self._has_begun:
|
||||
_error.print_error("_begin called twice; this shouldn't happen")
|
||||
return
|
||||
|
||||
self._stats = session.stats
|
||||
|
||||
# Operate on the subset of session players who have passed team/char
|
||||
# selection.
|
||||
players = []
|
||||
chooser_players = []
|
||||
for player in session.players:
|
||||
assert player # should we ever have invalid players?..
|
||||
if player:
|
||||
try:
|
||||
team: Optional[ba.Team] = player.team
|
||||
except _error.TeamNotFoundError:
|
||||
team = None
|
||||
|
||||
if team is not None:
|
||||
player.reset_input()
|
||||
players.append(player)
|
||||
else:
|
||||
# Simply ignore players sitting in the lobby.
|
||||
# (though this technically shouldn't happen anymore since
|
||||
# choosers now get cleared when starting new activities.)
|
||||
print('unexpected: got no-team player in _begin')
|
||||
chooser_players.append(player)
|
||||
# Now that it's going to be front and center,
|
||||
# set some global values based on what the activity wants.
|
||||
glb = sharedobj('globals')
|
||||
glb.use_fixed_vr_overlay = self.use_fixed_vr_overlay
|
||||
glb.allow_kick_idle_players = self.allow_kick_idle_players
|
||||
if self.inherits_slow_motion and prev_globals is not None:
|
||||
glb.slow_motion = prev_globals.slow_motion
|
||||
else:
|
||||
_error.print_error(
|
||||
'got nonexistent player in Activity._begin()')
|
||||
glb.slow_motion = self.slow_motion
|
||||
if self.inherits_music and prev_globals is not None:
|
||||
glb.music_continuous = True # Prevent restarting same music.
|
||||
glb.music = prev_globals.music
|
||||
glb.music_count += 1
|
||||
if self.inherits_camera_vr_offset and prev_globals is not None:
|
||||
glb.vr_camera_offset = prev_globals.vr_camera_offset
|
||||
if self.inherits_vr_overlay_center and prev_globals is not None:
|
||||
glb.vr_overlay_center = prev_globals.vr_overlay_center
|
||||
glb.vr_overlay_center_enabled = (
|
||||
prev_globals.vr_overlay_center_enabled)
|
||||
|
||||
# Add teams in one by one and send team-joined messages for each.
|
||||
for team in session.teams:
|
||||
if team in self.teams:
|
||||
raise Exception('Duplicate Team Entry')
|
||||
# If they want to inherit tint from the previous self.
|
||||
if self.inherits_tint and prev_globals is not None:
|
||||
glb.tint = prev_globals.tint
|
||||
glb.vignette_outer = prev_globals.vignette_outer
|
||||
glb.vignette_inner = prev_globals.vignette_inner
|
||||
|
||||
# Start pruning our transient actors periodically.
|
||||
self._prune_dead_actors_timer = _ba.Timer(
|
||||
5.17, WeakCall(self._prune_dead_actors), repeat=True)
|
||||
self._prune_dead_actors()
|
||||
|
||||
# Also start our low-level scene running.
|
||||
self._activity_data.start()
|
||||
|
||||
try:
|
||||
self.on_transition_in()
|
||||
except Exception:
|
||||
print_exception('Error in on_transition_in for', self)
|
||||
|
||||
# Tell the C++ layer that this activity is the main one, so it uses
|
||||
# settings from our globals, directs various events to us, etc.
|
||||
self._activity_data.make_foreground()
|
||||
|
||||
def transition_out(self) -> None:
|
||||
"""Called by the Session to start us transitioning out."""
|
||||
assert not self._transitioning_out
|
||||
self._transitioning_out = True
|
||||
with _ba.Context(self):
|
||||
try:
|
||||
self.on_transition_out()
|
||||
except Exception:
|
||||
print_exception('Error in on_transition_out for', self)
|
||||
|
||||
def create_player(self, sessionplayer: ba.SessionPlayer) -> PlayerType:
|
||||
"""Create the Player instance for this Activity.
|
||||
|
||||
Subclasses can override this if the activity's player class
|
||||
requires a custom constructor; otherwise it will be called with
|
||||
no args. Note that the player object should not be used at this
|
||||
point as it is not yet fully wired up; wait for on_player_join()
|
||||
for that.
|
||||
"""
|
||||
del sessionplayer # Unused
|
||||
player = self._playertype()
|
||||
return player
|
||||
|
||||
def create_team(self, sessionteam: ba.SessionTeam) -> TeamType:
|
||||
"""Create the Team instance for this Activity.
|
||||
|
||||
Subclasses can override this if the activity's team class
|
||||
requires a custom constructor; otherwise it will be called with
|
||||
no args. Note that the team object should not be used at this
|
||||
point as it is not yet fully wired up; wait for on_team_join()
|
||||
for that.
|
||||
"""
|
||||
del sessionteam # Unused.
|
||||
team = self._teamtype()
|
||||
return team
|
||||
|
||||
def add_player(self, sessionplayer: ba.SessionPlayer) -> None:
|
||||
"""(internal)"""
|
||||
assert sessionplayer.team is not None
|
||||
|
||||
sessionplayer.reset_input()
|
||||
sessionteam = sessionplayer.team
|
||||
assert sessionplayer in sessionteam.players
|
||||
team = sessionteam.gameteam
|
||||
assert team is not None
|
||||
sessionplayer.set_activity(self)
|
||||
with _ba.Context(self):
|
||||
sessionplayer.gameplayer = player = self.create_player(
|
||||
sessionplayer)
|
||||
player.postinit(sessionplayer)
|
||||
team.players.append(player)
|
||||
self.players.append(player)
|
||||
try:
|
||||
self.on_player_join(player)
|
||||
except Exception:
|
||||
print_exception('Error in on_player_join for', self)
|
||||
|
||||
def remove_player(self, sessionplayer: ba.SessionPlayer) -> None:
|
||||
"""(internal)"""
|
||||
|
||||
# This should only be called on unexpired activities
|
||||
# the player has been added to.
|
||||
assert not self.expired
|
||||
player = sessionplayer.gameplayer
|
||||
assert isinstance(player, self._playertype)
|
||||
assert player in self.players
|
||||
|
||||
self.players.remove(player)
|
||||
with _ba.Context(self):
|
||||
# Make a decent attempt to persevere if user code breaks.
|
||||
try:
|
||||
self.on_player_leave(player)
|
||||
except Exception:
|
||||
print_exception(f'Error in on_player_leave for {self}')
|
||||
try:
|
||||
sessionplayer.reset()
|
||||
sessionplayer.set_node(None)
|
||||
sessionplayer.set_activity(None)
|
||||
except Exception:
|
||||
print_exception(f'Error resetting player for {self}')
|
||||
|
||||
def add_team(self, sessionteam: ba.SessionTeam) -> None:
|
||||
"""(internal)"""
|
||||
assert not self.expired
|
||||
|
||||
with _ba.Context(self):
|
||||
sessionteam.gameteam = team = self.create_team(sessionteam)
|
||||
team.postinit(sessionteam)
|
||||
self.teams.append(team)
|
||||
try:
|
||||
with _ba.Context(self):
|
||||
self.on_team_join(team)
|
||||
self.on_team_join(team)
|
||||
except Exception:
|
||||
_error.print_exception('Error in on_team_join for', self)
|
||||
print_exception(f'Error in on_team_join for {self}')
|
||||
|
||||
# Now add each player to the activity and to its team's list,
|
||||
# and send player-joined messages for each.
|
||||
for player in players:
|
||||
self.players.append(player)
|
||||
player.team.players.append(player)
|
||||
player.set_activity(self)
|
||||
pnode = self.create_player_node(player)
|
||||
player.set_node(pnode)
|
||||
try:
|
||||
with _ba.Context(self):
|
||||
self.on_player_join(player)
|
||||
except Exception:
|
||||
_error.print_exception('Error in on_player_join for', self)
|
||||
def remove_team(self, sessionteam: ba.SessionTeam) -> None:
|
||||
"""(internal)"""
|
||||
|
||||
# This should only be called on unexpired activities the team has
|
||||
# been added to.
|
||||
assert not self.expired
|
||||
assert sessionteam.gameteam is not None
|
||||
assert sessionteam.gameteam in self.teams
|
||||
|
||||
team = sessionteam.gameteam
|
||||
assert isinstance(team, self._teamtype)
|
||||
self.teams.remove(team)
|
||||
with _ba.Context(self):
|
||||
# And finally tell the game to start.
|
||||
self._has_begun = True
|
||||
self.on_begin()
|
||||
# Make a decent attempt to persevere if user code breaks.
|
||||
try:
|
||||
self.on_team_leave(team)
|
||||
except Exception:
|
||||
print_exception(f'Error in on_team_leave for {self}')
|
||||
try:
|
||||
sessionteam.reset_gamedata()
|
||||
except Exception:
|
||||
print_exception(f'Error in reset_gamedata for {self}')
|
||||
sessionteam.gameteam = None
|
||||
|
||||
# Make sure that ba.Activity.on_transition_in() got called
|
||||
# at some point.
|
||||
def _sanity_check_begin_call(self) -> None:
|
||||
# Make sure ba.Activity.on_transition_in() got called at some point.
|
||||
if not self._called_activity_on_transition_in:
|
||||
_error.print_error(
|
||||
print_error(
|
||||
'ba.Activity.on_transition_in() never got called for ' +
|
||||
str(self) + '; did you forget to call it'
|
||||
' in your on_transition_in override?')
|
||||
|
||||
# Make sure that ba.Activity.on_begin() got called at some point.
|
||||
if not self._called_activity_on_begin:
|
||||
_error.print_error(
|
||||
print_error(
|
||||
'ba.Activity.on_begin() never got called for ' + str(self) +
|
||||
'; did you forget to call it in your on_begin override?')
|
||||
|
||||
# If the whole session wants to die and was waiting on us, can get
|
||||
# that going now.
|
||||
def begin(self, session: ba.Session) -> None:
|
||||
"""Begin the activity.
|
||||
|
||||
(internal)
|
||||
"""
|
||||
|
||||
# Is this ever still happening?...
|
||||
if self._has_begun:
|
||||
print_error("_begin called twice; this shouldn't happen")
|
||||
return
|
||||
|
||||
# Inherit stats from the session.
|
||||
self._stats = session.stats
|
||||
|
||||
# Add session's teams in.
|
||||
for team in session.teams:
|
||||
self.add_team(team)
|
||||
|
||||
# Add session's players in.
|
||||
for player in session.players:
|
||||
self.add_player(player)
|
||||
|
||||
# And finally tell the game to start.
|
||||
with _ba.Context(self):
|
||||
self._has_begun = True
|
||||
self.on_begin()
|
||||
|
||||
self._sanity_check_begin_call()
|
||||
|
||||
# If the whole session wants to die and was waiting on us,
|
||||
# can kick off that process now.
|
||||
if session.wants_to_end:
|
||||
session.launch_end_session_activity()
|
||||
else:
|
||||
@ -663,3 +778,30 @@ class Activity(DependencyComponent):
|
||||
if self._should_end_immediately:
|
||||
self.end(self._should_end_immediately_results,
|
||||
self._should_end_immediately_delay)
|
||||
|
||||
# noinspection PyUnresolvedReferences
|
||||
def _setup_player_and_team_types(self) -> None:
|
||||
"""Pull player and team types from our typing.Generic params."""
|
||||
|
||||
# TODO: There are proper calls for pulling these in Python 3.8;
|
||||
# should update this code when we adopt that.
|
||||
# NOTE: If we get Any as PlayerType or TeamType (generally due
|
||||
# to no generic params being passed) we automatically use the
|
||||
# base class types, but also warn the user since this will mean
|
||||
# less type safety for that class. (its better to pass the base
|
||||
# types explicitly vs. having them be Any)
|
||||
if not TYPE_CHECKING:
|
||||
self._playertype = type(self).__orig_bases__[-1].__args__[0]
|
||||
if not isinstance(self._playertype, type):
|
||||
self._playertype = Player
|
||||
print(f'ERROR: {type(self)} was not passed a Player'
|
||||
f' type argument; please explicitly pass ba.Player'
|
||||
f' if you do not want to override it.')
|
||||
self._teamtype = type(self).__orig_bases__[-1].__args__[1]
|
||||
if not isinstance(self._teamtype, type):
|
||||
self._teamtype = Team
|
||||
print(f'ERROR: {type(self)} was not passed a Team'
|
||||
f' type argument; please explicitly pass ba.Team'
|
||||
f' if you do not want to override it.')
|
||||
assert issubclass(self._playertype, Player)
|
||||
assert issubclass(self._teamtype, Team)
|
||||
|
||||
@ -26,6 +26,9 @@ from typing import TYPE_CHECKING
|
||||
import _ba
|
||||
from ba._activity import Activity
|
||||
from ba._music import setmusic, MusicType
|
||||
# False positive due to our class_generics_filter custom pylint filter.
|
||||
from ba._player import Player # pylint: disable=W0611
|
||||
from ba._team import Team # pylint: disable=W0611
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any, Dict, Optional
|
||||
@ -33,7 +36,7 @@ if TYPE_CHECKING:
|
||||
from ba._lobby import JoinInfo
|
||||
|
||||
|
||||
class EndSessionActivity(Activity):
|
||||
class EndSessionActivity(Activity[Player, Team]):
|
||||
"""Special ba.Activity to fade out and end the current ba.Session."""
|
||||
|
||||
def __init__(self, settings: Dict[str, Any]):
|
||||
@ -61,7 +64,7 @@ class EndSessionActivity(Activity):
|
||||
call_after_ad(Call(_ba.new_host_session, MainMenuSession))
|
||||
|
||||
|
||||
class JoinActivity(Activity):
|
||||
class JoinActivity(Activity[Player, Team]):
|
||||
"""Standard activity for waiting for players to join.
|
||||
|
||||
It shows tips and other info and waits for all players to check ready.
|
||||
@ -98,7 +101,7 @@ class JoinActivity(Activity):
|
||||
_ba.set_analytics_screen('Joining Screen')
|
||||
|
||||
|
||||
class TransitionActivity(Activity):
|
||||
class TransitionActivity(Activity[Player, Team]):
|
||||
"""A simple overlay fade out/in.
|
||||
|
||||
Useful as a bare minimum transition between two level based activities.
|
||||
@ -131,7 +134,7 @@ class TransitionActivity(Activity):
|
||||
_ba.timer(0.1, self.end)
|
||||
|
||||
|
||||
class ScoreScreenActivity(Activity):
|
||||
class ScoreScreenActivity(Activity[Player, Team]):
|
||||
"""A standard score screen that fades in and shows stuff for a while.
|
||||
|
||||
After a specified delay, player input is assigned to end the activity.
|
||||
|
||||
@ -25,8 +25,8 @@ from __future__ import annotations
|
||||
import weakref
|
||||
from typing import TYPE_CHECKING, TypeVar
|
||||
|
||||
from ba._messages import DieMessage, DeathType, OutOfBoundsMessage
|
||||
from ba import _error
|
||||
from ba._messages import DieMessage, DeathType, OutOfBoundsMessage, UNHANDLED
|
||||
from ba._error import print_error, print_exception, ActivityNotFoundError
|
||||
import _ba
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@ -97,10 +97,10 @@ class Actor:
|
||||
# Non-expired Actors send themselves a DieMessage when going down.
|
||||
# That way we can treat DieMessage handling as the single
|
||||
# point-of-action for death.
|
||||
if not self.is_expired():
|
||||
if not self.expired:
|
||||
self.handlemessage(DieMessage())
|
||||
except Exception:
|
||||
_error.print_exception('exception in ba.Actor.__del__() for', self)
|
||||
print_exception('exception in ba.Actor.__del__() for', self)
|
||||
|
||||
def handlemessage(self, msg: Any) -> Any:
|
||||
"""General message handling; can be passed any message object."""
|
||||
@ -111,7 +111,7 @@ class Actor:
|
||||
if isinstance(msg, OutOfBoundsMessage):
|
||||
return self.handlemessage(DieMessage(how=DeathType.OUT_OF_BOUNDS))
|
||||
|
||||
return _error.UNHANDLED
|
||||
return UNHANDLED
|
||||
|
||||
def autoretain(self: T) -> T:
|
||||
"""Keep this Actor alive without needing to hold a reference to it.
|
||||
@ -126,7 +126,7 @@ class Actor:
|
||||
"""
|
||||
activity = self._activity()
|
||||
if activity is None:
|
||||
raise _error.ActivityNotFoundError()
|
||||
raise ActivityNotFoundError()
|
||||
activity.retain_actor(self)
|
||||
return self
|
||||
|
||||
@ -144,13 +144,14 @@ class Actor:
|
||||
likely result in errors.
|
||||
"""
|
||||
|
||||
def is_expired(self) -> bool:
|
||||
"""Returns whether the Actor is expired.
|
||||
@property
|
||||
def expired(self) -> bool:
|
||||
"""Whether the Actor is expired.
|
||||
|
||||
(see ba.Actor.on_expire())
|
||||
"""
|
||||
activity = self.getactivity(doraise=False)
|
||||
return True if activity is None else activity.is_expired()
|
||||
return True if activity is None else activity.expired
|
||||
|
||||
def exists(self) -> bool:
|
||||
"""Returns whether the Actor is still present in a meaningful way.
|
||||
@ -195,13 +196,12 @@ class Actor:
|
||||
avoided.
|
||||
"""
|
||||
if not __debug__:
|
||||
_error.print_error('This should only be called in __debug__ mode.',
|
||||
once=True)
|
||||
print_error('This should only be called in __debug__ mode.',
|
||||
once=True)
|
||||
if not getattr(self, '_root_actor_init_called', False):
|
||||
_error.print_error('Root Actor __init__() not called.')
|
||||
if self.is_expired():
|
||||
_error.print_error(
|
||||
f'handlemessage() called on expired actor: {self}')
|
||||
print_error('Root Actor __init__() not called.')
|
||||
if self.expired:
|
||||
print_error(f'handlemessage() called on expired actor: {self}')
|
||||
|
||||
@property
|
||||
def activity(self) -> ba.Activity:
|
||||
@ -211,7 +211,7 @@ class Actor:
|
||||
"""
|
||||
activity = self._activity()
|
||||
if activity is None:
|
||||
raise _error.ActivityNotFoundError()
|
||||
raise ActivityNotFoundError()
|
||||
return activity
|
||||
|
||||
def getactivity(self, doraise: bool = True) -> Optional[ba.Activity]:
|
||||
@ -222,5 +222,5 @@ class Actor:
|
||||
"""
|
||||
activity = self._activity()
|
||||
if activity is None and doraise:
|
||||
raise _error.ActivityNotFoundError()
|
||||
raise ActivityNotFoundError()
|
||||
return activity
|
||||
|
||||
@ -62,7 +62,7 @@ def run_cpu_benchmark() -> None:
|
||||
cfg['Graphics Quality'] = self._old_quality
|
||||
cfg.apply()
|
||||
|
||||
def on_player_request(self, player: ba.Player) -> bool:
|
||||
def on_player_request(self, player: ba.SessionPlayer) -> bool:
|
||||
return False
|
||||
|
||||
_ba.new_host_session(BenchmarkSession, benchmark_type='cpu')
|
||||
|
||||
@ -21,7 +21,7 @@
|
||||
"""Functionality related to co-op games."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, TypeVar
|
||||
|
||||
import _ba
|
||||
from ba._gameactivity import GameActivity
|
||||
@ -31,8 +31,11 @@ if TYPE_CHECKING:
|
||||
from bastd.actor.playerspaz import PlayerSpaz
|
||||
import ba
|
||||
|
||||
PlayerType = TypeVar('PlayerType', bound='ba.Player')
|
||||
TeamType = TypeVar('TeamType', bound='ba.Team')
|
||||
|
||||
class CoopGameActivity(GameActivity):
|
||||
|
||||
class CoopGameActivity(GameActivity[PlayerType, TeamType]):
|
||||
"""Base class for cooperative-mode games.
|
||||
|
||||
Category: Gameplay Classes
|
||||
@ -187,7 +190,7 @@ class CoopGameActivity(GameActivity):
|
||||
vval -= 55
|
||||
|
||||
def spawn_player_spaz(self,
|
||||
player: ba.Player,
|
||||
player: PlayerType,
|
||||
position: Sequence[float] = (0.0, 0.0, 0.0),
|
||||
angle: float = None) -> PlayerSpaz:
|
||||
"""Spawn and wire up a standard player spaz."""
|
||||
|
||||
@ -173,9 +173,9 @@ class CoopSession(Session):
|
||||
def get_custom_menu_entries(self) -> List[Dict[str, Any]]:
|
||||
return self._custom_menu_ui
|
||||
|
||||
def on_player_leave(self, player: ba.Player) -> None:
|
||||
def on_player_leave(self, sessionplayer: ba.SessionPlayer) -> None:
|
||||
from ba._general import WeakCall
|
||||
super().on_player_leave(player)
|
||||
super().on_player_leave(sessionplayer)
|
||||
|
||||
# If all our players leave we wanna quit out of the session.
|
||||
_ba.timer(2.0, WeakCall(self._end_session_if_empty))
|
||||
@ -213,7 +213,7 @@ class CoopSession(Session):
|
||||
from bastd.ui.tournamententry import TournamentEntryWindow
|
||||
from ba._gameactivity import GameActivity
|
||||
activity = self.getactivity()
|
||||
if activity is not None and not activity.is_expired():
|
||||
if activity is not None and not activity.expired:
|
||||
assert self.tournament_id is not None
|
||||
assert isinstance(activity, GameActivity)
|
||||
TournamentEntryWindow(tournament_id=self.tournament_id,
|
||||
@ -235,7 +235,7 @@ class CoopSession(Session):
|
||||
# This method may get called from the UI context so make sure we
|
||||
# explicitly run in the activity's context.
|
||||
activity = self.getactivity()
|
||||
if activity is not None and not activity.is_expired():
|
||||
if activity is not None and not activity.expired:
|
||||
activity.can_show_ad_on_death = True
|
||||
with _ba.Context(activity):
|
||||
activity.end(results={'outcome': 'restart'}, force=True)
|
||||
|
||||
@ -31,16 +31,6 @@ if TYPE_CHECKING:
|
||||
import ba
|
||||
|
||||
|
||||
class _UnhandledType:
|
||||
pass
|
||||
|
||||
|
||||
# A special value that should be returned from handlemessage()
|
||||
# functions for unhandled message types. This may result
|
||||
# in fallback message types being attempted/etc.
|
||||
UNHANDLED = _UnhandledType()
|
||||
|
||||
|
||||
class DependencyError(Exception):
|
||||
"""Exception raised when one or more ba.Dependency items are missing.
|
||||
|
||||
@ -73,6 +63,13 @@ class PlayerNotFoundError(NotFoundError):
|
||||
"""
|
||||
|
||||
|
||||
class SessionPlayerNotFoundError(NotFoundError):
|
||||
"""Exception raised when an expected ba.SessionPlayer does not exist.
|
||||
|
||||
category: Exception Classes
|
||||
"""
|
||||
|
||||
|
||||
class TeamNotFoundError(NotFoundError):
|
||||
"""Exception raised when an expected ba.Team does not exist.
|
||||
|
||||
@ -80,6 +77,13 @@ class TeamNotFoundError(NotFoundError):
|
||||
"""
|
||||
|
||||
|
||||
class SessionTeamNotFoundError(NotFoundError):
|
||||
"""Exception raised when an expected ba.SessionTeam does not exist.
|
||||
|
||||
category: Exception Classes
|
||||
"""
|
||||
|
||||
|
||||
class NodeNotFoundError(NotFoundError):
|
||||
"""Exception raised when an expected ba.Node does not exist.
|
||||
|
||||
|
||||
@ -24,12 +24,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, TypeVar
|
||||
|
||||
import _ba
|
||||
from ba._activity import Activity
|
||||
from ba._score import ScoreInfo
|
||||
from ba._lang import Lstr
|
||||
import _ba
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import (List, Optional, Dict, Type, Any, Callable, Sequence,
|
||||
@ -38,8 +38,11 @@ if TYPE_CHECKING:
|
||||
from bastd.actor.bomb import TNTSpawner
|
||||
import ba
|
||||
|
||||
PlayerType = TypeVar('PlayerType', bound='ba.Player')
|
||||
TeamType = TypeVar('TeamType', bound='ba.Team')
|
||||
|
||||
class GameActivity(Activity):
|
||||
|
||||
class GameActivity(Activity[PlayerType, TeamType]):
|
||||
"""Common base class for all game ba.Activities.
|
||||
|
||||
category: Gameplay Classes
|
||||
@ -606,13 +609,13 @@ class GameActivity(Activity):
|
||||
self._setup_tournament_time_limit(
|
||||
max(5, data_t[0]['timeRemaining']))
|
||||
|
||||
def on_player_join(self, player: ba.Player) -> None:
|
||||
def on_player_join(self, player: PlayerType) -> None:
|
||||
super().on_player_join(player)
|
||||
|
||||
# By default, just spawn a dude.
|
||||
self.spawn_player(player)
|
||||
|
||||
def on_player_leave(self, player: ba.Player) -> None:
|
||||
def on_player_leave(self, player: PlayerType) -> None:
|
||||
from ba._general import Call
|
||||
from ba._messages import DieMessage, DeathType
|
||||
|
||||
@ -632,7 +635,7 @@ class GameActivity(Activity):
|
||||
from bastd.actor.playerspaz import PlayerSpazDeathMessage
|
||||
if isinstance(msg, PlayerSpazDeathMessage):
|
||||
|
||||
player = msg.spaz.player
|
||||
player = msg.getspaz(self).player
|
||||
killer = msg.killerplayer
|
||||
|
||||
# Inform our score-set of the demise.
|
||||
@ -642,7 +645,7 @@ class GameActivity(Activity):
|
||||
|
||||
# Award the killer points if he's on a different team.
|
||||
if killer and killer.team is not player.team:
|
||||
pts, importance = msg.spaz.get_death_points(msg.how)
|
||||
pts, importance = msg.getspaz(self).get_death_points(msg.how)
|
||||
if not self.has_ended():
|
||||
self.stats.player_scored(killer,
|
||||
pts,
|
||||
@ -928,7 +931,7 @@ class GameActivity(Activity):
|
||||
print('WARNING: default end_game() implementation called;'
|
||||
' your game should override this.')
|
||||
|
||||
def spawn_player_if_exists(self, player: ba.Player) -> None:
|
||||
def spawn_player_if_exists(self, player: PlayerType) -> None:
|
||||
"""
|
||||
A utility method which calls self.spawn_player() *only* if the
|
||||
ba.Player provided still exists; handy for use in timers and whatnot.
|
||||
@ -938,7 +941,7 @@ class GameActivity(Activity):
|
||||
if player:
|
||||
self.spawn_player(player)
|
||||
|
||||
def spawn_player(self, player: ba.Player) -> ba.Actor:
|
||||
def spawn_player(self, player: PlayerType) -> ba.Actor:
|
||||
"""Spawn *something* for the provided ba.Player.
|
||||
|
||||
The default implementation simply calls spawn_player_spaz().
|
||||
@ -949,7 +952,7 @@ class GameActivity(Activity):
|
||||
return self.spawn_player_spaz(player)
|
||||
|
||||
def respawn_player(self,
|
||||
player: ba.Player,
|
||||
player: PlayerType,
|
||||
respawn_time: Optional[float] = None) -> None:
|
||||
"""
|
||||
Given a ba.Player, sets up a standard respawn timer,
|
||||
@ -989,7 +992,7 @@ class GameActivity(Activity):
|
||||
player.gamedata['respawn_icon'] = RespawnIcon(player, respawn_time)
|
||||
|
||||
def spawn_player_spaz(self,
|
||||
player: ba.Player,
|
||||
player: PlayerType,
|
||||
position: Sequence[float] = (0, 0, 0),
|
||||
angle: float = None) -> PlayerSpaz:
|
||||
"""Create and wire up a ba.PlayerSpaz for the provided ba.Player."""
|
||||
|
||||
@ -26,9 +26,11 @@ import weakref
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from ba._team import Team
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from weakref import ReferenceType
|
||||
from typing import Sequence, Tuple, Any, Optional, Dict, List
|
||||
from typing import Sequence, Tuple, Any, Optional, Dict, List, Union
|
||||
import ba
|
||||
|
||||
|
||||
@ -36,7 +38,7 @@ if TYPE_CHECKING:
|
||||
class WinnerGroup:
|
||||
"""Entry for a winning team or teams calculated by game-results."""
|
||||
score: Optional[int]
|
||||
teams: Sequence[ba.Team]
|
||||
teams: Sequence[ba.SessionTeam]
|
||||
|
||||
|
||||
class TeamGameResults:
|
||||
@ -52,9 +54,9 @@ class TeamGameResults:
|
||||
def __init__(self) -> None:
|
||||
"""Instantiate a results instance."""
|
||||
self._game_set = False
|
||||
self._scores: Dict[int, Tuple[ReferenceType[ba.Team],
|
||||
self._scores: Dict[int, Tuple[ReferenceType[ba.SessionTeam],
|
||||
Optional[int]]] = {}
|
||||
self._teams: Optional[List[ReferenceType[ba.Team]]] = None
|
||||
self._teams: Optional[List[ReferenceType[ba.SessionTeam]]] = None
|
||||
self._player_info: Optional[List[Dict[str, Any]]] = None
|
||||
self._lower_is_better: Optional[bool] = None
|
||||
self._score_label: Optional[str] = None
|
||||
@ -74,16 +76,22 @@ class TeamGameResults:
|
||||
self._none_is_winner = score_info.none_is_winner
|
||||
self._score_type = score_info.scoretype
|
||||
|
||||
def set_team_score(self, team: ba.Team, score: int) -> None:
|
||||
def set_team_score(self, team: Union[ba.SessionTeam, ba.Team],
|
||||
score: int) -> None:
|
||||
"""Set the score for a given ba.Team.
|
||||
|
||||
This can be a number or None.
|
||||
(see the none_is_winner arg in the constructor)
|
||||
"""
|
||||
self._scores[team.get_id()] = (weakref.ref(team), score)
|
||||
if isinstance(team, Team):
|
||||
team = team.sessionteam
|
||||
self._scores[team.id] = (weakref.ref(team), score)
|
||||
|
||||
def get_team_score(self, team: ba.Team) -> Optional[int]:
|
||||
def get_team_score(self, team: Union[ba.SessionTeam,
|
||||
ba.Team]) -> Optional[int]:
|
||||
"""Return the score for a given team."""
|
||||
if isinstance(team, Team):
|
||||
team = team.sessionteam
|
||||
for score in list(self._scores.values()):
|
||||
if score[0]() is team:
|
||||
return score[1]
|
||||
@ -91,8 +99,8 @@ class TeamGameResults:
|
||||
# If we have no score value, assume None.
|
||||
return None
|
||||
|
||||
def get_teams(self) -> List[ba.Team]:
|
||||
"""Return all ba.Teams in the results."""
|
||||
def get_teams(self) -> List[ba.SessionTeam]:
|
||||
"""Return all ba.SessionTeams in the results."""
|
||||
if not self._game_set:
|
||||
raise RuntimeError("Can't get teams until game is set.")
|
||||
teams = []
|
||||
@ -103,12 +111,9 @@ class TeamGameResults:
|
||||
teams.append(team)
|
||||
return teams
|
||||
|
||||
def has_score_for_team(self, team: ba.Team) -> bool:
|
||||
def has_score_for_team(self, sessionteam: ba.SessionTeam) -> bool:
|
||||
"""Return whether there is a score for a given team."""
|
||||
for score in list(self._scores.values()):
|
||||
if score[0]() is team:
|
||||
return True
|
||||
return False
|
||||
return any(s[0]() is sessionteam for s in self._scores.values())
|
||||
|
||||
def get_team_score_str(self, team: ba.Team) -> ba.Lstr:
|
||||
"""Return the score for the given ba.Team as an Lstr.
|
||||
@ -122,7 +127,7 @@ class TeamGameResults:
|
||||
if not self._game_set:
|
||||
raise RuntimeError("Can't get team-score-str until game is set.")
|
||||
for score in list(self._scores.values()):
|
||||
if score[0]() is team:
|
||||
if score[0]() is team.sessionteam:
|
||||
if score[1] is None:
|
||||
return Lstr(value='-')
|
||||
if self._score_type is ScoreType.SECONDS:
|
||||
@ -164,7 +169,7 @@ class TeamGameResults:
|
||||
assert self._lower_is_better is not None
|
||||
return self._lower_is_better
|
||||
|
||||
def get_winning_team(self) -> Optional[ba.Team]:
|
||||
def get_winning_team(self) -> Optional[ba.SessionTeam]:
|
||||
"""Get the winning ba.Team if there is exactly one; None otherwise."""
|
||||
if not self._game_set:
|
||||
raise RuntimeError("Can't get winners until game is set.")
|
||||
@ -179,7 +184,7 @@ class TeamGameResults:
|
||||
raise RuntimeError("Can't get winners until game is set.")
|
||||
|
||||
# Group by best scoring teams.
|
||||
winners: Dict[int, List[ba.Team]] = {}
|
||||
winners: Dict[int, List[ba.SessionTeam]] = {}
|
||||
scores = [
|
||||
score for score in self._scores.values()
|
||||
if score[0]() is not None and score[1] is not None
|
||||
@ -191,11 +196,11 @@ class TeamGameResults:
|
||||
assert team is not None
|
||||
sval.append(team)
|
||||
results: List[Tuple[Optional[int],
|
||||
List[ba.Team]]] = list(winners.items())
|
||||
List[ba.SessionTeam]]] = list(winners.items())
|
||||
results.sort(reverse=not self._lower_is_better, key=lambda x: x[0])
|
||||
|
||||
# Also group the 'None' scores.
|
||||
none_teams: List[ba.Team] = []
|
||||
none_teams: List[ba.SessionTeam] = []
|
||||
for score in self._scores.values():
|
||||
scoreteam = score[0]()
|
||||
if scoreteam is not None and score[1] is None:
|
||||
@ -205,7 +210,7 @@ class TeamGameResults:
|
||||
# depending on the rules).
|
||||
if none_teams:
|
||||
nones: List[Tuple[Optional[int],
|
||||
List[ba.Team]]] = [(None, none_teams)]
|
||||
List[ba.SessionTeam]]] = [(None, none_teams)]
|
||||
if self._none_is_winner:
|
||||
results = nones + results
|
||||
else:
|
||||
|
||||
@ -29,7 +29,7 @@ import _ba
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any, Type
|
||||
from efro.call import Call
|
||||
from efro.call import Call as Call # 'as Call' so we re-export.
|
||||
|
||||
T = TypeVar('T')
|
||||
|
||||
|
||||
@ -36,7 +36,7 @@ from typing import TYPE_CHECKING
|
||||
import _ba
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import List, Sequence, Optional
|
||||
from typing import List, Sequence, Optional, Dict, Any
|
||||
import ba
|
||||
|
||||
|
||||
@ -346,3 +346,13 @@ def local_chat_message(msg: str) -> None:
|
||||
def handle_remote_achievement_list(completed_achievements: List[str]) -> None:
|
||||
from ba import _achievement
|
||||
_achievement.set_completed_achievements(completed_achievements)
|
||||
|
||||
|
||||
def get_player_icon(sessionplayer: ba.SessionPlayer) -> Dict[str, Any]:
|
||||
info = sessionplayer.get_icon_info()
|
||||
return {
|
||||
'texture': _ba.gettexture(info['texture']),
|
||||
'tint_texture': _ba.gettexture(info['tint_texture']),
|
||||
'tint_color': info['tint_color'],
|
||||
'tint2_color': info['tint2_color']
|
||||
}
|
||||
|
||||
@ -36,6 +36,7 @@ if TYPE_CHECKING:
|
||||
|
||||
MAX_QUICK_CHANGE_COUNT = 30
|
||||
QUICK_CHANGE_INTERVAL = 0.05
|
||||
QUICK_CHANGE_RESET_INTERVAL = 1.0
|
||||
|
||||
|
||||
# Hmm should we move this to actors?..
|
||||
@ -147,7 +148,7 @@ class Chooser:
|
||||
if self._text_node:
|
||||
self._text_node.delete()
|
||||
|
||||
def __init__(self, vpos: float, player: _ba.Player,
|
||||
def __init__(self, vpos: float, player: _ba.SessionPlayer,
|
||||
lobby: 'Lobby') -> None:
|
||||
# FIXME: Tidy up around here.
|
||||
# pylint: disable=too-many-branches
|
||||
@ -326,7 +327,7 @@ class Chooser:
|
||||
self._inited = True
|
||||
|
||||
@property
|
||||
def player(self) -> ba.Player:
|
||||
def player(self) -> ba.SessionPlayer:
|
||||
"""The ba.Player associated with this chooser."""
|
||||
return self._player
|
||||
|
||||
@ -343,7 +344,7 @@ class Chooser:
|
||||
"""(internal)"""
|
||||
self._dead = val
|
||||
|
||||
def get_team(self) -> ba.Team:
|
||||
def get_team(self) -> ba.SessionTeam:
|
||||
"""Return this chooser's selected ba.Team."""
|
||||
return self.lobby.teams[self._selected_team_index]
|
||||
|
||||
@ -641,18 +642,17 @@ class Chooser:
|
||||
# choosers that have been marked as ready.
|
||||
team_player_counts = {}
|
||||
for team in teams:
|
||||
team_player_counts[team.get_id()] = (len(team.players))
|
||||
team_player_counts[team.id] = (len(team.players))
|
||||
for chooser in lobby.choosers:
|
||||
if chooser.ready:
|
||||
team_player_counts[
|
||||
chooser.get_team().get_id()] += 1
|
||||
team_player_counts[chooser.get_team().id] += 1
|
||||
largest_team_size = max(team_player_counts.values())
|
||||
smallest_team_size = (min(team_player_counts.values()))
|
||||
|
||||
# Force switch if we're on the biggest team
|
||||
# and there's a smaller one available.
|
||||
if (largest_team_size != smallest_team_size
|
||||
and team_player_counts[self.get_team().get_id()] >=
|
||||
and team_player_counts[self.get_team().id] >=
|
||||
largest_team_size):
|
||||
force_team_switch = True
|
||||
|
||||
@ -664,17 +664,24 @@ class Chooser:
|
||||
_ba.playsound(self._punchsound)
|
||||
self._set_ready(ready)
|
||||
|
||||
def handlemessage(self, msg: Any) -> Any:
|
||||
"""Standard generic message handler."""
|
||||
if isinstance(msg, ChangeMessage):
|
||||
now = _ba.time()
|
||||
count = self.last_change[1] + 1
|
||||
if (now - self.last_change[0] < QUICK_CHANGE_INTERVAL
|
||||
and count > MAX_QUICK_CHANGE_COUNT):
|
||||
# Hmm maybe we should notify client?
|
||||
# TODO: should handle this at the engine layer so this is unnecessary.
|
||||
def _handle_repeat_message_attack(self) -> None:
|
||||
now = _ba.time()
|
||||
count = self.last_change[1]
|
||||
if now - self.last_change[0] < QUICK_CHANGE_INTERVAL:
|
||||
count += 1
|
||||
if count > MAX_QUICK_CHANGE_COUNT:
|
||||
_ba.disconnect_client(
|
||||
self._player.get_input_device().client_id)
|
||||
self.last_change = (now, count)
|
||||
elif now - self.last_change[0] > QUICK_CHANGE_RESET_INTERVAL:
|
||||
count = 0
|
||||
self.last_change = (now, count)
|
||||
|
||||
def handlemessage(self, msg: Any) -> Any:
|
||||
"""Standard generic message handler."""
|
||||
|
||||
if isinstance(msg, ChangeMessage):
|
||||
self._handle_repeat_message_attack()
|
||||
|
||||
# If we've been removed from the lobby, ignore this stuff.
|
||||
if self._dead:
|
||||
@ -806,7 +813,7 @@ class Chooser:
|
||||
highlight[(max_index + 2) % 3] += diff * 0.2
|
||||
return highlight
|
||||
|
||||
def getplayer(self) -> ba.Player:
|
||||
def getplayer(self) -> ba.SessionPlayer:
|
||||
"""Return the player associated with this chooser."""
|
||||
return self._player
|
||||
|
||||
@ -890,7 +897,7 @@ class Lobby:
|
||||
if teams is not None:
|
||||
self._teams = [weakref.ref(team) for team in teams]
|
||||
else:
|
||||
self._dummy_teams = bs_team.Team()
|
||||
self._dummy_teams = bs_team.SessionTeam()
|
||||
self._teams = [weakref.ref(self._dummy_teams)]
|
||||
v_offset = (-150
|
||||
if isinstance(session, _coopsession.CoopSession) else -50)
|
||||
@ -920,7 +927,7 @@ class Lobby:
|
||||
return self._use_team_colors
|
||||
|
||||
@property
|
||||
def teams(self) -> List[ba.Team]:
|
||||
def teams(self) -> List[ba.SessionTeam]:
|
||||
"""Teams available in this lobby."""
|
||||
allteams = []
|
||||
for tref in self._teams:
|
||||
@ -974,14 +981,14 @@ class Lobby:
|
||||
"""Return whether all choosers are marked ready."""
|
||||
return all(chooser.ready for chooser in self.choosers)
|
||||
|
||||
def add_chooser(self, player: ba.Player) -> None:
|
||||
def add_chooser(self, player: ba.SessionPlayer) -> None:
|
||||
"""Add a chooser to the lobby for the provided player."""
|
||||
self.choosers.append(
|
||||
Chooser(vpos=self._vpos, player=player, lobby=self))
|
||||
self._next_add_team = (self._next_add_team + 1) % len(self._teams)
|
||||
self._vpos -= 48
|
||||
|
||||
def remove_chooser(self, player: ba.Player) -> None:
|
||||
def remove_chooser(self, player: ba.SessionPlayer) -> None:
|
||||
"""Remove a single player's chooser; does not kick him.
|
||||
|
||||
This is used when a player enters the game and no longer
|
||||
|
||||
@ -33,6 +33,16 @@ if TYPE_CHECKING:
|
||||
import ba
|
||||
|
||||
|
||||
class _UnhandledType:
|
||||
pass
|
||||
|
||||
|
||||
# A special value that should be returned from handlemessage()
|
||||
# functions for unhandled message types. This may result
|
||||
# in fallback message types being attempted/etc.
|
||||
UNHANDLED = _UnhandledType()
|
||||
|
||||
|
||||
@dataclass
|
||||
class OutOfBoundsMessage:
|
||||
"""A message telling an object that it is out of bounds.
|
||||
|
||||
@ -27,6 +27,7 @@ from typing import TYPE_CHECKING
|
||||
|
||||
import _ba
|
||||
from ba._session import Session
|
||||
from ba._error import NotFoundError, print_error
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Optional, Any, Dict, List, Type, Sequence
|
||||
@ -155,7 +156,7 @@ class MultiTeamSession(Session):
|
||||
"""Returns which game in the series is currently being played."""
|
||||
return self._game_number
|
||||
|
||||
def on_team_join(self, team: ba.Team) -> None:
|
||||
def on_team_join(self, team: ba.SessionTeam) -> None:
|
||||
team.sessiondata['previous_score'] = team.sessiondata['score'] = 0
|
||||
|
||||
def get_max_players(self) -> int:
|
||||
@ -171,7 +172,6 @@ class MultiTeamSession(Session):
|
||||
|
||||
def on_activity_end(self, activity: ba.Activity, results: Any) -> None:
|
||||
# pylint: disable=cyclic-import
|
||||
from ba import _error
|
||||
from bastd.tutorial import TutorialActivity
|
||||
from bastd.activity.multiteamvictory import (
|
||||
TeamSeriesVictoryScoreScreenActivity)
|
||||
@ -223,7 +223,7 @@ class MultiTeamSession(Session):
|
||||
# (ie: no longer sitting in the lobby).
|
||||
try:
|
||||
has_team = (player.team is not None)
|
||||
except _error.TeamNotFoundError:
|
||||
except NotFoundError:
|
||||
has_team = False
|
||||
if has_team:
|
||||
self.stats.register_player(player)
|
||||
@ -238,9 +238,8 @@ class MultiTeamSession(Session):
|
||||
|
||||
def _switch_to_score_screen(self, results: Any) -> None:
|
||||
"""Switch to a score screen after leaving a round."""
|
||||
from ba import _error
|
||||
del results # Unused arg.
|
||||
_error.print_error('this should be overridden')
|
||||
print_error('this should be overridden')
|
||||
|
||||
def announce_game_results(self,
|
||||
activity: ba.GameActivity,
|
||||
@ -269,7 +268,8 @@ class MultiTeamSession(Session):
|
||||
if winning_team is not None:
|
||||
# Have all players celebrate.
|
||||
celebrate_msg = CelebrateMessage(duration=10.0)
|
||||
for player in winning_team.players:
|
||||
assert winning_team.gameteam is not None
|
||||
for player in winning_team.gameteam.players:
|
||||
if player.actor:
|
||||
player.actor.handlemessage(celebrate_msg)
|
||||
cameraflash()
|
||||
|
||||
@ -94,7 +94,7 @@ class ServerCallThread(threading.Thread):
|
||||
# this check manually?
|
||||
if self._activity is not None:
|
||||
activity = self._activity()
|
||||
if activity is None or activity.is_expired():
|
||||
if activity is None or activity.expired:
|
||||
return
|
||||
|
||||
# Technically we could do the same check for session contexts,
|
||||
|
||||
@ -20,35 +20,178 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
"""Player related functionality."""
|
||||
from __future__ import annotations
|
||||
from typing import TYPE_CHECKING, TypeVar
|
||||
|
||||
from typing import TYPE_CHECKING, TypeVar, Generic
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Type
|
||||
from typing import (Type, Optional, Sequence, Dict, Any, Union, Tuple,
|
||||
Callable)
|
||||
import ba
|
||||
|
||||
T = TypeVar('T')
|
||||
TeamType = TypeVar('TeamType', bound='ba.Team')
|
||||
|
||||
|
||||
class BasePlayerData:
|
||||
"""Base class for custom player data.
|
||||
class Player(Generic[TeamType]):
|
||||
"""Testing."""
|
||||
|
||||
Category: Gameplay Classes
|
||||
# Defining these types at the class level instead of in __init__ so
|
||||
# that types are introspectable (these are still instance attrs).
|
||||
team: TeamType
|
||||
character: str
|
||||
actor: Optional[ba.Actor]
|
||||
color: Sequence[float]
|
||||
highlight: Sequence[float]
|
||||
_sessionplayer: ba.SessionPlayer
|
||||
_nodeactor: Optional[ba.NodeActor]
|
||||
|
||||
A convenience class that can be used as a base class for custom
|
||||
per-game player data. It simply provides the ability to easily fetch
|
||||
an instance of itself for a given ba.Player.
|
||||
"""
|
||||
# Should aim to kill this eventually (at least gamedata).
|
||||
# Game-specific data can be tacked on to the per-game player class.
|
||||
sessiondata: Dict
|
||||
gamedata: Dict
|
||||
|
||||
@classmethod
|
||||
def get(cls: Type[T], player: ba.Player) -> T:
|
||||
"""Return the custom player data associated with a player.
|
||||
# NOTE: avoiding having any __init__() here since it seems to not
|
||||
# get called by default if a dataclass inherits from us.
|
||||
|
||||
If one does not exist, it will be created.
|
||||
def postinit(self, sessionplayer: ba.SessionPlayer) -> None:
|
||||
"""Wire up a newly created player.
|
||||
|
||||
(internal)
|
||||
"""
|
||||
from ba._nodeactor import NodeActor
|
||||
import _ba
|
||||
self.actor = None
|
||||
self.character = ''
|
||||
self._nodeactor: Optional[ba.NodeActor] = None
|
||||
self._sessionplayer = sessionplayer
|
||||
self.character = sessionplayer.character
|
||||
self.color = sessionplayer.color
|
||||
self.highlight = sessionplayer.highlight
|
||||
self.team = sessionplayer.team.gameteam # type: ignore
|
||||
assert self.team is not None
|
||||
self.sessiondata = sessionplayer.sessiondata
|
||||
self.gamedata = sessionplayer.gamedata
|
||||
|
||||
# Store/return an instance of ourself in the player's per-game dict.
|
||||
data = player.gamedata.get('playerdata')
|
||||
if data is None:
|
||||
player.gamedata['playerdata'] = data = cls()
|
||||
assert isinstance(data, cls)
|
||||
return data
|
||||
# Create our player node in the current activity.
|
||||
node = _ba.newnode('player', attrs={'playerID': sessionplayer.id})
|
||||
self._nodeactor = NodeActor(node)
|
||||
sessionplayer.set_node(node)
|
||||
|
||||
@property
|
||||
def sessionplayer(self) -> ba.SessionPlayer:
|
||||
"""Return the ba.SessionPlayer corresponding to this Player.
|
||||
|
||||
Throws a ba.SessionPlayerNotFoundError if it does not exist.
|
||||
"""
|
||||
if bool(self._sessionplayer):
|
||||
return self._sessionplayer
|
||||
from ba import _error
|
||||
raise _error.SessionPlayerNotFoundError()
|
||||
|
||||
@property
|
||||
def node(self) -> ba.Node:
|
||||
"""A ba.Node of type 'player' associated with this Player.
|
||||
|
||||
This node can be used to get a generic player position/etc.
|
||||
"""
|
||||
if not self._nodeactor:
|
||||
from ba import _error
|
||||
raise _error.NodeNotFoundError
|
||||
return self._nodeactor.node
|
||||
|
||||
@property
|
||||
def exists(self) -> bool:
|
||||
"""Whether the player still exists.
|
||||
|
||||
Most functionality will fail on a nonexistent player.
|
||||
Note that you can also use the boolean operator for this same
|
||||
functionality, so a statement such as "if player" will do
|
||||
the right thing both for Player objects and values of None.
|
||||
"""
|
||||
return bool(self._sessionplayer)
|
||||
|
||||
def get_name(self, full: bool = False, icon: bool = True) -> str:
|
||||
"""get_name(full: bool = False, icon: bool = True) -> str
|
||||
|
||||
Returns the player's name. If icon is True, the long version of the
|
||||
name may include an icon.
|
||||
"""
|
||||
return self._sessionplayer.get_name(full=full, icon=icon)
|
||||
|
||||
def set_actor(self, actor: Optional[ba.Actor]) -> None:
|
||||
"""set_actor(actor: Optional[ba.Actor]) -> None
|
||||
|
||||
Set the player's associated ba.Actor.
|
||||
"""
|
||||
self.actor = actor
|
||||
|
||||
def is_alive(self) -> bool:
|
||||
"""is_alive() -> bool
|
||||
|
||||
Returns True if the player has a ba.Actor assigned and its
|
||||
is_alive() method return True. False is returned otherwise.
|
||||
"""
|
||||
return self.actor is not None and self.actor.is_alive()
|
||||
|
||||
def get_icon(self) -> Dict[str, Any]:
|
||||
"""get_icon() -> Dict[str, Any]
|
||||
|
||||
Returns the character's icon (images, colors, etc contained in a dict)
|
||||
"""
|
||||
return self._sessionplayer.get_icon()
|
||||
|
||||
def assign_input_call(self, inputtype: Union[str, Tuple[str, ...]],
|
||||
call: Callable) -> None:
|
||||
"""assign_input_call(type: Union[str, Tuple[str, ...]],
|
||||
call: Callable) -> None
|
||||
|
||||
Set the python callable to be run for one or more types of input.
|
||||
Valid type values are: 'jumpPress', 'jumpRelease', 'punchPress',
|
||||
'punchRelease','bombPress', 'bombRelease', 'pickUpPress',
|
||||
'pickUpRelease', 'upDown','leftRight','upPress', 'upRelease',
|
||||
'downPress', 'downRelease', 'leftPress','leftRelease','rightPress',
|
||||
'rightRelease', 'run', 'flyPress', 'flyRelease', 'startPress',
|
||||
'startRelease'
|
||||
"""
|
||||
return self._sessionplayer.assign_input_call(type=inputtype, call=call)
|
||||
|
||||
def reset_input(self) -> None:
|
||||
"""reset_input() -> None
|
||||
|
||||
Clears out the player's assigned input actions.
|
||||
"""
|
||||
self._sessionplayer.reset_input()
|
||||
|
||||
def __bool__(self) -> bool:
|
||||
return bool(self._sessionplayer)
|
||||
|
||||
|
||||
PlayerType = TypeVar('PlayerType', bound='ba.Player')
|
||||
|
||||
|
||||
def playercast(totype: Type[PlayerType], player: ba.Player) -> PlayerType:
|
||||
"""Cast a ba.Player to a specific ba.Player subclass.
|
||||
|
||||
Category: Gameplay Functions
|
||||
|
||||
When writing type-checked code, sometimes code will deal with raw
|
||||
ba.Player objects which need to be cast back to the game's actual
|
||||
player type so that access can be properly type-checked. This function
|
||||
is a safe way to do so. It ensures that Optional values are not cast
|
||||
into Non-Optional, etc.
|
||||
"""
|
||||
assert isinstance(player, totype)
|
||||
return player
|
||||
|
||||
|
||||
# NOTE: ideally we should have a single playercast() call and use overloads
|
||||
# for the optional variety, but that currently seems to not be working.
|
||||
# See: https://github.com/python/mypy/issues/8800
|
||||
def playercast_o(totype: Type[PlayerType],
|
||||
player: Optional[ba.Player]) -> Optional[PlayerType]:
|
||||
"""A variant of ba.playercast() for use with optional ba.Player values.
|
||||
|
||||
Category: Gameplay Functions
|
||||
"""
|
||||
# noinspection PyTypeHints
|
||||
assert isinstance(player, (totype, type(None)))
|
||||
return player
|
||||
|
||||
@ -25,11 +25,12 @@ import weakref
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import _ba
|
||||
from ba._error import print_error, print_exception
|
||||
from ba._lang import Lstr
|
||||
from ba._player import Player
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from weakref import ReferenceType
|
||||
from typing import Sequence, List, Dict, Any, Optional, Set
|
||||
|
||||
import ba
|
||||
|
||||
|
||||
@ -81,8 +82,8 @@ class Session:
|
||||
lobby: ba.Lobby
|
||||
max_players: int
|
||||
min_players: int
|
||||
players: List[ba.Player]
|
||||
teams: List[ba.Team]
|
||||
players: List[ba.SessionPlayer]
|
||||
teams: List[ba.SessionTeam]
|
||||
|
||||
def __init__(self,
|
||||
depsets: Sequence[ba.DependencySet],
|
||||
@ -104,9 +105,11 @@ class Session:
|
||||
from ba._stats import Stats
|
||||
from ba._gameutils import sharedobj
|
||||
from ba._gameactivity import GameActivity
|
||||
from ba._team import Team
|
||||
from ba._activity import Activity
|
||||
from ba._team import SessionTeam
|
||||
from ba._error import DependencyError
|
||||
from ba._dependency import Dependency, AssetPackage
|
||||
from efro.util import empty_weakref
|
||||
|
||||
# First off, resolve all dependency-sets we were passed.
|
||||
# If things are missing, we'll try to gather them into a single
|
||||
@ -143,21 +146,12 @@ class Session:
|
||||
# print('Would set host-session asset-reqs to:',
|
||||
# required_asset_packages)
|
||||
|
||||
# First thing, wire up our internal engine data.
|
||||
# Stuff in this section should be removed from this class if possible.
|
||||
self._sessiondata = _ba.register_session(self)
|
||||
|
||||
self.tournament_id: Optional[str] = None
|
||||
|
||||
# FIXME: This stuff shouldn't be here.
|
||||
self.sharedobjs: Dict[str, Any] = {}
|
||||
|
||||
# TeamGameActivity uses this to display a help overlay on the first
|
||||
# activity only.
|
||||
self.have_shown_controls_help_overlay = False
|
||||
|
||||
self.campaign = None
|
||||
|
||||
# FIXME: Should be able to kill this I think.
|
||||
self.campaign_state: Dict[str, str] = {}
|
||||
|
||||
self._use_teams = (team_names is not None)
|
||||
@ -171,13 +165,7 @@ class Session:
|
||||
self._activity_retained: Optional[ba.Activity] = None
|
||||
self.launch_end_session_activity_time: Optional[float] = None
|
||||
self._activity_end_timer: Optional[ba.Timer] = None
|
||||
|
||||
# Hacky way to create empty weak ref; must be a better way.
|
||||
class _EmptyObj:
|
||||
pass
|
||||
|
||||
self._activity_weak: ReferenceType[ba.Activity]
|
||||
self._activity_weak = weakref.ref(_EmptyObj()) # type: ignore
|
||||
self._activity_weak = empty_weakref(Activity)
|
||||
if self._activity_weak() is not None:
|
||||
raise Exception('Error creating empty activity weak ref.')
|
||||
|
||||
@ -192,10 +180,10 @@ class Session:
|
||||
assert team_names is not None
|
||||
assert team_colors is not None
|
||||
for i, color in enumerate(team_colors):
|
||||
team = Team(team_id=self._next_team_id,
|
||||
name=GameActivity.get_team_display_string(
|
||||
team_names[i]),
|
||||
color=color)
|
||||
team = SessionTeam(team_id=self._next_team_id,
|
||||
name=GameActivity.get_team_display_string(
|
||||
team_names[i]),
|
||||
color=color)
|
||||
self.teams.append(team)
|
||||
self._next_team_id += 1
|
||||
|
||||
@ -203,15 +191,12 @@ class Session:
|
||||
with _ba.Context(self):
|
||||
self.on_team_join(team)
|
||||
except Exception:
|
||||
from ba import _error
|
||||
_error.print_exception(
|
||||
f'Error in on_team_join for {self}.')
|
||||
print_exception(f'Error in on_team_join for {self}.')
|
||||
|
||||
self.lobby = Lobby()
|
||||
self.stats = Stats()
|
||||
|
||||
# Instantiate our session globals node
|
||||
# (so it can apply default settings).
|
||||
# Instantiate our session globals node which will apply its settings.
|
||||
sharedobj('globals')
|
||||
|
||||
@property
|
||||
@ -224,12 +209,11 @@ class Session:
|
||||
"""(internal)"""
|
||||
return self._use_team_colors
|
||||
|
||||
def on_player_request(self, player: ba.Player) -> bool:
|
||||
def on_player_request(self, player: ba.SessionPlayer) -> bool:
|
||||
"""Called when a new ba.Player wants to join the Session.
|
||||
|
||||
This should return True or False to accept/reject.
|
||||
"""
|
||||
from ba._lang import Lstr
|
||||
|
||||
# Limit player counts *unless* we're in a stress test.
|
||||
if _ba.app.stress_test_reset_timer is None:
|
||||
@ -250,144 +234,88 @@ class Session:
|
||||
_ba.playsound(_ba.getsound('dripity'))
|
||||
return True
|
||||
|
||||
def on_player_leave(self, player: ba.Player) -> None:
|
||||
"""Called when a previously-accepted ba.Player leaves the session."""
|
||||
# pylint: disable=too-many-statements
|
||||
def on_player_leave(self, sessionplayer: ba.SessionPlayer) -> None:
|
||||
"""Called when a previously-accepted ba.SessionPlayer leaves."""
|
||||
# pylint: disable=too-many-branches
|
||||
# pylint: disable=cyclic-import
|
||||
from ba._freeforallsession import FreeForAllSession
|
||||
from ba._lang import Lstr
|
||||
from ba import _error
|
||||
|
||||
# Remove them from the game rosters.
|
||||
if player in self.players:
|
||||
|
||||
_ba.playsound(_ba.getsound('playerLeft'))
|
||||
|
||||
team: Optional[ba.Team]
|
||||
|
||||
# The player will have no team if they are still in the lobby.
|
||||
try:
|
||||
team = player.team
|
||||
except _error.TeamNotFoundError:
|
||||
team = None
|
||||
|
||||
activity = self._activity_weak()
|
||||
|
||||
# If he had no team, he's in the lobby.
|
||||
# If we have a current activity with a lobby, ask them to
|
||||
# remove him.
|
||||
if team is None:
|
||||
with _ba.Context(self):
|
||||
try:
|
||||
self.lobby.remove_chooser(player)
|
||||
except Exception:
|
||||
_error.print_exception(
|
||||
'Error in Lobby.remove_chooser()')
|
||||
|
||||
# *If* they were actually in the game, announce their departure.
|
||||
if team is not None:
|
||||
_ba.screenmessage(
|
||||
Lstr(resource='playerLeftText',
|
||||
subs=[('${PLAYER}', player.get_name(full=True))]))
|
||||
|
||||
# Remove him from his team and session lists.
|
||||
# (he may not be on the team list since player are re-added to
|
||||
# team lists every activity)
|
||||
if team is not None and player in team.players:
|
||||
|
||||
# Testing; can remove this eventually.
|
||||
if isinstance(self, FreeForAllSession):
|
||||
if len(team.players) != 1:
|
||||
_error.print_error('expected 1 player in FFA team')
|
||||
team.players.remove(player)
|
||||
|
||||
# Remove player from any current activity.
|
||||
if activity is not None and player in activity.players:
|
||||
activity.players.remove(player)
|
||||
|
||||
# Run the activity callback unless its been expired.
|
||||
if not activity.is_expired():
|
||||
try:
|
||||
with _ba.Context(activity):
|
||||
activity.on_player_leave(player)
|
||||
except Exception:
|
||||
_error.print_exception(
|
||||
'exception in on_player_leave for activity',
|
||||
activity)
|
||||
else:
|
||||
_error.print_error('expired activity in on_player_leave;'
|
||||
" shouldn't happen")
|
||||
|
||||
player.set_activity(None)
|
||||
player.set_node(None)
|
||||
|
||||
# Reset the player; this will remove its actor-ref and clear
|
||||
# its calls/etc
|
||||
try:
|
||||
with _ba.Context(activity):
|
||||
player.reset()
|
||||
except Exception:
|
||||
_error.print_exception(
|
||||
'exception in player.reset in'
|
||||
' on_player_leave for player', player)
|
||||
|
||||
# If we're a non-team session, remove the player's team completely.
|
||||
if not self._use_teams and team is not None:
|
||||
|
||||
# If the team's in an activity, call its on_team_leave
|
||||
# callback.
|
||||
if activity is not None and team in activity.teams:
|
||||
activity.teams.remove(team)
|
||||
|
||||
if not activity.is_expired():
|
||||
try:
|
||||
with _ba.Context(activity):
|
||||
activity.on_team_leave(team)
|
||||
except Exception:
|
||||
_error.print_exception(
|
||||
'exception in on_team_leave for activity',
|
||||
activity)
|
||||
else:
|
||||
_error.print_error(
|
||||
'expired activity in on_player_leave p2'
|
||||
"; shouldn't happen")
|
||||
|
||||
# Clear the team's game-data (so dying stuff will
|
||||
# have proper context).
|
||||
try:
|
||||
with _ba.Context(activity):
|
||||
team.reset_gamedata()
|
||||
except Exception:
|
||||
_error.print_exception(
|
||||
'exception clearing gamedata for team:', team,
|
||||
'for player:', player, 'in activity:', activity)
|
||||
|
||||
# Remove the team from the session.
|
||||
self.teams.remove(team)
|
||||
try:
|
||||
with _ba.Context(self):
|
||||
self.on_team_leave(team)
|
||||
except Exception:
|
||||
_error.print_exception(
|
||||
'exception in on_team_leave for session', self)
|
||||
|
||||
# Clear the team's session-data (so dying stuff will
|
||||
# have proper context).
|
||||
try:
|
||||
with _ba.Context(self):
|
||||
team.reset_sessiondata()
|
||||
except Exception:
|
||||
_error.print_exception(
|
||||
'exception clearing sessiondata for team:', team,
|
||||
'in session:', self)
|
||||
|
||||
# Now remove them from the session list.
|
||||
self.players.remove(player)
|
||||
|
||||
else:
|
||||
if sessionplayer not in self.players:
|
||||
print('ERROR: Session.on_player_leave called'
|
||||
' for player not in our list.')
|
||||
return
|
||||
|
||||
_ba.playsound(_ba.getsound('playerLeft'))
|
||||
|
||||
activity = self._activity_weak()
|
||||
|
||||
if not sessionplayer.in_game:
|
||||
# Ok, the player's still in the lobby. Simply remove them from it.
|
||||
with _ba.Context(self):
|
||||
try:
|
||||
self.lobby.remove_chooser(sessionplayer)
|
||||
except Exception:
|
||||
print_exception('Error in Lobby.remove_chooser().')
|
||||
else:
|
||||
# Ok, they've already entered the game. Remove them from
|
||||
# teams/activities/etc.
|
||||
sessionteam = sessionplayer.team
|
||||
assert sessionteam is not None
|
||||
assert sessionplayer in sessionteam.players
|
||||
|
||||
_ba.screenmessage(
|
||||
Lstr(resource='playerLeftText',
|
||||
subs=[('${PLAYER}', sessionplayer.get_name(full=True))]))
|
||||
|
||||
# Remove them from their SessionTeam.
|
||||
if sessionplayer in sessionteam.players:
|
||||
sessionteam.players.remove(sessionplayer)
|
||||
else:
|
||||
print('SessionPlayer not found in SessionTeam'
|
||||
' in on_player_leave.')
|
||||
|
||||
# Grab their activity-specific player instance.
|
||||
player = sessionplayer.gameplayer
|
||||
assert isinstance(player, (Player, type(None)))
|
||||
|
||||
# Remove them from any current Activity.
|
||||
if activity is not None:
|
||||
if player in activity.players:
|
||||
activity.remove_player(sessionplayer)
|
||||
else:
|
||||
print('Player not found in Activity in on_player_leave.')
|
||||
|
||||
# If we're a non-team session, remove their team too.
|
||||
if not self._use_teams:
|
||||
|
||||
# They should have been the only one on their team.
|
||||
assert not sessionteam.players
|
||||
|
||||
# Remove their Team from the Activity.
|
||||
if activity is not None:
|
||||
if sessionteam.gameteam in activity.teams:
|
||||
activity.remove_team(sessionteam)
|
||||
else:
|
||||
print('Team not found in Activity in on_player_leave.')
|
||||
|
||||
# And then from the Session.
|
||||
with _ba.Context(self):
|
||||
if sessionteam in self.teams:
|
||||
try:
|
||||
self.teams.remove(sessionteam)
|
||||
self.on_team_leave(sessionteam)
|
||||
except Exception:
|
||||
print_exception(
|
||||
f'Error in on_team_leave for Session {self}.')
|
||||
else:
|
||||
print('Team no in Session teams in on_player_leave.')
|
||||
try:
|
||||
sessionteam.reset_sessiondata()
|
||||
except Exception:
|
||||
print_exception(
|
||||
f'Error clearing sessiondata'
|
||||
f' for team {sessionteam} in session {self}.')
|
||||
|
||||
# Now remove them from the session list.
|
||||
self.players.remove(sessionplayer)
|
||||
|
||||
def end(self) -> None:
|
||||
"""Initiates an end to the session and a return to the main menu.
|
||||
@ -401,7 +329,6 @@ class Session:
|
||||
|
||||
def launch_end_session_activity(self) -> None:
|
||||
"""(internal)"""
|
||||
from ba import _error
|
||||
from ba._activitytypes import EndSessionActivity
|
||||
from ba._enums import TimeType
|
||||
with _ba.Context(self):
|
||||
@ -412,18 +339,18 @@ class Session:
|
||||
since_last = (curtime - self.launch_end_session_activity_time)
|
||||
if since_last < 30.0:
|
||||
return
|
||||
_error.print_error(
|
||||
print_error(
|
||||
'launch_end_session_activity called twice (since_last=' +
|
||||
str(since_last) + ')')
|
||||
self.launch_end_session_activity_time = curtime
|
||||
self.set_activity(_ba.new_activity(EndSessionActivity))
|
||||
self.wants_to_end = False
|
||||
self._ending = True # Prevent further activity-mucking.
|
||||
self._ending = True # Prevent further actions.
|
||||
|
||||
def on_team_join(self, team: ba.Team) -> None:
|
||||
def on_team_join(self, team: ba.SessionTeam) -> None:
|
||||
"""Called when a new ba.Team joins the session."""
|
||||
|
||||
def on_team_leave(self, team: ba.Team) -> None:
|
||||
def on_team_leave(self, team: ba.SessionTeam) -> None:
|
||||
"""Called when a ba.Team is leaving the session."""
|
||||
|
||||
def _complete_end_activity(self, activity: ba.Activity,
|
||||
@ -433,10 +360,8 @@ class Session:
|
||||
with _ba.Context(self):
|
||||
self.on_activity_end(activity, results)
|
||||
except Exception:
|
||||
from ba import _error
|
||||
_error.print_exception(
|
||||
'exception in on_activity_end() for session', self, 'activity',
|
||||
activity, 'with results', results)
|
||||
print_exception('exception in on_activity_end() for session', self,
|
||||
'activity', activity, 'with results', results)
|
||||
|
||||
def end_activity(self, activity: ba.Activity, results: Any, delay: float,
|
||||
force: bool) -> None:
|
||||
@ -473,8 +398,7 @@ class Session:
|
||||
def handlemessage(self, msg: Any) -> Any:
|
||||
"""General message handling; can be passed any message object."""
|
||||
from ba._lobby import PlayerReadyMessage
|
||||
from ba._error import UNHANDLED
|
||||
from ba._messages import PlayerProfilesChangedMessage
|
||||
from ba._messages import PlayerProfilesChangedMessage, UNHANDLED
|
||||
if isinstance(msg, PlayerReadyMessage):
|
||||
self._on_player_ready(msg.chooser)
|
||||
return None
|
||||
@ -496,84 +420,46 @@ class Session:
|
||||
(on_transition_in, etc) to get it. (so you can't do
|
||||
session.set_activity(foo) and then ba.newnode() to add a node to foo)
|
||||
"""
|
||||
# pylint: disable=too-many-statements
|
||||
# pylint: disable=too-many-branches
|
||||
from ba import _error
|
||||
from ba._gameutils import sharedobj
|
||||
from ba._enums import TimeType
|
||||
|
||||
# Sanity test: make sure this doesn't get called recursively.
|
||||
if self._in_set_activity:
|
||||
raise Exception(
|
||||
raise RuntimeError(
|
||||
'Session.set_activity() cannot be called recursively.')
|
||||
self._in_set_activity = True
|
||||
|
||||
if activity.session is not _ba.getsession():
|
||||
raise Exception("Provided Activity's Session is not current.")
|
||||
raise RuntimeError("Provided Activity's Session is not current.")
|
||||
|
||||
# Quietly ignore this if the whole session is going down.
|
||||
if self._ending:
|
||||
return
|
||||
|
||||
if activity is self._activity_retained:
|
||||
_error.print_error('activity set to already-current activity')
|
||||
print_error('activity set to already-current activity')
|
||||
return
|
||||
|
||||
if self._next_activity is not None:
|
||||
raise Exception('Activity switch already in progress (to ' +
|
||||
str(self._next_activity) + ')')
|
||||
|
||||
self._in_set_activity = True
|
||||
raise RuntimeError('Activity switch already in progress (to ' +
|
||||
str(self._next_activity) + ')')
|
||||
|
||||
prev_activity = self._activity_retained
|
||||
|
||||
if prev_activity is not None:
|
||||
with _ba.Context(prev_activity):
|
||||
gprev = sharedobj('globals')
|
||||
prev_globals = sharedobj('globals')
|
||||
else:
|
||||
gprev = None
|
||||
prev_globals = None
|
||||
|
||||
with _ba.Context(activity):
|
||||
|
||||
# Now that it's going to be front and center,
|
||||
# set some global values based on what the activity wants.
|
||||
glb = sharedobj('globals')
|
||||
glb.use_fixed_vr_overlay = activity.use_fixed_vr_overlay
|
||||
glb.allow_kick_idle_players = activity.allow_kick_idle_players
|
||||
if activity.inherits_slow_motion and gprev is not None:
|
||||
glb.slow_motion = gprev.slow_motion
|
||||
else:
|
||||
glb.slow_motion = activity.slow_motion
|
||||
if activity.inherits_music and gprev is not None:
|
||||
glb.music_continuous = True # Prevent restarting same music.
|
||||
glb.music = gprev.music
|
||||
glb.music_count += 1
|
||||
if activity.inherits_camera_vr_offset and gprev is not None:
|
||||
glb.vr_camera_offset = gprev.vr_camera_offset
|
||||
if activity.inherits_vr_overlay_center and gprev is not None:
|
||||
glb.vr_overlay_center = gprev.vr_overlay_center
|
||||
glb.vr_overlay_center_enabled = gprev.vr_overlay_center_enabled
|
||||
|
||||
# If they want to inherit tint from the previous activity.
|
||||
if activity.inherits_tint and gprev is not None:
|
||||
glb.tint = gprev.tint
|
||||
glb.vignette_outer = gprev.vignette_outer
|
||||
glb.vignette_inner = gprev.vignette_inner
|
||||
|
||||
# Let the activity do its thing.
|
||||
activity.start_transition_in()
|
||||
# Let the activity do its thing.
|
||||
activity.transition_in(prev_globals)
|
||||
|
||||
self._next_activity = activity
|
||||
|
||||
# If we have a current activity, tell it it's transitioning out;
|
||||
# the next one will become current once this one dies.
|
||||
if prev_activity is not None:
|
||||
# pylint: disable=protected-access
|
||||
prev_activity._transitioning_out = True
|
||||
# pylint: enable=protected-access
|
||||
|
||||
# Activity will be None until the next one begins.
|
||||
with _ba.Context(prev_activity):
|
||||
prev_activity.on_transition_out()
|
||||
prev_activity.transition_out()
|
||||
|
||||
# Setting this to None should free up the old activity to die,
|
||||
# which will call begin_next_activity.
|
||||
@ -586,35 +472,15 @@ class Session:
|
||||
else:
|
||||
self.begin_next_activity()
|
||||
|
||||
# Tell the C layer that this new activity is now 'foregrounded'.
|
||||
# This means that its globals node controls global stuff and stuff
|
||||
# like console operations, keyboard shortcuts, etc will run in it.
|
||||
# pylint: disable=protected-access
|
||||
# noinspection PyProtectedMember
|
||||
activity._activity_data.make_foreground()
|
||||
# pylint: enable=protected-access
|
||||
|
||||
# We want to call _destroy() for the previous activity once it should
|
||||
# tear itself down, clear out any self-refs, etc. If the new activity
|
||||
# has a transition-time, set it up to be called after that passes;
|
||||
# otherwise call it immediately. After this call the activity should
|
||||
# have no refs left to it and should die (which will trigger the next
|
||||
# activity to run).
|
||||
# We want to call destroy() for the previous activity once it should
|
||||
# tear itself down, clear out any self-refs, etc. After this call
|
||||
# the activity should have no refs left to it and should die (which
|
||||
# will trigger the next activity to run).
|
||||
if prev_activity is not None:
|
||||
if activity.transition_time > 0.0:
|
||||
# FIXME: We should tweak the activity to not allow
|
||||
# node-creation/etc when we call _destroy (or after).
|
||||
with _ba.Context('ui'):
|
||||
# pylint: disable=protected-access
|
||||
# noinspection PyProtectedMember
|
||||
_ba.timer(activity.transition_time,
|
||||
prev_activity._destroy,
|
||||
timetype=TimeType.REAL)
|
||||
|
||||
# Just run immediately.
|
||||
else:
|
||||
# noinspection PyProtectedMember
|
||||
prev_activity._destroy() # pylint: disable=protected-access
|
||||
with _ba.Context('ui'):
|
||||
_ba.timer(max(0.0, activity.transition_time),
|
||||
prev_activity.destroy,
|
||||
timetype=TimeType.REAL)
|
||||
self._in_set_activity = False
|
||||
|
||||
def getactivity(self) -> Optional[ba.Activity]:
|
||||
@ -631,34 +497,29 @@ class Session:
|
||||
"""
|
||||
return []
|
||||
|
||||
def _request_player(self, player: ba.Player) -> bool:
|
||||
def _request_player(self, sessionplayer: ba.SessionPlayer) -> bool:
|
||||
"""Called by the C++ layer when players want to join."""
|
||||
|
||||
# If we're ending, allow no new players.
|
||||
if self._ending:
|
||||
return False
|
||||
|
||||
# Ask the user.
|
||||
# Ask the session subclass to approve/deny this request.
|
||||
try:
|
||||
with _ba.Context(self):
|
||||
result = self.on_player_request(player)
|
||||
result = self.on_player_request(sessionplayer)
|
||||
except Exception:
|
||||
from ba import _error
|
||||
_error.print_exception('error in on_player_request call for', self)
|
||||
print_exception('error in on_player_request call for', self)
|
||||
result = False
|
||||
|
||||
# If the user said yes, add the player to the session list.
|
||||
if result:
|
||||
self.players.append(player)
|
||||
|
||||
# If we have a current activity with a lobby,
|
||||
# ask it to bring up a chooser for this player.
|
||||
# otherwise they'll have to wait around for the next activity.
|
||||
self.players.append(sessionplayer)
|
||||
with _ba.Context(self):
|
||||
try:
|
||||
self.lobby.add_chooser(player)
|
||||
self.lobby.add_chooser(sessionplayer)
|
||||
except Exception:
|
||||
from ba import _error
|
||||
_error.print_exception('exception in lobby.add_chooser()')
|
||||
print_exception('exception in lobby.add_chooser()')
|
||||
|
||||
return result
|
||||
|
||||
@ -683,17 +544,12 @@ class Session:
|
||||
self._activity_weak = weakref.ref(self._next_activity)
|
||||
self._next_activity = None
|
||||
|
||||
# Lets kick out any players sitting in the lobby since
|
||||
# new activities such as score screens could cover them up;
|
||||
# better to have them rejoin.
|
||||
# Kick out anyone loitering in the lobby.
|
||||
self.lobby.remove_all_choosers_and_kick_players()
|
||||
activity = self._activity_weak()
|
||||
assert activity is not None
|
||||
activity.begin(self)
|
||||
self._activity_retained.begin(self)
|
||||
|
||||
def _on_player_ready(self, chooser: ba.Chooser) -> None:
|
||||
"""Called when a ba.Player has checked themself ready."""
|
||||
from ba._lang import Lstr
|
||||
lobby = chooser.lobby
|
||||
activity = self._activity_weak()
|
||||
|
||||
@ -711,10 +567,11 @@ class Session:
|
||||
# Get our next activity going.
|
||||
self._complete_end_activity(activity, {})
|
||||
else:
|
||||
_ba.screenmessage(Lstr(resource='notEnoughPlayersText',
|
||||
subs=[('${COUNT}', str(min_players))
|
||||
]),
|
||||
color=(1, 1, 0))
|
||||
_ba.screenmessage(
|
||||
Lstr(resource='notEnoughPlayersText',
|
||||
subs=[('${COUNT}', str(min_players))]),
|
||||
color=(1, 1, 0),
|
||||
)
|
||||
_ba.playsound(_ba.getsound('error'))
|
||||
else:
|
||||
return
|
||||
@ -724,24 +581,19 @@ class Session:
|
||||
self._add_chosen_player(chooser)
|
||||
lobby.remove_chooser(chooser.getplayer())
|
||||
|
||||
def _add_chosen_player(self, chooser: ba.Chooser) -> ba.Player:
|
||||
# pylint: disable=too-many-statements
|
||||
# pylint: disable=too-many-branches
|
||||
from ba import _error
|
||||
from ba._lang import Lstr
|
||||
from ba._team import Team
|
||||
from ba import _freeforallsession
|
||||
player = chooser.getplayer()
|
||||
if player not in self.players:
|
||||
_error.print_error('player not found in session '
|
||||
'player-list after chooser selection')
|
||||
def _add_chosen_player(self, chooser: ba.Chooser) -> ba.SessionPlayer:
|
||||
from ba._team import SessionTeam
|
||||
sessionplayer = chooser.getplayer()
|
||||
assert sessionplayer in self.players, (
|
||||
'SessionPlayer not found in session '
|
||||
'player-list after chooser selection.')
|
||||
|
||||
activity = self._activity_weak()
|
||||
assert activity is not None
|
||||
|
||||
# We need to reset the player's input here, as it is currently
|
||||
# Reset the player's input here, as it is probably
|
||||
# referencing the chooser which could inadvertently keep it alive.
|
||||
player.reset_input()
|
||||
sessionplayer.reset_input()
|
||||
|
||||
# Pass it to the current activity if it has already begun
|
||||
# (otherwise it'll get passed once begin is called).
|
||||
@ -749,74 +601,51 @@ class Session:
|
||||
and not activity.is_joining_activity)
|
||||
|
||||
# If we're not allowing mid-game joins, don't pass; just announce
|
||||
# the arrival.
|
||||
# the arrival and say they'll partake next round.
|
||||
if pass_to_activity:
|
||||
if not self._allow_mid_activity_joins:
|
||||
pass_to_activity = False
|
||||
with _ba.Context(self):
|
||||
_ba.screenmessage(Lstr(resource='playerDelayedJoinText',
|
||||
subs=[('${PLAYER}',
|
||||
player.get_name(full=True))
|
||||
]),
|
||||
color=(0, 1, 0))
|
||||
_ba.screenmessage(
|
||||
Lstr(resource='playerDelayedJoinText',
|
||||
subs=[('${PLAYER}',
|
||||
sessionplayer.get_name(full=True))]),
|
||||
color=(0, 1, 0),
|
||||
)
|
||||
|
||||
# If we're a non-team game, each player gets their own team
|
||||
# If we're a non-team session, each player gets their own team.
|
||||
# (keeps mini-game coding simpler if we can always deal with teams).
|
||||
if self._use_teams:
|
||||
team = chooser.get_team()
|
||||
sessionteam = chooser.get_team()
|
||||
else:
|
||||
our_team_id = self._next_team_id
|
||||
team = Team(team_id=our_team_id,
|
||||
name=chooser.getplayer().get_name(full=True,
|
||||
icon=False),
|
||||
color=chooser.get_color())
|
||||
self.teams.append(team)
|
||||
self._next_team_id += 1
|
||||
sessionteam = SessionTeam(
|
||||
team_id=our_team_id,
|
||||
color=chooser.get_color(),
|
||||
name=chooser.getplayer().get_name(full=True, icon=False),
|
||||
)
|
||||
|
||||
# Add player's team to the Session.
|
||||
self.teams.append(sessionteam)
|
||||
try:
|
||||
with _ba.Context(self):
|
||||
self.on_team_join(team)
|
||||
self.on_team_join(sessionteam)
|
||||
except Exception:
|
||||
_error.print_exception(f'exception in on_team_join for {self}')
|
||||
print_exception(f'exception in on_team_join for {self}')
|
||||
|
||||
# Add player's team to the Activity.
|
||||
if pass_to_activity:
|
||||
if team in activity.teams:
|
||||
_error.print_error(
|
||||
'Duplicate team ID in ba.Session._add_chosen_player')
|
||||
activity.teams.append(team)
|
||||
try:
|
||||
with _ba.Context(activity):
|
||||
activity.on_team_join(team)
|
||||
except Exception:
|
||||
_error.print_exception(
|
||||
f'ERROR: exception in on_team_join for {activity}')
|
||||
activity.add_team(sessionteam)
|
||||
|
||||
player.set_data(team=team,
|
||||
character=chooser.get_character_name(),
|
||||
color=chooser.get_color(),
|
||||
highlight=chooser.get_highlight())
|
||||
assert sessionplayer not in sessionteam.players
|
||||
sessionteam.players.append(sessionplayer)
|
||||
sessionplayer.set_data(team=sessionteam,
|
||||
character=chooser.get_character_name(),
|
||||
color=chooser.get_color(),
|
||||
highlight=chooser.get_highlight())
|
||||
|
||||
self.stats.register_player(player)
|
||||
self.stats.register_player(sessionplayer)
|
||||
if pass_to_activity:
|
||||
if isinstance(self, _freeforallsession.FreeForAllSession):
|
||||
if player.team.players:
|
||||
_error.print_error('expected 0 players in FFA team')
|
||||
|
||||
# Don't actually add the player to their team list if we're not
|
||||
# in an activity. (players get (re)added to their team lists
|
||||
# when the activity begins).
|
||||
player.team.players.append(player)
|
||||
if player in activity.players:
|
||||
_error.print_exception(
|
||||
f'Dup player in ba.Session._add_chosen_player: {player}')
|
||||
else:
|
||||
activity.players.append(player)
|
||||
player.set_activity(activity)
|
||||
pnode = activity.create_player_node(player)
|
||||
player.set_node(pnode)
|
||||
try:
|
||||
with _ba.Context(activity):
|
||||
activity.on_player_join(player)
|
||||
except Exception:
|
||||
_error.print_exception(
|
||||
f'Error on on_player_join for {activity}')
|
||||
return player
|
||||
activity.add_player(sessionplayer)
|
||||
return sessionplayer
|
||||
|
||||
@ -19,7 +19,6 @@
|
||||
# SOFTWARE.
|
||||
# -----------------------------------------------------------------------------
|
||||
"""Functionality related to scores and statistics."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
@ -28,6 +27,8 @@ from typing import TYPE_CHECKING
|
||||
from dataclasses import dataclass
|
||||
|
||||
import _ba
|
||||
from ba._error import (print_exception, print_error, SessionTeamNotFoundError,
|
||||
SessionPlayerNotFoundError)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import ba
|
||||
@ -37,12 +38,11 @@ if TYPE_CHECKING:
|
||||
|
||||
@dataclass
|
||||
class PlayerScoredMessage:
|
||||
# noinspection PyUnresolvedReferences
|
||||
"""Informs something that a ba.Player scored.
|
||||
|
||||
Category: Message Classes
|
||||
|
||||
Attributes:
|
||||
Attrs:
|
||||
|
||||
score
|
||||
The score value.
|
||||
@ -61,7 +61,7 @@ class PlayerRecord:
|
||||
"""
|
||||
character: str
|
||||
|
||||
def __init__(self, name: str, name_full: str, player: ba.Player,
|
||||
def __init__(self, name: str, name_full: str, player: ba.SessionPlayer,
|
||||
stats: ba.Stats):
|
||||
self.name = name
|
||||
self.name_full = name_full
|
||||
@ -74,34 +74,34 @@ class PlayerRecord:
|
||||
self._multi_kill_timer: Optional[ba.Timer] = None
|
||||
self._multi_kill_count = 0
|
||||
self._stats = weakref.ref(stats)
|
||||
self._last_player: Optional[ba.Player] = None
|
||||
self._player: Optional[ba.Player] = None
|
||||
self._team: Optional[ReferenceType[ba.Team]] = None
|
||||
self._last_player: Optional[ba.SessionPlayer] = None
|
||||
self._player: Optional[ba.SessionPlayer] = None
|
||||
self._team: Optional[ReferenceType[ba.SessionTeam]] = None
|
||||
self.streak = 0
|
||||
self.associate_with_player(player)
|
||||
|
||||
@property
|
||||
def team(self) -> ba.Team:
|
||||
"""The ba.Team the last associated player was last on.
|
||||
def team(self) -> ba.SessionTeam:
|
||||
"""The ba.SessionTeam the last associated player was last on.
|
||||
|
||||
This can still return a valid result even if the player is gone.
|
||||
Raises a ba.TeamNotFoundError if the team no longer exists.
|
||||
Raises a ba.SessionTeamNotFoundError if the team no longer exists.
|
||||
"""
|
||||
assert self._team is not None
|
||||
team = self._team()
|
||||
if team is None:
|
||||
from ba._error import TeamNotFoundError
|
||||
raise TeamNotFoundError()
|
||||
raise SessionTeamNotFoundError()
|
||||
return team
|
||||
|
||||
@property
|
||||
def player(self) -> ba.Player:
|
||||
"""Return the instance's associated ba.Player.
|
||||
def player(self) -> ba.SessionPlayer:
|
||||
"""Return the instance's associated ba.SessionPlayer.
|
||||
|
||||
Raises a ba.PlayerNotFoundError if the player no longer exists."""
|
||||
Raises a ba.SessionPlayerNotFoundError if the player
|
||||
no longer exists.
|
||||
"""
|
||||
if not self._player:
|
||||
from ba._error import PlayerNotFoundError
|
||||
raise PlayerNotFoundError()
|
||||
raise SessionPlayerNotFoundError()
|
||||
return self._player
|
||||
|
||||
def get_name(self, full: bool = False) -> str:
|
||||
@ -127,7 +127,7 @@ class PlayerRecord:
|
||||
return stats.getactivity()
|
||||
return None
|
||||
|
||||
def associate_with_player(self, player: ba.Player) -> None:
|
||||
def associate_with_player(self, player: ba.SessionPlayer) -> None:
|
||||
"""Associate this entry with a ba.Player."""
|
||||
self._team = weakref.ref(player.team)
|
||||
self.character = player.character
|
||||
@ -139,7 +139,7 @@ class PlayerRecord:
|
||||
self._multi_kill_timer = None
|
||||
self._multi_kill_count = 0
|
||||
|
||||
def get_last_player(self) -> ba.Player:
|
||||
def get_last_player(self) -> ba.SessionPlayer:
|
||||
"""Return the last ba.Player we were associated with."""
|
||||
assert self._last_player is not None
|
||||
return self._last_player
|
||||
@ -203,10 +203,13 @@ class PlayerRecord:
|
||||
from bastd.actor.popuptext import PopupText
|
||||
|
||||
# Only award this if they're still alive and we can get
|
||||
# their pos.
|
||||
if self._player is not None and self._player.node:
|
||||
our_pos = self._player.node.position
|
||||
else:
|
||||
# a current position for them.
|
||||
our_pos: Optional[Sequence[float]] = None
|
||||
if self._player is not None:
|
||||
if self._player.gameplayer is not None:
|
||||
if self._player.gameplayer.node:
|
||||
our_pos = self._player.gameplayer.node.position
|
||||
if our_pos is None:
|
||||
return
|
||||
|
||||
# Jitter position a bit since these often come in clusters.
|
||||
@ -263,9 +266,8 @@ class Stats:
|
||||
|
||||
# Load our media into this activity's context.
|
||||
if activity is not None:
|
||||
if activity.is_expired():
|
||||
from ba import _error
|
||||
_error.print_error('unexpected finalized activity')
|
||||
if activity.expired:
|
||||
print_error('unexpected finalized activity')
|
||||
else:
|
||||
with _ba.Context(activity):
|
||||
self._load_activity_media()
|
||||
@ -303,7 +305,7 @@ class Stats:
|
||||
s_player.accum_killed_count = 0
|
||||
s_player.streak = 0
|
||||
|
||||
def register_player(self, player: ba.Player) -> None:
|
||||
def register_player(self, player: ba.SessionPlayer) -> None:
|
||||
"""Register a player with this score-set."""
|
||||
name = player.get_name()
|
||||
name_full = player.get_name(full=True)
|
||||
@ -329,7 +331,7 @@ class Stats:
|
||||
records[record_id] = record
|
||||
return records
|
||||
|
||||
def player_got_hit(self, player: ba.Player) -> None:
|
||||
def player_got_hit(self, player: ba.SessionPlayer) -> None:
|
||||
"""Call this when a player got hit."""
|
||||
s_player = self._player_records[player.get_name()]
|
||||
s_player.streak = 0
|
||||
@ -388,8 +390,7 @@ class Stats:
|
||||
subs=[('${NAME}', name_full)]),
|
||||
color=_math.normalized_color(player.team.color))
|
||||
except Exception:
|
||||
from ba import _error
|
||||
_error.print_exception('error showing big_message')
|
||||
print_exception('error showing big_message')
|
||||
|
||||
# If we currently have a actor, pop up a score over it.
|
||||
if display and showpoints:
|
||||
@ -430,8 +431,7 @@ class Stats:
|
||||
color=player.color,
|
||||
image=player.get_icon())
|
||||
except Exception:
|
||||
from ba import _error
|
||||
_error.print_exception('error announcing score')
|
||||
print_exception('error announcing score')
|
||||
|
||||
s_player.score += points
|
||||
s_player.accumscore += points
|
||||
@ -458,14 +458,14 @@ class Stats:
|
||||
prec.killed_count += 1
|
||||
try:
|
||||
if killed and _ba.getactivity().announce_player_deaths:
|
||||
if killer == player:
|
||||
if killer is player:
|
||||
_ba.screenmessage(Lstr(resource='nameSuicideText',
|
||||
subs=[('${NAME}', name)]),
|
||||
top=True,
|
||||
color=player.color,
|
||||
image=player.get_icon())
|
||||
elif killer is not None:
|
||||
if killer.team == player.team:
|
||||
if killer.team is player.team:
|
||||
_ba.screenmessage(Lstr(resource='nameBetrayedText',
|
||||
subs=[('${NAME}',
|
||||
killer.get_name()),
|
||||
@ -488,5 +488,4 @@ class Stats:
|
||||
color=player.color,
|
||||
image=player.get_icon())
|
||||
except Exception:
|
||||
from ba import _error
|
||||
_error.print_exception('error announcing kill')
|
||||
print_exception('error announcing kill')
|
||||
|
||||
@ -21,15 +21,17 @@
|
||||
"""Defines Team class."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
import weakref
|
||||
from typing import TYPE_CHECKING, TypeVar, Generic
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Dict, List, Sequence, Any, Tuple, Union
|
||||
from weakref import ReferenceType
|
||||
from typing import Dict, List, Sequence, Tuple, Union, Optional
|
||||
import ba
|
||||
|
||||
|
||||
class Team:
|
||||
"""A team of one or more ba.Players.
|
||||
class SessionTeam:
|
||||
"""A team of one or more ba.SessionPlayers.
|
||||
|
||||
Category: Gameplay Classes
|
||||
|
||||
@ -42,11 +44,14 @@ class Team:
|
||||
name
|
||||
The team's name.
|
||||
|
||||
id
|
||||
The unique numeric id of the team.
|
||||
|
||||
color
|
||||
The team's color.
|
||||
|
||||
players
|
||||
The list of ba.Players on the team.
|
||||
The list of ba.SessionPlayers on the team.
|
||||
|
||||
gamedata
|
||||
A dict for use by the current ba.Activity
|
||||
@ -62,56 +67,82 @@ class Team:
|
||||
|
||||
# Annotate our attr types at the class level so they're introspectable.
|
||||
name: Union[ba.Lstr, str]
|
||||
color: Tuple[float, ...]
|
||||
players: List[ba.Player]
|
||||
color: Tuple[float, ...] # FIXME: can't we make this fixed len?
|
||||
players: List[ba.SessionPlayer]
|
||||
gamedata: Dict
|
||||
sessiondata: Dict
|
||||
id: int
|
||||
|
||||
def __init__(self,
|
||||
team_id: int = 0,
|
||||
name: Union[ba.Lstr, str] = '',
|
||||
color: Sequence[float] = (1.0, 1.0, 1.0)):
|
||||
"""Instantiate a ba.Team.
|
||||
"""Instantiate a ba.SessionTeam.
|
||||
|
||||
In most cases, all teams are provided to you by the ba.Session,
|
||||
ba.Session, so calling this shouldn't be necessary.
|
||||
"""
|
||||
|
||||
# TODO: Once we spin off team copies for each activity, we don't
|
||||
# need to bother with trying to lock things down, since it won't
|
||||
# matter at that point if the activity mucks with them.
|
||||
|
||||
# Temporarily allow us to set our own attrs
|
||||
# (keeps pylint happier than using __setattr__ explicitly for all).
|
||||
object.__setattr__(self, '_locked', False)
|
||||
self._team_id: int = team_id
|
||||
self.id = team_id
|
||||
self.name = name
|
||||
self.color = tuple(color)
|
||||
self.players = []
|
||||
self.gamedata = {}
|
||||
self.sessiondata = {}
|
||||
|
||||
# Now prevent further attr sets.
|
||||
self._locked = True
|
||||
|
||||
def get_id(self) -> int:
|
||||
"""Returns the numeric team ID."""
|
||||
return self._team_id
|
||||
|
||||
def reset(self) -> None:
|
||||
"""(internal)"""
|
||||
self.reset_gamedata()
|
||||
object.__setattr__(self, 'players', [])
|
||||
self.gameteam: Optional[Team] = None
|
||||
|
||||
def reset_gamedata(self) -> None:
|
||||
"""(internal)"""
|
||||
object.__setattr__(self, 'gamedata', {})
|
||||
self.gamedata = {}
|
||||
|
||||
def reset_sessiondata(self) -> None:
|
||||
"""(internal)"""
|
||||
object.__setattr__(self, 'sessiondata', {})
|
||||
self.sessiondata = {}
|
||||
|
||||
def __setattr__(self, name: str, value: Any) -> None:
|
||||
if self._locked:
|
||||
raise Exception("can't set attrs on ba.Team objects")
|
||||
object.__setattr__(self, name, value)
|
||||
|
||||
PlayerType = TypeVar('PlayerType', bound='ba.Player')
|
||||
|
||||
|
||||
class Team(Generic[PlayerType]):
|
||||
"""Testing."""
|
||||
|
||||
# Defining these types at the class level instead of in __init__ so
|
||||
# that types are introspectable (these are still instance attrs).
|
||||
players: List[PlayerType]
|
||||
id: int
|
||||
name: Union[ba.Lstr, str]
|
||||
color: Tuple[float, ...] # FIXME: can't we make this fixed len?
|
||||
_sessionteam: ReferenceType[SessionTeam]
|
||||
|
||||
# TODO: kill these.
|
||||
gamedata: Dict
|
||||
sessiondata: Dict
|
||||
|
||||
# NOTE: avoiding having any __init__() here since it seems to not
|
||||
# get called by default if a dataclass inherits from us.
|
||||
|
||||
def postinit(self, sessionteam: SessionTeam) -> None:
|
||||
"""Wire up a newly created SessionTeam.
|
||||
|
||||
(internal)
|
||||
"""
|
||||
self.players = []
|
||||
self._sessionteam = weakref.ref(sessionteam)
|
||||
self.id = sessionteam.id
|
||||
self.name = sessionteam.name
|
||||
self.color = sessionteam.color
|
||||
self.gamedata = sessionteam.gamedata
|
||||
self.sessiondata = sessionteam.sessiondata
|
||||
|
||||
@property
|
||||
def sessionteam(self) -> SessionTeam:
|
||||
"""Return the ba.SessionTeam corresponding to this Team.
|
||||
|
||||
Throws a ba.SessionTeamNotFoundError if there is none.
|
||||
"""
|
||||
if self._sessionteam is not None:
|
||||
sessionteam = self._sessionteam()
|
||||
if sessionteam is not None:
|
||||
return sessionteam
|
||||
from ba import _error
|
||||
raise _error.SessionTeamNotFoundError()
|
||||
|
||||
@ -22,21 +22,24 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, TypeVar
|
||||
|
||||
import _ba
|
||||
from ba._freeforallsession import FreeForAllSession
|
||||
from ba._gameactivity import GameActivity
|
||||
from ba._gameresults import TeamGameResults
|
||||
from ba._dualteamsession import DualTeamSession
|
||||
import _ba
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any, Dict, Type, Sequence
|
||||
from bastd.actor.playerspaz import PlayerSpaz
|
||||
import ba
|
||||
|
||||
PlayerType = TypeVar('PlayerType', bound='ba.Player')
|
||||
TeamType = TypeVar('TeamType', bound='ba.Team')
|
||||
|
||||
class TeamGameActivity(GameActivity):
|
||||
|
||||
class TeamGameActivity(GameActivity[PlayerType, TeamType]):
|
||||
"""Base class for teams and free-for-all mode games.
|
||||
|
||||
Category: Gameplay Classes
|
||||
@ -56,6 +59,7 @@ class TeamGameActivity(GameActivity):
|
||||
or issubclass(sessiontype, FreeForAllSession))
|
||||
|
||||
def __init__(self, settings: Dict[str, Any]):
|
||||
|
||||
super().__init__(settings)
|
||||
|
||||
# By default we don't show kill-points in free-for-all.
|
||||
@ -104,7 +108,7 @@ class TeamGameActivity(GameActivity):
|
||||
_error.print_exception()
|
||||
|
||||
def spawn_player_spaz(self,
|
||||
player: ba.Player,
|
||||
player: PlayerType,
|
||||
position: Sequence[float] = None,
|
||||
angle: float = None) -> PlayerSpaz:
|
||||
"""
|
||||
@ -117,7 +121,7 @@ class TeamGameActivity(GameActivity):
|
||||
if position is None:
|
||||
# In teams-mode get our team-start-location.
|
||||
if isinstance(self.session, DualTeamSession):
|
||||
position = (self.map.get_start_position(player.team.get_id()))
|
||||
position = (self.map.get_start_position(player.team.id))
|
||||
else:
|
||||
# Otherwise do free-for-all spawn locations.
|
||||
position = self.map.get_ffa_start_position(self.players)
|
||||
|
||||
@ -38,12 +38,12 @@ if TYPE_CHECKING:
|
||||
from bastd.ui.league.rankbutton import LeagueRankButton
|
||||
|
||||
|
||||
class CoopScoreScreen(ba.Activity):
|
||||
class CoopScoreScreen(ba.Activity[ba.Player, ba.Team]):
|
||||
"""Score screen showing the results of a cooperative game."""
|
||||
|
||||
def __init__(self, settings: Dict[str, Any]):
|
||||
# pylint: disable=too-many-statements
|
||||
super().__init__(settings=settings)
|
||||
super().__init__(settings)
|
||||
|
||||
# Keep prev activity alive while we fade in
|
||||
self.transition_time = 0.5
|
||||
@ -1008,7 +1008,7 @@ class CoopScoreScreen(ba.Activity):
|
||||
# We need to manually run this in the context of our activity
|
||||
# and only if we aren't shutting down.
|
||||
# (really should make the submit_score call handle that stuff itself)
|
||||
if self.is_expired():
|
||||
if self.expired:
|
||||
return
|
||||
with ba.Context(self):
|
||||
# Delay a bit if results come in too fast.
|
||||
|
||||
@ -73,7 +73,7 @@ class TeamVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
|
||||
scale=0.25,
|
||||
color=(0.5, 0.5, 0.5, 1.0),
|
||||
jitter=3.0).autoretain()
|
||||
for team in self.teams:
|
||||
for team in self.session.teams:
|
||||
ba.timer(
|
||||
i * 0.15 + 0.15,
|
||||
ba.WeakCall(self._show_team_name, vval - i * height, team,
|
||||
@ -99,8 +99,8 @@ class TeamVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
|
||||
i += 1
|
||||
self.show_player_scores()
|
||||
|
||||
def _show_team_name(self, pos_v: float, team: ba.Team, kill_delay: float,
|
||||
shiftdelay: float) -> None:
|
||||
def _show_team_name(self, pos_v: float, team: ba.SessionTeam,
|
||||
kill_delay: float, shiftdelay: float) -> None:
|
||||
del kill_delay # unused arg
|
||||
ZoomText(ba.Lstr(value='${A}:', subs=[('${A}', team.name)]),
|
||||
position=(100, pos_v),
|
||||
@ -113,7 +113,7 @@ class TeamVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
|
||||
color=team.color,
|
||||
jitter=1.0).autoretain()
|
||||
|
||||
def _show_team_old_score(self, pos_v: float, team: ba.Team,
|
||||
def _show_team_old_score(self, pos_v: float, team: ba.SessionTeam,
|
||||
shiftdelay: float) -> None:
|
||||
ZoomText(str(team.sessiondata['score'] - 1),
|
||||
position=(150, pos_v),
|
||||
@ -127,8 +127,9 @@ class TeamVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
|
||||
h_align='left',
|
||||
jitter=1.0).autoretain()
|
||||
|
||||
def _show_team_score(self, pos_v: float, team: ba.Team, scored: bool,
|
||||
kill_delay: float, shiftdelay: float) -> None:
|
||||
def _show_team_score(self, pos_v: float, team: ba.SessionTeam,
|
||||
scored: bool, kill_delay: float,
|
||||
shiftdelay: float) -> None:
|
||||
del kill_delay # unused arg
|
||||
ZoomText(str(team.sessiondata['score']),
|
||||
position=(150, pos_v),
|
||||
|
||||
@ -19,13 +19,14 @@
|
||||
# SOFTWARE.
|
||||
# -----------------------------------------------------------------------------
|
||||
"""Functionality related to teams mode score screen."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import ba
|
||||
from ba.internal import ScoreScreenActivity
|
||||
from bastd.actor.text import Text
|
||||
from bastd.actor.image import Image
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any, Dict, Optional, Union
|
||||
@ -42,7 +43,6 @@ class MultiTeamScoreScreenActivity(ScoreScreenActivity):
|
||||
self._show_up_next: bool = True
|
||||
|
||||
def on_begin(self) -> None:
|
||||
from bastd.actor.text import Text
|
||||
super().on_begin()
|
||||
session = self.session
|
||||
if self._show_up_next and isinstance(session, ba.MultiTeamSession):
|
||||
@ -77,8 +77,6 @@ class MultiTeamScoreScreenActivity(ScoreScreenActivity):
|
||||
"""Show scores for individual players."""
|
||||
# pylint: disable=too-many-locals
|
||||
# pylint: disable=too-many-statements
|
||||
from bastd.actor.text import Text
|
||||
from bastd.actor.image import Image
|
||||
|
||||
ts_v_offset = 150.0 + y_offset
|
||||
ts_h_offs = 80.0 + x_offset
|
||||
@ -90,6 +88,7 @@ class MultiTeamScoreScreenActivity(ScoreScreenActivity):
|
||||
def _get_prec_score(p_rec: ba.PlayerRecord) -> Optional[int]:
|
||||
if is_free_for_all and results is not None:
|
||||
assert isinstance(results, ba.TeamGameResults)
|
||||
assert p_rec.team.gameteam is not None
|
||||
val = results.get_team_score(p_rec.team)
|
||||
return val
|
||||
return p_rec.accumscore
|
||||
@ -97,7 +96,8 @@ class MultiTeamScoreScreenActivity(ScoreScreenActivity):
|
||||
def _get_prec_score_str(p_rec: ba.PlayerRecord) -> Union[str, ba.Lstr]:
|
||||
if is_free_for_all and results is not None:
|
||||
assert isinstance(results, ba.TeamGameResults)
|
||||
val = results.get_team_score_str(p_rec.team)
|
||||
assert p_rec.team.gameteam is not None
|
||||
val = results.get_team_score_str(p_rec.team.gameteam)
|
||||
assert val is not None
|
||||
return val
|
||||
return str(p_rec.accumscore)
|
||||
@ -113,7 +113,7 @@ class MultiTeamScoreScreenActivity(ScoreScreenActivity):
|
||||
valid_players = list(self.stats.get_records().items())
|
||||
|
||||
def _get_player_score_set_entry(
|
||||
player: ba.Player) -> Optional[ba.PlayerRecord]:
|
||||
player: ba.SessionPlayer) -> Optional[ba.PlayerRecord]:
|
||||
for p_rec in valid_players:
|
||||
# PyCharm incorrectly thinks valid_players is a List[str]
|
||||
# noinspection PyUnresolvedReferences
|
||||
|
||||
@ -142,7 +142,7 @@ class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
|
||||
h_align=Text.HAlign.CENTER,
|
||||
transition_delay=t_incr * 4).autoretain()
|
||||
|
||||
win_score = (session.get_series_length() - 1) / 2 + 1
|
||||
win_score = (session.get_series_length() - 1) // 2 + 1
|
||||
lose_score = 0
|
||||
for team in self.teams:
|
||||
if team.sessiondata['score'] != win_score:
|
||||
@ -344,7 +344,7 @@ class TeamSeriesVictoryScoreScreenActivity(MultiTeamScoreScreenActivity):
|
||||
if not self.is_transitioning_out():
|
||||
ba.setmusic(ba.MusicType.VICTORY)
|
||||
|
||||
def _show_winner(self, team: ba.Team) -> None:
|
||||
def _show_winner(self, team: ba.SessionTeam) -> None:
|
||||
from bastd.actor.image import Image
|
||||
from bastd.actor.zoomtext import ZoomText
|
||||
if not self._is_ffa:
|
||||
|
||||
@ -22,13 +22,16 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Generic, TypeVar
|
||||
|
||||
import ba
|
||||
from bastd.actor.spaz import Spaz
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any, Optional, Sequence, Tuple
|
||||
from typing import Any, Sequence, Tuple, Optional
|
||||
|
||||
PlayerType = TypeVar('PlayerType', bound=ba.Player)
|
||||
TeamType = TypeVar('TeamType', bound=ba.Team)
|
||||
|
||||
|
||||
class PlayerSpazDeathMessage:
|
||||
@ -38,9 +41,6 @@ class PlayerSpazDeathMessage:
|
||||
|
||||
Attributes:
|
||||
|
||||
spaz
|
||||
The ba.PlayerSpaz that died.
|
||||
|
||||
killed
|
||||
If True, the spaz was killed;
|
||||
If False, they left the game or the round ended.
|
||||
@ -55,11 +55,22 @@ class PlayerSpazDeathMessage:
|
||||
def __init__(self, spaz: PlayerSpaz, was_killed: bool,
|
||||
killerplayer: Optional[ba.Player], how: ba.DeathType):
|
||||
"""Instantiate a message with the given values."""
|
||||
self.spaz = spaz
|
||||
self._spaz = spaz
|
||||
self.killed = was_killed
|
||||
self.killerplayer = killerplayer
|
||||
self.how = how
|
||||
|
||||
def getspaz(
|
||||
self, activity: ba.Activity[PlayerType,
|
||||
TeamType]) -> PlayerSpaz[PlayerType]:
|
||||
"""Return the spaz that died.
|
||||
|
||||
The current activity is required as an argument so the exact type of
|
||||
PlayerSpaz can be determined by the type checker.
|
||||
"""
|
||||
del activity # Unused
|
||||
return self._spaz
|
||||
|
||||
|
||||
class PlayerSpazHurtMessage:
|
||||
"""A message saying a ba.PlayerSpaz was hurt.
|
||||
@ -77,7 +88,7 @@ class PlayerSpazHurtMessage:
|
||||
self.spaz = spaz
|
||||
|
||||
|
||||
class PlayerSpaz(Spaz):
|
||||
class PlayerSpaz(Spaz, Generic[PlayerType]):
|
||||
"""A ba.Spaz subclass meant to be controlled by a ba.Player.
|
||||
|
||||
category: Gameplay Classes
|
||||
@ -91,10 +102,10 @@ class PlayerSpaz(Spaz):
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
player: PlayerType,
|
||||
color: Sequence[float] = (1.0, 1.0, 1.0),
|
||||
highlight: Sequence[float] = (0.5, 0.5, 0.5),
|
||||
character: str = 'Spaz',
|
||||
player: ba.Player = None,
|
||||
powerups_expire: bool = True):
|
||||
"""Create a spaz for the provided ba.Player.
|
||||
|
||||
@ -108,12 +119,13 @@ class PlayerSpaz(Spaz):
|
||||
source_player=player,
|
||||
start_invincible=True,
|
||||
powerups_expire=powerups_expire)
|
||||
self.last_player_attacked_by: Optional[ba.Player] = None
|
||||
self.last_player_attacked_by: Optional[PlayerType] = None
|
||||
self.last_attacked_time = 0.0
|
||||
self.last_attacked_type: Optional[Tuple[str, str]] = None
|
||||
self.held_count = 0
|
||||
self.last_player_held_by: Optional[ba.Player] = None
|
||||
self.last_player_held_by: Optional[PlayerType] = None
|
||||
self._player = player
|
||||
self.playertype = type(player)
|
||||
|
||||
# Grab the node for this player and wire it to follow our spaz
|
||||
# (so players' controllers know where to draw their guides, etc).
|
||||
@ -123,7 +135,7 @@ class PlayerSpaz(Spaz):
|
||||
self.node.connectattr('torso_position', player.node, 'position')
|
||||
|
||||
@property
|
||||
def player(self) -> ba.Player:
|
||||
def player(self) -> PlayerType:
|
||||
"""The ba.Player associated with this Spaz.
|
||||
|
||||
If the player no longer exists, raises an ba.PlayerNotFoundError.
|
||||
@ -132,7 +144,7 @@ class PlayerSpaz(Spaz):
|
||||
raise ba.PlayerNotFoundError()
|
||||
return self._player
|
||||
|
||||
def getplayer(self) -> Optional[ba.Player]:
|
||||
def getplayer(self) -> Optional[PlayerType]:
|
||||
"""Get the ba.Player associated with this Spaz.
|
||||
|
||||
Note that this may return None if the player has left.
|
||||
@ -226,7 +238,8 @@ class PlayerSpaz(Spaz):
|
||||
if isinstance(msg, ba.PickedUpMessage):
|
||||
super().handlemessage(msg) # Augment standard behavior.
|
||||
self.held_count += 1
|
||||
picked_up_by = msg.node.source_player
|
||||
picked_up_by = ba.playercast_o(self.playertype,
|
||||
msg.node.source_player)
|
||||
if picked_up_by:
|
||||
self.last_player_held_by = picked_up_by
|
||||
elif isinstance(msg, ba.DroppedMessage):
|
||||
@ -237,11 +250,12 @@ class PlayerSpaz(Spaz):
|
||||
|
||||
# Let's count someone dropping us as an attack.
|
||||
try:
|
||||
picked_up_by = msg.node.source_player
|
||||
picked_up_by_2 = ba.playercast_o(self.playertype,
|
||||
msg.node.source_player)
|
||||
except Exception:
|
||||
picked_up_by = None
|
||||
if picked_up_by:
|
||||
self.last_player_attacked_by = picked_up_by
|
||||
picked_up_by_2 = None
|
||||
if picked_up_by_2:
|
||||
self.last_player_attacked_by = picked_up_by_2
|
||||
self.last_attacked_time = ba.time()
|
||||
self.last_attacked_type = ('picked_up', 'default')
|
||||
elif isinstance(msg, ba.DieMessage):
|
||||
@ -296,7 +310,8 @@ class PlayerSpaz(Spaz):
|
||||
# Keep track of the player who last hit us for point rewarding.
|
||||
elif isinstance(msg, ba.HitMessage):
|
||||
if msg.source_player:
|
||||
self.last_player_attacked_by = msg.source_player
|
||||
srcplayer = ba.playercast_o(self.playertype, msg.source_player)
|
||||
self.last_player_attacked_by = srcplayer
|
||||
self.last_attacked_time = ba.time()
|
||||
self.last_attacked_type = (msg.hit_type, msg.hit_subtype)
|
||||
super().handlemessage(msg) # Augment standard behavior.
|
||||
|
||||
@ -136,7 +136,7 @@ class RespawnIcon:
|
||||
"""Return info on where we should be shown and stored."""
|
||||
activity = ba.getactivity()
|
||||
if isinstance(ba.getsession(), ba.DualTeamSession):
|
||||
on_right = player.team.get_id() % 2 == 1
|
||||
on_right = player.team.id % 2 == 1
|
||||
|
||||
# Store a list of icons in the team.
|
||||
try:
|
||||
|
||||
@ -317,7 +317,7 @@ class _EntryProxy:
|
||||
self._scoreboard = weakref.ref(scoreboard)
|
||||
# have to store ID here instead of a weak-ref since the team will be
|
||||
# dead when we die and need to remove it
|
||||
self._team_id = team.get_id()
|
||||
self._team_id = team.id
|
||||
|
||||
def __del__(self) -> None:
|
||||
scoreboard = self._scoreboard()
|
||||
@ -366,7 +366,7 @@ class Scoreboard:
|
||||
flash: bool = True,
|
||||
show_value: bool = True) -> None:
|
||||
"""Update the score-board display for the given ba.Team."""
|
||||
if not team.get_id() in self._entries:
|
||||
if not team.id in self._entries:
|
||||
self._add_team(team)
|
||||
# create a proxy in the team which will kill
|
||||
# our entry when it dies (for convenience)
|
||||
@ -374,21 +374,21 @@ class Scoreboard:
|
||||
raise Exception('existing _EntryProxy found')
|
||||
team.gamedata['_scoreboard_entry'] = _EntryProxy(self, team)
|
||||
# now set the entry..
|
||||
self._entries[team.get_id()].set_value(score=score,
|
||||
max_score=max_score,
|
||||
countdown=countdown,
|
||||
flash=flash,
|
||||
show_value=show_value)
|
||||
self._entries[team.id].set_value(score=score,
|
||||
max_score=max_score,
|
||||
countdown=countdown,
|
||||
flash=flash,
|
||||
show_value=show_value)
|
||||
|
||||
def _add_team(self, team: ba.Team) -> None:
|
||||
if team.get_id() in self._entries:
|
||||
if team.id in self._entries:
|
||||
raise Exception('Duplicate team add')
|
||||
self._entries[team.get_id()] = _Entry(self,
|
||||
team,
|
||||
do_cover=self._do_cover,
|
||||
scale=self._scale,
|
||||
label=self._label,
|
||||
flash_length=self._flash_length)
|
||||
self._entries[team.id] = _Entry(self,
|
||||
team,
|
||||
do_cover=self._do_cover,
|
||||
scale=self._scale,
|
||||
label=self._label,
|
||||
flash_length=self._flash_length)
|
||||
self._update_teams()
|
||||
|
||||
def remove_team(self, team_id: int) -> None:
|
||||
|
||||
@ -998,7 +998,7 @@ class BotSet:
|
||||
|
||||
# Don't do this if the activity is shutting down or dead.
|
||||
activity: Optional[ba.Activity] = ba.getactivity(doraise=False)
|
||||
if activity is None or activity.is_expired():
|
||||
if activity is None or activity.expired:
|
||||
return
|
||||
|
||||
for i in range(len(self._bot_lists)):
|
||||
|
||||
@ -29,14 +29,14 @@ import random
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import ba
|
||||
from bastd.actor import playerspaz
|
||||
from bastd.actor.playerspaz import PlayerSpaz, PlayerSpazDeathMessage
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any, Type, List, Dict, Tuple, Sequence, Union
|
||||
|
||||
|
||||
# ba_meta export game
|
||||
class AssaultGame(ba.TeamGameActivity):
|
||||
class AssaultGame(ba.TeamGameActivity[ba.Player, ba.Team]):
|
||||
"""Game where you score by touching the other team's flag."""
|
||||
|
||||
@classmethod
|
||||
@ -109,7 +109,7 @@ class AssaultGame(ba.TeamGameActivity):
|
||||
self.setup_standard_time_limit(self.settings_raw['Time Limit'])
|
||||
self.setup_standard_powerup_drops()
|
||||
for team in self.teams:
|
||||
mat = self._base_region_materials[team.get_id()] = ba.Material()
|
||||
mat = self._base_region_materials[team.id] = ba.Material()
|
||||
mat.add_actions(conditions=('they_have_material',
|
||||
ba.sharedobj('player_material')),
|
||||
actions=(('modify_part_collision', 'collide',
|
||||
@ -121,8 +121,7 @@ class AssaultGame(ba.TeamGameActivity):
|
||||
|
||||
# Create a score region and flag for each team.
|
||||
for team in self.teams:
|
||||
team.gamedata['base_pos'] = self.map.get_flag_position(
|
||||
team.get_id())
|
||||
team.gamedata['base_pos'] = self.map.get_flag_position(team.id)
|
||||
|
||||
ba.newnode('light',
|
||||
attrs={
|
||||
@ -139,20 +138,20 @@ class AssaultGame(ba.TeamGameActivity):
|
||||
position=team.gamedata['base_pos'],
|
||||
color=team.color)
|
||||
basepos = team.gamedata['base_pos']
|
||||
ba.newnode(
|
||||
'region',
|
||||
owner=team.gamedata['flag'].node,
|
||||
attrs={
|
||||
'position': (basepos[0], basepos[1] + 0.75, basepos[2]),
|
||||
'scale': (0.5, 0.5, 0.5),
|
||||
'type': 'sphere',
|
||||
'materials': [self._base_region_materials[team.get_id()]]
|
||||
})
|
||||
ba.newnode('region',
|
||||
owner=team.gamedata['flag'].node,
|
||||
attrs={
|
||||
'position':
|
||||
(basepos[0], basepos[1] + 0.75, basepos[2]),
|
||||
'scale': (0.5, 0.5, 0.5),
|
||||
'type': 'sphere',
|
||||
'materials': [self._base_region_materials[team.id]]
|
||||
})
|
||||
|
||||
def handlemessage(self, msg: Any) -> Any:
|
||||
if isinstance(msg, playerspaz.PlayerSpazDeathMessage):
|
||||
if isinstance(msg, PlayerSpazDeathMessage):
|
||||
super().handlemessage(msg) # Augment standard.
|
||||
self.respawn_player(msg.spaz.player)
|
||||
self.respawn_player(msg.getspaz(self).player)
|
||||
else:
|
||||
super().handlemessage(msg)
|
||||
|
||||
@ -173,14 +172,15 @@ class AssaultGame(ba.TeamGameActivity):
|
||||
cnode = ba.get_collision_info('opposing_node')
|
||||
assert isinstance(cnode, ba.Node)
|
||||
actor = cnode.getdelegate()
|
||||
if not isinstance(actor, playerspaz.PlayerSpaz):
|
||||
if not isinstance(actor, PlayerSpaz):
|
||||
return
|
||||
|
||||
player = actor.getplayer()
|
||||
if not player or not player.is_alive():
|
||||
if not player or not player.actor:
|
||||
return
|
||||
|
||||
# If its another team's player, they scored.
|
||||
player_team = player.team
|
||||
player_team: ba.Team[ba.Player] = player.team
|
||||
if player_team is not team:
|
||||
|
||||
# Prevent multiple simultaneous scores.
|
||||
@ -194,24 +194,22 @@ class AssaultGame(ba.TeamGameActivity):
|
||||
# and add flashes of light so its noticeable.
|
||||
for player in player_team.players:
|
||||
if player.is_alive():
|
||||
if player.node:
|
||||
pos = player.node.position
|
||||
light = ba.newnode('light',
|
||||
attrs={
|
||||
'position': pos,
|
||||
'color': player_team.color,
|
||||
'height_attenuated': False,
|
||||
'radius': 0.4
|
||||
})
|
||||
ba.timer(0.5, light.delete)
|
||||
ba.animate(light, 'intensity', {
|
||||
0: 0,
|
||||
0.1: 1.0,
|
||||
0.5: 0
|
||||
})
|
||||
pos = player.node.position
|
||||
light = ba.newnode('light',
|
||||
attrs={
|
||||
'position': pos,
|
||||
'color': player_team.color,
|
||||
'height_attenuated': False,
|
||||
'radius': 0.4
|
||||
})
|
||||
ba.timer(0.5, light.delete)
|
||||
ba.animate(light, 'intensity', {
|
||||
0: 0,
|
||||
0.1: 1.0,
|
||||
0.5: 0
|
||||
})
|
||||
|
||||
new_pos = (self.map.get_start_position(
|
||||
player_team.get_id()))
|
||||
new_pos = (self.map.get_start_position(player_team.id))
|
||||
light = ba.newnode('light',
|
||||
attrs={
|
||||
'position': new_pos,
|
||||
|
||||
@ -25,11 +25,13 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import ba
|
||||
from bastd.actor import flag as stdflag
|
||||
from bastd.actor.playerspaz import PlayerSpaz, PlayerSpazDeathMessage
|
||||
from bastd.actor.scoreboard import Scoreboard
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any, Type, List, Dict, Tuple, Sequence, Union, Optional
|
||||
@ -38,9 +40,10 @@ if TYPE_CHECKING:
|
||||
class CTFFlag(stdflag.Flag):
|
||||
"""Special flag type for ctf games."""
|
||||
|
||||
def __init__(self, team: ba.Team):
|
||||
super().__init__(materials=[team.gamedata['flagmaterial']],
|
||||
position=team.gamedata['base_pos'],
|
||||
def __init__(self, team: Team):
|
||||
assert team.flagmaterial is not None
|
||||
super().__init__(materials=[team.flagmaterial],
|
||||
position=team.base_pos,
|
||||
color=team.color)
|
||||
self._team = team
|
||||
self.held_count = 0
|
||||
@ -52,7 +55,7 @@ class CTFFlag(stdflag.Flag):
|
||||
'h_align': 'center'
|
||||
})
|
||||
self.reset_return_times()
|
||||
self.last_player_to_hold: Optional[ba.Player] = None
|
||||
self.last_player_to_hold: Optional[Player] = None
|
||||
self.time_out_respawn_time: Optional[int] = None
|
||||
self.touch_return_time: Optional[float] = None
|
||||
|
||||
@ -64,7 +67,7 @@ class CTFFlag(stdflag.Flag):
|
||||
self.activity.settings_raw['Flag Touch Return Time'])
|
||||
|
||||
@property
|
||||
def team(self) -> ba.Team:
|
||||
def team(self) -> Team:
|
||||
"""return the flag's team."""
|
||||
return self._team
|
||||
|
||||
@ -77,8 +80,33 @@ class CTFFlag(stdflag.Flag):
|
||||
return delegate if isinstance(delegate, CTFFlag) else None
|
||||
|
||||
|
||||
@dataclass
|
||||
class Player(ba.Player['Team']):
|
||||
"""Our player type for this game."""
|
||||
touching_own_flag: int = 0
|
||||
|
||||
|
||||
@dataclass
|
||||
class Team(ba.Team[Player]):
|
||||
"""Our team type for this game."""
|
||||
base_pos: Sequence[float]
|
||||
base_region_material: ba.Material
|
||||
base_region: ba.Node
|
||||
spaz_material_no_flag_physical: ba.Material
|
||||
spaz_material_no_flag_collide: ba.Material
|
||||
flagmaterial: ba.Material
|
||||
score: int = 0
|
||||
flag_return_touches: int = 0
|
||||
home_flag_at_base: bool = True
|
||||
touch_return_timer: Optional[ba.Timer] = None
|
||||
enemy_flag_at_base: bool = False
|
||||
flag: Optional[CTFFlag] = None
|
||||
last_flag_leave_time: Optional[float] = None
|
||||
touch_return_timer_ticking: Optional[ba.NodeActor] = None
|
||||
|
||||
|
||||
# ba_meta export game
|
||||
class CaptureTheFlagGame(ba.TeamGameActivity):
|
||||
class CaptureTheFlagGame(ba.TeamGameActivity[Player, Team]):
|
||||
"""Game of stealing other team's flag and returning it to your base."""
|
||||
|
||||
@classmethod
|
||||
@ -102,24 +130,37 @@ class CaptureTheFlagGame(ba.TeamGameActivity):
|
||||
cls,
|
||||
sessiontype: Type[ba.Session]) -> List[Tuple[str, Dict[str, Any]]]:
|
||||
return [
|
||||
('Score to Win', {'min_value': 1, 'default': 3}),
|
||||
('Score to Win', {
|
||||
'min_value': 1,
|
||||
'default': 3
|
||||
}),
|
||||
('Flag Touch Return Time', {
|
||||
'min_value': 0, 'default': 0, 'increment': 1}),
|
||||
'min_value': 0,
|
||||
'default': 0,
|
||||
'increment': 1
|
||||
}),
|
||||
('Flag Idle Return Time', {
|
||||
'min_value': 5, 'default': 30, 'increment': 5}),
|
||||
'min_value': 5,
|
||||
'default': 30,
|
||||
'increment': 5
|
||||
}),
|
||||
('Time Limit', {
|
||||
'choices': [('None', 0), ('1 Minute', 60),
|
||||
('2 Minutes', 120), ('5 Minutes', 300),
|
||||
('10 Minutes', 600), ('20 Minutes', 1200)],
|
||||
'default': 0}),
|
||||
'choices': [('None', 0), ('1 Minute', 60), ('2 Minutes', 120),
|
||||
('5 Minutes', 300), ('10 Minutes', 600),
|
||||
('20 Minutes', 1200)],
|
||||
'default': 0
|
||||
}),
|
||||
('Respawn Times', {
|
||||
'choices': [('Shorter', 0.25), ('Short', 0.5), ('Normal', 1.0),
|
||||
('Long', 2.0), ('Longer', 4.0)],
|
||||
'default': 1.0}),
|
||||
('Epic Mode', {'default': False})] # yapf: disable
|
||||
'default': 1.0
|
||||
}),
|
||||
('Epic Mode', {
|
||||
'default': False
|
||||
}),
|
||||
]
|
||||
|
||||
def __init__(self, settings: Dict[str, Any]):
|
||||
from bastd.actor.scoreboard import Scoreboard
|
||||
super().__init__(settings)
|
||||
self._scoreboard = Scoreboard()
|
||||
if self.settings_raw['Epic Mode']:
|
||||
@ -149,29 +190,24 @@ class CaptureTheFlagGame(ba.TeamGameActivity):
|
||||
ba.MusicType.FLAG_CATCHER)
|
||||
super().on_transition_in()
|
||||
|
||||
def on_team_join(self, team: ba.Team) -> None:
|
||||
team.gamedata['score'] = 0
|
||||
team.gamedata['flag_return_touches'] = 0
|
||||
team.gamedata['home_flag_at_base'] = True
|
||||
team.gamedata['touch_return_timer'] = None
|
||||
team.gamedata['enemy_flag_at_base'] = False
|
||||
team.gamedata['base_pos'] = (self.map.get_flag_position(team.get_id()))
|
||||
def create_team(self, sessionteam: ba.SessionTeam) -> Team:
|
||||
|
||||
self.project_flag_stand(team.gamedata['base_pos'])
|
||||
base_pos = self.map.get_flag_position(sessionteam.id)
|
||||
self.project_flag_stand(base_pos)
|
||||
|
||||
ba.newnode('light',
|
||||
attrs={
|
||||
'position': team.gamedata['base_pos'],
|
||||
'position': base_pos,
|
||||
'intensity': 0.6,
|
||||
'height_attenuated': False,
|
||||
'volume_intensity_scale': 0.1,
|
||||
'radius': 0.1,
|
||||
'color': team.color
|
||||
'color': sessionteam.color
|
||||
})
|
||||
|
||||
base_region_mat = team.gamedata['base_region_material'] = ba.Material()
|
||||
pos = team.gamedata['base_pos']
|
||||
team.gamedata['base_region'] = ba.newnode(
|
||||
base_region_mat = ba.Material()
|
||||
pos = base_pos
|
||||
base_region = ba.newnode(
|
||||
'region',
|
||||
attrs={
|
||||
'position': (pos[0], pos[1] + 0.75, pos[2]),
|
||||
@ -180,22 +216,28 @@ class CaptureTheFlagGame(ba.TeamGameActivity):
|
||||
'materials': [base_region_mat, self._all_bases_material]
|
||||
})
|
||||
|
||||
# create some materials for this team
|
||||
spaz_mat_no_flag_physical = team.gamedata[
|
||||
'spaz_material_no_flag_physical'] = ba.Material()
|
||||
spaz_mat_no_flag_collide = team.gamedata[
|
||||
'spaz_material_no_flag_collide'] = ba.Material()
|
||||
flagmat = team.gamedata['flagmaterial'] = ba.Material()
|
||||
spaz_mat_no_flag_physical = ba.Material()
|
||||
spaz_mat_no_flag_collide = ba.Material()
|
||||
flagmat = ba.Material()
|
||||
|
||||
team = Team(base_pos=base_pos,
|
||||
base_region_material=base_region_mat,
|
||||
base_region=base_region,
|
||||
spaz_material_no_flag_physical=spaz_mat_no_flag_physical,
|
||||
spaz_material_no_flag_collide=spaz_mat_no_flag_collide,
|
||||
flagmaterial=flagmat)
|
||||
|
||||
# Some parts of our spazzes don't collide physically with our
|
||||
# flags but generate callbacks.
|
||||
spaz_mat_no_flag_physical.add_actions(
|
||||
conditions=('they_have_material', flagmat),
|
||||
actions=(('modify_part_collision', 'physical',
|
||||
False), ('call', 'at_connect',
|
||||
lambda: self._handle_hit_own_flag(team, 1)),
|
||||
('call', 'at_disconnect',
|
||||
lambda: self._handle_hit_own_flag(team, 0))))
|
||||
actions=(
|
||||
('modify_part_collision', 'physical', False),
|
||||
('call', 'at_connect',
|
||||
lambda: self._handle_hit_own_flag(team, 1)),
|
||||
('call', 'at_disconnect',
|
||||
lambda: self._handle_hit_own_flag(team, 0)),
|
||||
))
|
||||
|
||||
# Other parts of our spazzes don't collide with our flags at all.
|
||||
spaz_mat_no_flag_collide.add_actions(conditions=('they_have_material',
|
||||
@ -214,6 +256,9 @@ class CaptureTheFlagGame(ba.TeamGameActivity):
|
||||
('call', 'at_disconnect',
|
||||
lambda: self._handle_flag_left_base(team))))
|
||||
|
||||
return team
|
||||
|
||||
def on_team_join(self, team: Team) -> None:
|
||||
self._spawn_flag_for_team(team)
|
||||
self._update_scoreboard()
|
||||
|
||||
@ -223,14 +268,14 @@ class CaptureTheFlagGame(ba.TeamGameActivity):
|
||||
self.setup_standard_powerup_drops()
|
||||
ba.timer(1.0, call=self._tick, repeat=True)
|
||||
|
||||
def _spawn_flag_for_team(self, team: ba.Team) -> None:
|
||||
flag = team.gamedata['flag'] = CTFFlag(team)
|
||||
team.gamedata['flag_return_touches'] = 0
|
||||
def _spawn_flag_for_team(self, team: Team) -> None:
|
||||
team.flag = CTFFlag(team)
|
||||
team.flag_return_touches = 0
|
||||
self._flash_base(team, length=1.0)
|
||||
assert flag.node
|
||||
ba.playsound(self._swipsound, position=flag.node.position)
|
||||
assert team.flag.node
|
||||
ba.playsound(self._swipsound, position=team.flag.node.position)
|
||||
|
||||
def _handle_flag_entered_base(self, team: ba.Team) -> None:
|
||||
def _handle_flag_entered_base(self, team: Team) -> None:
|
||||
node = ba.get_collision_info('opposing_node')
|
||||
assert isinstance(node, (ba.Node, type(None)))
|
||||
flag = CTFFlag.from_node(node)
|
||||
@ -239,14 +284,14 @@ class CaptureTheFlagGame(ba.TeamGameActivity):
|
||||
return
|
||||
|
||||
if flag.team is team:
|
||||
team.gamedata['home_flag_at_base'] = True
|
||||
team.home_flag_at_base = True
|
||||
|
||||
# If the enemy flag is already here, score!
|
||||
if team.gamedata['enemy_flag_at_base']:
|
||||
if team.enemy_flag_at_base:
|
||||
self._score(team)
|
||||
else:
|
||||
team.gamedata['enemy_flag_at_base'] = True
|
||||
if team.gamedata['home_flag_at_base']:
|
||||
team.enemy_flag_at_base = True
|
||||
if team.home_flag_at_base:
|
||||
# Award points to whoever was carrying the enemy flag.
|
||||
player = flag.last_player_to_hold
|
||||
if player and player.team is team:
|
||||
@ -262,7 +307,7 @@ class CaptureTheFlagGame(ba.TeamGameActivity):
|
||||
curtime = ba.time(ba.TimeType.BASE)
|
||||
if curtime - self._last_home_flag_notice_print_time > 5.0:
|
||||
self._last_home_flag_notice_print_time = curtime
|
||||
bpos = team.gamedata['base_pos']
|
||||
bpos = team.base_pos
|
||||
tval = ba.Lstr(resource='ownFlagAtYourBaseWarning')
|
||||
tnode = ba.newnode(
|
||||
'text',
|
||||
@ -286,10 +331,10 @@ class CaptureTheFlagGame(ba.TeamGameActivity):
|
||||
# If either flag is away from base and not being held, tick down its
|
||||
# respawn timer.
|
||||
for team in self.teams:
|
||||
flag = team.gamedata['flag']
|
||||
flag = team.flag
|
||||
assert flag is not None
|
||||
|
||||
if (not team.gamedata['home_flag_at_base']
|
||||
and flag.held_count == 0):
|
||||
if not team.home_flag_at_base and flag.held_count == 0:
|
||||
time_out_counting_down = True
|
||||
if flag.time_out_respawn_time is None:
|
||||
flag.reset_return_times()
|
||||
@ -307,16 +352,16 @@ class CaptureTheFlagGame(ba.TeamGameActivity):
|
||||
# If there's no self-touches on this flag, set its text
|
||||
# to show its auto-return counter. (if there's self-touches
|
||||
# its showing that time).
|
||||
if team.gamedata['flag_return_touches'] == 0:
|
||||
flag.counter.text = (str(flag.time_out_respawn_time) if
|
||||
(time_out_counting_down
|
||||
and flag.time_out_respawn_time <= 10)
|
||||
else '')
|
||||
if team.flag_return_touches == 0:
|
||||
flag.counter.text = (str(flag.time_out_respawn_time) if (
|
||||
time_out_counting_down
|
||||
and flag.time_out_respawn_time is not None
|
||||
and flag.time_out_respawn_time <= 10) else '')
|
||||
flag.counter.color = (1, 1, 1, 0.5)
|
||||
flag.counter.scale = 0.014
|
||||
|
||||
def _score(self, team: ba.Team) -> None:
|
||||
team.gamedata['score'] += 1
|
||||
def _score(self, team: Team) -> None:
|
||||
team.score += 1
|
||||
ba.playsound(self._score_sound)
|
||||
self._flash_base(team)
|
||||
self._update_scoreboard()
|
||||
@ -328,58 +373,57 @@ class CaptureTheFlagGame(ba.TeamGameActivity):
|
||||
|
||||
# Reset all flags/state.
|
||||
for reset_team in self.teams:
|
||||
if not reset_team.gamedata['home_flag_at_base']:
|
||||
reset_team.gamedata['flag'].handlemessage(ba.DieMessage())
|
||||
reset_team.gamedata['enemy_flag_at_base'] = False
|
||||
if team.gamedata['score'] >= self.settings_raw['Score to Win']:
|
||||
if not reset_team.home_flag_at_base:
|
||||
assert reset_team.flag is not None
|
||||
reset_team.flag.handlemessage(ba.DieMessage())
|
||||
reset_team.enemy_flag_at_base = False
|
||||
if team.score >= self.settings_raw['Score to Win']:
|
||||
self.end_game()
|
||||
|
||||
def end_game(self) -> None:
|
||||
results = ba.TeamGameResults()
|
||||
for team in self.teams:
|
||||
results.set_team_score(team, team.gamedata['score'])
|
||||
results.set_team_score(team, team.score)
|
||||
self.end(results=results, announce_delay=0.8)
|
||||
|
||||
def _handle_flag_left_base(self, team: ba.Team) -> None:
|
||||
def _handle_flag_left_base(self, team: Team) -> None:
|
||||
cur_time = ba.time()
|
||||
op_node = ba.get_collision_info('opposing_node')
|
||||
assert isinstance(op_node, (ba.Node, type(None)))
|
||||
flag = CTFFlag.from_node(op_node)
|
||||
if not flag:
|
||||
return
|
||||
|
||||
if flag.team is team:
|
||||
|
||||
# Check times here to prevent too much flashing.
|
||||
if ('last_flag_leave_time' not in team.gamedata
|
||||
or cur_time - team.gamedata['last_flag_leave_time'] > 3.0):
|
||||
ba.playsound(self._alarmsound,
|
||||
position=team.gamedata['base_pos'])
|
||||
if (team.last_flag_leave_time is None
|
||||
or cur_time - team.last_flag_leave_time > 3.0):
|
||||
ba.playsound(self._alarmsound, position=team.base_pos)
|
||||
self._flash_base(team)
|
||||
team.gamedata['last_flag_leave_time'] = cur_time
|
||||
team.gamedata['home_flag_at_base'] = False
|
||||
team.last_flag_leave_time = cur_time
|
||||
team.home_flag_at_base = False
|
||||
else:
|
||||
team.gamedata['enemy_flag_at_base'] = False
|
||||
|
||||
def _touch_return_update(self, team: ba.Team) -> None:
|
||||
team.enemy_flag_at_base = False
|
||||
|
||||
def _touch_return_update(self, team: Team) -> None:
|
||||
# Count down only while its away from base and not being held.
|
||||
if (team.gamedata['home_flag_at_base']
|
||||
or team.gamedata['flag'].held_count > 0):
|
||||
team.gamedata['touch_return_timer_ticking'] = None
|
||||
assert team.flag is not None
|
||||
if team.home_flag_at_base or team.flag.held_count > 0:
|
||||
team.touch_return_timer_ticking = None
|
||||
return # No need to return when its at home.
|
||||
if team.gamedata['touch_return_timer_ticking'] is None:
|
||||
team.gamedata['touch_return_timer_ticking'] = ba.NodeActor(
|
||||
if team.touch_return_timer_ticking is None:
|
||||
team.touch_return_timer_ticking = ba.NodeActor(
|
||||
ba.newnode('sound',
|
||||
attrs={
|
||||
'sound': self._ticking_sound,
|
||||
'positional': False,
|
||||
'loop': True
|
||||
}))
|
||||
flag = team.gamedata['flag']
|
||||
flag = team.flag
|
||||
assert flag.touch_return_time is not None
|
||||
flag.touch_return_time -= 0.1
|
||||
if flag.counter:
|
||||
flag.counter.text = '%.1f' % flag.touch_return_time
|
||||
flag.counter.text = f'{flag.touch_return_time:.1f}'
|
||||
flag.counter.color = (1, 1, 0, 1)
|
||||
flag.counter.scale = 0.02
|
||||
|
||||
@ -387,9 +431,9 @@ class CaptureTheFlagGame(ba.TeamGameActivity):
|
||||
self._award_players_touching_own_flag(team)
|
||||
flag.handlemessage(ba.DieMessage())
|
||||
|
||||
def _award_players_touching_own_flag(self, team: ba.Team) -> None:
|
||||
def _award_players_touching_own_flag(self, team: Team) -> None:
|
||||
for player in team.players:
|
||||
if player.gamedata['touching_own_flag'] > 0:
|
||||
if player.touching_own_flag > 0:
|
||||
return_score = 10 + 5 * int(
|
||||
self.settings_raw['Flag Touch Return Time'])
|
||||
self.stats.player_scored(player,
|
||||
@ -397,7 +441,7 @@ class CaptureTheFlagGame(ba.TeamGameActivity):
|
||||
screenmessage=False)
|
||||
|
||||
@staticmethod
|
||||
def _player_from_node(node: Optional[ba.Node]) -> Optional[ba.Player]:
|
||||
def _player_from_node(node: Optional[ba.Node]) -> Optional[Player]:
|
||||
"""Return a player if given a node that is part of one's actor."""
|
||||
if not node:
|
||||
return None
|
||||
@ -406,7 +450,7 @@ class CaptureTheFlagGame(ba.TeamGameActivity):
|
||||
return None
|
||||
return delegate.getplayer()
|
||||
|
||||
def _handle_hit_own_flag(self, team: ba.Team, val: int) -> None:
|
||||
def _handle_hit_own_flag(self, team: Team, val: int) -> None:
|
||||
"""
|
||||
keep track of when each player is touching their
|
||||
own flag so we can award points when returned
|
||||
@ -415,13 +459,13 @@ class CaptureTheFlagGame(ba.TeamGameActivity):
|
||||
assert isinstance(srcnode, (ba.Node, type(None)))
|
||||
player = self._player_from_node(srcnode)
|
||||
if player:
|
||||
player.gamedata['touching_own_flag'] += (1 if val else -1)
|
||||
player.touching_own_flag += (1 if val else -1)
|
||||
|
||||
# If return-time is zero, just kill it immediately.. otherwise keep
|
||||
# track of touches and count down.
|
||||
if float(self.settings_raw['Flag Touch Return Time']) <= 0.0:
|
||||
if (not team.gamedata['home_flag_at_base']
|
||||
and team.gamedata['flag'].held_count == 0):
|
||||
assert team.flag is not None
|
||||
if not team.home_flag_at_base and team.flag.held_count == 0:
|
||||
|
||||
# Use a node message to kill the flag instead of just killing
|
||||
# our team's. (avoids redundantly killing new flags if
|
||||
@ -434,26 +478,26 @@ class CaptureTheFlagGame(ba.TeamGameActivity):
|
||||
# Takes a non-zero amount of time to return.
|
||||
else:
|
||||
if val:
|
||||
team.gamedata['flag_return_touches'] += 1
|
||||
if team.gamedata['flag_return_touches'] == 1:
|
||||
team.gamedata['touch_return_timer'] = ba.Timer(
|
||||
team.flag_return_touches += 1
|
||||
if team.flag_return_touches == 1:
|
||||
team.touch_return_timer = ba.Timer(
|
||||
0.1,
|
||||
call=ba.Call(self._touch_return_update, team),
|
||||
repeat=True)
|
||||
team.gamedata['touch_return_timer_ticking'] = None
|
||||
team.touch_return_timer_ticking = None
|
||||
else:
|
||||
team.gamedata['flag_return_touches'] -= 1
|
||||
if team.gamedata['flag_return_touches'] == 0:
|
||||
team.gamedata['touch_return_timer'] = None
|
||||
team.gamedata['touch_return_timer_ticking'] = None
|
||||
if team.gamedata['flag_return_touches'] < 0:
|
||||
team.flag_return_touches -= 1
|
||||
if team.flag_return_touches == 0:
|
||||
team.touch_return_timer = None
|
||||
team.touch_return_timer_ticking = None
|
||||
if team.flag_return_touches < 0:
|
||||
ba.print_error(
|
||||
"CTF: flag_return_touches < 0; this shouldn't happen.")
|
||||
|
||||
def _flash_base(self, team: ba.Team, length: float = 2.0) -> None:
|
||||
def _flash_base(self, team: Team, length: float = 2.0) -> None:
|
||||
light = ba.newnode('light',
|
||||
attrs={
|
||||
'position': team.gamedata['base_pos'],
|
||||
'position': team.base_pos,
|
||||
'height_attenuated': False,
|
||||
'radius': 0.3,
|
||||
'color': team.color
|
||||
@ -461,22 +505,21 @@ class CaptureTheFlagGame(ba.TeamGameActivity):
|
||||
ba.animate(light, 'intensity', {0.0: 0, 0.25: 2.0, 0.5: 0}, loop=True)
|
||||
ba.timer(length, light.delete)
|
||||
|
||||
def spawn_player_spaz(self, *args: Any, **keywds: Any) -> Any:
|
||||
def spawn_player_spaz(self,
|
||||
player: Player,
|
||||
position: Sequence[float] = None,
|
||||
angle: float = None) -> PlayerSpaz:
|
||||
"""Intercept new spazzes and add our team material for them."""
|
||||
# (chill pylint; we're passing our exact args to parent call)
|
||||
# pylint: disable=signature-differs
|
||||
spaz = super().spawn_player_spaz(*args, **keywds)
|
||||
spaz = super().spawn_player_spaz(player, position, angle)
|
||||
player = spaz.player
|
||||
player.gamedata['touching_own_flag'] = 0
|
||||
|
||||
# Ignore false alarm for gamedata member.
|
||||
no_physical_mats = [
|
||||
player.team.gamedata['spaz_material_no_flag_physical']
|
||||
team: Team = player.team
|
||||
player.touching_own_flag = 0
|
||||
no_physical_mats: List[ba.Material] = [
|
||||
team.spaz_material_no_flag_physical
|
||||
]
|
||||
no_collide_mats = [
|
||||
player.team.gamedata['spaz_material_no_flag_collide']
|
||||
no_collide_mats: List[ba.Material] = [
|
||||
team.spaz_material_no_flag_collide
|
||||
]
|
||||
# pylint: enable=arguments-differ
|
||||
|
||||
# Our normal parts should still collide; just not physically
|
||||
# (so we can calc restores).
|
||||
@ -496,14 +539,14 @@ class CaptureTheFlagGame(ba.TeamGameActivity):
|
||||
|
||||
def _update_scoreboard(self) -> None:
|
||||
for team in self.teams:
|
||||
self._scoreboard.set_team_value(team, team.gamedata['score'],
|
||||
self._scoreboard.set_team_value(team, team.score,
|
||||
self.settings_raw['Score to Win'])
|
||||
|
||||
def handlemessage(self, msg: Any) -> Any:
|
||||
if isinstance(msg, PlayerSpazDeathMessage):
|
||||
# Augment standard behavior.
|
||||
super().handlemessage(msg)
|
||||
self.respawn_player(msg.spaz.player)
|
||||
self.respawn_player(msg.getspaz(self).player)
|
||||
elif isinstance(msg, stdflag.FlagDeathMessage):
|
||||
assert isinstance(msg.flag, CTFFlag)
|
||||
ba.timer(0.1, ba.Call(self._spawn_flag_for_team, msg.flag.team))
|
||||
|
||||
@ -37,7 +37,7 @@ if TYPE_CHECKING:
|
||||
|
||||
|
||||
# ba_meta export game
|
||||
class ChosenOneGame(ba.TeamGameActivity):
|
||||
class ChosenOneGame(ba.TeamGameActivity[ba.Player, ba.Team]):
|
||||
"""
|
||||
Game involving trying to remain the one 'chosen one'
|
||||
for a set length of time while everyone else tries to
|
||||
@ -327,7 +327,7 @@ class ChosenOneGame(ba.TeamGameActivity):
|
||||
if isinstance(msg, playerspaz.PlayerSpazDeathMessage):
|
||||
# Augment standard behavior.
|
||||
super().handlemessage(msg)
|
||||
player = msg.spaz.player
|
||||
player = msg.getspaz(self).player
|
||||
if player is self._get_chosen_one_player():
|
||||
killerplayer = msg.killerplayer
|
||||
self._set_chosen_one_player(None if (
|
||||
|
||||
@ -57,7 +57,7 @@ class ConquestFlag(Flag):
|
||||
|
||||
|
||||
# ba_meta export game
|
||||
class ConquestGame(ba.TeamGameActivity):
|
||||
class ConquestGame(ba.TeamGameActivity[ba.Player, ba.Team]):
|
||||
"""A game where teams try to claim all flags on the map."""
|
||||
|
||||
@classmethod
|
||||
@ -254,7 +254,7 @@ class ConquestGame(ba.TeamGameActivity):
|
||||
super().handlemessage(msg)
|
||||
|
||||
# Respawn only if this team has a flag.
|
||||
player = msg.spaz.player
|
||||
player = msg.getspaz(self).player
|
||||
if player.team.gamedata['flags_held'] > 0:
|
||||
self.respawn_player(player)
|
||||
else:
|
||||
|
||||
@ -35,7 +35,7 @@ if TYPE_CHECKING:
|
||||
|
||||
|
||||
# ba_meta export game
|
||||
class DeathMatchGame(ba.TeamGameActivity):
|
||||
class DeathMatchGame(ba.TeamGameActivity[ba.Player, ba.Team]):
|
||||
"""A game type based on acquiring kills."""
|
||||
|
||||
@classmethod
|
||||
@ -145,7 +145,7 @@ class DeathMatchGame(ba.TeamGameActivity):
|
||||
# Augment standard behavior.
|
||||
super().handlemessage(msg)
|
||||
|
||||
player = msg.spaz.player
|
||||
player = msg.getspaz(self).player
|
||||
self.respawn_player(player)
|
||||
|
||||
killer = msg.killerplayer
|
||||
|
||||
@ -39,7 +39,7 @@ if TYPE_CHECKING:
|
||||
|
||||
|
||||
# ba_meta export game
|
||||
class EasterEggHuntGame(ba.TeamGameActivity):
|
||||
class EasterEggHuntGame(ba.TeamGameActivity[ba.Player, ba.Team]):
|
||||
"""A game where score is based on collecting eggs."""
|
||||
|
||||
@classmethod
|
||||
@ -212,7 +212,7 @@ class EasterEggHuntGame(ba.TeamGameActivity):
|
||||
|
||||
# Augment standard behavior.
|
||||
super().handlemessage(msg)
|
||||
player = msg.spaz.getplayer()
|
||||
player = msg.getspaz(self).getplayer()
|
||||
if not player:
|
||||
return
|
||||
self.stats.player_was_killed(player)
|
||||
|
||||
@ -164,7 +164,7 @@ class Icon(ba.Actor):
|
||||
|
||||
|
||||
# ba_meta export game
|
||||
class EliminationGame(ba.TeamGameActivity):
|
||||
class EliminationGame(ba.TeamGameActivity[ba.Player, ba.Team]):
|
||||
"""Game type where last player(s) left alive win."""
|
||||
|
||||
@classmethod
|
||||
@ -330,7 +330,7 @@ class EliminationGame(ba.TeamGameActivity):
|
||||
# Now for each team, cycle through our available players
|
||||
# adding icons.
|
||||
for team in self.teams:
|
||||
if team.get_id() == 0:
|
||||
if team.id == 0:
|
||||
xval = -60
|
||||
x_offs = -78
|
||||
else:
|
||||
@ -362,7 +362,7 @@ class EliminationGame(ba.TeamGameActivity):
|
||||
# Non-solo mode.
|
||||
else:
|
||||
for team in self.teams:
|
||||
if team.get_id() == 0:
|
||||
if team.id == 0:
|
||||
xval = -50
|
||||
x_offs = -85
|
||||
else:
|
||||
@ -395,8 +395,7 @@ class EliminationGame(ba.TeamGameActivity):
|
||||
player_pos = ba.Vec3(living_player_pos)
|
||||
points: List[Tuple[float, ba.Vec3]] = []
|
||||
for team in self.teams:
|
||||
start_pos = ba.Vec3(
|
||||
self.map.get_start_position(team.get_id()))
|
||||
start_pos = ba.Vec3(self.map.get_start_position(team.id))
|
||||
points.append(
|
||||
((start_pos - player_pos).length(), start_pos))
|
||||
# Hmm.. we need to sorting vectors too?
|
||||
@ -492,7 +491,7 @@ class EliminationGame(ba.TeamGameActivity):
|
||||
|
||||
# Augment standard behavior.
|
||||
super().handlemessage(msg)
|
||||
player = msg.spaz.player
|
||||
player = msg.getspaz(self).player
|
||||
|
||||
player.gamedata['lives'] -= 1
|
||||
if player.gamedata['lives'] < 0:
|
||||
|
||||
@ -67,7 +67,7 @@ class FootballFlag(stdflag.Flag):
|
||||
|
||||
|
||||
# ba_meta export game
|
||||
class FootballTeamGame(ba.TeamGameActivity):
|
||||
class FootballTeamGame(ba.TeamGameActivity[ba.Player, ba.Team]):
|
||||
"""Football game for teams mode."""
|
||||
|
||||
@classmethod
|
||||
@ -204,7 +204,7 @@ class FootballTeamGame(ba.TeamGameActivity):
|
||||
if region == self._score_regions[i].node:
|
||||
break
|
||||
for team in self.teams:
|
||||
if team.get_id() == i:
|
||||
if team.id == i:
|
||||
team.gamedata['score'] += 7
|
||||
|
||||
# Tell all players to celebrate.
|
||||
@ -274,7 +274,7 @@ class FootballTeamGame(ba.TeamGameActivity):
|
||||
elif isinstance(msg, playerspaz.PlayerSpazDeathMessage):
|
||||
# Augment standard behavior.
|
||||
super().handlemessage(msg)
|
||||
self.respawn_player(msg.spaz.player)
|
||||
self.respawn_player(msg.getspaz(self).player)
|
||||
|
||||
# Respawn dead flags.
|
||||
elif isinstance(msg, stdflag.FlagDeathMessage):
|
||||
@ -320,7 +320,7 @@ class FootballTeamGame(ba.TeamGameActivity):
|
||||
self._flag = FootballFlag(position=self._flag_spawn_pos)
|
||||
|
||||
|
||||
class FootballCoopGame(ba.CoopGameActivity):
|
||||
class FootballCoopGame(ba.CoopGameActivity[ba.Player, ba.Team]):
|
||||
"""
|
||||
Co-op variant of football
|
||||
"""
|
||||
@ -508,7 +508,11 @@ class FootballCoopGame(ba.CoopGameActivity):
|
||||
|
||||
# Make a bogus team for our bots.
|
||||
bad_team_name = self.get_team_display_string('Bad Guys')
|
||||
self._bot_team = ba.Team(1, bad_team_name, (0.5, 0.4, 0.4))
|
||||
# self._bot_team = ba.Team(1, bad_team_name, (0.5, 0.4, 0.4))
|
||||
self._bot_team = ba.Team()
|
||||
self._bot_team.id = 1
|
||||
self._bot_team.name = bad_team_name
|
||||
self._bot_team.color = (0.5, 0.4, 0.4)
|
||||
|
||||
for team in [self.teams[0], self._bot_team]:
|
||||
team.gamedata['score'] = 0
|
||||
@ -562,7 +566,7 @@ class FootballCoopGame(ba.CoopGameActivity):
|
||||
spaz_type: Type[spazbot.SpazBot],
|
||||
immediate: bool = False) -> None:
|
||||
assert self._bot_team is not None
|
||||
pos = self.map.get_start_position(self._bot_team.get_id())
|
||||
pos = self.map.get_start_position(self._bot_team.id)
|
||||
self._bots.spawn_bot(spaz_type,
|
||||
pos=pos,
|
||||
spawn_time=0.001 if immediate else 3.0,
|
||||
@ -668,7 +672,7 @@ class FootballCoopGame(ba.CoopGameActivity):
|
||||
|
||||
for team in [self.teams[0], self._bot_team]:
|
||||
assert team is not None
|
||||
if team.get_id() == i:
|
||||
if team.id == i:
|
||||
team.gamedata['score'] += 7
|
||||
|
||||
# Tell all players (or bots) to celebrate.
|
||||
@ -808,7 +812,7 @@ class FootballCoopGame(ba.CoopGameActivity):
|
||||
from bastd.actor import respawnicon
|
||||
|
||||
# Respawn dead players.
|
||||
player = msg.spaz.player
|
||||
player = msg.getspaz(self).player
|
||||
self.stats.player_was_killed(player)
|
||||
assert self.initial_player_info is not None
|
||||
respawn_time = 2.0 + len(self.initial_player_info) * 1.0
|
||||
@ -871,7 +875,7 @@ class FootballCoopGame(ba.CoopGameActivity):
|
||||
def spawn_player(self, player: ba.Player) -> ba.Actor:
|
||||
spaz = self.spawn_player_spaz(player,
|
||||
position=self.map.get_start_position(
|
||||
player.team.get_id()))
|
||||
player.team.id))
|
||||
if self._preset in ['rookie_easy', 'pro_easy', 'uber_easy']:
|
||||
spaz.impact_scale = 0.25
|
||||
spaz.add_dropped_bomb_callback(self._handle_player_dropped_bomb)
|
||||
|
||||
@ -100,14 +100,13 @@ class Puck(ba.Actor):
|
||||
if activity:
|
||||
if msg.source_player in activity.players:
|
||||
self.last_players_to_touch[
|
||||
msg.source_player.team.get_id(
|
||||
)] = msg.source_player
|
||||
msg.source_player.team.id] = msg.source_player
|
||||
else:
|
||||
super().handlemessage(msg)
|
||||
|
||||
|
||||
# ba_meta export game
|
||||
class HockeyGame(ba.TeamGameActivity):
|
||||
class HockeyGame(ba.TeamGameActivity[ba.Player, ba.Team]):
|
||||
"""Ice hockey game."""
|
||||
|
||||
@classmethod
|
||||
@ -255,8 +254,10 @@ class HockeyGame(ba.TeamGameActivity):
|
||||
player = playernode.getdelegate().getplayer()
|
||||
except Exception:
|
||||
player = puck = None
|
||||
assert isinstance(player, ba.Player)
|
||||
assert isinstance(puck, Puck)
|
||||
if player and puck:
|
||||
puck.last_players_to_touch[player.team.get_id()] = player
|
||||
puck.last_players_to_touch[player.team.id] = player
|
||||
|
||||
def _kill_puck(self) -> None:
|
||||
self._puck = None
|
||||
@ -279,7 +280,7 @@ class HockeyGame(ba.TeamGameActivity):
|
||||
break
|
||||
|
||||
for team in self.teams:
|
||||
if team.get_id() == index:
|
||||
if team.id == index:
|
||||
scoring_team = team
|
||||
team.gamedata['score'] += 1
|
||||
|
||||
@ -290,13 +291,12 @@ class HockeyGame(ba.TeamGameActivity):
|
||||
|
||||
# If we've got the player from the scoring team that last
|
||||
# touched us, give them points.
|
||||
if (scoring_team.get_id() in self._puck.last_players_to_touch
|
||||
and self._puck.last_players_to_touch[
|
||||
scoring_team.get_id()]):
|
||||
self.stats.player_scored(self._puck.last_players_to_touch[
|
||||
scoring_team.get_id()],
|
||||
100,
|
||||
big_message=True)
|
||||
if (scoring_team.id in self._puck.last_players_to_touch
|
||||
and self._puck.last_players_to_touch[scoring_team.id]):
|
||||
self.stats.player_scored(
|
||||
self._puck.last_players_to_touch[scoring_team.id],
|
||||
100,
|
||||
big_message=True)
|
||||
|
||||
# End game if we won.
|
||||
if team.gamedata['score'] >= self.settings_raw['Score to Win']:
|
||||
@ -341,7 +341,7 @@ class HockeyGame(ba.TeamGameActivity):
|
||||
if isinstance(msg, playerspaz.PlayerSpazDeathMessage):
|
||||
# Augment standard behavior...
|
||||
super().handlemessage(msg)
|
||||
self.respawn_player(msg.spaz.player)
|
||||
self.respawn_player(msg.getspaz(self).player)
|
||||
|
||||
# Respawn dead pucks.
|
||||
elif isinstance(msg, PuckDeathMessage):
|
||||
|
||||
@ -37,7 +37,7 @@ if TYPE_CHECKING:
|
||||
|
||||
|
||||
# ba_meta export game
|
||||
class KeepAwayGame(ba.TeamGameActivity):
|
||||
class KeepAwayGame(ba.TeamGameActivity[ba.Player, ba.Team]):
|
||||
"""Game where you try to keep the flag away from your enemies."""
|
||||
|
||||
FLAG_NEW = 0
|
||||
@ -271,7 +271,7 @@ class KeepAwayGame(ba.TeamGameActivity):
|
||||
if isinstance(msg, playerspaz.PlayerSpazDeathMessage):
|
||||
# Augment standard behavior.
|
||||
super().handlemessage(msg)
|
||||
self.respawn_player(msg.spaz.player)
|
||||
self.respawn_player(msg.getspaz(self).player)
|
||||
elif isinstance(msg, stdflag.FlagDeathMessage):
|
||||
self._spawn_flag()
|
||||
elif isinstance(
|
||||
|
||||
@ -39,7 +39,7 @@ if TYPE_CHECKING:
|
||||
|
||||
|
||||
# ba_meta export game
|
||||
class KingOfTheHillGame(ba.TeamGameActivity):
|
||||
class KingOfTheHillGame(ba.TeamGameActivity[ba.Player, ba.Team]):
|
||||
"""Game where a team wins by holding a 'hill' for a set amount of time."""
|
||||
|
||||
FLAG_NEW = 0
|
||||
@ -281,7 +281,7 @@ class KingOfTheHillGame(ba.TeamGameActivity):
|
||||
super().handlemessage(msg) # Augment default.
|
||||
|
||||
# No longer can count as at_flag once dead.
|
||||
player = msg.spaz.player
|
||||
player = msg.getspaz(self).player
|
||||
player.gamedata['at_flag'] = 0
|
||||
self._update_flag_state()
|
||||
self.respawn_player(player)
|
||||
|
||||
@ -26,26 +26,31 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import ba
|
||||
from bastd.actor.bomb import Bomb
|
||||
from bastd.actor.playerspaz import PlayerSpazDeathMessage
|
||||
from bastd.actor.onscreentimer import OnScreenTimer
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any, Tuple, Sequence, Optional, List, Dict, Type, Type
|
||||
from bastd.actor.onscreentimer import OnScreenTimer
|
||||
|
||||
|
||||
@dataclass
|
||||
class PlayerData(ba.BasePlayerData):
|
||||
"""Data we store per player."""
|
||||
death_time: Optional[float] = None
|
||||
class Player(ba.Player['Team']):
|
||||
"""Our player type for this game."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.death_time: Optional[float] = None
|
||||
|
||||
|
||||
class Team(ba.Team[Player]):
|
||||
"""Our team type for this game."""
|
||||
|
||||
|
||||
# ba_meta export game
|
||||
class MeteorShowerGame(ba.TeamGameActivity):
|
||||
class MeteorShowerGame(ba.TeamGameActivity[Player, Team]):
|
||||
"""Minigame involving dodging falling bombs."""
|
||||
|
||||
@classmethod
|
||||
@ -80,6 +85,9 @@ class MeteorShowerGame(ba.TeamGameActivity):
|
||||
or issubclass(sessiontype, ba.FreeForAllSession)
|
||||
or issubclass(sessiontype, ba.CoopSession))
|
||||
|
||||
# Print messages when players die (since its meaningful in this game).
|
||||
announce_player_deaths = True
|
||||
|
||||
def __init__(self, settings: Dict[str, Any]):
|
||||
super().__init__(settings)
|
||||
|
||||
@ -94,12 +102,7 @@ class MeteorShowerGame(ba.TeamGameActivity):
|
||||
if self._epic_mode:
|
||||
self.slow_motion = True
|
||||
|
||||
# Print messages when players die (since its meaningful in this game).
|
||||
self.announce_player_deaths = True
|
||||
|
||||
def on_begin(self) -> None:
|
||||
from bastd.actor.onscreentimer import OnScreenTimer
|
||||
|
||||
super().on_begin()
|
||||
|
||||
# Drop a wave every few seconds.. and every so often drop the time
|
||||
@ -122,22 +125,23 @@ class MeteorShowerGame(ba.TeamGameActivity):
|
||||
# Check for immediate end (if we've only got 1 player, etc).
|
||||
ba.timer(5.0, self._check_end_game)
|
||||
|
||||
def on_player_join(self, player: ba.Player) -> None:
|
||||
def on_player_join(self, player: Player) -> None:
|
||||
# Don't allow joining after we start
|
||||
# (would enable leave/rejoin tomfoolery).
|
||||
if self.has_begun():
|
||||
ba.screenmessage(ba.Lstr(resource='playerDelayedJoinText',
|
||||
subs=[('${PLAYER}',
|
||||
player.get_name(full=True))]),
|
||||
color=(0, 1, 0))
|
||||
ba.screenmessage(
|
||||
ba.Lstr(resource='playerDelayedJoinText',
|
||||
subs=[('${PLAYER}', player.get_name(full=True))]),
|
||||
color=(0, 1, 0),
|
||||
)
|
||||
# For score purposes, mark them as having died right as the
|
||||
# game started.
|
||||
assert self._timer is not None
|
||||
PlayerData.get(player).death_time = self._timer.getstarttime()
|
||||
player.death_time = self._timer.getstarttime()
|
||||
return
|
||||
self.spawn_player(player)
|
||||
|
||||
def on_player_leave(self, player: ba.Player) -> None:
|
||||
def on_player_leave(self, player: Player) -> None:
|
||||
# Augment default behavior.
|
||||
super().on_player_leave(player)
|
||||
|
||||
@ -145,7 +149,7 @@ class MeteorShowerGame(ba.TeamGameActivity):
|
||||
self._check_end_game()
|
||||
|
||||
# overriding the default character spawning..
|
||||
def spawn_player(self, player: ba.Player) -> ba.Actor:
|
||||
def spawn_player(self, player: Player) -> ba.Actor:
|
||||
spaz = self.spawn_player_spaz(player)
|
||||
|
||||
# Let's reconnect this player's controls to this
|
||||
@ -168,7 +172,8 @@ class MeteorShowerGame(ba.TeamGameActivity):
|
||||
curtime = ba.time()
|
||||
|
||||
# Record the player's moment of death.
|
||||
PlayerData.get(msg.spaz.player).death_time = curtime
|
||||
# assert isinstance(msg.spaz.player
|
||||
msg.getspaz(self).player.death_time = curtime
|
||||
|
||||
# In co-op mode, end the game the instant everyone dies
|
||||
# (more accurate looking).
|
||||
@ -250,19 +255,18 @@ class MeteorShowerGame(ba.TeamGameActivity):
|
||||
# (these per-player scores are only meaningful in team-games)
|
||||
for team in self.teams:
|
||||
for player in team.players:
|
||||
playerdata = PlayerData.get(player)
|
||||
survived = False
|
||||
|
||||
# Throw an extra fudge factor in so teams that
|
||||
# didn't die come out ahead of teams that did.
|
||||
if playerdata.death_time is None:
|
||||
if player.death_time is None:
|
||||
survived = True
|
||||
playerdata.death_time = cur_time + 1
|
||||
player.death_time = cur_time + 1
|
||||
|
||||
# Award a per-player score depending on how many seconds
|
||||
# they lasted (per-player scores only affect teams mode;
|
||||
# everywhere else just looks at the per-team score).
|
||||
score = int(playerdata.death_time - self._timer.getstarttime())
|
||||
score = int(player.death_time - self._timer.getstarttime())
|
||||
if survived:
|
||||
score += 50 # A bit extra for survivors.
|
||||
self.stats.player_scored(player, score, screenmessage=False)
|
||||
@ -284,10 +288,9 @@ class MeteorShowerGame(ba.TeamGameActivity):
|
||||
# that team.
|
||||
longest_life = 0.0
|
||||
for player in team.players:
|
||||
playerdata = PlayerData.get(player)
|
||||
assert playerdata.death_time is not None
|
||||
assert player.death_time is not None
|
||||
longest_life = max(longest_life,
|
||||
playerdata.death_time - start_time)
|
||||
player.death_time - start_time)
|
||||
|
||||
# Submit the score value in milliseconds.
|
||||
results.set_team_score(team, int(1000.0 * longest_life))
|
||||
|
||||
@ -38,7 +38,7 @@ if TYPE_CHECKING:
|
||||
|
||||
|
||||
# ba_meta export game
|
||||
class NinjaFightGame(ba.TeamGameActivity):
|
||||
class NinjaFightGame(ba.TeamGameActivity[ba.Player, ba.Team]):
|
||||
"""
|
||||
A co-op game where you try to defeat a group
|
||||
of Ninjas as fast as possible
|
||||
@ -148,7 +148,7 @@ class NinjaFightGame(ba.TeamGameActivity):
|
||||
# A player has died.
|
||||
if isinstance(msg, playerspaz.PlayerSpazDeathMessage):
|
||||
super().handlemessage(msg) # do standard stuff
|
||||
self.respawn_player(msg.spaz.player) # kick off a respawn
|
||||
self.respawn_player(msg.getspaz(self).player) # kick off a respawn
|
||||
|
||||
# A spaz-bot has died.
|
||||
elif isinstance(msg, spazbot.SpazBotDeathMessage):
|
||||
|
||||
@ -38,7 +38,7 @@ if TYPE_CHECKING:
|
||||
from bastd.actor.scoreboard import Scoreboard
|
||||
|
||||
|
||||
class OnslaughtGame(ba.CoopGameActivity):
|
||||
class OnslaughtGame(ba.CoopGameActivity[ba.Player, ba.Team]):
|
||||
"""Co-op game where players try to survive attacking waves of enemies."""
|
||||
|
||||
tips: List[Union[str, Dict[str, Any]]] = [
|
||||
@ -1163,7 +1163,7 @@ class OnslaughtGame(ba.CoopGameActivity):
|
||||
|
||||
elif isinstance(msg, playerspaz.PlayerSpazDeathMessage):
|
||||
super().handlemessage(msg) # Augment standard behavior.
|
||||
player = msg.spaz.getplayer()
|
||||
player = msg.getspaz(self).getplayer()
|
||||
assert player is not None
|
||||
self._a_player_has_been_hurt = True
|
||||
|
||||
|
||||
@ -67,7 +67,7 @@ class RaceRegion(ba.Actor):
|
||||
|
||||
|
||||
# ba_meta export game
|
||||
class RaceGame(ba.TeamGameActivity):
|
||||
class RaceGame(ba.TeamGameActivity[ba.Player, ba.Team]):
|
||||
"""Game of racing around a track."""
|
||||
|
||||
@classmethod
|
||||
@ -733,7 +733,7 @@ class RaceGame(ba.TeamGameActivity):
|
||||
if isinstance(msg, PlayerSpazDeathMessage):
|
||||
# Augment default behavior.
|
||||
super().handlemessage(msg)
|
||||
player = msg.spaz.getplayer()
|
||||
player = msg.getspaz(self).getplayer()
|
||||
if not player:
|
||||
ba.print_error('got no player in PlayerSpazDeathMessage')
|
||||
return
|
||||
|
||||
@ -38,7 +38,7 @@ if TYPE_CHECKING:
|
||||
from typing import Type, Any, List, Dict, Tuple, Sequence, Optional
|
||||
|
||||
|
||||
class RunaroundGame(ba.CoopGameActivity):
|
||||
class RunaroundGame(ba.CoopGameActivity[ba.Player, ba.Team]):
|
||||
"""Game involving trying to bomb bots as they walk through the map."""
|
||||
|
||||
tips = [
|
||||
@ -48,7 +48,7 @@ class RunaroundGame(ba.CoopGameActivity):
|
||||
]
|
||||
|
||||
# How fast our various bot types walk.
|
||||
_bot_speed_map = {
|
||||
_bot_speed_map: Dict[Type[spazbot.SpazBot], float] = {
|
||||
spazbot.BomberBot: 0.48,
|
||||
spazbot.BomberBotPro: 0.48,
|
||||
spazbot.BomberBotProShielded: 0.48,
|
||||
@ -1127,7 +1127,7 @@ class RunaroundGame(ba.CoopGameActivity):
|
||||
elif isinstance(msg, playerspaz.PlayerSpazDeathMessage):
|
||||
from bastd.actor import respawnicon
|
||||
self._a_player_has_been_killed = True
|
||||
player = msg.spaz.getplayer()
|
||||
player = msg.getspaz(self).getplayer()
|
||||
if player is None:
|
||||
ba.print_error('FIXME: getplayer() should no'
|
||||
' longer ever be returning None')
|
||||
|
||||
@ -38,7 +38,7 @@ if TYPE_CHECKING:
|
||||
|
||||
|
||||
# ba_meta export game
|
||||
class TargetPracticeGame(ba.TeamGameActivity):
|
||||
class TargetPracticeGame(ba.TeamGameActivity[ba.Player, ba.Team]):
|
||||
"""Game where players try to hit targets with bombs."""
|
||||
|
||||
@classmethod
|
||||
@ -187,7 +187,7 @@ class TargetPracticeGame(ba.TeamGameActivity):
|
||||
# When players die, respawn them.
|
||||
if isinstance(msg, playerspaz.PlayerSpazDeathMessage):
|
||||
super().handlemessage(msg) # Do standard stuff.
|
||||
player = msg.spaz.getplayer()
|
||||
player = msg.getspaz(self).getplayer()
|
||||
assert player is not None
|
||||
self.respawn_player(player) # Kick off a respawn.
|
||||
elif isinstance(msg, Target.TargetHitMessage):
|
||||
|
||||
@ -35,7 +35,7 @@ if TYPE_CHECKING:
|
||||
from bastd.actor.scoreboard import Scoreboard
|
||||
|
||||
|
||||
class TheLastStandGame(ba.CoopGameActivity):
|
||||
class TheLastStandGame(ba.CoopGameActivity[ba.Player, ba.Team]):
|
||||
"""Slow motion how-long-can-you-last game."""
|
||||
|
||||
tips = [
|
||||
@ -257,7 +257,7 @@ class TheLastStandGame(ba.CoopGameActivity):
|
||||
|
||||
def handlemessage(self, msg: Any) -> Any:
|
||||
if isinstance(msg, playerspaz.PlayerSpazDeathMessage):
|
||||
player = msg.spaz.getplayer()
|
||||
player = msg.getspaz(self).getplayer()
|
||||
if player is None:
|
||||
ba.print_error('FIXME: getplayer() should no longer '
|
||||
'ever be returning None.')
|
||||
|
||||
@ -27,9 +27,9 @@ import time
|
||||
import weakref
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import _ba
|
||||
import ba
|
||||
from bastd.actor import spaz
|
||||
import _ba
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any, List, Optional
|
||||
@ -43,7 +43,7 @@ if TYPE_CHECKING:
|
||||
# noinspection PyAttributeOutsideInit
|
||||
|
||||
|
||||
class MainMenuActivity(ba.Activity):
|
||||
class MainMenuActivity(ba.Activity[ba.Player, ba.Team]):
|
||||
"""Activity showing the rotating main menu bg stuff."""
|
||||
|
||||
_stdassets = ba.Dependency(ba.AssetPackage, 'stdassets@1')
|
||||
@ -358,7 +358,7 @@ class MainMenuActivity(ba.Activity):
|
||||
# need to make nodes and stuff.. should fix the serverget
|
||||
# call so it.
|
||||
activity = self._activity()
|
||||
if activity is None or activity.is_expired():
|
||||
if activity is None or activity.expired:
|
||||
return
|
||||
with ba.Context(activity):
|
||||
|
||||
@ -830,7 +830,7 @@ class MainMenuActivity(ba.Activity):
|
||||
def _start_preloads(self) -> None:
|
||||
# FIXME: The func that calls us back doesn't save/restore state
|
||||
# or check for a dead activity so we have to do that ourself.
|
||||
if self.is_expired():
|
||||
if self.expired:
|
||||
return
|
||||
with ba.Context(self):
|
||||
_preload1()
|
||||
@ -929,6 +929,6 @@ class MainMenuSession(ba.Session):
|
||||
# Any ending activity leads us into the main menu one.
|
||||
self.set_activity(ba.new_activity(MainMenuActivity))
|
||||
|
||||
def on_player_request(self, player: ba.Player) -> bool:
|
||||
def on_player_request(self, player: ba.SessionPlayer) -> bool:
|
||||
# Reject all player requests.
|
||||
return False
|
||||
|
||||
@ -181,7 +181,7 @@ class ButtonRelease:
|
||||
timeformat=ba.TimeFormat.MILLISECONDS)
|
||||
|
||||
|
||||
class TutorialActivity(ba.Activity):
|
||||
class TutorialActivity(ba.Activity[ba.Player, ba.Team]):
|
||||
|
||||
def __init__(self, settings: Dict[str, Any] = None):
|
||||
from bastd.maps import Rampage
|
||||
|
||||
@ -31,22 +31,19 @@ from pathlib import Path
|
||||
from threading import Lock, Thread, current_thread
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
# We make use of the bacommon and efro packages as well as site-packages
|
||||
# included with our bundled Ballistica dist, so we need to add those paths
|
||||
# before we import them.
|
||||
sys.path += [
|
||||
str(Path(Path(__file__).parent, 'dist', 'ba_data', 'python')),
|
||||
str(Path(Path(__file__).parent, 'dist', 'ba_data', 'python-site-packages'))
|
||||
]
|
||||
|
||||
from bacommon.servermanager import ServerConfig, StartServerModeCommand
|
||||
from efro.dataclasses import dataclass_assign, dataclass_validate
|
||||
from efro.error import CleanError
|
||||
from efro.terminal import Clr
|
||||
|
||||
# We change our working directory according to file's path
|
||||
# so that the script can be properly executed from anywhere
|
||||
os.chdir(os.path.abspath(os.path.dirname(__file__)))
|
||||
|
||||
# We make use of the bacommon and efro packages as well as site-packages
|
||||
# included with our bundled Ballistica dist.
|
||||
sys.path += [
|
||||
str(Path(os.getcwd(), 'dist', 'ba_data', 'python')),
|
||||
str(Path(os.getcwd(), 'dist', 'ba_data', 'python-site-packages'))
|
||||
]
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Optional, List, Dict, Union, Tuple
|
||||
from types import FrameType
|
||||
@ -54,7 +51,7 @@ if TYPE_CHECKING:
|
||||
|
||||
# Not sure how much versioning we'll do with this, but this will get
|
||||
# printed at startup in case we need it.
|
||||
VERSION_STR = '1.0'
|
||||
VERSION_STR = '1.0.1'
|
||||
|
||||
|
||||
class ServerManagerApp:
|
||||
@ -432,6 +429,10 @@ class ServerManagerApp:
|
||||
def main() -> None:
|
||||
"""Run a BallisticaCore server manager in interactive mode."""
|
||||
try:
|
||||
# Change our working directory according to file's path
|
||||
# so that this script can be run from anywhere.
|
||||
os.chdir(os.path.abspath(os.path.dirname(__file__)))
|
||||
|
||||
ServerManagerApp().run_interactive()
|
||||
except CleanError as exc:
|
||||
# For clean errors, do a simple print and fail; no tracebacks/etc.
|
||||
|
||||
@ -3,6 +3,21 @@ mypy_path = __EFRO_PROJECT_ROOT__/tools:__EFRO_PROJECT_ROOT__/assets/src/ba_data
|
||||
|
||||
__EFRO_MYPY_STANDARD_SETTINGS__
|
||||
|
||||
# We have mypy alert us if we use any vars that have been imported
|
||||
# by other modules; we want to import everything directly from its
|
||||
# source. However there are some modules that are explicitly exist
|
||||
# to reexport things, so let's let those pass.
|
||||
# (we could also set __all__ in those modules, but that's a lot of
|
||||
# repeating ourself)
|
||||
[mypy-ba]
|
||||
no_implicit_reexport = False
|
||||
[mypy-efro.entity]
|
||||
no_implicit_reexport = False
|
||||
[mypy-ba.internal]
|
||||
no_implicit_reexport = False
|
||||
[mypy-ba.deprecated]
|
||||
no_implicit_reexport = False
|
||||
|
||||
[mypy-pylint.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
|
||||
@ -79,6 +79,7 @@ good-names=i,
|
||||
v2,
|
||||
ex,
|
||||
Run,
|
||||
id,
|
||||
T,
|
||||
S,
|
||||
U,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -237,7 +237,7 @@ class App:
|
||||
with open(self._state_data_path, 'r') as infile:
|
||||
self._state = StateData(**json.loads(infile.read()))
|
||||
except Exception:
|
||||
print(f'{Clr.SRED}Error loading {TOOL_NAME} data;'
|
||||
print(f'{Clr.RED}Error loading {TOOL_NAME} data;'
|
||||
f' resetting to defaults.{Clr.RST}')
|
||||
|
||||
def _save_state(self) -> None:
|
||||
@ -285,7 +285,7 @@ class App:
|
||||
return response
|
||||
|
||||
def _upload_file(self, filename: str, call: str, args: Dict) -> None:
|
||||
print(f'{Clr.SBLU}Uploading {filename}{Clr.RST}', flush=True)
|
||||
print(f'{Clr.BLU}Uploading {filename}{Clr.RST}', flush=True)
|
||||
with tempfile.TemporaryDirectory() as tempdir:
|
||||
srcpath = Path(filename)
|
||||
gzpath = Path(tempdir, 'file.gz')
|
||||
|
||||
@ -112,7 +112,7 @@ def _lazybuild_check_paths(inpaths: List[str], category: SourceCategory,
|
||||
# Now see this path is newer than our target..
|
||||
if mtime is None or os.path.getmtime(path) >= mtime:
|
||||
print(f'{Clr.SMAG}Build of {tnamepretty} triggered by'
|
||||
f' {path}{Clr.RST}')
|
||||
f" '{path}'{Clr.RST}")
|
||||
return True
|
||||
return False
|
||||
|
||||
@ -483,7 +483,7 @@ def _vstr(nums: Sequence[int]) -> str:
|
||||
def checkenv() -> None:
|
||||
"""Check for tools necessary to build and run the app."""
|
||||
from efrotools import PYTHON_BIN
|
||||
print('Checking environment...', flush=True)
|
||||
print(f'{Clr.BLD}Checking environment...{Clr.RST}', flush=True)
|
||||
|
||||
# Make sure they've got curl.
|
||||
if subprocess.run(['which', 'curl'], check=False,
|
||||
@ -539,7 +539,7 @@ def checkenv() -> None:
|
||||
'Alternately, "tools/snippets install_pip_reqs"'
|
||||
' will update all pip requirements.')
|
||||
|
||||
print('Environment ok.', flush=True)
|
||||
print(f'{Clr.BLD}Environment ok.{Clr.RST}', flush=True)
|
||||
|
||||
|
||||
def get_pip_reqs() -> List[str]:
|
||||
|
||||
@ -26,9 +26,9 @@ import copy
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Generic, TypeVar, overload
|
||||
|
||||
from efro.entity._support import (BaseField, BoundCompoundValue,
|
||||
BoundListField, BoundDictField,
|
||||
BoundCompoundListField,
|
||||
from efro.entity._base import BaseField
|
||||
from efro.entity._support import (BoundCompoundValue, BoundListField,
|
||||
BoundDictField, BoundCompoundListField,
|
||||
BoundCompoundDictField)
|
||||
from efro.entity.util import have_matching_fields
|
||||
|
||||
|
||||
@ -27,7 +27,9 @@ import inspect
|
||||
import logging
|
||||
from collections import abc
|
||||
from enum import Enum
|
||||
from typing import TYPE_CHECKING, TypeVar, Tuple, Optional, Generic
|
||||
from typing import TYPE_CHECKING, TypeVar, Generic
|
||||
# Our Pylint class_generics_filter gives us a false-positive unused-import.
|
||||
from typing import Tuple, Optional # pylint: disable=W0611
|
||||
|
||||
from efro.entity._base import DataHandler, BaseField
|
||||
from efro.entity.util import compound_eq
|
||||
|
||||
@ -24,17 +24,24 @@ from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
import time
|
||||
import weakref
|
||||
from typing import TYPE_CHECKING, cast, TypeVar, Generic
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import asyncio
|
||||
from typing import Any, Dict, Callable, Optional
|
||||
from weakref import ReferenceType
|
||||
from typing import Any, Dict, Callable, Optional, Type
|
||||
|
||||
T = TypeVar('T')
|
||||
TVAL = TypeVar('TVAL')
|
||||
TARG = TypeVar('TARG')
|
||||
TRET = TypeVar('TRET')
|
||||
|
||||
|
||||
class _EmptyObj:
|
||||
pass
|
||||
|
||||
|
||||
def utc_now() -> datetime.datetime:
|
||||
"""Get offset-aware current utc time.
|
||||
|
||||
@ -46,6 +53,15 @@ def utc_now() -> datetime.datetime:
|
||||
return datetime.datetime.now(datetime.timezone.utc)
|
||||
|
||||
|
||||
def empty_weakref(objtype: Type[T]) -> ReferenceType[T]:
|
||||
"""Return an invalidated weak-reference for the specified type."""
|
||||
# At runtime, all weakrefs are the same; our type arg is just
|
||||
# for the static type checker.
|
||||
del objtype # Unused.
|
||||
# Just create an object and let it die. Is there a cleaner way to do this?
|
||||
return weakref.ref(_EmptyObj()) # type: ignore
|
||||
|
||||
|
||||
def data_size_str(bytecount: int) -> str:
|
||||
"""Given a size in bytes, returns a short human readable string.
|
||||
|
||||
|
||||
@ -187,7 +187,7 @@ def _py_symbol_at_column(line: str, col: int) -> str:
|
||||
return line[start:end]
|
||||
|
||||
|
||||
def py_examine(filename: Path, line: int, column: int,
|
||||
def py_examine(projroot: Path, filename: Path, line: int, column: int,
|
||||
selection: Optional[str], operation: str) -> None:
|
||||
"""Given file position info, performs some code inspection."""
|
||||
# pylint: disable=too-many-locals
|
||||
@ -242,7 +242,7 @@ def py_examine(filename: Path, line: int, column: int,
|
||||
with tmppath.open('w') as outfile:
|
||||
outfile.write('\n'.join(flines))
|
||||
try:
|
||||
code.runmypy([str(tmppath)], check=False)
|
||||
code.runmypy(projroot, [str(tmppath)], check=False)
|
||||
except Exception as exc:
|
||||
print('error running mypy:', exc)
|
||||
tmppath.unlink()
|
||||
|
||||
@ -38,8 +38,8 @@ def formatcode(projroot: Path, full: bool) -> None:
|
||||
"""Run clang-format on all of our source code (multithreaded)."""
|
||||
import time
|
||||
import concurrent.futures
|
||||
from efrotools import get_files_hash
|
||||
from multiprocessing import cpu_count
|
||||
from efrotools import get_files_hash
|
||||
os.chdir(projroot)
|
||||
cachepath = Path(projroot, 'config/.cache-formatcode')
|
||||
if full and cachepath.exists():
|
||||
@ -66,13 +66,8 @@ def formatcode(projroot: Path, full: bool) -> None:
|
||||
sys.stdout.flush()
|
||||
return {'f': filename, 't': duration}
|
||||
|
||||
# NOTE: using fewer workers than we have logical procs for now;
|
||||
# we're bottlenecked by one or two long running instances
|
||||
# so it actually helps to lighten the load around them.
|
||||
# may want to revisit later when we have everything chopped up
|
||||
# better
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=cpu_count() //
|
||||
2) as executor:
|
||||
with concurrent.futures.ThreadPoolExecutor(
|
||||
max_workers=cpu_count()) as executor:
|
||||
# Converting this to a list will propagate any errors.
|
||||
list(executor.map(format_file, dirtyfiles))
|
||||
|
||||
@ -88,8 +83,9 @@ def formatcode(projroot: Path, full: bool) -> None:
|
||||
def cpplint(projroot: Path, full: bool) -> None:
|
||||
"""Run lint-checking on all code deemed lint-able."""
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from efrotools import get_config
|
||||
from multiprocessing import cpu_count
|
||||
from efrotools import get_config
|
||||
from efro.terminal import Clr
|
||||
|
||||
os.chdir(projroot)
|
||||
filenames = get_code_filenames(projroot)
|
||||
@ -115,21 +111,24 @@ def cpplint(projroot: Path, full: bool) -> None:
|
||||
dirtyfiles = cache.get_dirty_files()
|
||||
|
||||
if dirtyfiles:
|
||||
print(f'CppLint checking {len(dirtyfiles)} file(s)...')
|
||||
print(f'{Clr.BLU}CppLint checking'
|
||||
f' {len(dirtyfiles)} file(s)...{Clr.RST}')
|
||||
|
||||
def lint_file(filename: str) -> None:
|
||||
result = subprocess.call(['cpplint', '--root=src', filename])
|
||||
if result != 0:
|
||||
raise Exception(f'Linting failed for {filename}')
|
||||
|
||||
with ThreadPoolExecutor(max_workers=cpu_count() // 2) as executor:
|
||||
with ThreadPoolExecutor(max_workers=cpu_count()) as executor:
|
||||
# Converting this to a list will propagate any errors.
|
||||
list(executor.map(lint_file, dirtyfiles))
|
||||
|
||||
if dirtyfiles:
|
||||
cache.mark_clean(filenames)
|
||||
cache.write()
|
||||
print(f'CppLint: all {len(filenames)} files are passing.', flush=True)
|
||||
print(
|
||||
f'{Clr.GRN}CppLint: all {len(filenames)} files are passing.{Clr.RST}',
|
||||
flush=True)
|
||||
|
||||
|
||||
def get_code_filenames(projroot: Path) -> List[str]:
|
||||
@ -153,8 +152,8 @@ def formatscripts(projroot: Path, full: bool) -> None:
|
||||
"""Runs yapf on all our scripts (multithreaded)."""
|
||||
import time
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from efrotools import get_files_hash
|
||||
from multiprocessing import cpu_count
|
||||
from efrotools import get_files_hash
|
||||
os.chdir(projroot)
|
||||
cachepath = Path(projroot, 'config/.cache-formatscripts')
|
||||
if full and cachepath.exists():
|
||||
@ -178,12 +177,7 @@ def formatscripts(projroot: Path, full: bool) -> None:
|
||||
print(f'Formatted {filename} in {duration:.2f} seconds.')
|
||||
sys.stdout.flush()
|
||||
|
||||
# NOTE: using fewer workers than we have logical procs for now;
|
||||
# we're bottlenecked by one or two long running instances
|
||||
# so it actually helps to lighten the load around them.
|
||||
# may want to revisit later when we have everything chopped up
|
||||
# better
|
||||
with ThreadPoolExecutor(max_workers=cpu_count() // 2) as executor:
|
||||
with ThreadPoolExecutor(max_workers=cpu_count()) as executor:
|
||||
# Convert the futures to a list to propagate any errors even
|
||||
# though there are no return values we use.
|
||||
list(executor.map(format_file, dirtyfiles))
|
||||
@ -235,9 +229,27 @@ def get_script_filenames(projroot: Path) -> List[str]:
|
||||
return sorted(list(f for f in filenames if 'flycheck_' not in f))
|
||||
|
||||
|
||||
def runpylint(projroot: Path, filenames: List[str]) -> None:
|
||||
"""Run Pylint explicitly on files."""
|
||||
|
||||
pylintrc = Path(projroot, '.pylintrc')
|
||||
if not os.path.isfile(pylintrc):
|
||||
raise Exception('pylintrc not found where expected')
|
||||
|
||||
# Technically we could just run pylint standalone via command line here,
|
||||
# but let's go ahead and run it inline so we're consistent with our cached
|
||||
# full-project version.
|
||||
_run_pylint(projroot,
|
||||
pylintrc,
|
||||
cache=None,
|
||||
dirtyfiles=filenames,
|
||||
allfiles=None)
|
||||
|
||||
|
||||
def pylint(projroot: Path, full: bool, fast: bool) -> None:
|
||||
"""Run lint-checking on all scripts deemed lint-able."""
|
||||
"""Run Pylint on all scripts in our project (with smart dep tracking)."""
|
||||
from efrotools import get_files_hash
|
||||
from efro.terminal import Clr
|
||||
pylintrc = Path(projroot, '.pylintrc')
|
||||
if not os.path.isfile(pylintrc):
|
||||
raise Exception('pylintrc not found where expected')
|
||||
@ -270,22 +282,17 @@ def pylint(projroot: Path, full: bool, fast: bool) -> None:
|
||||
dirtyfiles.sort(reverse=True, key=lambda f: os.stat(f).st_mtime)
|
||||
|
||||
if dirtyfiles:
|
||||
print(f'Pylint checking {len(dirtyfiles)} file(s)...', flush=True)
|
||||
print(
|
||||
f'{Clr.BLU}Pylint checking {len(dirtyfiles)} file(s)...{Clr.RST}',
|
||||
flush=True)
|
||||
try:
|
||||
_run_script_lint(projroot, pylintrc, cache, dirtyfiles, filenames)
|
||||
except Exception:
|
||||
# Note: even if we fail here, we still want to
|
||||
_run_pylint(projroot, pylintrc, cache, dirtyfiles, filenames)
|
||||
finally:
|
||||
# No matter what happens, we still want to
|
||||
# update our disk cache (since some lints may have passed).
|
||||
print('Pylint failed.', flush=True)
|
||||
|
||||
# Hmm; this can be handy sometimes; perhaps should add an env
|
||||
# var to control it?
|
||||
if bool(False):
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
cache.write()
|
||||
sys.exit(255)
|
||||
print(f'Pylint: all {len(filenames)} files are passing.', flush=True)
|
||||
print(f'{Clr.GRN}Pylint: all {len(filenames)} files are passing.{Clr.RST}',
|
||||
flush=True)
|
||||
|
||||
cache.write()
|
||||
|
||||
@ -349,32 +356,39 @@ def _dirty_dep_check(fname: str, filestates: Dict[str, bool], cache: FileCache,
|
||||
return dirty
|
||||
|
||||
|
||||
def _run_script_lint(projroot: Path, pylintrc: Union[Path, str],
|
||||
cache: FileCache, dirtyfiles: List[str],
|
||||
allfiles: List[str]) -> Dict[str, Any]:
|
||||
def _run_pylint(projroot: Path, pylintrc: Union[Path, str],
|
||||
cache: Optional[FileCache], dirtyfiles: List[str],
|
||||
allfiles: Optional[List[str]]) -> Dict[str, Any]:
|
||||
import time
|
||||
from pylint import lint
|
||||
from efro.error import CleanError
|
||||
from efro.terminal import Clr
|
||||
start_time = time.time()
|
||||
args = ['--rcfile', str(pylintrc)]
|
||||
args = ['--rcfile', str(pylintrc), '--output-format=colorized']
|
||||
|
||||
args += dirtyfiles
|
||||
name = f'{len(dirtyfiles)} file(s)'
|
||||
run = lint.Run(args, do_exit=False)
|
||||
result = _apply_pylint_run_to_cache(projroot, run, dirtyfiles, allfiles,
|
||||
cache)
|
||||
if result != 0:
|
||||
raise Exception(f'Linting failed for {result} file(s).')
|
||||
if cache is not None:
|
||||
assert allfiles is not None
|
||||
result = _apply_pylint_run_to_cache(projroot, run, dirtyfiles,
|
||||
allfiles, cache)
|
||||
if result != 0:
|
||||
raise CleanError(f'Linting failed for {result} file(s).')
|
||||
|
||||
# Sanity check: when the linter fails we should always be failing too.
|
||||
# If not, it means we're probably missing something and incorrectly
|
||||
# marking a failed file as clean.
|
||||
if run.linter.msg_status != 0 and result == 0:
|
||||
raise RuntimeError('Pylint linter returned non-zero result'
|
||||
' but we did not; this is probably a bug.')
|
||||
else:
|
||||
if run.linter.msg_status != 0:
|
||||
raise CleanError('Pylint failed.')
|
||||
|
||||
# Sanity check: when the linter fails we should always be failing too.
|
||||
# If not, it means we're probably missing something and incorrectly
|
||||
# marking a failed file as clean.
|
||||
if run.linter.msg_status != 0 and result == 0:
|
||||
raise Exception('linter returned non-zero result but we did not;'
|
||||
' this is probably a bug.')
|
||||
# result = run.linter.msg_status
|
||||
# we can run
|
||||
duration = time.time() - start_time
|
||||
print(f'Pylint passed for {name} in {duration:.1f} seconds.')
|
||||
print(f'{Clr.GRN}Pylint passed for {name}'
|
||||
f' in {duration:.1f} seconds.{Clr.RST}')
|
||||
sys.stdout.flush()
|
||||
return {'f': dirtyfiles, 't': duration}
|
||||
|
||||
@ -415,7 +429,7 @@ def _apply_pylint_run_to_cache(projroot: Path, run: Any, dirtyfiles: List[str],
|
||||
|
||||
# Update dependencies for what we just ran.
|
||||
# A run leaves us with a map of modules to a list of the modules that
|
||||
# imports them. We want the opposite though: for each of our modules
|
||||
# imports them. We want the opposite though: for each of our modules
|
||||
# we want a list of the modules it imports.
|
||||
reversedeps = {}
|
||||
|
||||
@ -453,10 +467,8 @@ def _apply_pylint_run_to_cache(projroot: Path, run: Any, dirtyfiles: List[str],
|
||||
for fname in dirtyfiles:
|
||||
fmod = paths_to_names[fname]
|
||||
if fmod not in deps:
|
||||
|
||||
# Since this code is a bit flaky, lets always announce when
|
||||
# we come up empty and keep a whitelist of expected values to
|
||||
# ignore.
|
||||
# Since this code is a bit flaky, lets always announce when we
|
||||
# come up empty and keep a whitelist of expected values to ignore.
|
||||
no_deps_modules.add(fmod)
|
||||
depsval: List[str] = []
|
||||
else:
|
||||
@ -512,14 +524,16 @@ def _filter_module_name(mpath: str) -> str:
|
||||
return mpath[:-9] if mpath.endswith('.__init__') else mpath
|
||||
|
||||
|
||||
def runmypy(filenames: List[str],
|
||||
def runmypy(projroot: Path,
|
||||
filenames: List[str],
|
||||
full: bool = False,
|
||||
check: bool = True) -> None:
|
||||
"""Run MyPy on provided filenames."""
|
||||
from efrotools import PYTHON_BIN
|
||||
args = [
|
||||
PYTHON_BIN, '-m', 'mypy', '--pretty', '--no-error-summary',
|
||||
'--config-file', '.mypy.ini'
|
||||
'--config-file',
|
||||
str(Path(projroot, '.mypy.ini'))
|
||||
] + filenames
|
||||
if full:
|
||||
args.insert(args.index('mypy') + 1, '--no-incremental')
|
||||
@ -529,22 +543,26 @@ def runmypy(filenames: List[str],
|
||||
def mypy(projroot: Path, full: bool) -> None:
|
||||
"""Type check all of our scripts using mypy."""
|
||||
import time
|
||||
from efro.terminal import Clr
|
||||
from efro.error import CleanError
|
||||
filenames = get_script_filenames(projroot)
|
||||
print('Running Mypy ' + ('(full)' if full else '(incremental)') + '...',
|
||||
flush=True)
|
||||
desc = '(full)' if full else '(incremental)'
|
||||
print(f'{Clr.BLU}Running Mypy {desc}...{Clr.RST}', flush=True)
|
||||
starttime = time.time()
|
||||
try:
|
||||
runmypy(filenames, full)
|
||||
runmypy(projroot, filenames, full)
|
||||
except Exception:
|
||||
print('Mypy: fail.')
|
||||
sys.exit(255)
|
||||
raise CleanError('Mypy: fail.')
|
||||
duration = time.time() - starttime
|
||||
print(f'Mypy passed in {duration:.1f} seconds.', flush=True)
|
||||
print(f'{Clr.GRN}Mypy passed in {duration:.1f} seconds.{Clr.RST}',
|
||||
flush=True)
|
||||
|
||||
|
||||
def dmypy(projroot: Path) -> None:
|
||||
"""Type check all of our scripts using mypy in daemon mode."""
|
||||
import time
|
||||
from efro.terminal import Clr
|
||||
from efro.error import CleanError
|
||||
filenames = get_script_filenames(projroot)
|
||||
|
||||
# Special case; explicitly kill the daemon.
|
||||
@ -561,10 +579,10 @@ def dmypy(projroot: Path) -> None:
|
||||
] + filenames
|
||||
subprocess.run(args, check=True)
|
||||
except Exception:
|
||||
print('Mypy: fail.')
|
||||
sys.exit(255)
|
||||
raise CleanError('Mypy daemon: fail.')
|
||||
duration = time.time() - starttime
|
||||
print(f'Mypy daemon passed in {duration:.1f} seconds.', flush=True)
|
||||
print(f'{Clr.GRN}Mypy daemon passed in {duration:.1f} seconds.{Clr.RST}',
|
||||
flush=True)
|
||||
|
||||
|
||||
def _parse_idea_results(path: Path) -> int:
|
||||
@ -615,8 +633,13 @@ def _run_idea_inspections(projroot: Path,
|
||||
import tempfile
|
||||
import time
|
||||
import datetime
|
||||
from efro.error import CleanError
|
||||
from efro.terminal import Clr
|
||||
start_time = time.time()
|
||||
print(f'{displayname} checking', len(scripts), 'file(s)...', flush=True)
|
||||
print(
|
||||
f'{Clr.BLU}{displayname} checking'
|
||||
f' {len(scripts)} file(s)...{Clr.RST}',
|
||||
flush=True)
|
||||
tmpdir = tempfile.TemporaryDirectory()
|
||||
iprof = Path(projroot, '.idea/inspectionProfiles/Default.xml')
|
||||
if not iprof.exists():
|
||||
@ -659,13 +682,12 @@ def _run_idea_inspections(projroot: Path,
|
||||
for fname in files:
|
||||
total_errors += _parse_idea_results(Path(tmpdir.name, fname))
|
||||
if total_errors > 0:
|
||||
raise RuntimeError(
|
||||
f'{displayname} inspection found {total_errors} error(s).')
|
||||
raise CleanError(f'{Clr.SRED}{displayname} inspection'
|
||||
f' found {total_errors} error(s).{Clr.RST}')
|
||||
duration = time.time() - start_time
|
||||
|
||||
print(
|
||||
f'{displayname} passed for {len(scripts)} files'
|
||||
f' in {duration:.1f} seconds.',
|
||||
f'{Clr.GRN}{displayname} passed for {len(scripts)} files'
|
||||
f' in {duration:.1f} seconds.{Clr.RST}',
|
||||
flush=True)
|
||||
|
||||
|
||||
@ -680,6 +702,7 @@ def _run_idea_inspections_cached(cachepath: Path,
|
||||
# pylint: disable=too-many-locals
|
||||
import hashlib
|
||||
import json
|
||||
from efro.terminal import Clr
|
||||
md5 = hashlib.md5()
|
||||
|
||||
# Let's calc a single hash from the contents of all script files and only
|
||||
@ -717,8 +740,10 @@ def _run_idea_inspections_cached(cachepath: Path,
|
||||
inspectdir=inspectdir)
|
||||
with open(cachepath, 'w') as outfile:
|
||||
outfile.write(json.dumps({'hash': current_hash}))
|
||||
print(f'{displayname}: all {len(filenames)} files are passing.',
|
||||
flush=True)
|
||||
print(
|
||||
f'{Clr.GRN}{displayname}: all {len(filenames)}'
|
||||
f' files are passing.{Clr.RST}',
|
||||
flush=True)
|
||||
|
||||
|
||||
def pycharm(projroot: Path, full: bool, verbose: bool) -> None:
|
||||
|
||||
@ -28,7 +28,7 @@ import astroid
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from astroid import node_classes as nc
|
||||
from typing import Set, Dict, Any
|
||||
from typing import Set, Dict, Any, List
|
||||
|
||||
VERBOSE = False
|
||||
|
||||
@ -199,9 +199,49 @@ def var_annotations_filter(node: nc.NodeNG) -> nc.NodeNG:
|
||||
return node
|
||||
|
||||
|
||||
# Stripping subscripts on some generics seems to cause
|
||||
# more harm than good, so we leave some intact.
|
||||
ALLOWED_GENERICS = {'Sequence'}
|
||||
|
||||
|
||||
def _is_strippable_subscript(node: nc.NodeNG) -> bool:
|
||||
if isinstance(node, astroid.Subscript):
|
||||
# We can strip if its not in our allowed list.
|
||||
if not (isinstance(node.value, astroid.Name)
|
||||
and node.value.name in ALLOWED_GENERICS):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def class_generics_filter(node: nc.NodeNG) -> nc.NodeNG:
|
||||
"""Filter generics subscripts out of class declarations."""
|
||||
|
||||
# First, quick-out if nothing here should be filtered.
|
||||
found = False
|
||||
for base in node.bases:
|
||||
if _is_strippable_subscript(base):
|
||||
found = True
|
||||
|
||||
if not found:
|
||||
return node
|
||||
|
||||
# Now strip subscripts from base classes.
|
||||
new_bases: List[nc.NodeNG] = []
|
||||
for base in node.bases:
|
||||
if _is_strippable_subscript(base):
|
||||
new_bases.append(base.value)
|
||||
base.value.parent = node
|
||||
else:
|
||||
new_bases.append(base)
|
||||
node.bases = new_bases
|
||||
|
||||
return node
|
||||
|
||||
|
||||
def register_plugins(manager: astroid.Manager) -> None:
|
||||
"""Apply our transforms to a given astroid manager object."""
|
||||
|
||||
# Hmm; is this still necessary?
|
||||
if VERBOSE:
|
||||
manager.register_failed_import_hook(failed_import_hook)
|
||||
|
||||
@ -210,16 +250,30 @@ def register_plugins(manager: astroid.Manager) -> None:
|
||||
# check code as if it doesn't exist at all.
|
||||
manager.register_transform(astroid.If, ignore_type_check_filter)
|
||||
|
||||
# We use 'reveal_type()' quite often, which tells mypy to print
|
||||
# the type of an expression. Let's ignore it in Pylint's eyes so
|
||||
# we don't see an ugly error there.
|
||||
manager.register_transform(astroid.Call, ignore_reveal_type_call)
|
||||
|
||||
# Annotations on variables within a function are defer-eval'ed
|
||||
# in some cases, so lets replace them with simple strings in those
|
||||
# cases to avoid type complaints.
|
||||
# (mypy will still properly alert us to type errors for them)
|
||||
# We make use of 'from __future__ import annotations' which causes Python
|
||||
# to receive annotations as strings, and also 'if TYPE_CHECKING:' blocks,
|
||||
# which lets us do imports and whatnot that are limited to type-checking.
|
||||
# Let's make Pylint understand these.
|
||||
manager.register_transform(astroid.AnnAssign, var_annotations_filter)
|
||||
manager.register_transform(astroid.FunctionDef, func_annotations_filter)
|
||||
manager.register_transform(astroid.AsyncFunctionDef,
|
||||
func_annotations_filter)
|
||||
|
||||
# Pylint doesn't seem to support Generics much right now, and it seems
|
||||
# to lead to some buggy behavior and slowdowns. So let's filter them
|
||||
# out. So instead of this:
|
||||
# class MyClass(MyType[T]):
|
||||
# Pylint will see this:
|
||||
# class MyClass(MyType):
|
||||
# I've opened a github issue related to the problems I was hitting,
|
||||
# so we can revisit the need for this if that gets resolved.
|
||||
# https://github.com/PyCQA/pylint/issues/3605
|
||||
manager.register_transform(astroid.ClassDef, class_generics_filter)
|
||||
|
||||
|
||||
register_plugins(astroid.MANAGER)
|
||||
|
||||
@ -54,7 +54,7 @@ def snippets_main(globs: Dict[str, Any]) -> None:
|
||||
show_help = False
|
||||
retval = 0
|
||||
if len(sys.argv) < 2:
|
||||
print(f'{Clr.SRED}ERROR: command expected.{Clr.RST}')
|
||||
print(f'{Clr.RED}ERROR: command expected.{Clr.RST}')
|
||||
show_help = True
|
||||
retval = 255
|
||||
else:
|
||||
@ -67,13 +67,17 @@ def snippets_main(globs: Dict[str, Any]) -> None:
|
||||
else:
|
||||
docs = _trim_docstring(
|
||||
getattr(funcs[sys.argv[2]], '__doc__', '<no docs>'))
|
||||
print('\nsnippets ' + sys.argv[2] + ':\n' + docs + '\n')
|
||||
print(f'\n{Clr.MAG}{Clr.BLD}snippets {sys.argv[2]}:{Clr.RST}\n'
|
||||
f'{Clr.MAG}{docs}{Clr.RST}\n')
|
||||
elif sys.argv[1] in funcs:
|
||||
try:
|
||||
funcs[sys.argv[1]]()
|
||||
except KeyboardInterrupt as exc:
|
||||
print(f'{Clr.RED}{exc}{Clr.RST}')
|
||||
sys.exit(1)
|
||||
except CleanError as exc:
|
||||
exc.pretty_print()
|
||||
sys.exit(-1)
|
||||
sys.exit(1)
|
||||
else:
|
||||
print('Unknown snippets command: "' + sys.argv[1] + '"',
|
||||
file=sys.stderr)
|
||||
@ -82,11 +86,12 @@ def snippets_main(globs: Dict[str, Any]) -> None:
|
||||
if show_help:
|
||||
print('Snippets contains project related commands too small'
|
||||
' to warrant full scripts.')
|
||||
print("Run 'snippets help <COMMAND>' for full command documentation.")
|
||||
print(f"Run {Clr.MAG}'snippets help {Clr.BLD}<COMMAND>'"
|
||||
f'{Clr.RST} for full command documentation.')
|
||||
print('Available commands:')
|
||||
for func, obj in sorted(funcs.items()):
|
||||
doc = getattr(obj, '__doc__', '').splitlines()[0].strip()
|
||||
print(f'{Clr.SMAG}{func}{Clr.SBLU} - {doc}{Clr.RST}')
|
||||
print(f'{Clr.MAG}{func}{Clr.BLU} - {doc}{Clr.RST}')
|
||||
sys.exit(retval)
|
||||
|
||||
|
||||
@ -194,16 +199,16 @@ def check_clean_safety() -> None:
|
||||
|
||||
def formatcode() -> None:
|
||||
"""Run clang-format on all of our source code (multithreaded)."""
|
||||
from efrotools import code
|
||||
import efrotools.code
|
||||
full = '-full' in sys.argv
|
||||
code.formatcode(PROJROOT, full)
|
||||
efrotools.code.formatcode(PROJROOT, full)
|
||||
|
||||
|
||||
def formatscripts() -> None:
|
||||
"""Run yapf on all our scripts (multithreaded)."""
|
||||
from efrotools import code
|
||||
import efrotools.code
|
||||
full = '-full' in sys.argv
|
||||
code.formatscripts(PROJROOT, full)
|
||||
efrotools.code.formatscripts(PROJROOT, full)
|
||||
|
||||
|
||||
def formatmakefile() -> None:
|
||||
@ -222,9 +227,9 @@ def formatmakefile() -> None:
|
||||
|
||||
def cpplint() -> None:
|
||||
"""Run lint-checking on all code deemed lint-able."""
|
||||
from efrotools import code
|
||||
import efrotools.code
|
||||
full = '-full' in sys.argv
|
||||
code.cpplint(PROJROOT, full)
|
||||
efrotools.code.cpplint(PROJROOT, full)
|
||||
|
||||
|
||||
def scriptfiles() -> None:
|
||||
@ -232,8 +237,8 @@ def scriptfiles() -> None:
|
||||
|
||||
Pass -lines to use newlines as separators. The default is spaces.
|
||||
"""
|
||||
from efrotools import code
|
||||
paths = code.get_script_filenames(projroot=PROJROOT)
|
||||
import efrotools.code
|
||||
paths = efrotools.code.get_script_filenames(projroot=PROJROOT)
|
||||
assert not any(' ' in path for path in paths)
|
||||
if '-lines' in sys.argv:
|
||||
print('\n'.join(paths))
|
||||
@ -243,101 +248,102 @@ def scriptfiles() -> None:
|
||||
|
||||
def pylint() -> None:
|
||||
"""Run pylint checks on our scripts."""
|
||||
from efrotools import code
|
||||
from efro.error import CleanError
|
||||
import efrotools.code
|
||||
full = ('-full' in sys.argv)
|
||||
fast = ('-fast' in sys.argv)
|
||||
code.pylint(PROJROOT, full, fast)
|
||||
try:
|
||||
efrotools.code.pylint(PROJROOT, full, fast)
|
||||
except Exception:
|
||||
raise CleanError('Pylint failed.')
|
||||
|
||||
|
||||
def runpylint() -> None:
|
||||
"""Run pylint checks on provided filenames."""
|
||||
import os
|
||||
from efro.terminal import Clr
|
||||
from efro.error import CleanError
|
||||
import efrotools.code
|
||||
if len(sys.argv) < 3:
|
||||
raise CleanError('Expected at least 1 filename arg.')
|
||||
filenames = sys.argv[2:]
|
||||
try:
|
||||
efrotools.code.runpylint(PROJROOT, filenames)
|
||||
print(f'{Clr.GRN}Pylint Passed.{Clr.RST}')
|
||||
except Exception:
|
||||
if os.environ.get('VERBOSE') == '1':
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
raise CleanError('Pylint Failed.')
|
||||
|
||||
|
||||
def mypy() -> None:
|
||||
"""Run mypy checks on our scripts."""
|
||||
from efrotools import code
|
||||
import efrotools.code
|
||||
full = ('-full' in sys.argv)
|
||||
code.mypy(PROJROOT, full)
|
||||
efrotools.code.mypy(PROJROOT, full)
|
||||
|
||||
|
||||
def runmypy() -> None:
|
||||
"""Run mypy checks on provided filenames."""
|
||||
from efro.terminal import Clr
|
||||
from efro.error import CleanError
|
||||
import efrotools.code
|
||||
if len(sys.argv) < 3:
|
||||
raise CleanError('Expected at least 1 filename arg.')
|
||||
filenames = sys.argv[2:]
|
||||
try:
|
||||
efrotools.code.runmypy(PROJROOT, filenames)
|
||||
print(f'{Clr.GRN}Mypy Passed.{Clr.RST}')
|
||||
except Exception:
|
||||
raise CleanError('Mypy Failed.')
|
||||
|
||||
|
||||
def dmypy() -> None:
|
||||
"""Run mypy checks on our scripts using the mypy daemon."""
|
||||
from efrotools import code
|
||||
code.dmypy(PROJROOT)
|
||||
import efrotools.code
|
||||
efrotools.code.dmypy(PROJROOT)
|
||||
|
||||
|
||||
def pycharm() -> None:
|
||||
"""Run PyCharm checks on our scripts."""
|
||||
from efrotools import code
|
||||
import efrotools.code
|
||||
full = '-full' in sys.argv
|
||||
verbose = '-v' in sys.argv
|
||||
code.pycharm(PROJROOT, full, verbose)
|
||||
efrotools.code.pycharm(PROJROOT, full, verbose)
|
||||
|
||||
|
||||
def clioncode() -> None:
|
||||
"""Run CLion checks on our code."""
|
||||
from efrotools import code
|
||||
import efrotools.code
|
||||
full = '-full' in sys.argv
|
||||
verbose = '-v' in sys.argv
|
||||
code.clioncode(PROJROOT, full, verbose)
|
||||
efrotools.code.clioncode(PROJROOT, full, verbose)
|
||||
|
||||
|
||||
def androidstudiocode() -> None:
|
||||
"""Run Android Studio checks on our code."""
|
||||
from efrotools import code
|
||||
import efrotools.code
|
||||
full = '-full' in sys.argv
|
||||
verbose = '-v' in sys.argv
|
||||
code.androidstudiocode(PROJROOT, full, verbose)
|
||||
efrotools.code.androidstudiocode(PROJROOT, full, verbose)
|
||||
|
||||
|
||||
def tool_config_install() -> None:
|
||||
"""Install a tool config file (with some filtering)."""
|
||||
from efrotools import get_config
|
||||
import textwrap
|
||||
from efro.terminal import Clr
|
||||
if len(sys.argv) != 4:
|
||||
raise Exception('expected 2 args')
|
||||
src = Path(sys.argv[2])
|
||||
dst = Path(sys.argv[3])
|
||||
|
||||
print(f'Creating tool config: {Clr.BLD}{dst}{Clr.RST}')
|
||||
|
||||
with src.open() as infile:
|
||||
cfg = infile.read()
|
||||
|
||||
# Do a bit of filtering.
|
||||
|
||||
# Stick project-root wherever they want.
|
||||
cfg = cfg.replace('__EFRO_PROJECT_ROOT__', str(PROJROOT))
|
||||
|
||||
# Short project name.
|
||||
short_names = {'ballistica-internal': 'ba-int', 'ballistica': 'ba'}
|
||||
shortname = short_names.get(PROJROOT.name, PROJROOT.name)
|
||||
cfg = cfg.replace('__EFRO_PROJECT_SHORTNAME__', shortname)
|
||||
|
||||
stdsettings = textwrap.dedent("""
|
||||
# We don't want all of our plain scripts complaining
|
||||
# about __main__ being redefined.
|
||||
scripts_are_modules = True
|
||||
|
||||
# Try to be as strict as we can about using types everywhere.
|
||||
warn_unused_ignores = True
|
||||
warn_return_any = True
|
||||
warn_redundant_casts = True
|
||||
warn_unreachable=True
|
||||
disallow_incomplete_defs = True
|
||||
disallow_untyped_defs = True
|
||||
disallow_untyped_decorators = True
|
||||
disallow_untyped_calls = True
|
||||
disallow_any_unimported = True
|
||||
strict_equality = True
|
||||
""").strip()
|
||||
|
||||
cfg = cfg.replace('__EFRO_MYPY_STANDARD_SETTINGS__', stdsettings)
|
||||
|
||||
# Gen a pylint init to set up our python paths:
|
||||
pylint_init_tag = '__EFRO_PYLINT_INIT__'
|
||||
if pylint_init_tag in cfg:
|
||||
pypaths = get_config(PROJROOT).get('python_paths')
|
||||
if pypaths is None:
|
||||
raise RuntimeError('python_paths not set in project config')
|
||||
cstr = "init-hook='import sys;"
|
||||
for path in pypaths:
|
||||
cstr += f" sys.path.append('{PROJROOT}/{path}');"
|
||||
cstr += "'"
|
||||
cfg = cfg.replace(pylint_init_tag, cstr)
|
||||
# Rome substitutions, etc.
|
||||
cfg = _filter_tool_config(cfg)
|
||||
|
||||
# Add an auto-generated notice.
|
||||
comment = None
|
||||
@ -356,6 +362,57 @@ def tool_config_install() -> None:
|
||||
outfile.write(cfg)
|
||||
|
||||
|
||||
def _filter_tool_config(cfg: str) -> str:
|
||||
import textwrap
|
||||
from efrotools import get_config
|
||||
|
||||
# Stick project-root wherever they want.
|
||||
cfg = cfg.replace('__EFRO_PROJECT_ROOT__', str(PROJROOT))
|
||||
|
||||
# Short project name.
|
||||
short_names = {'ballistica-internal': 'ba-int', 'ballistica': 'ba'}
|
||||
shortname = short_names.get(PROJROOT.name, PROJROOT.name)
|
||||
cfg = cfg.replace('__EFRO_PROJECT_SHORTNAME__', shortname)
|
||||
|
||||
mypy_standard_settings = textwrap.dedent("""
|
||||
# We don't want all of our plain scripts complaining
|
||||
# about __main__ being redefined.
|
||||
scripts_are_modules = True
|
||||
|
||||
# Try to be as strict as we can about using types everywhere.
|
||||
warn_unused_ignores = True
|
||||
warn_return_any = True
|
||||
warn_redundant_casts = True
|
||||
warn_unreachable = True
|
||||
warn_unused_configs = True
|
||||
disallow_incomplete_defs = True
|
||||
disallow_untyped_defs = True
|
||||
disallow_untyped_decorators = True
|
||||
disallow_untyped_calls = True
|
||||
disallow_any_unimported = True
|
||||
disallow_subclassing_any = True
|
||||
strict_equality = True
|
||||
local_partial_types = True
|
||||
no_implicit_reexport = True
|
||||
""").strip()
|
||||
|
||||
cfg = cfg.replace('__EFRO_MYPY_STANDARD_SETTINGS__',
|
||||
mypy_standard_settings)
|
||||
|
||||
# Gen a pylint init to set up our python paths:
|
||||
pylint_init_tag = '__EFRO_PYLINT_INIT__'
|
||||
if pylint_init_tag in cfg:
|
||||
pypaths = get_config(PROJROOT).get('python_paths')
|
||||
if pypaths is None:
|
||||
raise RuntimeError('python_paths not set in project config')
|
||||
cstr = "init-hook='import sys;"
|
||||
for path in pypaths:
|
||||
cstr += f" sys.path.append('{PROJROOT}/{path}');"
|
||||
cstr += "'"
|
||||
cfg = cfg.replace(pylint_init_tag, cstr)
|
||||
return cfg
|
||||
|
||||
|
||||
def sync_all() -> None:
|
||||
"""Runs full syncs between all efrotools projects.
|
||||
|
||||
@ -368,7 +425,7 @@ def sync_all() -> None:
|
||||
import concurrent.futures
|
||||
from efro.error import CleanError
|
||||
from efro.terminal import Clr
|
||||
print(f'{Clr.SBLU}Updating formatting for all projects...{Clr.RST}')
|
||||
print(f'{Clr.BLU}Updating formatting for all projects...{Clr.RST}')
|
||||
projects_str = os.environ.get('EFROTOOLS_SYNC_PROJECTS')
|
||||
if projects_str is None:
|
||||
raise CleanError('EFROTOOL_SYNC_PROJECTS is not defined.')
|
||||
@ -399,17 +456,17 @@ def sync_all() -> None:
|
||||
# Real mode
|
||||
for i in range(2):
|
||||
if i == 0:
|
||||
print(Clr.SBLU + 'Running sync pass 1:'
|
||||
print(Clr.BLU + 'Running sync pass 1:'
|
||||
' (ensures all changes at dsts are pushed to src)' +
|
||||
Clr.RST)
|
||||
else:
|
||||
print(Clr.SBLU + 'Running sync pass 2:'
|
||||
print(Clr.BLU + 'Running sync pass 2:'
|
||||
' (ensures latest src is pulled to all dsts)' + Clr.RST)
|
||||
for project in projects_str.split(':'):
|
||||
cmd = f'cd "{project}" && make sync-full'
|
||||
print(cmd)
|
||||
subprocess.run(cmd, shell=True, check=True)
|
||||
print(Clr.SBLU + 'Sync-all successful!' + Clr.RST)
|
||||
print(Clr.BLU + 'Sync-all successful!' + Clr.RST)
|
||||
|
||||
|
||||
def sync() -> None:
|
||||
@ -530,5 +587,26 @@ def makefile_target_list() -> None:
|
||||
continue
|
||||
print('\n' + entry.title + '\n' + '-' * len(entry.title))
|
||||
elif entry.kind == 'target':
|
||||
print(Clr.SMAG + entry.title + Clr.SBLU +
|
||||
print(Clr.MAG + entry.title + Clr.BLU +
|
||||
_docstr(lines, entry.line) + Clr.RST)
|
||||
|
||||
|
||||
def echo() -> None:
|
||||
"""Echo with support for efro.terminal.Clr args (RED, GRN, BLU, etc).
|
||||
|
||||
Prints a Clr.RST at the end so that can be omitted.
|
||||
"""
|
||||
from efro.terminal import Clr
|
||||
clrnames = {n for n in dir(Clr) if n.isupper() and not n.startswith('_')}
|
||||
first = True
|
||||
out: List[str] = []
|
||||
for arg in sys.argv[2:]:
|
||||
if arg in clrnames:
|
||||
out.append(getattr(Clr, arg))
|
||||
else:
|
||||
if not first:
|
||||
out.append(' ')
|
||||
first = False
|
||||
out.append(arg)
|
||||
out.append(Clr.RST)
|
||||
print(''.join(out))
|
||||
|
||||
@ -40,9 +40,10 @@ from typing import TYPE_CHECKING
|
||||
# noinspection PyUnresolvedReferences
|
||||
from efrotools.snippets import (
|
||||
PROJROOT, snippets_main, formatcode, formatscripts, formatmakefile,
|
||||
cpplint, pylint, mypy, dmypy, tool_config_install, sync, sync_all,
|
||||
scriptfiles, pycharm, clioncode, androidstudiocode, makefile_target_list,
|
||||
spelling, spelling_all, compile_python_files, pytest)
|
||||
cpplint, pylint, runpylint, mypy, runmypy, dmypy, tool_config_install,
|
||||
sync, sync_all, scriptfiles, pycharm, clioncode, androidstudiocode,
|
||||
makefile_target_list, spelling, spelling_all, compile_python_files, pytest,
|
||||
echo)
|
||||
# pylint: enable=unused-import
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@ -259,7 +260,7 @@ def clean_orphaned_assets() -> None:
|
||||
# Operate from dist root..
|
||||
os.chdir(PROJROOT)
|
||||
|
||||
# Our manifest is split into 2 files (public and non-public)
|
||||
# Our manifest is split into 2 files (public and private)
|
||||
with open('assets/.asset_manifest_public.json') as infile:
|
||||
manifest = set(json.loads(infile.read()))
|
||||
with open('assets/.asset_manifest_private.json') as infile:
|
||||
@ -303,7 +304,8 @@ def py_examine() -> None:
|
||||
sys.path.append(scriptsdir)
|
||||
if toolsdir not in sys.path:
|
||||
sys.path.append(toolsdir)
|
||||
efrotools.py_examine(filename, line, column, selection, operation)
|
||||
efrotools.py_examine(PROJROOT, filename, line, column, selection,
|
||||
operation)
|
||||
|
||||
|
||||
def push_ipa() -> None:
|
||||
@ -553,6 +555,9 @@ def lazy_increment_build() -> None:
|
||||
"""Increment build number only if C++ sources have changed.
|
||||
|
||||
This is convenient to place in automatic commit/push scripts.
|
||||
It could make sense to auto update build number when scripts/assets
|
||||
change too, but a build number change requires rebuilding all binaries
|
||||
so I'll leave that as an explicit choice to save work.
|
||||
"""
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
@ -133,9 +133,9 @@ class App:
|
||||
self._update_docs_md()
|
||||
|
||||
if self._check:
|
||||
print('Check-Builds: Everything up to date.')
|
||||
print(f'{Clr.BLU}Check-Builds: Everything up to date.{Clr.RST}')
|
||||
else:
|
||||
print('Update-Builds: SUCCESS!')
|
||||
print(f'{Clr.GRN}Update-Project: SUCCESS!{Clr.RST}')
|
||||
|
||||
def _update_dummy_module(self) -> None:
|
||||
# Update our dummy _ba module.
|
||||
@ -144,8 +144,8 @@ class App:
|
||||
# been updated.
|
||||
if os.path.exists('tools/gendummymodule.py'):
|
||||
if os.system('tools/gendummymodule.py' + self._checkarg) != 0:
|
||||
print(Clr.SRED + 'Error checking/updating dummy module' +
|
||||
Clr.RST)
|
||||
print(
|
||||
f'{Clr.RED}Error checking/updating dummy module{Clr.RST}')
|
||||
sys.exit(255)
|
||||
|
||||
def _update_docs_md(self) -> None:
|
||||
@ -157,8 +157,8 @@ class App:
|
||||
if os.path.exists('tools/gendocs.py'):
|
||||
if os.system('tools/snippets update_docs_md' +
|
||||
self._checkarg) != 0:
|
||||
print(Clr.SRED + 'Error checking/updating docs markdown.' +
|
||||
Clr.RST)
|
||||
print(f'{Clr.RED}Error checking/updating'
|
||||
f' docs markdown.{Clr.RST}')
|
||||
sys.exit(255)
|
||||
|
||||
def _update_compile_commands_file(self) -> None:
|
||||
@ -166,7 +166,7 @@ class App:
|
||||
# our cmake stuff. Do this at end so cmake changes already happened.
|
||||
if not self._check and os.path.exists('ballisticacore-cmake'):
|
||||
if os.system('make .irony/compile_commands.json') != 0:
|
||||
print(Clr.SRED + 'Error updating compile-commands.' + Clr.RST)
|
||||
print(f'{Clr.RED}Error updating compile-commands.{Clr.RST}')
|
||||
sys.exit(255)
|
||||
|
||||
def _apply_file_changes(self) -> None:
|
||||
@ -184,11 +184,11 @@ class App:
|
||||
unchanged_project_count += 1
|
||||
else:
|
||||
if self._check:
|
||||
print(f'{Clr.SRED}ERROR: found out-of-date'
|
||||
print(f'{Clr.RED}ERROR: found out-of-date'
|
||||
f' project file: {fname}{Clr.RST}')
|
||||
sys.exit(255)
|
||||
|
||||
print(f'{Clr.SBLU}Writing project file: {fname}{Clr.RST}')
|
||||
print(f'{Clr.BLU}Writing project file: {fname}{Clr.RST}')
|
||||
with open(fname, 'w') as outfile:
|
||||
outfile.write(fcode)
|
||||
if unchanged_project_count > 0:
|
||||
@ -211,16 +211,16 @@ class App:
|
||||
# If there are any manual-only entries, list then and bail.
|
||||
# (Don't wanna allow auto-apply unless it fixes everything)
|
||||
if manual_changes:
|
||||
print(f'{Clr.SRED}Found erroneous lines '
|
||||
print(f'{Clr.RED}Found erroneous lines '
|
||||
f'requiring manual correction:{Clr.RST}')
|
||||
for change in manual_changes:
|
||||
print(
|
||||
f'{Clr.SRED}{change[0]}:{change[1].line_number + 1}:'
|
||||
f'{Clr.RED}{change[0]}:{change[1].line_number + 1}:'
|
||||
f' Expected line to be:\n {change[1].expected}{Clr.RST}')
|
||||
|
||||
# Make a note on copyright lines that this can be disabled.
|
||||
if 'Copyright' in change[1].expected:
|
||||
print(f'{Clr.SRED}NOTE: You can disable copyright'
|
||||
print(f'{Clr.RED}NOTE: You can disable copyright'
|
||||
f' checks by adding "copyright_checks": false\n'
|
||||
f'to the root dict in config/localconfig.json.\n'
|
||||
f'see https://github.com/efroemling/ballistica/wiki'
|
||||
@ -232,22 +232,21 @@ class App:
|
||||
if auto_changes:
|
||||
if not self._fix:
|
||||
for i, change in enumerate(auto_changes):
|
||||
print(f'{Clr.SRED}#{i}: {change[0]}:{Clr.RST}')
|
||||
print(f'{Clr.RED}#{i}: {change[0]}:{Clr.RST}')
|
||||
print(
|
||||
f'{Clr.SRED} Expected "{change[1].expected}"{Clr.RST}'
|
||||
)
|
||||
f'{Clr.RED} Expected "{change[1].expected}"{Clr.RST}')
|
||||
with open(change[0]) as infile:
|
||||
lines = infile.read().splitlines()
|
||||
line = lines[change[1].line_number]
|
||||
print(f'{Clr.SRED} Found "{line}"{Clr.RST}')
|
||||
print(Clr.SRED +
|
||||
print(f'{Clr.RED} Found "{line}"{Clr.RST}')
|
||||
print(Clr.RED +
|
||||
f'All {len(auto_changes)} errors are auto-fixable;'
|
||||
' run tools/update_project --fix to apply corrections.' +
|
||||
Clr.RST)
|
||||
sys.exit(255)
|
||||
else:
|
||||
for i, change in enumerate(auto_changes):
|
||||
print(f'{Clr.SBLU}Correcting file: {change[0]}{Clr.RST}')
|
||||
print(f'{Clr.BLU}Correcting file: {change[0]}{Clr.RST}')
|
||||
with open(change[0]) as infile:
|
||||
lines = infile.read().splitlines()
|
||||
lines[change[1].line_number] = change[1].expected
|
||||
@ -269,7 +268,7 @@ class App:
|
||||
# Could just ignore these but it probably means I intended
|
||||
# to save something and forgot.
|
||||
if '/.#' in fsrc:
|
||||
print(f'{Clr.SRED}'
|
||||
print(f'{Clr.RED}'
|
||||
f'ERROR: Found an unsaved emacs file: "{fsrc}"'
|
||||
f'{Clr.RST}')
|
||||
sys.exit(255)
|
||||
@ -393,7 +392,7 @@ class App:
|
||||
'tools/devtool', 'tools/version_utils', 'tools/vmshell'
|
||||
]:
|
||||
if not contents.startswith('#!/usr/bin/env python3.7'):
|
||||
print(f'{Clr.SRED}Incorrect shebang (first line) for '
|
||||
print(f'{Clr.RED}Incorrect shebang (first line) for '
|
||||
f'{fname}.{Clr.RST}')
|
||||
sys.exit(255)
|
||||
else:
|
||||
@ -482,7 +481,7 @@ class App:
|
||||
if ('__pycache__' not in root
|
||||
and os.path.basename(root) != '.vscode'):
|
||||
if '__init__.py' not in files:
|
||||
print(Clr.SRED +
|
||||
print(Clr.RED +
|
||||
'Error: no __init__.py in package dir: ' + root +
|
||||
Clr.RST)
|
||||
sys.exit(255)
|
||||
@ -630,14 +629,14 @@ class App:
|
||||
# Make sure none of our sync targets have been mucked with since
|
||||
# their last sync.
|
||||
if os.system('tools/snippets sync check') != 0:
|
||||
print(Clr.SRED + 'Sync check failed; you may need to run "sync".' +
|
||||
print(Clr.RED + 'Sync check failed; you may need to run "sync".' +
|
||||
Clr.RST)
|
||||
sys.exit(255)
|
||||
|
||||
def _update_assets_makefile(self) -> None:
|
||||
if os.path.exists('tools/update_assets_makefile'):
|
||||
if os.system('tools/update_assets_makefile' + self._checkarg) != 0:
|
||||
print(Clr.SRED + 'Error checking/updating assets Makefile' +
|
||||
print(Clr.RED + 'Error checking/updating assets Makefile' +
|
||||
Clr.RST)
|
||||
sys.exit(255)
|
||||
|
||||
@ -645,7 +644,7 @@ class App:
|
||||
if os.path.exists('tools/update_generated_code_makefile'):
|
||||
if os.system('tools/update_generated_code_makefile' +
|
||||
self._checkarg) != 0:
|
||||
print(Clr.SRED +
|
||||
print(Clr.RED +
|
||||
'Error checking/updating generated-code Makefile' +
|
||||
Clr.RST)
|
||||
sys.exit(255)
|
||||
@ -654,7 +653,7 @@ class App:
|
||||
if os.path.exists('tools/update_resources_makefile'):
|
||||
if os.system('tools/update_resources_makefile' +
|
||||
self._checkarg) != 0:
|
||||
print(Clr.SRED + 'Error checking/updating resources Makefile' +
|
||||
print(Clr.RED + 'Error checking/updating resources Makefile' +
|
||||
Clr.RST)
|
||||
sys.exit(255)
|
||||
|
||||
@ -662,8 +661,8 @@ class App:
|
||||
if os.path.exists('tools/update_python_enums_module'):
|
||||
if os.system('tools/update_python_enums_module' +
|
||||
self._checkarg) != 0:
|
||||
print(Clr.SRED +
|
||||
'Error checking/updating python enums module' + Clr.RST)
|
||||
print(Clr.RED + 'Error checking/updating python enums module' +
|
||||
Clr.RST)
|
||||
sys.exit(255)
|
||||
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user