mirror of
https://github.com/hypervortex/VH-Bombsquad-Modded-Server-Files
synced 2025-10-16 12:02:51 +00:00
1080 lines
39 KiB
Python
1080 lines
39 KiB
Python
|
|
# Released under the MIT License. See LICENSE for details.
|
||
|
|
#
|
||
|
|
"""Implements lobby system for gathering before games, char select, etc."""
|
||
|
|
# pylint: disable=too-many-lines
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import weakref
|
||
|
|
from dataclasses import dataclass
|
||
|
|
from typing import TYPE_CHECKING
|
||
|
|
|
||
|
|
import _ba
|
||
|
|
from ba._error import print_exception, print_error, NotFoundError
|
||
|
|
from ba._gameutils import animate, animate_array
|
||
|
|
from ba._language import Lstr
|
||
|
|
from ba._generated.enums import SpecialChar, InputType
|
||
|
|
from ba._profile import get_player_profile_colors
|
||
|
|
|
||
|
|
if TYPE_CHECKING:
|
||
|
|
from typing import Any, Sequence
|
||
|
|
import ba
|
||
|
|
|
||
|
|
MAX_QUICK_CHANGE_COUNT = 30
|
||
|
|
QUICK_CHANGE_INTERVAL = 0.05
|
||
|
|
QUICK_CHANGE_RESET_INTERVAL = 1.0
|
||
|
|
|
||
|
|
|
||
|
|
# Hmm should we move this to actors?..
|
||
|
|
class JoinInfo:
|
||
|
|
"""Display useful info for joiners."""
|
||
|
|
|
||
|
|
def __init__(self, lobby: ba.Lobby):
|
||
|
|
from ba._nodeactor import NodeActor
|
||
|
|
from ba._general import WeakCall
|
||
|
|
|
||
|
|
self._state = 0
|
||
|
|
self._press_to_punch: str | ba.Lstr = (
|
||
|
|
'C'
|
||
|
|
if _ba.app.iircade_mode
|
||
|
|
else _ba.charstr(SpecialChar.LEFT_BUTTON)
|
||
|
|
)
|
||
|
|
self._press_to_bomb: str | ba.Lstr = (
|
||
|
|
'B'
|
||
|
|
if _ba.app.iircade_mode
|
||
|
|
else _ba.charstr(SpecialChar.RIGHT_BUTTON)
|
||
|
|
)
|
||
|
|
self._joinmsg = Lstr(resource='pressAnyButtonToJoinText')
|
||
|
|
can_switch_teams = len(lobby.sessionteams) > 1
|
||
|
|
|
||
|
|
# If we have a keyboard, grab keys for punch and pickup.
|
||
|
|
# FIXME: This of course is only correct on the local device;
|
||
|
|
# Should change this for net games.
|
||
|
|
keyboard = _ba.getinputdevice('Keyboard', '#1', doraise=False)
|
||
|
|
if keyboard is not None:
|
||
|
|
self._update_for_keyboard(keyboard)
|
||
|
|
|
||
|
|
flatness = 1.0 if _ba.app.vr_mode else 0.0
|
||
|
|
self._text = NodeActor(
|
||
|
|
_ba.newnode(
|
||
|
|
'text',
|
||
|
|
attrs={
|
||
|
|
'position': (0, -40),
|
||
|
|
'h_attach': 'center',
|
||
|
|
'v_attach': 'top',
|
||
|
|
'h_align': 'center',
|
||
|
|
'color': (0.7, 0.7, 0.95, 1.0),
|
||
|
|
'flatness': flatness,
|
||
|
|
'text': self._joinmsg,
|
||
|
|
},
|
||
|
|
)
|
||
|
|
)
|
||
|
|
|
||
|
|
if _ba.app.demo_mode or _ba.app.arcade_mode:
|
||
|
|
self._messages = [self._joinmsg]
|
||
|
|
else:
|
||
|
|
msg1 = Lstr(
|
||
|
|
resource='pressToSelectProfileText',
|
||
|
|
subs=[
|
||
|
|
(
|
||
|
|
'${BUTTONS}',
|
||
|
|
_ba.charstr(SpecialChar.UP_ARROW)
|
||
|
|
+ ' '
|
||
|
|
+ _ba.charstr(SpecialChar.DOWN_ARROW),
|
||
|
|
)
|
||
|
|
],
|
||
|
|
)
|
||
|
|
msg2 = Lstr(
|
||
|
|
resource='pressToOverrideCharacterText',
|
||
|
|
subs=[('${BUTTONS}', Lstr(resource='bombBoldText'))],
|
||
|
|
)
|
||
|
|
msg3 = Lstr(
|
||
|
|
value='${A} < ${B} >',
|
||
|
|
subs=[('${A}', msg2), ('${B}', self._press_to_bomb)],
|
||
|
|
)
|
||
|
|
self._messages = (
|
||
|
|
(
|
||
|
|
[
|
||
|
|
Lstr(
|
||
|
|
resource='pressToSelectTeamText',
|
||
|
|
subs=[
|
||
|
|
(
|
||
|
|
'${BUTTONS}',
|
||
|
|
_ba.charstr(SpecialChar.LEFT_ARROW)
|
||
|
|
+ ' '
|
||
|
|
+ _ba.charstr(SpecialChar.RIGHT_ARROW),
|
||
|
|
)
|
||
|
|
],
|
||
|
|
)
|
||
|
|
]
|
||
|
|
if can_switch_teams
|
||
|
|
else []
|
||
|
|
)
|
||
|
|
+ [msg1]
|
||
|
|
+ [msg3]
|
||
|
|
+ [self._joinmsg]
|
||
|
|
)
|
||
|
|
|
||
|
|
self._timer = _ba.Timer(4.0, WeakCall(self._update), repeat=True)
|
||
|
|
|
||
|
|
def _update_for_keyboard(self, keyboard: ba.InputDevice) -> None:
|
||
|
|
from ba import _input
|
||
|
|
|
||
|
|
punch_key = keyboard.get_button_name(
|
||
|
|
_input.get_device_value(keyboard, 'buttonPunch')
|
||
|
|
)
|
||
|
|
self._press_to_punch = Lstr(
|
||
|
|
resource='orText',
|
||
|
|
subs=[
|
||
|
|
('${A}', Lstr(value='\'${K}\'', subs=[('${K}', punch_key)])),
|
||
|
|
('${B}', self._press_to_punch),
|
||
|
|
],
|
||
|
|
)
|
||
|
|
bomb_key = keyboard.get_button_name(
|
||
|
|
_input.get_device_value(keyboard, 'buttonBomb')
|
||
|
|
)
|
||
|
|
self._press_to_bomb = Lstr(
|
||
|
|
resource='orText',
|
||
|
|
subs=[
|
||
|
|
('${A}', Lstr(value='\'${K}\'', subs=[('${K}', bomb_key)])),
|
||
|
|
('${B}', self._press_to_bomb),
|
||
|
|
],
|
||
|
|
)
|
||
|
|
self._joinmsg = Lstr(
|
||
|
|
value='${A} < ${B} >',
|
||
|
|
subs=[
|
||
|
|
('${A}', Lstr(resource='pressPunchToJoinText')),
|
||
|
|
('${B}', self._press_to_punch),
|
||
|
|
],
|
||
|
|
)
|
||
|
|
|
||
|
|
def _update(self) -> None:
|
||
|
|
assert self._text.node
|
||
|
|
self._text.node.text = self._messages[self._state]
|
||
|
|
self._state = (self._state + 1) % len(self._messages)
|
||
|
|
|
||
|
|
|
||
|
|
@dataclass
|
||
|
|
class PlayerReadyMessage:
|
||
|
|
"""Tells an object a player has been selected from the given chooser."""
|
||
|
|
|
||
|
|
chooser: ba.Chooser
|
||
|
|
|
||
|
|
|
||
|
|
@dataclass
|
||
|
|
class ChangeMessage:
|
||
|
|
"""Tells an object that a selection is being changed."""
|
||
|
|
|
||
|
|
what: str
|
||
|
|
value: int
|
||
|
|
|
||
|
|
|
||
|
|
class Chooser:
|
||
|
|
"""A character/team selector for a ba.Player.
|
||
|
|
|
||
|
|
Category: Gameplay Classes
|
||
|
|
"""
|
||
|
|
|
||
|
|
def __del__(self) -> None:
|
||
|
|
|
||
|
|
# Just kill off our base node; the rest should go down with it.
|
||
|
|
if self._text_node:
|
||
|
|
self._text_node.delete()
|
||
|
|
|
||
|
|
def __init__(
|
||
|
|
self, vpos: float, sessionplayer: _ba.SessionPlayer, lobby: 'Lobby'
|
||
|
|
) -> None:
|
||
|
|
self._deek_sound = _ba.getsound('deek')
|
||
|
|
self._click_sound = _ba.getsound('click01')
|
||
|
|
self._punchsound = _ba.getsound('punch01')
|
||
|
|
self._swish_sound = _ba.getsound('punchSwish')
|
||
|
|
self._errorsound = _ba.getsound('error')
|
||
|
|
self._mask_texture = _ba.gettexture('characterIconMask')
|
||
|
|
self._vpos = vpos
|
||
|
|
self._lobby = weakref.ref(lobby)
|
||
|
|
self._sessionplayer = sessionplayer
|
||
|
|
self._inited = False
|
||
|
|
self._dead = False
|
||
|
|
self._text_node: ba.Node | None = None
|
||
|
|
self._profilename = ''
|
||
|
|
self._profilenames: list[str] = []
|
||
|
|
self._ready: bool = False
|
||
|
|
self._character_names: list[str] = []
|
||
|
|
self._last_change: Sequence[float | int] = (0, 0)
|
||
|
|
self._profiles: dict[str, dict[str, Any]] = {}
|
||
|
|
|
||
|
|
app = _ba.app
|
||
|
|
|
||
|
|
# Load available player profiles either from the local config or
|
||
|
|
# from the remote device.
|
||
|
|
self.reload_profiles()
|
||
|
|
|
||
|
|
# Note: this is just our local index out of available teams; *not*
|
||
|
|
# the team-id!
|
||
|
|
self._selected_team_index: int = self.lobby.next_add_team
|
||
|
|
|
||
|
|
# Store a persistent random character index and colors; we'll use this
|
||
|
|
# for the '_random' profile. Let's use their input_device id to seed
|
||
|
|
# it. This will give a persistent character for them between games
|
||
|
|
# and will distribute characters nicely if everyone is random.
|
||
|
|
self._random_color, self._random_highlight = get_player_profile_colors(
|
||
|
|
None
|
||
|
|
)
|
||
|
|
|
||
|
|
# To calc our random character we pick a random one out of our
|
||
|
|
# unlocked list and then locate that character's index in the full
|
||
|
|
# list.
|
||
|
|
char_index_offset = app.lobby_random_char_index_offset
|
||
|
|
self._random_character_index = (
|
||
|
|
sessionplayer.inputdevice.id + char_index_offset
|
||
|
|
) % len(self._character_names)
|
||
|
|
|
||
|
|
# Attempt to set an initial profile based on what was used previously
|
||
|
|
# for this input-device, etc.
|
||
|
|
self._profileindex = self._select_initial_profile()
|
||
|
|
self._profilename = self._profilenames[self._profileindex]
|
||
|
|
|
||
|
|
self._text_node = _ba.newnode(
|
||
|
|
'text',
|
||
|
|
delegate=self,
|
||
|
|
attrs={
|
||
|
|
'position': (-100, self._vpos),
|
||
|
|
'maxwidth': 160,
|
||
|
|
'shadow': 0.5,
|
||
|
|
'vr_depth': -20,
|
||
|
|
'h_align': 'left',
|
||
|
|
'v_align': 'center',
|
||
|
|
'v_attach': 'top',
|
||
|
|
},
|
||
|
|
)
|
||
|
|
animate(self._text_node, 'scale', {0: 0, 0.1: 1.0})
|
||
|
|
self.icon = _ba.newnode(
|
||
|
|
'image',
|
||
|
|
owner=self._text_node,
|
||
|
|
attrs={
|
||
|
|
'position': (-130, self._vpos + 20),
|
||
|
|
'mask_texture': self._mask_texture,
|
||
|
|
'vr_depth': -10,
|
||
|
|
'attach': 'topCenter',
|
||
|
|
},
|
||
|
|
)
|
||
|
|
|
||
|
|
animate_array(self.icon, 'scale', 2, {0: (0, 0), 0.1: (45, 45)})
|
||
|
|
|
||
|
|
# Set our initial name to '<choosing player>' in case anyone asks.
|
||
|
|
self._sessionplayer.setname(
|
||
|
|
Lstr(resource='choosingPlayerText').evaluate(), real=False
|
||
|
|
)
|
||
|
|
|
||
|
|
# Init these to our rando but they should get switched to the
|
||
|
|
# selected profile (if any) right after.
|
||
|
|
self._character_index = self._random_character_index
|
||
|
|
self._color = self._random_color
|
||
|
|
self._highlight = self._random_highlight
|
||
|
|
|
||
|
|
self.update_from_profile()
|
||
|
|
self.update_position()
|
||
|
|
self._inited = True
|
||
|
|
|
||
|
|
self._set_ready(False)
|
||
|
|
|
||
|
|
def _select_initial_profile(self) -> int:
|
||
|
|
app = _ba.app
|
||
|
|
profilenames = self._profilenames
|
||
|
|
inputdevice = self._sessionplayer.inputdevice
|
||
|
|
|
||
|
|
# If we've got a set profile name for this device, work backwards
|
||
|
|
# from that to get our index.
|
||
|
|
dprofilename = app.config.get('Default Player Profiles', {}).get(
|
||
|
|
inputdevice.name + ' ' + inputdevice.unique_identifier
|
||
|
|
)
|
||
|
|
if dprofilename is not None and dprofilename in profilenames:
|
||
|
|
# If we got '__account__' and its local and we haven't marked
|
||
|
|
# anyone as the 'account profile' device yet, mark this guy as
|
||
|
|
# it. (prevents the next joiner from getting the account
|
||
|
|
# profile too).
|
||
|
|
if (
|
||
|
|
dprofilename == '__account__'
|
||
|
|
and not inputdevice.is_remote_client
|
||
|
|
and app.lobby_account_profile_device_id is None
|
||
|
|
):
|
||
|
|
app.lobby_account_profile_device_id = inputdevice.id
|
||
|
|
return profilenames.index(dprofilename)
|
||
|
|
|
||
|
|
# We want to mark the first local input-device in the game
|
||
|
|
# as the 'account profile' device.
|
||
|
|
if (
|
||
|
|
not inputdevice.is_remote_client
|
||
|
|
and not inputdevice.is_controller_app
|
||
|
|
):
|
||
|
|
if (
|
||
|
|
app.lobby_account_profile_device_id is None
|
||
|
|
and '__account__' in profilenames
|
||
|
|
):
|
||
|
|
app.lobby_account_profile_device_id = inputdevice.id
|
||
|
|
|
||
|
|
# If this is the designated account-profile-device, try to default
|
||
|
|
# to the account profile.
|
||
|
|
if (
|
||
|
|
inputdevice.id == app.lobby_account_profile_device_id
|
||
|
|
and '__account__' in profilenames
|
||
|
|
):
|
||
|
|
return profilenames.index('__account__')
|
||
|
|
|
||
|
|
# If this is the controller app, it defaults to using a random
|
||
|
|
# profile (since we can pull the random name from the app).
|
||
|
|
if inputdevice.is_controller_app and '_random' in profilenames:
|
||
|
|
return profilenames.index('_random')
|
||
|
|
|
||
|
|
# If its a client connection, for now just force
|
||
|
|
# the account profile if possible.. (need to provide a
|
||
|
|
# way for clients to specify/remember their default
|
||
|
|
# profile on remote servers that do not already know them).
|
||
|
|
if inputdevice.is_remote_client and '__account__' in profilenames:
|
||
|
|
return profilenames.index('__account__')
|
||
|
|
|
||
|
|
# Cycle through our non-random profiles once; after
|
||
|
|
# that, everyone gets random.
|
||
|
|
while app.lobby_random_profile_index < len(
|
||
|
|
profilenames
|
||
|
|
) and profilenames[app.lobby_random_profile_index] in (
|
||
|
|
'_random',
|
||
|
|
'__account__',
|
||
|
|
'_edit',
|
||
|
|
):
|
||
|
|
app.lobby_random_profile_index += 1
|
||
|
|
if app.lobby_random_profile_index < len(profilenames):
|
||
|
|
profileindex = app.lobby_random_profile_index
|
||
|
|
app.lobby_random_profile_index += 1
|
||
|
|
return profileindex
|
||
|
|
assert '_random' in profilenames
|
||
|
|
return profilenames.index('_random')
|
||
|
|
|
||
|
|
@property
|
||
|
|
def sessionplayer(self) -> ba.SessionPlayer:
|
||
|
|
"""The ba.SessionPlayer associated with this chooser."""
|
||
|
|
return self._sessionplayer
|
||
|
|
|
||
|
|
@property
|
||
|
|
def ready(self) -> bool:
|
||
|
|
"""Whether this chooser is checked in as ready."""
|
||
|
|
return self._ready
|
||
|
|
|
||
|
|
def set_vpos(self, vpos: float) -> None:
|
||
|
|
"""(internal)"""
|
||
|
|
self._vpos = vpos
|
||
|
|
|
||
|
|
def set_dead(self, val: bool) -> None:
|
||
|
|
"""(internal)"""
|
||
|
|
self._dead = val
|
||
|
|
|
||
|
|
@property
|
||
|
|
def sessionteam(self) -> ba.SessionTeam:
|
||
|
|
"""Return this chooser's currently selected ba.SessionTeam."""
|
||
|
|
return self.lobby.sessionteams[self._selected_team_index]
|
||
|
|
|
||
|
|
@property
|
||
|
|
def lobby(self) -> ba.Lobby:
|
||
|
|
"""The chooser's ba.Lobby."""
|
||
|
|
lobby = self._lobby()
|
||
|
|
if lobby is None:
|
||
|
|
raise NotFoundError('Lobby does not exist.')
|
||
|
|
return lobby
|
||
|
|
|
||
|
|
def get_lobby(self) -> ba.Lobby | None:
|
||
|
|
"""Return this chooser's lobby if it still exists; otherwise None."""
|
||
|
|
return self._lobby()
|
||
|
|
|
||
|
|
def update_from_profile(self) -> None:
|
||
|
|
"""Set character/colors based on the current profile."""
|
||
|
|
self._profilename = self._profilenames[self._profileindex]
|
||
|
|
if self._profilename == '_edit':
|
||
|
|
pass
|
||
|
|
elif self._profilename == '_random':
|
||
|
|
self._character_index = self._random_character_index
|
||
|
|
self._color = self._random_color
|
||
|
|
self._highlight = self._random_highlight
|
||
|
|
else:
|
||
|
|
character = self._profiles[self._profilename]['character']
|
||
|
|
|
||
|
|
# At the moment we're not properly pulling the list
|
||
|
|
# of available characters from clients, so profiles might use a
|
||
|
|
# character not in their list. For now, just go ahead and add
|
||
|
|
# a character name to their list as long as we're aware of it.
|
||
|
|
# This just means they won't always be able to override their
|
||
|
|
# character to others they own, but profile characters
|
||
|
|
# should work (and we validate profiles on the master server
|
||
|
|
# so no exploit opportunities)
|
||
|
|
if (
|
||
|
|
character not in self._character_names
|
||
|
|
and character in _ba.app.spaz_appearances
|
||
|
|
):
|
||
|
|
self._character_names.append(character)
|
||
|
|
self._character_index = self._character_names.index(character)
|
||
|
|
self._color, self._highlight = get_player_profile_colors(
|
||
|
|
self._profilename, profiles=self._profiles
|
||
|
|
)
|
||
|
|
self._update_icon()
|
||
|
|
self._update_text()
|
||
|
|
|
||
|
|
def reload_profiles(self) -> None:
|
||
|
|
"""Reload all player profiles."""
|
||
|
|
from ba._general import json_prep
|
||
|
|
|
||
|
|
app = _ba.app
|
||
|
|
|
||
|
|
# Re-construct our profile index and other stuff since the profile
|
||
|
|
# list might have changed.
|
||
|
|
input_device = self._sessionplayer.inputdevice
|
||
|
|
is_remote = input_device.is_remote_client
|
||
|
|
is_test_input = input_device.name.startswith('TestInput')
|
||
|
|
|
||
|
|
# Pull this player's list of unlocked characters.
|
||
|
|
if is_remote:
|
||
|
|
# TODO: Pull this from the remote player.
|
||
|
|
# (but make sure to filter it to the ones we've got).
|
||
|
|
self._character_names = ['Spaz']
|
||
|
|
else:
|
||
|
|
self._character_names = self.lobby.character_names_local_unlocked
|
||
|
|
|
||
|
|
# If we're a local player, pull our local profiles from the config.
|
||
|
|
# Otherwise ask the remote-input-device for its profile list.
|
||
|
|
if is_remote:
|
||
|
|
self._profiles = input_device.get_player_profiles()
|
||
|
|
else:
|
||
|
|
self._profiles = app.config.get('Player Profiles', {})
|
||
|
|
|
||
|
|
# These may have come over the wire from an older
|
||
|
|
# (non-unicode/non-json) version.
|
||
|
|
# Make sure they conform to our standards
|
||
|
|
# (unicode strings, no tuples, etc)
|
||
|
|
self._profiles = json_prep(self._profiles)
|
||
|
|
|
||
|
|
# Filter out any characters we're unaware of.
|
||
|
|
for profile in list(self._profiles.items()):
|
||
|
|
if profile[1].get('character', '') not in app.spaz_appearances:
|
||
|
|
profile[1]['character'] = 'Spaz'
|
||
|
|
|
||
|
|
# Add in a random one so we're ok even if there's no user profiles.
|
||
|
|
self._profiles['_random'] = {}
|
||
|
|
|
||
|
|
# In kiosk mode we disable account profiles to force random.
|
||
|
|
if app.demo_mode or app.arcade_mode:
|
||
|
|
if '__account__' in self._profiles:
|
||
|
|
del self._profiles['__account__']
|
||
|
|
|
||
|
|
# For local devices, add it an 'edit' option which will pop up
|
||
|
|
# the profile window.
|
||
|
|
if (
|
||
|
|
not is_remote
|
||
|
|
and not is_test_input
|
||
|
|
and not (app.demo_mode or app.arcade_mode)
|
||
|
|
):
|
||
|
|
self._profiles['_edit'] = {}
|
||
|
|
|
||
|
|
# Build a sorted name list we can iterate through.
|
||
|
|
self._profilenames = list(self._profiles.keys())
|
||
|
|
self._profilenames.sort(key=lambda x: x.lower())
|
||
|
|
|
||
|
|
if self._profilename in self._profilenames:
|
||
|
|
self._profileindex = self._profilenames.index(self._profilename)
|
||
|
|
else:
|
||
|
|
self._profileindex = 0
|
||
|
|
# noinspection PyUnresolvedReferences
|
||
|
|
self._profilename = self._profilenames[self._profileindex]
|
||
|
|
|
||
|
|
def update_position(self) -> None:
|
||
|
|
"""Update this chooser's position."""
|
||
|
|
|
||
|
|
assert self._text_node
|
||
|
|
spacing = 350
|
||
|
|
sessionteams = self.lobby.sessionteams
|
||
|
|
offs = (
|
||
|
|
spacing * -0.5 * len(sessionteams)
|
||
|
|
+ spacing * self._selected_team_index
|
||
|
|
+ 250
|
||
|
|
)
|
||
|
|
if len(sessionteams) > 1:
|
||
|
|
offs -= 35
|
||
|
|
animate_array(
|
||
|
|
self._text_node,
|
||
|
|
'position',
|
||
|
|
2,
|
||
|
|
{0: self._text_node.position, 0.1: (-100 + offs, self._vpos + 23)},
|
||
|
|
)
|
||
|
|
animate_array(
|
||
|
|
self.icon,
|
||
|
|
'position',
|
||
|
|
2,
|
||
|
|
{0: self.icon.position, 0.1: (-130 + offs, self._vpos + 22)},
|
||
|
|
)
|
||
|
|
|
||
|
|
def get_character_name(self) -> str:
|
||
|
|
"""Return the selected character name."""
|
||
|
|
return self._character_names[self._character_index]
|
||
|
|
|
||
|
|
def _do_nothing(self) -> None:
|
||
|
|
"""Does nothing! (hacky way to disable callbacks)"""
|
||
|
|
|
||
|
|
def _getname(self, full: bool = False) -> str:
|
||
|
|
name_raw = name = self._profilenames[self._profileindex]
|
||
|
|
clamp = False
|
||
|
|
if name == '_random':
|
||
|
|
try:
|
||
|
|
name = self._sessionplayer.inputdevice.get_default_player_name()
|
||
|
|
except Exception:
|
||
|
|
print_exception('Error getting _random chooser name.')
|
||
|
|
name = 'Invalid'
|
||
|
|
clamp = not full
|
||
|
|
elif name == '__account__':
|
||
|
|
try:
|
||
|
|
name = self._sessionplayer.inputdevice.get_v1_account_name(full)
|
||
|
|
except Exception:
|
||
|
|
print_exception('Error getting account name for chooser.')
|
||
|
|
name = 'Invalid'
|
||
|
|
clamp = not full
|
||
|
|
elif name == '_edit':
|
||
|
|
# Explicitly flattening this to a str; it's only relevant on
|
||
|
|
# the host so that's ok.
|
||
|
|
name = Lstr(
|
||
|
|
resource='createEditPlayerText',
|
||
|
|
fallback_resource='editProfileWindow.titleNewText',
|
||
|
|
).evaluate()
|
||
|
|
else:
|
||
|
|
# If we have a regular profile marked as global with an icon,
|
||
|
|
# use it (for full only).
|
||
|
|
if full:
|
||
|
|
try:
|
||
|
|
if self._profiles[name_raw].get('global', False):
|
||
|
|
icon = (
|
||
|
|
self._profiles[name_raw]['icon']
|
||
|
|
if 'icon' in self._profiles[name_raw]
|
||
|
|
else _ba.charstr(SpecialChar.LOGO)
|
||
|
|
)
|
||
|
|
name = icon + name
|
||
|
|
except Exception:
|
||
|
|
print_exception('Error applying global icon.')
|
||
|
|
else:
|
||
|
|
# We now clamp non-full versions of names so there's at
|
||
|
|
# least some hope of reading them in-game.
|
||
|
|
clamp = True
|
||
|
|
|
||
|
|
if clamp:
|
||
|
|
if len(name) > 10:
|
||
|
|
name = name[:10] + '...'
|
||
|
|
return name
|
||
|
|
|
||
|
|
def _set_ready(self, ready: bool) -> None:
|
||
|
|
# pylint: disable=cyclic-import
|
||
|
|
from bastd.ui.profile import browser as pbrowser
|
||
|
|
from ba._general import Call
|
||
|
|
|
||
|
|
profilename = self._profilenames[self._profileindex]
|
||
|
|
|
||
|
|
# Handle '_edit' as a special case.
|
||
|
|
if profilename == '_edit' and ready:
|
||
|
|
with _ba.Context('ui'):
|
||
|
|
pbrowser.ProfileBrowserWindow(in_main_menu=False)
|
||
|
|
|
||
|
|
# Give their input-device UI ownership too
|
||
|
|
# (prevent someone else from snatching it in crowded games)
|
||
|
|
_ba.set_ui_input_device(self._sessionplayer.inputdevice)
|
||
|
|
return
|
||
|
|
|
||
|
|
if not ready:
|
||
|
|
self._sessionplayer.assigninput(
|
||
|
|
InputType.LEFT_PRESS,
|
||
|
|
Call(self.handlemessage, ChangeMessage('team', -1)),
|
||
|
|
)
|
||
|
|
self._sessionplayer.assigninput(
|
||
|
|
InputType.RIGHT_PRESS,
|
||
|
|
Call(self.handlemessage, ChangeMessage('team', 1)),
|
||
|
|
)
|
||
|
|
self._sessionplayer.assigninput(
|
||
|
|
InputType.BOMB_PRESS,
|
||
|
|
Call(self.handlemessage, ChangeMessage('character', 1)),
|
||
|
|
)
|
||
|
|
self._sessionplayer.assigninput(
|
||
|
|
InputType.UP_PRESS,
|
||
|
|
Call(self.handlemessage, ChangeMessage('profileindex', -1)),
|
||
|
|
)
|
||
|
|
self._sessionplayer.assigninput(
|
||
|
|
InputType.DOWN_PRESS,
|
||
|
|
Call(self.handlemessage, ChangeMessage('profileindex', 1)),
|
||
|
|
)
|
||
|
|
self._sessionplayer.assigninput(
|
||
|
|
(
|
||
|
|
InputType.JUMP_PRESS,
|
||
|
|
InputType.PICK_UP_PRESS,
|
||
|
|
InputType.PUNCH_PRESS,
|
||
|
|
),
|
||
|
|
Call(self.handlemessage, ChangeMessage('ready', 1)),
|
||
|
|
)
|
||
|
|
self._ready = False
|
||
|
|
self._update_text()
|
||
|
|
self._sessionplayer.setname('untitled', real=False)
|
||
|
|
else:
|
||
|
|
self._sessionplayer.assigninput(
|
||
|
|
(
|
||
|
|
InputType.LEFT_PRESS,
|
||
|
|
InputType.RIGHT_PRESS,
|
||
|
|
InputType.UP_PRESS,
|
||
|
|
InputType.DOWN_PRESS,
|
||
|
|
InputType.JUMP_PRESS,
|
||
|
|
InputType.BOMB_PRESS,
|
||
|
|
InputType.PICK_UP_PRESS,
|
||
|
|
),
|
||
|
|
self._do_nothing,
|
||
|
|
)
|
||
|
|
self._sessionplayer.assigninput(
|
||
|
|
(
|
||
|
|
InputType.JUMP_PRESS,
|
||
|
|
InputType.BOMB_PRESS,
|
||
|
|
InputType.PICK_UP_PRESS,
|
||
|
|
InputType.PUNCH_PRESS,
|
||
|
|
),
|
||
|
|
Call(self.handlemessage, ChangeMessage('ready', 0)),
|
||
|
|
)
|
||
|
|
|
||
|
|
# Store the last profile picked by this input for reuse.
|
||
|
|
input_device = self._sessionplayer.inputdevice
|
||
|
|
name = input_device.name
|
||
|
|
unique_id = input_device.unique_identifier
|
||
|
|
device_profiles = _ba.app.config.setdefault(
|
||
|
|
'Default Player Profiles', {}
|
||
|
|
)
|
||
|
|
|
||
|
|
# Make an exception if we have no custom profiles and are set
|
||
|
|
# to random; in that case we'll want to start picking up custom
|
||
|
|
# profiles if/when one is made so keep our setting cleared.
|
||
|
|
special = ('_random', '_edit', '__account__')
|
||
|
|
have_custom_profiles = any(p not in special for p in self._profiles)
|
||
|
|
|
||
|
|
profilekey = name + ' ' + unique_id
|
||
|
|
if profilename == '_random' and not have_custom_profiles:
|
||
|
|
if profilekey in device_profiles:
|
||
|
|
del device_profiles[profilekey]
|
||
|
|
else:
|
||
|
|
device_profiles[profilekey] = profilename
|
||
|
|
_ba.app.config.commit()
|
||
|
|
|
||
|
|
# Set this player's short and full name.
|
||
|
|
self._sessionplayer.setname(
|
||
|
|
self._getname(), self._getname(full=True), real=True
|
||
|
|
)
|
||
|
|
self._ready = True
|
||
|
|
self._update_text()
|
||
|
|
|
||
|
|
# Inform the session that this player is ready.
|
||
|
|
_ba.getsession().handlemessage(PlayerReadyMessage(self))
|
||
|
|
|
||
|
|
def _handle_ready_msg(self, ready: bool) -> None:
|
||
|
|
force_team_switch = False
|
||
|
|
|
||
|
|
# Team auto-balance kicks us to another team if we try to
|
||
|
|
# join the team with the most players.
|
||
|
|
if not self._ready:
|
||
|
|
if _ba.app.config.get('Auto Balance Teams', False):
|
||
|
|
lobby = self.lobby
|
||
|
|
sessionteams = lobby.sessionteams
|
||
|
|
if len(sessionteams) > 1:
|
||
|
|
|
||
|
|
# First, calc how many players are on each team
|
||
|
|
# ..we need to count both active players and
|
||
|
|
# choosers that have been marked as ready.
|
||
|
|
team_player_counts = {}
|
||
|
|
for sessionteam in sessionteams:
|
||
|
|
team_player_counts[sessionteam.id] = len(
|
||
|
|
sessionteam.players
|
||
|
|
)
|
||
|
|
for chooser in lobby.choosers:
|
||
|
|
if chooser.ready:
|
||
|
|
team_player_counts[chooser.sessionteam.id] += 1
|
||
|
|
largest_team_size = max(team_player_counts.values())
|
||
|
|
smallest_team_size = min(team_player_counts.values())
|
||
|
|
|
||
|
|
# Force switch if we're on the biggest sessionteam
|
||
|
|
# and there's a smaller one available.
|
||
|
|
if (
|
||
|
|
largest_team_size != smallest_team_size
|
||
|
|
and team_player_counts[self.sessionteam.id]
|
||
|
|
>= largest_team_size
|
||
|
|
):
|
||
|
|
force_team_switch = True
|
||
|
|
|
||
|
|
# Either force switch teams, or actually for realsies do the set-ready.
|
||
|
|
if force_team_switch:
|
||
|
|
_ba.playsound(self._errorsound)
|
||
|
|
self.handlemessage(ChangeMessage('team', 1))
|
||
|
|
else:
|
||
|
|
_ba.playsound(self._punchsound)
|
||
|
|
self._set_ready(ready)
|
||
|
|
|
||
|
|
# TODO: should handle this at the engine layer so this is unnecessary.
|
||
|
|
def _handle_repeat_message_attack(self) -> None:
|
||
|
|
now = _ba.time()
|
||
|
|
count = self._last_change[1]
|
||
|
|
if now - self._last_change[0] < QUICK_CHANGE_INTERVAL:
|
||
|
|
count += 1
|
||
|
|
if count > MAX_QUICK_CHANGE_COUNT:
|
||
|
|
_ba.disconnect_client(self._sessionplayer.inputdevice.client_id)
|
||
|
|
elif now - self._last_change[0] > QUICK_CHANGE_RESET_INTERVAL:
|
||
|
|
count = 0
|
||
|
|
self._last_change = (now, count)
|
||
|
|
|
||
|
|
def handlemessage(self, msg: Any) -> Any:
|
||
|
|
"""Standard generic message handler."""
|
||
|
|
|
||
|
|
if isinstance(msg, ChangeMessage):
|
||
|
|
self._handle_repeat_message_attack()
|
||
|
|
|
||
|
|
# If we've been removed from the lobby, ignore this stuff.
|
||
|
|
if self._dead:
|
||
|
|
print_error('chooser got ChangeMessage after dying')
|
||
|
|
return
|
||
|
|
|
||
|
|
if not self._text_node:
|
||
|
|
print_error('got ChangeMessage after nodes died')
|
||
|
|
return
|
||
|
|
|
||
|
|
if msg.what == 'team':
|
||
|
|
sessionteams = self.lobby.sessionteams
|
||
|
|
if len(sessionteams) > 1:
|
||
|
|
_ba.playsound(self._swish_sound)
|
||
|
|
self._selected_team_index = (
|
||
|
|
self._selected_team_index + msg.value
|
||
|
|
) % len(sessionteams)
|
||
|
|
self._update_text()
|
||
|
|
self.update_position()
|
||
|
|
self._update_icon()
|
||
|
|
|
||
|
|
elif msg.what == 'profileindex':
|
||
|
|
if len(self._profilenames) == 1:
|
||
|
|
|
||
|
|
# This should be pretty hard to hit now with
|
||
|
|
# automatic local accounts.
|
||
|
|
_ba.playsound(_ba.getsound('error'))
|
||
|
|
else:
|
||
|
|
|
||
|
|
# Pick the next player profile and assign our name
|
||
|
|
# and character based on that.
|
||
|
|
_ba.playsound(self._deek_sound)
|
||
|
|
self._profileindex = (self._profileindex + msg.value) % len(
|
||
|
|
self._profilenames
|
||
|
|
)
|
||
|
|
self.update_from_profile()
|
||
|
|
|
||
|
|
elif msg.what == 'character':
|
||
|
|
_ba.playsound(self._click_sound)
|
||
|
|
# update our index in our local list of characters
|
||
|
|
self._character_index = (
|
||
|
|
self._character_index + msg.value
|
||
|
|
) % len(self._character_names)
|
||
|
|
self._update_text()
|
||
|
|
self._update_icon()
|
||
|
|
|
||
|
|
elif msg.what == 'ready':
|
||
|
|
self._handle_ready_msg(bool(msg.value))
|
||
|
|
|
||
|
|
def _update_text(self) -> None:
|
||
|
|
assert self._text_node is not None
|
||
|
|
if self._ready:
|
||
|
|
|
||
|
|
# Once we're ready, we've saved the name, so lets ask the system
|
||
|
|
# for it so we get appended numbers and stuff.
|
||
|
|
text = Lstr(value=self._sessionplayer.getname(full=True))
|
||
|
|
text = Lstr(
|
||
|
|
value='${A} (${B})',
|
||
|
|
subs=[('${A}', text), ('${B}', Lstr(resource='readyText'))],
|
||
|
|
)
|
||
|
|
else:
|
||
|
|
text = Lstr(value=self._getname(full=True))
|
||
|
|
|
||
|
|
can_switch_teams = len(self.lobby.sessionteams) > 1
|
||
|
|
|
||
|
|
# Flash as we're coming in.
|
||
|
|
fin_color = _ba.safecolor(self.get_color()) + (1,)
|
||
|
|
if not self._inited:
|
||
|
|
animate_array(
|
||
|
|
self._text_node,
|
||
|
|
'color',
|
||
|
|
4,
|
||
|
|
{0.15: fin_color, 0.25: (2, 2, 2, 1), 0.35: fin_color},
|
||
|
|
)
|
||
|
|
else:
|
||
|
|
|
||
|
|
# Blend if we're in teams mode; switch instantly otherwise.
|
||
|
|
if can_switch_teams:
|
||
|
|
animate_array(
|
||
|
|
self._text_node,
|
||
|
|
'color',
|
||
|
|
4,
|
||
|
|
{0: self._text_node.color, 0.1: fin_color},
|
||
|
|
)
|
||
|
|
else:
|
||
|
|
self._text_node.color = fin_color
|
||
|
|
|
||
|
|
self._text_node.text = text
|
||
|
|
|
||
|
|
def get_color(self) -> Sequence[float]:
|
||
|
|
"""Return the currently selected color."""
|
||
|
|
val: Sequence[float]
|
||
|
|
if self.lobby.use_team_colors:
|
||
|
|
val = self.lobby.sessionteams[self._selected_team_index].color
|
||
|
|
else:
|
||
|
|
val = self._color
|
||
|
|
if len(val) != 3:
|
||
|
|
print('get_color: ignoring invalid color of len', len(val))
|
||
|
|
val = (0, 1, 0)
|
||
|
|
return val
|
||
|
|
|
||
|
|
def get_highlight(self) -> Sequence[float]:
|
||
|
|
"""Return the currently selected highlight."""
|
||
|
|
if self._profilenames[self._profileindex] == '_edit':
|
||
|
|
return 0, 1, 0
|
||
|
|
|
||
|
|
# If we're using team colors we wanna make sure our highlight color
|
||
|
|
# isn't too close to any other team's color.
|
||
|
|
highlight = list(self._highlight)
|
||
|
|
if self.lobby.use_team_colors:
|
||
|
|
for i, sessionteam in enumerate(self.lobby.sessionteams):
|
||
|
|
if i != self._selected_team_index:
|
||
|
|
|
||
|
|
# Find the dominant component of this sessionteam's color
|
||
|
|
# and adjust ours so that the component is
|
||
|
|
# not super-dominant.
|
||
|
|
max_val = 0.0
|
||
|
|
max_index = 0
|
||
|
|
for j in range(3):
|
||
|
|
if sessionteam.color[j] > max_val:
|
||
|
|
max_val = sessionteam.color[j]
|
||
|
|
max_index = j
|
||
|
|
that_color_for_us = highlight[max_index]
|
||
|
|
our_second_biggest = max(
|
||
|
|
highlight[(max_index + 1) % 3],
|
||
|
|
highlight[(max_index + 2) % 3],
|
||
|
|
)
|
||
|
|
diff = that_color_for_us - our_second_biggest
|
||
|
|
if diff > 0:
|
||
|
|
highlight[max_index] -= diff * 0.6
|
||
|
|
highlight[(max_index + 1) % 3] += diff * 0.3
|
||
|
|
highlight[(max_index + 2) % 3] += diff * 0.2
|
||
|
|
return highlight
|
||
|
|
|
||
|
|
def getplayer(self) -> ba.SessionPlayer:
|
||
|
|
"""Return the player associated with this chooser."""
|
||
|
|
return self._sessionplayer
|
||
|
|
|
||
|
|
def _update_icon(self) -> None:
|
||
|
|
if self._profilenames[self._profileindex] == '_edit':
|
||
|
|
tex = _ba.gettexture('black')
|
||
|
|
tint_tex = _ba.gettexture('black')
|
||
|
|
self.icon.color = (1, 1, 1)
|
||
|
|
self.icon.texture = tex
|
||
|
|
self.icon.tint_texture = tint_tex
|
||
|
|
self.icon.tint_color = (0, 1, 0)
|
||
|
|
return
|
||
|
|
|
||
|
|
try:
|
||
|
|
tex_name = _ba.app.spaz_appearances[
|
||
|
|
self._character_names[self._character_index]
|
||
|
|
].icon_texture
|
||
|
|
tint_tex_name = _ba.app.spaz_appearances[
|
||
|
|
self._character_names[self._character_index]
|
||
|
|
].icon_mask_texture
|
||
|
|
except Exception:
|
||
|
|
print_exception('Error updating char icon list')
|
||
|
|
tex_name = 'neoSpazIcon'
|
||
|
|
tint_tex_name = 'neoSpazIconColorMask'
|
||
|
|
|
||
|
|
tex = _ba.gettexture(tex_name)
|
||
|
|
tint_tex = _ba.gettexture(tint_tex_name)
|
||
|
|
|
||
|
|
self.icon.color = (1, 1, 1)
|
||
|
|
self.icon.texture = tex
|
||
|
|
self.icon.tint_texture = tint_tex
|
||
|
|
clr = self.get_color()
|
||
|
|
clr2 = self.get_highlight()
|
||
|
|
|
||
|
|
can_switch_teams = len(self.lobby.sessionteams) > 1
|
||
|
|
|
||
|
|
# If we're initing, flash.
|
||
|
|
if not self._inited:
|
||
|
|
animate_array(
|
||
|
|
self.icon,
|
||
|
|
'color',
|
||
|
|
3,
|
||
|
|
{0.15: (1, 1, 1), 0.25: (2, 2, 2), 0.35: (1, 1, 1)},
|
||
|
|
)
|
||
|
|
|
||
|
|
# Blend in teams mode; switch instantly in ffa-mode.
|
||
|
|
if can_switch_teams:
|
||
|
|
animate_array(
|
||
|
|
self.icon, 'tint_color', 3, {0: self.icon.tint_color, 0.1: clr}
|
||
|
|
)
|
||
|
|
else:
|
||
|
|
self.icon.tint_color = clr
|
||
|
|
self.icon.tint2_color = clr2
|
||
|
|
|
||
|
|
# Store the icon info the the player.
|
||
|
|
self._sessionplayer.set_icon_info(tex_name, tint_tex_name, clr, clr2)
|
||
|
|
|
||
|
|
|
||
|
|
class Lobby:
|
||
|
|
"""Container for ba.Choosers.
|
||
|
|
|
||
|
|
Category: Gameplay Classes
|
||
|
|
"""
|
||
|
|
|
||
|
|
def __del__(self) -> None:
|
||
|
|
|
||
|
|
# Reset any players that still have a chooser in us.
|
||
|
|
# (should allow the choosers to die).
|
||
|
|
sessionplayers = [
|
||
|
|
c.sessionplayer for c in self.choosers if c.sessionplayer
|
||
|
|
]
|
||
|
|
for sessionplayer in sessionplayers:
|
||
|
|
sessionplayer.resetinput()
|
||
|
|
|
||
|
|
def __init__(self) -> None:
|
||
|
|
from ba._team import SessionTeam
|
||
|
|
from ba._coopsession import CoopSession
|
||
|
|
|
||
|
|
session = _ba.getsession()
|
||
|
|
self._use_team_colors = session.use_team_colors
|
||
|
|
if session.use_teams:
|
||
|
|
self._sessionteams = [
|
||
|
|
weakref.ref(team) for team in session.sessionteams
|
||
|
|
]
|
||
|
|
else:
|
||
|
|
self._dummy_teams = SessionTeam()
|
||
|
|
self._sessionteams = [weakref.ref(self._dummy_teams)]
|
||
|
|
v_offset = -150 if isinstance(session, CoopSession) else -50
|
||
|
|
self.choosers: list[Chooser] = []
|
||
|
|
self.base_v_offset = v_offset
|
||
|
|
self.update_positions()
|
||
|
|
self._next_add_team = 0
|
||
|
|
self.character_names_local_unlocked: list[str] = []
|
||
|
|
self._vpos = 0
|
||
|
|
|
||
|
|
# Grab available profiles.
|
||
|
|
self.reload_profiles()
|
||
|
|
|
||
|
|
self._join_info_text = None
|
||
|
|
|
||
|
|
@property
|
||
|
|
def next_add_team(self) -> int:
|
||
|
|
"""(internal)"""
|
||
|
|
return self._next_add_team
|
||
|
|
|
||
|
|
@property
|
||
|
|
def use_team_colors(self) -> bool:
|
||
|
|
"""A bool for whether this lobby is using team colors.
|
||
|
|
|
||
|
|
If False, inidividual player colors are used instead.
|
||
|
|
"""
|
||
|
|
return self._use_team_colors
|
||
|
|
|
||
|
|
@property
|
||
|
|
def sessionteams(self) -> list[ba.SessionTeam]:
|
||
|
|
"""ba.SessionTeams available in this lobby."""
|
||
|
|
allteams = []
|
||
|
|
for tref in self._sessionteams:
|
||
|
|
team = tref()
|
||
|
|
assert team is not None
|
||
|
|
allteams.append(team)
|
||
|
|
return allteams
|
||
|
|
|
||
|
|
def get_choosers(self) -> list[Chooser]:
|
||
|
|
"""Return the lobby's current choosers."""
|
||
|
|
return self.choosers
|
||
|
|
|
||
|
|
def create_join_info(self) -> JoinInfo:
|
||
|
|
"""Create a display of on-screen information for joiners.
|
||
|
|
|
||
|
|
(how to switch teams, players, etc.)
|
||
|
|
Intended for use in initial joining-screens.
|
||
|
|
"""
|
||
|
|
return JoinInfo(self)
|
||
|
|
|
||
|
|
def reload_profiles(self) -> None:
|
||
|
|
"""Reload available player profiles."""
|
||
|
|
# pylint: disable=cyclic-import
|
||
|
|
from bastd.actor.spazappearance import get_appearances
|
||
|
|
|
||
|
|
# We may have gained or lost character names if the user
|
||
|
|
# bought something; reload these too.
|
||
|
|
self.character_names_local_unlocked = get_appearances()
|
||
|
|
self.character_names_local_unlocked.sort(key=lambda x: x.lower())
|
||
|
|
|
||
|
|
# Do any overall prep we need to such as creating account profile.
|
||
|
|
_ba.app.accounts_v1.ensure_have_account_player_profile()
|
||
|
|
for chooser in self.choosers:
|
||
|
|
try:
|
||
|
|
chooser.reload_profiles()
|
||
|
|
chooser.update_from_profile()
|
||
|
|
except Exception:
|
||
|
|
print_exception('Error reloading profiles.')
|
||
|
|
|
||
|
|
def update_positions(self) -> None:
|
||
|
|
"""Update positions for all choosers."""
|
||
|
|
self._vpos = -100 + self.base_v_offset
|
||
|
|
for chooser in self.choosers:
|
||
|
|
chooser.set_vpos(self._vpos)
|
||
|
|
chooser.update_position()
|
||
|
|
self._vpos -= 48
|
||
|
|
|
||
|
|
def check_all_ready(self) -> bool:
|
||
|
|
"""Return whether all choosers are marked ready."""
|
||
|
|
return all(chooser.ready for chooser in self.choosers)
|
||
|
|
|
||
|
|
def add_chooser(self, sessionplayer: ba.SessionPlayer) -> None:
|
||
|
|
"""Add a chooser to the lobby for the provided player."""
|
||
|
|
self.choosers.append(
|
||
|
|
Chooser(vpos=self._vpos, sessionplayer=sessionplayer, lobby=self)
|
||
|
|
)
|
||
|
|
self._next_add_team = (self._next_add_team + 1) % len(
|
||
|
|
self._sessionteams
|
||
|
|
)
|
||
|
|
self._vpos -= 48
|
||
|
|
|
||
|
|
def remove_chooser(self, player: ba.SessionPlayer) -> None:
|
||
|
|
"""Remove a single player's chooser; does not kick them.
|
||
|
|
|
||
|
|
This is used when a player enters the game and no longer
|
||
|
|
needs a chooser."""
|
||
|
|
found = False
|
||
|
|
chooser = None
|
||
|
|
for chooser in self.choosers:
|
||
|
|
if chooser.getplayer() is player:
|
||
|
|
found = True
|
||
|
|
|
||
|
|
# Mark it as dead since there could be more
|
||
|
|
# change-commands/etc coming in still for it;
|
||
|
|
# want to avoid duplicate player-adds/etc.
|
||
|
|
chooser.set_dead(True)
|
||
|
|
self.choosers.remove(chooser)
|
||
|
|
break
|
||
|
|
if not found:
|
||
|
|
print_error(f'remove_chooser did not find player {player}')
|
||
|
|
elif chooser in self.choosers:
|
||
|
|
print_error(f'chooser remains after removal for {player}')
|
||
|
|
self.update_positions()
|
||
|
|
|
||
|
|
def remove_all_choosers(self) -> None:
|
||
|
|
"""Remove all choosers without kicking players.
|
||
|
|
|
||
|
|
This is called after all players check in and enter a game.
|
||
|
|
"""
|
||
|
|
self.choosers = []
|
||
|
|
self.update_positions()
|
||
|
|
|
||
|
|
def remove_all_choosers_and_kick_players(self) -> None:
|
||
|
|
"""Remove all player choosers and kick attached players."""
|
||
|
|
|
||
|
|
# Copy the list; it can change under us otherwise.
|
||
|
|
for chooser in list(self.choosers):
|
||
|
|
if chooser.sessionplayer:
|
||
|
|
chooser.sessionplayer.remove_from_game()
|
||
|
|
self.remove_all_choosers()
|