mirror of
https://github.com/hypervortex/VH-Bombsquad-Modded-Server-Files
synced 2025-11-07 17:36:08 +00:00
Delete dist directory
This commit is contained in:
parent
2e2c838750
commit
867634cc5c
1779 changed files with 0 additions and 565850 deletions
769
dist/ba_data/python/ba/_app.py
vendored
769
dist/ba_data/python/ba/_app.py
vendored
|
|
@ -1,769 +0,0 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Functionality related to the high level state of the app."""
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
import logging
|
||||
from enum import Enum
|
||||
from typing import TYPE_CHECKING
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
import _ba
|
||||
from ba._music import MusicSubsystem
|
||||
from ba._language import LanguageSubsystem
|
||||
from ba._ui import UISubsystem
|
||||
from ba._achievement import AchievementSubsystem
|
||||
from ba._plugin import PluginSubsystem
|
||||
from ba._accountv1 import AccountV1Subsystem
|
||||
from ba._meta import MetadataSubsystem
|
||||
from ba._ads import AdsSubsystem
|
||||
from ba._net import NetworkSubsystem
|
||||
from ba._workspace import WorkspaceSubsystem
|
||||
from ba._appcomponent import AppComponentSubsystem
|
||||
from ba import _internal
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import asyncio
|
||||
from typing import Any, Callable
|
||||
|
||||
import efro.log
|
||||
import ba
|
||||
from ba._cloud import CloudSubsystem
|
||||
from bastd.actor import spazappearance
|
||||
from ba._accountv2 import AccountV2Subsystem
|
||||
from ba._level import Level
|
||||
from ba._apputils import AppHealthMonitor
|
||||
|
||||
|
||||
class App:
|
||||
"""A class for high level app functionality and state.
|
||||
|
||||
Category: **App Classes**
|
||||
|
||||
Use ba.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.
|
||||
"""
|
||||
|
||||
# pylint: disable=too-many-public-methods
|
||||
|
||||
# Implementations for these will be filled in by internal libs.
|
||||
accounts_v2: AccountV2Subsystem
|
||||
cloud: CloudSubsystem
|
||||
|
||||
log_handler: efro.log.LogHandler
|
||||
health_monitor: AppHealthMonitor
|
||||
|
||||
class State(Enum):
|
||||
"""High level state the app can be in."""
|
||||
|
||||
# The launch process has not yet begun.
|
||||
INITIAL = 0
|
||||
|
||||
# Our app subsystems are being inited but should not yet interact.
|
||||
LAUNCHING = 1
|
||||
|
||||
# App subsystems are inited and interacting, but the app has not
|
||||
# yet embarked on a high level course of action. It is doing initial
|
||||
# account logins, workspace & asset downloads, etc. in order to
|
||||
# prepare for this.
|
||||
LOADING = 2
|
||||
|
||||
# All pieces are in place and the app is now doing its thing.
|
||||
RUNNING = 3
|
||||
|
||||
# The app is backgrounded or otherwise suspended.
|
||||
PAUSED = 4
|
||||
|
||||
# The app is shutting down.
|
||||
SHUTTING_DOWN = 5
|
||||
|
||||
@property
|
||||
def aioloop(self) -> asyncio.AbstractEventLoop:
|
||||
"""The Logic Thread's Asyncio Event Loop.
|
||||
|
||||
This allow async tasks to be run in the logic thread.
|
||||
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 not return this
|
||||
loop from most places in the logic thread; only from within a
|
||||
task explicitly created in this loop.
|
||||
"""
|
||||
assert self._aioloop is not None
|
||||
return self._aioloop
|
||||
|
||||
@property
|
||||
def build_number(self) -> int:
|
||||
"""Integer build number.
|
||||
|
||||
This value increases by at least 1 with each release of the game.
|
||||
It is independent of the human readable ba.App.version string.
|
||||
"""
|
||||
assert isinstance(self._env['build_number'], int)
|
||||
return self._env['build_number']
|
||||
|
||||
@property
|
||||
def device_name(self) -> str:
|
||||
"""Name of the device running the game."""
|
||||
assert isinstance(self._env['device_name'], str)
|
||||
return self._env['device_name']
|
||||
|
||||
@property
|
||||
def config_file_path(self) -> str:
|
||||
"""Where the game's config file is stored on disk."""
|
||||
assert isinstance(self._env['config_file_path'], str)
|
||||
return self._env['config_file_path']
|
||||
|
||||
@property
|
||||
def user_agent_string(self) -> str:
|
||||
"""String containing various bits of info about OS/device/etc."""
|
||||
assert isinstance(self._env['user_agent_string'], str)
|
||||
return self._env['user_agent_string']
|
||||
|
||||
@property
|
||||
def version(self) -> str:
|
||||
"""Human-readable version string; something like '1.3.24'.
|
||||
|
||||
This should not be interpreted as a number; it may contain
|
||||
string elements such as 'alpha', 'beta', 'test', etc.
|
||||
If a numeric version is needed, use 'ba.App.build_number'.
|
||||
"""
|
||||
assert isinstance(self._env['version'], str)
|
||||
return self._env['version']
|
||||
|
||||
@property
|
||||
def debug_build(self) -> bool:
|
||||
"""Whether the app was compiled in debug mode.
|
||||
|
||||
Debug builds generally run substantially slower than non-debug
|
||||
builds due to compiler optimizations being disabled and extra
|
||||
checks being run.
|
||||
"""
|
||||
assert isinstance(self._env['debug_build'], bool)
|
||||
return self._env['debug_build']
|
||||
|
||||
@property
|
||||
def test_build(self) -> bool:
|
||||
"""Whether the game was compiled in test mode.
|
||||
|
||||
Test mode enables extra checks and features that are useful for
|
||||
release testing but which do not slow the game down significantly.
|
||||
"""
|
||||
assert isinstance(self._env['test_build'], bool)
|
||||
return self._env['test_build']
|
||||
|
||||
@property
|
||||
def python_directory_user(self) -> str:
|
||||
"""Path where the app looks for custom user scripts."""
|
||||
assert isinstance(self._env['python_directory_user'], str)
|
||||
return self._env['python_directory_user']
|
||||
|
||||
@property
|
||||
def python_directory_app(self) -> str:
|
||||
"""Path where the app looks for its bundled scripts."""
|
||||
assert isinstance(self._env['python_directory_app'], str)
|
||||
return self._env['python_directory_app']
|
||||
|
||||
@property
|
||||
def python_directory_app_site(self) -> str:
|
||||
"""Path containing pip packages bundled with the app."""
|
||||
assert isinstance(self._env['python_directory_app_site'], str)
|
||||
return self._env['python_directory_app_site']
|
||||
|
||||
@property
|
||||
def config(self) -> ba.AppConfig:
|
||||
"""The ba.AppConfig instance representing the app's config state."""
|
||||
assert self._config is not None
|
||||
return self._config
|
||||
|
||||
@property
|
||||
def platform(self) -> str:
|
||||
"""Name of the current platform.
|
||||
|
||||
Examples are: 'mac', 'windows', android'.
|
||||
"""
|
||||
assert isinstance(self._env['platform'], str)
|
||||
return self._env['platform']
|
||||
|
||||
@property
|
||||
def subplatform(self) -> str:
|
||||
"""String for subplatform.
|
||||
|
||||
Can be empty. For the 'android' platform, subplatform may
|
||||
be 'google', 'amazon', etc.
|
||||
"""
|
||||
assert isinstance(self._env['subplatform'], str)
|
||||
return self._env['subplatform']
|
||||
|
||||
@property
|
||||
def api_version(self) -> int:
|
||||
"""The game's api version.
|
||||
|
||||
Only Python modules and packages associated with the current API
|
||||
version number will be detected by the game (see the ba_meta tag).
|
||||
This value will change whenever backward-incompatible changes are
|
||||
introduced to game APIs. When that happens, scripts should be updated
|
||||
accordingly and set to target the new API version number.
|
||||
"""
|
||||
from ba._meta import CURRENT_API_VERSION
|
||||
|
||||
return CURRENT_API_VERSION
|
||||
|
||||
@property
|
||||
def on_tv(self) -> bool:
|
||||
"""Whether the game is currently running on a TV."""
|
||||
assert isinstance(self._env['on_tv'], bool)
|
||||
return self._env['on_tv']
|
||||
|
||||
@property
|
||||
def vr_mode(self) -> bool:
|
||||
"""Whether the game is currently running in VR."""
|
||||
assert isinstance(self._env['vr_mode'], bool)
|
||||
return self._env['vr_mode']
|
||||
|
||||
@property
|
||||
def ui_bounds(self) -> tuple[float, float, float, float]:
|
||||
"""Bounds of the 'safe' screen area in ui space.
|
||||
|
||||
This tuple contains: (x-min, x-max, y-min, y-max)
|
||||
"""
|
||||
return _ba.uibounds()
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""(internal)
|
||||
|
||||
Do not instantiate this class; use ba.app to access
|
||||
the single shared instance.
|
||||
"""
|
||||
# pylint: disable=too-many-statements
|
||||
|
||||
self.state = self.State.INITIAL
|
||||
|
||||
self._bootstrapping_completed = False
|
||||
self._called_on_app_launching = False
|
||||
self._launch_completed = False
|
||||
self._initial_sign_in_completed = False
|
||||
self._meta_scan_completed = False
|
||||
self._called_on_app_loading = False
|
||||
self._called_on_app_running = False
|
||||
self._app_paused = False
|
||||
|
||||
# Config.
|
||||
self.config_file_healthy = False
|
||||
|
||||
# This is incremented any time the app is backgrounded/foregrounded;
|
||||
# can be a simple way to determine if network data should be
|
||||
# refreshed/etc.
|
||||
self.fg_state = 0
|
||||
|
||||
self._aioloop: asyncio.AbstractEventLoop | None = None
|
||||
|
||||
self._env = _ba.env()
|
||||
self.protocol_version: int = self._env['protocol_version']
|
||||
assert isinstance(self.protocol_version, int)
|
||||
self.toolbar_test: bool = self._env['toolbar_test']
|
||||
assert isinstance(self.toolbar_test, bool)
|
||||
self.demo_mode: bool = self._env['demo_mode']
|
||||
assert isinstance(self.demo_mode, bool)
|
||||
self.arcade_mode: bool = self._env['arcade_mode']
|
||||
assert isinstance(self.arcade_mode, bool)
|
||||
self.headless_mode: bool = self._env['headless_mode']
|
||||
assert isinstance(self.headless_mode, bool)
|
||||
self.iircade_mode: bool = self._env['iircade_mode']
|
||||
assert isinstance(self.iircade_mode, bool)
|
||||
self.allow_ticket_purchases: bool = not self.iircade_mode
|
||||
|
||||
# Default executor which can be used for misc background processing.
|
||||
# It should also be passed to any asyncio loops we create so that
|
||||
# everything shares the same single set of threads.
|
||||
self.threadpool = ThreadPoolExecutor(thread_name_prefix='baworker')
|
||||
|
||||
# Misc.
|
||||
self.tips: list[str] = []
|
||||
self.stress_test_reset_timer: ba.Timer | None = None
|
||||
self.did_weak_call_warning = False
|
||||
|
||||
self.log_have_new = False
|
||||
self.log_upload_timer_started = False
|
||||
self._config: ba.AppConfig | None = None
|
||||
self.printed_live_object_warning = False
|
||||
|
||||
# We include this extra hash with shared input-mapping names so
|
||||
# that we don't share mappings between differently-configured
|
||||
# systems. For instance, different android devices may give different
|
||||
# key values for the same controller type so we keep their mappings
|
||||
# distinct.
|
||||
self.input_map_hash: str | None = None
|
||||
|
||||
# Co-op Campaigns.
|
||||
self.campaigns: dict[str, ba.Campaign] = {}
|
||||
self.custom_coop_practice_games: list[str] = []
|
||||
|
||||
# Server Mode.
|
||||
self.server: ba.ServerController | None = None
|
||||
|
||||
self.components = AppComponentSubsystem()
|
||||
self.meta = MetadataSubsystem()
|
||||
self.accounts_v1 = AccountV1Subsystem()
|
||||
self.plugins = PluginSubsystem()
|
||||
self.music = MusicSubsystem()
|
||||
self.lang = LanguageSubsystem()
|
||||
self.ach = AchievementSubsystem()
|
||||
self.ui = UISubsystem()
|
||||
self.ads = AdsSubsystem()
|
||||
self.net = NetworkSubsystem()
|
||||
self.workspaces = WorkspaceSubsystem()
|
||||
|
||||
# Lobby.
|
||||
self.lobby_random_profile_index: int = 1
|
||||
self.lobby_random_char_index_offset = random.randrange(1000)
|
||||
self.lobby_account_profile_device_id: int | None = None
|
||||
|
||||
# Main Menu.
|
||||
self.main_menu_did_initial_transition = False
|
||||
self.main_menu_last_news_fetch_time: float | None = None
|
||||
|
||||
# Spaz.
|
||||
self.spaz_appearances: dict[str, spazappearance.Appearance] = {}
|
||||
self.last_spaz_turbo_warn_time: float = -99999.0
|
||||
|
||||
# Maps.
|
||||
self.maps: dict[str, type[ba.Map]] = {}
|
||||
|
||||
# Gameplay.
|
||||
self.teams_series_length = 7
|
||||
self.ffa_series_length = 24
|
||||
self.coop_session_args: dict = {}
|
||||
|
||||
self.value_test_defaults: dict = {}
|
||||
self.first_main_menu = True # FIXME: Move to mainmenu class.
|
||||
self.did_menu_intro = False # FIXME: Move to mainmenu class.
|
||||
self.main_menu_window_refresh_check_count = 0 # FIXME: Mv to mainmenu.
|
||||
self.main_menu_resume_callbacks: list = [] # Can probably go away.
|
||||
self.special_offer: dict | None = None
|
||||
self.ping_thread_count = 0
|
||||
self.invite_confirm_windows: list[Any] = [] # FIXME: Don't use Any.
|
||||
self.store_layout: dict[str, list[dict[str, Any]]] | None = None
|
||||
self.store_items: dict[str, dict] | None = None
|
||||
self.pro_sale_start_time: int | None = None
|
||||
self.pro_sale_start_val: int | None = None
|
||||
|
||||
self.delegate: ba.AppDelegate | None = None
|
||||
self._asyncio_timer: ba.Timer | None = None
|
||||
|
||||
def on_app_launching(self) -> None:
|
||||
"""Called when the app is first entering the launching state."""
|
||||
# pylint: disable=cyclic-import
|
||||
# pylint: disable=too-many-locals
|
||||
from ba import _asyncio
|
||||
from ba import _appconfig
|
||||
from ba import _map
|
||||
from ba import _campaign
|
||||
from bastd import appdelegate
|
||||
from bastd import maps as stdmaps
|
||||
from bastd.actor import spazappearance
|
||||
from ba._generated.enums import TimeType
|
||||
from ba._apputils import (
|
||||
log_dumped_app_state,
|
||||
handle_leftover_v1_cloud_log_file,
|
||||
AppHealthMonitor,
|
||||
)
|
||||
|
||||
assert _ba.in_logic_thread()
|
||||
|
||||
self._aioloop = _asyncio.setup_asyncio()
|
||||
self.health_monitor = AppHealthMonitor()
|
||||
|
||||
cfg = self.config
|
||||
|
||||
self.delegate = appdelegate.AppDelegate()
|
||||
|
||||
self.ui.on_app_launch()
|
||||
|
||||
spazappearance.register_appearances()
|
||||
_campaign.init_campaigns()
|
||||
|
||||
# FIXME: This should not be hard-coded.
|
||||
for maptype in [
|
||||
stdmaps.HockeyStadium,
|
||||
stdmaps.FootballStadium,
|
||||
stdmaps.Bridgit,
|
||||
stdmaps.BigG,
|
||||
stdmaps.Roundabout,
|
||||
stdmaps.MonkeyFace,
|
||||
stdmaps.ZigZag,
|
||||
stdmaps.ThePad,
|
||||
stdmaps.DoomShroom,
|
||||
stdmaps.LakeFrigid,
|
||||
stdmaps.TipTop,
|
||||
stdmaps.CragCastle,
|
||||
stdmaps.TowerD,
|
||||
stdmaps.HappyThoughts,
|
||||
stdmaps.StepRightUp,
|
||||
stdmaps.Courtyard,
|
||||
stdmaps.Rampage,
|
||||
]:
|
||||
_map.register_map(maptype)
|
||||
|
||||
# Non-test, non-debug builds should generally be blessed; warn if not.
|
||||
# (so I don't accidentally release a build that can't play tourneys)
|
||||
if (
|
||||
not self.debug_build
|
||||
and not self.test_build
|
||||
and not _internal.is_blessed()
|
||||
):
|
||||
_ba.screenmessage('WARNING: NON-BLESSED BUILD', color=(1, 0, 0))
|
||||
|
||||
# If there's a leftover log file, attempt to upload it to the
|
||||
# master-server and/or get rid of it.
|
||||
handle_leftover_v1_cloud_log_file()
|
||||
|
||||
# Only do this stuff if our config file is healthy so we don't
|
||||
# overwrite a broken one or whatnot and wipe out data.
|
||||
if not self.config_file_healthy:
|
||||
if self.platform in ('mac', 'linux', 'windows'):
|
||||
from bastd.ui.configerror import ConfigErrorWindow
|
||||
|
||||
_ba.pushcall(ConfigErrorWindow)
|
||||
return
|
||||
|
||||
# For now on other systems we just overwrite the bum config.
|
||||
# At this point settings are already set; lets just commit them
|
||||
# to disk.
|
||||
_appconfig.commit_app_config(force=True)
|
||||
|
||||
self.music.on_app_launch()
|
||||
|
||||
launch_count = cfg.get('launchCount', 0)
|
||||
launch_count += 1
|
||||
|
||||
# So we know how many times we've run the game at various
|
||||
# version milestones.
|
||||
for key in ('lc14173', 'lc14292'):
|
||||
cfg.setdefault(key, launch_count)
|
||||
|
||||
cfg['launchCount'] = launch_count
|
||||
cfg.commit()
|
||||
|
||||
# Run a test in a few seconds to see if we should pop up an existing
|
||||
# pending special offer.
|
||||
def check_special_offer() -> None:
|
||||
from bastd.ui.specialoffer import show_offer
|
||||
|
||||
config = self.config
|
||||
if (
|
||||
'pendingSpecialOffer' in config
|
||||
and _internal.get_public_login_id()
|
||||
== config['pendingSpecialOffer']['a']
|
||||
):
|
||||
self.special_offer = config['pendingSpecialOffer']['o']
|
||||
show_offer()
|
||||
|
||||
if not self.headless_mode:
|
||||
_ba.timer(3.0, check_special_offer, timetype=TimeType.REAL)
|
||||
|
||||
# Get meta-system scanning built-in stuff in the bg.
|
||||
self.meta.start_scan(scan_complete_cb=self.on_meta_scan_complete)
|
||||
|
||||
self.accounts_v2.on_app_launch()
|
||||
self.accounts_v1.on_app_launch()
|
||||
|
||||
# See note below in on_app_pause.
|
||||
if self.state != self.State.LAUNCHING:
|
||||
logging.error(
|
||||
'on_app_launch found state %s; expected LAUNCHING.', self.state
|
||||
)
|
||||
|
||||
# If any traceback dumps happened last run, log and clear them.
|
||||
log_dumped_app_state()
|
||||
|
||||
self._launch_completed = True
|
||||
self._update_state()
|
||||
|
||||
def on_app_loading(self) -> None:
|
||||
"""Called when initially entering the loading state."""
|
||||
|
||||
def on_app_running(self) -> None:
|
||||
"""Called when initially entering the running state."""
|
||||
|
||||
self.plugins.on_app_running()
|
||||
|
||||
# from ba._dependency import test_depset
|
||||
# test_depset()
|
||||
|
||||
def on_bootstrapping_completed(self) -> None:
|
||||
"""Called by the C++ layer once its ready to rock."""
|
||||
assert _ba.in_logic_thread()
|
||||
assert not self._bootstrapping_completed
|
||||
self._bootstrapping_completed = True
|
||||
self._update_state()
|
||||
|
||||
def on_meta_scan_complete(self) -> None:
|
||||
"""Called by meta-scan when it is done doing its thing."""
|
||||
assert _ba.in_logic_thread()
|
||||
self.plugins.on_meta_scan_complete()
|
||||
|
||||
assert not self._meta_scan_completed
|
||||
self._meta_scan_completed = True
|
||||
self._update_state()
|
||||
|
||||
def _update_state(self) -> None:
|
||||
assert _ba.in_logic_thread()
|
||||
|
||||
if self._app_paused:
|
||||
# Entering paused state:
|
||||
if self.state is not self.State.PAUSED:
|
||||
self.state = self.State.PAUSED
|
||||
self.cloud.on_app_pause()
|
||||
self.accounts_v1.on_app_pause()
|
||||
self.plugins.on_app_pause()
|
||||
self.health_monitor.on_app_pause()
|
||||
else:
|
||||
# Leaving paused state:
|
||||
if self.state is self.State.PAUSED:
|
||||
self.fg_state += 1
|
||||
self.cloud.on_app_resume()
|
||||
self.accounts_v1.on_app_resume()
|
||||
self.music.on_app_resume()
|
||||
self.plugins.on_app_resume()
|
||||
self.health_monitor.on_app_resume()
|
||||
|
||||
# Handle initially entering or returning to other states.
|
||||
if self._initial_sign_in_completed and self._meta_scan_completed:
|
||||
self.state = self.State.RUNNING
|
||||
if not self._called_on_app_running:
|
||||
self._called_on_app_running = True
|
||||
self.on_app_running()
|
||||
elif self._launch_completed:
|
||||
self.state = self.State.LOADING
|
||||
if not self._called_on_app_loading:
|
||||
self._called_on_app_loading = True
|
||||
self.on_app_loading()
|
||||
else:
|
||||
# Only thing left is launching. We shouldn't be getting
|
||||
# called before at least that is complete.
|
||||
assert self._bootstrapping_completed
|
||||
self.state = self.State.LAUNCHING
|
||||
if not self._called_on_app_launching:
|
||||
self._called_on_app_launching = True
|
||||
self.on_app_launching()
|
||||
|
||||
def on_app_pause(self) -> None:
|
||||
"""Called when the app goes to a suspended state."""
|
||||
|
||||
assert not self._app_paused # Should avoid redundant calls.
|
||||
self._app_paused = True
|
||||
self._update_state()
|
||||
|
||||
def on_app_resume(self) -> None:
|
||||
"""Run when the app resumes from a suspended state."""
|
||||
|
||||
assert self._app_paused # Should avoid redundant calls.
|
||||
self._app_paused = False
|
||||
self._update_state()
|
||||
|
||||
def on_app_shutdown(self) -> None:
|
||||
"""(internal)"""
|
||||
self.state = self.State.SHUTTING_DOWN
|
||||
self.music.on_app_shutdown()
|
||||
self.plugins.on_app_shutdown()
|
||||
|
||||
def read_config(self) -> None:
|
||||
"""(internal)"""
|
||||
from ba._appconfig import read_config
|
||||
|
||||
self._config, self.config_file_healthy = read_config()
|
||||
|
||||
def pause(self) -> None:
|
||||
"""Pause the game due to a user request or menu popping up.
|
||||
|
||||
If there's a foreground host-activity that says it's pausable, tell it
|
||||
to pause ..we now no longer pause if there are connected clients.
|
||||
"""
|
||||
activity: ba.Activity | None = _ba.get_foreground_host_activity()
|
||||
if (
|
||||
activity is not None
|
||||
and activity.allow_pausing
|
||||
and not _ba.have_connected_clients()
|
||||
):
|
||||
from ba._language import Lstr
|
||||
from ba._nodeactor import NodeActor
|
||||
|
||||
# FIXME: Shouldn't be touching scene stuff here;
|
||||
# should just pass the request on to the host-session.
|
||||
with _ba.Context(activity):
|
||||
globs = activity.globalsnode
|
||||
if not globs.paused:
|
||||
_ba.playsound(_ba.getsound('refWhistle'))
|
||||
globs.paused = True
|
||||
|
||||
# FIXME: This should not be an attr on Actor.
|
||||
activity.paused_text = NodeActor(
|
||||
_ba.newnode(
|
||||
'text',
|
||||
attrs={
|
||||
'text': Lstr(resource='pausedByHostText'),
|
||||
'client_only': True,
|
||||
'flatness': 1.0,
|
||||
'h_align': 'center',
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
def resume(self) -> None:
|
||||
"""Resume the game due to a user request or menu closing.
|
||||
|
||||
If there's a foreground host-activity that's currently paused, tell it
|
||||
to resume.
|
||||
"""
|
||||
|
||||
# FIXME: Shouldn't be touching scene stuff here;
|
||||
# should just pass the request on to the host-session.
|
||||
activity = _ba.get_foreground_host_activity()
|
||||
if activity is not None:
|
||||
with _ba.Context(activity):
|
||||
globs = activity.globalsnode
|
||||
if globs.paused:
|
||||
_ba.playsound(_ba.getsound('refWhistle'))
|
||||
globs.paused = False
|
||||
|
||||
# FIXME: This should not be an actor attr.
|
||||
activity.paused_text = None
|
||||
|
||||
def add_coop_practice_level(self, level: Level) -> None:
|
||||
"""Adds an individual level to the 'practice' section in Co-op."""
|
||||
|
||||
# Assign this level to our catch-all campaign.
|
||||
self.campaigns['Challenges'].addlevel(level)
|
||||
|
||||
# Make note to add it to our challenges UI.
|
||||
self.custom_coop_practice_games.append(f'Challenges:{level.name}')
|
||||
|
||||
def return_to_main_menu_session_gracefully(
|
||||
self, reset_ui: bool = True
|
||||
) -> None:
|
||||
"""Attempt to cleanly get back to the main menu."""
|
||||
# pylint: disable=cyclic-import
|
||||
from ba import _benchmark
|
||||
from ba._general import Call
|
||||
from bastd.mainmenu import MainMenuSession
|
||||
|
||||
if reset_ui:
|
||||
_ba.app.ui.clear_main_menu_window()
|
||||
|
||||
if isinstance(_ba.get_foreground_host_session(), MainMenuSession):
|
||||
# It may be possible we're on the main menu but the screen is faded
|
||||
# so fade back in.
|
||||
_ba.fade_screen(True)
|
||||
return
|
||||
|
||||
_benchmark.stop_stress_test() # Stop stress-test if in progress.
|
||||
|
||||
# If we're in a host-session, tell them to end.
|
||||
# This lets them tear themselves down gracefully.
|
||||
host_session: ba.Session | None = _ba.get_foreground_host_session()
|
||||
if host_session is not None:
|
||||
|
||||
# Kick off a little transaction so we'll hopefully have all the
|
||||
# latest account state when we get back to the menu.
|
||||
_internal.add_transaction(
|
||||
{'type': 'END_SESSION', 'sType': str(type(host_session))}
|
||||
)
|
||||
_internal.run_transactions()
|
||||
|
||||
host_session.end()
|
||||
|
||||
# Otherwise just force the issue.
|
||||
else:
|
||||
_ba.pushcall(Call(_ba.new_host_session, MainMenuSession))
|
||||
|
||||
def add_main_menu_close_callback(self, call: Callable[[], Any]) -> None:
|
||||
"""(internal)"""
|
||||
|
||||
# If there's no main menu up, just call immediately.
|
||||
if not self.ui.has_main_menu_window():
|
||||
with _ba.Context('ui'):
|
||||
call()
|
||||
else:
|
||||
self.main_menu_resume_callbacks.append(call)
|
||||
|
||||
def launch_coop_game(
|
||||
self, game: str, force: bool = False, args: dict | None = None
|
||||
) -> bool:
|
||||
"""High level way to launch a local co-op session."""
|
||||
# pylint: disable=cyclic-import
|
||||
from ba._campaign import getcampaign
|
||||
from bastd.ui.coop.level import CoopLevelLockedWindow
|
||||
|
||||
if args is None:
|
||||
args = {}
|
||||
if game == '':
|
||||
raise ValueError('empty game name')
|
||||
campaignname, levelname = game.split(':')
|
||||
campaign = getcampaign(campaignname)
|
||||
|
||||
# If this campaign is sequential, make sure we've completed the
|
||||
# one before this.
|
||||
if campaign.sequential and not force:
|
||||
for level in campaign.levels:
|
||||
if level.name == levelname:
|
||||
break
|
||||
if not level.complete:
|
||||
CoopLevelLockedWindow(
|
||||
campaign.getlevel(levelname).displayname,
|
||||
campaign.getlevel(level.name).displayname,
|
||||
)
|
||||
return False
|
||||
|
||||
# Ok, we're good to go.
|
||||
self.coop_session_args = {
|
||||
'campaign': campaignname,
|
||||
'level': levelname,
|
||||
}
|
||||
for arg_name, arg_val in list(args.items()):
|
||||
self.coop_session_args[arg_name] = arg_val
|
||||
|
||||
def _fade_end() -> None:
|
||||
from ba import _coopsession
|
||||
|
||||
try:
|
||||
_ba.new_host_session(_coopsession.CoopSession)
|
||||
except Exception:
|
||||
from ba import _error
|
||||
|
||||
_error.print_exception()
|
||||
from bastd.mainmenu import MainMenuSession
|
||||
|
||||
_ba.new_host_session(MainMenuSession)
|
||||
|
||||
_ba.fade_screen(False, endcall=_fade_end)
|
||||
return True
|
||||
|
||||
def handle_deep_link(self, url: str) -> None:
|
||||
"""Handle a deep link URL."""
|
||||
from ba._language import Lstr
|
||||
|
||||
appname = _ba.appname()
|
||||
if url.startswith(f'{appname}://code/'):
|
||||
code = url.replace(f'{appname}://code/', '')
|
||||
self.accounts_v1.add_pending_promo_code(code)
|
||||
else:
|
||||
_ba.screenmessage(Lstr(resource='errorText'), color=(1, 0, 0))
|
||||
_ba.playsound(_ba.getsound('error'))
|
||||
|
||||
def on_initial_sign_in_completed(self) -> None:
|
||||
"""Callback to be run after initial sign-in (or lack thereof).
|
||||
|
||||
This period includes things such as syncing account workspaces
|
||||
or other data so it may take a substantial amount of time.
|
||||
This should also run after a short amount of time if no login
|
||||
has occurred.
|
||||
"""
|
||||
# Tell meta it can start scanning extra stuff that just showed up
|
||||
# (account workspaces).
|
||||
self.meta.start_extra_scan()
|
||||
|
||||
self._initial_sign_in_completed = True
|
||||
self._update_state()
|
||||
Loading…
Add table
Add a link
Reference in a new issue