mirror of
https://github.com/imayushsaini/Bombsquad-Ballistica-Modded-Server.git
synced 2025-11-14 17:46:03 +00:00
updated to 1.7.20
This commit is contained in:
parent
0dfe2de8e5
commit
dab1db4141
67 changed files with 5236 additions and 2272 deletions
2
dist/ba_data/python/ba/__init__.py
vendored
2
dist/ba_data/python/ba/__init__.py
vendored
|
|
@ -102,6 +102,7 @@ from ba._error import (
|
|||
WidgetNotFoundError,
|
||||
ActivityNotFoundError,
|
||||
TeamNotFoundError,
|
||||
MapNotFoundError,
|
||||
SessionTeamNotFoundError,
|
||||
SessionNotFoundError,
|
||||
DelegateNotFoundError,
|
||||
|
|
@ -282,6 +283,7 @@ __all__ = [
|
|||
'Lobby',
|
||||
'Lstr',
|
||||
'Map',
|
||||
'MapNotFoundError',
|
||||
'Material',
|
||||
'MetadataSubsystem',
|
||||
'Model',
|
||||
|
|
|
|||
43
dist/ba_data/python/ba/_accountv2.py
vendored
43
dist/ba_data/python/ba/_accountv2.py
vendored
|
|
@ -36,7 +36,7 @@ class AccountV2Subsystem:
|
|||
# (or lack thereof) has completed. This includes things like
|
||||
# workspace syncing. Completion of this is what flips the app
|
||||
# into 'running' state.
|
||||
self._initial_login_completed = False
|
||||
self._initial_sign_in_completed = False
|
||||
|
||||
self._kicked_off_workspace_load = False
|
||||
|
||||
|
|
@ -98,7 +98,7 @@ class AccountV2Subsystem:
|
|||
if account.workspaceid is not None:
|
||||
assert account.workspacename is not None
|
||||
if (
|
||||
not self._initial_login_completed
|
||||
not self._initial_sign_in_completed
|
||||
and not self._kicked_off_workspace_load
|
||||
):
|
||||
self._kicked_off_workspace_load = True
|
||||
|
|
@ -121,9 +121,9 @@ class AccountV2Subsystem:
|
|||
return
|
||||
|
||||
# Ok; no workspace to worry about; carry on.
|
||||
if not self._initial_login_completed:
|
||||
self._initial_login_completed = True
|
||||
_ba.app.on_initial_login_completed()
|
||||
if not self._initial_sign_in_completed:
|
||||
self._initial_sign_in_completed = True
|
||||
_ba.app.on_initial_sign_in_completed()
|
||||
|
||||
def on_active_logins_changed(self, logins: dict[LoginType, str]) -> None:
|
||||
"""Should be called when logins for the active account change."""
|
||||
|
|
@ -156,9 +156,9 @@ class AccountV2Subsystem:
|
|||
within a few seconds of app launch; the app can move forward
|
||||
with the startup sequence at that point.
|
||||
"""
|
||||
if not self._initial_login_completed:
|
||||
self._initial_login_completed = True
|
||||
_ba.app.on_initial_login_completed()
|
||||
if not self._initial_sign_in_completed:
|
||||
self._initial_sign_in_completed = True
|
||||
_ba.app.on_initial_sign_in_completed()
|
||||
|
||||
@staticmethod
|
||||
def _hashstr(val: str) -> str:
|
||||
|
|
@ -271,7 +271,7 @@ class AccountV2Subsystem:
|
|||
self._implicit_state_changed = False
|
||||
|
||||
# Once we've made a move here we don't want to
|
||||
# do any more automatic ones.
|
||||
# do any more automatic stuff.
|
||||
self._can_do_auto_sign_in = False
|
||||
|
||||
else:
|
||||
|
|
@ -290,22 +290,23 @@ class AccountV2Subsystem:
|
|||
' of implicit state change...',
|
||||
)
|
||||
self._implicit_signed_in_adapter.sign_in(
|
||||
self._on_explicit_sign_in_completed
|
||||
self._on_explicit_sign_in_completed,
|
||||
description='implicit state change',
|
||||
)
|
||||
self._implicit_state_changed = False
|
||||
|
||||
# Once we've made a move here we don't want to
|
||||
# do any more automatic ones.
|
||||
# do any more automatic stuff.
|
||||
self._can_do_auto_sign_in = False
|
||||
|
||||
if not self._can_do_auto_sign_in:
|
||||
return
|
||||
|
||||
# If we're not currently signed in, we have connectivity, and
|
||||
# we have an available implicit login, auto-sign-in with it.
|
||||
# we have an available implicit login, auto-sign-in with it once.
|
||||
# The implicit-state-change logic above should keep things
|
||||
# mostly in-sync, but due to connectivity or other issues that
|
||||
# might not always be the case. We prefer to keep people signed
|
||||
# mostly in-sync, but that might not always be the case due to
|
||||
# connectivity or other issues. We prefer to keep people signed
|
||||
# in as a rule, even if there are corner cases where this might
|
||||
# not be what they want (A user signing out and then restarting
|
||||
# may be auto-signed back in).
|
||||
|
|
@ -324,7 +325,7 @@ class AccountV2Subsystem:
|
|||
)
|
||||
self._can_do_auto_sign_in = False # Only ATTEMPT once
|
||||
self._implicit_signed_in_adapter.sign_in(
|
||||
self._on_implicit_sign_in_completed
|
||||
self._on_implicit_sign_in_completed, description='auto-sign-in'
|
||||
)
|
||||
|
||||
def _on_explicit_sign_in_completed(
|
||||
|
|
@ -337,8 +338,8 @@ class AccountV2Subsystem:
|
|||
|
||||
del adapter # Unused.
|
||||
|
||||
# Make some noise on errors since the user knows
|
||||
# a sign-in attempt is happening in this case.
|
||||
# Make some noise on errors since the user knows a
|
||||
# sign-in attempt is happening in this case (the 'explicit' part).
|
||||
if isinstance(result, Exception):
|
||||
# We expect the occasional communication errors;
|
||||
# Log a full exception for anything else though.
|
||||
|
|
@ -347,6 +348,8 @@ class AccountV2Subsystem:
|
|||
'Error on explicit accountv2 sign in attempt.',
|
||||
exc_info=result,
|
||||
)
|
||||
|
||||
# For now just show 'error'. Should do better than this.
|
||||
with _ba.Context('ui'):
|
||||
_ba.screenmessage(
|
||||
Lstr(resource='internal.signInErrorText'),
|
||||
|
|
@ -395,9 +398,9 @@ class AccountV2Subsystem:
|
|||
_ba.app.accounts_v2.set_primary_credentials(result.credentials)
|
||||
|
||||
def _on_set_active_workspace_completed(self) -> None:
|
||||
if not self._initial_login_completed:
|
||||
self._initial_login_completed = True
|
||||
_ba.app.on_initial_login_completed()
|
||||
if not self._initial_sign_in_completed:
|
||||
self._initial_sign_in_completed = True
|
||||
_ba.app.on_initial_sign_in_completed()
|
||||
|
||||
|
||||
class AccountV2Handle:
|
||||
|
|
|
|||
71
dist/ba_data/python/ba/_app.py
vendored
71
dist/ba_data/python/ba/_app.py
vendored
|
|
@ -20,6 +20,7 @@ from ba._meta import MetadataSubsystem
|
|||
from ba._ads import AdsSubsystem
|
||||
from ba._net import NetworkSubsystem
|
||||
from ba._workspace import WorkspaceSubsystem
|
||||
from ba._appcomponent import AppComponentSubsystem
|
||||
from ba import _internal
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
|
@ -58,20 +59,26 @@ class App:
|
|||
class State(Enum):
|
||||
"""High level state the app can be in."""
|
||||
|
||||
# Python-level systems being inited but should not interact.
|
||||
LAUNCHING = 0
|
||||
# The launch process has not yet begun.
|
||||
INITIAL = 0
|
||||
|
||||
# Initial account logins, workspace & asset downloads, etc.
|
||||
LOADING = 1
|
||||
# Our app subsystems are being inited but should not yet interact.
|
||||
LAUNCHING = 1
|
||||
|
||||
# Normal running state.
|
||||
RUNNING = 2
|
||||
# App subsystems are inited and interacting, but the app has not
|
||||
# yet embarked on a high level course of action. It is doing initial
|
||||
# account logins, workspace & asset downloads, etc. in order to
|
||||
# prepare for this.
|
||||
LOADING = 2
|
||||
|
||||
# App is backgrounded or otherwise suspended.
|
||||
PAUSED = 3
|
||||
# All pieces are in place and the app is now doing its thing.
|
||||
RUNNING = 3
|
||||
|
||||
# App is shutting down.
|
||||
SHUTTING_DOWN = 4
|
||||
# The app is backgrounded or otherwise suspended.
|
||||
PAUSED = 4
|
||||
|
||||
# The app is shutting down.
|
||||
SHUTTING_DOWN = 5
|
||||
|
||||
@property
|
||||
def aioloop(self) -> asyncio.AbstractEventLoop:
|
||||
|
|
@ -128,7 +135,7 @@ class App:
|
|||
|
||||
@property
|
||||
def debug_build(self) -> bool:
|
||||
"""Whether the game was compiled in debug mode.
|
||||
"""Whether the app was compiled in debug mode.
|
||||
|
||||
Debug builds generally run substantially slower than non-debug
|
||||
builds due to compiler optimizations being disabled and extra
|
||||
|
|
@ -232,11 +239,14 @@ class App:
|
|||
"""
|
||||
# pylint: disable=too-many-statements
|
||||
|
||||
self.state = self.State.LAUNCHING
|
||||
self.state = self.State.INITIAL
|
||||
|
||||
self._bootstrapping_completed = False
|
||||
self._called_on_app_launching = False
|
||||
self._launch_completed = False
|
||||
self._initial_login_completed = False
|
||||
self._initial_sign_in_completed = False
|
||||
self._meta_scan_completed = False
|
||||
self._called_on_app_loading = False
|
||||
self._called_on_app_running = False
|
||||
self._app_paused = False
|
||||
|
||||
|
|
@ -294,6 +304,7 @@ class App:
|
|||
# Server Mode.
|
||||
self.server: ba.ServerController | None = None
|
||||
|
||||
self.components = AppComponentSubsystem()
|
||||
self.meta = MetadataSubsystem()
|
||||
self.accounts_v1 = AccountV1Subsystem()
|
||||
self.plugins = PluginSubsystem()
|
||||
|
|
@ -342,10 +353,8 @@ class App:
|
|||
self.delegate: ba.AppDelegate | None = None
|
||||
self._asyncio_timer: ba.Timer | None = None
|
||||
|
||||
def on_app_launch(self) -> None:
|
||||
"""Runs after the app finishes low level bootstrapping.
|
||||
|
||||
(internal)"""
|
||||
def on_app_launching(self) -> None:
|
||||
"""Called when the app is first entering the launching state."""
|
||||
# pylint: disable=cyclic-import
|
||||
# pylint: disable=too-many-locals
|
||||
from ba import _asyncio
|
||||
|
|
@ -473,6 +482,9 @@ class App:
|
|||
self._launch_completed = True
|
||||
self._update_state()
|
||||
|
||||
def on_app_loading(self) -> None:
|
||||
"""Called when initially entering the loading state."""
|
||||
|
||||
def on_app_running(self) -> None:
|
||||
"""Called when initially entering the running state."""
|
||||
|
||||
|
|
@ -481,6 +493,13 @@ class App:
|
|||
# from ba._dependency import test_depset
|
||||
# test_depset()
|
||||
|
||||
def on_bootstrapping_completed(self) -> None:
|
||||
"""Called by the C++ layer once its ready to rock."""
|
||||
assert _ba.in_logic_thread()
|
||||
assert not self._bootstrapping_completed
|
||||
self._bootstrapping_completed = True
|
||||
self._update_state()
|
||||
|
||||
def on_meta_scan_complete(self) -> None:
|
||||
"""Called by meta-scan when it is done doing its thing."""
|
||||
assert _ba.in_logic_thread()
|
||||
|
|
@ -511,15 +530,25 @@ class App:
|
|||
self.plugins.on_app_resume()
|
||||
self.health_monitor.on_app_resume()
|
||||
|
||||
if self._initial_login_completed and self._meta_scan_completed:
|
||||
# Handle initially entering or returning to other states.
|
||||
if self._initial_sign_in_completed and self._meta_scan_completed:
|
||||
self.state = self.State.RUNNING
|
||||
if not self._called_on_app_running:
|
||||
self._called_on_app_running = True
|
||||
self.on_app_running()
|
||||
elif self._launch_completed:
|
||||
self.state = self.State.LOADING
|
||||
if not self._called_on_app_loading:
|
||||
self._called_on_app_loading = True
|
||||
self.on_app_loading()
|
||||
else:
|
||||
# Only thing left is launching. We shouldn't be getting
|
||||
# called before at least that is complete.
|
||||
assert self._bootstrapping_completed
|
||||
self.state = self.State.LAUNCHING
|
||||
if not self._called_on_app_launching:
|
||||
self._called_on_app_launching = True
|
||||
self.on_app_launching()
|
||||
|
||||
def on_app_pause(self) -> None:
|
||||
"""Called when the app goes to a suspended state."""
|
||||
|
|
@ -724,8 +753,8 @@ class App:
|
|||
_ba.screenmessage(Lstr(resource='errorText'), color=(1, 0, 0))
|
||||
_ba.playsound(_ba.getsound('error'))
|
||||
|
||||
def on_initial_login_completed(self) -> None:
|
||||
"""Callback to be run after initial login process (or lack thereof).
|
||||
def on_initial_sign_in_completed(self) -> None:
|
||||
"""Callback to be run after initial sign-in (or lack thereof).
|
||||
|
||||
This period includes things such as syncing account workspaces
|
||||
or other data so it may take a substantial amount of time.
|
||||
|
|
@ -736,5 +765,5 @@ class App:
|
|||
# (account workspaces).
|
||||
self.meta.start_extra_scan()
|
||||
|
||||
self._initial_login_completed = True
|
||||
self._initial_sign_in_completed = True
|
||||
self._update_state()
|
||||
|
|
|
|||
90
dist/ba_data/python/ba/_appcomponent.py
vendored
Normal file
90
dist/ba_data/python/ba/_appcomponent.py
vendored
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Provides the AppComponent class."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, TypeVar, cast
|
||||
|
||||
import _ba
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Callable, Any
|
||||
|
||||
T = TypeVar('T', bound=type)
|
||||
|
||||
|
||||
class AppComponentSubsystem:
|
||||
"""Subsystem for wrangling AppComponents.
|
||||
|
||||
Category: **App Classes**
|
||||
|
||||
This subsystem acts as a registry for classes providing particular
|
||||
functionality for the app, and allows plugins or other custom code to
|
||||
easily override said functionality.
|
||||
|
||||
Use ba.app.components to get the single shared instance of this class.
|
||||
|
||||
The general idea with this setup is that a base-class is defined to
|
||||
provide some functionality and then anyone wanting that functionality
|
||||
uses the getclass() method with that base class to return the current
|
||||
registered implementation. The user should not know or care whether
|
||||
they are getting the base class itself or some other implementation.
|
||||
|
||||
Change-callbacks can also be requested for base classes which will
|
||||
fire in a deferred manner when particular base-classes are overridden.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._implementations: dict[type, type] = {}
|
||||
self._prev_implementations: dict[type, type] = {}
|
||||
self._dirty_base_classes: set[type] = set()
|
||||
self._change_callbacks: dict[type, list[Callable[[Any], None]]] = {}
|
||||
|
||||
def setclass(self, baseclass: type, implementation: type) -> None:
|
||||
"""Set the class providing an implementation of some base-class.
|
||||
|
||||
The provided implementation class must be a subclass of baseclass.
|
||||
"""
|
||||
# Currently limiting this to logic-thread use; can revisit if needed
|
||||
# (would need to guard access to our implementations dict).
|
||||
assert _ba.in_logic_thread()
|
||||
|
||||
if not issubclass(implementation, baseclass):
|
||||
raise TypeError(
|
||||
f'Implementation {implementation}'
|
||||
f' is not a subclass of baseclass {baseclass}.'
|
||||
)
|
||||
|
||||
self._implementations[baseclass] = implementation
|
||||
|
||||
# If we're the first thing getting dirtied, set up a callback to
|
||||
# clean everything. And add ourself to the dirty list regardless.
|
||||
if not self._dirty_base_classes:
|
||||
_ba.pushcall(self._run_change_callbacks)
|
||||
self._dirty_base_classes.add(baseclass)
|
||||
|
||||
def getclass(self, baseclass: T) -> T:
|
||||
"""Given a base-class, return the currently set implementation class.
|
||||
|
||||
If no custom implementation has been set, the provided base-class
|
||||
is returned.
|
||||
"""
|
||||
assert _ba.in_logic_thread()
|
||||
|
||||
del baseclass # Unused.
|
||||
return cast(T, None)
|
||||
|
||||
def register_change_callback(
|
||||
self, baseclass: T, callback: Callable[[T], None]
|
||||
) -> None:
|
||||
"""Register a callback to fire when a class implementation changes.
|
||||
|
||||
The callback will be scheduled to run in the logic thread event
|
||||
loop. Note that any further setclass calls before the callback
|
||||
runs will not result in additional callbacks.
|
||||
"""
|
||||
assert _ba.in_logic_thread()
|
||||
self._change_callbacks.setdefault(baseclass, []).append(callback)
|
||||
|
||||
def _run_change_callbacks(self) -> None:
|
||||
pass
|
||||
35
dist/ba_data/python/ba/_apputils.py
vendored
35
dist/ba_data/python/ba/_apputils.py
vendored
|
|
@ -476,35 +476,18 @@ def on_too_many_file_descriptors() -> None:
|
|||
real_time = _ba.time(TimeType.REAL)
|
||||
|
||||
def _do_log() -> None:
|
||||
import subprocess
|
||||
|
||||
pid = os.getpid()
|
||||
out = f'TOO MANY FDS at {real_time}.\nWe are pid {pid}\n'
|
||||
|
||||
out += (
|
||||
'FD Count: '
|
||||
+ subprocess.run(
|
||||
f'ls -l /proc/{pid}/fd | wc -l',
|
||||
shell=True,
|
||||
check=False,
|
||||
capture_output=True,
|
||||
).stdout.decode()
|
||||
+ '\n'
|
||||
try:
|
||||
fdcount: int | str = len(os.listdir(f'/proc/{pid}/fd'))
|
||||
except Exception as exc:
|
||||
fdcount = f'? ({exc})'
|
||||
logging.warning(
|
||||
'TOO MANY FDS at %.2f. We are pid %d. FDCount is %s.',
|
||||
real_time,
|
||||
pid,
|
||||
fdcount,
|
||||
)
|
||||
|
||||
out += (
|
||||
'lsof output:\n'
|
||||
+ subprocess.run(
|
||||
f'lsof -p {pid}',
|
||||
shell=True,
|
||||
check=False,
|
||||
capture_output=True,
|
||||
).stdout.decode()
|
||||
+ '\n'
|
||||
)
|
||||
|
||||
logging.warning(out)
|
||||
|
||||
Thread(target=_do_log, daemon=True).start()
|
||||
|
||||
# import io
|
||||
|
|
|
|||
2
dist/ba_data/python/ba/_bootstrap.py
vendored
2
dist/ba_data/python/ba/_bootstrap.py
vendored
|
|
@ -47,7 +47,7 @@ def bootstrap() -> None:
|
|||
|
||||
# Give a soft warning if we're being used with a different binary
|
||||
# version than we expect.
|
||||
expected_build = 20982
|
||||
expected_build = 21005
|
||||
running_build: int = env['build_number']
|
||||
if running_build != expected_build:
|
||||
print(
|
||||
|
|
|
|||
7
dist/ba_data/python/ba/_error.py
vendored
7
dist/ba_data/python/ba/_error.py
vendored
|
|
@ -69,6 +69,13 @@ class TeamNotFoundError(NotFoundError):
|
|||
"""
|
||||
|
||||
|
||||
class MapNotFoundError(NotFoundError):
|
||||
"""Exception raised when an expected ba.Map does not exist.
|
||||
|
||||
Category: **Exception Classes**
|
||||
"""
|
||||
|
||||
|
||||
class DelegateNotFoundError(NotFoundError):
|
||||
"""Exception raised when an expected delegate object does not exist.
|
||||
|
||||
|
|
|
|||
6
dist/ba_data/python/ba/_gameactivity.py
vendored
6
dist/ba_data/python/ba/_gameactivity.py
vendored
|
|
@ -14,7 +14,7 @@ from ba._activity import Activity
|
|||
from ba._score import ScoreConfig
|
||||
from ba._language import Lstr
|
||||
from ba._messages import PlayerDiedMessage, StandMessage
|
||||
from ba._error import NotFoundError, print_error, print_exception
|
||||
from ba._error import MapNotFoundError, print_error, print_exception
|
||||
from ba._general import Call, WeakCall
|
||||
from ba._player import PlayerInfo
|
||||
from ba import _map
|
||||
|
|
@ -274,10 +274,10 @@ class GameActivity(Activity[PlayerType, TeamType]):
|
|||
def map(self) -> ba.Map:
|
||||
"""The map being used for this game.
|
||||
|
||||
Raises a ba.NotFoundError if the map does not currently exist.
|
||||
Raises a ba.MapNotFoundError if the map does not currently exist.
|
||||
"""
|
||||
if self._map is None:
|
||||
raise NotFoundError
|
||||
raise MapNotFoundError
|
||||
return self._map
|
||||
|
||||
def get_instance_display_string(self) -> ba.Lstr:
|
||||
|
|
|
|||
52
dist/ba_data/python/ba/_hooks.py
vendored
52
dist/ba_data/python/ba/_hooks.py
vendored
|
|
@ -1,13 +1,13 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Snippets of code for use by the internal C++ layer.
|
||||
"""Snippets of code for use by the internal layer.
|
||||
|
||||
History: originally I would dynamically compile/eval bits of Python text
|
||||
from within C++ code, but the major downside there was that none of that was
|
||||
type-checked so if names or arguments changed I would never catch code breakage
|
||||
until the code was next run. By defining all snippets I use here and then
|
||||
capturing references to them all at launch I can immediately verify everything
|
||||
I'm looking for exists and pylint/mypy can do their magic on this file.
|
||||
History: originally the engine would dynamically compile/eval various Python
|
||||
code from within C++ code, but the major downside there was that none of it
|
||||
was type-checked so if names or arguments changed it would go unnoticed
|
||||
until it broke at runtime. By instead defining such snippets here and then
|
||||
capturing references to them all at launch it is possible to allow linting
|
||||
and type-checking magic to happen and most issues will be caught immediately.
|
||||
"""
|
||||
# (most of these are self-explanatory)
|
||||
# pylint: disable=missing-function-docstring
|
||||
|
|
@ -27,12 +27,8 @@ def finish_bootstrapping() -> None:
|
|||
"""Do final bootstrapping related bits."""
|
||||
assert _ba.in_logic_thread()
|
||||
|
||||
# Kick off our asyncio event handling, allowing us to use coroutines
|
||||
# in our logic thread alongside our internal event handling.
|
||||
# setup_asyncio()
|
||||
|
||||
# Ok, bootstrapping is done; time to get the show started.
|
||||
_ba.app.on_app_launch()
|
||||
# Ok, low level bootstrapping is done; time to get Python stuff started.
|
||||
_ba.app.on_bootstrapping_completed()
|
||||
|
||||
|
||||
def reset_to_main_menu() -> None:
|
||||
|
|
@ -495,3 +491,33 @@ def login_adapter_get_sign_in_token_response(
|
|||
adapter = _ba.app.accounts_v2.login_adapters[login_type]
|
||||
assert isinstance(adapter, LoginAdapterNative)
|
||||
adapter.on_sign_in_complete(attempt_id=attempt_id, result=result)
|
||||
|
||||
|
||||
def show_client_too_old_error() -> None:
|
||||
"""Called at launch if the server tells us we're too old to talk to it."""
|
||||
from ba._language import Lstr
|
||||
|
||||
# If you are using an old build of the app and would like to stop
|
||||
# seeing this error at launch, do:
|
||||
# ba.app.config['SuppressClientTooOldErrorForBuild'] = ba.app.build_number
|
||||
# ba.app.config.commit()
|
||||
# Note that you will have to do that again later if you update to
|
||||
# a newer build.
|
||||
if (
|
||||
_ba.app.config.get('SuppressClientTooOldErrorForBuild')
|
||||
== _ba.app.build_number
|
||||
):
|
||||
return
|
||||
|
||||
_ba.playsound(_ba.getsound('error'))
|
||||
_ba.screenmessage(
|
||||
Lstr(
|
||||
translate=(
|
||||
'serverResponses',
|
||||
'Server functionality is no longer supported'
|
||||
' in this version of the game;\n'
|
||||
'Please update to a newer version.',
|
||||
)
|
||||
),
|
||||
color=(1, 0, 0),
|
||||
)
|
||||
|
|
|
|||
44
dist/ba_data/python/ba/_login.py
vendored
44
dist/ba_data/python/ba/_login.py
vendored
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, final
|
||||
|
|
@ -57,6 +58,9 @@ class LoginAdapter:
|
|||
# current active primary account.
|
||||
self._active_login_id: str | None = None
|
||||
|
||||
self._last_sign_in_time: float | None = None
|
||||
self._last_sign_in_desc: str | None = None
|
||||
|
||||
def on_app_launch(self) -> None:
|
||||
"""Should be called for each adapter in on_app_launch."""
|
||||
|
||||
|
|
@ -142,6 +146,7 @@ class LoginAdapter:
|
|||
def sign_in(
|
||||
self,
|
||||
result_cb: Callable[[LoginAdapter, SignInResult | Exception], None],
|
||||
description: str,
|
||||
) -> None:
|
||||
"""Attempt an explicit sign in via this adapter.
|
||||
|
||||
|
|
@ -151,6 +156,38 @@ class LoginAdapter:
|
|||
"""
|
||||
assert _ba.in_logic_thread()
|
||||
from ba._general import Call
|
||||
from ba._generated.enums import TimeType
|
||||
|
||||
# Have been seeing multiple sign-in attempts come through
|
||||
# nearly simultaneously which can be problematic server-side.
|
||||
# Let's error if a sign-in attempt is made within a few seconds
|
||||
# of the last one to address this.
|
||||
now = time.monotonic()
|
||||
appnow = _ba.time(TimeType.REAL)
|
||||
if self._last_sign_in_time is not None:
|
||||
since_last = now - self._last_sign_in_time
|
||||
if since_last < 1.0:
|
||||
logging.warning(
|
||||
'LoginAdapter: %s adapter sign_in() called too soon'
|
||||
' (%.2fs) after last; this-desc="%s", last-desc="%s",'
|
||||
' ba-real-time=%.2f.',
|
||||
self.login_type.name,
|
||||
since_last,
|
||||
description,
|
||||
self._last_sign_in_desc,
|
||||
appnow,
|
||||
)
|
||||
_ba.pushcall(
|
||||
Call(
|
||||
result_cb,
|
||||
self,
|
||||
RuntimeError('sign_in called too soon after last.'),
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
self._last_sign_in_desc = description
|
||||
self._last_sign_in_time = now
|
||||
|
||||
if DEBUG_LOG:
|
||||
logging.debug(
|
||||
|
|
@ -223,7 +260,12 @@ class LoginAdapter:
|
|||
_ba.pushcall(Call(result_cb, self, result2))
|
||||
|
||||
_ba.app.cloud.send_message_cb(
|
||||
bacommon.cloud.SignInMessage(self.login_type, result),
|
||||
bacommon.cloud.SignInMessage(
|
||||
self.login_type,
|
||||
result,
|
||||
description=description,
|
||||
apptime=appnow,
|
||||
),
|
||||
on_response=_got_sign_in_response,
|
||||
)
|
||||
|
||||
|
|
|
|||
16
dist/ba_data/python/ba/_playlist.py
vendored
16
dist/ba_data/python/ba/_playlist.py
vendored
|
|
@ -9,6 +9,8 @@ import copy
|
|||
import logging
|
||||
from typing import Any, TYPE_CHECKING
|
||||
|
||||
import _ba
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Sequence
|
||||
from ba import _session
|
||||
|
|
@ -34,6 +36,7 @@ def filter_playlist(
|
|||
# pylint: disable=too-many-branches
|
||||
# pylint: disable=too-many-statements
|
||||
from ba._map import get_filtered_map_name
|
||||
from ba._error import MapNotFoundError
|
||||
from ba._store import get_unowned_maps, get_unowned_game_types
|
||||
from ba._general import getclass
|
||||
from ba._gameactivity import GameActivity
|
||||
|
|
@ -160,7 +163,7 @@ def filter_playlist(
|
|||
gameclass = getclass(entry['type'], GameActivity)
|
||||
|
||||
if entry['settings']['map'] not in available_maps:
|
||||
raise ImportError(f"Map not found: '{entry['settings']['map']}'")
|
||||
raise MapNotFoundError()
|
||||
|
||||
if remove_unowned and gameclass in unowned_game_types:
|
||||
continue
|
||||
|
|
@ -176,15 +179,22 @@ def filter_playlist(
|
|||
for setting in neededsettings:
|
||||
if setting.name not in entry['settings']:
|
||||
entry['settings'][setting.name] = setting.default
|
||||
|
||||
goodlist.append(entry)
|
||||
|
||||
except MapNotFoundError:
|
||||
logging.warning(
|
||||
'Map \'%s\' not found while scanning playlist \'%s\'.',
|
||||
name,
|
||||
entry['settings']['map'],
|
||||
)
|
||||
except ImportError as exc:
|
||||
logging.warning(
|
||||
'Import failed while scanning playlist \'%s\': %s', name, exc
|
||||
)
|
||||
except Exception:
|
||||
from ba import _error
|
||||
logging.exception('Error in filter_playlist.')
|
||||
|
||||
_error.print_exception()
|
||||
return goodlist
|
||||
|
||||
|
||||
|
|
|
|||
24
dist/ba_data/python/ba/_plugin.py
vendored
24
dist/ba_data/python/ba/_plugin.py
vendored
|
|
@ -22,6 +22,9 @@ class PluginSubsystem:
|
|||
Access the single shared instance of this class at `ba.app.plugins`.
|
||||
"""
|
||||
|
||||
AUTO_ENABLE_NEW_PLUGINS_CONFIG_KEY = 'Auto Enable New Plugins'
|
||||
AUTO_ENABLE_NEW_PLUGINS_DEFAULT = True
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.potential_plugins: list[ba.PotentialPlugin] = []
|
||||
self.active_plugins: dict[str, ba.Plugin] = {}
|
||||
|
|
@ -48,13 +51,20 @@ class PluginSubsystem:
|
|||
available=True,
|
||||
)
|
||||
)
|
||||
if class_path not in plugstates:
|
||||
# Go ahead and enable new plugins by default, but we'll
|
||||
# inform the user that they need to restart to pick them up.
|
||||
# they can also disable them in settings so they never load.
|
||||
plugstates[class_path] = {'enabled': True}
|
||||
config_changed = True
|
||||
found_new = True
|
||||
if (
|
||||
_ba.app.config.get(
|
||||
self.AUTO_ENABLE_NEW_PLUGINS_CONFIG_KEY,
|
||||
self.AUTO_ENABLE_NEW_PLUGINS_DEFAULT,
|
||||
)
|
||||
is True
|
||||
):
|
||||
if class_path not in plugstates:
|
||||
# Go ahead and enable new plugins by default, but we'll
|
||||
# inform the user that they need to restart to pick them up.
|
||||
# they can also disable them in settings so they never load.
|
||||
plugstates[class_path] = {'enabled': True}
|
||||
config_changed = True
|
||||
found_new = True
|
||||
|
||||
plugs.potential_plugins.sort(key=lambda p: p.class_path)
|
||||
|
||||
|
|
|
|||
5
dist/ba_data/python/ba/macmusicapp.py
vendored
5
dist/ba_data/python/ba/macmusicapp.py
vendored
|
|
@ -4,6 +4,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import threading
|
||||
from collections import deque
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import _ba
|
||||
|
|
@ -68,7 +69,7 @@ class _MacMusicAppThread(threading.Thread):
|
|||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self._commands_available = threading.Event()
|
||||
self._commands: list[list] = []
|
||||
self._commands = deque[list]()
|
||||
self._volume = 1.0
|
||||
self._current_playlist: str | None = None
|
||||
self._orig_volume: int | None = None
|
||||
|
|
@ -109,7 +110,7 @@ class _MacMusicAppThread(threading.Thread):
|
|||
# We're not protecting this list with a mutex but we're
|
||||
# just using it as a simple queue so it should be fine.
|
||||
while self._commands:
|
||||
cmd = self._commands.pop(0)
|
||||
cmd = self._commands.popleft()
|
||||
if cmd[0] == 'DIE':
|
||||
self._handle_die_command()
|
||||
done = True
|
||||
|
|
|
|||
4
dist/ba_data/python/bacommon/cloud.py
vendored
4
dist/ba_data/python/bacommon/cloud.py
vendored
|
|
@ -183,6 +183,10 @@ class SignInMessage(Message):
|
|||
login_type: Annotated[LoginType, IOAttrs('l')]
|
||||
sign_in_token: Annotated[str, IOAttrs('t')]
|
||||
|
||||
# For debugging. Can remove soft_default once build 20988+ is ubiquitous.
|
||||
description: Annotated[str, IOAttrs('d', soft_default='-')]
|
||||
apptime: Annotated[float, IOAttrs('at', soft_default=-1.0)]
|
||||
|
||||
@classmethod
|
||||
def get_response_types(cls) -> list[type[Response] | None]:
|
||||
return [SignInResponse]
|
||||
|
|
|
|||
11
dist/ba_data/python/bastd/actor/spaz.py
vendored
11
dist/ba_data/python/bastd/actor/spaz.py
vendored
|
|
@ -88,12 +88,12 @@ class Spaz(ba.Actor):
|
|||
|
||||
self.play_big_death_sound = False
|
||||
|
||||
# scales how much impacts affect us (most damage calcs)
|
||||
# Scales how much impacts affect us (most damage calcs).
|
||||
self.impact_scale = 1.0
|
||||
|
||||
self.source_player = source_player
|
||||
self._dead = False
|
||||
if self._demo_mode: # preserve old behavior
|
||||
if self._demo_mode: # Preserve old behavior.
|
||||
self._punch_power_scale = 1.2
|
||||
else:
|
||||
self._punch_power_scale = factory.punch_power_scale
|
||||
|
|
@ -180,6 +180,7 @@ class Spaz(ba.Actor):
|
|||
self._bomb_wear_off_flash_timer: ba.Timer | None = None
|
||||
self._multi_bomb_wear_off_timer: ba.Timer | None = None
|
||||
self._multi_bomb_wear_off_flash_timer: ba.Timer | None = None
|
||||
self._curse_timer: ba.Timer | None = None
|
||||
self.bomb_count = self.default_bomb_count
|
||||
self._max_bomb_count = self.default_bomb_count
|
||||
self.bomb_type_default = self.default_bomb_type
|
||||
|
|
@ -262,7 +263,7 @@ class Spaz(ba.Actor):
|
|||
def _turbo_filter_add_press(self, source: str) -> None:
|
||||
"""
|
||||
Can pass all button presses through here; if we see an obscene number
|
||||
of them in a short time let's shame/pushish this guy for using turbo
|
||||
of them in a short time let's shame/pushish this guy for using turbo.
|
||||
"""
|
||||
t_ms = ba.time(
|
||||
timetype=ba.TimeType.BASE, timeformat=ba.TimeFormat.MILLISECONDS
|
||||
|
|
@ -620,7 +621,9 @@ class Spaz(ba.Actor):
|
|||
self.node.curse_death_time = int(
|
||||
1000.0 * (tval + self.curse_time)
|
||||
)
|
||||
ba.timer(5.0, ba.WeakCall(self.curse_explode))
|
||||
self._curse_timer = ba.Timer(
|
||||
5.0, ba.WeakCall(self.curse_explode)
|
||||
)
|
||||
|
||||
def equip_boxing_gloves(self) -> None:
|
||||
"""
|
||||
|
|
|
|||
2
dist/ba_data/python/bastd/mainmenu.py
vendored
2
dist/ba_data/python/bastd/mainmenu.py
vendored
|
|
@ -63,7 +63,7 @@ class MainMenuActivity(ba.Activity[ba.Player, ba.Team]):
|
|||
'scale': scale,
|
||||
'position': (0, 10),
|
||||
'vr_depth': -10,
|
||||
'text': '\xa9 2011-2022 Eric Froemling',
|
||||
'text': '\xa9 2011-2023 Eric Froemling',
|
||||
},
|
||||
)
|
||||
)
|
||||
|
|
|
|||
3087
dist/ba_data/python/bastd/tutorial.py
vendored
3087
dist/ba_data/python/bastd/tutorial.py
vendored
File diff suppressed because it is too large
Load diff
|
|
@ -1403,7 +1403,8 @@ class AccountSettingsWindow(ba.Window):
|
|||
if adapter is not None:
|
||||
self._signing_in_adapter = adapter
|
||||
adapter.sign_in(
|
||||
result_cb=ba.WeakCall(self._on_adapter_sign_in_result)
|
||||
result_cb=ba.WeakCall(self._on_adapter_sign_in_result),
|
||||
description='account settings button',
|
||||
)
|
||||
# Will get 'Signing in...' to show.
|
||||
self._needs_refresh = True
|
||||
|
|
|
|||
|
|
@ -234,6 +234,7 @@ class ManualGatherTab(GatherTab):
|
|||
c_width = region_width
|
||||
c_height = region_height - 20
|
||||
last_addr = ba.app.config.get('Last Manual Party Connect Address', '')
|
||||
last_port = ba.app.config.get('Last Manual Party Connect Port', 43210)
|
||||
v = c_height - 70
|
||||
v -= 70
|
||||
ba.textwidget(
|
||||
|
|
@ -256,6 +257,7 @@ class ManualGatherTab(GatherTab):
|
|||
autoselect=True,
|
||||
v_align='center',
|
||||
scale=1.0,
|
||||
maxwidth=380,
|
||||
size=(420, 60),
|
||||
)
|
||||
ba.widget(edit=self._join_by_address_text, down_widget=txt)
|
||||
|
|
@ -275,7 +277,7 @@ class ManualGatherTab(GatherTab):
|
|||
parent=self._container,
|
||||
editable=True,
|
||||
description=ba.Lstr(resource='gatherWindow.' 'portText'),
|
||||
text='43210',
|
||||
text=str(last_port),
|
||||
autoselect=True,
|
||||
max_chars=5,
|
||||
position=(c_width * 0.5 - 240 + 490, v - 30),
|
||||
|
|
@ -811,6 +813,7 @@ class ManualGatherTab(GatherTab):
|
|||
# Store for later.
|
||||
config = ba.app.config
|
||||
config['Last Manual Party Connect Address'] = resolved_address
|
||||
config['Last Manual Party Connect Port'] = port
|
||||
config.commit()
|
||||
ba.internal.connect_to_party(resolved_address, port=port)
|
||||
|
||||
|
|
|
|||
|
|
@ -571,6 +571,7 @@ class PublicGatherTab(GatherTab):
|
|||
h_align='left',
|
||||
v_align='center',
|
||||
editable=True,
|
||||
maxwidth=310,
|
||||
description=filter_txt,
|
||||
)
|
||||
ba.widget(edit=self._filter_text, up_widget=self._join_text)
|
||||
|
|
|
|||
5
dist/ba_data/python/bastd/ui/party.py
vendored
5
dist/ba_data/python/bastd/ui/party.py
vendored
|
|
@ -211,9 +211,8 @@ class PartyWindow(ba.Window):
|
|||
flatness=1.0,
|
||||
)
|
||||
self._chat_texts.append(txt)
|
||||
if len(self._chat_texts) > 40:
|
||||
first = self._chat_texts.pop(0)
|
||||
first.delete()
|
||||
while len(self._chat_texts) > 40:
|
||||
self._chat_texts.pop(0).delete()
|
||||
ba.containerwidget(edit=self._columnwidget, visible_child=txt)
|
||||
|
||||
def _on_menu_button_press(self) -> None:
|
||||
|
|
|
|||
|
|
@ -473,7 +473,6 @@ class PlaylistEditGameWindow(ba.Window):
|
|||
|
||||
# Ok now wire up the column.
|
||||
try:
|
||||
# pylint: disable=unsubscriptable-object
|
||||
prev_widgets: list[ba.Widget] | None = None
|
||||
for cwdg in widget_column:
|
||||
if prev_widgets is not None:
|
||||
|
|
|
|||
|
|
@ -134,7 +134,7 @@ class AdvancedSettingsWindow(ba.Window):
|
|||
parent=self._root_widget,
|
||||
position=(0, self._height - 52),
|
||||
size=(self._width, 25),
|
||||
text=ba.Lstr(resource=self._r + '.titleText'),
|
||||
text=ba.Lstr(resource=f'{self._r}.titleText'),
|
||||
color=app.ui.title_color,
|
||||
h_align='center',
|
||||
v_align='top',
|
||||
|
|
@ -203,10 +203,10 @@ class AdvancedSettingsWindow(ba.Window):
|
|||
text=''
|
||||
if ba.app.lang.language == 'Test'
|
||||
else ba.Lstr(
|
||||
resource=self._r + '.translationNoUpdateNeededText'
|
||||
resource=f'{self._r}.translationNoUpdateNeededText'
|
||||
)
|
||||
if up_to_date
|
||||
else ba.Lstr(resource=self._r + '.translationUpdateNeededText'),
|
||||
else ba.Lstr(resource=f'{self._r}.translationUpdateNeededText'),
|
||||
color=(0.2, 1.0, 0.2, 0.8)
|
||||
if up_to_date
|
||||
else (1.0, 0.2, 0.2, 0.8),
|
||||
|
|
@ -214,10 +214,10 @@ class AdvancedSettingsWindow(ba.Window):
|
|||
else:
|
||||
ba.textwidget(
|
||||
edit=self._lang_status_text,
|
||||
text=ba.Lstr(resource=self._r + '.translationFetchErrorText')
|
||||
text=ba.Lstr(resource=f'{self._r}.translationFetchErrorText')
|
||||
if self._complete_langs_error
|
||||
else ba.Lstr(
|
||||
resource=self._r + '.translationFetchingStatusText'
|
||||
resource=f'{self._r}.translationFetchingStatusText'
|
||||
),
|
||||
color=(1.0, 0.5, 0.2)
|
||||
if self._complete_langs_error
|
||||
|
|
@ -267,7 +267,7 @@ class AdvancedSettingsWindow(ba.Window):
|
|||
)
|
||||
|
||||
ba.textwidget(
|
||||
edit=self._title_text, text=ba.Lstr(resource=self._r + '.titleText')
|
||||
edit=self._title_text, text=ba.Lstr(resource=f'{self._r}.titleText')
|
||||
)
|
||||
|
||||
this_button_width = 410
|
||||
|
|
@ -277,7 +277,7 @@ class AdvancedSettingsWindow(ba.Window):
|
|||
position=(self._sub_width / 2 - this_button_width / 2, v - 14),
|
||||
size=(this_button_width, 60),
|
||||
autoselect=True,
|
||||
label=ba.Lstr(resource=self._r + '.enterPromoCodeText'),
|
||||
label=ba.Lstr(resource=f'{self._r}.enterPromoCodeText'),
|
||||
text_scale=1.0,
|
||||
on_activate_call=self._on_promo_code_press,
|
||||
)
|
||||
|
|
@ -293,7 +293,7 @@ class AdvancedSettingsWindow(ba.Window):
|
|||
parent=self._subcontainer,
|
||||
position=(200, v + 10),
|
||||
size=(0, 0),
|
||||
text=ba.Lstr(resource=self._r + '.languageText'),
|
||||
text=ba.Lstr(resource=f'{self._r}.languageText'),
|
||||
maxwidth=150,
|
||||
scale=0.95,
|
||||
color=ba.app.ui.title_color,
|
||||
|
|
@ -371,7 +371,7 @@ class AdvancedSettingsWindow(ba.Window):
|
|||
position=(self._sub_width * 0.5, v + 10),
|
||||
size=(0, 0),
|
||||
text=ba.Lstr(
|
||||
resource=self._r + '.helpTranslateText',
|
||||
resource=f'{self._r}.helpTranslateText',
|
||||
subs=[('${APP_NAME}', ba.Lstr(resource='titleText'))],
|
||||
),
|
||||
maxwidth=self._sub_width * 0.9,
|
||||
|
|
@ -389,7 +389,7 @@ class AdvancedSettingsWindow(ba.Window):
|
|||
position=(self._sub_width / 2 - this_button_width / 2, v - 24),
|
||||
size=(this_button_width, 60),
|
||||
label=ba.Lstr(
|
||||
resource=self._r + '.translationEditorButtonText',
|
||||
resource=f'{self._r}.translationEditorButtonText',
|
||||
subs=[('${APP_NAME}', ba.Lstr(resource='titleText'))],
|
||||
),
|
||||
autoselect=True,
|
||||
|
|
@ -422,7 +422,7 @@ class AdvancedSettingsWindow(ba.Window):
|
|||
maxwidth=430,
|
||||
textcolor=(0.8, 0.8, 0.8),
|
||||
value=lang_inform,
|
||||
text=ba.Lstr(resource=self._r + '.translationInformMe'),
|
||||
text=ba.Lstr(resource=f'{self._r}.translationInformMe'),
|
||||
on_value_change_call=ba.WeakCall(self._on_lang_inform_value_change),
|
||||
)
|
||||
|
||||
|
|
@ -439,7 +439,7 @@ class AdvancedSettingsWindow(ba.Window):
|
|||
position=(50, v),
|
||||
size=(self._sub_width - 100, 30),
|
||||
configkey='Kick Idle Players',
|
||||
displayname=ba.Lstr(resource=self._r + '.kickIdlePlayersText'),
|
||||
displayname=ba.Lstr(resource=f'{self._r}.kickIdlePlayersText'),
|
||||
scale=1.0,
|
||||
maxwidth=430,
|
||||
)
|
||||
|
|
@ -450,7 +450,7 @@ class AdvancedSettingsWindow(ba.Window):
|
|||
position=(50, v),
|
||||
size=(self._sub_width - 100, 30),
|
||||
configkey='Show Ping',
|
||||
displayname=ba.Lstr(value='Show InGame Ping'),
|
||||
displayname=ba.Lstr(resource=f'{self._r}.showInGamePingText'),
|
||||
scale=1.0,
|
||||
maxwidth=430,
|
||||
)
|
||||
|
|
@ -461,7 +461,7 @@ class AdvancedSettingsWindow(ba.Window):
|
|||
position=(50, v),
|
||||
size=(self._sub_width - 100, 30),
|
||||
configkey='Disable Camera Shake',
|
||||
displayname=ba.Lstr(resource=self._r + '.disableCameraShakeText'),
|
||||
displayname=ba.Lstr(resource=f'{self._r}.disableCameraShakeText'),
|
||||
scale=1.0,
|
||||
maxwidth=430,
|
||||
)
|
||||
|
|
@ -475,7 +475,7 @@ class AdvancedSettingsWindow(ba.Window):
|
|||
size=(self._sub_width - 100, 30),
|
||||
configkey='Disable Camera Gyro',
|
||||
displayname=ba.Lstr(
|
||||
resource=self._r + '.disableCameraGyroscopeMotionText'
|
||||
resource=f'{self._r}.disableCameraGyroscopeMotionText'
|
||||
),
|
||||
scale=1.0,
|
||||
maxwidth=430,
|
||||
|
|
@ -491,7 +491,7 @@ class AdvancedSettingsWindow(ba.Window):
|
|||
configkey='Always Use Internal Keyboard',
|
||||
autoselect=True,
|
||||
displayname=ba.Lstr(
|
||||
resource=self._r + '.alwaysUseInternalKeyboardText'
|
||||
resource=f'{self._r}.alwaysUseInternalKeyboardText'
|
||||
),
|
||||
scale=1.0,
|
||||
maxwidth=430,
|
||||
|
|
@ -501,8 +501,9 @@ class AdvancedSettingsWindow(ba.Window):
|
|||
position=(90, v - 10),
|
||||
size=(0, 0),
|
||||
text=ba.Lstr(
|
||||
resource=self._r
|
||||
+ '.alwaysUseInternalKeyboardDescriptionText'
|
||||
resource=(
|
||||
f'{self._r}.alwaysUseInternalKeyboardDescriptionText'
|
||||
)
|
||||
),
|
||||
maxwidth=400,
|
||||
flatness=1.0,
|
||||
|
|
@ -523,7 +524,7 @@ class AdvancedSettingsWindow(ba.Window):
|
|||
position=(self._sub_width / 2 - this_button_width / 2, v - 10),
|
||||
size=(this_button_width, 60),
|
||||
autoselect=True,
|
||||
label=ba.Lstr(resource=self._r + '.moddingGuideText'),
|
||||
label=ba.Lstr(resource=f'{self._r}.moddingGuideText'),
|
||||
text_scale=1.0,
|
||||
on_activate_call=ba.Call(
|
||||
ba.open_url, 'https://ballistica.net/wiki/modding-guide'
|
||||
|
|
@ -556,7 +557,7 @@ class AdvancedSettingsWindow(ba.Window):
|
|||
position=(self._sub_width / 2 - this_button_width / 2, v - 10),
|
||||
size=(this_button_width, 60),
|
||||
autoselect=True,
|
||||
label=ba.Lstr(resource=self._r + '.showUserModsText'),
|
||||
label=ba.Lstr(resource=f'{self._r}.showUserModsText'),
|
||||
text_scale=1.0,
|
||||
on_activate_call=show_user_scripts,
|
||||
)
|
||||
|
|
@ -583,7 +584,7 @@ class AdvancedSettingsWindow(ba.Window):
|
|||
position=(self._sub_width / 2 - this_button_width / 2, v - 14),
|
||||
size=(this_button_width, 60),
|
||||
autoselect=True,
|
||||
label=ba.Lstr(resource=self._r + '.vrTestingText'),
|
||||
label=ba.Lstr(resource=f'{self._r}.vrTestingText'),
|
||||
text_scale=1.0,
|
||||
on_activate_call=self._on_vr_test_press,
|
||||
)
|
||||
|
|
@ -598,7 +599,7 @@ class AdvancedSettingsWindow(ba.Window):
|
|||
position=(self._sub_width / 2 - this_button_width / 2, v - 14),
|
||||
size=(this_button_width, 60),
|
||||
autoselect=True,
|
||||
label=ba.Lstr(resource=self._r + '.netTestingText'),
|
||||
label=ba.Lstr(resource=f'{self._r}.netTestingText'),
|
||||
text_scale=1.0,
|
||||
on_activate_call=self._on_net_test_press,
|
||||
)
|
||||
|
|
@ -611,7 +612,7 @@ class AdvancedSettingsWindow(ba.Window):
|
|||
position=(self._sub_width / 2 - this_button_width / 2, v - 14),
|
||||
size=(this_button_width, 60),
|
||||
autoselect=True,
|
||||
label=ba.Lstr(resource=self._r + '.benchmarksText'),
|
||||
label=ba.Lstr(resource=f'{self._r}.benchmarksText'),
|
||||
text_scale=1.0,
|
||||
on_activate_call=self._on_benchmark_press,
|
||||
)
|
||||
|
|
@ -633,7 +634,7 @@ class AdvancedSettingsWindow(ba.Window):
|
|||
def _show_restart_needed(self, value: Any) -> None:
|
||||
del value # Unused.
|
||||
ba.screenmessage(
|
||||
ba.Lstr(resource=self._r + '.mustRestartText'), color=(1, 1, 0)
|
||||
ba.Lstr(resource=f'{self._r}.mustRestartText'), color=(1, 1, 0)
|
||||
)
|
||||
|
||||
def _on_lang_inform_value_change(self, val: bool) -> None:
|
||||
|
|
@ -678,14 +679,12 @@ class AdvancedSettingsWindow(ba.Window):
|
|||
appinvite.handle_app_invites_press()
|
||||
|
||||
def _on_plugins_button_press(self) -> None:
|
||||
from bastd.ui.settings.plugins import PluginSettingsWindow
|
||||
from bastd.ui.settings.plugins import PluginWindow
|
||||
|
||||
self._save_state()
|
||||
ba.containerwidget(edit=self._root_widget, transition='out_left')
|
||||
ba.app.ui.set_main_menu_window(
|
||||
PluginSettingsWindow(
|
||||
origin_widget=self._plugins_button
|
||||
).get_root_widget()
|
||||
PluginWindow(origin_widget=self._plugins_button).get_root_widget()
|
||||
)
|
||||
|
||||
def _on_promo_code_press(self) -> None:
|
||||
|
|
|
|||
37
dist/ba_data/python/bastd/ui/settings/plugins.py
vendored
37
dist/ba_data/python/bastd/ui/settings/plugins.py
vendored
|
|
@ -1,6 +1,6 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Plugin settings UI."""
|
||||
"""Plugin Window UI."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
|
@ -12,7 +12,7 @@ if TYPE_CHECKING:
|
|||
pass
|
||||
|
||||
|
||||
class PluginSettingsWindow(ba.Window):
|
||||
class PluginWindow(ba.Window):
|
||||
"""Window for configuring plugins."""
|
||||
|
||||
def __init__(
|
||||
|
|
@ -106,6 +106,27 @@ class PluginSettingsWindow(ba.Window):
|
|||
size=(60, 60),
|
||||
label=ba.charstr(ba.SpecialChar.BACK),
|
||||
)
|
||||
settings_button_x = 670 if uiscale is ba.UIScale.SMALL else 570
|
||||
self._settings_button = ba.buttonwidget(
|
||||
parent=self._root_widget,
|
||||
position=(settings_button_x, self._height - 60),
|
||||
size=(40, 40),
|
||||
label='',
|
||||
on_activate_call=self._open_settings,
|
||||
)
|
||||
|
||||
ba.imagewidget(
|
||||
parent=self._root_widget,
|
||||
position=(settings_button_x + 3, self._height - 60),
|
||||
size=(35, 35),
|
||||
texture=ba.gettexture('settingsIcon'),
|
||||
)
|
||||
|
||||
ba.widget(
|
||||
edit=self._settings_button,
|
||||
up_widget=self._settings_button,
|
||||
right_widget=self._settings_button,
|
||||
)
|
||||
|
||||
self._scrollwidget = ba.scrollwidget(
|
||||
parent=self._root_widget,
|
||||
|
|
@ -185,6 +206,7 @@ class PluginSettingsWindow(ba.Window):
|
|||
edit=check,
|
||||
up_widget=self._back_button,
|
||||
left_widget=self._back_button,
|
||||
right_widget=self._settings_button,
|
||||
)
|
||||
if button is not None:
|
||||
ba.widget(edit=button, up_widget=self._back_button)
|
||||
|
|
@ -212,6 +234,17 @@ class PluginSettingsWindow(ba.Window):
|
|||
plugstate['enabled'] = value
|
||||
ba.app.config.commit()
|
||||
|
||||
def _open_settings(self) -> None:
|
||||
# pylint: disable=cyclic-import
|
||||
from bastd.ui.settings.pluginsettings import PluginSettingsWindow
|
||||
|
||||
ba.playsound(ba.getsound('swish'))
|
||||
|
||||
ba.containerwidget(edit=self._root_widget, transition='out_left')
|
||||
ba.app.ui.set_main_menu_window(
|
||||
PluginSettingsWindow(transition='in_right').get_root_widget()
|
||||
)
|
||||
|
||||
def _save_state(self) -> None:
|
||||
pass
|
||||
|
||||
|
|
|
|||
174
dist/ba_data/python/bastd/ui/settings/pluginsettings.py
vendored
Normal file
174
dist/ba_data/python/bastd/ui/settings/pluginsettings.py
vendored
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Plugin Settings UI."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import ba
|
||||
from bastd.ui.confirm import ConfirmWindow
|
||||
|
||||
if TYPE_CHECKING:
|
||||
pass
|
||||
|
||||
|
||||
class PluginSettingsWindow(ba.Window):
|
||||
"""Plugin Settings Window"""
|
||||
|
||||
def __init__(self, transition: str = 'in_right'):
|
||||
|
||||
scale_origin: tuple[float, float] | None
|
||||
self._transition_out = 'out_right'
|
||||
scale_origin = None
|
||||
|
||||
uiscale = ba.app.ui.uiscale
|
||||
width = 470.0 if uiscale is ba.UIScale.SMALL else 470.0
|
||||
height = (
|
||||
365.0
|
||||
if uiscale is ba.UIScale.SMALL
|
||||
else 300.0
|
||||
if uiscale is ba.UIScale.MEDIUM
|
||||
else 370.0
|
||||
)
|
||||
top_extra = 10 if uiscale is ba.UIScale.SMALL else 0
|
||||
|
||||
super().__init__(
|
||||
root_widget=ba.containerwidget(
|
||||
size=(width, height + top_extra),
|
||||
transition=transition,
|
||||
toolbar_visibility='menu_minimal',
|
||||
scale_origin_stack_offset=scale_origin,
|
||||
scale=(
|
||||
2.06
|
||||
if uiscale is ba.UIScale.SMALL
|
||||
else 1.4
|
||||
if uiscale is ba.UIScale.MEDIUM
|
||||
else 1.0
|
||||
),
|
||||
stack_offset=(0, -25)
|
||||
if uiscale is ba.UIScale.SMALL
|
||||
else (0, 0),
|
||||
)
|
||||
)
|
||||
|
||||
self._back_button = ba.buttonwidget(
|
||||
parent=self._root_widget,
|
||||
position=(53, height - 60),
|
||||
size=(60, 60),
|
||||
scale=0.8,
|
||||
autoselect=True,
|
||||
label=ba.charstr(ba.SpecialChar.BACK),
|
||||
button_type='backSmall',
|
||||
on_activate_call=self._do_back,
|
||||
)
|
||||
ba.containerwidget(
|
||||
edit=self._root_widget, cancel_button=self._back_button
|
||||
)
|
||||
|
||||
self._title_text = ba.textwidget(
|
||||
parent=self._root_widget,
|
||||
position=(0, height - 52),
|
||||
size=(width, 25),
|
||||
text=ba.Lstr(resource='pluginSettingsText'),
|
||||
color=ba.app.ui.title_color,
|
||||
h_align='center',
|
||||
v_align='top',
|
||||
)
|
||||
|
||||
self._y_position = 170 if uiscale is ba.UIScale.MEDIUM else 205
|
||||
self._enable_plugins_button = ba.buttonwidget(
|
||||
parent=self._root_widget,
|
||||
position=(65, self._y_position),
|
||||
size=(350, 60),
|
||||
autoselect=True,
|
||||
label=ba.Lstr(resource='pluginsEnableAllText'),
|
||||
text_scale=1.0,
|
||||
on_activate_call=lambda: ConfirmWindow(
|
||||
action=self._enable_all_plugins,
|
||||
),
|
||||
)
|
||||
|
||||
self._y_position -= 70
|
||||
self._disable_plugins_button = ba.buttonwidget(
|
||||
parent=self._root_widget,
|
||||
position=(65, self._y_position),
|
||||
size=(350, 60),
|
||||
autoselect=True,
|
||||
label=ba.Lstr(resource='pluginsDisableAllText'),
|
||||
text_scale=1.0,
|
||||
on_activate_call=lambda: ConfirmWindow(
|
||||
action=self._disable_all_plugins,
|
||||
),
|
||||
)
|
||||
|
||||
self._y_position -= 70
|
||||
self._enable_new_plugins_check_box = ba.checkboxwidget(
|
||||
parent=self._root_widget,
|
||||
position=(65, self._y_position),
|
||||
size=(350, 60),
|
||||
value=ba.app.config.get(
|
||||
ba.app.plugins.AUTO_ENABLE_NEW_PLUGINS_CONFIG_KEY,
|
||||
ba.app.plugins.AUTO_ENABLE_NEW_PLUGINS_DEFAULT,
|
||||
),
|
||||
text=ba.Lstr(resource='pluginsAutoEnableNewText'),
|
||||
scale=1.0,
|
||||
maxwidth=308,
|
||||
on_value_change_call=self._update_value,
|
||||
)
|
||||
|
||||
ba.widget(
|
||||
edit=self._back_button, down_widget=self._enable_plugins_button
|
||||
)
|
||||
|
||||
ba.widget(
|
||||
edit=self._disable_plugins_button,
|
||||
left_widget=self._disable_plugins_button,
|
||||
)
|
||||
|
||||
ba.widget(
|
||||
edit=self._enable_new_plugins_check_box,
|
||||
left_widget=self._enable_new_plugins_check_box,
|
||||
right_widget=self._enable_new_plugins_check_box,
|
||||
down_widget=self._enable_new_plugins_check_box,
|
||||
)
|
||||
|
||||
def _enable_all_plugins(self) -> None:
|
||||
cfg = ba.app.config
|
||||
plugs: dict[str, dict] = cfg.setdefault('Plugins', {})
|
||||
for plug in plugs.values():
|
||||
plug['enabled'] = True
|
||||
cfg.apply_and_commit()
|
||||
|
||||
ba.screenmessage(
|
||||
ba.Lstr(resource='settingsWindowAdvanced.mustRestartText'),
|
||||
color=(1.0, 0.5, 0.0),
|
||||
)
|
||||
|
||||
def _disable_all_plugins(self) -> None:
|
||||
cfg = ba.app.config
|
||||
plugs: dict[str, dict] = cfg.setdefault('Plugins', {})
|
||||
for plug in plugs.values():
|
||||
plug['enabled'] = False
|
||||
cfg.apply_and_commit()
|
||||
|
||||
ba.screenmessage(
|
||||
ba.Lstr(resource='settingsWindowAdvanced.mustRestartText'),
|
||||
color=(1.0, 0.5, 0.0),
|
||||
)
|
||||
|
||||
def _update_value(self, val: bool) -> None:
|
||||
cfg = ba.app.config
|
||||
cfg[ba.app.plugins.AUTO_ENABLE_NEW_PLUGINS_CONFIG_KEY] = val
|
||||
cfg.apply_and_commit()
|
||||
|
||||
def _do_back(self) -> None:
|
||||
# pylint: disable=cyclic-import
|
||||
from bastd.ui.settings.plugins import PluginWindow
|
||||
|
||||
ba.containerwidget(
|
||||
edit=self._root_widget, transition=self._transition_out
|
||||
)
|
||||
ba.app.ui.set_main_menu_window(
|
||||
PluginWindow(transition='in_left').get_root_widget()
|
||||
)
|
||||
|
|
@ -1155,7 +1155,6 @@ class StoreBrowserWindow(ba.Window):
|
|||
# Wire this button to the equivalent in the
|
||||
# previous row.
|
||||
if prev_row_buttons is not None:
|
||||
# pylint: disable=unsubscriptable-object
|
||||
if len(prev_row_buttons) > col:
|
||||
ba.widget(
|
||||
edit=btn,
|
||||
|
|
|
|||
49
dist/ba_data/python/efro/cloudshell.py
vendored
Normal file
49
dist/ba_data/python/efro/cloudshell.py
vendored
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""My nifty ssh/mosh/rsync mishmash."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import Enum
|
||||
from dataclasses import dataclass
|
||||
|
||||
from efro.dataclassio import ioprepped
|
||||
|
||||
|
||||
class LockType(Enum):
|
||||
"""Types of locks that can be acquired on a host."""
|
||||
|
||||
HOST = 'host'
|
||||
WORKSPACE = 'workspace'
|
||||
PYCHARM = 'pycharm'
|
||||
CLION = 'clion'
|
||||
|
||||
|
||||
@ioprepped
|
||||
@dataclass
|
||||
class HostConfig:
|
||||
"""Config for a cloud machine to run commands on.
|
||||
|
||||
precommand, if set, will be run before the passed commands.
|
||||
Note that it is not run in interactive mode (when no command is given).
|
||||
"""
|
||||
|
||||
address: str | None = None
|
||||
user: str = 'ubuntu'
|
||||
port: int = 22
|
||||
mosh_port: int | None = None
|
||||
mosh_server_path: str | None = None
|
||||
mosh_shell: str = 'sh'
|
||||
workspaces_root: str = '/home/${USER}/cloudshell_workspaces'
|
||||
sync_perms: bool = True
|
||||
precommand: str | None = None
|
||||
managed: bool = False
|
||||
idle_minutes: int = 5
|
||||
can_sudo_reboot: bool = False
|
||||
max_sessions: int = 3
|
||||
reboot_wait_seconds: int = 20
|
||||
reboot_attempts: int = 1
|
||||
|
||||
def resolved_workspaces_root(self) -> str:
|
||||
"""Returns workspaces_root with standard substitutions."""
|
||||
return self.workspaces_root.replace('${USER}', self.user)
|
||||
200
dist/ba_data/python/efro/log.py
vendored
200
dist/ba_data/python/efro/log.py
vendored
|
|
@ -8,7 +8,9 @@ import time
|
|||
import asyncio
|
||||
import logging
|
||||
import datetime
|
||||
import itertools
|
||||
from enum import Enum
|
||||
from collections import deque
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Annotated
|
||||
from threading import Thread, current_thread, Lock
|
||||
|
|
@ -143,12 +145,14 @@ class LogHandler(logging.Handler):
|
|||
assert cache_size_limit >= 0
|
||||
self._cache_size_limit = cache_size_limit
|
||||
self._cache_time_limit = cache_time_limit
|
||||
self._cache: list[tuple[int, LogEntry]] = []
|
||||
self._cache = deque[tuple[int, LogEntry]]()
|
||||
self._cache_index_offset = 0
|
||||
self._cache_lock = Lock()
|
||||
self._printed_callback_error = False
|
||||
self._thread_bootstrapped = False
|
||||
self._thread = Thread(target=self._log_thread_main, daemon=True)
|
||||
if __debug__:
|
||||
self._last_slow_emit_warning_time: float | None = None
|
||||
self._thread.start()
|
||||
|
||||
# Spin until our thread is up and running; otherwise we could
|
||||
|
|
@ -167,6 +171,12 @@ class LogHandler(logging.Handler):
|
|||
|
||||
def _log_thread_main(self) -> None:
|
||||
self._event_loop = asyncio.new_event_loop()
|
||||
|
||||
# In our background thread event loop we do a fair amount of
|
||||
# slow synchronous stuff such as mucking with the log cache.
|
||||
# Let's avoid getting tons of warnings about this in debug mode.
|
||||
self._event_loop.slow_callback_duration = 2.0 # Default is 0.1
|
||||
|
||||
# NOTE: if we ever use default threadpool at all we should allow
|
||||
# setting it for our loop.
|
||||
asyncio.set_event_loop(self._event_loop)
|
||||
|
|
@ -192,20 +202,15 @@ class LogHandler(logging.Handler):
|
|||
now = utc_now()
|
||||
with self._cache_lock:
|
||||
|
||||
# Quick out: if oldest cache entry is still valid,
|
||||
# don't touch anything.
|
||||
if (
|
||||
# Prune the oldest entry as long as there is a first one that
|
||||
# is too old.
|
||||
while (
|
||||
self._cache
|
||||
and (now - self._cache[0][1].time) < self._cache_time_limit
|
||||
and (now - self._cache[0][1].time) >= self._cache_time_limit
|
||||
):
|
||||
continue
|
||||
|
||||
# Ok; full prune.
|
||||
self._cache = [
|
||||
e
|
||||
for e in self._cache
|
||||
if (now - e[1].time) < self._cache_time_limit
|
||||
]
|
||||
popped = self._cache.popleft()
|
||||
self._cache_size -= popped[0]
|
||||
self._cache_index_offset += 1
|
||||
|
||||
def get_cached(
|
||||
self, start_index: int = 0, max_entries: int | None = None
|
||||
|
|
@ -239,19 +244,40 @@ class LogHandler(logging.Handler):
|
|||
return LogArchive(
|
||||
log_size=self._cache_index_offset + len(self._cache),
|
||||
start_index=start_index + self._cache_index_offset,
|
||||
entries=[e[1] for e in self._cache[start_index:end_index]],
|
||||
entries=self._cache_slice(start_index, end_index),
|
||||
)
|
||||
|
||||
def emit(self, record: logging.LogRecord) -> None:
|
||||
# Called by logging to send us records.
|
||||
# We simply package them up and ship them to our thread.
|
||||
# UPDATE: turns out we CAN get log messages from this thread
|
||||
# (the C++ layer can spit out some performance metrics when
|
||||
# calls take too long/etc.)
|
||||
# assert current_thread() is not self._thread
|
||||
def _cache_slice(
|
||||
self, start: int, end: int, step: int = 1
|
||||
) -> list[LogEntry]:
|
||||
# Deque doesn't natively support slicing but we can do it manually.
|
||||
# It sounds like rotating the deque and pulling from the beginning
|
||||
# is the most efficient way to do this. The downside is the deque
|
||||
# gets temporarily modified in the process so we need to make sure
|
||||
# we're holding the lock.
|
||||
assert self._cache_lock.locked()
|
||||
cache = self._cache
|
||||
cache.rotate(-start)
|
||||
slc = [e[1] for e in itertools.islice(cache, 0, end - start, step)]
|
||||
cache.rotate(start)
|
||||
return slc
|
||||
|
||||
# Special case - filter out this common extra-chatty category.
|
||||
# TODO - should use a standard logging.Filter for this.
|
||||
@classmethod
|
||||
def _is_immutable_log_data(cls, data: Any) -> bool:
|
||||
if isinstance(data, (str, bool, int, float, bytes)):
|
||||
return True
|
||||
if isinstance(data, tuple):
|
||||
return all(cls._is_immutable_log_data(x) for x in data)
|
||||
return False
|
||||
|
||||
def emit(self, record: logging.LogRecord) -> None:
|
||||
if __debug__:
|
||||
starttime = time.monotonic()
|
||||
|
||||
# Called by logging to send us records.
|
||||
|
||||
# Special case: filter out this common extra-chatty category.
|
||||
# TODO - perhaps should use a standard logging.Filter for this.
|
||||
if (
|
||||
self._suppress_non_root_debug
|
||||
and record.name != 'root'
|
||||
|
|
@ -259,38 +285,106 @@ class LogHandler(logging.Handler):
|
|||
):
|
||||
return
|
||||
|
||||
# We want to forward as much as we can along without processing it
|
||||
# (better to do so in a bg thread).
|
||||
# However its probably best to flatten the message string here since
|
||||
# it could cause problems stringifying things in threads where they
|
||||
# didn't expect to be stringified.
|
||||
msg = self.format(record)
|
||||
|
||||
# Also immediately print pretty colored output to our echo file
|
||||
# (generally stderr). We do this part here instead of in our bg
|
||||
# thread because the delay can throw off command line prompts or
|
||||
# make tight debugging harder.
|
||||
if self._echofile is not None:
|
||||
ends = LEVELNO_COLOR_CODES.get(record.levelno)
|
||||
if ends is not None:
|
||||
self._echofile.write(f'{ends[0]}{msg}{ends[1]}\n')
|
||||
else:
|
||||
self._echofile.write(f'{msg}\n')
|
||||
|
||||
self._event_loop.call_soon_threadsafe(
|
||||
tpartial(
|
||||
self._emit_in_thread,
|
||||
record.name,
|
||||
record.levelno,
|
||||
record.created,
|
||||
msg,
|
||||
)
|
||||
# Optimization: if our log args are all simple immutable values,
|
||||
# we can just kick the whole thing over to our background thread to
|
||||
# be formatted there at our leisure. If anything is mutable and
|
||||
# thus could possibly change between now and then or if we want
|
||||
# to do immediate file echoing then we need to bite the bullet
|
||||
# and do that stuff here at the call site.
|
||||
fast_path = self._echofile is None and self._is_immutable_log_data(
|
||||
record.args
|
||||
)
|
||||
|
||||
if fast_path:
|
||||
if __debug__:
|
||||
formattime = echotime = time.monotonic()
|
||||
self._event_loop.call_soon_threadsafe(
|
||||
tpartial(
|
||||
self._emit_in_thread,
|
||||
record.name,
|
||||
record.levelno,
|
||||
record.created,
|
||||
record,
|
||||
)
|
||||
)
|
||||
else:
|
||||
# Slow case; do formatting and echoing here at the log call
|
||||
# site.
|
||||
msg = self.format(record)
|
||||
|
||||
if __debug__:
|
||||
formattime = time.monotonic()
|
||||
|
||||
# Also immediately print pretty colored output to our echo file
|
||||
# (generally stderr). We do this part here instead of in our bg
|
||||
# thread because the delay can throw off command line prompts or
|
||||
# make tight debugging harder.
|
||||
if self._echofile is not None:
|
||||
ends = LEVELNO_COLOR_CODES.get(record.levelno)
|
||||
if ends is not None:
|
||||
self._echofile.write(f'{ends[0]}{msg}{ends[1]}\n')
|
||||
else:
|
||||
self._echofile.write(f'{msg}\n')
|
||||
|
||||
if __debug__:
|
||||
echotime = time.monotonic()
|
||||
|
||||
self._event_loop.call_soon_threadsafe(
|
||||
tpartial(
|
||||
self._emit_in_thread,
|
||||
record.name,
|
||||
record.levelno,
|
||||
record.created,
|
||||
msg,
|
||||
)
|
||||
)
|
||||
|
||||
if __debug__:
|
||||
# Make noise if we're taking a significant amount of time here.
|
||||
# Limit the noise to once every so often though; otherwise we
|
||||
# could get a feedback loop where every log emit results in a
|
||||
# warning log which results in another, etc.
|
||||
now = time.monotonic()
|
||||
# noinspection PyUnboundLocalVariable
|
||||
duration = now - starttime
|
||||
# noinspection PyUnboundLocalVariable
|
||||
format_duration = formattime - starttime
|
||||
# noinspection PyUnboundLocalVariable
|
||||
echo_duration = echotime - formattime
|
||||
if duration > 0.05 and (
|
||||
self._last_slow_emit_warning_time is None
|
||||
or now > self._last_slow_emit_warning_time + 10.0
|
||||
):
|
||||
# Logging calls from *within* a logging handler
|
||||
# sounds sketchy, so let's just kick this over to
|
||||
# the bg event loop thread we've already got.
|
||||
self._last_slow_emit_warning_time = now
|
||||
self._event_loop.call_soon_threadsafe(
|
||||
tpartial(
|
||||
logging.warning,
|
||||
'efro.log.LogHandler emit took too long'
|
||||
' (%.2fs total; %.2fs format, %.2fs echo,'
|
||||
' fast_path=%s).',
|
||||
duration,
|
||||
format_duration,
|
||||
echo_duration,
|
||||
fast_path,
|
||||
)
|
||||
)
|
||||
|
||||
def _emit_in_thread(
|
||||
self, name: str, levelno: int, created: float, message: str
|
||||
self,
|
||||
name: str,
|
||||
levelno: int,
|
||||
created: float,
|
||||
message: str | logging.LogRecord,
|
||||
) -> None:
|
||||
try:
|
||||
|
||||
# If they passed a raw record here, bake it down to a string.
|
||||
if isinstance(message, logging.LogRecord):
|
||||
message = self.format(message)
|
||||
|
||||
self._emit_entry(
|
||||
LogEntry(
|
||||
name=name,
|
||||
|
|
@ -409,7 +503,7 @@ class LogHandler(logging.Handler):
|
|||
|
||||
# Prune old until we are back at or under our limit.
|
||||
while self._cache_size > self._cache_size_limit:
|
||||
popped = self._cache.pop(0)
|
||||
popped = self._cache.popleft()
|
||||
self._cache_size -= popped[0]
|
||||
self._cache_index_offset += 1
|
||||
|
||||
|
|
@ -469,6 +563,7 @@ def setup_logging(
|
|||
level: LogLevel,
|
||||
suppress_non_root_debug: bool = False,
|
||||
log_stdout_stderr: bool = False,
|
||||
echo_to_stderr: bool = True,
|
||||
cache_size_limit: int = 0,
|
||||
cache_time_limit: datetime.timedelta | None = None,
|
||||
) -> LogHandler:
|
||||
|
|
@ -499,8 +594,7 @@ def setup_logging(
|
|||
# which would create an infinite loop.
|
||||
loghandler = LogHandler(
|
||||
log_path,
|
||||
# echofile=sys.stderr if sys.stderr.isatty() else None,
|
||||
echofile=sys.stderr,
|
||||
echofile=sys.stderr if echo_to_stderr else None,
|
||||
suppress_non_root_debug=suppress_non_root_debug,
|
||||
cache_size_limit=cache_size_limit,
|
||||
cache_time_limit=cache_time_limit,
|
||||
|
|
|
|||
58
dist/ba_data/python/efro/message/_protocol.py
vendored
58
dist/ba_data/python/efro/message/_protocol.py
vendored
|
|
@ -282,10 +282,13 @@ class MessageProtocol:
|
|||
def _get_module_header(
|
||||
self,
|
||||
part: Literal['sender', 'receiver'],
|
||||
extra_import_code: str | None = None,
|
||||
extra_import_code: str | None,
|
||||
enable_async_sends: bool,
|
||||
) -> str:
|
||||
"""Return common parts of generated modules."""
|
||||
# pylint: disable=too-many-locals, too-many-branches
|
||||
# pylint: disable=too-many-locals
|
||||
# pylint: disable=too-many-branches
|
||||
# pylint: disable=too-many-statements
|
||||
import textwrap
|
||||
|
||||
tpimports: dict[str, list[str]] = {}
|
||||
|
|
@ -342,7 +345,7 @@ class MessageProtocol:
|
|||
|
||||
if part == 'sender':
|
||||
import_lines += (
|
||||
'from efro.message import MessageSender,' ' BoundMessageSender'
|
||||
'from efro.message import MessageSender, BoundMessageSender'
|
||||
)
|
||||
tpimport_typing_extras = ''
|
||||
else:
|
||||
|
|
@ -362,11 +365,18 @@ class MessageProtocol:
|
|||
import_lines += f'\n{extra_import_code}\n'
|
||||
|
||||
ovld = ', overload' if not single_message_type else ''
|
||||
ovld2 = (
|
||||
', cast, Awaitable'
|
||||
if (single_message_type and part == 'sender' and enable_async_sends)
|
||||
else ''
|
||||
)
|
||||
tpimport_lines = textwrap.indent(tpimport_lines, ' ')
|
||||
|
||||
baseimps = ['Any']
|
||||
if part == 'receiver':
|
||||
baseimps.append('Callable')
|
||||
if part == 'sender' and enable_async_sends:
|
||||
baseimps.append('Awaitable')
|
||||
baseimps_s = ', '.join(baseimps)
|
||||
out = (
|
||||
'# Released under the MIT License. See LICENSE for details.\n'
|
||||
|
|
@ -375,7 +385,7 @@ class MessageProtocol:
|
|||
f'\n'
|
||||
f'from __future__ import annotations\n'
|
||||
f'\n'
|
||||
f'from typing import TYPE_CHECKING{ovld}\n'
|
||||
f'from typing import TYPE_CHECKING{ovld}{ovld2}\n'
|
||||
f'\n'
|
||||
f'{import_lines}\n'
|
||||
f'\n'
|
||||
|
|
@ -399,13 +409,16 @@ class MessageProtocol:
|
|||
) -> str:
|
||||
"""Used by create_sender_module(); do not call directly."""
|
||||
# pylint: disable=too-many-locals
|
||||
# pylint: disable=too-many-branches
|
||||
import textwrap
|
||||
|
||||
msgtypes = list(self.message_ids_by_type.keys())
|
||||
|
||||
ppre = '_' if private else ''
|
||||
out = self._get_module_header(
|
||||
'sender', extra_import_code=protocol_module_level_import_code
|
||||
'sender',
|
||||
extra_import_code=protocol_module_level_import_code,
|
||||
enable_async_sends=enable_async_sends,
|
||||
)
|
||||
ccind = textwrap.indent(protocol_create_code, ' ')
|
||||
out += (
|
||||
|
|
@ -438,7 +451,8 @@ class MessageProtocol:
|
|||
continue
|
||||
pfx = 'async ' if async_pass else ''
|
||||
sfx = '_async' if async_pass else ''
|
||||
awt = 'await ' if async_pass else ''
|
||||
# awt = 'await ' if async_pass else ''
|
||||
awt = ''
|
||||
how = 'asynchronously' if async_pass else 'synchronously'
|
||||
|
||||
if len(msgtypes) == 1:
|
||||
|
|
@ -451,22 +465,29 @@ class MessageProtocol:
|
|||
rtypevar = ' | '.join(_filt_tp_name(t) for t in rtypes)
|
||||
else:
|
||||
rtypevar = _filt_tp_name(rtypes[0])
|
||||
if async_pass:
|
||||
rtypevar = f'Awaitable[{rtypevar}]'
|
||||
out += (
|
||||
f'\n'
|
||||
f' {pfx}def send{sfx}(self,'
|
||||
f' def send{sfx}(self,'
|
||||
f' message: {msgtypevar})'
|
||||
f' -> {rtypevar}:\n'
|
||||
f' """Send a message {how}."""\n'
|
||||
f' out = {awt}self._sender.'
|
||||
f'send{sfx}(self._obj, message)\n'
|
||||
f' assert isinstance(out, {rtypevar})\n'
|
||||
f' return out\n'
|
||||
)
|
||||
if not async_pass:
|
||||
out += (
|
||||
f' assert isinstance(out, {rtypevar})\n'
|
||||
' return out\n'
|
||||
)
|
||||
else:
|
||||
out += f' return cast({rtypevar}, out)\n'
|
||||
|
||||
else:
|
||||
|
||||
for msgtype in msgtypes:
|
||||
msgtypevar = msgtype.__name__
|
||||
# rtypes = msgtype.get_response_types()
|
||||
rtypes = msgtype.get_response_types()
|
||||
if len(rtypes) > 1:
|
||||
rtypevar = ' | '.join(
|
||||
|
|
@ -482,10 +503,13 @@ class MessageProtocol:
|
|||
f' -> {rtypevar}:\n'
|
||||
f' ...\n'
|
||||
)
|
||||
rtypevar = 'Response | None'
|
||||
if async_pass:
|
||||
rtypevar = f'Awaitable[{rtypevar}]'
|
||||
out += (
|
||||
f'\n'
|
||||
f' {pfx}def send{sfx}(self, message: Message)'
|
||||
f' -> Response | None:\n'
|
||||
f' def send{sfx}(self, message: Message)'
|
||||
f' -> {rtypevar}:\n'
|
||||
f' """Send a message {how}."""\n'
|
||||
f' return {awt}self._sender.'
|
||||
f'send{sfx}(self._obj, message)\n'
|
||||
|
|
@ -509,7 +533,9 @@ class MessageProtocol:
|
|||
ppre = '_' if private else ''
|
||||
msgtypes = list(self.message_ids_by_type.keys())
|
||||
out = self._get_module_header(
|
||||
'receiver', extra_import_code=protocol_module_level_import_code
|
||||
'receiver',
|
||||
extra_import_code=protocol_module_level_import_code,
|
||||
enable_async_sends=False,
|
||||
)
|
||||
ccind = textwrap.indent(protocol_create_code, ' ')
|
||||
out += (
|
||||
|
|
@ -602,11 +628,11 @@ class MessageProtocol:
|
|||
if is_async:
|
||||
out += (
|
||||
'\n'
|
||||
' async def handle_raw_message(\n'
|
||||
' def handle_raw_message(\n'
|
||||
' self, message: str, raise_unregistered: bool = False\n'
|
||||
' ) -> str:\n'
|
||||
' ) -> Awaitable[str]:\n'
|
||||
' """Asynchronously handle a raw incoming message."""\n'
|
||||
' return await self._receiver.'
|
||||
' return self._receiver.'
|
||||
'handle_raw_message_async(\n'
|
||||
' self._obj, message, raise_unregistered\n'
|
||||
' )\n'
|
||||
|
|
|
|||
135
dist/ba_data/python/efro/message/_receiver.py
vendored
135
dist/ba_data/python/efro/message/_receiver.py
vendored
|
|
@ -62,12 +62,6 @@ class MessageReceiver:
|
|||
[Any, Message | None, Response | SysResponse, dict], None
|
||||
] | None = None
|
||||
|
||||
# TODO: don't currently have async encode equivalent
|
||||
# or either for sender; can add as needed.
|
||||
self._decode_filter_async_call: Callable[
|
||||
[Any, dict, Message], Awaitable[None]
|
||||
] | None = None
|
||||
|
||||
# noinspection PyProtectedMember
|
||||
def register_handler(
|
||||
self, call: Callable[[Any, Message], Response | None]
|
||||
|
|
@ -96,14 +90,17 @@ class MessageReceiver:
|
|||
|
||||
# Make sure we are only given async methods if we are an async handler
|
||||
# and sync ones otherwise.
|
||||
is_async = inspect.iscoroutinefunction(call)
|
||||
if self.is_async != is_async:
|
||||
msg = (
|
||||
'Expected a sync method; found an async one.'
|
||||
if is_async
|
||||
else 'Expected an async method; found a sync one.'
|
||||
)
|
||||
raise ValueError(msg)
|
||||
# UPDATE - can't do this anymore since we now sometimes use
|
||||
# regular functions which return awaitables instead of having
|
||||
# the entire function be async.
|
||||
# is_async = inspect.iscoroutinefunction(call)
|
||||
# if self.is_async != is_async:
|
||||
# msg = (
|
||||
# 'Expected a sync method; found an async one.'
|
||||
# if is_async
|
||||
# else 'Expected an async method; found a sync one.'
|
||||
# )
|
||||
# raise ValueError(msg)
|
||||
|
||||
# Check annotation types to determine what message types we handle.
|
||||
# Return-type annotation can be a Union, but we probably don't
|
||||
|
|
@ -189,19 +186,6 @@ class MessageReceiver:
|
|||
self._decode_filter_call = call
|
||||
return call
|
||||
|
||||
def decode_filter_async_method(
|
||||
self, call: Callable[[Any, dict, Message], Awaitable[None]]
|
||||
) -> Callable[[Any, dict, Message], Awaitable[None]]:
|
||||
"""Function decorator for defining a decode filter.
|
||||
|
||||
Decode filters can be used to extract extra data from incoming
|
||||
message dicts. Note that this version will only work with
|
||||
handle_raw_message_async().
|
||||
"""
|
||||
assert self._decode_filter_async_call is None
|
||||
self._decode_filter_async_call = call
|
||||
return call
|
||||
|
||||
def encode_filter_method(
|
||||
self,
|
||||
call: Callable[
|
||||
|
|
@ -247,24 +231,6 @@ class MessageReceiver:
|
|||
bound_obj, _msg_dict, msg_decoded = self._decode_incoming_message_base(
|
||||
bound_obj=bound_obj, msg=msg
|
||||
)
|
||||
|
||||
# If they've set an async filter but are calling sync
|
||||
# handle_raw_message() its likely a bug.
|
||||
assert self._decode_filter_async_call is None
|
||||
|
||||
return msg_decoded
|
||||
|
||||
async def _decode_incoming_message_async(
|
||||
self, bound_obj: Any, msg: str
|
||||
) -> Message:
|
||||
bound_obj, msg_dict, msg_decoded = self._decode_incoming_message_base(
|
||||
bound_obj=bound_obj, msg=msg
|
||||
)
|
||||
|
||||
if self._decode_filter_async_call is not None:
|
||||
await self._decode_filter_async_call(
|
||||
bound_obj, msg_dict, msg_decoded
|
||||
)
|
||||
return msg_decoded
|
||||
|
||||
def encode_user_response(
|
||||
|
|
@ -316,6 +282,7 @@ class MessageReceiver:
|
|||
"""
|
||||
assert not self.is_async, "can't call sync handler on async receiver"
|
||||
msg_decoded: Message | None = None
|
||||
msgtype: type[Message] | None = None
|
||||
try:
|
||||
msg_decoded = self._decode_incoming_message(bound_obj, msg)
|
||||
msgtype = type(msg_decoded)
|
||||
|
|
@ -335,41 +302,93 @@ class MessageReceiver:
|
|||
bound_obj, msg_decoded, exc
|
||||
)
|
||||
if dolog:
|
||||
logging.exception('Error in efro.message handling.')
|
||||
if msgtype is not None:
|
||||
logging.exception(
|
||||
'Error handling %s.%s message.',
|
||||
msgtype.__module__,
|
||||
msgtype.__qualname__,
|
||||
)
|
||||
else:
|
||||
logging.exception('Error in efro.message handling.')
|
||||
return rstr
|
||||
|
||||
async def handle_raw_message_async(
|
||||
def handle_raw_message_async(
|
||||
self, bound_obj: Any, msg: str, raise_unregistered: bool = False
|
||||
) -> str:
|
||||
) -> Awaitable[str]:
|
||||
"""Should be called when the receiver gets a message.
|
||||
|
||||
The return value is the raw response to the message.
|
||||
"""
|
||||
|
||||
# Note: This call is synchronous so that the first part of it can
|
||||
# happen synchronously. If the whole call were async we wouldn't be
|
||||
# able to guarantee that messages handlers would be called in the
|
||||
# order the messages were received.
|
||||
|
||||
assert self.is_async, "can't call async handler on sync receiver"
|
||||
msg_decoded: Message | None = None
|
||||
msgtype: type[Message] | None = None
|
||||
try:
|
||||
msg_decoded = await self._decode_incoming_message_async(
|
||||
bound_obj, msg
|
||||
)
|
||||
msg_decoded = self._decode_incoming_message(bound_obj, msg)
|
||||
msgtype = type(msg_decoded)
|
||||
handler = self._handlers.get(msgtype)
|
||||
if handler is None:
|
||||
raise RuntimeError(f'Got unhandled message type: {msgtype}.')
|
||||
response = await handler(bound_obj, msg_decoded)
|
||||
assert isinstance(response, Response | None)
|
||||
return self.encode_user_response(bound_obj, msg_decoded, response)
|
||||
handler_awaitable = handler(bound_obj, msg_decoded)
|
||||
|
||||
except Exception as exc:
|
||||
if raise_unregistered and isinstance(
|
||||
exc, UnregisteredMessageIDError
|
||||
):
|
||||
raise
|
||||
rstr, dolog = self.encode_error_response(
|
||||
bound_obj, msg_decoded, exc
|
||||
return self._handle_raw_message_async_error(
|
||||
bound_obj, msg_decoded, msgtype, exc
|
||||
)
|
||||
if dolog:
|
||||
|
||||
# Return an awaitable to handle the rest asynchronously.
|
||||
return self._handle_raw_message_async(
|
||||
bound_obj, msg_decoded, msgtype, handler_awaitable
|
||||
)
|
||||
|
||||
async def _handle_raw_message_async_error(
|
||||
self,
|
||||
bound_obj: Any,
|
||||
msg_decoded: Message | None,
|
||||
msgtype: type[Message] | None,
|
||||
exc: Exception,
|
||||
) -> str:
|
||||
rstr, dolog = self.encode_error_response(bound_obj, msg_decoded, exc)
|
||||
if dolog:
|
||||
if msgtype is not None:
|
||||
logging.exception(
|
||||
'Error handling %s.%s message.',
|
||||
msgtype.__module__,
|
||||
msgtype.__qualname__,
|
||||
)
|
||||
else:
|
||||
logging.exception('Error in efro.message handling.')
|
||||
return rstr
|
||||
return rstr
|
||||
|
||||
async def _handle_raw_message_async(
|
||||
self,
|
||||
bound_obj: Any,
|
||||
msg_decoded: Message,
|
||||
msgtype: type[Message] | None,
|
||||
handler_awaitable: Awaitable[Response | None],
|
||||
) -> str:
|
||||
"""Should be called when the receiver gets a message.
|
||||
|
||||
The return value is the raw response to the message.
|
||||
"""
|
||||
try:
|
||||
response = await handler_awaitable
|
||||
assert isinstance(response, Response | None)
|
||||
return self.encode_user_response(bound_obj, msg_decoded, response)
|
||||
|
||||
except Exception as exc:
|
||||
return await self._handle_raw_message_async_error(
|
||||
bound_obj, msg_decoded, msgtype, exc
|
||||
)
|
||||
|
||||
|
||||
class BoundMessageReceiver:
|
||||
|
|
|
|||
126
dist/ba_data/python/efro/message/_sender.py
vendored
126
dist/ba_data/python/efro/message/_sender.py
vendored
|
|
@ -44,6 +44,9 @@ class MessageSender:
|
|||
self._send_async_raw_message_call: Callable[
|
||||
[Any, str], Awaitable[str]
|
||||
] | None = None
|
||||
self._send_async_raw_message_ex_call: Callable[
|
||||
[Any, str, Message], Awaitable[str]
|
||||
] | None = None
|
||||
self._encode_filter_call: Callable[
|
||||
[Any, Message, dict], None
|
||||
] | None = None
|
||||
|
|
@ -75,11 +78,32 @@ class MessageSender:
|
|||
CommunicationErrors raised here will be returned to the sender
|
||||
as such; all other exceptions will result in a RuntimeError for
|
||||
the sender.
|
||||
|
||||
IMPORTANT: Generally async send methods should not be implemented
|
||||
as 'async' methods, but instead should be regular methods that
|
||||
return awaitable objects. This way it can be guaranteed that
|
||||
outgoing messages are synchronously enqueued in the correct
|
||||
order, and then async calls can be returned which finish each
|
||||
send. If the entire call is async, they may be enqueued out of
|
||||
order in rare cases.
|
||||
"""
|
||||
assert self._send_async_raw_message_call is None
|
||||
self._send_async_raw_message_call = call
|
||||
return call
|
||||
|
||||
def send_async_ex_method(
|
||||
self, call: Callable[[Any, str, Message], Awaitable[str]]
|
||||
) -> Callable[[Any, str, Message], Awaitable[str]]:
|
||||
"""Function decorator for extended send-async method.
|
||||
|
||||
Version of send_async_method which is also is passed the original
|
||||
unencoded message; can be useful for cases where metadata is sent
|
||||
along with messages referring to their payloads/etc.
|
||||
"""
|
||||
assert self._send_async_raw_message_ex_call is None
|
||||
self._send_async_raw_message_ex_call = call
|
||||
return call
|
||||
|
||||
def encode_filter_method(
|
||||
self, call: Callable[[Any, Message, dict], None]
|
||||
) -> Callable[[Any, Message, dict], None]:
|
||||
|
|
@ -126,17 +150,34 @@ class MessageSender:
|
|||
),
|
||||
)
|
||||
|
||||
async def send_async(
|
||||
def send_async(
|
||||
self, bound_obj: Any, message: Message
|
||||
) -> Response | None:
|
||||
) -> Awaitable[Response | None]:
|
||||
"""Send a message asynchronously."""
|
||||
|
||||
# Note: This call is synchronous so that the first part of it can
|
||||
# happen synchronously. If the whole call were async we wouldn't be
|
||||
# able to guarantee that messages sent in order would actually go
|
||||
# out in order.
|
||||
raw_response_awaitable = self.fetch_raw_response_async(
|
||||
bound_obj=bound_obj,
|
||||
message=message,
|
||||
)
|
||||
# Now return an awaitable that will finish the send.
|
||||
return self._send_async_awaitable(
|
||||
bound_obj, message, raw_response_awaitable
|
||||
)
|
||||
|
||||
async def _send_async_awaitable(
|
||||
self,
|
||||
bound_obj: Any,
|
||||
message: Message,
|
||||
raw_response_awaitable: Awaitable[Response | SysResponse],
|
||||
) -> Response | None:
|
||||
return self.unpack_raw_response(
|
||||
bound_obj=bound_obj,
|
||||
message=message,
|
||||
raw_response=await self.fetch_raw_response_async(
|
||||
bound_obj=bound_obj,
|
||||
message=message,
|
||||
),
|
||||
raw_response=await raw_response_awaitable,
|
||||
)
|
||||
|
||||
def fetch_raw_response(
|
||||
|
|
@ -171,27 +212,68 @@ class MessageSender:
|
|||
return response
|
||||
return self._decode_raw_response(bound_obj, message, response_encoded)
|
||||
|
||||
async def fetch_raw_response_async(
|
||||
def fetch_raw_response_async(
|
||||
self, bound_obj: Any, message: Message
|
||||
) -> Response | SysResponse:
|
||||
"""Fetch a raw message response.
|
||||
) -> Awaitable[Response | SysResponse]:
|
||||
"""Fetch a raw message response awaitable.
|
||||
|
||||
The result of this should be passed to unpack_raw_response() to
|
||||
produce the final message result.
|
||||
The result of this should be awaited and then passed to
|
||||
unpack_raw_response() to produce the final message result.
|
||||
|
||||
Generally you can just call send(); calling fetch and unpack
|
||||
manually is for when message sending and response handling need
|
||||
to happen in different contexts/threads.
|
||||
"""
|
||||
|
||||
if self._send_async_raw_message_call is None:
|
||||
# Note: This call is synchronous so that the first part of it can
|
||||
# happen synchronously. If the whole call were async we wouldn't be
|
||||
# able to guarantee that messages sent in order would actually go
|
||||
# out in order.
|
||||
if (
|
||||
self._send_async_raw_message_call is None
|
||||
and self._send_async_raw_message_ex_call is None
|
||||
):
|
||||
raise RuntimeError('send_async() is unimplemented for this type.')
|
||||
|
||||
msg_encoded = self._encode_message(bound_obj, message)
|
||||
try:
|
||||
response_encoded = await self._send_async_raw_message_call(
|
||||
bound_obj, msg_encoded
|
||||
)
|
||||
if self._send_async_raw_message_ex_call is not None:
|
||||
send_awaitable = self._send_async_raw_message_ex_call(
|
||||
bound_obj, msg_encoded, message
|
||||
)
|
||||
else:
|
||||
assert self._send_async_raw_message_call is not None
|
||||
send_awaitable = self._send_async_raw_message_call(
|
||||
bound_obj, msg_encoded
|
||||
)
|
||||
except Exception as exc:
|
||||
return self._error_awaitable(exc)
|
||||
|
||||
# Now return an awaitable to finish the job.
|
||||
return self._fetch_raw_response_awaitable(
|
||||
bound_obj, message, send_awaitable
|
||||
)
|
||||
|
||||
async def _error_awaitable(self, exc: Exception) -> SysResponse:
|
||||
response = ErrorSysResponse(
|
||||
error_message='Error in MessageSender @send_async_method.',
|
||||
error_type=(
|
||||
ErrorSysResponse.ErrorType.COMMUNICATION
|
||||
if isinstance(exc, CommunicationError)
|
||||
else ErrorSysResponse.ErrorType.LOCAL
|
||||
),
|
||||
)
|
||||
# Can include the actual exception since we'll be looking at
|
||||
# this locally; might be helpful.
|
||||
response.set_local_exception(exc)
|
||||
return response
|
||||
|
||||
async def _fetch_raw_response_awaitable(
|
||||
self, bound_obj: Any, message: Message, send_awaitable: Awaitable[str]
|
||||
) -> Response | SysResponse:
|
||||
|
||||
try:
|
||||
response_encoded = await send_awaitable
|
||||
except Exception as exc:
|
||||
response = ErrorSysResponse(
|
||||
error_message='Error in MessageSender @send_async_method.',
|
||||
|
|
@ -354,23 +436,23 @@ class BoundMessageSender:
|
|||
assert self._obj is not None
|
||||
return self._sender.send(bound_obj=self._obj, message=message)
|
||||
|
||||
async def send_async_untyped(self, message: Message) -> Response | None:
|
||||
def send_async_untyped(
|
||||
self, message: Message
|
||||
) -> Awaitable[Response | None]:
|
||||
"""Send a message asynchronously.
|
||||
|
||||
Whenever possible, use the send_async() call provided by generated
|
||||
subclasses instead of this; it will provide better type safety.
|
||||
"""
|
||||
assert self._obj is not None
|
||||
return await self._sender.send_async(
|
||||
bound_obj=self._obj, message=message
|
||||
)
|
||||
return self._sender.send_async(bound_obj=self._obj, message=message)
|
||||
|
||||
async def fetch_raw_response_async_untyped(
|
||||
def fetch_raw_response_async_untyped(
|
||||
self, message: Message
|
||||
) -> Response | SysResponse:
|
||||
) -> Awaitable[Response | SysResponse]:
|
||||
"""Split send (part 1 of 2)."""
|
||||
assert self._obj is not None
|
||||
return await self._sender.fetch_raw_response_async(
|
||||
return self._sender.fetch_raw_response_async(
|
||||
bound_obj=self._obj, message=message
|
||||
)
|
||||
|
||||
|
|
|
|||
55
dist/ba_data/python/efro/rpc.py
vendored
55
dist/ba_data/python/efro/rpc.py
vendored
|
|
@ -9,6 +9,7 @@ import asyncio
|
|||
import logging
|
||||
import weakref
|
||||
from enum import Enum
|
||||
from collections import deque
|
||||
from dataclasses import dataclass
|
||||
from threading import current_thread
|
||||
from typing import TYPE_CHECKING, Annotated
|
||||
|
|
@ -201,7 +202,7 @@ class RPCEndpoint:
|
|||
self._closing = False
|
||||
self._did_wait_closed = False
|
||||
self._event_loop = asyncio.get_running_loop()
|
||||
self._out_packets: list[bytes] = []
|
||||
self._out_packets = deque[bytes]()
|
||||
self._have_out_packets = asyncio.Event()
|
||||
self._run_called = False
|
||||
self._peer_info: _PeerInfo | None = None
|
||||
|
|
@ -323,12 +324,12 @@ class RPCEndpoint:
|
|||
if self.debug_print:
|
||||
self.debug_print_call(f'{self._label}: finished.')
|
||||
|
||||
async def send_message(
|
||||
def send_message(
|
||||
self,
|
||||
message: bytes,
|
||||
timeout: float | None = None,
|
||||
close_on_error: bool = True,
|
||||
) -> bytes:
|
||||
) -> Awaitable[bytes]:
|
||||
"""Send a message to the peer and return a response.
|
||||
|
||||
If timeout is not provided, the default will be used.
|
||||
|
|
@ -340,7 +341,10 @@ class RPCEndpoint:
|
|||
respect to a given endpoint. Pass close_on_error=False to
|
||||
override this for a particular message.
|
||||
"""
|
||||
# pylint: disable=too-many-branches
|
||||
# Note: This call is synchronous so that the first part of it
|
||||
# (enqueueing outgoing messages) happens synchronously. If it were
|
||||
# a pure async call it could be possible for send order to vary
|
||||
# based on how the async tasks get processed.
|
||||
|
||||
if self.debug_print_io:
|
||||
self.debug_print_call(
|
||||
|
|
@ -358,16 +362,6 @@ class RPCEndpoint:
|
|||
f'{self._label}: have peerinfo? {self._peer_info is not None}.'
|
||||
)
|
||||
|
||||
# We need to know their protocol, so if we haven't gotten a handshake
|
||||
# from them yet, just wait.
|
||||
while self._peer_info is None:
|
||||
await asyncio.sleep(0.01)
|
||||
assert self._peer_info is not None
|
||||
|
||||
if self._peer_info.protocol == 1:
|
||||
if len(message) > 65535:
|
||||
raise RuntimeError('Message cannot be larger than 65535 bytes')
|
||||
|
||||
# message_id is a 16 bit looping value.
|
||||
message_id = self._next_message_id
|
||||
self._next_message_id = (self._next_message_id + 1) % 65536
|
||||
|
|
@ -420,8 +414,35 @@ class RPCEndpoint:
|
|||
if timeout is None:
|
||||
timeout = self.DEFAULT_MESSAGE_TIMEOUT
|
||||
assert timeout is not None
|
||||
|
||||
bytes_awaitable = msgobj.wait_task
|
||||
|
||||
# Now complete the send asynchronously.
|
||||
return self._send_message(
|
||||
message, timeout, close_on_error, bytes_awaitable, message_id
|
||||
)
|
||||
|
||||
async def _send_message(
|
||||
self,
|
||||
message: bytes,
|
||||
timeout: float | None,
|
||||
close_on_error: bool,
|
||||
bytes_awaitable: asyncio.Task[bytes],
|
||||
message_id: int,
|
||||
) -> bytes:
|
||||
|
||||
# We need to know their protocol, so if we haven't gotten a handshake
|
||||
# from them yet, just wait.
|
||||
while self._peer_info is None:
|
||||
await asyncio.sleep(0.01)
|
||||
assert self._peer_info is not None
|
||||
|
||||
if self._peer_info.protocol == 1:
|
||||
if len(message) > 65535:
|
||||
raise RuntimeError('Message cannot be larger than 65535 bytes')
|
||||
|
||||
try:
|
||||
return await asyncio.wait_for(msgobj.wait_task, timeout=timeout)
|
||||
return await asyncio.wait_for(bytes_awaitable, timeout=timeout)
|
||||
except asyncio.CancelledError as exc:
|
||||
# Question: we assume this means the above wait_for() was
|
||||
# cancelled; how do we distinguish between this and *us* being
|
||||
|
|
@ -449,7 +470,7 @@ class RPCEndpoint:
|
|||
)
|
||||
|
||||
# Stop waiting on the response.
|
||||
msgobj.wait_task.cancel()
|
||||
bytes_awaitable.cancel()
|
||||
|
||||
# Remove the record of this message.
|
||||
del self._in_flight_messages[message_id]
|
||||
|
|
@ -738,7 +759,7 @@ class RPCEndpoint:
|
|||
await self._have_out_packets.wait()
|
||||
|
||||
assert self._out_packets
|
||||
data = self._out_packets.pop(0)
|
||||
data = self._out_packets.popleft()
|
||||
|
||||
# Important: only clear this once all packets are sent.
|
||||
if not self._out_packets:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue