mirror of
https://github.com/imayushsaini/Bombsquad-Ballistica-Modded-Server.git
synced 2025-10-20 00:00:39 +00:00
syncing with ballisitca/master
This commit is contained in:
parent
9c57ee13f5
commit
a748699245
132 changed files with 2955 additions and 2192 deletions
34
dist/ba_data/python/babase/__init__.py
vendored
34
dist/ba_data/python/babase/__init__.py
vendored
|
|
@ -2,10 +2,10 @@
|
|||
#
|
||||
"""Common shared Ballistica components.
|
||||
|
||||
For modding purposes, this package should generally not be used directly.
|
||||
Instead one should use purpose-built packages such as bascenev1 or bauiv1
|
||||
which themselves import various functionality from here and reexpose it in
|
||||
a more focused way.
|
||||
For modding purposes, this package should generally not be used
|
||||
directly. Instead one should use purpose-built packages such as
|
||||
:mod:`bascenev1` or :mod:`bauiv1` which themselves import various
|
||||
functionality from here and reexpose it in a more focused way.
|
||||
"""
|
||||
# pylint: disable=redefined-builtin
|
||||
|
||||
|
|
@ -35,6 +35,7 @@ from _babase import (
|
|||
fullscreen_control_get,
|
||||
fullscreen_control_key_shortcut,
|
||||
fullscreen_control_set,
|
||||
can_display_chars,
|
||||
charstr,
|
||||
clipboard_get_text,
|
||||
clipboard_has_text,
|
||||
|
|
@ -64,7 +65,6 @@ from _babase import (
|
|||
get_virtual_screen_size,
|
||||
getsimplesound,
|
||||
has_user_run_commands,
|
||||
have_chars,
|
||||
have_permission,
|
||||
in_logic_thread,
|
||||
in_main_menu,
|
||||
|
|
@ -123,7 +123,7 @@ from _babase import (
|
|||
)
|
||||
|
||||
from babase._accountv2 import AccountV2Handle, AccountV2Subsystem
|
||||
from babase._app import App
|
||||
from babase._app import App, AppState
|
||||
from babase._appconfig import commit_app_config
|
||||
from babase._appintent import AppIntent, AppIntentDefault, AppIntentExec
|
||||
from babase._appmode import AppMode
|
||||
|
|
@ -135,7 +135,7 @@ from babase._apputils import (
|
|||
is_browser_likely_available,
|
||||
garbage_collect,
|
||||
get_remote_app_name,
|
||||
AppHealthMonitor,
|
||||
AppHealthSubsystem,
|
||||
utc_now_cloud,
|
||||
)
|
||||
from babase._cloud import CloudSubscription
|
||||
|
|
@ -146,8 +146,6 @@ from babase._devconsole import (
|
|||
)
|
||||
from babase._emptyappmode import EmptyAppMode
|
||||
from babase._error import (
|
||||
print_exception,
|
||||
print_error,
|
||||
ContextError,
|
||||
NotFoundError,
|
||||
PlayerNotFoundError,
|
||||
|
|
@ -164,7 +162,6 @@ from babase._error import (
|
|||
DelegateNotFoundError,
|
||||
)
|
||||
from babase._general import (
|
||||
utf8_all,
|
||||
DisplayTime,
|
||||
AppTime,
|
||||
WeakCall,
|
||||
|
|
@ -189,12 +186,15 @@ from babase._mgen.enums import (
|
|||
)
|
||||
from babase._math import normalized_color, is_point_in_box, vec3validate
|
||||
from babase._meta import MetadataSubsystem
|
||||
from babase._net import get_ip_address_type, DEFAULT_REQUEST_TIMEOUT_SECONDS
|
||||
from babase._net import (
|
||||
get_ip_address_type,
|
||||
DEFAULT_REQUEST_TIMEOUT_SECONDS,
|
||||
NetworkSubsystem,
|
||||
)
|
||||
from babase._plugin import PluginSpec, Plugin, PluginSubsystem
|
||||
from babase._stringedit import StringEditAdapter, StringEditSubsystem
|
||||
from babase._text import timestring
|
||||
|
||||
|
||||
_babase.app = app = App()
|
||||
app.postinit()
|
||||
|
||||
|
|
@ -207,14 +207,14 @@ __all__ = [
|
|||
'add_clean_frame_callback',
|
||||
'android_get_external_files_dir',
|
||||
'app',
|
||||
'app',
|
||||
'App',
|
||||
'AppConfig',
|
||||
'AppHealthMonitor',
|
||||
'AppHealthSubsystem',
|
||||
'AppIntent',
|
||||
'AppIntentDefault',
|
||||
'AppIntentExec',
|
||||
'AppMode',
|
||||
'AppState',
|
||||
'app_instance_uuid',
|
||||
'applog',
|
||||
'appname',
|
||||
|
|
@ -233,6 +233,7 @@ __all__ = [
|
|||
'fullscreen_control_get',
|
||||
'fullscreen_control_key_shortcut',
|
||||
'fullscreen_control_set',
|
||||
'can_display_chars',
|
||||
'charstr',
|
||||
'clipboard_get_text',
|
||||
'clipboard_has_text',
|
||||
|
|
@ -279,7 +280,6 @@ __all__ = [
|
|||
'getsimplesound',
|
||||
'handle_leftover_v1_cloud_log_file',
|
||||
'has_user_run_commands',
|
||||
'have_chars',
|
||||
'have_permission',
|
||||
'in_logic_thread',
|
||||
'in_main_menu',
|
||||
|
|
@ -313,6 +313,7 @@ __all__ = [
|
|||
'native_review_request',
|
||||
'native_review_request_supported',
|
||||
'native_stack_trace',
|
||||
'NetworkSubsystem',
|
||||
'NodeNotFoundError',
|
||||
'normalized_color',
|
||||
'NotFoundError',
|
||||
|
|
@ -327,8 +328,6 @@ __all__ = [
|
|||
'Plugin',
|
||||
'PluginSubsystem',
|
||||
'PluginSpec',
|
||||
'print_error',
|
||||
'print_exception',
|
||||
'print_load_info',
|
||||
'push_back_press',
|
||||
'pushcall',
|
||||
|
|
@ -367,7 +366,6 @@ __all__ = [
|
|||
'user_agent_string',
|
||||
'user_ran_commands',
|
||||
'utc_now_cloud',
|
||||
'utf8_all',
|
||||
'Vec3',
|
||||
'vec3validate',
|
||||
'verify_object_death',
|
||||
|
|
|
|||
65
dist/ba_data/python/babase/_accountv2.py
vendored
65
dist/ba_data/python/babase/_accountv2.py
vendored
|
|
@ -25,9 +25,9 @@ logger = logging.getLogger('ba.accountv2')
|
|||
class AccountV2Subsystem:
|
||||
"""Subsystem for modern account handling in the app.
|
||||
|
||||
Category: **App Classes**
|
||||
|
||||
Access the single shared instance of this class at 'ba.app.plus.accounts'.
|
||||
Access the single shared instance of this class via the
|
||||
:attr:`~baplus.PlusAppSubsystem.accounts` attr on the
|
||||
:class:`~baplus.PlusAppSubsystem` class.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
|
|
@ -71,8 +71,10 @@ class AccountV2Subsystem:
|
|||
self.login_adapters[adapter.login_type] = adapter
|
||||
|
||||
def on_app_loading(self) -> None:
|
||||
"""Should be called at standard on_app_loading time."""
|
||||
"""Internal; Called at standard on_app_loading time.
|
||||
|
||||
:meta private:
|
||||
"""
|
||||
for adapter in self.login_adapters.values():
|
||||
adapter.on_app_loading()
|
||||
|
||||
|
|
@ -81,7 +83,7 @@ class AccountV2Subsystem:
|
|||
|
||||
Note that this does not mean these credentials have been checked
|
||||
for validity; only that they exist. If/when credentials are
|
||||
validated, the 'primary' account handle will be set.
|
||||
validated, the :attr:`primary` account handle will be set.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
|
@ -97,6 +99,8 @@ class AccountV2Subsystem:
|
|||
|
||||
Will be called with None on log-outs and when new credentials
|
||||
are set but have not yet been verified.
|
||||
|
||||
:meta private:
|
||||
"""
|
||||
assert _babase.in_logic_thread()
|
||||
|
||||
|
|
@ -144,15 +148,20 @@ class AccountV2Subsystem:
|
|||
_babase.app.on_initial_sign_in_complete()
|
||||
|
||||
def on_active_logins_changed(self, logins: dict[LoginType, str]) -> None:
|
||||
"""Should be called when logins for the active account change."""
|
||||
"""Called when logins for the active account change.
|
||||
|
||||
:meta private:
|
||||
"""
|
||||
for adapter in self.login_adapters.values():
|
||||
adapter.set_active_logins(logins)
|
||||
|
||||
def on_implicit_sign_in(
|
||||
self, login_type: LoginType, login_id: str, display_name: str
|
||||
) -> None:
|
||||
"""An implicit sign-in happened (called by native layer)."""
|
||||
"""An implicit sign-in happened (called by native layer).
|
||||
|
||||
:meta private:
|
||||
"""
|
||||
from babase._login import LoginAdapter
|
||||
|
||||
assert _babase.in_logic_thread()
|
||||
|
|
@ -165,17 +174,22 @@ class AccountV2Subsystem:
|
|||
)
|
||||
|
||||
def on_implicit_sign_out(self, login_type: LoginType) -> None:
|
||||
"""An implicit sign-out happened (called by native layer)."""
|
||||
"""An implicit sign-out happened (called by native layer).
|
||||
|
||||
:meta private:
|
||||
"""
|
||||
assert _babase.in_logic_thread()
|
||||
with _babase.ContextRef.empty():
|
||||
self.login_adapters[login_type].set_implicit_login_state(None)
|
||||
|
||||
def on_no_initial_primary_account(self) -> None:
|
||||
"""Callback run if the app has no primary account after launch.
|
||||
"""Internal; run if the app has no primary account after launch.
|
||||
|
||||
Either this callback or on_primary_account_changed will be called
|
||||
within a few seconds of app launch; the app can move forward
|
||||
with the startup sequence at that point.
|
||||
Either this callback or on_primary_account_changed will be
|
||||
called within a few seconds of app launch; the app can move
|
||||
forward with the startup sequence at that point.
|
||||
|
||||
:meta private:
|
||||
"""
|
||||
if not self._initial_sign_in_completed:
|
||||
self._initial_sign_in_completed = True
|
||||
|
|
@ -192,13 +206,15 @@ class AccountV2Subsystem:
|
|||
login_type: LoginType,
|
||||
state: LoginAdapter.ImplicitLoginState | None,
|
||||
) -> None:
|
||||
"""Called when implicit login state changes.
|
||||
"""Internal; Called when implicit login state changes.
|
||||
|
||||
Login systems that tend to sign themselves in/out in the
|
||||
background are considered implicit. We may choose to honor or
|
||||
ignore their states, allowing the user to opt for other login
|
||||
types even if the default implicit one can't be explicitly
|
||||
logged out or otherwise controlled.
|
||||
|
||||
:meta private:
|
||||
"""
|
||||
from babase._language import Lstr
|
||||
|
||||
|
|
@ -284,11 +300,19 @@ class AccountV2Subsystem:
|
|||
self._update_auto_sign_in()
|
||||
|
||||
def do_get_primary(self) -> AccountV2Handle | None:
|
||||
"""Internal - should be overridden by subclass."""
|
||||
"""Internal; should be overridden by subclass.
|
||||
|
||||
:meta private:
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def set_primary_credentials(self, credentials: str | None) -> None:
|
||||
"""Set credentials for the primary app account."""
|
||||
"""Set credentials for the primary app account.
|
||||
|
||||
Once credentials are set, they will be verified in the cloud
|
||||
asynchronously. If verification is successful, the
|
||||
:attr:`primary` attr will be set to the resulting account.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def _update_auto_sign_in(self) -> None:
|
||||
|
|
@ -441,14 +465,23 @@ class AccountV2Subsystem:
|
|||
class AccountV2Handle:
|
||||
"""Handle for interacting with a V2 account.
|
||||
|
||||
This class supports the 'with' statement, which is how it is
|
||||
This class supports the ``with`` statement, which is how it is
|
||||
used with some operations such as cloud messaging.
|
||||
"""
|
||||
|
||||
#: The id of this account.
|
||||
accountid: str
|
||||
|
||||
#: The last known tag for this account.
|
||||
tag: str
|
||||
|
||||
#: The name of the workspace being synced to this client.
|
||||
workspacename: str | None
|
||||
|
||||
#: The id of the workspace being synced to this client, if any.
|
||||
workspaceid: str | None
|
||||
|
||||
#: Info about last known logins associated with this account.
|
||||
logins: dict[LoginType, LoginInfo]
|
||||
|
||||
def __enter__(self) -> None:
|
||||
|
|
|
|||
412
dist/ba_data/python/babase/_app.py
vendored
412
dist/ba_data/python/babase/_app.py
vendored
|
|
@ -11,7 +11,7 @@ from functools import partial
|
|||
from typing import TYPE_CHECKING, TypeVar, override
|
||||
from threading import RLock
|
||||
|
||||
from efro.threadpool import ThreadPoolExecutorPlus
|
||||
from efro.threadpool import ThreadPoolExecutorEx
|
||||
|
||||
import _babase
|
||||
from babase._language import LanguageSubsystem
|
||||
|
|
@ -34,7 +34,7 @@ if TYPE_CHECKING:
|
|||
|
||||
import babase
|
||||
from babase import AppIntent, AppMode, AppSubsystem
|
||||
from babase._apputils import AppHealthMonitor
|
||||
from babase._apputils import AppHealthSubsystem
|
||||
|
||||
# __FEATURESET_APP_SUBSYSTEM_IMPORTS_BEGIN__
|
||||
# This section generated by batools.appmodule; do not edit.
|
||||
|
|
@ -49,112 +49,34 @@ T = TypeVar('T')
|
|||
|
||||
|
||||
class App:
|
||||
"""A class for high level app functionality and state.
|
||||
"""High level Ballistica app functionality and state.
|
||||
|
||||
Category: **App Classes**
|
||||
|
||||
Use babase.app to access the single shared instance of this class.
|
||||
|
||||
Note that properties not documented here should be considered internal
|
||||
and subject to change without warning.
|
||||
Access the single shared instance of this class via the ``app`` attr
|
||||
available on various high level modules such as :mod:`babase`,
|
||||
:mod:`bauiv1`, and :mod:`bascenev1`.
|
||||
"""
|
||||
|
||||
# pylint: disable=too-many-public-methods
|
||||
|
||||
# A few things defined as non-optional values but not actually
|
||||
# available until the app starts.
|
||||
# available until the app starts (so we need to predeclare them
|
||||
# here).
|
||||
|
||||
#: Subsystem for wrangling plugins.
|
||||
plugins: PluginSubsystem
|
||||
|
||||
#: Language subsystem.
|
||||
lang: LanguageSubsystem
|
||||
health_monitor: AppHealthMonitor
|
||||
|
||||
# How long we allow shutdown tasks to run before killing them.
|
||||
# Currently the entire app hard-exits if shutdown takes 15 seconds,
|
||||
# so we need to keep it under that. Staying above 10 should allow
|
||||
# 10 second network timeouts to happen though.
|
||||
#: Subsystem for keeping tabs on app health.
|
||||
health: AppHealthSubsystem
|
||||
|
||||
#: How long we allow shutdown tasks to run before killing them.
|
||||
#: Currently the entire app hard-exits if shutdown takes 15 seconds,
|
||||
#: so we need to keep it under that. Staying above 10 should allow
|
||||
#: 10 second network timeouts to happen though.
|
||||
SHUTDOWN_TASK_TIMEOUT_SECONDS = 12
|
||||
|
||||
class State(Enum):
|
||||
"""High level state the app can be in."""
|
||||
|
||||
# The app has not yet begun starting and should not be used in
|
||||
# any way.
|
||||
NOT_STARTED = 0
|
||||
|
||||
# The native layer is spinning up its machinery (screens,
|
||||
# renderers, etc.). Nothing should happen in the Python layer
|
||||
# until this completes.
|
||||
NATIVE_BOOTSTRAPPING = 1
|
||||
|
||||
# Python app subsystems are being inited but should not yet
|
||||
# interact or do any work.
|
||||
INITING = 2
|
||||
|
||||
# Python app subsystems are inited and interacting, but the app
|
||||
# has not yet embarked on a high level course of action. It is
|
||||
# doing initial account logins, workspace & asset downloads,
|
||||
# etc.
|
||||
LOADING = 3
|
||||
|
||||
# All pieces are in place and the app is now doing its thing.
|
||||
RUNNING = 4
|
||||
|
||||
# Used on platforms such as mobile where the app basically needs
|
||||
# to shut down while backgrounded. In this state, all event
|
||||
# loops are suspended and all graphics and audio must cease
|
||||
# completely. Be aware that the suspended state can be entered
|
||||
# from any other state including NATIVE_BOOTSTRAPPING and
|
||||
# SHUTTING_DOWN.
|
||||
SUSPENDED = 5
|
||||
|
||||
# The app is shutting down. This process may involve sending
|
||||
# network messages or other things that can take up to a few
|
||||
# seconds, so ideally graphics and audio should remain
|
||||
# functional (with fades or spinners or whatever to show
|
||||
# something is happening).
|
||||
SHUTTING_DOWN = 6
|
||||
|
||||
# The app has completed shutdown. Any code running here should
|
||||
# be basically immediate.
|
||||
SHUTDOWN_COMPLETE = 7
|
||||
|
||||
class DefaultAppModeSelector(AppModeSelector):
|
||||
"""Decides which AppModes to use to handle AppIntents.
|
||||
|
||||
This default version is generated by the project updater based
|
||||
on the 'default_app_modes' value in the projectconfig.
|
||||
|
||||
It is also possible to modify app mode selection behavior by
|
||||
setting app.mode_selector to an instance of a custom
|
||||
AppModeSelector subclass. This is a good way to go if you are
|
||||
modifying app behavior dynamically via a plugin instead of
|
||||
statically in a spinoff project.
|
||||
"""
|
||||
|
||||
@override
|
||||
def app_mode_for_intent(
|
||||
self, intent: AppIntent
|
||||
) -> type[AppMode] | None:
|
||||
# pylint: disable=cyclic-import
|
||||
|
||||
# __DEFAULT_APP_MODE_SELECTION_BEGIN__
|
||||
# This section generated by batools.appmodule; do not edit.
|
||||
|
||||
# Ask our default app modes to handle it.
|
||||
# (generated from 'default_app_modes' in projectconfig).
|
||||
import baclassic
|
||||
import babase
|
||||
|
||||
for appmode in [
|
||||
baclassic.ClassicAppMode,
|
||||
babase.EmptyAppMode,
|
||||
]:
|
||||
if appmode.can_handle_intent(intent):
|
||||
return appmode
|
||||
|
||||
return None
|
||||
|
||||
# __DEFAULT_APP_MODE_SELECTION_END__
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""(internal)
|
||||
|
||||
|
|
@ -168,34 +90,47 @@ class App:
|
|||
if os.environ.get('BA_RUNNING_WITH_DUMMY_MODULES') == '1':
|
||||
return
|
||||
|
||||
# Wrap our raw app config in our special wrapper and pass it to
|
||||
# the native layer.
|
||||
self.config = AppConfig(_babase.get_initial_app_config())
|
||||
#: Config values for the app.
|
||||
self.config: AppConfig = AppConfig(_babase.get_initial_app_config())
|
||||
_babase.set_app_config(self.config)
|
||||
|
||||
#: Static environment values for the app.
|
||||
self.env: babase.Env = _babase.Env()
|
||||
self.state = self.State.NOT_STARTED
|
||||
|
||||
# Default executor which can be used for misc background
|
||||
# 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 = ThreadPoolExecutorPlus(
|
||||
#: Current app state.
|
||||
self.state: AppState = AppState.NOT_STARTED
|
||||
|
||||
#: Default executor which can be used for misc background
|
||||
#: 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: ThreadPoolExecutorEx = ThreadPoolExecutorEx(
|
||||
thread_name_prefix='baworker',
|
||||
initializer=self._thread_pool_thread_init,
|
||||
)
|
||||
|
||||
self.meta = MetadataSubsystem()
|
||||
self.net = NetworkSubsystem()
|
||||
self.workspaces = WorkspaceSubsystem()
|
||||
self.components = AppComponentSubsystem()
|
||||
self.stringedit = StringEditSubsystem()
|
||||
self.devconsole = DevConsoleSubsystem()
|
||||
#: Subsystem for wrangling metadata.
|
||||
self.meta: MetadataSubsystem = MetadataSubsystem()
|
||||
|
||||
# This is incremented any time the app is backgrounded or
|
||||
# foregrounded; can be a simple way to determine if network data
|
||||
# should be refreshed/etc.
|
||||
self.fg_state = 0
|
||||
#: Subsystem for network functionality.
|
||||
self.net: NetworkSubsystem = NetworkSubsystem()
|
||||
|
||||
#: Subsystem for wrangling workspaces.
|
||||
self.workspaces: WorkspaceSubsystem = WorkspaceSubsystem()
|
||||
|
||||
# (not actually in use yet)
|
||||
self.components: AppComponentSubsystem = AppComponentSubsystem()
|
||||
|
||||
#: Subsystem for wrangling text input from various sources.
|
||||
self.stringedit: StringEditSubsystem = StringEditSubsystem()
|
||||
|
||||
#: Subsystem for wrangling the dev-console UI.
|
||||
self.devconsole: DevConsoleSubsystem = DevConsoleSubsystem()
|
||||
|
||||
#: Incremented each time the app leaves the
|
||||
#: :attr:`~babase.AppState.SUSPENDED` state. This can be a simple
|
||||
#: way to determine if network data should be refreshed/etc.
|
||||
self.fg_state: int = 0
|
||||
|
||||
self._subsystems: list[AppSubsystem] = []
|
||||
self._native_bootstrapping_completed = False
|
||||
|
|
@ -235,9 +170,9 @@ class App:
|
|||
self._subsystem_property_data: dict[str, AppSubsystem | bool] = {}
|
||||
|
||||
def postinit(self) -> None:
|
||||
"""Called after we've been inited and assigned to babase.app.
|
||||
"""Called after we've been inited and assigned to ``babase.app``.
|
||||
|
||||
Anything that accesses babase.app as part of its init process
|
||||
Anything that accesses ``babase.app`` as part of its init process
|
||||
must go here instead of __init__.
|
||||
"""
|
||||
|
||||
|
|
@ -267,26 +202,27 @@ class App:
|
|||
|
||||
@property
|
||||
def asyncio_loop(self) -> asyncio.AbstractEventLoop:
|
||||
"""The logic thread's asyncio event loop.
|
||||
"""The logic thread's :mod:`asyncio` event-loop.
|
||||
|
||||
This allow async tasks to be run in the logic thread.
|
||||
This allows :mod:`asyncio` tasks to be run in the logic thread.
|
||||
|
||||
Generally you should call App.create_async_task() to schedule
|
||||
async code to run instead of using this directly. That will
|
||||
handle retaining the task and logging errors automatically.
|
||||
Only schedule tasks onto asyncio_loop yourself when you intend
|
||||
to hold on to the returned task and await its results. Releasing
|
||||
Generally you should call
|
||||
:meth:`~babase.App.create_async_task()` to schedule async code
|
||||
to run instead of using this directly. That will handle
|
||||
retaining the task and logging errors automatically. Only
|
||||
schedule tasks onto ``asyncio_loop`` yourself when you intend to
|
||||
hold on to the returned task and await its results. Releasing
|
||||
the task reference can lead to subtle bugs such as unreported
|
||||
errors and garbage-collected tasks disappearing before their
|
||||
work is done.
|
||||
|
||||
Note that, at this time, the asyncio loop is encapsulated
|
||||
and explicitly stepped by the engine's logic thread loop and
|
||||
thus things like asyncio.get_running_loop() will unintuitively
|
||||
*not* return this loop from most places in the logic thread;
|
||||
only from within a task explicitly created in this loop.
|
||||
Hopefully this situation will be improved in the future with a
|
||||
unified event loop.
|
||||
Note that, at this time, the asyncio loop is encapsulated and
|
||||
explicitly stepped by the engine's logic thread loop and thus
|
||||
things like :meth:`asyncio.get_running_loop()` will
|
||||
unintuitively *not* return this loop from most places in the
|
||||
logic thread; only from within a task explicitly created in this
|
||||
loop. Hopefully this situation will be improved in the future
|
||||
with a unified event loop.
|
||||
"""
|
||||
assert _babase.in_logic_thread()
|
||||
assert self._asyncio_loop is not None
|
||||
|
|
@ -295,12 +231,12 @@ class App:
|
|||
def create_async_task(
|
||||
self, coro: Coroutine[Any, Any, T], *, name: str | None = None
|
||||
) -> None:
|
||||
"""Create a fully managed async task.
|
||||
"""Create a fully managed :mod:`asyncio` task.
|
||||
|
||||
This will automatically retain and release a reference to the task
|
||||
and log any exceptions that occur in it. If you need to await a task
|
||||
or otherwise need more control, schedule a task directly using
|
||||
App.asyncio_loop.
|
||||
:attr:`asyncio_loop`.
|
||||
"""
|
||||
assert _babase.in_logic_thread()
|
||||
|
||||
|
|
@ -453,7 +389,10 @@ class App:
|
|||
# __FEATURESET_APP_SUBSYSTEM_PROPERTIES_END__
|
||||
|
||||
def register_subsystem(self, subsystem: AppSubsystem) -> None:
|
||||
"""Called by the AppSubsystem class. Do not use directly."""
|
||||
"""Called by the AppSubsystem class. Do not use directly.
|
||||
|
||||
:meta private:
|
||||
"""
|
||||
|
||||
# We only allow registering new subsystems if we've not yet
|
||||
# reached the 'running' state. This ensures that all subsystems
|
||||
|
|
@ -470,11 +409,12 @@ class App:
|
|||
"""Add a task to be run on app shutdown.
|
||||
|
||||
Note that shutdown tasks will be canceled after
|
||||
App.SHUTDOWN_TASK_TIMEOUT_SECONDS if they are still running.
|
||||
:py:const:`SHUTDOWN_TASK_TIMEOUT_SECONDS` if they are still
|
||||
running.
|
||||
"""
|
||||
if (
|
||||
self.state is self.State.SHUTTING_DOWN
|
||||
or self.state is self.State.SHUTDOWN_COMPLETE
|
||||
self.state is AppState.SHUTTING_DOWN
|
||||
or self.state is AppState.SHUTDOWN_COMPLETE
|
||||
):
|
||||
stname = self.state.name
|
||||
raise RuntimeError(
|
||||
|
|
@ -485,8 +425,8 @@ class App:
|
|||
def run(self) -> None:
|
||||
"""Run the app to completion.
|
||||
|
||||
Note that this only works on builds where Ballistica manages
|
||||
its own event loop.
|
||||
Note that this only works on builds/runs where Ballistica is
|
||||
managing its own event loop.
|
||||
"""
|
||||
_babase.run_app()
|
||||
|
||||
|
|
@ -495,9 +435,9 @@ class App:
|
|||
|
||||
Intent defines what the app is trying to do at a given time.
|
||||
This call is asynchronous; the intent switch will happen in the
|
||||
logic thread in the near future. If set_intent is called
|
||||
repeatedly before the change takes place, the final intent to be
|
||||
set will be used.
|
||||
logic thread in the near future. If this is called repeatedly
|
||||
before the change takes place, the final intent to be set will
|
||||
be used.
|
||||
"""
|
||||
|
||||
# Mark this one as pending. We do this synchronously so that the
|
||||
|
|
@ -510,7 +450,10 @@ class App:
|
|||
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."""
|
||||
"""Internal. Use app.config.apply() to apply app config changes.
|
||||
|
||||
:meta private:
|
||||
"""
|
||||
# To be safe, let's run this by itself in the event loop.
|
||||
# This avoids potential trouble if this gets called mid-draw or
|
||||
# something like that.
|
||||
|
|
@ -518,47 +461,68 @@ class App:
|
|||
_babase.pushcall(self._apply_app_config, raw=True)
|
||||
|
||||
def on_native_start(self) -> None:
|
||||
"""Called by the native layer when the app is being started."""
|
||||
"""Called by the native layer when the app is being started.
|
||||
|
||||
:meta private:
|
||||
"""
|
||||
assert _babase.in_logic_thread()
|
||||
assert not self._native_start_called
|
||||
self._native_start_called = True
|
||||
self._update_state()
|
||||
|
||||
def on_native_bootstrapping_complete(self) -> None:
|
||||
"""Called by the native layer once its ready to rock."""
|
||||
"""Called by the native layer once its ready to rock.
|
||||
|
||||
:meta private:
|
||||
"""
|
||||
assert _babase.in_logic_thread()
|
||||
assert not self._native_bootstrapping_completed
|
||||
self._native_bootstrapping_completed = True
|
||||
self._update_state()
|
||||
|
||||
def on_native_suspend(self) -> None:
|
||||
"""Called by the native layer when the app is suspended."""
|
||||
"""Called by the native layer when the app is suspended.
|
||||
|
||||
:meta private:
|
||||
"""
|
||||
assert _babase.in_logic_thread()
|
||||
assert not self._native_suspended # Should avoid redundant calls.
|
||||
self._native_suspended = True
|
||||
self._update_state()
|
||||
|
||||
def on_native_unsuspend(self) -> None:
|
||||
"""Called by the native layer when the app suspension ends."""
|
||||
"""Called by the native layer when the app suspension ends.
|
||||
|
||||
:meta private:
|
||||
"""
|
||||
assert _babase.in_logic_thread()
|
||||
assert self._native_suspended # Should avoid redundant calls.
|
||||
self._native_suspended = False
|
||||
self._update_state()
|
||||
|
||||
def on_native_shutdown(self) -> None:
|
||||
"""Called by the native layer when the app starts shutting down."""
|
||||
"""Called by the native layer when the app starts shutting down.
|
||||
|
||||
:meta private:
|
||||
"""
|
||||
assert _babase.in_logic_thread()
|
||||
self._native_shutdown_called = True
|
||||
self._update_state()
|
||||
|
||||
def on_native_shutdown_complete(self) -> None:
|
||||
"""Called by the native layer when the app is done shutting down."""
|
||||
"""Called by the native layer when the app is done shutting down.
|
||||
|
||||
:meta private:
|
||||
"""
|
||||
assert _babase.in_logic_thread()
|
||||
self._native_shutdown_complete_called = True
|
||||
self._update_state()
|
||||
|
||||
def on_native_active_changed(self) -> None:
|
||||
"""Called by the native layer when the app active state changes."""
|
||||
"""Called by the native layer when the app active state changes.
|
||||
|
||||
:meta private:
|
||||
"""
|
||||
assert _babase.in_logic_thread()
|
||||
if self._mode is not None:
|
||||
self._mode.on_app_active_changed()
|
||||
|
|
@ -590,6 +554,8 @@ class App:
|
|||
initial-sign-in process may include tasks such as syncing
|
||||
account workspaces or other data so it may take a substantial
|
||||
amount of time.
|
||||
|
||||
:meta private:
|
||||
"""
|
||||
assert _babase.in_logic_thread()
|
||||
assert not self._initial_sign_in_completed
|
||||
|
|
@ -604,8 +570,11 @@ class App:
|
|||
def set_ui_scale(self, scale: babase.UIScale) -> None:
|
||||
"""Change ui-scale on the fly.
|
||||
|
||||
Currently this is mainly for debugging and will not be called as
|
||||
part of normal app operation.
|
||||
Currently this is mainly for testing/debugging and will not be
|
||||
called as part of normal app operation, though this may change
|
||||
in the future.
|
||||
|
||||
:meta private:
|
||||
"""
|
||||
assert _babase.in_logic_thread()
|
||||
|
||||
|
|
@ -625,7 +594,10 @@ class App:
|
|||
)
|
||||
|
||||
def on_screen_size_change(self) -> None:
|
||||
"""Screen size has changed."""
|
||||
"""Screen size has changed.
|
||||
|
||||
:meta private:
|
||||
"""
|
||||
|
||||
# Inform all app subsystems in the same order they were inited.
|
||||
# Operate on a copy of the list here because this can be called
|
||||
|
|
@ -768,7 +740,7 @@ class App:
|
|||
# pylint: disable=cyclic-import
|
||||
from babase import _asyncio
|
||||
from babase import _appconfig
|
||||
from babase._apputils import AppHealthMonitor
|
||||
from babase._apputils import AppHealthSubsystem
|
||||
from babase import _env
|
||||
|
||||
assert _babase.in_logic_thread()
|
||||
|
|
@ -776,7 +748,7 @@ class App:
|
|||
_env.on_app_state_initing()
|
||||
|
||||
self._asyncio_loop = _asyncio.setup_asyncio()
|
||||
self.health_monitor = AppHealthMonitor()
|
||||
self.health = AppHealthSubsystem()
|
||||
|
||||
# __FEATURESET_APP_SUBSYSTEM_CREATE_BEGIN__
|
||||
# This section generated by batools.appmodule; do not edit.
|
||||
|
|
@ -849,7 +821,7 @@ class App:
|
|||
# Set a default app-mode-selector if none has been set yet
|
||||
# by a plugin or whatnot.
|
||||
if self._mode_selector is None:
|
||||
self._mode_selector = self.DefaultAppModeSelector()
|
||||
self._mode_selector = DefaultAppModeSelector()
|
||||
|
||||
# Inform all app subsystems in the same order they were
|
||||
# registered. Operate on a copy here because subsystems can
|
||||
|
|
@ -913,8 +885,8 @@ class App:
|
|||
|
||||
# Shutdown-complete trumps absolutely all.
|
||||
if self._native_shutdown_complete_called:
|
||||
if self.state is not self.State.SHUTDOWN_COMPLETE:
|
||||
self.state = self.State.SHUTDOWN_COMPLETE
|
||||
if self.state is not AppState.SHUTDOWN_COMPLETE:
|
||||
self.state = AppState.SHUTDOWN_COMPLETE
|
||||
lifecyclelog.info('app-state is now %s', self.state.name)
|
||||
self._on_shutdown_complete()
|
||||
|
||||
|
|
@ -923,26 +895,26 @@ class App:
|
|||
# the shutdown process.
|
||||
elif self._native_shutdown_called and self._init_completed:
|
||||
# Entering shutdown state:
|
||||
if self.state is not self.State.SHUTTING_DOWN:
|
||||
self.state = self.State.SHUTTING_DOWN
|
||||
if self.state is not AppState.SHUTTING_DOWN:
|
||||
self.state = AppState.SHUTTING_DOWN
|
||||
applog.info('Shutting down...')
|
||||
lifecyclelog.info('app-state is now %s', self.state.name)
|
||||
self._on_shutting_down()
|
||||
|
||||
elif self._native_suspended:
|
||||
# Entering suspended state:
|
||||
if self.state is not self.State.SUSPENDED:
|
||||
self.state = self.State.SUSPENDED
|
||||
if self.state is not AppState.SUSPENDED:
|
||||
self.state = AppState.SUSPENDED
|
||||
self._on_suspend()
|
||||
else:
|
||||
# Leaving suspended state:
|
||||
if self.state is self.State.SUSPENDED:
|
||||
if self.state is AppState.SUSPENDED:
|
||||
self._on_unsuspend()
|
||||
|
||||
# Entering or returning to running state
|
||||
if self._initial_sign_in_completed and self._meta_scan_completed:
|
||||
if self.state != self.State.RUNNING:
|
||||
self.state = self.State.RUNNING
|
||||
if self.state != AppState.RUNNING:
|
||||
self.state = AppState.RUNNING
|
||||
lifecyclelog.info('app-state is now %s', self.state.name)
|
||||
if not self._called_on_running:
|
||||
self._called_on_running = True
|
||||
|
|
@ -950,8 +922,8 @@ class App:
|
|||
|
||||
# Entering or returning to loading state:
|
||||
elif self._init_completed:
|
||||
if self.state is not self.State.LOADING:
|
||||
self.state = self.State.LOADING
|
||||
if self.state is not AppState.LOADING:
|
||||
self.state = AppState.LOADING
|
||||
lifecyclelog.info('app-state is now %s', self.state.name)
|
||||
if not self._called_on_loading:
|
||||
self._called_on_loading = True
|
||||
|
|
@ -959,8 +931,8 @@ class App:
|
|||
|
||||
# Entering or returning to initing state:
|
||||
elif self._native_bootstrapping_completed:
|
||||
if self.state is not self.State.INITING:
|
||||
self.state = self.State.INITING
|
||||
if self.state is not AppState.INITING:
|
||||
self.state = AppState.INITING
|
||||
lifecyclelog.info('app-state is now %s', self.state.name)
|
||||
if not self._called_on_initing:
|
||||
self._called_on_initing = True
|
||||
|
|
@ -968,8 +940,8 @@ class App:
|
|||
|
||||
# Entering or returning to native bootstrapping:
|
||||
elif self._native_start_called:
|
||||
if self.state is not self.State.NATIVE_BOOTSTRAPPING:
|
||||
self.state = self.State.NATIVE_BOOTSTRAPPING
|
||||
if self.state is not AppState.NATIVE_BOOTSTRAPPING:
|
||||
self.state = AppState.NATIVE_BOOTSTRAPPING
|
||||
lifecyclelog.info('app-state is now %s', self.state.name)
|
||||
else:
|
||||
# Only logical possibility left is NOT_STARTED, in which
|
||||
|
|
@ -1065,6 +1037,19 @@ class App:
|
|||
"""(internal)"""
|
||||
assert _babase.in_logic_thread()
|
||||
|
||||
# Deactivate any active app-mode. This allows things like saving
|
||||
# state to happen naturally without needing to handle
|
||||
# app-shutdown as a special case.
|
||||
if self._mode is not None:
|
||||
try:
|
||||
self._mode.on_deactivate()
|
||||
except Exception:
|
||||
logging.exception(
|
||||
'Error deactivating app-mode %s at app shutdown.',
|
||||
self._mode,
|
||||
)
|
||||
self._mode = None
|
||||
|
||||
# Inform app subsystems that we're done shutting down in the opposite
|
||||
# order they were inited.
|
||||
for subsystem in reversed(self._subsystems):
|
||||
|
|
@ -1124,3 +1109,86 @@ class App:
|
|||
# Help keep things clear in profiling tools/etc.
|
||||
self._pool_thread_count += 1
|
||||
_babase.set_thread_name(f'ballistica worker-{self._pool_thread_count}')
|
||||
|
||||
|
||||
class AppState(Enum):
|
||||
"""High level state the app can be in."""
|
||||
|
||||
#: The app has not yet begun starting and should not be used in
|
||||
#: any way.
|
||||
NOT_STARTED = 0
|
||||
|
||||
#: The native layer is spinning up its machinery (screens,
|
||||
#: renderers, etc.). Nothing should happen in the Python layer
|
||||
#: until this completes.
|
||||
NATIVE_BOOTSTRAPPING = 1
|
||||
|
||||
#: Python app subsystems are being inited but should not yet
|
||||
#: interact or do any work.
|
||||
INITING = 2
|
||||
|
||||
#: Python app subsystems are inited and interacting, but the app
|
||||
#: has not yet embarked on a high level course of action. It is
|
||||
#: doing initial account logins, workspace & asset downloads,
|
||||
#: etc.
|
||||
LOADING = 3
|
||||
|
||||
#: All pieces are in place and the app is now doing its thing.
|
||||
RUNNING = 4
|
||||
|
||||
#: Used on platforms such as mobile where the app basically needs
|
||||
#: to shut down while backgrounded. In this state, all event
|
||||
#: loops are suspended and all graphics and audio must cease
|
||||
#: completely. Be aware that the suspended state can be entered
|
||||
#: from any other state including :attr:`NATIVE_BOOTSTRAPPING` and
|
||||
#: :attr:`SHUTTING_DOWN`.
|
||||
SUSPENDED = 5
|
||||
|
||||
#: The app is shutting down. This process may involve sending
|
||||
#: network messages or other things that can take up to a few
|
||||
#: seconds, so ideally graphics and audio should remain
|
||||
#: functional (with fades or spinners or whatever to show
|
||||
#: something is happening).
|
||||
SHUTTING_DOWN = 6
|
||||
|
||||
#: The app has completed shutdown. Any code running here should
|
||||
#: be basically immediate.
|
||||
SHUTDOWN_COMPLETE = 7
|
||||
|
||||
|
||||
class DefaultAppModeSelector(AppModeSelector):
|
||||
"""Selects an :class:`AppMode` to handle an :class:`AppIntent`.
|
||||
|
||||
This default version is generated by the project updater based
|
||||
on the 'default_app_modes' value in the projectconfig.
|
||||
|
||||
It is also possible to modify app mode selection behavior by
|
||||
setting the app's :attr:`~babase.App.mode_selector` to an
|
||||
instance of a custom :class:`~babase.AppModeSelector` subclass.
|
||||
This is a good way to go if you are modifying app behavior
|
||||
dynamically via a :class:`~babase.Plugin` instead of statically
|
||||
in a spinoff project.
|
||||
"""
|
||||
|
||||
@override
|
||||
def app_mode_for_intent(self, intent: AppIntent) -> type[AppMode] | None:
|
||||
# pylint: disable=cyclic-import
|
||||
|
||||
# __DEFAULT_APP_MODE_SELECTION_BEGIN__
|
||||
# This section generated by batools.appmodule; do not edit.
|
||||
|
||||
# Ask our default app modes to handle it.
|
||||
# (generated from 'default_app_modes' in projectconfig).
|
||||
import baclassic
|
||||
import babase
|
||||
|
||||
for appmode in [
|
||||
baclassic.ClassicAppMode,
|
||||
babase.EmptyAppMode,
|
||||
]:
|
||||
if appmode.can_handle_intent(intent):
|
||||
return appmode
|
||||
|
||||
return None
|
||||
|
||||
# __DEFAULT_APP_MODE_SELECTION_END__
|
||||
|
|
|
|||
2
dist/ba_data/python/babase/_appcomponent.py
vendored
2
dist/ba_data/python/babase/_appcomponent.py
vendored
|
|
@ -16,8 +16,6 @@ T = TypeVar('T', bound=type)
|
|||
class AppComponentSubsystem:
|
||||
"""Subsystem for wrangling AppComponents.
|
||||
|
||||
Category: **App Classes**
|
||||
|
||||
This subsystem acts as a registry for classes providing particular
|
||||
functionality for the app, and allows plugins or other custom code
|
||||
to easily override said functionality.
|
||||
|
|
|
|||
70
dist/ba_data/python/babase/_appconfig.py
vendored
70
dist/ba_data/python/babase/_appconfig.py
vendored
|
|
@ -14,41 +14,42 @@ _g_pending_apply = False # pylint: disable=invalid-name
|
|||
|
||||
|
||||
class AppConfig(dict):
|
||||
"""A special dict that holds the game's persistent configuration values.
|
||||
"""A special dict that holds persistent app configuration values.
|
||||
|
||||
Category: **App Classes**
|
||||
It also provides methods for fetching values with app-defined
|
||||
fallback defaults, applying contained values to the game, and
|
||||
committing the config to storage.
|
||||
|
||||
It also provides methods for fetching values with app-defined fallback
|
||||
defaults, applying contained values to the game, and committing the
|
||||
config to storage.
|
||||
Access the single shared instance of this config via the
|
||||
:attr:`~babase.App.config` attr on the :class:`~babase.App` class.
|
||||
|
||||
Call babase.appconfig() to get the single shared instance of this class.
|
||||
|
||||
AppConfig data is stored as json on disk on so make sure to only place
|
||||
json-friendly values in it (dict, list, str, float, int, bool).
|
||||
Be aware that tuples will be quietly converted to lists when stored.
|
||||
App-config data is stored as json on disk on so make sure to only
|
||||
place json-friendly values in it (``dict``, ``list``, ``str``,
|
||||
``float``, ``int``, ``bool``). Be aware that tuples will be quietly
|
||||
converted to lists when stored.
|
||||
"""
|
||||
|
||||
def resolve(self, key: str) -> Any:
|
||||
"""Given a string key, return a config value (type varies).
|
||||
|
||||
This will substitute application defaults for values not present in
|
||||
the config dict, filter some invalid values, etc. Note that these
|
||||
values do not represent the state of the app; simply the state of its
|
||||
config. Use babase.App to access actual live state.
|
||||
This will substitute application defaults for values not present
|
||||
in the config dict, filter some invalid values, etc. Note that
|
||||
these values do not represent the state of the app; simply the
|
||||
state of its config. Use the :class:`~babase.App` class to
|
||||
access actual live state.
|
||||
|
||||
Raises an Exception for unrecognized key names. To get the list of keys
|
||||
supported by this method, use babase.AppConfig.builtin_keys(). Note
|
||||
that it is perfectly legal to store other data in the config; it just
|
||||
needs to be accessed through standard dict methods and missing values
|
||||
handled manually.
|
||||
Raises an :class:`KeyError` for unrecognized key names. To get
|
||||
the list of keys supported by this method, use
|
||||
:meth:`builtin_keys()`. Note that it is perfectly legal to store
|
||||
other data in the config; it just needs to be accessed through
|
||||
standard dict methods and missing values handled manually.
|
||||
"""
|
||||
return _babase.resolve_appconfig_value(key)
|
||||
|
||||
def default_value(self, key: str) -> Any:
|
||||
"""Given a string key, return its predefined default value.
|
||||
|
||||
This is the value that will be returned by babase.AppConfig.resolve()
|
||||
This is the value that will be returned by :meth:`resolve()`
|
||||
if the key is not present in the config dict or of an incompatible
|
||||
type.
|
||||
|
||||
|
|
@ -61,17 +62,19 @@ class AppConfig(dict):
|
|||
return _babase.get_appconfig_default_value(key)
|
||||
|
||||
def builtin_keys(self) -> list[str]:
|
||||
"""Return the list of valid key names recognized by babase.AppConfig.
|
||||
"""Return the list of valid key names recognized by this class.
|
||||
|
||||
This set of keys can be used with resolve(), default_value(), etc.
|
||||
It does not vary across platforms and may include keys that are
|
||||
obsolete or not relevant on the current running version. (for instance,
|
||||
VR related keys on non-VR platforms). This is to minimize the amount
|
||||
of platform checking necessary)
|
||||
This set of keys can be used with :meth:`resolve()`,
|
||||
:meth:`default_value()`, etc. It does not vary across platforms
|
||||
and may include keys that are obsolete or not relevant on the
|
||||
current running version. (for instance, VR related keys on
|
||||
non-VR platforms). This is to minimize the amount of platform
|
||||
checking necessary)
|
||||
|
||||
Note that it is perfectly legal to store arbitrary named data in the
|
||||
config, but in that case it is up to the user to test for the existence
|
||||
of the key in the config dict, fall back to consistent defaults, etc.
|
||||
Note that it is perfectly legal to store arbitrary named data in
|
||||
the config, but in that case it is up to the user to test for
|
||||
the existence of the key in the config dict, fall back to
|
||||
consistent defaults, etc.
|
||||
"""
|
||||
return _babase.get_appconfig_builtin_keys()
|
||||
|
||||
|
|
@ -92,9 +95,10 @@ class AppConfig(dict):
|
|||
commit_app_config()
|
||||
|
||||
def apply_and_commit(self) -> None:
|
||||
"""Run apply() followed by commit(); for convenience.
|
||||
"""Shortcut to run :meth:`apply()` followed by :meth:`commit()`.
|
||||
|
||||
(This way the commit() will not occur if apply() hits invalid data)
|
||||
This way the :meth:`commit()` will not occur if :meth:`apply()`
|
||||
hits invalid data, which is generally desirable.
|
||||
"""
|
||||
self.apply()
|
||||
self.commit()
|
||||
|
|
@ -103,9 +107,7 @@ class AppConfig(dict):
|
|||
def commit_app_config() -> None:
|
||||
"""Commit the config to persistent storage.
|
||||
|
||||
Category: **General Utility Functions**
|
||||
|
||||
(internal)
|
||||
:meta private:
|
||||
"""
|
||||
# FIXME - this should not require plus.
|
||||
plus = _babase.app.plus
|
||||
|
|
|
|||
5
dist/ba_data/python/babase/_appintent.py
vendored
5
dist/ba_data/python/babase/_appintent.py
vendored
|
|
@ -10,10 +10,7 @@ if TYPE_CHECKING:
|
|||
|
||||
|
||||
class AppIntent:
|
||||
"""A high level directive given to the app.
|
||||
|
||||
Category: **App Classes**
|
||||
"""
|
||||
"""Base class for high level directives given to the app."""
|
||||
|
||||
|
||||
class AppIntentDefault(AppIntent):
|
||||
|
|
|
|||
131
dist/ba_data/python/babase/_appmode.py
vendored
131
dist/ba_data/python/babase/_appmode.py
vendored
|
|
@ -3,18 +3,19 @@
|
|||
"""Provides AppMode functionality."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, final
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bacommon.app import AppExperience
|
||||
from babase._appintent import AppIntent
|
||||
from babase import AppIntent
|
||||
|
||||
|
||||
class AppMode:
|
||||
"""A high level mode for the app.
|
||||
|
||||
Category: **App Classes**
|
||||
"""A low level mode the app can be in.
|
||||
|
||||
App-modes fundamentally change app behavior related to input
|
||||
handling, networking, graphics, and more. In a way, different
|
||||
app-modes can almost be considered different apps.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
|
|
@ -22,26 +23,27 @@ class AppMode:
|
|||
"""Return the overall experience provided by this mode."""
|
||||
raise NotImplementedError('AppMode subclasses must override this.')
|
||||
|
||||
@final
|
||||
@classmethod
|
||||
def can_handle_intent(cls, intent: AppIntent) -> bool:
|
||||
"""Return whether this mode can handle the provided intent.
|
||||
|
||||
For this to return True, the AppMode must claim to support the
|
||||
provided intent (via its _can_handle_intent() method) AND the
|
||||
AppExperience associated with the AppMode must be supported by
|
||||
the current app and runtime environment.
|
||||
For this to return True, the app-mode must claim to support the
|
||||
provided intent (via its :meth:`can_handle_intent_impl()`
|
||||
method) *AND* the :class:`~bacommon.app.AppExperience` associated
|
||||
with the app-mode must be supported by the current app and
|
||||
runtime environment.
|
||||
"""
|
||||
# TODO: check AppExperience against current environment.
|
||||
return cls._can_handle_intent(intent)
|
||||
return cls.can_handle_intent_impl(intent)
|
||||
|
||||
@classmethod
|
||||
def _can_handle_intent(cls, intent: AppIntent) -> bool:
|
||||
"""Return whether our mode can handle the provided intent.
|
||||
def can_handle_intent_impl(cls, intent: AppIntent) -> bool:
|
||||
"""Override this to define indent handling for an app-mode.
|
||||
|
||||
AppModes should override this to communicate what they can
|
||||
handle. Note that AppExperience does not have to be considered
|
||||
here; that is handled automatically by the can_handle_intent()
|
||||
call.
|
||||
Note that :class:`~bacommon.app.AppExperience` does not have to
|
||||
be considered here; that is handled automatically by the
|
||||
:meth:`can_handle_intent()` call.
|
||||
"""
|
||||
raise NotImplementedError('AppMode subclasses must override this.')
|
||||
|
||||
|
|
@ -50,14 +52,101 @@ class AppMode:
|
|||
raise NotImplementedError('AppMode subclasses must override this.')
|
||||
|
||||
def on_activate(self) -> None:
|
||||
"""Called when the mode is being activated."""
|
||||
"""Called when the mode is becoming the active one fro the app."""
|
||||
|
||||
def on_deactivate(self) -> None:
|
||||
"""Called when the mode is being deactivated."""
|
||||
"""Called when the mode stops being the active one for the app.
|
||||
|
||||
On platforms where the app is explicitly exited (such as desktop
|
||||
PC) this will also be called at app shutdown.
|
||||
|
||||
To best cover both mobile and desktop style platforms, actions
|
||||
such as saving state should generally happen in response to both
|
||||
:meth:`on_deactivate()` and :meth:`on_app_active_changed()`
|
||||
(when active is False).
|
||||
"""
|
||||
|
||||
def on_app_active_changed(self) -> None:
|
||||
"""Called when ba*.app.active changes while this mode is active.
|
||||
"""Called when the app's active state changes while in this app-mode.
|
||||
|
||||
The app-mode may want to take action such as pausing a running
|
||||
game in such cases.
|
||||
This corresponds to the app's :attr:`~babase.App.active` attr.
|
||||
App-active state becomes false when the app is hidden,
|
||||
minimized, backgrounded, etc. The app-mode may want to take
|
||||
action such as pausing a running game or saving state when this
|
||||
occurs.
|
||||
|
||||
On platforms such as mobile where apps get suspended and later
|
||||
silently terminated by the OS, this is likely to be the last
|
||||
reliable place to save state/etc.
|
||||
|
||||
To best cover both mobile and desktop style platforms, actions
|
||||
such as saving state should generally happen in response to both
|
||||
:meth:`on_deactivate()` and :meth:`on_app_active_changed()`
|
||||
(when active is False).
|
||||
"""
|
||||
|
||||
def on_purchase_process_begin(
|
||||
self, item_id: str, user_initiated: bool
|
||||
) -> None:
|
||||
"""Called when in-app-purchase processing is beginning.
|
||||
|
||||
This call happens after a purchase has been completed locally
|
||||
but before its receipt/info is sent to the master-server to
|
||||
apply to the account.
|
||||
|
||||
:meta private:
|
||||
"""
|
||||
# pylint: disable=cyclic-import
|
||||
import babase
|
||||
|
||||
del item_id # Unused.
|
||||
|
||||
# Show nothing for stuff not directly kicked off by the user.
|
||||
if not user_initiated:
|
||||
return
|
||||
|
||||
babase.screenmessage(
|
||||
babase.Lstr(resource='updatingAccountText'),
|
||||
color=(0, 1, 0),
|
||||
)
|
||||
# Ick; we can be called early in the bootstrapping process
|
||||
# before we're allowed to load assets. Guard against that.
|
||||
if babase.asset_loads_allowed():
|
||||
babase.getsimplesound('click01').play()
|
||||
|
||||
def on_purchase_process_end(
|
||||
self, item_id: str, user_initiated: bool, applied: bool
|
||||
) -> None:
|
||||
"""Called when in-app-purchase processing completes.
|
||||
|
||||
Each call to :meth:`on_purchase_process_begin()` will be
|
||||
followed up by a call to this method. If the purchase was found
|
||||
to be valid and was applied to the account, applied will be
|
||||
True. In the case of redundant or invalid purchases or
|
||||
communication failures it will be False.
|
||||
|
||||
:meta private:
|
||||
"""
|
||||
# pylint: disable=cyclic-import
|
||||
import babase
|
||||
|
||||
# Ignore this; we want to announce newly applied stuff even if
|
||||
# it was from a different launch or client or whatever.
|
||||
del user_initiated
|
||||
|
||||
# If the purchase wasn't applied, do nothing. This likely means it
|
||||
# was redundant or something else harmless.
|
||||
if not applied:
|
||||
return
|
||||
|
||||
# By default just announce the item id we got. Real app-modes
|
||||
# probably want to do something more specific based on item-id.
|
||||
babase.screenmessage(
|
||||
babase.Lstr(
|
||||
translate=('serverResponses', 'You got a ${ITEM}!'),
|
||||
subs=[('${ITEM}', item_id)],
|
||||
),
|
||||
color=(0, 1, 0),
|
||||
)
|
||||
if babase.asset_loads_allowed():
|
||||
babase.getsimplesound('cashRegister').play()
|
||||
|
|
|
|||
21
dist/ba_data/python/babase/_appmodeselector.py
vendored
21
dist/ba_data/python/babase/_appmodeselector.py
vendored
|
|
@ -6,25 +6,24 @@ from __future__ import annotations
|
|||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from babase._appintent import AppIntent
|
||||
from babase._appmode import AppMode
|
||||
from babase import AppMode, AppIntent
|
||||
|
||||
|
||||
class AppModeSelector:
|
||||
"""Defines which AppModes are available or used to handle given AppIntents.
|
||||
"""Defines which app-modes should handle which app-intents.
|
||||
|
||||
Category: **App Classes**
|
||||
|
||||
The app calls an instance of this class when passed an AppIntent to
|
||||
determine which AppMode to use to handle the intent. Plugins or
|
||||
spinoff projects can modify high level app behavior by replacing or
|
||||
modifying the app's mode-selector.
|
||||
The app calls an instance of this class when passed an
|
||||
:class:`~babase.AppIntent` to determine which
|
||||
:class:`~babase.AppMode` to use to handle it. Plugins or spinoff
|
||||
projects can modify high level app behavior by replacing or
|
||||
modifying the app's :attr:`~babase.App.mode_selector` attr or by
|
||||
modifying settings used to construct the default one.
|
||||
"""
|
||||
|
||||
def app_mode_for_intent(self, intent: AppIntent) -> type[AppMode] | None:
|
||||
"""Given an AppIntent, return the AppMode that should handle it.
|
||||
"""Given an app-intent, return the app-mode that should handle it.
|
||||
|
||||
If None is returned, the AppIntent will be ignored.
|
||||
If None is returned, the intent will be ignored.
|
||||
|
||||
This may be called in a background thread, so avoid any calls
|
||||
limited to logic thread use/etc.
|
||||
|
|
|
|||
16
dist/ba_data/python/babase/_appsubsystem.py
vendored
16
dist/ba_data/python/babase/_appsubsystem.py
vendored
|
|
@ -14,8 +14,6 @@ if TYPE_CHECKING:
|
|||
class AppSubsystem:
|
||||
"""Base class for an app subsystem.
|
||||
|
||||
Category: **App Classes**
|
||||
|
||||
An app 'subsystem' is a bit of a vague term, as pieces of the app
|
||||
can technically be any class and are not required to use this, but
|
||||
building one out of this base class provides conveniences such as
|
||||
|
|
@ -37,19 +35,19 @@ class AppSubsystem:
|
|||
"""
|
||||
|
||||
def on_app_running(self) -> None:
|
||||
"""Called when the app reaches the running state."""
|
||||
"""Called when app enters :attr:`~AppState.RUNNING` state."""
|
||||
|
||||
def on_app_suspend(self) -> None:
|
||||
"""Called when the app enters the suspended state."""
|
||||
"""Called when app enters :attr:`~AppState.SUSPENDED` state."""
|
||||
|
||||
def on_app_unsuspend(self) -> None:
|
||||
"""Called when the app exits the suspended state."""
|
||||
"""Called when app exits :attr:`~AppState.SUSPENDED` state."""
|
||||
|
||||
def on_app_shutdown(self) -> None:
|
||||
"""Called when the app begins shutting down."""
|
||||
"""Called when app enters :attr:`~AppState.SHUTTING_DOWN` state."""
|
||||
|
||||
def on_app_shutdown_complete(self) -> None:
|
||||
"""Called when the app completes shutting down."""
|
||||
"""Called when app enters :attr:`~AppState.SHUTDOWN_COMPLETE` state."""
|
||||
|
||||
def do_apply_app_config(self) -> None:
|
||||
"""Called when the app config should be applied."""
|
||||
|
|
@ -69,6 +67,6 @@ class AppSubsystem:
|
|||
def reset(self) -> None:
|
||||
"""Reset the subsystem to a default state.
|
||||
|
||||
This is called when switching app modes, but may be called
|
||||
at other times too.
|
||||
This is called when switching app modes, but may be called at
|
||||
other times too.
|
||||
"""
|
||||
|
|
|
|||
91
dist/ba_data/python/babase/_apputils.py
vendored
91
dist/ba_data/python/babase/_apputils.py
vendored
|
|
@ -5,7 +5,6 @@ from __future__ import annotations
|
|||
|
||||
import gc
|
||||
import os
|
||||
import logging
|
||||
from threading import Thread
|
||||
from functools import partial
|
||||
from dataclasses import dataclass
|
||||
|
|
@ -17,6 +16,7 @@ from efro.dataclassio import ioprepped, dataclass_to_json, dataclass_from_json
|
|||
|
||||
import _babase
|
||||
from babase._appsubsystem import AppSubsystem
|
||||
from babase._logging import balog
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import datetime
|
||||
|
|
@ -39,17 +39,16 @@ def utc_now_cloud() -> datetime.datetime:
|
|||
def is_browser_likely_available() -> bool:
|
||||
"""Return whether a browser likely exists on the current device.
|
||||
|
||||
category: General Utility Functions
|
||||
|
||||
If this returns False you may want to avoid calling babase.open_url()
|
||||
with any lengthy addresses. (babase.open_url() will display an address
|
||||
as a string in a window if unable to bring up a browser, but that
|
||||
is only useful for simple URLs.)
|
||||
If this returns False, you may want to avoid calling
|
||||
:meth:`~babase.open_url()` with any lengthy addresses.
|
||||
(:meth:`~babase.open_url()` will display an address as a
|
||||
string/qr-code in a window if unable to bring up a browser, but that
|
||||
is only reasonable for small-ish URLs.)
|
||||
"""
|
||||
app = _babase.app
|
||||
|
||||
if app.classic is None:
|
||||
logging.warning(
|
||||
balog.warning(
|
||||
'is_browser_likely_available() needs to be updated'
|
||||
' to work without classic.'
|
||||
)
|
||||
|
|
@ -70,14 +69,14 @@ def is_browser_likely_available() -> bool:
|
|||
|
||||
|
||||
def get_remote_app_name() -> babase.Lstr:
|
||||
"""(internal)"""
|
||||
""":meta private:"""
|
||||
from babase import _language
|
||||
|
||||
return _language.Lstr(resource='remote_app.app_name')
|
||||
|
||||
|
||||
def should_submit_debug_info() -> bool:
|
||||
"""(internal)"""
|
||||
""":meta private:"""
|
||||
val = _babase.app.config.get('Submit Debug Info', True)
|
||||
assert isinstance(val, bool)
|
||||
return val
|
||||
|
|
@ -96,7 +95,7 @@ def handle_v1_cloud_log() -> None:
|
|||
|
||||
if classic is None or plus is None:
|
||||
if _babase.do_once():
|
||||
logging.warning(
|
||||
balog.warning(
|
||||
'handle_v1_cloud_log should not be getting called'
|
||||
' without classic and plus present.'
|
||||
)
|
||||
|
|
@ -133,9 +132,8 @@ def handle_v1_cloud_log() -> None:
|
|||
|
||||
def response(data: Any) -> None:
|
||||
assert classic is not None
|
||||
# A non-None response means success; lets
|
||||
# take note that we don't need to report further
|
||||
# log info this run
|
||||
# A non-None response means success; lets take note that
|
||||
# we don't need to report further log info this run
|
||||
if data is not None:
|
||||
classic.log_have_new = False
|
||||
_babase.mark_log_sent()
|
||||
|
|
@ -144,8 +142,8 @@ def handle_v1_cloud_log() -> None:
|
|||
|
||||
classic.log_upload_timer_started = True
|
||||
|
||||
# Delay our log upload slightly in case other
|
||||
# pertinent info gets printed between now and then.
|
||||
# Delay our log upload slightly in case other pertinent info
|
||||
# gets printed between now and then.
|
||||
with _babase.ContextRef.empty():
|
||||
_babase.apptimer(3.0, _put_log)
|
||||
|
||||
|
|
@ -162,7 +160,10 @@ def handle_v1_cloud_log() -> None:
|
|||
|
||||
|
||||
def handle_leftover_v1_cloud_log_file() -> None:
|
||||
"""Handle an un-uploaded v1-cloud-log from a previous run."""
|
||||
"""Handle an un-uploaded v1-cloud-log from a previous run.
|
||||
|
||||
:meta private:
|
||||
"""
|
||||
|
||||
# Only applies with classic present.
|
||||
if _babase.app.classic is None:
|
||||
|
|
@ -180,8 +181,8 @@ def handle_leftover_v1_cloud_log_file() -> None:
|
|||
if do_send:
|
||||
|
||||
def response(data: Any) -> None:
|
||||
# Non-None response means we were successful;
|
||||
# lets kill it.
|
||||
# Non-None response means we were successful; lets
|
||||
# kill it.
|
||||
if data is not None:
|
||||
try:
|
||||
os.remove(_babase.get_v1_cloud_log_file_path())
|
||||
|
|
@ -197,10 +198,9 @@ def handle_leftover_v1_cloud_log_file() -> None:
|
|||
else:
|
||||
# If they don't want logs uploaded just kill it.
|
||||
os.remove(_babase.get_v1_cloud_log_file_path())
|
||||
except Exception:
|
||||
from babase import _error
|
||||
|
||||
_error.print_exception('Error handling leftover log file.')
|
||||
except Exception:
|
||||
balog.exception('Error handling leftover log file.')
|
||||
|
||||
|
||||
def garbage_collect_session_end() -> None:
|
||||
|
|
@ -219,6 +219,7 @@ def garbage_collect_session_end() -> None:
|
|||
# running them with an explicit flag passed, but we should never
|
||||
# run them by default because gc.get_objects() can mess up the app.
|
||||
# See notes at top of efro.debug.
|
||||
|
||||
# if bool(False):
|
||||
# print_live_object_warnings('after session shutdown')
|
||||
|
||||
|
|
@ -226,11 +227,11 @@ def garbage_collect_session_end() -> None:
|
|||
def garbage_collect() -> None:
|
||||
"""Run an explicit pass of garbage collection.
|
||||
|
||||
category: General Utility Functions
|
||||
|
||||
May also print warnings/etc. if collection takes too long or if
|
||||
uncollectible objects are found (so use this instead of simply
|
||||
gc.collect().
|
||||
:meth:`gc.collect()`.
|
||||
|
||||
:meta private:
|
||||
"""
|
||||
gc.collect()
|
||||
|
||||
|
|
@ -309,7 +310,7 @@ def dump_app_state(
|
|||
)
|
||||
except Exception:
|
||||
# Abandon whole dump if we can't write metadata.
|
||||
logging.exception('Error writing app state dump metadata.')
|
||||
balog.exception('Error writing app state dump metadata.')
|
||||
return
|
||||
|
||||
tbpath = os.path.join(
|
||||
|
|
@ -383,13 +384,18 @@ def log_dumped_app_state(from_previous_run: bool = False) -> None:
|
|||
with open(tbpath, 'r', encoding='utf-8') as infile:
|
||||
out += '\nPython tracebacks:\n' + infile.read()
|
||||
os.unlink(tbpath)
|
||||
logging.log(metadata.log_level.python_logging_level, out)
|
||||
balog.log(metadata.log_level.python_logging_level, out)
|
||||
except Exception:
|
||||
logging.exception('Error logging dumped app state.')
|
||||
balog.exception('Error logging dumped app state.')
|
||||
|
||||
|
||||
class AppHealthMonitor(AppSubsystem):
|
||||
"""Logs things like app-not-responding issues."""
|
||||
class AppHealthSubsystem(AppSubsystem):
|
||||
"""Subsystem for monitoring app health; logs not-responding issues, etc.
|
||||
|
||||
The single shared instance of this class can be found on the
|
||||
:attr:`~babase.App.health` attr on the :class:`~babase.App`
|
||||
class.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
assert _babase.in_logic_thread()
|
||||
|
|
@ -402,15 +408,28 @@ class AppHealthMonitor(AppSubsystem):
|
|||
|
||||
@override
|
||||
def on_app_loading(self) -> None:
|
||||
""":meta private:"""
|
||||
# If any traceback dumps happened last run, log and clear them.
|
||||
log_dumped_app_state(from_previous_run=True)
|
||||
|
||||
@override
|
||||
def on_app_suspend(self) -> None:
|
||||
""":meta private:"""
|
||||
assert _babase.in_logic_thread()
|
||||
self._running = False
|
||||
|
||||
@override
|
||||
def on_app_unsuspend(self) -> None:
|
||||
""":meta private:"""
|
||||
assert _babase.in_logic_thread()
|
||||
self._running = True
|
||||
|
||||
def _app_monitor_thread_main(self) -> None:
|
||||
_babase.set_thread_name('ballistica app-monitor')
|
||||
try:
|
||||
self._monitor_app()
|
||||
except Exception:
|
||||
logging.exception('Error in AppHealthMonitor thread.')
|
||||
balog.exception('Error in AppHealthSubsystem thread.')
|
||||
|
||||
def _set_response(self) -> None:
|
||||
assert _babase.in_logic_thread()
|
||||
|
|
@ -463,13 +482,3 @@ class AppHealthMonitor(AppSubsystem):
|
|||
time.sleep(1.042)
|
||||
|
||||
self._first_check = False
|
||||
|
||||
@override
|
||||
def on_app_suspend(self) -> None:
|
||||
assert _babase.in_logic_thread()
|
||||
self._running = False
|
||||
|
||||
@override
|
||||
def on_app_unsuspend(self) -> None:
|
||||
assert _babase.in_logic_thread()
|
||||
self._running = True
|
||||
|
|
|
|||
4
dist/ba_data/python/babase/_cloud.py
vendored
4
dist/ba_data/python/babase/_cloud.py
vendored
|
|
@ -14,8 +14,8 @@ if TYPE_CHECKING:
|
|||
class CloudSubscription:
|
||||
"""User handle to a subscription to some cloud data.
|
||||
|
||||
Do not instantiate these directly; use the subscribe methods
|
||||
in *.app.plus.cloud to create them.
|
||||
Do not instantiate these directly; use the subscribe methods in
|
||||
:class:`~baplus.CloudSubsystem` to create them.
|
||||
"""
|
||||
|
||||
def __init__(self, subscription_id: int) -> None:
|
||||
|
|
|
|||
42
dist/ba_data/python/babase/_devconsole.py
vendored
42
dist/ba_data/python/babase/_devconsole.py
vendored
|
|
@ -15,10 +15,13 @@ if TYPE_CHECKING:
|
|||
|
||||
|
||||
class DevConsoleTab:
|
||||
"""Defines behavior for a tab in the dev-console."""
|
||||
"""Base class for a :class:`~babase.DevConsoleSubsystem` tab."""
|
||||
|
||||
def refresh(self) -> None:
|
||||
"""Called when the tab should refresh itself."""
|
||||
"""Called when the tab should refresh itself.
|
||||
|
||||
Overridden by subclasses to implement tab behavior.
|
||||
"""
|
||||
|
||||
def request_refresh(self) -> None:
|
||||
"""The tab can call this to request that it be refreshed."""
|
||||
|
|
@ -91,22 +94,23 @@ class DevConsoleTab:
|
|||
|
||||
@property
|
||||
def width(self) -> float:
|
||||
"""Return the current tab width. Only call during refreshes."""
|
||||
"""The current tab width. Only valid during refreshes."""
|
||||
assert _babase.app.devconsole.is_refreshing
|
||||
return _babase.dev_console_tab_width()
|
||||
|
||||
@property
|
||||
def height(self) -> float:
|
||||
"""Return the current tab height. Only call during refreshes."""
|
||||
"""The current tab height. Only valid during refreshes."""
|
||||
assert _babase.app.devconsole.is_refreshing
|
||||
return _babase.dev_console_tab_height()
|
||||
|
||||
@property
|
||||
def base_scale(self) -> float:
|
||||
"""A scale value set depending on the app's UI scale.
|
||||
"""A scale value based on the app's current :class:`~babase.UIScale`.
|
||||
|
||||
Dev-console tabs can incorporate this into their UI sizes and
|
||||
positions if they desire. This must be done manually however.
|
||||
Dev-console tabs can manually incorporate this into their UI
|
||||
sizes and positions if they desire. By default, dev-console tabs
|
||||
are uniform across all ui-scales.
|
||||
"""
|
||||
assert _babase.app.devconsole.is_refreshing
|
||||
return _babase.dev_console_base_scale()
|
||||
|
|
@ -114,20 +118,21 @@ class DevConsoleTab:
|
|||
|
||||
@dataclass
|
||||
class DevConsoleTabEntry:
|
||||
"""Represents a distinct tab in the dev-console."""
|
||||
"""Represents a distinct tab in the :class:`~babase.DevConsoleSubsystem`."""
|
||||
|
||||
name: str
|
||||
factory: Callable[[], DevConsoleTab]
|
||||
|
||||
|
||||
class DevConsoleSubsystem:
|
||||
"""Subsystem for wrangling the dev console.
|
||||
"""Subsystem for wrangling the dev-console.
|
||||
|
||||
The single instance of this class can be found at
|
||||
babase.app.devconsole. The dev-console is a simple always-available
|
||||
UI intended for use by developers; not end users. Traditionally it
|
||||
is available by typing a backtick (`) key on a keyboard, but now can
|
||||
be accessed via an on-screen button (see settings/advanced to enable
|
||||
Access the single shared instance of this class via the
|
||||
:attr:`~babase.App.devconsole` attr on the :class:`~babase.App`
|
||||
class. The dev-console is a simple always-available UI intended for
|
||||
use by developers; not end users. Traditionally it is available by
|
||||
typing a backtick (`) key on a keyboard, but can also be accessed
|
||||
via an on-screen button (see settings/advanced/dev-tools to enable
|
||||
said button).
|
||||
"""
|
||||
|
||||
|
|
@ -141,8 +146,8 @@ class DevConsoleSubsystem:
|
|||
DevConsoleTabTest,
|
||||
)
|
||||
|
||||
# All tabs in the dev-console. Add your own stuff here via
|
||||
# plugins or whatnot.
|
||||
#: All tabs in the dev-console. Add your own stuff here via
|
||||
#: plugins or whatnot to customize the console.
|
||||
self.tabs: list[DevConsoleTabEntry] = [
|
||||
DevConsoleTabEntry('Python', DevConsoleTabPython),
|
||||
DevConsoleTabEntry('AppModes', DevConsoleTabAppModes),
|
||||
|
|
@ -155,7 +160,10 @@ class DevConsoleSubsystem:
|
|||
self._tab_instances: dict[str, DevConsoleTab] = {}
|
||||
|
||||
def do_refresh_tab(self, tabname: str) -> None:
|
||||
"""Called by the C++ layer when a tab should be filled out."""
|
||||
"""Called by the C++ layer when a tab should be filled out.
|
||||
|
||||
:meta private:
|
||||
"""
|
||||
assert _babase.in_logic_thread()
|
||||
|
||||
# Make noise if we have repeating tab names, as that breaks our
|
||||
|
|
|
|||
7
dist/ba_data/python/babase/_emptyappmode.py
vendored
7
dist/ba_data/python/babase/_emptyappmode.py
vendored
|
|
@ -17,7 +17,10 @@ if TYPE_CHECKING:
|
|||
|
||||
# ba_meta export babase.AppMode
|
||||
class EmptyAppMode(AppMode):
|
||||
"""An AppMode that does not do much at all."""
|
||||
"""An AppMode that does not do much at all.
|
||||
|
||||
:meta private:
|
||||
"""
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
|
|
@ -26,7 +29,7 @@ class EmptyAppMode(AppMode):
|
|||
|
||||
@override
|
||||
@classmethod
|
||||
def _can_handle_intent(cls, intent: AppIntent) -> bool:
|
||||
def can_handle_intent_impl(cls, intent: AppIntent) -> bool:
|
||||
# We support default and exec intents currently.
|
||||
return isinstance(intent, AppIntentExec | AppIntentDefault)
|
||||
|
||||
|
|
|
|||
151
dist/ba_data/python/babase/_error.py
vendored
151
dist/ba_data/python/babase/_error.py
vendored
|
|
@ -6,183 +6,66 @@ from __future__ import annotations
|
|||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import _babase
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any
|
||||
|
||||
|
||||
class ContextError(Exception):
|
||||
"""Exception raised when a call is made in an invalid context.
|
||||
"""Raised when a call is made in an invalid context.
|
||||
|
||||
Category: **Exception Classes**
|
||||
|
||||
Examples of this include calling UI functions within an Activity context
|
||||
or calling scene manipulation functions outside of a game context.
|
||||
Examples of this include calling UI functions within an activity
|
||||
context or calling scene manipulation functions outside of a scene
|
||||
context.
|
||||
"""
|
||||
|
||||
|
||||
class NotFoundError(Exception):
|
||||
"""Exception raised when a referenced object does not exist.
|
||||
|
||||
Category: **Exception Classes**
|
||||
"""
|
||||
"""Raised when a referenced object does not exist."""
|
||||
|
||||
|
||||
class PlayerNotFoundError(NotFoundError):
|
||||
"""Exception raised when an expected player does not exist.
|
||||
|
||||
Category: **Exception Classes**
|
||||
"""
|
||||
"""Raised when an expected player does not exist."""
|
||||
|
||||
|
||||
class SessionPlayerNotFoundError(NotFoundError):
|
||||
"""Exception raised when an expected session-player does not exist.
|
||||
|
||||
Category: **Exception Classes**
|
||||
"""
|
||||
"""Exception raised when an expected session-player does not exist."""
|
||||
|
||||
|
||||
class TeamNotFoundError(NotFoundError):
|
||||
"""Exception raised when an expected bascenev1.Team does not exist.
|
||||
|
||||
Category: **Exception Classes**
|
||||
"""
|
||||
"""Raised when an expected team does not exist."""
|
||||
|
||||
|
||||
class MapNotFoundError(NotFoundError):
|
||||
"""Exception raised when an expected bascenev1.Map does not exist.
|
||||
|
||||
Category: **Exception Classes**
|
||||
"""
|
||||
"""Raised when an expected map does not exist."""
|
||||
|
||||
|
||||
class DelegateNotFoundError(NotFoundError):
|
||||
"""Exception raised when an expected delegate object does not exist.
|
||||
|
||||
Category: **Exception Classes**
|
||||
"""
|
||||
"""Raised when an expected delegate object does not exist."""
|
||||
|
||||
|
||||
class SessionTeamNotFoundError(NotFoundError):
|
||||
"""Exception raised when an expected session-team does not exist.
|
||||
|
||||
Category: **Exception Classes**
|
||||
"""
|
||||
"""Raised when an expected session-team does not exist."""
|
||||
|
||||
|
||||
class NodeNotFoundError(NotFoundError):
|
||||
"""Exception raised when an expected Node does not exist.
|
||||
|
||||
Category: **Exception Classes**
|
||||
"""
|
||||
"""Raised when an expected node does not exist."""
|
||||
|
||||
|
||||
class ActorNotFoundError(NotFoundError):
|
||||
"""Exception raised when an expected actor does not exist.
|
||||
|
||||
Category: **Exception Classes**
|
||||
"""
|
||||
"""Raised when an expected actor does not exist."""
|
||||
|
||||
|
||||
class ActivityNotFoundError(NotFoundError):
|
||||
"""Exception raised when an expected bascenev1.Activity does not exist.
|
||||
|
||||
Category: **Exception Classes**
|
||||
"""
|
||||
"""Raised when an expected activity does not exist."""
|
||||
|
||||
|
||||
class SessionNotFoundError(NotFoundError):
|
||||
"""Exception raised when an expected session does not exist.
|
||||
|
||||
Category: **Exception Classes**
|
||||
"""
|
||||
"""Raised when an expected session does not exist."""
|
||||
|
||||
|
||||
class InputDeviceNotFoundError(NotFoundError):
|
||||
"""Exception raised when an expected input-device does not exist.
|
||||
|
||||
Category: **Exception Classes**
|
||||
"""
|
||||
"""Raised when an expected input-device does not exist."""
|
||||
|
||||
|
||||
class WidgetNotFoundError(NotFoundError):
|
||||
"""Exception raised when an expected widget does not exist.
|
||||
|
||||
Category: **Exception Classes**
|
||||
"""
|
||||
|
||||
|
||||
# TODO: Should integrate some sort of context printing into our
|
||||
# log handling so we can just use logging.exception() and kill these
|
||||
# two functions.
|
||||
|
||||
|
||||
def print_exception(*args: Any, **keywds: Any) -> None:
|
||||
"""Print info about an exception along with pertinent context state.
|
||||
|
||||
Category: **General Utility Functions**
|
||||
|
||||
Prints all arguments provided along with various info about the
|
||||
current context and the outstanding exception.
|
||||
Pass the keyword 'once' as True if you want the call to only happen
|
||||
one time from an exact calling location.
|
||||
"""
|
||||
import traceback
|
||||
|
||||
if keywds:
|
||||
allowed_keywds = ['once']
|
||||
if any(keywd not in allowed_keywds for keywd in keywds):
|
||||
raise TypeError('invalid keyword(s)')
|
||||
try:
|
||||
# If we're only printing once and already have, bail.
|
||||
if keywds.get('once', False):
|
||||
if not _babase.do_once():
|
||||
return
|
||||
|
||||
err_str = ' '.join([str(a) for a in args])
|
||||
print('ERROR:', err_str)
|
||||
_babase.print_context()
|
||||
print('PRINTED-FROM:')
|
||||
|
||||
# Basically the output of traceback.print_stack()
|
||||
stackstr = ''.join(traceback.format_stack())
|
||||
print(stackstr, end='')
|
||||
print('EXCEPTION:')
|
||||
|
||||
# Basically the output of traceback.print_exc()
|
||||
excstr = traceback.format_exc()
|
||||
print('\n'.join(' ' + l for l in excstr.splitlines()))
|
||||
except Exception:
|
||||
# I suppose using print_exception here would be a bad idea.
|
||||
print('ERROR: exception in babase.print_exception():')
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
def print_error(err_str: str, once: bool = False) -> None:
|
||||
"""Print info about an error along with pertinent context state.
|
||||
|
||||
Category: **General Utility Functions**
|
||||
|
||||
Prints all positional arguments provided along with various info about the
|
||||
current context.
|
||||
Pass the keyword 'once' as True if you want the call to only happen
|
||||
one time from an exact calling location.
|
||||
"""
|
||||
import traceback
|
||||
|
||||
try:
|
||||
# If we're only printing once and already have, bail.
|
||||
if once:
|
||||
if not _babase.do_once():
|
||||
return
|
||||
|
||||
print('ERROR:', err_str)
|
||||
_babase.print_context()
|
||||
|
||||
# Basically the output of traceback.print_stack()
|
||||
stackstr = ''.join(traceback.format_stack())
|
||||
print(stackstr, end='')
|
||||
except Exception:
|
||||
print('ERROR: exception in babase.print_error():')
|
||||
traceback.print_exc()
|
||||
"""Raised when an expected widget does not exist."""
|
||||
|
|
|
|||
179
dist/ba_data/python/babase/_general.py
vendored
179
dist/ba_data/python/babase/_general.py
vendored
|
|
@ -34,10 +34,7 @@ DisplayTime = NewType('DisplayTime', float)
|
|||
|
||||
|
||||
class Existable(Protocol):
|
||||
"""A Protocol for objects supporting an exists() method.
|
||||
|
||||
Category: **Protocols**
|
||||
"""
|
||||
"""A Protocol for objects supporting an exists() method."""
|
||||
|
||||
def exists(self) -> bool:
|
||||
"""Whether this object exists."""
|
||||
|
|
@ -50,15 +47,13 @@ T = TypeVar('T')
|
|||
def existing(obj: ExistableT | None) -> ExistableT | None:
|
||||
"""Convert invalid references to None for any babase.Existable object.
|
||||
|
||||
Category: **Gameplay Functions**
|
||||
|
||||
To best support type checking, it is important that invalid references
|
||||
not be passed around and instead get converted to values of None.
|
||||
That way the type checker can properly flag attempts to pass possibly-dead
|
||||
objects (FooType | None) into functions expecting only live ones
|
||||
(FooType), etc. This call can be used on any 'existable' object
|
||||
(one with an exists() method) and will convert it to a None value
|
||||
if it does not exist.
|
||||
To best support type checking, it is important that invalid
|
||||
references not be passed around and instead get converted to values
|
||||
of None. That way the type checker can properly flag attempts to
|
||||
pass possibly-dead objects (``FooType | None``) into functions
|
||||
expecting only live ones (``FooType``), etc. This call can be used
|
||||
on any 'existable' object (one with an ``exists()`` method) and will
|
||||
convert it to a ``None`` value if it does not exist.
|
||||
|
||||
For more info, see notes on 'existables' here:
|
||||
https://ballistica.net/wiki/Coding-Style-Guide
|
||||
|
|
@ -70,12 +65,11 @@ def existing(obj: ExistableT | None) -> ExistableT | None:
|
|||
def getclass(
|
||||
name: str, subclassof: type[T], check_sdlib_modulename_clash: bool = False
|
||||
) -> type[T]:
|
||||
"""Given a full class name such as foo.bar.MyClass, return the class.
|
||||
"""Given a full class name such as ``foo.bar.MyClass``, return the class.
|
||||
|
||||
Category: **General Utility Functions**
|
||||
|
||||
The class will be checked to make sure it is a subclass of the provided
|
||||
'subclassof' class, and a TypeError will be raised if not.
|
||||
The class will be checked to make sure it is a subclass of the
|
||||
provided 'subclassof' class, and a :class:`TypeError` will be raised
|
||||
if not.
|
||||
"""
|
||||
import importlib
|
||||
|
||||
|
|
@ -92,68 +86,54 @@ def getclass(
|
|||
return cls
|
||||
|
||||
|
||||
def utf8_all(data: Any) -> Any:
|
||||
"""Convert any unicode data in provided sequence(s) to utf8 bytes."""
|
||||
if isinstance(data, dict):
|
||||
return dict(
|
||||
(utf8_all(key), utf8_all(value))
|
||||
for key, value in list(data.items())
|
||||
)
|
||||
if isinstance(data, list):
|
||||
return [utf8_all(element) for element in data]
|
||||
if isinstance(data, tuple):
|
||||
return tuple(utf8_all(element) for element in data)
|
||||
if isinstance(data, str):
|
||||
return data.encode('utf-8', errors='ignore')
|
||||
return data
|
||||
|
||||
|
||||
def get_type_name(cls: type) -> str:
|
||||
"""Return a full type name including module for a class."""
|
||||
return f'{cls.__module__}.{cls.__name__}'
|
||||
"""Return a fully qualified type name for a class."""
|
||||
return f'{cls.__module__}.{cls.__qualname__}'
|
||||
|
||||
|
||||
class _WeakCall:
|
||||
"""Wrap a callable and arguments into a single callable object.
|
||||
|
||||
Category: **General Utility Classes**
|
||||
When passed a bound method as the callable, the instance portion of
|
||||
it is weak-referenced, meaning the underlying instance is free to
|
||||
die if all other references to it go away. Should this occur,
|
||||
calling the weak-call is simply a no-op.
|
||||
|
||||
When passed a bound method as the callable, the instance portion
|
||||
of it is weak-referenced, meaning the underlying instance is
|
||||
free to die if all other references to it go away. Should this
|
||||
occur, calling the WeakCall is simply a no-op.
|
||||
Think of this as a handy way to tell an object to do something at
|
||||
some point in the future if it happens to still exist.
|
||||
|
||||
Think of this as a handy way to tell an object to do something
|
||||
at some point in the future if it happens to still exist.
|
||||
**EXAMPLE A:** This code will create a ``FooClass`` instance and
|
||||
call its ``bar()`` method 5 seconds later; it will be kept alive
|
||||
even though we overwrite its variable with None because the bound
|
||||
method we pass as a timer callback (``foo.bar``) strong-references
|
||||
it::
|
||||
|
||||
##### Examples
|
||||
**EXAMPLE A:** this code will create a FooClass instance and call its
|
||||
bar() method 5 seconds later; it will be kept alive even though
|
||||
we overwrite its variable with None because the bound method
|
||||
we pass as a timer callback (foo.bar) strong-references it
|
||||
>>> foo = FooClass()
|
||||
... babase.apptimer(5.0, foo.bar)
|
||||
... foo = None
|
||||
foo = FooClass()
|
||||
babase.apptimer(5.0, foo.bar)
|
||||
foo = None
|
||||
|
||||
**EXAMPLE B:** This code will *not* keep our object alive; it will die
|
||||
when we overwrite it with None and the timer will be a no-op when it
|
||||
fires
|
||||
>>> foo = FooClass()
|
||||
... babase.apptimer(5.0, ba.WeakCall(foo.bar))
|
||||
... foo = None
|
||||
**EXAMPLE B:** This code will *not* keep our object alive; it will
|
||||
die when we overwrite it with ``None`` and the timer will be a no-op
|
||||
when it fires::
|
||||
|
||||
**EXAMPLE C:** Wrap a method call with some positional and keyword args:
|
||||
>>> myweakcall = babase.WeakCall(self.dostuff, argval1,
|
||||
... namedarg=argval2)
|
||||
... # Now we have a single callable to run that whole mess.
|
||||
... # The same as calling myobj.dostuff(argval1, namedarg=argval2)
|
||||
... # (provided my_obj still exists; this will do nothing
|
||||
... # otherwise).
|
||||
... myweakcall()
|
||||
foo = FooClass()
|
||||
babase.apptimer(5.0, ba.WeakCall(foo.bar))
|
||||
foo = None
|
||||
|
||||
Note: additional args and keywords you provide to the WeakCall()
|
||||
constructor are stored as regular strong-references; you'll need
|
||||
to wrap them in weakrefs manually if desired.
|
||||
**EXAMPLE C:** Wrap a method call with some positional and keyword
|
||||
args::
|
||||
|
||||
myweakcall = babase.WeakCall(self.dostuff, argval1,
|
||||
namedarg=argval2)
|
||||
|
||||
# Now we have a single callable to run that whole mess.
|
||||
# The same as calling myobj.dostuff(argval1, namedarg=argval2)
|
||||
# (provided my_obj still exists; this will do nothing otherwise).
|
||||
myweakcall()
|
||||
|
||||
Note: additional args and keywords you provide to the weak-call
|
||||
constructor are stored as regular strong-references; you'll need to
|
||||
wrap them in weakrefs manually if desired.
|
||||
"""
|
||||
|
||||
# Optimize performance a bit; we shouldn't need to be super dynamic.
|
||||
|
|
@ -162,11 +142,6 @@ class _WeakCall:
|
|||
_did_invalid_call_warning = False
|
||||
|
||||
def __init__(self, *args: Any, **keywds: Any) -> None:
|
||||
"""Instantiate a WeakCall.
|
||||
|
||||
Pass a callable as the first arg, followed by any number of
|
||||
arguments or keywords.
|
||||
"""
|
||||
if hasattr(args[0], '__func__'):
|
||||
self._call = WeakMethod(args[0])
|
||||
else:
|
||||
|
|
@ -203,37 +178,27 @@ class _WeakCall:
|
|||
class _Call:
|
||||
"""Wraps a callable and arguments into a single callable object.
|
||||
|
||||
Category: **General Utility Classes**
|
||||
|
||||
The callable is strong-referenced so it won't die until this
|
||||
object does.
|
||||
|
||||
WARNING: This is exactly the same as Python's built in functools.partial().
|
||||
Use functools.partial instead of this for new code, as this will probably
|
||||
be deprecated at some point.
|
||||
|
||||
Note that a bound method (ex: ``myobj.dosomething``) contains a reference
|
||||
to ``self`` (``myobj`` in that case), so you will be keeping that object
|
||||
alive too. Use babase.WeakCall if you want to pass a method to a callback
|
||||
without keeping its object alive.
|
||||
|
||||
Example: Wrap a method call with 1 positional and 1 keyword arg::
|
||||
|
||||
mycall = babase.Call(myobj.dostuff, argval, namedarg=argval2)
|
||||
|
||||
# Now we have a single callable to run that whole mess.
|
||||
# ..the same as calling myobj.dostuff(argval, namedarg=argval2)
|
||||
mycall()
|
||||
"""
|
||||
|
||||
# Optimize performance a bit; we shouldn't need to be super dynamic.
|
||||
__slots__ = ['_call', '_args', '_keywds']
|
||||
|
||||
def __init__(self, *args: Any, **keywds: Any):
|
||||
"""Instantiate a Call.
|
||||
|
||||
Pass a callable as the first arg, followed by any number of
|
||||
arguments or keywords.
|
||||
|
||||
##### Example
|
||||
Wrap a method call with 1 positional and 1 keyword arg:
|
||||
>>> mycall = babase.Call(myobj.dostuff, argval, namedarg=argval2)
|
||||
... # Now we have a single callable to run that whole mess.
|
||||
... # ..the same as calling myobj.dostuff(argval, namedarg=argval2)
|
||||
... mycall()
|
||||
"""
|
||||
self._call = args[0]
|
||||
self._args = args[1:]
|
||||
self._keywds = keywds
|
||||
|
|
@ -309,8 +274,6 @@ class WeakMethod:
|
|||
def verify_object_death(obj: object) -> None:
|
||||
"""Warn if an object does not get freed within a short period.
|
||||
|
||||
Category: **General Utility Functions**
|
||||
|
||||
This can be handy to detect and prevent memory/resource leaks.
|
||||
"""
|
||||
|
||||
|
|
@ -351,27 +314,27 @@ def _verify_object_death(wref: weakref.ref) -> None:
|
|||
def storagename(suffix: str | None = None) -> str:
|
||||
"""Generate a unique name for storing class data in shared places.
|
||||
|
||||
Category: **General Utility Functions**
|
||||
|
||||
This consists of a leading underscore, the module path at the
|
||||
call site with dots replaced by underscores, the containing class's
|
||||
This consists of a leading underscore, the module path at the call
|
||||
site with dots replaced by underscores, the containing class's
|
||||
qualified name, and the provided suffix. When storing data in public
|
||||
places such as 'customdata' dicts, this minimizes the chance of
|
||||
collisions with other similarly named classes.
|
||||
|
||||
Note that this will function even if called in the class definition.
|
||||
|
||||
##### Examples
|
||||
Generate a unique name for storage purposes:
|
||||
>>> class MyThingie:
|
||||
... # This will give something like
|
||||
... # '_mymodule_submodule_mythingie_data'.
|
||||
... _STORENAME = babase.storagename('data')
|
||||
...
|
||||
... # Use that name to store some data in the Activity we were
|
||||
... # passed.
|
||||
... def __init__(self, activity):
|
||||
... activity.customdata[self._STORENAME] = {}
|
||||
Example: Generate a unique name for storage purposes::
|
||||
|
||||
class MyThingie:
|
||||
|
||||
# This will give something like
|
||||
# '_mymodule_submodule_mythingie_data'.
|
||||
_STORENAME = babase.storagename('data')
|
||||
|
||||
# Use that name to store some data in the Activity we were
|
||||
# passed.
|
||||
def __init__(self, activity):
|
||||
activity.customdata[self._STORENAME] = {}
|
||||
|
||||
"""
|
||||
frame = inspect.currentframe()
|
||||
if frame is None:
|
||||
|
|
|
|||
181
dist/ba_data/python/babase/_language.py
vendored
181
dist/ba_data/python/babase/_language.py
vendored
|
|
@ -5,12 +5,12 @@ from __future__ import annotations
|
|||
|
||||
import os
|
||||
import json
|
||||
import logging
|
||||
from functools import partial
|
||||
from typing import TYPE_CHECKING, overload, override
|
||||
|
||||
import _babase
|
||||
from babase._appsubsystem import AppSubsystem
|
||||
from babase._logging import applog
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any, Sequence
|
||||
|
|
@ -21,8 +21,6 @@ if TYPE_CHECKING:
|
|||
class LanguageSubsystem(AppSubsystem):
|
||||
"""Language functionality for the app.
|
||||
|
||||
Category: **App Classes**
|
||||
|
||||
Access the single instance of this class at 'babase.app.lang'.
|
||||
"""
|
||||
|
||||
|
|
@ -37,16 +35,16 @@ class LanguageSubsystem(AppSubsystem):
|
|||
|
||||
@property
|
||||
def locale(self) -> str:
|
||||
"""Raw country/language code detected by the game (such as 'en_US').
|
||||
"""Raw country/language code detected by the game (such as "en_US").
|
||||
|
||||
Generally for language-specific code you should look at
|
||||
babase.App.language, which is the language the game is using
|
||||
(which may differ from locale if the user sets a language, etc.)
|
||||
:attr:`language`, which is the language the game is using (which
|
||||
may differ from locale if the user sets a language, etc.)
|
||||
"""
|
||||
env = _babase.env()
|
||||
locale = env.get('locale')
|
||||
if not isinstance(locale, str):
|
||||
logging.warning(
|
||||
applog.warning(
|
||||
'Seem to be running in a dummy env; returning en_US locale.'
|
||||
)
|
||||
locale = 'en_US'
|
||||
|
|
@ -83,18 +81,17 @@ class LanguageSubsystem(AppSubsystem):
|
|||
)
|
||||
names = [n.replace('.json', '').capitalize() for n in names]
|
||||
|
||||
# FIXME: our simple capitalization fails on multi-word names;
|
||||
# should handle this in a better way...
|
||||
# FIXME: our simple capitalization fails on multi-word
|
||||
# names; should handle this in a better way...
|
||||
for i, name in enumerate(names):
|
||||
if name == 'Chinesetraditional':
|
||||
names[i] = 'ChineseTraditional'
|
||||
elif name == 'Piratespeak':
|
||||
names[i] = 'PirateSpeak'
|
||||
except Exception:
|
||||
from babase import _error
|
||||
|
||||
_error.print_exception()
|
||||
applog.exception('Error building available language list.')
|
||||
names = []
|
||||
|
||||
for name in names:
|
||||
if self._can_display_language(name):
|
||||
langs.add(name)
|
||||
|
|
@ -205,7 +202,7 @@ class LanguageSubsystem(AppSubsystem):
|
|||
with open(lmodfile, encoding='utf-8') as infile:
|
||||
lmodvalues = json.loads(infile.read())
|
||||
except Exception:
|
||||
logging.exception("Error importing language '%s'.", language)
|
||||
applog.exception("Error importing language '%s'.", language)
|
||||
_babase.screenmessage(
|
||||
f"Error setting language to '{language}';"
|
||||
f' see log for details.',
|
||||
|
|
@ -275,6 +272,7 @@ class LanguageSubsystem(AppSubsystem):
|
|||
|
||||
@override
|
||||
def do_apply_app_config(self) -> None:
|
||||
""":meta private:"""
|
||||
assert _babase.in_logic_thread()
|
||||
assert isinstance(_babase.app.config, dict)
|
||||
lang = _babase.app.config.get('Lang', self.default_language)
|
||||
|
|
@ -289,7 +287,11 @@ class LanguageSubsystem(AppSubsystem):
|
|||
) -> Any:
|
||||
"""Return a translation resource by name.
|
||||
|
||||
DEPRECATED; use babase.Lstr functionality for these purposes.
|
||||
.. warning::
|
||||
|
||||
Use :class:`~babase.Lstr` instead of this function whenever
|
||||
possible, as it will gracefully handle displaying correctly
|
||||
across multiple clients in multiple languages simultaneously.
|
||||
"""
|
||||
try:
|
||||
# If we have no language set, try and set it to english.
|
||||
|
|
@ -297,7 +299,7 @@ class LanguageSubsystem(AppSubsystem):
|
|||
if self._language_merged is None:
|
||||
try:
|
||||
if _babase.do_once():
|
||||
logging.warning(
|
||||
applog.warning(
|
||||
'get_resource() called before language'
|
||||
' set; falling back to english.'
|
||||
)
|
||||
|
|
@ -305,9 +307,7 @@ class LanguageSubsystem(AppSubsystem):
|
|||
'English', print_change=False, store_to_config=False
|
||||
)
|
||||
except Exception:
|
||||
logging.exception(
|
||||
'Error setting fallback english language.'
|
||||
)
|
||||
applog.exception('Error setting fallback english language.')
|
||||
raise
|
||||
|
||||
# If they provided a fallback_resource value, try the
|
||||
|
|
@ -382,7 +382,11 @@ class LanguageSubsystem(AppSubsystem):
|
|||
) -> str:
|
||||
"""Translate a value (or return the value if no translation available)
|
||||
|
||||
DEPRECATED; use babase.Lstr functionality for these purposes.
|
||||
.. warning::
|
||||
|
||||
Use :class:`~babase.Lstr` instead of this function whenever
|
||||
possible, as it will gracefully handle displaying correctly
|
||||
across multiple clients in multiple languages simultaneously.
|
||||
"""
|
||||
try:
|
||||
translated = self.get_resource('translations')[category][strval]
|
||||
|
|
@ -495,38 +499,75 @@ class LanguageSubsystem(AppSubsystem):
|
|||
class Lstr:
|
||||
"""Used to define strings in a language-independent way.
|
||||
|
||||
Category: **General Utility Classes**
|
||||
|
||||
These should be used whenever possible in place of hard-coded
|
||||
strings so that in-game or UI elements show up correctly on all
|
||||
clients in their currently-active language.
|
||||
clients in their currently active language.
|
||||
|
||||
To see available resource keys, look at any of the bs_language_*.py
|
||||
files in the game or the translations pages at
|
||||
legacy.ballistica.net/translate.
|
||||
To see available resource keys, look at any of the
|
||||
``ba_data/data/languages/*.json`` files in the game or the
|
||||
translations pages at `legacy.ballistica.net/translate
|
||||
<https://legacy.ballistica.net/translate>`_.
|
||||
|
||||
##### Examples
|
||||
EXAMPLE 1: specify a string from a resource path
|
||||
>>> mynode.text = babase.Lstr(resource='audioSettingsWindow.titleText')
|
||||
Args:
|
||||
|
||||
EXAMPLE 2: specify a translated string via a category and english
|
||||
value; if a translated value is available, it will be used; otherwise
|
||||
the english value will be. To see available translation categories,
|
||||
look under the 'translations' resource section.
|
||||
>>> mynode.text = babase.Lstr(translate=('gameDescriptions',
|
||||
... 'Defeat all enemies'))
|
||||
resource:
|
||||
Pass a string to look up a translation by resource key.
|
||||
|
||||
EXAMPLE 3: specify a raw value and some substitutions. Substitutions
|
||||
can be used with resource and translate modes as well.
|
||||
>>> mynode.text = babase.Lstr(value='${A} / ${B}',
|
||||
... subs=[('${A}', str(score)), ('${B}', str(total))])
|
||||
translate:
|
||||
Pass a tuple consisting of a translation category and
|
||||
untranslated value. Any matching translation found in that
|
||||
category will be used. Otherwise the untranslated value will
|
||||
be.
|
||||
|
||||
EXAMPLE 4: babase.Lstr's can be nested. This example would display the
|
||||
resource at res_a but replace ${NAME} with the value of the
|
||||
resource at res_b
|
||||
>>> mytextnode.text = babase.Lstr(
|
||||
... resource='res_a',
|
||||
... subs=[('${NAME}', babase.Lstr(resource='res_b'))])
|
||||
value:
|
||||
Pass a regular string value to be used as-is.
|
||||
|
||||
subs:
|
||||
A sequence of 2-member tuples consisting of values and
|
||||
replacements. Replacements can be regular strings or other ``Lstr``
|
||||
values.
|
||||
|
||||
fallback_resource:
|
||||
A resource key that will be used if the main one is not present for
|
||||
the current language instead of falling back to the english value
|
||||
('resource' mode only).
|
||||
|
||||
fallback_value:
|
||||
A regular string that will be used if neither the resource nor the
|
||||
fallback resource is found ('resource' mode only).
|
||||
|
||||
|
||||
**Example 1: Resource path** ::
|
||||
|
||||
mynode.text = babase.Lstr(resource='audioSettingsWindow.titleText')
|
||||
|
||||
**Example 2: Translation**
|
||||
|
||||
If a translated value is available, it will be used; otherwise the
|
||||
English value will be. To see available translation categories, look
|
||||
under the ``translations`` resource section. ::
|
||||
|
||||
mynode.text = babase.Lstr(translate=('gameDescriptions',
|
||||
'Defeat all enemies'))
|
||||
|
||||
**Example 3: Substitutions**
|
||||
|
||||
Substitutions can be used with ``resource`` and ``translate`` modes
|
||||
as well as the ``value`` shown here. ::
|
||||
|
||||
mynode.text = babase.Lstr(value='${A} / ${B}',
|
||||
subs=[('${A}', str(score)),
|
||||
('${B}', str(total))])
|
||||
|
||||
**Example 4: Nesting**
|
||||
|
||||
``Lstr`` instances can be nested. This example would display
|
||||
the translated resource at ``'res_a'`` but replace any instances of
|
||||
``'${NAME}'`` it contains with the translated resource at ``'res_b'``. ::
|
||||
|
||||
mytextnode.text = babase.Lstr(
|
||||
resource='res_a',
|
||||
subs=[('${NAME}', babase.Lstr(resource='res_b'))])
|
||||
"""
|
||||
|
||||
# This class is used a lot in UI stuff and doesn't need to be
|
||||
|
|
@ -563,26 +604,13 @@ class Lstr:
|
|||
"""Create an Lstr from a raw string value."""
|
||||
|
||||
def __init__(self, *args: Any, **keywds: Any) -> None:
|
||||
"""Instantiate a Lstr.
|
||||
|
||||
Pass a value for either 'resource', 'translate',
|
||||
or 'value'. (see Lstr help for examples).
|
||||
'subs' can be a sequence of 2-member sequences consisting of values
|
||||
and replacements.
|
||||
'fallback_resource' can be a resource key that will be used if the
|
||||
main one is not present for
|
||||
the current language in place of falling back to the english value
|
||||
('resource' mode only).
|
||||
'fallback_value' can be a literal string that will be used if neither
|
||||
the resource nor the fallback resource is found ('resource' mode only).
|
||||
"""
|
||||
# pylint: disable=too-many-branches
|
||||
if args:
|
||||
raise TypeError('Lstr accepts only keyword arguments')
|
||||
|
||||
# Basically just store the exact args they passed.
|
||||
# However if they passed any Lstr values for subs,
|
||||
# replace them with that Lstr's dict.
|
||||
#: Basically just stores the exact args passed. However if Lstr
|
||||
#: values were passed for subs, they are replaced with that
|
||||
#: Lstr's dict.
|
||||
self.args = keywds
|
||||
our_type = type(self)
|
||||
|
||||
|
|
@ -600,8 +628,8 @@ class Lstr:
|
|||
subs_filtered.append((key, value))
|
||||
self.args['subs'] = subs_filtered
|
||||
|
||||
# As of protocol 31 we support compact key names
|
||||
# ('t' instead of 'translate', etc). Convert as needed.
|
||||
# As of protocol 31 we support compact key names ('t' instead of
|
||||
# 'translate', etc). Convert as needed.
|
||||
if 'translate' in keywds:
|
||||
keywds['t'] = keywds['translate']
|
||||
del keywds['translate']
|
||||
|
|
@ -612,13 +640,11 @@ class Lstr:
|
|||
keywds['v'] = keywds['value']
|
||||
del keywds['value']
|
||||
if 'fallback' in keywds:
|
||||
from babase import _error
|
||||
|
||||
_error.print_error(
|
||||
'deprecated "fallback" arg passed to Lstr(); use '
|
||||
'either "fallback_resource" or "fallback_value"',
|
||||
once=True,
|
||||
)
|
||||
if _babase.do_once():
|
||||
applog.error(
|
||||
'Deprecated "fallback" arg passed to Lstr(); use '
|
||||
'either "fallback_resource" or "fallback_value".'
|
||||
)
|
||||
keywds['f'] = keywds['fallback']
|
||||
del keywds['fallback']
|
||||
if 'fallback_resource' in keywds:
|
||||
|
|
@ -632,15 +658,15 @@ class Lstr:
|
|||
del keywds['fallback_value']
|
||||
|
||||
def evaluate(self) -> str:
|
||||
"""Evaluate the Lstr and returns a flat string in the current language.
|
||||
"""Evaluate to a flat string in the current language.
|
||||
|
||||
You should avoid doing this as much as possible and instead pass
|
||||
and store Lstr values.
|
||||
and store ``Lstr`` values.
|
||||
"""
|
||||
return _babase.evaluate_lstr(self._get_json())
|
||||
|
||||
def is_flat_value(self) -> bool:
|
||||
"""Return whether the Lstr is a 'flat' value.
|
||||
"""Return whether this instance represents a 'flat' value.
|
||||
|
||||
This is defined as a simple string value incorporating no
|
||||
translations, resources, or substitutions. In this case it may
|
||||
|
|
@ -655,20 +681,23 @@ class Lstr:
|
|||
except Exception:
|
||||
from babase import _error
|
||||
|
||||
_error.print_exception('_get_json failed for', self.args)
|
||||
applog.exception('_get_json failed for %s.', self.args)
|
||||
return 'JSON_ERR'
|
||||
|
||||
@override
|
||||
def __str__(self) -> str:
|
||||
return '<ba.Lstr: ' + self._get_json() + '>'
|
||||
return f'<ba.Lstr: {self._get_json()}>'
|
||||
|
||||
@override
|
||||
def __repr__(self) -> str:
|
||||
return '<ba.Lstr: ' + self._get_json() + '>'
|
||||
return f'<ba.Lstr: {self._get_json()}>'
|
||||
|
||||
@staticmethod
|
||||
def from_json(json_string: str) -> babase.Lstr:
|
||||
"""Given a json string, returns a babase.Lstr. Does no validation."""
|
||||
"""Given a json string, returns a ``Lstr``.
|
||||
|
||||
Does no validation.
|
||||
"""
|
||||
lstr = Lstr(value='')
|
||||
lstr.args = json.loads(json_string)
|
||||
return lstr
|
||||
|
|
|
|||
34
dist/ba_data/python/babase/_login.py
vendored
34
dist/ba_data/python/babase/_login.py
vendored
|
|
@ -22,7 +22,7 @@ logger = logging.getLogger('ba.loginadapter')
|
|||
|
||||
@dataclass
|
||||
class LoginInfo:
|
||||
"""Basic info about a login available in the app.plus.accounts section."""
|
||||
"""Info for a login used by :class:`~babase.AccountV2Handle`."""
|
||||
|
||||
name: str
|
||||
|
||||
|
|
@ -70,7 +70,10 @@ class LoginAdapter:
|
|||
self._last_sign_in_desc: str | None = None
|
||||
|
||||
def on_app_loading(self) -> None:
|
||||
"""Should be called for each adapter in on_app_loading."""
|
||||
"""Should be called for each adapter in on_app_loading.
|
||||
|
||||
:meta private:
|
||||
"""
|
||||
|
||||
assert not self._on_app_loading_called
|
||||
self._on_app_loading_called = True
|
||||
|
|
@ -122,6 +125,8 @@ class LoginAdapter:
|
|||
to the currently-in-use account.
|
||||
Note that the logins dict passed in should be immutable as
|
||||
only a reference to it is stored, not a copy.
|
||||
|
||||
:meta private:
|
||||
"""
|
||||
assert _babase.in_logic_thread()
|
||||
logger.debug(
|
||||
|
|
@ -136,12 +141,12 @@ class LoginAdapter:
|
|||
def on_back_end_active_change(self, active: bool) -> None:
|
||||
"""Called when active state for the back-end is (possibly) changing.
|
||||
|
||||
Meant to be overridden by subclasses.
|
||||
Being active means that the implicit login provided by the back-end
|
||||
is actually being used by the app. It should therefore register
|
||||
unlocked achievements, leaderboard scores, allow viewing native
|
||||
UIs, etc. When not active it should ignore everything and behave
|
||||
as if signed out, even if it technically is still signed in.
|
||||
Meant to be overridden by subclasses. Being active means that
|
||||
the implicit login provided by the back-end is actually being
|
||||
used by the app. It should therefore register unlocked
|
||||
achievements, leaderboard scores, allow viewing native UIs, etc.
|
||||
When not active it should ignore everything and behave as if
|
||||
signed out, even if it technically is still signed in.
|
||||
"""
|
||||
assert _babase.in_logic_thread()
|
||||
del active # Unused.
|
||||
|
|
@ -156,7 +161,7 @@ class LoginAdapter:
|
|||
|
||||
This can be called even if the back-end is not implicitly signed in;
|
||||
the adapter will attempt to sign in if possible. An exception will
|
||||
be returned if the sign-in attempt fails.
|
||||
be passed to the callback if the sign-in attempt fails.
|
||||
"""
|
||||
|
||||
assert _babase.in_logic_thread()
|
||||
|
|
@ -275,11 +280,12 @@ class LoginAdapter:
|
|||
) -> None:
|
||||
"""Get a sign-in token from the adapter back end.
|
||||
|
||||
This token is then passed to the master-server to complete the
|
||||
sign-in process. The adapter can use this opportunity to bring
|
||||
up account creation UI, call its internal sign_in function, etc.
|
||||
as needed. The provided completion_cb should then be called with
|
||||
either a token or None if sign in failed or was cancelled.
|
||||
This token is then passed to the cloud to complete the sign-in
|
||||
process. The adapter can use this opportunity to bring up
|
||||
account creation UI, call its internal sign-in function, etc. as
|
||||
needed. The provided ``completion_cb`` should then be called
|
||||
with either a token or with ``None`` if sign in failed or was
|
||||
cancelled.
|
||||
"""
|
||||
|
||||
# Default implementation simply fails immediately.
|
||||
|
|
|
|||
22
dist/ba_data/python/babase/_math.py
vendored
22
dist/ba_data/python/babase/_math.py
vendored
|
|
@ -14,14 +14,13 @@ if TYPE_CHECKING:
|
|||
def vec3validate(value: Sequence[float]) -> Sequence[float]:
|
||||
"""Ensure a value is valid for use as a Vec3.
|
||||
|
||||
category: General Utility Functions
|
||||
Raises a TypeError exception if not. Valid values include any type
|
||||
of sequence consisting of 3 numeric values. Returns the same value
|
||||
as passed in (but with a definite type so this can be used to
|
||||
disambiguate 'Any' types). Generally this should be used in 'if
|
||||
__debug__' or assert clauses to keep runtime overhead minimal.
|
||||
|
||||
Raises a TypeError exception if not.
|
||||
Valid values include any type of sequence consisting of 3 numeric values.
|
||||
Returns the same value as passed in (but with a definite type
|
||||
so this can be used to disambiguate 'Any' types).
|
||||
Generally this should be used in 'if __debug__' or assert clauses
|
||||
to keep runtime overhead minimal.
|
||||
:meta private:
|
||||
"""
|
||||
from numbers import Number
|
||||
|
||||
|
|
@ -37,9 +36,9 @@ def vec3validate(value: Sequence[float]) -> Sequence[float]:
|
|||
def is_point_in_box(pnt: Sequence[float], box: Sequence[float]) -> bool:
|
||||
"""Return whether a given point is within a given box.
|
||||
|
||||
category: General Utility Functions
|
||||
|
||||
For use with standard def boxes (position|rotate|scale).
|
||||
|
||||
:meta private:
|
||||
"""
|
||||
return (
|
||||
(abs(pnt[0] - box[0]) <= box[6] * 0.5)
|
||||
|
|
@ -49,10 +48,7 @@ def is_point_in_box(pnt: Sequence[float], box: Sequence[float]) -> bool:
|
|||
|
||||
|
||||
def normalized_color(color: Sequence[float]) -> tuple[float, ...]:
|
||||
"""Scale a color so its largest value is 1; useful for coloring lights.
|
||||
|
||||
category: General Utility Functions
|
||||
"""
|
||||
"""Scale a color so its largest value is 1.0; useful for coloring lights."""
|
||||
color_biased = tuple(max(c, 0.01) for c in color) # account for black
|
||||
mult = 1.0 / max(color_biased)
|
||||
return tuple(c * mult for c in color_biased)
|
||||
|
|
|
|||
15
dist/ba_data/python/babase/_meta.py
vendored
15
dist/ba_data/python/babase/_meta.py
vendored
|
|
@ -48,9 +48,8 @@ class ScanResults:
|
|||
class MetadataSubsystem:
|
||||
"""Subsystem for working with script metadata in the app.
|
||||
|
||||
Category: **App Classes**
|
||||
|
||||
Access the single shared instance of this class at 'babase.app.meta'.
|
||||
Access the single shared instance of this class via the
|
||||
:attr:`~babase.App.meta` attr on the :class:`~babase.App` class.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
|
|
@ -68,8 +67,10 @@ class MetadataSubsystem:
|
|||
"""Begin the overall scan.
|
||||
|
||||
This will start scanning built in directories (which for vanilla
|
||||
installs should be the vast majority of the work). This should only
|
||||
be called once.
|
||||
installs should be the vast majority of the work). This should
|
||||
only be called once.
|
||||
|
||||
:meta private:
|
||||
"""
|
||||
assert self._scan_complete_cb is None
|
||||
assert self._scan is None
|
||||
|
|
@ -95,6 +96,8 @@ class MetadataSubsystem:
|
|||
This is for parts of the scan that must be delayed until
|
||||
workspace sync completion or other such events. This must be
|
||||
called exactly once.
|
||||
|
||||
:meta private:
|
||||
"""
|
||||
assert self._scan is not None
|
||||
self._scan.set_extras(self.extra_scan_dirs)
|
||||
|
|
@ -113,7 +116,7 @@ class MetadataSubsystem:
|
|||
to messaged to the user in some way but the callback will be called
|
||||
regardless.
|
||||
To run the completion callback directly in the bg thread where the
|
||||
loading work happens, pass completion_cb_in_bg_thread=True.
|
||||
loading work happens, pass ``completion_cb_in_bg_thread=True``.
|
||||
"""
|
||||
Thread(
|
||||
target=partial(
|
||||
|
|
|
|||
27
dist/ba_data/python/babase/_mgen/enums.py
vendored
27
dist/ba_data/python/babase/_mgen/enums.py
vendored
|
|
@ -5,11 +5,7 @@ from enum import Enum
|
|||
|
||||
|
||||
class InputType(Enum):
|
||||
"""Types of input a controller can send to the game.
|
||||
|
||||
Category: Enums
|
||||
|
||||
"""
|
||||
"""Types of input a controller can send to the game."""
|
||||
|
||||
UP_DOWN = 2
|
||||
LEFT_RIGHT = 3
|
||||
|
|
@ -39,19 +35,18 @@ class InputType(Enum):
|
|||
|
||||
|
||||
class QuitType(Enum):
|
||||
"""Types of input a controller can send to the game.
|
||||
|
||||
Category: Enums
|
||||
"""Types of quit behavior that can be requested from the app.
|
||||
|
||||
'soft' may hide/reset the app but keep the process running, depending
|
||||
on the platform.
|
||||
on the platform (generally a thing on mobile).
|
||||
|
||||
'back' is a variant of 'soft' which may give 'back-button-pressed'
|
||||
behavior depending on the platform. (returning to some previous
|
||||
activity instead of dumping to the home screen, etc.)
|
||||
|
||||
'hard' leads to the process exiting. This generally should be avoided
|
||||
on platforms such as mobile.
|
||||
on platforms such as mobile where apps are expected to keep running
|
||||
until killed by the OS.
|
||||
"""
|
||||
|
||||
SOFT = 0
|
||||
|
|
@ -65,8 +60,6 @@ class UIScale(Enum):
|
|||
might render the game at similar pixel resolutions but the size they
|
||||
display content at will vary significantly.
|
||||
|
||||
Category: Enums
|
||||
|
||||
'large' is used for devices such as desktop PCs where fine details can
|
||||
be clearly seen. UI elements are generally smaller on the screen
|
||||
and more content can be seen at once.
|
||||
|
|
@ -86,19 +79,13 @@ class UIScale(Enum):
|
|||
|
||||
|
||||
class Permission(Enum):
|
||||
"""Permissions that can be requested from the OS.
|
||||
|
||||
Category: Enums
|
||||
"""
|
||||
"""Permissions that can be requested from the OS."""
|
||||
|
||||
STORAGE = 0
|
||||
|
||||
|
||||
class SpecialChar(Enum):
|
||||
"""Special characters the game can print.
|
||||
|
||||
Category: Enums
|
||||
"""
|
||||
"""Special characters the game can print."""
|
||||
|
||||
DOWN_ARROW = 0
|
||||
UP_ARROW = 1
|
||||
|
|
|
|||
5
dist/ba_data/python/babase/_net.py
vendored
5
dist/ba_data/python/babase/_net.py
vendored
|
|
@ -42,7 +42,10 @@ class NetworkSubsystem:
|
|||
|
||||
|
||||
def get_ip_address_type(addr: str) -> socket.AddressFamily:
|
||||
"""Return socket.AF_INET6 or socket.AF_INET4 for the provided address."""
|
||||
"""Return an address-type given an address.
|
||||
|
||||
Can be :attr:`socket.AF_INET` or :attr:`socket.AF_INET6`.
|
||||
"""
|
||||
|
||||
version = ipaddress.ip_address(addr).version
|
||||
if version == 4:
|
||||
|
|
|
|||
108
dist/ba_data/python/babase/_plugin.py
vendored
108
dist/ba_data/python/babase/_plugin.py
vendored
|
|
@ -10,6 +10,7 @@ from typing import TYPE_CHECKING, override
|
|||
|
||||
import _babase
|
||||
from babase._appsubsystem import AppSubsystem
|
||||
from babase._logging import balog
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any
|
||||
|
|
@ -18,31 +19,36 @@ if TYPE_CHECKING:
|
|||
|
||||
|
||||
class PluginSubsystem(AppSubsystem):
|
||||
"""Subsystem for plugin handling in the app.
|
||||
"""Subsystem for wrangling plugins.
|
||||
|
||||
Category: **App Classes**
|
||||
|
||||
Access the single shared instance of this class at `ba.app.plugins`.
|
||||
Access the single shared instance of this class via the
|
||||
:attr:`~babase.App.plugins` attr on the :class:`~babase.App` class.
|
||||
"""
|
||||
|
||||
#: :meta private:
|
||||
AUTO_ENABLE_NEW_PLUGINS_CONFIG_KEY = 'Auto Enable New Plugins'
|
||||
|
||||
#: :meta private:
|
||||
AUTO_ENABLE_NEW_PLUGINS_DEFAULT = True
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
|
||||
# Info about plugins that we are aware of. This may include
|
||||
# plugins discovered through meta-scanning as well as plugins
|
||||
# registered in the app-config. This may include plugins that
|
||||
# cannot be loaded for various reasons or that have been
|
||||
# intentionally disabled.
|
||||
#: Info about plugins that we are aware of. This may include
|
||||
#: plugins discovered through meta-scanning as well as plugins
|
||||
#: registered in the app-config. This may include plugins that
|
||||
#: cannot be loaded for various reasons or that have been
|
||||
#: intentionally disabled.
|
||||
self.plugin_specs: dict[str, babase.PluginSpec] = {}
|
||||
|
||||
# The set of live active plugin objects.
|
||||
#: The set of live active plugin instances.
|
||||
self.active_plugins: list[babase.Plugin] = []
|
||||
|
||||
def on_meta_scan_complete(self) -> None:
|
||||
"""Called when meta-scanning is complete."""
|
||||
"""Called when meta-scanning is complete.
|
||||
|
||||
:meta private:
|
||||
"""
|
||||
from babase._language import Lstr
|
||||
|
||||
config_changed = False
|
||||
|
|
@ -160,61 +166,53 @@ class PluginSubsystem(AppSubsystem):
|
|||
|
||||
@override
|
||||
def on_app_running(self) -> None:
|
||||
""":meta private:"""
|
||||
# Load up our plugins and go ahead and call their on_app_running
|
||||
# calls.
|
||||
self.load_plugins()
|
||||
self._load_plugins()
|
||||
for plugin in self.active_plugins:
|
||||
try:
|
||||
plugin.on_app_running()
|
||||
except Exception:
|
||||
from babase import _error
|
||||
|
||||
_error.print_exception('Error in plugin on_app_running()')
|
||||
balog.exception('Error in plugin on_app_running().')
|
||||
|
||||
@override
|
||||
def on_app_suspend(self) -> None:
|
||||
""":meta private:"""
|
||||
for plugin in self.active_plugins:
|
||||
try:
|
||||
plugin.on_app_suspend()
|
||||
except Exception:
|
||||
from babase import _error
|
||||
|
||||
_error.print_exception('Error in plugin on_app_suspend()')
|
||||
balog.exception('Error in plugin on_app_suspend().')
|
||||
|
||||
@override
|
||||
def on_app_unsuspend(self) -> None:
|
||||
""":meta private:"""
|
||||
for plugin in self.active_plugins:
|
||||
try:
|
||||
plugin.on_app_unsuspend()
|
||||
except Exception:
|
||||
from babase import _error
|
||||
|
||||
_error.print_exception('Error in plugin on_app_unsuspend()')
|
||||
balog.exception('Error in plugin on_app_unsuspend().')
|
||||
|
||||
@override
|
||||
def on_app_shutdown(self) -> None:
|
||||
""":meta private:"""
|
||||
for plugin in self.active_plugins:
|
||||
try:
|
||||
plugin.on_app_shutdown()
|
||||
except Exception:
|
||||
from babase import _error
|
||||
|
||||
_error.print_exception('Error in plugin on_app_shutdown()')
|
||||
balog.exception('Error in plugin on_app_shutdown().')
|
||||
|
||||
@override
|
||||
def on_app_shutdown_complete(self) -> None:
|
||||
""":meta private:"""
|
||||
for plugin in self.active_plugins:
|
||||
try:
|
||||
plugin.on_app_shutdown_complete()
|
||||
except Exception:
|
||||
from babase import _error
|
||||
balog.exception('Error in plugin on_app_shutdown_complete().')
|
||||
|
||||
_error.print_exception(
|
||||
'Error in plugin on_app_shutdown_complete()'
|
||||
)
|
||||
|
||||
def load_plugins(self) -> None:
|
||||
"""(internal)"""
|
||||
def _load_plugins(self) -> None:
|
||||
|
||||
# Load plugins from any specs that are enabled & able to.
|
||||
for _class_path, plug_spec in sorted(self.plugin_specs.items()):
|
||||
|
|
@ -224,32 +222,36 @@ class PluginSubsystem(AppSubsystem):
|
|||
|
||||
|
||||
class PluginSpec:
|
||||
"""Represents a plugin the engine knows about.
|
||||
|
||||
Category: **App Classes**
|
||||
|
||||
The 'enabled' attr represents whether this plugin is set to load.
|
||||
Getting or setting that attr affects the corresponding app-config
|
||||
key. Remember to commit the app-config after making any changes.
|
||||
|
||||
The 'attempted_load' attr will be True if the engine has attempted
|
||||
to load the plugin. If 'attempted_load' is True for a PluginSpec
|
||||
but the 'plugin' attr is None, it means there was an error loading
|
||||
the plugin. If a plugin's api-version does not match the running
|
||||
app, if a new plugin is detected with auto-enable-plugins disabled,
|
||||
or if the user has explicitly disabled a plugin, the engine will not
|
||||
even attempt to load it.
|
||||
"""
|
||||
"""Represents a plugin the engine knows about."""
|
||||
|
||||
def __init__(self, class_path: str, loadable: bool):
|
||||
|
||||
#: Fully qualified class path for the plugin.
|
||||
self.class_path = class_path
|
||||
|
||||
#: Can we attempt to load the plugin?
|
||||
self.loadable = loadable
|
||||
|
||||
#: Whether the engine has attempted to load the plugin. If this
|
||||
#: is True but the value of :attr:`plugin` is None, it means
|
||||
#: there was an error loading the plugin. If a plugin's
|
||||
#: api-version does not match the running app, if a new plugin is
|
||||
#: detected with auto-enable-plugins disabled, or if the user has
|
||||
#: explicitly disabled a plugin, the engine will not even attempt
|
||||
#: to load it.
|
||||
self.attempted_load = False
|
||||
|
||||
#: The associated :class:`~babase.Plugin`, if any.
|
||||
self.plugin: Plugin | None = None
|
||||
|
||||
@property
|
||||
def enabled(self) -> bool:
|
||||
"""Whether the user wants this plugin to load."""
|
||||
"""Whether this plugin is set to load.
|
||||
|
||||
Getting or setting this attr affects the corresponding
|
||||
app-config key. Remember to commit the app-config after making any
|
||||
changes.
|
||||
"""
|
||||
plugstates: dict[str, dict] = _babase.app.config.get('Plugins', {})
|
||||
assert isinstance(plugstates, dict)
|
||||
val = plugstates.get(self.class_path, {}).get('enabled', False) is True
|
||||
|
|
@ -321,12 +323,10 @@ class PluginSpec:
|
|||
class Plugin:
|
||||
"""A plugin to alter app behavior in some way.
|
||||
|
||||
Category: **App Classes**
|
||||
|
||||
Plugins are discoverable by the meta-tag system
|
||||
and the user can select which ones they want to enable.
|
||||
Enabled plugins are then called at specific times as the
|
||||
app is running in order to modify its behavior in some way.
|
||||
Plugins are discoverable by the :class:`~babase.MetadataSubsystem`
|
||||
system and the user can select which ones they want to enable.
|
||||
Enabled plugins are then called at specific times as the app is
|
||||
running in order to modify its behavior in some way.
|
||||
"""
|
||||
|
||||
def on_app_running(self) -> None:
|
||||
|
|
|
|||
23
dist/ba_data/python/babase/_stringedit.py
vendored
23
dist/ba_data/python/babase/_stringedit.py
vendored
|
|
@ -22,7 +22,12 @@ if TYPE_CHECKING:
|
|||
|
||||
|
||||
class StringEditSubsystem:
|
||||
"""Full string-edit state for the app."""
|
||||
"""Full string-edit state for the app.
|
||||
|
||||
Access the single shared instance of this class via the
|
||||
:attr:`~babase.App.stringedit` attr on the :class:`~babase.App`
|
||||
class.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.active_adapter = empty_weakref(StringEditAdapter)
|
||||
|
|
@ -35,11 +40,11 @@ class StringEditAdapter:
|
|||
subclass this to make their contents editable on all platforms.
|
||||
|
||||
There can only be one string-edit at a time for the app. New
|
||||
StringEdits will attempt to register themselves as the globally
|
||||
active one in their constructor, but this may not succeed. When
|
||||
creating a StringEditAdapter, always check its 'is_valid()' value after
|
||||
creating it. If this is False, it was not able to set itself as
|
||||
the global active one and should be discarded.
|
||||
string-edits will attempt to register themselves as the globally
|
||||
active one in their constructor, but this may not succeed. If
|
||||
:meth:`can_be_replaced()` returns ``True`` for an adapter
|
||||
immediately after creating it, that means it was not able to set
|
||||
itself as the global one.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
|
|
@ -72,8 +77,8 @@ class StringEditAdapter:
|
|||
"""Return whether this adapter can be replaced by a new one.
|
||||
|
||||
This is mainly a safeguard to allow adapters whose drivers have
|
||||
gone away without calling apply or cancel to time out and be
|
||||
replaced with new ones.
|
||||
gone away without calling :meth:`apply` or :meth:`cancel` to
|
||||
time out and be replaced with new ones.
|
||||
"""
|
||||
if not _babase.in_logic_thread():
|
||||
raise RuntimeError('This must be called from the logic thread.')
|
||||
|
|
@ -104,7 +109,7 @@ class StringEditAdapter:
|
|||
"""Should be called by the owner when editing is complete.
|
||||
|
||||
Note that in some cases this call may be a no-op (such as if
|
||||
this StringEditAdapter is no longer the globally active one).
|
||||
this adapter is no longer the globally active one).
|
||||
"""
|
||||
if not _babase.in_logic_thread():
|
||||
raise RuntimeError('This must be called from the logic thread.')
|
||||
|
|
|
|||
16
dist/ba_data/python/babase/_text.py
vendored
16
dist/ba_data/python/babase/_text.py
vendored
|
|
@ -15,18 +15,18 @@ def timestring(
|
|||
timeval: float | int,
|
||||
centi: bool = True,
|
||||
) -> babase.Lstr:
|
||||
"""Generate a babase.Lstr for displaying a time value.
|
||||
"""Generate a localized string for displaying a time value.
|
||||
|
||||
Category: **General Utility Functions**
|
||||
|
||||
Given a time value, returns a babase.Lstr with:
|
||||
Given a time value, returns a localized string with:
|
||||
(hours if > 0 ) : minutes : seconds : (centiseconds if centi=True).
|
||||
|
||||
WARNING: the underlying Lstr value is somewhat large so don't use this
|
||||
to rapidly update Node text values for an onscreen timer or you may
|
||||
consume significant network bandwidth. For that purpose you should
|
||||
use a 'timedisplay' Node and attribute connections.
|
||||
.. warning::
|
||||
|
||||
the underlying localized-string value is somewhat large, so don't
|
||||
use this to rapidly update text values for an in-game timer or you
|
||||
may consume significant network bandwidth. For that sort of thing
|
||||
you should use things like 'timedisplay' nodes and attribute
|
||||
connections.
|
||||
"""
|
||||
from babase._language import Lstr
|
||||
|
||||
|
|
|
|||
5
dist/ba_data/python/babase/_workspace.py
vendored
5
dist/ba_data/python/babase/_workspace.py
vendored
|
|
@ -26,9 +26,8 @@ if TYPE_CHECKING:
|
|||
class WorkspaceSubsystem:
|
||||
"""Subsystem for workspace handling in the app.
|
||||
|
||||
Category: **App Classes**
|
||||
|
||||
Access the single shared instance of this class at `ba.app.workspaces`.
|
||||
Access the single shared instance of this class at
|
||||
`ba.app.workspaces`.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
|
|
|
|||
8
dist/ba_data/python/babase/modutils.py
vendored
8
dist/ba_data/python/babase/modutils.py
vendored
|
|
@ -7,6 +7,7 @@ from typing import TYPE_CHECKING
|
|||
import os
|
||||
|
||||
import _babase
|
||||
from babase._logging import applog
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Sequence
|
||||
|
|
@ -95,14 +96,11 @@ def show_user_scripts() -> None:
|
|||
with open(file_name, 'w', encoding='utf-8') as outfile:
|
||||
outfile.write(
|
||||
'You can drop files in here to mod the game.'
|
||||
' See settings/advanced'
|
||||
' in the game for more info.'
|
||||
' See settings/advanced in the game for more info.'
|
||||
)
|
||||
|
||||
except Exception:
|
||||
from babase import _error
|
||||
|
||||
_error.print_exception('error writing about_this_folder stuff')
|
||||
applog.exception('Error writing about_this_folder stuff.')
|
||||
|
||||
# On platforms that support it, open the dir in the UI.
|
||||
if _babase.supports_open_dir_externally():
|
||||
|
|
|
|||
4
dist/ba_data/python/baclassic/__init__.py
vendored
4
dist/ba_data/python/baclassic/__init__.py
vendored
|
|
@ -5,8 +5,8 @@
|
|||
This package/feature-set contains functionality related to the classic
|
||||
BombSquad experience. Note that much legacy BombSquad code is still a
|
||||
bit tangled and thus this feature-set is largely inseperable from
|
||||
scenev1 and uiv1. Future feature-sets will be designed in a more modular
|
||||
way.
|
||||
:mod:`bascenev1` and :mod:`bauiv1`. Future feature-sets will be
|
||||
designed in a more modular way.
|
||||
"""
|
||||
|
||||
# ba_meta require api 9
|
||||
|
|
|
|||
7
dist/ba_data/python/baclassic/_accountv1.py
vendored
7
dist/ba_data/python/baclassic/_accountv1.py
vendored
|
|
@ -17,9 +17,8 @@ if TYPE_CHECKING:
|
|||
class AccountV1Subsystem:
|
||||
"""Subsystem for legacy account handling in the app.
|
||||
|
||||
Category: **App Classes**
|
||||
|
||||
Access the single instance of this class at 'ba.app.classic.accounts'.
|
||||
Access the single instance of this class at
|
||||
'ba.app.classic.accounts'.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
|
|
@ -202,7 +201,7 @@ class AccountV1Subsystem:
|
|||
|
||||
# If the short version of our account name currently cant be
|
||||
# displayed by the game, cancel.
|
||||
if not babase.have_chars(
|
||||
if not babase.can_display_chars(
|
||||
plus.get_v1_account_display_string(full=False)
|
||||
):
|
||||
return
|
||||
|
|
|
|||
|
|
@ -73,8 +73,6 @@ ACH_LEVEL_NAMES = {
|
|||
class AchievementSubsystem:
|
||||
"""Subsystem for achievement handling.
|
||||
|
||||
Category: **App Classes**
|
||||
|
||||
Access the single shared instance of this class at 'ba.app.ach'.
|
||||
"""
|
||||
|
||||
|
|
@ -530,10 +528,7 @@ def _display_next_achievement() -> None:
|
|||
|
||||
|
||||
class Achievement:
|
||||
"""Represents attributes and state for an individual achievement.
|
||||
|
||||
Category: **App Classes**
|
||||
"""
|
||||
"""Represents attributes and state for an individual achievement."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
|
|
|||
2
dist/ba_data/python/baclassic/_ads.py
vendored
2
dist/ba_data/python/baclassic/_ads.py
vendored
|
|
@ -18,8 +18,6 @@ if TYPE_CHECKING:
|
|||
class AdsSubsystem:
|
||||
"""Subsystem for ads functionality in the app.
|
||||
|
||||
Category: **App Classes**
|
||||
|
||||
Access the single shared instance of this class at 'ba.app.ads'.
|
||||
"""
|
||||
|
||||
|
|
|
|||
273
dist/ba_data/python/baclassic/_appmode.py
vendored
273
dist/ba_data/python/baclassic/_appmode.py
vendored
|
|
@ -9,6 +9,7 @@ from functools import partial
|
|||
from typing import TYPE_CHECKING, override
|
||||
|
||||
from bacommon.app import AppExperience
|
||||
import bacommon.bs
|
||||
import babase
|
||||
import bauiv1
|
||||
from bauiv1lib.connectivity import wait_for_connectivity
|
||||
|
|
@ -28,6 +29,8 @@ if TYPE_CHECKING:
|
|||
class ClassicAppMode(babase.AppMode):
|
||||
"""AppMode for the classic BombSquad experience."""
|
||||
|
||||
_LEAGUE_VIS_VALS_CONFIG_KEY = 'ClassicLeagueVisVals'
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._on_primary_account_changed_callback: (
|
||||
CallbackRegistration | None
|
||||
|
|
@ -40,6 +43,11 @@ class ClassicAppMode(babase.AppMode):
|
|||
|
||||
self._have_account_values = False
|
||||
self._have_connectivity = False
|
||||
self._current_account_id: str | None = None
|
||||
self._should_restore_account_display_state = False
|
||||
|
||||
self._purchase_ui_pause: bauiv1.RootUIUpdatePause | None = None
|
||||
self._last_tokens_value = 0
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
|
|
@ -48,7 +56,7 @@ class ClassicAppMode(babase.AppMode):
|
|||
|
||||
@override
|
||||
@classmethod
|
||||
def _can_handle_intent(cls, intent: babase.AppIntent) -> bool:
|
||||
def can_handle_intent_impl(cls, intent: babase.AppIntent) -> bool:
|
||||
# We support default and exec intents currently.
|
||||
return isinstance(
|
||||
intent, babase.AppIntentExec | babase.AppIntentDefault
|
||||
|
|
@ -148,9 +156,15 @@ class ClassicAppMode(babase.AppMode):
|
|||
|
||||
classic = babase.app.classic
|
||||
|
||||
# Store latest league vis vals for any active account.
|
||||
self._save_account_display_state()
|
||||
|
||||
# Stop being informed of account changes.
|
||||
self._on_primary_account_changed_callback = None
|
||||
|
||||
# Cancel any ui-pause we may have had going.
|
||||
self._purchase_ui_pause = None
|
||||
|
||||
# Remove anything following any current account.
|
||||
self._update_for_primary_account(None)
|
||||
|
||||
|
|
@ -163,11 +177,151 @@ class ClassicAppMode(babase.AppMode):
|
|||
|
||||
@override
|
||||
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 babase.app.active:
|
||||
# If we've gone inactive, bring up the main menu, which has the
|
||||
# side effect of pausing the action (when possible).
|
||||
babase.invoke_main_menu()
|
||||
|
||||
# Also store any league vis state for the active account.
|
||||
# this may be our last chance to do this on mobile.
|
||||
self._save_account_display_state()
|
||||
|
||||
@override
|
||||
def on_purchase_process_begin(
|
||||
self, item_id: str, user_initiated: bool
|
||||
) -> None:
|
||||
|
||||
# Do the default thing (announces 'updating account...')
|
||||
super().on_purchase_process_begin(
|
||||
item_id=item_id, user_initiated=user_initiated
|
||||
)
|
||||
|
||||
# Pause the root ui so stuff like token counts don't change
|
||||
# automatically, allowing us to animate them. Note that we
|
||||
# need to explicitly kill this pause if we are deactivated since
|
||||
# we wouldn't get the on_purchase_process_end() call; the next
|
||||
# app-mode would.
|
||||
self._purchase_ui_pause = bauiv1.RootUIUpdatePause()
|
||||
|
||||
# Also grab our last known token count here to plug into animations.
|
||||
# We need to do this here before the purchase gets submitted so that
|
||||
# we know we're seeing the old value.
|
||||
assert babase.app.classic is not None
|
||||
self._last_tokens_value = babase.app.classic.tokens
|
||||
|
||||
@override
|
||||
def on_purchase_process_end(
|
||||
self, item_id: str, user_initiated: bool, applied: bool
|
||||
) -> None:
|
||||
|
||||
# Let the UI auto-update again after any animations we may apply
|
||||
# here.
|
||||
self._purchase_ui_pause = None
|
||||
|
||||
# Ignore user_initiated; we want to announce newly applied stuff
|
||||
# even if it was from a different launch or client or whatever.
|
||||
del user_initiated
|
||||
|
||||
# If the purchase wasn't applied, do nothing. This likely means it
|
||||
# was redundant or something else harmless.
|
||||
if not applied:
|
||||
return
|
||||
|
||||
if item_id.startswith('tokens'):
|
||||
if item_id == 'tokens1':
|
||||
tokens = bacommon.bs.TOKENS1_COUNT
|
||||
tokens_str = str(tokens)
|
||||
anim_time = 2.0
|
||||
elif item_id == 'tokens2':
|
||||
tokens = bacommon.bs.TOKENS2_COUNT
|
||||
tokens_str = str(tokens)
|
||||
anim_time = 2.5
|
||||
elif item_id == 'tokens3':
|
||||
tokens = bacommon.bs.TOKENS3_COUNT
|
||||
tokens_str = str(tokens)
|
||||
anim_time = 3.0
|
||||
elif item_id == 'tokens4':
|
||||
tokens = bacommon.bs.TOKENS4_COUNT
|
||||
tokens_str = str(tokens)
|
||||
anim_time = 3.5
|
||||
else:
|
||||
tokens = 0
|
||||
tokens_str = '???'
|
||||
anim_time = 2.5
|
||||
logging.warning(
|
||||
'Unhandled item_id in on_purchase_process_end: %s', item_id
|
||||
)
|
||||
|
||||
assert babase.app.classic is not None
|
||||
effects: list[bacommon.bs.ClientEffect] = [
|
||||
bacommon.bs.ClientEffectTokensAnimation(
|
||||
duration=anim_time,
|
||||
startvalue=self._last_tokens_value,
|
||||
endvalue=self._last_tokens_value + tokens,
|
||||
),
|
||||
bacommon.bs.ClientEffectDelay(anim_time),
|
||||
bacommon.bs.ClientEffectScreenMessage(
|
||||
message='You got ${COUNT} tokens!',
|
||||
subs=['${COUNT}', tokens_str],
|
||||
color=(0, 1, 0),
|
||||
),
|
||||
bacommon.bs.ClientEffectSound(
|
||||
sound=bacommon.bs.ClientEffectSound.Sound.CASH_REGISTER
|
||||
),
|
||||
]
|
||||
babase.app.classic.run_bs_client_effects(effects)
|
||||
|
||||
elif item_id.startswith('gold_pass'):
|
||||
babase.screenmessage(
|
||||
babase.Lstr(
|
||||
translate=('serverResponses', 'You got a ${ITEM}!'),
|
||||
subs=[
|
||||
(
|
||||
'${ITEM}',
|
||||
babase.Lstr(resource='goldPass.goldPassText'),
|
||||
)
|
||||
],
|
||||
),
|
||||
color=(0, 1, 0),
|
||||
)
|
||||
if babase.asset_loads_allowed():
|
||||
babase.getsimplesound('cashRegister').play()
|
||||
|
||||
else:
|
||||
|
||||
# Fallback: simply announce item id.
|
||||
logging.warning(
|
||||
'on_purchase_process_end got unexpected item_id: %s.', item_id
|
||||
)
|
||||
babase.screenmessage(
|
||||
babase.Lstr(
|
||||
translate=('serverResponses', 'You got a ${ITEM}!'),
|
||||
subs=[('${ITEM}', item_id)],
|
||||
),
|
||||
color=(0, 1, 0),
|
||||
)
|
||||
if babase.asset_loads_allowed():
|
||||
babase.getsimplesound('cashRegister').play()
|
||||
|
||||
def on_engine_will_reset(self) -> None:
|
||||
"""Called just before classic resets the engine.
|
||||
|
||||
This happens at various times such as session switches.
|
||||
"""
|
||||
|
||||
self._save_account_display_state()
|
||||
|
||||
def on_engine_did_reset(self) -> None:
|
||||
"""Called just after classic resets the engine.
|
||||
|
||||
This happens at various times such as session switches.
|
||||
"""
|
||||
|
||||
# Restore any old league vis state we had; this allows the user
|
||||
# to see animations for league improvements or other changes
|
||||
# that have occurred since the last time we were visible.
|
||||
self._restore_account_display_state()
|
||||
|
||||
def _update_for_primary_account(
|
||||
self, account: babase.AccountV2Handle | None
|
||||
) -> None:
|
||||
|
|
@ -181,9 +335,18 @@ class ClassicAppMode(babase.AppMode):
|
|||
assert classic is not None
|
||||
|
||||
if account is not None:
|
||||
self._current_account_id = account.accountid
|
||||
babase.set_ui_account_state(True, account.tag)
|
||||
self._should_restore_account_display_state = True
|
||||
else:
|
||||
# If we had an account, save any existing league vis state
|
||||
# so we'll properly animate to new values the next time we
|
||||
# sign in.
|
||||
self._save_account_display_state()
|
||||
|
||||
self._current_account_id = None
|
||||
babase.set_ui_account_state(False)
|
||||
self._should_restore_account_display_state = False
|
||||
|
||||
# For testing subscription functionality.
|
||||
if os.environ.get('BA_SUBSCRIPTION_TEST') == '1':
|
||||
|
|
@ -199,27 +362,39 @@ class ClassicAppMode(babase.AppMode):
|
|||
|
||||
if account is None:
|
||||
classic.gold_pass = False
|
||||
classic.tokens = 0
|
||||
classic.chest_dock_full = False
|
||||
classic.remove_ads = False
|
||||
self._account_data_sub = None
|
||||
_baclassic.set_root_ui_account_values(
|
||||
tickets=-1,
|
||||
tokens=-1,
|
||||
league_rank=-1,
|
||||
league_type='',
|
||||
league_number=-1,
|
||||
league_rank=-1,
|
||||
achievements_percent_text='',
|
||||
level_text='',
|
||||
xp_text='',
|
||||
inbox_count_text='',
|
||||
inbox_count=-1,
|
||||
inbox_count_is_max=False,
|
||||
inbox_announce_text='',
|
||||
gold_pass=False,
|
||||
chest_0_appearance='',
|
||||
chest_1_appearance='',
|
||||
chest_2_appearance='',
|
||||
chest_3_appearance='',
|
||||
chest_0_create_time=-1.0,
|
||||
chest_1_create_time=-1.0,
|
||||
chest_2_create_time=-1.0,
|
||||
chest_3_create_time=-1.0,
|
||||
chest_0_unlock_time=-1.0,
|
||||
chest_1_unlock_time=-1.0,
|
||||
chest_2_unlock_time=-1.0,
|
||||
chest_3_unlock_time=-1.0,
|
||||
chest_0_unlock_tokens=-1,
|
||||
chest_1_unlock_tokens=-1,
|
||||
chest_2_unlock_tokens=-1,
|
||||
chest_3_unlock_tokens=-1,
|
||||
chest_0_ad_allow_time=-1.0,
|
||||
chest_1_ad_allow_time=-1.0,
|
||||
chest_2_ad_allow_time=-1.0,
|
||||
|
|
@ -248,7 +423,7 @@ class ClassicAppMode(babase.AppMode):
|
|||
# connectivity state here we get UI stuff un-fading a moment or
|
||||
# two before values appear (since the subscriptions have not
|
||||
# sent us any values yet) which looks odd.
|
||||
_baclassic.set_root_ui_have_live_values(
|
||||
_baclassic.set_have_live_account_values(
|
||||
self._have_connectivity and self._have_account_values
|
||||
)
|
||||
|
||||
|
|
@ -258,11 +433,10 @@ class ClassicAppMode(babase.AppMode):
|
|||
def _on_classic_account_data_change(
|
||||
self, val: bacommon.bs.ClassicAccountLiveData
|
||||
) -> None:
|
||||
# print('ACCOUNT CHANGED:', val)
|
||||
achp = round(val.achievements / max(val.achievements_total, 1) * 100.0)
|
||||
ibc = str(val.inbox_count)
|
||||
if val.inbox_count_is_max:
|
||||
ibc += '+'
|
||||
# ibc = str(val.inbox_count)
|
||||
# if val.inbox_count_is_max:
|
||||
# ibc += '+'
|
||||
|
||||
chest0 = val.chests.get('0')
|
||||
chest1 = val.chests.get('1')
|
||||
|
|
@ -275,6 +449,7 @@ class ClassicAppMode(babase.AppMode):
|
|||
assert classic is not None
|
||||
classic.remove_ads = val.remove_ads
|
||||
classic.gold_pass = val.gold_pass
|
||||
classic.tokens = val.tokens
|
||||
classic.chest_dock_full = (
|
||||
chest0 is not None
|
||||
and chest1 is not None
|
||||
|
|
@ -285,14 +460,21 @@ class ClassicAppMode(babase.AppMode):
|
|||
_baclassic.set_root_ui_account_values(
|
||||
tickets=val.tickets,
|
||||
tokens=val.tokens,
|
||||
league_rank=(-1 if val.league_rank is None else val.league_rank),
|
||||
league_type=(
|
||||
'' if val.league_type is None else val.league_type.value
|
||||
),
|
||||
league_number=(-1 if val.league_num is None else val.league_num),
|
||||
league_rank=(-1 if val.league_rank is None else val.league_rank),
|
||||
achievements_percent_text=f'{achp}%',
|
||||
level_text=str(val.level),
|
||||
xp_text=f'{val.xp}/{val.xpmax}',
|
||||
inbox_count_text=ibc,
|
||||
inbox_count=val.inbox_count,
|
||||
inbox_count_is_max=val.inbox_count_is_max,
|
||||
inbox_announce_text=(
|
||||
babase.Lstr(resource='unclaimedPrizesText').evaluate()
|
||||
if val.inbox_contains_prize
|
||||
else ''
|
||||
),
|
||||
gold_pass=val.gold_pass,
|
||||
chest_0_appearance=(
|
||||
'' if chest0 is None else chest0.appearance.value
|
||||
|
|
@ -306,6 +488,18 @@ class ClassicAppMode(babase.AppMode):
|
|||
chest_3_appearance=(
|
||||
'' if chest3 is None else chest3.appearance.value
|
||||
),
|
||||
chest_0_create_time=(
|
||||
-1.0 if chest0 is None else chest0.create_time.timestamp()
|
||||
),
|
||||
chest_1_create_time=(
|
||||
-1.0 if chest1 is None else chest1.create_time.timestamp()
|
||||
),
|
||||
chest_2_create_time=(
|
||||
-1.0 if chest2 is None else chest2.create_time.timestamp()
|
||||
),
|
||||
chest_3_create_time=(
|
||||
-1.0 if chest3 is None else chest3.create_time.timestamp()
|
||||
),
|
||||
chest_0_unlock_time=(
|
||||
-1.0 if chest0 is None else chest0.unlock_time.timestamp()
|
||||
),
|
||||
|
|
@ -318,6 +512,18 @@ class ClassicAppMode(babase.AppMode):
|
|||
chest_3_unlock_time=(
|
||||
-1.0 if chest3 is None else chest3.unlock_time.timestamp()
|
||||
),
|
||||
chest_0_unlock_tokens=(
|
||||
-1 if chest0 is None else chest0.unlock_tokens
|
||||
),
|
||||
chest_1_unlock_tokens=(
|
||||
-1 if chest1 is None else chest1.unlock_tokens
|
||||
),
|
||||
chest_2_unlock_tokens=(
|
||||
-1 if chest2 is None else chest2.unlock_tokens
|
||||
),
|
||||
chest_3_unlock_tokens=(
|
||||
-1 if chest3 is None else chest3.unlock_tokens
|
||||
),
|
||||
chest_0_ad_allow_time=(
|
||||
-1.0
|
||||
if chest0 is None or chest0.ad_allow_time is None
|
||||
|
|
@ -339,6 +545,14 @@ class ClassicAppMode(babase.AppMode):
|
|||
else chest3.ad_allow_time.timestamp()
|
||||
),
|
||||
)
|
||||
if self._should_restore_account_display_state:
|
||||
# If we have a previous display-state for this account,
|
||||
# restore it. This will cause us to animate or otherwise
|
||||
# display league changes that have occurred since we were
|
||||
# last visible. Note we need to do this *after* setting real
|
||||
# vals so there is a current state to animate to.
|
||||
self._restore_account_display_state()
|
||||
self._should_restore_account_display_state = False
|
||||
|
||||
# Note that we have values and updated faded state accordingly.
|
||||
self._have_account_values = True
|
||||
|
|
@ -656,3 +870,38 @@ class ClassicAppMode(babase.AppMode):
|
|||
),
|
||||
)
|
||||
)
|
||||
|
||||
def _save_account_display_state(self) -> None:
|
||||
|
||||
# If we currently have an account, save the state of what we're
|
||||
# currently displaying for it in the root ui/etc. We'll then
|
||||
# restore that state as a starting point the next time we are
|
||||
# active. This allows things like league rank changes to be
|
||||
# properly animated even if they occurred while we were offline
|
||||
# or while the UI was hidden.
|
||||
|
||||
if self._current_account_id is not None:
|
||||
vals = _baclassic.get_account_display_state()
|
||||
if vals is not None:
|
||||
# Stuff our account id in there and save it to our
|
||||
# config.
|
||||
assert 'a' not in vals
|
||||
vals['a'] = self._current_account_id
|
||||
cfg = babase.app.config
|
||||
cfg[self._LEAGUE_VIS_VALS_CONFIG_KEY] = vals
|
||||
cfg.commit()
|
||||
|
||||
def _restore_account_display_state(self) -> None:
|
||||
|
||||
# If we currently have an account and it matches the
|
||||
# display-state we have stored in the config, restore the state.
|
||||
if self._current_account_id is not None:
|
||||
cfg = babase.app.config
|
||||
vals = cfg.get(self._LEAGUE_VIS_VALS_CONFIG_KEY)
|
||||
if isinstance(vals, dict):
|
||||
valsaccount = vals.get('a')
|
||||
if (
|
||||
isinstance(valsaccount, str)
|
||||
and valsaccount == self._current_account_id
|
||||
):
|
||||
_baclassic.set_account_display_state(vals)
|
||||
|
|
|
|||
17
dist/ba_data/python/baclassic/_appsubsystem.py
vendored
17
dist/ba_data/python/baclassic/_appsubsystem.py
vendored
|
|
@ -77,6 +77,7 @@ class ClassicAppSubsystem(babase.AppSubsystem):
|
|||
# Classic-specific account state.
|
||||
self.remove_ads = False
|
||||
self.gold_pass = False
|
||||
self.tokens = 0
|
||||
self.chest_dock_full = False
|
||||
|
||||
# Main Menu.
|
||||
|
|
@ -384,8 +385,6 @@ class ClassicAppSubsystem(babase.AppSubsystem):
|
|||
def getmaps(self, playtype: str) -> list[str]:
|
||||
"""Return a list of bascenev1.Map types supporting a playtype str.
|
||||
|
||||
Category: **Asset Functions**
|
||||
|
||||
Maps supporting a given playtype must provide a particular set of
|
||||
features and lend themselves to a certain style of play.
|
||||
|
||||
|
|
@ -751,10 +750,7 @@ class ClassicAppSubsystem(babase.AppSubsystem):
|
|||
)
|
||||
|
||||
def preload_map_preview_media(self) -> None:
|
||||
"""Preload media needed for map preview UIs.
|
||||
|
||||
Category: **Asset Functions**
|
||||
"""
|
||||
"""Preload media needed for map preview UIs."""
|
||||
try:
|
||||
bauiv1.getmesh('level_select_button_opaque')
|
||||
bauiv1.getmesh('level_select_button_transparent')
|
||||
|
|
@ -802,6 +798,9 @@ class ClassicAppSubsystem(babase.AppSubsystem):
|
|||
if babase.app.env.gui:
|
||||
bauiv1.getsound('swish').play()
|
||||
|
||||
# Pause gameplay.
|
||||
self.pause()
|
||||
|
||||
babase.app.ui_v1.set_main_window(
|
||||
InGameMenuWindow(), is_top_level=True, suppress_warning=True
|
||||
)
|
||||
|
|
@ -854,11 +853,13 @@ class ClassicAppSubsystem(babase.AppSubsystem):
|
|||
)
|
||||
|
||||
@staticmethod
|
||||
def run_bs_client_effects(effects: list[bacommon.bs.ClientEffect]) -> None:
|
||||
def run_bs_client_effects(
|
||||
effects: list[bacommon.bs.ClientEffect], delay: float = 0.0
|
||||
) -> None:
|
||||
"""Run client effects sent from the master server."""
|
||||
from baclassic._clienteffect import run_bs_client_effects
|
||||
|
||||
run_bs_client_effects(effects)
|
||||
run_bs_client_effects(effects, delay=delay)
|
||||
|
||||
@staticmethod
|
||||
def basic_client_ui_button_label_str(
|
||||
|
|
|
|||
70
dist/ba_data/python/baclassic/_clienteffect.py
vendored
70
dist/ba_data/python/baclassic/_clienteffect.py
vendored
|
|
@ -12,17 +12,23 @@ from efro.util import strict_partial
|
|||
import bacommon.bs
|
||||
import bauiv1
|
||||
|
||||
import _baclassic
|
||||
|
||||
if TYPE_CHECKING:
|
||||
pass
|
||||
|
||||
|
||||
def run_bs_client_effects(effects: list[bacommon.bs.ClientEffect]) -> None:
|
||||
def run_bs_client_effects(
|
||||
effects: list[bacommon.bs.ClientEffect], delay: float = 0.0
|
||||
) -> None:
|
||||
"""Run effects."""
|
||||
# pylint: disable=too-many-branches
|
||||
from bacommon.bs import ClientEffectTypeID
|
||||
|
||||
delay = 0.0
|
||||
for effect in effects:
|
||||
if isinstance(effect, bacommon.bs.ClientEffectScreenMessage):
|
||||
effecttype = effect.get_type_id()
|
||||
if effecttype is ClientEffectTypeID.SCREEN_MESSAGE:
|
||||
assert isinstance(effect, bacommon.bs.ClientEffectScreenMessage)
|
||||
textfin = bauiv1.Lstr(
|
||||
translate=('serverResponses', effect.message)
|
||||
).evaluate()
|
||||
|
|
@ -41,7 +47,8 @@ def run_bs_client_effects(effects: list[bacommon.bs.ClientEffect]) -> None:
|
|||
),
|
||||
)
|
||||
|
||||
elif isinstance(effect, bacommon.bs.ClientEffectSound):
|
||||
elif effecttype is ClientEffectTypeID.SOUND:
|
||||
assert isinstance(effect, bacommon.bs.ClientEffectSound)
|
||||
smcls = bacommon.bs.ClientEffectSound.Sound
|
||||
soundfile: str | None = None
|
||||
if effect.sound is smcls.UNKNOWN:
|
||||
|
|
@ -66,12 +73,63 @@ def run_bs_client_effects(effects: list[bacommon.bs.ClientEffect]) -> None:
|
|||
),
|
||||
)
|
||||
|
||||
elif isinstance(effect, bacommon.bs.ClientEffectDelay):
|
||||
elif effecttype is ClientEffectTypeID.DELAY:
|
||||
assert isinstance(effect, bacommon.bs.ClientEffectDelay)
|
||||
delay += effect.seconds
|
||||
else:
|
||||
|
||||
elif effecttype is ClientEffectTypeID.CHEST_WAIT_TIME_ANIMATION:
|
||||
assert isinstance(
|
||||
effect, bacommon.bs.ClientEffectChestWaitTimeAnimation
|
||||
)
|
||||
bauiv1.apptimer(
|
||||
delay,
|
||||
strict_partial(
|
||||
_baclassic.animate_root_ui_chest_unlock_time,
|
||||
chestid=effect.chestid,
|
||||
duration=effect.duration,
|
||||
startvalue=effect.startvalue.timestamp(),
|
||||
endvalue=effect.endvalue.timestamp(),
|
||||
),
|
||||
)
|
||||
|
||||
elif effecttype is ClientEffectTypeID.TICKETS_ANIMATION:
|
||||
assert isinstance(effect, bacommon.bs.ClientEffectTicketsAnimation)
|
||||
bauiv1.apptimer(
|
||||
delay,
|
||||
strict_partial(
|
||||
_baclassic.animate_root_ui_tickets,
|
||||
duration=effect.duration,
|
||||
startvalue=effect.startvalue,
|
||||
endvalue=effect.endvalue,
|
||||
),
|
||||
)
|
||||
|
||||
elif effecttype is ClientEffectTypeID.TOKENS_ANIMATION:
|
||||
assert isinstance(effect, bacommon.bs.ClientEffectTokensAnimation)
|
||||
bauiv1.apptimer(
|
||||
delay,
|
||||
strict_partial(
|
||||
_baclassic.animate_root_ui_tokens,
|
||||
duration=effect.duration,
|
||||
startvalue=effect.startvalue,
|
||||
endvalue=effect.endvalue,
|
||||
),
|
||||
)
|
||||
|
||||
elif effecttype is ClientEffectTypeID.UNKNOWN:
|
||||
# Server should not send us stuff we can't digest. Make
|
||||
# some noise if it happens.
|
||||
logging.error(
|
||||
'Got unrecognized bacommon.bs.ClientEffect;'
|
||||
' should not happen.'
|
||||
)
|
||||
|
||||
else:
|
||||
# For type-checking purposes to remind us to implement new
|
||||
# types; should this this in real life.
|
||||
assert_never(effecttype)
|
||||
|
||||
# Lastly, put a pause on root ui auto-updates so that everything we
|
||||
# just scheduled is free to muck with it freely.
|
||||
bauiv1.root_ui_pause_updates()
|
||||
bauiv1.apptimer(delay + 0.25, bauiv1.root_ui_resume_updates)
|
||||
|
|
|
|||
38
dist/ba_data/python/baclassic/_hooks.py
vendored
Normal file
38
dist/ba_data/python/baclassic/_hooks.py
vendored
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Hooks for C++ layer to use for ClassicAppMode."""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
import babase
|
||||
|
||||
|
||||
def on_engine_will_reset() -> None:
|
||||
"""Called just before classic resets the engine."""
|
||||
from baclassic._appmode import ClassicAppMode
|
||||
|
||||
appmode = babase.app.mode
|
||||
|
||||
# Just pass this along to our mode instance for handling.
|
||||
if isinstance(appmode, ClassicAppMode):
|
||||
appmode.on_engine_will_reset()
|
||||
else:
|
||||
logging.error(
|
||||
'on_engine_will_reset called without ClassicAppMode active.'
|
||||
)
|
||||
|
||||
|
||||
def on_engine_did_reset() -> None:
|
||||
"""Called just after classic resets the engine."""
|
||||
from baclassic._appmode import ClassicAppMode
|
||||
|
||||
appmode = babase.app.mode
|
||||
|
||||
# Just pass this along to our mode instance for handling.
|
||||
if isinstance(appmode, ClassicAppMode):
|
||||
appmode.on_engine_did_reset()
|
||||
else:
|
||||
logging.error(
|
||||
'on_engine_did_reset called without ClassicAppMode active.'
|
||||
)
|
||||
14
dist/ba_data/python/baclassic/_music.py
vendored
14
dist/ba_data/python/baclassic/_music.py
vendored
|
|
@ -20,10 +20,7 @@ if TYPE_CHECKING:
|
|||
|
||||
|
||||
class MusicPlayMode(Enum):
|
||||
"""Influences behavior when playing music.
|
||||
|
||||
Category: **Enums**
|
||||
"""
|
||||
"""Influences behavior when playing music."""
|
||||
|
||||
REGULAR = 'regular'
|
||||
TEST = 'test'
|
||||
|
|
@ -31,10 +28,7 @@ class MusicPlayMode(Enum):
|
|||
|
||||
@dataclass
|
||||
class AssetSoundtrackEntry:
|
||||
"""A music entry using an internal asset.
|
||||
|
||||
Category: **App Classes**
|
||||
"""
|
||||
"""A music entry using an internal asset."""
|
||||
|
||||
assetname: str
|
||||
volume: float = 1.0
|
||||
|
|
@ -81,8 +75,6 @@ ASSET_SOUNDTRACK_ENTRIES: dict[MusicType, AssetSoundtrackEntry] = {
|
|||
class MusicSubsystem:
|
||||
"""Subsystem for music playback in the app.
|
||||
|
||||
Category: **App Classes**
|
||||
|
||||
Access the single shared instance of this class at 'ba.app.music'.
|
||||
"""
|
||||
|
||||
|
|
@ -374,8 +366,6 @@ class MusicSubsystem:
|
|||
class MusicPlayer:
|
||||
"""Wrangles soundtrack music playback.
|
||||
|
||||
Category: **App Classes**
|
||||
|
||||
Music can be played either through the game itself
|
||||
or via a platform-specific external player.
|
||||
"""
|
||||
|
|
|
|||
18
dist/ba_data/python/baclassic/_net.py
vendored
18
dist/ba_data/python/baclassic/_net.py
vendored
|
|
@ -96,7 +96,7 @@ class MasterServerV1CallThread(threading.Thread):
|
|||
try:
|
||||
classic = babase.app.classic
|
||||
assert classic is not None
|
||||
self._data = babase.utf8_all(self._data)
|
||||
self._data = _utf8_all(self._data)
|
||||
babase.set_thread_name('BA_ServerCallThread')
|
||||
if self._request_type == 'get':
|
||||
msaddr = plus.get_master_server_address()
|
||||
|
|
@ -164,3 +164,19 @@ class MasterServerV1CallThread(threading.Thread):
|
|||
babase.Call(self._run_callback, response_data),
|
||||
from_other_thread=True,
|
||||
)
|
||||
|
||||
|
||||
def _utf8_all(data: Any) -> Any:
|
||||
"""Convert any unicode data in provided sequence(s) to utf8 bytes."""
|
||||
if isinstance(data, dict):
|
||||
return dict(
|
||||
(_utf8_all(key), _utf8_all(value))
|
||||
for key, value in list(data.items())
|
||||
)
|
||||
if isinstance(data, list):
|
||||
return [_utf8_all(element) for element in data]
|
||||
if isinstance(data, tuple):
|
||||
return tuple(_utf8_all(element) for element in data)
|
||||
if isinstance(data, str):
|
||||
return data.encode('utf-8', errors='ignore')
|
||||
return data
|
||||
|
|
|
|||
7
dist/ba_data/python/baclassic/_servermode.py
vendored
7
dist/ba_data/python/baclassic/_servermode.py
vendored
|
|
@ -87,10 +87,7 @@ def _cmd(command_data: bytes) -> None:
|
|||
|
||||
|
||||
class ServerController:
|
||||
"""Overall controller for the app in server mode.
|
||||
|
||||
Category: **App Classes**
|
||||
"""
|
||||
"""Overall controller for the app in server mode."""
|
||||
|
||||
def __init__(self, config: ServerConfig) -> None:
|
||||
self._config = config
|
||||
|
|
@ -390,7 +387,7 @@ class ServerController:
|
|||
f' ({app.env.engine_build_number})'
|
||||
f' entering server-mode {curtimestr}{Clr.RST}'
|
||||
)
|
||||
print(startupmsg)
|
||||
logging.info(startupmsg)
|
||||
|
||||
if sessiontype is bascenev1.FreeForAllSession:
|
||||
appcfg['Free-for-All Playlist Selection'] = self._playlist_name
|
||||
|
|
|
|||
5
dist/ba_data/python/baclassic/_store.py
vendored
5
dist/ba_data/python/baclassic/_store.py
vendored
|
|
@ -565,10 +565,7 @@ class StoreSubsystem:
|
|||
return None
|
||||
|
||||
def get_unowned_maps(self) -> list[str]:
|
||||
"""Return the list of local maps not owned by the current account.
|
||||
|
||||
Category: **Asset Functions**
|
||||
"""
|
||||
"""Return the list of local maps not owned by the current account."""
|
||||
plus = babase.app.plus
|
||||
unowned_maps: set[str] = set()
|
||||
if babase.app.env.gui:
|
||||
|
|
|
|||
5
dist/ba_data/python/bacommon/__init__.py
vendored
5
dist/ba_data/python/bacommon/__init__.py
vendored
|
|
@ -1,3 +1,6 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Functionality and data common to ballistica client and server components."""
|
||||
"""Functionality and data shared by all Ballistica components.
|
||||
|
||||
This includes clients, various servers, tools, etc.
|
||||
"""
|
||||
|
|
|
|||
137
dist/ba_data/python/bacommon/app.py
vendored
137
dist/ba_data/python/bacommon/app.py
vendored
|
|
@ -1,6 +1,6 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Common high level values/functionality related to apps."""
|
||||
"""Common high level values/functionality related to Ballistica apps."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
|
@ -10,6 +10,8 @@ from typing import TYPE_CHECKING, Annotated
|
|||
|
||||
from efro.dataclassio import ioprepped, IOAttrs
|
||||
|
||||
from bacommon.locale import Locale
|
||||
|
||||
if TYPE_CHECKING:
|
||||
pass
|
||||
|
||||
|
|
@ -17,19 +19,41 @@ if TYPE_CHECKING:
|
|||
class AppInterfaceIdiom(Enum):
|
||||
"""A general form-factor or method of experiencing a Ballistica app.
|
||||
|
||||
Note that it is possible for a running app to switch idioms (for
|
||||
Note that it may be possible for a running app to switch idioms (for
|
||||
instance if a mobile device or computer is connected to a TV).
|
||||
"""
|
||||
|
||||
PHONE = 'phone'
|
||||
TABLET = 'tablet'
|
||||
DESKTOP = 'desktop'
|
||||
#: Small screen; assumed to have touch as primary input.
|
||||
PHONE = 'phn'
|
||||
|
||||
#: Medium size screen; assumed to have touch as primary input.
|
||||
TABLET = 'tab'
|
||||
|
||||
#: Medium size screen; assumed to have game controller as primary
|
||||
#: input.
|
||||
HANDHELD = 'hnd'
|
||||
|
||||
#: Large screen with high amount of detail visible; assumed to have
|
||||
#: keyboard/mouse as primary input.
|
||||
DESKTOP = 'dsk'
|
||||
|
||||
#: Large screen with medium amount of detail visible; assumed to have
|
||||
#: game controller as primary input.
|
||||
TV = 'tv'
|
||||
XR = 'xr'
|
||||
|
||||
#: Displayed over or in place of of the real world on a headset;
|
||||
#: assumed to have hand tracking or spacial controllers as primary
|
||||
#: input.
|
||||
XR_HEADSET = 'xrh'
|
||||
|
||||
#: Displayed over or instead of the real world on a screen; assumed
|
||||
#: to have device movement augmented by physical or touchscreen
|
||||
#: controls as primary input.
|
||||
XR_SCREEN = 'xrs'
|
||||
|
||||
|
||||
class AppExperience(Enum):
|
||||
"""A particular experience that can be provided by a Ballistica app.
|
||||
"""A particular experience provided by a Ballistica app.
|
||||
|
||||
This is one metric used to isolate different playerbases from each
|
||||
other where there might be no technical barriers doing so. For
|
||||
|
|
@ -44,36 +68,36 @@ class AppExperience(Enum):
|
|||
support multiple experiences, or there may be multiple apps
|
||||
targeting one experience. Cloud components such as leagues are
|
||||
generally associated with an AppExperience so that they are only
|
||||
visible to client apps designed for that play style.
|
||||
visible to client apps designed for that play style, and the same is
|
||||
true for games joinable over the local network, bluetooth, etc.
|
||||
"""
|
||||
|
||||
# An experience that is supported everywhere. Used for the default
|
||||
# empty AppMode when starting the app, etc.
|
||||
EMPTY = 'empty'
|
||||
#: An experience that is supported everywhere. Used for the default
|
||||
#: empty AppMode when starting the app, etc.
|
||||
EMPTY = 'empt'
|
||||
|
||||
# The traditional BombSquad experience: multiple players using
|
||||
# traditional game controllers (or touch screen equivalents) in a
|
||||
# single arena small enough for all action to be viewed on a single
|
||||
# screen.
|
||||
MELEE = 'melee'
|
||||
#: The traditional BombSquad experience - multiple players using
|
||||
#: game controllers (or touch screen equivalents) in a single arena
|
||||
#: small enough for all action to be viewed on a single screen.
|
||||
MELEE = 'mlee'
|
||||
|
||||
# The traditional BombSquad Remote experience; buttons on a
|
||||
# touch-screen allowing a mobile device to be used as a game
|
||||
# controller.
|
||||
REMOTE = 'remote'
|
||||
#: The traditional BombSquad Remote experience; buttons on a
|
||||
#: touch-screen allowing a mobile device to be used as a game
|
||||
#: controller.
|
||||
REMOTE = 'rmt'
|
||||
|
||||
|
||||
class AppArchitecture(Enum):
|
||||
"""Processor architecture the App is running on."""
|
||||
"""Processor architecture an app can be running on."""
|
||||
|
||||
ARM = 'arm'
|
||||
ARM64 = 'arm64'
|
||||
X86 = 'x86'
|
||||
X86_64 = 'x86_64'
|
||||
X86_64 = 'x64'
|
||||
|
||||
|
||||
class AppPlatform(Enum):
|
||||
"""Overall platform a Ballistica build is targeting.
|
||||
"""Overall platform a build can target.
|
||||
|
||||
Each distinct flavor of an app has a unique combination of
|
||||
AppPlatform and AppVariant. Generally platform describes a set of
|
||||
|
|
@ -82,9 +106,9 @@ class AppPlatform(Enum):
|
|||
"""
|
||||
|
||||
MAC = 'mac'
|
||||
WINDOWS = 'windows'
|
||||
LINUX = 'linux'
|
||||
ANDROID = 'android'
|
||||
WINDOWS = 'win'
|
||||
LINUX = 'lin'
|
||||
ANDROID = 'andr'
|
||||
IOS = 'ios'
|
||||
TVOS = 'tvos'
|
||||
|
||||
|
|
@ -98,43 +122,58 @@ class AppVariant(Enum):
|
|||
build.
|
||||
"""
|
||||
|
||||
# Default builds.
|
||||
GENERIC = 'generic'
|
||||
#: Default builds.
|
||||
GENERIC = 'gen'
|
||||
|
||||
# Builds intended for public testing (may have some extra checks
|
||||
# or logging enabled).
|
||||
TEST = 'test'
|
||||
#: Builds intended for public testing (may have some extra checks
|
||||
#: or logging enabled).
|
||||
TEST = 'tst'
|
||||
|
||||
# Various stores.
|
||||
AMAZON_APPSTORE = 'amazon_appstore'
|
||||
GOOGLE_PLAY = 'google_play'
|
||||
APP_STORE = 'app_store'
|
||||
WINDOWS_STORE = 'windows_store'
|
||||
STEAM = 'steam'
|
||||
AMAZON_APPSTORE = 'amzn'
|
||||
GOOGLE_PLAY = 'gpl'
|
||||
APPLE_APP_STORE = 'appl'
|
||||
WINDOWS_STORE = 'wins'
|
||||
STEAM = 'stm'
|
||||
META = 'meta'
|
||||
EPIC_GAMES_STORE = 'epic_games_store'
|
||||
EPIC_GAMES_STORE = 'epic'
|
||||
|
||||
# Other.
|
||||
ARCADE = 'arcade'
|
||||
ARCADE = 'arcd'
|
||||
DEMO = 'demo'
|
||||
|
||||
|
||||
class AppName(Enum):
|
||||
"""A predefined Ballistica app name.
|
||||
|
||||
This encompasses official or well-known apps. Other app projects
|
||||
should set this to CUSTOM and provide a 'name_custom' value.
|
||||
"""
|
||||
|
||||
BOMBSQUAD = 'bs'
|
||||
CUSTOM = 'c'
|
||||
|
||||
|
||||
@ioprepped
|
||||
@dataclass
|
||||
class AppInstanceInfo:
|
||||
"""General info about an individual running app."""
|
||||
"""General info about an individual running ballistica app."""
|
||||
|
||||
name = Annotated[str, IOAttrs('n')]
|
||||
name: Annotated[str, IOAttrs('name')]
|
||||
name_custom: Annotated[
|
||||
str | None, IOAttrs('namc', soft_default=None, store_default=False)
|
||||
]
|
||||
|
||||
engine_version = Annotated[str, IOAttrs('ev')]
|
||||
engine_build = Annotated[int, IOAttrs('eb')]
|
||||
engine_version: Annotated[str, IOAttrs('evrs')]
|
||||
engine_build: Annotated[int, IOAttrs('ebld')]
|
||||
|
||||
platform = Annotated[AppPlatform, IOAttrs('p')]
|
||||
variant = Annotated[AppVariant, IOAttrs('va')]
|
||||
architecture = Annotated[AppArchitecture, IOAttrs('a')]
|
||||
os_version = Annotated[str | None, IOAttrs('o')]
|
||||
platform: Annotated[AppPlatform, IOAttrs('plat')]
|
||||
variant: Annotated[AppVariant, IOAttrs('vrnt')]
|
||||
architecture: Annotated[AppArchitecture, IOAttrs('arch')]
|
||||
os_version: Annotated[str | None, IOAttrs('osvr')]
|
||||
|
||||
interface_idiom: Annotated[AppInterfaceIdiom, IOAttrs('i')]
|
||||
locale: Annotated[str, IOAttrs('l')]
|
||||
interface_idiom: Annotated[AppInterfaceIdiom, IOAttrs('intf')]
|
||||
locale: Annotated[Locale, IOAttrs('loc')]
|
||||
|
||||
device: Annotated[str | None, IOAttrs('d')]
|
||||
#: OS-specific string describing the device running the app.
|
||||
device: Annotated[str | None, IOAttrs('devc')]
|
||||
|
|
|
|||
109
dist/ba_data/python/bacommon/bacloud.py
vendored
109
dist/ba_data/python/bacommon/bacloud.py
vendored
|
|
@ -51,44 +51,7 @@ class RequestData:
|
|||
@ioprepped
|
||||
@dataclass
|
||||
class ResponseData:
|
||||
"""Response sent from the bacloud server to the client.
|
||||
|
||||
Attributes:
|
||||
message: If present, client should print this message before any other
|
||||
response processing (including error handling) occurs.
|
||||
message_end: end arg for message print() call.
|
||||
error: If present, client should abort with this error message.
|
||||
delay_seconds: How long to wait before proceeding with remaining
|
||||
response (can be useful when waiting for server progress in a loop).
|
||||
login: If present, a token that should be stored client-side and passed
|
||||
with subsequent commands.
|
||||
logout: If True, any existing client-side token should be discarded.
|
||||
dir_manifest: If present, client should generate a manifest of this dir.
|
||||
It should be added to end_command args as 'manifest'.
|
||||
uploads: If present, client should upload the requested files (arg1)
|
||||
individually to a server command (arg2) with provided args (arg3).
|
||||
uploads_inline: If present, a list of pathnames that should be gzipped
|
||||
and uploaded to an 'uploads_inline' bytes dict in end_command args.
|
||||
This should be limited to relatively small files.
|
||||
deletes: If present, file paths that should be deleted on the client.
|
||||
downloads: If present, describes files the client should individually
|
||||
request from the server if not already present on the client.
|
||||
downloads_inline: If present, pathnames mapped to gzipped data to
|
||||
be written to the client. This should only be used for relatively
|
||||
small files as they are all included inline as part of the response.
|
||||
dir_prune_empty: If present, all empty dirs under this one should be
|
||||
removed.
|
||||
open_url: If present, url to display to the user.
|
||||
input_prompt: If present, a line of input is read and placed into
|
||||
end_command args as 'input'. The first value is the prompt printed
|
||||
before reading and the second is whether it should be read as a
|
||||
password (without echoing to the terminal).
|
||||
end_message: If present, a message that should be printed after all other
|
||||
response processing is done.
|
||||
end_message_end: end arg for end_message print() call.
|
||||
end_command: If present, this command is run with these args at the end
|
||||
of response processing.
|
||||
"""
|
||||
"""Response sent from the bacloud server to the client."""
|
||||
|
||||
@ioprepped
|
||||
@dataclass
|
||||
|
|
@ -101,62 +64,114 @@ class ResponseData:
|
|||
"""Individual download."""
|
||||
|
||||
path: Annotated[str, IOAttrs('p')]
|
||||
# Args include with this particular request (combined with
|
||||
# baseargs).
|
||||
|
||||
#: Args include with this particular request (combined with
|
||||
#: baseargs).
|
||||
args: Annotated[dict[str, str], IOAttrs('a')]
|
||||
|
||||
# TODO: could add a hash here if we want the client to
|
||||
# verify hashes.
|
||||
|
||||
# If present, will be prepended to all entry paths via os.path.join.
|
||||
#: If present, will be prepended to all entry paths via os.path.join.
|
||||
basepath: Annotated[str | None, IOAttrs('p')]
|
||||
|
||||
# Server command that should be called for each download. The
|
||||
# server command is expected to respond with a downloads_inline
|
||||
# containing a single 'default' entry. In the future this may
|
||||
# be expanded to a more streaming-friendly process.
|
||||
#: Server command that should be called for each download. The
|
||||
#: server command is expected to respond with a downloads_inline
|
||||
#: containing a single 'default' entry. In the future this may
|
||||
#: be expanded to a more streaming-friendly process.
|
||||
cmd: Annotated[str, IOAttrs('c')]
|
||||
|
||||
# Args that should be included with all download requests.
|
||||
#: Args that should be included with all download requests.
|
||||
baseargs: Annotated[dict[str, str], IOAttrs('a')]
|
||||
|
||||
# Everything that should be downloaded.
|
||||
#: Everything that should be downloaded.
|
||||
entries: Annotated[list[Entry], IOAttrs('e')]
|
||||
|
||||
#: If present, client should print this message before any other
|
||||
#: response processing (including error handling) occurs.
|
||||
message: Annotated[str | None, IOAttrs('m', store_default=False)] = None
|
||||
|
||||
#: End arg for message print() call.
|
||||
message_end: Annotated[str, IOAttrs('m_end', store_default=False)] = '\n'
|
||||
|
||||
#: If present, client should abort with this error message.
|
||||
error: Annotated[str | None, IOAttrs('e', store_default=False)] = None
|
||||
|
||||
#: How long to wait before proceeding with remaining response (can
|
||||
#: be useful when waiting for server progress in a loop).
|
||||
delay_seconds: Annotated[float, IOAttrs('d', store_default=False)] = 0.0
|
||||
|
||||
#: If present, a token that should be stored client-side and passed
|
||||
#: with subsequent commands.
|
||||
login: Annotated[str | None, IOAttrs('l', store_default=False)] = None
|
||||
|
||||
#: If True, any existing client-side token should be discarded.
|
||||
logout: Annotated[bool, IOAttrs('lo', store_default=False)] = False
|
||||
|
||||
#: If present, client should generate a manifest of this dir.
|
||||
#: It should be added to end_command args as 'manifest'.
|
||||
dir_manifest: Annotated[str | None, IOAttrs('man', store_default=False)] = (
|
||||
None
|
||||
)
|
||||
|
||||
#: If present, client should upload the requested files (arg1)
|
||||
#: individually to a server command (arg2) with provided args (arg3).
|
||||
uploads: Annotated[
|
||||
tuple[list[str], str, dict] | None, IOAttrs('u', store_default=False)
|
||||
] = None
|
||||
|
||||
#: If present, a list of pathnames that should be gzipped
|
||||
#: and uploaded to an 'uploads_inline' bytes dict in end_command args.
|
||||
#: This should be limited to relatively small files.
|
||||
uploads_inline: Annotated[
|
||||
list[str] | None, IOAttrs('uinl', store_default=False)
|
||||
] = None
|
||||
|
||||
#: If present, file paths that should be deleted on the client.
|
||||
deletes: Annotated[
|
||||
list[str] | None, IOAttrs('dlt', store_default=False)
|
||||
] = None
|
||||
|
||||
#: If present, describes files the client should individually
|
||||
#: request from the server if not already present on the client.
|
||||
downloads: Annotated[
|
||||
Downloads | None, IOAttrs('dl', store_default=False)
|
||||
] = None
|
||||
|
||||
#: If present, pathnames mapped to gzipped data to
|
||||
#: be written to the client. This should only be used for relatively
|
||||
#: small files as they are all included inline as part of the response.
|
||||
downloads_inline: Annotated[
|
||||
dict[str, bytes] | None, IOAttrs('dinl', store_default=False)
|
||||
] = None
|
||||
|
||||
#: If present, all empty dirs under this one should be removed.
|
||||
dir_prune_empty: Annotated[
|
||||
str | None, IOAttrs('dpe', store_default=False)
|
||||
] = None
|
||||
|
||||
#: If present, url to display to the user.
|
||||
open_url: Annotated[str | None, IOAttrs('url', store_default=False)] = None
|
||||
|
||||
#: If present, a line of input is read and placed into
|
||||
#: end_command args as 'input'. The first value is the prompt printed
|
||||
#: before reading and the second is whether it should be read as a
|
||||
#: password (without echoing to the terminal).
|
||||
input_prompt: Annotated[
|
||||
tuple[str, bool] | None, IOAttrs('inp', store_default=False)
|
||||
] = None
|
||||
|
||||
#: If present, a message that should be printed after all other
|
||||
#: response processing is done.
|
||||
end_message: Annotated[str | None, IOAttrs('em', store_default=False)] = (
|
||||
None
|
||||
)
|
||||
|
||||
#: End arg for end_message print() call.
|
||||
end_message_end: Annotated[str, IOAttrs('eme', store_default=False)] = '\n'
|
||||
|
||||
#: If present, this command is run with these args at the end
|
||||
#: of response processing.
|
||||
end_command: Annotated[
|
||||
tuple[str, dict] | None, IOAttrs('ec', store_default=False)
|
||||
] = None
|
||||
|
|
|
|||
188
dist/ba_data/python/bacommon/bs.py
vendored
188
dist/ba_data/python/bacommon/bs.py
vendored
|
|
@ -13,6 +13,12 @@ from efro.util import pairs_to_flat
|
|||
from efro.dataclassio import ioprepped, IOAttrs, IOMultiType
|
||||
from efro.message import Message, Response
|
||||
|
||||
# Token counts for our various packs.
|
||||
TOKENS1_COUNT = 50
|
||||
TOKENS2_COUNT = 500
|
||||
TOKENS3_COUNT = 1200
|
||||
TOKENS4_COUNT = 2600
|
||||
|
||||
|
||||
@ioprepped
|
||||
@dataclass
|
||||
|
|
@ -89,7 +95,9 @@ class ClassicAccountLiveData:
|
|||
ClassicChestAppearance,
|
||||
IOAttrs('a', enum_fallback=ClassicChestAppearance.UNKNOWN),
|
||||
]
|
||||
create_time: Annotated[datetime.datetime, IOAttrs('c')]
|
||||
unlock_time: Annotated[datetime.datetime, IOAttrs('t')]
|
||||
unlock_tokens: Annotated[int, IOAttrs('k')]
|
||||
ad_allow_time: Annotated[datetime.datetime | None, IOAttrs('at')]
|
||||
|
||||
class LeagueType(Enum):
|
||||
|
|
@ -119,6 +127,7 @@ class ClassicAccountLiveData:
|
|||
|
||||
inbox_count: Annotated[int, IOAttrs('ibc')]
|
||||
inbox_count_is_max: Annotated[bool, IOAttrs('ibcm')]
|
||||
inbox_contains_prize: Annotated[bool, IOAttrs('icp')]
|
||||
|
||||
chests: Annotated[dict[str, Chest], IOAttrs('c')]
|
||||
|
||||
|
|
@ -341,64 +350,6 @@ class ChestInfoResponse(Response):
|
|||
user_tokens: Annotated[int | None, IOAttrs('t')]
|
||||
|
||||
|
||||
@ioprepped
|
||||
@dataclass
|
||||
class ChestActionMessage(Message):
|
||||
"""Request action about a chest."""
|
||||
|
||||
class Action(Enum):
|
||||
"""Types of actions we can request."""
|
||||
|
||||
# Unlocking (for free or with tokens).
|
||||
UNLOCK = 'u'
|
||||
|
||||
# Watched an ad to reduce wait.
|
||||
AD = 'ad'
|
||||
|
||||
action: Annotated[Action, IOAttrs('a')]
|
||||
|
||||
# Tokens we are paying (only applies to unlock).
|
||||
token_payment: Annotated[int, IOAttrs('t')]
|
||||
|
||||
chest_id: Annotated[str, IOAttrs('i')]
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
def get_response_types(cls) -> list[type[Response] | None]:
|
||||
return [ChestActionResponse]
|
||||
|
||||
|
||||
@ioprepped
|
||||
@dataclass
|
||||
class ChestActionResponse(Response):
|
||||
"""Here's the results of that action you asked for, boss."""
|
||||
|
||||
# Tokens that were actually charged.
|
||||
tokens_charged: Annotated[int, IOAttrs('t')] = 0
|
||||
|
||||
# If present, signifies the chest has been opened and we should show
|
||||
# the user this stuff that was in it.
|
||||
contents: Annotated[list[DisplayItemWrapper] | None, IOAttrs('c')] = None
|
||||
|
||||
# If contents are present, which of the chest's prize-sets they
|
||||
# represent.
|
||||
prizeindex: Annotated[int, IOAttrs('i')] = 0
|
||||
|
||||
# Printable error if something goes wrong.
|
||||
error: Annotated[str | None, IOAttrs('e')] = None
|
||||
|
||||
# Printable warning. Shown in orange with an error sound. Does not
|
||||
# mean the action failed; only that there's something to tell the
|
||||
# users such as 'It looks like you are faking ad views; stop it or
|
||||
# you won't have ad options anymore.'
|
||||
warning: Annotated[str | None, IOAttrs('w')] = None
|
||||
|
||||
# Printable success message. Shown in green with a cash-register
|
||||
# sound. Can be used for things like successful wait reductions via
|
||||
# ad views.
|
||||
success_msg: Annotated[str | None, IOAttrs('s')] = None
|
||||
|
||||
|
||||
class ClientUITypeID(Enum):
|
||||
"""Type ID for each of our subclasses."""
|
||||
|
||||
|
|
@ -717,6 +668,9 @@ class ClientEffectTypeID(Enum):
|
|||
SCREEN_MESSAGE = 'm'
|
||||
SOUND = 's'
|
||||
DELAY = 'd'
|
||||
CHEST_WAIT_TIME_ANIMATION = 't'
|
||||
TICKETS_ANIMATION = 'ta'
|
||||
TOKENS_ANIMATION = 'toa'
|
||||
|
||||
|
||||
class ClientEffect(IOMultiType[ClientEffectTypeID]):
|
||||
|
|
@ -738,6 +692,7 @@ class ClientEffect(IOMultiType[ClientEffectTypeID]):
|
|||
def get_type(cls, type_id: ClientEffectTypeID) -> type[ClientEffect]:
|
||||
"""Return the subclass for each of our type-ids."""
|
||||
# pylint: disable=cyclic-import
|
||||
# pylint: disable=too-many-return-statements
|
||||
|
||||
t = ClientEffectTypeID
|
||||
if type_id is t.UNKNOWN:
|
||||
|
|
@ -748,6 +703,12 @@ class ClientEffect(IOMultiType[ClientEffectTypeID]):
|
|||
return ClientEffectSound
|
||||
if type_id is t.DELAY:
|
||||
return ClientEffectDelay
|
||||
if type_id is t.CHEST_WAIT_TIME_ANIMATION:
|
||||
return ClientEffectChestWaitTimeAnimation
|
||||
if type_id is t.TICKETS_ANIMATION:
|
||||
return ClientEffectTicketsAnimation
|
||||
if type_id is t.TOKENS_ANIMATION:
|
||||
return ClientEffectTokensAnimation
|
||||
|
||||
# Important to make sure we provide all types.
|
||||
assert_never(type_id)
|
||||
|
|
@ -809,6 +770,52 @@ class ClientEffectSound(ClientEffect):
|
|||
return ClientEffectTypeID.SOUND
|
||||
|
||||
|
||||
@ioprepped
|
||||
@dataclass
|
||||
class ClientEffectChestWaitTimeAnimation(ClientEffect):
|
||||
"""Animate chest wait time changing."""
|
||||
|
||||
chestid: Annotated[str, IOAttrs('c')]
|
||||
duration: Annotated[float, IOAttrs('u')]
|
||||
startvalue: Annotated[datetime.datetime, IOAttrs('o')]
|
||||
endvalue: Annotated[datetime.datetime, IOAttrs('n')]
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
def get_type_id(cls) -> ClientEffectTypeID:
|
||||
return ClientEffectTypeID.CHEST_WAIT_TIME_ANIMATION
|
||||
|
||||
|
||||
@ioprepped
|
||||
@dataclass
|
||||
class ClientEffectTicketsAnimation(ClientEffect):
|
||||
"""Animate tickets count."""
|
||||
|
||||
duration: Annotated[float, IOAttrs('u')]
|
||||
startvalue: Annotated[int, IOAttrs('s')]
|
||||
endvalue: Annotated[int, IOAttrs('e')]
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
def get_type_id(cls) -> ClientEffectTypeID:
|
||||
return ClientEffectTypeID.TICKETS_ANIMATION
|
||||
|
||||
|
||||
@ioprepped
|
||||
@dataclass
|
||||
class ClientEffectTokensAnimation(ClientEffect):
|
||||
"""Animate tokens count."""
|
||||
|
||||
duration: Annotated[float, IOAttrs('u')]
|
||||
startvalue: Annotated[int, IOAttrs('s')]
|
||||
endvalue: Annotated[int, IOAttrs('e')]
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
def get_type_id(cls) -> ClientEffectTypeID:
|
||||
return ClientEffectTypeID.TOKENS_ANIMATION
|
||||
|
||||
|
||||
@ioprepped
|
||||
@dataclass
|
||||
class ClientEffectDelay(ClientEffect):
|
||||
|
|
@ -885,3 +892,68 @@ class ScoreSubmitResponse(Response):
|
|||
|
||||
# Things we should show on our end.
|
||||
effects: Annotated[list[ClientEffect], IOAttrs('fx')]
|
||||
|
||||
|
||||
@ioprepped
|
||||
@dataclass
|
||||
class ChestActionMessage(Message):
|
||||
"""Request action about a chest."""
|
||||
|
||||
class Action(Enum):
|
||||
"""Types of actions we can request."""
|
||||
|
||||
# Unlocking (for free or with tokens).
|
||||
UNLOCK = 'u'
|
||||
|
||||
# Watched an ad to reduce wait.
|
||||
AD = 'ad'
|
||||
|
||||
action: Annotated[Action, IOAttrs('a')]
|
||||
|
||||
# Tokens we are paying (only applies to unlock).
|
||||
token_payment: Annotated[int, IOAttrs('t')]
|
||||
|
||||
chest_id: Annotated[str, IOAttrs('i')]
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
def get_response_types(cls) -> list[type[Response] | None]:
|
||||
return [ChestActionResponse]
|
||||
|
||||
|
||||
@ioprepped
|
||||
@dataclass
|
||||
class ChestActionResponse(Response):
|
||||
"""Here's the results of that action you asked for, boss."""
|
||||
|
||||
# Tokens that were actually charged.
|
||||
tokens_charged: Annotated[int, IOAttrs('t')] = 0
|
||||
|
||||
# If present, signifies the chest has been opened and we should show
|
||||
# the user this stuff that was in it.
|
||||
contents: Annotated[list[DisplayItemWrapper] | None, IOAttrs('c')] = None
|
||||
|
||||
# If contents are present, which of the chest's prize-sets they
|
||||
# represent.
|
||||
prizeindex: Annotated[int, IOAttrs('i')] = 0
|
||||
|
||||
# Printable error if something goes wrong.
|
||||
error: Annotated[str | None, IOAttrs('e')] = None
|
||||
|
||||
# Printable warning. Shown in orange with an error sound. Does not
|
||||
# mean the action failed; only that there's something to tell the
|
||||
# users such as 'It looks like you are faking ad views; stop it or
|
||||
# you won't have ad options anymore.'
|
||||
warning: Annotated[str | None, IOAttrs('w', store_default=False)] = None
|
||||
|
||||
# Printable success message. Shown in green with a cash-register
|
||||
# sound. Can be used for things like successful wait reductions via
|
||||
# ad views. Used in builds earlier than 22311; can remove once
|
||||
# 22311+ is ubiquitous.
|
||||
success_msg: Annotated[str | None, IOAttrs('s', store_default=False)] = None
|
||||
|
||||
# Effects to show on the client. Replaces warning and success_msg in
|
||||
# build 22311 or newer.
|
||||
effects: Annotated[
|
||||
list[ClientEffect], IOAttrs('fx', store_default=False)
|
||||
] = field(default_factory=list)
|
||||
|
|
|
|||
43
dist/ba_data/python/bacommon/cloud.py
vendored
43
dist/ba_data/python/bacommon/cloud.py
vendored
|
|
@ -10,6 +10,7 @@ from typing import TYPE_CHECKING, Annotated, override
|
|||
|
||||
from efro.message import Message, Response
|
||||
from efro.dataclassio import ioprepped, IOAttrs
|
||||
from bacommon.securedata import SecureDataChecker
|
||||
from bacommon.transfer import DirectoryManifest
|
||||
from bacommon.login import LoginType
|
||||
|
||||
|
|
@ -300,3 +301,45 @@ class StoreQueryResponse(Response):
|
|||
|
||||
available_purchases: Annotated[list[Purchase], IOAttrs('p')]
|
||||
token_info_url: Annotated[str, IOAttrs('tiu')]
|
||||
|
||||
|
||||
@ioprepped
|
||||
@dataclass
|
||||
class SecureDataCheckMessage(Message):
|
||||
"""Was this data signed by the master-server?."""
|
||||
|
||||
data: Annotated[bytes, IOAttrs('d')]
|
||||
signature: Annotated[bytes, IOAttrs('s')]
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
def get_response_types(cls) -> list[type[Response] | None]:
|
||||
return [SecureDataCheckResponse]
|
||||
|
||||
|
||||
@ioprepped
|
||||
@dataclass
|
||||
class SecureDataCheckResponse(Response):
|
||||
"""Here's the result of that data check, boss."""
|
||||
|
||||
# Whether the data signature was valid.
|
||||
result: Annotated[bool, IOAttrs('v')]
|
||||
|
||||
|
||||
@ioprepped
|
||||
@dataclass
|
||||
class SecureDataCheckerRequest(Message):
|
||||
"""Can I get a checker over here?."""
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
def get_response_types(cls) -> list[type[Response] | None]:
|
||||
return [SecureDataCheckerResponse]
|
||||
|
||||
|
||||
@ioprepped
|
||||
@dataclass
|
||||
class SecureDataCheckerResponse(Response):
|
||||
"""Here's that checker ya asked for, boss."""
|
||||
|
||||
checker: Annotated[SecureDataChecker, IOAttrs('c')]
|
||||
|
|
|
|||
23
dist/ba_data/python/bacommon/locale.py
vendored
Normal file
23
dist/ba_data/python/bacommon/locale.py
vendored
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Functionality for wrangling locale info."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import Enum
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
pass
|
||||
|
||||
|
||||
class Locale(Enum):
|
||||
"""A distinct combination of language and possibly country/etc.
|
||||
|
||||
Note that some locales here may be superseded by other more specific
|
||||
ones (for instance PORTUGUESE -> PORTUGUESE_BRAZIL), but the
|
||||
originals must continue to exist here since they may remain in use
|
||||
in the wild.
|
||||
"""
|
||||
|
||||
ENGLISH = 'en'
|
||||
10
dist/ba_data/python/bacommon/login.py
vendored
10
dist/ba_data/python/bacommon/login.py
vendored
|
|
@ -20,18 +20,18 @@ if TYPE_CHECKING:
|
|||
class LoginType(Enum):
|
||||
"""Types of logins available."""
|
||||
|
||||
# Email/password
|
||||
#: Email/password
|
||||
EMAIL = 'email'
|
||||
|
||||
# Google Play Game Services
|
||||
#: Google Play Game Services
|
||||
GPGS = 'gpgs'
|
||||
|
||||
# Apple's Game Center
|
||||
#: Apple's Game Center
|
||||
GAME_CENTER = 'game_center'
|
||||
|
||||
@property
|
||||
def displayname(self) -> str:
|
||||
"""Human readable name for this value."""
|
||||
"""A human readable name for this value."""
|
||||
cls = type(self)
|
||||
match self:
|
||||
case cls.EMAIL:
|
||||
|
|
@ -43,7 +43,7 @@ class LoginType(Enum):
|
|||
|
||||
@property
|
||||
def displaynameshort(self) -> str:
|
||||
"""Human readable name for this value."""
|
||||
"""A short human readable name for this value."""
|
||||
cls = type(self)
|
||||
match self:
|
||||
case cls.EMAIL:
|
||||
|
|
|
|||
56
dist/ba_data/python/bacommon/securedata.py
vendored
Normal file
56
dist/ba_data/python/bacommon/securedata.py
vendored
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Functionality related to verifying ballistica server generated data."""
|
||||
|
||||
import datetime
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Annotated
|
||||
|
||||
from efro.util import utc_now
|
||||
from efro.dataclassio import ioprepped, IOAttrs
|
||||
|
||||
if TYPE_CHECKING:
|
||||
pass
|
||||
|
||||
|
||||
@ioprepped
|
||||
@dataclass
|
||||
class SecureDataChecker:
|
||||
"""Verifies data as being signed by our master server."""
|
||||
|
||||
# Time period this checker is valid for.
|
||||
starttime: Annotated[datetime.datetime, IOAttrs('s')]
|
||||
endtime: Annotated[datetime.datetime, IOAttrs('e')]
|
||||
|
||||
# Current set of public keys.
|
||||
publickeys: Annotated[list[bytes], IOAttrs('k')]
|
||||
|
||||
def check(self, data: bytes, signature: bytes) -> bool:
|
||||
"""Verify data, returning True if successful.
|
||||
|
||||
Note that this call imports and uses the cryptography module and
|
||||
can be slow; it generally should be done in a background thread
|
||||
or on a server.
|
||||
"""
|
||||
from cryptography.hazmat.primitives.asymmetric import ed25519
|
||||
from cryptography.exceptions import InvalidSignature
|
||||
|
||||
now = utc_now()
|
||||
|
||||
# Make sure we seem valid based on local time.
|
||||
if now < self.starttime:
|
||||
raise RuntimeError('SecureDataChecker starttime is in the future.')
|
||||
if now > self.endtime:
|
||||
raise RuntimeError('SecureDataChecker endtime is in the past.')
|
||||
|
||||
# Try our keys from newest to oldest. Most stuff will be using
|
||||
# the newest key so this should be most efficient.
|
||||
for key in reversed(self.publickeys):
|
||||
try:
|
||||
publickey = ed25519.Ed25519PublicKey.from_public_bytes(key)
|
||||
publickey.verify(signature, data)
|
||||
return True
|
||||
except InvalidSignature:
|
||||
pass
|
||||
|
||||
return False
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Workspace functionality."""
|
||||
"""Functionality related to ballistica.net workspaces."""
|
||||
|
|
|
|||
45
dist/ba_data/python/baenv.py
vendored
45
dist/ba_data/python/baenv.py
vendored
|
|
@ -1,11 +1,11 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Manage ballistica execution environment.
|
||||
"""Manage Ballistica execution environment.
|
||||
|
||||
This module is used to set up and/or check the global Python environment
|
||||
before running a ballistica app. This includes things such as paths,
|
||||
before running a Ballistica app. This includes things such as paths,
|
||||
logging, and app-dirs. Because these things are global in nature, this
|
||||
should be done before any ballistica modules are imported.
|
||||
should be done before any Ballistica modules are imported.
|
||||
|
||||
This module can also be exec'ed directly to set up a default environment
|
||||
and then run the app.
|
||||
|
|
@ -53,46 +53,46 @@ if TYPE_CHECKING:
|
|||
|
||||
# Build number and version of the ballistica binary we expect to be
|
||||
# using.
|
||||
TARGET_BALLISTICA_BUILD = 22278
|
||||
TARGET_BALLISTICA_VERSION = '1.7.37'
|
||||
TARGET_BALLISTICA_BUILD = 22350
|
||||
TARGET_BALLISTICA_VERSION = '1.7.39'
|
||||
|
||||
|
||||
@dataclass
|
||||
class EnvConfig:
|
||||
"""Final config values we provide to the engine."""
|
||||
|
||||
# Where app config/state data lives.
|
||||
#: Where app config/state data lives.
|
||||
config_dir: str
|
||||
|
||||
# Directory containing ba_data and any other platform-specific data.
|
||||
#: Directory containing ba_data and any other platform-specific data.
|
||||
data_dir: str
|
||||
|
||||
# Where the app's built-in Python stuff lives.
|
||||
#: Where the app's built-in Python stuff lives.
|
||||
app_python_dir: str | None
|
||||
|
||||
# Where the app's built-in Python stuff lives in the default case.
|
||||
#: Where the app's built-in Python stuff lives in the default case.
|
||||
standard_app_python_dir: str
|
||||
|
||||
# Where the app's bundled third party Python stuff lives.
|
||||
#: Where the app's bundled third party Python stuff lives.
|
||||
site_python_dir: str | None
|
||||
|
||||
# Custom Python provided by the user (mods).
|
||||
#: Custom Python provided by the user (mods).
|
||||
user_python_dir: str | None
|
||||
|
||||
# We have a mechanism allowing app scripts to be overridden by
|
||||
# placing a specially named directory in a user-scripts dir.
|
||||
# This is true if that is enabled.
|
||||
#: We have a mechanism allowing app scripts to be overridden by
|
||||
#: placing a specially named directory in a user-scripts dir. This is
|
||||
#: true if that is enabled.
|
||||
is_user_app_python_dir: bool
|
||||
|
||||
# Our fancy app log handler. This handles feeding logs, stdout, and
|
||||
# stderr into the engine so they show up on in-app consoles, etc.
|
||||
#: Our fancy app log handler. This handles feeding logs, stdout, and
|
||||
#: stderr into the engine so they show up on in-app consoles, etc.
|
||||
log_handler: LogHandler | None
|
||||
|
||||
# Initial data from the config.json file in the config dir. The
|
||||
# config file is parsed by
|
||||
#: Initial data from the config.json file in the config dir. The
|
||||
#: config file is parsed by
|
||||
initial_app_config: Any
|
||||
|
||||
# Timestamp when we first started doing stuff.
|
||||
#: Timestamp when we first started doing stuff.
|
||||
launch_time: float
|
||||
|
||||
|
||||
|
|
@ -100,10 +100,9 @@ class EnvConfig:
|
|||
class _EnvGlobals:
|
||||
"""Globals related to baenv's operation.
|
||||
|
||||
We store this in __main__ instead of in our own module because it
|
||||
is likely that multiple versions of our module will be spun up
|
||||
and we want a single set of globals (see notes at top of our module
|
||||
code).
|
||||
We store this in __main__ instead of in our own module because it is
|
||||
likely that multiple versions of our module will be spun up and we
|
||||
want a single set of globals (see notes at top of our module code).
|
||||
"""
|
||||
|
||||
config: EnvConfig | None = None
|
||||
|
|
|
|||
6
dist/ba_data/python/baplus/__init__.py
vendored
6
dist/ba_data/python/baplus/__init__.py
vendored
|
|
@ -3,10 +3,10 @@
|
|||
"""Closed-source bits of ballistica.
|
||||
|
||||
This code concerns sensitive things like accounts and master-server
|
||||
communication so the native C++ parts of it remain closed. Native
|
||||
communication, so the native C++ parts of it remain closed. Native
|
||||
precompiled static libraries of this portion are provided for those who
|
||||
want to compile the rest of the engine, or a fully open-source app
|
||||
can also be built by removing this feature-set.
|
||||
want to compile the rest of the engine, or a fully open-source app can
|
||||
also be built by removing this feature-set.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
|
|||
135
dist/ba_data/python/baplus/_appsubsystem.py
vendored
135
dist/ba_data/python/baplus/_appsubsystem.py
vendored
|
|
@ -21,10 +21,10 @@ if TYPE_CHECKING:
|
|||
class PlusAppSubsystem(AppSubsystem):
|
||||
"""Subsystem for plus functionality in the app.
|
||||
|
||||
The single shared instance of this app can be accessed at
|
||||
babase.app.plus. Note that it is possible for this to be None if the
|
||||
plus package is not present, and code should handle that case
|
||||
gracefully.
|
||||
Access the single shared instance of this class via the
|
||||
:attr:`~babase.App.plus` attr on the :class:`~babase.App` class.
|
||||
Note that it is possible for this to be ``None`` if the plus package
|
||||
is not present, so code should handle that case gracefully.
|
||||
"""
|
||||
|
||||
# pylint: disable=too-many-public-methods
|
||||
|
|
@ -38,6 +38,7 @@ class PlusAppSubsystem(AppSubsystem):
|
|||
|
||||
@override
|
||||
def on_app_loading(self) -> None:
|
||||
""":meta private:"""
|
||||
_baplus.on_app_loading()
|
||||
self.accounts.on_app_loading()
|
||||
|
||||
|
|
@ -45,173 +46,164 @@ class PlusAppSubsystem(AppSubsystem):
|
|||
def add_v1_account_transaction(
|
||||
transaction: dict, callback: Callable | None = None
|
||||
) -> None:
|
||||
"""(internal)"""
|
||||
""":meta private:"""
|
||||
return _baplus.add_v1_account_transaction(transaction, callback)
|
||||
|
||||
@staticmethod
|
||||
def game_service_has_leaderboard(game: str, config: str) -> bool:
|
||||
"""(internal)
|
||||
"""Given a game and config string, returns whether there is a
|
||||
leaderboard for it on the game service.
|
||||
|
||||
Given a game and config string, returns whether there is a leaderboard
|
||||
for it on the game service.
|
||||
:meta private:
|
||||
"""
|
||||
return _baplus.game_service_has_leaderboard(game, config)
|
||||
|
||||
@staticmethod
|
||||
def get_master_server_address(source: int = -1, version: int = 1) -> str:
|
||||
"""(internal)
|
||||
"""Return the address of the master server.
|
||||
|
||||
Return the address of the master server.
|
||||
:meta private:
|
||||
"""
|
||||
return _baplus.get_master_server_address(source, version)
|
||||
|
||||
@staticmethod
|
||||
def get_classic_news_show() -> str:
|
||||
"""(internal)"""
|
||||
""":meta private:"""
|
||||
return _baplus.get_classic_news_show()
|
||||
|
||||
@staticmethod
|
||||
def get_price(item: str) -> str | None:
|
||||
"""(internal)"""
|
||||
""":meta private:"""
|
||||
return _baplus.get_price(item)
|
||||
|
||||
@staticmethod
|
||||
def get_v1_account_product_purchased(item: str) -> bool:
|
||||
"""(internal)"""
|
||||
""":meta private:"""
|
||||
return _baplus.get_v1_account_product_purchased(item)
|
||||
|
||||
@staticmethod
|
||||
def get_v1_account_product_purchases_state() -> int:
|
||||
"""(internal)"""
|
||||
""":meta private:"""
|
||||
return _baplus.get_v1_account_product_purchases_state()
|
||||
|
||||
@staticmethod
|
||||
def get_v1_account_display_string(full: bool = True) -> str:
|
||||
"""(internal)"""
|
||||
""":meta private:"""
|
||||
return _baplus.get_v1_account_display_string(full)
|
||||
|
||||
@staticmethod
|
||||
def get_v1_account_misc_read_val(name: str, default_value: Any) -> Any:
|
||||
"""(internal)"""
|
||||
""":meta private:"""
|
||||
return _baplus.get_v1_account_misc_read_val(name, default_value)
|
||||
|
||||
@staticmethod
|
||||
def get_v1_account_misc_read_val_2(name: str, default_value: Any) -> Any:
|
||||
"""(internal)"""
|
||||
""":meta private:"""
|
||||
return _baplus.get_v1_account_misc_read_val_2(name, default_value)
|
||||
|
||||
@staticmethod
|
||||
def get_v1_account_misc_val(name: str, default_value: Any) -> Any:
|
||||
"""(internal)"""
|
||||
""":meta private:"""
|
||||
return _baplus.get_v1_account_misc_val(name, default_value)
|
||||
|
||||
@staticmethod
|
||||
def get_v1_account_name() -> str:
|
||||
"""(internal)"""
|
||||
""":meta private:"""
|
||||
return _baplus.get_v1_account_name()
|
||||
|
||||
@staticmethod
|
||||
def get_v1_account_public_login_id() -> str | None:
|
||||
"""(internal)"""
|
||||
""":meta private:"""
|
||||
return _baplus.get_v1_account_public_login_id()
|
||||
|
||||
@staticmethod
|
||||
def get_v1_account_state() -> str:
|
||||
"""(internal)"""
|
||||
""":meta private:"""
|
||||
return _baplus.get_v1_account_state()
|
||||
|
||||
@staticmethod
|
||||
def get_v1_account_state_num() -> int:
|
||||
"""(internal)"""
|
||||
""":meta private:"""
|
||||
return _baplus.get_v1_account_state_num()
|
||||
|
||||
@staticmethod
|
||||
def get_v1_account_ticket_count() -> int:
|
||||
"""(internal)
|
||||
"""Return the number of tickets for the current account.
|
||||
|
||||
Return the number of tickets for the current account.
|
||||
:meta private:
|
||||
"""
|
||||
return _baplus.get_v1_account_ticket_count()
|
||||
|
||||
@staticmethod
|
||||
def get_v1_account_type() -> str:
|
||||
"""(internal)"""
|
||||
""":meta private:"""
|
||||
return _baplus.get_v1_account_type()
|
||||
|
||||
@staticmethod
|
||||
def get_v2_fleet() -> str:
|
||||
"""(internal)"""
|
||||
""":meta private:"""
|
||||
return _baplus.get_v2_fleet()
|
||||
|
||||
@staticmethod
|
||||
def have_outstanding_v1_account_transactions() -> bool:
|
||||
"""(internal)"""
|
||||
""":meta private:"""
|
||||
return _baplus.have_outstanding_v1_account_transactions()
|
||||
|
||||
@staticmethod
|
||||
def in_game_purchase(item: str, price: int) -> None:
|
||||
"""(internal)"""
|
||||
""":meta private:"""
|
||||
return _baplus.in_game_purchase(item, price)
|
||||
|
||||
@staticmethod
|
||||
def is_blessed() -> bool:
|
||||
"""(internal)"""
|
||||
""":meta private:"""
|
||||
return _baplus.is_blessed()
|
||||
|
||||
@staticmethod
|
||||
def mark_config_dirty() -> None:
|
||||
"""(internal)
|
||||
|
||||
Category: General Utility Functions
|
||||
"""
|
||||
""":meta private:"""
|
||||
return _baplus.mark_config_dirty()
|
||||
|
||||
@staticmethod
|
||||
def power_ranking_query(callback: Callable, season: Any = None) -> None:
|
||||
"""(internal)"""
|
||||
""":meta private:"""
|
||||
return _baplus.power_ranking_query(callback, season)
|
||||
|
||||
@staticmethod
|
||||
def purchase(item: str) -> None:
|
||||
"""(internal)"""
|
||||
""":meta private:"""
|
||||
return _baplus.purchase(item)
|
||||
|
||||
@staticmethod
|
||||
def report_achievement(
|
||||
achievement: str, pass_to_account: bool = True
|
||||
) -> None:
|
||||
"""(internal)"""
|
||||
""":meta private:"""
|
||||
return _baplus.report_achievement(achievement, pass_to_account)
|
||||
|
||||
@staticmethod
|
||||
def reset_achievements() -> None:
|
||||
"""(internal)"""
|
||||
""":meta private:"""
|
||||
return _baplus.reset_achievements()
|
||||
|
||||
@staticmethod
|
||||
def restore_purchases() -> None:
|
||||
"""(internal)"""
|
||||
""":meta private:"""
|
||||
return _baplus.restore_purchases()
|
||||
|
||||
@staticmethod
|
||||
def run_v1_account_transactions() -> None:
|
||||
"""(internal)"""
|
||||
""":meta private:"""
|
||||
return _baplus.run_v1_account_transactions()
|
||||
|
||||
@staticmethod
|
||||
def sign_in_v1(account_type: str) -> None:
|
||||
"""(internal)
|
||||
|
||||
Category: General Utility Functions
|
||||
"""
|
||||
""":meta private:"""
|
||||
return _baplus.sign_in_v1(account_type)
|
||||
|
||||
@staticmethod
|
||||
def sign_out_v1(v2_embedded: bool = False) -> None:
|
||||
"""(internal)
|
||||
|
||||
Category: General Utility Functions
|
||||
"""
|
||||
""":meta private:"""
|
||||
return _baplus.sign_out_v1(v2_embedded)
|
||||
|
||||
@staticmethod
|
||||
|
|
@ -228,12 +220,14 @@ class PlusAppSubsystem(AppSubsystem):
|
|||
campaign: str | None = None,
|
||||
level: str | None = None,
|
||||
) -> None:
|
||||
"""(internal)
|
||||
"""Submit a score to the server.
|
||||
|
||||
Submit a score to the server; callback will be called with the results.
|
||||
As a courtesy, please don't send fake scores to the server. I'd prefer
|
||||
to devote my time to improving the game instead of trying to make the
|
||||
score server more mischief-proof.
|
||||
Callback will be called with the results. As a courtesy, please
|
||||
don't send fake scores to the server. I'd prefer to devote my
|
||||
time to improving the game instead of trying to make the score
|
||||
server more mischief-proof.
|
||||
|
||||
:meta private:
|
||||
"""
|
||||
return _baplus.submit_score(
|
||||
game,
|
||||
|
|
@ -252,41 +246,59 @@ class PlusAppSubsystem(AppSubsystem):
|
|||
def tournament_query(
|
||||
callback: Callable[[dict | None], None], args: dict
|
||||
) -> None:
|
||||
"""(internal)"""
|
||||
""":meta private:"""
|
||||
return _baplus.tournament_query(callback, args)
|
||||
|
||||
@staticmethod
|
||||
def supports_purchases() -> bool:
|
||||
"""Does this platform support in-app-purchases?"""
|
||||
"""Does this platform support in-app-purchases?
|
||||
|
||||
:meta private:
|
||||
"""
|
||||
return _baplus.supports_purchases()
|
||||
|
||||
@staticmethod
|
||||
def have_incentivized_ad() -> bool:
|
||||
"""Is an incentivized ad available?"""
|
||||
"""Is an incentivized ad available?
|
||||
|
||||
:meta private:
|
||||
"""
|
||||
return _baplus.have_incentivized_ad()
|
||||
|
||||
@staticmethod
|
||||
def has_video_ads() -> bool:
|
||||
"""Are video ads available?"""
|
||||
"""Are video ads available?
|
||||
|
||||
:meta private:
|
||||
"""
|
||||
return _baplus.has_video_ads()
|
||||
|
||||
@staticmethod
|
||||
def can_show_ad() -> bool:
|
||||
"""Can we show an ad?"""
|
||||
"""Can we show an ad?
|
||||
|
||||
:meta private:
|
||||
"""
|
||||
return _baplus.can_show_ad()
|
||||
|
||||
@staticmethod
|
||||
def show_ad(
|
||||
purpose: str, on_completion_call: Callable[[], None] | None = None
|
||||
) -> None:
|
||||
"""Show an ad."""
|
||||
"""Show an ad.
|
||||
|
||||
:meta private:
|
||||
"""
|
||||
_baplus.show_ad(purpose, on_completion_call)
|
||||
|
||||
@staticmethod
|
||||
def show_ad_2(
|
||||
purpose: str, on_completion_call: Callable[[bool], None] | None = None
|
||||
) -> None:
|
||||
"""Show an ad."""
|
||||
"""Show an ad.
|
||||
|
||||
:meta private:
|
||||
"""
|
||||
_baplus.show_ad_2(purpose, on_completion_call)
|
||||
|
||||
@staticmethod
|
||||
|
|
@ -295,5 +307,8 @@ class PlusAppSubsystem(AppSubsystem):
|
|||
game: str | None = None,
|
||||
game_version: str | None = None,
|
||||
) -> None:
|
||||
"""Show game-service provided UI."""
|
||||
"""Show game-service provided UI.
|
||||
|
||||
:meta private:
|
||||
"""
|
||||
_baplus.show_game_service_ui(show, game, game_version)
|
||||
|
|
|
|||
56
dist/ba_data/python/baplus/_cloud.py
vendored
56
dist/ba_data/python/baplus/_cloud.py
vendored
|
|
@ -24,7 +24,12 @@ if TYPE_CHECKING:
|
|||
|
||||
|
||||
class CloudSubsystem(babase.AppSubsystem):
|
||||
"""Manages communication with cloud components."""
|
||||
"""Manages communication with cloud components.
|
||||
|
||||
Access the shared single instance of this class via the
|
||||
:attr:`~baplus.PlusAppSubsystem.cloud` attr on the
|
||||
:class:`~baplus.PlusAppSubsystem` class.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
|
|
@ -34,19 +39,25 @@ class CloudSubsystem(babase.AppSubsystem):
|
|||
|
||||
@property
|
||||
def connected(self) -> bool:
|
||||
"""Property equivalent of CloudSubsystem.is_connected()."""
|
||||
return self.is_connected()
|
||||
|
||||
def is_connected(self) -> bool:
|
||||
"""Return whether a connection to the cloud is present.
|
||||
"""Whether a connection to the cloud is present.
|
||||
|
||||
This is a good indicator (though not for certain) that sending
|
||||
messages will succeed.
|
||||
"""
|
||||
return False # Needs to be overridden
|
||||
return self.is_connected()
|
||||
|
||||
def is_connected(self) -> bool:
|
||||
"""Implementation for connected attr.
|
||||
|
||||
:meta private:
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def on_connectivity_changed(self, connected: bool) -> None:
|
||||
"""Called when cloud connectivity state changes."""
|
||||
"""Called when cloud connectivity state changes.
|
||||
|
||||
:meta private:
|
||||
"""
|
||||
babase.balog.debug('Connectivity is now %s.', connected)
|
||||
|
||||
plus = babase.app.plus
|
||||
|
|
@ -172,6 +183,24 @@ class CloudSubsystem(babase.AppSubsystem):
|
|||
],
|
||||
) -> None: ...
|
||||
|
||||
@overload
|
||||
def send_message_cb(
|
||||
self,
|
||||
msg: bacommon.cloud.SecureDataCheckMessage,
|
||||
on_response: Callable[
|
||||
[bacommon.cloud.SecureDataCheckResponse | Exception], None
|
||||
],
|
||||
) -> None: ...
|
||||
|
||||
@overload
|
||||
def send_message_cb(
|
||||
self,
|
||||
msg: bacommon.cloud.SecureDataCheckerRequest,
|
||||
on_response: Callable[
|
||||
[bacommon.cloud.SecureDataCheckerResponse | Exception], None
|
||||
],
|
||||
) -> None: ...
|
||||
|
||||
def send_message_cb(
|
||||
self,
|
||||
msg: Message,
|
||||
|
|
@ -179,7 +208,7 @@ class CloudSubsystem(babase.AppSubsystem):
|
|||
) -> None:
|
||||
"""Asynchronously send a message to the cloud from the logic thread.
|
||||
|
||||
The provided on_response call will be run in the logic thread
|
||||
The provided ``on_response`` call will be run in the logic thread
|
||||
and passed either the response or the error that occurred.
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
|
|
@ -221,7 +250,7 @@ class CloudSubsystem(babase.AppSubsystem):
|
|||
) -> bacommon.cloud.TestResponse: ...
|
||||
|
||||
async def send_message_async(self, msg: Message) -> Response | None:
|
||||
"""Synchronously send a message to the cloud.
|
||||
"""Asynchronously send a message to the cloud.
|
||||
|
||||
Must be called from the logic thread.
|
||||
"""
|
||||
|
|
@ -232,7 +261,10 @@ class CloudSubsystem(babase.AppSubsystem):
|
|||
def subscribe_test(
|
||||
self, updatecall: Callable[[int | None], None]
|
||||
) -> babase.CloudSubscription:
|
||||
"""Subscribe to some test data."""
|
||||
"""Subscribe to some test data.
|
||||
|
||||
:meta private:
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
'Cloud functionality is not present in this build.'
|
||||
)
|
||||
|
|
@ -250,6 +282,8 @@ class CloudSubsystem(babase.AppSubsystem):
|
|||
"""Unsubscribe from some subscription.
|
||||
|
||||
Do not call this manually; it is called by CloudSubscription.
|
||||
|
||||
:meta private:
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
'Cloud functionality is not present in this build.'
|
||||
|
|
|
|||
11
dist/ba_data/python/bascenev1/__init__.py
vendored
11
dist/ba_data/python/bascenev1/__init__.py
vendored
|
|
@ -18,12 +18,15 @@ import logging
|
|||
|
||||
from efro.util import set_canonical_module_names
|
||||
from babase import (
|
||||
ActivityNotFoundError,
|
||||
add_clean_frame_callback,
|
||||
app,
|
||||
App,
|
||||
AppIntent,
|
||||
AppIntentDefault,
|
||||
AppIntentExec,
|
||||
AppMode,
|
||||
AppState,
|
||||
apptime,
|
||||
AppTime,
|
||||
apptimer,
|
||||
|
|
@ -52,6 +55,7 @@ from babase import (
|
|||
safecolor,
|
||||
screenmessage,
|
||||
set_analytics_screen,
|
||||
SessionNotFoundError,
|
||||
storagename,
|
||||
timestring,
|
||||
UIScale,
|
||||
|
|
@ -164,7 +168,7 @@ from bascenev1._dependency import (
|
|||
from bascenev1._dualteamsession import DualTeamSession
|
||||
from bascenev1._freeforallsession import FreeForAllSession
|
||||
from bascenev1._gameactivity import GameActivity
|
||||
from bascenev1._gameresults import GameResults
|
||||
from bascenev1._gameresults import GameResults, WinnerGroup
|
||||
from bascenev1._gameutils import (
|
||||
animate,
|
||||
animate_array,
|
||||
|
|
@ -246,15 +250,18 @@ from bascenev1._teamgame import TeamGameActivity
|
|||
__all__ = [
|
||||
'Activity',
|
||||
'ActivityData',
|
||||
'ActivityNotFoundError',
|
||||
'Actor',
|
||||
'animate',
|
||||
'animate_array',
|
||||
'add_clean_frame_callback',
|
||||
'app',
|
||||
'App',
|
||||
'AppIntent',
|
||||
'AppIntentDefault',
|
||||
'AppIntentExec',
|
||||
'AppMode',
|
||||
'AppState',
|
||||
'AppTime',
|
||||
'apptime',
|
||||
'apptimer',
|
||||
|
|
@ -414,6 +421,7 @@ __all__ = [
|
|||
'ScoreConfig',
|
||||
'ScoreScreenActivity',
|
||||
'ScoreType',
|
||||
'SessionNotFoundError',
|
||||
'broadcastmessage',
|
||||
'Session',
|
||||
'SessionData',
|
||||
|
|
@ -462,6 +470,7 @@ __all__ = [
|
|||
'unlock_all_input',
|
||||
'Vec3',
|
||||
'WeakCall',
|
||||
'WinnerGroup',
|
||||
]
|
||||
|
||||
# We want stuff here to show up as bascenev1.Foo instead of
|
||||
|
|
|
|||
244
dist/ba_data/python/bascenev1/_activity.py
vendored
244
dist/ba_data/python/bascenev1/_activity.py
vendored
|
|
@ -23,99 +23,98 @@ TeamT = TypeVar('TeamT', bound=Team)
|
|||
|
||||
|
||||
class Activity(DependencyComponent, Generic[PlayerT, TeamT]):
|
||||
"""Units of execution wrangled by a bascenev1.Session.
|
||||
"""Units of execution wrangled by a :class:`bascenev1.Session`.
|
||||
|
||||
Category: Gameplay Classes
|
||||
|
||||
Examples of Activities include games, score-screens, cutscenes, etc.
|
||||
A bascenev1.Session has one 'current' Activity at any time, though
|
||||
their existence can overlap during transitions.
|
||||
Examples of activities include games, score-screens, cutscenes, etc.
|
||||
A :class:`bascenev1.Session` has one 'current' activity at any time,
|
||||
though their existence can overlap during transitions.
|
||||
"""
|
||||
|
||||
# pylint: disable=too-many-public-methods
|
||||
|
||||
#: The settings dict passed in when the activity was made. This
|
||||
#: attribute is deprecated and should be avoided when possible;
|
||||
#: activities should pull all values they need from the ``settings``
|
||||
#: arg passed to the activity's __init__ call.
|
||||
settings_raw: dict[str, Any]
|
||||
"""The settings dict passed in when the activity was made.
|
||||
This attribute is deprecated and should be avoided when possible;
|
||||
activities should pull all values they need from the 'settings' arg
|
||||
passed to the Activity __init__ call."""
|
||||
|
||||
#: The list of teams in the activity. This gets populated just before
|
||||
#: on_begin() is called and is updated automatically as players join
|
||||
#: or leave the game. (at least in free-for-all mode where every
|
||||
#: player gets their own team; in teams mode there are always 2 teams
|
||||
#: regardless of the player count).
|
||||
teams: list[TeamT]
|
||||
"""The list of bascenev1.Team-s in the Activity. This gets populated just
|
||||
before on_begin() is called and is updated automatically as players
|
||||
join or leave the game. (at least in free-for-all mode where every
|
||||
player gets their own team; in teams mode there are always 2 teams
|
||||
regardless of the player count)."""
|
||||
|
||||
#: The list of players in the activity. This gets populated just
|
||||
#: before :meth:`~bascenev1.Activity.on_begin()` is called and is
|
||||
#: updated automatically as players join or leave the game.
|
||||
players: list[PlayerT]
|
||||
"""The list of bascenev1.Player-s in the Activity. This gets populated
|
||||
just before on_begin() is called and is updated automatically as
|
||||
players join or leave the game."""
|
||||
|
||||
#: Whether to print every time a player dies. This can be pertinent
|
||||
#: in games such as Death-Match but can be annoying in games where it
|
||||
#: doesn't matter.
|
||||
announce_player_deaths = False
|
||||
"""Whether to print every time a player dies. This can be pertinent
|
||||
in games such as Death-Match but can be annoying in games where it
|
||||
doesn't matter."""
|
||||
|
||||
#: Joining activities are for waiting for initial player joins. They
|
||||
#: are treated slightly differently than regular activities, mainly
|
||||
#: in that all players are passed to the activity at once instead of
|
||||
#: as each joins.
|
||||
is_joining_activity = False
|
||||
"""Joining activities are for waiting for initial player joins.
|
||||
They are treated slightly differently than regular activities,
|
||||
mainly in that all players are passed to the activity at once
|
||||
instead of as each joins."""
|
||||
|
||||
#: Whether scene-time should still progress when in menus/etc.
|
||||
allow_pausing = False
|
||||
"""Whether game-time should still progress when in menus/etc."""
|
||||
|
||||
#: Whether idle players can potentially be kicked (should not happen
|
||||
#: in menus/etc).
|
||||
allow_kick_idle_players = True
|
||||
"""Whether idle players can potentially be kicked (should not happen in
|
||||
menus/etc)."""
|
||||
|
||||
#: In vr mode, this determines whether overlay nodes (text, images,
|
||||
#: etc) are created at a fixed position in space or one that moves
|
||||
#: based on the current map. Generally this should be on for games
|
||||
#: and off for transitions/score-screens/etc. that persist between
|
||||
#: maps.
|
||||
use_fixed_vr_overlay = False
|
||||
"""In vr mode, this determines whether overlay nodes (text, images, etc)
|
||||
are created at a fixed position in space or one that moves based on
|
||||
the current map. Generally this should be on for games and off for
|
||||
transitions/score-screens/etc. that persist between maps."""
|
||||
|
||||
#: If True, runs in slow motion and turns down sound pitch.
|
||||
slow_motion = False
|
||||
"""If True, runs in slow motion and turns down sound pitch."""
|
||||
|
||||
#: Set this to True to inherit slow motion setting from previous
|
||||
#: activity (useful for transitions to avoid hitches).
|
||||
inherits_slow_motion = False
|
||||
"""Set this to True to inherit slow motion setting from previous
|
||||
activity (useful for transitions to avoid hitches)."""
|
||||
|
||||
#: Set this to True to keep playing the music from the previous
|
||||
#: activity (without even restarting it).
|
||||
inherits_music = False
|
||||
"""Set this to True to keep playing the music from the previous activity
|
||||
(without even restarting it)."""
|
||||
|
||||
#: Set this to true to inherit VR camera offsets from the previous
|
||||
#: activity (useful for preventing sporadic camera movement during
|
||||
#: transitions).
|
||||
inherits_vr_camera_offset = False
|
||||
"""Set this to true to inherit VR camera offsets from the previous
|
||||
activity (useful for preventing sporadic camera movement
|
||||
during transitions)."""
|
||||
|
||||
#: Set this to true to inherit (non-fixed) VR overlay positioning
|
||||
#: from the previous activity (useful for prevent sporadic overlay
|
||||
#: jostling during transitions).
|
||||
inherits_vr_overlay_center = False
|
||||
"""Set this to true to inherit (non-fixed) VR overlay positioning from
|
||||
the previous activity (useful for prevent sporadic overlay jostling
|
||||
during transitions)."""
|
||||
|
||||
#: Set this to true to inherit screen tint/vignette colors from the
|
||||
#: previous activity (useful to prevent sudden color changes during
|
||||
#: transitions).
|
||||
inherits_tint = False
|
||||
"""Set this to true to inherit screen tint/vignette colors from the
|
||||
previous activity (useful to prevent sudden color changes during
|
||||
transitions)."""
|
||||
|
||||
#: Whether players should be allowed to join in the middle of this
|
||||
#: activity. Note that a :class:`bascenev1.Session` may not allow
|
||||
#: mid-activity-joins even if the activity says it is ok.
|
||||
allow_mid_activity_joins: bool = True
|
||||
"""Whether players should be allowed to join in the middle of this
|
||||
activity. Note that Sessions may not allow mid-activity-joins even
|
||||
if the activity says its ok."""
|
||||
|
||||
#: If the activity fades or transitions in, it should set the length
|
||||
#: of time here so that previous activities will be kept alive for
|
||||
#: that long (avoiding 'holes' in the screen) This value is given in
|
||||
#: real-time seconds.
|
||||
transition_time = 0.0
|
||||
"""If the activity fades or transitions in, it should set the length of
|
||||
time here so that previous activities will be kept alive for that
|
||||
long (avoiding 'holes' in the screen)
|
||||
This value is given in real-time seconds."""
|
||||
|
||||
#: Is it ok to show an ad after this activity ends before showing the
|
||||
#: next activity?
|
||||
can_show_ad_on_death = False
|
||||
"""Is it ok to show an ad after this activity ends before showing
|
||||
the next activity?"""
|
||||
|
||||
def __init__(self, settings: dict):
|
||||
"""Creates an Activity in the current bascenev1.Session.
|
||||
|
|
@ -149,7 +148,7 @@ class Activity(DependencyComponent, Generic[PlayerT, TeamT]):
|
|||
|
||||
self._session = weakref.ref(_bascenev1.getsession())
|
||||
|
||||
# Preloaded data for actors, maps, etc; indexed by type.
|
||||
#: Preloaded data for actors, maps, etc; indexed by type.
|
||||
self.preloads: dict[type, Any] = {}
|
||||
|
||||
# Hopefully can eventually kill this; activities should
|
||||
|
|
@ -208,8 +207,9 @@ class Activity(DependencyComponent, Generic[PlayerT, TeamT]):
|
|||
|
||||
@property
|
||||
def globalsnode(self) -> bascenev1.Node:
|
||||
"""The 'globals' bascenev1.Node for the activity. This contains various
|
||||
global controls and values.
|
||||
"""The 'globals' :class:`~bascenev1.Node` for the activity.
|
||||
|
||||
This contains various global controls and values.
|
||||
"""
|
||||
node = self._globalsnode
|
||||
if not node:
|
||||
|
|
@ -221,7 +221,7 @@ class Activity(DependencyComponent, Generic[PlayerT, TeamT]):
|
|||
"""The stats instance accessible while the activity is running.
|
||||
|
||||
If access is attempted before or after, raises a
|
||||
bascenev1.NotFoundError.
|
||||
:class:`~bascenev1.NotFoundError`.
|
||||
"""
|
||||
if self._stats is None:
|
||||
raise babase.NotFoundError()
|
||||
|
|
@ -260,22 +260,24 @@ class Activity(DependencyComponent, Generic[PlayerT, TeamT]):
|
|||
|
||||
@property
|
||||
def playertype(self) -> type[PlayerT]:
|
||||
"""The type of bascenev1.Player this Activity is using."""
|
||||
"""The :class:`~bascenev1.Player` subclass this activity uses."""
|
||||
return self._playertype
|
||||
|
||||
@property
|
||||
def teamtype(self) -> type[TeamT]:
|
||||
"""The type of bascenev1.Team this Activity is using."""
|
||||
"""The :class:`~bascenev1.Team` subclass this activity uses."""
|
||||
return self._teamtype
|
||||
|
||||
def set_has_ended(self, val: bool) -> None:
|
||||
"""(internal)"""
|
||||
"""Internal - used by session.
|
||||
|
||||
:meta private:"""
|
||||
self._has_ended = val
|
||||
|
||||
def expire(self) -> None:
|
||||
"""Begin the process of tearing down the activity.
|
||||
"""Internal; Begin the process of tearing down the activity.
|
||||
|
||||
(internal)
|
||||
:meta private:
|
||||
"""
|
||||
|
||||
# Create an app-timer that watches a weak-ref of this activity
|
||||
|
|
@ -304,11 +306,12 @@ class Activity(DependencyComponent, Generic[PlayerT, TeamT]):
|
|||
)
|
||||
|
||||
def retain_actor(self, actor: bascenev1.Actor) -> None:
|
||||
"""Add a strong-reference to a bascenev1.Actor to this Activity.
|
||||
"""Add a strong-ref to a :class:`bascenev1.Actor` to this activity.
|
||||
|
||||
The reference will be lazily released once bascenev1.Actor.exists()
|
||||
returns False for the Actor. The bascenev1.Actor.autoretain() method
|
||||
is a convenient way to access this same functionality.
|
||||
The reference will be lazily released once
|
||||
:meth:`bascenev1.Actor.exists()` returns False for the actor.
|
||||
The :meth:`bascenev1.Actor.autoretain()` method is a convenient
|
||||
way to access this same functionality.
|
||||
"""
|
||||
if __debug__:
|
||||
from bascenev1._actor import Actor
|
||||
|
|
@ -317,9 +320,9 @@ class Activity(DependencyComponent, Generic[PlayerT, TeamT]):
|
|||
self._actor_refs.append(actor)
|
||||
|
||||
def add_actor_weak_ref(self, actor: bascenev1.Actor) -> None:
|
||||
"""Add a weak-reference to a bascenev1.Actor to the bascenev1.Activity.
|
||||
"""Add a weak-ref to a :class:`bascenev1.Actor` to the activity.
|
||||
|
||||
(called by the bascenev1.Actor base class)
|
||||
(called by the :class:`bascenev1.Actor` base class)
|
||||
"""
|
||||
if __debug__:
|
||||
from bascenev1._actor import Actor
|
||||
|
|
@ -329,9 +332,10 @@ class Activity(DependencyComponent, Generic[PlayerT, TeamT]):
|
|||
|
||||
@property
|
||||
def session(self) -> bascenev1.Session:
|
||||
"""The bascenev1.Session this bascenev1.Activity belongs to.
|
||||
"""The session this activity belongs to.
|
||||
|
||||
Raises a babase.SessionNotFoundError if the Session no longer exists.
|
||||
Raises a :class:`~bascenev1.SessionNotFoundError` if the session
|
||||
no longer exists.
|
||||
"""
|
||||
session = self._session()
|
||||
if session is None:
|
||||
|
|
@ -339,44 +343,44 @@ class Activity(DependencyComponent, Generic[PlayerT, TeamT]):
|
|||
return session
|
||||
|
||||
def on_player_join(self, player: PlayerT) -> None:
|
||||
"""Called when a new bascenev1.Player has joined the Activity.
|
||||
"""Called when a player joins the activity.
|
||||
|
||||
(including the initial set of Players)
|
||||
(including the initial set of players)
|
||||
"""
|
||||
|
||||
def on_player_leave(self, player: PlayerT) -> None:
|
||||
"""Called when a bascenev1.Player is leaving the Activity."""
|
||||
"""Called when a player is leaving the Activity."""
|
||||
|
||||
def on_team_join(self, team: TeamT) -> None:
|
||||
"""Called when a new bascenev1.Team joins the Activity.
|
||||
"""Called when a new team joins the activity.
|
||||
|
||||
(including the initial set of Teams)
|
||||
(including the initial set of teams)
|
||||
"""
|
||||
|
||||
def on_team_leave(self, team: TeamT) -> None:
|
||||
"""Called when a bascenev1.Team leaves the Activity."""
|
||||
"""Called when a team leaves the activity."""
|
||||
|
||||
def on_transition_in(self) -> None:
|
||||
"""Called when the Activity is first becoming visible.
|
||||
"""Called when the activity is first becoming visible.
|
||||
|
||||
Upon this call, the Activity should fade in backgrounds,
|
||||
start playing music, etc. It does not yet have access to players
|
||||
or teams, however. They remain owned by the previous Activity
|
||||
up until bascenev1.Activity.on_begin() is called.
|
||||
Upon this call, the activity should fade in backgrounds, start
|
||||
playing music, etc. It does not yet have access to players or
|
||||
teams, however. They remain owned by the previous activity up
|
||||
until :meth:`~bascenev1.Activity.on_begin()` is called.
|
||||
"""
|
||||
|
||||
def on_transition_out(self) -> None:
|
||||
"""Called when your activity begins transitioning out.
|
||||
|
||||
Note that this may happen at any time even if bascenev1.Activity.end()
|
||||
has not been called.
|
||||
Note that this may happen at any time even if
|
||||
:meth:`bascenev1.Activity.end()` has not been called.
|
||||
"""
|
||||
|
||||
def on_begin(self) -> None:
|
||||
"""Called once the previous Activity has finished transitioning out.
|
||||
"""Called once the previous activity has finished transitioning out.
|
||||
|
||||
At this point the activity's initial players and teams are filled in
|
||||
and it should begin its actual game logic.
|
||||
At this point the activity's initial players and teams are
|
||||
filled in and it should begin its actual game logic.
|
||||
"""
|
||||
|
||||
def handlemessage(self, msg: Any) -> Any:
|
||||
|
|
@ -385,11 +389,11 @@ class Activity(DependencyComponent, Generic[PlayerT, TeamT]):
|
|||
return UNHANDLED
|
||||
|
||||
def has_transitioned_in(self) -> bool:
|
||||
"""Return whether bascenev1.Activity.on_transition_in() has run."""
|
||||
"""Whether :meth:`~bascenev1.Activity.on_transition_in()` has run."""
|
||||
return self._has_transitioned_in
|
||||
|
||||
def has_begun(self) -> bool:
|
||||
"""Return whether bascenev1.Activity.on_begin() has run."""
|
||||
"""Whether :meth:`~bascenev1.Activity.on_begin()` has run."""
|
||||
return self._has_begun
|
||||
|
||||
def has_ended(self) -> bool:
|
||||
|
|
@ -397,13 +401,13 @@ class Activity(DependencyComponent, Generic[PlayerT, TeamT]):
|
|||
return self._has_ended
|
||||
|
||||
def is_transitioning_out(self) -> bool:
|
||||
"""Return whether bascenev1.Activity.on_transition_out() has run."""
|
||||
"""Whether :meth:`~bascenev1.Activity.on_transition_out()` has run."""
|
||||
return self._transitioning_out
|
||||
|
||||
def transition_in(self, prev_globals: bascenev1.Node | None) -> None:
|
||||
"""Called by Session to kick off transition-in.
|
||||
"""Internal; called by session to kick off transition-in.
|
||||
|
||||
(internal)
|
||||
:meta private:
|
||||
"""
|
||||
assert not self._has_transitioned_in
|
||||
self._has_transitioned_in = True
|
||||
|
|
@ -459,7 +463,10 @@ class Activity(DependencyComponent, Generic[PlayerT, TeamT]):
|
|||
self._activity_data.make_foreground()
|
||||
|
||||
def transition_out(self) -> None:
|
||||
"""Called by the Session to start us transitioning out."""
|
||||
"""Internal; called by session to start us transitioning out.
|
||||
|
||||
:meta private:
|
||||
"""
|
||||
assert not self._transitioning_out
|
||||
self._transitioning_out = True
|
||||
with self.context:
|
||||
|
|
@ -469,9 +476,9 @@ class Activity(DependencyComponent, Generic[PlayerT, TeamT]):
|
|||
logging.exception('Error in on_transition_out for %s.', self)
|
||||
|
||||
def begin(self, session: bascenev1.Session) -> None:
|
||||
"""Begin the activity.
|
||||
"""Internal; Begin the activity.
|
||||
|
||||
(internal)
|
||||
:meta private:
|
||||
"""
|
||||
|
||||
assert not self._has_begun
|
||||
|
|
@ -499,45 +506,46 @@ class Activity(DependencyComponent, Generic[PlayerT, TeamT]):
|
|||
def end(
|
||||
self, results: Any = None, delay: float = 0.0, force: bool = False
|
||||
) -> None:
|
||||
"""Commences Activity shutdown and delivers results to the Session.
|
||||
"""Commence activity shutdown and delivers results to the session.
|
||||
|
||||
'delay' is the time delay before the Activity actually ends
|
||||
(in seconds). Further calls to end() will be ignored up until
|
||||
this time, unless 'force' is True, in which case the new results
|
||||
will replace the old.
|
||||
'delay' is the time delay before the Activity actually ends (in
|
||||
seconds). Further end calls will be ignored up until this time,
|
||||
unless 'force' is True, in which case the new results will
|
||||
replace the old.
|
||||
"""
|
||||
|
||||
# Ask the session to end us.
|
||||
self.session.end_activity(self, results, delay, force)
|
||||
|
||||
def create_player(self, sessionplayer: bascenev1.SessionPlayer) -> PlayerT:
|
||||
"""Create the Player instance for this Activity.
|
||||
"""Create a :class:`bascenev1.Player` instance for this activity.
|
||||
|
||||
Subclasses can override this if the activity's player class
|
||||
requires a custom constructor; otherwise it will be called with
|
||||
no args. Note that the player object should not be used at this
|
||||
point as it is not yet fully wired up; wait for
|
||||
bascenev1.Activity.on_player_join() for that.
|
||||
Note that the player object should not be used at this point as
|
||||
it is not yet fully wired up; wait for
|
||||
:meth:`bascenev1.Activity.on_player_join()` for that.
|
||||
"""
|
||||
del sessionplayer # Unused.
|
||||
player = self._playertype()
|
||||
return player
|
||||
|
||||
def create_team(self, sessionteam: bascenev1.SessionTeam) -> TeamT:
|
||||
"""Create the Team instance for this Activity.
|
||||
"""Create a :class:`bascenev1.Team` instance for this activity.
|
||||
|
||||
Subclasses can override this if the activity's team class
|
||||
requires a custom constructor; otherwise it will be called with
|
||||
no args. Note that the team object should not be used at this
|
||||
point as it is not yet fully wired up; wait for on_team_join()
|
||||
for that.
|
||||
point as it is not yet fully wired up; wait for
|
||||
:meth:`bascenev1.Activity.on_team_join()` for that.
|
||||
"""
|
||||
del sessionteam # Unused.
|
||||
team = self._teamtype()
|
||||
return team
|
||||
|
||||
def add_player(self, sessionplayer: bascenev1.SessionPlayer) -> None:
|
||||
"""(internal)"""
|
||||
"""Internal
|
||||
|
||||
:meta private:
|
||||
"""
|
||||
assert sessionplayer.sessionteam is not None
|
||||
sessionplayer.resetinput()
|
||||
sessionteam = sessionplayer.sessionteam
|
||||
|
|
@ -565,7 +573,7 @@ class Activity(DependencyComponent, Generic[PlayerT, TeamT]):
|
|||
logging.exception('Error in on_player_join for %s.', self)
|
||||
|
||||
def remove_player(self, sessionplayer: bascenev1.SessionPlayer) -> None:
|
||||
"""Remove a player from the Activity while it is running.
|
||||
"""Remove a player from the activity while it is running.
|
||||
|
||||
(internal)
|
||||
"""
|
||||
|
|
@ -584,10 +592,6 @@ class Activity(DependencyComponent, Generic[PlayerT, TeamT]):
|
|||
self.players.remove(player)
|
||||
assert player not in self.players
|
||||
|
||||
# This should allow our bascenev1.Player instance to die.
|
||||
# Complain if that doesn't happen.
|
||||
# verify_object_death(player)
|
||||
|
||||
with self.context:
|
||||
try:
|
||||
self.on_player_leave(player)
|
||||
|
|
@ -608,9 +612,9 @@ class Activity(DependencyComponent, Generic[PlayerT, TeamT]):
|
|||
self._players_that_left.append(weakref.ref(player))
|
||||
|
||||
def add_team(self, sessionteam: bascenev1.SessionTeam) -> None:
|
||||
"""Add a team to the Activity
|
||||
"""Internal; Add a team to the activity
|
||||
|
||||
(internal)
|
||||
:meta private:
|
||||
"""
|
||||
assert not self.expired
|
||||
|
||||
|
|
@ -624,9 +628,9 @@ class Activity(DependencyComponent, Generic[PlayerT, TeamT]):
|
|||
logging.exception('Error in on_team_join for %s.', self)
|
||||
|
||||
def remove_team(self, sessionteam: bascenev1.SessionTeam) -> None:
|
||||
"""Remove a team from a Running Activity
|
||||
"""Internal; remove a team from a running activity
|
||||
|
||||
(internal)
|
||||
:meta private:
|
||||
"""
|
||||
assert not self.expired
|
||||
assert sessionteam.activityteam is not None
|
||||
|
|
|
|||
152
dist/ba_data/python/bascenev1/_actor.py
vendored
152
dist/ba_data/python/bascenev1/_actor.py
vendored
|
|
@ -27,53 +27,57 @@ ActorT = TypeVar('ActorT', bound='Actor')
|
|||
|
||||
|
||||
class Actor:
|
||||
"""High level logical entities in a bascenev1.Activity.
|
||||
"""High level logical entities in an :class:`~bascenev1.Activity`.
|
||||
|
||||
Category: **Gameplay Classes**
|
||||
Actors act as controllers, combining some number of
|
||||
:class:`~bascenev1.Node`, :class:`~bascenev1.Texture`,
|
||||
:class:`~bascenev1.Sound`, and other type objects into a high-level
|
||||
cohesive unit.
|
||||
|
||||
Actors act as controllers, combining some number of Nodes, Textures,
|
||||
Sounds, etc. into a high-level cohesive unit.
|
||||
Some example actors include the :class:`~bascenev1lib.actor.bomb.Bomb`,
|
||||
:class:`~bascenev1lib.actor.flag.Flag`, and
|
||||
:class:`~bascenev1lib.actor.spaz.Spaz`, classes that live in the
|
||||
:mod:`bascenev1lib.actor` package.
|
||||
|
||||
Some example actors include the Bomb, Flag, and Spaz classes that
|
||||
live in the bascenev1lib.actor.* modules.
|
||||
One key feature of actors is that they generally 'die' (killing off
|
||||
or transitioning out their nodes) when the last Python reference to
|
||||
them disappears, so you can use logic such as::
|
||||
|
||||
One key feature of Actors is that they generally 'die'
|
||||
(killing off or transitioning out their nodes) when the last Python
|
||||
reference to them disappears, so you can use logic such as:
|
||||
# Create a flag actor in our game activity (self):
|
||||
from bascenev1lib.actor.flag import Flag
|
||||
|
||||
##### Example
|
||||
>>> # Create a flag Actor in our game activity:
|
||||
... from bascenev1lib.actor.flag import Flag
|
||||
... self.flag = Flag(position=(0, 10, 0))
|
||||
...
|
||||
... # Later, destroy the flag.
|
||||
... # (provided nothing else is holding a reference to it)
|
||||
... # We could also just assign a new flag to this value.
|
||||
... # Either way, the old flag disappears.
|
||||
... self.flag = None
|
||||
self.flag = Flag(position=(0, 10, 0))
|
||||
|
||||
# Later, destroy the flag (provided nothing else is holding a
|
||||
# reference to it). We could also just assign a new flag to this
|
||||
# value. Either way, the old flag should disappear.
|
||||
self.flag = None
|
||||
|
||||
This is in contrast to the behavior of the more low level
|
||||
bascenev1.Node, which is always explicitly created and destroyed
|
||||
and doesn't care how many Python references to it exist.
|
||||
:class:`~bascenev1.Node` class, which is always explicitly created
|
||||
and destroyed and doesn't care how many Python references to it
|
||||
exist.
|
||||
|
||||
Note, however, that you can use the bascenev1.Actor.autoretain() method
|
||||
if you want an Actor to stick around until explicitly killed
|
||||
Note, however, that you can use the :meth:`~bascenev1.Actor.autoretain()`
|
||||
method if you want an actor to stick around until explicitly killed
|
||||
regardless of references.
|
||||
|
||||
Another key feature of bascenev1.Actor is its
|
||||
bascenev1.Actor.handlemessage() method, which takes a single arbitrary
|
||||
object as an argument. This provides a safe way to communicate between
|
||||
bascenev1.Actor, bascenev1.Activity, bascenev1.Session, and any other
|
||||
class providing a handlemessage() method. The most universally handled
|
||||
message type for Actors is the bascenev1.DieMessage.
|
||||
Another key feature of actors is their
|
||||
:meth:`~bascenev1.Actor.handlemessage()` method, which takes a single
|
||||
arbitrary object as an argument. This provides a safe way to communicate
|
||||
between :class:`~bascenev1.Actor`, :class:`~bascenev1.Activity`,
|
||||
:class:`~bascenev1.Session`, and any other class providing a
|
||||
``handlemessage()`` method. The most universally handled
|
||||
message type for actors is the :class:`~bascenev1.DieMessage`.
|
||||
|
||||
Another way to kill the flag from the example above:
|
||||
We can safely call this on any type with a 'handlemessage' method
|
||||
We can safely call this on any type with a ``handlemessage`` method
|
||||
(though its not guaranteed to always have a meaningful effect).
|
||||
In this case the Actor instance will still be around, but its
|
||||
bascenev1.Actor.exists() and bascenev1.Actor.is_alive() methods will
|
||||
both return False.
|
||||
>>> self.flag.handlemessage(bascenev1.DieMessage())
|
||||
In this case the actor instance will still be around, but its
|
||||
:meth:`~bascenev1.Actor.exists()` and :meth:`~bascenev1.Actor.is_alive()`
|
||||
methods will both return False::
|
||||
|
||||
self.flag.handlemessage(bascenev1.DieMessage())
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
|
|
@ -108,17 +112,17 @@ class Actor:
|
|||
return UNHANDLED
|
||||
|
||||
def autoretain(self: ActorT) -> ActorT:
|
||||
"""Keep this Actor alive without needing to hold a reference to it.
|
||||
"""Keep this actor alive without needing to hold a reference to it.
|
||||
|
||||
This keeps the bascenev1.Actor in existence by storing a reference
|
||||
to it with the bascenev1.Activity it was created in. The reference
|
||||
is lazily released once bascenev1.Actor.exists() returns False for
|
||||
it or when the Activity is set as expired. This can be a convenient
|
||||
alternative to storing references explicitly just to keep a
|
||||
bascenev1.Actor from dying.
|
||||
For convenience, this method returns the bascenev1.Actor it is called
|
||||
with, enabling chained statements such as:
|
||||
myflag = bascenev1.Flag().autoretain()
|
||||
This keeps the actor in existence by storing a reference to it
|
||||
with the :class:`~bascenev1.Activity` it was created in. The
|
||||
reference is lazily released once
|
||||
:meth:`~bascenev1.Actor.exists()` returns False for the actor or
|
||||
when the :class:`~bascenev1.Activity` is set as expired. This
|
||||
can be a convenient alternative to storing references explicitly
|
||||
just to keep an actor from dying. For convenience, this method
|
||||
returns the actor it is called with, enabling chained statements
|
||||
such as: ``myflag = bascenev1.Flag().autoretain()``
|
||||
"""
|
||||
activity = self._activity()
|
||||
if activity is None:
|
||||
|
|
@ -127,43 +131,44 @@ class Actor:
|
|||
return self
|
||||
|
||||
def on_expire(self) -> None:
|
||||
"""Called for remaining `bascenev1.Actor`s when their activity dies.
|
||||
"""Called for remaining actors when their activity dies.
|
||||
|
||||
Actors can use this opportunity to clear callbacks or other
|
||||
references which have the potential of keeping the bascenev1.Activity
|
||||
alive inadvertently (Activities can not exit cleanly while
|
||||
any Python references to them remain.)
|
||||
references which have the potential of keeping the
|
||||
:class:`~bascenev1.Activity` alive inadvertently (activities can
|
||||
not exit cleanly while any Python references to them remain.)
|
||||
|
||||
Once an actor is expired (see bascenev1.Actor.is_expired()) it should
|
||||
no longer perform any game-affecting operations (creating, modifying,
|
||||
or deleting nodes, media, timers, etc.) Attempts to do so will
|
||||
likely result in errors.
|
||||
Once an actor is expired (see :attr:`~bascenev1.Actor.expired`)
|
||||
it should no longer perform any game-affecting operations
|
||||
(creating, modifying, or deleting nodes, media, timers, etc.)
|
||||
Attempts to do so will likely result in errors.
|
||||
"""
|
||||
|
||||
@property
|
||||
def expired(self) -> bool:
|
||||
"""Whether the Actor is expired.
|
||||
"""Whether the actor is expired.
|
||||
|
||||
(see bascenev1.Actor.on_expire())
|
||||
(see :meth:`~bascenev1.Actor.on_expire()`)
|
||||
"""
|
||||
activity = self.getactivity(doraise=False)
|
||||
return True if activity is None else activity.expired
|
||||
|
||||
def exists(self) -> bool:
|
||||
"""Returns whether the Actor is still present in a meaningful way.
|
||||
"""Returns whether the actor is still present in a meaningful way.
|
||||
|
||||
Note that a dying character should still return True here as long as
|
||||
their corpse is visible; this is about presence, not being 'alive'
|
||||
(see bascenev1.Actor.is_alive() for that).
|
||||
(see :meth:`~bascenev1.Actor.is_alive()` for that).
|
||||
|
||||
If this returns False, it is assumed the Actor can be completely
|
||||
deleted without affecting the game; this call is often used
|
||||
when pruning lists of Actors, such as with bascenev1.Actor.autoretain()
|
||||
If this returns False, it is assumed the actor can be completely
|
||||
deleted without affecting the game; this call is often used when
|
||||
pruning lists of actors, such as with
|
||||
:meth:`bascenev1.Actor.autoretain()`
|
||||
|
||||
The default implementation of this method always return True.
|
||||
|
||||
Note that the boolean operator for the Actor class calls this method,
|
||||
so a simple "if myactor" test will conveniently do the right thing
|
||||
Note that the boolean operator for the actor class calls this method,
|
||||
so a simple ``if myactor`` test will conveniently do the right thing
|
||||
even if myactor is set to None.
|
||||
"""
|
||||
return True
|
||||
|
|
@ -173,22 +178,21 @@ class Actor:
|
|||
return self.exists()
|
||||
|
||||
def is_alive(self) -> bool:
|
||||
"""Returns whether the Actor is 'alive'.
|
||||
"""Returns whether the actor is 'alive'.
|
||||
|
||||
What this means is up to the Actor.
|
||||
It is not a requirement for Actors to be able to die;
|
||||
just that they report whether they consider themselves
|
||||
to be alive or not. In cases where dead/alive is
|
||||
irrelevant, True should be returned.
|
||||
What this means is up to the actor. It is not a requirement for
|
||||
actors to be able to die; just that they report whether they
|
||||
consider themselves to be alive or not. In cases where
|
||||
dead/alive is irrelevant, True should be returned.
|
||||
"""
|
||||
return True
|
||||
|
||||
@property
|
||||
def activity(self) -> bascenev1.Activity:
|
||||
"""The Activity this Actor was created in.
|
||||
"""The activity this actor was created in.
|
||||
|
||||
Raises a bascenev1.ActivityNotFoundError if the Activity no longer
|
||||
exists.
|
||||
Raises a :class:`~bascenev1.ActivityNotFoundError` if the
|
||||
activity no longer exists.
|
||||
"""
|
||||
activity = self._activity()
|
||||
if activity is None:
|
||||
|
|
@ -208,11 +212,11 @@ class Actor:
|
|||
) -> bascenev1.Activity | None: ...
|
||||
|
||||
def getactivity(self, doraise: bool = True) -> bascenev1.Activity | None:
|
||||
"""Return the bascenev1.Activity this Actor is associated with.
|
||||
"""Return the activity this actor is associated with.
|
||||
|
||||
If the Activity no longer exists, raises a
|
||||
bascenev1.ActivityNotFoundError or returns None depending on whether
|
||||
'doraise' is True.
|
||||
If the activity no longer exists, raises a
|
||||
:class:`~bascenev1.ActivityNotFoundError` or returns None
|
||||
depending on whether ``doraise`` is True.
|
||||
"""
|
||||
activity = self._activity()
|
||||
if activity is None and doraise:
|
||||
|
|
|
|||
21
dist/ba_data/python/bascenev1/_campaign.py
vendored
21
dist/ba_data/python/bascenev1/_campaign.py
vendored
|
|
@ -19,10 +19,7 @@ def register_campaign(campaign: bascenev1.Campaign) -> None:
|
|||
|
||||
|
||||
class Campaign:
|
||||
"""Represents a unique set or series of baclassic.Level-s.
|
||||
|
||||
Category: **App Classes**
|
||||
"""
|
||||
"""Represents a unique set of :class:`~bascenev1.Level` instances."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
|
@ -39,18 +36,18 @@ class Campaign:
|
|||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""The name of the Campaign."""
|
||||
"""The name of the campaign."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def sequential(self) -> bool:
|
||||
"""Whether this Campaign's levels must be played in sequence."""
|
||||
"""Whether this campaign's levels must be played in sequence."""
|
||||
return self._sequential
|
||||
|
||||
def addlevel(
|
||||
self, level: bascenev1.Level, index: int | None = None
|
||||
) -> None:
|
||||
"""Adds a baclassic.Level to the Campaign."""
|
||||
"""Add a level to the campaign."""
|
||||
if level.campaign is not None:
|
||||
raise RuntimeError('Level already belongs to a campaign.')
|
||||
level.set_campaign(self, len(self._levels))
|
||||
|
|
@ -61,11 +58,11 @@ class Campaign:
|
|||
|
||||
@property
|
||||
def levels(self) -> list[bascenev1.Level]:
|
||||
"""The list of baclassic.Level-s in the Campaign."""
|
||||
"""The list of levels in the campaign."""
|
||||
return self._levels
|
||||
|
||||
def getlevel(self, name: str) -> bascenev1.Level:
|
||||
"""Return a contained baclassic.Level by name."""
|
||||
"""Return a contained level by name."""
|
||||
|
||||
for level in self._levels:
|
||||
if level.name == name:
|
||||
|
|
@ -75,11 +72,11 @@ class Campaign:
|
|||
)
|
||||
|
||||
def reset(self) -> None:
|
||||
"""Reset state for the Campaign."""
|
||||
"""Reset state for the campaign."""
|
||||
babase.app.config.setdefault('Campaigns', {})[self._name] = {}
|
||||
|
||||
# FIXME should these give/take baclassic.Level instances instead
|
||||
# of level names?..
|
||||
# FIXME: should these give/take baclassic.Level instances instead of
|
||||
# level names?..
|
||||
def set_selected_level(self, levelname: str) -> None:
|
||||
"""Set the Level currently selected in the UI (by name)."""
|
||||
self.configdict['Selection'] = levelname
|
||||
|
|
|
|||
23
dist/ba_data/python/bascenev1/_collision.py
vendored
23
dist/ba_data/python/bascenev1/_collision.py
vendored
|
|
@ -14,10 +14,7 @@ if TYPE_CHECKING:
|
|||
|
||||
|
||||
class Collision:
|
||||
"""A class providing info about occurring collisions.
|
||||
|
||||
Category: **Gameplay Classes**
|
||||
"""
|
||||
"""A class providing info about occurring collisions."""
|
||||
|
||||
@property
|
||||
def position(self) -> bascenev1.Vec3:
|
||||
|
|
@ -28,9 +25,9 @@ class Collision:
|
|||
def sourcenode(self) -> bascenev1.Node:
|
||||
"""The node containing the material triggering the current callback.
|
||||
|
||||
Throws a bascenev1.NodeNotFoundError if the node does not exist,
|
||||
though the node should always exist (at least at the start of the
|
||||
collision callback).
|
||||
Throws a :class:`~babase.NodeNotFoundError` if the node does
|
||||
not exist, though the node should always exist (at least at the
|
||||
start of the collision callback).
|
||||
"""
|
||||
node = _bascenev1.get_collision_info('sourcenode')
|
||||
assert isinstance(node, (_bascenev1.Node, type(None)))
|
||||
|
|
@ -42,9 +39,10 @@ class Collision:
|
|||
def opposingnode(self) -> bascenev1.Node:
|
||||
"""The node the current callback material node is hitting.
|
||||
|
||||
Throws a bascenev1.NodeNotFoundError if the node does not exist.
|
||||
This can be expected in some cases such as in 'disconnect'
|
||||
callbacks triggered by deleting a currently-colliding node.
|
||||
Throws a :class:`~babase.NodeNotFoundError` if the node does
|
||||
not exist. This can be expected in some cases such as in
|
||||
'disconnect' callbacks triggered by deleting a
|
||||
currently-colliding node.
|
||||
"""
|
||||
node = _bascenev1.get_collision_info('opposingnode')
|
||||
assert isinstance(node, (_bascenev1.Node, type(None)))
|
||||
|
|
@ -65,8 +63,5 @@ _collision = Collision()
|
|||
|
||||
|
||||
def getcollision() -> Collision:
|
||||
"""Return the in-progress collision.
|
||||
|
||||
Category: **Gameplay Functions**
|
||||
"""
|
||||
"""Return the in-progress collision."""
|
||||
return _collision
|
||||
|
|
|
|||
5
dist/ba_data/python/bascenev1/_coopgame.py
vendored
5
dist/ba_data/python/bascenev1/_coopgame.py
vendored
|
|
@ -23,10 +23,7 @@ TeamT = TypeVar('TeamT', bound='bascenev1.Team')
|
|||
|
||||
|
||||
class CoopGameActivity(GameActivity[PlayerT, TeamT]):
|
||||
"""Base class for cooperative-mode games.
|
||||
|
||||
Category: **Gameplay Classes**
|
||||
"""
|
||||
"""Base class for cooperative-mode games."""
|
||||
|
||||
# We can assume our session is a CoopSession.
|
||||
session: bascenev1.CoopSession
|
||||
|
|
|
|||
|
|
@ -20,13 +20,10 @@ TEAM_NAMES = ['Good Guys']
|
|||
|
||||
|
||||
class CoopSession(Session):
|
||||
"""A bascenev1.Session which runs cooperative-mode games.
|
||||
"""A session which runs cooperative-mode games.
|
||||
|
||||
Category: **Gameplay Classes**
|
||||
|
||||
These generally consist of 1-4 players against
|
||||
the computer and include functionality such as
|
||||
high score lists.
|
||||
These generally consist of 1-4 players against the computer and
|
||||
include functionality such as high score lists.
|
||||
"""
|
||||
|
||||
use_teams = True
|
||||
|
|
|
|||
18
dist/ba_data/python/bascenev1/_dependency.py
vendored
18
dist/ba_data/python/bascenev1/_dependency.py
vendored
|
|
@ -22,8 +22,6 @@ T = TypeVar('T', bound='DependencyComponent')
|
|||
class Dependency(Generic[T]):
|
||||
"""A dependency on a DependencyComponent (with an optional config).
|
||||
|
||||
Category: **Dependency Classes**
|
||||
|
||||
This class is used to request and access functionality provided
|
||||
by other DependencyComponent classes from a DependencyComponent class.
|
||||
The class functions as a descriptor, allowing dependencies to
|
||||
|
|
@ -92,10 +90,7 @@ class Dependency(Generic[T]):
|
|||
|
||||
|
||||
class DependencyComponent:
|
||||
"""Base class for all classes that can act as or use dependencies.
|
||||
|
||||
Category: **Dependency Classes**
|
||||
"""
|
||||
"""Base class for all classes that can act as or use dependencies."""
|
||||
|
||||
_dep_entry: weakref.ref[DependencyEntry]
|
||||
|
||||
|
|
@ -173,8 +168,6 @@ class DependencyEntry:
|
|||
class DependencySet(Generic[T]):
|
||||
"""Set of resolved dependencies and their associated data.
|
||||
|
||||
Category: **Dependency Classes**
|
||||
|
||||
To use DependencyComponents, a set must be created, resolved, and then
|
||||
loaded. The DependencyComponents are only valid while the set remains
|
||||
in existence.
|
||||
|
|
@ -296,10 +289,7 @@ class DependencySet(Generic[T]):
|
|||
|
||||
|
||||
class AssetPackage(DependencyComponent):
|
||||
"""bascenev1.DependencyComponent representing a bundled package of assets.
|
||||
|
||||
Category: **Asset Classes**
|
||||
"""
|
||||
"""bascenev1.DependencyComponent representing a package of assets."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
|
|
@ -430,9 +420,7 @@ def test_depset() -> None:
|
|||
|
||||
|
||||
class DependencyError(Exception):
|
||||
"""Exception raised when one or more bascenev1.Dependency items are missing.
|
||||
|
||||
Category: **Exception Classes**
|
||||
""":class:`Exception` raised when bascenev1.Dependency items are missing.
|
||||
|
||||
(this will generally be missing assets).
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -15,10 +15,7 @@ if TYPE_CHECKING:
|
|||
|
||||
|
||||
class DualTeamSession(MultiTeamSession):
|
||||
"""bascenev1.Session type for teams mode games.
|
||||
|
||||
Category: **Gameplay Classes**
|
||||
"""
|
||||
"""bascenev1.Session type for teams mode games."""
|
||||
|
||||
# Base class overrides:
|
||||
use_teams = True
|
||||
|
|
|
|||
|
|
@ -16,10 +16,7 @@ if TYPE_CHECKING:
|
|||
|
||||
|
||||
class FreeForAllSession(MultiTeamSession):
|
||||
"""bascenev1.Session type for free-for-all mode games.
|
||||
|
||||
Category: **Gameplay Classes**
|
||||
"""
|
||||
"""bascenev1.Session type for free-for-all mode games."""
|
||||
|
||||
use_teams = False
|
||||
use_team_colors = False
|
||||
|
|
|
|||
18
dist/ba_data/python/bascenev1/_gameactivity.py
vendored
18
dist/ba_data/python/bascenev1/_gameactivity.py
vendored
|
|
@ -31,10 +31,7 @@ TeamT = TypeVar('TeamT', bound='bascenev1.Team')
|
|||
|
||||
|
||||
class GameActivity(Activity[PlayerT, TeamT]):
|
||||
"""Common base class for all game bascenev1.Activities.
|
||||
|
||||
Category: **Gameplay Classes**
|
||||
"""
|
||||
"""Common base class for all game bascenev1.Activities."""
|
||||
|
||||
# pylint: disable=too-many-public-methods
|
||||
|
||||
|
|
@ -198,7 +195,7 @@ class GameActivity(Activity[PlayerT, TeamT]):
|
|||
def supports_session_type(
|
||||
cls, sessiontype: type[bascenev1.Session]
|
||||
) -> bool:
|
||||
"""Return whether this game supports the provided Session type."""
|
||||
"""Return whether this game supports the provided session type."""
|
||||
from bascenev1._multiteamsession import MultiTeamSession
|
||||
|
||||
# By default, games support any versus mode
|
||||
|
|
@ -208,8 +205,8 @@ class GameActivity(Activity[PlayerT, TeamT]):
|
|||
"""Instantiate the Activity."""
|
||||
super().__init__(settings)
|
||||
|
||||
# Holds some flattened info about the player set at the point
|
||||
# when on_begin() is called.
|
||||
#: Holds some flattened info about the player set at the point
|
||||
#: when :meth:`on_begin()` is called.
|
||||
self.initialplayerinfos: list[bascenev1.PlayerInfo] | None = None
|
||||
|
||||
# Go ahead and get our map loading.
|
||||
|
|
@ -794,9 +791,10 @@ class GameActivity(Activity[PlayerT, TeamT]):
|
|||
self.spawn_player(player)
|
||||
|
||||
def spawn_player(self, player: PlayerT) -> bascenev1.Actor:
|
||||
"""Spawn *something* for the provided bascenev1.Player.
|
||||
"""Spawn *something* for the provided player.
|
||||
|
||||
The default implementation simply calls spawn_player_spaz().
|
||||
The default implementation simply calls
|
||||
:meth:`spawn_player_spaz()`.
|
||||
"""
|
||||
assert player # Dead references should never be passed as args.
|
||||
|
||||
|
|
@ -808,7 +806,7 @@ class GameActivity(Activity[PlayerT, TeamT]):
|
|||
position: Sequence[float] = (0, 0, 0),
|
||||
angle: float | None = None,
|
||||
) -> PlayerSpaz:
|
||||
"""Create and wire up a bascenev1.PlayerSpaz for the provided Player."""
|
||||
"""Create and wire up a player-spaz for the provided player."""
|
||||
# pylint: disable=too-many-locals
|
||||
# pylint: disable=cyclic-import
|
||||
from bascenev1._gameutils import animate
|
||||
|
|
|
|||
29
dist/ba_data/python/bascenev1/_gameresults.py
vendored
29
dist/ba_data/python/bascenev1/_gameresults.py
vendored
|
|
@ -21,20 +21,17 @@ if TYPE_CHECKING:
|
|||
|
||||
@dataclass
|
||||
class WinnerGroup:
|
||||
"""Entry for a winning team or teams calculated by game-results."""
|
||||
"""Winning team or teams as calculated by a :class:`GameResults`."""
|
||||
|
||||
score: int | None
|
||||
teams: Sequence[bascenev1.SessionTeam]
|
||||
teams: list[bascenev1.SessionTeam]
|
||||
|
||||
|
||||
class GameResults:
|
||||
"""
|
||||
Results for a completed game.
|
||||
"""Results for a completed game.
|
||||
|
||||
Category: **Gameplay Classes**
|
||||
|
||||
Upon completion, a game should fill one of these out and pass it to its
|
||||
bascenev1.Activity.end call.
|
||||
Upon completion, a game should fill one of these out and pass it to
|
||||
its :meth:`~bascenev1.Activity.end()` call.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
|
|
@ -69,8 +66,8 @@ class GameResults:
|
|||
def set_team_score(self, team: bascenev1.Team, score: int | None) -> None:
|
||||
"""Set the score for a given team.
|
||||
|
||||
This can be a number or None.
|
||||
(see the none_is_winner arg in the constructor)
|
||||
This can be a number or None (see the ``none_is_winner`` arg in
|
||||
the constructor).
|
||||
"""
|
||||
assert isinstance(team, Team)
|
||||
sessionteam = team.sessionteam
|
||||
|
|
@ -79,7 +76,7 @@ class GameResults:
|
|||
def get_sessionteam_score(
|
||||
self, sessionteam: bascenev1.SessionTeam
|
||||
) -> int | None:
|
||||
"""Return the score for a given bascenev1.SessionTeam."""
|
||||
"""Return the score for a given team."""
|
||||
assert isinstance(sessionteam, SessionTeam)
|
||||
for score in list(self._scores.values()):
|
||||
if score[0]() is sessionteam:
|
||||
|
|
@ -90,7 +87,7 @@ class GameResults:
|
|||
|
||||
@property
|
||||
def sessionteams(self) -> list[bascenev1.SessionTeam]:
|
||||
"""Return all bascenev1.SessionTeams in the results."""
|
||||
"""Return all teams in the results."""
|
||||
if not self._game_set:
|
||||
raise RuntimeError("Can't get teams until game is set.")
|
||||
teams = []
|
||||
|
|
@ -104,13 +101,13 @@ class GameResults:
|
|||
def has_score_for_sessionteam(
|
||||
self, sessionteam: bascenev1.SessionTeam
|
||||
) -> bool:
|
||||
"""Return whether there is a score for a given session-team."""
|
||||
"""Return whether there is a score for a given team."""
|
||||
return any(s[0]() is sessionteam for s in self._scores.values())
|
||||
|
||||
def get_sessionteam_score_str(
|
||||
self, sessionteam: bascenev1.SessionTeam
|
||||
) -> babase.Lstr:
|
||||
"""Return the score for the given session-team as an Lstr.
|
||||
"""Return the score for the given team as an :class:`~bascenev1.Lstr`.
|
||||
|
||||
(properly formatted for the score type.)
|
||||
"""
|
||||
|
|
@ -163,7 +160,7 @@ class GameResults:
|
|||
|
||||
@property
|
||||
def winning_sessionteam(self) -> bascenev1.SessionTeam | None:
|
||||
"""The winning SessionTeam if there is exactly one, or else None."""
|
||||
"""The winning team if there is exactly one, or else None."""
|
||||
if not self._game_set:
|
||||
raise RuntimeError("Can't get winners until game is set.")
|
||||
winners = self.winnergroups
|
||||
|
|
@ -173,7 +170,7 @@ class GameResults:
|
|||
|
||||
@property
|
||||
def winnergroups(self) -> list[WinnerGroup]:
|
||||
"""Get an ordered list of winner groups."""
|
||||
"""The ordered list of winner-groups."""
|
||||
if not self._game_set:
|
||||
raise RuntimeError("Can't get winners until game is set.")
|
||||
|
||||
|
|
|
|||
16
dist/ba_data/python/bascenev1/_gameutils.py
vendored
16
dist/ba_data/python/bascenev1/_gameutils.py
vendored
|
|
@ -31,10 +31,7 @@ TROPHY_CHARS = {
|
|||
|
||||
@dataclass
|
||||
class GameTip:
|
||||
"""Defines a tip presentable to the user at the start of a game.
|
||||
|
||||
Category: **Gameplay Classes**
|
||||
"""
|
||||
"""Defines a tip presentable to the user at the start of a game."""
|
||||
|
||||
text: str
|
||||
icon: bascenev1.Texture | None = None
|
||||
|
|
@ -57,8 +54,6 @@ def animate(
|
|||
) -> bascenev1.Node:
|
||||
"""Animate values on a target bascenev1.Node.
|
||||
|
||||
Category: **Gameplay Functions**
|
||||
|
||||
Creates an 'animcurve' node with the provided values and time as an input,
|
||||
connect it to the provided attribute, and set it to die with the target.
|
||||
Key values are provided as time:value dictionary pairs. Time values are
|
||||
|
|
@ -118,8 +113,6 @@ def animate_array(
|
|||
) -> None:
|
||||
"""Animate an array of values on a target bascenev1.Node.
|
||||
|
||||
Category: **Gameplay Functions**
|
||||
|
||||
Like bs.animate, but operates on array attributes.
|
||||
"""
|
||||
combine = _bascenev1.newnode('combine', owner=node, attrs={'size': size})
|
||||
|
|
@ -176,10 +169,7 @@ def animate_array(
|
|||
def show_damage_count(
|
||||
damage: str, position: Sequence[float], direction: Sequence[float]
|
||||
) -> None:
|
||||
"""Pop up a damage count at a position in space.
|
||||
|
||||
Category: **Gameplay Functions**
|
||||
"""
|
||||
"""Pop up a damage count at a position in space."""
|
||||
lifespan = 1.0
|
||||
app = babase.app
|
||||
|
||||
|
|
@ -239,8 +229,6 @@ def show_damage_count(
|
|||
def cameraflash(duration: float = 999.0) -> None:
|
||||
"""Create a strobing camera flash effect.
|
||||
|
||||
Category: **Gameplay Functions**
|
||||
|
||||
(as seen when a team wins a game)
|
||||
Duration is in seconds.
|
||||
"""
|
||||
|
|
|
|||
50
dist/ba_data/python/bascenev1/_level.py
vendored
50
dist/ba_data/python/bascenev1/_level.py
vendored
|
|
@ -16,10 +16,7 @@ if TYPE_CHECKING:
|
|||
|
||||
|
||||
class Level:
|
||||
"""An entry in a bascenev1.Campaign.
|
||||
|
||||
Category: **Gameplay Classes**
|
||||
"""
|
||||
"""An entry in a :class:`~bascenev1.Campaign`."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
|
@ -46,7 +43,7 @@ class Level:
|
|||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""The unique name for this Level."""
|
||||
"""The unique name for this level."""
|
||||
return self._name
|
||||
|
||||
def get_settings(self) -> dict[str, Any]:
|
||||
|
|
@ -60,16 +57,12 @@ class Level:
|
|||
|
||||
@property
|
||||
def preview_texture_name(self) -> str:
|
||||
"""The preview texture name for this Level."""
|
||||
"""The preview texture name for this level."""
|
||||
return self._preview_texture_name
|
||||
|
||||
# def get_preview_texture(self) -> bauiv1.Texture:
|
||||
# """Load/return the preview Texture for this Level."""
|
||||
# return _bauiv1.gettexture(self._preview_texture_name)
|
||||
|
||||
@property
|
||||
def displayname(self) -> bascenev1.Lstr:
|
||||
"""The localized name for this Level."""
|
||||
"""The localized name for this level."""
|
||||
return babase.Lstr(
|
||||
translate=(
|
||||
'coopLevelNames',
|
||||
|
|
@ -86,20 +79,20 @@ class Level:
|
|||
|
||||
@property
|
||||
def gametype(self) -> type[bascenev1.GameActivity]:
|
||||
"""The type of game used for this Level."""
|
||||
"""The type of game used for this level."""
|
||||
return self._gametype
|
||||
|
||||
@property
|
||||
def campaign(self) -> bascenev1.Campaign | None:
|
||||
"""The baclassic.Campaign this Level is associated with, or None."""
|
||||
"""The campaign this level is associated with, or None."""
|
||||
return None if self._campaign is None else self._campaign()
|
||||
|
||||
@property
|
||||
def index(self) -> int:
|
||||
"""The zero-based index of this Level in its baclassic.Campaign.
|
||||
"""The zero-based index of this level in its campaign.
|
||||
|
||||
Access results in a RuntimeError if the Level is not assigned to a
|
||||
Campaign.
|
||||
Access results in a RuntimeError if the level is not assigned to
|
||||
a campaign.
|
||||
"""
|
||||
if self._index is None:
|
||||
raise RuntimeError('Level is not part of a Campaign')
|
||||
|
|
@ -107,7 +100,7 @@ class Level:
|
|||
|
||||
@property
|
||||
def complete(self) -> bool:
|
||||
"""Whether this Level has been completed."""
|
||||
"""Whether this level has been completed."""
|
||||
config = self._get_config_dict()
|
||||
val = config.get('Complete', False)
|
||||
assert isinstance(val, bool)
|
||||
|
|
@ -123,7 +116,7 @@ class Level:
|
|||
config['Complete'] = val
|
||||
|
||||
def get_high_scores(self) -> dict:
|
||||
"""Return the current high scores for this Level."""
|
||||
"""Return the current high scores for this level."""
|
||||
config = self._get_config_dict()
|
||||
high_scores_key = 'High Scores' + self.get_score_version_string()
|
||||
if high_scores_key not in config:
|
||||
|
|
@ -137,10 +130,11 @@ class Level:
|
|||
config[high_scores_key] = high_scores
|
||||
|
||||
def get_score_version_string(self) -> str:
|
||||
"""Return the score version string for this Level.
|
||||
"""Return the score version string for this level.
|
||||
|
||||
If a Level's gameplay changes significantly, its version string
|
||||
can be changed to separate its new high score lists/etc. from the old.
|
||||
If a level's gameplay changes significantly, its version string
|
||||
can be changed to separate its new high score lists/etc. from
|
||||
the old.
|
||||
"""
|
||||
if self._score_version_string is None:
|
||||
scorever = self._gametype.getscoreconfig().version
|
||||
|
|
@ -152,13 +146,13 @@ class Level:
|
|||
|
||||
@property
|
||||
def rating(self) -> float:
|
||||
"""The current rating for this Level."""
|
||||
"""The current rating for this level."""
|
||||
val = self._get_config_dict().get('Rating', 0.0)
|
||||
assert isinstance(val, float)
|
||||
return val
|
||||
|
||||
def set_rating(self, rating: float) -> None:
|
||||
"""Set a rating for this Level, replacing the old ONLY IF higher."""
|
||||
"""Set a rating for this level, replacing the old ONLY IF higher."""
|
||||
old_rating = self.rating
|
||||
config = self._get_config_dict()
|
||||
config['Rating'] = max(old_rating, rating)
|
||||
|
|
@ -166,8 +160,9 @@ class Level:
|
|||
def _get_config_dict(self) -> dict[str, Any]:
|
||||
"""Return/create the persistent state dict for this level.
|
||||
|
||||
The referenced dict exists under the game's config dict and
|
||||
can be modified in place."""
|
||||
The referenced dict exists under the game's config dict and can
|
||||
be modified in place.
|
||||
"""
|
||||
campaign = self.campaign
|
||||
if campaign is None:
|
||||
raise RuntimeError('Level is not in a campaign.')
|
||||
|
|
@ -179,8 +174,9 @@ class Level:
|
|||
return val
|
||||
|
||||
def set_campaign(self, campaign: bascenev1.Campaign, index: int) -> None:
|
||||
"""For use by baclassic.Campaign when adding levels to itself.
|
||||
"""Internal: Used by campaign when adding levels to itself.
|
||||
|
||||
(internal)"""
|
||||
:meta private:
|
||||
"""
|
||||
self._campaign = weakref.ref(campaign)
|
||||
self._index = index
|
||||
|
|
|
|||
34
dist/ba_data/python/bascenev1/_lobby.py
vendored
34
dist/ba_data/python/bascenev1/_lobby.py
vendored
|
|
@ -178,10 +178,7 @@ class ChangeMessage:
|
|||
|
||||
|
||||
class Chooser:
|
||||
"""A character/team selector for a bascenev1.Player.
|
||||
|
||||
Category: Gameplay Classes
|
||||
"""
|
||||
"""A character/team selector for a player."""
|
||||
|
||||
def __del__(self) -> None:
|
||||
# Just kill off our base node; the rest should go down with it.
|
||||
|
|
@ -364,7 +361,7 @@ class Chooser:
|
|||
|
||||
@property
|
||||
def sessionplayer(self) -> bascenev1.SessionPlayer:
|
||||
"""The bascenev1.SessionPlayer associated with this chooser."""
|
||||
"""The session-player associated with this chooser."""
|
||||
return self._sessionplayer
|
||||
|
||||
@property
|
||||
|
|
@ -373,11 +370,17 @@ class Chooser:
|
|||
return self._ready
|
||||
|
||||
def set_vpos(self, vpos: float) -> None:
|
||||
"""(internal)"""
|
||||
"""(internal)
|
||||
|
||||
:meta private:
|
||||
"""
|
||||
self._vpos = vpos
|
||||
|
||||
def set_dead(self, val: bool) -> None:
|
||||
"""(internal)"""
|
||||
"""(internal)
|
||||
|
||||
:meta private:
|
||||
"""
|
||||
self._dead = val
|
||||
|
||||
@property
|
||||
|
|
@ -387,7 +390,7 @@ class Chooser:
|
|||
|
||||
@property
|
||||
def lobby(self) -> bascenev1.Lobby:
|
||||
"""The chooser's baclassic.Lobby."""
|
||||
"""The chooser's lobby."""
|
||||
lobby = self._lobby()
|
||||
if lobby is None:
|
||||
raise babase.NotFoundError('Lobby does not exist.')
|
||||
|
|
@ -940,10 +943,7 @@ class Chooser:
|
|||
|
||||
|
||||
class Lobby:
|
||||
"""Container for baclassic.Choosers.
|
||||
|
||||
Category: Gameplay Classes
|
||||
"""
|
||||
"""Environment where players can selecting characters, etc."""
|
||||
|
||||
def __del__(self) -> None:
|
||||
# Reset any players that still have a chooser in us.
|
||||
|
|
@ -987,7 +987,7 @@ class Lobby:
|
|||
|
||||
@property
|
||||
def use_team_colors(self) -> bool:
|
||||
"""A bool for whether this lobby is using team colors.
|
||||
"""Whether this lobby is using team colors.
|
||||
|
||||
If False, inidividual player colors are used instead.
|
||||
"""
|
||||
|
|
@ -995,7 +995,7 @@ class Lobby:
|
|||
|
||||
@property
|
||||
def sessionteams(self) -> list[bascenev1.SessionTeam]:
|
||||
"""bascenev1.SessionTeams available in this lobby."""
|
||||
"""The teams available in this lobby."""
|
||||
allteams = []
|
||||
for tref in self._sessionteams:
|
||||
team = tref()
|
||||
|
|
@ -1004,7 +1004,7 @@ class Lobby:
|
|||
return allteams
|
||||
|
||||
def get_choosers(self) -> list[Chooser]:
|
||||
"""Return the lobby's current choosers."""
|
||||
"""The current choosers present."""
|
||||
return self.choosers
|
||||
|
||||
def create_join_info(self) -> JoinInfo:
|
||||
|
|
@ -1070,8 +1070,8 @@ class Lobby:
|
|||
found = True
|
||||
|
||||
# Mark it as dead since there could be more
|
||||
# change-commands/etc coming in still for it;
|
||||
# want to avoid duplicate player-adds/etc.
|
||||
# change-commands/etc coming in still for it; want to
|
||||
# avoid duplicate player-adds/etc.
|
||||
chooser.set_dead(True)
|
||||
self.choosers.remove(chooser)
|
||||
break
|
||||
|
|
|
|||
14
dist/ba_data/python/bascenev1/_map.py
vendored
14
dist/ba_data/python/bascenev1/_map.py
vendored
|
|
@ -20,8 +20,6 @@ if TYPE_CHECKING:
|
|||
def get_filtered_map_name(name: str) -> str:
|
||||
"""Filter a map name to account for name changes, etc.
|
||||
|
||||
Category: **Asset Functions**
|
||||
|
||||
This can be used to support old playlists, etc.
|
||||
"""
|
||||
# Some legacy name fallbacks... can remove these eventually.
|
||||
|
|
@ -33,18 +31,12 @@ def get_filtered_map_name(name: str) -> str:
|
|||
|
||||
|
||||
def get_map_display_string(name: str) -> babase.Lstr:
|
||||
"""Return a babase.Lstr for displaying a given map\'s name.
|
||||
|
||||
Category: **Asset Functions**
|
||||
"""
|
||||
"""Return a babase.Lstr for displaying a given map's name."""
|
||||
return babase.Lstr(translate=('mapsNames', name))
|
||||
|
||||
|
||||
def get_map_class(name: str) -> type[Map]:
|
||||
"""Return a map type given a name.
|
||||
|
||||
Category: **Asset Functions**
|
||||
"""
|
||||
"""Return a map type given a name."""
|
||||
assert babase.app.classic is not None
|
||||
name = get_filtered_map_name(name)
|
||||
try:
|
||||
|
|
@ -57,8 +49,6 @@ def get_map_class(name: str) -> type[Map]:
|
|||
class Map(Actor):
|
||||
"""A game map.
|
||||
|
||||
Category: **Gameplay Classes**
|
||||
|
||||
Consists of a collection of terrain nodes, metadata, and other
|
||||
functionality comprising a game map.
|
||||
"""
|
||||
|
|
|
|||
80
dist/ba_data/python/bascenev1/_messages.py
vendored
80
dist/ba_data/python/bascenev1/_messages.py
vendored
|
|
@ -28,17 +28,11 @@ UNHANDLED = _UnhandledType()
|
|||
|
||||
@dataclass
|
||||
class OutOfBoundsMessage:
|
||||
"""A message telling an object that it is out of bounds.
|
||||
|
||||
Category: Message Classes
|
||||
"""
|
||||
"""A message telling an object that it is out of bounds."""
|
||||
|
||||
|
||||
class DeathType(Enum):
|
||||
"""A reason for a death.
|
||||
|
||||
Category: Enums
|
||||
"""
|
||||
"""A reason for a death."""
|
||||
|
||||
GENERIC = 'generic'
|
||||
OUT_OF_BOUNDS = 'out_of_bounds'
|
||||
|
|
@ -52,29 +46,24 @@ class DeathType(Enum):
|
|||
class DieMessage:
|
||||
"""A message telling an object to die.
|
||||
|
||||
Category: **Message Classes**
|
||||
|
||||
Most bascenev1.Actor-s respond to this.
|
||||
"""
|
||||
|
||||
#: If this is set to True, the actor should disappear immediately.
|
||||
#: This is for 'removing' stuff from the game more so than 'killing'
|
||||
#: it. If False, the actor should die a 'normal' death and can take
|
||||
#: its time with lingering corpses, sound effects, etc.
|
||||
immediate: bool = False
|
||||
"""If this is set to True, the actor should disappear immediately.
|
||||
This is for 'removing' stuff from the game more so than 'killing'
|
||||
it. If False, the actor should die a 'normal' death and can take
|
||||
its time with lingering corpses, sound effects, etc."""
|
||||
|
||||
#: The particular reason for death.
|
||||
how: DeathType = DeathType.GENERIC
|
||||
"""The particular reason for death."""
|
||||
|
||||
|
||||
PlayerT = TypeVar('PlayerT', bound='bascenev1.Player')
|
||||
|
||||
|
||||
class PlayerDiedMessage:
|
||||
"""A message saying a bascenev1.Player has died.
|
||||
|
||||
Category: **Message Classes**
|
||||
"""
|
||||
"""A message saying a bascenev1.Player has died."""
|
||||
|
||||
killed: bool
|
||||
"""If True, the player was killed;
|
||||
|
|
@ -129,8 +118,6 @@ class PlayerDiedMessage:
|
|||
class StandMessage:
|
||||
"""A message telling an object to move to a position in space.
|
||||
|
||||
Category: **Message Classes**
|
||||
|
||||
Used when teleporting players to home base, etc.
|
||||
"""
|
||||
|
||||
|
|
@ -143,10 +130,7 @@ class StandMessage:
|
|||
|
||||
@dataclass
|
||||
class PickUpMessage:
|
||||
"""Tells an object that it has picked something up.
|
||||
|
||||
Category: **Message Classes**
|
||||
"""
|
||||
"""Tells an object that it has picked something up."""
|
||||
|
||||
node: bascenev1.Node
|
||||
"""The bascenev1.Node that is getting picked up."""
|
||||
|
|
@ -154,18 +138,12 @@ class PickUpMessage:
|
|||
|
||||
@dataclass
|
||||
class DropMessage:
|
||||
"""Tells an object that it has dropped what it was holding.
|
||||
|
||||
Category: **Message Classes**
|
||||
"""
|
||||
"""Tells an object that it has dropped what it was holding."""
|
||||
|
||||
|
||||
@dataclass
|
||||
class PickedUpMessage:
|
||||
"""Tells an object that it has been picked up by something.
|
||||
|
||||
Category: **Message Classes**
|
||||
"""
|
||||
"""Tells an object that it has been picked up by something."""
|
||||
|
||||
node: bascenev1.Node
|
||||
"""The bascenev1.Node doing the picking up."""
|
||||
|
|
@ -173,10 +151,7 @@ class PickedUpMessage:
|
|||
|
||||
@dataclass
|
||||
class DroppedMessage:
|
||||
"""Tells an object that it has been dropped.
|
||||
|
||||
Category: **Message Classes**
|
||||
"""
|
||||
"""Tells an object that it has been dropped."""
|
||||
|
||||
node: bascenev1.Node
|
||||
"""The bascenev1.Node doing the dropping."""
|
||||
|
|
@ -184,18 +159,12 @@ class DroppedMessage:
|
|||
|
||||
@dataclass
|
||||
class ShouldShatterMessage:
|
||||
"""Tells an object that it should shatter.
|
||||
|
||||
Category: **Message Classes**
|
||||
"""
|
||||
"""Tells an object that it should shatter."""
|
||||
|
||||
|
||||
@dataclass
|
||||
class ImpactDamageMessage:
|
||||
"""Tells an object that it has been jarred violently.
|
||||
|
||||
Category: **Message Classes**
|
||||
"""
|
||||
"""Tells an object that it has been jarred violently."""
|
||||
|
||||
intensity: float
|
||||
"""The intensity of the impact."""
|
||||
|
|
@ -205,26 +174,21 @@ class ImpactDamageMessage:
|
|||
class FreezeMessage:
|
||||
"""Tells an object to become frozen.
|
||||
|
||||
Category: **Message Classes**
|
||||
|
||||
As seen in the effects of an ice bascenev1.Bomb.
|
||||
"""
|
||||
|
||||
time: float = 5.0
|
||||
"""The amount of time the object will be frozen."""
|
||||
|
||||
|
||||
@dataclass
|
||||
class ThawMessage:
|
||||
"""Tells an object to stop being frozen.
|
||||
|
||||
Category: **Message Classes**
|
||||
"""
|
||||
"""Tells an object to stop being frozen."""
|
||||
|
||||
|
||||
@dataclass
|
||||
class CelebrateMessage:
|
||||
"""Tells an object to celebrate.
|
||||
|
||||
Category: **Message Classes**
|
||||
"""
|
||||
"""Tells an object to celebrate."""
|
||||
|
||||
duration: float = 10.0
|
||||
"""Amount of time to celebrate in seconds."""
|
||||
|
|
@ -233,10 +197,8 @@ class CelebrateMessage:
|
|||
class HitMessage:
|
||||
"""Tells an object it has been hit in some way.
|
||||
|
||||
Category: **Message Classes**
|
||||
|
||||
This is used by punches, explosions, etc to convey
|
||||
their effect to a target.
|
||||
This is used by punches, explosions, etc to convey their effect to a
|
||||
target.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
|
|
|
|||
|
|
@ -25,8 +25,6 @@ DEFAULT_TEAM_NAMES = ('Blue', 'Red')
|
|||
class MultiTeamSession(Session):
|
||||
"""Common base for DualTeamSession and FreeForAllSession.
|
||||
|
||||
Category: **Gameplay Classes**
|
||||
|
||||
Free-for-all-mode is essentially just teams-mode with each
|
||||
bascenev1.Player having their own bascenev1.Team, so there is much
|
||||
overlap in functionality.
|
||||
|
|
|
|||
15
dist/ba_data/python/bascenev1/_music.py
vendored
15
dist/ba_data/python/bascenev1/_music.py
vendored
|
|
@ -16,8 +16,6 @@ if TYPE_CHECKING:
|
|||
class MusicType(Enum):
|
||||
"""Types of music available to play in-game.
|
||||
|
||||
Category: **Enums**
|
||||
|
||||
These do not correspond to specific pieces of music, but rather to
|
||||
'situations'. The actual music played for each type can be overridden
|
||||
by the game or by the user.
|
||||
|
|
@ -50,16 +48,15 @@ class MusicType(Enum):
|
|||
def setmusic(musictype: MusicType | None, continuous: bool = False) -> None:
|
||||
"""Set the app to play (or stop playing) a certain type of music.
|
||||
|
||||
category: **Gameplay Functions**
|
||||
|
||||
This function will handle loading and playing sound assets as necessary,
|
||||
and also supports custom user soundtracks on specific platforms so the
|
||||
user can override particular game music with their own.
|
||||
This function will handle loading and playing sound assets as
|
||||
necessary, and also supports custom user soundtracks on specific
|
||||
platforms so the user can override particular game music with their
|
||||
own.
|
||||
|
||||
Pass None to stop music.
|
||||
|
||||
if 'continuous' is True and musictype is the same as what is already
|
||||
playing, the playing track will not be restarted.
|
||||
if ``continuous`` is True and musictype is the same as what is
|
||||
already playing, the playing track will not be restarted.
|
||||
"""
|
||||
|
||||
# All we do here now is set a few music attrs on the current globals
|
||||
|
|
|
|||
2
dist/ba_data/python/bascenev1/_nodeactor.py
vendored
2
dist/ba_data/python/bascenev1/_nodeactor.py
vendored
|
|
@ -18,8 +18,6 @@ if TYPE_CHECKING:
|
|||
class NodeActor(Actor):
|
||||
"""A simple bascenev1.Actor type that wraps a single bascenev1.Node.
|
||||
|
||||
Category: **Gameplay Classes**
|
||||
|
||||
This Actor will delete its Node when told to die, and it's
|
||||
exists() call will return whether the Node still exists or not.
|
||||
"""
|
||||
|
|
|
|||
21
dist/ba_data/python/bascenev1/_player.py
vendored
21
dist/ba_data/python/bascenev1/_player.py
vendored
|
|
@ -24,10 +24,7 @@ TeamT = TypeVar('TeamT', bound='bascenev1.Team')
|
|||
|
||||
@dataclass
|
||||
class PlayerInfo:
|
||||
"""Holds basic info about a player.
|
||||
|
||||
Category: Gameplay Classes
|
||||
"""
|
||||
"""Holds basic info about a player."""
|
||||
|
||||
name: str
|
||||
character: str
|
||||
|
|
@ -35,10 +32,7 @@ class PlayerInfo:
|
|||
|
||||
@dataclass
|
||||
class StandLocation:
|
||||
"""Describes a point in space and an angle to face.
|
||||
|
||||
Category: Gameplay Classes
|
||||
"""
|
||||
"""Describes a point in space and an angle to face."""
|
||||
|
||||
position: babase.Vec3
|
||||
angle: float | None = None
|
||||
|
|
@ -47,8 +41,6 @@ class StandLocation:
|
|||
class Player(Generic[TeamT]):
|
||||
"""A player in a specific bascenev1.Activity.
|
||||
|
||||
Category: Gameplay Classes
|
||||
|
||||
These correspond to bascenev1.SessionPlayer objects, but are associated
|
||||
with a single bascenev1.Activity instance. This allows activities to
|
||||
specify their own custom bascenev1.Player types.
|
||||
|
|
@ -282,8 +274,6 @@ class Player(Generic[TeamT]):
|
|||
class EmptyPlayer(Player['bascenev1.EmptyTeam']):
|
||||
"""An empty player for use by Activities that don't need to define one.
|
||||
|
||||
Category: Gameplay Classes
|
||||
|
||||
bascenev1.Player and bascenev1.Team are 'Generic' types, and so passing
|
||||
those top level classes as type arguments when defining a
|
||||
bascenev1.Activity reduces type safety. For example,
|
||||
|
|
@ -306,8 +296,6 @@ class EmptyPlayer(Player['bascenev1.EmptyTeam']):
|
|||
def playercast(totype: type[PlayerT], player: bascenev1.Player) -> PlayerT:
|
||||
"""Cast a bascenev1.Player to a specific bascenev1.Player subclass.
|
||||
|
||||
Category: Gameplay Functions
|
||||
|
||||
When writing type-checked code, sometimes code will deal with raw
|
||||
bascenev1.Player objects which need to be cast back to a game's actual
|
||||
player type so that access can be properly type-checked. This function
|
||||
|
|
@ -324,9 +312,6 @@ def playercast(totype: type[PlayerT], player: bascenev1.Player) -> PlayerT:
|
|||
def playercast_o(
|
||||
totype: type[PlayerT], player: bascenev1.Player | None
|
||||
) -> PlayerT | None:
|
||||
"""A variant of bascenev1.playercast() for use with optional Player values.
|
||||
|
||||
Category: Gameplay Functions
|
||||
"""
|
||||
"""A variant of bascenev1.playercast() for optional Player values."""
|
||||
assert isinstance(player, (totype, type(None)))
|
||||
return player
|
||||
|
|
|
|||
11
dist/ba_data/python/bascenev1/_powerup.py
vendored
11
dist/ba_data/python/bascenev1/_powerup.py
vendored
|
|
@ -17,9 +17,8 @@ if TYPE_CHECKING:
|
|||
class PowerupMessage:
|
||||
"""A message telling an object to accept a powerup.
|
||||
|
||||
Category: **Message Classes**
|
||||
|
||||
This message is normally received by touching a bascenev1.PowerupBox.
|
||||
This message is normally received by touching a
|
||||
bascenev1.PowerupBox.
|
||||
"""
|
||||
|
||||
poweruptype: str
|
||||
|
|
@ -38,10 +37,8 @@ class PowerupMessage:
|
|||
class PowerupAcceptMessage:
|
||||
"""A message informing a bascenev1.Powerup that it was accepted.
|
||||
|
||||
Category: **Message Classes**
|
||||
|
||||
This is generally sent in response to a bascenev1.PowerupMessage
|
||||
to inform the box (or whoever granted it) that it can go away.
|
||||
This is generally sent in response to a bascenev1.PowerupMessage to
|
||||
inform the box (or whoever granted it) that it can go away.
|
||||
"""
|
||||
|
||||
|
||||
|
|
|
|||
10
dist/ba_data/python/bascenev1/_score.py
vendored
10
dist/ba_data/python/bascenev1/_score.py
vendored
|
|
@ -14,10 +14,7 @@ if TYPE_CHECKING:
|
|||
|
||||
@unique
|
||||
class ScoreType(Enum):
|
||||
"""Type of scores.
|
||||
|
||||
Category: **Enums**
|
||||
"""
|
||||
"""Type of scores."""
|
||||
|
||||
SECONDS = 's'
|
||||
MILLISECONDS = 'ms'
|
||||
|
|
@ -26,10 +23,7 @@ class ScoreType(Enum):
|
|||
|
||||
@dataclass
|
||||
class ScoreConfig:
|
||||
"""Settings for how a game handles scores.
|
||||
|
||||
Category: **Gameplay Classes**
|
||||
"""
|
||||
"""Settings for how a game handles scores."""
|
||||
|
||||
label: str = 'Score'
|
||||
"""A label show to the user for scores; 'Score', 'Time Survived', etc."""
|
||||
|
|
|
|||
118
dist/ba_data/python/bascenev1/_session.py
vendored
118
dist/ba_data/python/bascenev1/_session.py
vendored
|
|
@ -40,58 +40,59 @@ def set_max_players_override(max_players: int | None) -> None:
|
|||
|
||||
|
||||
class Session:
|
||||
"""Defines a high level series of bascenev1.Activity-es.
|
||||
"""Wrangles a series of :class:`~bascenev1.Activity` instances.
|
||||
|
||||
Category: **Gameplay Classes**
|
||||
Examples of sessions are :class:`bascenev1.FreeForAllSession`,
|
||||
:class:`bascenev1.DualTeamSession`, and
|
||||
:class:`bascenev1.CoopSession`.
|
||||
|
||||
Examples of sessions are bascenev1.FreeForAllSession,
|
||||
bascenev1.DualTeamSession, and bascenev1.CoopSession.
|
||||
|
||||
A Session is responsible for wrangling and transitioning between various
|
||||
bascenev1.Activity instances such as mini-games and score-screens, and for
|
||||
maintaining state between them (players, teams, score tallies, etc).
|
||||
A session is responsible for wrangling and transitioning between
|
||||
various activity instances such as mini-games and score-screens, and
|
||||
for maintaining state between them (players, teams, score tallies,
|
||||
etc).
|
||||
"""
|
||||
|
||||
#: Whether this session groups players into an explicit set of teams.
|
||||
#: If this is off, a unique team is generated for each player that
|
||||
#: joins.
|
||||
use_teams: bool = False
|
||||
"""Whether this session groups players into an explicit set of
|
||||
teams. If this is off, a unique team is generated for each
|
||||
player that joins."""
|
||||
|
||||
#: Whether players on a team should all adopt the colors of that team
|
||||
#: instead of their own profile colors. This only applies if
|
||||
#: :attr:`use_teams` is enabled.
|
||||
use_team_colors: bool = True
|
||||
"""Whether players on a team should all adopt the colors of that
|
||||
team instead of their own profile colors. This only applies if
|
||||
use_teams is enabled."""
|
||||
|
||||
# Note: even though these are instance vars, we annotate and document them
|
||||
# at the class level so that looks better and nobody get lost while
|
||||
# reading large __init__
|
||||
# Note: even though these are instance vars, we annotate and
|
||||
# document them at the class level so that looks better and nobody
|
||||
# get lost while reading large __init__
|
||||
|
||||
#: The lobby instance where new players go to select a
|
||||
#: profile/team/etc. before being added to games. Be aware this value
|
||||
#: may be None if a session does not allow any such selection.
|
||||
lobby: bascenev1.Lobby
|
||||
"""The baclassic.Lobby instance where new bascenev1.Player-s go to select
|
||||
a Profile/Team/etc. before being added to games.
|
||||
Be aware this value may be None if a Session does not allow
|
||||
any such selection."""
|
||||
|
||||
#: The maximum number of players allowed in the Session.
|
||||
max_players: int
|
||||
"""The maximum number of players allowed in the Session."""
|
||||
|
||||
#: The minimum number of players who must be present for the Session
|
||||
#: to proceed past the initial joining screen
|
||||
min_players: int
|
||||
"""The minimum number of players who must be present for the Session
|
||||
to proceed past the initial joining screen"""
|
||||
|
||||
#: All players in the session. Note that most things should use the
|
||||
#: list of :class:`~bascenev1.Player` instances found in the
|
||||
#: :class:`~bascenev1.Activity`; not this. Some players, such as
|
||||
#: those who have not yet selected a character, will only be found on
|
||||
#: this list.
|
||||
sessionplayers: list[bascenev1.SessionPlayer]
|
||||
"""All bascenev1.SessionPlayers in the Session. Most things should use
|
||||
the list of bascenev1.Player-s in bascenev1.Activity; not this. Some
|
||||
players, such as those who have not yet selected a character, will
|
||||
only be found on this list."""
|
||||
|
||||
#: A shared dictionary for objects to use as storage on this session.
|
||||
#: Ensure that keys here are unique to avoid collisions.
|
||||
customdata: dict
|
||||
"""A shared dictionary for objects to use as storage on this session.
|
||||
Ensure that keys here are unique to avoid collisions."""
|
||||
|
||||
#: All the teams in the session. Most things will operate on the list
|
||||
#: of :class:`~bascenev1.Team` instances found in an
|
||||
#: :class:`~bascenev1.Activity`; not this.
|
||||
sessionteams: list[bascenev1.SessionTeam]
|
||||
"""All the bascenev1.SessionTeams in the Session. Most things should
|
||||
use the list of bascenev1.Team-s in bascenev1.Activity; not this."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
|
@ -243,7 +244,7 @@ class Session:
|
|||
|
||||
@property
|
||||
def sessionglobalsnode(self) -> bascenev1.Node:
|
||||
"""The sessionglobals bascenev1.Node for the session."""
|
||||
"""The sessionglobals node for the session."""
|
||||
node = self._sessionglobalsnode
|
||||
if not node:
|
||||
raise babase.NodeNotFoundError()
|
||||
|
|
@ -254,15 +255,15 @@ class Session:
|
|||
) -> bool:
|
||||
"""Ask ourself if we should allow joins during an Activity.
|
||||
|
||||
Note that for a join to be allowed, both the Session and Activity
|
||||
have to be ok with it (via this function and the
|
||||
Activity.allow_mid_activity_joins property.
|
||||
Note that for a join to be allowed, both the session and
|
||||
activity have to be ok with it (via this function and the
|
||||
:attr:`bascenev1.Activity.allow_mid_activity_joins` property.
|
||||
"""
|
||||
del activity # Unused.
|
||||
return True
|
||||
|
||||
def on_player_request(self, player: bascenev1.SessionPlayer) -> bool:
|
||||
"""Called when a new bascenev1.Player wants to join the Session.
|
||||
"""Called when a new player wants to join the session.
|
||||
|
||||
This should return True or False to accept/reject.
|
||||
"""
|
||||
|
|
@ -426,10 +427,10 @@ class Session:
|
|||
)
|
||||
|
||||
def end(self) -> None:
|
||||
"""Initiates an end to the session and a return to the main menu.
|
||||
"""Initiate an end to the session and a return to the main menu.
|
||||
|
||||
Note that this happens asynchronously, allowing the
|
||||
session and its activities to shut down gracefully.
|
||||
Note that this happens asynchronously, allowing the session and
|
||||
its activities to shut down gracefully.
|
||||
"""
|
||||
self._wants_to_end = True
|
||||
if self._next_activity is None:
|
||||
|
|
@ -457,10 +458,10 @@ class Session:
|
|||
self._ending = True # Prevent further actions.
|
||||
|
||||
def on_team_join(self, team: bascenev1.SessionTeam) -> None:
|
||||
"""Called when a new bascenev1.Team joins the session."""
|
||||
"""Called when a new team joins the session."""
|
||||
|
||||
def on_team_leave(self, team: bascenev1.SessionTeam) -> None:
|
||||
"""Called when a bascenev1.Team is leaving the session."""
|
||||
"""Called when a team is leaving the session."""
|
||||
|
||||
def end_activity(
|
||||
self,
|
||||
|
|
@ -469,12 +470,12 @@ class Session:
|
|||
delay: float,
|
||||
force: bool,
|
||||
) -> None:
|
||||
"""Commence shutdown of a bascenev1.Activity (if not already occurring).
|
||||
"""Commence shutdown of an activity (if not already occurring).
|
||||
|
||||
'delay' is the time delay before the Activity actually ends
|
||||
(in seconds). Further calls to end() will be ignored up until
|
||||
this time, unless 'force' is True, in which case the new results
|
||||
will replace the old.
|
||||
'delay' is the time delay before the activity actually ends (in
|
||||
seconds). Further calls to end the activity will be ignored up
|
||||
until this time, unless 'force' is True, in which case the new
|
||||
results will replace the old.
|
||||
"""
|
||||
# Only pay attention if this is coming from our current activity.
|
||||
if activity is not self._activity_retained:
|
||||
|
|
@ -530,13 +531,13 @@ class Session:
|
|||
self._session._in_set_activity = False
|
||||
|
||||
def setactivity(self, activity: bascenev1.Activity) -> None:
|
||||
"""Assign a new current bascenev1.Activity for the session.
|
||||
"""Assign a new current activity for the session.
|
||||
|
||||
Note that this will not change the current context to the new
|
||||
Activity's. Code must be run in the new activity's methods
|
||||
(on_transition_in, etc) to get it. (so you can't do
|
||||
session.setactivity(foo) and then bascenev1.newnode() to add a node
|
||||
to foo)
|
||||
activity's. Code must be run in the new activity's methods
|
||||
(:meth:`~bascenev1.Activity.on_transition_in()`, etc) to get it.
|
||||
(so you can't do ``session.setactivity(foo)`` and then
|
||||
``bascenev1.newnode()`` to add a node to foo).
|
||||
"""
|
||||
|
||||
# Make sure we don't get called recursively.
|
||||
|
|
@ -656,16 +657,16 @@ class Session:
|
|||
def on_activity_end(
|
||||
self, activity: bascenev1.Activity, results: Any
|
||||
) -> None:
|
||||
"""Called when the current bascenev1.Activity has ended.
|
||||
"""Called when the current activity has ended.
|
||||
|
||||
The bascenev1.Session should look at the results and start
|
||||
another bascenev1.Activity.
|
||||
The session should look at the results and start another
|
||||
activity.
|
||||
"""
|
||||
|
||||
def begin_next_activity(self) -> None:
|
||||
"""Called once the previous activity has been totally torn down.
|
||||
|
||||
This means we're ready to begin the next one
|
||||
This means we're ready to begin the next one.
|
||||
"""
|
||||
if self._next_activity is None:
|
||||
# Should this ever happen?
|
||||
|
|
@ -742,7 +743,10 @@ class Session:
|
|||
def transitioning_out_activity_was_freed(
|
||||
self, can_show_ad_on_death: bool
|
||||
) -> None:
|
||||
"""(internal)"""
|
||||
"""(internal)
|
||||
|
||||
:meta private:
|
||||
"""
|
||||
# pylint: disable=cyclic-import
|
||||
|
||||
# Since things should be generally still right now, it's a good time
|
||||
|
|
|
|||
35
dist/ba_data/python/bascenev1/_settings.py
vendored
35
dist/ba_data/python/bascenev1/_settings.py
vendored
|
|
@ -13,10 +13,7 @@ if TYPE_CHECKING:
|
|||
|
||||
@dataclass
|
||||
class Setting:
|
||||
"""Defines a user-controllable setting for a game or other entity.
|
||||
|
||||
Category: Gameplay Classes
|
||||
"""
|
||||
"""Defines a user-controllable setting for a game or other entity."""
|
||||
|
||||
name: str
|
||||
default: Any
|
||||
|
|
@ -24,20 +21,14 @@ class Setting:
|
|||
|
||||
@dataclass
|
||||
class BoolSetting(Setting):
|
||||
"""A boolean game setting.
|
||||
|
||||
Category: Settings Classes
|
||||
"""
|
||||
"""A boolean game setting."""
|
||||
|
||||
default: bool
|
||||
|
||||
|
||||
@dataclass
|
||||
class IntSetting(Setting):
|
||||
"""An integer game setting.
|
||||
|
||||
Category: Settings Classes
|
||||
"""
|
||||
"""An integer game setting."""
|
||||
|
||||
default: int
|
||||
min_value: int = 0
|
||||
|
|
@ -47,10 +38,7 @@ class IntSetting(Setting):
|
|||
|
||||
@dataclass
|
||||
class FloatSetting(Setting):
|
||||
"""A floating point game setting.
|
||||
|
||||
Category: Settings Classes
|
||||
"""
|
||||
"""A floating point game setting."""
|
||||
|
||||
default: float
|
||||
min_value: float = 0.0
|
||||
|
|
@ -60,20 +48,14 @@ class FloatSetting(Setting):
|
|||
|
||||
@dataclass
|
||||
class ChoiceSetting(Setting):
|
||||
"""A setting with multiple choices.
|
||||
|
||||
Category: Settings Classes
|
||||
"""
|
||||
"""A setting with multiple choices."""
|
||||
|
||||
choices: list[tuple[str, Any]]
|
||||
|
||||
|
||||
@dataclass
|
||||
class IntChoiceSetting(ChoiceSetting):
|
||||
"""An int setting with multiple choices.
|
||||
|
||||
Category: Settings Classes
|
||||
"""
|
||||
"""An int setting with multiple choices."""
|
||||
|
||||
default: int
|
||||
choices: list[tuple[str, int]]
|
||||
|
|
@ -81,10 +63,7 @@ class IntChoiceSetting(ChoiceSetting):
|
|||
|
||||
@dataclass
|
||||
class FloatChoiceSetting(ChoiceSetting):
|
||||
"""A float setting with multiple choices.
|
||||
|
||||
Category: Settings Classes
|
||||
"""
|
||||
"""A float setting with multiple choices."""
|
||||
|
||||
default: float
|
||||
choices: list[tuple[str, float]]
|
||||
|
|
|
|||
12
dist/ba_data/python/bascenev1/_stats.py
vendored
12
dist/ba_data/python/bascenev1/_stats.py
vendored
|
|
@ -22,10 +22,7 @@ if TYPE_CHECKING:
|
|||
|
||||
@dataclass
|
||||
class PlayerScoredMessage:
|
||||
"""Informs something that a bascenev1.Player scored.
|
||||
|
||||
Category: **Message Classes**
|
||||
"""
|
||||
"""Informs something that a bascenev1.Player scored."""
|
||||
|
||||
score: int
|
||||
"""The score value."""
|
||||
|
|
@ -34,8 +31,6 @@ class PlayerScoredMessage:
|
|||
class PlayerRecord:
|
||||
"""Stats for an individual player in a bascenev1.Stats object.
|
||||
|
||||
Category: **Gameplay Classes**
|
||||
|
||||
This does not necessarily correspond to a bascenev1.Player that is
|
||||
still present (stats may be retained for players that leave
|
||||
mid-game)
|
||||
|
|
@ -253,10 +248,7 @@ class PlayerRecord:
|
|||
|
||||
|
||||
class Stats:
|
||||
"""Manages scores and statistics for a bascenev1.Session.
|
||||
|
||||
Category: **Gameplay Classes**
|
||||
"""
|
||||
"""Manages scores and statistics for a bascenev1.Session."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._activity: weakref.ref[bascenev1.Activity] | None = None
|
||||
|
|
|
|||
94
dist/ba_data/python/bascenev1/_team.py
vendored
94
dist/ba_data/python/bascenev1/_team.py
vendored
|
|
@ -17,34 +17,32 @@ if TYPE_CHECKING:
|
|||
|
||||
|
||||
class SessionTeam:
|
||||
"""A team of one or more bascenev1.SessionPlayers.
|
||||
"""A team of one or more :class:`~bascenev1.SessionPlayer`.
|
||||
|
||||
Category: **Gameplay Classes**
|
||||
|
||||
Note that a SessionPlayer *always* has a SessionTeam;
|
||||
in some cases, such as free-for-all bascenev1.Sessions,
|
||||
each SessionTeam consists of just one SessionPlayer.
|
||||
Note that a player will *always* have a team. in some cases, such as
|
||||
free-for-all :class:`~bascenev1.Sessions`, each team consists of
|
||||
just one player.
|
||||
"""
|
||||
|
||||
# Annotate our attr types at the class level so they're introspectable.
|
||||
# We annotate our attr types at the class level so they're more
|
||||
# introspectable by docs tools/etc.
|
||||
|
||||
#: The team's name.
|
||||
name: babase.Lstr | str
|
||||
"""The team's name."""
|
||||
|
||||
#: The team's color.
|
||||
color: tuple[float, ...] # FIXME: can't we make this fixed len?
|
||||
"""The team's color."""
|
||||
|
||||
#: The list of players on the team.
|
||||
players: list[bascenev1.SessionPlayer]
|
||||
"""The list of bascenev1.SessionPlayer-s on the team."""
|
||||
|
||||
#: A dict for use by the current :class:`~bascenev1.Session` for
|
||||
#: storing data associated with this team. Unlike customdata, this
|
||||
#: persists for the duration of the session.
|
||||
customdata: dict
|
||||
"""A dict for use by the current bascenev1.Session for
|
||||
storing data associated with this team.
|
||||
Unlike customdata, this persists for the duration
|
||||
of the session."""
|
||||
|
||||
#: The unique numeric id of the team.
|
||||
id: int
|
||||
"""The unique numeric id of the team."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
|
@ -52,12 +50,6 @@ class SessionTeam:
|
|||
name: babase.Lstr | str = '',
|
||||
color: Sequence[float] = (1.0, 1.0, 1.0),
|
||||
):
|
||||
"""Instantiate a bascenev1.SessionTeam.
|
||||
|
||||
In most cases, all teams are provided to you by the bascenev1.Session,
|
||||
bascenev1.Session, so calling this shouldn't be necessary.
|
||||
"""
|
||||
|
||||
self.id = team_id
|
||||
self.name = name
|
||||
self.color = tuple(color)
|
||||
|
|
@ -66,7 +58,10 @@ class SessionTeam:
|
|||
self.activityteam: Team | None = None
|
||||
|
||||
def leave(self) -> None:
|
||||
"""(internal)"""
|
||||
"""(internal)
|
||||
|
||||
:meta private:
|
||||
"""
|
||||
self.customdata = {}
|
||||
|
||||
|
||||
|
|
@ -74,12 +69,11 @@ PlayerT = TypeVar('PlayerT', bound='bascenev1.Player')
|
|||
|
||||
|
||||
class Team(Generic[PlayerT]):
|
||||
"""A team in a specific bascenev1.Activity.
|
||||
"""A team in a specific :class:`~bascenev1.Activity`.
|
||||
|
||||
Category: **Gameplay Classes**
|
||||
|
||||
These correspond to bascenev1.SessionTeam objects, but are created
|
||||
per activity so that the activity can use its own custom team subclass.
|
||||
These correspond to :class:`~bascenev1.SessionTeam` objects, but are
|
||||
created per activity so that the activity can use its own custom
|
||||
team subclass.
|
||||
"""
|
||||
|
||||
# Defining these types at the class level instead of in __init__ so
|
||||
|
|
@ -97,9 +91,9 @@ class Team(Generic[PlayerT]):
|
|||
# get called by default if a dataclass inherits from us.
|
||||
|
||||
def postinit(self, sessionteam: SessionTeam) -> None:
|
||||
"""Wire up a newly created SessionTeam.
|
||||
"""Internal: Wire up a newly created SessionTeam.
|
||||
|
||||
(internal)
|
||||
:meta private:
|
||||
"""
|
||||
|
||||
# Sanity check; if a dataclass is created that inherits from us,
|
||||
|
|
@ -136,22 +130,23 @@ class Team(Generic[PlayerT]):
|
|||
|
||||
@property
|
||||
def customdata(self) -> dict:
|
||||
"""Arbitrary values associated with the team.
|
||||
Though it is encouraged that most player values be properly defined
|
||||
on the bascenev1.Team subclass, it may be useful for player-agnostic
|
||||
objects to store values here. This dict is cleared when the team
|
||||
leaves or expires so objects stored here will be disposed of at
|
||||
the expected time, unlike the Team instance itself which may
|
||||
continue to be referenced after it is no longer part of the game.
|
||||
"""Arbitrary values associated with the team. Though it is
|
||||
encouraged that most player values be properly defined on the
|
||||
:class:`~bascenev1.Team` subclass, it may be useful for
|
||||
player-agnostic objects to store values here. This dict is
|
||||
cleared when the team leaves or expires so objects stored here
|
||||
will be disposed of at the expected time, unlike the
|
||||
:class:`~bascenev1.Team` instance itself which may continue to
|
||||
be referenced after it is no longer part of the game.
|
||||
"""
|
||||
assert self._postinited
|
||||
assert not self._expired
|
||||
return self._customdata
|
||||
|
||||
def leave(self) -> None:
|
||||
"""Called when the Team leaves a running game.
|
||||
"""Internal: Called when the team leaves a running game.
|
||||
|
||||
(internal)
|
||||
:meta private:
|
||||
"""
|
||||
assert self._postinited
|
||||
assert not self._expired
|
||||
|
|
@ -159,9 +154,9 @@ class Team(Generic[PlayerT]):
|
|||
del self.players
|
||||
|
||||
def expire(self) -> None:
|
||||
"""Called when the Team is expiring (due to the Activity expiring).
|
||||
"""Internal: Called when team is expiring (due to its activity).
|
||||
|
||||
(internal)
|
||||
:meta private:
|
||||
"""
|
||||
assert self._postinited
|
||||
assert not self._expired
|
||||
|
|
@ -180,9 +175,10 @@ class Team(Generic[PlayerT]):
|
|||
|
||||
@property
|
||||
def sessionteam(self) -> SessionTeam:
|
||||
"""Return the bascenev1.SessionTeam corresponding to this Team.
|
||||
"""The :class:`~bascenev1.SessionTeam` corresponding to this team.
|
||||
|
||||
Throws a babase.SessionTeamNotFoundError if there is none.
|
||||
Throws a :class:`~babase.SessionTeamNotFoundError` if there is
|
||||
none.
|
||||
"""
|
||||
assert self._postinited
|
||||
if self._sessionteam is not None:
|
||||
|
|
@ -194,18 +190,16 @@ class Team(Generic[PlayerT]):
|
|||
|
||||
|
||||
class EmptyTeam(Team['bascenev1.EmptyPlayer']):
|
||||
"""An empty player for use by Activities that don't need to define one.
|
||||
"""An empty player for use by Activities that don't define one.
|
||||
|
||||
Category: **Gameplay Classes**
|
||||
|
||||
bascenev1.Player and bascenev1.Team are 'Generic' types, and so passing
|
||||
those top level classes as type arguments when defining a
|
||||
bascenev1.Player and bascenev1.Team are 'Generic' types, and so
|
||||
passing those top level classes as type arguments when defining a
|
||||
bascenev1.Activity reduces type safety. For example,
|
||||
activity.teams[0].player will have type 'Any' in that case. For that
|
||||
reason, it is better to pass EmptyPlayer and EmptyTeam when defining
|
||||
a bascenev1.Activity that does not need custom types of its own.
|
||||
|
||||
Note that EmptyPlayer defines its team type as EmptyTeam and vice versa,
|
||||
so if you want to define your own class for one of them you should do so
|
||||
for both.
|
||||
Note that EmptyPlayer defines its team type as EmptyTeam and vice
|
||||
versa, so if you want to define your own class for one of them you
|
||||
should do so for both.
|
||||
"""
|
||||
|
|
|
|||
33
dist/ba_data/python/bascenev1/_teamgame.py
vendored
33
dist/ba_data/python/bascenev1/_teamgame.py
vendored
|
|
@ -29,10 +29,8 @@ TeamT = TypeVar('TeamT', bound='bascenev1.Team')
|
|||
class TeamGameActivity(GameActivity[PlayerT, TeamT]):
|
||||
"""Base class for teams and free-for-all mode games.
|
||||
|
||||
Category: **Gameplay Classes**
|
||||
|
||||
(Free-for-all is essentially just a special case where every
|
||||
bascenev1.Player has their own bascenev1.Team)
|
||||
(Free-for-all is essentially just a special case where every player
|
||||
has their own team)
|
||||
"""
|
||||
|
||||
@override
|
||||
|
|
@ -40,11 +38,7 @@ class TeamGameActivity(GameActivity[PlayerT, TeamT]):
|
|||
def supports_session_type(
|
||||
cls, sessiontype: type[bascenev1.Session]
|
||||
) -> bool:
|
||||
"""
|
||||
Class method override;
|
||||
returns True for ba.DualTeamSessions and ba.FreeForAllSessions;
|
||||
False otherwise.
|
||||
"""
|
||||
# By default, team games support dual-teams and ffa.
|
||||
return issubclass(sessiontype, DualTeamSession) or issubclass(
|
||||
sessiontype, FreeForAllSession
|
||||
)
|
||||
|
|
@ -52,9 +46,9 @@ class TeamGameActivity(GameActivity[PlayerT, TeamT]):
|
|||
def __init__(self, settings: dict):
|
||||
super().__init__(settings)
|
||||
|
||||
# By default we don't show kill-points in free-for-all sessions.
|
||||
# By default we don't show kill-points in free-for-all sessions
|
||||
# (there's usually some activity-specific score and we don't
|
||||
# wanna confuse things)
|
||||
# wanna confuse things).
|
||||
if isinstance(self.session, FreeForAllSession):
|
||||
self.show_kill_points = False
|
||||
|
||||
|
|
@ -66,9 +60,9 @@ class TeamGameActivity(GameActivity[PlayerT, TeamT]):
|
|||
|
||||
super().on_transition_in()
|
||||
|
||||
# On the first game, show the controls UI momentarily.
|
||||
# (unless we're being run in co-op mode, in which case we leave
|
||||
# it up to them)
|
||||
# On the first game, show the controls UI momentarily (unless
|
||||
# we're being run in co-op mode, in which case we leave it up to
|
||||
# them).
|
||||
if not isinstance(self.session, CoopSession) and getattr(
|
||||
self, 'show_controls_guide', True
|
||||
):
|
||||
|
|
@ -114,12 +108,13 @@ class TeamGameActivity(GameActivity[PlayerT, TeamT]):
|
|||
position: Sequence[float] | None = None,
|
||||
angle: float | None = None,
|
||||
) -> PlayerSpaz:
|
||||
"""
|
||||
Method override; spawns and wires up a standard bascenev1.PlayerSpaz
|
||||
for a bascenev1.Player.
|
||||
"""Override to spawn and wire up a standard
|
||||
:class:`~bascenev1lib.actor.playerspaz.PlayerSpaz` for a
|
||||
:class:`~bascenev1.Player`.
|
||||
|
||||
If position or angle is not supplied, a default will be chosen based
|
||||
on the bascenev1.Player and their bascenev1.Team.
|
||||
If position or angle is not supplied, a default will be chosen
|
||||
based on the :class:`~bascenev1.Player` and their
|
||||
:class:`~bascenev1.Team`.
|
||||
"""
|
||||
if position is None:
|
||||
# In teams-mode get our team-start-location.
|
||||
|
|
|
|||
104
dist/ba_data/python/bascenev1lib/actor/bomb.py
vendored
104
dist/ba_data/python/bascenev1lib/actor/bomb.py
vendored
|
|
@ -17,115 +17,115 @@ from bascenev1lib.gameutils import SharedObjects
|
|||
if TYPE_CHECKING:
|
||||
from typing import Any, Sequence, Callable
|
||||
|
||||
import bascenev1
|
||||
|
||||
PlayerT = TypeVar('PlayerT', bound='bs.Player')
|
||||
|
||||
|
||||
class BombFactory:
|
||||
"""Wraps up media and other resources used by bs.Bombs.
|
||||
|
||||
Category: **Gameplay Classes**
|
||||
|
||||
A single instance of this is shared between all bombs
|
||||
and can be retrieved via bascenev1lib.actor.bomb.get_factory().
|
||||
"""
|
||||
|
||||
bomb_mesh: bs.Mesh
|
||||
"""The bs.Mesh of a standard or ice bomb."""
|
||||
bomb_mesh: bascenev1.Mesh
|
||||
"""The mesh used for standard or ice bombs."""
|
||||
|
||||
sticky_bomb_mesh: bs.Mesh
|
||||
"""The bs.Mesh of a sticky-bomb."""
|
||||
sticky_bomb_mesh: bascenev1.Mesh
|
||||
"""The mesh used for sticky-bombs."""
|
||||
|
||||
impact_bomb_mesh: bs.Mesh
|
||||
"""The bs.Mesh of an impact-bomb."""
|
||||
impact_bomb_mesh: bascenev1.Mesh
|
||||
"""The mesh used for impact-bombs."""
|
||||
|
||||
land_mine_mesh: bs.Mesh
|
||||
"""The bs.Mesh of a land-mine."""
|
||||
land_mine_mesh: bascenev1.Mesh
|
||||
"""The mesh used for land-mines."""
|
||||
|
||||
tnt_mesh: bs.Mesh
|
||||
"""The bs.Mesh of a tnt box."""
|
||||
tnt_mesh: bascenev1.Mesh
|
||||
"""The mesh used of a tnt box."""
|
||||
|
||||
regular_tex: bs.Texture
|
||||
"""The bs.Texture for regular bombs."""
|
||||
regular_tex: bascenev1.Texture
|
||||
"""The texture used for regular bombs."""
|
||||
|
||||
ice_tex: bs.Texture
|
||||
ice_tex: bascenev1.Texture
|
||||
"""The bs.Texture for ice bombs."""
|
||||
|
||||
sticky_tex: bs.Texture
|
||||
sticky_tex: bascenev1.Texture
|
||||
"""The bs.Texture for sticky bombs."""
|
||||
|
||||
impact_tex: bs.Texture
|
||||
impact_tex: bascenev1.Texture
|
||||
"""The bs.Texture for impact bombs."""
|
||||
|
||||
impact_lit_tex: bs.Texture
|
||||
impact_lit_tex: bascenev1.Texture
|
||||
"""The bs.Texture for impact bombs with lights lit."""
|
||||
|
||||
land_mine_tex: bs.Texture
|
||||
land_mine_tex: bascenev1.Texture
|
||||
"""The bs.Texture for land-mines."""
|
||||
|
||||
land_mine_lit_tex: bs.Texture
|
||||
land_mine_lit_tex: bascenev1.Texture
|
||||
"""The bs.Texture for land-mines with the light lit."""
|
||||
|
||||
tnt_tex: bs.Texture
|
||||
tnt_tex: bascenev1.Texture
|
||||
"""The bs.Texture for tnt boxes."""
|
||||
|
||||
hiss_sound: bs.Sound
|
||||
"""The bs.Sound for the hiss sound an ice bomb makes."""
|
||||
hiss_sound: bascenev1.Sound
|
||||
"""The sound for the hiss sound an ice bomb makes."""
|
||||
|
||||
debris_fall_sound: bs.Sound
|
||||
"""The bs.Sound for random falling debris after an explosion."""
|
||||
debris_fall_sound: bascenev1.Sound
|
||||
"""The sound for random falling debris after an explosion."""
|
||||
|
||||
wood_debris_fall_sound: bs.Sound
|
||||
"""A bs.Sound for random wood debris falling after an explosion."""
|
||||
wood_debris_fall_sound: bascenev1.Sound
|
||||
"""A sound for random wood debris falling after an explosion."""
|
||||
|
||||
explode_sounds: Sequence[bs.Sound]
|
||||
"""A tuple of bs.Sound-s for explosions."""
|
||||
explode_sounds: Sequence[bascenev1.Sound]
|
||||
"""A tuple of sounds for explosions."""
|
||||
|
||||
freeze_sound: bs.Sound
|
||||
"""A bs.Sound of an ice bomb freezing something."""
|
||||
freeze_sound: bascenev1.Sound
|
||||
"""A sound of an ice bomb freezing something."""
|
||||
|
||||
fuse_sound: bs.Sound
|
||||
"""A bs.Sound of a burning fuse."""
|
||||
fuse_sound: bascenev1.Sound
|
||||
"""A sound of a burning fuse."""
|
||||
|
||||
activate_sound: bs.Sound
|
||||
"""A bs.Sound for an activating impact bomb."""
|
||||
activate_sound: bascenev1.Sound
|
||||
"""A sound for an activating impact bomb."""
|
||||
|
||||
warn_sound: bs.Sound
|
||||
"""A bs.Sound for an impact bomb about to explode due to time-out."""
|
||||
warn_sound: bascenev1.Sound
|
||||
"""A sound for an impact bomb about to explode due to time-out."""
|
||||
|
||||
bomb_material: bs.Material
|
||||
bomb_material: bascenev1.Material
|
||||
"""A bs.Material applied to all bombs."""
|
||||
|
||||
normal_sound_material: bs.Material
|
||||
normal_sound_material: bascenev1.Material
|
||||
"""A bs.Material that generates standard bomb noises on impacts, etc."""
|
||||
|
||||
sticky_material: bs.Material
|
||||
sticky_material: bascenev1.Material
|
||||
"""A bs.Material that makes 'splat' sounds and makes collisions softer."""
|
||||
|
||||
land_mine_no_explode_material: bs.Material
|
||||
land_mine_no_explode_material: bascenev1.Material
|
||||
"""A bs.Material that keeps land-mines from blowing up.
|
||||
Applied to land-mines when they are created to allow land-mines to
|
||||
touch without exploding."""
|
||||
|
||||
land_mine_blast_material: bs.Material
|
||||
land_mine_blast_material: bascenev1.Material
|
||||
"""A bs.Material applied to activated land-mines that causes them to
|
||||
explode on impact."""
|
||||
|
||||
impact_blast_material: bs.Material
|
||||
impact_blast_material: bascenev1.Material
|
||||
"""A bs.Material applied to activated impact-bombs that causes them to
|
||||
explode on impact."""
|
||||
|
||||
blast_material: bs.Material
|
||||
blast_material: bascenev1.Material
|
||||
"""A bs.Material applied to bomb blast geometry which triggers impact
|
||||
events with what it touches."""
|
||||
|
||||
dink_sounds: Sequence[bs.Sound]
|
||||
"""A tuple of bs.Sound-s for when bombs hit the ground."""
|
||||
dink_sounds: Sequence[bascenev1.Sound]
|
||||
"""A tuple of sounds for when bombs hit the ground."""
|
||||
|
||||
sticky_impact_sound: bs.Sound
|
||||
"""The bs.Sound for a squish made by a sticky bomb hitting something."""
|
||||
sticky_impact_sound: bascenev1.Sound
|
||||
"""The sound for a squish made by a sticky bomb hitting something."""
|
||||
|
||||
roll_sound: bs.Sound
|
||||
"""bs.Sound for a rolling bomb."""
|
||||
roll_sound: bascenev1.Sound
|
||||
"""The sound for a rolling bomb."""
|
||||
|
||||
_STORENAME = bs.storagename()
|
||||
|
||||
|
|
@ -140,8 +140,8 @@ class BombFactory:
|
|||
assert isinstance(factory, BombFactory)
|
||||
return factory
|
||||
|
||||
def random_explode_sound(self) -> bs.Sound:
|
||||
"""Return a random explosion bs.Sound from the factory."""
|
||||
def random_explode_sound(self) -> bascenev1.Sound:
|
||||
"""Return a random explosion sound from the factory."""
|
||||
return self.explode_sounds[random.randrange(len(self.explode_sounds))]
|
||||
|
||||
def __init__(self) -> None:
|
||||
|
|
|
|||
58
dist/ba_data/python/bascenev1lib/actor/flag.py
vendored
58
dist/ba_data/python/bascenev1lib/actor/flag.py
vendored
|
|
@ -16,38 +16,31 @@ if TYPE_CHECKING:
|
|||
|
||||
|
||||
class FlagFactory:
|
||||
"""Wraps up media and other resources used by `Flag`s.
|
||||
"""Wraps up media and resources used by :class:`Flag`.
|
||||
|
||||
Category: **Gameplay Classes**
|
||||
|
||||
A single instance of this is shared between all flags
|
||||
and can be retrieved via FlagFactory.get().
|
||||
A single instance of this is shared between all flags and can be
|
||||
retrieved via :meth:`FlagFactory.get()`.
|
||||
"""
|
||||
|
||||
#: The material applied to all flags.
|
||||
flagmaterial: bs.Material
|
||||
"""The bs.Material applied to all `Flag`s."""
|
||||
|
||||
#: The sound used when a flag hits the ground.
|
||||
impact_sound: bs.Sound
|
||||
"""The bs.Sound used when a `Flag` hits the ground."""
|
||||
|
||||
#: The sound used when a flag skids along the ground.
|
||||
skid_sound: bs.Sound
|
||||
"""The bs.Sound used when a `Flag` skids along the ground."""
|
||||
|
||||
#: A material that prevents contact with most objects.
|
||||
#: This gets applied to 'non-touchable' flags.
|
||||
no_hit_material: bs.Material
|
||||
"""A bs.Material that prevents contact with most objects;
|
||||
applied to 'non-touchable' flags."""
|
||||
|
||||
flag_texture: bs.Texture
|
||||
"""The bs.Texture for flags."""
|
||||
"""The texture for flags."""
|
||||
|
||||
_STORENAME = bs.storagename()
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Instantiate a `FlagFactory`.
|
||||
|
||||
You shouldn't need to do this; call FlagFactory.get() to
|
||||
get a shared instance.
|
||||
"""
|
||||
shared = SharedObjects.get()
|
||||
self.flagmaterial = bs.Material()
|
||||
self.flagmaterial.add_actions(
|
||||
|
|
@ -110,7 +103,7 @@ class FlagFactory:
|
|||
|
||||
@classmethod
|
||||
def get(cls) -> FlagFactory:
|
||||
"""Get/create a shared `FlagFactory` instance."""
|
||||
"""Get/create a shared flag-factory instance."""
|
||||
activity = bs.getactivity()
|
||||
factory = activity.customdata.get(cls._STORENAME)
|
||||
if factory is None:
|
||||
|
|
@ -122,51 +115,40 @@ class FlagFactory:
|
|||
|
||||
@dataclass
|
||||
class FlagPickedUpMessage:
|
||||
"""A message saying a `Flag` has been picked up.
|
||||
|
||||
Category: **Message Classes**
|
||||
"""
|
||||
"""A message saying a flag has been picked up."""
|
||||
|
||||
#: The flag that has been picked up.
|
||||
flag: Flag
|
||||
"""The `Flag` that has been picked up."""
|
||||
|
||||
#: The bs.Node doing the picking up.
|
||||
node: bs.Node
|
||||
"""The bs.Node doing the picking up."""
|
||||
|
||||
|
||||
@dataclass
|
||||
class FlagDiedMessage:
|
||||
"""A message saying a `Flag` has died.
|
||||
|
||||
Category: **Message Classes**
|
||||
"""
|
||||
"""A message saying a `Flag` has died."""
|
||||
|
||||
#: The flag that died.
|
||||
flag: Flag
|
||||
"""The `Flag` that died."""
|
||||
|
||||
#: Whether the flag killed itself.
|
||||
self_kill: bool = False
|
||||
"""If the `Flag` killed itself or not."""
|
||||
|
||||
|
||||
@dataclass
|
||||
class FlagDroppedMessage:
|
||||
"""A message saying a `Flag` has been dropped.
|
||||
|
||||
Category: **Message Classes**
|
||||
"""
|
||||
"""A message saying a `Flag` has been dropped."""
|
||||
|
||||
#: The flag that was dropped.
|
||||
flag: Flag
|
||||
"""The `Flag` that was dropped."""
|
||||
|
||||
#: The node that was holding the flag.
|
||||
node: bs.Node
|
||||
"""The bs.Node that was holding it."""
|
||||
|
||||
|
||||
class Flag(bs.Actor):
|
||||
"""A flag; used in games such as capture-the-flag or king-of-the-hill.
|
||||
|
||||
Category: **Gameplay Classes**
|
||||
|
||||
Can be stationary or carry-able by players.
|
||||
"""
|
||||
|
||||
|
|
@ -382,7 +364,7 @@ class Flag(bs.Actor):
|
|||
|
||||
@staticmethod
|
||||
def project_stand(pos: Sequence[float]) -> None:
|
||||
"""Project a flag-stand onto the ground at the given position.
|
||||
"""Project a flag-stand onto the ground from a position.
|
||||
|
||||
Useful for games such as capture-the-flag to show where a
|
||||
movable flag originated from.
|
||||
|
|
|
|||
|
|
@ -15,8 +15,6 @@ if TYPE_CHECKING:
|
|||
class OnScreenCountdown(bs.Actor):
|
||||
"""A Handy On-Screen Timer.
|
||||
|
||||
category: Gameplay Classes
|
||||
|
||||
Useful for time-based games that count down to zero.
|
||||
"""
|
||||
|
||||
|
|
|
|||
|
|
@ -15,8 +15,6 @@ if TYPE_CHECKING:
|
|||
class OnScreenTimer(bs.Actor):
|
||||
"""A handy on-screen timer.
|
||||
|
||||
category: Gameplay Classes
|
||||
|
||||
Useful for time-based games where time increases.
|
||||
"""
|
||||
|
||||
|
|
|
|||
|
|
@ -17,10 +17,7 @@ PlayerT = TypeVar('PlayerT', bound=bs.Player)
|
|||
|
||||
|
||||
class PlayerSpazHurtMessage:
|
||||
"""A message saying a PlayerSpaz was hurt.
|
||||
|
||||
Category: **Message Classes**
|
||||
"""
|
||||
"""A message saying a PlayerSpaz was hurt."""
|
||||
|
||||
spaz: PlayerSpaz
|
||||
"""The PlayerSpaz that was hurt"""
|
||||
|
|
@ -33,8 +30,6 @@ class PlayerSpazHurtMessage:
|
|||
class PlayerSpaz(Spaz):
|
||||
"""A Spaz subclass meant to be controlled by a bascenev1.Player.
|
||||
|
||||
Category: **Gameplay Classes**
|
||||
|
||||
When a PlayerSpaz dies, it delivers a bascenev1.PlayerDiedMessage
|
||||
to the current bascenev1.Activity. (unless the death was the result
|
||||
of the player leaving the game, in which case no message is sent)
|
||||
|
|
|
|||
|
|
@ -24,8 +24,6 @@ class _TouchedMessage:
|
|||
class PowerupBoxFactory:
|
||||
"""A collection of media and other resources used by bs.Powerups.
|
||||
|
||||
Category: **Gameplay Classes**
|
||||
|
||||
A single instance of this is shared between all powerups
|
||||
and can be retrieved via bs.Powerup.get_factory().
|
||||
"""
|
||||
|
|
@ -190,19 +188,18 @@ class PowerupBoxFactory:
|
|||
class PowerupBox(bs.Actor):
|
||||
"""A box that grants a powerup.
|
||||
|
||||
category: Gameplay Classes
|
||||
|
||||
This will deliver a bs.PowerupMessage to anything that touches it
|
||||
which has the bs.PowerupBoxFactory.powerup_accept_material applied.
|
||||
This will deliver a :class:`~bascenev1.PowerupMessage` to anything
|
||||
that touches it which has the
|
||||
:class:`~PowerupBoxFactory.powerup_accept_material` applied.
|
||||
"""
|
||||
|
||||
#: The string powerup type. This can be 'triple_bombs', 'punch',
|
||||
#: 'ice_bombs', 'impact_bombs', 'land_mines', 'sticky_bombs',
|
||||
#: 'shield', 'health', or 'curse'.
|
||||
poweruptype: str
|
||||
"""The string powerup type. This can be 'triple_bombs', 'punch',
|
||||
'ice_bombs', 'impact_bombs', 'land_mines', 'sticky_bombs', 'shield',
|
||||
'health', or 'curse'."""
|
||||
|
||||
node: bs.Node
|
||||
"""The 'prop' bs.Node representing this box."""
|
||||
"""The 'prop' node representing this box."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
|
|
|||
|
|
@ -12,9 +12,7 @@ import bascenev1 as bs
|
|||
class RespawnIcon:
|
||||
"""An icon with a countdown that appears alongside the screen.
|
||||
|
||||
category: Gameplay Classes
|
||||
|
||||
This is used to indicate that a bascenev1.Player is waiting to respawn.
|
||||
This is used to indicate that a player is waiting to respawn.
|
||||
"""
|
||||
|
||||
_MASKTEXSTORENAME = bs.storagename('masktex')
|
||||
|
|
|
|||
|
|
@ -16,17 +16,12 @@ if TYPE_CHECKING:
|
|||
class Spawner:
|
||||
"""Utility for delayed spawning of objects.
|
||||
|
||||
Category: **Gameplay Classes**
|
||||
|
||||
Creates a light flash and sends a Spawner.SpawnMessage
|
||||
to the current activity after a delay.
|
||||
Creates a light flash and sends a Spawner.SpawnMessage to the
|
||||
current activity after a delay.
|
||||
"""
|
||||
|
||||
class SpawnMessage:
|
||||
"""Spawn message sent by a Spawner after its delay has passed.
|
||||
|
||||
Category: **Message Classes**
|
||||
"""
|
||||
"""Spawn message sent by a Spawner after its delay has passed."""
|
||||
|
||||
spawner: Spawner
|
||||
"""The bascenev1.Spawner we came from."""
|
||||
|
|
|
|||
|
|
@ -46,8 +46,6 @@ class Spaz(bs.Actor):
|
|||
"""
|
||||
Base class for various Spazzes.
|
||||
|
||||
Category: **Gameplay Classes**
|
||||
|
||||
A Spaz is the standard little humanoid character in the game.
|
||||
It can be controlled by a player or by AI, and can have
|
||||
various different appearances. The name 'Spaz' is not to be
|
||||
|
|
@ -886,7 +884,9 @@ class Spaz(bs.Actor):
|
|||
if not self.frozen:
|
||||
self.frozen = True
|
||||
self.node.frozen = True
|
||||
bs.timer(5.0, bs.WeakCall(self.handlemessage, bs.ThawMessage()))
|
||||
bs.timer(
|
||||
msg.time, bs.WeakCall(self.handlemessage, bs.ThawMessage())
|
||||
)
|
||||
# Instantly shatter if we're already dead.
|
||||
# (otherwise its hard to tell we're dead).
|
||||
if self.hitpoints <= 0:
|
||||
|
|
|
|||
|
|
@ -26,10 +26,7 @@ PRO_BOT_HIGHLIGHT = (0.6, 0.1, 0.05)
|
|||
|
||||
|
||||
class SpazBotPunchedMessage:
|
||||
"""A message saying a bs.SpazBot got punched.
|
||||
|
||||
Category: **Message Classes**
|
||||
"""
|
||||
"""A message saying a bs.SpazBot got punched."""
|
||||
|
||||
spazbot: SpazBot
|
||||
"""The bs.SpazBot that got punched."""
|
||||
|
|
@ -44,10 +41,7 @@ class SpazBotPunchedMessage:
|
|||
|
||||
|
||||
class SpazBotDiedMessage:
|
||||
"""A message saying a bs.SpazBot has died.
|
||||
|
||||
Category: **Message Classes**
|
||||
"""
|
||||
"""A message saying a bs.SpazBot has died."""
|
||||
|
||||
spazbot: SpazBot
|
||||
"""The SpazBot that was killed."""
|
||||
|
|
@ -73,8 +67,6 @@ class SpazBotDiedMessage:
|
|||
class SpazBot(Spaz):
|
||||
"""A really dumb AI version of bs.Spaz.
|
||||
|
||||
Category: **Bot Classes**
|
||||
|
||||
Add these to a bs.BotSet to use them.
|
||||
|
||||
Note: currently the AI has no real ability to
|
||||
|
|
|
|||
|
|
@ -16,8 +16,6 @@ if TYPE_CHECKING:
|
|||
class SpazFactory:
|
||||
"""Wraps up media and other resources used by bs.Spaz instances.
|
||||
|
||||
Category: **Gameplay Classes**
|
||||
|
||||
Generally one of these is created per bascenev1.Activity and shared
|
||||
between all spaz instances. Use bs.Spaz.get_factory() to return
|
||||
the shared factory for the current activity.
|
||||
|
|
|
|||
|
|
@ -17,8 +17,6 @@ if TYPE_CHECKING:
|
|||
class ZoomText(bs.Actor):
|
||||
"""Big Zooming Text.
|
||||
|
||||
Category: Gameplay Classes
|
||||
|
||||
Used for things such as the 'BOB WINS' victory messages.
|
||||
"""
|
||||
|
||||
|
|
|
|||
|
|
@ -29,8 +29,14 @@ class Team(bs.Team[Player]):
|
|||
"""Our team type for this game."""
|
||||
|
||||
def __init__(self, base_pos: Sequence[float], flag: Flag) -> None:
|
||||
|
||||
#: Where our base is.
|
||||
self.base_pos = base_pos
|
||||
|
||||
#: Flag for this team.
|
||||
self.flag = flag
|
||||
|
||||
#: Current score.
|
||||
self.score = 0
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -15,8 +15,6 @@ if TYPE_CHECKING:
|
|||
class SharedObjects:
|
||||
"""Various common components for use in games.
|
||||
|
||||
Category: Gameplay Classes
|
||||
|
||||
Objects contained here are created on-demand as accessed and shared
|
||||
by everything in the current activity. This includes things such as
|
||||
standard materials.
|
||||
|
|
|
|||
|
|
@ -12,8 +12,8 @@ if TYPE_CHECKING:
|
|||
class TemplateFsAppSubsystem:
|
||||
"""Subsystem for TemplateFs functionality in the app.
|
||||
|
||||
The single shared instance of this class can be accessed at
|
||||
ba*.app.templatefs. Note that it is possible for ba*.app.templatefs
|
||||
to be None if the TemplateFs feature-set is not enabled, and code
|
||||
should handle that case gracefully.
|
||||
If :attr:`~batools.featureset.FeatureSet.has_python_app_subsystem`
|
||||
is enabled for our feature-set, the single shared instance of this
|
||||
class can be accessed as `template_fs` on the :class:`~babase.App`
|
||||
instance.
|
||||
"""
|
||||
|
|
|
|||
6
dist/ba_data/python/bauiv1/__init__.py
vendored
6
dist/ba_data/python/bauiv1/__init__.py
vendored
|
|
@ -21,6 +21,7 @@ from babase import (
|
|||
add_clean_frame_callback,
|
||||
allows_ticket_sales,
|
||||
app,
|
||||
App,
|
||||
AppIntent,
|
||||
AppIntentDefault,
|
||||
AppIntentExec,
|
||||
|
|
@ -28,6 +29,7 @@ from babase import (
|
|||
appname,
|
||||
appnameupper,
|
||||
apptime,
|
||||
AppState,
|
||||
AppTime,
|
||||
apptimer,
|
||||
AppTimer,
|
||||
|
|
@ -137,6 +139,7 @@ from bauiv1._uitypes import (
|
|||
BasicMainWindowState,
|
||||
uicleanupcheck,
|
||||
MainWindow,
|
||||
RootUIUpdatePause,
|
||||
)
|
||||
from bauiv1._appsubsystem import UIV1AppSubsystem
|
||||
|
||||
|
|
@ -144,6 +147,7 @@ __all__ = [
|
|||
'add_clean_frame_callback',
|
||||
'allows_ticket_sales',
|
||||
'app',
|
||||
'App',
|
||||
'AppIntent',
|
||||
'AppIntentDefault',
|
||||
'AppIntentExec',
|
||||
|
|
@ -151,6 +155,7 @@ __all__ = [
|
|||
'appname',
|
||||
'appnameupper',
|
||||
'appnameupper',
|
||||
'AppState',
|
||||
'apptime',
|
||||
'AppTime',
|
||||
'apptimer',
|
||||
|
|
@ -229,6 +234,7 @@ __all__ = [
|
|||
'request_permission',
|
||||
'root_ui_pause_updates',
|
||||
'root_ui_resume_updates',
|
||||
'RootUIUpdatePause',
|
||||
'rowwidget',
|
||||
'safecolor',
|
||||
'screenmessage',
|
||||
|
|
|
|||
137
dist/ba_data/python/bauiv1/_appsubsystem.py
vendored
137
dist/ba_data/python/bauiv1/_appsubsystem.py
vendored
|
|
@ -32,8 +32,6 @@ if TYPE_CHECKING:
|
|||
class UIV1AppSubsystem(babase.AppSubsystem):
|
||||
"""Consolidated UI functionality for the app.
|
||||
|
||||
Category: **App Classes**
|
||||
|
||||
To use this class, access the single instance of it at 'ba.app.ui'.
|
||||
"""
|
||||
|
||||
|
|
@ -86,9 +84,12 @@ class UIV1AppSubsystem(babase.AppSubsystem):
|
|||
self.heading_color = (0.72, 0.7, 0.75)
|
||||
self.infotextcolor = (0.7, 0.9, 0.7)
|
||||
|
||||
self._last_win_recreate_size: tuple[float, float] | None = None
|
||||
self._last_screen_size_win_recreate_time: float | None = None
|
||||
self._screen_size_win_recreate_timer: babase.AppTimer | None = None
|
||||
self.window_auto_recreate_suppress_count = 0
|
||||
|
||||
self._last_win_recreate_screen_size: tuple[float, float] | None = None
|
||||
self._last_win_recreate_uiscale: bauiv1.UIScale | None = None
|
||||
self._last_win_recreate_time: float | None = None
|
||||
self._win_recreate_timer: babase.AppTimer | None = None
|
||||
|
||||
# Elements in our root UI will call anything here when
|
||||
# activated.
|
||||
|
|
@ -196,6 +197,15 @@ class UIV1AppSubsystem(babase.AppSubsystem):
|
|||
# pylint: disable=too-many-statements
|
||||
from bauiv1._uitypes import MainWindow
|
||||
|
||||
# If we haven't grabbed initial uiscale or screen size for recreate
|
||||
# comparision purposes, this is a good time to do so.
|
||||
if self._last_win_recreate_screen_size is None:
|
||||
self._last_win_recreate_screen_size = (
|
||||
babase.get_virtual_screen_size()
|
||||
)
|
||||
if self._last_win_recreate_uiscale is None:
|
||||
self._last_win_recreate_uiscale = babase.app.ui_v1.uiscale
|
||||
|
||||
# Encourage migration to the new higher level nav calls.
|
||||
if not suppress_warning:
|
||||
warnings.warn(
|
||||
|
|
@ -398,6 +408,23 @@ class UIV1AppSubsystem(babase.AppSubsystem):
|
|||
suppress_warning=True,
|
||||
)
|
||||
|
||||
def should_suppress_window_recreates(self) -> bool:
|
||||
"""Should we avoid auto-recreating windows at the current time?"""
|
||||
|
||||
# This is slightly hack-ish and ideally we can get to the point
|
||||
# where we never need this and can remove it.
|
||||
|
||||
# Currently string-edits grab a weak-ref to the exact text
|
||||
# widget they're targeting. So we need to suppress recreates
|
||||
# while edits are in progress. Ideally we should change that to
|
||||
# use ids or something that would survive a recreate.
|
||||
if babase.app.stringedit.active_adapter() is not None:
|
||||
return True
|
||||
|
||||
# Suppress if anything else is requesting suppression (such as
|
||||
# generic Windows that don't handle being recreated).
|
||||
return babase.app.ui_v1.window_auto_recreate_suppress_count > 0
|
||||
|
||||
@override
|
||||
def on_ui_scale_change(self) -> None:
|
||||
# Update our stored UIScale.
|
||||
|
|
@ -406,64 +433,82 @@ class UIV1AppSubsystem(babase.AppSubsystem):
|
|||
# Update native bits (allow root widget to rebuild itself/etc.)
|
||||
_bauiv1.on_ui_scale_change()
|
||||
|
||||
# Lastly, if we have a main window, recreate it to pick up the
|
||||
# new UIScale/etc.
|
||||
mainwindow = self.get_main_window()
|
||||
if mainwindow is not None:
|
||||
winstate = self.save_main_window_state(mainwindow)
|
||||
self.clear_main_window(transition='instant')
|
||||
self.restore_main_window_state(winstate)
|
||||
|
||||
# Store the size we created this for to avoid redundant
|
||||
# future recreates.
|
||||
self._last_win_recreate_size = babase.get_virtual_screen_size()
|
||||
self._schedule_main_win_recreate()
|
||||
|
||||
@override
|
||||
def on_screen_size_change(self) -> None:
|
||||
|
||||
# Recreating a MainWindow is a kinda heavy thing and it doesn't
|
||||
# seem like we should be doing it at 120hz during a live window
|
||||
# resize, so let's limit the max rate we do it.
|
||||
now = time.monotonic()
|
||||
self._schedule_main_win_recreate()
|
||||
|
||||
# 4 refreshes per second seems reasonable.
|
||||
interval = 0.25
|
||||
def _schedule_main_win_recreate(self) -> None:
|
||||
|
||||
# If there is a timer set already, do nothing.
|
||||
if self._screen_size_win_recreate_timer is not None:
|
||||
if self._win_recreate_timer is not None:
|
||||
return
|
||||
|
||||
# Recreating a MainWindow is a kinda heavy thing and it doesn't
|
||||
# seem like we should be doing it at 120hz during a live window
|
||||
# resize, so let's limit the max rate we do it. We also use the
|
||||
# same mechanism to defer window recreates while anything is
|
||||
# suppressing them.
|
||||
now = time.monotonic()
|
||||
|
||||
# Up to 4 refreshes per second seems reasonable.
|
||||
interval = 0.25
|
||||
|
||||
# Ok; there's no timer. Schedule one.
|
||||
till_update = (
|
||||
0.0
|
||||
if self._last_screen_size_win_recreate_time is None
|
||||
else max(
|
||||
0.0, self._last_screen_size_win_recreate_time + interval - now
|
||||
interval
|
||||
if self.should_suppress_window_recreates()
|
||||
else (
|
||||
0.0
|
||||
if self._last_win_recreate_time is None
|
||||
else max(0.0, self._last_win_recreate_time + interval - now)
|
||||
)
|
||||
)
|
||||
self._screen_size_win_recreate_timer = babase.AppTimer(
|
||||
till_update, self._do_screen_size_win_recreate
|
||||
self._win_recreate_timer = babase.AppTimer(
|
||||
till_update, self._do_main_win_recreate
|
||||
)
|
||||
|
||||
def _do_screen_size_win_recreate(self) -> None:
|
||||
self._last_screen_size_win_recreate_time = time.monotonic()
|
||||
self._screen_size_win_recreate_timer = None
|
||||
def _do_main_win_recreate(self) -> None:
|
||||
self._last_win_recreate_time = time.monotonic()
|
||||
self._win_recreate_timer = None
|
||||
|
||||
# Avoid recreating if we're already at this size. This prevents
|
||||
# a redundant recreate when ui scale changes.
|
||||
virtual_screen_size = babase.get_virtual_screen_size()
|
||||
if virtual_screen_size == self._last_win_recreate_size:
|
||||
# If win-recreates are currently suppressed, just kick off
|
||||
# another timer. We'll do our actual thing once suppression
|
||||
# finally ends.
|
||||
if self.should_suppress_window_recreates():
|
||||
self._schedule_main_win_recreate()
|
||||
return
|
||||
|
||||
mainwindow = self.get_main_window()
|
||||
if (
|
||||
mainwindow is not None
|
||||
and mainwindow.refreshes_on_screen_size_changes
|
||||
):
|
||||
winstate = self.save_main_window_state(mainwindow)
|
||||
self.clear_main_window(transition='instant')
|
||||
self.restore_main_window_state(winstate)
|
||||
|
||||
# Store the size we created this for to avoid redundant
|
||||
# future recreates.
|
||||
self._last_win_recreate_size = virtual_screen_size
|
||||
# Can't recreate what doesn't exist.
|
||||
if mainwindow is None:
|
||||
return
|
||||
|
||||
virtual_screen_size = babase.get_virtual_screen_size()
|
||||
uiscale = babase.app.ui_v1.uiscale
|
||||
|
||||
# These should always get actual values when a main-window is
|
||||
# assigned so should never still be None here.
|
||||
assert self._last_win_recreate_uiscale is not None
|
||||
assert self._last_win_recreate_screen_size is not None
|
||||
|
||||
# If uiscale hasn't changed and our screen-size hasn't either
|
||||
# (or it has but we don't care) then we're done.
|
||||
if uiscale is self._last_win_recreate_uiscale and (
|
||||
virtual_screen_size == self._last_win_recreate_screen_size
|
||||
or not mainwindow.refreshes_on_screen_size_changes
|
||||
):
|
||||
return
|
||||
|
||||
# Do the recreate.
|
||||
winstate = self.save_main_window_state(mainwindow)
|
||||
self.clear_main_window(transition='instant')
|
||||
self.restore_main_window_state(winstate)
|
||||
|
||||
# Store the size we created this for to avoid redundant
|
||||
# future recreates.
|
||||
self._last_win_recreate_uiscale = uiscale
|
||||
self._last_win_recreate_screen_size = virtual_screen_size
|
||||
|
|
|
|||
8
dist/ba_data/python/bauiv1/_keyboard.py
vendored
8
dist/ba_data/python/bauiv1/_keyboard.py
vendored
|
|
@ -13,11 +13,9 @@ if TYPE_CHECKING:
|
|||
class Keyboard:
|
||||
"""Chars definitions for on-screen keyboard.
|
||||
|
||||
Category: **App Classes**
|
||||
|
||||
Keyboards are discoverable by the meta-tag system
|
||||
and the user can select which one they want to use.
|
||||
On-screen keyboard uses chars from active babase.Keyboard.
|
||||
Keyboards are discoverable by the meta-tag system and the user can
|
||||
select which one they want to use. On-screen keyboard uses chars
|
||||
from active babase.Keyboard.
|
||||
"""
|
||||
|
||||
name: str
|
||||
|
|
|
|||
50
dist/ba_data/python/bauiv1/_uitypes.py
vendored
50
dist/ba_data/python/bauiv1/_uitypes.py
vendored
|
|
@ -27,19 +27,37 @@ DEBUG_UI_CLEANUP_CHECKS = os.environ.get('BA_DEBUG_UI_CLEANUP_CHECKS') == '1'
|
|||
class Window:
|
||||
"""A basic window.
|
||||
|
||||
Category: User Interface Classes
|
||||
|
||||
Essentially wraps a ContainerWidget with some higher level
|
||||
functionality.
|
||||
"""
|
||||
|
||||
def __init__(self, root_widget: bauiv1.Widget, cleanupcheck: bool = True):
|
||||
def __init__(
|
||||
self,
|
||||
root_widget: bauiv1.Widget,
|
||||
cleanupcheck: bool = True,
|
||||
prevent_main_window_auto_recreate: bool = True,
|
||||
):
|
||||
self._root_widget = root_widget
|
||||
|
||||
# Complain if we outlive our root widget.
|
||||
# By default, the presence of any generic windows prevents the
|
||||
# app from running its fancy main-window-auto-recreate mechanism
|
||||
# on screen-resizes and whatnot. This avoids things like
|
||||
# temporary popup windows getting stuck under auto-re-created
|
||||
# main-windows.
|
||||
self._prevent_main_window_auto_recreate = (
|
||||
prevent_main_window_auto_recreate
|
||||
)
|
||||
if prevent_main_window_auto_recreate:
|
||||
babase.app.ui_v1.window_auto_recreate_suppress_count += 1
|
||||
|
||||
# Generally we complain if we outlive our root widget.
|
||||
if cleanupcheck:
|
||||
uicleanupcheck(self, root_widget)
|
||||
|
||||
def __del__(self) -> None:
|
||||
if self._prevent_main_window_auto_recreate:
|
||||
babase.app.ui_v1.window_auto_recreate_suppress_count -= 1
|
||||
|
||||
def get_root_widget(self) -> bauiv1.Widget:
|
||||
"""Return the root widget."""
|
||||
return self._root_widget
|
||||
|
|
@ -66,8 +84,8 @@ class MainWindow(Window):
|
|||
):
|
||||
"""Create a MainWindow given a root widget and transition info.
|
||||
|
||||
Automatically handles in and out transitions on the provided widget,
|
||||
so there is no need to set transitions when creating it.
|
||||
Automatically handles in and out transitions on the provided
|
||||
widget, so there is no need to set transitions when creating it.
|
||||
"""
|
||||
# A back-state supplied by the ui system.
|
||||
self.main_window_back_state: MainWindowState | None = None
|
||||
|
|
@ -89,7 +107,11 @@ class MainWindow(Window):
|
|||
|
||||
self._main_window_transition = transition
|
||||
self._main_window_origin_widget = origin_widget
|
||||
super().__init__(root_widget, cleanupcheck)
|
||||
super().__init__(
|
||||
root_widget,
|
||||
cleanupcheck=cleanupcheck,
|
||||
prevent_main_window_auto_recreate=False,
|
||||
)
|
||||
|
||||
scale_origin: tuple[float, float] | None
|
||||
if origin_widget is not None:
|
||||
|
|
@ -120,7 +142,7 @@ class MainWindow(Window):
|
|||
|
||||
# Note: normally transition of None means instant, but we use
|
||||
# that to mean 'do the default' so we support a special
|
||||
# 'instant' string..
|
||||
# 'instant' string.
|
||||
if transition == 'instant':
|
||||
self._root_widget.delete()
|
||||
else:
|
||||
|
|
@ -304,8 +326,6 @@ class UICleanupCheck:
|
|||
def uicleanupcheck(obj: Any, widget: bauiv1.Widget) -> None:
|
||||
"""Checks to ensure a widget-owning object gets cleaned up properly.
|
||||
|
||||
Category: User Interface Functions
|
||||
|
||||
This adds a check which will print an error message if the provided
|
||||
object still exists ~5 seconds after the provided bauiv1.Widget dies.
|
||||
|
||||
|
|
@ -407,3 +427,13 @@ class TextWidgetStringEditAdapter(babase.StringEditAdapter):
|
|||
def _do_cancel(self) -> None:
|
||||
if self.widget:
|
||||
_bauiv1.textwidget(edit=self.widget, adapter_finished=True)
|
||||
|
||||
|
||||
class RootUIUpdatePause:
|
||||
"""Pauses updates to the root-ui while in existence."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
_bauiv1.root_ui_pause_updates()
|
||||
|
||||
def __del__(self) -> None:
|
||||
_bauiv1.root_ui_resume_updates()
|
||||
|
|
|
|||
|
|
@ -338,8 +338,8 @@ class AccountSettingsWindow(bui.MainWindow):
|
|||
deprecated_space = 60
|
||||
|
||||
# Game Center currently has a single UI for everything.
|
||||
show_game_service_button = game_center_active
|
||||
game_service_button_space = 60.0
|
||||
show_game_center_button = game_center_active
|
||||
game_center_button_space = 60.0
|
||||
|
||||
# Phasing this out (for V2 accounts at least).
|
||||
show_linked_accounts_text = (
|
||||
|
|
@ -431,8 +431,8 @@ class AccountSettingsWindow(bui.MainWindow):
|
|||
self._sub_height += sign_in_button_space
|
||||
if show_device_sign_in_button:
|
||||
self._sub_height += sign_in_button_space + deprecated_space
|
||||
if show_game_service_button:
|
||||
self._sub_height += game_service_button_space
|
||||
if show_game_center_button:
|
||||
self._sub_height += game_center_button_space
|
||||
if show_linked_accounts_text:
|
||||
self._sub_height += linked_accounts_text_space
|
||||
if show_achievements_text:
|
||||
|
|
@ -880,14 +880,14 @@ class AccountSettingsWindow(bui.MainWindow):
|
|||
bui.widget(edit=btn, left_widget=bbtn)
|
||||
|
||||
# the button to go to OS-Specific leaderboards/high-score-lists/etc.
|
||||
if show_game_service_button:
|
||||
if show_game_center_button:
|
||||
button_width = 300
|
||||
v -= game_service_button_space * 0.6
|
||||
v -= game_center_button_space * 1.0
|
||||
if game_center_active:
|
||||
# Note: Apparently Game Center is just called 'Game Center'
|
||||
# in all languages. Can revisit if not true.
|
||||
# https://developer.apple.com/forums/thread/725779
|
||||
game_service_button_label = bui.Lstr(
|
||||
game_center_button_label = bui.Lstr(
|
||||
value=bui.charstr(bui.SpecialChar.GAME_CENTER_LOGO)
|
||||
+ 'Game Center'
|
||||
)
|
||||
|
|
@ -895,15 +895,15 @@ class AccountSettingsWindow(bui.MainWindow):
|
|||
raise ValueError(
|
||||
"unknown account type: '" + str(v1_account_type) + "'"
|
||||
)
|
||||
self._game_service_button = btn = bui.buttonwidget(
|
||||
self._game_center_button = btn = bui.buttonwidget(
|
||||
parent=self._subcontainer,
|
||||
position=((self._sub_width - button_width) * 0.5, v),
|
||||
color=(0.55, 0.5, 0.6),
|
||||
textcolor=(0.75, 0.7, 0.8),
|
||||
autoselect=True,
|
||||
on_activate_call=self._on_game_service_button_press,
|
||||
on_activate_call=self._on_game_center_button_press,
|
||||
size=(button_width, 50),
|
||||
label=game_service_button_label,
|
||||
label=game_center_button_label,
|
||||
)
|
||||
if first_selectable is None:
|
||||
first_selectable = btn
|
||||
|
|
@ -911,9 +911,9 @@ class AccountSettingsWindow(bui.MainWindow):
|
|||
edit=btn, right_widget=bui.get_special_widget('squad_button')
|
||||
)
|
||||
bui.widget(edit=btn, left_widget=bbtn)
|
||||
v -= game_service_button_space * 0.4
|
||||
v -= game_center_button_space * 0.4
|
||||
else:
|
||||
self.game_service_button = None
|
||||
self.game_center_button = None
|
||||
|
||||
self._achievements_text: bui.Widget | None
|
||||
if show_achievements_text:
|
||||
|
|
@ -1214,7 +1214,7 @@ class AccountSettingsWindow(bui.MainWindow):
|
|||
)
|
||||
self._needs_refresh = False
|
||||
|
||||
def _on_game_service_button_press(self) -> None:
|
||||
def _on_game_center_button_press(self) -> None:
|
||||
if bui.app.plus is not None:
|
||||
bui.app.plus.show_game_service_ui()
|
||||
else:
|
||||
|
|
|
|||
17
dist/ba_data/python/bauiv1lib/account/v2proxy.py
vendored
17
dist/ba_data/python/bauiv1lib/account/v2proxy.py
vendored
|
|
@ -41,6 +41,12 @@ class V2ProxySignInWindow(bui.Window):
|
|||
)
|
||||
)
|
||||
|
||||
self._loading_spinner = bui.spinnerwidget(
|
||||
parent=self._root_widget,
|
||||
position=(self._width * 0.5, self._height * 0.5),
|
||||
size=60,
|
||||
style='bomb',
|
||||
)
|
||||
self._state_text = bui.textwidget(
|
||||
parent=self._root_widget,
|
||||
position=(self._width * 0.5, self._height * 0.6),
|
||||
|
|
@ -49,10 +55,11 @@ class V2ProxySignInWindow(bui.Window):
|
|||
size=(0, 0),
|
||||
scale=1.4,
|
||||
maxwidth=0.9 * self._width,
|
||||
text=bui.Lstr(
|
||||
value='${A}...',
|
||||
subs=[('${A}', bui.Lstr(resource='loadingText'))],
|
||||
),
|
||||
# text=bui.Lstr(
|
||||
# value='${A}...',
|
||||
# subs=[('${A}', bui.Lstr(resource='loadingText'))],
|
||||
# ),
|
||||
text='',
|
||||
color=(1, 1, 1),
|
||||
)
|
||||
self._sub_state_text = bui.textwidget(
|
||||
|
|
@ -141,6 +148,7 @@ class V2ProxySignInWindow(bui.Window):
|
|||
def _set_error_state(self, error_location: str) -> None:
|
||||
msaddress = self._get_server_address()
|
||||
addr = msaddress.removeprefix('https://')
|
||||
bui.spinnerwidget(edit=self._loading_spinner, visible=False)
|
||||
bui.textwidget(
|
||||
edit=self._state_text,
|
||||
text=f'Unable to connect to {addr}.',
|
||||
|
|
@ -190,6 +198,7 @@ class V2ProxySignInWindow(bui.Window):
|
|||
self._complete = True
|
||||
|
||||
# Clear out stuff we use to show progress/errors.
|
||||
self._loading_spinner.delete()
|
||||
self._sub_state_text.delete()
|
||||
self._sub_state_text2.delete()
|
||||
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue