subscription wiring - account button now updates

This commit is contained in:
Eric Froemling 2024-11-26 16:39:59 -08:00
parent 2cb0bf0f4f
commit 8f176d274c
No known key found for this signature in database
30 changed files with 505 additions and 149 deletions

72
.efrocachemap generated
View File

@ -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",

View File

@ -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

View File

@ -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

View File

@ -46,7 +46,8 @@
"daemon",
"jinja2",
"jinja2.Environment",
"tomlkit"
"tomlkit",
"urllib3.exceptions"
],
"python_paths": [
"src/assets/ba_data/python",

View File

@ -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"

View File

@ -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.

View File

@ -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',

View File

@ -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

View File

@ -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.')

View File

@ -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'

View File

@ -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(

View File

@ -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')

View File

@ -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()

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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,
};
}

View File

@ -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

View File

@ -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_{};

View File

@ -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;

View File

@ -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;

View File

@ -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());

View File

@ -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

View File

@ -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);
}
}
}

View 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

View File

@ -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')]

View File

@ -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]):

View File

@ -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
View 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().')

View File

@ -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.