This commit is contained in:
Ayush Saini 2022-10-08 01:41:38 +05:30
parent 7d3de819bf
commit 59730e19b3
17 changed files with 180 additions and 65 deletions

View file

@ -78,7 +78,7 @@
"adan",
"Adeel (AdeZ {@adez_})",
"Adel",
"Rio adi",
"Rio Adi",
"Rayhan Adiansyah",
"Yonas Adiel",
"admin",

View file

@ -1238,7 +1238,7 @@
"enablePackageModsText": "Ativa pacheti mod łogałi",
"enterPromoCodeText": "Insarisi còdaze",
"forTestingText": "Nota: vałori vàłidi soło par proe. Sortendo da l'apl łi vegnarà perdesti.",
"helpTranslateText": "Łe tradusion de ${APP_NAME} łe ze curàe da vołontari.\nSe A te vol darghe na ociada a cheła veneta, struca so'l boton\ncuà soto. Curada da Còdaze Veneto: codazeveneto@gmail.com",
"helpTranslateText": "Łe tradusion de ${APP_NAME} łe ze curàe da vołontari.\nSe te vołi darghe na ociada a cheła veneta, struca so'l boton\ncuà soto. Curada da VeC: venetianlanguage@gmail.com",
"kickIdlePlayersText": "Para fora zugadori in sonera",
"kidFriendlyModeText": "Modałidà bocia (viołensa reduzesta, evc)",
"languageText": "Łengua",

View file

@ -9,7 +9,7 @@ from typing import TYPE_CHECKING
import _ba
if TYPE_CHECKING:
pass
from typing import Any
class AccountV2Subsystem:
@ -111,10 +111,21 @@ class AccountV2Subsystem:
class AccountV2Handle:
"""Handle for interacting with a v2 account."""
"""Handle for interacting with a V2 account.
This class supports the 'with' statement, which is how it is
used with some operations such as cloud messaging.
"""
def __init__(self) -> None:
self.tag = '?'
self.workspacename: str | None = None
self.workspaceid: str | None = None
def __enter__(self) -> None:
"""Support for "with" statement.
"""
def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> Any:
"""Support for "with" statement."""

View file

@ -60,7 +60,7 @@ def run_stress_test(playlist_type: str = 'Random',
from ba._generated.enums import TimeType
_ba.screenmessage(
'Beginning stress test.. use '
"'End Game' to stop testing.",
"'End Test' to stop testing.",
color=(1, 1, 0))
with _ba.Context('ui'):
start_stress_test({
@ -138,7 +138,8 @@ def _reset_stress_test(args: dict[str, Any]) -> None:
def run_gpu_benchmark() -> None:
"""Kick off a benchmark to test gpu speeds."""
_ba.screenmessage('FIXME: Not wired up yet.', color=(1, 0, 0))
# FIXME: Not wired up yet.
_ba.screenmessage('Not wired up yet.', color=(1, 0, 0))
def run_media_reload_benchmark() -> None:

View file

@ -45,7 +45,7 @@ def bootstrap() -> None:
# Give a soft warning if we're being used with a different binary
# version than we expect.
expected_build = 20887
expected_build = 20891
running_build: int = env['build_number']
if running_build != expected_build:
print(

View file

@ -552,7 +552,7 @@ class AccountSettingsWindow(ba.Window):
position=((self._sub_width - button_width) * 0.5, v + 30),
autoselect=True,
size=(button_width, 60),
label=ba.Lstr(resource=self._r + '.manageAccount'),
label=ba.Lstr(resource=self._r + '.manageAccountText'),
color=(0.55, 0.5, 0.6),
icon=ba.gettexture('settingsIcon'),
textcolor=(0.75, 0.7, 0.8),

View file

@ -56,10 +56,11 @@ class MainMenuWindow(ba.Window):
self._gather_button: ba.Widget | None = None
self._start_button: ba.Widget | None = None
self._watch_button: ba.Widget | None = None
self._gc_button: ba.Widget | None = None
self._account_button: ba.Widget | None = None
self._how_to_play_button: ba.Widget | None = None
self._credits_button: ba.Widget | None = None
self._settings_button: ba.Widget | None = None
self._next_refresh_allow_time = 0.0
self._store_char_tex = self._get_store_char_tex()
@ -71,7 +72,7 @@ class MainMenuWindow(ba.Window):
self._account_state_num = ba.internal.get_v1_account_state_num()
self._account_type = (ba.internal.get_v1_account_type()
if self._account_state == 'signed_in' else None)
self._refresh_timer = ba.Timer(1.0,
self._refresh_timer = ba.Timer(0.27,
ba.WeakCall(self._check_refresh),
repeat=True,
timetype=ba.TimeType.REAL)
@ -132,11 +133,15 @@ class MainMenuWindow(ba.Window):
if not self._root_widget:
return
now = ba.time(ba.TimeType.REAL)
if now < self._next_refresh_allow_time:
return
# Don't refresh for the first few seconds the game is up so we don't
# interrupt the transition in.
ba.app.main_menu_window_refresh_check_count += 1
if ba.app.main_menu_window_refresh_check_count < 4:
return
# ba.app.main_menu_window_refresh_check_count += 1
# if ba.app.main_menu_window_refresh_check_count < 4:
# return
store_char_tex = self._get_store_char_tex()
account_state_num = ba.internal.get_v1_account_state_num()
@ -250,8 +255,12 @@ class MainMenuWindow(ba.Window):
scale=scale,
size=(self._button_width, self._button_height),
autoselect=self._use_autoselect,
label=ba.Lstr(resource=self._r + '.endGameText'),
on_activate_call=self._confirm_end_game)
label=ba.Lstr(resource=self._r +
('.endTestText' if self._is_benchmark(
) else '.endGameText')),
on_activate_call=(self._confirm_end_test
if self._is_benchmark() else
self._confirm_end_game))
# Assume we're in a client-session.
else:
ba.buttonwidget(
@ -424,6 +433,7 @@ class MainMenuWindow(ba.Window):
self._tdelay = 2.0
self._t_delay_inc = 0.02
self._t_delay_play = 1.7
self._next_refresh_allow_time = ba.time(ba.TimeType.REAL) + 2.01
ba.app.did_menu_intro = True
self._width = 400.0
self._height = 200.0
@ -605,7 +615,7 @@ class MainMenuWindow(ba.Window):
this_b_width = self._button_width
h, v, scale = positions[self._p_index]
self._p_index += 1
self._gc_button = ba.buttonwidget(
self._account_button = ba.buttonwidget(
parent=self._root_widget,
position=(h - this_b_width * 0.5 * scale, v),
size=(this_b_width, self._button_height),
@ -633,7 +643,7 @@ class MainMenuWindow(ba.Window):
tilt_scale=0.0)
self._tdelay += self._t_delay_inc
else:
self._gc_button = None
self._account_button = None
# How-to-play button.
h, v, scale = positions[self._p_index]
@ -848,7 +858,7 @@ class MainMenuWindow(ba.Window):
ba.containerwidget(edit=self._root_widget, transition='out_left')
ba.app.ui.set_main_menu_window(
AccountSettingsWindow(
origin_widget=self._gc_button).get_root_widget())
origin_widget=self._account_button).get_root_widget())
def _on_store_pressed(self) -> None:
# pylint: disable=cyclic-import
@ -863,6 +873,11 @@ class MainMenuWindow(ba.Window):
StoreBrowserWindow(
origin_widget=self._store_button).get_root_widget())
def _is_benchmark(self) -> bool:
session = ba.internal.get_foreground_host_session()
return (getattr(session, 'benchmark_type', None) == 'cpu'
or ba.app.stress_test_reset_timer is not None)
def _confirm_end_game(self) -> None:
# pylint: disable=cyclic-import
from bastd.ui.confirm import ConfirmWindow
@ -874,6 +889,16 @@ class MainMenuWindow(ba.Window):
self._end_game,
cancel_is_selected=True)
def _confirm_end_test(self) -> None:
# pylint: disable=cyclic-import
from bastd.ui.confirm import ConfirmWindow
# Select cancel by default; this occasionally gets called by accident
# in a fit of button mashing and this will help reduce damage.
ConfirmWindow(ba.Lstr(resource=self._r + '.exitToMenuText'),
self._end_game,
cancel_is_selected=True)
def _confirm_end_replay(self) -> None:
# pylint: disable=cyclic-import
from bastd.ui.confirm import ConfirmWindow
@ -965,8 +990,8 @@ class MainMenuWindow(ba.Window):
ba.app.ui.main_menu_selection = 'Credits'
elif sel == self._settings_button:
ba.app.ui.main_menu_selection = 'Settings'
elif sel == self._gc_button:
ba.app.ui.main_menu_selection = 'GameService'
elif sel == self._account_button:
ba.app.ui.main_menu_selection = 'Account'
elif sel == self._store_button:
ba.app.ui.main_menu_selection = 'Store'
elif sel == self._quit_button:
@ -997,8 +1022,8 @@ class MainMenuWindow(ba.Window):
sel = self._credits_button
elif sel_name == 'Settings':
sel = self._settings_button
elif sel_name == 'GameService':
sel = self._gc_button
elif sel_name == 'Account':
sel = self._account_button
elif sel_name == 'Store':
sel = self._store_button
elif sel_name == 'Quit':

View file

@ -146,13 +146,14 @@ def _run_diagnostics(weakwin: weakref.ref[NetTestingWindow]) -> None:
ba.pushcall(_print_in_logic_thread, from_other_thread=True)
def _print_test_results(call: Callable[[], Any]) -> None:
"""Run the provided call; return success/fail text & color."""
def _print_test_results(call: Callable[[], Any]) -> bool:
"""Run the provided call, print result, & return success."""
starttime = time.monotonic()
try:
call()
duration = time.monotonic() - starttime
_print(f'Succeeded in {duration:.2f}s.', color=(0, 1, 0))
return True
except Exception as exc:
import traceback
duration = time.monotonic() - starttime
@ -161,6 +162,7 @@ def _run_diagnostics(weakwin: weakref.ref[NetTestingWindow]) -> None:
_print(msg, color=(1.0, 1.0, 0.3))
_print(f'Failed in {duration:.2f}s.', color=(1, 0, 0))
have_error[0] = True
return False
try:
_print(f'Running network diagnostics...\n'
@ -177,14 +179,23 @@ def _run_diagnostics(weakwin: weakref.ref[NetTestingWindow]) -> None:
# V1 ping
baseaddr = ba.internal.get_master_server_address(source=0, version=1)
_print(f'\nContacting V1 master-server src0 ({baseaddr})...')
_print_test_results(lambda: _test_fetch(baseaddr))
v1worked = _print_test_results(lambda: _test_fetch(baseaddr))
# V1 alternate ping
baseaddr = ba.internal.get_master_server_address(source=1, version=1)
_print(f'\nContacting V1 master-server src1 ({baseaddr})...')
_print_test_results(lambda: _test_fetch(baseaddr))
# V1 alternate ping (only if primary fails since this often fails).
if v1worked:
_print('\nSkipping V1 master-server src1 test since src0 worked.')
else:
baseaddr = ba.internal.get_master_server_address(source=1,
version=1)
_print(f'\nContacting V1 master-server src1 ({baseaddr})...')
_print_test_results(lambda: _test_fetch(baseaddr))
_print(f'\nV1-test-log: {ba.app.net.v1_test_log}')
if 'none succeeded' in ba.app.net.v1_test_log:
_print(f'\nV1-test-log failed: {ba.app.net.v1_test_log}',
color=(1, 0, 0))
have_error[0] = True
else:
_print(f'\nV1-test-log ok: {ba.app.net.v1_test_log}')
for srcid, result in sorted(ba.app.net.v1_ctest_results.items()):
_print(f'\nV1 src{srcid} result: {result}')

View file

@ -522,6 +522,7 @@ class TournamentEntryWindow(popup.PopupWindow):
if ticket_count is not None and ticket_count < ticket_cost:
getcurrency.show_get_tickets_prompt()
ba.playsound(ba.getsound('error'))
self._transition_out()
return
cur_time = ba.time(ba.TimeType.REAL, ba.TimeFormat.MILLISECONDS)

View file

@ -61,6 +61,7 @@ class ErrorSysResponse(SysResponse):
REMOTE_CLEAN = 1
LOCAL = 2
COMMUNICATION = 3
REMOTE_COMMUNICATION = 4
error_message: Annotated[str, IOAttrs('m')]
error_type: Annotated[ErrorType, IOAttrs('e')] = ErrorType.REMOTE

View file

@ -8,10 +8,9 @@ from __future__ import annotations
from typing import TYPE_CHECKING
import traceback
import logging
import json
from efro.error import CleanError
from efro.error import CleanError, CommunicationError
from efro.dataclassio import (is_ioprepped_dataclass, dataclass_to_dict,
dataclass_from_dict)
from efro.message._message import (Message, Response, SysResponse,
@ -34,24 +33,50 @@ class MessageProtocol:
def __init__(self,
message_types: dict[int, type[Message]],
response_types: dict[int, type[Response]],
forward_communication_errors: bool = False,
forward_clean_errors: bool = False,
remote_errors_include_stack_traces: bool = False) -> None:
remote_errors_include_stack_traces: bool = False,
log_remote_errors: bool = True) -> None:
"""Create a protocol with a given configuration.
Note that common response types are automatically registered
with (unchanging negative ids) so they don't need to be passed
explicitly (but can be if a different id is desired).
If 'forward_communication_errors' is True,
efro.error.CommunicationErrors raised on the receiver end will
result in a matching error raised back on the sender. This can
be useful if the receiver will be in some way forwarding
messages along and the sender doesn't need to know where
communication breakdowns occurred; only that they did.
If 'forward_clean_errors' is True, efro.error.CleanError
exceptions raised on the receiver end will result in a matching
CleanError raised back on the sender. All other Exception types
come across as efro.error.RemoteError.
CleanError raised back on the sender.
If 'remote_errors_include_stack_traces' is True, stringified stack
traces will be returned to the sender for exceptions occurring
on the receiver end. This can make debugging easier but should
only be used when the client is trusted to see such info.
When an exception is not covered by the optional forwarding
mechanisms above, it will come across as efro.error.RemoteError
and the exception will be logged on the receiver
end - at least by default (see details below).
If 'remote_errors_include_stack_traces' is True, stringified
stack traces will be returned with efro.error.RemoteError
exceptions. This is useful for debugging but should only be
enabled in cases where the sender is trusted to see internal
details of the receiver.
By default, when a message-handling exception will result in an
efro.error.RemoteError being returned to the sender, the
exception will be logged on the receiver. This is because the
goal is usually to avoid returning opaque RemoteErrors and to
instead return something meaningful as part of the expected
response type (even if that value itself represents a logical
error state). If 'log_remote_errors' is False, however, such
exceptions will not be logged on the receiver. This can be
useful in combination with 'remote_errors_include_stack_traces'
and 'forward_clean_errors' in situations where all error
logging/management will be happening on the sender end. Be
aware, however, that in that case it may be possible for
communication errors to prevent such error messages from
ever being seen.
"""
# pylint: disable=too-many-locals
self.message_types_by_id: dict[int, type[Message]] = {}
self.message_ids_by_type: dict[type[Message], int] = {}
self.response_types_by_id: dict[int, type[Response]
@ -123,8 +148,10 @@ class MessageProtocol:
' all types are required to have unique names.')
self.forward_clean_errors = forward_clean_errors
self.forward_communication_errors = forward_communication_errors
self.remote_errors_include_stack_traces = (
remote_errors_include_stack_traces)
self.log_remote_errors = log_remote_errors
@staticmethod
def encode_dict(obj: dict) -> str:
@ -139,23 +166,31 @@ class MessageProtocol:
"""Encode a response to a json ready dict."""
return self._to_dict(response, self.response_ids_by_type, 'response')
def error_to_response(self, exc: Exception) -> SysResponse:
"""Translate an error to a response."""
def error_to_response(self, exc: Exception) -> tuple[SysResponse, bool]:
"""Translate an Exception to a SysResponse.
# Log any errors we got during handling.
logging.exception('Error in efro.message handling.')
Also returns whether the error should be logged if this happened
within handle_raw_message().
"""
# If anything goes wrong, return a ErrorSysResponse instead.
# (either CLEAN or generic REMOTE)
if isinstance(exc, CleanError) and self.forward_clean_errors:
return ErrorSysResponse(
if self.forward_clean_errors and isinstance(exc, CleanError):
return (ErrorSysResponse(
error_message=str(exc),
error_type=ErrorSysResponse.ErrorType.REMOTE_CLEAN)
return ErrorSysResponse(
error_type=ErrorSysResponse.ErrorType.REMOTE_CLEAN), False)
if self.forward_communication_errors and isinstance(
exc, CommunicationError):
return (ErrorSysResponse(
error_message=str(exc),
error_type=ErrorSysResponse.ErrorType.REMOTE_COMMUNICATION),
False)
return (ErrorSysResponse(
error_message=(traceback.format_exc()
if self.remote_errors_include_stack_traces else
'An internal error has occurred.'),
error_type=ErrorSysResponse.ErrorType.REMOTE)
error_type=ErrorSysResponse.ErrorType.REMOTE),
self.log_remote_errors)
def _to_dict(self, message: Any, ids_by_type: dict[type, int],
opname: str) -> dict:

View file

@ -260,14 +260,14 @@ class MessageReceiver:
return self.protocol.encode_dict(response_dict)
def encode_error_response(self, bound_obj: Any, message: Message | None,
exc: Exception) -> str:
"""Given an error, return a response ready for sending."""
response = self.protocol.error_to_response(exc)
exc: Exception) -> tuple[str, bool]:
"""Given an error, return sysresponse str and whether to log."""
response, dolog = self.protocol.error_to_response(exc)
response_dict = self.protocol.response_to_dict(response)
if self._encode_filter_call is not None:
self._encode_filter_call(bound_obj, message, response,
response_dict)
return self.protocol.encode_dict(response_dict)
return self.protocol.encode_dict(response_dict), dolog
def handle_raw_message(self,
bound_obj: Any,
@ -296,7 +296,11 @@ class MessageReceiver:
if (raise_unregistered
and isinstance(exc, UnregisteredMessageIDError)):
raise
return self.encode_error_response(bound_obj, msg_decoded, exc)
rstr, dolog = self.encode_error_response(bound_obj, msg_decoded,
exc)
if dolog:
logging.exception('Error in efro.message handling.')
return rstr
async def handle_raw_message_async(
self,
@ -324,7 +328,11 @@ class MessageReceiver:
if (raise_unregistered
and isinstance(exc, UnregisteredMessageIDError)):
raise
return self.encode_error_response(bound_obj, msg_decoded, exc)
rstr, dolog = self.encode_error_response(bound_obj, msg_decoded,
exc)
if dolog:
logging.exception('Error in efro.message handling.')
return rstr
class BoundMessageReceiver:
@ -348,9 +356,9 @@ class BoundMessageReceiver:
"""Given an error, return a response ready to send.
This should be used for any errors that happen outside of
of standard handle_raw_message calls. Any errors within those
calls should be automatically returned as encoded strings.
standard handle_raw_message calls. Any errors within those
calls will be automatically returned as encoded strings.
"""
# Passing None for Message here; we would only have that available
# for things going wrong in the handler (which this is not for).
return self._receiver.encode_error_response(self._obj, None, exc)
return self._receiver.encode_error_response(self._obj, None, exc)[0]

View file

@ -272,6 +272,11 @@ class MessageSender:
is ErrorSysResponse.ErrorType.REMOTE_CLEAN):
raise CleanError(raw_response.error_message)
if (self.protocol.forward_communication_errors
and raw_response.error_type is
ErrorSysResponse.ErrorType.REMOTE_COMMUNICATION):
raise CommunicationError(raw_response.error_message)
# Everything else gets lumped in as a remote error.
raise RemoteError(raw_response.error_message,
peer_desc=('peer' if self._peer_desc_call is None

View file

@ -290,17 +290,24 @@ class RPCEndpoint:
async def send_message(self,
message: bytes,
timeout: float | None = None) -> bytes:
timeout: float | None = None,
close_on_error: bool = True) -> bytes:
"""Send a message to the peer and return a response.
If timeout is not provided, the default will be used.
Raises a CommunicationError if the round trip is not completed
for any reason.
By default, the entire endpoint will go down in the case of
errors. This allows messages to be treated as 'reliable' with
respect to a given endpoint. Pass close_on_error=False to
override this for a particular message.
"""
# pylint: disable=too-many-branches
self._check_env()
if self._closing:
raise CommunicationError('Endpoint is closed')
raise CommunicationError('Endpoint is closed.')
# We need to know their protocol, so if we haven't gotten a handshake
# from them yet, just wait.
@ -358,6 +365,8 @@ class RPCEndpoint:
if self._debug_print:
self._debug_print_call(
f'{self._label}: message {message_id} was cancelled.')
if close_on_error:
self.close()
raise CommunicationError() from exc
except asyncio.TimeoutError as exc:
if self._debug_print:
@ -370,6 +379,9 @@ class RPCEndpoint:
# Remove the record of this message.
del self._in_flight_messages[message_id]
if close_on_error:
self.close()
# Let the user know something went wrong.
raise CommunicationError() from exc
@ -407,7 +419,11 @@ class RPCEndpoint:
return self._closing
async def wait_closed(self) -> None:
"""I said seagulls; mmmm; stop it now."""
"""I said seagulls; mmmm; stop it now.
Wait for the endpoint to finish closing. This is called by run()
so generally does not need to be explicitly called.
"""
# pylint: disable=too-many-branches
self._check_env()
@ -729,8 +745,9 @@ class RPCEndpoint:
def _check_env(self) -> None:
# I was seeing that asyncio stuff wasn't working as expected if
# created in one thread and used in another, so let's enforce
# a single thread for all use of an instance.
# created in one thread and used in another (and have verified
# that this is part of the design), so let's enforce a single
# thread for all use of an instance.
if current_thread() is not self._thread:
raise RuntimeError('This must be called from the same thread'
' that the endpoint was created in.')

Binary file not shown.

Binary file not shown.

Binary file not shown.