API update to 1.6.3 stable

This commit is contained in:
imayushsaini 2021-05-30 02:08:06 +05:30
parent 463bae3913
commit fcd8a94e81
146 changed files with 2254 additions and 510 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -181,7 +181,7 @@ class AccountSubsystem:
"""Return whether pro is currently unlocked."""
# Check our tickets-based pro upgrade and our two real-IAP based
# upgrades. Also unlock this stuff in ballistica-core builds.
# upgrades. Also always unlock this stuff in ballistica-core builds.
return bool(
_ba.get_purchased('upgrades.pro')
or _ba.get_purchased('static.pro')
@ -191,8 +191,8 @@ class AccountSubsystem:
def have_pro_options(self) -> bool:
"""Return whether pro-options are present.
This is True for owners of Pro or old installs
before Pro was a requirement for these.
This is True for owners of Pro or for old installs
before Pro was a requirement for these options.
"""
# We expose pro options if the server tells us to

View file

@ -161,9 +161,10 @@ class Actor:
"""Returns whether the Actor is 'alive'.
What this means is up to the Actor.
It is not a requirement for Actors to be
able to die; just that they report whether
they are Alive or not.
It is not a requirement for Actors to be able to die;
just that they report whether they consider themselves
to be alive or not. In cases where dead/alive is
irrelevant, True should be returned.
"""
return True

View file

@ -3,6 +3,7 @@
"""Functionality related to the high level state of the app."""
from __future__ import annotations
from enum import Enum
import random
from typing import TYPE_CHECKING
@ -15,6 +16,7 @@ from ba._plugin import PluginSubsystem
from ba._account import AccountSubsystem
from ba._meta import MetadataSubsystem
from ba._ads import AdsSubsystem
from ba._net import NetworkSubsystem
if TYPE_CHECKING:
import ba
@ -32,8 +34,16 @@ class App:
Note that properties not documented here should be considered internal
and subject to change without warning.
"""
# pylint: disable=too-many-public-methods
class State(Enum):
"""High level state the app can be in."""
LAUNCHING = 0
RUNNING = 1
PAUSED = 2
SHUTTING_DOWN = 3
@property
def build_number(self) -> int:
"""Integer build number.
@ -172,6 +182,8 @@ class App:
"""
# pylint: disable=too-many-statements
self.state = self.State.LAUNCHING
# Config.
self.config_file_healthy = False
@ -199,7 +211,6 @@ class App:
self.tips: List[str] = []
self.stress_test_reset_timer: Optional[ba.Timer] = None
self.did_weak_call_warning = False
self.ran_on_app_launch = False
self.log_have_new = False
self.log_upload_timer_started = False
@ -227,6 +238,7 @@ class App:
self.ach = AchievementSubsystem()
self.ui = UISubsystem()
self.ads = AdsSubsystem()
self.net = NetworkSubsystem()
# Lobby.
self.lobby_random_profile_index: int = 1
@ -281,7 +293,6 @@ class App:
from ba._enums import TimeType
import custom_hooks
custom_hooks.on_app_launch()
cfg = self.config
self.delegate = appdelegate.AppDelegate()
@ -376,15 +387,35 @@ class App:
self.accounts.on_app_launch()
self.plugins.on_app_launch()
self.ran_on_app_launch = True
self.state = self.State.RUNNING
# from ba._dependency import test_depset
# test_depset()
def on_app_pause(self) -> None:
"""Called when the app goes to a suspended state."""
self.state = self.State.PAUSED
self.plugins.on_app_pause()
def on_app_resume(self) -> None:
"""Run when the app resumes from a suspended state."""
self.state = self.State.RUNNING
self.fg_state += 1
self.accounts.on_app_resume()
self.music.on_app_resume()
self.plugins.on_app_resume()
def on_app_shutdown(self) -> None:
"""(internal)"""
self.state = self.State.SHUTTING_DOWN
self.music.on_app_shutdown()
self.plugins.on_app_shutdown()
def read_config(self) -> None:
"""(internal)"""
from ba import _appconfig
self._config, self.config_file_healthy = _appconfig.read_config()
from ba._appconfig import read_config
self._config, self.config_file_healthy = read_config()
def pause(self) -> None:
"""Pause the game due to a user request or menu popping up.
@ -484,16 +515,6 @@ class App:
else:
self.main_menu_resume_callbacks.append(call)
def on_app_pause(self) -> None:
"""Called when the app goes to a suspended state."""
def on_app_resume(self) -> None:
"""Run when the app resumes from a suspended state."""
self.fg_state += 1
self.accounts.on_app_resume()
self.music.on_app_resume()
def launch_coop_game(self,
game: str,
force: bool = False,
@ -542,10 +563,6 @@ class App:
_ba.fade_screen(False, endcall=_fade_end)
return True
def on_app_shutdown(self) -> None:
"""(internal)"""
self.music.on_app_shutdown()
def handle_deep_link(self, url: str) -> None:
"""Handle a deep link URL."""
from ba._language import Lstr

View file

@ -56,7 +56,7 @@ def handle_log() -> None:
When this happens, we can upload our log to the server
after a short bit if desired.
"""
from ba._netutils import master_server_post
from ba._net import master_server_post
from ba._enums import TimeType
app = _ba.app
app.log_have_new = True
@ -121,7 +121,7 @@ def handle_leftover_log_file() -> None:
"""Handle an un-uploaded log from a previous run."""
try:
import json
from ba._netutils import master_server_post
from ba._net import master_server_post
if os.path.exists(_ba.get_log_file_path()):
with open(_ba.get_log_file_path()) as infile:

View file

@ -158,6 +158,7 @@ def fetch_url(url: str, filename: Path, asset_gather: AssetGather) -> None:
"""Fetch a given url to a given filename for a given AssetGather.
"""
# pylint: disable=consider-using-with
import socket

View file

@ -307,7 +307,6 @@ def party_invite_revoke(invite_id: str) -> None:
_ba.containerwidget(edit=win.get_root_widget(),
transition='out_right')
import custom_hooks as chooks
def filter_chat_message(msg: str, client_id: int) -> Optional[str]:
"""Intercept/filter chat messages.
@ -317,10 +316,8 @@ def filter_chat_message(msg: str, client_id: int) -> Optional[str]:
Should filter and return the string to be displayed, or return None
to ignore the message.
"""
return chooks.filter_chat_message(msg,client_id)
def local_chat_message(msg: str) -> None:

View file

@ -215,11 +215,10 @@ class Map(Actor):
# I DONT THINK YOU REALLY WANT TO REMOVE MY NAME , DO YOU ?
self.hg=ba.NodeActor(
_ba.newnode('text',
attrs={
'text': "Smoothy Build\n v1.0",
'text': "Smoothy Build\n v1.2",
'flatness': 1.0,
'h_align': 'center',
@ -229,8 +228,6 @@ class Map(Actor):
'position':(-60,23),
'color':(0.3,0.3,0.3)
}))
# Set area-of-interest bounds.
aoi_bounds = self.get_def_bound_box('area_of_interest_bounds')
if aoi_bounds is None:
@ -297,7 +294,10 @@ class Map(Actor):
self.is_flying = False
# FIXME: this should be part of game; not map.
self._next_ffa_start_index = 0
# Let's select random index for first spawn point,
# so that no one is offended by the constant spawn on the edge.
self._next_ffa_start_index = random.randrange(
len(self.ffa_spawn_points))
def is_point_near_edge(self,
point: ba.Vec3,

190
dist/ba_data/python/ba/_net.py vendored Normal file
View file

@ -0,0 +1,190 @@
# Released under the MIT License. See LICENSE for details.
#
"""Networking related functionality."""
from __future__ import annotations
import copy
import threading
import weakref
from enum import Enum
from typing import TYPE_CHECKING
import _ba
if TYPE_CHECKING:
from typing import Any, Dict, Union, Callable, Optional
import socket
import ba
MasterServerCallback = Callable[[Union[None, Dict[str, Any]]], None]
# Timeout for standard functions talking to the master-server/etc.
DEFAULT_REQUEST_TIMEOUT_SECONDS = 60
class NetworkSubsystem:
"""Network related app subsystem."""
def __init__(self) -> None:
self.region_pings: Dict[str, float] = {}
def get_ip_address_type(addr: str) -> socket.AddressFamily:
"""Return socket.AF_INET6 or socket.AF_INET4 for the provided address."""
import socket
socket_type = None
# First try it as an ipv4 address.
try:
socket.inet_pton(socket.AF_INET, addr)
socket_type = socket.AF_INET
except OSError:
pass
# Hmm apparently not ipv4; try ipv6.
if socket_type is None:
try:
socket.inet_pton(socket.AF_INET6, addr)
socket_type = socket.AF_INET6
except OSError:
pass
if socket_type is None:
raise ValueError(f'addr seems to be neither v4 or v6: {addr}')
return socket_type
class MasterServerResponseType(Enum):
"""How to interpret responses from the master-server."""
JSON = 0
class MasterServerCallThread(threading.Thread):
"""Thread to communicate with the master-server."""
def __init__(self, request: str, request_type: str,
data: Optional[Dict[str, Any]],
callback: Optional[MasterServerCallback],
response_type: MasterServerResponseType):
super().__init__()
self._request = request
self._request_type = request_type
if not isinstance(response_type, MasterServerResponseType):
raise TypeError(f'Invalid response type: {response_type}')
self._response_type = response_type
self._data = {} if data is None else copy.deepcopy(data)
self._callback: Optional[MasterServerCallback] = callback
self._context = _ba.Context('current')
# Save and restore the context we were created from.
activity = _ba.getactivity(doraise=False)
self._activity = weakref.ref(
activity) if activity is not None else None
def _run_callback(self, arg: Union[None, Dict[str, Any]]) -> None:
# If we were created in an activity context and that activity has
# since died, do nothing.
# FIXME: Should we just be using a ContextCall instead of doing
# this check manually?
if self._activity is not None:
activity = self._activity()
if activity is None or activity.expired:
return
# Technically we could do the same check for session contexts,
# but not gonna worry about it for now.
assert self._context is not None
assert self._callback is not None
with self._context:
self._callback(arg)
def run(self) -> None:
# pylint: disable=too-many-branches, consider-using-with
import urllib.request
import urllib.error
import json
from efro.net import is_urllib_network_error
from ba import _general
try:
self._data = _general.utf8_all(self._data)
_ba.set_thread_name('BA_ServerCallThread')
parse = urllib.parse
if self._request_type == 'get':
response = urllib.request.urlopen(
urllib.request.Request(
(_ba.get_master_server_address() + '/' +
self._request + '?' + parse.urlencode(self._data)),
None, {'User-Agent': _ba.app.user_agent_string}),
timeout=DEFAULT_REQUEST_TIMEOUT_SECONDS)
elif self._request_type == 'post':
response = urllib.request.urlopen(
urllib.request.Request(
_ba.get_master_server_address() + '/' + self._request,
parse.urlencode(self._data).encode(),
{'User-Agent': _ba.app.user_agent_string}),
timeout=DEFAULT_REQUEST_TIMEOUT_SECONDS)
else:
raise TypeError('Invalid request_type: ' + self._request_type)
# If html request failed.
if response.getcode() != 200:
response_data = None
elif self._response_type == MasterServerResponseType.JSON:
raw_data = response.read()
# Empty string here means something failed server side.
if raw_data == b'':
response_data = None
else:
response_data = json.loads(raw_data)
else:
raise TypeError(f'invalid responsetype: {self._response_type}')
except Exception as exc:
do_print = False
response_data = None
# Ignore common network errors; note unexpected ones.
if is_urllib_network_error(exc):
pass
elif (self._response_type == MasterServerResponseType.JSON
and isinstance(exc, json.decoder.JSONDecodeError)):
# FIXME: should handle this better; could mean either the
# server sent us bad data or it got corrupted along the way.
pass
else:
do_print = True
if do_print:
# Any other error here is unexpected,
# so let's make a note of it,
print(f'Error in MasterServerCallThread'
f' (response-type={self._response_type},'
f' response-data={response_data}):')
import traceback
traceback.print_exc()
if self._callback is not None:
_ba.pushcall(_general.Call(self._run_callback, response_data),
from_other_thread=True)
def master_server_get(
request: str,
data: Dict[str, Any],
callback: Optional[MasterServerCallback] = None,
response_type: MasterServerResponseType = MasterServerResponseType.JSON
) -> None:
"""Make a call to the master server via a http GET."""
MasterServerCallThread(request, 'get', data, callback,
response_type).start()
def master_server_post(
request: str,
data: Dict[str, Any],
callback: Optional[MasterServerCallback] = None,
response_type: MasterServerResponseType = MasterServerResponseType.JSON
) -> None:
"""Make a call to the master server via a http POST."""
MasterServerCallThread(request, 'post', data, callback,
response_type).start()

View file

@ -6,6 +6,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING
from dataclasses import dataclass
import _ba
if TYPE_CHECKING:
@ -36,6 +37,33 @@ class PluginSubsystem:
from ba import _error
_error.print_exception('Error in plugin on_app_launch()')
def on_app_pause(self) -> None:
"""Called when the app goes to a suspended state."""
for plugin in self.active_plugins.values():
try:
plugin.on_app_pause()
except Exception:
from ba import _error
_error.print_exception('Error in plugin on_app_pause()')
def on_app_resume(self) -> None:
"""Run when the app resumes from a suspended state."""
for plugin in self.active_plugins.values():
try:
plugin.on_app_resume()
except Exception:
from ba import _error
_error.print_exception('Error in plugin on_app_resume()')
def on_app_shutdown(self) -> None:
"""Called when the app is being closed."""
for plugin in self.active_plugins.values():
try:
plugin.on_app_shutdown()
except Exception:
from ba import _error
_error.print_exception('Error in plugin on_app_shutdown()')
def load_plugins(self) -> None:
"""(internal)"""
from ba._general import getclass
@ -94,3 +122,12 @@ class Plugin:
def on_app_launch(self) -> None:
"""Called when the app is being launched."""
def on_app_pause(self) -> None:
"""Called after pausing game activity."""
def on_app_resume(self) -> None:
"""Called after the game continues."""
def on_app_shutdown(self) -> None:
"""Called before closing the application."""

View file

@ -19,6 +19,7 @@ from ba._dualteamsession import DualTeamSession
if TYPE_CHECKING:
from typing import Optional, Dict, Any, Type
import ba
from bacommon.servermanager import ServerConfig
@ -186,7 +187,7 @@ class ServerController:
def _run_access_check(self) -> None:
"""Check with the master server to see if we're likely joinable."""
from ba._netutils import master_server_get
from ba._net import master_server_get
master_server_get(
'bsAccessCheck',
{
@ -301,6 +302,28 @@ class ServerController:
print('WARNING: launch_server_session() expects to run '
'with a signed in server account')
# If we didn't fetch a playlist but there's an inline one in the
# server-config, pull it in to the game config and use it.
if (self._config.playlist_code is None
and self._config.playlist_inline is not None):
self._playlist_name = 'ServerModePlaylist'
if sessiontype is FreeForAllSession:
ptypename = 'Free-for-All'
elif sessiontype is DualTeamSession:
ptypename = 'Team Tournament'
else:
raise RuntimeError(f'Unknown session type {sessiontype}')
# Need to add this in a transaction instead of just setting
# it directly or it will get overwritten by the master-server.
_ba.add_transaction({
'type': 'ADD_PLAYLIST',
'playlistType': ptypename,
'playlistName': self._playlist_name,
'playlist': self._config.playlist_inline
})
_ba.run_transactions()
if self._first_run:
curtimestr = time.strftime('%c')
_ba.log(

View file

@ -17,7 +17,7 @@ if TYPE_CHECKING:
class Session:
"""Defines a high level series of activities with a common purpose.
"""Defines a high level series of ba.Activities with a common purpose.
category: Gameplay Classes
@ -99,6 +99,7 @@ class Session:
# pylint: disable=too-many-statements
# pylint: disable=too-many-locals
# pylint: disable=cyclic-import
# pylint: disable=too-many-branches
from ba._lobby import Lobby
from ba._stats import Stats
from ba._gameactivity import GameActivity
@ -172,8 +173,16 @@ class Session:
# Create static teams if we're using them.
if self.use_teams:
assert team_names is not None
assert team_colors is not None
if team_names is None:
raise RuntimeError(
'use_teams is True but team_names not provided.')
if team_colors is None:
raise RuntimeError(
'use_teams is True but team_colors not provided.')
if len(team_colors) != len(team_names):
raise RuntimeError(f'Got {len(team_names)} team_names'
f' and {len(team_colors)} team_colors;'
f' these numbers must match.')
for i, color in enumerate(team_colors):
team = SessionTeam(team_id=self._next_team_id,
name=GameActivity.get_team_display_string(

View file

@ -25,8 +25,8 @@ from ba._campaign import getcampaign
from ba._messages import PlayerProfilesChangedMessage
from ba._multiteamsession import DEFAULT_TEAM_COLORS, DEFAULT_TEAM_NAMES
from ba._music import do_play_music
from ba._netutils import (master_server_get, master_server_post,
get_ip_address_type)
from ba._net import (master_server_get, master_server_post,
get_ip_address_type, DEFAULT_REQUEST_TIMEOUT_SECONDS)
from ba._powerup import get_default_powerup_distribution
from ba._profile import (get_player_profile_colors, get_player_profile_icon,
get_player_colors)

Binary file not shown.

69
dist/ba_data/python/bacommon/net.py vendored Normal file
View file

@ -0,0 +1,69 @@
# Released under the MIT License. See LICENSE for details.
#
"""Network related data and functionality."""
from __future__ import annotations
from typing import TYPE_CHECKING, Optional, List, Dict, Any, Tuple
from dataclasses import dataclass
from efro import entity
from efro.dataclassio import ioprepped
if TYPE_CHECKING:
pass
class ServerNodeEntry(entity.CompoundValue):
"""Information about a specific server."""
region = entity.Field('r', entity.StringValue())
address = entity.Field('a', entity.StringValue())
port = entity.Field('p', entity.IntValue())
class ServerNodeQueryResponse(entity.Entity):
"""A response to a query about server-nodes."""
# If present, something went wrong, and this describes it.
error = entity.Field('e', entity.OptionalStringValue(store_default=False))
# The set of servernodes.
servers = entity.CompoundListField('s',
ServerNodeEntry(),
store_default=False)
@ioprepped
@dataclass
class PrivateHostingState:
"""Combined state of whether we're hosting, whether we can, etc."""
unavailable_error: Optional[str] = None
party_code: Optional[str] = None
able_to_host: bool = False
tickets_to_host_now: int = 0
minutes_until_free_host: Optional[float] = None
free_host_minutes_remaining: Optional[float] = None
@ioprepped
@dataclass
class PrivateHostingConfig:
"""Config provided when hosting a private party."""
session_type: str = 'ffa'
playlist_name: str = 'Unknown'
randomize: bool = False
tutorial: bool = False
custom_team_names: Optional[Tuple[str, str]] = None
custom_team_colors: Optional[Tuple[Tuple[float, float, float],
Tuple[float, float, float]]] = None
playlist: Optional[List[Dict[str, Any]]] = None
@ioprepped
@dataclass
class PrivatePartyConnectResult:
"""Info about a server we get back when connecting."""
error: Optional[str] = None
addr: Optional[str] = None
port: Optional[int] = None
password: Optional[str] = None

View file

@ -4,13 +4,16 @@
from __future__ import annotations
from enum import Enum
from dataclasses import dataclass, field
from typing import TYPE_CHECKING
from dataclasses import field, dataclass
from typing import TYPE_CHECKING, List, Optional, Tuple, Dict, Any
from efro.dataclassio import ioprepped
if TYPE_CHECKING:
from typing import Optional, Tuple, List
pass
@ioprepped
@dataclass
class ServerConfig:
"""Configuration for the server manager app (<appname>_server)."""
@ -49,8 +52,7 @@ class ServerConfig:
max_party_size: int = 6
# Options here are 'ffa' (free-for-all) and 'teams'
# This value is only used if you do not supply a playlist_code (see below).
# In that case the default teams or free-for-all playlist gets used.
# This value is ignored if you supply a playlist_code (see below).
session_type: str = 'ffa'
# To host your own custom playlists, use the 'share' functionality in the
@ -59,6 +61,10 @@ class ServerConfig:
# playlist.
playlist_code: Optional[int] = None
# Alternately, you can embed playlist data here instead of using codes.
# Make sure to set session_type to the correct type for the data here.
playlist_inline: Optional[List[Dict[str, Any]]] = None
# Whether to shuffle the playlist or play its games in designated order.
playlist_shuffle: bool = True
@ -82,12 +88,12 @@ class ServerConfig:
# performance)
ffa_series_length: int = 24
# If you provide a custom stats webpage for your server, you can use
# this to provide a convenient in-game link to it in the server-browser
# beside the server name.
# If you have a custom stats webpage for your server, you can use this
# to provide a convenient in-game link to it in the server-browser
# alongside the server name.
# if ${ACCOUNT} is present in the string, it will be replaced by the
# currently-signed-in account's id. To fetch info about an account,
# your backend server can use the following url:
# your back-end server can use the following url:
# http://bombsquadgame.com/accountquery?id=ACCOUNT_ID_HERE
stats_url: Optional[str] = None
@ -107,10 +113,20 @@ class ServerConfig:
# If present, the server subprocess will shut down immediately if this
# amount of time passes with no activity from any players. The server
# manager will then spin up a fresh server subprocess if
# auto-restart is enabled (the default).
# manager will then spin up a fresh server subprocess if auto-restart is
# enabled (the default).
idle_exit_minutes: Optional[float] = None
# Should the tutorial be shown at the beginning of games?
show_tutorial: bool = False
# Team names (teams mode only).
team_names: Optional[Tuple[str, str]] = None
# Team colors (teams mode only).
team_colors: Optional[Tuple[Tuple[float, float, float],
Tuple[float, float, float]]] = None
# (internal) stress-testing mode.
stress_test_players: Optional[int] = None

View file

@ -483,6 +483,13 @@ class EliminationGame(ba.TeamGameActivity[Player, Team]):
# list then.
ba.timer(0, self._update_icons)
# If the player to leave was the last in spawn order and had
# their final turn currently in-progress, mark the survival time
# for their team.
if self._get_total_team_lives(player.team) == 0:
assert self._start_time is not None
player.team.survival_seconds = int(ba.time() - self._start_time)
def _get_total_team_lives(self, team: Team) -> int:
return sum(player.lives for player in team.players)

View file

@ -60,7 +60,7 @@ class MainMenuActivity(ba.Activity[ba.Player, ba.Team]):
'scale': scale,
'position': (0, 10),
'vr_depth': -10,
'text': '\xa9 2011-2020 Eric Froemling'
'text': '\xa9 2011-2021 Eric Froemling'
}))
# Throw up some text that only clients can see so they know that the

View file

@ -34,8 +34,7 @@ class ConfirmWindow:
if cancel_text is None:
cancel_text = ba.Lstr(resource='cancelText')
height += 40
if width < 360:
width = 360
width = max(width, 360)
self._action = action
# if they provided an origin-widget, scale up from that

View file

@ -633,11 +633,8 @@ class ManualGatherTab(GatherTab):
from_other_thread=True,
)
except Exception as exc:
err_str = str(exc)
# FIXME: Should look at exception types here,
# not strings.
if 'Network is unreachable' in err_str:
from efro.net import is_udp_network_error
if is_udp_network_error(exc):
ba.pushcall(ba.Call(
_safe_set_text, self._checking_state_text,
ba.Lstr(resource='gatherWindow.'

View file

@ -8,12 +8,14 @@ import os
import copy
import time
from enum import Enum
from dataclasses import dataclass, asdict
from dataclasses import dataclass
from typing import TYPE_CHECKING, cast
import ba
import _ba
from efro.dataclasses import dataclass_from_dict
from efro.dataclassio import dataclass_from_dict, dataclass_to_dict
from bacommon.net import (PrivateHostingState, PrivateHostingConfig,
PrivatePartyConnectResult)
from bastd.ui.gather import GatherTab
from bastd.ui import getcurrency
@ -37,37 +39,6 @@ class State:
sub_tab: SubTabType = SubTabType.JOIN
@dataclass
class ConnectResult:
"""Info about a server we get back when connecting."""
error: Optional[str] = None
addr: Optional[str] = None
port: Optional[int] = None
@dataclass
class HostingState:
"""Our combined state of whether we're hosting, whether we can, etc."""
unavailable_error: Optional[str] = None
party_code: Optional[str] = None
able_to_host: bool = False
tickets_to_host_now: int = 0
minutes_until_free_host: Optional[float] = None
free_host_minutes_remaining: Optional[float] = None
@dataclass
class HostingConfig:
"""Config we provide when hosting."""
session_type: str = 'ffa'
playlist_name: str = 'Unknown'
randomize: bool = False
tutorial: bool = False
custom_team_names: Optional[List[str]] = None
custom_team_colors: Optional[List[List[float]]] = None
playlist: Optional[List[Dict[str, Any]]] = None
class PrivateGatherTab(GatherTab):
"""The private tab in the gather UI"""
@ -75,7 +46,7 @@ class PrivateGatherTab(GatherTab):
super().__init__(window)
self._container: Optional[ba.Widget] = None
self._state: State = State()
self._hostingstate = HostingState()
self._hostingstate = PrivateHostingState()
self._join_sub_tab_text: Optional[ba.Widget] = None
self._host_sub_tab_text: Optional[ba.Widget] = None
self._update_timer: Optional[ba.Timer] = None
@ -99,7 +70,7 @@ class PrivateGatherTab(GatherTab):
self._hostingconfig = self._build_hosting_config()
except Exception:
ba.print_exception('Error building hosting config')
self._hostingconfig = HostingConfig()
self._hostingconfig = PrivateHostingConfig()
def on_activate(
self,
@ -178,10 +149,11 @@ class PrivateGatherTab(GatherTab):
return self._container
def _build_hosting_config(self) -> HostingConfig:
def _build_hosting_config(self) -> PrivateHostingConfig:
# pylint: disable=too-many-branches
from bastd.ui.playlist import PlaylistTypeVars
from ba.internal import filter_playlist
hcfg = HostingConfig()
hcfg = PrivateHostingConfig()
cfg = ba.app.config
sessiontypestr = cfg.get('Private Party Host Session Type', 'ffa')
if not isinstance(sessiontypestr, str):
@ -205,10 +177,13 @@ class PrivateGatherTab(GatherTab):
if playlist_name == '__default__' else
playlist_name)
if playlist_name == '__default__':
playlist: Optional[List[Dict[str, Any]]] = None
if playlist_name != '__default__':
playlist = (cfg.get(f'{pvars.config_name} Playlists',
{}).get(playlist_name))
if playlist is None:
playlist = pvars.get_default_list_call()
else:
playlist = cfg[f'{pvars.config_name} Playlists'][playlist_name]
hcfg.playlist = filter_playlist(playlist, sessiontype)
randomize = cfg.get(f'{pvars.config_name} Playlist Randomize')
@ -218,12 +193,29 @@ class PrivateGatherTab(GatherTab):
tutorial = cfg.get('Show Tutorial')
if not isinstance(tutorial, bool):
tutorial = False
tutorial = True
hcfg.tutorial = tutorial
if hcfg.session_type == 'teams':
hcfg.custom_team_names = copy.copy(cfg.get('Custom Team Names'))
hcfg.custom_team_colors = copy.copy(cfg.get('Custom Team Colors'))
ctn: Optional[List[str]] = cfg.get('Custom Team Names')
if ctn is not None:
if (isinstance(ctn, (list, tuple)) and len(ctn) == 2
and all(isinstance(x, str) for x in ctn)):
hcfg.custom_team_names = (ctn[0], ctn[1])
else:
print(f'Found invalid custom-team-names data: {ctn}')
ctc: Optional[List[List[float]]] = cfg.get('Custom Team Colors')
if ctc is not None:
if (isinstance(ctc, (list, tuple)) and len(ctc) == 2
and all(isinstance(x, (list, tuple)) for x in ctc)
and all(len(x) == 3 for x in ctc)):
hcfg.custom_team_colors = ((ctc[0][0], ctc[0][1],
ctc[0][2]),
(ctc[1][0], ctc[1][1],
ctc[1][2]))
else:
print(f'Found invalid custom-team-colors data: {ctc}')
return hcfg
@ -297,13 +289,15 @@ class PrivateGatherTab(GatherTab):
if not self._container:
return
state: Optional[HostingState] = None
state: Optional[PrivateHostingState] = None
if result is not None:
self._debug_server_comm('got private party state response')
try:
state = dataclass_from_dict(HostingState, result)
state = dataclass_from_dict(PrivateHostingState,
result,
discard_unknown_attrs=True)
except Exception:
pass
ba.print_exception('Got invalid PrivateHostingState data')
else:
self._debug_server_comm('private party state response errored')
@ -788,6 +782,8 @@ class PrivateGatherTab(GatherTab):
ba.playsound(ba.getsound('error'))
return
ba.playsound(ba.getsound('click01'))
# If we're not hosting, start.
if self._hostingstate.party_code is None:
@ -808,7 +804,8 @@ class PrivateGatherTab(GatherTab):
_ba.add_transaction(
{
'type': 'PRIVATE_PARTY_START',
'config': asdict(self._hostingconfig)
'config': dataclass_to_dict(self._hostingconfig),
'region_pings': ba.app.net.region_pings,
},
callback=ba.WeakCall(self._hosting_state_response))
_ba.run_transactions()
@ -844,7 +841,9 @@ class PrivateGatherTab(GatherTab):
self._connect_press_time = None
if result is None:
raise RuntimeError()
cresult = dataclass_from_dict(ConnectResult, result)
cresult = dataclass_from_dict(PrivatePartyConnectResult,
result,
discard_unknown_attrs=True)
if cresult.error is not None:
self._debug_server_comm('got error connect response')
ba.screenmessage(

View file

@ -219,9 +219,9 @@ class AddrFetchThread(threading.Thread):
sock.close()
ba.pushcall(ba.Call(self._call, val), from_other_thread=True)
except Exception as exc:
from efro.net import is_udp_network_error
# Ignore expected network errors; log others.
import errno
if isinstance(exc, OSError) and exc.errno == errno.ENETUNREACH:
if is_udp_network_error(exc):
pass
else:
ba.print_exception()
@ -238,8 +238,6 @@ class PingThread(threading.Thread):
self._call = call
def run(self) -> None:
# pylint: disable=too-many-branches
# pylint: disable=too-many-statements
ba.app.ping_thread_count += 1
sock: Optional[socket.socket] = None
try:
@ -272,39 +270,12 @@ class PingThread(threading.Thread):
ba.pushcall(ba.Call(self._call, self._address, self._port,
ping if accessible else None),
from_other_thread=True)
except ConnectionRefusedError:
# Fine, server; sorry we pinged you. Hmph.
pass
except OSError as exc:
import errno
# Ignore harmless errors.
if exc.errno in {
errno.EHOSTUNREACH, errno.ENETUNREACH, errno.EINVAL,
errno.EPERM, errno.EACCES
}:
except Exception as exc:
from efro.net import is_udp_network_error
if is_udp_network_error(exc):
pass
elif exc.errno == 10022:
# Windows 'invalid argument' error.
pass
elif exc.errno == 10051:
# Windows 'a socket operation was attempted
# to an unreachable network' error.
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(
f'Error on gather ping '
f'(errno={exc.errno})', once=True)
except Exception:
ba.print_exception('Error on gather ping', once=True)
ba.print_exception('Error on gather ping', once=True)
finally:
try:
if sock is not None:
@ -1261,10 +1232,7 @@ class PublicGatherTab(GatherTab):
self._have_user_selected_row = True
def _on_max_public_party_size_minus_press(self) -> None:
val = _ba.get_public_party_max_size()
val -= 1
if val < 1:
val = 1
val = max(1, _ba.get_public_party_max_size() - 1)
_ba.set_public_party_max_size(val)
ba.textwidget(edit=self._host_max_party_size_value, text=str(val))

View file

@ -372,7 +372,7 @@ class PartyWindow(ba.Window):
cfg.apply_and_commit()
self._update()
else:
print('unhandled popup type: ' + str(self._popup_type))
print(f'unhandled popup type: {self._popup_type}')
def popup_menu_closing(self, popup_window: popup.PopupWindow) -> None:
"""Called when the popup is closing."""

Binary file not shown.

1028
dist/ba_data/python/efro/dataclassio.py vendored Normal file

File diff suppressed because it is too large Load diff

View file

@ -7,6 +7,8 @@ from __future__ import annotations
from enum import Enum
from typing import TYPE_CHECKING
from efro.util import enum_by_value
if TYPE_CHECKING:
from typing import Any, Type
@ -17,14 +19,24 @@ def dict_key_to_raw(key: Any, keytype: Type) -> Any:
raise TypeError(
f'Invalid key type; expected {keytype}, got {type(key)}.')
if issubclass(keytype, Enum):
return key.value
val = key.value
# We convert int enums to string since that is what firestore supports.
if isinstance(val, int):
val = str(val)
return val
return key
def dict_key_from_raw(key: Any, keytype: Type) -> Any:
"""Given internal key, filter to world visible type."""
if issubclass(keytype, Enum):
return keytype(key)
# We store all enum keys as strings; if the enum uses
# int keys, convert back.
for enumval in keytype:
if isinstance(enumval.value, int):
return enum_by_value(keytype, int(key))
break
return enum_by_value(keytype, key)
return key

Some files were not shown because too many files have changed in this diff Show more