Restored NodeActor, tidying, minor gameplay fixes.

This commit is contained in:
Eric Froemling 2020-04-05 22:01:04 -07:00
parent 3e7ed8c0e3
commit bc600f602e
31 changed files with 379 additions and 274 deletions

View File

@ -479,6 +479,7 @@
<w>dxml</w> <w>dxml</w>
<w>dynload</w> <w>dynload</w>
<w>eachother</w> <w>eachother</w>
<w>eaddrnotavail</w>
<w>easteregghunt</w> <w>easteregghunt</w>
<w>edcc</w> <w>edcc</w>
<w>editcontroller</w> <w>editcontroller</w>
@ -1132,6 +1133,7 @@
<w>nline</w> <w>nline</w>
<w>nlines</w> <w>nlines</w>
<w>nntplib</w> <w>nntplib</w>
<w>nodeactor</w>
<w>nodepos</w> <w>nodepos</w>
<w>nodpi</w> <w>nodpi</w>
<w>nofiles</w> <w>nofiles</w>

View File

@ -35,6 +35,7 @@
"ba_data/python/ba/__pycache__/_modutils.cpython-37.opt-1.pyc", "ba_data/python/ba/__pycache__/_modutils.cpython-37.opt-1.pyc",
"ba_data/python/ba/__pycache__/_music.cpython-37.opt-1.pyc", "ba_data/python/ba/__pycache__/_music.cpython-37.opt-1.pyc",
"ba_data/python/ba/__pycache__/_netutils.cpython-37.opt-1.pyc", "ba_data/python/ba/__pycache__/_netutils.cpython-37.opt-1.pyc",
"ba_data/python/ba/__pycache__/_nodeactor.cpython-37.opt-1.pyc",
"ba_data/python/ba/__pycache__/_playlist.cpython-37.opt-1.pyc", "ba_data/python/ba/__pycache__/_playlist.cpython-37.opt-1.pyc",
"ba_data/python/ba/__pycache__/_powerup.cpython-37.opt-1.pyc", "ba_data/python/ba/__pycache__/_powerup.cpython-37.opt-1.pyc",
"ba_data/python/ba/__pycache__/_profile.cpython-37.opt-1.pyc", "ba_data/python/ba/__pycache__/_profile.cpython-37.opt-1.pyc",
@ -84,6 +85,7 @@
"ba_data/python/ba/_modutils.py", "ba_data/python/ba/_modutils.py",
"ba_data/python/ba/_music.py", "ba_data/python/ba/_music.py",
"ba_data/python/ba/_netutils.py", "ba_data/python/ba/_netutils.py",
"ba_data/python/ba/_nodeactor.py",
"ba_data/python/ba/_playlist.py", "ba_data/python/ba/_playlist.py",
"ba_data/python/ba/_powerup.py", "ba_data/python/ba/_powerup.py",
"ba_data/python/ba/_profile.py", "ba_data/python/ba/_profile.py",

View File

@ -207,6 +207,7 @@ SCRIPT_TARGETS_PY_1 = \
build/ba_data/python/ba/_account.py \ build/ba_data/python/ba/_account.py \
build/ba_data/python/ba/_music.py \ build/ba_data/python/ba/_music.py \
build/ba_data/python/ba/_lang.py \ build/ba_data/python/ba/_lang.py \
build/ba_data/python/ba/_nodeactor.py \
build/ba_data/python/ba/_teamgame.py \ build/ba_data/python/ba/_teamgame.py \
build/ba_data/python/ba/ui/__init__.py \ build/ba_data/python/ba/ui/__init__.py \
build/ba_data/python/bastd/mainmenu.py \ build/ba_data/python/bastd/mainmenu.py \
@ -444,6 +445,7 @@ SCRIPT_TARGETS_PYC_1 = \
build/ba_data/python/ba/__pycache__/_account.cpython-37.opt-1.pyc \ build/ba_data/python/ba/__pycache__/_account.cpython-37.opt-1.pyc \
build/ba_data/python/ba/__pycache__/_music.cpython-37.opt-1.pyc \ build/ba_data/python/ba/__pycache__/_music.cpython-37.opt-1.pyc \
build/ba_data/python/ba/__pycache__/_lang.cpython-37.opt-1.pyc \ build/ba_data/python/ba/__pycache__/_lang.cpython-37.opt-1.pyc \
build/ba_data/python/ba/__pycache__/_nodeactor.cpython-37.opt-1.pyc \
build/ba_data/python/ba/__pycache__/_teamgame.cpython-37.opt-1.pyc \ build/ba_data/python/ba/__pycache__/_teamgame.cpython-37.opt-1.pyc \
build/ba_data/python/ba/ui/__pycache__/__init__.cpython-37.opt-1.pyc \ build/ba_data/python/ba/ui/__pycache__/__init__.cpython-37.opt-1.pyc \
build/ba_data/python/bastd/__pycache__/mainmenu.cpython-37.opt-1.pyc \ build/ba_data/python/bastd/__pycache__/mainmenu.cpython-37.opt-1.pyc \
@ -953,6 +955,11 @@ build/ba_data/python/ba/__pycache__/_lang.cpython-37.opt-1.pyc: \
@echo Compiling script: $^ @echo Compiling script: $^
@rm -rf $@ && $(TOOLS_DIR)/snippets compile_python_files $^ && chmod 444 $@ @rm -rf $@ && $(TOOLS_DIR)/snippets compile_python_files $^ && chmod 444 $@
build/ba_data/python/ba/__pycache__/_nodeactor.cpython-37.opt-1.pyc: \
build/ba_data/python/ba/_nodeactor.py
@echo Compiling script: $^
@rm -rf $@ && $(TOOLS_DIR)/snippets compile_python_files $^ && chmod 444 $@
build/ba_data/python/ba/__pycache__/_teamgame.cpython-37.opt-1.pyc: \ build/ba_data/python/ba/__pycache__/_teamgame.cpython-37.opt-1.pyc: \
build/ba_data/python/ba/_teamgame.py build/ba_data/python/ba/_teamgame.py
@echo Compiling script: $^ @echo Compiling script: $^

View File

@ -34,7 +34,7 @@ NOTE: This file was autogenerated by gendummymodule; do not edit by hand.
""" """
# (hash we can use to see if this file is out of date) # (hash we can use to see if this file is out of date)
# SOURCES_HASH=279775284016994471710131432123768789736 # SOURCES_HASH=92329394206923431348342003830010439152
# I'm sorry Pylint. I know this file saddens you. Be strong. # I'm sorry Pylint. I know this file saddens you. Be strong.
# pylint: disable=useless-suppression # pylint: disable=useless-suppression
@ -161,8 +161,8 @@ class Context:
sets the context as current on entry and resets it to the previous sets the context as current on entry and resets it to the previous
value on exit. value on exit.
# example: load a few textures into the UI context # Example: load a few textures into the UI context
# (for use in widgets, etc) # (for use in widgets, etc):
with ba.Context('ui'): with ba.Context('ui'):
tex1 = ba.gettexture('foo_tex_1') tex1 = ba.gettexture('foo_tex_1')
tex2 = ba.gettexture('foo_tex_2') tex2 = ba.gettexture('foo_tex_2')
@ -667,7 +667,7 @@ class Node:
target attribute to any value or connecting another node attribute target attribute to any value or connecting another node attribute
to it. to it.
# example: create a locator and attach a light to it # Example: create a locator and attach a light to it:
light = ba.newnode('light') light = ba.newnode('light')
loc = ba.newnode('locator', attrs={'position': (0,10,0)}) loc = ba.newnode('locator', attrs={'position': (0,10,0)})
loc.connectattr('position', light, 'position') loc.connectattr('position', light, 'position')
@ -1044,7 +1044,7 @@ class Timer:
the 'timeformat' arg defaults to SECONDS but can also be MILLISECONDS the 'timeformat' arg defaults to SECONDS but can also be MILLISECONDS
if you want to pass time as milliseconds. if you want to pass time as milliseconds.
# example: use a Timer object to print repeatedly for a few seconds: # Example: use a Timer object to print repeatedly for a few seconds:
def say_it(): def say_it():
ba.screenmessage('BADGER!') ba.screenmessage('BADGER!')
def stop_saying_it(): def stop_saying_it():
@ -1745,6 +1745,11 @@ def do_once() -> bool:
logs. The call functions by registering the filename and line where logs. The call functions by registering the filename and line where
The call is made from. Returns True if this location has not been The call is made from. Returns True if this location has not been
registered already, and False if it has. registered already, and False if it has.
# Example: this print will only fire for the first loop iteration:
for i in range(10):
if ba.do_once():
print('Hello once from loop!')
""" """
return bool() return bool()

View File

@ -40,6 +40,7 @@ from _ba import (CollideModel, Context, ContextCall, Data, InputDevice,
charstr, textwidget, time, timer, open_url, widget) charstr, textwidget, time, timer, open_url, widget)
from ba._activity import Activity from ba._activity import Activity
from ba._actor import Actor from ba._actor import Actor
from ba._nodeactor import NodeActor
from ba._app import App from ba._app import App
from ba._coopgame import CoopGameActivity from ba._coopgame import CoopGameActivity
from ba._coopsession import CoopSession from ba._coopsession import CoopSession

View File

@ -558,12 +558,12 @@ class Activity(DependencyComponent):
def create_player_node(self, player: ba.Player) -> ba.Node: def create_player_node(self, player: ba.Player) -> ba.Node:
"""Create the 'player' node associated with the provided ba.Player.""" """Create the 'player' node associated with the provided ba.Player."""
from ba import _actor from ba._nodeactor import NodeActor
with _ba.Context(self): with _ba.Context(self):
node = _ba.newnode('player', attrs={'playerID': player.get_id()}) node = _ba.newnode('player', attrs={'playerID': player.get_id()})
# FIXME: Should add a dedicated slot for this on ba.Player # FIXME: Should add a dedicated slot for this on ba.Player
# instead of cluttering up their gamedata dict. # instead of cluttering up their gamedata dict.
player.gamedata['_playernode'] = _actor.Actor(node) player.gamedata['_playernode'] = NodeActor(node)
return node return node
def begin(self, session: ba.Session) -> None: def begin(self, session: ba.Session) -> None:

View File

@ -37,24 +37,25 @@ T = TypeVar('T', bound='Actor')
class Actor: class Actor:
"""High level logical entities in a game/activity. """High level logical entities in a game/activity.
category: Gameplay Classes Category: Gameplay Classes
Actors act as controllers, combining some number of ba.Nodes, Actors act as controllers, combining some number of ba.Nodes,
ba.Textures, ba.Sounds, etc. into one cohesive unit. ba.Textures, ba.Sounds, etc. into a high-level cohesive unit.
Some example actors include ba.Bomb, ba.Flag, and ba.Spaz. Some example actors include Bomb, Flag, and Spaz classes in bastd.
One key feature of Actors is that they generally 'die' One key feature of Actors is that they generally 'die'
(killing off or transitioning out their nodes) when the last Python (killing off or transitioning out their nodes) when the last Python
reference to them disappears, so you can use logic such as: reference to them disappears, so you can use logic such as:
# create a flag Actor in our game activity # Create a flag Actor in our game activity:
self.flag = ba.Flag(position=(0, 10, 0)) from bastd.actor.flag import Flag
self.flag = Flag(position=(0, 10, 0))
# later, destroy the flag.. # Later, destroy the flag.
# (provided nothing else is holding a reference to it) # (provided nothing else is holding a reference to it)
# we could also just assign a new flag to this value. # We could also just assign a new flag to this value.
# either way, the old flag disappears. # Either way, the old flag disappears.
self.flag = None self.flag = None
This is in contrast to the behavior of the more low level ba.Nodes, This is in contrast to the behavior of the more low level ba.Nodes,
@ -69,30 +70,25 @@ class Actor:
takes a single arbitrary object as an argument. This provides a safe way takes a single arbitrary object as an argument. This provides a safe way
to communicate between ba.Actor, ba.Activity, ba.Session, and any other to communicate between ba.Actor, ba.Activity, ba.Session, and any other
class providing a handlemessage() method. The most universally handled class providing a handlemessage() method. The most universally handled
message type for actors is the ba.DieMessage. message type for Actors is the ba.DieMessage.
# another way to kill the flag from the example above: # Another way to kill the flag from the example above:
# we can safely call this on any type with a 'handlemessage' method # We can safely call this on any type with a 'handlemessage' method
# (though its not guaranteed to always have a meaningful effect) # (though its not guaranteed to always have a meaningful effect).
# in this case the Actor instance will still be around, but its exists() # In this case the Actor instance will still be around, but its exists()
# and is_alive() methods will both return False # and is_alive() methods will both return False.
self.flag.handlemessage(ba.DieMessage()) self.flag.handlemessage(ba.DieMessage())
""" """
def __init__(self, node: ba.Node = None): def __init__(self) -> None:
"""Instantiates an Actor in the current ba.Activity. """Instantiates an Actor in the current ba.Activity."""
If 'node' is provided, it is stored as the 'node' attribute # FIXME: Actor should not be require to have a 'node' attr.
and the default ba.Actor.handlemessage() and ba.Actor.exists()
implementations will apply to it. This allows the creation of
simple node-wrapping Actors without having to create a new subclass.
"""
self.node: Optional[ba.Node] = None self.node: Optional[ba.Node] = None
activity = _ba.getactivity() activity = _ba.getactivity()
self._activity = weakref.ref(activity) self._activity = weakref.ref(activity)
activity.add_actor_weak_ref(self) activity.add_actor_weak_ref(self)
if node is not None:
self.node = node
def __del__(self) -> None: def __del__(self) -> None:
try: try:
@ -112,14 +108,9 @@ class Actor:
The default implementation will handle ba.DieMessages by The default implementation will handle ba.DieMessages by
calling self.node.delete() if self contains a 'node' attribute. calling self.node.delete() if self contains a 'node' attribute.
""" """
from ba import _messages from ba._error import UNHANDLED
from ba import _error del msg # Unused.
if isinstance(msg, _messages.DieMessage): return UNHANDLED
node = getattr(self, 'node', None)
if node is not None:
node.delete()
return None
return _error.UNHANDLED
def _handlemessage_sanity_check(self) -> None: def _handlemessage_sanity_check(self) -> None:
if self.is_expired(): if self.is_expired():
@ -178,18 +169,13 @@ class Actor:
deleted without affecting the game; this call is often used deleted without affecting the game; this call is often used
when pruning lists of Actors, such as with ba.Actor.autoretain() when pruning lists of Actors, such as with ba.Actor.autoretain()
The default implementation of this method returns 'node.exists()' The default implementation of this method always return True.
if the Actor has a 'node' attr; otherwise True.
Note that the boolean operator for the Actor class calls this method, Note that the boolean operator for the Actor class calls this method,
so a simple "if myactor" test will conveniently do the right thing so a simple "if myactor" test will conveniently do the right thing
even if myactor is set to None. even if myactor is set to None.
""" """
# As a default, if we have a 'node' attr, return whether it exists.
node: ba.Node = getattr(self, 'node', None)
if node is not None:
return node.exists()
return True return True
def __bool__(self) -> bool: def __bool__(self) -> bool:

View File

@ -638,7 +638,8 @@ class App:
activity = _ba.get_foreground_host_activity() activity = _ba.get_foreground_host_activity()
if (activity is not None and activity.allow_pausing if (activity is not None and activity.allow_pausing
and not _ba.have_connected_clients()): and not _ba.have_connected_clients()):
from ba import _gameutils, _actor, _lang from ba import _gameutils, _lang
from ba._nodeactor import NodeActor
# FIXME: Shouldn't be touching scene stuff here; # FIXME: Shouldn't be touching scene stuff here;
# should just pass the request on to the host-session. # should just pass the request on to the host-session.
with _ba.Context(activity): with _ba.Context(activity):
@ -648,7 +649,7 @@ class App:
globs.paused = True globs.paused = True
# FIXME: This should not be an attr on Actor. # FIXME: This should not be an attr on Actor.
activity.paused_text = _actor.Actor( activity.paused_text = NodeActor(
_ba.newnode( _ba.newnode(
'text', 'text',
attrs={ attrs={

View File

@ -80,8 +80,8 @@ class CoopGameActivity(GameActivity):
def _show_standard_scores_to_beat_ui(self, def _show_standard_scores_to_beat_ui(self,
scores: List[Dict[str, Any]]) -> None: scores: List[Dict[str, Any]]) -> None:
from ba import _gameutils from ba._gameutils import timestring, animate
from ba import _actor from ba._nodeactor import NodeActor
from ba._enums import TimeFormat from ba._enums import TimeFormat
display_type = self.get_score_type() display_type = self.get_score_type()
if scores is not None: if scores is not None:
@ -92,16 +92,14 @@ class CoopGameActivity(GameActivity):
# Now make a display for the most recent challenge. # Now make a display for the most recent challenge.
for score in scores: for score in scores:
if score['type'] == 'score_challenge': if score['type'] == 'score_challenge':
tval = ( tval = (score['player'] + ': ' + timestring(
score['player'] + ': ' + int(score['value']) * 10,
(_gameutils.timestring( timeformat=TimeFormat.MILLISECONDS).evaluate()
int(score['value']) * 10, if display_type == 'time' else str(score['value']))
timeformat=TimeFormat.MILLISECONDS).evaluate()
if display_type == 'time' else str(score['value'])))
hattach = 'center' if display_type == 'time' else 'left' hattach = 'center' if display_type == 'time' else 'left'
halign = 'center' if display_type == 'time' else 'left' halign = 'center' if display_type == 'time' else 'left'
pos = (20, -70) if display_type == 'time' else (20, -130) pos = (20, -70) if display_type == 'time' else (20, -130)
txt = _actor.Actor( txt = NodeActor(
_ba.newnode('text', _ba.newnode('text',
attrs={ attrs={
'v_attach': 'top', 'v_attach': 'top',
@ -115,11 +113,7 @@ class CoopGameActivity(GameActivity):
'text': tval 'text': tval
})).autoretain() })).autoretain()
assert txt.node is not None assert txt.node is not None
_gameutils.animate(txt.node, 'scale', { animate(txt.node, 'scale', {1.0: 0.0, 1.1: 0.7, 1.2: 0.6})
1.0: 0.0,
1.1: 0.7,
1.2: 0.6
})
break break
# FIXME: this is now redundant with activityutils.get_score_info(); # FIXME: this is now redundant with activityutils.get_score_info();
@ -279,8 +273,8 @@ class CoopGameActivity(GameActivity):
should_beep = True should_beep = True
break break
if should_beep and self._life_warning_beep is None: if should_beep and self._life_warning_beep is None:
from ba import _actor from ba._nodeactor import NodeActor
self._life_warning_beep = _actor.Actor( self._life_warning_beep = NodeActor(
_ba.newnode('sound', _ba.newnode('sound',
attrs={ attrs={
'sound': self._warn_beeps_sound, 'sound': self._warn_beeps_sound,

View File

@ -703,7 +703,7 @@ class GameActivity(Activity):
# pylint: disable=too-many-locals # pylint: disable=too-many-locals
from ba._freeforallsession import FreeForAllSession from ba._freeforallsession import FreeForAllSession
from ba._gameutils import animate from ba._gameutils import animate
from ba._actor import Actor from ba._nodeactor import NodeActor
sb_name = self.get_instance_scoreboard_display_string() sb_name = self.get_instance_scoreboard_display_string()
# the description can be either a string or a sequence with args # the description can be either a string or a sequence with args
@ -731,7 +731,7 @@ class GameActivity(Activity):
yval -= 16 yval -= 16
sbpos = ((15, yval) if isinstance(self.session, FreeForAllSession) else sbpos = ((15, yval) if isinstance(self.session, FreeForAllSession) else
(15, yval)) (15, yval))
self._game_scoreboard_name_text = Actor( self._game_scoreboard_name_text = NodeActor(
_ba.newnode("text", _ba.newnode("text",
attrs={ attrs={
'text': sb_name, 'text': sb_name,
@ -756,7 +756,7 @@ class GameActivity(Activity):
descpos = (((17, -44 + descpos = (((17, -44 +
10) if isinstance(self.session, FreeForAllSession) else 10) if isinstance(self.session, FreeForAllSession) else
(17, -44 + 10))) (17, -44 + 10)))
self._game_scoreboard_description_text = Actor( self._game_scoreboard_description_text = NodeActor(
_ba.newnode( _ba.newnode(
"text", "text",
attrs={ attrs={
@ -1163,13 +1163,13 @@ class GameActivity(Activity):
""" """
from ba._gameutils import sharedobj from ba._gameutils import sharedobj
from ba._general import WeakCall from ba._general import WeakCall
from ba._actor import Actor from ba._nodeactor import NodeActor
if duration <= 0.0: if duration <= 0.0:
return return
self._standard_time_limit_time = int(duration) self._standard_time_limit_time = int(duration)
self._standard_time_limit_timer = _ba.Timer( self._standard_time_limit_timer = _ba.Timer(
1.0, WeakCall(self._standard_time_limit_tick), repeat=True) 1.0, WeakCall(self._standard_time_limit_tick), repeat=True)
self._standard_time_limit_text = Actor( self._standard_time_limit_text = NodeActor(
_ba.newnode('text', _ba.newnode('text',
attrs={ attrs={
'v_attach': 'top', 'v_attach': 'top',
@ -1180,7 +1180,7 @@ class GameActivity(Activity):
'flatness': 1.0, 'flatness': 1.0,
'scale': 0.9 'scale': 0.9
})) }))
self._standard_time_limit_text_input = Actor( self._standard_time_limit_text_input = NodeActor(
_ba.newnode('timedisplay', _ba.newnode('timedisplay',
attrs={ attrs={
'time2': duration * 1000, 'time2': duration * 1000,
@ -1237,7 +1237,7 @@ class GameActivity(Activity):
If the time-limit expires, end_game() will be called. If the time-limit expires, end_game() will be called.
""" """
from ba._general import WeakCall from ba._general import WeakCall
from ba._actor import Actor from ba._nodeactor import NodeActor
from ba._enums import TimeType from ba._enums import TimeType
if duration <= 0.0: if duration <= 0.0:
return return
@ -1251,7 +1251,7 @@ class GameActivity(Activity):
WeakCall(self._tournament_time_limit_tick), WeakCall(self._tournament_time_limit_tick),
repeat=True, repeat=True,
timetype=TimeType.BASE) timetype=TimeType.BASE)
self._tournament_time_limit_title_text = Actor( self._tournament_time_limit_title_text = NodeActor(
_ba.newnode('text', _ba.newnode('text',
attrs={ attrs={
'v_attach': 'bottom', 'v_attach': 'bottom',
@ -1266,7 +1266,7 @@ class GameActivity(Activity):
'scale': 0.5, 'scale': 0.5,
'text': Lstr(resource='tournamentText') 'text': Lstr(resource='tournamentText')
})) }))
self._tournament_time_limit_text = Actor( self._tournament_time_limit_text = NodeActor(
_ba.newnode('text', _ba.newnode('text',
attrs={ attrs={
'v_attach': 'bottom', 'v_attach': 'bottom',
@ -1280,7 +1280,7 @@ class GameActivity(Activity):
'flatness': 1.0, 'flatness': 1.0,
'scale': 0.9 'scale': 0.9
})) }))
self._tournament_time_limit_text_input = Actor( self._tournament_time_limit_text_input = NodeActor(
_ba.newnode('timedisplay', _ba.newnode('timedisplay',
attrs={ attrs={
'timemin': 0, 'timemin': 0,

View File

@ -441,7 +441,7 @@ def cameraflash(duration: float = 999.0) -> None:
""" """
# pylint: disable=too-many-locals # pylint: disable=too-many-locals
import random import random
from ba._actor import Actor from ba._nodeactor import NodeActor
x_spread = 10 x_spread = 10
y_spread = 5 y_spread = 5
positions = [[-x_spread, -y_spread], [0, -y_spread], [0, y_spread], positions = [[-x_spread, -y_spread], [0, -y_spread], [0, y_spread],
@ -455,7 +455,7 @@ def cameraflash(duration: float = 999.0) -> None:
# noinspection PyTypeHints # noinspection PyTypeHints
activity.camera_flash_data = [] # type: ignore activity.camera_flash_data = [] # type: ignore
for i in range(6): for i in range(6):
light = Actor( light = NodeActor(
_ba.newnode("light", _ba.newnode("light",
attrs={ attrs={
'position': (positions[i][0], 0, positions[i][1]), 'position': (positions[i][0], 0, positions[i][1]),

View File

@ -159,7 +159,7 @@ class _WeakCall:
Instantiate a WeakCall; pass a callable as the first Instantiate a WeakCall; pass a callable as the first
arg, followed by any number of arguments or keywords. arg, followed by any number of arguments or keywords.
# example: wrap a method call with some positional and # Example: wrap a method call with some positional and
# keyword args: # keyword args:
myweakcall = ba.WeakCall(myobj.dostuff, argval1, namedarg=argval2) myweakcall = ba.WeakCall(myobj.dostuff, argval1, namedarg=argval2)
@ -211,7 +211,7 @@ class _Call:
Instantiate a Call; pass a callable as the first Instantiate a Call; pass a callable as the first
arg, followed by any number of arguments or keywords. arg, followed by any number of arguments or keywords.
# Example: wrap a method call with 1 positional and 1 keyword arg. # Example: wrap a method call with 1 positional and 1 keyword arg:
mycall = ba.Call(myobj.dostuff, argval1, namedarg=argval2) mycall = ba.Call(myobj.dostuff, argval1, namedarg=argval2)
# Now we have a single callable to run that whole mess. # Now we have a single callable to run that whole mess.

View File

@ -42,8 +42,8 @@ class JoinInfo:
# pylint: disable=too-many-locals # pylint: disable=too-many-locals
from ba import _input from ba import _input
from ba._lang import Lstr from ba._lang import Lstr
from ba import _actor from ba._nodeactor import NodeActor
from ba import _general from ba._general import WeakCall
from ba._enums import SpecialChar from ba._enums import SpecialChar
can_switch_teams = (len(lobby.teams) > 1) can_switch_teams = (len(lobby.teams) > 1)
self._state = 0 self._state = 0
@ -79,7 +79,7 @@ class JoinInfo:
join_str = Lstr(resource='pressAnyButtonToJoinText') join_str = Lstr(resource='pressAnyButtonToJoinText')
flatness = 1.0 if _ba.app.vr_mode else 0.0 flatness = 1.0 if _ba.app.vr_mode else 0.0
self._text = _actor.Actor( self._text = NodeActor(
_ba.newnode('text', _ba.newnode('text',
attrs={ attrs={
'position': (0, -40), 'position': (0, -40),
@ -109,9 +109,7 @@ class JoinInfo:
' ' + _ba.charstr(SpecialChar.RIGHT_ARROW))]) ' ' + _ba.charstr(SpecialChar.RIGHT_ARROW))])
] if can_switch_teams else []) + [msg1] + [msg3] + [join_str]) ] if can_switch_teams else []) + [msg1] + [msg3] + [join_str])
self._timer = _ba.Timer(4.0, self._timer = _ba.Timer(4.0, WeakCall(self._update), repeat=True)
_general.WeakCall(self._update),
repeat=True)
def _update(self) -> None: def _update(self) -> None:
assert self._text.node assert self._text.node
@ -393,7 +391,7 @@ class Chooser:
def reload_profiles(self) -> None: def reload_profiles(self) -> None:
"""Reload all player profiles.""" """Reload all player profiles."""
from ba import _general from ba._general import json_prep
app = _ba.app app = _ba.app
# Re-construct our profile index and other stuff since the profile # Re-construct our profile index and other stuff since the profile
@ -422,7 +420,7 @@ class Chooser:
# (non-unicode/non-json) version. # (non-unicode/non-json) version.
# Make sure they conform to our standards # Make sure they conform to our standards
# (unicode strings, no tuples, etc) # (unicode strings, no tuples, etc)
self.profiles = _general.json_prep(self.profiles) self.profiles = json_prep(self.profiles)
# Filter out any characters we're unaware of. # Filter out any characters we're unaware of.
for profile in list(self.profiles.items()): for profile in list(self.profiles.items()):
@ -551,7 +549,7 @@ class Chooser:
def _set_ready(self, ready: bool) -> None: def _set_ready(self, ready: bool) -> None:
# pylint: disable=cyclic-import # pylint: disable=cyclic-import
from bastd.ui.profile import browser as pbrowser from bastd.ui.profile import browser as pbrowser
from ba import _general from ba._general import Call
profilename = self._profilenames[self._profileindex] profilename = self._profilenames[self._profileindex]
# Handle '_edit' as a special case. # Handle '_edit' as a special case.
@ -566,26 +564,23 @@ class Chooser:
if not ready: if not ready:
self._player.assign_input_call( self._player.assign_input_call(
'leftPress', 'leftPress', Call(self.handlemessage,
_general.Call(self.handlemessage, ChangeMessage('team', -1))) ChangeMessage('team', -1)))
self._player.assign_input_call( self._player.assign_input_call(
'rightPress', 'rightPress', Call(self.handlemessage,
_general.Call(self.handlemessage, ChangeMessage('team', 1))) ChangeMessage('team', 1)))
self._player.assign_input_call( self._player.assign_input_call(
'bombPress', 'bombPress',
_general.Call(self.handlemessage, Call(self.handlemessage, ChangeMessage('character', 1)))
ChangeMessage('character', 1)))
self._player.assign_input_call( self._player.assign_input_call(
'upPress', 'upPress',
_general.Call(self.handlemessage, Call(self.handlemessage, ChangeMessage('profileindex', -1)))
ChangeMessage('profileindex', -1)))
self._player.assign_input_call( self._player.assign_input_call(
'downPress', 'downPress',
_general.Call(self.handlemessage, Call(self.handlemessage, ChangeMessage('profileindex', 1)))
ChangeMessage('profileindex', 1)))
self._player.assign_input_call( self._player.assign_input_call(
('jumpPress', 'pickUpPress', 'punchPress'), ('jumpPress', 'pickUpPress', 'punchPress'),
_general.Call(self.handlemessage, ChangeMessage('ready', 1))) Call(self.handlemessage, ChangeMessage('ready', 1)))
self._ready = False self._ready = False
self._update_text() self._update_text()
self._player.set_name('untitled', real=False) self._player.set_name('untitled', real=False)
@ -595,7 +590,7 @@ class Chooser:
'jumpPress', 'bombPress', 'pickUpPress'), self._do_nothing) 'jumpPress', 'bombPress', 'pickUpPress'), self._do_nothing)
self._player.assign_input_call( self._player.assign_input_call(
('jumpPress', 'bombPress', 'pickUpPress', 'punchPress'), ('jumpPress', 'bombPress', 'pickUpPress', 'punchPress'),
_general.Call(self.handlemessage, ChangeMessage('ready', 0))) Call(self.handlemessage, ChangeMessage('ready', 0)))
# Store the last profile picked by this input for reuse. # Store the last profile picked by this input for reuse.
input_device = self._player.get_input_device() input_device = self._player.get_input_device()

View File

@ -0,0 +1,53 @@
# Copyright (c) 2011-2020 Eric Froemling
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# -----------------------------------------------------------------------------
"""Defines NodeActor class."""
from __future__ import annotations
from typing import TYPE_CHECKING
from ba._messages import DieMessage
from ba._actor import Actor
if TYPE_CHECKING:
import ba
from typing import Any
class NodeActor(Actor):
"""A simple ba.Actor type that wraps a single ba.Node.
Category: Gameplay Classes
This Actor will delete its Node when told to die, and it's
exists() call will return whether the Node still exists or not.
"""
def __init__(self, node: ba.Node):
super().__init__()
self.node = node
def handlemessage(self, msg: Any) -> Any:
if isinstance(msg, DieMessage):
if self.node:
self.node.delete()
return None
return super().handlemessage(msg)

View File

@ -65,7 +65,7 @@ class TeamBaseSession(Session):
team_names = None team_names = None
team_colors = None team_colors = None
print('FIXME: TEAM BASE SESSION WOULD CALC DEPS.') # print('FIXME: TEAM BASE SESSION WOULD CALC DEPS.')
depsets: Sequence[ba.DependencySet] = [] depsets: Sequence[ba.DependencySet] = []
super().__init__(depsets, super().__init__(depsets,
team_names=team_names, team_names=team_names,

View File

@ -867,7 +867,7 @@ class CoopScoreScreen(ba.Activity):
ba.timer(5.0, ba.WeakCall(self._show_tips)) ba.timer(5.0, ba.WeakCall(self._show_tips))
def _play_drumroll(self) -> None: def _play_drumroll(self) -> None:
ba.Actor( ba.NodeActor(
ba.newnode('sound', ba.newnode('sound',
attrs={ attrs={
'sound': self.drum_roll_sound, 'sound': self.drum_roll_sound,
@ -1314,7 +1314,7 @@ class CoopScoreScreen(ba.Activity):
star_tex = ba.gettexture('star') star_tex = ba.gettexture('star')
star_x = 135 + offs_x star_x = 135 + offs_x
for _i in range(stars): for _i in range(stars):
img = ba.Actor( img = ba.NodeActor(
ba.newnode('image', ba.newnode('image',
attrs={ attrs={
'texture': star_tex, 'texture': star_tex,
@ -1329,7 +1329,7 @@ class CoopScoreScreen(ba.Activity):
ba.animate(img.node, 'opacity', {0.15: 0, 0.4: 1}) ba.animate(img.node, 'opacity', {0.15: 0, 0.4: 1})
star_x += 60 star_x += 60
for _i in range(3 - stars): for _i in range(3 - stars):
img = ba.Actor( img = ba.NodeActor(
ba.newnode('image', ba.newnode('image',
attrs={ attrs={
'texture': star_tex, 'texture': star_tex,
@ -1355,7 +1355,7 @@ class CoopScoreScreen(ba.Activity):
transition_delay=1.0).autoretain() transition_delay=1.0).autoretain()
stx = xval + 20 stx = xval + 20
for _i2 in range(count): for _i2 in range(count):
img2 = ba.Actor( img2 = ba.NodeActor(
ba.newnode('image', ba.newnode('image',
attrs={ attrs={
'texture': star_tex, 'texture': star_tex,

View File

@ -89,7 +89,7 @@ class RespawnIcon:
texture = icon['texture'] texture = icon['texture']
h_offs = -10 h_offs = -10
ipos = (-40 - h_offs if on_right else 40 + h_offs, -180 + offs) ipos = (-40 - h_offs if on_right else 40 + h_offs, -180 + offs)
self._image: Optional[ba.Actor] = ba.Actor( self._image: Optional[ba.NodeActor] = ba.NodeActor(
ba.newnode('image', ba.newnode('image',
attrs={ attrs={
'texture': texture, 'texture': texture,
@ -108,7 +108,7 @@ class RespawnIcon:
ba.animate(self._image.node, 'opacity', {0.0: 0, 0.2: 0.7}) ba.animate(self._image.node, 'opacity', {0.0: 0, 0.2: 0.7})
npos = (-40 - h_offs if on_right else 40 + h_offs, -205 + 49 + offs) npos = (-40 - h_offs if on_right else 40 + h_offs, -205 + 49 + offs)
self._name: Optional[ba.Actor] = ba.Actor( self._name: Optional[ba.NodeActor] = ba.NodeActor(
ba.newnode('text', ba.newnode('text',
attrs={ attrs={
'v_attach': 'top', 'v_attach': 'top',
@ -128,7 +128,7 @@ class RespawnIcon:
ba.animate(self._name.node, 'scale', {0: 0, 0.1: 0.5}) ba.animate(self._name.node, 'scale', {0: 0, 0.1: 0.5})
tpos = (-60 - h_offs if on_right else 60 + h_offs, -192 + offs) tpos = (-60 - h_offs if on_right else 60 + h_offs, -192 + offs)
self._text: Optional[ba.Actor] = ba.Actor( self._text: Optional[ba.NodeActor] = ba.NodeActor(
ba.newnode('text', ba.newnode('text',
attrs={ attrs={
'position': tpos, 'position': tpos,

View File

@ -72,7 +72,7 @@ class _Entry:
self._backing_color = [0.05 + c * 0.1 for c in safe_team_color] self._backing_color = [0.05 + c * 0.1 for c in safe_team_color]
opacity = (0.8 if vrmode else 0.8) if self._do_cover else 0.5 opacity = (0.8 if vrmode else 0.8) if self._do_cover else 0.5
self._backing = ba.Actor( self._backing = ba.NodeActor(
ba.newnode('image', ba.newnode('image',
attrs={ attrs={
'scale': (self._width, self._height), 'scale': (self._width, self._height),
@ -84,7 +84,7 @@ class _Entry:
})) }))
self._barcolor = safe_team_color self._barcolor = safe_team_color
self._bar = ba.Actor( self._bar = ba.NodeActor(
ba.newnode('image', ba.newnode('image',
attrs={ attrs={
'opacity': 0.7, 'opacity': 0.7,
@ -112,7 +112,7 @@ class _Entry:
self._bar_position.connectattr('output', self._bar.node, 'position') self._bar_position.connectattr('output', self._bar.node, 'position')
self._cover_color = safe_team_color self._cover_color = safe_team_color
if self._do_cover: if self._do_cover:
self._cover = ba.Actor( self._cover = ba.NodeActor(
ba.newnode('image', ba.newnode('image',
attrs={ attrs={
'scale': 'scale':
@ -128,7 +128,7 @@ class _Entry:
clr = safe_team_color clr = safe_team_color
maxwidth = 130.0 * (1.0 - scoreboard.score_split) maxwidth = 130.0 * (1.0 - scoreboard.score_split)
flatness = ((1.0 if vrmode else 0.5) if self._do_cover else 1.0) flatness = ((1.0 if vrmode else 0.5) if self._do_cover else 1.0)
self._score_text = ba.Actor( self._score_text = ba.NodeActor(
ba.newnode('text', ba.newnode('text',
attrs={ attrs={
'h_attach': 'left', 'h_attach': 'left',
@ -169,7 +169,7 @@ class _Entry:
team_name_label = ba.Lstr(value=team_name_label) team_name_label = ba.Lstr(value=team_name_label)
flatness = ((1.0 if vrmode else 0.5) if self._do_cover else 1.0) flatness = ((1.0 if vrmode else 0.5) if self._do_cover else 1.0)
self._name_text = ba.Actor( self._name_text = ba.NodeActor(
ba.newnode('text', ba.newnode('text',
attrs={ attrs={
'h_attach': 'left', 'h_attach': 'left',

View File

@ -58,10 +58,10 @@ class BombDiedMessage:
def get_factory() -> SpazFactory: def get_factory() -> SpazFactory:
"""Return the shared ba.SpazFactory object, creating it if necessary.""" """Return the shared ba.SpazFactory object, creating it if necessary."""
# pylint: disable=cyclic-import # pylint: disable=cyclic-import
from bastd.actor.spazfactory import SpazFactory
activity = ba.getactivity() activity = ba.getactivity()
factory = getattr(activity, 'shared_spaz_factory', None) factory = getattr(activity, 'shared_spaz_factory', None)
if factory is None: if factory is None:
from bastd.actor.spazfactory import SpazFactory
# noinspection PyTypeHints # noinspection PyTypeHints
factory = activity.shared_spaz_factory = SpazFactory() # type: ignore factory = activity.shared_spaz_factory = SpazFactory() # type: ignore
assert isinstance(factory, SpazFactory) assert isinstance(factory, SpazFactory)
@ -693,11 +693,11 @@ class Spaz(ba.Actor):
# pylint: disable=too-many-branches # pylint: disable=too-many-branches
if __debug__ is True: if __debug__ is True:
self._handlemessage_sanity_check() self._handlemessage_sanity_check()
assert self.node
if isinstance(msg, ba.PickedUpMessage): if isinstance(msg, ba.PickedUpMessage):
self.node.handlemessage("hurt_sound") if self.node:
self.node.handlemessage("picked_up") self.node.handlemessage("hurt_sound")
self.node.handlemessage("picked_up")
# this counts as a hit # this counts as a hit
self._num_times_hit += 1 self._num_times_hit += 1
@ -713,7 +713,7 @@ class Spaz(ba.Actor):
ba.timer(0.001, ba.WeakCall(self._hit_self, msg.intensity)) ba.timer(0.001, ba.WeakCall(self._hit_self, msg.intensity))
elif isinstance(msg, ba.PowerupMessage): elif isinstance(msg, ba.PowerupMessage):
if self._dead: if self._dead or not self.node:
return True return True
if self.pick_up_powerup_callback is not None: if self.pick_up_powerup_callback is not None:
self.pick_up_powerup_callback(self) self.pick_up_powerup_callback(self)
@ -1069,14 +1069,15 @@ class Spaz(ba.Actor):
newdamage = max(damage - 200, self.hitpoints - 10) newdamage = max(damage - 200, self.hitpoints - 10)
damage = newdamage damage = newdamage
self.node.handlemessage("flash") self.node.handlemessage("flash")
# if we're holding something, drop it
# If we're holding something, drop it.
if damage > 0.0 and self.node.hold_node: if damage > 0.0 and self.node.hold_node:
# self.node.hold_node = ba.Node(None) # self.node.hold_node = ba.Node(None)
self.node.hold_node = None self.node.hold_node = None
self.hitpoints -= damage self.hitpoints -= damage
self.node.hurt = 1.0 - float( self.node.hurt = 1.0 - float(
self.hitpoints) / self.hitpoints_max self.hitpoints) / self.hitpoints_max
# if we're cursed, *any* damage blows us up # If we're cursed, *any* damage blows us up.
if self._cursed and damage > 0: if self._cursed and damage > 0:
ba.timer( ba.timer(
0.05, ba.WeakCall(self.curse_explode, 0.05, ba.WeakCall(self.curse_explode,
@ -1103,7 +1104,8 @@ class Spaz(ba.Actor):
self._dead = True self._dead = True
self.hitpoints = 0 self.hitpoints = 0
if msg.immediate: if msg.immediate:
self.node.delete() if self.node:
self.node.delete()
elif self.node: elif self.node:
self.node.hurt = 1.0 self.node.hurt = 1.0
if self.play_big_death_sound and not wasdead: if self.play_big_death_sound and not wasdead:
@ -1117,11 +1119,15 @@ class Spaz(ba.Actor):
elif isinstance(msg, ba.StandMessage): elif isinstance(msg, ba.StandMessage):
self._last_stand_pos = (msg.position[0], msg.position[1], self._last_stand_pos = (msg.position[0], msg.position[1],
msg.position[2]) msg.position[2])
self.node.handlemessage("stand", msg.position[0], msg.position[1], if self.node:
msg.position[2], msg.angle) self.node.handlemessage("stand", msg.position[0],
msg.position[1], msg.position[2],
msg.angle)
elif isinstance(msg, CurseExplodeMessage): elif isinstance(msg, CurseExplodeMessage):
self.curse_explode() self.curse_explode()
elif isinstance(msg, PunchHitMessage): elif isinstance(msg, PunchHitMessage):
if not self.node:
return None
node = ba.get_collision_info("opposing_node") node = ba.get_collision_info("opposing_node")
# only allow one hit per node per punch # only allow one hit per node per punch
@ -1178,6 +1184,9 @@ class Spaz(ba.Actor):
ppos[2], punchdir[0], punchdir[1], ppos[2], punchdir[0], punchdir[1],
punchdir[2], mag) punchdir[2], mag)
elif isinstance(msg, PickupMessage): elif isinstance(msg, PickupMessage):
if not self.node:
return None
opposing_node, opposing_body = ba.get_collision_info( opposing_node, opposing_body = ba.get_collision_info(
'opposing_node', 'opposing_body') 'opposing_node', 'opposing_body')

View File

@ -354,7 +354,7 @@ class CaptureTheFlagGame(ba.TeamGameActivity):
team.gamedata['touch_return_timer_ticking'] = None team.gamedata['touch_return_timer_ticking'] = None
return # No need to return when its at home. return # No need to return when its at home.
if team.gamedata['touch_return_timer_ticking'] is None: if team.gamedata['touch_return_timer_ticking'] is None:
team.gamedata['touch_return_timer_ticking'] = ba.Actor( team.gamedata['touch_return_timer_ticking'] = ba.NodeActor(
ba.newnode('sound', ba.newnode('sound',
attrs={ attrs={
'sound': self._ticking_sound, 'sound': self._ticking_sound,

View File

@ -293,7 +293,7 @@ class ChosenOneGame(ba.TeamGameActivity):
0.3 + c * 0.7 0.3 + c * 0.7
for c in ba.normalized_color(player.team.color) for c in ba.normalized_color(player.team.color)
] ]
light = player.gamedata['chosen_light'] = ba.Actor( light = player.gamedata['chosen_light'] = ba.NodeActor(
ba.newnode('light', ba.newnode('light',
attrs={ attrs={
"intensity": 0.6, "intensity": 0.6,

View File

@ -449,7 +449,7 @@ class EliminationGame(ba.TeamGameActivity):
self.setup_standard_time_limit(self.settings['Time Limit']) self.setup_standard_time_limit(self.settings['Time Limit'])
self.setup_standard_powerup_drops() self.setup_standard_powerup_drops()
if self._solo_mode: if self._solo_mode:
self._vs_text = ba.Actor( self._vs_text = ba.NodeActor(
ba.newnode("text", ba.newnode("text",
attrs={ attrs={
'position': (0, 105), 'position': (0, 105),

View File

@ -157,7 +157,7 @@ class FootballTeamGame(ba.TeamGameActivity):
self._spawn_flag() self._spawn_flag()
defs = self.map.defs defs = self.map.defs
self._score_regions.append( self._score_regions.append(
ba.Actor( ba.NodeActor(
ba.newnode('region', ba.newnode('region',
attrs={ attrs={
'position': defs.boxes['goal1'][0:3], 'position': defs.boxes['goal1'][0:3],
@ -166,7 +166,7 @@ class FootballTeamGame(ba.TeamGameActivity):
'materials': (self.score_region_material, ) 'materials': (self.score_region_material, )
}))) })))
self._score_regions.append( self._score_regions.append(
ba.Actor( ba.NodeActor(
ba.newnode('region', ba.newnode('region',
attrs={ attrs={
'position': defs.boxes['goal2'][0:3], 'position': defs.boxes['goal2'][0:3],
@ -279,7 +279,7 @@ class FootballTeamGame(ba.TeamGameActivity):
elif isinstance(msg, stdflag.FlagDeathMessage): elif isinstance(msg, stdflag.FlagDeathMessage):
if not self.has_ended(): if not self.has_ended():
self._flag_respawn_timer = ba.Timer(3.0, self._spawn_flag) self._flag_respawn_timer = ba.Timer(3.0, self._spawn_flag)
self._flag_respawn_light = ba.Actor( self._flag_respawn_light = ba.NodeActor(
ba.newnode('light', ba.newnode('light',
attrs={ attrs={
'position': self._flag_spawn_pos, 'position': self._flag_spawn_pos,
@ -406,7 +406,7 @@ class FootballCoopGame(ba.CoopGameActivity):
# Set up the two score regions. # Set up the two score regions.
defs = self.map.defs defs = self.map.defs
self.score_regions.append( self.score_regions.append(
ba.Actor( ba.NodeActor(
ba.newnode('region', ba.newnode('region',
attrs={ attrs={
'position': defs.boxes['goal1'][0:3], 'position': defs.boxes['goal1'][0:3],
@ -415,7 +415,7 @@ class FootballCoopGame(ba.CoopGameActivity):
'materials': [self._score_region_material] 'materials': [self._score_region_material]
}))) })))
self.score_regions.append( self.score_regions.append(
ba.Actor( ba.NodeActor(
ba.newnode('region', ba.newnode('region',
attrs={ attrs={
'position': defs.boxes['goal2'][0:3], 'position': defs.boxes['goal2'][0:3],
@ -516,7 +516,7 @@ class FootballCoopGame(ba.CoopGameActivity):
starttime_ms = ba.time(timeformat=ba.TimeFormat.MILLISECONDS) starttime_ms = ba.time(timeformat=ba.TimeFormat.MILLISECONDS)
assert isinstance(starttime_ms, int) assert isinstance(starttime_ms, int)
self._starttime_ms = starttime_ms self._starttime_ms = starttime_ms
self._time_text = ba.Actor( self._time_text = ba.NodeActor(
ba.newnode('text', ba.newnode('text',
attrs={ attrs={
'v_attach': 'top', 'v_attach': 'top',
@ -529,7 +529,7 @@ class FootballCoopGame(ba.CoopGameActivity):
'scale': 1.3, 'scale': 1.3,
'text': '' 'text': ''
})) }))
self._time_text_input = ba.Actor( self._time_text_input = ba.NodeActor(
ba.newnode('timedisplay', attrs={'showsubseconds': True})) ba.newnode('timedisplay', attrs={'showsubseconds': True}))
ba.sharedobj('globals').connectattr('time', self._time_text_input.node, ba.sharedobj('globals').connectattr('time', self._time_text_input.node,
'time2') 'time2')
@ -844,7 +844,7 @@ class FootballCoopGame(ba.CoopGameActivity):
elif isinstance(msg, stdflag.FlagDeathMessage): elif isinstance(msg, stdflag.FlagDeathMessage):
assert isinstance(msg.flag, FootballFlag) assert isinstance(msg.flag, FootballFlag)
msg.flag.respawn_timer = ba.Timer(3.0, self._spawn_flag) msg.flag.respawn_timer = ba.Timer(3.0, self._spawn_flag)
self._flag_respawn_light = ba.Actor( self._flag_respawn_light = ba.NodeActor(
ba.newnode('light', ba.newnode('light',
attrs={ attrs={
'position': self._flag_spawn_pos, 'position': self._flag_spawn_pos,

View File

@ -222,7 +222,7 @@ class HockeyGame(ba.TeamGameActivity):
defs = self.map.defs defs = self.map.defs
self._score_regions = [] self._score_regions = []
self._score_regions.append( self._score_regions.append(
ba.Actor( ba.NodeActor(
ba.newnode("region", ba.newnode("region",
attrs={ attrs={
'position': defs.boxes["goal1"][0:3], 'position': defs.boxes["goal1"][0:3],
@ -231,7 +231,7 @@ class HockeyGame(ba.TeamGameActivity):
'materials': [self._score_region_material] 'materials': [self._score_region_material]
}))) })))
self._score_regions.append( self._score_regions.append(
ba.Actor( ba.NodeActor(
ba.newnode("region", ba.newnode("region",
attrs={ attrs={
'position': defs.boxes["goal2"][0:3], 'position': defs.boxes["goal2"][0:3],

View File

@ -170,7 +170,7 @@ class OnslaughtGame(ba.CoopGameActivity):
'sound': ba.getsound('ding') 'sound': ba.getsound('ding')
}] }]
self._spawn_info_text = ba.Actor( self._spawn_info_text = ba.NodeActor(
ba.newnode("text", ba.newnode("text",
attrs={ attrs={
'position': (15, -130), 'position': (15, -130),
@ -1049,7 +1049,7 @@ class OnslaughtGame(ba.CoopGameActivity):
('${A}', ba.Lstr(resource='timeBonusText')), ('${A}', ba.Lstr(resource='timeBonusText')),
('${B}', str(self._time_bonus)), ('${B}', str(self._time_bonus)),
]) ])
self._time_bonus_text = ba.Actor( self._time_bonus_text = ba.NodeActor(
ba.newnode('text', ba.newnode('text',
attrs={ attrs={
'v_attach': 'top', 'v_attach': 'top',
@ -1075,7 +1075,7 @@ class OnslaughtGame(ba.CoopGameActivity):
('' if self._preset in ['endless', 'endless_tournament'] else ('' if self._preset in ['endless', 'endless_tournament'] else
('/' + str(len(self._waves))))) ('/' + str(len(self._waves)))))
]) ])
self._wave_text = ba.Actor( self._wave_text = ba.NodeActor(
ba.newnode('text', ba.newnode('text',
attrs={ attrs={
'v_attach': 'top', 'v_attach': 'top',

View File

@ -406,7 +406,7 @@ class RaceGame(ba.TeamGameActivity):
self._team_finish_pts = 100 self._team_finish_pts = 100
# Throw a timer up on-screen. # Throw a timer up on-screen.
self._time_text = ba.Actor( self._time_text = ba.NodeActor(
ba.newnode('text', ba.newnode('text',
attrs={ attrs={
'v_attach': 'top', 'v_attach': 'top',

View File

@ -134,7 +134,7 @@ class RunaroundGame(ba.CoopGameActivity):
super().on_transition_in() super().on_transition_in()
self._scoreboard = Scoreboard(label=ba.Lstr(resource='scoreText'), self._scoreboard = Scoreboard(label=ba.Lstr(resource='scoreText'),
score_split=0.5) score_split=0.5)
self._score_region = ba.Actor( self._score_region = ba.NodeActor(
ba.newnode( ba.newnode(
'region', 'region',
attrs={ attrs={
@ -365,7 +365,7 @@ class RunaroundGame(ba.CoopGameActivity):
l_offs = (-80 if interface_type == 'small' else l_offs = (-80 if interface_type == 'small' else
-40 if interface_type == 'medium' else 0) -40 if interface_type == 'medium' else 0)
self._lives_bg = ba.Actor( self._lives_bg = ba.NodeActor(
ba.newnode('image', ba.newnode('image',
attrs={ attrs={
'texture': self._heart_tex, 'texture': self._heart_tex,
@ -379,7 +379,7 @@ class RunaroundGame(ba.CoopGameActivity):
# FIXME; should not set things based on vr mode. # FIXME; should not set things based on vr mode.
# (won't look right to non-vr connected clients, etc) # (won't look right to non-vr connected clients, etc)
vrmode = ba.app.vr_mode vrmode = ba.app.vr_mode
self._lives_text = ba.Actor( self._lives_text = ba.NodeActor(
ba.newnode( ba.newnode(
'text', 'text',
attrs={ attrs={
@ -943,7 +943,7 @@ class RunaroundGame(ba.CoopGameActivity):
subs=[('${A}', ba.Lstr(resource='timeBonusText')), subs=[('${A}', ba.Lstr(resource='timeBonusText')),
('${B}', str(int(self._time_bonus * self._time_bonus_mult))) ('${B}', str(int(self._time_bonus * self._time_bonus_mult)))
]) ])
self._time_bonus_text = ba.Actor( self._time_bonus_text = ba.NodeActor(
ba.newnode('text', ba.newnode('text',
attrs={ attrs={
'v_attach': 'top', 'v_attach': 'top',
@ -973,7 +973,7 @@ class RunaroundGame(ba.CoopGameActivity):
('' if self._preset in ['endless', 'endless_tournament'] else ('' if self._preset in ['endless', 'endless_tournament'] else
('/' + str(len(self._waves))))) ('/' + str(len(self._waves)))))
]) ])
self._wave_text = ba.Actor( self._wave_text = ba.NodeActor(
ba.newnode('text', ba.newnode('text',
attrs={ attrs={
'v_attach': 'top', 'v_attach': 'top',

View File

@ -67,7 +67,7 @@ class MainMenuActivity(ba.Activity):
# FIXME: Need a node attr for vr-specific-scale. # FIXME: Need a node attr for vr-specific-scale.
scale = (0.9 if scale = (0.9 if
(app.interface_type == 'small' or vr_mode) else 0.7) (app.interface_type == 'small' or vr_mode) else 0.7)
self.my_name = ba.Actor( self.my_name = ba.NodeActor(
ba.newnode('text', ba.newnode('text',
attrs={ attrs={
'v_attach': 'bottom', 'v_attach': 'bottom',
@ -86,7 +86,7 @@ class MainMenuActivity(ba.Activity):
# empty-ish screen. # empty-ish screen.
tval = ba.Lstr(resource='hostIsNavigatingMenusText', tval = ba.Lstr(resource='hostIsNavigatingMenusText',
subs=[('${HOST}', _ba.get_account_display_string())]) subs=[('${HOST}', _ba.get_account_display_string())])
self._host_is_navigating_text = ba.Actor( self._host_is_navigating_text = ba.NodeActor(
ba.newnode('text', ba.newnode('text',
attrs={ attrs={
'text': tval, 'text': tval,
@ -130,7 +130,7 @@ class MainMenuActivity(ba.Activity):
text = ba.Lstr(value='${V}', subs=[('${V}', app.version)]) text = ba.Lstr(value='${V}', subs=[('${V}', app.version)])
scale = 0.9 if (interface_type == 'small' or vr_mode) else 0.7 scale = 0.9 if (interface_type == 'small' or vr_mode) else 0.7
color = (1, 1, 1, 1) if vr_mode else (0.5, 0.6, 0.5, 0.7) color = (1, 1, 1, 1) if vr_mode else (0.5, 0.6, 0.5, 0.7)
self.version = ba.Actor( self.version = ba.NodeActor(
ba.newnode( ba.newnode(
'text', 'text',
attrs={ attrs={
@ -153,7 +153,7 @@ class MainMenuActivity(ba.Activity):
self.beta_info = self.beta_info_2 = None self.beta_info = self.beta_info_2 = None
if app.test_build and not app.kiosk_mode: if app.test_build and not app.kiosk_mode:
pos = (230, 125) if app.kiosk_mode else (230, 35) pos = (230, 125) if app.kiosk_mode else (230, 35)
self.beta_info = ba.Actor( self.beta_info = ba.NodeActor(
ba.newnode('text', ba.newnode('text',
attrs={ attrs={
'v_attach': 'center', 'v_attach': 'center',
@ -191,7 +191,7 @@ class MainMenuActivity(ba.Activity):
gnode.vignette_outer = (0.45, 0.55, 0.54) gnode.vignette_outer = (0.45, 0.55, 0.54)
gnode.vignette_inner = (0.99, 0.98, 0.98) gnode.vignette_inner = (0.99, 0.98, 0.98)
self.bottom = ba.Actor( self.bottom = ba.NodeActor(
ba.newnode('terrain', ba.newnode('terrain',
attrs={ attrs={
'model': bottom_model, 'model': bottom_model,
@ -200,7 +200,7 @@ class MainMenuActivity(ba.Activity):
'reflection_scale': [0.45], 'reflection_scale': [0.45],
'color_texture': color_texture 'color_texture': color_texture
})) }))
self.vr_bottom_fill = ba.Actor( self.vr_bottom_fill = ba.NodeActor(
ba.newnode('terrain', ba.newnode('terrain',
attrs={ attrs={
'model': vr_bottom_fill_model, 'model': vr_bottom_fill_model,
@ -208,7 +208,7 @@ class MainMenuActivity(ba.Activity):
'vr_only': True, 'vr_only': True,
'color_texture': color_texture 'color_texture': color_texture
})) }))
self.vr_top_fill = ba.Actor( self.vr_top_fill = ba.NodeActor(
ba.newnode('terrain', ba.newnode('terrain',
attrs={ attrs={
'model': vr_top_fill_model, 'model': vr_top_fill_model,
@ -216,7 +216,7 @@ class MainMenuActivity(ba.Activity):
'lighting': False, 'lighting': False,
'color_texture': bgtex 'color_texture': bgtex
})) }))
self.terrain = ba.Actor( self.terrain = ba.NodeActor(
ba.newnode('terrain', ba.newnode('terrain',
attrs={ attrs={
'model': model, 'model': model,
@ -224,7 +224,7 @@ class MainMenuActivity(ba.Activity):
'reflection': 'soft', 'reflection': 'soft',
'reflection_scale': [0.3] 'reflection_scale': [0.3]
})) }))
self.trees = ba.Actor( self.trees = ba.NodeActor(
ba.newnode('terrain', ba.newnode('terrain',
attrs={ attrs={
'model': trees_model, 'model': trees_model,
@ -233,7 +233,7 @@ class MainMenuActivity(ba.Activity):
'reflection_scale': [0.1], 'reflection_scale': [0.1],
'color_texture': trees_texture 'color_texture': trees_texture
})) }))
self.bgterrain = ba.Actor( self.bgterrain = ba.NodeActor(
ba.newnode('terrain', ba.newnode('terrain',
attrs={ attrs={
'model': bgmodel, 'model': bgmodel,
@ -380,7 +380,7 @@ class MainMenuActivity(ba.Activity):
color2 = ((1, 1, 1, 1) if ba.app.vr_mode else color2 = ((1, 1, 1, 1) if ba.app.vr_mode else
(0.7, 0.65, 0.75, 1.0)) (0.7, 0.65, 0.75, 1.0))
shadow = (1.0 if ba.app.vr_mode else 0.4) shadow = (1.0 if ba.app.vr_mode else 0.4)
self._text = ba.Actor( self._text = ba.NodeActor(
ba.newnode('text', ba.newnode('text',
attrs={ attrs={
'v_attach': 'top', 'v_attach': 'top',
@ -652,7 +652,7 @@ class MainMenuActivity(ba.Activity):
vr_depth_offset: float = 0.0, vr_depth_offset: float = 0.0,
shadow: bool = False) -> None: shadow: bool = False) -> None:
if shadow: if shadow:
word_obj = ba.Actor( word_obj = ba.NodeActor(
ba.newnode('text', ba.newnode('text',
attrs={ attrs={
'position': (x, y), 'position': (x, y),
@ -669,7 +669,7 @@ class MainMenuActivity(ba.Activity):
})) }))
self._word_actors.append(word_obj) self._word_actors.append(word_obj)
else: else:
word_obj = ba.Actor( word_obj = ba.NodeActor(
ba.newnode('text', ba.newnode('text',
attrs={ attrs={
'position': (x, y), 'position': (x, y),
@ -776,7 +776,7 @@ class MainMenuActivity(ba.Activity):
mopaque = (None if custom_texture is not None else ba.getmodel('logo')) mopaque = (None if custom_texture is not None else ba.getmodel('logo'))
mtrans = (None if custom_texture is not None else mtrans = (None if custom_texture is not None else
ba.getmodel('logoTransparent')) ba.getmodel('logoTransparent'))
logo = ba.Actor( logo = ba.NodeActor(
ba.newnode('image', ba.newnode('image',
attrs={ attrs={
'texture': ltex, 'texture': ltex,

View File

@ -180,7 +180,7 @@ class GatherWindow(ba.Window):
self._scroll_width = self._width - scroll_buffer_h self._scroll_width = self._width - scroll_buffer_h
self._scroll_height = self._height - 180.0 self._scroll_height = self._height - 180.0
# not actually using a scroll widget anymore; just an image # Not actually using a scroll widget anymore; just an image.
scroll_left = (self._width - self._scroll_width) * 0.5 scroll_left = (self._width - self._scroll_width) * 0.5
scroll_bottom = self._height - self._scroll_height - 79 - 48 scroll_bottom = self._height - self._scroll_height - 79 - 48
buffer_h = 10 buffer_h = 10
@ -214,8 +214,8 @@ class GatherWindow(ba.Window):
or _ba.get_account_type() != 'Google Play'): or _ba.get_account_type() != 'Google Play'):
account.show_sign_in_prompt('Google Play') account.show_sign_in_prompt('Google Play')
else: else:
# if there's google play people connected to us, inform the user # If there's google play people connected to us, inform the user
# that they will get disconnected.. otherwise just go ahead.. # that they will get disconnected. Otherwise just go ahead.
google_player_count = (_ba.get_google_play_party_client_count()) google_player_count = (_ba.get_google_play_party_client_count())
if google_player_count > 0: if google_player_count > 0:
confirm.ConfirmWindow( confirm.ConfirmWindow(
@ -246,25 +246,25 @@ class GatherWindow(ba.Window):
return return
self._current_tab = tab self._current_tab = tab
# we wanna preserve our current tab between runs # We wanna preserve our current tab between runs.
cfg = ba.app.config cfg = ba.app.config
cfg['Gather Tab'] = tab cfg['Gather Tab'] = tab
cfg.commit() cfg.commit()
# update tab colors based on which is selected # Update tab colors based on which is selected.
tabs.update_tab_button_colors(self._tab_buttons, tab) tabs.update_tab_button_colors(self._tab_buttons, tab)
# (re)create scroll widget # (Re)create scroll widget.
if self._tab_container: if self._tab_container:
self._tab_container.delete() self._tab_container.delete()
scroll_left = (self._width - self._scroll_width) * 0.5 scroll_left = (self._width - self._scroll_width) * 0.5
scroll_bottom = self._height - self._scroll_height - 79 - 48 scroll_bottom = self._height - self._scroll_height - 79 - 48
# a place where tabs can store data to get cleared when switching to # A place where tabs can store data to get cleared when switching to
# a different tab # a different tab.
self._tab_data = {} self._tab_data = {}
# so we can still select root level widgets with direction buttons # So we can still select root level widgets with direction buttons.
def _simple_message(tab2: str, def _simple_message(tab2: str,
message: ba.Lstr, message: ba.Lstr,
string_height: float, string_height: float,
@ -331,7 +331,7 @@ class GatherWindow(ba.Window):
('${BUTTON}', ('${BUTTON}',
ba.charstr(ba.SpecialChar.TOP_BUTTON))]) ba.charstr(ba.SpecialChar.TOP_BUTTON))])
# let's not talk about sharing in vr-mode; its tricky to fit more # Let's not talk about sharing in vr-mode; its tricky to fit more
# than one head in a VR-headset ;-) # than one head in a VR-headset ;-)
if not ba.app.vr_mode: if not ba.app.vr_mode:
msg = ba.Lstr( msg = ba.Lstr(
@ -472,8 +472,8 @@ class GatherWindow(ba.Window):
up_widget=self._tab_buttons[tab]) up_widget=self._tab_buttons[tab])
ba.widget(edit=self._internet_join_text, right_widget=txt) ba.widget(edit=self._internet_join_text, right_widget=txt)
# attempt to fetch our local address so we have it for # Attempt to fetch our local address so we have it for
# error messages # error messages.
self._internet_local_address = None self._internet_local_address = None
class AddrFetchThread(threading.Thread): class AddrFetchThread(threading.Thread):
@ -501,9 +501,9 @@ class GatherWindow(ba.Window):
ba.pushcall(ba.Call(self._call, val), ba.pushcall(ba.Call(self._call, val),
from_other_thread=True) from_other_thread=True)
except Exception: except Exception:
# FIXME: Should filter out expected errors and
# report others here.
ba.print_exception() ba.print_exception()
# FIXME: Should screen out expected errors and
# report others here.
AddrFetchThread(ba.WeakCall( AddrFetchThread(ba.WeakCall(
self._internet_fetch_local_addr_cb)).start() self._internet_fetch_local_addr_cb)).start()
@ -518,8 +518,8 @@ class GatherWindow(ba.Window):
timetype=ba.TimeType.REAL) timetype=ba.TimeType.REAL)
} }
# also update it immediately so we don't have to wait for the # Also update it immediately so we don't have to wait for the
# initial query.. # initial query.
self._update_internet_tab() self._update_internet_tab()
elif tab == 'local_network': elif tab == 'local_network':
@ -545,8 +545,8 @@ class GatherWindow(ba.Window):
ba.WeakCall(self.update), ba.WeakCall(self.update),
timetype=ba.TimeType.REAL, timetype=ba.TimeType.REAL,
repeat=True) repeat=True)
# go ahead and run a few *almost* immediately so we don't # Go ahead and run a few *almost* immediately so we don't
# have to wait a second # have to wait a second.
self.update() self.update()
ba.timer(0.25, ba.timer(0.25,
ba.WeakCall(self.update), ba.WeakCall(self.update),
@ -566,7 +566,8 @@ class GatherWindow(ba.Window):
t_scale = 1.6 t_scale = 1.6
for child in self._columnwidget.get_children(): for child in self._columnwidget.get_children():
child.delete() child.delete()
# grab this now this since adding widgets will change it
# Grab this now this since adding widgets will change it.
last_selected_host = self._last_selected_host last_selected_host = self._last_selected_host
hosts = _ba.host_scan_cycle() hosts = _ba.host_scan_cycle()
for i, host in enumerate(hosts): for i, host in enumerate(hosts):
@ -843,7 +844,7 @@ class GatherWindow(ba.Window):
color=(1, 0, 0)) color=(1, 0, 0))
ba.playsound(ba.getsound('error')) ba.playsound(ba.getsound('error'))
else: else:
# store for later # Store for later.
cfg2 = ba.app.config cfg2 = ba.app.config
cfg2['Last Manual Party Connect Address'] = addr2 cfg2['Last Manual Party Connect Address'] = addr2
cfg2.commit() cfg2.commit()
@ -866,7 +867,6 @@ class GatherWindow(ba.Window):
tscl = 0.85 tscl = 0.85
tspc = 25 tspc = 25
# v -= 35
def _safe_set_text(txt3: ba.Widget, def _safe_set_text(txt3: ba.Widget,
val: Union[str, ba.Lstr], val: Union[str, ba.Lstr],
success: bool = True) -> None: success: bool = True) -> None:
@ -875,11 +875,10 @@ class GatherWindow(ba.Window):
text=val, text=val,
color=(0, 1, 0) if success else (1, 1, 0)) color=(0, 1, 0) if success else (1, 1, 0))
# this currently doesn't work from china since we go through a # This currently doesn't work from china since we go through a
# reverse proxy there # reverse proxy there.
# EDIT - it should work now; our proxy server forwards along # UPDATE: it should work now; our proxy server forwards along
# original IPs # original IPs.
# app = ba.app
do_internet_check = True do_internet_check = True
def do_it(v2: float, cnt2: Optional[ba.Widget]) -> None: def do_it(v2: float, cnt2: Optional[ba.Widget]) -> None:
@ -940,6 +939,7 @@ class GatherWindow(ba.Window):
from_other_thread=True) from_other_thread=True)
except Exception as exc: except Exception as exc:
err_str = str(exc) err_str = str(exc)
# FIXME: Should look at exception types here, # FIXME: Should look at exception types here,
# not strings. # not strings.
if 'Network is unreachable' in err_str: if 'Network is unreachable' in err_str:
@ -1024,14 +1024,15 @@ class GatherWindow(ba.Window):
text='') text='')
self._doing_access_check = False self._doing_access_check = False
self._access_check_count = 0 # cap our refreshes eventually.. self._access_check_count = 0 # Cap our refreshes eventually.
self._tab_data['access_check_timer'] = ba.Timer( self._tab_data['access_check_timer'] = ba.Timer(
10.0, 10.0,
ba.WeakCall(self._access_check_update, t_addr, ba.WeakCall(self._access_check_update, t_addr,
t_accessible, t_accessible_extra), t_accessible, t_accessible_extra),
repeat=True, repeat=True,
timetype=ba.TimeType.REAL) timetype=ba.TimeType.REAL)
# kick initial off
# Kick initial off.
self._access_check_update(t_addr, t_accessible, self._access_check_update(t_addr, t_accessible,
t_accessible_extra) t_accessible_extra)
if check_button: if check_button:
@ -1061,7 +1062,7 @@ class GatherWindow(ba.Window):
if playsound: if playsound:
ba.playsound(ba.getsound('click01')) ba.playsound(ba.getsound('click01'))
# if we're switching in from elsewhere, reset our selection # If we're switching in from elsewhere, reset our selection.
# (prevents selecting something way down the list if we switched away # (prevents selecting something way down the list if we switched away
# and came back) # and came back)
if self._internet_tab != value: if self._internet_tab != value:
@ -1077,7 +1078,7 @@ class GatherWindow(ba.Window):
edit=self._internet_host_text, edit=self._internet_host_text,
color=active_color if value == 'host' else inactive_color) color=active_color if value == 'host' else inactive_color)
# clear anything in existence.. # Clear anything in existence.
for widget in [ for widget in [
self._internet_host_scrollwidget, self._internet_host_scrollwidget,
self._internet_host_name_text, self._internet_host_name_text,
@ -1095,7 +1096,6 @@ class GatherWindow(ba.Window):
self._internet_join_status_text, self._internet_join_status_text,
self._internet_host_dedicated_server_info_text self._internet_host_dedicated_server_info_text
]: ]:
# widget = getattr(self, attr, None)
if widget is not None: if widget is not None:
widget.delete() widget.delete()
@ -1107,9 +1107,10 @@ class GatherWindow(ba.Window):
v -= 25 v -= 25
is_public_enabled = _ba.get_public_party_enabled() is_public_enabled = _ba.get_public_party_enabled()
if value == 'join': if value == 'join':
# reset this so we do an immediate refresh query # Reset this so we do an immediate refresh query.
self._internet_join_last_refresh_time = -99999.0 self._internet_join_last_refresh_time = -99999.0
# reset our list of public parties
# Reset our list of public parties.
self._public_parties = {} self._public_parties = {}
self._last_public_party_list_rebuild_time = 0 self._last_public_party_list_rebuild_time = 0
self._first_public_party_list_rebuild_time = None self._first_public_party_list_rebuild_time = None
@ -1125,8 +1126,7 @@ class GatherWindow(ba.Window):
shadow=0.0, shadow=0.0,
h_align='center', h_align='center',
v_align='center') v_align='center')
# noinspection PyUnreachableCode if bool(False):
if False: # pylint: disable=using-constant-test
self._internet_join_party_language_label = ba.textwidget( self._internet_join_party_language_label = ba.textwidget(
text=ba.Lstr( text=ba.Lstr(
resource='settingsWindowAdvanced.languageText'), resource='settingsWindowAdvanced.languageText'),
@ -1192,8 +1192,6 @@ class GatherWindow(ba.Window):
color=(0.6, 0.6, 0.6), color=(0.6, 0.6, 0.6),
position=(c_width * 0.5, c_height * 0.5)) position=(c_width * 0.5, c_height * 0.5))
# t_scale = 1.6
if value == 'host': if value == 'host':
v -= 30 v -= 30
party_name_text = ba.Lstr( party_name_text = ba.Lstr(
@ -1313,15 +1311,15 @@ class GatherWindow(ba.Window):
color=ba.app.infotextcolor, color=ba.app.infotextcolor,
position=(c_width * 0.5, v)) position=(c_width * 0.5, v))
# if public sharing is already on, # If public sharing is already on,
# launch a status-check immediately # launch a status-check immediately.
if _ba.get_public_party_enabled(): if _ba.get_public_party_enabled():
self._do_internet_status_check() self._do_internet_status_check()
# now add a lock icon overlay for if we don't have pro # Now add a lock icon overlay for if we don't have pro.
icon = self._internet_lock_icon icon = self._internet_lock_icon
if icon and self._internet_lock_icon: if icon and self._internet_lock_icon:
self._internet_lock_icon.delete() # kill any existing self._internet_lock_icon.delete() # Kill any existing.
self._internet_lock_icon = ba.imagewidget( self._internet_lock_icon = ba.imagewidget(
parent=self._tab_container, parent=self._tab_container,
position=(c_width * 0.5 - 60, c_height * 0.5 - 50), position=(c_width * 0.5 - 60, c_height * 0.5 - 50),
@ -1354,11 +1352,11 @@ class GatherWindow(ba.Window):
def _on_public_party_query_result( def _on_public_party_query_result(
self, result: Optional[Dict[str, Any]]) -> None: self, result: Optional[Dict[str, Any]]) -> None:
with ba.Context('ui'): with ba.Context('ui'):
# any time we get any result at all, kill our loading status # Any time we get any result at all, kill our loading status.
status_text = self._internet_join_status_text status_text = self._internet_join_status_text
if status_text: if status_text:
# don't show results if not signed in (probably didn't get any # Don't show results if not signed in
# anyway) # (probably didn't get any anyway).
if _ba.get_account_state() != 'signed_in': if _ba.get_account_state() != 'signed_in':
ba.textwidget(edit=status_text, ba.textwidget(edit=status_text,
text=ba.Lstr(resource='notSignedInText')) text=ba.Lstr(resource='notSignedInText'))
@ -1378,11 +1376,11 @@ class GatherWindow(ba.Window):
partyval['claimed'] = False partyval['claimed'] = False
for party_in in parties_in: for party_in in parties_in:
# party is indexed by (ADDR)_(PORT) # Party is indexed by (ADDR)_(PORT)
party_key = party_in['a'] + '_' + str(party_in['p']) party_key = party_in['a'] + '_' + str(party_in['p'])
party = self._public_parties.get(party_key) party = self._public_parties.get(party_key)
if party is None: if party is None:
# if this party is new to us, init it.. # If this party is new to us, init it.
index = self._next_public_party_entry_index index = self._next_public_party_entry_index
self._next_public_party_entry_index = index + 1 self._next_public_party_entry_index = index + 1
party = self._public_parties[party_key] = { party = self._public_parties[party_key] = {
@ -1395,7 +1393,8 @@ class GatherWindow(ba.Window):
'index': 'index':
index, index,
} }
# now, new or not, update its values
# Now, new or not, update its values.
party['queue'] = party_in.get('q') party['queue'] = party_in.get('q')
party['port'] = party_in.get('p') party['port'] = party_in.get('p')
party['name'] = party_in['n'] party['name'] = party_in['n']
@ -1407,7 +1406,7 @@ class GatherWindow(ba.Window):
party['ping_interval'] = 0.001 * party_in['pi'] party['ping_interval'] = 0.001 * party_in['pi']
party['stats_addr'] = party_in['sa'] party['stats_addr'] = party_in['sa']
# prune unclaimed party entries # Prune unclaimed party entries.
self._public_parties = { self._public_parties = {
key: val key: val
for key, val in list(self._public_parties.items()) for key, val in list(self._public_parties.items())
@ -1423,8 +1422,9 @@ class GatherWindow(ba.Window):
cur_time = ba.time(ba.TimeType.REAL) cur_time = ba.time(ba.TimeType.REAL)
if self._first_public_party_list_rebuild_time is None: if self._first_public_party_list_rebuild_time is None:
self._first_public_party_list_rebuild_time = cur_time self._first_public_party_list_rebuild_time = cur_time
# update faster for the first few seconds;
# then ease off to keep the list from jumping around # Update faster for the first few seconds;
# then ease off to keep the list from jumping around.
since_first = cur_time - self._first_public_party_list_rebuild_time since_first = cur_time - self._first_public_party_list_rebuild_time
wait_time = (1.0 if since_first < 2.0 else wait_time = (1.0 if since_first < 2.0 else
2.5 if since_first < 10.0 else 5.0) 2.5 if since_first < 10.0 else 5.0)
@ -1433,23 +1433,23 @@ class GatherWindow(ba.Window):
return return
self._last_public_party_list_rebuild_time = cur_time self._last_public_party_list_rebuild_time = cur_time
# first off, check for the existence of our column widget; # First off, check for the existence of our column widget;
# if we don't have this, we're done # if we don't have this, we're done.
columnwidget = self._internet_host_columnwidget columnwidget = self._internet_host_columnwidget
if not columnwidget: if not columnwidget:
return return
with ba.Context('ui'): with ba.Context('ui'):
# now kill and recreate all widgets # Now kill and recreate all widgets.
for widget in columnwidget.get_children(): for widget in columnwidget.get_children():
widget.delete() widget.delete()
# sort - show queue-enabled ones first and sort by lowest ping # Sort - show queue-enabled ones first and sort by lowest ping.
ordered_parties = sorted( ordered_parties = sorted(
list(self._public_parties.values()), list(self._public_parties.values()),
key=lambda p: ( key=lambda p: (
p['queue'] is None, # show non-queued last p['queue'] is None, # Show non-queued last.
p['ping'] if p['ping'] is not None else 999999, p['ping'] if p['ping'] is not None else 999999,
p['index'], p['index'],
p)) p))
@ -1457,22 +1457,19 @@ class GatherWindow(ba.Window):
first = True first = True
sub_scroll_width = 830 sub_scroll_width = 830
# rval = random.randrange(4, 10)
# print 'doing', rval
# ordered_parties = ordered_parties[:rval]
lineheight = 42 lineheight = 42
sub_scroll_height = lineheight * len(ordered_parties) + 50 sub_scroll_height = lineheight * len(ordered_parties) + 50
ba.containerwidget(edit=columnwidget, ba.containerwidget(edit=columnwidget,
size=(sub_scroll_width, sub_scroll_height)) size=(sub_scroll_width, sub_scroll_height))
# ew; this rebuilding generates deferred selection callbacks # Ew; this rebuilding generates deferred selection callbacks
# so we need to generated deferred ignore notices for ourself # so we need to generated deferred ignore notices for ourself.
def refresh_on() -> None: def refresh_on() -> None:
self._refreshing_public_party_list = True self._refreshing_public_party_list = True
ba.pushcall(refresh_on) ba.pushcall(refresh_on)
# janky - allow escaping if there's nothing in us # Janky - allow escaping if there's nothing in us.
ba.containerwidget(edit=self._internet_host_scrollwidget, ba.containerwidget(edit=self._internet_host_scrollwidget,
claims_up_down=(len(ordered_parties) > 0)) claims_up_down=(len(ordered_parties) > 0))
@ -1504,8 +1501,7 @@ class GatherWindow(ba.Window):
if existing_selection == (party['address'], 'name'): if existing_selection == (party['address'], 'name'):
ba.containerwidget(edit=columnwidget, ba.containerwidget(edit=columnwidget,
selected_child=party['name_widget']) selected_child=party['name_widget'])
# noinspection PyUnreachableCode if bool(False):
if False: # pylint: disable=using-constant-test
party['language_widget'] = ba.textwidget( party['language_widget'] = ba.textwidget(
text=ba.Lstr(translate=('languages', text=ba.Lstr(translate=('languages',
party['language'])), party['language'])),
@ -1621,20 +1617,20 @@ class GatherWindow(ba.Window):
def _update_internet_tab(self) -> None: def _update_internet_tab(self) -> None:
# pylint: disable=too-many-statements # pylint: disable=too-many-statements
# special case - if a party-queue window is up, don't do any of this # Special case: if a party-queue window is up, don't do any of this
# (keeps things smoother) # (keeps things smoother).
if ba.app.have_party_queue_window: if ba.app.have_party_queue_window:
return return
# if we've got a party-name text widget, keep its value plugged # If we've got a party-name text widget, keep its value plugged
# into our public host name... # into our public host name.
text = self._internet_host_name_text text = self._internet_host_name_text
if text: if text:
name = cast(str, name = cast(str,
ba.textwidget(query=self._internet_host_name_text)) ba.textwidget(query=self._internet_host_name_text))
_ba.set_public_party_name(name) _ba.set_public_party_name(name)
# show/hide the lock icon depending on if we've got pro # Show/hide the lock icon depending on if we've got pro.
icon = self._internet_lock_icon icon = self._internet_lock_icon
if icon: if icon:
if self._is_internet_locked(): if self._is_internet_locked():
@ -1657,17 +1653,17 @@ class GatherWindow(ba.Window):
callback=ba.WeakCall(self._on_public_party_query_result)) callback=ba.WeakCall(self._on_public_party_query_result))
_ba.run_transactions() _ba.run_transactions()
# go through our existing public party entries firing off pings # Go through our existing public party entries firing off pings
# for any that have timed out # for any that have timed out.
for party in list(self._public_parties.values()): for party in list(self._public_parties.values()):
if (party['next_ping_time'] <= now if (party['next_ping_time'] <= now
and ba.app.ping_thread_count < 15): and ba.app.ping_thread_count < 15):
# make sure to fully catch up and not to multi-ping if # Make sure to fully catch up and not to multi-ping if
# we're way behind somehow.. # we're way behind somehow.
while party['next_ping_time'] <= now: while party['next_ping_time'] <= now:
# crank the interval up for high-latency parties to # Crank the interval up for high-latency parties to
# save us some work # save us some work.
mult = 1 mult = 1
if party['ping'] is not None: if party['ping'] is not None:
mult = (10 if party['ping'] > 300 else mult = (10 if party['ping'] > 300 else
@ -1682,8 +1678,6 @@ class GatherWindow(ba.Window):
call: Callable[[str, int, Optional[int]], call: Callable[[str, int, Optional[int]],
Optional[int]]): Optional[int]]):
super().__init__() super().__init__()
# need utf8 here to avoid an error on our minimum
# bundled python
self._address = address self._address = address
self._port = port self._port = port
self._call = call self._call = call
@ -1701,11 +1695,9 @@ class GatherWindow(ba.Window):
accessible = False accessible = False
starttime = time.time() starttime = time.time()
# send a simple ping and wait for a response;
# if we get it, they're accessible...
# send a few pings and wait a second for # Send a few pings and wait a second for
# a response # a response.
sock.settimeout(1) sock.settimeout(1)
for _i in range(3): for _i in range(3):
sock.send(b'\x0b') sock.send(b'\x0b')
@ -1728,12 +1720,25 @@ class GatherWindow(ba.Window):
from_other_thread=True) from_other_thread=True)
except OSError as exc: except OSError as exc:
import errno import errno
# ignore harmless errors
if exc.errno != errno.EHOSTUNREACH: # Ignore harmless errors.
ba.print_exception('error on ping', if exc.errno == errno.EHOSTUNREACH:
pass
elif exc.errno == errno.EADDRNOTAVAIL:
if self._port == 0:
# This has happened. Ignore.
pass
elif ba.do_once():
print(
f'Got EADDRNOTAVAIL on gather ping'
f' for addr {self._address}'
f' port {self._port}.')
else:
ba.print_exception('Error on gather ping.',
once=True) once=True)
except Exception: except Exception:
ba.print_exception('error on ping', once=True) ba.print_exception('Error on gather ping',
once=True)
ba.app.ping_thread_count -= 1 ba.app.ping_thread_count -= 1
PingThread(party['address'], party['port'], PingThread(party['address'], party['port'],
@ -1741,8 +1746,8 @@ class GatherWindow(ba.Window):
def _ping_callback(self, address: str, port: Optional[int], def _ping_callback(self, address: str, port: Optional[int],
result: Optional[int]) -> None: result: Optional[int]) -> None:
# Look for a widget corresponding to this target; if we find one, # Look for a widget corresponding to this target.
# update our list. # If we find one, update our list.
party = self._public_parties.get(address + '_' + str(port)) party = self._public_parties.get(address + '_' + str(port))
if party is not None: if party is not None:
# We now smooth ping a bit to reduce jumping around in the list # We now smooth ping a bit to reduce jumping around in the list
@ -1755,9 +1760,11 @@ class GatherWindow(ba.Window):
(1.0 - smoothing) * result) (1.0 - smoothing) * result)
else: else:
party['ping'] = result party['ping'] = result
if 'ping_widget' not in party:
pass # This can happen if we switch away and then back to the # This can happen if we switch away and then back to the
# client tab while pings are in flight. # client tab while pings are in flight.
if 'ping_widget' not in party:
pass
elif party['ping_widget']: elif party['ping_widget']:
self._rebuild_public_party_list() self._rebuild_public_party_list()
@ -1946,7 +1953,7 @@ class GatherWindow(ba.Window):
ba.app.window_states[self.__class__.__name__] = { ba.app.window_states[self.__class__.__name__] = {
'sel_name': sel_name, 'sel_name': sel_name,
'tab': self._current_tab, 'tab': self._current_tab,
'internetTab': self._internet_tab 'internet_tab': self._internet_tab
} }
except Exception: except Exception:
ba.print_exception('error saving state for', self.__class__) ba.print_exception('error saving state for', self.__class__)
@ -1955,7 +1962,7 @@ class GatherWindow(ba.Window):
try: try:
winstate = ba.app.window_states.get(self.__class__.__name__, {}) winstate = ba.app.window_states.get(self.__class__.__name__, {})
sel_name = winstate.get('sel_name', None) sel_name = winstate.get('sel_name', None)
self._internet_tab = winstate.get('internetTab', 'join') self._internet_tab = winstate.get('internet_tab', 'join')
current_tab = ba.app.config.get('Gather Tab', None) current_tab = ba.app.config.get('Gather Tab', None)
if current_tab is None or current_tab not in self._tab_buttons: if current_tab is None or current_tab not in self._tab_buttons:
current_tab = 'about' current_tab = 'about'

View File

@ -75,7 +75,7 @@ class CallbackSet(Generic[CT]):
# passed. # passed.
# To use this, simply assign your call type to this Call for type checking: # To use this, simply assign your call type to this Call for type checking:
# example: # Example:
# class _MyCallWrapper: # class _MyCallWrapper:
# <runtime class defined here> # <runtime class defined here>
# if TYPE_CHECKING: # if TYPE_CHECKING:

View File

@ -17,6 +17,7 @@
<li><a href="#class_ba_Actor">ba.Actor</a></li> <li><a href="#class_ba_Actor">ba.Actor</a></li>
<ul> <ul>
<li><a href="#class_ba_Map">ba.Map</a></li> <li><a href="#class_ba_Map">ba.Map</a></li>
<li><a href="#class_ba_NodeActor">ba.NodeActor</a></li>
</ul> </ul>
<li><a href="#class_ba_Chooser">ba.Chooser</a></li> <li><a href="#class_ba_Chooser">ba.Chooser</a></li>
<li><a href="#class_ba_InputDevice">ba.InputDevice</a></li> <li><a href="#class_ba_InputDevice">ba.InputDevice</a></li>
@ -537,21 +538,22 @@ is a convenient way to access this same functionality.</p>
<p>Category: <a href="#class_category_Gameplay_Classes">Gameplay Classes</a></p> <p>Category: <a href="#class_category_Gameplay_Classes">Gameplay Classes</a></p>
<p> Actors act as controllers, combining some number of <a href="#class_ba_Node">ba.Nodes</a>, <p> Actors act as controllers, combining some number of <a href="#class_ba_Node">ba.Nodes</a>,
<a href="#class_ba_Texture">ba.Textures</a>, <a href="#class_ba_Sound">ba.Sounds</a>, etc. into one cohesive unit.</p> <a href="#class_ba_Texture">ba.Textures</a>, <a href="#class_ba_Sound">ba.Sounds</a>, etc. into a high-level cohesive unit.</p>
<p> Some example actors include ba.Bomb, ba.Flag, and ba.Spaz.</p> <p> Some example actors include Bomb, Flag, and Spaz classes in bastd.</p>
<p> One key feature of Actors is that they generally 'die' <p> One key feature of Actors is that they generally 'die'
(killing off or transitioning out their nodes) when the last Python (killing off or transitioning out their nodes) when the last Python
reference to them disappears, so you can use logic such as:</p> reference to them disappears, so you can use logic such as:</p>
<pre><span><em><small> # create a flag Actor in our game activity</small></em></span> <pre><span><em><small> # Create a flag Actor in our game activity:</small></em></span>
self.flag = ba.Flag(position=(0, 10, 0))</pre> from bastd.actor.flag import Flag
self.flag = Flag(position=(0, 10, 0))</pre>
<pre><span><em><small> # later, destroy the flag..</small></em></span> <pre><span><em><small> # Later, destroy the flag.</small></em></span>
<span><em><small> # (provided nothing else is holding a reference to it)</small></em></span> <span><em><small> # (provided nothing else is holding a reference to it)</small></em></span>
<span><em><small> # we could also just assign a new flag to this value.</small></em></span> <span><em><small> # We could also just assign a new flag to this value.</small></em></span>
<span><em><small> # either way, the old flag disappears.</small></em></span> <span><em><small> # Either way, the old flag disappears.</small></em></span>
self.flag = None</pre> self.flag = None</pre>
<p> This is in contrast to the behavior of the more low level <a href="#class_ba_Node">ba.Nodes</a>, <p> This is in contrast to the behavior of the more low level <a href="#class_ba_Node">ba.Nodes</a>,
@ -566,13 +568,13 @@ is a convenient way to access this same functionality.</p>
takes a single arbitrary object as an argument. This provides a safe way takes a single arbitrary object as an argument. This provides a safe way
to communicate between <a href="#class_ba_Actor">ba.Actor</a>, <a href="#class_ba_Activity">ba.Activity</a>, <a href="#class_ba_Session">ba.Session</a>, and any other to communicate between <a href="#class_ba_Actor">ba.Actor</a>, <a href="#class_ba_Activity">ba.Activity</a>, <a href="#class_ba_Session">ba.Session</a>, and any other
class providing a handlemessage() method. The most universally handled class providing a handlemessage() method. The most universally handled
message type for actors is the <a href="#class_ba_DieMessage">ba.DieMessage</a>.</p> message type for Actors is the <a href="#class_ba_DieMessage">ba.DieMessage</a>.</p>
<pre><span><em><small> # another way to kill the flag from the example above:</small></em></span> <pre><span><em><small> # Another way to kill the flag from the example above:</small></em></span>
<span><em><small> # we can safely call this on any type with a 'handlemessage' method</small></em></span> <span><em><small> # We can safely call this on any type with a 'handlemessage' method</small></em></span>
<span><em><small> # (though its not guaranteed to always have a meaningful effect)</small></em></span> <span><em><small> # (though its not guaranteed to always have a meaningful effect).</small></em></span>
<span><em><small> # in this case the Actor instance will still be around, but its exists()</small></em></span> <span><em><small> # In this case the Actor instance will still be around, but its exists()</small></em></span>
<span><em><small> # and is_alive() methods will both return False</small></em></span> <span><em><small> # and is_alive() methods will both return False.</small></em></span>
self.flag.handlemessage(<a href="#class_ba_DieMessage">ba.DieMessage</a>()) self.flag.handlemessage(<a href="#class_ba_DieMessage">ba.DieMessage</a>())
</pre> </pre>
@ -590,15 +592,10 @@ is a convenient way to access this same functionality.</p>
<h5><a href="#method_ba_Actor____init__">&lt;constructor&gt;</a>, <a href="#method_ba_Actor__autoretain">autoretain()</a>, <a href="#method_ba_Actor__exists">exists()</a>, <a href="#method_ba_Actor__getactivity">getactivity()</a>, <a href="#method_ba_Actor__handlemessage">handlemessage()</a>, <a href="#method_ba_Actor__is_alive">is_alive()</a>, <a href="#method_ba_Actor__is_expired">is_expired()</a>, <a href="#method_ba_Actor__on_expire">on_expire()</a></h5> <h5><a href="#method_ba_Actor____init__">&lt;constructor&gt;</a>, <a href="#method_ba_Actor__autoretain">autoretain()</a>, <a href="#method_ba_Actor__exists">exists()</a>, <a href="#method_ba_Actor__getactivity">getactivity()</a>, <a href="#method_ba_Actor__handlemessage">handlemessage()</a>, <a href="#method_ba_Actor__is_alive">is_alive()</a>, <a href="#method_ba_Actor__is_expired">is_expired()</a>, <a href="#method_ba_Actor__on_expire">on_expire()</a></h5>
<dl> <dl>
<dt><h4><a name="method_ba_Actor____init__">&lt;constructor&gt;</a></dt></h4><dd> <dt><h4><a name="method_ba_Actor____init__">&lt;constructor&gt;</a></dt></h4><dd>
<p><span>ba.Actor(node: <a href="#class_ba_Node">ba.Node</a> = None)</span></p> <p><span>ba.Actor()</span></p>
<p>Instantiates an Actor in the current <a href="#class_ba_Activity">ba.Activity</a>.</p> <p>Instantiates an Actor in the current <a href="#class_ba_Activity">ba.Activity</a>.</p>
<p>If 'node' is provided, it is stored as the 'node' attribute
and the default <a href="#method_ba_Actor__handlemessage">ba.Actor.handlemessage</a>() and <a href="#method_ba_Actor__exists">ba.Actor.exists</a>()
implementations will apply to it. This allows the creation of
simple node-wrapping Actors without having to create a new subclass.</p>
</dd> </dd>
<dt><h4><a name="method_ba_Actor__autoretain">autoretain()</a></dt></h4><dd> <dt><h4><a name="method_ba_Actor__autoretain">autoretain()</a></dt></h4><dd>
<p><span>autoretain(self: T) -&gt; T</span></p> <p><span>autoretain(self: T) -&gt; T</span></p>
@ -627,8 +624,7 @@ their corpse is visible; this is about presence, not being 'alive'
deleted without affecting the game; this call is often used deleted without affecting the game; this call is often used
when pruning lists of Actors, such as with <a href="#method_ba_Actor__autoretain">ba.Actor.autoretain</a>()</p> when pruning lists of Actors, such as with <a href="#method_ba_Actor__autoretain">ba.Actor.autoretain</a>()</p>
<p>The default implementation of this method returns 'node.exists()' <p>The default implementation of this method always return True.</p>
if the Actor has a 'node' attr; otherwise True.</p>
<p>Note that the boolean operator for the Actor class calls this method, <p>Note that the boolean operator for the Actor class calls this method,
so a simple "if myactor" test will conveniently do the right thing so a simple "if myactor" test will conveniently do the right thing
@ -1106,7 +1102,7 @@ when done.</p>
<p>Instantiate a Call; pass a callable as the first <p>Instantiate a Call; pass a callable as the first
arg, followed by any number of arguments or keywords.</p> arg, followed by any number of arguments or keywords.</p>
<pre><span><em><small># Example: wrap a method call with 1 positional and 1 keyword arg.</small></em></span> <pre><span><em><small># Example: wrap a method call with 1 positional and 1 keyword arg:</small></em></span>
mycall = ba.Call(myobj.dostuff, argval1, namedarg=argval2)</pre> mycall = ba.Call(myobj.dostuff, argval1, namedarg=argval2)</pre>
<pre><span><em><small># Now we have a single callable to run that whole mess.</small></em></span> <pre><span><em><small># Now we have a single callable to run that whole mess.</small></em></span>
@ -1345,8 +1341,8 @@ Usage:</strong></p>
sets the context as current on entry and resets it to the previous sets the context as current on entry and resets it to the previous
value on exit.</p> value on exit.</p>
<pre><span><em><small># example: load a few textures into the UI context</small></em></span> <pre><span><em><small># Example: load a few textures into the UI context</small></em></span>
<span><em><small># (for use in widgets, etc)</small></em></span> <span><em><small># (for use in widgets, etc):</small></em></span>
with <a href="#class_ba_Context">ba.Context</a>('ui'): with <a href="#class_ba_Context">ba.Context</a>('ui'):
tex1 = <a href="#function_ba_gettexture">ba.gettexture</a>('foo_tex_1') tex1 = <a href="#function_ba_gettexture">ba.gettexture</a>('foo_tex_1')
tex2 = <a href="#function_ba_gettexture">ba.gettexture</a>('foo_tex_2')</pre> tex2 = <a href="#function_ba_gettexture">ba.gettexture</a>('foo_tex_2')</pre>
@ -3250,7 +3246,7 @@ the two nodes exist. The connection can be severed by setting the
target attribute to any value or connecting another node attribute target attribute to any value or connecting another node attribute
to it.</p> to it.</p>
<pre><span><em><small># example: create a locator and attach a light to it</small></em></span> <pre><span><em><small># Example: create a locator and attach a light to it:</small></em></span>
light = <a href="#function_ba_newnode">ba.newnode</a>('light') light = <a href="#function_ba_newnode">ba.newnode</a>('light')
loc = <a href="#function_ba_newnode">ba.newnode</a>('locator', attrs={'position': (0,10,0)}) loc = <a href="#function_ba_newnode">ba.newnode</a>('locator', attrs={'position': (0,10,0)})
loc.connectattr('position', light, 'position')</pre> loc.connectattr('position', light, 'position')</pre>
@ -3311,6 +3307,48 @@ Node-messages communicate directly with the low-level node layer
and are delivered simultaneously on all game clients, and are delivered simultaneously on all game clients,
acting as an alternative to setting node attributes.</p> acting as an alternative to setting node attributes.</p>
</dd>
</dl>
<hr>
<h2><strong><a name="class_ba_NodeActor">ba.NodeActor</a></strong></h3>
<p>inherits from: <a href="#class_ba_Actor">ba.Actor</a></p>
<p>A simple <a href="#class_ba_Actor">ba.Actor</a> type that wraps a single <a href="#class_ba_Node">ba.Node</a>.</p>
<p>Category: <a href="#class_category_Gameplay_Classes">Gameplay Classes</a></p>
<p> This Actor will delete its Node when told to die, and it's
exists() call will return whether the Node still exists or not.
</p>
<h3>Attributes:</h3>
<dl>
<dt><h4><a name="attr_ba_NodeActor__activity">activity</a></h4></dt><dd>
<p><span><a href="#class_ba_Activity">ba.Activity</a></span></p>
<p>The Activity this Actor was created in.</p>
<p> Raises a <a href="#class_ba_ActivityNotFoundError">ba.ActivityNotFoundError</a> if the Activity no longer exists.</p>
</dd>
</dl>
<h3>Methods Inherited:</h3>
<h5><a href="#method_ba_Actor__autoretain">autoretain()</a>, <a href="#method_ba_Actor__exists">exists()</a>, <a href="#method_ba_Actor__getactivity">getactivity()</a>, <a href="#method_ba_Actor__is_alive">is_alive()</a>, <a href="#method_ba_Actor__is_expired">is_expired()</a>, <a href="#method_ba_Actor__on_expire">on_expire()</a></h5>
<h3>Methods Defined or Overridden:</h3>
<h5><a href="#method_ba_NodeActor____init__">&lt;constructor&gt;</a>, <a href="#method_ba_NodeActor__handlemessage">handlemessage()</a></h5>
<dl>
<dt><h4><a name="method_ba_NodeActor____init__">&lt;constructor&gt;</a></dt></h4><dd>
<p><span>ba.NodeActor(node: <a href="#class_ba_Node">ba.Node</a>)</span></p>
<p>Instantiates an Actor in the current <a href="#class_ba_Activity">ba.Activity</a>.</p>
</dd>
<dt><h4><a name="method_ba_NodeActor__handlemessage">handlemessage()</a></dt></h4><dd>
<p><span>handlemessage(self, msg: Any) -&gt; Any</span></p>
<p>General message handling; can be passed any <a href="#class_category_Message_Classes">message object</a>.</p>
<p>The default implementation will handle <a href="#class_ba_DieMessage">ba.DieMessages</a> by
calling self.node.delete() if self contains a 'node' attribute.</p>
</dd> </dd>
</dl> </dl>
<hr> <hr>
@ -4652,7 +4690,7 @@ Real time timers are currently only available in the UI context.</p>
<p>the 'timeformat' arg defaults to SECONDS but can also be MILLISECONDS <p>the 'timeformat' arg defaults to SECONDS but can also be MILLISECONDS
if you want to pass time as milliseconds.</p> if you want to pass time as milliseconds.</p>
<pre><span><em><small># example: use a Timer object to print repeatedly for a few seconds:</small></em></span> <pre><span><em><small># Example: use a Timer object to print repeatedly for a few seconds:</small></em></span>
def say_it(): def say_it():
<a href="#function_ba_screenmessage">ba.screenmessage</a>('BADGER!') <a href="#function_ba_screenmessage">ba.screenmessage</a>('BADGER!')
def stop_saying_it(): def stop_saying_it():
@ -4817,7 +4855,7 @@ self.t = <a href="#class_ba_Timer">ba.Timer</a>(0.3, say_it, repeat=True)
<p>Instantiate a WeakCall; pass a callable as the first <p>Instantiate a WeakCall; pass a callable as the first
arg, followed by any number of arguments or keywords.</p> arg, followed by any number of arguments or keywords.</p>
<pre><span><em><small># example: wrap a method call with some positional and</small></em></span> <pre><span><em><small># Example: wrap a method call with some positional and</small></em></span>
<span><em><small># keyword args:</small></em></span> <span><em><small># keyword args:</small></em></span>
myweakcall = ba.WeakCall(myobj.dostuff, argval1, namedarg=argval2)</pre> myweakcall = ba.WeakCall(myobj.dostuff, argval1, namedarg=argval2)</pre>
@ -5149,6 +5187,11 @@ logs. The call functions by registering the filename and line where
The call is made from. Returns True if this location has not been The call is made from. Returns True if this location has not been
registered already, and False if it has.</p> registered already, and False if it has.</p>
<pre><span><em><small># Example: this print will only fire for the first loop iteration:</small></em></span>
for i in range(10):
if <a href="#function_ba_do_once">ba.do_once</a>():
print('Hello once from loop!')</pre>
<hr> <hr>
<h2><strong><a name="function_ba_emitfx">ba.emitfx()</a></strong></h3> <h2><strong><a name="function_ba_emitfx">ba.emitfx()</a></strong></h3>
<p><span>emitfx(position: Sequence[float], <p><span>emitfx(position: Sequence[float],