mirror of
https://github.com/RYDE-WORK/ballistica.git
synced 2026-01-19 13:25:31 +08:00
subscription wiring - account button now updates
This commit is contained in:
parent
2cb0bf0f4f
commit
8f176d274c
72
.efrocachemap
generated
72
.efrocachemap
generated
@ -4099,42 +4099,42 @@
|
||||
"build/assets/windows/Win32/ucrtbased.dll": "bfd1180c269d3950b76f35a63655e9e1",
|
||||
"build/assets/windows/Win32/vc_redist.x86.exe": "15a5f1f876503885adbdf5b3989b3718",
|
||||
"build/assets/windows/Win32/vcruntime140d.dll": "865b2af4d1e26a1a8073c89acb06e599",
|
||||
"build/prefab/full/linux_arm64_gui/debug/ballisticakit": "0921e644948438c78c8621f1199dc6d6",
|
||||
"build/prefab/full/linux_arm64_gui/release/ballisticakit": "54486e895c6be0128a2e25624aaef051",
|
||||
"build/prefab/full/linux_arm64_server/debug/dist/ballisticakit_headless": "bcfc357760d1030c66e3b65c30a649c8",
|
||||
"build/prefab/full/linux_arm64_server/release/dist/ballisticakit_headless": "a398b7f179cf6ee31af8037bae59bdc4",
|
||||
"build/prefab/full/linux_x86_64_gui/debug/ballisticakit": "9c5e37721e8834491c249b3f1e986e8a",
|
||||
"build/prefab/full/linux_x86_64_gui/release/ballisticakit": "57dd4fa7356ea5a1a102c5a0422a4485",
|
||||
"build/prefab/full/linux_x86_64_server/debug/dist/ballisticakit_headless": "cad5283f88be83233e7a8b674db02c17",
|
||||
"build/prefab/full/linux_x86_64_server/release/dist/ballisticakit_headless": "f36a3b1ef5d5650c373ac3230f3e64a3",
|
||||
"build/prefab/full/mac_arm64_gui/debug/ballisticakit": "327abf1bd532b104cbb63fa9dd11f9b7",
|
||||
"build/prefab/full/mac_arm64_gui/release/ballisticakit": "685e787af247ba2694c7632c469e4853",
|
||||
"build/prefab/full/mac_arm64_server/debug/dist/ballisticakit_headless": "6c41aad3f4c60d56350be66a6ba28a3f",
|
||||
"build/prefab/full/mac_arm64_server/release/dist/ballisticakit_headless": "d752bc8ee063e3d95038b3ce8bdb0e3c",
|
||||
"build/prefab/full/windows_x86_gui/debug/BallisticaKit.exe": "5f2e7c646ce28814d5d87ba2e4b826c4",
|
||||
"build/prefab/full/windows_x86_gui/release/BallisticaKit.exe": "b440e073afd70d018fc14ee10f41bb85",
|
||||
"build/prefab/full/windows_x86_server/debug/dist/BallisticaKitHeadless.exe": "bc56615fbc3d671deebf36942b36c706",
|
||||
"build/prefab/full/windows_x86_server/release/dist/BallisticaKitHeadless.exe": "e5d47e94497951d438520cde86d487b2",
|
||||
"build/prefab/lib/linux_arm64_gui/debug/libballisticaplus.a": "86eaadb3dee0a1f1137192fe943996f6",
|
||||
"build/prefab/lib/linux_arm64_gui/release/libballisticaplus.a": "8c795d1a871f0d82a198b5aeb506d770",
|
||||
"build/prefab/lib/linux_arm64_server/debug/libballisticaplus.a": "86eaadb3dee0a1f1137192fe943996f6",
|
||||
"build/prefab/lib/linux_arm64_server/release/libballisticaplus.a": "8c795d1a871f0d82a198b5aeb506d770",
|
||||
"build/prefab/lib/linux_x86_64_gui/debug/libballisticaplus.a": "ab0cf0e9d6001748927660d84c1da87f",
|
||||
"build/prefab/lib/linux_x86_64_gui/release/libballisticaplus.a": "62c22d7a25fd62831cb4a089bbee1b0b",
|
||||
"build/prefab/lib/linux_x86_64_server/debug/libballisticaplus.a": "ab0cf0e9d6001748927660d84c1da87f",
|
||||
"build/prefab/lib/linux_x86_64_server/release/libballisticaplus.a": "62c22d7a25fd62831cb4a089bbee1b0b",
|
||||
"build/prefab/lib/mac_arm64_gui/debug/libballisticaplus.a": "75a5ab56d54304e602fff8dae6435904",
|
||||
"build/prefab/lib/mac_arm64_gui/release/libballisticaplus.a": "19b6e806d066affc80b32c4899c6ba2f",
|
||||
"build/prefab/lib/mac_arm64_server/debug/libballisticaplus.a": "75a5ab56d54304e602fff8dae6435904",
|
||||
"build/prefab/lib/mac_arm64_server/release/libballisticaplus.a": "19b6e806d066affc80b32c4899c6ba2f",
|
||||
"build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.lib": "ca6096df15041b6af9a89ed81880c98b",
|
||||
"build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.pdb": "f06c730c9cee3599679df778ec565842",
|
||||
"build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.lib": "1a77179b3548e78791ddd1205ff505a3",
|
||||
"build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.pdb": "d755dc558bba71715ed0b543fa966be4",
|
||||
"build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.lib": "722aff67a010cc0a435bc70018b0b49a",
|
||||
"build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.pdb": "373deb2c02283978f415c1b937229292",
|
||||
"build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.lib": "fe26aafc763a1198e1921caeb261bdb8",
|
||||
"build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.pdb": "73f3c9211175f452e15c45296db89744",
|
||||
"build/prefab/full/linux_arm64_gui/debug/ballisticakit": "258017f68d714fa993399ba73fe35a47",
|
||||
"build/prefab/full/linux_arm64_gui/release/ballisticakit": "fb7661a6c77d99491e577594dd132262",
|
||||
"build/prefab/full/linux_arm64_server/debug/dist/ballisticakit_headless": "91fef82f37ca46dfaf4d3ebb1cd4c911",
|
||||
"build/prefab/full/linux_arm64_server/release/dist/ballisticakit_headless": "97361c0f527572ee1e81f372b21c6eea",
|
||||
"build/prefab/full/linux_x86_64_gui/debug/ballisticakit": "bb14d62cebe5f8889b36ee580db68f67",
|
||||
"build/prefab/full/linux_x86_64_gui/release/ballisticakit": "f4b9c6ef2d3252e46f2e9e5987092259",
|
||||
"build/prefab/full/linux_x86_64_server/debug/dist/ballisticakit_headless": "ba771845ad99c8274c0a2e390306c1c6",
|
||||
"build/prefab/full/linux_x86_64_server/release/dist/ballisticakit_headless": "62a0abe5d11268e7433b7880066558e3",
|
||||
"build/prefab/full/mac_arm64_gui/debug/ballisticakit": "b180ee641e805d1a6eed1bdf194a88ba",
|
||||
"build/prefab/full/mac_arm64_gui/release/ballisticakit": "ec633c984c8be568434bdcc39d3fbf75",
|
||||
"build/prefab/full/mac_arm64_server/debug/dist/ballisticakit_headless": "6ee62f6bec7345ad7863014cd6ab6f91",
|
||||
"build/prefab/full/mac_arm64_server/release/dist/ballisticakit_headless": "74f3a19a8b11f0a26d3b61c01c5cfb0f",
|
||||
"build/prefab/full/windows_x86_gui/debug/BallisticaKit.exe": "93e722f0f0286a20320372a0ed52fb3b",
|
||||
"build/prefab/full/windows_x86_gui/release/BallisticaKit.exe": "56f71c820702382f2c50b6693426a9a4",
|
||||
"build/prefab/full/windows_x86_server/debug/dist/BallisticaKitHeadless.exe": "83730297d803f6f7458b8c7ac95f6bbc",
|
||||
"build/prefab/full/windows_x86_server/release/dist/BallisticaKitHeadless.exe": "69c38b343c62dc4f687d9ac937aacc73",
|
||||
"build/prefab/lib/linux_arm64_gui/debug/libballisticaplus.a": "5ef674f19fa3e2bfc061d8ebc2e97624",
|
||||
"build/prefab/lib/linux_arm64_gui/release/libballisticaplus.a": "55cd9cb73acf7a0005a091ebf362cba2",
|
||||
"build/prefab/lib/linux_arm64_server/debug/libballisticaplus.a": "5ef674f19fa3e2bfc061d8ebc2e97624",
|
||||
"build/prefab/lib/linux_arm64_server/release/libballisticaplus.a": "55cd9cb73acf7a0005a091ebf362cba2",
|
||||
"build/prefab/lib/linux_x86_64_gui/debug/libballisticaplus.a": "e3bd2d824b6bcf4cf820d87ebf1f969a",
|
||||
"build/prefab/lib/linux_x86_64_gui/release/libballisticaplus.a": "5fe38e71bdbd7daf2554965233677d41",
|
||||
"build/prefab/lib/linux_x86_64_server/debug/libballisticaplus.a": "e3bd2d824b6bcf4cf820d87ebf1f969a",
|
||||
"build/prefab/lib/linux_x86_64_server/release/libballisticaplus.a": "5fe38e71bdbd7daf2554965233677d41",
|
||||
"build/prefab/lib/mac_arm64_gui/debug/libballisticaplus.a": "5f7c9ed9bdfcbf1806f613d8b1b479ef",
|
||||
"build/prefab/lib/mac_arm64_gui/release/libballisticaplus.a": "fc11b650398c55a0124943939017e9ec",
|
||||
"build/prefab/lib/mac_arm64_server/debug/libballisticaplus.a": "5f7c9ed9bdfcbf1806f613d8b1b479ef",
|
||||
"build/prefab/lib/mac_arm64_server/release/libballisticaplus.a": "fc11b650398c55a0124943939017e9ec",
|
||||
"build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.lib": "668de5ef2c6c47adea8bf3b13cf29dce",
|
||||
"build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.pdb": "27c9d03cf46b9771d6bbbebb18e7c6b2",
|
||||
"build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.lib": "6045009c5fd0f8b3e21429fa5594ed9a",
|
||||
"build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.pdb": "3430e09462e8e6954e276c70c9e692be",
|
||||
"build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.lib": "543e53a1521fbf4acd6c47a6d124c5ee",
|
||||
"build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.pdb": "8412cac6845eed87cd792a9b6cd7a540",
|
||||
"build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.lib": "4b93778e70a8149dbe91115cae916fe3",
|
||||
"build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.pdb": "6bfb80e2784c47f48b11079f240299b5",
|
||||
"src/assets/ba_data/python/babase/_mgen/__init__.py": "f885fed7f2ed98ff2ba271f9dbe3391c",
|
||||
"src/assets/ba_data/python/babase/_mgen/enums.py": "794d258d59fd17a61752843a9a0551ad",
|
||||
"src/ballistica/base/mgen/pyembed/binding_base.inc": "3a583e7e03bd4907b21adc3bf5729d15",
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
### 1.7.37 (build 22112, api 9, 2024-11-23)
|
||||
### 1.7.37 (build 22113, api 9, 2024-11-26)
|
||||
- Bumping api version to 9. As you'll see below, there's some UI changes that
|
||||
will require a bit of work for any UI mods to adapt to. If your mods don't
|
||||
touch UI stuff at all you can simply bump your api version and call it a day.
|
||||
@ -164,6 +164,8 @@
|
||||
app-modes at runtime; to change this you need to either change your
|
||||
projectconfig and rebuild or replace `ba*.app.mode_selector` at runtime with
|
||||
a custom selector that selects your custom app-mode(s).
|
||||
- The `ba*.app.threadpool_submit_no_wait()` method has been merged into the
|
||||
`threadpool` object, so it now is `ba*.app.threadpool.submit_no_wait()`.
|
||||
|
||||
### 1.7.36 (build 21944, api 8, 2024-07-26)
|
||||
- Wired up Tokens, BombSquad's new purchasable currency. The first thing these
|
||||
|
||||
5
Makefile
5
Makefile
@ -929,6 +929,11 @@ test-rpc:
|
||||
@$(PCOMMAND) pytest -o log_cli=true -o log_cli_level=debug -s -vv \
|
||||
tests/test_efro/test_rpc.py
|
||||
|
||||
# Shortcut to test efro.threadpool only.
|
||||
test-threadpool:
|
||||
@$(PCOMMAND) pytest -o log_cli=true -o log_cli_level=debug -s -vv \
|
||||
tests/test_efro/test_threadpool.py
|
||||
|
||||
# Tell make which of these targets don't represent files.
|
||||
.PHONY: test test-fast test-verbose test-full test-fast-full \
|
||||
test-message test-dataclassio test-rpc
|
||||
|
||||
@ -46,7 +46,8 @@
|
||||
"daemon",
|
||||
"jinja2",
|
||||
"jinja2.Environment",
|
||||
"tomlkit"
|
||||
"tomlkit",
|
||||
"urllib3.exceptions"
|
||||
],
|
||||
"python_paths": [
|
||||
"src/assets/ba_data/python",
|
||||
|
||||
@ -590,6 +590,7 @@
|
||||
"ba_data/python/efro/__pycache__/logging.cpython-312.opt-1.pyc",
|
||||
"ba_data/python/efro/__pycache__/rpc.cpython-312.opt-1.pyc",
|
||||
"ba_data/python/efro/__pycache__/terminal.cpython-312.opt-1.pyc",
|
||||
"ba_data/python/efro/__pycache__/threadpool.cpython-312.opt-1.pyc",
|
||||
"ba_data/python/efro/__pycache__/util.cpython-312.opt-1.pyc",
|
||||
"ba_data/python/efro/call.py",
|
||||
"ba_data/python/efro/cloudshell.py",
|
||||
@ -626,6 +627,7 @@
|
||||
"ba_data/python/efro/message/_sender.py",
|
||||
"ba_data/python/efro/rpc.py",
|
||||
"ba_data/python/efro/terminal.py",
|
||||
"ba_data/python/efro/threadpool.py",
|
||||
"ba_data/python/efro/util.py",
|
||||
"server_package/__pycache__/ballisticakit_server.cpython-312.opt-1.pyc",
|
||||
"server_package/ballisticakit_server.py"
|
||||
|
||||
@ -770,6 +770,7 @@ SCRIPT_TARGETS_PY_PUBLIC_TOOLS = \
|
||||
$(BUILD_DIR)/ba_data/python/efro/message/_sender.py \
|
||||
$(BUILD_DIR)/ba_data/python/efro/rpc.py \
|
||||
$(BUILD_DIR)/ba_data/python/efro/terminal.py \
|
||||
$(BUILD_DIR)/ba_data/python/efro/threadpool.py \
|
||||
$(BUILD_DIR)/ba_data/python/efro/util.py
|
||||
|
||||
SCRIPT_TARGETS_PYC_PUBLIC_TOOLS = \
|
||||
@ -809,6 +810,7 @@ SCRIPT_TARGETS_PYC_PUBLIC_TOOLS = \
|
||||
$(BUILD_DIR)/ba_data/python/efro/message/__pycache__/_sender.cpython-312.opt-1.pyc \
|
||||
$(BUILD_DIR)/ba_data/python/efro/__pycache__/rpc.cpython-312.opt-1.pyc \
|
||||
$(BUILD_DIR)/ba_data/python/efro/__pycache__/terminal.cpython-312.opt-1.pyc \
|
||||
$(BUILD_DIR)/ba_data/python/efro/__pycache__/threadpool.cpython-312.opt-1.pyc \
|
||||
$(BUILD_DIR)/ba_data/python/efro/__pycache__/util.cpython-312.opt-1.pyc
|
||||
|
||||
# Rule to copy src asset scripts to dst.
|
||||
|
||||
@ -101,6 +101,7 @@ from _babase import (
|
||||
set_analytics_screen,
|
||||
set_low_level_config_value,
|
||||
set_thread_name,
|
||||
set_ui_account_state,
|
||||
set_ui_input_device,
|
||||
set_ui_scale,
|
||||
show_progress_bar,
|
||||
@ -336,6 +337,7 @@ __all__ = [
|
||||
'set_analytics_screen',
|
||||
'set_low_level_config_value',
|
||||
'set_thread_name',
|
||||
'set_ui_account_state',
|
||||
'set_ui_input_device',
|
||||
'set_ui_scale',
|
||||
'show_progress_bar',
|
||||
|
||||
@ -9,9 +9,10 @@ import logging
|
||||
from enum import Enum
|
||||
from functools import partial
|
||||
from typing import TYPE_CHECKING, TypeVar, override
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from threading import RLock
|
||||
|
||||
from efro.threadpool import ThreadPoolExecutorPlus
|
||||
|
||||
import _babase
|
||||
from babase._language import LanguageSubsystem
|
||||
from babase._plugin import PluginSubsystem
|
||||
@ -178,7 +179,7 @@ class App:
|
||||
# processing. It should also be passed to any additional asyncio
|
||||
# loops we create so that everything shares the same single set
|
||||
# of worker threads.
|
||||
self.threadpool = ThreadPoolExecutor(
|
||||
self.threadpool = ThreadPoolExecutorPlus(
|
||||
thread_name_prefix='baworker',
|
||||
initializer=self._thread_pool_thread_init,
|
||||
)
|
||||
@ -482,18 +483,6 @@ class App:
|
||||
"""
|
||||
_babase.run_app()
|
||||
|
||||
def threadpool_submit_no_wait(self, call: Callable[[], Any]) -> None:
|
||||
"""Submit a call to the app threadpool where result is not needed.
|
||||
|
||||
Normally, doing work in a thread-pool involves creating a future
|
||||
and waiting for its result, which is an important step because it
|
||||
propagates any Exceptions raised by the submitted work. When the
|
||||
result in not important, however, this call can be used. The app
|
||||
will log any exceptions that occur.
|
||||
"""
|
||||
fut = self.threadpool.submit(call)
|
||||
fut.add_done_callback(self._threadpool_no_wait_done)
|
||||
|
||||
def set_intent(self, intent: AppIntent) -> None:
|
||||
"""Set the intent for the app.
|
||||
|
||||
@ -511,7 +500,7 @@ class App:
|
||||
|
||||
# Do the actual work of calcing our app-mode/etc. in a bg thread
|
||||
# since it may block for a moment to load modules/etc.
|
||||
self.threadpool_submit_no_wait(partial(self._set_intent, intent))
|
||||
self.threadpool.submit_no_wait(self._set_intent, intent)
|
||||
|
||||
def push_apply_app_config(self) -> None:
|
||||
"""Internal. Use app.config.apply() to apply app config changes."""
|
||||
@ -1108,14 +1097,6 @@ class App:
|
||||
await asyncio.sleep(0.01)
|
||||
lifecyclelog.info('fade-and-shutdown-audio end')
|
||||
|
||||
def _threadpool_no_wait_done(self, fut: Future) -> None:
|
||||
try:
|
||||
fut.result()
|
||||
except Exception:
|
||||
logging.exception(
|
||||
'Error in work submitted via threadpool_submit_no_wait()'
|
||||
)
|
||||
|
||||
def _thread_pool_thread_init(self) -> None:
|
||||
# Help keep things clear in profiling tools/etc.
|
||||
self._pool_thread_count += 1
|
||||
|
||||
@ -9,15 +9,7 @@ from functools import partial
|
||||
from typing import TYPE_CHECKING, override
|
||||
|
||||
from bacommon.app import AppExperience
|
||||
from babase import (
|
||||
app,
|
||||
AppMode,
|
||||
AppIntentExec,
|
||||
AppIntentDefault,
|
||||
invoke_main_menu,
|
||||
in_logic_thread,
|
||||
screenmessage,
|
||||
)
|
||||
import babase
|
||||
|
||||
import _baclassic
|
||||
|
||||
@ -25,21 +17,20 @@ if TYPE_CHECKING:
|
||||
from typing import Callable, Any
|
||||
|
||||
from efro.call import CallbackRegistration
|
||||
from bacommon.cloud import ClassicAccountData
|
||||
from babase import AppIntent, AccountV2Handle, CloudSubscription
|
||||
from bauiv1 import UIV1AppSubsystem, MainWindow, MainWindowState
|
||||
import bacommon.cloud
|
||||
import bauiv1
|
||||
|
||||
|
||||
# ba_meta export babase.AppMode
|
||||
class ClassicAppMode(AppMode):
|
||||
class ClassicAppMode(babase.AppMode):
|
||||
"""AppMode for the classic BombSquad experience."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._on_primary_account_changed_callback: (
|
||||
CallbackRegistration | None
|
||||
) = None
|
||||
self._test_sub: CloudSubscription | None = None
|
||||
self._account_data_sub: CloudSubscription | None = None
|
||||
self._test_sub: babase.CloudSubscription | None = None
|
||||
self._account_data_sub: babase.CloudSubscription | None = None
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
@ -48,16 +39,18 @@ class ClassicAppMode(AppMode):
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
def _supports_intent(cls, intent: AppIntent) -> bool:
|
||||
def _supports_intent(cls, intent: babase.AppIntent) -> bool:
|
||||
# We support default and exec intents currently.
|
||||
return isinstance(intent, AppIntentExec | AppIntentDefault)
|
||||
return isinstance(
|
||||
intent, babase.AppIntentExec | babase.AppIntentDefault
|
||||
)
|
||||
|
||||
@override
|
||||
def handle_intent(self, intent: AppIntent) -> None:
|
||||
if isinstance(intent, AppIntentExec):
|
||||
def handle_intent(self, intent: babase.AppIntent) -> None:
|
||||
if isinstance(intent, babase.AppIntentExec):
|
||||
_baclassic.classic_app_mode_handle_app_intent_exec(intent.code)
|
||||
return
|
||||
assert isinstance(intent, AppIntentDefault)
|
||||
assert isinstance(intent, babase.AppIntentDefault)
|
||||
_baclassic.classic_app_mode_handle_app_intent_default()
|
||||
|
||||
@override
|
||||
@ -66,7 +59,9 @@ class ClassicAppMode(AppMode):
|
||||
# Let the native layer do its thing.
|
||||
_baclassic.classic_app_mode_activate()
|
||||
|
||||
assert app.plus is not None
|
||||
app = babase.app
|
||||
plus = app.plus
|
||||
assert plus is not None
|
||||
|
||||
# Wire up the root ui to do what we want.
|
||||
ui = app.ui_v1
|
||||
@ -124,16 +119,18 @@ class ClassicAppMode(AppMode):
|
||||
|
||||
# We want to be informed when primary account changes.
|
||||
self._on_primary_account_changed_callback = (
|
||||
app.plus.accounts.on_primary_account_changed_callbacks.register(
|
||||
plus.accounts.on_primary_account_changed_callbacks.register(
|
||||
self.update_for_primary_account
|
||||
)
|
||||
)
|
||||
# Establish subscriptions/etc. for any current primary account.
|
||||
self.update_for_primary_account(app.plus.accounts.primary)
|
||||
self.update_for_primary_account(plus.accounts.primary)
|
||||
|
||||
@override
|
||||
def on_deactivate(self) -> None:
|
||||
|
||||
classic = babase.app.classic
|
||||
|
||||
# Stop being informed of account changes.
|
||||
self._on_primary_account_changed_callback = None
|
||||
|
||||
@ -141,8 +138,8 @@ class ClassicAppMode(AppMode):
|
||||
self.update_for_primary_account(None)
|
||||
|
||||
# Save where we were in the UI so we return there next time.
|
||||
if app.classic is not None:
|
||||
app.classic.save_ui_state()
|
||||
if classic is not None:
|
||||
classic.save_ui_state()
|
||||
|
||||
# Let the native layer do its thing.
|
||||
_baclassic.classic_app_mode_deactivate()
|
||||
@ -151,15 +148,22 @@ class ClassicAppMode(AppMode):
|
||||
def on_app_active_changed(self) -> None:
|
||||
# If we've gone inactive, bring up the main menu, which has the
|
||||
# side effect of pausing the action (when possible).
|
||||
if not app.active:
|
||||
invoke_main_menu()
|
||||
if not babase.app.active:
|
||||
babase.invoke_main_menu()
|
||||
|
||||
def update_for_primary_account(
|
||||
self, account: AccountV2Handle | None
|
||||
self, account: babase.AccountV2Handle | None
|
||||
) -> None:
|
||||
"""Update subscriptions/etc. for a new primary account state."""
|
||||
assert in_logic_thread()
|
||||
assert app.plus is not None
|
||||
assert babase.in_logic_thread()
|
||||
plus = babase.app.plus
|
||||
|
||||
assert plus is not None
|
||||
|
||||
if account is not None:
|
||||
babase.set_ui_account_state(True, account.tag)
|
||||
else:
|
||||
babase.set_ui_account_state(False)
|
||||
|
||||
# For testing subscription functionality.
|
||||
if os.environ.get('BA_SUBSCRIPTION_TEST') == '1':
|
||||
@ -167,18 +171,18 @@ class ClassicAppMode(AppMode):
|
||||
self._test_sub = None
|
||||
else:
|
||||
with account:
|
||||
self._test_sub = app.plus.cloud.subscribe_test(
|
||||
self._test_sub = plus.cloud.subscribe_test(
|
||||
self._on_sub_test_update
|
||||
)
|
||||
else:
|
||||
self._test_sub = None
|
||||
|
||||
if account is None or bool(True):
|
||||
if account is None:
|
||||
self._account_data_sub = None
|
||||
else:
|
||||
with account:
|
||||
self._account_data_sub = (
|
||||
app.plus.cloud.subscribe_classic_account_data(
|
||||
plus.cloud.subscribe_classic_account_data(
|
||||
self._on_classic_account_data_change
|
||||
)
|
||||
)
|
||||
@ -186,19 +190,22 @@ class ClassicAppMode(AppMode):
|
||||
def _on_sub_test_update(self, val: int | None) -> None:
|
||||
print(f'GOT SUB TEST UPDATE: {val}')
|
||||
|
||||
def _on_classic_account_data_change(self, val: ClassicAccountData) -> None:
|
||||
print(f'GOT CLASSIC ACCOUNT DATA: {val}')
|
||||
def _on_classic_account_data_change(
|
||||
self, val: bacommon.cloud.ClassicAccountLiveData
|
||||
) -> None:
|
||||
pass
|
||||
# print(f'GOT CLASSIC ACCOUNT DATA: {val}')
|
||||
|
||||
def _root_ui_menu_press(self) -> None:
|
||||
from babase import push_back_press
|
||||
|
||||
ui = app.ui_v1
|
||||
ui = babase.app.ui_v1
|
||||
|
||||
# If *any* main-window is up, kill it and resume play.
|
||||
old_window = ui.get_main_window()
|
||||
if old_window is not None:
|
||||
|
||||
classic = app.classic
|
||||
classic = babase.app.classic
|
||||
assert classic is not None
|
||||
classic.resume()
|
||||
|
||||
@ -241,8 +248,8 @@ class ClassicAppMode(AppMode):
|
||||
|
||||
def _auxiliary_window_nav(
|
||||
self,
|
||||
win_type: type[MainWindow],
|
||||
win_create_call: Callable[[], MainWindow],
|
||||
win_type: type[bauiv1.MainWindow],
|
||||
win_create_call: Callable[[], bauiv1.MainWindow],
|
||||
) -> None:
|
||||
"""Navigate to or away from an Auxiliary window.
|
||||
|
||||
@ -253,14 +260,14 @@ class ClassicAppMode(AppMode):
|
||||
"""
|
||||
# pylint: disable=unidiomatic-typecheck
|
||||
|
||||
ui = app.ui_v1
|
||||
ui = babase.app.ui_v1
|
||||
|
||||
current_main_window = ui.get_main_window()
|
||||
|
||||
# Scan our ancestors for auxiliary states matching our type as
|
||||
# well as auxiliary states in general.
|
||||
aux_matching_state: MainWindowState | None = None
|
||||
aux_state: MainWindowState | None = None
|
||||
aux_matching_state: bauiv1.MainWindowState | None = None
|
||||
aux_state: bauiv1.MainWindowState | None = None
|
||||
|
||||
if current_main_window is None:
|
||||
raise RuntimeError(
|
||||
@ -433,4 +440,4 @@ class ClassicAppMode(AppMode):
|
||||
|
||||
def _root_ui_chest_slot_pressed(self, index: int) -> None:
|
||||
print(f'CHEST {index} PRESSED')
|
||||
screenmessage('UNDER CONSTRUCTION.')
|
||||
babase.screenmessage('UNDER CONSTRUCTION.')
|
||||
|
||||
@ -53,7 +53,7 @@ if TYPE_CHECKING:
|
||||
|
||||
# Build number and version of the ballistica binary we expect to be
|
||||
# using.
|
||||
TARGET_BALLISTICA_BUILD = 22112
|
||||
TARGET_BALLISTICA_BUILD = 22113
|
||||
TARGET_BALLISTICA_VERSION = '1.7.37'
|
||||
|
||||
|
||||
|
||||
@ -183,7 +183,8 @@ class CloudSubsystem(babase.AppSubsystem):
|
||||
)
|
||||
|
||||
def subscribe_classic_account_data(
|
||||
self, updatecall: Callable[[bacommon.cloud.ClassicAccountData], None]
|
||||
self,
|
||||
updatecall: Callable[[bacommon.cloud.ClassicAccountLiveData], None],
|
||||
) -> babase.CloudSubscription:
|
||||
"""Subscribe to classic account data."""
|
||||
raise NotImplementedError(
|
||||
|
||||
@ -35,7 +35,7 @@ class CoopBrowserWindow(bui.MainWindow):
|
||||
|
||||
# Preload some modules we use in a background thread so we won't
|
||||
# have a visual hitch when the user taps them.
|
||||
bui.app.threadpool_submit_no_wait(self._preload_modules)
|
||||
bui.app.threadpool.submit_no_wait(self._preload_modules)
|
||||
|
||||
bui.set_analytics_screen('Coop Window')
|
||||
|
||||
|
||||
@ -25,7 +25,7 @@ class MainMenuWindow(bui.MainWindow):
|
||||
|
||||
# Preload some modules we use in a background thread so we won't
|
||||
# have a visual hitch when the user taps them.
|
||||
bui.app.threadpool_submit_no_wait(self._preload_modules)
|
||||
bui.app.threadpool.submit_no_wait(self._preload_modules)
|
||||
|
||||
bui.set_analytics_screen('Main Menu')
|
||||
self._show_remote_app_info_on_first_launch()
|
||||
|
||||
@ -34,7 +34,7 @@ class PlayWindow(bui.MainWindow):
|
||||
|
||||
# Preload some modules we use in a background thread so we won't
|
||||
# have a visual hitch when the user taps them.
|
||||
bui.app.threadpool_submit_no_wait(self._preload_modules)
|
||||
bui.app.threadpool.submit_no_wait(self._preload_modules)
|
||||
|
||||
classic = bui.app.classic
|
||||
assert classic is not None
|
||||
|
||||
@ -32,7 +32,7 @@ class AdvancedSettingsWindow(bui.MainWindow):
|
||||
|
||||
# Preload some modules we use in a background thread so we won't
|
||||
# have a visual hitch when the user taps them.
|
||||
bui.app.threadpool_submit_no_wait(self._preload_modules)
|
||||
bui.app.threadpool.submit_no_wait(self._preload_modules)
|
||||
|
||||
app = bui.app
|
||||
assert app.classic is not None
|
||||
|
||||
@ -26,7 +26,7 @@ class AllSettingsWindow(bui.MainWindow):
|
||||
|
||||
# Preload some modules we use in a background thread so we won't
|
||||
# have a visual hitch when the user taps them.
|
||||
bui.app.threadpool_submit_no_wait(self._preload_modules)
|
||||
bui.app.threadpool.submit_no_wait(self._preload_modules)
|
||||
|
||||
bui.set_analytics_screen('Settings Window')
|
||||
assert bui.app.classic is not None
|
||||
|
||||
@ -928,6 +928,43 @@ static PyMethodDef PyShowProgressBarDef = {
|
||||
"Category: **General Utility Functions**",
|
||||
};
|
||||
|
||||
// ------------------------- set_ui_account_state ------------------------------
|
||||
|
||||
static auto PySetUIAccountState(PyObject* self, PyObject* args,
|
||||
PyObject* keywds) -> PyObject* {
|
||||
BA_PYTHON_TRY;
|
||||
|
||||
BA_PRECONDITION(g_base->InLogicThread());
|
||||
|
||||
int signed_in{};
|
||||
PyObject* name_obj{Py_None};
|
||||
static const char* kwlist[] = {"signed_in", "name", nullptr};
|
||||
if (!PyArg_ParseTupleAndKeywords(args, keywds, "p|O",
|
||||
const_cast<char**>(kwlist), &signed_in,
|
||||
&name_obj)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (signed_in) {
|
||||
auto name = Python::GetPyString(name_obj);
|
||||
g_base->ui->SetAccountState(true, name);
|
||||
} else {
|
||||
g_base->ui->SetAccountState(false, "");
|
||||
}
|
||||
|
||||
Py_RETURN_NONE;
|
||||
BA_PYTHON_CATCH;
|
||||
}
|
||||
|
||||
static PyMethodDef PySetUIAccountStateDef = {
|
||||
"set_ui_account_state", // name
|
||||
(PyCFunction)PySetUIAccountState, // method
|
||||
METH_VARARGS | METH_KEYWORDS, // flags
|
||||
|
||||
"set_ui_account_state(signed_in: bool, name: str | None = None) -> None\n"
|
||||
"\n"
|
||||
"(internal)\n",
|
||||
};
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
auto PythonMethodsBase2::GetMethods() -> std::vector<PyMethodDef> {
|
||||
@ -961,6 +998,7 @@ auto PythonMethodsBase2::GetMethods() -> std::vector<PyMethodDef> {
|
||||
PyFullscreenControlKeyShortcutDef,
|
||||
PyFullscreenControlGetDef,
|
||||
PyFullscreenControlSetDef,
|
||||
PySetUIAccountStateDef,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -223,8 +223,37 @@ void UI::ActivatePartyIcon() {
|
||||
}
|
||||
|
||||
void UI::SetSquadSizeLabel(int val) {
|
||||
assert(g_base->InLogicThread());
|
||||
|
||||
// No-op if this exactly matches what we already have.
|
||||
if (val == squad_size_label_) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Store the val so we'll have it for future delegates.
|
||||
squad_size_label_ = val;
|
||||
|
||||
// Pass it to any current delegate.
|
||||
if (auto* ui_delegate = g_base->ui->delegate()) {
|
||||
ui_delegate->SetSquadSizeLabel(val);
|
||||
ui_delegate->SetSquadSizeLabel(squad_size_label_);
|
||||
}
|
||||
}
|
||||
|
||||
void UI::SetAccountState(bool signed_in, const std::string& name) {
|
||||
assert(g_base->InLogicThread());
|
||||
|
||||
// No-op if this exactly matches what we already have.
|
||||
if (account_state_signed_in_ == signed_in && account_state_name_ == name) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Store the val so we'll have it for future delegates.
|
||||
account_state_signed_in_ = signed_in;
|
||||
account_state_name_ = name;
|
||||
|
||||
// Pass it to any current delegate.
|
||||
if (auto* ui_delegate = g_base->ui->delegate()) {
|
||||
ui_delegate->SetAccountState(account_state_signed_in_, account_state_name_);
|
||||
}
|
||||
}
|
||||
|
||||
@ -306,10 +335,6 @@ auto UI::UIHasDirectKeyboardInput() const -> bool {
|
||||
void UI::HandleMouseMotion(float x, float y) {
|
||||
SendWidgetMessage(
|
||||
WidgetMessage(WidgetMessage::Type::kMouseMove, nullptr, x, y));
|
||||
|
||||
// if (auto* ui_delegate = g_base->ui->delegate()) {
|
||||
// ui_delegate->HandleLegacyRootUIMouseMotion(x, y);
|
||||
// }
|
||||
}
|
||||
|
||||
void UI::PushBackButtonCall(InputDevice* input_device) {
|
||||
@ -612,12 +637,13 @@ void UI::SetUIDelegate(base::UIDelegateInterface* delegate) {
|
||||
if (delegate_) {
|
||||
delegate_->OnActivate();
|
||||
|
||||
// Inform them that a few things changed, since they might have since
|
||||
// the last time they were active (these callbacks only go to the
|
||||
// *active* ui delegate).
|
||||
// Push values to them and trigger various 'changed' callbacks so they
|
||||
// pick up the latest state of the world.
|
||||
delegate_->DoApplyAppConfig();
|
||||
delegate_->OnScreenSizeChange();
|
||||
delegate_->OnLanguageChange();
|
||||
delegate_->SetSquadSizeLabel(squad_size_label_);
|
||||
delegate_->SetAccountState(account_state_signed_in_, account_state_name_);
|
||||
}
|
||||
} catch (const Exception& exc) {
|
||||
// Switching UI delegates is a big deal; don't try to continue if
|
||||
|
||||
@ -63,8 +63,14 @@ class UI {
|
||||
auto PartyWindowOpen() -> bool;
|
||||
void ActivatePartyIcon();
|
||||
|
||||
/// Set persistent squad size label; will be provided to current and
|
||||
/// future delegates.
|
||||
void SetSquadSizeLabel(int val);
|
||||
|
||||
/// Set persistent account state info; will be provided to current and
|
||||
/// future delegates.
|
||||
void SetAccountState(bool signed_in, const std::string& name);
|
||||
|
||||
auto HandleMouseDown(int button, float x, float y, bool double_click) -> bool;
|
||||
void HandleMouseUp(int button, float x, float y);
|
||||
void HandleMouseMotion(float x, float y);
|
||||
@ -154,6 +160,7 @@ class UI {
|
||||
|
||||
Object::Ref<TextGroup> dev_console_button_txt_;
|
||||
Object::WeakRef<InputDevice> ui_input_device_;
|
||||
std::string account_state_name_;
|
||||
OperationContext* operation_context_{};
|
||||
base::UIDelegateInterface* delegate_{};
|
||||
DevConsole* dev_console_{};
|
||||
@ -162,6 +169,8 @@ class UI {
|
||||
millisecs_t last_input_device_use_time_{};
|
||||
millisecs_t last_widget_input_reject_err_sound_time_{};
|
||||
UIScale scale_{UIScale::kLarge};
|
||||
int squad_size_label_{};
|
||||
bool account_state_signed_in_{};
|
||||
bool force_scale_{};
|
||||
bool show_dev_console_button_{};
|
||||
bool dev_console_button_pressed_{};
|
||||
|
||||
@ -43,6 +43,7 @@ class UIDelegateInterface {
|
||||
virtual auto GetRootWidget() -> ui_v1::Widget* = 0;
|
||||
virtual auto SendWidgetMessage(const WidgetMessage& m) -> int = 0;
|
||||
virtual void SetSquadSizeLabel(int num) = 0;
|
||||
virtual void SetAccountState(bool signed_in, const std::string& name) = 0;
|
||||
|
||||
/// Should return true if this app mode can confirm quitting the app.
|
||||
virtual auto HasQuitConfirmDialog() -> bool = 0;
|
||||
|
||||
@ -39,7 +39,7 @@ auto main(int argc, char** argv) -> int {
|
||||
namespace ballistica {
|
||||
|
||||
// These are set automatically via script; don't modify them here.
|
||||
const int kEngineBuildNumber = 22112;
|
||||
const int kEngineBuildNumber = 22113;
|
||||
const char* kEngineVersion = "1.7.37";
|
||||
const int kEngineApiVersion = 9;
|
||||
|
||||
|
||||
@ -81,21 +81,24 @@ bool UIV1FeatureSet::PartyIconVisible() {
|
||||
// Currently this is always visible.
|
||||
return true;
|
||||
}
|
||||
|
||||
void UIV1FeatureSet::SetAccountState(bool signed_in, const std::string& name) {
|
||||
assert(root_widget_.Exists());
|
||||
// Store the value and plug it in if we've got a live widget.
|
||||
account_signed_in_ = signed_in;
|
||||
account_name_ = name;
|
||||
if (auto* r = root_widget()) {
|
||||
root_widget_->SetAccountState(signed_in, name);
|
||||
}
|
||||
// account_signed_in_ = signed_in;
|
||||
// account_name_ = name;
|
||||
// if (auto* r = root_widget()) {
|
||||
root_widget_->SetAccountState(signed_in, name);
|
||||
// }
|
||||
}
|
||||
|
||||
void UIV1FeatureSet::SetSquadSizeLabel(int num) {
|
||||
assert(root_widget_.Exists());
|
||||
// Store the value and plug it in if we've got a live widget.
|
||||
party_icon_number_ = num;
|
||||
if (auto* r = root_widget()) {
|
||||
root_widget_->SetSquadSizeLabel(num);
|
||||
}
|
||||
// party_icon_number_ = num;
|
||||
// if (auto* r = root_widget()) {
|
||||
root_widget_->SetSquadSizeLabel(num);
|
||||
// }
|
||||
}
|
||||
|
||||
void UIV1FeatureSet::ActivatePartyIcon() {
|
||||
@ -185,9 +188,9 @@ void UIV1FeatureSet::OnActivate() {
|
||||
rw->Setup();
|
||||
rw->SetOverlayWidget(ow.Get());
|
||||
|
||||
// Plug in current values for everything.
|
||||
rw->SetSquadSizeLabel(party_icon_number_);
|
||||
rw->SetAccountState(account_signed_in_, account_name_);
|
||||
// Plug in all values we're storing.
|
||||
// rw->SetSquadSizeLabel(party_icon_number_);
|
||||
// rw->SetAccountState(account_signed_in_, account_name_);
|
||||
|
||||
sw->GlobalSelect();
|
||||
}
|
||||
@ -199,11 +202,6 @@ void UIV1FeatureSet::OnDeactivate() {
|
||||
screen_root_widget_.Clear();
|
||||
}
|
||||
|
||||
// void UIV1FeatureSet::Reset() {
|
||||
// printf("UIV1::Reset()\n");
|
||||
|
||||
// }
|
||||
|
||||
void UIV1FeatureSet::AddWidget(Widget* w, ContainerWidget* parent) {
|
||||
assert(g_base->InLogicThread());
|
||||
|
||||
|
||||
@ -91,7 +91,7 @@ class UIV1FeatureSet : public FeatureSetNativeComponent,
|
||||
void Draw(base::FrameDef* frame_def) override;
|
||||
|
||||
void SetSquadSizeLabel(int num) override;
|
||||
void SetAccountState(bool signed_in, const std::string& name);
|
||||
void SetAccountState(bool signed_in, const std::string& name) override;
|
||||
|
||||
UIV1Python* const python;
|
||||
|
||||
@ -143,11 +143,11 @@ class UIV1FeatureSet : public FeatureSetNativeComponent,
|
||||
Object::Ref<RootWidget> root_widget_;
|
||||
int ui_lock_count_{};
|
||||
int language_state_{};
|
||||
int party_icon_number_{};
|
||||
// int party_icon_number_{};
|
||||
bool always_use_internal_on_screen_keyboard_{};
|
||||
bool party_window_open_{};
|
||||
bool account_signed_in_{};
|
||||
std::string account_name_{};
|
||||
// bool account_signed_in_{};
|
||||
// std::string account_name_{};
|
||||
};
|
||||
|
||||
} // namespace ballistica::ui_v1
|
||||
|
||||
@ -1261,14 +1261,22 @@ auto RootWidget::GetSpecialWidget(const std::string& s) const -> Widget* {
|
||||
void RootWidget::SetAccountState(bool signed_in, const std::string& name) {
|
||||
if (account_name_text_) {
|
||||
auto* w{account_name_text_->widget.Get()};
|
||||
auto* wb{account_button_->widget.Get()};
|
||||
assert(w);
|
||||
assert(wb);
|
||||
|
||||
if (signed_in) {
|
||||
w->SetText(name);
|
||||
w->set_color(0.0f, 1.0f, 0.0f, 1.0f);
|
||||
w->set_color(0.0f, 0.4f, 0.1f, 1.0f);
|
||||
w->set_shadow(0.2f);
|
||||
w->set_flatness(1.0f);
|
||||
wb->SetColor(0.8f, 1.2f, 0.8f);
|
||||
} else {
|
||||
w->SetText("NOT SIGNED IN");
|
||||
w->SetText("{\"r\":\"notSignedInText\"}");
|
||||
w->set_color(1.0f, 0.2f, 0.2f, 1.0f);
|
||||
w->set_shadow(0.5f);
|
||||
w->set_flatness(1.0f);
|
||||
wb->SetColor(0.45f, 0.4f, 0.4f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
64
tests/test_efro/test_threadpool.py
Normal file
64
tests/test_efro/test_threadpool.py
Normal file
@ -0,0 +1,64 @@
|
||||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Testing rpc functionality."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import time
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import pytest
|
||||
|
||||
from efro.threadpool import ThreadPoolExecutorPlus
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Awaitable
|
||||
|
||||
FAST_MODE = os.environ.get('BA_TEST_FAST_MODE') == '1'
|
||||
|
||||
|
||||
@pytest.mark.skipif(FAST_MODE, reason='fast mode')
|
||||
def test_no_wait_back_pressure(caplog: pytest.LogCaptureFixture) -> None:
|
||||
"""Make sure we start blocking if too many no-wait calls are submitted."""
|
||||
|
||||
tasktime = 0.1
|
||||
|
||||
def _do_test(max_no_wait_count: int) -> None:
|
||||
threadpool = ThreadPoolExecutorPlus(
|
||||
max_workers=10, max_no_wait_count=max_no_wait_count
|
||||
)
|
||||
|
||||
def _long_call() -> None:
|
||||
time.sleep(tasktime)
|
||||
print('HELLO FROM FINISHED CALL')
|
||||
|
||||
for _i in range(10):
|
||||
threadpool.submit_no_wait(_long_call)
|
||||
|
||||
threadpool.shutdown(wait=True)
|
||||
|
||||
assert threadpool.no_wait_count == 0
|
||||
|
||||
# If we limit our no-wait-tasks it should take roughtly 2 * tasktime
|
||||
# to get everything through and we should see a warning about
|
||||
# hitting the limit.
|
||||
print('\nTesting WITH no-wait-tasks bottleneck...')
|
||||
starttime = time.monotonic()
|
||||
with caplog.at_level(logging.WARNING):
|
||||
_do_test(max_no_wait_count=6)
|
||||
duration = time.monotonic() - starttime
|
||||
print(f'TOOK {duration}')
|
||||
assert duration > 2.0 * tasktime
|
||||
assert 'hit max no-wait limit' in caplog.text
|
||||
|
||||
# If no-wait-tasks is not the bottleneck, it should take just about
|
||||
# tasktime exactly and there should be no warnings.
|
||||
print('\nTesting WITHOUT no-wait-tasks bottleneck...')
|
||||
starttime = time.monotonic()
|
||||
with caplog.at_level(logging.WARNING):
|
||||
_do_test(max_no_wait_count=20)
|
||||
duration = time.monotonic() - starttime
|
||||
print(f'TOOK {duration}')
|
||||
assert duration < 1.2 * tasktime
|
||||
@ -327,8 +327,31 @@ class BSPrivatePartyResponse(Response):
|
||||
|
||||
@ioprepped
|
||||
@dataclass
|
||||
class ClassicAccountData:
|
||||
"""Account related data for classic app mode."""
|
||||
class ClassicAccountLiveData:
|
||||
"""Account related data kept up to date live for classic app mode."""
|
||||
|
||||
tokens: Annotated[int, IOAttrs('t')]
|
||||
class LeagueType(Enum):
|
||||
"""Type of league we are in."""
|
||||
|
||||
BRONZE = 'b'
|
||||
SILVER = 's'
|
||||
GOLD = 'g'
|
||||
DIAMOND = 'd'
|
||||
|
||||
tickets: Annotated[int, IOAttrs('ti')]
|
||||
|
||||
tokens: Annotated[int, IOAttrs('to')]
|
||||
gold_pass: Annotated[bool, IOAttrs('g')]
|
||||
|
||||
achievements: Annotated[int, IOAttrs('a')]
|
||||
achievements_total: Annotated[int, IOAttrs('at')]
|
||||
|
||||
league_type: Annotated[LeagueType | None, IOAttrs('lt')]
|
||||
league_num: Annotated[int | None, IOAttrs('ln')]
|
||||
league_rank: Annotated[int | None, IOAttrs('lr')]
|
||||
|
||||
level: Annotated[int, IOAttrs('lv')]
|
||||
xp: Annotated[int, IOAttrs('xp')]
|
||||
xpmax: Annotated[int, IOAttrs('xpm')]
|
||||
|
||||
inbox_count: Annotated[int, IOAttrs('ibc')]
|
||||
|
||||
@ -4,14 +4,14 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import threading
|
||||
import weakref
|
||||
import threading
|
||||
from typing import TYPE_CHECKING, TypeVar, Generic
|
||||
|
||||
T = TypeVar('T')
|
||||
|
||||
if TYPE_CHECKING:
|
||||
pass
|
||||
from typing import Callable
|
||||
|
||||
|
||||
class CallbackSet(Generic[T]):
|
||||
|
||||
@ -9,6 +9,7 @@ import errno
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any
|
||||
|
||||
import urllib3.response
|
||||
from efro.terminal import ClrBase
|
||||
|
||||
|
||||
@ -105,6 +106,71 @@ class AuthenticationError(Exception):
|
||||
"""
|
||||
|
||||
|
||||
class _Urllib3HttpError(Exception):
|
||||
"""Exception raised for non-200 html codes."""
|
||||
|
||||
def __init__(self, code: int) -> None:
|
||||
self.code = code
|
||||
|
||||
# So we can see code in tracebacks.
|
||||
@override
|
||||
def __str__(self) -> str:
|
||||
from http import HTTPStatus
|
||||
|
||||
try:
|
||||
desc = HTTPStatus(self.code).description
|
||||
except ValueError:
|
||||
desc = 'Unknown HTTP Status Code'
|
||||
return f'{self.code}: {desc}'
|
||||
|
||||
|
||||
def raise_for_urllib3_status(
|
||||
response: urllib3.response.BaseHTTPResponse,
|
||||
) -> None:
|
||||
"""Raise an exception for html error codes aside from 200."""
|
||||
if response.status != 200:
|
||||
raise _Urllib3HttpError(code=response.status)
|
||||
|
||||
|
||||
def is_urllib3_communication_error(exc: BaseException, url: str | None) -> bool:
|
||||
"""Is the provided exception from urllib3 a communication-related error?
|
||||
|
||||
Url, if provided, can provide extra context for when to treat an error
|
||||
as such an error.
|
||||
|
||||
This should be passed an exception which resulted from making
|
||||
requests with urllib3. It returns True for any errors that could
|
||||
conceivably arise due to unavailable/poor network connections,
|
||||
firewall/connectivity issues, or other issues out of our control.
|
||||
These errors can often be safely ignored or presented to the user as
|
||||
general 'network-unavailable' states.
|
||||
"""
|
||||
# Need to start building these up. For now treat everything as a
|
||||
# real error.
|
||||
import urllib3.exceptions
|
||||
|
||||
if isinstance(exc, _Urllib3HttpError):
|
||||
# Special sub-case: appspot.com hosting seems to give 403 errors
|
||||
# (forbidden) to some countries. I'm assuming for legal reasons?..
|
||||
# Let's consider that a communication error since its out of our
|
||||
# control so we don't fill up logs with it.
|
||||
if exc.code == 403 and url is not None and '.appspot.com' in url:
|
||||
return True
|
||||
|
||||
elif isinstance(exc, urllib3.exceptions.ReadTimeoutError):
|
||||
return True
|
||||
|
||||
elif isinstance(exc, urllib3.exceptions.ProtocolError):
|
||||
# Most protocol errors quality as CommunicationErrors, but some
|
||||
# may be due to server misconfigurations or whatnot so let's
|
||||
# take it on a case by case basis.
|
||||
excstr = str(exc)
|
||||
if 'Connection aborted.' in excstr:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def is_urllib_communication_error(exc: BaseException, url: str | None) -> bool:
|
||||
"""Is the provided exception from urllib a communication-related error?
|
||||
|
||||
|
||||
87
tools/efro/threadpool.py
Normal file
87
tools/efro/threadpool.py
Normal file
@ -0,0 +1,87 @@
|
||||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Thread pool functionality."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
import logging
|
||||
import threading
|
||||
from typing import TYPE_CHECKING, ParamSpec
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any, Callable
|
||||
from concurrent.futures import Future
|
||||
|
||||
P = ParamSpec('P')
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ThreadPoolExecutorPlus(ThreadPoolExecutor):
|
||||
"""A ThreadPoolExecutor with additional functionality added."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
max_workers: int | None = None,
|
||||
thread_name_prefix: str = '',
|
||||
initializer: Callable[[], None] | None = None,
|
||||
max_no_wait_count: int | None = None,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
max_workers=max_workers,
|
||||
thread_name_prefix=thread_name_prefix,
|
||||
initializer=initializer,
|
||||
)
|
||||
self.no_wait_count = 0
|
||||
|
||||
self._max_no_wait_count = (
|
||||
max_no_wait_count
|
||||
if max_no_wait_count is not None
|
||||
else 50 if max_workers is None else max_workers * 4
|
||||
)
|
||||
self._last_no_wait_warn_time: float | None = None
|
||||
self._no_wait_count_lock = threading.Lock()
|
||||
|
||||
def submit_no_wait(
|
||||
self, call: Callable[P, Any], *args: P.args, **keywds: P.kwargs
|
||||
) -> None:
|
||||
"""Submit work to the threadpool with no expectation of waiting.
|
||||
|
||||
Any errors occurring in the passed callable will be logged. This
|
||||
call will block and log a warning if the threadpool reaches its
|
||||
max queued no-wait call count.
|
||||
"""
|
||||
# If we're too backlogged, issue a warning and block until we
|
||||
# aren't. We don't bother with the lock here since this can be
|
||||
# slightly inexact. In general we should aim to not hit this
|
||||
# limit but it is good to have backpressure to avoid runaway
|
||||
# queues in cases of network outages/etc.
|
||||
if self.no_wait_count > self._max_no_wait_count:
|
||||
now = time.monotonic()
|
||||
if (
|
||||
self._last_no_wait_warn_time is None
|
||||
or now - self._last_no_wait_warn_time > 10.0
|
||||
):
|
||||
logger.warning(
|
||||
'ThreadPoolExecutorPlus hit max no-wait limit of %s;'
|
||||
' blocking.',
|
||||
self._max_no_wait_count,
|
||||
)
|
||||
self._last_no_wait_warn_time = now
|
||||
while self.no_wait_count > self._max_no_wait_count:
|
||||
time.sleep(0.01)
|
||||
|
||||
fut = self.submit(call, *args, **keywds)
|
||||
with self._no_wait_count_lock:
|
||||
self.no_wait_count += 1
|
||||
fut.add_done_callback(self._no_wait_done)
|
||||
|
||||
def _no_wait_done(self, fut: Future) -> None:
|
||||
with self._no_wait_count_lock:
|
||||
self.no_wait_count -= 1
|
||||
try:
|
||||
fut.result()
|
||||
except Exception:
|
||||
logger.exception('Error in work submitted via submit_no_wait().')
|
||||
@ -7,9 +7,10 @@ from __future__ import annotations
|
||||
import os
|
||||
import time
|
||||
import weakref
|
||||
import functools
|
||||
import datetime
|
||||
from enum import Enum
|
||||
from typing import TYPE_CHECKING, cast, TypeVar, Generic, overload
|
||||
from typing import TYPE_CHECKING, cast, TypeVar, Generic, overload, ParamSpec
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import asyncio
|
||||
@ -22,6 +23,8 @@ SelfT = TypeVar('SelfT')
|
||||
RetT = TypeVar('RetT')
|
||||
EnumT = TypeVar('EnumT', bound=Enum)
|
||||
|
||||
P = ParamSpec('P')
|
||||
|
||||
|
||||
class _EmptyObj:
|
||||
pass
|
||||
@ -32,6 +35,36 @@ class _EmptyObj:
|
||||
_g_empty_weak_ref = weakref.ref(_EmptyObj())
|
||||
assert _g_empty_weak_ref() is None
|
||||
|
||||
# Note to self: adding a special form of partial for when we don't need
|
||||
# to pass further args/kwargs (which I think is most cases). Even though
|
||||
# partial is now type-checked in Mypy (as of Nov 2024) there are still some
|
||||
# pitfalls that this avoids (see func docs below). Perhaps it would make
|
||||
# sense to simply define a Call class for this purpose; it might be more
|
||||
# efficient than wrapping partial anyway (should test this).
|
||||
if TYPE_CHECKING:
|
||||
|
||||
def strict_partial(
|
||||
func: Callable[P, T], *args: P.args, **kwargs: P.kwargs
|
||||
) -> Callable[[], T]:
|
||||
"""A version of functools.partial requiring all args to be passed.
|
||||
|
||||
This helps avoid pitfalls where a function is wrapped in a
|
||||
partial but then an extra required arg is added to the function
|
||||
but no type checking error is triggered at usage sites because
|
||||
vanilla partial assumes that extra arg will be provided at call
|
||||
time.
|
||||
|
||||
Note: it would seem like this pitfall could also be avoided on
|
||||
the back end by ensuring that the thing accepting the partial
|
||||
asks for Callable[[], None] instead of just Callable, but as of
|
||||
Nov 2024 it seems that Mypy does not support this; it in fact
|
||||
allows partials to be passed for any callable signature(!).
|
||||
"""
|
||||
...
|
||||
|
||||
else:
|
||||
strict_partial = functools.partial
|
||||
|
||||
|
||||
def explicit_bool(val: bool) -> bool:
|
||||
"""Return a non-inferable boolean value.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user