mirror of
https://github.com/imayushsaini/Bombsquad-Ballistica-Modded-Server.git
synced 2025-11-14 17:46:03 +00:00
hello API 8 !
This commit is contained in:
parent
3a2b6ade68
commit
0284fee95c
1166 changed files with 26061 additions and 375100 deletions
277
dist/ba_data/python/_bainternal.py
vendored
277
dist/ba_data/python/_bainternal.py
vendored
|
|
@ -1,277 +0,0 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""A dummy stub module for the real _bainternal.
|
||||
|
||||
The real _bainternal is a compiled extension module and only available
|
||||
in the live engine. This dummy-module allows Pylint/Mypy/etc. to
|
||||
function reasonably well outside of that environment.
|
||||
|
||||
Make sure this file is never included in dirs seen by the engine!
|
||||
|
||||
In the future perhaps this can be a stub (.pyi) file, but we will need
|
||||
to make sure that it works with all our tools (mypy, pylint, pycharm).
|
||||
|
||||
NOTE: This file was autogenerated by batools.dummymodule; do not edit by hand.
|
||||
"""
|
||||
|
||||
# I'm sorry Pylint. I know this file saddens you. Be strong.
|
||||
# pylint: disable=useless-suppression
|
||||
# pylint: disable=unnecessary-pass
|
||||
# pylint: disable=use-dict-literal
|
||||
# pylint: disable=use-list-literal
|
||||
# pylint: disable=unused-argument
|
||||
# pylint: disable=missing-docstring
|
||||
# pylint: disable=too-many-locals
|
||||
# pylint: disable=redefined-builtin
|
||||
# pylint: disable=too-many-lines
|
||||
# pylint: disable=redefined-outer-name
|
||||
# pylint: disable=invalid-name
|
||||
# pylint: disable=no-value-for-parameter
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, TypeVar
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any, Callable
|
||||
|
||||
|
||||
_T = TypeVar('_T')
|
||||
|
||||
|
||||
def _uninferrable() -> Any:
|
||||
"""Get an "Any" in mypy and "uninferrable" in Pylint."""
|
||||
# pylint: disable=undefined-variable
|
||||
return _not_a_real_variable # type: ignore
|
||||
|
||||
|
||||
def add_transaction(
|
||||
transaction: dict, callback: Callable | None = None
|
||||
) -> None:
|
||||
|
||||
"""(internal)"""
|
||||
return None
|
||||
|
||||
|
||||
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.
|
||||
"""
|
||||
return bool()
|
||||
|
||||
|
||||
def get_master_server_address(source: int = -1, version: int = 1) -> str:
|
||||
|
||||
"""(internal)
|
||||
|
||||
Return the address of the master server.
|
||||
"""
|
||||
return str()
|
||||
|
||||
|
||||
def get_news_show() -> str:
|
||||
|
||||
"""(internal)"""
|
||||
return str()
|
||||
|
||||
|
||||
def get_price(item: str) -> str | None:
|
||||
|
||||
"""(internal)"""
|
||||
return ''
|
||||
|
||||
|
||||
def get_public_login_id() -> str | None:
|
||||
|
||||
"""(internal)"""
|
||||
return ''
|
||||
|
||||
|
||||
def get_purchased(item: str) -> bool:
|
||||
|
||||
"""(internal)"""
|
||||
return bool()
|
||||
|
||||
|
||||
def get_purchases_state() -> int:
|
||||
|
||||
"""(internal)"""
|
||||
return int()
|
||||
|
||||
|
||||
def get_v1_account_display_string(full: bool = True) -> str:
|
||||
|
||||
"""(internal)"""
|
||||
return str()
|
||||
|
||||
|
||||
def get_v1_account_misc_read_val(name: str, default_value: Any) -> Any:
|
||||
|
||||
"""(internal)"""
|
||||
return _uninferrable()
|
||||
|
||||
|
||||
def get_v1_account_misc_read_val_2(name: str, default_value: Any) -> Any:
|
||||
|
||||
"""(internal)"""
|
||||
return _uninferrable()
|
||||
|
||||
|
||||
def get_v1_account_misc_val(name: str, default_value: Any) -> Any:
|
||||
|
||||
"""(internal)"""
|
||||
return _uninferrable()
|
||||
|
||||
|
||||
def get_v1_account_name() -> str:
|
||||
|
||||
"""(internal)"""
|
||||
return str()
|
||||
|
||||
|
||||
def get_v1_account_state() -> str:
|
||||
|
||||
"""(internal)"""
|
||||
return str()
|
||||
|
||||
|
||||
def get_v1_account_state_num() -> int:
|
||||
|
||||
"""(internal)"""
|
||||
return int()
|
||||
|
||||
|
||||
def get_v1_account_ticket_count() -> int:
|
||||
|
||||
"""(internal)
|
||||
|
||||
Returns the number of tickets for the current account.
|
||||
"""
|
||||
return int()
|
||||
|
||||
|
||||
def get_v1_account_type() -> str:
|
||||
|
||||
"""(internal)"""
|
||||
return str()
|
||||
|
||||
|
||||
def get_v2_fleet() -> str:
|
||||
|
||||
"""(internal)"""
|
||||
return str()
|
||||
|
||||
|
||||
def have_outstanding_transactions() -> bool:
|
||||
|
||||
"""(internal)"""
|
||||
return bool()
|
||||
|
||||
|
||||
def in_game_purchase(item: str, price: int) -> None:
|
||||
|
||||
"""(internal)"""
|
||||
return None
|
||||
|
||||
|
||||
def is_blessed() -> bool:
|
||||
|
||||
"""(internal)"""
|
||||
return bool()
|
||||
|
||||
|
||||
def mark_config_dirty() -> None:
|
||||
|
||||
"""(internal)
|
||||
|
||||
Category: General Utility Functions
|
||||
"""
|
||||
return None
|
||||
|
||||
|
||||
def power_ranking_query(callback: Callable, season: Any = None) -> None:
|
||||
|
||||
"""(internal)"""
|
||||
return None
|
||||
|
||||
|
||||
def purchase(item: str) -> None:
|
||||
|
||||
"""(internal)"""
|
||||
return None
|
||||
|
||||
|
||||
def report_achievement(achievement: str, pass_to_account: bool = True) -> None:
|
||||
|
||||
"""(internal)"""
|
||||
return None
|
||||
|
||||
|
||||
def reset_achievements() -> None:
|
||||
|
||||
"""(internal)"""
|
||||
return None
|
||||
|
||||
|
||||
def restore_purchases() -> None:
|
||||
|
||||
"""(internal)"""
|
||||
return None
|
||||
|
||||
|
||||
def run_transactions() -> None:
|
||||
|
||||
"""(internal)"""
|
||||
return None
|
||||
|
||||
|
||||
def sign_in_v1(account_type: str) -> None:
|
||||
|
||||
"""(internal)
|
||||
|
||||
Category: General Utility Functions
|
||||
"""
|
||||
return None
|
||||
|
||||
|
||||
def sign_out_v1(v2_embedded: bool = False) -> None:
|
||||
|
||||
"""(internal)
|
||||
|
||||
Category: General Utility Functions
|
||||
"""
|
||||
return None
|
||||
|
||||
|
||||
def submit_score(
|
||||
game: str,
|
||||
config: str,
|
||||
name: Any,
|
||||
score: int | None,
|
||||
callback: Callable,
|
||||
order: str = 'increasing',
|
||||
tournament_id: str | None = None,
|
||||
score_type: str = 'points',
|
||||
campaign: str | None = None,
|
||||
level: str | None = None,
|
||||
) -> None:
|
||||
|
||||
"""(internal)
|
||||
|
||||
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.
|
||||
"""
|
||||
return None
|
||||
|
||||
|
||||
def tournament_query(
|
||||
callback: Callable[[dict | None], None], args: dict
|
||||
) -> None:
|
||||
|
||||
"""(internal)"""
|
||||
return None
|
||||
396
dist/ba_data/python/ba/__init__.py
vendored
396
dist/ba_data/python/ba/__init__.py
vendored
|
|
@ -1,396 +0,0 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""The public face of Ballistica.
|
||||
|
||||
This top level module is a collection of most commonly used functionality.
|
||||
For many modding purposes, the bits exposed here are all you'll need.
|
||||
In some specific cases you may need to pull in individual submodules instead.
|
||||
"""
|
||||
# pylint: disable=redefined-builtin
|
||||
|
||||
from _ba import (
|
||||
CollideModel,
|
||||
Context,
|
||||
ContextCall,
|
||||
Data,
|
||||
InputDevice,
|
||||
Material,
|
||||
Model,
|
||||
Node,
|
||||
SessionPlayer,
|
||||
Sound,
|
||||
Texture,
|
||||
Timer,
|
||||
Vec3,
|
||||
Widget,
|
||||
buttonwidget,
|
||||
camerashake,
|
||||
checkboxwidget,
|
||||
columnwidget,
|
||||
containerwidget,
|
||||
do_once,
|
||||
emitfx,
|
||||
getactivity,
|
||||
getcollidemodel,
|
||||
getmodel,
|
||||
getnodes,
|
||||
getsession,
|
||||
getsound,
|
||||
gettexture,
|
||||
hscrollwidget,
|
||||
imagewidget,
|
||||
newactivity,
|
||||
newnode,
|
||||
playsound,
|
||||
printnodes,
|
||||
ls_objects,
|
||||
ls_input_devices,
|
||||
pushcall,
|
||||
quit,
|
||||
rowwidget,
|
||||
safecolor,
|
||||
screenmessage,
|
||||
scrollwidget,
|
||||
set_analytics_screen,
|
||||
charstr,
|
||||
textwidget,
|
||||
time,
|
||||
timer,
|
||||
open_url,
|
||||
widget,
|
||||
clipboard_is_supported,
|
||||
clipboard_has_text,
|
||||
clipboard_get_text,
|
||||
clipboard_set_text,
|
||||
getdata,
|
||||
in_logic_thread,
|
||||
)
|
||||
from ba._accountv2 import AccountV2Handle
|
||||
from ba._activity import Activity
|
||||
from ba._plugin import PotentialPlugin, Plugin, PluginSubsystem
|
||||
from ba._actor import Actor
|
||||
from ba._player import PlayerInfo, Player, EmptyPlayer, StandLocation
|
||||
from ba._nodeactor import NodeActor
|
||||
from ba._app import App
|
||||
from ba._cloud import CloudSubsystem
|
||||
from ba._coopgame import CoopGameActivity
|
||||
from ba._coopsession import CoopSession
|
||||
from ba._dependency import (
|
||||
Dependency,
|
||||
DependencyComponent,
|
||||
DependencySet,
|
||||
AssetPackage,
|
||||
)
|
||||
from ba._generated.enums import (
|
||||
TimeType,
|
||||
Permission,
|
||||
TimeFormat,
|
||||
SpecialChar,
|
||||
InputType,
|
||||
UIScale,
|
||||
)
|
||||
from ba._error import (
|
||||
print_exception,
|
||||
print_error,
|
||||
ContextError,
|
||||
NotFoundError,
|
||||
PlayerNotFoundError,
|
||||
SessionPlayerNotFoundError,
|
||||
NodeNotFoundError,
|
||||
ActorNotFoundError,
|
||||
InputDeviceNotFoundError,
|
||||
WidgetNotFoundError,
|
||||
ActivityNotFoundError,
|
||||
TeamNotFoundError,
|
||||
MapNotFoundError,
|
||||
SessionTeamNotFoundError,
|
||||
SessionNotFoundError,
|
||||
DelegateNotFoundError,
|
||||
DependencyError,
|
||||
)
|
||||
from ba._freeforallsession import FreeForAllSession
|
||||
from ba._gameactivity import GameActivity
|
||||
from ba._gameresults import GameResults
|
||||
from ba._settings import (
|
||||
Setting,
|
||||
IntSetting,
|
||||
FloatSetting,
|
||||
ChoiceSetting,
|
||||
BoolSetting,
|
||||
IntChoiceSetting,
|
||||
FloatChoiceSetting,
|
||||
)
|
||||
from ba._language import Lstr, LanguageSubsystem
|
||||
from ba._map import Map, getmaps
|
||||
from ba._session import Session
|
||||
from ba._ui import UISubsystem
|
||||
from ba._servermode import ServerController
|
||||
from ba._score import ScoreType, ScoreConfig
|
||||
from ba._stats import PlayerScoredMessage, PlayerRecord, Stats
|
||||
from ba._team import SessionTeam, Team, EmptyTeam
|
||||
from ba._teamgame import TeamGameActivity
|
||||
from ba._dualteamsession import DualTeamSession
|
||||
from ba._achievement import Achievement, AchievementSubsystem
|
||||
from ba._appconfig import AppConfig
|
||||
from ba._appdelegate import AppDelegate
|
||||
from ba._apputils import is_browser_likely_available, garbage_collect
|
||||
from ba._campaign import Campaign
|
||||
from ba._gameutils import (
|
||||
GameTip,
|
||||
animate,
|
||||
animate_array,
|
||||
show_damage_count,
|
||||
timestring,
|
||||
cameraflash,
|
||||
)
|
||||
from ba._general import (
|
||||
WeakCall,
|
||||
Call,
|
||||
existing,
|
||||
Existable,
|
||||
verify_object_death,
|
||||
storagename,
|
||||
getclass,
|
||||
)
|
||||
from ba._keyboard import Keyboard
|
||||
from ba._level import Level
|
||||
from ba._lobby import Lobby, Chooser
|
||||
from ba._math import normalized_color, is_point_in_box, vec3validate
|
||||
from ba._meta import MetadataSubsystem
|
||||
from ba._messages import (
|
||||
UNHANDLED,
|
||||
OutOfBoundsMessage,
|
||||
DeathType,
|
||||
DieMessage,
|
||||
PlayerDiedMessage,
|
||||
StandMessage,
|
||||
PickUpMessage,
|
||||
DropMessage,
|
||||
PickedUpMessage,
|
||||
DroppedMessage,
|
||||
ShouldShatterMessage,
|
||||
ImpactDamageMessage,
|
||||
FreezeMessage,
|
||||
ThawMessage,
|
||||
HitMessage,
|
||||
CelebrateMessage,
|
||||
)
|
||||
from ba._music import (
|
||||
setmusic,
|
||||
MusicPlayer,
|
||||
MusicType,
|
||||
MusicPlayMode,
|
||||
MusicSubsystem,
|
||||
)
|
||||
from ba._powerup import PowerupMessage, PowerupAcceptMessage
|
||||
from ba._multiteamsession import MultiTeamSession
|
||||
from ba.ui import Window, UIController, uicleanupcheck
|
||||
from ba._collision import Collision, getcollision
|
||||
|
||||
app: App
|
||||
|
||||
__all__ = [
|
||||
'AccountV2Handle',
|
||||
'Achievement',
|
||||
'AchievementSubsystem',
|
||||
'Activity',
|
||||
'ActivityNotFoundError',
|
||||
'Actor',
|
||||
'ActorNotFoundError',
|
||||
'animate',
|
||||
'animate_array',
|
||||
'app',
|
||||
'App',
|
||||
'AppConfig',
|
||||
'AppDelegate',
|
||||
'AssetPackage',
|
||||
'BoolSetting',
|
||||
'buttonwidget',
|
||||
'Call',
|
||||
'cameraflash',
|
||||
'camerashake',
|
||||
'Campaign',
|
||||
'CelebrateMessage',
|
||||
'charstr',
|
||||
'checkboxwidget',
|
||||
'ChoiceSetting',
|
||||
'Chooser',
|
||||
'clipboard_get_text',
|
||||
'clipboard_has_text',
|
||||
'clipboard_is_supported',
|
||||
'clipboard_set_text',
|
||||
'CollideModel',
|
||||
'Collision',
|
||||
'columnwidget',
|
||||
'containerwidget',
|
||||
'Context',
|
||||
'ContextCall',
|
||||
'ContextError',
|
||||
'CloudSubsystem',
|
||||
'CoopGameActivity',
|
||||
'CoopSession',
|
||||
'Data',
|
||||
'DeathType',
|
||||
'DelegateNotFoundError',
|
||||
'Dependency',
|
||||
'DependencyComponent',
|
||||
'DependencyError',
|
||||
'DependencySet',
|
||||
'DieMessage',
|
||||
'do_once',
|
||||
'DropMessage',
|
||||
'DroppedMessage',
|
||||
'DualTeamSession',
|
||||
'emitfx',
|
||||
'EmptyPlayer',
|
||||
'EmptyTeam',
|
||||
'Existable',
|
||||
'existing',
|
||||
'FloatChoiceSetting',
|
||||
'FloatSetting',
|
||||
'FreeForAllSession',
|
||||
'FreezeMessage',
|
||||
'GameActivity',
|
||||
'GameResults',
|
||||
'GameTip',
|
||||
'garbage_collect',
|
||||
'getactivity',
|
||||
'getclass',
|
||||
'getcollidemodel',
|
||||
'getcollision',
|
||||
'getdata',
|
||||
'getmaps',
|
||||
'getmodel',
|
||||
'getnodes',
|
||||
'getsession',
|
||||
'getsound',
|
||||
'gettexture',
|
||||
'HitMessage',
|
||||
'hscrollwidget',
|
||||
'imagewidget',
|
||||
'ImpactDamageMessage',
|
||||
'in_logic_thread',
|
||||
'InputDevice',
|
||||
'InputDeviceNotFoundError',
|
||||
'InputType',
|
||||
'IntChoiceSetting',
|
||||
'IntSetting',
|
||||
'is_browser_likely_available',
|
||||
'is_point_in_box',
|
||||
'Keyboard',
|
||||
'LanguageSubsystem',
|
||||
'Level',
|
||||
'Lobby',
|
||||
'Lstr',
|
||||
'Map',
|
||||
'MapNotFoundError',
|
||||
'Material',
|
||||
'MetadataSubsystem',
|
||||
'Model',
|
||||
'MultiTeamSession',
|
||||
'MusicPlayer',
|
||||
'MusicPlayMode',
|
||||
'MusicSubsystem',
|
||||
'MusicType',
|
||||
'newactivity',
|
||||
'newnode',
|
||||
'Node',
|
||||
'NodeActor',
|
||||
'NodeNotFoundError',
|
||||
'normalized_color',
|
||||
'NotFoundError',
|
||||
'open_url',
|
||||
'OutOfBoundsMessage',
|
||||
'Permission',
|
||||
'PickedUpMessage',
|
||||
'PickUpMessage',
|
||||
'Player',
|
||||
'PlayerDiedMessage',
|
||||
'PlayerInfo',
|
||||
'PlayerNotFoundError',
|
||||
'PlayerRecord',
|
||||
'PlayerScoredMessage',
|
||||
'playsound',
|
||||
'Plugin',
|
||||
'PluginSubsystem',
|
||||
'PotentialPlugin',
|
||||
'PowerupAcceptMessage',
|
||||
'PowerupMessage',
|
||||
'print_error',
|
||||
'print_exception',
|
||||
'printnodes',
|
||||
'ls_objects',
|
||||
'ls_input_devices',
|
||||
'pushcall',
|
||||
'quit',
|
||||
'rowwidget',
|
||||
'safecolor',
|
||||
'ScoreConfig',
|
||||
'ScoreType',
|
||||
'screenmessage',
|
||||
'scrollwidget',
|
||||
'ServerController',
|
||||
'Session',
|
||||
'SessionNotFoundError',
|
||||
'SessionPlayer',
|
||||
'SessionPlayerNotFoundError',
|
||||
'SessionTeam',
|
||||
'SessionTeamNotFoundError',
|
||||
'set_analytics_screen',
|
||||
'setmusic',
|
||||
'Setting',
|
||||
'ShouldShatterMessage',
|
||||
'show_damage_count',
|
||||
'Sound',
|
||||
'SpecialChar',
|
||||
'StandLocation',
|
||||
'StandMessage',
|
||||
'Stats',
|
||||
'storagename',
|
||||
'Team',
|
||||
'TeamGameActivity',
|
||||
'TeamNotFoundError',
|
||||
'Texture',
|
||||
'textwidget',
|
||||
'ThawMessage',
|
||||
'time',
|
||||
'TimeFormat',
|
||||
'Timer',
|
||||
'timer',
|
||||
'timestring',
|
||||
'TimeType',
|
||||
'uicleanupcheck',
|
||||
'UIController',
|
||||
'UIScale',
|
||||
'UISubsystem',
|
||||
'UNHANDLED',
|
||||
'Vec3',
|
||||
'vec3validate',
|
||||
'verify_object_death',
|
||||
'WeakCall',
|
||||
'Widget',
|
||||
'widget',
|
||||
'WidgetNotFoundError',
|
||||
'Window',
|
||||
]
|
||||
|
||||
|
||||
# Have these things present themselves cleanly as 'ba.Foo'
|
||||
# instead of 'ba._submodule.Foo'
|
||||
def _simplify_module_names() -> None:
|
||||
import os
|
||||
|
||||
# Though pdoc gets confused when we override __module__,
|
||||
# so let's make an exception for it.
|
||||
if os.environ.get('BA_DOCS_GENERATION', '0') != '1':
|
||||
from efro.util import set_canonical_module
|
||||
|
||||
globs = globals()
|
||||
set_canonical_module(
|
||||
module_globals=globs,
|
||||
names=[n for n in globs.keys() if not n.startswith('_')],
|
||||
)
|
||||
|
||||
|
||||
_simplify_module_names()
|
||||
del _simplify_module_names
|
||||
267
dist/ba_data/python/ba/_account.py
vendored
267
dist/ba_data/python/ba/_account.py
vendored
|
|
@ -1,267 +0,0 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Account related functionality."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import time
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import _ba
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any, Optional
|
||||
import ba
|
||||
|
||||
|
||||
class AccountSubsystem:
|
||||
"""Subsystem for account handling in the app.
|
||||
|
||||
Category: App Classes
|
||||
|
||||
Access the single shared instance of this class at 'ba.app.plugins'.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.account_tournament_list: Optional[tuple[int, list[str]]] = None
|
||||
|
||||
# FIXME: should abstract/structure these.
|
||||
self.tournament_info: dict = {}
|
||||
self.league_rank_cache: dict = {}
|
||||
self.last_post_purchase_message_time: Optional[float] = None
|
||||
|
||||
# If we try to run promo-codes due to launch-args/etc we might
|
||||
# not be signed in yet; go ahead and queue them up in that case.
|
||||
self.pending_promo_codes: list[str] = []
|
||||
|
||||
def on_app_launch(self) -> None:
|
||||
"""Called when the app is done bootstrapping."""
|
||||
|
||||
# Auto-sign-in to a local account in a moment if we're set to.
|
||||
def do_auto_sign_in() -> None:
|
||||
if _ba.app.headless_mode or _ba.app.config.get(
|
||||
'Auto Account State') == 'Local':
|
||||
_ba.sign_in('Local')
|
||||
|
||||
_ba.pushcall(do_auto_sign_in)
|
||||
|
||||
def on_app_resume(self) -> None:
|
||||
"""Should be called when the app is resumed."""
|
||||
|
||||
# Mark our cached tourneys as invalid so anyone using them knows
|
||||
# they might be out of date.
|
||||
for entry in list(self.tournament_info.values()):
|
||||
entry['valid'] = False
|
||||
|
||||
def handle_account_gained_tickets(self, count: int) -> None:
|
||||
"""Called when the current account has been awarded tickets.
|
||||
|
||||
(internal)
|
||||
"""
|
||||
from ba._language import Lstr
|
||||
_ba.screenmessage(Lstr(resource='getTicketsWindow.receivedTicketsText',
|
||||
subs=[('${COUNT}', str(count))]),
|
||||
color=(0, 1, 0))
|
||||
_ba.playsound(_ba.getsound('cashRegister'))
|
||||
|
||||
def cache_league_rank_data(self, data: Any) -> None:
|
||||
"""(internal)"""
|
||||
self.league_rank_cache['info'] = copy.deepcopy(data)
|
||||
|
||||
def get_cached_league_rank_data(self) -> Any:
|
||||
"""(internal)"""
|
||||
return self.league_rank_cache.get('info', None)
|
||||
|
||||
def get_league_rank_points(self,
|
||||
data: Optional[dict[str, Any]],
|
||||
subset: str = None) -> int:
|
||||
"""(internal)"""
|
||||
if data is None:
|
||||
return 0
|
||||
|
||||
# If the data contains an achievement total, use that. otherwise calc
|
||||
# locally.
|
||||
if data['at'] is not None:
|
||||
total_ach_value = data['at']
|
||||
else:
|
||||
total_ach_value = 0
|
||||
for ach in _ba.app.ach.achievements:
|
||||
if ach.complete:
|
||||
total_ach_value += ach.power_ranking_value
|
||||
|
||||
trophies_total: int = (data['t0a'] * data['t0am'] +
|
||||
data['t0b'] * data['t0bm'] +
|
||||
data['t1'] * data['t1m'] +
|
||||
data['t2'] * data['t2m'] +
|
||||
data['t3'] * data['t3m'] +
|
||||
data['t4'] * data['t4m'])
|
||||
if subset == 'trophyCount':
|
||||
val: int = (data['t0a'] + data['t0b'] + data['t1'] + data['t2'] +
|
||||
data['t3'] + data['t4'])
|
||||
assert isinstance(val, int)
|
||||
return val
|
||||
if subset == 'trophies':
|
||||
assert isinstance(trophies_total, int)
|
||||
return trophies_total
|
||||
if subset is not None:
|
||||
raise ValueError('invalid subset value: ' + str(subset))
|
||||
|
||||
if data['p']:
|
||||
pro_mult = 1.0 + float(
|
||||
_ba.get_account_misc_read_val('proPowerRankingBoost',
|
||||
0.0)) * 0.01
|
||||
else:
|
||||
pro_mult = 1.0
|
||||
|
||||
# For final value, apply our pro mult and activeness-mult.
|
||||
return int(
|
||||
(total_ach_value + trophies_total) *
|
||||
(data['act'] if data['act'] is not None else 1.0) * pro_mult)
|
||||
|
||||
def cache_tournament_info(self, info: Any) -> None:
|
||||
"""(internal)"""
|
||||
from ba._generated.enums import TimeType, TimeFormat
|
||||
for entry in info:
|
||||
cache_entry = self.tournament_info[entry['tournamentID']] = (
|
||||
copy.deepcopy(entry))
|
||||
|
||||
# Also store the time we received this, so we can adjust
|
||||
# time-remaining values/etc.
|
||||
cache_entry['timeReceived'] = _ba.time(TimeType.REAL,
|
||||
TimeFormat.MILLISECONDS)
|
||||
cache_entry['valid'] = True
|
||||
|
||||
def get_purchased_icons(self) -> list[str]:
|
||||
"""(internal)"""
|
||||
# pylint: disable=cyclic-import
|
||||
from ba import _store
|
||||
if _ba.get_account_state() != 'signed_in':
|
||||
return []
|
||||
icons = []
|
||||
store_items = _store.get_store_items()
|
||||
for item_name, item in list(store_items.items()):
|
||||
if item_name.startswith('icons.') and _ba.get_purchased(item_name):
|
||||
icons.append(item['icon'])
|
||||
return icons
|
||||
|
||||
def ensure_have_account_player_profile(self) -> None:
|
||||
"""
|
||||
Ensure the standard account-named player profile exists;
|
||||
creating if needed.
|
||||
|
||||
(internal)
|
||||
"""
|
||||
# This only applies when we're signed in.
|
||||
if _ba.get_account_state() != 'signed_in':
|
||||
return
|
||||
|
||||
# If the short version of our account name currently cant be
|
||||
# displayed by the game, cancel.
|
||||
if not _ba.have_chars(_ba.get_account_display_string(full=False)):
|
||||
return
|
||||
|
||||
config = _ba.app.config
|
||||
if ('Player Profiles' not in config
|
||||
or '__account__' not in config['Player Profiles']):
|
||||
|
||||
# Create a spaz with a nice default purply color.
|
||||
_ba.add_transaction({
|
||||
'type': 'ADD_PLAYER_PROFILE',
|
||||
'name': '__account__',
|
||||
'profile': {
|
||||
'character': 'Spaz',
|
||||
'color': [0.5, 0.25, 1.0],
|
||||
'highlight': [0.5, 0.25, 1.0]
|
||||
}
|
||||
})
|
||||
_ba.run_transactions()
|
||||
|
||||
def have_pro(self) -> bool:
|
||||
"""Return whether pro is currently unlocked."""
|
||||
|
||||
# Check our tickets-based pro upgrade and our two real-IAP based
|
||||
# upgrades. Also always unlock this stuff in ballistica-core builds.
|
||||
return bool(
|
||||
_ba.get_purchased('upgrades.pro')
|
||||
or _ba.get_purchased('static.pro')
|
||||
or _ba.get_purchased('static.pro_sale')
|
||||
or 'ballistica' + 'core' == _ba.appname())
|
||||
|
||||
def have_pro_options(self) -> bool:
|
||||
"""Return whether pro-options are present.
|
||||
|
||||
This is True for owners of Pro or for old installs
|
||||
before Pro was a requirement for these options.
|
||||
"""
|
||||
|
||||
# We expose pro options if the server tells us to
|
||||
# (which is generally just when we own pro),
|
||||
# or also if we've been grandfathered in or are using ballistica-core
|
||||
# builds.
|
||||
return self.have_pro() or bool(
|
||||
_ba.get_account_misc_read_val_2('proOptionsUnlocked', False)
|
||||
or _ba.app.config.get('lc14292', 0) > 1)
|
||||
|
||||
def show_post_purchase_message(self) -> None:
|
||||
"""(internal)"""
|
||||
from ba._language import Lstr
|
||||
from ba._generated.enums import TimeType
|
||||
cur_time = _ba.time(TimeType.REAL)
|
||||
if (self.last_post_purchase_message_time is None
|
||||
or cur_time - self.last_post_purchase_message_time > 3.0):
|
||||
self.last_post_purchase_message_time = cur_time
|
||||
with _ba.Context('ui'):
|
||||
_ba.screenmessage(Lstr(resource='updatingAccountText',
|
||||
fallback_resource='purchasingText'),
|
||||
color=(0, 1, 0))
|
||||
_ba.playsound(_ba.getsound('click01'))
|
||||
|
||||
def on_account_state_changed(self) -> None:
|
||||
"""(internal)"""
|
||||
from ba._language import Lstr
|
||||
|
||||
# Run any pending promo codes we had queued up while not signed in.
|
||||
if _ba.get_account_state() == 'signed_in' and self.pending_promo_codes:
|
||||
for code in self.pending_promo_codes:
|
||||
_ba.screenmessage(Lstr(resource='submittingPromoCodeText'),
|
||||
color=(0, 1, 0))
|
||||
_ba.add_transaction({
|
||||
'type': 'PROMO_CODE',
|
||||
'expire_time': time.time() + 5,
|
||||
'code': code
|
||||
})
|
||||
_ba.run_transactions()
|
||||
self.pending_promo_codes = []
|
||||
|
||||
def add_pending_promo_code(self, code: str) -> None:
|
||||
"""(internal)"""
|
||||
from ba._language import Lstr
|
||||
from ba._generated.enums import TimeType
|
||||
|
||||
# If we're not signed in, queue up the code to run the next time we
|
||||
# are and issue a warning if we haven't signed in within the next
|
||||
# few seconds.
|
||||
if _ba.get_account_state() != 'signed_in':
|
||||
|
||||
def check_pending_codes() -> None:
|
||||
"""(internal)"""
|
||||
|
||||
# If we're still not signed in and have pending codes,
|
||||
# inform the user that they need to sign in to use them.
|
||||
if self.pending_promo_codes:
|
||||
_ba.screenmessage(Lstr(resource='signInForPromoCodeText'),
|
||||
color=(1, 0, 0))
|
||||
_ba.playsound(_ba.getsound('error'))
|
||||
|
||||
self.pending_promo_codes.append(code)
|
||||
_ba.timer(6.0, check_pending_codes, timetype=TimeType.REAL)
|
||||
return
|
||||
_ba.screenmessage(Lstr(resource='submittingPromoCodeText'),
|
||||
color=(0, 1, 0))
|
||||
_ba.add_transaction({
|
||||
'type': 'PROMO_CODE',
|
||||
'expire_time': time.time() + 5,
|
||||
'code': code
|
||||
})
|
||||
_ba.run_transactions()
|
||||
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()
|
||||
3
dist/ba_data/python/ba/_appmode.py
vendored
3
dist/ba_data/python/ba/_appmode.py
vendored
|
|
@ -1,3 +0,0 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Functionality related to the high level state of the app."""
|
||||
192
dist/ba_data/python/ba/_bootstrap.py
vendored
192
dist/ba_data/python/ba/_bootstrap.py
vendored
|
|
@ -1,192 +0,0 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Bootstrapping."""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from efro.log import setup_logging, LogLevel
|
||||
import _ba
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any
|
||||
from efro.log import LogEntry
|
||||
|
||||
_g_did_bootstrap = False # pylint: disable=invalid-name
|
||||
|
||||
|
||||
def bootstrap() -> None:
|
||||
"""Run bootstrapping logic.
|
||||
|
||||
This is the very first ballistica code that runs (aside from imports).
|
||||
It sets up low level environment bits and creates the app instance.
|
||||
"""
|
||||
|
||||
global _g_did_bootstrap # pylint: disable=global-statement, invalid-name
|
||||
if _g_did_bootstrap:
|
||||
raise RuntimeError('Bootstrap has already been called.')
|
||||
_g_did_bootstrap = True
|
||||
|
||||
# The first thing we do is set up our logging system and feed
|
||||
# Python's stdout/stderr into it. Then we can at least debug problems
|
||||
# on systems where native stdout/stderr is not easily accessible
|
||||
# such as Android.
|
||||
log_handler = setup_logging(
|
||||
log_path=None,
|
||||
level=LogLevel.DEBUG,
|
||||
suppress_non_root_debug=True,
|
||||
log_stdout_stderr=True,
|
||||
cache_size_limit=1024 * 1024,
|
||||
)
|
||||
|
||||
log_handler.add_callback(_on_log)
|
||||
|
||||
env = _ba.env()
|
||||
|
||||
# Give a soft warning if we're being used with a different binary
|
||||
# version than we expect.
|
||||
expected_build = 21005
|
||||
running_build: int = env['build_number']
|
||||
if running_build != expected_build:
|
||||
print(
|
||||
f'WARNING: These script files are meant to be used with'
|
||||
f' Ballistica build {expected_build}.\n'
|
||||
f' You are running build {running_build}.'
|
||||
f' This might cause the app to error or misbehave.',
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
# In bootstrap_monolithic.py we told Python not to handle SIGINT itself
|
||||
# (because that must be done in the main thread). Now we finish the
|
||||
# job by adding our own handler to replace it.
|
||||
|
||||
# Note: I've found we need to set up our C signal handling AFTER
|
||||
# we've told Python to disable its own; otherwise (on Mac at least) it
|
||||
# wipes out our existing C handler.
|
||||
_ba.setup_sigint()
|
||||
|
||||
# Sanity check: we should always be run in UTF-8 mode.
|
||||
if sys.flags.utf8_mode != 1:
|
||||
print(
|
||||
'ERROR: Python\'s UTF-8 mode is not set.'
|
||||
' This will likely result in errors.',
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
debug_build = env['debug_build']
|
||||
|
||||
# We expect dev_mode on in debug builds and off otherwise.
|
||||
if debug_build != sys.flags.dev_mode:
|
||||
print(
|
||||
f'WARNING: Mismatch in debug_build {debug_build}'
|
||||
f' and sys.flags.dev_mode {sys.flags.dev_mode}',
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
# In embedded situations (when we're providing our own Python) let's
|
||||
# also provide our own root certs so ssl works. We can consider overriding
|
||||
# this in particular embedded cases if we can verify that system certs
|
||||
# are working.
|
||||
# (We also allow forcing this via an env var if the user desires)
|
||||
if (
|
||||
_ba.contains_python_dist()
|
||||
or os.environ.get('BA_USE_BUNDLED_ROOT_CERTS') == '1'
|
||||
):
|
||||
import certifi
|
||||
|
||||
# Let both OpenSSL and requests (if present) know to use this.
|
||||
os.environ['SSL_CERT_FILE'] = os.environ[
|
||||
'REQUESTS_CA_BUNDLE'
|
||||
] = certifi.where()
|
||||
|
||||
# On Windows I'm seeing the following error creating asyncio loops in
|
||||
# background threads with the default proactor setup:
|
||||
# ValueError: set_wakeup_fd only works in main thread of the main
|
||||
# interpreter
|
||||
# So let's explicitly request selector loops.
|
||||
# Interestingly this error only started showing up once I moved
|
||||
# Python init to the main thread; previously the various asyncio
|
||||
# bg thread loops were working fine (maybe something caused them
|
||||
# to default to selector in that case?..
|
||||
if sys.platform == 'win32':
|
||||
import asyncio
|
||||
|
||||
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
||||
|
||||
# pylint: disable=c-extension-no-member
|
||||
if not TYPE_CHECKING:
|
||||
import __main__
|
||||
|
||||
# Clear out the standard quit/exit messages since they don't
|
||||
# work in our embedded situation (should revisit this once we're
|
||||
# usable from a standard interpreter).
|
||||
del __main__.__builtins__.quit
|
||||
del __main__.__builtins__.exit
|
||||
|
||||
# Also replace standard interactive help with our simplified
|
||||
# one which is more friendly to cloud/in-game console situations.
|
||||
__main__.__builtins__.help = _CustomHelper()
|
||||
|
||||
# Now spin up our App instance and store it on both _ba and ba.
|
||||
from ba._app import App
|
||||
import ba
|
||||
|
||||
_ba.app = ba.app = App()
|
||||
_ba.app.log_handler = log_handler
|
||||
|
||||
|
||||
class _CustomHelper:
|
||||
"""Replacement 'help' that behaves better for our setup."""
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return 'Type help(object) for help about object.'
|
||||
|
||||
def __call__(self, *args: Any, **kwds: Any) -> Any:
|
||||
# We get an ugly error importing pydoc on our embedded
|
||||
# platforms due to _sysconfigdata_xxx.py not being present
|
||||
# (but then things mostly work). Let's get the ugly error out
|
||||
# of the way explicitly.
|
||||
import sysconfig
|
||||
|
||||
try:
|
||||
# This errors once but seems to run cleanly after, so let's
|
||||
# get the error out of the way.
|
||||
sysconfig.get_path('stdlib')
|
||||
except ModuleNotFoundError:
|
||||
pass
|
||||
|
||||
import pydoc
|
||||
|
||||
# Disable pager and interactive help since neither works well
|
||||
# with our funky multi-threaded setup or in-game/cloud consoles.
|
||||
# Let's just do simple text dumps.
|
||||
pydoc.pager = pydoc.plainpager
|
||||
if not args and not kwds:
|
||||
print(
|
||||
'Interactive help is not available in this environment.\n'
|
||||
'Type help(object) for help about object.'
|
||||
)
|
||||
return None
|
||||
return pydoc.help(*args, **kwds)
|
||||
|
||||
|
||||
def _on_log(entry: LogEntry) -> None:
|
||||
|
||||
# Just forward this along to the engine to display in the in-game console,
|
||||
# in the Android log, etc.
|
||||
_ba.display_log(
|
||||
name=entry.name,
|
||||
level=entry.level.name,
|
||||
message=entry.message,
|
||||
)
|
||||
|
||||
# We also want to feed some logs to the old V1-cloud-log system.
|
||||
# Let's go with anything warning or higher as well as the stdout/stderr
|
||||
# log messages that ba.app.log_handler creates for us.
|
||||
if entry.level.value >= LogLevel.WARNING.value or entry.name in (
|
||||
'stdout',
|
||||
'stderr',
|
||||
):
|
||||
_ba.v1_cloud_log(entry.message)
|
||||
199
dist/ba_data/python/ba/_enums.py
vendored
199
dist/ba_data/python/ba/_enums.py
vendored
|
|
@ -1,199 +0,0 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
"""Enum vals generated by batools.pythonenumsmodule; do not edit by hand."""
|
||||
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class InputType(Enum):
|
||||
"""Types of input a controller can send to the game.
|
||||
|
||||
Category: Enums
|
||||
|
||||
"""
|
||||
UP_DOWN = 2
|
||||
LEFT_RIGHT = 3
|
||||
JUMP_PRESS = 4
|
||||
JUMP_RELEASE = 5
|
||||
PUNCH_PRESS = 6
|
||||
PUNCH_RELEASE = 7
|
||||
BOMB_PRESS = 8
|
||||
BOMB_RELEASE = 9
|
||||
PICK_UP_PRESS = 10
|
||||
PICK_UP_RELEASE = 11
|
||||
RUN = 12
|
||||
FLY_PRESS = 13
|
||||
FLY_RELEASE = 14
|
||||
START_PRESS = 15
|
||||
START_RELEASE = 16
|
||||
HOLD_POSITION_PRESS = 17
|
||||
HOLD_POSITION_RELEASE = 18
|
||||
LEFT_PRESS = 19
|
||||
LEFT_RELEASE = 20
|
||||
RIGHT_PRESS = 21
|
||||
RIGHT_RELEASE = 22
|
||||
UP_PRESS = 23
|
||||
UP_RELEASE = 24
|
||||
DOWN_PRESS = 25
|
||||
DOWN_RELEASE = 26
|
||||
|
||||
|
||||
class UIScale(Enum):
|
||||
"""The overall scale the UI is being rendered for. Note that this is
|
||||
independent of pixel resolution. For example, a phone and a desktop PC
|
||||
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.
|
||||
|
||||
'medium' is used for devices such as tablets, TVs, or VR headsets.
|
||||
This mode strikes a balance between clean readability and amount of
|
||||
content visible.
|
||||
|
||||
'small' is used primarily for phones or other small devices where
|
||||
content needs to be presented as large and clear in order to remain
|
||||
readable from an average distance.
|
||||
"""
|
||||
LARGE = 0
|
||||
MEDIUM = 1
|
||||
SMALL = 2
|
||||
|
||||
|
||||
class TimeType(Enum):
|
||||
"""Specifies the type of time for various operations to target/use.
|
||||
|
||||
Category: Enums
|
||||
|
||||
'sim' time is the local simulation time for an activity or session.
|
||||
It can proceed at different rates depending on game speed, stops
|
||||
for pauses, etc.
|
||||
|
||||
'base' is the baseline time for an activity or session. It proceeds
|
||||
consistently regardless of game speed or pausing, but may stop during
|
||||
occurrences such as network outages.
|
||||
|
||||
'real' time is mostly based on clock time, with a few exceptions. It may
|
||||
not advance while the app is backgrounded for instance. (the engine
|
||||
attempts to prevent single large time jumps from occurring)
|
||||
"""
|
||||
SIM = 0
|
||||
BASE = 1
|
||||
REAL = 2
|
||||
|
||||
|
||||
class TimeFormat(Enum):
|
||||
"""Specifies the format time values are provided in.
|
||||
|
||||
Category: Enums
|
||||
"""
|
||||
SECONDS = 0
|
||||
MILLISECONDS = 1
|
||||
|
||||
|
||||
class Permission(Enum):
|
||||
"""Permissions that can be requested from the OS.
|
||||
|
||||
Category: Enums
|
||||
"""
|
||||
STORAGE = 0
|
||||
|
||||
|
||||
class SpecialChar(Enum):
|
||||
"""Special characters the game can print.
|
||||
|
||||
Category: Enums
|
||||
"""
|
||||
DOWN_ARROW = 0
|
||||
UP_ARROW = 1
|
||||
LEFT_ARROW = 2
|
||||
RIGHT_ARROW = 3
|
||||
TOP_BUTTON = 4
|
||||
LEFT_BUTTON = 5
|
||||
RIGHT_BUTTON = 6
|
||||
BOTTOM_BUTTON = 7
|
||||
DELETE = 8
|
||||
SHIFT = 9
|
||||
BACK = 10
|
||||
LOGO_FLAT = 11
|
||||
REWIND_BUTTON = 12
|
||||
PLAY_PAUSE_BUTTON = 13
|
||||
FAST_FORWARD_BUTTON = 14
|
||||
DPAD_CENTER_BUTTON = 15
|
||||
OUYA_BUTTON_O = 16
|
||||
OUYA_BUTTON_U = 17
|
||||
OUYA_BUTTON_Y = 18
|
||||
OUYA_BUTTON_A = 19
|
||||
OUYA_LOGO = 20
|
||||
LOGO = 21
|
||||
TICKET = 22
|
||||
GOOGLE_PLAY_GAMES_LOGO = 23
|
||||
GAME_CENTER_LOGO = 24
|
||||
DICE_BUTTON1 = 25
|
||||
DICE_BUTTON2 = 26
|
||||
DICE_BUTTON3 = 27
|
||||
DICE_BUTTON4 = 28
|
||||
GAME_CIRCLE_LOGO = 29
|
||||
PARTY_ICON = 30
|
||||
TEST_ACCOUNT = 31
|
||||
TICKET_BACKING = 32
|
||||
TROPHY1 = 33
|
||||
TROPHY2 = 34
|
||||
TROPHY3 = 35
|
||||
TROPHY0A = 36
|
||||
TROPHY0B = 37
|
||||
TROPHY4 = 38
|
||||
LOCAL_ACCOUNT = 39
|
||||
ALIBABA_LOGO = 40
|
||||
FLAG_UNITED_STATES = 41
|
||||
FLAG_MEXICO = 42
|
||||
FLAG_GERMANY = 43
|
||||
FLAG_BRAZIL = 44
|
||||
FLAG_RUSSIA = 45
|
||||
FLAG_CHINA = 46
|
||||
FLAG_UNITED_KINGDOM = 47
|
||||
FLAG_CANADA = 48
|
||||
FLAG_INDIA = 49
|
||||
FLAG_JAPAN = 50
|
||||
FLAG_FRANCE = 51
|
||||
FLAG_INDONESIA = 52
|
||||
FLAG_ITALY = 53
|
||||
FLAG_SOUTH_KOREA = 54
|
||||
FLAG_NETHERLANDS = 55
|
||||
FEDORA = 56
|
||||
HAL = 57
|
||||
CROWN = 58
|
||||
YIN_YANG = 59
|
||||
EYE_BALL = 60
|
||||
SKULL = 61
|
||||
HEART = 62
|
||||
DRAGON = 63
|
||||
HELMET = 64
|
||||
MUSHROOM = 65
|
||||
NINJA_STAR = 66
|
||||
VIKING_HELMET = 67
|
||||
MOON = 68
|
||||
SPIDER = 69
|
||||
FIREBALL = 70
|
||||
FLAG_UNITED_ARAB_EMIRATES = 71
|
||||
FLAG_QATAR = 72
|
||||
FLAG_EGYPT = 73
|
||||
FLAG_KUWAIT = 74
|
||||
FLAG_ALGERIA = 75
|
||||
FLAG_SAUDI_ARABIA = 76
|
||||
FLAG_MALAYSIA = 77
|
||||
FLAG_CZECH_REPUBLIC = 78
|
||||
FLAG_AUSTRALIA = 79
|
||||
FLAG_SINGAPORE = 80
|
||||
OCULUS_LOGO = 81
|
||||
STEAM_LOGO = 82
|
||||
NVIDIA_LOGO = 83
|
||||
FLAG_IRAN = 84
|
||||
FLAG_POLAND = 85
|
||||
FLAG_ARGENTINA = 86
|
||||
FLAG_PHILIPPINES = 87
|
||||
FLAG_CHILE = 88
|
||||
MIKIROG = 89
|
||||
V2_LOGO = 90
|
||||
523
dist/ba_data/python/ba/_hooks.py
vendored
523
dist/ba_data/python/ba/_hooks.py
vendored
|
|
@ -1,523 +0,0 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Snippets of code for use by the internal layer.
|
||||
|
||||
History: originally the engine would dynamically compile/eval various Python
|
||||
code from within C++ code, but the major downside there was that none of it
|
||||
was type-checked so if names or arguments changed it would go unnoticed
|
||||
until it broke at runtime. By instead defining such snippets here and then
|
||||
capturing references to them all at launch it is possible to allow linting
|
||||
and type-checking magic to happen and most issues will be caught immediately.
|
||||
"""
|
||||
# (most of these are self-explanatory)
|
||||
# pylint: disable=missing-function-docstring
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import _ba
|
||||
from ba import _internal
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Sequence, Any
|
||||
import ba
|
||||
|
||||
|
||||
def finish_bootstrapping() -> None:
|
||||
"""Do final bootstrapping related bits."""
|
||||
assert _ba.in_logic_thread()
|
||||
|
||||
# Ok, low level bootstrapping is done; time to get Python stuff started.
|
||||
_ba.app.on_bootstrapping_completed()
|
||||
|
||||
|
||||
def reset_to_main_menu() -> None:
|
||||
"""Reset the game to the main menu gracefully."""
|
||||
_ba.app.return_to_main_menu_session_gracefully()
|
||||
|
||||
|
||||
def set_config_fullscreen_on() -> None:
|
||||
"""The app has set fullscreen on its own and we should note it."""
|
||||
_ba.app.config['Fullscreen'] = True
|
||||
_ba.app.config.commit()
|
||||
|
||||
|
||||
def set_config_fullscreen_off() -> None:
|
||||
"""The app has set fullscreen on its own and we should note it."""
|
||||
_ba.app.config['Fullscreen'] = False
|
||||
_ba.app.config.commit()
|
||||
|
||||
|
||||
def not_signed_in_screen_message() -> None:
|
||||
from ba._language import Lstr
|
||||
|
||||
_ba.screenmessage(Lstr(resource='notSignedInErrorText'))
|
||||
|
||||
|
||||
def connecting_to_party_message() -> None:
|
||||
from ba._language import Lstr
|
||||
|
||||
_ba.screenmessage(
|
||||
Lstr(resource='internal.connectingToPartyText'), color=(1, 1, 1)
|
||||
)
|
||||
|
||||
|
||||
def rejecting_invite_already_in_party_message() -> None:
|
||||
from ba._language import Lstr
|
||||
|
||||
_ba.screenmessage(
|
||||
Lstr(resource='internal.rejectingInviteAlreadyInPartyText'),
|
||||
color=(1, 0.5, 0),
|
||||
)
|
||||
|
||||
|
||||
def connection_failed_message() -> None:
|
||||
from ba._language import Lstr
|
||||
|
||||
_ba.screenmessage(
|
||||
Lstr(resource='internal.connectionFailedText'), color=(1, 0.5, 0)
|
||||
)
|
||||
|
||||
|
||||
def temporarily_unavailable_message() -> None:
|
||||
from ba._language import Lstr
|
||||
|
||||
_ba.playsound(_ba.getsound('error'))
|
||||
_ba.screenmessage(
|
||||
Lstr(resource='getTicketsWindow.unavailableTemporarilyText'),
|
||||
color=(1, 0, 0),
|
||||
)
|
||||
|
||||
|
||||
def in_progress_message() -> None:
|
||||
from ba._language import Lstr
|
||||
|
||||
_ba.playsound(_ba.getsound('error'))
|
||||
_ba.screenmessage(
|
||||
Lstr(resource='getTicketsWindow.inProgressText'), color=(1, 0, 0)
|
||||
)
|
||||
|
||||
|
||||
def error_message() -> None:
|
||||
from ba._language import Lstr
|
||||
|
||||
_ba.playsound(_ba.getsound('error'))
|
||||
_ba.screenmessage(Lstr(resource='errorText'), color=(1, 0, 0))
|
||||
|
||||
|
||||
def purchase_not_valid_error() -> None:
|
||||
from ba._language import Lstr
|
||||
|
||||
_ba.playsound(_ba.getsound('error'))
|
||||
_ba.screenmessage(
|
||||
Lstr(
|
||||
resource='store.purchaseNotValidError',
|
||||
subs=[('${EMAIL}', 'support@froemling.net')],
|
||||
),
|
||||
color=(1, 0, 0),
|
||||
)
|
||||
|
||||
|
||||
def purchase_already_in_progress_error() -> None:
|
||||
from ba._language import Lstr
|
||||
|
||||
_ba.playsound(_ba.getsound('error'))
|
||||
_ba.screenmessage(
|
||||
Lstr(resource='store.purchaseAlreadyInProgressText'), color=(1, 0, 0)
|
||||
)
|
||||
|
||||
|
||||
def gear_vr_controller_warning() -> None:
|
||||
from ba._language import Lstr
|
||||
|
||||
_ba.playsound(_ba.getsound('error'))
|
||||
_ba.screenmessage(
|
||||
Lstr(resource='usesExternalControllerText'), color=(1, 0, 0)
|
||||
)
|
||||
|
||||
|
||||
def uuid_str() -> str:
|
||||
import uuid
|
||||
|
||||
return str(uuid.uuid4())
|
||||
|
||||
|
||||
def orientation_reset_cb_message() -> None:
|
||||
from ba._language import Lstr
|
||||
|
||||
_ba.screenmessage(
|
||||
Lstr(resource='internal.vrOrientationResetCardboardText'),
|
||||
color=(0, 1, 0),
|
||||
)
|
||||
|
||||
|
||||
def orientation_reset_message() -> None:
|
||||
from ba._language import Lstr
|
||||
|
||||
_ba.screenmessage(
|
||||
Lstr(resource='internal.vrOrientationResetText'), color=(0, 1, 0)
|
||||
)
|
||||
|
||||
|
||||
def on_app_pause() -> None:
|
||||
_ba.app.on_app_pause()
|
||||
|
||||
|
||||
def on_app_resume() -> None:
|
||||
_ba.app.on_app_resume()
|
||||
|
||||
|
||||
def launch_main_menu_session() -> None:
|
||||
from bastd.mainmenu import MainMenuSession
|
||||
|
||||
_ba.new_host_session(MainMenuSession)
|
||||
|
||||
|
||||
def language_test_toggle() -> None:
|
||||
_ba.app.lang.setlanguage(
|
||||
'Gibberish' if _ba.app.lang.language == 'English' else 'English'
|
||||
)
|
||||
|
||||
|
||||
def award_in_control_achievement() -> None:
|
||||
_ba.app.ach.award_local_achievement('In Control')
|
||||
|
||||
|
||||
def award_dual_wielding_achievement() -> None:
|
||||
_ba.app.ach.award_local_achievement('Dual Wielding')
|
||||
|
||||
|
||||
def play_gong_sound() -> None:
|
||||
_ba.playsound(_ba.getsound('gong'))
|
||||
|
||||
|
||||
def launch_coop_game(name: str) -> None:
|
||||
_ba.app.launch_coop_game(name)
|
||||
|
||||
|
||||
def purchases_restored_message() -> None:
|
||||
from ba._language import Lstr
|
||||
|
||||
_ba.screenmessage(
|
||||
Lstr(resource='getTicketsWindow.purchasesRestoredText'), color=(0, 1, 0)
|
||||
)
|
||||
|
||||
|
||||
def dismiss_wii_remotes_window() -> None:
|
||||
call = _ba.app.ui.dismiss_wii_remotes_window_call
|
||||
if call is not None:
|
||||
call()
|
||||
|
||||
|
||||
def unavailable_message() -> None:
|
||||
from ba._language import Lstr
|
||||
|
||||
_ba.screenmessage(
|
||||
Lstr(resource='getTicketsWindow.unavailableText'), color=(1, 0, 0)
|
||||
)
|
||||
|
||||
|
||||
def submit_analytics_counts(sval: str) -> None:
|
||||
_internal.add_transaction({'type': 'ANALYTICS_COUNTS', 'values': sval})
|
||||
_internal.run_transactions()
|
||||
|
||||
|
||||
def set_last_ad_network(sval: str) -> None:
|
||||
import time
|
||||
|
||||
_ba.app.ads.last_ad_network = sval
|
||||
_ba.app.ads.last_ad_network_set_time = time.time()
|
||||
|
||||
|
||||
def no_game_circle_message() -> None:
|
||||
from ba._language import Lstr
|
||||
|
||||
_ba.screenmessage(Lstr(resource='noGameCircleText'), color=(1, 0, 0))
|
||||
|
||||
|
||||
def google_play_purchases_not_available_message() -> None:
|
||||
from ba._language import Lstr
|
||||
|
||||
_ba.screenmessage(
|
||||
Lstr(resource='googlePlayPurchasesNotAvailableText'), color=(1, 0, 0)
|
||||
)
|
||||
|
||||
|
||||
def google_play_services_not_available_message() -> None:
|
||||
from ba._language import Lstr
|
||||
|
||||
_ba.screenmessage(
|
||||
Lstr(resource='googlePlayServicesNotAvailableText'), color=(1, 0, 0)
|
||||
)
|
||||
|
||||
|
||||
def empty_call() -> None:
|
||||
pass
|
||||
|
||||
|
||||
def level_icon_press() -> None:
|
||||
print('LEVEL ICON PRESSED')
|
||||
|
||||
|
||||
def trophy_icon_press() -> None:
|
||||
print('TROPHY ICON PRESSED')
|
||||
|
||||
|
||||
def coin_icon_press() -> None:
|
||||
print('COIN ICON PRESSED')
|
||||
|
||||
|
||||
def ticket_icon_press() -> None:
|
||||
from bastd.ui.resourcetypeinfo import ResourceTypeInfoWindow
|
||||
|
||||
ResourceTypeInfoWindow(
|
||||
origin_widget=_ba.get_special_widget('tickets_info_button')
|
||||
)
|
||||
|
||||
|
||||
def back_button_press() -> None:
|
||||
_ba.back_press()
|
||||
|
||||
|
||||
def friends_button_press() -> None:
|
||||
print('FRIEND BUTTON PRESSED!')
|
||||
|
||||
|
||||
def print_trace() -> None:
|
||||
import traceback
|
||||
|
||||
print('Python Traceback (most recent call last):')
|
||||
traceback.print_stack()
|
||||
|
||||
|
||||
def toggle_fullscreen() -> None:
|
||||
cfg = _ba.app.config
|
||||
cfg['Fullscreen'] = not cfg.resolve('Fullscreen')
|
||||
cfg.apply_and_commit()
|
||||
|
||||
|
||||
def party_icon_activate(origin: Sequence[float]) -> None:
|
||||
import weakref
|
||||
from bastd.ui.party import PartyWindow
|
||||
|
||||
app = _ba.app
|
||||
_ba.playsound(_ba.getsound('swish'))
|
||||
|
||||
# If it exists, dismiss it; otherwise make a new one.
|
||||
if app.ui.party_window is not None and app.ui.party_window() is not None:
|
||||
app.ui.party_window().close()
|
||||
else:
|
||||
app.ui.party_window = weakref.ref(PartyWindow(origin=origin))
|
||||
|
||||
|
||||
def read_config() -> None:
|
||||
_ba.app.read_config()
|
||||
|
||||
|
||||
def ui_remote_press() -> None:
|
||||
"""Handle a press by a remote device that is only usable for nav."""
|
||||
from ba._language import Lstr
|
||||
|
||||
# Can be called without a context; need a context for getsound.
|
||||
with _ba.Context('ui'):
|
||||
_ba.screenmessage(
|
||||
Lstr(resource='internal.controllerForMenusOnlyText'),
|
||||
color=(1, 0, 0),
|
||||
)
|
||||
_ba.playsound(_ba.getsound('error'))
|
||||
|
||||
|
||||
def quit_window() -> None:
|
||||
from bastd.ui.confirm import QuitWindow
|
||||
|
||||
QuitWindow()
|
||||
|
||||
|
||||
def remove_in_game_ads_message() -> None:
|
||||
_ba.app.ads.do_remove_in_game_ads_message()
|
||||
|
||||
|
||||
def telnet_access_request() -> None:
|
||||
from bastd.ui.telnet import TelnetAccessRequestWindow
|
||||
|
||||
TelnetAccessRequestWindow()
|
||||
|
||||
|
||||
def do_quit() -> None:
|
||||
_ba.quit()
|
||||
|
||||
|
||||
def shutdown() -> None:
|
||||
_ba.app.on_app_shutdown()
|
||||
|
||||
|
||||
def gc_disable() -> None:
|
||||
import gc
|
||||
|
||||
gc.disable()
|
||||
|
||||
|
||||
def device_menu_press(device: ba.InputDevice) -> None:
|
||||
from bastd.ui.mainmenu import MainMenuWindow
|
||||
|
||||
in_main_menu = _ba.app.ui.has_main_menu_window()
|
||||
if not in_main_menu:
|
||||
_ba.set_ui_input_device(device)
|
||||
_ba.playsound(_ba.getsound('swish'))
|
||||
_ba.app.ui.set_main_menu_window(MainMenuWindow().get_root_widget())
|
||||
|
||||
|
||||
def show_url_window(address: str) -> None:
|
||||
from bastd.ui.url import ShowURLWindow
|
||||
|
||||
ShowURLWindow(address)
|
||||
|
||||
|
||||
def party_invite_revoke(invite_id: str) -> None:
|
||||
# If there's a confirm window up for joining this particular
|
||||
# invite, kill it.
|
||||
for winref in _ba.app.invite_confirm_windows:
|
||||
win = winref()
|
||||
if win is not None and win.ew_party_invite_id == invite_id:
|
||||
_ba.containerwidget(
|
||||
edit=win.get_root_widget(), transition='out_right'
|
||||
)
|
||||
|
||||
import custom_hooks as chooks
|
||||
def filter_chat_message(msg: str, client_id: int) -> str | None:
|
||||
|
||||
"""Intercept/filter chat messages.
|
||||
|
||||
Called for all chat messages while hosting.
|
||||
Messages originating from the host will have clientID -1.
|
||||
Should filter and return the string to be displayed, or return None
|
||||
to ignore the message.
|
||||
"""
|
||||
|
||||
return chooks.filter_chat_message(msg,client_id)
|
||||
|
||||
def on_client_request(ip):
|
||||
chooks.on_join_request(ip)
|
||||
|
||||
def kick_vote_started(by:str,to:str) -> None:
|
||||
"""
|
||||
get account ids of who started kick vote for whom ,
|
||||
do what ever u want logging to files , whatever.
|
||||
"""
|
||||
chooks.kick_vote_started(by,to)
|
||||
|
||||
def on_kicked(account_id:str) -> None:
|
||||
chooks.on_kicked(account_id)
|
||||
|
||||
def on_kick_vote_end() -> None:
|
||||
chooks.on_kick_vote_end()
|
||||
|
||||
def on_player_join(pb_id:str)-> None:
|
||||
# not integrated yet
|
||||
pass
|
||||
|
||||
def on_player_leave(pb_id:str)-> None:
|
||||
# not integrated yet
|
||||
pass
|
||||
|
||||
|
||||
def local_chat_message(msg: str) -> None:
|
||||
if (
|
||||
_ba.app.ui.party_window is not None
|
||||
and _ba.app.ui.party_window() is not None
|
||||
):
|
||||
_ba.app.ui.party_window().on_chat_message(msg)
|
||||
|
||||
|
||||
def get_player_icon(sessionplayer: ba.SessionPlayer) -> dict[str, Any]:
|
||||
info = sessionplayer.get_icon_info()
|
||||
return {
|
||||
'texture': _ba.gettexture(info['texture']),
|
||||
'tint_texture': _ba.gettexture(info['tint_texture']),
|
||||
'tint_color': info['tint_color'],
|
||||
'tint2_color': info['tint2_color'],
|
||||
}
|
||||
|
||||
|
||||
def hash_strings(inputs: list[str]) -> str:
|
||||
"""Hash provided strings into a short output string."""
|
||||
import hashlib
|
||||
|
||||
sha = hashlib.sha1()
|
||||
for inp in inputs:
|
||||
sha.update(inp.encode())
|
||||
|
||||
return sha.hexdigest()
|
||||
|
||||
|
||||
def have_account_v2_credentials() -> bool:
|
||||
"""Do we have primary account-v2 credentials set?"""
|
||||
return _ba.app.accounts_v2.have_primary_credentials()
|
||||
|
||||
|
||||
def implicit_sign_in(
|
||||
login_type_str: str, login_id: str, display_name: str
|
||||
) -> None:
|
||||
"""An implicit login happened."""
|
||||
from bacommon.login import LoginType
|
||||
|
||||
_ba.app.accounts_v2.on_implicit_sign_in(
|
||||
login_type=LoginType(login_type_str),
|
||||
login_id=login_id,
|
||||
display_name=display_name,
|
||||
)
|
||||
|
||||
|
||||
def implicit_sign_out(login_type_str: str) -> None:
|
||||
"""An implicit logout happened."""
|
||||
from bacommon.login import LoginType
|
||||
|
||||
_ba.app.accounts_v2.on_implicit_sign_out(
|
||||
login_type=LoginType(login_type_str)
|
||||
)
|
||||
|
||||
|
||||
def login_adapter_get_sign_in_token_response(
|
||||
login_type_str: str, attempt_id_str: str, result_str: str
|
||||
) -> None:
|
||||
"""Login adapter do-sign-in completed."""
|
||||
from bacommon.login import LoginType
|
||||
from ba._login import LoginAdapterNative
|
||||
|
||||
login_type = LoginType(login_type_str)
|
||||
attempt_id = int(attempt_id_str)
|
||||
result = None if result_str == '' else result_str
|
||||
with _ba.Context('ui'):
|
||||
adapter = _ba.app.accounts_v2.login_adapters[login_type]
|
||||
assert isinstance(adapter, LoginAdapterNative)
|
||||
adapter.on_sign_in_complete(attempt_id=attempt_id, result=result)
|
||||
|
||||
|
||||
def show_client_too_old_error() -> None:
|
||||
"""Called at launch if the server tells us we're too old to talk to it."""
|
||||
from ba._language import Lstr
|
||||
|
||||
# If you are using an old build of the app and would like to stop
|
||||
# seeing this error at launch, do:
|
||||
# ba.app.config['SuppressClientTooOldErrorForBuild'] = ba.app.build_number
|
||||
# ba.app.config.commit()
|
||||
# Note that you will have to do that again later if you update to
|
||||
# a newer build.
|
||||
if (
|
||||
_ba.app.config.get('SuppressClientTooOldErrorForBuild')
|
||||
== _ba.app.build_number
|
||||
):
|
||||
return
|
||||
|
||||
_ba.playsound(_ba.getsound('error'))
|
||||
_ba.screenmessage(
|
||||
Lstr(
|
||||
translate=(
|
||||
'serverResponses',
|
||||
'Server functionality is no longer supported'
|
||||
' in this version of the game;\n'
|
||||
'Please update to a newer version.',
|
||||
)
|
||||
),
|
||||
color=(1, 0, 0),
|
||||
)
|
||||
381
dist/ba_data/python/ba/_internal.py
vendored
381
dist/ba_data/python/ba/_internal.py
vendored
|
|
@ -1,381 +0,0 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""A soft wrapper around _bainternal.
|
||||
|
||||
This allows code to use _bainternal functionality and get warnings
|
||||
or fallbacks in some cases instead of hard errors. Code that absolutely
|
||||
relies on the presence of _bainternal can just use that module directly.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
try:
|
||||
# noinspection PyUnresolvedReferences
|
||||
import _bainternal
|
||||
|
||||
HAVE_INTERNAL = True
|
||||
except ImportError:
|
||||
HAVE_INTERNAL = False
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Callable, Any
|
||||
|
||||
|
||||
# Code that will function without _bainternal but which should be updated
|
||||
# to account for its absence should call this to draw attention to itself.
|
||||
def _no_bainternal_warning() -> None:
|
||||
import logging
|
||||
|
||||
logging.warning('INTERNAL CALL RUN WITHOUT INTERNAL PRESENT.')
|
||||
|
||||
|
||||
# Code that won't work without _bainternal should raise these errors.
|
||||
def _no_bainternal_error() -> RuntimeError:
|
||||
raise RuntimeError('_bainternal is not present')
|
||||
|
||||
|
||||
def get_v2_fleet() -> str:
|
||||
"""(internal)"""
|
||||
if HAVE_INTERNAL:
|
||||
return _bainternal.get_v2_fleet()
|
||||
raise _no_bainternal_error()
|
||||
|
||||
|
||||
def get_master_server_address(source: int = -1, version: int = 1) -> str:
|
||||
"""(internal)
|
||||
|
||||
Return the address of the master server.
|
||||
"""
|
||||
if HAVE_INTERNAL:
|
||||
return _bainternal.get_master_server_address(
|
||||
source=source, version=version
|
||||
)
|
||||
raise _no_bainternal_error()
|
||||
|
||||
|
||||
def is_blessed() -> bool:
|
||||
"""(internal)"""
|
||||
if HAVE_INTERNAL:
|
||||
return _bainternal.is_blessed()
|
||||
|
||||
# Harmless to always just say no here.
|
||||
return False
|
||||
|
||||
|
||||
def get_news_show() -> str:
|
||||
"""(internal)"""
|
||||
if HAVE_INTERNAL:
|
||||
return _bainternal.get_news_show()
|
||||
raise _no_bainternal_error()
|
||||
|
||||
|
||||
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.
|
||||
"""
|
||||
if HAVE_INTERNAL:
|
||||
return _bainternal.game_service_has_leaderboard(
|
||||
game=game, config=config
|
||||
)
|
||||
# Harmless to always just say no here.
|
||||
return False
|
||||
|
||||
|
||||
def report_achievement(achievement: str, pass_to_account: bool = True) -> None:
|
||||
"""(internal)"""
|
||||
if HAVE_INTERNAL:
|
||||
_bainternal.report_achievement(
|
||||
achievement=achievement, pass_to_account=pass_to_account
|
||||
)
|
||||
return
|
||||
|
||||
# Need to see if this actually still works as expected.. warning for now.
|
||||
_no_bainternal_warning()
|
||||
|
||||
|
||||
# noinspection PyUnresolvedReferences
|
||||
def submit_score(
|
||||
game: str,
|
||||
config: str,
|
||||
name: Any,
|
||||
score: int | None,
|
||||
callback: Callable,
|
||||
order: str = 'increasing',
|
||||
tournament_id: str | None = None,
|
||||
score_type: str = 'points',
|
||||
campaign: str | None = None,
|
||||
level: str | None = None,
|
||||
) -> None:
|
||||
"""(internal)
|
||||
|
||||
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.
|
||||
"""
|
||||
if HAVE_INTERNAL:
|
||||
_bainternal.submit_score(
|
||||
game=game,
|
||||
config=config,
|
||||
name=name,
|
||||
score=score,
|
||||
callback=callback,
|
||||
order=order,
|
||||
tournament_id=tournament_id,
|
||||
score_type=score_type,
|
||||
campaign=campaign,
|
||||
level=level,
|
||||
)
|
||||
return
|
||||
# This technically breaks since callback will never be called/etc.
|
||||
raise _no_bainternal_error()
|
||||
|
||||
|
||||
def tournament_query(
|
||||
callback: Callable[[dict | None], None], args: dict
|
||||
) -> None:
|
||||
"""(internal)"""
|
||||
if HAVE_INTERNAL:
|
||||
_bainternal.tournament_query(callback=callback, args=args)
|
||||
return
|
||||
|
||||
# This technically breaks since callback will never be called/etc.
|
||||
raise _no_bainternal_error()
|
||||
|
||||
|
||||
def power_ranking_query(callback: Callable, season: Any = None) -> None:
|
||||
"""(internal)"""
|
||||
if HAVE_INTERNAL:
|
||||
_bainternal.power_ranking_query(callback=callback, season=season)
|
||||
return
|
||||
|
||||
# This technically breaks since callback will never be called/etc.
|
||||
raise _no_bainternal_error()
|
||||
|
||||
|
||||
def restore_purchases() -> None:
|
||||
"""(internal)"""
|
||||
if HAVE_INTERNAL:
|
||||
_bainternal.restore_purchases()
|
||||
return
|
||||
|
||||
# This shouldn't break anything but should try to avoid calling it.
|
||||
_no_bainternal_warning()
|
||||
|
||||
|
||||
def purchase(item: str) -> None:
|
||||
"""(internal)"""
|
||||
|
||||
if HAVE_INTERNAL:
|
||||
_bainternal.purchase(item)
|
||||
return
|
||||
|
||||
# This won't break messily but won't function as intended.
|
||||
_no_bainternal_warning()
|
||||
|
||||
|
||||
def get_purchases_state() -> int:
|
||||
"""(internal)"""
|
||||
|
||||
if HAVE_INTERNAL:
|
||||
return _bainternal.get_purchases_state()
|
||||
|
||||
# This won't function correctly without internal.
|
||||
raise _no_bainternal_error()
|
||||
|
||||
|
||||
def get_purchased(item: str) -> bool:
|
||||
"""(internal)"""
|
||||
|
||||
if HAVE_INTERNAL:
|
||||
return _bainternal.get_purchased(item)
|
||||
|
||||
# Without internal we can just assume no purchases.
|
||||
return False
|
||||
|
||||
|
||||
def get_price(item: str) -> str | None:
|
||||
"""(internal)"""
|
||||
|
||||
if HAVE_INTERNAL:
|
||||
return _bainternal.get_price(item)
|
||||
|
||||
# Without internal we can just assume no prices.
|
||||
return None
|
||||
|
||||
|
||||
def in_game_purchase(item: str, price: int) -> None:
|
||||
"""(internal)"""
|
||||
|
||||
if HAVE_INTERNAL:
|
||||
_bainternal.in_game_purchase(item=item, price=price)
|
||||
return
|
||||
|
||||
# Without internal this doesn't function as expected.
|
||||
raise _no_bainternal_error()
|
||||
|
||||
|
||||
# noinspection PyUnresolvedReferences
|
||||
def add_transaction(
|
||||
transaction: dict, callback: Callable | None = None
|
||||
) -> None:
|
||||
"""(internal)"""
|
||||
if HAVE_INTERNAL:
|
||||
_bainternal.add_transaction(transaction=transaction, callback=callback)
|
||||
return
|
||||
|
||||
# This won't function correctly without internal (callback never called).
|
||||
raise _no_bainternal_error()
|
||||
|
||||
|
||||
def reset_achievements() -> None:
|
||||
"""(internal)"""
|
||||
if HAVE_INTERNAL:
|
||||
_bainternal.reset_achievements()
|
||||
return
|
||||
|
||||
# Technically doesnt break but won't do anything.
|
||||
_no_bainternal_warning()
|
||||
|
||||
|
||||
def get_public_login_id() -> str | None:
|
||||
"""(internal)"""
|
||||
|
||||
if HAVE_INTERNAL:
|
||||
return _bainternal.get_public_login_id()
|
||||
|
||||
# Harmless to return nothing in this case.
|
||||
return None
|
||||
|
||||
|
||||
def have_outstanding_transactions() -> bool:
|
||||
"""(internal)"""
|
||||
|
||||
if HAVE_INTERNAL:
|
||||
return _bainternal.have_outstanding_transactions()
|
||||
|
||||
# Harmless to return False here.
|
||||
return False
|
||||
|
||||
|
||||
def run_transactions() -> None:
|
||||
"""(internal)"""
|
||||
if HAVE_INTERNAL:
|
||||
_bainternal.run_transactions()
|
||||
|
||||
# Harmless no-op in this case.
|
||||
|
||||
|
||||
def get_v1_account_misc_read_val(name: str, default_value: Any) -> Any:
|
||||
"""(internal)"""
|
||||
if HAVE_INTERNAL:
|
||||
return _bainternal.get_v1_account_misc_read_val(
|
||||
name=name, default_value=default_value
|
||||
)
|
||||
raise _no_bainternal_error()
|
||||
|
||||
|
||||
def get_v1_account_misc_read_val_2(name: str, default_value: Any) -> Any:
|
||||
"""(internal)"""
|
||||
if HAVE_INTERNAL:
|
||||
return _bainternal.get_v1_account_misc_read_val_2(
|
||||
name=name, default_value=default_value
|
||||
)
|
||||
raise _no_bainternal_error()
|
||||
|
||||
|
||||
def get_v1_account_misc_val(name: str, default_value: Any) -> Any:
|
||||
"""(internal)"""
|
||||
if HAVE_INTERNAL:
|
||||
return _bainternal.get_v1_account_misc_val(
|
||||
name=name, default_value=default_value
|
||||
)
|
||||
raise _no_bainternal_error()
|
||||
|
||||
|
||||
def get_v1_account_ticket_count() -> int:
|
||||
"""(internal)
|
||||
|
||||
Returns the number of tickets for the current account.
|
||||
"""
|
||||
|
||||
if HAVE_INTERNAL:
|
||||
return _bainternal.get_v1_account_ticket_count()
|
||||
return 0
|
||||
|
||||
|
||||
def get_v1_account_state_num() -> int:
|
||||
"""(internal)"""
|
||||
if HAVE_INTERNAL:
|
||||
return _bainternal.get_v1_account_state_num()
|
||||
return 0
|
||||
|
||||
|
||||
def get_v1_account_state() -> str:
|
||||
"""(internal)"""
|
||||
if HAVE_INTERNAL:
|
||||
return _bainternal.get_v1_account_state()
|
||||
|
||||
# Without internal present just consider ourself always signed out.
|
||||
return 'signed_out'
|
||||
|
||||
|
||||
def get_v1_account_display_string(full: bool = True) -> str:
|
||||
"""(internal)"""
|
||||
if HAVE_INTERNAL:
|
||||
return _bainternal.get_v1_account_display_string(full=full)
|
||||
raise _no_bainternal_error()
|
||||
|
||||
|
||||
def get_v1_account_type() -> str:
|
||||
"""(internal)"""
|
||||
if HAVE_INTERNAL:
|
||||
return _bainternal.get_v1_account_type()
|
||||
raise _no_bainternal_error()
|
||||
|
||||
|
||||
def get_v1_account_name() -> str:
|
||||
"""(internal)"""
|
||||
if HAVE_INTERNAL:
|
||||
return _bainternal.get_v1_account_name()
|
||||
raise _no_bainternal_error()
|
||||
|
||||
|
||||
def sign_out_v1(v2_embedded: bool = False) -> None:
|
||||
"""(internal)
|
||||
|
||||
Category: General Utility Functions
|
||||
"""
|
||||
if HAVE_INTERNAL:
|
||||
_bainternal.sign_out_v1(v2_embedded=v2_embedded)
|
||||
return
|
||||
raise _no_bainternal_error()
|
||||
|
||||
|
||||
def sign_in_v1(account_type: str) -> None:
|
||||
"""(internal)
|
||||
|
||||
Category: General Utility Functions
|
||||
"""
|
||||
if HAVE_INTERNAL:
|
||||
_bainternal.sign_in_v1(account_type=account_type)
|
||||
return
|
||||
raise _no_bainternal_error()
|
||||
|
||||
|
||||
def mark_config_dirty() -> None:
|
||||
"""(internal)
|
||||
|
||||
Category: General Utility Functions
|
||||
"""
|
||||
if HAVE_INTERNAL:
|
||||
_bainternal.mark_config_dirty()
|
||||
return
|
||||
|
||||
# Note to self - need to fix config writing to not rely on
|
||||
# internal lib.
|
||||
_no_bainternal_warning()
|
||||
186
dist/ba_data/python/ba/_netutils.py
vendored
186
dist/ba_data/python/ba/_netutils.py
vendored
|
|
@ -1,186 +0,0 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Networking related functionality."""
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import threading
|
||||
import weakref
|
||||
from enum import Enum
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import _ba
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any, Dict, Union, Callable, Optional
|
||||
import socket
|
||||
import ba
|
||||
ServerCallbackType = Callable[[Union[None, Dict[str, Any]]], None]
|
||||
|
||||
|
||||
def get_ip_address_type(addr: str) -> socket.AddressFamily:
|
||||
"""Return socket.AF_INET6 or socket.AF_INET4 for the provided address."""
|
||||
import socket
|
||||
socket_type = None
|
||||
|
||||
# First try it as an ipv4 address.
|
||||
try:
|
||||
socket.inet_pton(socket.AF_INET, addr)
|
||||
socket_type = socket.AF_INET
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
# Hmm apparently not ipv4; try ipv6.
|
||||
if socket_type is None:
|
||||
try:
|
||||
socket.inet_pton(socket.AF_INET6, addr)
|
||||
socket_type = socket.AF_INET6
|
||||
except OSError:
|
||||
pass
|
||||
if socket_type is None:
|
||||
raise ValueError(f'addr seems to be neither v4 or v6: {addr}')
|
||||
return socket_type
|
||||
|
||||
|
||||
class ServerResponseType(Enum):
|
||||
"""How to interpret responses from the server."""
|
||||
JSON = 0
|
||||
|
||||
|
||||
class ServerCallThread(threading.Thread):
|
||||
"""Thread to communicate with the master server."""
|
||||
|
||||
def __init__(self, request: str, request_type: str,
|
||||
data: Optional[Dict[str, Any]],
|
||||
callback: Optional[ServerCallbackType],
|
||||
response_type: ServerResponseType):
|
||||
super().__init__()
|
||||
self._request = request
|
||||
self._request_type = request_type
|
||||
if not isinstance(response_type, ServerResponseType):
|
||||
raise TypeError(f'Invalid response type: {response_type}')
|
||||
self._response_type = response_type
|
||||
self._data = {} if data is None else copy.deepcopy(data)
|
||||
self._callback: Optional[ServerCallbackType] = callback
|
||||
self._context = _ba.Context('current')
|
||||
|
||||
# Save and restore the context we were created from.
|
||||
activity = _ba.getactivity(doraise=False)
|
||||
self._activity = weakref.ref(
|
||||
activity) if activity is not None else None
|
||||
|
||||
def _run_callback(self, arg: Union[None, Dict[str, Any]]) -> None:
|
||||
# If we were created in an activity context and that activity has
|
||||
# since died, do nothing.
|
||||
# FIXME: Should we just be using a ContextCall instead of doing
|
||||
# this check manually?
|
||||
if self._activity is not None:
|
||||
activity = self._activity()
|
||||
if activity is None or activity.expired:
|
||||
return
|
||||
|
||||
# Technically we could do the same check for session contexts,
|
||||
# but not gonna worry about it for now.
|
||||
assert self._context is not None
|
||||
assert self._callback is not None
|
||||
with self._context:
|
||||
self._callback(arg)
|
||||
|
||||
def run(self) -> None:
|
||||
# pylint: disable=too-many-branches
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
import json
|
||||
import http.client
|
||||
from ba import _general
|
||||
try:
|
||||
self._data = _general.utf8_all(self._data)
|
||||
_ba.set_thread_name('BA_ServerCallThread')
|
||||
parse = urllib.parse
|
||||
if self._request_type == 'get':
|
||||
response = urllib.request.urlopen(
|
||||
urllib.request.Request(
|
||||
(_ba.get_master_server_address() + '/' +
|
||||
self._request + '?' + parse.urlencode(self._data)),
|
||||
None, {'User-Agent': _ba.app.user_agent_string}))
|
||||
elif self._request_type == 'post':
|
||||
response = urllib.request.urlopen(
|
||||
urllib.request.Request(
|
||||
_ba.get_master_server_address() + '/' + self._request,
|
||||
parse.urlencode(self._data).encode(),
|
||||
{'User-Agent': _ba.app.user_agent_string}))
|
||||
else:
|
||||
raise TypeError('Invalid request_type: ' + self._request_type)
|
||||
|
||||
# If html request failed.
|
||||
if response.getcode() != 200:
|
||||
response_data = None
|
||||
elif self._response_type == ServerResponseType.JSON:
|
||||
raw_data = response.read()
|
||||
|
||||
# Empty string here means something failed server side.
|
||||
if raw_data == b'':
|
||||
response_data = None
|
||||
else:
|
||||
# Json.loads requires str in python < 3.6.
|
||||
raw_data_s = raw_data.decode()
|
||||
response_data = json.loads(raw_data_s)
|
||||
else:
|
||||
raise TypeError(f'invalid responsetype: {self._response_type}')
|
||||
|
||||
except Exception as exc:
|
||||
import errno
|
||||
do_print = False
|
||||
response_data = None
|
||||
|
||||
# Ignore common network errors; note unexpected ones.
|
||||
if isinstance(
|
||||
exc,
|
||||
(urllib.error.URLError, ConnectionError,
|
||||
http.client.IncompleteRead, http.client.BadStatusLine)):
|
||||
pass
|
||||
elif isinstance(exc, OSError):
|
||||
if exc.errno == 10051: # Windows unreachable network error.
|
||||
pass
|
||||
elif exc.errno in [
|
||||
errno.ETIMEDOUT, errno.EHOSTUNREACH, errno.ENETUNREACH
|
||||
]:
|
||||
pass
|
||||
else:
|
||||
do_print = True
|
||||
elif (self._response_type == ServerResponseType.JSON
|
||||
and isinstance(exc, json.decoder.JSONDecodeError)):
|
||||
pass
|
||||
else:
|
||||
do_print = True
|
||||
|
||||
if do_print:
|
||||
# Any other error here is unexpected,
|
||||
# so let's make a note of it,
|
||||
print(f'Error in ServerCallThread'
|
||||
f' (response-type={self._response_type},'
|
||||
f' response-data={response_data}):')
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
if self._callback is not None:
|
||||
_ba.pushcall(_general.Call(self._run_callback, response_data),
|
||||
from_other_thread=True)
|
||||
|
||||
|
||||
def master_server_get(
|
||||
request: str,
|
||||
data: Dict[str, Any],
|
||||
callback: Optional[ServerCallbackType] = None,
|
||||
response_type: ServerResponseType = ServerResponseType.JSON) -> None:
|
||||
"""Make a call to the master server via a http GET."""
|
||||
ServerCallThread(request, 'get', data, callback, response_type).start()
|
||||
|
||||
|
||||
def master_server_post(
|
||||
request: str,
|
||||
data: Dict[str, Any],
|
||||
callback: Optional[ServerCallbackType] = None,
|
||||
response_type: ServerResponseType = ServerResponseType.JSON) -> None:
|
||||
"""Make a call to the master server via a http POST."""
|
||||
ServerCallThread(request, 'post', data, callback, response_type).start()
|
||||
241
dist/ba_data/python/ba/_plugin.py
vendored
241
dist/ba_data/python/ba/_plugin.py
vendored
|
|
@ -1,241 +0,0 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Plugin related functionality."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
from dataclasses import dataclass
|
||||
|
||||
import _ba
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import ba
|
||||
|
||||
|
||||
class PluginSubsystem:
|
||||
"""Subsystem for plugin handling in the app.
|
||||
|
||||
Category: **App Classes**
|
||||
|
||||
Access the single shared instance of this class at `ba.app.plugins`.
|
||||
"""
|
||||
|
||||
AUTO_ENABLE_NEW_PLUGINS_CONFIG_KEY = 'Auto Enable New Plugins'
|
||||
AUTO_ENABLE_NEW_PLUGINS_DEFAULT = True
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.potential_plugins: list[ba.PotentialPlugin] = []
|
||||
self.active_plugins: dict[str, ba.Plugin] = {}
|
||||
|
||||
def on_meta_scan_complete(self) -> None:
|
||||
"""Should be called when meta-scanning is complete."""
|
||||
from ba._language import Lstr
|
||||
|
||||
plugs = _ba.app.plugins
|
||||
config_changed = False
|
||||
found_new = False
|
||||
plugstates: dict[str, dict] = _ba.app.config.setdefault('Plugins', {})
|
||||
assert isinstance(plugstates, dict)
|
||||
|
||||
results = _ba.app.meta.scanresults
|
||||
assert results is not None
|
||||
|
||||
# Create a potential-plugin for each class we found in the scan.
|
||||
for class_path in results.exports_of_class(Plugin):
|
||||
plugs.potential_plugins.append(
|
||||
PotentialPlugin(
|
||||
display_name=Lstr(value=class_path),
|
||||
class_path=class_path,
|
||||
available=True,
|
||||
)
|
||||
)
|
||||
if (
|
||||
_ba.app.config.get(
|
||||
self.AUTO_ENABLE_NEW_PLUGINS_CONFIG_KEY,
|
||||
self.AUTO_ENABLE_NEW_PLUGINS_DEFAULT,
|
||||
)
|
||||
is True
|
||||
):
|
||||
if class_path not in plugstates:
|
||||
# Go ahead and enable new plugins by default, but we'll
|
||||
# inform the user that they need to restart to pick them up.
|
||||
# they can also disable them in settings so they never load.
|
||||
plugstates[class_path] = {'enabled': True}
|
||||
config_changed = True
|
||||
found_new = True
|
||||
|
||||
plugs.potential_plugins.sort(key=lambda p: p.class_path)
|
||||
|
||||
# Note: these days we complete meta-scan and immediately activate
|
||||
# plugins, so we don't need the message about 'restart to activate'
|
||||
# anymore.
|
||||
if found_new and bool(False):
|
||||
_ba.screenmessage(
|
||||
Lstr(resource='pluginsDetectedText'), color=(0, 1, 0)
|
||||
)
|
||||
_ba.playsound(_ba.getsound('ding'))
|
||||
|
||||
if config_changed:
|
||||
_ba.app.config.commit()
|
||||
|
||||
def on_app_running(self) -> None:
|
||||
"""Should be called when the app reaches the running state."""
|
||||
# Load up our plugins and go ahead and call their on_app_running calls.
|
||||
self.load_plugins()
|
||||
for plugin in self.active_plugins.values():
|
||||
try:
|
||||
plugin.on_app_running()
|
||||
except Exception:
|
||||
from ba import _error
|
||||
|
||||
_error.print_exception('Error in plugin on_app_running()')
|
||||
|
||||
def on_app_pause(self) -> None:
|
||||
"""Called when the app goes to a suspended state."""
|
||||
for plugin in self.active_plugins.values():
|
||||
try:
|
||||
plugin.on_app_pause()
|
||||
except Exception:
|
||||
from ba import _error
|
||||
|
||||
_error.print_exception('Error in plugin on_app_pause()')
|
||||
|
||||
def on_app_resume(self) -> None:
|
||||
"""Run when the app resumes from a suspended state."""
|
||||
for plugin in self.active_plugins.values():
|
||||
try:
|
||||
plugin.on_app_resume()
|
||||
except Exception:
|
||||
from ba import _error
|
||||
|
||||
_error.print_exception('Error in plugin on_app_resume()')
|
||||
|
||||
def on_app_shutdown(self) -> None:
|
||||
"""Called when the app is being closed."""
|
||||
for plugin in self.active_plugins.values():
|
||||
try:
|
||||
plugin.on_app_shutdown()
|
||||
except Exception:
|
||||
from ba import _error
|
||||
|
||||
_error.print_exception('Error in plugin on_app_shutdown()')
|
||||
|
||||
def load_plugins(self) -> None:
|
||||
"""(internal)"""
|
||||
from ba._general import getclass
|
||||
from ba._language import Lstr
|
||||
|
||||
# Note: the plugins we load is purely based on what's enabled
|
||||
# in the app config. Its not our job to look at meta stuff here.
|
||||
plugstates: dict[str, dict] = _ba.app.config.get('Plugins', {})
|
||||
assert isinstance(plugstates, dict)
|
||||
plugkeys: list[str] = sorted(
|
||||
key for key, val in plugstates.items() if val.get('enabled', False)
|
||||
)
|
||||
disappeared_plugs: set[str] = set()
|
||||
for plugkey in plugkeys:
|
||||
try:
|
||||
cls = getclass(plugkey, Plugin)
|
||||
except ModuleNotFoundError:
|
||||
disappeared_plugs.add(plugkey)
|
||||
continue
|
||||
except Exception as exc:
|
||||
_ba.playsound(_ba.getsound('error'))
|
||||
_ba.screenmessage(
|
||||
Lstr(
|
||||
resource='pluginClassLoadErrorText',
|
||||
subs=[('${PLUGIN}', plugkey), ('${ERROR}', str(exc))],
|
||||
),
|
||||
color=(1, 0, 0),
|
||||
)
|
||||
logging.exception("Error loading plugin class '%s'", plugkey)
|
||||
continue
|
||||
try:
|
||||
plugin = cls()
|
||||
assert plugkey not in self.active_plugins
|
||||
self.active_plugins[plugkey] = plugin
|
||||
except Exception as exc:
|
||||
from ba import _error
|
||||
|
||||
_ba.playsound(_ba.getsound('error'))
|
||||
_ba.screenmessage(
|
||||
Lstr(
|
||||
resource='pluginInitErrorText',
|
||||
subs=[('${PLUGIN}', plugkey), ('${ERROR}', str(exc))],
|
||||
),
|
||||
color=(1, 0, 0),
|
||||
)
|
||||
_error.print_exception(f"Error initing plugin: '{plugkey}'.")
|
||||
|
||||
# If plugins disappeared, let the user know gently and remove them
|
||||
# from the config so we'll again let the user know if they later
|
||||
# reappear. This makes it much smoother to switch between users
|
||||
# or workspaces.
|
||||
if disappeared_plugs:
|
||||
_ba.playsound(_ba.getsound('shieldDown'))
|
||||
_ba.screenmessage(
|
||||
Lstr(
|
||||
resource='pluginsRemovedText',
|
||||
subs=[('${NUM}', str(len(disappeared_plugs)))],
|
||||
),
|
||||
color=(1, 1, 0),
|
||||
)
|
||||
plugnames = ', '.join(disappeared_plugs)
|
||||
logging.info(
|
||||
'%d plugin(s) no longer found: %s.',
|
||||
len(disappeared_plugs),
|
||||
plugnames,
|
||||
)
|
||||
for goneplug in disappeared_plugs:
|
||||
del _ba.app.config['Plugins'][goneplug]
|
||||
_ba.app.config.commit()
|
||||
|
||||
|
||||
@dataclass
|
||||
class PotentialPlugin:
|
||||
"""Represents a ba.Plugin which can potentially be loaded.
|
||||
|
||||
Category: **App Classes**
|
||||
|
||||
These generally represent plugins which were detected by the
|
||||
meta-tag scan. However they may also represent plugins which
|
||||
were previously set to be loaded but which were unable to be
|
||||
for some reason. In that case, 'available' will be set to False.
|
||||
"""
|
||||
|
||||
display_name: ba.Lstr
|
||||
class_path: str
|
||||
available: bool
|
||||
|
||||
|
||||
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 activate.
|
||||
Active 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:
|
||||
"""Called when the app reaches the running state."""
|
||||
|
||||
def on_app_pause(self) -> None:
|
||||
"""Called after pausing game activity."""
|
||||
|
||||
def on_app_resume(self) -> None:
|
||||
"""Called after the game continues."""
|
||||
|
||||
def on_app_shutdown(self) -> None:
|
||||
"""Called before closing the application."""
|
||||
|
||||
def has_settings_ui(self) -> bool:
|
||||
"""Called to ask if we have settings UI we can show."""
|
||||
return False
|
||||
|
||||
def show_settings_ui(self, source_widget: ba.Widget | None) -> None:
|
||||
"""Called to show our settings UI."""
|
||||
498
dist/ba_data/python/ba/_store.py
vendored
498
dist/ba_data/python/ba/_store.py
vendored
|
|
@ -1,498 +0,0 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Store related functionality for classic mode."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import _ba
|
||||
from ba import _internal
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any
|
||||
import ba
|
||||
|
||||
|
||||
def get_store_item(item: str) -> dict[str, Any]:
|
||||
"""(internal)"""
|
||||
return get_store_items()[item]
|
||||
|
||||
|
||||
def get_store_item_name_translated(item_name: str) -> ba.Lstr:
|
||||
"""Return a ba.Lstr for a store item name."""
|
||||
# pylint: disable=cyclic-import
|
||||
from ba import _language
|
||||
from ba import _map
|
||||
|
||||
item_info = get_store_item(item_name)
|
||||
if item_name.startswith('characters.'):
|
||||
return _language.Lstr(
|
||||
translate=('characterNames', item_info['character'])
|
||||
)
|
||||
if item_name in ['merch']:
|
||||
return _language.Lstr(resource='merchText')
|
||||
if item_name in ['upgrades.pro', 'pro']:
|
||||
return _language.Lstr(
|
||||
resource='store.bombSquadProNameText',
|
||||
subs=[('${APP_NAME}', _language.Lstr(resource='titleText'))],
|
||||
)
|
||||
if item_name.startswith('maps.'):
|
||||
map_type: type[ba.Map] = item_info['map_type']
|
||||
return _map.get_map_display_string(map_type.name)
|
||||
if item_name.startswith('games.'):
|
||||
gametype: type[ba.GameActivity] = item_info['gametype']
|
||||
return gametype.get_display_string()
|
||||
if item_name.startswith('icons.'):
|
||||
return _language.Lstr(resource='editProfileWindow.iconText')
|
||||
raise ValueError('unrecognized item: ' + item_name)
|
||||
|
||||
|
||||
def get_store_item_display_size(item_name: str) -> tuple[float, float]:
|
||||
"""(internal)"""
|
||||
if item_name.startswith('characters.'):
|
||||
return 340 * 0.6, 430 * 0.6
|
||||
if item_name in ['pro', 'upgrades.pro', 'merch']:
|
||||
from ba._generated.enums import UIScale
|
||||
|
||||
return 650 * 0.9, 500 * (
|
||||
0.72
|
||||
if (
|
||||
_ba.app.config.get('Merch Link')
|
||||
and _ba.app.ui.uiscale is UIScale.SMALL
|
||||
)
|
||||
else 0.85
|
||||
)
|
||||
if item_name.startswith('maps.'):
|
||||
return 510 * 0.6, 450 * 0.6
|
||||
if item_name.startswith('icons.'):
|
||||
return 265 * 0.6, 250 * 0.6
|
||||
return 450 * 0.6, 450 * 0.6
|
||||
|
||||
|
||||
def get_store_items() -> dict[str, dict]:
|
||||
"""Returns info about purchasable items.
|
||||
|
||||
(internal)
|
||||
"""
|
||||
# pylint: disable=cyclic-import
|
||||
from ba._generated.enums import SpecialChar
|
||||
from bastd import maps
|
||||
|
||||
if _ba.app.store_items is None:
|
||||
from bastd.game import ninjafight
|
||||
from bastd.game import meteorshower
|
||||
from bastd.game import targetpractice
|
||||
from bastd.game import easteregghunt
|
||||
|
||||
# IMPORTANT - need to keep this synced with the master server.
|
||||
# (doing so manually for now)
|
||||
_ba.app.store_items = {
|
||||
'characters.kronk': {'character': 'Kronk'},
|
||||
'characters.zoe': {'character': 'Zoe'},
|
||||
'characters.jackmorgan': {'character': 'Jack Morgan'},
|
||||
'characters.mel': {'character': 'Mel'},
|
||||
'characters.snakeshadow': {'character': 'Snake Shadow'},
|
||||
'characters.bones': {'character': 'Bones'},
|
||||
'characters.bernard': {
|
||||
'character': 'Bernard',
|
||||
'highlight': (0.6, 0.5, 0.8),
|
||||
},
|
||||
'characters.pixie': {'character': 'Pixel'},
|
||||
'characters.wizard': {'character': 'Grumbledorf'},
|
||||
'characters.frosty': {'character': 'Frosty'},
|
||||
'characters.pascal': {'character': 'Pascal'},
|
||||
'characters.cyborg': {'character': 'B-9000'},
|
||||
'characters.agent': {'character': 'Agent Johnson'},
|
||||
'characters.taobaomascot': {'character': 'Taobao Mascot'},
|
||||
'characters.santa': {'character': 'Santa Claus'},
|
||||
'characters.bunny': {'character': 'Easter Bunny'},
|
||||
'merch': {},
|
||||
'pro': {},
|
||||
'maps.lake_frigid': {'map_type': maps.LakeFrigid},
|
||||
'games.ninja_fight': {
|
||||
'gametype': ninjafight.NinjaFightGame,
|
||||
'previewTex': 'courtyardPreview',
|
||||
},
|
||||
'games.meteor_shower': {
|
||||
'gametype': meteorshower.MeteorShowerGame,
|
||||
'previewTex': 'rampagePreview',
|
||||
},
|
||||
'games.target_practice': {
|
||||
'gametype': targetpractice.TargetPracticeGame,
|
||||
'previewTex': 'doomShroomPreview',
|
||||
},
|
||||
'games.easter_egg_hunt': {
|
||||
'gametype': easteregghunt.EasterEggHuntGame,
|
||||
'previewTex': 'towerDPreview',
|
||||
},
|
||||
'icons.flag_us': {
|
||||
'icon': _ba.charstr(SpecialChar.FLAG_UNITED_STATES)
|
||||
},
|
||||
'icons.flag_mexico': {'icon': _ba.charstr(SpecialChar.FLAG_MEXICO)},
|
||||
'icons.flag_germany': {
|
||||
'icon': _ba.charstr(SpecialChar.FLAG_GERMANY)
|
||||
},
|
||||
'icons.flag_brazil': {'icon': _ba.charstr(SpecialChar.FLAG_BRAZIL)},
|
||||
'icons.flag_russia': {'icon': _ba.charstr(SpecialChar.FLAG_RUSSIA)},
|
||||
'icons.flag_china': {'icon': _ba.charstr(SpecialChar.FLAG_CHINA)},
|
||||
'icons.flag_uk': {
|
||||
'icon': _ba.charstr(SpecialChar.FLAG_UNITED_KINGDOM)
|
||||
},
|
||||
'icons.flag_canada': {'icon': _ba.charstr(SpecialChar.FLAG_CANADA)},
|
||||
'icons.flag_india': {'icon': _ba.charstr(SpecialChar.FLAG_INDIA)},
|
||||
'icons.flag_japan': {'icon': _ba.charstr(SpecialChar.FLAG_JAPAN)},
|
||||
'icons.flag_france': {'icon': _ba.charstr(SpecialChar.FLAG_FRANCE)},
|
||||
'icons.flag_indonesia': {
|
||||
'icon': _ba.charstr(SpecialChar.FLAG_INDONESIA)
|
||||
},
|
||||
'icons.flag_italy': {'icon': _ba.charstr(SpecialChar.FLAG_ITALY)},
|
||||
'icons.flag_south_korea': {
|
||||
'icon': _ba.charstr(SpecialChar.FLAG_SOUTH_KOREA)
|
||||
},
|
||||
'icons.flag_netherlands': {
|
||||
'icon': _ba.charstr(SpecialChar.FLAG_NETHERLANDS)
|
||||
},
|
||||
'icons.flag_uae': {
|
||||
'icon': _ba.charstr(SpecialChar.FLAG_UNITED_ARAB_EMIRATES)
|
||||
},
|
||||
'icons.flag_qatar': {'icon': _ba.charstr(SpecialChar.FLAG_QATAR)},
|
||||
'icons.flag_egypt': {'icon': _ba.charstr(SpecialChar.FLAG_EGYPT)},
|
||||
'icons.flag_kuwait': {'icon': _ba.charstr(SpecialChar.FLAG_KUWAIT)},
|
||||
'icons.flag_algeria': {
|
||||
'icon': _ba.charstr(SpecialChar.FLAG_ALGERIA)
|
||||
},
|
||||
'icons.flag_saudi_arabia': {
|
||||
'icon': _ba.charstr(SpecialChar.FLAG_SAUDI_ARABIA)
|
||||
},
|
||||
'icons.flag_malaysia': {
|
||||
'icon': _ba.charstr(SpecialChar.FLAG_MALAYSIA)
|
||||
},
|
||||
'icons.flag_czech_republic': {
|
||||
'icon': _ba.charstr(SpecialChar.FLAG_CZECH_REPUBLIC)
|
||||
},
|
||||
'icons.flag_australia': {
|
||||
'icon': _ba.charstr(SpecialChar.FLAG_AUSTRALIA)
|
||||
},
|
||||
'icons.flag_singapore': {
|
||||
'icon': _ba.charstr(SpecialChar.FLAG_SINGAPORE)
|
||||
},
|
||||
'icons.flag_iran': {'icon': _ba.charstr(SpecialChar.FLAG_IRAN)},
|
||||
'icons.flag_poland': {'icon': _ba.charstr(SpecialChar.FLAG_POLAND)},
|
||||
'icons.flag_argentina': {
|
||||
'icon': _ba.charstr(SpecialChar.FLAG_ARGENTINA)
|
||||
},
|
||||
'icons.flag_philippines': {
|
||||
'icon': _ba.charstr(SpecialChar.FLAG_PHILIPPINES)
|
||||
},
|
||||
'icons.flag_chile': {'icon': _ba.charstr(SpecialChar.FLAG_CHILE)},
|
||||
'icons.fedora': {'icon': _ba.charstr(SpecialChar.FEDORA)},
|
||||
'icons.hal': {'icon': _ba.charstr(SpecialChar.HAL)},
|
||||
'icons.crown': {'icon': _ba.charstr(SpecialChar.CROWN)},
|
||||
'icons.yinyang': {'icon': _ba.charstr(SpecialChar.YIN_YANG)},
|
||||
'icons.eyeball': {'icon': _ba.charstr(SpecialChar.EYE_BALL)},
|
||||
'icons.skull': {'icon': _ba.charstr(SpecialChar.SKULL)},
|
||||
'icons.heart': {'icon': _ba.charstr(SpecialChar.HEART)},
|
||||
'icons.dragon': {'icon': _ba.charstr(SpecialChar.DRAGON)},
|
||||
'icons.helmet': {'icon': _ba.charstr(SpecialChar.HELMET)},
|
||||
'icons.mushroom': {'icon': _ba.charstr(SpecialChar.MUSHROOM)},
|
||||
'icons.ninja_star': {'icon': _ba.charstr(SpecialChar.NINJA_STAR)},
|
||||
'icons.viking_helmet': {
|
||||
'icon': _ba.charstr(SpecialChar.VIKING_HELMET)
|
||||
},
|
||||
'icons.moon': {'icon': _ba.charstr(SpecialChar.MOON)},
|
||||
'icons.spider': {'icon': _ba.charstr(SpecialChar.SPIDER)},
|
||||
'icons.fireball': {'icon': _ba.charstr(SpecialChar.FIREBALL)},
|
||||
'icons.mikirog': {'icon': _ba.charstr(SpecialChar.MIKIROG)},
|
||||
}
|
||||
return _ba.app.store_items
|
||||
|
||||
|
||||
def get_store_layout() -> dict[str, list[dict[str, Any]]]:
|
||||
"""Return what's available in the store at a given time.
|
||||
|
||||
Categorized by tab and by section."""
|
||||
if _ba.app.store_layout is None:
|
||||
_ba.app.store_layout = {
|
||||
'characters': [{'items': []}],
|
||||
'extras': [{'items': ['pro']}],
|
||||
'maps': [{'items': ['maps.lake_frigid']}],
|
||||
'minigames': [],
|
||||
'icons': [
|
||||
{
|
||||
'items': [
|
||||
'icons.mushroom',
|
||||
'icons.heart',
|
||||
'icons.eyeball',
|
||||
'icons.yinyang',
|
||||
'icons.hal',
|
||||
'icons.flag_us',
|
||||
'icons.flag_mexico',
|
||||
'icons.flag_germany',
|
||||
'icons.flag_brazil',
|
||||
'icons.flag_russia',
|
||||
'icons.flag_china',
|
||||
'icons.flag_uk',
|
||||
'icons.flag_canada',
|
||||
'icons.flag_india',
|
||||
'icons.flag_japan',
|
||||
'icons.flag_france',
|
||||
'icons.flag_indonesia',
|
||||
'icons.flag_italy',
|
||||
'icons.flag_south_korea',
|
||||
'icons.flag_netherlands',
|
||||
'icons.flag_uae',
|
||||
'icons.flag_qatar',
|
||||
'icons.flag_egypt',
|
||||
'icons.flag_kuwait',
|
||||
'icons.flag_algeria',
|
||||
'icons.flag_saudi_arabia',
|
||||
'icons.flag_malaysia',
|
||||
'icons.flag_czech_republic',
|
||||
'icons.flag_australia',
|
||||
'icons.flag_singapore',
|
||||
'icons.flag_iran',
|
||||
'icons.flag_poland',
|
||||
'icons.flag_argentina',
|
||||
'icons.flag_philippines',
|
||||
'icons.flag_chile',
|
||||
'icons.moon',
|
||||
'icons.fedora',
|
||||
'icons.spider',
|
||||
'icons.ninja_star',
|
||||
'icons.skull',
|
||||
'icons.dragon',
|
||||
'icons.viking_helmet',
|
||||
'icons.fireball',
|
||||
'icons.helmet',
|
||||
'icons.crown',
|
||||
]
|
||||
}
|
||||
],
|
||||
}
|
||||
store_layout = _ba.app.store_layout
|
||||
store_layout['characters'] = [
|
||||
{
|
||||
'items': [
|
||||
'characters.kronk',
|
||||
'characters.zoe',
|
||||
'characters.jackmorgan',
|
||||
'characters.mel',
|
||||
'characters.snakeshadow',
|
||||
'characters.bones',
|
||||
'characters.bernard',
|
||||
'characters.agent',
|
||||
'characters.frosty',
|
||||
'characters.pascal',
|
||||
'characters.pixie',
|
||||
]
|
||||
}
|
||||
]
|
||||
store_layout['minigames'] = [
|
||||
{
|
||||
'items': [
|
||||
'games.ninja_fight',
|
||||
'games.meteor_shower',
|
||||
'games.target_practice',
|
||||
]
|
||||
}
|
||||
]
|
||||
if _internal.get_v1_account_misc_read_val('xmas', False):
|
||||
store_layout['characters'][0]['items'].append('characters.santa')
|
||||
store_layout['characters'][0]['items'].append('characters.wizard')
|
||||
store_layout['characters'][0]['items'].append('characters.cyborg')
|
||||
if _internal.get_v1_account_misc_read_val('easter', False):
|
||||
store_layout['characters'].append(
|
||||
{'title': 'store.holidaySpecialText', 'items': ['characters.bunny']}
|
||||
)
|
||||
store_layout['minigames'].append(
|
||||
{
|
||||
'title': 'store.holidaySpecialText',
|
||||
'items': ['games.easter_egg_hunt'],
|
||||
}
|
||||
)
|
||||
|
||||
# This will cause merch to show only if the master-server has
|
||||
# given us a link (which means merch is available in our region).
|
||||
store_layout['extras'] = [{'items': ['pro']}]
|
||||
if _ba.app.config.get('Merch Link'):
|
||||
store_layout['extras'][0]['items'].append('merch')
|
||||
return store_layout
|
||||
|
||||
|
||||
def get_clean_price(price_string: str) -> str:
|
||||
"""(internal)"""
|
||||
|
||||
# I'm not brave enough to try and do any numerical
|
||||
# manipulation on formatted price strings, but lets do a
|
||||
# few swap-outs to tidy things up a bit.
|
||||
psubs = {
|
||||
'$2.99': '$3.00',
|
||||
'$4.99': '$5.00',
|
||||
'$9.99': '$10.00',
|
||||
'$19.99': '$20.00',
|
||||
'$49.99': '$50.00',
|
||||
}
|
||||
return psubs.get(price_string, price_string)
|
||||
|
||||
|
||||
def get_available_purchase_count(tab: str | None = None) -> int:
|
||||
"""(internal)"""
|
||||
try:
|
||||
if _internal.get_v1_account_state() != 'signed_in':
|
||||
return 0
|
||||
count = 0
|
||||
our_tickets = _internal.get_v1_account_ticket_count()
|
||||
store_data = get_store_layout()
|
||||
if tab is not None:
|
||||
tabs = [(tab, store_data[tab])]
|
||||
else:
|
||||
tabs = list(store_data.items())
|
||||
for tab_name, tabval in tabs:
|
||||
if tab_name == 'icons':
|
||||
continue # too many of these; don't show..
|
||||
count = _calc_count_for_tab(tabval, our_tickets, count)
|
||||
return count
|
||||
except Exception:
|
||||
from ba import _error
|
||||
|
||||
_error.print_exception('error calcing available purchases')
|
||||
return 0
|
||||
|
||||
|
||||
def _calc_count_for_tab(
|
||||
tabval: list[dict[str, Any]], our_tickets: int, count: int
|
||||
) -> int:
|
||||
for section in tabval:
|
||||
for item in section['items']:
|
||||
ticket_cost = _internal.get_v1_account_misc_read_val(
|
||||
'price.' + item, None
|
||||
)
|
||||
if ticket_cost is not None:
|
||||
if our_tickets >= ticket_cost and not _internal.get_purchased(
|
||||
item
|
||||
):
|
||||
count += 1
|
||||
return count
|
||||
|
||||
|
||||
def get_available_sale_time(tab: str) -> int | None:
|
||||
"""(internal)"""
|
||||
# pylint: disable=too-many-branches
|
||||
# pylint: disable=too-many-nested-blocks
|
||||
# pylint: disable=too-many-locals
|
||||
try:
|
||||
import datetime
|
||||
from ba._generated.enums import TimeType, TimeFormat
|
||||
|
||||
app = _ba.app
|
||||
sale_times: list[int | None] = []
|
||||
|
||||
# Calc time for our pro sale (old special case).
|
||||
if tab == 'extras':
|
||||
config = app.config
|
||||
if app.accounts_v1.have_pro():
|
||||
return None
|
||||
|
||||
# If we haven't calced/loaded start times yet.
|
||||
if app.pro_sale_start_time is None:
|
||||
|
||||
# If we've got a time-remaining in our config, start there.
|
||||
if 'PSTR' in config:
|
||||
app.pro_sale_start_time = int(
|
||||
_ba.time(TimeType.REAL, TimeFormat.MILLISECONDS)
|
||||
)
|
||||
app.pro_sale_start_val = config['PSTR']
|
||||
else:
|
||||
|
||||
# We start the timer once we get the duration from
|
||||
# the server.
|
||||
start_duration = _internal.get_v1_account_misc_read_val(
|
||||
'proSaleDurationMinutes', None
|
||||
)
|
||||
if start_duration is not None:
|
||||
app.pro_sale_start_time = int(
|
||||
_ba.time(TimeType.REAL, TimeFormat.MILLISECONDS)
|
||||
)
|
||||
app.pro_sale_start_val = 60000 * start_duration
|
||||
|
||||
# If we haven't heard from the server yet, no sale..
|
||||
else:
|
||||
return None
|
||||
|
||||
assert app.pro_sale_start_val is not None
|
||||
val: int | None = max(
|
||||
0,
|
||||
app.pro_sale_start_val
|
||||
- (
|
||||
_ba.time(TimeType.REAL, TimeFormat.MILLISECONDS)
|
||||
- app.pro_sale_start_time
|
||||
),
|
||||
)
|
||||
|
||||
# Keep the value in the config up to date. I suppose we should
|
||||
# write the config occasionally but it should happen often enough
|
||||
# for other reasons.
|
||||
config['PSTR'] = val
|
||||
if val == 0:
|
||||
val = None
|
||||
sale_times.append(val)
|
||||
|
||||
# Now look for sales in this tab.
|
||||
sales_raw = _internal.get_v1_account_misc_read_val('sales', {})
|
||||
store_layout = get_store_layout()
|
||||
for section in store_layout[tab]:
|
||||
for item in section['items']:
|
||||
if item in sales_raw:
|
||||
if not _internal.get_purchased(item):
|
||||
to_end = (
|
||||
datetime.datetime.utcfromtimestamp(
|
||||
sales_raw[item]['e']
|
||||
)
|
||||
- datetime.datetime.utcnow()
|
||||
).total_seconds()
|
||||
if to_end > 0:
|
||||
sale_times.append(int(to_end * 1000))
|
||||
|
||||
# Return the smallest time I guess?
|
||||
sale_times_int = [t for t in sale_times if isinstance(t, int)]
|
||||
return min(sale_times_int) if sale_times_int else None
|
||||
|
||||
except Exception:
|
||||
from ba import _error
|
||||
|
||||
_error.print_exception('error calcing sale time')
|
||||
return None
|
||||
|
||||
|
||||
def get_unowned_maps() -> list[str]:
|
||||
"""Return the list of local maps not owned by the current account.
|
||||
|
||||
Category: **Asset Functions**
|
||||
"""
|
||||
unowned_maps: set[str] = set()
|
||||
if not _ba.app.headless_mode:
|
||||
for map_section in get_store_layout()['maps']:
|
||||
for mapitem in map_section['items']:
|
||||
if not _internal.get_purchased(mapitem):
|
||||
m_info = get_store_item(mapitem)
|
||||
unowned_maps.add(m_info['map_type'].name)
|
||||
return sorted(unowned_maps)
|
||||
|
||||
|
||||
def get_unowned_game_types() -> set[type[ba.GameActivity]]:
|
||||
"""Return present game types not owned by the current account."""
|
||||
try:
|
||||
unowned_games: set[type[ba.GameActivity]] = set()
|
||||
if not _ba.app.headless_mode:
|
||||
for section in get_store_layout()['minigames']:
|
||||
for mname in section['items']:
|
||||
if not _internal.get_purchased(mname):
|
||||
m_info = get_store_item(mname)
|
||||
unowned_games.add(m_info['gametype'])
|
||||
return unowned_games
|
||||
except Exception:
|
||||
from ba import _error
|
||||
|
||||
_error.print_exception('error calcing un-owned games')
|
||||
return set()
|
||||
93
dist/ba_data/python/ba/cloud.py
vendored
93
dist/ba_data/python/ba/cloud.py
vendored
|
|
@ -1,93 +0,0 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Functionality related to the cloud."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, overload
|
||||
|
||||
import _ba
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Union, Callable, Any
|
||||
|
||||
from efro.message import Message
|
||||
import bacommon.cloud
|
||||
|
||||
# TODO: Should make it possible to define a protocol in bacommon.cloud and
|
||||
# autogenerate this. That would give us type safety between this and
|
||||
# internal protocols.
|
||||
|
||||
|
||||
class CloudSubsystem:
|
||||
"""Used for communicating with the cloud."""
|
||||
|
||||
def is_connected(self) -> bool:
|
||||
"""Return 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
|
||||
|
||||
@overload
|
||||
def send_message(
|
||||
self,
|
||||
msg: bacommon.cloud.LoginProxyRequestMessage,
|
||||
on_response: Callable[
|
||||
[Union[bacommon.cloud.LoginProxyRequestResponse,
|
||||
Exception]], None],
|
||||
) -> None:
|
||||
...
|
||||
|
||||
@overload
|
||||
def send_message(
|
||||
self,
|
||||
msg: bacommon.cloud.LoginProxyStateQueryMessage,
|
||||
on_response: Callable[
|
||||
[Union[bacommon.cloud.LoginProxyStateQueryResponse,
|
||||
Exception]], None],
|
||||
) -> None:
|
||||
...
|
||||
|
||||
@overload
|
||||
def send_message(
|
||||
self,
|
||||
msg: bacommon.cloud.LoginProxyCompleteMessage,
|
||||
on_response: Callable[[Union[None, Exception]], None],
|
||||
) -> None:
|
||||
...
|
||||
|
||||
@overload
|
||||
def send_message(
|
||||
self,
|
||||
msg: bacommon.cloud.CredentialsCheckMessage,
|
||||
on_response: Callable[
|
||||
[Union[bacommon.cloud.CredentialsCheckResponse, Exception]], None],
|
||||
) -> None:
|
||||
...
|
||||
|
||||
@overload
|
||||
def send_message(
|
||||
self,
|
||||
msg: bacommon.cloud.AccountSessionReleaseMessage,
|
||||
on_response: Callable[[Union[None, Exception]], None],
|
||||
) -> None:
|
||||
...
|
||||
|
||||
def send_message(
|
||||
self,
|
||||
msg: Message,
|
||||
on_response: Callable[[Any], None],
|
||||
) -> None:
|
||||
"""Asynchronously send a message to the cloud from the game thread.
|
||||
|
||||
The provided on_response call will be run in the logic thread
|
||||
and passed either the response or the error that occurred.
|
||||
"""
|
||||
from ba._general import Call
|
||||
del msg # Unused.
|
||||
|
||||
_ba.pushcall(
|
||||
Call(on_response,
|
||||
RuntimeError('Cloud functionality is not available.')))
|
||||
8
dist/ba_data/python/ba/deprecated.py
vendored
8
dist/ba_data/python/ba/deprecated.py
vendored
|
|
@ -1,8 +0,0 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Deprecated functionality.
|
||||
|
||||
Classes or functions can be relocated here when they are deprecated.
|
||||
Any code using them should migrate to alternative methods, as
|
||||
deprecated items will eventually be fully removed.
|
||||
"""
|
||||
339
dist/ba_data/python/ba/internal.py
vendored
339
dist/ba_data/python/ba/internal.py
vendored
|
|
@ -1,339 +0,0 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Exposed functionality not intended for full public use.
|
||||
|
||||
Classes and functions contained here, while technically 'public', may change
|
||||
or disappear without warning, so should be avoided (or used sparingly and
|
||||
defensively) in mods.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from _ba import (
|
||||
show_online_score_ui,
|
||||
set_ui_input_device,
|
||||
is_party_icon_visible,
|
||||
getinputdevice,
|
||||
add_clean_frame_callback,
|
||||
unlock_all_input,
|
||||
increment_analytics_count,
|
||||
set_debug_speed_exponent,
|
||||
get_special_widget,
|
||||
get_qrcode_texture,
|
||||
get_string_height,
|
||||
get_string_width,
|
||||
show_app_invite,
|
||||
appnameupper,
|
||||
lock_all_input,
|
||||
open_file_externally,
|
||||
fade_screen,
|
||||
appname,
|
||||
have_incentivized_ad,
|
||||
has_video_ads,
|
||||
workspaces_in_use,
|
||||
set_party_icon_always_visible,
|
||||
connect_to_party,
|
||||
get_game_port,
|
||||
end_host_scanning,
|
||||
host_scan_cycle,
|
||||
charstr,
|
||||
get_public_party_enabled,
|
||||
get_public_party_max_size,
|
||||
set_public_party_name,
|
||||
set_public_party_max_size,
|
||||
set_public_party_queue_enabled,
|
||||
set_authenticate_clients,
|
||||
set_public_party_enabled,
|
||||
reset_random_player_names,
|
||||
new_host_session,
|
||||
get_foreground_host_session,
|
||||
get_local_active_input_devices_count,
|
||||
get_ui_input_device,
|
||||
is_in_replay,
|
||||
set_replay_speed_exponent,
|
||||
get_replay_speed_exponent,
|
||||
disconnect_from_host,
|
||||
set_party_window_open,
|
||||
get_connection_to_host_info,
|
||||
get_chat_messages,
|
||||
get_game_roster,
|
||||
disconnect_client,
|
||||
chatmessage,
|
||||
get_random_names,
|
||||
have_permission,
|
||||
request_permission,
|
||||
have_touchscreen_input,
|
||||
is_xcode_build,
|
||||
set_low_level_config_value,
|
||||
get_low_level_config_value,
|
||||
capture_gamepad_input,
|
||||
release_gamepad_input,
|
||||
has_gamma_control,
|
||||
get_max_graphics_quality,
|
||||
get_display_resolution,
|
||||
capture_keyboard_input,
|
||||
release_keyboard_input,
|
||||
value_test,
|
||||
set_touchscreen_editing,
|
||||
is_running_on_fire_tv,
|
||||
android_get_external_files_dir,
|
||||
set_telnet_access_enabled,
|
||||
new_replay_session,
|
||||
get_replays_dir,
|
||||
)
|
||||
|
||||
from ba._login import LoginAdapter
|
||||
from ba._map import (
|
||||
get_map_class,
|
||||
register_map,
|
||||
preload_map_preview_media,
|
||||
get_map_display_string,
|
||||
get_filtered_map_name,
|
||||
)
|
||||
from ba._appconfig import commit_app_config
|
||||
from ba._input import (
|
||||
get_device_value,
|
||||
get_input_map_hash,
|
||||
get_input_device_config,
|
||||
)
|
||||
from ba._general import getclass, json_prep, get_type_name
|
||||
from ba._activitytypes import JoinActivity, ScoreScreenActivity
|
||||
from ba._apputils import (
|
||||
is_browser_likely_available,
|
||||
get_remote_app_name,
|
||||
should_submit_debug_info,
|
||||
dump_app_state,
|
||||
log_dumped_app_state,
|
||||
)
|
||||
from ba._benchmark import (
|
||||
run_gpu_benchmark,
|
||||
run_cpu_benchmark,
|
||||
run_media_reload_benchmark,
|
||||
run_stress_test,
|
||||
)
|
||||
from ba._campaign import getcampaign
|
||||
from ba._messages import PlayerProfilesChangedMessage
|
||||
from ba._multiteamsession import DEFAULT_TEAM_COLORS, DEFAULT_TEAM_NAMES
|
||||
from ba._music import do_play_music
|
||||
from ba._net import (
|
||||
master_server_get,
|
||||
master_server_post,
|
||||
get_ip_address_type,
|
||||
DEFAULT_REQUEST_TIMEOUT_SECONDS,
|
||||
)
|
||||
from ba._powerup import get_default_powerup_distribution
|
||||
from ba._profile import (
|
||||
get_player_profile_colors,
|
||||
get_player_profile_icon,
|
||||
get_player_colors,
|
||||
)
|
||||
from ba._tips import get_next_tip
|
||||
from ba._playlist import (
|
||||
get_default_free_for_all_playlist,
|
||||
get_default_teams_playlist,
|
||||
filter_playlist,
|
||||
)
|
||||
from ba._store import (
|
||||
get_available_sale_time,
|
||||
get_available_purchase_count,
|
||||
get_store_item_name_translated,
|
||||
get_store_item_display_size,
|
||||
get_store_layout,
|
||||
get_store_item,
|
||||
get_clean_price,
|
||||
get_unowned_maps,
|
||||
get_unowned_game_types,
|
||||
)
|
||||
from ba._tournament import get_tournament_prize_strings
|
||||
from ba._gameutils import get_trophy_string
|
||||
|
||||
from ba._internal import (
|
||||
get_v2_fleet,
|
||||
get_master_server_address,
|
||||
is_blessed,
|
||||
get_news_show,
|
||||
game_service_has_leaderboard,
|
||||
report_achievement,
|
||||
submit_score,
|
||||
tournament_query,
|
||||
power_ranking_query,
|
||||
restore_purchases,
|
||||
purchase,
|
||||
get_purchases_state,
|
||||
get_purchased,
|
||||
get_price,
|
||||
in_game_purchase,
|
||||
add_transaction,
|
||||
reset_achievements,
|
||||
get_public_login_id,
|
||||
have_outstanding_transactions,
|
||||
run_transactions,
|
||||
get_v1_account_misc_read_val,
|
||||
get_v1_account_misc_read_val_2,
|
||||
get_v1_account_misc_val,
|
||||
get_v1_account_ticket_count,
|
||||
get_v1_account_state_num,
|
||||
get_v1_account_state,
|
||||
get_v1_account_display_string,
|
||||
get_v1_account_type,
|
||||
get_v1_account_name,
|
||||
sign_out_v1,
|
||||
sign_in_v1,
|
||||
mark_config_dirty,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
'LoginAdapter',
|
||||
'show_online_score_ui',
|
||||
'set_ui_input_device',
|
||||
'is_party_icon_visible',
|
||||
'getinputdevice',
|
||||
'add_clean_frame_callback',
|
||||
'unlock_all_input',
|
||||
'increment_analytics_count',
|
||||
'set_debug_speed_exponent',
|
||||
'get_special_widget',
|
||||
'get_qrcode_texture',
|
||||
'get_string_height',
|
||||
'get_string_width',
|
||||
'show_app_invite',
|
||||
'appnameupper',
|
||||
'lock_all_input',
|
||||
'open_file_externally',
|
||||
'fade_screen',
|
||||
'appname',
|
||||
'have_incentivized_ad',
|
||||
'has_video_ads',
|
||||
'workspaces_in_use',
|
||||
'set_party_icon_always_visible',
|
||||
'connect_to_party',
|
||||
'get_game_port',
|
||||
'end_host_scanning',
|
||||
'host_scan_cycle',
|
||||
'charstr',
|
||||
'get_public_party_enabled',
|
||||
'get_public_party_max_size',
|
||||
'set_public_party_name',
|
||||
'set_public_party_max_size',
|
||||
'set_public_party_queue_enabled',
|
||||
'set_authenticate_clients',
|
||||
'set_public_party_enabled',
|
||||
'reset_random_player_names',
|
||||
'new_host_session',
|
||||
'get_foreground_host_session',
|
||||
'get_local_active_input_devices_count',
|
||||
'get_ui_input_device',
|
||||
'is_in_replay',
|
||||
'set_replay_speed_exponent',
|
||||
'get_replay_speed_exponent',
|
||||
'disconnect_from_host',
|
||||
'set_party_window_open',
|
||||
'get_connection_to_host_info',
|
||||
'get_chat_messages',
|
||||
'get_game_roster',
|
||||
'disconnect_client',
|
||||
'chatmessage',
|
||||
'get_random_names',
|
||||
'have_permission',
|
||||
'request_permission',
|
||||
'have_touchscreen_input',
|
||||
'is_xcode_build',
|
||||
'set_low_level_config_value',
|
||||
'get_low_level_config_value',
|
||||
'capture_gamepad_input',
|
||||
'release_gamepad_input',
|
||||
'has_gamma_control',
|
||||
'get_max_graphics_quality',
|
||||
'get_display_resolution',
|
||||
'capture_keyboard_input',
|
||||
'release_keyboard_input',
|
||||
'value_test',
|
||||
'set_touchscreen_editing',
|
||||
'is_running_on_fire_tv',
|
||||
'android_get_external_files_dir',
|
||||
'set_telnet_access_enabled',
|
||||
'new_replay_session',
|
||||
'get_replays_dir',
|
||||
'get_unowned_maps',
|
||||
'get_unowned_game_types',
|
||||
'get_map_class',
|
||||
'register_map',
|
||||
'preload_map_preview_media',
|
||||
'get_map_display_string',
|
||||
'get_filtered_map_name',
|
||||
'commit_app_config',
|
||||
'get_device_value',
|
||||
'get_input_map_hash',
|
||||
'get_input_device_config',
|
||||
'getclass',
|
||||
'json_prep',
|
||||
'get_type_name',
|
||||
'JoinActivity',
|
||||
'ScoreScreenActivity',
|
||||
'is_browser_likely_available',
|
||||
'get_remote_app_name',
|
||||
'should_submit_debug_info',
|
||||
'run_gpu_benchmark',
|
||||
'run_cpu_benchmark',
|
||||
'run_media_reload_benchmark',
|
||||
'run_stress_test',
|
||||
'getcampaign',
|
||||
'PlayerProfilesChangedMessage',
|
||||
'DEFAULT_TEAM_COLORS',
|
||||
'DEFAULT_TEAM_NAMES',
|
||||
'do_play_music',
|
||||
'master_server_get',
|
||||
'master_server_post',
|
||||
'get_ip_address_type',
|
||||
'DEFAULT_REQUEST_TIMEOUT_SECONDS',
|
||||
'get_default_powerup_distribution',
|
||||
'get_player_profile_colors',
|
||||
'get_player_profile_icon',
|
||||
'get_player_colors',
|
||||
'get_next_tip',
|
||||
'get_default_free_for_all_playlist',
|
||||
'get_default_teams_playlist',
|
||||
'filter_playlist',
|
||||
'get_available_sale_time',
|
||||
'get_available_purchase_count',
|
||||
'get_store_item_name_translated',
|
||||
'get_store_item_display_size',
|
||||
'get_store_layout',
|
||||
'get_store_item',
|
||||
'get_clean_price',
|
||||
'get_tournament_prize_strings',
|
||||
'get_trophy_string',
|
||||
'get_v2_fleet',
|
||||
'get_master_server_address',
|
||||
'is_blessed',
|
||||
'get_news_show',
|
||||
'game_service_has_leaderboard',
|
||||
'report_achievement',
|
||||
'submit_score',
|
||||
'tournament_query',
|
||||
'power_ranking_query',
|
||||
'restore_purchases',
|
||||
'purchase',
|
||||
'get_purchases_state',
|
||||
'get_purchased',
|
||||
'get_price',
|
||||
'in_game_purchase',
|
||||
'add_transaction',
|
||||
'reset_achievements',
|
||||
'get_public_login_id',
|
||||
'have_outstanding_transactions',
|
||||
'run_transactions',
|
||||
'get_v1_account_misc_read_val',
|
||||
'get_v1_account_misc_read_val_2',
|
||||
'get_v1_account_misc_val',
|
||||
'get_v1_account_ticket_count',
|
||||
'get_v1_account_state_num',
|
||||
'get_v1_account_state',
|
||||
'get_v1_account_display_string',
|
||||
'get_v1_account_type',
|
||||
'get_v1_account_name',
|
||||
'sign_out_v1',
|
||||
'sign_in_v1',
|
||||
'mark_config_dirty',
|
||||
'dump_app_state',
|
||||
'log_dumped_app_state',
|
||||
]
|
||||
307
dist/ba_data/python/babase/__init__.py
vendored
Normal file
307
dist/ba_data/python/babase/__init__.py
vendored
Normal file
|
|
@ -0,0 +1,307 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""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.
|
||||
"""
|
||||
# pylint: disable=redefined-builtin
|
||||
|
||||
# The stuff we expose here at the top level is our 'public' api for use
|
||||
# from other modules/packages. Code *within* this package should import
|
||||
# things from this package's submodules directly to reduce the chance of
|
||||
# dependency loops. The exception is TYPE_CHECKING blocks and
|
||||
# annotations since those aren't evaluated at runtime.
|
||||
|
||||
from efro.util import set_canonical_module_names
|
||||
|
||||
|
||||
import _babase
|
||||
from _babase import (
|
||||
add_clean_frame_callback,
|
||||
android_get_external_files_dir,
|
||||
appname,
|
||||
appnameupper,
|
||||
apptime,
|
||||
apptimer,
|
||||
AppTimer,
|
||||
charstr,
|
||||
clipboard_get_text,
|
||||
clipboard_has_text,
|
||||
clipboard_is_supported,
|
||||
clipboard_set_text,
|
||||
ContextCall,
|
||||
ContextRef,
|
||||
displaytime,
|
||||
displaytimer,
|
||||
DisplayTimer,
|
||||
do_once,
|
||||
env,
|
||||
fade_screen,
|
||||
fatal_error,
|
||||
get_display_resolution,
|
||||
get_immediate_return_code,
|
||||
get_low_level_config_value,
|
||||
get_max_graphics_quality,
|
||||
get_replays_dir,
|
||||
get_string_height,
|
||||
get_string_width,
|
||||
getsimplesound,
|
||||
has_gamma_control,
|
||||
have_chars,
|
||||
have_permission,
|
||||
in_logic_thread,
|
||||
increment_analytics_count,
|
||||
is_os_playing_music,
|
||||
is_running_on_fire_tv,
|
||||
is_xcode_build,
|
||||
lock_all_input,
|
||||
mac_music_app_get_library_source,
|
||||
mac_music_app_get_playlists,
|
||||
mac_music_app_get_volume,
|
||||
mac_music_app_init,
|
||||
mac_music_app_play_playlist,
|
||||
mac_music_app_set_volume,
|
||||
mac_music_app_stop,
|
||||
music_player_play,
|
||||
music_player_set_volume,
|
||||
music_player_shutdown,
|
||||
music_player_stop,
|
||||
native_stack_trace,
|
||||
print_load_info,
|
||||
pushcall,
|
||||
quit,
|
||||
reload_media,
|
||||
request_permission,
|
||||
safecolor,
|
||||
screenmessage,
|
||||
set_analytics_screen,
|
||||
set_low_level_config_value,
|
||||
set_stress_testing,
|
||||
set_thread_name,
|
||||
set_ui_input_device,
|
||||
show_progress_bar,
|
||||
SimpleSound,
|
||||
unlock_all_input,
|
||||
user_agent_string,
|
||||
Vec3,
|
||||
workspaces_in_use,
|
||||
)
|
||||
|
||||
from babase._accountv2 import AccountV2Handle, AccountV2Subsystem
|
||||
from babase._app import App
|
||||
from babase._appconfig import commit_app_config
|
||||
from babase._appintent import AppIntent, AppIntentDefault, AppIntentExec
|
||||
from babase._appmode import AppMode
|
||||
from babase._appsubsystem import AppSubsystem
|
||||
from babase._appconfig import AppConfig
|
||||
from babase._apputils import (
|
||||
handle_leftover_v1_cloud_log_file,
|
||||
is_browser_likely_available,
|
||||
garbage_collect,
|
||||
get_remote_app_name,
|
||||
)
|
||||
from babase._cloud import CloudSubsystem
|
||||
from babase._emptyappmode import EmptyAppMode
|
||||
from babase._error import (
|
||||
print_exception,
|
||||
print_error,
|
||||
ContextError,
|
||||
NotFoundError,
|
||||
PlayerNotFoundError,
|
||||
SessionPlayerNotFoundError,
|
||||
NodeNotFoundError,
|
||||
ActorNotFoundError,
|
||||
InputDeviceNotFoundError,
|
||||
WidgetNotFoundError,
|
||||
ActivityNotFoundError,
|
||||
TeamNotFoundError,
|
||||
MapNotFoundError,
|
||||
SessionTeamNotFoundError,
|
||||
SessionNotFoundError,
|
||||
DelegateNotFoundError,
|
||||
)
|
||||
from babase._general import (
|
||||
utf8_all,
|
||||
DisplayTime,
|
||||
AppTime,
|
||||
WeakCall,
|
||||
Call,
|
||||
existing,
|
||||
Existable,
|
||||
verify_object_death,
|
||||
storagename,
|
||||
getclass,
|
||||
get_type_name,
|
||||
json_prep,
|
||||
)
|
||||
from babase._keyboard import Keyboard
|
||||
from babase._language import Lstr, LanguageSubsystem
|
||||
from babase._login import LoginAdapter
|
||||
|
||||
# noinspection PyProtectedMember
|
||||
# (PyCharm inspection bug?)
|
||||
from babase._mgen.enums import (
|
||||
Permission,
|
||||
SpecialChar,
|
||||
InputType,
|
||||
UIScale,
|
||||
)
|
||||
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._plugin import PluginSpec, Plugin, PluginSubsystem
|
||||
from babase._text import timestring
|
||||
|
||||
_babase.app = app = App()
|
||||
app.postinit()
|
||||
|
||||
__all__ = [
|
||||
'AccountV2Handle',
|
||||
'AccountV2Subsystem',
|
||||
'ActivityNotFoundError',
|
||||
'ActorNotFoundError',
|
||||
'add_clean_frame_callback',
|
||||
'android_get_external_files_dir',
|
||||
'app',
|
||||
'app',
|
||||
'App',
|
||||
'AppConfig',
|
||||
'AppIntent',
|
||||
'AppIntentDefault',
|
||||
'AppIntentExec',
|
||||
'AppMode',
|
||||
'appname',
|
||||
'appnameupper',
|
||||
'AppSubsystem',
|
||||
'apptime',
|
||||
'AppTime',
|
||||
'apptime',
|
||||
'apptimer',
|
||||
'AppTimer',
|
||||
'Call',
|
||||
'charstr',
|
||||
'clipboard_get_text',
|
||||
'clipboard_has_text',
|
||||
'clipboard_is_supported',
|
||||
'clipboard_set_text',
|
||||
'CloudSubsystem',
|
||||
'commit_app_config',
|
||||
'ContextCall',
|
||||
'ContextError',
|
||||
'ContextRef',
|
||||
'DelegateNotFoundError',
|
||||
'DisplayTime',
|
||||
'displaytime',
|
||||
'displaytimer',
|
||||
'DisplayTimer',
|
||||
'do_once',
|
||||
'EmptyAppMode',
|
||||
'env',
|
||||
'Existable',
|
||||
'existing',
|
||||
'fade_screen',
|
||||
'fatal_error',
|
||||
'garbage_collect',
|
||||
'get_display_resolution',
|
||||
'get_immediate_return_code',
|
||||
'get_ip_address_type',
|
||||
'get_low_level_config_value',
|
||||
'get_max_graphics_quality',
|
||||
'get_remote_app_name',
|
||||
'get_replays_dir',
|
||||
'get_string_height',
|
||||
'get_string_width',
|
||||
'get_type_name',
|
||||
'getclass',
|
||||
'getsimplesound',
|
||||
'handle_leftover_v1_cloud_log_file',
|
||||
'has_gamma_control',
|
||||
'have_chars',
|
||||
'have_permission',
|
||||
'in_logic_thread',
|
||||
'increment_analytics_count',
|
||||
'InputDeviceNotFoundError',
|
||||
'InputType',
|
||||
'is_browser_likely_available',
|
||||
'is_browser_likely_available',
|
||||
'is_os_playing_music',
|
||||
'is_point_in_box',
|
||||
'is_running_on_fire_tv',
|
||||
'is_xcode_build',
|
||||
'json_prep',
|
||||
'Keyboard',
|
||||
'LanguageSubsystem',
|
||||
'lock_all_input',
|
||||
'LoginAdapter',
|
||||
'Lstr',
|
||||
'mac_music_app_get_library_source',
|
||||
'mac_music_app_get_playlists',
|
||||
'mac_music_app_get_volume',
|
||||
'mac_music_app_init',
|
||||
'mac_music_app_play_playlist',
|
||||
'mac_music_app_set_volume',
|
||||
'mac_music_app_stop',
|
||||
'MapNotFoundError',
|
||||
'MetadataSubsystem',
|
||||
'music_player_play',
|
||||
'music_player_set_volume',
|
||||
'music_player_shutdown',
|
||||
'music_player_stop',
|
||||
'native_stack_trace',
|
||||
'NodeNotFoundError',
|
||||
'normalized_color',
|
||||
'NotFoundError',
|
||||
'Permission',
|
||||
'PlayerNotFoundError',
|
||||
'Plugin',
|
||||
'PluginSubsystem',
|
||||
'PluginSpec',
|
||||
'print_error',
|
||||
'print_exception',
|
||||
'print_load_info',
|
||||
'pushcall',
|
||||
'quit',
|
||||
'reload_media',
|
||||
'request_permission',
|
||||
'safecolor',
|
||||
'screenmessage',
|
||||
'SessionNotFoundError',
|
||||
'SessionPlayerNotFoundError',
|
||||
'SessionTeamNotFoundError',
|
||||
'set_analytics_screen',
|
||||
'set_low_level_config_value',
|
||||
'set_stress_testing',
|
||||
'set_thread_name',
|
||||
'set_ui_input_device',
|
||||
'show_progress_bar',
|
||||
'SimpleSound',
|
||||
'SpecialChar',
|
||||
'storagename',
|
||||
'TeamNotFoundError',
|
||||
'timestring',
|
||||
'UIScale',
|
||||
'unlock_all_input',
|
||||
'user_agent_string',
|
||||
'utf8_all',
|
||||
'Vec3',
|
||||
'vec3validate',
|
||||
'verify_object_death',
|
||||
'WeakCall',
|
||||
'WidgetNotFoundError',
|
||||
'workspaces_in_use',
|
||||
'DEFAULT_REQUEST_TIMEOUT_SECONDS',
|
||||
]
|
||||
|
||||
# We want stuff to show up as babase.Foo instead of babase._sub.Foo.
|
||||
set_canonical_module_names(globals())
|
||||
|
||||
# Allow the native layer to wrap a few things up.
|
||||
_babase.reached_end_of_babase()
|
||||
|
||||
# Marker we pop down at the very end so other modules can run sanity
|
||||
# checks to make sure we aren't importing them reciprocally when they
|
||||
# import us.
|
||||
_REACHED_END_OF_MODULE = True
|
||||
|
|
@ -11,12 +11,12 @@ from typing import TYPE_CHECKING
|
|||
from efro.call import tpartial
|
||||
from efro.error import CommunicationError
|
||||
from bacommon.login import LoginType
|
||||
import _ba
|
||||
import _babase
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any
|
||||
|
||||
from ba._login import LoginAdapter
|
||||
from babase._login import LoginAdapter
|
||||
|
||||
|
||||
DEBUG_LOG = False
|
||||
|
|
@ -27,11 +27,10 @@ class AccountV2Subsystem:
|
|||
|
||||
Category: **App Classes**
|
||||
|
||||
Access the single shared instance of this class at 'ba.app.accounts_v2'.
|
||||
Access the single shared instance of this class at 'ba.app.accounts'.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
|
||||
# Whether or not everything related to an initial login
|
||||
# (or lack thereof) has completed. This includes things like
|
||||
# workspace syncing. Completion of this is what flips the app
|
||||
|
|
@ -46,16 +45,22 @@ class AccountV2Subsystem:
|
|||
self._implicit_state_changed = False
|
||||
self._can_do_auto_sign_in = True
|
||||
|
||||
if _ba.app.platform == 'android' and _ba.app.subplatform == 'google':
|
||||
from ba._login import LoginAdapterGPGS
|
||||
if _babase.app.classic is None:
|
||||
raise RuntimeError('Needs updating for no-classic case.')
|
||||
|
||||
if (
|
||||
_babase.app.classic.platform == 'android'
|
||||
and _babase.app.classic.subplatform == 'google'
|
||||
):
|
||||
from babase._login import LoginAdapterGPGS
|
||||
|
||||
self.login_adapters[LoginType.GPGS] = LoginAdapterGPGS()
|
||||
|
||||
def on_app_launch(self) -> None:
|
||||
"""Should be called at standard on_app_launch time."""
|
||||
def on_app_loading(self) -> None:
|
||||
"""Should be called at standard on_app_loading time."""
|
||||
|
||||
for adapter in self.login_adapters.values():
|
||||
adapter.on_app_launch()
|
||||
adapter.on_app_loading()
|
||||
|
||||
def set_primary_credentials(self, credentials: str | None) -> None:
|
||||
"""Set credentials for the primary app account."""
|
||||
|
|
@ -87,7 +92,7 @@ class AccountV2Subsystem:
|
|||
Will be called with None on log-outs and when new credentials
|
||||
are set but have not yet been verified.
|
||||
"""
|
||||
assert _ba.in_logic_thread()
|
||||
assert _babase.in_logic_thread()
|
||||
|
||||
# Currently don't do anything special on sign-outs.
|
||||
if account is None:
|
||||
|
|
@ -102,7 +107,7 @@ class AccountV2Subsystem:
|
|||
and not self._kicked_off_workspace_load
|
||||
):
|
||||
self._kicked_off_workspace_load = True
|
||||
_ba.app.workspaces.set_active_workspace(
|
||||
_babase.app.workspaces.set_active_workspace(
|
||||
account=account,
|
||||
workspaceid=account.workspaceid,
|
||||
workspacename=account.workspacename,
|
||||
|
|
@ -112,18 +117,18 @@ class AccountV2Subsystem:
|
|||
# Don't activate workspaces if we've already told the game
|
||||
# that initial-log-in is done or if we've already kicked
|
||||
# off a workspace load.
|
||||
_ba.screenmessage(
|
||||
_babase.screenmessage(
|
||||
f'\'{account.workspacename}\''
|
||||
f' will be activated at next app launch.',
|
||||
color=(1, 1, 0),
|
||||
)
|
||||
_ba.playsound(_ba.getsound('error'))
|
||||
_babase.getsimplesound('error').play()
|
||||
return
|
||||
|
||||
# Ok; no workspace to worry about; carry on.
|
||||
if not self._initial_sign_in_completed:
|
||||
self._initial_sign_in_completed = True
|
||||
_ba.app.on_initial_sign_in_completed()
|
||||
_babase.app.on_initial_sign_in_completed()
|
||||
|
||||
def on_active_logins_changed(self, logins: dict[LoginType, str]) -> None:
|
||||
"""Should be called when logins for the active account change."""
|
||||
|
|
@ -135,9 +140,9 @@ class AccountV2Subsystem:
|
|||
self, login_type: LoginType, login_id: str, display_name: str
|
||||
) -> None:
|
||||
"""An implicit sign-in happened (called by native layer)."""
|
||||
from ba._login import LoginAdapter
|
||||
from babase._login import LoginAdapter
|
||||
|
||||
with _ba.Context('ui'):
|
||||
with _babase.ContextRef.empty():
|
||||
self.login_adapters[login_type].set_implicit_login_state(
|
||||
LoginAdapter.ImplicitLoginState(
|
||||
login_id=login_id, display_name=display_name
|
||||
|
|
@ -146,7 +151,7 @@ class AccountV2Subsystem:
|
|||
|
||||
def on_implicit_sign_out(self, login_type: LoginType) -> None:
|
||||
"""An implicit sign-out happened (called by native layer)."""
|
||||
with _ba.Context('ui'):
|
||||
with _babase.ContextRef.empty():
|
||||
self.login_adapters[login_type].set_implicit_login_state(None)
|
||||
|
||||
def on_no_initial_primary_account(self) -> None:
|
||||
|
|
@ -158,7 +163,7 @@ class AccountV2Subsystem:
|
|||
"""
|
||||
if not self._initial_sign_in_completed:
|
||||
self._initial_sign_in_completed = True
|
||||
_ba.app.on_initial_sign_in_completed()
|
||||
_babase.app.on_initial_sign_in_completed()
|
||||
|
||||
@staticmethod
|
||||
def _hashstr(val: str) -> str:
|
||||
|
|
@ -179,13 +184,13 @@ class AccountV2Subsystem:
|
|||
types even if the default implicit one can't be explicitly
|
||||
logged out or otherwise controlled.
|
||||
"""
|
||||
from ba._language import Lstr
|
||||
from babase._language import Lstr
|
||||
|
||||
assert _ba.in_logic_thread()
|
||||
assert _babase.in_logic_thread()
|
||||
|
||||
cfg = _ba.app.config
|
||||
cfg = _babase.app.config
|
||||
cfgkey = 'ImplicitLoginStates'
|
||||
cfgdict = _ba.app.config.setdefault(cfgkey, {})
|
||||
cfgdict = _babase.app.config.setdefault(cfgkey, {})
|
||||
|
||||
# Store which (if any) adapter is currently implicitly signed in.
|
||||
# Making the assumption there will only ever be one implicit
|
||||
|
|
@ -213,10 +218,10 @@ class AccountV2Subsystem:
|
|||
else:
|
||||
service_str = None
|
||||
if service_str is not None:
|
||||
_ba.timer(
|
||||
_babase.apptimer(
|
||||
2.0,
|
||||
tpartial(
|
||||
_ba.screenmessage,
|
||||
_babase.screenmessage,
|
||||
Lstr(
|
||||
resource='notUsingAccountText',
|
||||
subs=[
|
||||
|
|
@ -249,13 +254,14 @@ class AccountV2Subsystem:
|
|||
def on_cloud_connectivity_changed(self, connected: bool) -> None:
|
||||
"""Should be called with cloud connectivity changes."""
|
||||
del connected # Unused.
|
||||
assert _ba.in_logic_thread()
|
||||
assert _babase.in_logic_thread()
|
||||
|
||||
# We may want to auto-sign-in based on this new state.
|
||||
self._update_auto_sign_in()
|
||||
|
||||
def _update_auto_sign_in(self) -> None:
|
||||
from ba._internal import get_v1_account_state
|
||||
plus = _babase.app.plus
|
||||
assert plus is not None
|
||||
|
||||
# If implicit state has changed, try to respond.
|
||||
if self._implicit_state_changed:
|
||||
|
|
@ -267,7 +273,7 @@ class AccountV2Subsystem:
|
|||
'AccountV2: Signing out as result'
|
||||
' of implicit state change...',
|
||||
)
|
||||
_ba.app.accounts_v2.set_primary_credentials(None)
|
||||
plus.accounts.set_primary_credentials(None)
|
||||
self._implicit_state_changed = False
|
||||
|
||||
# Once we've made a move here we don't want to
|
||||
|
|
@ -283,7 +289,7 @@ class AccountV2Subsystem:
|
|||
# switching accounts via the back-end).
|
||||
# NOTE: should test case where we don't have
|
||||
# connectivity here.
|
||||
if _ba.app.cloud.is_connected():
|
||||
if plus.cloud.is_connected():
|
||||
if DEBUG_LOG:
|
||||
logging.debug(
|
||||
'AccountV2: Signing in as result'
|
||||
|
|
@ -310,9 +316,9 @@ class AccountV2Subsystem:
|
|||
# in as a rule, even if there are corner cases where this might
|
||||
# not be what they want (A user signing out and then restarting
|
||||
# may be auto-signed back in).
|
||||
connected = _ba.app.cloud.is_connected()
|
||||
signed_in_v1 = get_v1_account_state() == 'signed_in'
|
||||
signed_in_v2 = _ba.app.accounts_v2.have_primary_credentials()
|
||||
connected = plus.cloud.is_connected()
|
||||
signed_in_v1 = plus.get_v1_account_state() == 'signed_in'
|
||||
signed_in_v2 = plus.accounts.have_primary_credentials()
|
||||
if (
|
||||
connected
|
||||
and not signed_in_v1
|
||||
|
|
@ -334,10 +340,13 @@ class AccountV2Subsystem:
|
|||
result: LoginAdapter.SignInResult | Exception,
|
||||
) -> None:
|
||||
"""A sign-in has completed that the user asked for explicitly."""
|
||||
from ba._language import Lstr
|
||||
from babase._language import Lstr
|
||||
|
||||
del adapter # Unused.
|
||||
|
||||
plus = _babase.app.plus
|
||||
assert plus is not None
|
||||
|
||||
# Make some noise on errors since the user knows a
|
||||
# sign-in attempt is happening in this case (the 'explicit' part).
|
||||
if isinstance(result, Exception):
|
||||
|
|
@ -350,20 +359,19 @@ class AccountV2Subsystem:
|
|||
)
|
||||
|
||||
# For now just show 'error'. Should do better than this.
|
||||
with _ba.Context('ui'):
|
||||
_ba.screenmessage(
|
||||
Lstr(resource='internal.signInErrorText'),
|
||||
color=(1, 0, 0),
|
||||
)
|
||||
_ba.playsound(_ba.getsound('error'))
|
||||
_babase.screenmessage(
|
||||
Lstr(resource='internal.signInErrorText'),
|
||||
color=(1, 0, 0),
|
||||
)
|
||||
_babase.getsimplesound('error').play()
|
||||
|
||||
# Also I suppose we should sign them out in this case since
|
||||
# it could be misleading to be still signed in with the old
|
||||
# account.
|
||||
_ba.app.accounts_v2.set_primary_credentials(None)
|
||||
plus.accounts.set_primary_credentials(None)
|
||||
return
|
||||
|
||||
_ba.app.accounts_v2.set_primary_credentials(result.credentials)
|
||||
plus.accounts.set_primary_credentials(result.credentials)
|
||||
|
||||
def _on_implicit_sign_in_completed(
|
||||
self,
|
||||
|
|
@ -371,7 +379,8 @@ class AccountV2Subsystem:
|
|||
result: LoginAdapter.SignInResult | Exception,
|
||||
) -> None:
|
||||
"""A sign-in has completed that the user didn't ask for explicitly."""
|
||||
from ba._internal import get_v1_account_state
|
||||
plus = _babase.app.plus
|
||||
assert plus is not None
|
||||
|
||||
del adapter # Unused.
|
||||
|
||||
|
|
@ -391,16 +400,16 @@ class AccountV2Subsystem:
|
|||
# plug in the credentials we got. We want to be extra cautious
|
||||
# in case the user has since explicitly signed in since we
|
||||
# kicked off.
|
||||
connected = _ba.app.cloud.is_connected()
|
||||
signed_in_v1 = get_v1_account_state() == 'signed_in'
|
||||
signed_in_v2 = _ba.app.accounts_v2.have_primary_credentials()
|
||||
connected = plus.cloud.is_connected()
|
||||
signed_in_v1 = plus.get_v1_account_state() == 'signed_in'
|
||||
signed_in_v2 = plus.accounts.have_primary_credentials()
|
||||
if connected and not signed_in_v1 and not signed_in_v2:
|
||||
_ba.app.accounts_v2.set_primary_credentials(result.credentials)
|
||||
plus.accounts.set_primary_credentials(result.credentials)
|
||||
|
||||
def _on_set_active_workspace_completed(self) -> None:
|
||||
if not self._initial_sign_in_completed:
|
||||
self._initial_sign_in_completed = True
|
||||
_ba.app.on_initial_sign_in_completed()
|
||||
_babase.app.on_initial_sign_in_completed()
|
||||
|
||||
|
||||
class AccountV2Handle:
|
||||
837
dist/ba_data/python/babase/_app.py
vendored
Normal file
837
dist/ba_data/python/babase/_app.py
vendored
Normal file
|
|
@ -0,0 +1,837 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Functionality related to the high level state of the app."""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from enum import Enum
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from functools import cached_property
|
||||
|
||||
from efro.call import tpartial
|
||||
import _babase
|
||||
from babase._language import LanguageSubsystem
|
||||
from babase._plugin import PluginSubsystem
|
||||
from babase._meta import MetadataSubsystem
|
||||
from babase._net import NetworkSubsystem
|
||||
from babase._workspace import WorkspaceSubsystem
|
||||
from babase._appcomponent import AppComponentSubsystem
|
||||
from babase._appmodeselector import AppModeSelector
|
||||
from babase._appintent import AppIntentDefault, AppIntentExec
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import asyncio
|
||||
from typing import Any, Callable
|
||||
from concurrent.futures import Future
|
||||
|
||||
import babase
|
||||
from babase import AppIntent, AppMode, AppSubsystem
|
||||
from babase._apputils import AppHealthMonitor
|
||||
|
||||
# __FEATURESET_APP_SUBSYSTEM_IMPORTS_BEGIN__
|
||||
# This section generated by batools.appmodule; do not edit.
|
||||
|
||||
from baclassic import ClassicSubsystem
|
||||
from baplus import PlusSubsystem
|
||||
from bauiv1 import UIV1Subsystem
|
||||
|
||||
# __FEATURESET_APP_SUBSYSTEM_IMPORTS_END__
|
||||
|
||||
|
||||
class App:
|
||||
"""A class for high level 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.
|
||||
"""
|
||||
|
||||
# pylint: disable=too-many-public-methods
|
||||
|
||||
plugins: PluginSubsystem
|
||||
lang: LanguageSubsystem
|
||||
|
||||
health_monitor: AppHealthMonitor
|
||||
|
||||
class State(Enum):
|
||||
"""High level state the app can be in."""
|
||||
|
||||
# The app 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 babase.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 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 'babase.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 data_directory(self) -> str:
|
||||
"""Path where static app data lives."""
|
||||
assert isinstance(self._env['data_directory'], str)
|
||||
return self._env['data_directory']
|
||||
|
||||
@property
|
||||
def python_directory_user(self) -> str | None:
|
||||
"""Path where ballistica expects its custom user scripts (mods) to live.
|
||||
|
||||
Be aware that this value may be None if ballistica is running in
|
||||
a non-standard environment, and that python-path modifications may
|
||||
cause modules to be loaded from other locations.
|
||||
"""
|
||||
assert isinstance(self._env['python_directory_user'], (str, type(None)))
|
||||
return self._env['python_directory_user']
|
||||
|
||||
@property
|
||||
def python_directory_app(self) -> str | None:
|
||||
"""Path where ballistica expects its bundled modules to live.
|
||||
|
||||
Be aware that this value may be None if ballistica is running in
|
||||
a non-standard environment, and that python-path modifications may
|
||||
cause modules to be loaded from other locations.
|
||||
"""
|
||||
assert isinstance(self._env['python_directory_app'], (str, type(None)))
|
||||
return self._env['python_directory_app']
|
||||
|
||||
@property
|
||||
def python_directory_app_site(self) -> str | None:
|
||||
"""Path where ballistica expects its bundled pip modules to live.
|
||||
|
||||
Be aware that this value may be None if ballistica is running in
|
||||
a non-standard environment, and that python-path modifications may
|
||||
cause modules to be loaded from other locations.
|
||||
"""
|
||||
assert isinstance(
|
||||
self._env['python_directory_app_site'], (str, type(None))
|
||||
)
|
||||
return self._env['python_directory_app_site']
|
||||
|
||||
@property
|
||||
def config(self) -> babase.AppConfig:
|
||||
"""The babase.AppConfig instance representing the app's config state."""
|
||||
assert self._config is not None
|
||||
return self._config
|
||||
|
||||
@property
|
||||
def api_version(self) -> int:
|
||||
"""The app'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 substantial backward-incompatible
|
||||
changes are introduced to ballistica APIs. When that happens,
|
||||
modules/packages should be updated accordingly and set to target
|
||||
the newer API version number.
|
||||
"""
|
||||
from babase._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']
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""(internal)
|
||||
|
||||
Do not instantiate this class; use babase.app to access
|
||||
the single shared instance.
|
||||
"""
|
||||
|
||||
if os.environ.get('BA_RUNNING_WITH_DUMMY_MODULES') == '1':
|
||||
return
|
||||
|
||||
self.state = self.State.INITIAL
|
||||
|
||||
self._subsystems: list[AppSubsystem] = []
|
||||
|
||||
self._app_bootstrapping_complete = 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
|
||||
self._subsystem_registration_ended = False
|
||||
self._pending_apply_app_config = 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 = _babase.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)
|
||||
|
||||
# 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 = ThreadPoolExecutor(thread_name_prefix='baworker')
|
||||
|
||||
self._config: babase.AppConfig | None = None
|
||||
|
||||
self.components = AppComponentSubsystem()
|
||||
self.meta = MetadataSubsystem()
|
||||
self.net = NetworkSubsystem()
|
||||
self.workspaces = WorkspaceSubsystem()
|
||||
self._pending_intent: AppIntent | None = None
|
||||
self._intent: AppIntent | None = None
|
||||
self._mode: AppMode | None = None
|
||||
|
||||
# Controls which app-modes we use for handling given app-intents.
|
||||
# Plugins can override this to change high level app behavior and
|
||||
# spinoff projects can change the default implementation for the
|
||||
# same effect.
|
||||
self.mode_selector: AppModeSelector | None = None
|
||||
|
||||
self._asyncio_timer: babase.AppTimer | None = None
|
||||
|
||||
def postinit(self) -> None:
|
||||
"""Called after we've been inited and assigned to babase.app."""
|
||||
|
||||
if os.environ.get('BA_RUNNING_WITH_DUMMY_MODULES') == '1':
|
||||
return
|
||||
|
||||
# NOTE: the reason we need a postinit here is that
|
||||
# some of this stuff accesses babase.app and that doesn't
|
||||
# exist yet as of our __init__() call.
|
||||
|
||||
self.lang = LanguageSubsystem()
|
||||
self.plugins = PluginSubsystem()
|
||||
|
||||
def register_subsystem(self, subsystem: AppSubsystem) -> None:
|
||||
"""Called by the AppSubsystem class. Do not use directly."""
|
||||
|
||||
# We only allow registering new subsystems if we've not yet
|
||||
# reached the 'running' state. This ensures that all subsystems
|
||||
# receive a consistent set of callbacks starting with
|
||||
# on_app_running().
|
||||
if self._subsystem_registration_ended:
|
||||
raise RuntimeError(
|
||||
'Subsystems can no longer be registered at this point.'
|
||||
)
|
||||
self._subsystems.append(subsystem)
|
||||
|
||||
def _threadpool_no_wait_done(self, fut: Future) -> None:
|
||||
try:
|
||||
fut.result()
|
||||
except Exception:
|
||||
logging.exception(
|
||||
'Error in work submitted via threadpool_submit_no_wait()'
|
||||
)
|
||||
|
||||
def threadpool_submit_no_wait(self, call: Callable[[], Any]) -> None:
|
||||
"""Submit work to our threadpool and log any errors.
|
||||
|
||||
Use this when you want to run something asynchronously but don't
|
||||
intend to call result() on it to handle errors/etc.
|
||||
"""
|
||||
fut = self.threadpool.submit(call)
|
||||
fut.add_done_callback(self._threadpool_no_wait_done)
|
||||
|
||||
# __FEATURESET_APP_SUBSYSTEM_PROPERTIES_BEGIN__
|
||||
# This section generated by batools.appmodule; do not edit.
|
||||
|
||||
@cached_property
|
||||
def classic(self) -> ClassicSubsystem | None:
|
||||
"""Our classic subsystem (if available)."""
|
||||
# pylint: disable=cyclic-import
|
||||
|
||||
try:
|
||||
from baclassic import ClassicSubsystem
|
||||
|
||||
return ClassicSubsystem()
|
||||
except ImportError:
|
||||
return None
|
||||
except Exception:
|
||||
logging.exception('Error importing baclassic.')
|
||||
return None
|
||||
|
||||
@cached_property
|
||||
def plus(self) -> PlusSubsystem | None:
|
||||
"""Our plus subsystem (if available)."""
|
||||
# pylint: disable=cyclic-import
|
||||
|
||||
try:
|
||||
from baplus import PlusSubsystem
|
||||
|
||||
return PlusSubsystem()
|
||||
except ImportError:
|
||||
return None
|
||||
except Exception:
|
||||
logging.exception('Error importing baplus.')
|
||||
return None
|
||||
|
||||
@cached_property
|
||||
def ui_v1(self) -> UIV1Subsystem:
|
||||
"""Our ui_v1 subsystem (always available)."""
|
||||
# pylint: disable=cyclic-import
|
||||
|
||||
from bauiv1 import UIV1Subsystem
|
||||
|
||||
return UIV1Subsystem()
|
||||
|
||||
# __FEATURESET_APP_SUBSYSTEM_PROPERTIES_END__
|
||||
|
||||
def set_intent(self, intent: AppIntent) -> None:
|
||||
"""Set the intent for the 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.
|
||||
"""
|
||||
|
||||
# Mark this one as pending. We do this synchronously so that the
|
||||
# last one marked actually takes effect if there is overlap
|
||||
# (doing this in the bg thread could result in race conditions).
|
||||
self._pending_intent = intent
|
||||
|
||||
# Do the actual work of calcing our app-mode/etc. in a bg thread
|
||||
# since it may block for a moment to load modules/etc.
|
||||
self.threadpool_submit_no_wait(tpartial(self._set_intent, intent))
|
||||
|
||||
def _set_intent(self, intent: AppIntent) -> None:
|
||||
# This should be running in a bg thread.
|
||||
assert not _babase.in_logic_thread()
|
||||
try:
|
||||
# Ask the selector what app-mode to use for this intent.
|
||||
if self.mode_selector is None:
|
||||
raise RuntimeError('No AppModeSelector set.')
|
||||
modetype = self.mode_selector.app_mode_for_intent(intent)
|
||||
|
||||
# Make sure the app-mode they return *actually* supports the
|
||||
# intent.
|
||||
if not modetype.supports_intent(intent):
|
||||
raise RuntimeError(
|
||||
f'Intent {intent} is not supported by AppMode class'
|
||||
f' {modetype}'
|
||||
)
|
||||
|
||||
# Kick back to the logic thread to apply.
|
||||
mode = modetype()
|
||||
_babase.pushcall(
|
||||
tpartial(self._apply_intent, intent, mode),
|
||||
from_other_thread=True,
|
||||
)
|
||||
except Exception:
|
||||
logging.exception('Error setting app intent to %s.', intent)
|
||||
_babase.pushcall(
|
||||
tpartial(self._apply_intent_error, intent),
|
||||
from_other_thread=True,
|
||||
)
|
||||
|
||||
def _apply_intent(self, intent: AppIntent, mode: AppMode) -> None:
|
||||
assert _babase.in_logic_thread()
|
||||
|
||||
# ONLY apply this intent if it is still the most recent one
|
||||
# submitted.
|
||||
if intent is not self._pending_intent:
|
||||
return
|
||||
|
||||
# If the app-mode for this intent is different than the active
|
||||
# one, switch.
|
||||
if type(mode) is not type(self._mode):
|
||||
if self._mode is None:
|
||||
is_initial_mode = True
|
||||
else:
|
||||
is_initial_mode = False
|
||||
try:
|
||||
self._mode.on_deactivate()
|
||||
except Exception:
|
||||
logging.exception(
|
||||
'Error deactivating app-mode %s.', self._mode
|
||||
)
|
||||
self._mode = mode
|
||||
try:
|
||||
mode.on_activate()
|
||||
except Exception:
|
||||
# Hmm; what should we do in this case?...
|
||||
logging.exception('Error activating app-mode %s.', mode)
|
||||
|
||||
if is_initial_mode:
|
||||
_babase.on_initial_app_mode_set()
|
||||
|
||||
try:
|
||||
mode.handle_intent(intent)
|
||||
except Exception:
|
||||
logging.exception(
|
||||
'Error handling intent %s in app-mode %s.', intent, mode
|
||||
)
|
||||
|
||||
def _apply_intent_error(self, intent: AppIntent) -> None:
|
||||
from babase._language import Lstr
|
||||
|
||||
del intent # Unused.
|
||||
_babase.screenmessage(Lstr(resource='errorText'), color=(1, 0, 0))
|
||||
_babase.getsimplesound('error').play()
|
||||
|
||||
def run(self) -> None:
|
||||
"""Run the app to completion.
|
||||
|
||||
Note that this only works on platforms where ballistica
|
||||
manages its own event loop.
|
||||
"""
|
||||
_babase.run_app()
|
||||
|
||||
def on_app_bootstrapping_complete(self) -> None:
|
||||
"""Called by the C++ layer once its ready to rock."""
|
||||
assert _babase.in_logic_thread()
|
||||
assert not self._app_bootstrapping_complete
|
||||
self._app_bootstrapping_complete = True
|
||||
self._update_state()
|
||||
|
||||
def on_app_launching(self) -> None:
|
||||
"""Called when the app enters the launching state.
|
||||
|
||||
Here we can put together subsystems and other pieces for the
|
||||
app, but most things should not be doing any work yet.
|
||||
"""
|
||||
# pylint: disable=cyclic-import
|
||||
from babase import _asyncio
|
||||
from babase import _appconfig
|
||||
from babase._apputils import AppHealthMonitor
|
||||
from babase import _env
|
||||
|
||||
assert _babase.in_logic_thread()
|
||||
|
||||
_env.on_app_launching()
|
||||
|
||||
self._aioloop = _asyncio.setup_asyncio()
|
||||
self.health_monitor = AppHealthMonitor()
|
||||
|
||||
# Only proceed 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.classic is not None:
|
||||
handled = self.classic.show_config_error_window()
|
||||
if handled:
|
||||
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)
|
||||
|
||||
# __FEATURESET_APP_SUBSYSTEM_CREATE_BEGIN__
|
||||
# This section generated by batools.appmodule; do not edit.
|
||||
|
||||
# Poke these attrs to create all our subsystems.
|
||||
_ = self.plus
|
||||
_ = self.classic
|
||||
_ = self.ui_v1
|
||||
|
||||
# __FEATURESET_APP_SUBSYSTEM_CREATE_END__
|
||||
|
||||
self._launch_completed = True
|
||||
self._update_state()
|
||||
|
||||
def on_app_loading(self) -> None:
|
||||
"""Called when the app enters the loading state.
|
||||
|
||||
At this point, all built-in pieces of the app should be in place
|
||||
and can start doing 'work'. Though at a high level, the goal of
|
||||
the app at this point is only to sign in to initial accounts,
|
||||
download workspaces, and otherwise prepare itself to really
|
||||
'run'.
|
||||
"""
|
||||
from babase._apputils import log_dumped_app_state
|
||||
|
||||
assert _babase.in_logic_thread()
|
||||
|
||||
# Get meta-system scanning built-in stuff in the bg.
|
||||
self.meta.start_scan(scan_complete_cb=self.on_meta_scan_complete)
|
||||
|
||||
# If any traceback dumps happened last run, log and clear them.
|
||||
log_dumped_app_state()
|
||||
|
||||
# Inform all app subsystems in the same order they were inited.
|
||||
# Operate on a copy here because subsystems can still be added
|
||||
# at this point.
|
||||
for subsystem in self._subsystems.copy():
|
||||
try:
|
||||
subsystem.on_app_loading()
|
||||
except Exception:
|
||||
logging.exception(
|
||||
'Error in on_app_loading for subsystem %s.', subsystem
|
||||
)
|
||||
|
||||
# Normally plus tells us when initial sign-in is done. If it's
|
||||
# not present, however, we just do that ourself so we can
|
||||
# proceed on to the running state.
|
||||
if self.plus is None:
|
||||
_babase.pushcall(self.on_initial_sign_in_completed)
|
||||
|
||||
def on_meta_scan_complete(self) -> None:
|
||||
"""Called when meta-scan is done doing its thing."""
|
||||
assert _babase.in_logic_thread()
|
||||
|
||||
# Now that we know what's out there, build our final plugin set.
|
||||
self.plugins.on_meta_scan_complete()
|
||||
|
||||
assert not self._meta_scan_completed
|
||||
self._meta_scan_completed = True
|
||||
self._update_state()
|
||||
|
||||
def on_app_running(self) -> None:
|
||||
"""Called when the app enters the running state.
|
||||
|
||||
At this point, all workspaces, initial accounts, etc. are in place
|
||||
and we can actually get started doing whatever we're gonna do.
|
||||
"""
|
||||
assert _babase.in_logic_thread()
|
||||
|
||||
# Let our native layer know.
|
||||
_babase.on_app_running()
|
||||
|
||||
# Set a default app-mode-selector. Plugins can then override
|
||||
# this if they want in the on_app_running callback below.
|
||||
self.mode_selector = self.DefaultAppModeSelector()
|
||||
|
||||
# Inform all app subsystems in the same order they were inited.
|
||||
# Operate on a copy here because subsystems can still be added
|
||||
# at this point.
|
||||
for subsystem in self._subsystems.copy():
|
||||
try:
|
||||
subsystem.on_app_running()
|
||||
except Exception:
|
||||
logging.exception(
|
||||
'Error in on_app_running for subsystem %s.', subsystem
|
||||
)
|
||||
|
||||
# Cut off new subsystem additions at this point.
|
||||
self._subsystem_registration_ended = True
|
||||
|
||||
# If 'exec' code was provided to the app, always kick that off
|
||||
# here as an intent.
|
||||
exec_cmd = _babase.exec_arg()
|
||||
if exec_cmd is not None:
|
||||
self.set_intent(AppIntentExec(exec_cmd))
|
||||
elif self._pending_intent is None:
|
||||
# Otherwise tell the app to do its default thing *only* if a
|
||||
# plugin hasn't already told it to do something.
|
||||
self.set_intent(AppIntentDefault())
|
||||
|
||||
def push_apply_app_config(self) -> None:
|
||||
"""Internal. Use app.config.apply() to apply app config changes."""
|
||||
# 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.
|
||||
self._pending_apply_app_config = True
|
||||
_babase.pushcall(self._apply_app_config, raw=True)
|
||||
|
||||
def _apply_app_config(self) -> None:
|
||||
assert _babase.in_logic_thread()
|
||||
|
||||
_babase.lifecyclelog('apply-app-config')
|
||||
|
||||
# If multiple apply calls have been made, only actually apply once.
|
||||
if not self._pending_apply_app_config:
|
||||
return
|
||||
|
||||
_pending_apply_app_config = False
|
||||
|
||||
# Inform all app subsystems in the same order they were inited.
|
||||
# Operate on a copy here because subsystems may still be able to
|
||||
# be added at this point.
|
||||
for subsystem in self._subsystems.copy():
|
||||
try:
|
||||
subsystem.do_apply_app_config()
|
||||
except Exception:
|
||||
logging.exception(
|
||||
'Error in do_apply_app_config for subsystem %s.', subsystem
|
||||
)
|
||||
|
||||
# Let the native layer do its thing.
|
||||
_babase.do_apply_app_config()
|
||||
|
||||
class DefaultAppModeSelector(AppModeSelector):
|
||||
"""Decides which app modes to use to handle intents.
|
||||
|
||||
The behavior here is generated by the project updater based on
|
||||
the set of feature-sets in the project. Spinoff projects can
|
||||
also inject their own behavior by replacing the text
|
||||
'__GOOD_PLACE_FOR_CUSTOM_SPINOFF_LOGIC__' with their own code
|
||||
through spinoff filtering.
|
||||
|
||||
It is also possible to modify mode selection behavior by writing
|
||||
a custom AppModeSelector class and replacing app.mode_selector
|
||||
with an instance of it. This is a good way to go if you are
|
||||
modifying app behavior with a plugin instead of in a spinoff
|
||||
project.
|
||||
"""
|
||||
|
||||
def app_mode_for_intent(self, intent: AppIntent) -> type[AppMode]:
|
||||
# pylint: disable=cyclic-import
|
||||
|
||||
# __GOOD_PLACE_FOR_CUSTOM_SPINOFF_LOGIC__
|
||||
|
||||
# __DEFAULT_APP_MODE_SELECTION_BEGIN__
|
||||
# This section generated by batools.appmodule; do not edit.
|
||||
|
||||
# Hmm; need to think about how we auto-construct this; how
|
||||
# should we determine which app modes to check and in what
|
||||
# order?
|
||||
import bascenev1
|
||||
|
||||
import babase
|
||||
|
||||
if bascenev1.SceneV1AppMode.supports_intent(intent):
|
||||
return bascenev1.SceneV1AppMode
|
||||
|
||||
if babase.EmptyAppMode.supports_intent(intent):
|
||||
return babase.EmptyAppMode
|
||||
|
||||
raise RuntimeError(f'No handler found for intent {type(intent)}.')
|
||||
|
||||
# __DEFAULT_APP_MODE_SELECTION_END__
|
||||
|
||||
def _update_state(self) -> None:
|
||||
# pylint: disable=too-many-branches
|
||||
assert _babase.in_logic_thread()
|
||||
|
||||
if self._app_paused:
|
||||
# Entering paused state:
|
||||
if self.state is not self.State.PAUSED:
|
||||
self.state = self.State.PAUSED
|
||||
self.on_app_pause()
|
||||
else:
|
||||
# Leaving paused state:
|
||||
if self.state is self.State.PAUSED:
|
||||
self.on_app_resume()
|
||||
|
||||
# Handle initially entering or returning to other states.
|
||||
if self._initial_sign_in_completed and self._meta_scan_completed:
|
||||
if self.state != self.State.RUNNING:
|
||||
self.state = self.State.RUNNING
|
||||
_babase.lifecyclelog('app state running')
|
||||
if not self._called_on_app_running:
|
||||
self._called_on_app_running = True
|
||||
self.on_app_running()
|
||||
elif self._launch_completed:
|
||||
if self.state is not self.State.LOADING:
|
||||
self.state = self.State.LOADING
|
||||
_babase.lifecyclelog('app 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._app_bootstrapping_complete
|
||||
if self.state is not self.State.LAUNCHING:
|
||||
self.state = self.State.LAUNCHING
|
||||
_babase.lifecyclelog('app state launching')
|
||||
if not self._called_on_app_launching:
|
||||
self._called_on_app_launching = True
|
||||
self.on_app_launching()
|
||||
|
||||
def pause(self) -> None:
|
||||
"""Should be called by the native layer when the app pauses."""
|
||||
assert not self._app_paused # Should avoid redundant calls.
|
||||
self._app_paused = True
|
||||
self._update_state()
|
||||
|
||||
def resume(self) -> None:
|
||||
"""Should be called by the native layer when the app resumes."""
|
||||
assert self._app_paused # Should avoid redundant calls.
|
||||
self._app_paused = False
|
||||
self._update_state()
|
||||
|
||||
def on_app_pause(self) -> None:
|
||||
"""Called when the app goes to a paused state."""
|
||||
assert _babase.in_logic_thread()
|
||||
|
||||
# Pause all app subsystems in the opposite order they were inited.
|
||||
for subsystem in reversed(self._subsystems):
|
||||
try:
|
||||
subsystem.on_app_pause()
|
||||
except Exception:
|
||||
logging.exception(
|
||||
'Error in on_app_pause for subsystem %s.', subsystem
|
||||
)
|
||||
|
||||
def on_app_resume(self) -> None:
|
||||
"""Called when resuming."""
|
||||
assert _babase.in_logic_thread()
|
||||
self.fg_state += 1
|
||||
|
||||
# Resume all app subsystems in the same order they were inited.
|
||||
for subsystem in self._subsystems:
|
||||
try:
|
||||
subsystem.on_app_resume()
|
||||
except Exception:
|
||||
logging.exception(
|
||||
'Error in on_app_resume for subsystem %s.', subsystem
|
||||
)
|
||||
|
||||
def on_app_shutdown(self) -> None:
|
||||
"""(internal)"""
|
||||
assert _babase.in_logic_thread()
|
||||
self.state = self.State.SHUTTING_DOWN
|
||||
|
||||
# Shutdown all app subsystems in the opposite order they were
|
||||
# inited.
|
||||
for subsystem in reversed(self._subsystems):
|
||||
try:
|
||||
subsystem.on_app_shutdown()
|
||||
except Exception:
|
||||
logging.exception(
|
||||
'Error in on_app_shutdown for subsystem %s.', subsystem
|
||||
)
|
||||
|
||||
def read_config(self) -> None:
|
||||
"""(internal)"""
|
||||
from babase._appconfig import read_app_config
|
||||
|
||||
self._config, self.config_file_healthy = read_app_config()
|
||||
|
||||
def handle_deep_link(self, url: str) -> None:
|
||||
"""Handle a deep link URL."""
|
||||
from babase._language import Lstr
|
||||
|
||||
assert _babase.in_logic_thread()
|
||||
|
||||
appname = _babase.appname()
|
||||
if url.startswith(f'{appname}://code/'):
|
||||
code = url.replace(f'{appname}://code/', '')
|
||||
if self.classic is not None:
|
||||
self.classic.accounts.add_pending_promo_code(code)
|
||||
else:
|
||||
try:
|
||||
_babase.screenmessage(
|
||||
Lstr(resource='errorText'), color=(1, 0, 0)
|
||||
)
|
||||
_babase.getsimplesound('error').play()
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
def on_initial_sign_in_completed(self) -> None:
|
||||
"""Callback to be run after initial sign-in (or lack thereof).
|
||||
|
||||
This normally gets called by the plus subsystem.
|
||||
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.
|
||||
"""
|
||||
assert _babase.in_logic_thread()
|
||||
assert not self._initial_sign_in_completed
|
||||
|
||||
# Tell meta it can start scanning extra stuff that just showed up
|
||||
# (namely account workspaces).
|
||||
self.meta.start_extra_scan()
|
||||
|
||||
self._initial_sign_in_completed = True
|
||||
self._update_state()
|
||||
|
|
@ -5,7 +5,7 @@ from __future__ import annotations
|
|||
|
||||
from typing import TYPE_CHECKING, TypeVar, cast
|
||||
|
||||
import _ba
|
||||
import _babase
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Callable, Any
|
||||
|
|
@ -19,19 +19,21 @@ class AppComponentSubsystem:
|
|||
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.
|
||||
functionality for the app, and allows plugins or other custom code
|
||||
to easily override said functionality.
|
||||
|
||||
Use ba.app.components to get the single shared instance of this class.
|
||||
Access the single shared instance of this class at
|
||||
babase.app.components.
|
||||
|
||||
The general idea with this setup is that a base-class is defined to
|
||||
provide some functionality and then anyone wanting that functionality
|
||||
uses the getclass() method with that base class to return the current
|
||||
registered implementation. The user should not know or care whether
|
||||
they are getting the base class itself or some other implementation.
|
||||
The general idea with this setup is that a base-class Foo is defined
|
||||
to provide some functionality and then anyone wanting that
|
||||
functionality calls getclass(Foo) to return the current registered
|
||||
implementation. The user should not know or care whether they are
|
||||
getting Foo itself or some subclass of it.
|
||||
|
||||
Change-callbacks can also be requested for base classes which will
|
||||
fire in a deferred manner when particular base-classes are overridden.
|
||||
fire in a deferred manner when particular base-classes are
|
||||
overridden.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
|
|
@ -45,9 +47,10 @@ class AppComponentSubsystem:
|
|||
|
||||
The provided implementation class must be a subclass of baseclass.
|
||||
"""
|
||||
# Currently limiting this to logic-thread use; can revisit if needed
|
||||
# (would need to guard access to our implementations dict).
|
||||
assert _ba.in_logic_thread()
|
||||
# Currently limiting this to logic-thread use; can revisit if
|
||||
# needed (would need to guard access to our implementations
|
||||
# dict).
|
||||
assert _babase.in_logic_thread()
|
||||
|
||||
if not issubclass(implementation, baseclass):
|
||||
raise TypeError(
|
||||
|
|
@ -58,18 +61,19 @@ class AppComponentSubsystem:
|
|||
self._implementations[baseclass] = implementation
|
||||
|
||||
# If we're the first thing getting dirtied, set up a callback to
|
||||
# clean everything. And add ourself to the dirty list regardless.
|
||||
# clean everything. And add ourself to the dirty list
|
||||
# regardless.
|
||||
if not self._dirty_base_classes:
|
||||
_ba.pushcall(self._run_change_callbacks)
|
||||
_babase.pushcall(self._run_change_callbacks)
|
||||
self._dirty_base_classes.add(baseclass)
|
||||
|
||||
def getclass(self, baseclass: T) -> T:
|
||||
"""Given a base-class, return the currently set implementation class.
|
||||
"""Given a base-class, return the current implementation class.
|
||||
|
||||
If no custom implementation has been set, the provided base-class
|
||||
is returned.
|
||||
If no custom implementation has been set, the provided
|
||||
base-class is returned.
|
||||
"""
|
||||
assert _ba.in_logic_thread()
|
||||
assert _babase.in_logic_thread()
|
||||
|
||||
del baseclass # Unused.
|
||||
return cast(T, None)
|
||||
|
|
@ -77,13 +81,13 @@ class AppComponentSubsystem:
|
|||
def register_change_callback(
|
||||
self, baseclass: T, callback: Callable[[T], None]
|
||||
) -> None:
|
||||
"""Register a callback to fire when a class implementation changes.
|
||||
"""Register a callback to fire on class implementation changes.
|
||||
|
||||
The callback will be scheduled to run in the logic thread event
|
||||
loop. Note that any further setclass calls before the callback
|
||||
runs will not result in additional callbacks.
|
||||
"""
|
||||
assert _ba.in_logic_thread()
|
||||
assert _babase.in_logic_thread()
|
||||
self._change_callbacks.setdefault(baseclass, []).append(callback)
|
||||
|
||||
def _run_change_callbacks(self) -> None:
|
||||
|
|
@ -5,11 +5,13 @@ from __future__ import annotations
|
|||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import _ba
|
||||
import _babase
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any
|
||||
|
||||
_g_pending_apply = False # pylint: disable=invalid-name
|
||||
|
||||
|
||||
class AppConfig(dict):
|
||||
"""A special dict that holds the game's persistent configuration values.
|
||||
|
|
@ -20,7 +22,7 @@ class AppConfig(dict):
|
|||
defaults, applying contained values to the game, and committing the
|
||||
config to storage.
|
||||
|
||||
Call ba.appconfig() to get the single shared instance of this 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).
|
||||
|
|
@ -33,32 +35,33 @@ class AppConfig(dict):
|
|||
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 ba.App to access actual live state.
|
||||
config. Use babase.App to access actual live state.
|
||||
|
||||
Raises an Exception for unrecognized key names. To get the list of keys
|
||||
supported by this method, use ba.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.
|
||||
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.
|
||||
"""
|
||||
return _ba.resolve_appconfig_value(key)
|
||||
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 ba.AppConfig.resolve() if
|
||||
the key is not present in the config dict or of an incompatible type.
|
||||
This is the value that will be returned by babase.AppConfig.resolve()
|
||||
if the key is not present in the config dict or of an incompatible
|
||||
type.
|
||||
|
||||
Raises an Exception for unrecognized key names. To get the list of keys
|
||||
supported by this method, use ba.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.
|
||||
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.
|
||||
"""
|
||||
return _ba.get_appconfig_default_value(key)
|
||||
return _babase.get_appconfig_default_value(key)
|
||||
|
||||
def builtin_keys(self) -> list[str]:
|
||||
"""Return the list of valid key names recognized by ba.AppConfig.
|
||||
"""Return the list of valid key names recognized by babase.AppConfig.
|
||||
|
||||
This set of keys can be used with resolve(), default_value(), etc.
|
||||
It does not vary across platforms and may include keys that are
|
||||
|
|
@ -70,11 +73,15 @@ class AppConfig(dict):
|
|||
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 _ba.get_appconfig_builtin_keys()
|
||||
return _babase.get_appconfig_builtin_keys()
|
||||
|
||||
def apply(self) -> None:
|
||||
"""Apply config values to the running app."""
|
||||
_ba.apply_config()
|
||||
"""Apply config values to the running app.
|
||||
|
||||
This call is thread-safe and asynchronous; changes will happen
|
||||
in the next logic event loop cycle.
|
||||
"""
|
||||
_babase.app.push_apply_app_config()
|
||||
|
||||
def commit(self) -> None:
|
||||
"""Commits the config to local storage.
|
||||
|
|
@ -93,17 +100,16 @@ class AppConfig(dict):
|
|||
self.commit()
|
||||
|
||||
|
||||
def read_config() -> tuple[AppConfig, bool]:
|
||||
"""Read the game config."""
|
||||
def read_app_config() -> tuple[AppConfig, bool]:
|
||||
"""Read the app config."""
|
||||
import os
|
||||
import json
|
||||
from ba._generated.enums import TimeType
|
||||
|
||||
config_file_healthy = False
|
||||
|
||||
# NOTE: it is assumed that this only gets called once and the
|
||||
# config object will not change from here on out
|
||||
config_file_path = _ba.app.config_file_path
|
||||
config_file_path = _babase.app.config_file_path
|
||||
config_contents = ''
|
||||
try:
|
||||
if os.path.exists(config_file_path):
|
||||
|
|
@ -118,7 +124,7 @@ def read_config() -> tuple[AppConfig, bool]:
|
|||
print(
|
||||
(
|
||||
'error reading config file at time '
|
||||
+ str(_ba.time(TimeType.REAL))
|
||||
+ str(_babase.apptime())
|
||||
+ ': \''
|
||||
+ config_file_path
|
||||
+ '\':\n'
|
||||
|
|
@ -166,12 +172,13 @@ def commit_app_config(force: bool = False) -> None:
|
|||
|
||||
(internal)
|
||||
"""
|
||||
from ba._internal import mark_config_dirty
|
||||
plus = _babase.app.plus
|
||||
assert plus is not None
|
||||
|
||||
if not _ba.app.config_file_healthy and not force:
|
||||
if not _babase.app.config_file_healthy and not force:
|
||||
print(
|
||||
'Current config file is broken; '
|
||||
'skipping write to avoid losing settings.'
|
||||
)
|
||||
return
|
||||
mark_config_dirty()
|
||||
plus.mark_config_dirty()
|
||||
27
dist/ba_data/python/babase/_appintent.py
vendored
Normal file
27
dist/ba_data/python/babase/_appintent.py
vendored
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Provides AppIntent functionality."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
pass
|
||||
|
||||
|
||||
class AppIntent:
|
||||
"""A high level directive given to the app.
|
||||
|
||||
Category: **App Classes**
|
||||
"""
|
||||
|
||||
|
||||
class AppIntentDefault(AppIntent):
|
||||
"""Tells the app to simply run in its default mode."""
|
||||
|
||||
|
||||
class AppIntentExec(AppIntent):
|
||||
"""Tells the app to exec some Python code."""
|
||||
|
||||
def __init__(self, code: str):
|
||||
self.code = code
|
||||
35
dist/ba_data/python/babase/_appmode.py
vendored
Normal file
35
dist/ba_data/python/babase/_appmode.py
vendored
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Provides AppMode functionality."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from babase._appintent import AppIntent
|
||||
|
||||
|
||||
class AppMode:
|
||||
"""A high level mode for the app.
|
||||
|
||||
Category: **App Classes**
|
||||
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def supports_intent(cls, intent: AppIntent) -> bool:
|
||||
"""Return whether our mode can handle the provided intent."""
|
||||
del intent
|
||||
|
||||
# Say no to everything by default. Let's make mode explicitly
|
||||
# lay out everything they *do* support.
|
||||
return False
|
||||
|
||||
def handle_intent(self, intent: AppIntent) -> None:
|
||||
"""Handle an intent."""
|
||||
|
||||
def on_activate(self) -> None:
|
||||
"""Called when the mode is being activated."""
|
||||
|
||||
def on_deactivate(self) -> None:
|
||||
"""Called when the mode is being deactivated."""
|
||||
32
dist/ba_data/python/babase/_appmodeselector.py
vendored
Normal file
32
dist/ba_data/python/babase/_appmodeselector.py
vendored
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Provides AppMode functionality."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from babase._appintent import AppIntent
|
||||
from babase._appmode import AppMode
|
||||
|
||||
|
||||
class AppModeSelector:
|
||||
"""Defines which AppModes to use to handle given AppIntents.
|
||||
|
||||
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 this.
|
||||
"""
|
||||
|
||||
def app_mode_for_intent(self, intent: AppIntent) -> type[AppMode]:
|
||||
"""Given an AppIntent, return the AppMode that should handle it.
|
||||
|
||||
If None is returned, the AppIntent will be ignored.
|
||||
|
||||
This is called in a background thread, so avoid any calls
|
||||
limited to logic thread use/etc.
|
||||
"""
|
||||
raise RuntimeError('app_mode_for_intent() should be overridden.')
|
||||
52
dist/ba_data/python/babase/_appsubsystem.py
vendored
Normal file
52
dist/ba_data/python/babase/_appsubsystem.py
vendored
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Provides the AppSubsystem base class."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import _babase
|
||||
|
||||
if TYPE_CHECKING:
|
||||
pass
|
||||
|
||||
|
||||
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 some conveniences such
|
||||
as predefined callbacks during app state changes.
|
||||
|
||||
Subsystems must be registered with the app before it completes its
|
||||
transition to the 'running' state.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
_babase.app.register_subsystem(self)
|
||||
|
||||
def on_app_loading(self) -> None:
|
||||
"""Called when the app reaches the loading state.
|
||||
|
||||
Note that subsystems created after the app switches to the
|
||||
loading state will not receive this callback. Subsystems created
|
||||
by plugins are an example of this.
|
||||
"""
|
||||
|
||||
def on_app_running(self) -> None:
|
||||
"""Called when the app reaches the running state."""
|
||||
|
||||
def on_app_pause(self) -> None:
|
||||
"""Called when the app enters the paused state."""
|
||||
|
||||
def on_app_resume(self) -> None:
|
||||
"""Called when the app exits the paused state."""
|
||||
|
||||
def on_app_shutdown(self) -> None:
|
||||
"""Called when the app is shutting down."""
|
||||
|
||||
def do_apply_app_config(self) -> None:
|
||||
"""Called when the app config should be applied."""
|
||||
|
|
@ -10,13 +10,16 @@ from threading import Thread
|
|||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from efro.call import tpartial
|
||||
from efro.log import LogLevel
|
||||
from efro.dataclassio import ioprepped, dataclass_to_json, dataclass_from_json
|
||||
import _ba
|
||||
import _babase
|
||||
from babase._appsubsystem import AppSubsystem
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any, TextIO
|
||||
import ba
|
||||
|
||||
import babase
|
||||
|
||||
|
||||
def is_browser_likely_available() -> bool:
|
||||
|
|
@ -24,118 +27,136 @@ def is_browser_likely_available() -> bool:
|
|||
|
||||
category: General Utility Functions
|
||||
|
||||
If this returns False you may want to avoid calling ba.show_url()
|
||||
If this returns False you may want to avoid calling babase.show_url()
|
||||
with any lengthy addresses. (ba.show_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.)
|
||||
"""
|
||||
app = _ba.app
|
||||
platform = app.platform
|
||||
touchscreen = _ba.getinputdevice('TouchScreen', '#1', doraise=False)
|
||||
app = _babase.app
|
||||
|
||||
if app.classic is None:
|
||||
logging.warning(
|
||||
'is_browser_likely_available() needs to be updated'
|
||||
' to work without classic.'
|
||||
)
|
||||
return True
|
||||
|
||||
platform = app.classic.platform
|
||||
hastouchscreen = _babase.hastouchscreen()
|
||||
|
||||
# If we're on a vr device or an android device with no touchscreen,
|
||||
# assume no browser.
|
||||
# FIXME: Might not be the case anymore; should make this definable
|
||||
# at the platform level.
|
||||
if app.vr_mode or (platform == 'android' and touchscreen is None):
|
||||
if app.vr_mode or (platform == 'android' and not hastouchscreen):
|
||||
return False
|
||||
|
||||
# Anywhere else assume we've got one.
|
||||
return True
|
||||
|
||||
|
||||
def get_remote_app_name() -> ba.Lstr:
|
||||
def get_remote_app_name() -> babase.Lstr:
|
||||
"""(internal)"""
|
||||
from ba import _language
|
||||
from babase import _language
|
||||
|
||||
return _language.Lstr(resource='remote_app.app_name')
|
||||
|
||||
|
||||
def should_submit_debug_info() -> bool:
|
||||
"""(internal)"""
|
||||
return _ba.app.config.get('Submit Debug Info', True)
|
||||
return _babase.app.config.get('Submit Debug Info', True)
|
||||
|
||||
|
||||
def handle_v1_cloud_log() -> None:
|
||||
"""Called on debug log prints.
|
||||
"""Called when new messages have been added to v1-cloud-log.
|
||||
|
||||
When this happens, we can upload our log to the server
|
||||
after a short bit if desired.
|
||||
When this happens, we can upload our log to the server after a short
|
||||
bit if desired.
|
||||
"""
|
||||
from ba._net import master_server_post
|
||||
from ba._generated.enums import TimeType
|
||||
from ba._internal import get_news_show
|
||||
|
||||
app = _ba.app
|
||||
app.log_have_new = True
|
||||
if not app.log_upload_timer_started:
|
||||
app = _babase.app
|
||||
classic = app.classic
|
||||
plus = app.plus
|
||||
|
||||
if classic is None or plus is None:
|
||||
if _babase.do_once():
|
||||
logging.warning(
|
||||
'handle_v1_cloud_log should not be getting called'
|
||||
' without classic and plus present.'
|
||||
)
|
||||
return
|
||||
|
||||
classic.log_have_new = True
|
||||
if not classic.log_upload_timer_started:
|
||||
|
||||
def _put_log() -> None:
|
||||
assert plus is not None
|
||||
assert classic is not None
|
||||
try:
|
||||
sessionname = str(_ba.get_foreground_host_session())
|
||||
sessionname = str(classic.get_foreground_host_session())
|
||||
except Exception:
|
||||
sessionname = 'unavailable'
|
||||
try:
|
||||
activityname = str(_ba.get_foreground_host_activity())
|
||||
activityname = str(classic.get_foreground_host_activity())
|
||||
except Exception:
|
||||
activityname = 'unavailable'
|
||||
|
||||
info = {
|
||||
'log': _ba.get_v1_cloud_log(),
|
||||
'log': _babase.get_v1_cloud_log(),
|
||||
'version': app.version,
|
||||
'build': app.build_number,
|
||||
'userAgentString': app.user_agent_string,
|
||||
'userAgentString': classic.legacy_user_agent_string,
|
||||
'session': sessionname,
|
||||
'activity': activityname,
|
||||
'fatal': 0,
|
||||
'userRanCommands': _ba.has_user_run_commands(),
|
||||
'time': _ba.time(TimeType.REAL),
|
||||
'userModded': _ba.workspaces_in_use(),
|
||||
'newsShow': get_news_show(),
|
||||
'userRanCommands': _babase.has_user_run_commands(),
|
||||
'time': _babase.apptime(),
|
||||
'userModded': _babase.workspaces_in_use(),
|
||||
'newsShow': plus.get_news_show(),
|
||||
}
|
||||
|
||||
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
|
||||
if data is not None:
|
||||
app.log_have_new = False
|
||||
_ba.mark_log_sent()
|
||||
classic.log_have_new = False
|
||||
_babase.mark_log_sent()
|
||||
|
||||
master_server_post('bsLog', info, response)
|
||||
classic.master_server_v1_post('bsLog', info, response)
|
||||
|
||||
app.log_upload_timer_started = True
|
||||
classic.log_upload_timer_started = True
|
||||
|
||||
# Delay our log upload slightly in case other
|
||||
# pertinent info gets printed between now and then.
|
||||
with _ba.Context('ui'):
|
||||
_ba.timer(3.0, _put_log, timetype=TimeType.REAL)
|
||||
with _babase.ContextRef.empty():
|
||||
_babase.apptimer(3.0, _put_log)
|
||||
|
||||
# After a while, allow another log-put.
|
||||
def _reset() -> None:
|
||||
app.log_upload_timer_started = False
|
||||
if app.log_have_new:
|
||||
assert classic is not None
|
||||
classic.log_upload_timer_started = False
|
||||
if classic.log_have_new:
|
||||
handle_v1_cloud_log()
|
||||
|
||||
if not _ba.is_log_full():
|
||||
with _ba.Context('ui'):
|
||||
_ba.timer(
|
||||
600.0,
|
||||
_reset,
|
||||
timetype=TimeType.REAL,
|
||||
suppress_format_warning=True,
|
||||
)
|
||||
if not _babase.is_log_full():
|
||||
with _babase.ContextRef.empty():
|
||||
_babase.apptimer(600.0, _reset)
|
||||
|
||||
|
||||
def handle_leftover_v1_cloud_log_file() -> None:
|
||||
"""Handle an un-uploaded v1-cloud-log from a previous run."""
|
||||
|
||||
# Only applies with classic present.
|
||||
if _babase.app.classic is None:
|
||||
return
|
||||
try:
|
||||
import json
|
||||
from ba._net import master_server_post
|
||||
|
||||
if os.path.exists(_ba.get_v1_cloud_log_file_path()):
|
||||
if os.path.exists(_babase.get_v1_cloud_log_file_path()):
|
||||
with open(
|
||||
_ba.get_v1_cloud_log_file_path(), encoding='utf-8'
|
||||
_babase.get_v1_cloud_log_file_path(), encoding='utf-8'
|
||||
) as infile:
|
||||
info = json.loads(infile.read())
|
||||
infile.close()
|
||||
|
|
@ -147,19 +168,21 @@ def handle_leftover_v1_cloud_log_file() -> None:
|
|||
# lets kill it.
|
||||
if data is not None:
|
||||
try:
|
||||
os.remove(_ba.get_v1_cloud_log_file_path())
|
||||
os.remove(_babase.get_v1_cloud_log_file_path())
|
||||
except FileNotFoundError:
|
||||
# Saw this in the wild. The file just existed
|
||||
# a moment ago but I suppose something could have
|
||||
# killed it since. ¯\_(ツ)_/¯
|
||||
pass
|
||||
|
||||
master_server_post('bsLog', info, response)
|
||||
_babase.app.classic.master_server_v1_post(
|
||||
'bsLog', info, response
|
||||
)
|
||||
else:
|
||||
# If they don't want logs uploaded just kill it.
|
||||
os.remove(_ba.get_v1_cloud_log_file_path())
|
||||
os.remove(_babase.get_v1_cloud_log_file_path())
|
||||
except Exception:
|
||||
from ba import _error
|
||||
from babase import _error
|
||||
|
||||
_error.print_exception('Error handling leftover log file.')
|
||||
|
||||
|
|
@ -180,8 +203,8 @@ 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')
|
||||
# if bool(False):
|
||||
# print_live_object_warnings('after session shutdown')
|
||||
|
||||
|
||||
def garbage_collect() -> None:
|
||||
|
|
@ -196,78 +219,24 @@ def garbage_collect() -> None:
|
|||
gc.collect()
|
||||
|
||||
|
||||
def print_live_object_warnings(
|
||||
when: Any,
|
||||
ignore_session: ba.Session | None = None,
|
||||
ignore_activity: ba.Activity | None = None,
|
||||
) -> None:
|
||||
"""Print warnings for remaining objects in the current context.
|
||||
|
||||
IMPORTANT - don't call this in production; usage of gc.get_objects()
|
||||
can bork Python. See notes at top of efro.debug module.
|
||||
"""
|
||||
# pylint: disable=cyclic-import
|
||||
from ba._session import Session
|
||||
from ba._actor import Actor
|
||||
from ba._activity import Activity
|
||||
|
||||
sessions: list[ba.Session] = []
|
||||
activities: list[ba.Activity] = []
|
||||
actors: list[ba.Actor] = []
|
||||
|
||||
# Once we come across leaked stuff, printing again is probably
|
||||
# redundant.
|
||||
if _ba.app.printed_live_object_warning:
|
||||
return
|
||||
for obj in gc.get_objects():
|
||||
if isinstance(obj, Actor):
|
||||
actors.append(obj)
|
||||
elif isinstance(obj, Session):
|
||||
sessions.append(obj)
|
||||
elif isinstance(obj, Activity):
|
||||
activities.append(obj)
|
||||
|
||||
# Complain about any remaining sessions.
|
||||
for session in sessions:
|
||||
if session is ignore_session:
|
||||
continue
|
||||
_ba.app.printed_live_object_warning = True
|
||||
print(f'ERROR: Session found {when}: {session}')
|
||||
|
||||
# Complain about any remaining activities.
|
||||
for activity in activities:
|
||||
if activity is ignore_activity:
|
||||
continue
|
||||
_ba.app.printed_live_object_warning = True
|
||||
print(f'ERROR: Activity found {when}: {activity}')
|
||||
|
||||
# Complain about any remaining actors.
|
||||
for actor in actors:
|
||||
_ba.app.printed_live_object_warning = True
|
||||
print(f'ERROR: Actor found {when}: {actor}')
|
||||
|
||||
|
||||
def print_corrupt_file_error() -> None:
|
||||
"""Print an error if a corrupt file is found."""
|
||||
from ba._general import Call
|
||||
from ba._generated.enums import TimeType
|
||||
|
||||
_ba.timer(
|
||||
2.0,
|
||||
lambda: _ba.screenmessage(
|
||||
_ba.app.lang.get_resource('internal.corruptFileText').replace(
|
||||
'${EMAIL}', 'support@froemling.net'
|
||||
# FIXME - filter this out for builds without bauiv1.
|
||||
if not _babase.app.headless_mode:
|
||||
_babase.apptimer(
|
||||
2.0,
|
||||
lambda: _babase.screenmessage(
|
||||
_babase.app.lang.get_resource(
|
||||
'internal.corruptFileText'
|
||||
).replace('${EMAIL}', 'support@froemling.net'),
|
||||
color=(1, 0, 0),
|
||||
),
|
||||
color=(1, 0, 0),
|
||||
),
|
||||
timetype=TimeType.REAL,
|
||||
)
|
||||
_ba.timer(
|
||||
2.0, Call(_ba.playsound, _ba.getsound('error')), timetype=TimeType.REAL
|
||||
)
|
||||
)
|
||||
_babase.apptimer(2.0, _babase.getsimplesound('error').play)
|
||||
|
||||
|
||||
_tbfiles: list[TextIO] = []
|
||||
_tb_held_files: list[TextIO] = []
|
||||
|
||||
|
||||
@ioprepped
|
||||
|
|
@ -302,7 +271,6 @@ def dump_app_state(
|
|||
"""
|
||||
# pylint: disable=consider-using-with
|
||||
import faulthandler
|
||||
from ba._generated.enums import TimeType
|
||||
|
||||
# Dump our metadata immediately. If a delay is passed, it generally
|
||||
# means we expect things to hang momentarily, so we should not delay
|
||||
|
|
@ -311,14 +279,14 @@ def dump_app_state(
|
|||
# the dump in that case.
|
||||
try:
|
||||
mdpath = os.path.join(
|
||||
os.path.dirname(_ba.app.config_file_path), '_appstate_dump_md'
|
||||
os.path.dirname(_babase.app.config_file_path), '_appstate_dump_md'
|
||||
)
|
||||
with open(mdpath, 'w', encoding='utf-8') as outfile:
|
||||
outfile.write(
|
||||
dataclass_to_json(
|
||||
DumpedAppStateMetadata(
|
||||
reason=reason,
|
||||
app_time=_ba.time(TimeType.REAL),
|
||||
app_time=_babase.apptime(),
|
||||
log_level=log_level,
|
||||
)
|
||||
)
|
||||
|
|
@ -329,14 +297,15 @@ def dump_app_state(
|
|||
return
|
||||
|
||||
tbpath = os.path.join(
|
||||
os.path.dirname(_ba.app.config_file_path), '_appstate_dump_tb'
|
||||
os.path.dirname(_babase.app.config_file_path), '_appstate_dump_tb'
|
||||
)
|
||||
|
||||
tbfile = open(tbpath, 'w', encoding='utf-8')
|
||||
|
||||
# faulthandler needs the raw file descriptor to still be valid when
|
||||
# it fires, so stuff this into a global var to make sure it doesn't get
|
||||
# cleaned up.
|
||||
tbfile = open(tbpath, 'w', encoding='utf-8')
|
||||
_tbfiles.append(tbfile)
|
||||
_tb_held_files.append(tbfile)
|
||||
|
||||
if delay > 0.0:
|
||||
faulthandler.dump_traceback_later(delay, file=tbfile)
|
||||
|
|
@ -347,10 +316,8 @@ def dump_app_state(
|
|||
# Allow sufficient time since we don't know how long the dump takes.
|
||||
# We want this to work from any thread, so need to kick this part
|
||||
# over to the logic thread so timer works.
|
||||
_ba.pushcall(
|
||||
lambda: _ba.timer(
|
||||
delay + 1.0, log_dumped_app_state, timetype=TimeType.REAL
|
||||
),
|
||||
_babase.pushcall(
|
||||
tpartial(_babase.apptimer, delay + 1.0, log_dumped_app_state),
|
||||
from_other_thread=True,
|
||||
suppress_other_thread_warning=True,
|
||||
)
|
||||
|
|
@ -362,20 +329,33 @@ def log_dumped_app_state() -> None:
|
|||
try:
|
||||
out = ''
|
||||
mdpath = os.path.join(
|
||||
os.path.dirname(_ba.app.config_file_path), '_appstate_dump_md'
|
||||
os.path.dirname(_babase.app.config_file_path), '_appstate_dump_md'
|
||||
)
|
||||
if os.path.exists(mdpath):
|
||||
# We may be hanging on to open file descriptors for use by
|
||||
# faulthandler (see above). If we are, we need to clear them
|
||||
# now or else we'll get 'file in use' errors below when we
|
||||
# try to unlink it on windows.
|
||||
for heldfile in _tb_held_files:
|
||||
heldfile.close()
|
||||
_tb_held_files.clear()
|
||||
|
||||
with open(mdpath, 'r', encoding='utf-8') as infile:
|
||||
metadata = dataclass_from_json(
|
||||
DumpedAppStateMetadata, infile.read()
|
||||
)
|
||||
appstatedata = infile.read()
|
||||
|
||||
# Kill the file first in case we can't parse the data; don't
|
||||
# want to get stuck doing this repeatedly.
|
||||
os.unlink(mdpath)
|
||||
|
||||
metadata = dataclass_from_json(DumpedAppStateMetadata, appstatedata)
|
||||
|
||||
out += (
|
||||
f'App state dump:\nReason: {metadata.reason}\n'
|
||||
f'Time: {metadata.app_time:.2f}'
|
||||
)
|
||||
tbpath = os.path.join(
|
||||
os.path.dirname(_ba.app.config_file_path), '_appstate_dump_tb'
|
||||
os.path.dirname(_babase.app.config_file_path),
|
||||
'_appstate_dump_tb',
|
||||
)
|
||||
if os.path.exists(tbpath):
|
||||
with open(tbpath, 'r', encoding='utf-8') as infile:
|
||||
|
|
@ -386,11 +366,12 @@ def log_dumped_app_state() -> None:
|
|||
logging.exception('Error logging dumped app state.')
|
||||
|
||||
|
||||
class AppHealthMonitor:
|
||||
class AppHealthMonitor(AppSubsystem):
|
||||
"""Logs things like app-not-responding issues."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
assert _ba.in_logic_thread()
|
||||
assert _babase.in_logic_thread()
|
||||
super().__init__()
|
||||
self._running = True
|
||||
self._thread = Thread(target=self._app_monitor_thread_main, daemon=True)
|
||||
self._thread.start()
|
||||
|
|
@ -398,14 +379,13 @@ class AppHealthMonitor:
|
|||
self._first_check = True
|
||||
|
||||
def _app_monitor_thread_main(self) -> None:
|
||||
|
||||
try:
|
||||
self._monitor_app()
|
||||
except Exception:
|
||||
logging.exception('Error in AppHealthMonitor thread.')
|
||||
|
||||
def _set_response(self) -> None:
|
||||
assert _ba.in_logic_thread()
|
||||
assert _babase.in_logic_thread()
|
||||
self._response = True
|
||||
|
||||
def _check_running(self) -> bool:
|
||||
|
|
@ -417,7 +397,6 @@ class AppHealthMonitor:
|
|||
import time
|
||||
|
||||
while bool(True):
|
||||
|
||||
# Always sleep a bit between checks.
|
||||
time.sleep(1.234)
|
||||
|
||||
|
|
@ -428,9 +407,8 @@ class AppHealthMonitor:
|
|||
# Wait for the logic thread to run something we send it.
|
||||
starttime = time.monotonic()
|
||||
self._response = False
|
||||
_ba.pushcall(self._set_response, raw=True)
|
||||
_babase.pushcall(self._set_response, raw=True)
|
||||
while not self._response:
|
||||
|
||||
# Abort this check if we went into the background.
|
||||
if not self._check_running():
|
||||
break
|
||||
|
|
@ -459,51 +437,9 @@ class AppHealthMonitor:
|
|||
self._first_check = False
|
||||
|
||||
def on_app_pause(self) -> None:
|
||||
"""Should be called when the app pauses."""
|
||||
assert _ba.in_logic_thread()
|
||||
assert _babase.in_logic_thread()
|
||||
self._running = False
|
||||
|
||||
def on_app_resume(self) -> None:
|
||||
"""Should be called when the app resumes."""
|
||||
assert _ba.in_logic_thread()
|
||||
assert _babase.in_logic_thread()
|
||||
self._running = True
|
||||
|
||||
|
||||
def on_too_many_file_descriptors() -> None:
|
||||
"""Called when too many file descriptors are open; trying to debug."""
|
||||
from ba._generated.enums import TimeType
|
||||
|
||||
real_time = _ba.time(TimeType.REAL)
|
||||
|
||||
def _do_log() -> None:
|
||||
pid = os.getpid()
|
||||
try:
|
||||
fdcount: int | str = len(os.listdir(f'/proc/{pid}/fd'))
|
||||
except Exception as exc:
|
||||
fdcount = f'? ({exc})'
|
||||
logging.warning(
|
||||
'TOO MANY FDS at %.2f. We are pid %d. FDCount is %s.',
|
||||
real_time,
|
||||
pid,
|
||||
fdcount,
|
||||
)
|
||||
|
||||
Thread(target=_do_log, daemon=True).start()
|
||||
|
||||
# import io
|
||||
# from efro.debug import printtypes
|
||||
|
||||
# with io.StringIO() as fstr:
|
||||
# fstr.write('Too many FDs.\n')
|
||||
# printtypes(file=fstr)
|
||||
# fstr.seek(0)
|
||||
# logging.warning(fstr.read())
|
||||
# import socket
|
||||
|
||||
# objs: list[Any] = []
|
||||
# for obj in gc.get_objects():
|
||||
# if isinstance(obj, socket.socket):
|
||||
# objs.append(obj)
|
||||
# test = open('/Users/ericf/.zshrc', 'r', encoding='utf-8')
|
||||
# reveal_type(test)
|
||||
# print('FOUND', len(objs))
|
||||
|
|
@ -21,7 +21,7 @@ from efro.dataclassio import (
|
|||
dataclass_from_json,
|
||||
dataclass_to_json,
|
||||
)
|
||||
import _ba
|
||||
import _babase
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bacommon.assets import AssetPackageFlavor
|
||||
|
|
@ -184,7 +184,13 @@ def fetch_url(url: str, filename: Path, asset_gather: AssetGather) -> None:
|
|||
|
||||
# Pass a very short timeout to urllib so we have opportunities
|
||||
# to cancel even with network blockage.
|
||||
req = urllib.request.urlopen(url, context=_ba.app.net.sslcontext, timeout=1)
|
||||
req = urllib.request.urlopen(
|
||||
urllib.request.Request(
|
||||
url, None, {'User-Agent': _babase.user_agent_string()}
|
||||
),
|
||||
context=_babase.app.net.sslcontext,
|
||||
timeout=1,
|
||||
)
|
||||
file_size = int(req.headers['Content-Length'])
|
||||
print(f'\nDownloading: {filename} Bytes: {file_size:,}')
|
||||
|
||||
|
|
@ -16,10 +16,10 @@ import time
|
|||
import os
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import ba
|
||||
import babase
|
||||
|
||||
# Our timer and event loop for the ballistica logic thread.
|
||||
_asyncio_timer: ba.Timer | None = None
|
||||
_asyncio_timer: babase.AppTimer | None = None
|
||||
_asyncio_event_loop: asyncio.AbstractEventLoop | None = None
|
||||
|
||||
DEBUG_TIMING = os.environ.get('BA_DEBUG_TIMING') == '1'
|
||||
|
|
@ -29,11 +29,10 @@ def setup_asyncio() -> asyncio.AbstractEventLoop:
|
|||
"""Setup asyncio functionality for the logic thread."""
|
||||
# pylint: disable=global-statement
|
||||
|
||||
import _ba
|
||||
import ba
|
||||
from ba._generated.enums import TimeType
|
||||
import _babase
|
||||
import babase
|
||||
|
||||
assert _ba.in_logic_thread()
|
||||
assert _babase.in_logic_thread()
|
||||
|
||||
# Create our event-loop. We don't expect there to be one
|
||||
# running on this thread before we do.
|
||||
|
|
@ -43,9 +42,9 @@ def setup_asyncio() -> asyncio.AbstractEventLoop:
|
|||
except RuntimeError:
|
||||
pass
|
||||
|
||||
global _asyncio_event_loop # pylint: disable=invalid-name
|
||||
global _asyncio_event_loop
|
||||
_asyncio_event_loop = asyncio.new_event_loop()
|
||||
_asyncio_event_loop.set_default_executor(ba.app.threadpool)
|
||||
_asyncio_event_loop.set_default_executor(babase.app.threadpool)
|
||||
|
||||
# Ideally we should integrate asyncio into our C++ Thread class's
|
||||
# low level event loop so that asyncio timers/sockets/etc. could
|
||||
|
|
@ -73,10 +72,8 @@ def setup_asyncio() -> asyncio.AbstractEventLoop:
|
|||
warn_time,
|
||||
)
|
||||
|
||||
global _asyncio_timer # pylint: disable=invalid-name
|
||||
_asyncio_timer = _ba.Timer(
|
||||
1.0 / 30.0, run_cycle, timetype=TimeType.REAL, repeat=True
|
||||
)
|
||||
global _asyncio_timer
|
||||
_asyncio_timer = _babase.AppTimer(1.0 / 30.0, run_cycle, repeat=True)
|
||||
|
||||
if bool(False):
|
||||
|
||||
|
|
@ -87,6 +84,6 @@ def setup_asyncio() -> asyncio.AbstractEventLoop:
|
|||
await asyncio.sleep(2.0)
|
||||
print('TEST AIO TASK ENDING')
|
||||
|
||||
_asyncio_event_loop.create_task(aio_test())
|
||||
_testtask = _asyncio_event_loop.create_task(aio_test())
|
||||
|
||||
return _asyncio_event_loop
|
||||
|
|
@ -7,7 +7,8 @@ from __future__ import annotations
|
|||
import logging
|
||||
from typing import TYPE_CHECKING, overload
|
||||
|
||||
import _ba
|
||||
import _babase
|
||||
from babase._appsubsystem import AppSubsystem
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Callable, Any
|
||||
|
|
@ -22,7 +23,7 @@ DEBUG_LOG = False
|
|||
# internal protocols.
|
||||
|
||||
|
||||
class CloudSubsystem:
|
||||
class CloudSubsystem(AppSubsystem):
|
||||
"""Manages communication with cloud components."""
|
||||
|
||||
def is_connected(self) -> bool:
|
||||
|
|
@ -33,20 +34,17 @@ class CloudSubsystem:
|
|||
"""
|
||||
return False # Needs to be overridden
|
||||
|
||||
def on_app_pause(self) -> None:
|
||||
"""Should be called when the app pauses."""
|
||||
|
||||
def on_app_resume(self) -> None:
|
||||
"""Should be called when the app resumes."""
|
||||
|
||||
def on_connectivity_changed(self, connected: bool) -> None:
|
||||
"""Called when cloud connectivity state changes."""
|
||||
if DEBUG_LOG:
|
||||
logging.debug('CloudSubsystem: Connectivity is now %s.', connected)
|
||||
|
||||
plus = _babase.app.plus
|
||||
assert plus is not None
|
||||
|
||||
# Inform things that use this.
|
||||
# (TODO: should generalize this into some sort of registration system)
|
||||
_ba.app.accounts_v2.on_cloud_connectivity_changed(connected)
|
||||
plus.accounts.on_cloud_connectivity_changed(connected)
|
||||
|
||||
@overload
|
||||
def send_message_cb(
|
||||
|
|
@ -114,11 +112,11 @@ class CloudSubsystem:
|
|||
The provided on_response call will be run in the logic thread
|
||||
and passed either the response or the error that occurred.
|
||||
"""
|
||||
from ba._general import Call
|
||||
from babase._general import Call
|
||||
|
||||
del msg # Unused.
|
||||
|
||||
_ba.pushcall(
|
||||
_babase.pushcall(
|
||||
Call(
|
||||
on_response,
|
||||
RuntimeError('Cloud functionality is not available.'),
|
||||
|
|
@ -155,10 +153,8 @@ def cloud_console_exec(code: str) -> None:
|
|||
"""Called by the cloud console to run code in the logic thread."""
|
||||
import sys
|
||||
import __main__
|
||||
from ba._generated.enums import TimeType
|
||||
|
||||
try:
|
||||
|
||||
# First try it as eval.
|
||||
try:
|
||||
evalcode = compile(code, '<console>', 'eval')
|
||||
|
|
@ -187,7 +183,7 @@ def cloud_console_exec(code: str) -> None:
|
|||
except Exception:
|
||||
import traceback
|
||||
|
||||
apptime = _ba.time(TimeType.REAL)
|
||||
apptime = _babase.apptime()
|
||||
print(f'Exec error at time {apptime:.2f}.', file=sys.stderr)
|
||||
traceback.print_exc()
|
||||
|
||||
37
dist/ba_data/python/babase/_emptyappmode.py
vendored
Normal file
37
dist/ba_data/python/babase/_emptyappmode.py
vendored
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Provides AppMode functionality."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import _babase
|
||||
from babase._appmode import AppMode
|
||||
from babase._appintent import AppIntentExec, AppIntentDefault
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from babase import AppIntent
|
||||
|
||||
|
||||
class EmptyAppMode(AppMode):
|
||||
"""An empty app mode that can be used as a fallback/etc."""
|
||||
|
||||
@classmethod
|
||||
def supports_intent(cls, intent: AppIntent) -> bool:
|
||||
# We support default and exec intents currently.
|
||||
return isinstance(intent, AppIntentExec | AppIntentDefault)
|
||||
|
||||
def handle_intent(self, intent: AppIntent) -> None:
|
||||
if isinstance(intent, AppIntentExec):
|
||||
_babase.empty_app_mode_handle_intent_exec(intent.code)
|
||||
return
|
||||
assert isinstance(intent, AppIntentDefault)
|
||||
_babase.empty_app_mode_handle_intent_default()
|
||||
|
||||
def on_activate(self) -> None:
|
||||
# Let the native layer do its thing.
|
||||
_babase.empty_app_mode_activate()
|
||||
|
||||
def on_deactivate(self) -> None:
|
||||
# Let the native layer do its thing.
|
||||
_babase.empty_app_mode_deactivate()
|
||||
239
dist/ba_data/python/babase/_env.py
vendored
Normal file
239
dist/ba_data/python/babase/_env.py
vendored
Normal file
|
|
@ -0,0 +1,239 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Environment related functionality."""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import signal
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from efro.log import LogLevel
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any
|
||||
from efro.log import LogEntry, LogHandler
|
||||
|
||||
_g_babase_imported = False # pylint: disable=invalid-name
|
||||
_g_babase_app_started = False # pylint: disable=invalid-name
|
||||
|
||||
|
||||
def on_native_module_import() -> None:
|
||||
"""Called when _babase is being imported.
|
||||
|
||||
This code should do as little as possible; we want to defer all
|
||||
environment modifications until we actually commit to running an
|
||||
app.
|
||||
"""
|
||||
import _babase
|
||||
import baenv
|
||||
|
||||
global _g_babase_imported # pylint: disable=global-statement
|
||||
|
||||
assert not _g_babase_imported
|
||||
_g_babase_imported = True
|
||||
|
||||
# If we have a log_handler set up, wire it up to feed _babase its
|
||||
# output.
|
||||
envconfig = baenv.get_config()
|
||||
if envconfig.log_handler is not None:
|
||||
_feed_logs_to_babase(envconfig.log_handler)
|
||||
|
||||
env = _babase.pre_env()
|
||||
|
||||
# Give a soft warning if we're being used with a different binary
|
||||
# version than we were built for.
|
||||
running_build: int = env['build_number']
|
||||
if running_build != baenv.TARGET_BALLISTICA_BUILD:
|
||||
logging.warning(
|
||||
'These scripts are meant to be used with'
|
||||
' Ballistica build %d, but you are running build %d.'
|
||||
" This might cause problems. Module path: '%s'.",
|
||||
baenv.TARGET_BALLISTICA_BUILD,
|
||||
running_build,
|
||||
__file__,
|
||||
)
|
||||
|
||||
debug_build = env['debug_build']
|
||||
|
||||
# We expect dev_mode on in debug builds and off otherwise;
|
||||
# make noise if that's not the case.
|
||||
if debug_build != sys.flags.dev_mode:
|
||||
logging.warning(
|
||||
'Ballistica was built with debug-mode %s'
|
||||
' but Python is running with dev-mode %s;'
|
||||
' this mismatch may cause problems.'
|
||||
' See https://docs.python.org/3/library/devmode.html',
|
||||
debug_build,
|
||||
sys.flags.dev_mode,
|
||||
)
|
||||
|
||||
|
||||
def on_main_thread_start_app() -> None:
|
||||
"""Called in the main thread when we're starting an app.
|
||||
|
||||
We use this opportunity to set up the Python runtime environment
|
||||
as we like it for running our app stuff. This includes things like
|
||||
signal-handling, garbage-collection, and logging.
|
||||
"""
|
||||
import gc
|
||||
import baenv
|
||||
import _babase
|
||||
|
||||
global _g_babase_app_started # pylint: disable=global-statement
|
||||
|
||||
_g_babase_app_started = True
|
||||
|
||||
assert _g_babase_imported
|
||||
assert baenv.config_exists()
|
||||
|
||||
# If we were unable to set paths earlier, complain now.
|
||||
if baenv.did_paths_set_fail():
|
||||
logging.warning(
|
||||
'Ballistica Python paths have not been set. This may cause'
|
||||
' problems. To ensure paths are set, run baenv.configure()'
|
||||
' BEFORE importing any Ballistica modules.'
|
||||
)
|
||||
|
||||
# Set up interrupt-signal handling.
|
||||
|
||||
# Note: I've found we need to set up our C signal handling AFTER
|
||||
# we've told Python to disable its own; otherwise (on Mac at least)
|
||||
# it wipes out our existing C handler.
|
||||
signal.signal(signal.SIGINT, signal.SIG_DFL) # Do default handling.
|
||||
_babase.setup_sigint()
|
||||
|
||||
# Turn off fancy-pants cyclic garbage-collection. We run it only at
|
||||
# explicit times to avoid random hitches and keep things more
|
||||
# deterministic. Non-reference-looped objects will still get cleaned
|
||||
# up immediately, so we should try to structure things to avoid
|
||||
# reference loops (just like Swift, ObjC, etc).
|
||||
|
||||
# FIXME - move this to Python bootstrapping code. or perhaps disable
|
||||
# it completely since we've got more bg stuff happening now?...
|
||||
# (but put safeguards in place to time/minimize gc pauses).
|
||||
gc.disable()
|
||||
|
||||
# pylint: disable=c-extension-no-member
|
||||
if not TYPE_CHECKING:
|
||||
import __main__
|
||||
|
||||
# Clear out the standard quit/exit messages since they don't
|
||||
# work in our embedded situation (should revisit this once we're
|
||||
# usable from a standard interpreter). Note that these don't
|
||||
# exist in the first place for our monolithic builds which don't
|
||||
# use site.py.
|
||||
for attr in ('quit', 'exit'):
|
||||
if hasattr(__main__.__builtins__, attr):
|
||||
delattr(__main__.__builtins__, attr)
|
||||
|
||||
# Also replace standard interactive help with our simplified
|
||||
# non-blocking one which is more friendly to cloud/in-app console
|
||||
# situations.
|
||||
__main__.__builtins__.help = _CustomHelper()
|
||||
|
||||
# On Windows I'm seeing the following error creating asyncio loops
|
||||
# in background threads with the default proactor setup:
|
||||
|
||||
# ValueError: set_wakeup_fd only works in main thread of the main
|
||||
# interpreter.
|
||||
|
||||
# So let's explicitly request selector loops. Interestingly this
|
||||
# error only started showing up once I moved Python init to the main
|
||||
# thread; previously the various asyncio bg thread loops were
|
||||
# working fine (maybe something caused them to default to selector
|
||||
# in that case?..
|
||||
if sys.platform == 'win32':
|
||||
import asyncio
|
||||
|
||||
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
||||
|
||||
|
||||
def on_app_launching() -> None:
|
||||
"""Called when the app reaches the launching state."""
|
||||
import _babase
|
||||
import baenv
|
||||
|
||||
assert _babase.in_logic_thread()
|
||||
|
||||
# Let the user know if the app Python dir is a 'user' one.
|
||||
envconfig = baenv.get_config()
|
||||
if envconfig.is_user_app_python_dir:
|
||||
_babase.screenmessage(
|
||||
f"Using user system scripts: '{envconfig.app_python_dir}'",
|
||||
color=(0.6, 0.6, 1.0),
|
||||
)
|
||||
|
||||
|
||||
def _feed_logs_to_babase(log_handler: LogHandler) -> None:
|
||||
"""Route log/print output to internal ballistica console/etc."""
|
||||
import _babase
|
||||
|
||||
def _on_log(entry: LogEntry) -> None:
|
||||
# Forward this along to the engine to display in the in-app
|
||||
# console, in the Android log, etc.
|
||||
_babase.display_log(
|
||||
name=entry.name,
|
||||
level=entry.level.name,
|
||||
message=entry.message,
|
||||
)
|
||||
|
||||
# We also want to feed some logs to the old v1-cloud-log system.
|
||||
# Let's go with anything warning or higher as well as the
|
||||
# stdout/stderr log messages that babase.app.log_handler creates
|
||||
# for us. We should retire or upgrade this system at some point.
|
||||
if entry.level.value >= LogLevel.WARNING.value or entry.name in (
|
||||
'stdout',
|
||||
'stderr',
|
||||
):
|
||||
_babase.v1_cloud_log(entry.message)
|
||||
|
||||
# Add our callback and also feed it all entries already in the
|
||||
# cache. This will feed the engine any logs that happened between
|
||||
# baenv.configure() and now.
|
||||
|
||||
# FIXME: while this works for now, the downside is that these
|
||||
# callbacks fire in a bg thread so certain things like android
|
||||
# logging will be delayed compared to code that uses native logging
|
||||
# calls directly. Perhaps we should add some sort of 'immediate'
|
||||
# callback option to better handle such cases (similar to the
|
||||
# immediate echofile stderr print that already occurs).
|
||||
log_handler.add_callback(_on_log, feed_existing_logs=True)
|
||||
|
||||
|
||||
class _CustomHelper:
|
||||
"""Replacement 'help' that behaves better for our setup."""
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return 'Type help(object) for help about object.'
|
||||
|
||||
def __call__(self, *args: Any, **kwds: Any) -> Any:
|
||||
# We get an ugly error importing pydoc on our embedded platforms
|
||||
# due to _sysconfigdata_xxx.py not being present (but then
|
||||
# things mostly work). Let's get the ugly error out of the way
|
||||
# explicitly.
|
||||
|
||||
# FIXME: we shouldn't be seeing this error anymore. Should
|
||||
# revisit this.
|
||||
import sysconfig
|
||||
|
||||
try:
|
||||
# This errors once but seems to run cleanly after, so let's
|
||||
# get the error out of the way.
|
||||
sysconfig.get_path('stdlib')
|
||||
except ModuleNotFoundError:
|
||||
pass
|
||||
|
||||
import pydoc
|
||||
|
||||
# Disable pager and interactive help since neither works well
|
||||
# with our funky multi-threaded setup or in-game/cloud consoles.
|
||||
# Let's just do simple text dumps.
|
||||
pydoc.pager = pydoc.plainpager
|
||||
if not args and not kwds:
|
||||
print(
|
||||
'Interactive help is not available in this environment.\n'
|
||||
'Type help(object) for help about object.'
|
||||
)
|
||||
return None
|
||||
return pydoc.help(*args, **kwds)
|
||||
|
|
@ -6,29 +6,10 @@ from __future__ import annotations
|
|||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import _ba
|
||||
import _babase
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any
|
||||
import ba
|
||||
|
||||
|
||||
class DependencyError(Exception):
|
||||
"""Exception raised when one or more ba.Dependency items are missing.
|
||||
|
||||
Category: **Exception Classes**
|
||||
|
||||
(this will generally be missing assets).
|
||||
"""
|
||||
|
||||
def __init__(self, deps: list[ba.Dependency]):
|
||||
super().__init__()
|
||||
self._deps = deps
|
||||
|
||||
@property
|
||||
def deps(self) -> list[ba.Dependency]:
|
||||
"""The list of missing dependencies causing this error."""
|
||||
return self._deps
|
||||
|
||||
|
||||
class ContextError(Exception):
|
||||
|
|
@ -49,28 +30,28 @@ class NotFoundError(Exception):
|
|||
|
||||
|
||||
class PlayerNotFoundError(NotFoundError):
|
||||
"""Exception raised when an expected ba.Player does not exist.
|
||||
"""Exception raised when an expected player does not exist.
|
||||
|
||||
Category: **Exception Classes**
|
||||
"""
|
||||
|
||||
|
||||
class SessionPlayerNotFoundError(NotFoundError):
|
||||
"""Exception raised when an expected ba.SessionPlayer does not exist.
|
||||
"""Exception raised when an expected session-player does not exist.
|
||||
|
||||
Category: **Exception Classes**
|
||||
"""
|
||||
|
||||
|
||||
class TeamNotFoundError(NotFoundError):
|
||||
"""Exception raised when an expected ba.Team does not exist.
|
||||
"""Exception raised when an expected bascenev1.Team does not exist.
|
||||
|
||||
Category: **Exception Classes**
|
||||
"""
|
||||
|
||||
|
||||
class MapNotFoundError(NotFoundError):
|
||||
"""Exception raised when an expected ba.Map does not exist.
|
||||
"""Exception raised when an expected bascenev1.Map does not exist.
|
||||
|
||||
Category: **Exception Classes**
|
||||
"""
|
||||
|
|
@ -84,49 +65,49 @@ class DelegateNotFoundError(NotFoundError):
|
|||
|
||||
|
||||
class SessionTeamNotFoundError(NotFoundError):
|
||||
"""Exception raised when an expected ba.SessionTeam does not exist.
|
||||
"""Exception raised when an expected session-team does not exist.
|
||||
|
||||
Category: **Exception Classes**
|
||||
"""
|
||||
|
||||
|
||||
class NodeNotFoundError(NotFoundError):
|
||||
"""Exception raised when an expected ba.Node does not exist.
|
||||
"""Exception raised when an expected Node does not exist.
|
||||
|
||||
Category: **Exception Classes**
|
||||
"""
|
||||
|
||||
|
||||
class ActorNotFoundError(NotFoundError):
|
||||
"""Exception raised when an expected ba.Actor does not exist.
|
||||
"""Exception raised when an expected actor does not exist.
|
||||
|
||||
Category: **Exception Classes**
|
||||
"""
|
||||
|
||||
|
||||
class ActivityNotFoundError(NotFoundError):
|
||||
"""Exception raised when an expected ba.Activity does not exist.
|
||||
"""Exception raised when an expected bascenev1.Activity does not exist.
|
||||
|
||||
Category: **Exception Classes**
|
||||
"""
|
||||
|
||||
|
||||
class SessionNotFoundError(NotFoundError):
|
||||
"""Exception raised when an expected ba.Session does not exist.
|
||||
"""Exception raised when an expected session does not exist.
|
||||
|
||||
Category: **Exception Classes**
|
||||
"""
|
||||
|
||||
|
||||
class InputDeviceNotFoundError(NotFoundError):
|
||||
"""Exception raised when an expected ba.InputDevice does not exist.
|
||||
"""Exception raised when an expected input-device does not exist.
|
||||
|
||||
Category: **Exception Classes**
|
||||
"""
|
||||
|
||||
|
||||
class WidgetNotFoundError(NotFoundError):
|
||||
"""Exception raised when an expected ba.Widget does not exist.
|
||||
"""Exception raised when an expected widget does not exist.
|
||||
|
||||
Category: **Exception Classes**
|
||||
"""
|
||||
|
|
@ -156,12 +137,12 @@ def print_exception(*args: Any, **keywds: Any) -> None:
|
|||
try:
|
||||
# If we're only printing once and already have, bail.
|
||||
if keywds.get('once', False):
|
||||
if not _ba.do_once():
|
||||
if not _babase.do_once():
|
||||
return
|
||||
|
||||
err_str = ' '.join([str(a) for a in args])
|
||||
print('ERROR:', err_str)
|
||||
_ba.print_context()
|
||||
_babase.print_context()
|
||||
print('PRINTED-FROM:')
|
||||
|
||||
# Basically the output of traceback.print_stack()
|
||||
|
|
@ -174,7 +155,7 @@ def print_exception(*args: Any, **keywds: Any) -> None:
|
|||
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 ba.print_exception():')
|
||||
print('ERROR: exception in babase.print_exception():')
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
|
|
@ -193,15 +174,15 @@ def print_error(err_str: str, once: bool = False) -> None:
|
|||
try:
|
||||
# If we're only printing once and already have, bail.
|
||||
if once:
|
||||
if not _ba.do_once():
|
||||
if not _babase.do_once():
|
||||
return
|
||||
|
||||
print('ERROR:', err_str)
|
||||
_ba.print_context()
|
||||
_babase.print_context()
|
||||
|
||||
# Basically the output of traceback.print_stack()
|
||||
stackstr = ''.join(traceback.format_stack())
|
||||
print(stackstr, end='')
|
||||
except Exception:
|
||||
print('ERROR: exception in ba.print_error():')
|
||||
print('ERROR: exception in babase.print_error():')
|
||||
traceback.print_exc()
|
||||
|
|
@ -7,18 +7,29 @@ import types
|
|||
import weakref
|
||||
import random
|
||||
import inspect
|
||||
from typing import TYPE_CHECKING, TypeVar, Protocol
|
||||
from typing import TYPE_CHECKING, TypeVar, Protocol, NewType
|
||||
|
||||
from efro.terminal import Clr
|
||||
import _ba
|
||||
from ba._error import print_error, print_exception
|
||||
from ba._generated.enums import TimeType
|
||||
import _babase
|
||||
from babase._error import print_error, print_exception
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any
|
||||
from efro.call import Call as Call # 'as Call' so we re-export.
|
||||
|
||||
|
||||
# Declare distinct types for different time measurements we use so the
|
||||
# type-checker can help prevent us from mixing and matching accidentally.
|
||||
|
||||
# Our monotonic time measurement that starts at 0 when the app launches
|
||||
# and pauses while the app is suspended.
|
||||
AppTime = NewType('AppTime', float)
|
||||
|
||||
# Like app-time but incremented at frame draw time and in a smooth
|
||||
# consistent manner; useful to keep animations smooth and jitter-free.
|
||||
DisplayTime = NewType('DisplayTime', float)
|
||||
|
||||
|
||||
class Existable(Protocol):
|
||||
"""A Protocol for objects supporting an exists() method.
|
||||
|
||||
|
|
@ -34,7 +45,7 @@ T = TypeVar('T')
|
|||
|
||||
|
||||
def existing(obj: ExistableT | None) -> ExistableT | None:
|
||||
"""Convert invalid references to None for any ba.Existable object.
|
||||
"""Convert invalid references to None for any babase.Existable object.
|
||||
|
||||
Category: **Gameplay Functions**
|
||||
|
||||
|
|
@ -96,7 +107,7 @@ def json_prep(data: Any) -> Any:
|
|||
try:
|
||||
return data.decode(errors='ignore')
|
||||
except Exception:
|
||||
from ba import _error
|
||||
from babase import _error
|
||||
|
||||
print_error('json_prep encountered utf-8 decode error', once=True)
|
||||
return data.decode(errors='ignore')
|
||||
|
|
@ -147,18 +158,18 @@ class _WeakCall:
|
|||
we overwrite its variable with None because the bound method
|
||||
we pass as a timer callback (foo.bar) strong-references it
|
||||
>>> foo = FooClass()
|
||||
... ba.timer(5.0, foo.bar)
|
||||
... 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()
|
||||
... ba.timer(5.0, ba.WeakCall(foo.bar))
|
||||
... babase.apptimer(5.0, ba.WeakCall(foo.bar))
|
||||
... foo = None
|
||||
|
||||
**EXAMPLE C:** Wrap a method call with some positional and keyword args:
|
||||
>>> myweakcall = ba.WeakCall(self.dostuff, argval1,
|
||||
>>> 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)
|
||||
|
|
@ -171,6 +182,8 @@ class _WeakCall:
|
|||
to wrap them in weakrefs manually if desired.
|
||||
"""
|
||||
|
||||
_did_invalid_call_warning = False
|
||||
|
||||
def __init__(self, *args: Any, **keywds: Any) -> None:
|
||||
"""Instantiate a WeakCall.
|
||||
|
||||
|
|
@ -180,21 +193,21 @@ class _WeakCall:
|
|||
if hasattr(args[0], '__func__'):
|
||||
self._call = WeakMethod(args[0])
|
||||
else:
|
||||
app = _ba.app
|
||||
if not app.did_weak_call_warning:
|
||||
app = _babase.app
|
||||
if not self._did_invalid_call_warning:
|
||||
print(
|
||||
(
|
||||
'Warning: callable passed to ba.WeakCall() is not'
|
||||
'Warning: callable passed to babase.WeakCall() is not'
|
||||
' weak-referencable ('
|
||||
+ str(args[0])
|
||||
+ '); use ba.Call() instead to avoid this '
|
||||
+ '); use babase.Call() instead to avoid this '
|
||||
'warning. Stack-trace:'
|
||||
)
|
||||
)
|
||||
import traceback
|
||||
|
||||
traceback.print_stack()
|
||||
app.did_weak_call_warning = True
|
||||
self._did_invalid_call_warning = True
|
||||
self._call = args[0]
|
||||
self._args = args[1:]
|
||||
self._keywds = keywds
|
||||
|
|
@ -224,7 +237,7 @@ class _Call:
|
|||
|
||||
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 ba.WeakCall if you want to pass a method to callback
|
||||
alive too. Use babase.WeakCall if you want to pass a method to callback
|
||||
without keeping its object alive.
|
||||
"""
|
||||
|
||||
|
|
@ -236,7 +249,7 @@ class _Call:
|
|||
|
||||
##### Example
|
||||
Wrap a method call with 1 positional and 1 keyword arg:
|
||||
>>> mycall = ba.Call(myobj.dostuff, argval, namedarg=argval2)
|
||||
>>> 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()
|
||||
|
|
@ -303,18 +316,21 @@ def verify_object_death(obj: object) -> None:
|
|||
|
||||
This can be handy to detect and prevent memory/resource leaks.
|
||||
"""
|
||||
|
||||
try:
|
||||
ref = weakref.ref(obj)
|
||||
except Exception:
|
||||
print_exception('Unable to create weak-ref in verify_object_death')
|
||||
return
|
||||
|
||||
# Use a slight range for our checks so they don't all land at once
|
||||
# if we queue a lot of them.
|
||||
delay = random.uniform(2.0, 5.5)
|
||||
with _ba.Context('ui'):
|
||||
_ba.timer(
|
||||
delay, lambda: _verify_object_death(ref), timetype=TimeType.REAL
|
||||
)
|
||||
|
||||
# Make this timer in an empty context; don't want it dying with the
|
||||
# scene/etc.
|
||||
with _babase.ContextRef.empty():
|
||||
_babase.apptimer(delay, Call(_verify_object_death, ref))
|
||||
|
||||
|
||||
def _verify_object_death(wref: weakref.ref) -> None:
|
||||
|
|
@ -353,7 +369,7 @@ def storagename(suffix: str | None = None) -> str:
|
|||
>>> class MyThingie:
|
||||
... # This will give something like
|
||||
... # '_mymodule_submodule_mythingie_data'.
|
||||
... _STORENAME = ba.storagename('data')
|
||||
... _STORENAME = babase.storagename('data')
|
||||
...
|
||||
... # Use that name to store some data in the Activity we were
|
||||
... # passed.
|
||||
395
dist/ba_data/python/babase/_hooks.py
vendored
Normal file
395
dist/ba_data/python/babase/_hooks.py
vendored
Normal file
|
|
@ -0,0 +1,395 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Snippets of code for use by the internal layer.
|
||||
|
||||
History: originally the engine would dynamically compile/eval various Python
|
||||
code from within C++ code, but the major downside there was that none of it
|
||||
was type-checked so if names or arguments changed it would go unnoticed
|
||||
until it broke at runtime. By instead defining such snippets here and then
|
||||
capturing references to them all at launch it is possible to allow linting
|
||||
and type-checking magic to happen and most issues will be caught immediately.
|
||||
"""
|
||||
# (most of these are self-explanatory)
|
||||
# pylint: disable=missing-function-docstring
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import _babase
|
||||
|
||||
if TYPE_CHECKING:
|
||||
pass
|
||||
|
||||
|
||||
def on_app_bootstrapping_complete() -> None:
|
||||
"""Called by C++ layer when bootstrapping finishes."""
|
||||
_babase.app.on_app_bootstrapping_complete()
|
||||
|
||||
|
||||
def reset_to_main_menu() -> None:
|
||||
# Some high-level event wants us to return to the main menu.
|
||||
# an example of this is re-opening the game after we 'soft' quit it
|
||||
# on Android.
|
||||
if _babase.app.classic is not None:
|
||||
_babase.app.classic.return_to_main_menu_session_gracefully()
|
||||
else:
|
||||
logging.warning('reset_to_main_menu: no-op due to classic not present.')
|
||||
|
||||
|
||||
def set_config_fullscreen_on() -> None:
|
||||
"""The OS has changed our fullscreen state and we should take note."""
|
||||
_babase.app.config['Fullscreen'] = True
|
||||
_babase.app.config.commit()
|
||||
|
||||
|
||||
def set_config_fullscreen_off() -> None:
|
||||
"""The OS has changed our fullscreen state and we should take note."""
|
||||
_babase.app.config['Fullscreen'] = False
|
||||
_babase.app.config.commit()
|
||||
|
||||
|
||||
def not_signed_in_screen_message() -> None:
|
||||
from babase._language import Lstr
|
||||
|
||||
_babase.screenmessage(Lstr(resource='notSignedInErrorText'))
|
||||
|
||||
|
||||
def open_url_with_webbrowser_module(url: str) -> None:
|
||||
"""Show a URL in the browser or print on-screen error if we can't."""
|
||||
import webbrowser
|
||||
from babase._language import Lstr
|
||||
|
||||
try:
|
||||
webbrowser.open(url)
|
||||
except Exception:
|
||||
logging.exception("Error displaying url '%s'.", url)
|
||||
_babase.getsimplesound('error').play()
|
||||
_babase.screenmessage(Lstr(resource='errorText'), color=(1, 0, 0))
|
||||
|
||||
|
||||
def connecting_to_party_message() -> None:
|
||||
from babase._language import Lstr
|
||||
|
||||
_babase.screenmessage(
|
||||
Lstr(resource='internal.connectingToPartyText'), color=(1, 1, 1)
|
||||
)
|
||||
|
||||
|
||||
def rejecting_invite_already_in_party_message() -> None:
|
||||
from babase._language import Lstr
|
||||
|
||||
_babase.screenmessage(
|
||||
Lstr(resource='internal.rejectingInviteAlreadyInPartyText'),
|
||||
color=(1, 0.5, 0),
|
||||
)
|
||||
|
||||
|
||||
def connection_failed_message() -> None:
|
||||
from babase._language import Lstr
|
||||
|
||||
_babase.screenmessage(
|
||||
Lstr(resource='internal.connectionFailedText'), color=(1, 0.5, 0)
|
||||
)
|
||||
|
||||
|
||||
def temporarily_unavailable_message() -> None:
|
||||
from babase._language import Lstr
|
||||
|
||||
if not _babase.app.headless_mode:
|
||||
_babase.getsimplesound('error').play()
|
||||
_babase.screenmessage(
|
||||
Lstr(resource='getTicketsWindow.unavailableTemporarilyText'),
|
||||
color=(1, 0, 0),
|
||||
)
|
||||
|
||||
|
||||
def in_progress_message() -> None:
|
||||
from babase._language import Lstr
|
||||
|
||||
if not _babase.app.headless_mode:
|
||||
_babase.getsimplesound('error').play()
|
||||
_babase.screenmessage(
|
||||
Lstr(resource='getTicketsWindow.inProgressText'),
|
||||
color=(1, 0, 0),
|
||||
)
|
||||
|
||||
|
||||
def error_message() -> None:
|
||||
from babase._language import Lstr
|
||||
|
||||
if not _babase.app.headless_mode:
|
||||
_babase.getsimplesound('error').play()
|
||||
_babase.screenmessage(Lstr(resource='errorText'), color=(1, 0, 0))
|
||||
|
||||
|
||||
def purchase_not_valid_error() -> None:
|
||||
from babase._language import Lstr
|
||||
|
||||
if not _babase.app.headless_mode:
|
||||
_babase.getsimplesound('error').play()
|
||||
_babase.screenmessage(
|
||||
Lstr(
|
||||
resource='store.purchaseNotValidError',
|
||||
subs=[('${EMAIL}', 'support@froemling.net')],
|
||||
),
|
||||
color=(1, 0, 0),
|
||||
)
|
||||
|
||||
|
||||
def purchase_already_in_progress_error() -> None:
|
||||
from babase._language import Lstr
|
||||
|
||||
if not _babase.app.headless_mode:
|
||||
_babase.getsimplesound('error').play()
|
||||
_babase.screenmessage(
|
||||
Lstr(resource='store.purchaseAlreadyInProgressText'),
|
||||
color=(1, 0, 0),
|
||||
)
|
||||
|
||||
|
||||
def uuid_str() -> str:
|
||||
import uuid
|
||||
|
||||
return str(uuid.uuid4())
|
||||
|
||||
|
||||
def orientation_reset_cb_message() -> None:
|
||||
from babase._language import Lstr
|
||||
|
||||
_babase.screenmessage(
|
||||
Lstr(resource='internal.vrOrientationResetCardboardText'),
|
||||
color=(0, 1, 0),
|
||||
)
|
||||
|
||||
|
||||
def orientation_reset_message() -> None:
|
||||
from babase._language import Lstr
|
||||
|
||||
_babase.screenmessage(
|
||||
Lstr(resource='internal.vrOrientationResetText'), color=(0, 1, 0)
|
||||
)
|
||||
|
||||
|
||||
def on_app_pause() -> None:
|
||||
_babase.app.pause()
|
||||
|
||||
|
||||
def on_app_resume() -> None:
|
||||
_babase.app.resume()
|
||||
|
||||
|
||||
def show_post_purchase_message() -> None:
|
||||
assert _babase.app.classic is not None
|
||||
_babase.app.classic.accounts.show_post_purchase_message()
|
||||
|
||||
|
||||
def language_test_toggle() -> None:
|
||||
_babase.app.lang.setlanguage(
|
||||
'Gibberish' if _babase.app.lang.language == 'English' else 'English'
|
||||
)
|
||||
|
||||
|
||||
def award_in_control_achievement() -> None:
|
||||
if _babase.app.classic is not None:
|
||||
_babase.app.classic.ach.award_local_achievement('In Control')
|
||||
else:
|
||||
logging.warning('award_in_control_achievement is no-op without classic')
|
||||
|
||||
|
||||
def award_dual_wielding_achievement() -> None:
|
||||
if _babase.app.classic is not None:
|
||||
_babase.app.classic.ach.award_local_achievement('Dual Wielding')
|
||||
else:
|
||||
logging.warning(
|
||||
'award_dual_wielding_achievement is no-op without classic'
|
||||
)
|
||||
|
||||
|
||||
def play_gong_sound() -> None:
|
||||
if not _babase.app.headless_mode:
|
||||
_babase.getsimplesound('gong').play()
|
||||
|
||||
|
||||
def launch_coop_game(name: str) -> None:
|
||||
assert _babase.app.classic is not None
|
||||
_babase.app.classic.launch_coop_game(name)
|
||||
|
||||
|
||||
def purchases_restored_message() -> None:
|
||||
from babase._language import Lstr
|
||||
|
||||
_babase.screenmessage(
|
||||
Lstr(resource='getTicketsWindow.purchasesRestoredText'), color=(0, 1, 0)
|
||||
)
|
||||
|
||||
|
||||
def unavailable_message() -> None:
|
||||
from babase._language import Lstr
|
||||
|
||||
_babase.screenmessage(
|
||||
Lstr(resource='getTicketsWindow.unavailableText'), color=(1, 0, 0)
|
||||
)
|
||||
|
||||
|
||||
def set_last_ad_network(sval: str) -> None:
|
||||
if _babase.app.classic is not None:
|
||||
_babase.app.classic.ads.last_ad_network = sval
|
||||
_babase.app.classic.ads.last_ad_network_set_time = time.time()
|
||||
|
||||
|
||||
def google_play_purchases_not_available_message() -> None:
|
||||
from babase._language import Lstr
|
||||
|
||||
_babase.screenmessage(
|
||||
Lstr(resource='googlePlayPurchasesNotAvailableText'), color=(1, 0, 0)
|
||||
)
|
||||
|
||||
|
||||
def google_play_services_not_available_message() -> None:
|
||||
from babase._language import Lstr
|
||||
|
||||
_babase.screenmessage(
|
||||
Lstr(resource='googlePlayServicesNotAvailableText'), color=(1, 0, 0)
|
||||
)
|
||||
|
||||
|
||||
def empty_call() -> None:
|
||||
pass
|
||||
|
||||
|
||||
def print_trace() -> None:
|
||||
import traceback
|
||||
|
||||
print('Python Traceback (most recent call last):')
|
||||
traceback.print_stack()
|
||||
|
||||
|
||||
def toggle_fullscreen() -> None:
|
||||
cfg = _babase.app.config
|
||||
cfg['Fullscreen'] = not cfg.resolve('Fullscreen')
|
||||
cfg.apply_and_commit()
|
||||
|
||||
|
||||
def read_config() -> None:
|
||||
_babase.app.read_config()
|
||||
|
||||
|
||||
def ui_remote_press() -> None:
|
||||
"""Handle a press by a remote device that is only usable for nav."""
|
||||
from babase._language import Lstr
|
||||
|
||||
if _babase.app.headless_mode:
|
||||
return
|
||||
|
||||
# Can be called without a context; need a context for getsound.
|
||||
_babase.screenmessage(
|
||||
Lstr(resource='internal.controllerForMenusOnlyText'),
|
||||
color=(1, 0, 0),
|
||||
)
|
||||
_babase.getsimplesound('error').play()
|
||||
|
||||
|
||||
def remove_in_game_ads_message() -> None:
|
||||
if _babase.app.classic is not None:
|
||||
_babase.app.classic.ads.do_remove_in_game_ads_message()
|
||||
|
||||
|
||||
def do_quit() -> None:
|
||||
_babase.quit()
|
||||
|
||||
|
||||
def shutdown() -> None:
|
||||
_babase.app.on_app_shutdown()
|
||||
|
||||
|
||||
def hash_strings(inputs: list[str]) -> str:
|
||||
"""Hash provided strings into a short output string."""
|
||||
import hashlib
|
||||
|
||||
sha = hashlib.sha1()
|
||||
for inp in inputs:
|
||||
sha.update(inp.encode())
|
||||
|
||||
return sha.hexdigest()
|
||||
|
||||
|
||||
def have_account_v2_credentials() -> bool:
|
||||
"""Do we have primary account-v2 credentials set?"""
|
||||
assert _babase.app.plus is not None
|
||||
have: bool = _babase.app.plus.accounts.have_primary_credentials()
|
||||
return have
|
||||
|
||||
|
||||
def implicit_sign_in(
|
||||
login_type_str: str, login_id: str, display_name: str
|
||||
) -> None:
|
||||
"""An implicit login happened."""
|
||||
from bacommon.login import LoginType
|
||||
|
||||
assert _babase.app.plus is not None
|
||||
_babase.app.plus.accounts.on_implicit_sign_in(
|
||||
login_type=LoginType(login_type_str),
|
||||
login_id=login_id,
|
||||
display_name=display_name,
|
||||
)
|
||||
|
||||
|
||||
def implicit_sign_out(login_type_str: str) -> None:
|
||||
"""An implicit logout happened."""
|
||||
from bacommon.login import LoginType
|
||||
|
||||
assert _babase.app.plus is not None
|
||||
_babase.app.plus.accounts.on_implicit_sign_out(
|
||||
login_type=LoginType(login_type_str)
|
||||
)
|
||||
|
||||
|
||||
def login_adapter_get_sign_in_token_response(
|
||||
login_type_str: str, attempt_id_str: str, result_str: str
|
||||
) -> None:
|
||||
"""Login adapter do-sign-in completed."""
|
||||
from bacommon.login import LoginType
|
||||
from babase._login import LoginAdapterNative
|
||||
|
||||
login_type = LoginType(login_type_str)
|
||||
attempt_id = int(attempt_id_str)
|
||||
result = None if result_str == '' else result_str
|
||||
|
||||
assert _babase.app.plus is not None
|
||||
adapter = _babase.app.plus.accounts.login_adapters[login_type]
|
||||
assert isinstance(adapter, LoginAdapterNative)
|
||||
adapter.on_sign_in_complete(attempt_id=attempt_id, result=result)
|
||||
|
||||
|
||||
def show_client_too_old_error() -> None:
|
||||
"""Called at launch if the server tells us we're too old to talk to it."""
|
||||
from babase._language import Lstr
|
||||
|
||||
# If you are using an old build of the app and would like to stop
|
||||
# seeing this error at launch, do:
|
||||
# ba.app.config['SuppressClientTooOldErrorForBuild'] = ba.app.build_number
|
||||
# ba.app.config.commit()
|
||||
# Note that you will have to do that again later if you update to
|
||||
# a newer build.
|
||||
if (
|
||||
_babase.app.config.get('SuppressClientTooOldErrorForBuild')
|
||||
== _babase.app.build_number
|
||||
):
|
||||
return
|
||||
|
||||
if not _babase.app.headless_mode:
|
||||
_babase.getsimplesound('error').play()
|
||||
|
||||
_babase.screenmessage(
|
||||
Lstr(
|
||||
translate=(
|
||||
'serverResponses',
|
||||
'Server functionality is no longer supported'
|
||||
' in this version of the game;\n'
|
||||
'Please update to a newer version.',
|
||||
)
|
||||
),
|
||||
color=(1, 0, 0),
|
||||
)
|
||||
|
|
@ -17,7 +17,7 @@ class 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 ba.Keyboard.
|
||||
On-screen keyboard uses chars from active babase.Keyboard.
|
||||
"""
|
||||
|
||||
name: str
|
||||
|
|
@ -3,35 +3,365 @@
|
|||
"""Language related functionality."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import json
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, overload
|
||||
|
||||
import _ba
|
||||
import _babase
|
||||
from babase._appsubsystem import AppSubsystem
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import ba
|
||||
from typing import Any, Sequence
|
||||
|
||||
import babase
|
||||
|
||||
class LanguageSubsystem:
|
||||
"""Wraps up language related app functionality.
|
||||
|
||||
class LanguageSubsystem(AppSubsystem):
|
||||
"""Language functionality for the app.
|
||||
|
||||
Category: **App Classes**
|
||||
|
||||
To use this class, access the single instance of it at 'ba.app.lang'.
|
||||
Access the single instance of this class at 'babase.app.lang'.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.language_target: AttrDict | None = None
|
||||
self.language_merged: AttrDict | None = None
|
||||
self.default_language = self._get_default_language()
|
||||
super().__init__()
|
||||
self.default_language: str = self._get_default_language()
|
||||
|
||||
self._language: str | None = None
|
||||
self._language_target: AttrDict | None = None
|
||||
self._language_merged: AttrDict | None = None
|
||||
|
||||
@property
|
||||
def locale(self) -> str:
|
||||
"""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.)
|
||||
"""
|
||||
env = _babase.env()
|
||||
assert isinstance(env['locale'], str)
|
||||
return env['locale']
|
||||
|
||||
@property
|
||||
def language(self) -> str:
|
||||
"""The current active language for the app.
|
||||
|
||||
This can be selected explicitly by the user or may be set
|
||||
automatically based on locale or other factors.
|
||||
"""
|
||||
if self._language is None:
|
||||
raise RuntimeError('App language is not yet set.')
|
||||
return self._language
|
||||
|
||||
@property
|
||||
def available_languages(self) -> list[str]:
|
||||
"""A list of all available languages.
|
||||
|
||||
Note that languages that may be present in game assets but which
|
||||
are not displayable on the running version of the game are not
|
||||
included here.
|
||||
"""
|
||||
langs = set()
|
||||
try:
|
||||
names = os.listdir(
|
||||
os.path.join(
|
||||
_babase.app.data_directory, 'ba_data', 'data', 'languages'
|
||||
)
|
||||
)
|
||||
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...
|
||||
for i, name in enumerate(names):
|
||||
if name == 'Chinesetraditional':
|
||||
names[i] = 'ChineseTraditional'
|
||||
except Exception:
|
||||
from babase import _error
|
||||
|
||||
_error.print_exception()
|
||||
names = []
|
||||
for name in names:
|
||||
if self._can_display_language(name):
|
||||
langs.add(name)
|
||||
return sorted(
|
||||
name for name in names if self._can_display_language(name)
|
||||
)
|
||||
|
||||
def setlanguage(
|
||||
self,
|
||||
language: str | None,
|
||||
print_change: bool = True,
|
||||
store_to_config: bool = True,
|
||||
) -> None:
|
||||
"""Set the active app language.
|
||||
|
||||
Pass None to use OS default language.
|
||||
"""
|
||||
# pylint: disable=too-many-locals
|
||||
# pylint: disable=too-many-statements
|
||||
# pylint: disable=too-many-branches
|
||||
assert _babase.in_logic_thread()
|
||||
cfg = _babase.app.config
|
||||
cur_language = cfg.get('Lang', None)
|
||||
|
||||
# Store this in the config if its changing.
|
||||
if language != cur_language and store_to_config:
|
||||
if language is None:
|
||||
if 'Lang' in cfg:
|
||||
del cfg['Lang'] # Clear it out for default.
|
||||
else:
|
||||
cfg['Lang'] = language
|
||||
cfg.commit()
|
||||
switched = True
|
||||
else:
|
||||
switched = False
|
||||
|
||||
with open(
|
||||
os.path.join(
|
||||
_babase.app.data_directory,
|
||||
'ba_data',
|
||||
'data',
|
||||
'languages',
|
||||
'english.json',
|
||||
),
|
||||
encoding='utf-8',
|
||||
) as infile:
|
||||
lenglishvalues = json.loads(infile.read())
|
||||
|
||||
# None implies default.
|
||||
if language is None:
|
||||
language = self.default_language
|
||||
try:
|
||||
if language == 'English':
|
||||
lmodvalues = None
|
||||
else:
|
||||
lmodfile = os.path.join(
|
||||
_babase.app.data_directory,
|
||||
'ba_data',
|
||||
'data',
|
||||
'languages',
|
||||
language.lower() + '.json',
|
||||
)
|
||||
with open(lmodfile, encoding='utf-8') as infile:
|
||||
lmodvalues = json.loads(infile.read())
|
||||
except Exception:
|
||||
logging.exception("Error importing language '%s'.", language)
|
||||
_babase.screenmessage(
|
||||
f"Error setting language to '{language}'; see log for details.",
|
||||
color=(1, 0, 0),
|
||||
)
|
||||
switched = False
|
||||
lmodvalues = None
|
||||
|
||||
self._language = language
|
||||
|
||||
# Create an attrdict of *just* our target language.
|
||||
self._language_target = AttrDict()
|
||||
langtarget = self._language_target
|
||||
assert langtarget is not None
|
||||
_add_to_attr_dict(
|
||||
langtarget, lmodvalues if lmodvalues is not None else lenglishvalues
|
||||
)
|
||||
|
||||
# Create an attrdict of our target language overlaid on our base
|
||||
# (english).
|
||||
languages = [lenglishvalues]
|
||||
if lmodvalues is not None:
|
||||
languages.append(lmodvalues)
|
||||
lfull = AttrDict()
|
||||
for lmod in languages:
|
||||
_add_to_attr_dict(lfull, lmod)
|
||||
self._language_merged = lfull
|
||||
|
||||
# Pass some keys/values in for low level code to use; start with
|
||||
# everything in their 'internal' section.
|
||||
internal_vals = [
|
||||
v for v in list(lfull['internal'].items()) if isinstance(v[1], str)
|
||||
]
|
||||
|
||||
# Cherry-pick various other values to include.
|
||||
# (should probably get rid of the 'internal' section
|
||||
# and do everything this way)
|
||||
for value in [
|
||||
'replayNameDefaultText',
|
||||
'replayWriteErrorText',
|
||||
'replayVersionErrorText',
|
||||
'replayReadErrorText',
|
||||
]:
|
||||
internal_vals.append((value, lfull[value]))
|
||||
internal_vals.append(
|
||||
('axisText', lfull['configGamepadWindow']['axisText'])
|
||||
)
|
||||
internal_vals.append(('buttonText', lfull['buttonText']))
|
||||
lmerged = self._language_merged
|
||||
assert lmerged is not None
|
||||
random_names = [
|
||||
n.strip() for n in lmerged['randomPlayerNamesText'].split(',')
|
||||
]
|
||||
random_names = [n for n in random_names if n != '']
|
||||
_babase.set_internal_language_keys(internal_vals, random_names)
|
||||
if switched and print_change:
|
||||
_babase.screenmessage(
|
||||
Lstr(
|
||||
resource='languageSetText',
|
||||
subs=[
|
||||
('${LANGUAGE}', Lstr(translate=('languages', language)))
|
||||
],
|
||||
),
|
||||
color=(0, 1, 0),
|
||||
)
|
||||
|
||||
def do_apply_app_config(self) -> None:
|
||||
assert _babase.in_logic_thread()
|
||||
assert isinstance(_babase.app.config, dict)
|
||||
lang = _babase.app.config.get('Lang', self.default_language)
|
||||
if lang != self._language:
|
||||
self.setlanguage(lang, print_change=False, store_to_config=False)
|
||||
|
||||
def get_resource(
|
||||
self,
|
||||
resource: str,
|
||||
fallback_resource: str | None = None,
|
||||
fallback_value: Any = None,
|
||||
) -> Any:
|
||||
"""Return a translation resource by name.
|
||||
|
||||
DEPRECATED; use babase.Lstr functionality for these purposes.
|
||||
"""
|
||||
try:
|
||||
# If we have no language set, try and set it to english.
|
||||
# Also make a fuss because we should try to avoid this.
|
||||
if self._language_merged is None:
|
||||
try:
|
||||
if _babase.do_once():
|
||||
logging.warning(
|
||||
'get_resource() called before language'
|
||||
' set; falling back to english.'
|
||||
)
|
||||
self.setlanguage(
|
||||
'English', print_change=False, store_to_config=False
|
||||
)
|
||||
except Exception:
|
||||
logging.exception(
|
||||
'Error setting fallback english language.'
|
||||
)
|
||||
raise
|
||||
|
||||
# If they provided a fallback_resource value, try the
|
||||
# target-language-only dict first and then fall back to
|
||||
# trying the fallback_resource value in the merged dict.
|
||||
if fallback_resource is not None:
|
||||
try:
|
||||
values = self._language_target
|
||||
splits = resource.split('.')
|
||||
dicts = splits[:-1]
|
||||
key = splits[-1]
|
||||
for dct in dicts:
|
||||
assert values is not None
|
||||
values = values[dct]
|
||||
assert values is not None
|
||||
val = values[key]
|
||||
return val
|
||||
except Exception:
|
||||
# FIXME: Shouldn't we try the fallback resource in
|
||||
# the merged dict AFTER we try the main resource in
|
||||
# the merged dict?
|
||||
try:
|
||||
values = self._language_merged
|
||||
splits = fallback_resource.split('.')
|
||||
dicts = splits[:-1]
|
||||
key = splits[-1]
|
||||
for dct in dicts:
|
||||
assert values is not None
|
||||
values = values[dct]
|
||||
assert values is not None
|
||||
val = values[key]
|
||||
return val
|
||||
|
||||
except Exception:
|
||||
# If we got nothing for fallback_resource,
|
||||
# default to the normal code which checks or
|
||||
# primary value in the merge dict; there's a
|
||||
# chance we can get an english value for it
|
||||
# (which we weren't looking for the first time
|
||||
# through).
|
||||
pass
|
||||
|
||||
values = self._language_merged
|
||||
splits = resource.split('.')
|
||||
dicts = splits[:-1]
|
||||
key = splits[-1]
|
||||
for dct in dicts:
|
||||
assert values is not None
|
||||
values = values[dct]
|
||||
assert values is not None
|
||||
val = values[key]
|
||||
return val
|
||||
|
||||
except Exception:
|
||||
# Ok, looks like we couldn't find our main or fallback
|
||||
# resource anywhere. Now if we've been given a fallback
|
||||
# value, return it; otherwise fail.
|
||||
from babase import _error
|
||||
|
||||
if fallback_value is not None:
|
||||
return fallback_value
|
||||
raise _error.NotFoundError(
|
||||
f"Resource not found: '{resource}'"
|
||||
) from None
|
||||
|
||||
def translate(
|
||||
self,
|
||||
category: str,
|
||||
strval: str,
|
||||
raise_exceptions: bool = False,
|
||||
print_errors: bool = False,
|
||||
) -> str:
|
||||
"""Translate a value (or return the value if no translation available)
|
||||
|
||||
DEPRECATED; use babase.Lstr functionality for these purposes.
|
||||
"""
|
||||
try:
|
||||
translated = self.get_resource('translations')[category][strval]
|
||||
except Exception as exc:
|
||||
if raise_exceptions:
|
||||
raise
|
||||
if print_errors:
|
||||
print(
|
||||
(
|
||||
'Translate error: category=\''
|
||||
+ category
|
||||
+ '\' name=\''
|
||||
+ strval
|
||||
+ '\' exc='
|
||||
+ str(exc)
|
||||
+ ''
|
||||
)
|
||||
)
|
||||
translated = None
|
||||
translated_out: str
|
||||
if translated is None:
|
||||
translated_out = strval
|
||||
else:
|
||||
translated_out = translated
|
||||
assert isinstance(translated_out, str)
|
||||
return translated_out
|
||||
|
||||
def is_custom_unicode_char(self, char: str) -> bool:
|
||||
"""Return whether a char is in the custom unicode range we use."""
|
||||
assert isinstance(char, str)
|
||||
if len(char) != 1:
|
||||
raise ValueError('Invalid Input; must be length 1')
|
||||
return 0xE000 <= ord(char) <= 0xF8FF
|
||||
|
||||
def _can_display_language(self, language: str) -> bool:
|
||||
"""Tell whether we can display a particular language.
|
||||
|
||||
On some platforms we don't have unicode rendering yet
|
||||
which limits the languages we can draw.
|
||||
On some platforms we don't have unicode rendering yet which
|
||||
limits the languages we can draw.
|
||||
"""
|
||||
|
||||
# We don't yet support full unicode display on windows or linux :-(.
|
||||
|
|
@ -48,23 +378,11 @@ class LanguageSubsystem:
|
|||
'Thai',
|
||||
'Tamil',
|
||||
}
|
||||
and not _ba.can_display_full_unicode()
|
||||
and not _babase.can_display_full_unicode()
|
||||
):
|
||||
return False
|
||||
return True
|
||||
|
||||
@property
|
||||
def locale(self) -> str:
|
||||
"""Raw country/language code detected by the game (such as 'en_US').
|
||||
|
||||
Generally for language-specific code you should look at
|
||||
ba.App.language, which is the language the game is using
|
||||
(which may differ from locale if the user sets a language, etc.)
|
||||
"""
|
||||
env = _ba.env()
|
||||
assert isinstance(env['locale'], str)
|
||||
return env['locale']
|
||||
|
||||
def _get_default_language(self) -> str:
|
||||
languages = {
|
||||
'ar': 'Arabic',
|
||||
|
|
@ -102,8 +420,9 @@ class LanguageSubsystem:
|
|||
'vi': 'Vietnamese',
|
||||
}
|
||||
|
||||
# Special case for Chinese: map specific variations to traditional.
|
||||
# (otherwise will map to 'Chinese' which is simplified)
|
||||
# Special case for Chinese: map specific variations to
|
||||
# traditional. (otherwise will map to 'Chinese' which is
|
||||
# simplified)
|
||||
if self.locale in ('zh_HANT', 'zh_TW'):
|
||||
language = 'ChineseTraditional'
|
||||
else:
|
||||
|
|
@ -112,340 +431,42 @@ class LanguageSubsystem:
|
|||
language = 'English'
|
||||
return language
|
||||
|
||||
@property
|
||||
def language(self) -> str:
|
||||
"""The name of the language the game is running in.
|
||||
|
||||
This can be selected explicitly by the user or may be set
|
||||
automatically based on ba.App.locale or other factors.
|
||||
"""
|
||||
assert isinstance(_ba.app.config, dict)
|
||||
return _ba.app.config.get('Lang', self.default_language)
|
||||
|
||||
@property
|
||||
def available_languages(self) -> list[str]:
|
||||
"""A list of all available languages.
|
||||
|
||||
Note that languages that may be present in game assets but which
|
||||
are not displayable on the running version of the game are not
|
||||
included here.
|
||||
"""
|
||||
langs = set()
|
||||
try:
|
||||
names = os.listdir('ba_data/data/languages')
|
||||
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...
|
||||
for i, name in enumerate(names):
|
||||
if name == 'Chinesetraditional':
|
||||
names[i] = 'ChineseTraditional'
|
||||
except Exception:
|
||||
from ba import _error
|
||||
|
||||
_error.print_exception()
|
||||
names = []
|
||||
for name in names:
|
||||
if self._can_display_language(name):
|
||||
langs.add(name)
|
||||
return sorted(
|
||||
name for name in names if self._can_display_language(name)
|
||||
)
|
||||
|
||||
def setlanguage(
|
||||
self,
|
||||
language: str | None,
|
||||
print_change: bool = True,
|
||||
store_to_config: bool = True,
|
||||
) -> None:
|
||||
"""Set the active language used for the game.
|
||||
|
||||
Pass None to use OS default language.
|
||||
"""
|
||||
# pylint: disable=too-many-locals
|
||||
# pylint: disable=too-many-statements
|
||||
# pylint: disable=too-many-branches
|
||||
cfg = _ba.app.config
|
||||
cur_language = cfg.get('Lang', None)
|
||||
|
||||
# Store this in the config if its changing.
|
||||
if language != cur_language and store_to_config:
|
||||
if language is None:
|
||||
if 'Lang' in cfg:
|
||||
del cfg['Lang'] # Clear it out for default.
|
||||
else:
|
||||
cfg['Lang'] = language
|
||||
cfg.commit()
|
||||
switched = True
|
||||
else:
|
||||
switched = False
|
||||
|
||||
with open(
|
||||
'ba_data/data/languages/english.json', encoding='utf-8'
|
||||
) as infile:
|
||||
lenglishvalues = json.loads(infile.read())
|
||||
|
||||
# None implies default.
|
||||
if language is None:
|
||||
language = self.default_language
|
||||
try:
|
||||
if language == 'English':
|
||||
lmodvalues = None
|
||||
else:
|
||||
lmodfile = (
|
||||
'ba_data/data/languages/' + language.lower() + '.json'
|
||||
)
|
||||
with open(lmodfile, encoding='utf-8') as infile:
|
||||
lmodvalues = json.loads(infile.read())
|
||||
except Exception:
|
||||
from ba import _error
|
||||
|
||||
_error.print_exception('Exception importing language:', language)
|
||||
_ba.screenmessage(
|
||||
"Error setting language to '"
|
||||
+ language
|
||||
+ "'; see log for details",
|
||||
color=(1, 0, 0),
|
||||
)
|
||||
switched = False
|
||||
lmodvalues = None
|
||||
|
||||
# Create an attrdict of *just* our target language.
|
||||
self.language_target = AttrDict()
|
||||
langtarget = self.language_target
|
||||
assert langtarget is not None
|
||||
_add_to_attr_dict(
|
||||
langtarget, lmodvalues if lmodvalues is not None else lenglishvalues
|
||||
)
|
||||
|
||||
# Create an attrdict of our target language overlaid
|
||||
# on our base (english).
|
||||
languages = [lenglishvalues]
|
||||
if lmodvalues is not None:
|
||||
languages.append(lmodvalues)
|
||||
lfull = AttrDict()
|
||||
for lmod in languages:
|
||||
_add_to_attr_dict(lfull, lmod)
|
||||
self.language_merged = lfull
|
||||
|
||||
# Pass some keys/values in for low level code to use;
|
||||
# start with everything in their 'internal' section.
|
||||
internal_vals = [
|
||||
v for v in list(lfull['internal'].items()) if isinstance(v[1], str)
|
||||
]
|
||||
|
||||
# Cherry-pick various other values to include.
|
||||
# (should probably get rid of the 'internal' section
|
||||
# and do everything this way)
|
||||
for value in [
|
||||
'replayNameDefaultText',
|
||||
'replayWriteErrorText',
|
||||
'replayVersionErrorText',
|
||||
'replayReadErrorText',
|
||||
]:
|
||||
internal_vals.append((value, lfull[value]))
|
||||
internal_vals.append(
|
||||
('axisText', lfull['configGamepadWindow']['axisText'])
|
||||
)
|
||||
internal_vals.append(('buttonText', lfull['buttonText']))
|
||||
lmerged = self.language_merged
|
||||
assert lmerged is not None
|
||||
random_names = [
|
||||
n.strip() for n in lmerged['randomPlayerNamesText'].split(',')
|
||||
]
|
||||
random_names = [n for n in random_names if n != '']
|
||||
_ba.set_internal_language_keys(internal_vals, random_names)
|
||||
if switched and print_change:
|
||||
_ba.screenmessage(
|
||||
Lstr(
|
||||
resource='languageSetText',
|
||||
subs=[
|
||||
('${LANGUAGE}', Lstr(translate=('languages', language)))
|
||||
],
|
||||
),
|
||||
color=(0, 1, 0),
|
||||
)
|
||||
|
||||
def get_resource(
|
||||
self,
|
||||
resource: str,
|
||||
fallback_resource: str | None = None,
|
||||
fallback_value: Any = None,
|
||||
) -> Any:
|
||||
"""Return a translation resource by name.
|
||||
|
||||
DEPRECATED; use ba.Lstr functionality for these purposes.
|
||||
"""
|
||||
try:
|
||||
# If we have no language set, go ahead and set it.
|
||||
if self.language_merged is None:
|
||||
language = self.language
|
||||
try:
|
||||
self.setlanguage(
|
||||
language, print_change=False, store_to_config=False
|
||||
)
|
||||
except Exception:
|
||||
from ba import _error
|
||||
|
||||
_error.print_exception(
|
||||
'exception setting language to', language
|
||||
)
|
||||
|
||||
# Try english as a fallback.
|
||||
if language != 'English':
|
||||
print('Resorting to fallback language (English)')
|
||||
try:
|
||||
self.setlanguage(
|
||||
'English',
|
||||
print_change=False,
|
||||
store_to_config=False,
|
||||
)
|
||||
except Exception:
|
||||
_error.print_exception(
|
||||
'error setting language to english fallback'
|
||||
)
|
||||
|
||||
# If they provided a fallback_resource value, try the
|
||||
# target-language-only dict first and then fall back to trying the
|
||||
# fallback_resource value in the merged dict.
|
||||
if fallback_resource is not None:
|
||||
try:
|
||||
values = self.language_target
|
||||
splits = resource.split('.')
|
||||
dicts = splits[:-1]
|
||||
key = splits[-1]
|
||||
for dct in dicts:
|
||||
assert values is not None
|
||||
values = values[dct]
|
||||
assert values is not None
|
||||
val = values[key]
|
||||
return val
|
||||
except Exception:
|
||||
# FIXME: Shouldn't we try the fallback resource in the
|
||||
# merged dict AFTER we try the main resource in the
|
||||
# merged dict?
|
||||
try:
|
||||
values = self.language_merged
|
||||
splits = fallback_resource.split('.')
|
||||
dicts = splits[:-1]
|
||||
key = splits[-1]
|
||||
for dct in dicts:
|
||||
assert values is not None
|
||||
values = values[dct]
|
||||
assert values is not None
|
||||
val = values[key]
|
||||
return val
|
||||
|
||||
except Exception:
|
||||
# If we got nothing for fallback_resource, default
|
||||
# to the normal code which checks or primary
|
||||
# value in the merge dict; there's a chance we can
|
||||
# get an english value for it (which we weren't
|
||||
# looking for the first time through).
|
||||
pass
|
||||
|
||||
values = self.language_merged
|
||||
splits = resource.split('.')
|
||||
dicts = splits[:-1]
|
||||
key = splits[-1]
|
||||
for dct in dicts:
|
||||
assert values is not None
|
||||
values = values[dct]
|
||||
assert values is not None
|
||||
val = values[key]
|
||||
return val
|
||||
|
||||
except Exception:
|
||||
# Ok, looks like we couldn't find our main or fallback resource
|
||||
# anywhere. Now if we've been given a fallback value, return it;
|
||||
# otherwise fail.
|
||||
from ba import _error
|
||||
|
||||
if fallback_value is not None:
|
||||
return fallback_value
|
||||
raise _error.NotFoundError(
|
||||
f"Resource not found: '{resource}'"
|
||||
) from None
|
||||
|
||||
def translate(
|
||||
self,
|
||||
category: str,
|
||||
strval: str,
|
||||
raise_exceptions: bool = False,
|
||||
print_errors: bool = False,
|
||||
) -> str:
|
||||
"""Translate a value (or return the value if no translation available)
|
||||
|
||||
DEPRECATED; use ba.Lstr functionality for these purposes.
|
||||
"""
|
||||
try:
|
||||
translated = self.get_resource('translations')[category][strval]
|
||||
except Exception as exc:
|
||||
if raise_exceptions:
|
||||
raise
|
||||
if print_errors:
|
||||
print(
|
||||
(
|
||||
'Translate error: category=\''
|
||||
+ category
|
||||
+ '\' name=\''
|
||||
+ strval
|
||||
+ '\' exc='
|
||||
+ str(exc)
|
||||
+ ''
|
||||
)
|
||||
)
|
||||
translated = None
|
||||
translated_out: str
|
||||
if translated is None:
|
||||
translated_out = strval
|
||||
else:
|
||||
translated_out = translated
|
||||
assert isinstance(translated_out, str)
|
||||
return translated_out
|
||||
|
||||
def is_custom_unicode_char(self, char: str) -> bool:
|
||||
"""Return whether a char is in the custom unicode range we use."""
|
||||
assert isinstance(char, str)
|
||||
if len(char) != 1:
|
||||
raise ValueError('Invalid Input; must be length 1')
|
||||
return 0xE000 <= ord(char) <= 0xF8FF
|
||||
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
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 bs_language_*.py
|
||||
files in the game or the translations pages at
|
||||
legacy.ballistica.net/translate.
|
||||
|
||||
##### Examples
|
||||
EXAMPLE 1: specify a string from a resource path
|
||||
>>> mynode.text = ba.Lstr(resource='audioSettingsWindow.titleText')
|
||||
>>> mynode.text = babase.Lstr(resource='audioSettingsWindow.titleText')
|
||||
|
||||
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 = ba.Lstr(translate=('gameDescriptions',
|
||||
>>> mynode.text = babase.Lstr(translate=('gameDescriptions',
|
||||
... 'Defeat all enemies'))
|
||||
|
||||
EXAMPLE 3: specify a raw value and some substitutions. Substitutions
|
||||
can be used with resource and translate modes as well.
|
||||
>>> mynode.text = ba.Lstr(value='${A} / ${B}',
|
||||
>>> mynode.text = babase.Lstr(value='${A} / ${B}',
|
||||
... subs=[('${A}', str(score)), ('${B}', str(total))])
|
||||
|
||||
EXAMPLE 4: ba.Lstr's can be nested. This example would display the
|
||||
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 = ba.Lstr(
|
||||
>>> mytextnode.text = babase.Lstr(
|
||||
... resource='res_a',
|
||||
... subs=[('${NAME}', ba.Lstr(resource='res_b'))])
|
||||
... subs=[('${NAME}', babase.Lstr(resource='res_b'))])
|
||||
"""
|
||||
|
||||
# pylint: disable=dangerous-default-value
|
||||
|
|
@ -528,7 +549,7 @@ class Lstr:
|
|||
keywds['v'] = keywds['value']
|
||||
del keywds['value']
|
||||
if 'fallback' in keywds:
|
||||
from ba import _error
|
||||
from babase import _error
|
||||
|
||||
_error.print_error(
|
||||
'deprecated "fallback" arg passed to Lstr(); use '
|
||||
|
|
@ -553,15 +574,15 @@ class Lstr:
|
|||
You should avoid doing this as much as possible and instead pass
|
||||
and store Lstr values.
|
||||
"""
|
||||
return _ba.evaluate_lstr(self._get_json())
|
||||
return _babase.evaluate_lstr(self._get_json())
|
||||
|
||||
def is_flat_value(self) -> bool:
|
||||
"""Return whether the Lstr is a 'flat' value.
|
||||
|
||||
This is defined as a simple string value incorporating no translations,
|
||||
resources, or substitutions. In this case it may be reasonable to
|
||||
replace it with a raw string value, perform string manipulation on it,
|
||||
etc.
|
||||
This is defined as a simple string value incorporating no
|
||||
translations, resources, or substitutions. In this case it may
|
||||
be reasonable to replace it with a raw string value, perform
|
||||
string manipulation on it, etc.
|
||||
"""
|
||||
return bool('v' in self.args and not self.args.get('s', []))
|
||||
|
||||
|
|
@ -569,7 +590,7 @@ class Lstr:
|
|||
try:
|
||||
return json.dumps(self.args, separators=(',', ':'))
|
||||
except Exception:
|
||||
from ba import _error
|
||||
from babase import _error
|
||||
|
||||
_error.print_exception('_get_json failed for', self.args)
|
||||
return 'JSON_ERR'
|
||||
|
|
@ -581,8 +602,8 @@ class Lstr:
|
|||
return '<ba.Lstr: ' + self._get_json() + '>'
|
||||
|
||||
@staticmethod
|
||||
def from_json(json_string: str) -> ba.Lstr:
|
||||
"""Given a json string, returns a ba.Lstr. Does no data validation."""
|
||||
def from_json(json_string: str) -> babase.Lstr:
|
||||
"""Given a json string, returns a babase.Lstr. Does no validation."""
|
||||
lstr = Lstr(value='')
|
||||
lstr.args = json.loads(json_string)
|
||||
return lstr
|
||||
|
|
@ -625,4 +646,4 @@ class AttrDict(dict):
|
|||
return val
|
||||
|
||||
def __setattr__(self, attr: str, value: Any) -> None:
|
||||
raise Exception()
|
||||
raise AttributeError()
|
||||
|
|
@ -10,7 +10,7 @@ from dataclasses import dataclass
|
|||
from typing import TYPE_CHECKING, final
|
||||
|
||||
from bacommon.login import LoginType
|
||||
import _ba
|
||||
import _babase
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Callable
|
||||
|
|
@ -45,12 +45,12 @@ class LoginAdapter:
|
|||
display_name: str
|
||||
|
||||
def __init__(self, login_type: LoginType):
|
||||
assert _ba.in_logic_thread()
|
||||
assert _babase.in_logic_thread()
|
||||
self.login_type = login_type
|
||||
self._implicit_login_state: LoginAdapter.ImplicitLoginState | None = (
|
||||
None
|
||||
)
|
||||
self._on_app_launch_called = False
|
||||
self._on_app_loading_called = False
|
||||
self._implicit_login_state_dirty = False
|
||||
self._back_end_active = False
|
||||
|
||||
|
|
@ -61,11 +61,11 @@ class LoginAdapter:
|
|||
self._last_sign_in_time: float | None = None
|
||||
self._last_sign_in_desc: str | None = None
|
||||
|
||||
def on_app_launch(self) -> None:
|
||||
"""Should be called for each adapter in on_app_launch."""
|
||||
def on_app_loading(self) -> None:
|
||||
"""Should be called for each adapter in on_app_loading."""
|
||||
|
||||
assert not self._on_app_launch_called
|
||||
self._on_app_launch_called = True
|
||||
assert not self._on_app_loading_called
|
||||
self._on_app_loading_called = True
|
||||
|
||||
# Any implicit state we received up until now needs to be pushed
|
||||
# to the app account subsystem.
|
||||
|
|
@ -79,7 +79,7 @@ class LoginAdapter:
|
|||
This should be called by the adapter back-end when an account
|
||||
of their associated type gets logged in or out.
|
||||
"""
|
||||
assert _ba.in_logic_thread()
|
||||
assert _babase.in_logic_thread()
|
||||
|
||||
# Ignore redundant sets.
|
||||
if state == self._implicit_login_state:
|
||||
|
|
@ -118,7 +118,7 @@ class LoginAdapter:
|
|||
Note that the logins dict passed in should be immutable as
|
||||
only a reference to it is stored, not a copy.
|
||||
"""
|
||||
assert _ba.in_logic_thread()
|
||||
assert _babase.in_logic_thread()
|
||||
if DEBUG_LOG:
|
||||
logging.debug(
|
||||
'LoginAdapter: %s adapter got active logins %s.',
|
||||
|
|
@ -139,7 +139,7 @@ class LoginAdapter:
|
|||
UIs, etc. When not active it should ignore everything and behave
|
||||
as if logged out, even if it technically is still logged in.
|
||||
"""
|
||||
assert _ba.in_logic_thread()
|
||||
assert _babase.in_logic_thread()
|
||||
del active # Unused.
|
||||
|
||||
@final
|
||||
|
|
@ -154,30 +154,29 @@ class LoginAdapter:
|
|||
the adapter will attempt to sign in if possible. An exception will
|
||||
be returned if the sign-in attempt fails.
|
||||
"""
|
||||
assert _ba.in_logic_thread()
|
||||
from ba._general import Call
|
||||
from ba._generated.enums import TimeType
|
||||
assert _babase.in_logic_thread()
|
||||
from babase._general import Call
|
||||
|
||||
# Have been seeing multiple sign-in attempts come through
|
||||
# nearly simultaneously which can be problematic server-side.
|
||||
# Let's error if a sign-in attempt is made within a few seconds
|
||||
# of the last one to address this.
|
||||
now = time.monotonic()
|
||||
appnow = _ba.time(TimeType.REAL)
|
||||
appnow = _babase.apptime()
|
||||
if self._last_sign_in_time is not None:
|
||||
since_last = now - self._last_sign_in_time
|
||||
if since_last < 1.0:
|
||||
logging.warning(
|
||||
'LoginAdapter: %s adapter sign_in() called too soon'
|
||||
' (%.2fs) after last; this-desc="%s", last-desc="%s",'
|
||||
' ba-real-time=%.2f.',
|
||||
' ba-app-time=%.2f.',
|
||||
self.login_type.name,
|
||||
since_last,
|
||||
description,
|
||||
self._last_sign_in_desc,
|
||||
appnow,
|
||||
)
|
||||
_ba.pushcall(
|
||||
_babase.pushcall(
|
||||
Call(
|
||||
result_cb,
|
||||
self,
|
||||
|
|
@ -207,7 +206,7 @@ class LoginAdapter:
|
|||
' aborting sign-in.',
|
||||
self.login_type.name,
|
||||
)
|
||||
_ba.pushcall(
|
||||
_babase.pushcall(
|
||||
Call(
|
||||
result_cb,
|
||||
self,
|
||||
|
|
@ -229,7 +228,6 @@ class LoginAdapter:
|
|||
def _got_sign_in_response(
|
||||
response: bacommon.cloud.SignInResponse | Exception,
|
||||
) -> None:
|
||||
|
||||
if isinstance(response, Exception):
|
||||
if DEBUG_LOG:
|
||||
logging.debug(
|
||||
|
|
@ -238,7 +236,7 @@ class LoginAdapter:
|
|||
self.login_type.name,
|
||||
response,
|
||||
)
|
||||
_ba.pushcall(Call(result_cb, self, response))
|
||||
_babase.pushcall(Call(result_cb, self, response))
|
||||
else:
|
||||
if DEBUG_LOG:
|
||||
logging.debug(
|
||||
|
|
@ -257,9 +255,10 @@ class LoginAdapter:
|
|||
result2 = self.SignInResult(
|
||||
credentials=response.credentials
|
||||
)
|
||||
_ba.pushcall(Call(result_cb, self, result2))
|
||||
_babase.pushcall(Call(result_cb, self, result2))
|
||||
|
||||
_ba.app.cloud.send_message_cb(
|
||||
assert _babase.app.plus is not None
|
||||
_babase.app.plus.cloud.send_message_cb(
|
||||
bacommon.cloud.SignInMessage(
|
||||
self.login_type,
|
||||
result,
|
||||
|
|
@ -288,18 +287,18 @@ class LoginAdapter:
|
|||
The provided completion_cb should then be called with either a token
|
||||
or None if sign in failed or was cancelled.
|
||||
"""
|
||||
from ba._general import Call
|
||||
from babase._general import Call
|
||||
|
||||
# Default implementation simply fails immediately.
|
||||
_ba.pushcall(Call(completion_cb, None))
|
||||
_babase.pushcall(Call(completion_cb, None))
|
||||
|
||||
def _update_implicit_login_state(self) -> None:
|
||||
# If we've received an implicit login state, schedule it to be
|
||||
# sent along to the app. We wait until on-app-launch has been
|
||||
# called so that account-client-v2 has had a chance to load
|
||||
# any existing state so it can properly respond to this.
|
||||
if self._implicit_login_state_dirty and self._on_app_launch_called:
|
||||
from ba._general import Call
|
||||
if self._implicit_login_state_dirty and self._on_app_loading_called:
|
||||
from babase._general import Call
|
||||
|
||||
if DEBUG_LOG:
|
||||
logging.debug(
|
||||
|
|
@ -308,9 +307,10 @@ class LoginAdapter:
|
|||
self.login_type.name,
|
||||
)
|
||||
|
||||
_ba.pushcall(
|
||||
assert _babase.app.plus is not None
|
||||
_babase.pushcall(
|
||||
Call(
|
||||
_ba.app.accounts_v2.on_implicit_login_state_changed,
|
||||
_babase.app.plus.accounts.on_implicit_login_state_changed,
|
||||
self.login_type,
|
||||
self._implicit_login_state,
|
||||
)
|
||||
|
|
@ -353,14 +353,18 @@ class LoginAdapterNative(LoginAdapter):
|
|||
attempt_id = self._sign_in_attempt_num
|
||||
self._sign_in_attempts[attempt_id] = completion_cb
|
||||
self._sign_in_attempt_num += 1
|
||||
_ba.login_adapter_get_sign_in_token(self.login_type.value, attempt_id)
|
||||
_babase.login_adapter_get_sign_in_token(
|
||||
self.login_type.value, attempt_id
|
||||
)
|
||||
|
||||
def on_back_end_active_change(self, active: bool) -> None:
|
||||
_ba.login_adapter_back_end_active_change(self.login_type.value, active)
|
||||
_babase.login_adapter_back_end_active_change(
|
||||
self.login_type.value, active
|
||||
)
|
||||
|
||||
def on_sign_in_complete(self, attempt_id: int, result: str | None) -> None:
|
||||
"""Called by the native layer on a completed attempt."""
|
||||
assert _ba.in_logic_thread()
|
||||
assert _babase.in_logic_thread()
|
||||
if attempt_id not in self._sign_in_attempts:
|
||||
logging.exception('sign-in attempt_id %d not found', attempt_id)
|
||||
return
|
||||
|
|
@ -13,7 +13,7 @@ from typing import TYPE_CHECKING, TypeVar
|
|||
from dataclasses import dataclass, field
|
||||
|
||||
from efro.call import tpartial
|
||||
import _ba
|
||||
import _babase
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Callable
|
||||
|
|
@ -21,20 +21,15 @@ if TYPE_CHECKING:
|
|||
# The meta api version of this build of the game.
|
||||
# Only packages and modules requiring this exact api version
|
||||
# will be considered when scanning directories.
|
||||
|
||||
# See: https://ballistica.net/wiki/Meta-Tags
|
||||
CURRENT_API_VERSION = 6 #TODO update it to latest
|
||||
# current API version is 7 , im downgrading it to 6 to support mini games which i cant update to 7 bcoz of encryption
|
||||
# shouldn't be a issue , I manually updated all plugin on_app_launch to on_app_running and that was the only change btw API 6 and 7
|
||||
|
||||
# See: https://ballistica.net/wiki/Meta-Tag-System
|
||||
CURRENT_API_VERSION = 8
|
||||
|
||||
# Meta export lines can use these names to represent these classes.
|
||||
# This is purely a convenience; it is possible to use full class paths
|
||||
# instead of these or to make the meta system aware of arbitrary classes.
|
||||
EXPORT_CLASS_NAME_SHORTCUTS: dict[str, str] = {
|
||||
'plugin': 'ba.Plugin',
|
||||
'keyboard': 'ba.Keyboard',
|
||||
'game': 'ba.GameActivity',
|
||||
'plugin': 'babase.Plugin',
|
||||
'keyboard': 'babase.Keyboard',
|
||||
}
|
||||
|
||||
T = TypeVar('T')
|
||||
|
|
@ -45,8 +40,8 @@ class ScanResults:
|
|||
"""Final results from a meta-scan."""
|
||||
|
||||
exports: dict[str, list[str]] = field(default_factory=dict)
|
||||
errors: list[str] = field(default_factory=list)
|
||||
warnings: list[str] = field(default_factory=list)
|
||||
incorrect_api_modules: list[str] = field(default_factory=list)
|
||||
announce_errors_occurred: bool = False
|
||||
|
||||
def exports_of_class(self, cls: type) -> list[str]:
|
||||
"""Return exports of a given class."""
|
||||
|
|
@ -58,11 +53,10 @@ class MetadataSubsystem:
|
|||
|
||||
Category: **App Classes**
|
||||
|
||||
Access the single shared instance of this class at 'ba.app.meta'.
|
||||
Access the single shared instance of this class at 'babase.app.meta'.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
|
||||
self._scan: DirectoryScan | None = None
|
||||
|
||||
# Can be populated before starting the scan.
|
||||
|
|
@ -85,7 +79,14 @@ class MetadataSubsystem:
|
|||
|
||||
self._scan_complete_cb = scan_complete_cb
|
||||
self._scan = DirectoryScan(
|
||||
[_ba.app.python_directory_app, _ba.app.python_directory_user]
|
||||
[
|
||||
path
|
||||
for path in [
|
||||
_babase.app.python_directory_app,
|
||||
_babase.app.python_directory_user,
|
||||
]
|
||||
if path is not None
|
||||
]
|
||||
)
|
||||
|
||||
Thread(target=self._run_scan_in_bg, daemon=True).start()
|
||||
|
|
@ -132,7 +133,7 @@ class MetadataSubsystem:
|
|||
completion_cb: Callable[[list[type[T]]], None],
|
||||
completion_cb_in_bg_thread: bool,
|
||||
) -> None:
|
||||
from ba._general import getclass
|
||||
from babase._general import getclass
|
||||
|
||||
classes: list[type[T]] = []
|
||||
try:
|
||||
|
|
@ -150,14 +151,14 @@ class MetadataSubsystem:
|
|||
if completion_cb_in_bg_thread:
|
||||
completion_call()
|
||||
else:
|
||||
_ba.pushcall(completion_call, from_other_thread=True)
|
||||
_babase.pushcall(completion_call, from_other_thread=True)
|
||||
|
||||
def _wait_for_scan_results(self) -> ScanResults:
|
||||
"""Return scan results, blocking if the scan is not yet complete."""
|
||||
if self.scanresults is None:
|
||||
if _ba.in_logic_thread():
|
||||
if _babase.in_logic_thread():
|
||||
logging.warning(
|
||||
'ba.meta._wait_for_scan_results()'
|
||||
'babase.meta._wait_for_scan_results()'
|
||||
' called in logic thread before scan completed;'
|
||||
' this can cause hitches.'
|
||||
)
|
||||
|
|
@ -180,43 +181,61 @@ class MetadataSubsystem:
|
|||
self._scan.run()
|
||||
results = self._scan.results
|
||||
self._scan = None
|
||||
except Exception as exc:
|
||||
results = ScanResults(errors=[f'Scan exception: {exc}'])
|
||||
except Exception:
|
||||
logging.exception('metascan: Error running scan in bg.')
|
||||
results = ScanResults(announce_errors_occurred=True)
|
||||
|
||||
# Place results and tell the logic thread they're ready.
|
||||
self.scanresults = results
|
||||
_ba.pushcall(self._handle_scan_results, from_other_thread=True)
|
||||
_babase.pushcall(self._handle_scan_results, from_other_thread=True)
|
||||
|
||||
def _handle_scan_results(self) -> None:
|
||||
"""Called in the logic thread with results of a completed scan."""
|
||||
from ba._language import Lstr
|
||||
from babase._language import Lstr
|
||||
|
||||
assert _ba.in_logic_thread()
|
||||
assert _babase.in_logic_thread()
|
||||
|
||||
results = self.scanresults
|
||||
assert results is not None
|
||||
|
||||
# Spit out any warnings/errors that happened.
|
||||
# Warnings generally only get printed locally for users' benefit
|
||||
# (things like out-of-date scripts being ignored, etc.)
|
||||
# Errors are more serious and will get included in the regular log.
|
||||
if results.warnings or results.errors:
|
||||
import textwrap
|
||||
do_play_error_sound = False
|
||||
|
||||
_ba.screenmessage(
|
||||
# If we found modules needing to be updated to the newer api version,
|
||||
# mention that specifically.
|
||||
if results.incorrect_api_modules:
|
||||
if len(results.incorrect_api_modules) > 1:
|
||||
msg = Lstr(
|
||||
resource='scanScriptsMultipleModulesNeedUpdatesText',
|
||||
subs=[
|
||||
('${PATH}', results.incorrect_api_modules[0]),
|
||||
(
|
||||
'${NUM}',
|
||||
str(len(results.incorrect_api_modules) - 1),
|
||||
),
|
||||
('${API}', str(CURRENT_API_VERSION)),
|
||||
],
|
||||
)
|
||||
else:
|
||||
msg = Lstr(
|
||||
resource='scanScriptsSingleModuleNeedsUpdatesText',
|
||||
subs=[
|
||||
('${PATH}', results.incorrect_api_modules[0]),
|
||||
('${API}', str(CURRENT_API_VERSION)),
|
||||
],
|
||||
)
|
||||
_babase.screenmessage(msg, color=(1, 0, 0))
|
||||
do_play_error_sound = True
|
||||
|
||||
# Let the user know if there's warning/errors in their log
|
||||
# they may want to look at.
|
||||
if results.announce_errors_occurred:
|
||||
_babase.screenmessage(
|
||||
Lstr(resource='scanScriptsErrorText'), color=(1, 0, 0)
|
||||
)
|
||||
_ba.playsound(_ba.getsound('error'))
|
||||
if results.warnings:
|
||||
allwarnings = textwrap.indent(
|
||||
'\n'.join(results.warnings), 'Warning (meta-scan): '
|
||||
)
|
||||
logging.warning(allwarnings)
|
||||
if results.errors:
|
||||
allerrors = textwrap.indent(
|
||||
'\n'.join(results.errors), 'Error (meta-scan): '
|
||||
)
|
||||
logging.error(allerrors)
|
||||
do_play_error_sound = True
|
||||
|
||||
if do_play_error_sound:
|
||||
_babase.getsimplesound('error').play()
|
||||
|
||||
# Let the game know we're done.
|
||||
assert self._scan_complete_cb is not None
|
||||
|
|
@ -248,7 +267,6 @@ class DirectoryScan:
|
|||
def run(self) -> None:
|
||||
"""Do the thing."""
|
||||
for pathlist in [self.base_paths, self.extra_paths]:
|
||||
|
||||
# Spin and wait until extra paths are provided before doing them.
|
||||
if pathlist is self.extra_paths:
|
||||
while not self.extra_paths_set:
|
||||
|
|
@ -261,11 +279,7 @@ class DirectoryScan:
|
|||
try:
|
||||
self._scan_module(moduledir, subpath)
|
||||
except Exception:
|
||||
import traceback
|
||||
|
||||
self.results.warnings.append(
|
||||
f"Error scanning '{subpath}': " + traceback.format_exc()
|
||||
)
|
||||
logging.exception("metascan: Error scanning '%s'.", subpath)
|
||||
|
||||
# Sort our results
|
||||
for exportlist in self.results.exports.values():
|
||||
|
|
@ -276,20 +290,22 @@ class DirectoryScan:
|
|||
) -> None:
|
||||
"""Scan provided path and add module entries to provided list."""
|
||||
try:
|
||||
# Special case: let's save some time and skip the whole 'ba'
|
||||
# Special case: let's save some time and skip the whole 'babase'
|
||||
# package since we know it doesn't contain any meta tags.
|
||||
fullpath = Path(path, subpath)
|
||||
entries = [
|
||||
(path, Path(subpath, name))
|
||||
for name in os.listdir(fullpath)
|
||||
if name != 'ba'
|
||||
# Actually scratch that for now; trying to avoid special cases.
|
||||
# if name != 'babase'
|
||||
]
|
||||
except PermissionError:
|
||||
# Expected sometimes.
|
||||
entries = []
|
||||
except Exception as exc:
|
||||
except Exception:
|
||||
# Unexpected; report this.
|
||||
self.results.errors.append(str(exc))
|
||||
logging.exception('metascan: Error in _get_path_module_entries.')
|
||||
self.results.announce_errors_occurred = True
|
||||
entries = []
|
||||
|
||||
# Now identify python packages/modules out of what we found.
|
||||
|
|
@ -328,10 +344,16 @@ class DirectoryScan:
|
|||
|
||||
# If we find a module requiring a different api version, warn
|
||||
# and ignore.
|
||||
if required_api is not None and required_api < CURRENT_API_VERSION:
|
||||
self.results.warnings += (
|
||||
f'Warning: {subpath} requires api {required_api} but'
|
||||
f' we are running {CURRENT_API_VERSION}; ignoring module.'
|
||||
if required_api is not None and required_api != CURRENT_API_VERSION:
|
||||
logging.warning(
|
||||
'metascan: %s requires api %s but we are running'
|
||||
' %s. Ignoring module.',
|
||||
subpath,
|
||||
required_api,
|
||||
CURRENT_API_VERSION,
|
||||
)
|
||||
self.results.incorrect_api_modules.append(
|
||||
self._module_name_for_subpath(subpath)
|
||||
)
|
||||
return
|
||||
|
||||
|
|
@ -347,11 +369,13 @@ class DirectoryScan:
|
|||
if submodule[1].name != '__init__.py':
|
||||
self._scan_module(submodule[0], submodule[1])
|
||||
except Exception:
|
||||
import traceback
|
||||
logging.exception('metascan: Error scanning %s.', subpath)
|
||||
|
||||
self.results.warnings.append(
|
||||
f"Error scanning '{subpath}': {traceback.format_exc()}"
|
||||
)
|
||||
def _module_name_for_subpath(self, subpath: Path) -> str:
|
||||
# (should not be getting these)
|
||||
assert '__init__.py' not in str(subpath)
|
||||
|
||||
return '.'.join(subpath.parts).removesuffix('.py')
|
||||
|
||||
def _process_module_meta_tags(
|
||||
self, subpath: Path, flines: list[str], meta_lines: dict[int, list[str]]
|
||||
|
|
@ -361,10 +385,12 @@ class DirectoryScan:
|
|||
# meta_lines is just anything containing '# ba_meta '; make sure
|
||||
# the ba_meta is in the right place.
|
||||
if mline[0] != 'ba_meta':
|
||||
self.results.warnings.append(
|
||||
f'Warning: {subpath}:'
|
||||
f' malformed ba_meta statement on line {lindex + 1}.'
|
||||
logging.warning(
|
||||
'metascan: %s:%d: malformed ba_meta statement.',
|
||||
subpath,
|
||||
lindex + 1,
|
||||
)
|
||||
self.results.announce_errors_occurred = True
|
||||
elif (
|
||||
len(mline) == 4 and mline[1] == 'require' and mline[2] == 'api'
|
||||
):
|
||||
|
|
@ -373,15 +399,15 @@ class DirectoryScan:
|
|||
elif len(mline) != 3 or mline[1] != 'export':
|
||||
# Currently we only support 'ba_meta export FOO';
|
||||
# complain for anything else we see.
|
||||
self.results.warnings.append(
|
||||
f'Warning: {subpath}'
|
||||
f': unrecognized ba_meta statement on line {lindex + 1}.'
|
||||
logging.warning(
|
||||
'metascan: %s:%d: unrecognized ba_meta statement.',
|
||||
subpath,
|
||||
lindex + 1,
|
||||
)
|
||||
self.results.announce_errors_occurred = True
|
||||
else:
|
||||
# Looks like we've got a valid export line!
|
||||
modulename = '.'.join(subpath.parts)
|
||||
if subpath.name.endswith('.py'):
|
||||
modulename = modulename[:-3]
|
||||
modulename = self._module_name_for_subpath(subpath)
|
||||
exporttypestr = mline[2]
|
||||
export_class_name = self._get_export_class_name(
|
||||
subpath, flines, lindex
|
||||
|
|
@ -389,15 +415,30 @@ class DirectoryScan:
|
|||
if export_class_name is not None:
|
||||
classname = modulename + '.' + export_class_name
|
||||
|
||||
# If export type is one of our shortcuts, sub in the
|
||||
# actual class path. Otherwise assume its a classpath
|
||||
# itself.
|
||||
exporttype = EXPORT_CLASS_NAME_SHORTCUTS.get(exporttypestr)
|
||||
if exporttype is None:
|
||||
exporttype = exporttypestr
|
||||
self.results.exports.setdefault(exporttype, []).append(
|
||||
classname
|
||||
)
|
||||
# Since we'll soon have multiple versions of 'game'
|
||||
# classes we need to migrate people to using base
|
||||
# class names for them.
|
||||
if exporttypestr == 'game':
|
||||
logging.warning(
|
||||
"metascan: %s:%d: '# ba_meta export"
|
||||
" game' tag should be replaced by '# ba_meta"
|
||||
" export bascenev1.GameActivity'.",
|
||||
subpath,
|
||||
lindex + 1,
|
||||
)
|
||||
self.results.announce_errors_occurred = True
|
||||
else:
|
||||
# If export type is one of our shortcuts, sub in the
|
||||
# actual class path. Otherwise assume its a classpath
|
||||
# itself.
|
||||
exporttype = EXPORT_CLASS_NAME_SHORTCUTS.get(
|
||||
exporttypestr
|
||||
)
|
||||
if exporttype is None:
|
||||
exporttype = exporttypestr
|
||||
self.results.exports.setdefault(exporttype, []).append(
|
||||
classname
|
||||
)
|
||||
|
||||
def _get_export_class_name(
|
||||
self, subpath: Path, lines: list[str], lindex: int
|
||||
|
|
@ -420,10 +461,13 @@ class DirectoryScan:
|
|||
classname = cbits[0]
|
||||
break # Success!
|
||||
if classname is None:
|
||||
self.results.warnings.append(
|
||||
f'Warning: {subpath}: class definition not found below'
|
||||
f' "ba_meta export" statement on line {lindexorig + 1}.'
|
||||
logging.warning(
|
||||
'metascan: %s:%d: class definition not found below'
|
||||
" 'ba_meta export' statement.",
|
||||
subpath,
|
||||
lindexorig + 1,
|
||||
)
|
||||
self.results.announce_errors_occurred = True
|
||||
return classname
|
||||
|
||||
def _get_api_requirement(
|
||||
|
|
@ -446,23 +490,26 @@ class DirectoryScan:
|
|||
and l[3].isdigit()
|
||||
]
|
||||
|
||||
# We're successful if we find exactly one properly formatted line.
|
||||
# We're successful if we find exactly one properly formatted
|
||||
# line.
|
||||
if len(lines) == 1:
|
||||
return int(lines[0][3])
|
||||
|
||||
# Ok; not successful. lets issue warnings for a few error cases.
|
||||
if len(lines) > 1:
|
||||
self.results.warnings.append(
|
||||
f'Warning: {subpath}: multiple'
|
||||
' "# ba_meta require api <NUM>" lines found;'
|
||||
' ignoring module.'
|
||||
logging.warning(
|
||||
"metascan: %s: multiple '# ba_meta require api <NUM>'"
|
||||
' lines found; ignoring module.',
|
||||
subpath,
|
||||
)
|
||||
self.results.announce_errors_occurred = True
|
||||
elif not lines and toplevel and meta_lines:
|
||||
# If we're a top-level module containing meta lines but
|
||||
# no valid "require api" line found, complain.
|
||||
self.results.warnings.append(
|
||||
f'Warning: {subpath}:'
|
||||
' no valid "# ba_meta require api <NUM>" line found;'
|
||||
' ignoring module.'
|
||||
# If we're a top-level module containing meta lines but no
|
||||
# valid "require api" line found, complain.
|
||||
logging.warning(
|
||||
"metascan: %s: no valid '# ba_meta require api <NUM>"
|
||||
' line found; ignoring module.',
|
||||
subpath,
|
||||
)
|
||||
self.results.announce_errors_occurred = True
|
||||
return None
|
||||
|
|
@ -152,7 +152,7 @@ class SpecialChar(Enum):
|
|||
TROPHY0B = 37
|
||||
TROPHY4 = 38
|
||||
LOCAL_ACCOUNT = 39
|
||||
ALIBABA_LOGO = 40
|
||||
EXPLODINARY_LOGO = 40
|
||||
FLAG_UNITED_STATES = 41
|
||||
FLAG_MEXICO = 42
|
||||
FLAG_GERMANY = 43
|
||||
75
dist/ba_data/python/babase/_net.py
vendored
Normal file
75
dist/ba_data/python/babase/_net.py
vendored
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Networking related functionality."""
|
||||
from __future__ import annotations
|
||||
|
||||
import ssl
|
||||
import threading
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import socket
|
||||
|
||||
# Timeout for standard functions talking to the master-server/etc.
|
||||
DEFAULT_REQUEST_TIMEOUT_SECONDS = 60
|
||||
|
||||
|
||||
class NetworkSubsystem:
|
||||
"""Network related app subsystem."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
# Anyone accessing/modifying zone_pings should hold this lock,
|
||||
# as it is updated by a background thread.
|
||||
self.zone_pings_lock = threading.Lock()
|
||||
|
||||
# Zone IDs mapped to average pings. This will remain empty
|
||||
# until enough pings have been run to be reasonably certain
|
||||
# that a nearby server has been pinged.
|
||||
self.zone_pings: dict[str, float] = {}
|
||||
|
||||
self._sslcontext: ssl.SSLContext | None = None
|
||||
|
||||
# For debugging.
|
||||
self.v1_test_log: str = ''
|
||||
self.v1_ctest_results: dict[int, str] = {}
|
||||
self.server_time_offset_hours: float | None = None
|
||||
|
||||
@property
|
||||
def sslcontext(self) -> ssl.SSLContext:
|
||||
"""Create/return our shared SSLContext.
|
||||
|
||||
This can be reused for all standard urllib requests/etc.
|
||||
"""
|
||||
# Note: I've run into older Android devices taking upwards of 1 second
|
||||
# to put together a default SSLContext, so recycling one can definitely
|
||||
# be a worthwhile optimization. This was suggested to me in this
|
||||
# thread by one of Python's SSL maintainers:
|
||||
# https://github.com/python/cpython/issues/94637
|
||||
if self._sslcontext is None:
|
||||
self._sslcontext = ssl.create_default_context()
|
||||
return self._sslcontext
|
||||
|
||||
|
||||
def get_ip_address_type(addr: str) -> socket.AddressFamily:
|
||||
"""Return socket.AF_INET6 or socket.AF_INET4 for the provided address."""
|
||||
import socket
|
||||
|
||||
socket_type = None
|
||||
|
||||
# First try it as an ipv4 address.
|
||||
try:
|
||||
socket.inet_pton(socket.AF_INET, addr)
|
||||
socket_type = socket.AF_INET
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
# Hmm apparently not ipv4; try ipv6.
|
||||
if socket_type is None:
|
||||
try:
|
||||
socket.inet_pton(socket.AF_INET6, addr)
|
||||
socket_type = socket.AF_INET6
|
||||
except OSError:
|
||||
pass
|
||||
if socket_type is None:
|
||||
raise ValueError(f'addr seems to be neither v4 or v6: {addr}')
|
||||
return socket_type
|
||||
333
dist/ba_data/python/babase/_plugin.py
vendored
Normal file
333
dist/ba_data/python/babase/_plugin.py
vendored
Normal file
|
|
@ -0,0 +1,333 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Plugin related functionality."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import importlib.util
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import _babase
|
||||
from babase._appsubsystem import AppSubsystem
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any
|
||||
|
||||
import babase
|
||||
|
||||
|
||||
class PluginSubsystem(AppSubsystem):
|
||||
"""Subsystem for plugin handling in the app.
|
||||
|
||||
Category: **App Classes**
|
||||
|
||||
Access the single shared instance of this class at `ba.app.plugins`.
|
||||
"""
|
||||
|
||||
AUTO_ENABLE_NEW_PLUGINS_CONFIG_KEY = 'Auto Enable New Plugins'
|
||||
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.
|
||||
self.plugin_specs: dict[str, babase.PluginSpec] = {}
|
||||
|
||||
# The set of live active plugin objects.
|
||||
self.active_plugins: list[babase.Plugin] = []
|
||||
|
||||
def on_meta_scan_complete(self) -> None:
|
||||
"""Called when meta-scanning is complete."""
|
||||
from babase._language import Lstr
|
||||
|
||||
config_changed = False
|
||||
found_new = False
|
||||
plugstates: dict[str, dict] = _babase.app.config.setdefault(
|
||||
'Plugins', {}
|
||||
)
|
||||
assert isinstance(plugstates, dict)
|
||||
|
||||
results = _babase.app.meta.scanresults
|
||||
assert results is not None
|
||||
|
||||
auto_enable_new_plugins = (
|
||||
_babase.app.config.get(
|
||||
self.AUTO_ENABLE_NEW_PLUGINS_CONFIG_KEY,
|
||||
self.AUTO_ENABLE_NEW_PLUGINS_DEFAULT,
|
||||
)
|
||||
is True
|
||||
)
|
||||
|
||||
assert not self.plugin_specs
|
||||
assert not self.active_plugins
|
||||
|
||||
# Create a plugin-spec for each plugin class we found in the
|
||||
# meta-scan.
|
||||
for class_path in results.exports_of_class(Plugin):
|
||||
assert class_path not in self.plugin_specs
|
||||
plugspec = self.plugin_specs[class_path] = PluginSpec(
|
||||
class_path=class_path, loadable=True
|
||||
)
|
||||
|
||||
# Auto-enable new ones if desired.
|
||||
if auto_enable_new_plugins:
|
||||
if class_path not in plugstates:
|
||||
plugspec.enabled = True
|
||||
config_changed = True
|
||||
found_new = True
|
||||
|
||||
# If we're *not* auto-enabling, just let the user know if we
|
||||
# found new ones.
|
||||
if found_new and not auto_enable_new_plugins:
|
||||
_babase.screenmessage(
|
||||
Lstr(resource='pluginsDetectedText'), color=(0, 1, 0)
|
||||
)
|
||||
_babase.getsimplesound('ding').play()
|
||||
|
||||
# Ok, now go through all plugins registered in the app-config
|
||||
# that weren't covered by the meta stuff above, either creating
|
||||
# plugin-specs for them or clearing them out. This covers
|
||||
# plugins with api versions not matching ours, plugins without
|
||||
# ba_meta tags, and plugins that have since disappeared.
|
||||
assert isinstance(plugstates, dict)
|
||||
wrong_api_prefixes = [f'{m}.' for m in results.incorrect_api_modules]
|
||||
|
||||
disappeared_plugs: set[str] = set()
|
||||
|
||||
for class_path in sorted(plugstates.keys()):
|
||||
# Already have a spec for it; nothing to be done.
|
||||
if class_path in self.plugin_specs:
|
||||
continue
|
||||
|
||||
# If this plugin corresponds to any modules that we've
|
||||
# identified as having incorrect api versions, we'll take
|
||||
# note of its existence but we won't try to load it.
|
||||
if any(
|
||||
class_path.startswith(prefix) for prefix in wrong_api_prefixes
|
||||
):
|
||||
plugspec = self.plugin_specs[class_path] = PluginSpec(
|
||||
class_path=class_path, loadable=False
|
||||
)
|
||||
continue
|
||||
|
||||
# Ok, it seems to be a class we have no metadata for. Look
|
||||
# to see if it appears to be an actual class we could
|
||||
# theoretically load. If so, we'll try. If not, we consider
|
||||
# the plugin to have disappeared and inform the user as
|
||||
# such.
|
||||
try:
|
||||
spec = importlib.util.find_spec(
|
||||
'.'.join(class_path.split('.')[:-1])
|
||||
)
|
||||
except Exception:
|
||||
spec = None
|
||||
|
||||
if spec is None:
|
||||
disappeared_plugs.add(class_path)
|
||||
continue
|
||||
|
||||
# If plugins disappeared, let the user know gently and remove them
|
||||
# from the config so we'll again let the user know if they later
|
||||
# reappear. This makes it much smoother to switch between users
|
||||
# or workspaces.
|
||||
if disappeared_plugs:
|
||||
_babase.getsimplesound('shieldDown').play()
|
||||
_babase.screenmessage(
|
||||
Lstr(
|
||||
resource='pluginsRemovedText',
|
||||
subs=[('${NUM}', str(len(disappeared_plugs)))],
|
||||
),
|
||||
color=(1, 1, 0),
|
||||
)
|
||||
|
||||
plugnames = ', '.join(disappeared_plugs)
|
||||
logging.info(
|
||||
'%d plugin(s) no longer found: %s.',
|
||||
len(disappeared_plugs),
|
||||
plugnames,
|
||||
)
|
||||
for goneplug in disappeared_plugs:
|
||||
del _babase.app.config['Plugins'][goneplug]
|
||||
_babase.app.config.commit()
|
||||
|
||||
if config_changed:
|
||||
_babase.app.config.commit()
|
||||
|
||||
def on_app_running(self) -> None:
|
||||
# Load up our plugins and go ahead and call their on_app_running
|
||||
# calls.
|
||||
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()')
|
||||
|
||||
def on_app_pause(self) -> None:
|
||||
for plugin in self.active_plugins:
|
||||
try:
|
||||
plugin.on_app_pause()
|
||||
except Exception:
|
||||
from babase import _error
|
||||
|
||||
_error.print_exception('Error in plugin on_app_pause()')
|
||||
|
||||
def on_app_resume(self) -> None:
|
||||
for plugin in self.active_plugins:
|
||||
try:
|
||||
plugin.on_app_resume()
|
||||
except Exception:
|
||||
from babase import _error
|
||||
|
||||
_error.print_exception('Error in plugin on_app_resume()')
|
||||
|
||||
def on_app_shutdown(self) -> None:
|
||||
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()')
|
||||
|
||||
def load_plugins(self) -> None:
|
||||
"""(internal)"""
|
||||
|
||||
# Load plugins from any specs that are enabled & able to.
|
||||
for _class_path, plug_spec in sorted(self.plugin_specs.items()):
|
||||
plugin = plug_spec.attempt_load_if_enabled()
|
||||
if plugin is not None:
|
||||
self.active_plugins.append(plugin)
|
||||
|
||||
|
||||
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 plugin-spec
|
||||
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.
|
||||
"""
|
||||
|
||||
def __init__(self, class_path: str, loadable: bool):
|
||||
self.class_path = class_path
|
||||
self.loadable = loadable
|
||||
self.attempted_load = False
|
||||
self.plugin: Plugin | None = None
|
||||
|
||||
@property
|
||||
def enabled(self) -> bool:
|
||||
"""Whether the user wants this plugin to load."""
|
||||
plugstates: dict[str, dict] = _babase.app.config.get('Plugins', {})
|
||||
assert isinstance(plugstates, dict)
|
||||
val = plugstates.get(self.class_path, {}).get('enabled', False) is True
|
||||
return val
|
||||
|
||||
@enabled.setter
|
||||
def enabled(self, val: bool) -> None:
|
||||
plugstates: dict[str, dict] = _babase.app.config.setdefault(
|
||||
'Plugins', {}
|
||||
)
|
||||
assert isinstance(plugstates, dict)
|
||||
plugstate = plugstates.setdefault(self.class_path, {})
|
||||
plugstate['enabled'] = val
|
||||
|
||||
def attempt_load_if_enabled(self) -> Plugin | None:
|
||||
"""Possibly load the plugin and report errors."""
|
||||
from babase._general import getclass
|
||||
from babase._language import Lstr
|
||||
|
||||
assert not self.attempted_load
|
||||
assert self.plugin is None
|
||||
|
||||
if not self.enabled:
|
||||
return None
|
||||
self.attempted_load = True
|
||||
if not self.loadable:
|
||||
return None
|
||||
try:
|
||||
cls = getclass(self.class_path, Plugin)
|
||||
except Exception as exc:
|
||||
_babase.getsimplesound('error').play()
|
||||
_babase.screenmessage(
|
||||
Lstr(
|
||||
resource='pluginClassLoadErrorText',
|
||||
subs=[
|
||||
('${PLUGIN}', self.class_path),
|
||||
('${ERROR}', str(exc)),
|
||||
],
|
||||
),
|
||||
color=(1, 0, 0),
|
||||
)
|
||||
logging.exception(
|
||||
"Error loading plugin class '%s'.", self.class_path
|
||||
)
|
||||
return None
|
||||
try:
|
||||
self.plugin = cls()
|
||||
return self.plugin
|
||||
except Exception as exc:
|
||||
from babase import _error
|
||||
|
||||
_babase.getsimplesound('error').play()
|
||||
_babase.screenmessage(
|
||||
Lstr(
|
||||
resource='pluginInitErrorText',
|
||||
subs=[
|
||||
('${PLUGIN}', self.class_path),
|
||||
('${ERROR}', str(exc)),
|
||||
],
|
||||
),
|
||||
color=(1, 0, 0),
|
||||
)
|
||||
logging.exception(
|
||||
"Error initing plugin class: '%s'.", self.class_path
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
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 activate.
|
||||
Active 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:
|
||||
"""Called when the app reaches the running state."""
|
||||
|
||||
def on_app_pause(self) -> None:
|
||||
"""Called after pausing game activity."""
|
||||
|
||||
def on_app_resume(self) -> None:
|
||||
"""Called after the game continues."""
|
||||
|
||||
def on_app_shutdown(self) -> None:
|
||||
"""Called before closing the application."""
|
||||
|
||||
def has_settings_ui(self) -> bool:
|
||||
"""Called to ask if we have settings UI we can show."""
|
||||
return False
|
||||
|
||||
def show_settings_ui(self, source_widget: Any | None) -> None:
|
||||
"""Called to show our settings UI."""
|
||||
90
dist/ba_data/python/babase/_text.py
vendored
Normal file
90
dist/ba_data/python/babase/_text.py
vendored
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Text related functionality."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import babase
|
||||
|
||||
|
||||
def timestring(
|
||||
timeval: float | int,
|
||||
centi: bool = True,
|
||||
) -> babase.Lstr:
|
||||
"""Generate a babase.Lstr for displaying a time value.
|
||||
|
||||
Category: **General Utility Functions**
|
||||
|
||||
Given a time value, returns a babase.Lstr 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.
|
||||
|
||||
"""
|
||||
from babase._language import Lstr
|
||||
|
||||
# We take float seconds but operate on int milliseconds internally.
|
||||
timeval = int(1000 * timeval)
|
||||
bits = []
|
||||
subs = []
|
||||
hval = (timeval // 1000) // (60 * 60)
|
||||
if hval != 0:
|
||||
bits.append('${H}')
|
||||
subs.append(
|
||||
(
|
||||
'${H}',
|
||||
Lstr(
|
||||
resource='timeSuffixHoursText',
|
||||
subs=[('${COUNT}', str(hval))],
|
||||
),
|
||||
)
|
||||
)
|
||||
mval = ((timeval // 1000) // 60) % 60
|
||||
if mval != 0:
|
||||
bits.append('${M}')
|
||||
subs.append(
|
||||
(
|
||||
'${M}',
|
||||
Lstr(
|
||||
resource='timeSuffixMinutesText',
|
||||
subs=[('${COUNT}', str(mval))],
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
# We add seconds if its non-zero *or* we haven't added anything else.
|
||||
if centi:
|
||||
# pylint: disable=consider-using-f-string
|
||||
sval = timeval / 1000.0 % 60.0
|
||||
if sval >= 0.005 or not bits:
|
||||
bits.append('${S}')
|
||||
subs.append(
|
||||
(
|
||||
'${S}',
|
||||
Lstr(
|
||||
resource='timeSuffixSecondsText',
|
||||
subs=[('${COUNT}', ('%.2f' % sval))],
|
||||
),
|
||||
)
|
||||
)
|
||||
else:
|
||||
sval = timeval // 1000 % 60
|
||||
if sval != 0 or not bits:
|
||||
bits.append('${S}')
|
||||
subs.append(
|
||||
(
|
||||
'${S}',
|
||||
Lstr(
|
||||
resource='timeSuffixSecondsText',
|
||||
subs=[('${COUNT}', str(sval))],
|
||||
),
|
||||
)
|
||||
)
|
||||
return Lstr(value=' '.join(bits), subs=subs)
|
||||
|
|
@ -13,14 +13,14 @@ from typing import TYPE_CHECKING
|
|||
|
||||
from efro.call import tpartial
|
||||
from efro.error import CleanError
|
||||
import _ba
|
||||
import _babase
|
||||
import bacommon.cloud
|
||||
from bacommon.transfer import DirectoryManifest
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Callable
|
||||
|
||||
import ba
|
||||
import babase
|
||||
|
||||
|
||||
class WorkspaceSubsystem:
|
||||
|
|
@ -36,7 +36,7 @@ class WorkspaceSubsystem:
|
|||
|
||||
def set_active_workspace(
|
||||
self,
|
||||
account: ba.AccountV2Handle,
|
||||
account: babase.AccountV2Handle,
|
||||
workspaceid: str,
|
||||
workspacename: str,
|
||||
on_completed: Callable[[], None],
|
||||
|
|
@ -55,36 +55,38 @@ class WorkspaceSubsystem:
|
|||
daemon=True,
|
||||
).start()
|
||||
|
||||
def _errmsg(self, msg: ba.Lstr) -> None:
|
||||
_ba.screenmessage(msg, color=(1, 0, 0))
|
||||
_ba.playsound(_ba.getsound('error'))
|
||||
def _errmsg(self, msg: babase.Lstr) -> None:
|
||||
_babase.screenmessage(msg, color=(1, 0, 0))
|
||||
_babase.getsimplesound('error').play()
|
||||
|
||||
def _successmsg(self, msg: ba.Lstr) -> None:
|
||||
_ba.screenmessage(msg, color=(0, 1, 0))
|
||||
_ba.playsound(_ba.getsound('gunCocking'))
|
||||
def _successmsg(self, msg: babase.Lstr) -> None:
|
||||
_babase.screenmessage(msg, color=(0, 1, 0))
|
||||
_babase.getsimplesound('gunCocking').play()
|
||||
|
||||
def _set_active_workspace_bg(
|
||||
self,
|
||||
account: ba.AccountV2Handle,
|
||||
account: babase.AccountV2Handle,
|
||||
workspaceid: str,
|
||||
workspacename: str,
|
||||
on_completed: Callable[[], None],
|
||||
) -> None:
|
||||
from ba._language import Lstr
|
||||
from babase._language import Lstr
|
||||
|
||||
class _SkipSyncError(RuntimeError):
|
||||
pass
|
||||
|
||||
plus = _babase.app.plus
|
||||
assert plus is not None
|
||||
|
||||
set_path = True
|
||||
wspath = Path(
|
||||
_ba.get_volatile_data_directory(), 'workspaces', workspaceid
|
||||
_babase.get_volatile_data_directory(), 'workspaces', workspaceid
|
||||
)
|
||||
try:
|
||||
|
||||
# If it seems we're offline, don't even attempt a sync,
|
||||
# but allow using the previous synced state.
|
||||
# (is this a good idea?)
|
||||
if not _ba.app.cloud.is_connected():
|
||||
if not plus.cloud.is_connected():
|
||||
raise _SkipSyncError()
|
||||
|
||||
manifest = DirectoryManifest.create_from_disk(wspath)
|
||||
|
|
@ -95,7 +97,7 @@ class WorkspaceSubsystem:
|
|||
|
||||
while True:
|
||||
with account:
|
||||
response = _ba.app.cloud.send_message(
|
||||
response = plus.cloud.send_message(
|
||||
bacommon.cloud.WorkspaceFetchMessage(
|
||||
workspaceid=workspaceid, state=state
|
||||
)
|
||||
|
|
@ -115,7 +117,7 @@ class WorkspaceSubsystem:
|
|||
break
|
||||
state.iteration += 1
|
||||
|
||||
_ba.pushcall(
|
||||
_babase.pushcall(
|
||||
tpartial(
|
||||
self._successmsg,
|
||||
Lstr(
|
||||
|
|
@ -127,7 +129,7 @@ class WorkspaceSubsystem:
|
|||
)
|
||||
|
||||
except _SkipSyncError:
|
||||
_ba.pushcall(
|
||||
_babase.pushcall(
|
||||
tpartial(
|
||||
self._errmsg,
|
||||
Lstr(
|
||||
|
|
@ -142,7 +144,7 @@ class WorkspaceSubsystem:
|
|||
# Avoid reusing existing if we fail in the middle; could
|
||||
# be in wonky state.
|
||||
set_path = False
|
||||
_ba.pushcall(
|
||||
_babase.pushcall(
|
||||
tpartial(self._errmsg, Lstr(value=str(exc))),
|
||||
from_other_thread=True,
|
||||
)
|
||||
|
|
@ -150,7 +152,7 @@ class WorkspaceSubsystem:
|
|||
# Ditto.
|
||||
set_path = False
|
||||
logging.exception("Error syncing workspace '%s'.", workspacename)
|
||||
_ba.pushcall(
|
||||
_babase.pushcall(
|
||||
tpartial(
|
||||
self._errmsg,
|
||||
Lstr(
|
||||
|
|
@ -165,10 +167,10 @@ class WorkspaceSubsystem:
|
|||
# Add to Python paths and also to list of stuff to be scanned
|
||||
# for meta tags.
|
||||
sys.path.insert(0, str(wspath))
|
||||
_ba.app.meta.extra_scan_dirs.append(str(wspath))
|
||||
_babase.app.meta.extra_scan_dirs.append(str(wspath))
|
||||
|
||||
# Job's done!
|
||||
_ba.pushcall(on_completed, from_other_thread=True)
|
||||
_babase.pushcall(on_completed, from_other_thread=True)
|
||||
|
||||
def _handle_deletes(self, workspace_dir: Path, deletes: list[str]) -> None:
|
||||
"""Handle file deletes."""
|
||||
|
|
@ -6,7 +6,7 @@ from __future__ import annotations
|
|||
from typing import TYPE_CHECKING
|
||||
import os
|
||||
|
||||
import _ba
|
||||
import _babase
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Sequence
|
||||
|
|
@ -17,7 +17,7 @@ def get_human_readable_user_scripts_path() -> str:
|
|||
|
||||
This is NOT a valid filesystem path; may be something like "(SD Card)".
|
||||
"""
|
||||
app = _ba.app
|
||||
app = _babase.app
|
||||
path: str | None = app.python_directory_user
|
||||
if path is None:
|
||||
return '<Not Available>'
|
||||
|
|
@ -35,7 +35,7 @@ def get_human_readable_user_scripts_path() -> str:
|
|||
# and show the whole ugly path as a fallback.
|
||||
# Note that we used to use externalStorageText resource but gonna try
|
||||
# without it for now. (simply 'foo' instead of <External Storage>/foo).
|
||||
if app.platform == 'android':
|
||||
if app.classic is not None and app.classic.platform == 'android':
|
||||
for pre in ['/storage/emulated/0/']:
|
||||
if path.startswith(pre):
|
||||
path = path.removeprefix(pre)
|
||||
|
|
@ -45,27 +45,37 @@ def get_human_readable_user_scripts_path() -> str:
|
|||
|
||||
def _request_storage_permission() -> bool:
|
||||
"""If needed, requests storage permission from the user (& return true)."""
|
||||
from ba._language import Lstr
|
||||
from ba._generated.enums import Permission
|
||||
from babase._language import Lstr
|
||||
|
||||
if not _ba.have_permission(Permission.STORAGE):
|
||||
_ba.playsound(_ba.getsound('error'))
|
||||
_ba.screenmessage(
|
||||
# noinspection PyProtectedMember
|
||||
# (PyCharm inspection bug?)
|
||||
from babase._mgen.enums import Permission
|
||||
|
||||
if not _babase.have_permission(Permission.STORAGE):
|
||||
_babase.getsimplesound('error').play()
|
||||
_babase.screenmessage(
|
||||
Lstr(resource='storagePermissionAccessText'), color=(1, 0, 0)
|
||||
)
|
||||
_ba.timer(1.0, lambda: _ba.request_permission(Permission.STORAGE))
|
||||
_babase.apptimer(
|
||||
1.0, lambda: _babase.request_permission(Permission.STORAGE)
|
||||
)
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def show_user_scripts() -> None:
|
||||
"""Open or nicely print the location of the user-scripts directory."""
|
||||
app = _ba.app
|
||||
app = _babase.app
|
||||
|
||||
# First off, if we need permission for this, ask for it.
|
||||
if _request_storage_permission():
|
||||
return
|
||||
|
||||
# If we're running in a nonstandard environment its possible this is unset.
|
||||
if app.python_directory_user is None:
|
||||
_babase.screenmessage('<unset>')
|
||||
return
|
||||
|
||||
# Secondly, if the dir doesn't exist, attempt to make it.
|
||||
if not os.path.exists(app.python_directory_user):
|
||||
os.makedirs(app.python_directory_user)
|
||||
|
|
@ -76,7 +86,7 @@ def show_user_scripts() -> None:
|
|||
# doesn't seem like there's a way to inform the media scanner of an empty
|
||||
# directory, which means they would have to reboot their device before
|
||||
# they can see it.
|
||||
if app.platform == 'android':
|
||||
if app.classic is not None and app.classic.platform == 'android':
|
||||
try:
|
||||
usd: str | None = app.python_directory_user
|
||||
if usd is not None and os.path.isdir(usd):
|
||||
|
|
@ -89,32 +99,38 @@ def show_user_scripts() -> None:
|
|||
)
|
||||
|
||||
except Exception:
|
||||
from ba import _error
|
||||
from babase import _error
|
||||
|
||||
_error.print_exception('error writing about_this_folder stuff')
|
||||
|
||||
# On a few platforms we try to open the dir in the UI.
|
||||
if app.platform in ['mac', 'windows']:
|
||||
_ba.open_dir_externally(app.python_directory_user)
|
||||
if app.classic is not None and app.classic.platform in ['mac', 'windows']:
|
||||
_babase.open_dir_externally(app.python_directory_user)
|
||||
|
||||
# Otherwise we just print a pretty version of it.
|
||||
else:
|
||||
_ba.screenmessage(get_human_readable_user_scripts_path())
|
||||
_babase.screenmessage(get_human_readable_user_scripts_path())
|
||||
|
||||
|
||||
def create_user_system_scripts() -> None:
|
||||
"""Set up a copy of Ballistica system scripts under your user scripts dir.
|
||||
"""Set up a copy of Ballistica app scripts under user scripts dir.
|
||||
|
||||
(for editing and experiment with)
|
||||
(for editing and experimenting)
|
||||
"""
|
||||
import shutil
|
||||
|
||||
app = _ba.app
|
||||
app = _babase.app
|
||||
|
||||
# First off, if we need permission for this, ask for it.
|
||||
if _request_storage_permission():
|
||||
return
|
||||
|
||||
# Its possible these are unset in non-standard environments.
|
||||
if app.python_directory_user is None:
|
||||
raise RuntimeError('user python dir unset')
|
||||
if app.python_directory_app is None:
|
||||
raise RuntimeError('app python dir unset')
|
||||
|
||||
path = app.python_directory_user + '/sys/' + app.version
|
||||
pathtmp = path + '_tmp'
|
||||
if os.path.exists(path):
|
||||
|
|
@ -138,10 +154,10 @@ def create_user_system_scripts() -> None:
|
|||
shutil.move(pathtmp, path)
|
||||
print(
|
||||
f"Created system scripts at :'{path}"
|
||||
f"'\nRestart {_ba.appname()} to use them."
|
||||
f' (use ba.quit() to exit the game)'
|
||||
f"'\nRestart {_babase.appname()} to use them."
|
||||
f' (use babase.quit() to exit the game)'
|
||||
)
|
||||
if app.platform == 'android':
|
||||
if app.classic is not None and app.classic.platform == 'android':
|
||||
print(
|
||||
'Note: the new files may not be visible via '
|
||||
'android-file-transfer until you restart your device.'
|
||||
|
|
@ -152,17 +168,21 @@ def delete_user_system_scripts() -> None:
|
|||
"""Clean out the scripts created by create_user_system_scripts()."""
|
||||
import shutil
|
||||
|
||||
app = _ba.app
|
||||
app = _babase.app
|
||||
|
||||
if app.python_directory_user is None:
|
||||
raise RuntimeError('user python dir unset')
|
||||
|
||||
path = app.python_directory_user + '/sys/' + app.version
|
||||
if os.path.exists(path):
|
||||
shutil.rmtree(path)
|
||||
print(
|
||||
f'User system scripts deleted.\n'
|
||||
f'Restart {_ba.appname()} to use internal'
|
||||
f' scripts. (use ba.quit() to exit the game)'
|
||||
f'Restart {_babase.appname()} to use internal'
|
||||
f' scripts. (use babase.quit() to exit the game)'
|
||||
)
|
||||
else:
|
||||
print('User system scripts not found.')
|
||||
print(f"User system scripts not found at '{path}'.")
|
||||
|
||||
# If the sys path is empty, kill it.
|
||||
dpath = app.python_directory_user + '/sys'
|
||||
48
dist/ba_data/python/baclassic/__init__.py
vendored
Normal file
48
dist/ba_data/python/baclassic/__init__.py
vendored
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Classic ballistica components.
|
||||
|
||||
This package is used as a 'dumping ground' for functionality that is
|
||||
necessary to keep legacy parts of the app working, but which may no
|
||||
longer be the best way to do things going forward.
|
||||
|
||||
New code should try to avoid using code from here when possible.
|
||||
|
||||
Functionality in this package should be exposed through the
|
||||
ClassicSubsystem. This allows type-checked code to go through the
|
||||
babase.app.classic singleton which forces it to explicitly handle the
|
||||
possibility of babase.app.classic being None. When code instead imports
|
||||
classic submodules directly, it is much harder to make it cleanly handle
|
||||
classic not being present.
|
||||
"""
|
||||
|
||||
# ba_meta require api 8
|
||||
|
||||
# Note: Code relying on classic should import things from here *only*
|
||||
# for type-checking and use the versions in app.classic at runtime; that
|
||||
# way type-checking will cleanly cover the classic-not-present case
|
||||
# (app.classic being None).
|
||||
import logging
|
||||
|
||||
from baclassic._subsystem import ClassicSubsystem
|
||||
from baclassic._achievement import Achievement, AchievementSubsystem
|
||||
|
||||
__all__ = [
|
||||
'ClassicSubsystem',
|
||||
'Achievement',
|
||||
'AchievementSubsystem',
|
||||
]
|
||||
|
||||
# Sanity check: we want to keep ballistica's dependencies and
|
||||
# bootstrapping order clearly defined; let's check a few particular
|
||||
# modules to make sure they never directly or indirectly import us
|
||||
# before their own execs complete.
|
||||
if __debug__:
|
||||
for _mdl in 'babase', '_babase':
|
||||
if not hasattr(__import__(_mdl), '_REACHED_END_OF_MODULE'):
|
||||
logging.warning(
|
||||
'%s was imported before %s finished importing;'
|
||||
' should not happen.',
|
||||
__name__,
|
||||
_mdl,
|
||||
)
|
||||
|
|
@ -8,8 +8,7 @@ import copy
|
|||
import time
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import _ba
|
||||
from ba import _internal
|
||||
import babase
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any
|
||||
|
|
@ -20,7 +19,7 @@ class AccountV1Subsystem:
|
|||
|
||||
Category: **App Classes**
|
||||
|
||||
Access the single shared instance of this class at 'ba.app.accounts_v1'.
|
||||
Access the single instance of this class at 'ba.app.classic.accounts'.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
|
|
@ -35,18 +34,20 @@ class AccountV1Subsystem:
|
|||
# not be signed in yet; go ahead and queue them up in that case.
|
||||
self.pending_promo_codes: list[str] = []
|
||||
|
||||
def on_app_launch(self) -> None:
|
||||
def on_app_loading(self) -> None:
|
||||
"""Called when the app is done bootstrapping."""
|
||||
|
||||
# Auto-sign-in to a local account in a moment if we're set to.
|
||||
def do_auto_sign_in() -> None:
|
||||
if babase.app.plus is None:
|
||||
return
|
||||
if (
|
||||
_ba.app.headless_mode
|
||||
or _ba.app.config.get('Auto Account State') == 'Local'
|
||||
babase.app.headless_mode
|
||||
or babase.app.config.get('Auto Account State') == 'Local'
|
||||
):
|
||||
_internal.sign_in_v1('Local')
|
||||
babase.app.plus.sign_in_v1('Local')
|
||||
|
||||
_ba.pushcall(do_auto_sign_in)
|
||||
babase.pushcall(do_auto_sign_in)
|
||||
|
||||
def on_app_pause(self) -> None:
|
||||
"""Should be called when app is pausing."""
|
||||
|
|
@ -64,16 +65,14 @@ class AccountV1Subsystem:
|
|||
|
||||
(internal)
|
||||
"""
|
||||
from ba._language import Lstr
|
||||
|
||||
_ba.screenmessage(
|
||||
Lstr(
|
||||
babase.screenmessage(
|
||||
babase.Lstr(
|
||||
resource='getTicketsWindow.receivedTicketsText',
|
||||
subs=[('${COUNT}', str(count))],
|
||||
),
|
||||
color=(0, 1, 0),
|
||||
)
|
||||
_ba.playsound(_ba.getsound('cashRegister'))
|
||||
babase.getsimplesound('cashRegister').play()
|
||||
|
||||
def cache_league_rank_data(self, data: Any) -> None:
|
||||
"""(internal)"""
|
||||
|
|
@ -96,7 +95,8 @@ class AccountV1Subsystem:
|
|||
total_ach_value = data['at']
|
||||
else:
|
||||
total_ach_value = 0
|
||||
for ach in _ba.app.ach.achievements:
|
||||
assert babase.app.classic is not None
|
||||
for ach in babase.app.classic.ach.achievements:
|
||||
if ach.complete:
|
||||
total_ach_value += ach.power_ranking_value
|
||||
|
||||
|
|
@ -126,15 +126,18 @@ class AccountV1Subsystem:
|
|||
raise ValueError('invalid subset value: ' + str(subset))
|
||||
|
||||
if data['p']:
|
||||
pro_mult = (
|
||||
1.0
|
||||
+ float(
|
||||
_internal.get_v1_account_misc_read_val(
|
||||
'proPowerRankingBoost', 0.0
|
||||
if babase.app.plus is None:
|
||||
pro_mult = 1.0
|
||||
else:
|
||||
pro_mult = (
|
||||
1.0
|
||||
+ float(
|
||||
babase.app.plus.get_v1_account_misc_read_val(
|
||||
'proPowerRankingBoost', 0.0
|
||||
)
|
||||
)
|
||||
* 0.01
|
||||
)
|
||||
* 0.01
|
||||
)
|
||||
else:
|
||||
pro_mult = 1.0
|
||||
|
||||
|
|
@ -147,7 +150,6 @@ class AccountV1Subsystem:
|
|||
|
||||
def cache_tournament_info(self, info: Any) -> None:
|
||||
"""(internal)"""
|
||||
from ba._generated.enums import TimeType, TimeFormat
|
||||
|
||||
for entry in info:
|
||||
cache_entry = self.tournament_info[
|
||||
|
|
@ -156,24 +158,25 @@ class AccountV1Subsystem:
|
|||
|
||||
# Also store the time we received this, so we can adjust
|
||||
# time-remaining values/etc.
|
||||
cache_entry['timeReceived'] = _ba.time(
|
||||
TimeType.REAL, TimeFormat.MILLISECONDS
|
||||
)
|
||||
cache_entry['timeReceived'] = babase.apptime()
|
||||
cache_entry['valid'] = True
|
||||
|
||||
def get_purchased_icons(self) -> list[str]:
|
||||
"""(internal)"""
|
||||
# pylint: disable=cyclic-import
|
||||
from ba import _store
|
||||
|
||||
if _internal.get_v1_account_state() != 'signed_in':
|
||||
plus = babase.app.plus
|
||||
if plus is None:
|
||||
return []
|
||||
if plus.get_v1_account_state() != 'signed_in':
|
||||
return []
|
||||
icons = []
|
||||
store_items = _store.get_store_items()
|
||||
store_items: dict[str, Any] = (
|
||||
babase.app.classic.store.get_store_items()
|
||||
if babase.app.classic is not None
|
||||
else {}
|
||||
)
|
||||
for item_name, item in list(store_items.items()):
|
||||
if item_name.startswith('icons.') and _internal.get_purchased(
|
||||
item_name
|
||||
):
|
||||
if item_name.startswith('icons.') and plus.get_purchased(item_name):
|
||||
icons.append(item['icon'])
|
||||
return icons
|
||||
|
||||
|
|
@ -184,25 +187,27 @@ class AccountV1Subsystem:
|
|||
|
||||
(internal)
|
||||
"""
|
||||
plus = babase.app.plus
|
||||
if plus is None:
|
||||
return
|
||||
# This only applies when we're signed in.
|
||||
if _internal.get_v1_account_state() != 'signed_in':
|
||||
if plus.get_v1_account_state() != 'signed_in':
|
||||
return
|
||||
|
||||
# If the short version of our account name currently cant be
|
||||
# displayed by the game, cancel.
|
||||
if not _ba.have_chars(
|
||||
_internal.get_v1_account_display_string(full=False)
|
||||
if not babase.have_chars(
|
||||
plus.get_v1_account_display_string(full=False)
|
||||
):
|
||||
return
|
||||
|
||||
config = _ba.app.config
|
||||
config = babase.app.config
|
||||
if (
|
||||
'Player Profiles' not in config
|
||||
or '__account__' not in config['Player Profiles']
|
||||
):
|
||||
|
||||
# Create a spaz with a nice default purply color.
|
||||
_internal.add_transaction(
|
||||
plus.add_v1_account_transaction(
|
||||
{
|
||||
'type': 'ADD_PLAYER_PROFILE',
|
||||
'name': '__account__',
|
||||
|
|
@ -213,18 +218,22 @@ class AccountV1Subsystem:
|
|||
},
|
||||
}
|
||||
)
|
||||
_internal.run_transactions()
|
||||
plus.run_v1_account_transactions()
|
||||
|
||||
def have_pro(self) -> bool:
|
||||
"""Return whether pro is currently unlocked."""
|
||||
|
||||
plus = babase.app.plus
|
||||
if plus is None:
|
||||
return False
|
||||
|
||||
# Check our tickets-based pro upgrade and our two real-IAP based
|
||||
# upgrades. Also always unlock this stuff in ballistica-core builds.
|
||||
return bool(
|
||||
_internal.get_purchased('upgrades.pro')
|
||||
or _internal.get_purchased('static.pro')
|
||||
or _internal.get_purchased('static.pro_sale')
|
||||
or 'ballistica' + 'core' == _ba.appname()
|
||||
plus.get_purchased('upgrades.pro')
|
||||
or plus.get_purchased('static.pro')
|
||||
or plus.get_purchased('static.pro_sale')
|
||||
or 'ballistica' + 'kit' == babase.appname()
|
||||
)
|
||||
|
||||
def have_pro_options(self) -> bool:
|
||||
|
|
@ -234,70 +243,75 @@ class AccountV1Subsystem:
|
|||
before Pro was a requirement for these options.
|
||||
"""
|
||||
|
||||
plus = babase.app.plus
|
||||
if plus is None:
|
||||
return False
|
||||
|
||||
# We expose pro options if the server tells us to
|
||||
# (which is generally just when we own pro),
|
||||
# or also if we've been grandfathered in
|
||||
# or are using ballistica-core builds.
|
||||
return self.have_pro() or bool(
|
||||
_internal.get_v1_account_misc_read_val_2(
|
||||
'proOptionsUnlocked', False
|
||||
)
|
||||
or _ba.app.config.get('lc14292', 0) > 1
|
||||
plus.get_v1_account_misc_read_val_2('proOptionsUnlocked', False)
|
||||
or babase.app.config.get('lc14292', 0) > 1
|
||||
)
|
||||
|
||||
def show_post_purchase_message(self) -> None:
|
||||
"""(internal)"""
|
||||
from ba._language import Lstr
|
||||
from ba._generated.enums import TimeType
|
||||
|
||||
cur_time = _ba.time(TimeType.REAL)
|
||||
cur_time = babase.apptime()
|
||||
if (
|
||||
self.last_post_purchase_message_time is None
|
||||
or cur_time - self.last_post_purchase_message_time > 3.0
|
||||
):
|
||||
self.last_post_purchase_message_time = cur_time
|
||||
with _ba.Context('ui'):
|
||||
_ba.screenmessage(
|
||||
Lstr(
|
||||
resource='updatingAccountText',
|
||||
fallback_resource='purchasingText',
|
||||
),
|
||||
color=(0, 1, 0),
|
||||
)
|
||||
_ba.playsound(_ba.getsound('click01'))
|
||||
babase.screenmessage(
|
||||
babase.Lstr(
|
||||
resource='updatingAccountText',
|
||||
fallback_resource='purchasingText',
|
||||
),
|
||||
color=(0, 1, 0),
|
||||
)
|
||||
babase.getsimplesound('click01').play()
|
||||
|
||||
def on_account_state_changed(self) -> None:
|
||||
"""(internal)"""
|
||||
from ba._language import Lstr
|
||||
|
||||
plus = babase.app.plus
|
||||
if plus is None:
|
||||
return
|
||||
# Run any pending promo codes we had queued up while not signed in.
|
||||
if (
|
||||
_internal.get_v1_account_state() == 'signed_in'
|
||||
plus.get_v1_account_state() == 'signed_in'
|
||||
and self.pending_promo_codes
|
||||
):
|
||||
for code in self.pending_promo_codes:
|
||||
_ba.screenmessage(
|
||||
Lstr(resource='submittingPromoCodeText'), color=(0, 1, 0)
|
||||
babase.screenmessage(
|
||||
babase.Lstr(resource='submittingPromoCodeText'),
|
||||
color=(0, 1, 0),
|
||||
)
|
||||
_internal.add_transaction(
|
||||
plus.add_v1_account_transaction(
|
||||
{
|
||||
'type': 'PROMO_CODE',
|
||||
'expire_time': time.time() + 5,
|
||||
'code': code,
|
||||
}
|
||||
)
|
||||
_internal.run_transactions()
|
||||
plus.run_v1_account_transactions()
|
||||
self.pending_promo_codes = []
|
||||
|
||||
def add_pending_promo_code(self, code: str) -> None:
|
||||
"""(internal)"""
|
||||
from ba._language import Lstr
|
||||
from ba._generated.enums import TimeType
|
||||
plus = babase.app.plus
|
||||
if plus is None:
|
||||
babase.screenmessage(
|
||||
babase.Lstr(resource='errorText'), color=(1, 0, 0)
|
||||
)
|
||||
babase.getsimplesound('error').play()
|
||||
return
|
||||
|
||||
# If we're not signed in, queue up the code to run the next time we
|
||||
# are and issue a warning if we haven't signed in within the next
|
||||
# few seconds.
|
||||
if _internal.get_v1_account_state() != 'signed_in':
|
||||
if plus.get_v1_account_state() != 'signed_in':
|
||||
|
||||
def check_pending_codes() -> None:
|
||||
"""(internal)"""
|
||||
|
|
@ -305,18 +319,19 @@ class AccountV1Subsystem:
|
|||
# If we're still not signed in and have pending codes,
|
||||
# inform the user that they need to sign in to use them.
|
||||
if self.pending_promo_codes:
|
||||
_ba.screenmessage(
|
||||
Lstr(resource='signInForPromoCodeText'), color=(1, 0, 0)
|
||||
babase.screenmessage(
|
||||
babase.Lstr(resource='signInForPromoCodeText'),
|
||||
color=(1, 0, 0),
|
||||
)
|
||||
_ba.playsound(_ba.getsound('error'))
|
||||
babase.getsimplesound('error').play()
|
||||
|
||||
self.pending_promo_codes.append(code)
|
||||
_ba.timer(6.0, check_pending_codes, timetype=TimeType.REAL)
|
||||
babase.apptimer(6.0, check_pending_codes)
|
||||
return
|
||||
_ba.screenmessage(
|
||||
Lstr(resource='submittingPromoCodeText'), color=(0, 1, 0)
|
||||
babase.screenmessage(
|
||||
babase.Lstr(resource='submittingPromoCodeText'), color=(0, 1, 0)
|
||||
)
|
||||
_internal.add_transaction(
|
||||
plus.add_v1_account_transaction(
|
||||
{'type': 'PROMO_CODE', 'expire_time': time.time() + 5, 'code': code}
|
||||
)
|
||||
_internal.run_transactions()
|
||||
plus.run_v1_account_transactions()
|
||||
|
|
@ -3,15 +3,17 @@
|
|||
"""Various functionality related to achievements."""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import _ba
|
||||
from ba import _internal
|
||||
from ba._error import print_exception
|
||||
import babase
|
||||
import bascenev1
|
||||
import bauiv1
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any, Sequence
|
||||
import ba
|
||||
|
||||
import baclassic
|
||||
|
||||
# This could use some cleanup.
|
||||
# We wear the cone of shame.
|
||||
|
|
@ -73,8 +75,10 @@ class AchievementSubsystem:
|
|||
|
||||
def __init__(self) -> None:
|
||||
self.achievements: list[Achievement] = []
|
||||
self.achievements_to_display: (list[tuple[ba.Achievement, bool]]) = []
|
||||
self.achievement_display_timer: _ba.Timer | None = None
|
||||
self.achievements_to_display: (
|
||||
list[tuple[baclassic.Achievement, bool]]
|
||||
) = []
|
||||
self.achievement_display_timer: bascenev1.BaseTimer | None = None
|
||||
self.last_achievement_display_time: float = 0.0
|
||||
self.achievement_completion_banner_slots: set[int] = set()
|
||||
self._init_achievements()
|
||||
|
|
@ -508,15 +512,18 @@ class AchievementSubsystem:
|
|||
|
||||
def award_local_achievement(self, achname: str) -> None:
|
||||
"""For non-game-based achievements such as controller-connection."""
|
||||
plus = babase.app.plus
|
||||
if plus is None:
|
||||
logging.warning('achievements require plus feature-set')
|
||||
return
|
||||
try:
|
||||
ach = self.get_achievement(achname)
|
||||
if not ach.complete:
|
||||
|
||||
# Report new achievements to the game-service.
|
||||
_internal.report_achievement(achname)
|
||||
plus.report_achievement(achname)
|
||||
|
||||
# And to our account.
|
||||
_internal.add_transaction(
|
||||
plus.add_v1_account_transaction(
|
||||
{'type': 'ACHIEVEMENT', 'name': achname}
|
||||
)
|
||||
|
||||
|
|
@ -524,7 +531,7 @@ class AchievementSubsystem:
|
|||
self.display_achievement_banner(achname)
|
||||
|
||||
except Exception:
|
||||
print_exception()
|
||||
logging.exception('Error in award_local_achievement.')
|
||||
|
||||
def display_achievement_banner(self, achname: str) -> None:
|
||||
"""Display a completion banner for an achievement.
|
||||
|
|
@ -538,12 +545,12 @@ class AchievementSubsystem:
|
|||
# purely local context somehow instead of trying to inject these
|
||||
# into whatever activity happens to be active
|
||||
# (since that won't work while in client mode).
|
||||
activity = _ba.get_foreground_host_activity()
|
||||
activity = bascenev1.get_foreground_host_activity()
|
||||
if activity is not None:
|
||||
with _ba.Context(activity):
|
||||
with activity.context:
|
||||
self.get_achievement(achname).announce_completion()
|
||||
except Exception:
|
||||
print_exception('error showing server ach')
|
||||
logging.exception('Error in display_achievement_banner.')
|
||||
|
||||
def set_completed_achievements(self, achs: Sequence[str]) -> None:
|
||||
"""Set the current state of completed achievements.
|
||||
|
|
@ -557,7 +564,7 @@ class AchievementSubsystem:
|
|||
# us which achievements we currently have. We always defer to them,
|
||||
# even if that means we have to un-set an achievement we think we have.
|
||||
|
||||
cfg = _ba.app.config
|
||||
cfg = babase.app.config
|
||||
cfg['Achievements'] = {}
|
||||
for a_name in achs:
|
||||
self.get_achievement(a_name).set_complete(True)
|
||||
|
|
@ -586,7 +593,6 @@ class AchievementSubsystem:
|
|||
|
||||
def _test(self) -> None:
|
||||
"""For testing achievement animations."""
|
||||
from ba._generated.enums import TimeType
|
||||
|
||||
def testcall1() -> None:
|
||||
self.achievements[0].announce_completion()
|
||||
|
|
@ -598,8 +604,8 @@ class AchievementSubsystem:
|
|||
self.achievements[4].announce_completion()
|
||||
self.achievements[5].announce_completion()
|
||||
|
||||
_ba.timer(3.0, testcall1, timetype=TimeType.BASE)
|
||||
_ba.timer(7.0, testcall2, timetype=TimeType.BASE)
|
||||
bascenev1.basetimer(3.0, testcall1)
|
||||
bascenev1.basetimer(7.0, testcall2)
|
||||
|
||||
|
||||
def _get_ach_mult(include_pro_bonus: bool = False) -> int:
|
||||
|
|
@ -607,28 +613,33 @@ def _get_ach_mult(include_pro_bonus: bool = False) -> int:
|
|||
|
||||
(just for display; changing this here won't affect actual rewards)
|
||||
"""
|
||||
val: int = _internal.get_v1_account_misc_read_val('achAwardMult', 5)
|
||||
plus = babase.app.plus
|
||||
classic = babase.app.classic
|
||||
if plus is None or classic is None:
|
||||
return 5
|
||||
val: int = plus.get_v1_account_misc_read_val('achAwardMult', 5)
|
||||
assert isinstance(val, int)
|
||||
if include_pro_bonus and _ba.app.accounts_v1.have_pro():
|
||||
if include_pro_bonus and classic.accounts.have_pro():
|
||||
val *= 2
|
||||
return val
|
||||
|
||||
|
||||
def _display_next_achievement() -> None:
|
||||
|
||||
# Pull the first achievement off the list and display it, or kill the
|
||||
# display-timer if the list is empty.
|
||||
app = _ba.app
|
||||
if app.ach.achievements_to_display:
|
||||
app = babase.app
|
||||
assert app.classic is not None
|
||||
ach_ss = app.classic.ach
|
||||
if app.classic.ach.achievements_to_display:
|
||||
try:
|
||||
ach, sound = app.ach.achievements_to_display.pop(0)
|
||||
ach, sound = ach_ss.achievements_to_display.pop(0)
|
||||
ach.show_completion_banner(sound)
|
||||
except Exception:
|
||||
print_exception('error showing next achievement')
|
||||
app.ach.achievements_to_display = []
|
||||
app.ach.achievement_display_timer = None
|
||||
logging.exception('Error in _display_next_achievement.')
|
||||
ach_ss.achievements_to_display = []
|
||||
ach_ss.achievement_display_timer = None
|
||||
else:
|
||||
app.ach.achievement_display_timer = None
|
||||
ach_ss.achievement_display_timer = None
|
||||
|
||||
|
||||
class Achievement:
|
||||
|
|
@ -664,9 +675,15 @@ class Achievement:
|
|||
"""The name of the level this achievement applies to."""
|
||||
return self._level_name
|
||||
|
||||
def get_icon_texture(self, complete: bool) -> ba.Texture:
|
||||
def get_icon_ui_texture(self, complete: bool) -> bauiv1.Texture:
|
||||
"""Return the icon texture to display for this achievement"""
|
||||
return _ba.gettexture(
|
||||
return bauiv1.gettexture(
|
||||
self._icon_name if complete else 'achievementEmpty'
|
||||
)
|
||||
|
||||
def get_icon_texture(self, complete: bool) -> bascenev1.Texture:
|
||||
"""Return the icon texture to display for this achievement"""
|
||||
return bascenev1.gettexture(
|
||||
self._icon_name if complete else 'achievementEmpty'
|
||||
)
|
||||
|
||||
|
|
@ -690,20 +707,26 @@ class Achievement:
|
|||
|
||||
def announce_completion(self, sound: bool = True) -> None:
|
||||
"""Kick off an announcement for this achievement's completion."""
|
||||
from ba._generated.enums import TimeType
|
||||
|
||||
app = _ba.app
|
||||
app = babase.app
|
||||
plus = app.plus
|
||||
classic = app.classic
|
||||
if plus is None or classic is None:
|
||||
logging.warning('ach account_completion not available.')
|
||||
return
|
||||
|
||||
ach_ss = classic.ach
|
||||
|
||||
# Even though there are technically achievements when we're not
|
||||
# signed in, lets not show them (otherwise we tend to get
|
||||
# confusing 'controller connected' achievements popping up while
|
||||
# waiting to sign in which can be confusing).
|
||||
if _internal.get_v1_account_state() != 'signed_in':
|
||||
if plus.get_v1_account_state() != 'signed_in':
|
||||
return
|
||||
|
||||
# If we're being freshly complete, display/report it and whatnot.
|
||||
if (self, sound) not in app.ach.achievements_to_display:
|
||||
app.ach.achievements_to_display.append((self, sound))
|
||||
if (self, sound) not in ach_ss.achievements_to_display:
|
||||
ach_ss.achievements_to_display.append((self, sound))
|
||||
|
||||
# If there's no achievement display timer going, kick one off
|
||||
# (if one's already running it will pick this up before it dies).
|
||||
|
|
@ -711,15 +734,11 @@ class Achievement:
|
|||
# Need to check last time too; its possible our timer wasn't able to
|
||||
# clear itself if an activity died and took it down with it.
|
||||
if (
|
||||
app.ach.achievement_display_timer is None
|
||||
or _ba.time(TimeType.REAL) - app.ach.last_achievement_display_time
|
||||
> 2.0
|
||||
) and _ba.getactivity(doraise=False) is not None:
|
||||
app.ach.achievement_display_timer = _ba.Timer(
|
||||
1.0,
|
||||
_display_next_achievement,
|
||||
repeat=True,
|
||||
timetype=TimeType.BASE,
|
||||
ach_ss.achievement_display_timer is None
|
||||
or babase.apptime() - ach_ss.last_achievement_display_time > 2.0
|
||||
) and bascenev1.getactivity(doraise=False) is not None:
|
||||
ach_ss.achievement_display_timer = bascenev1.BaseTimer(
|
||||
1.0, _display_next_achievement, repeat=True
|
||||
)
|
||||
|
||||
# Show the first immediately.
|
||||
|
|
@ -736,18 +755,16 @@ class Achievement:
|
|||
config['Complete'] = complete
|
||||
|
||||
@property
|
||||
def display_name(self) -> ba.Lstr:
|
||||
"""Return a ba.Lstr for this Achievement's name."""
|
||||
from ba._language import Lstr
|
||||
|
||||
name: ba.Lstr | str
|
||||
def display_name(self) -> babase.Lstr:
|
||||
"""Return a babase.Lstr for this Achievement's name."""
|
||||
name: babase.Lstr | str
|
||||
try:
|
||||
if self._level_name != '':
|
||||
from ba._campaign import getcampaign
|
||||
|
||||
campaignname, campaign_level = self._level_name.split(':')
|
||||
classic = babase.app.classic
|
||||
assert classic is not None
|
||||
name = (
|
||||
getcampaign(campaignname)
|
||||
classic.getcampaign(campaignname)
|
||||
.getlevel(campaign_level)
|
||||
.displayname
|
||||
)
|
||||
|
|
@ -755,51 +772,49 @@ class Achievement:
|
|||
name = ''
|
||||
except Exception:
|
||||
name = ''
|
||||
print_exception()
|
||||
return Lstr(
|
||||
logging.exception('Error calcing achievement display-name.')
|
||||
return babase.Lstr(
|
||||
resource='achievements.' + self._name + '.name',
|
||||
subs=[('${LEVEL}', name)],
|
||||
)
|
||||
|
||||
@property
|
||||
def description(self) -> ba.Lstr:
|
||||
"""Get a ba.Lstr for the Achievement's brief description."""
|
||||
from ba._language import Lstr
|
||||
|
||||
def description(self) -> babase.Lstr:
|
||||
"""Get a babase.Lstr for the Achievement's brief description."""
|
||||
if (
|
||||
'description'
|
||||
in _ba.app.lang.get_resource('achievements')[self._name]
|
||||
in babase.app.lang.get_resource('achievements')[self._name]
|
||||
):
|
||||
return Lstr(resource='achievements.' + self._name + '.description')
|
||||
return Lstr(resource='achievements.' + self._name + '.descriptionFull')
|
||||
return babase.Lstr(
|
||||
resource='achievements.' + self._name + '.description'
|
||||
)
|
||||
return babase.Lstr(
|
||||
resource='achievements.' + self._name + '.descriptionFull'
|
||||
)
|
||||
|
||||
@property
|
||||
def description_complete(self) -> ba.Lstr:
|
||||
"""Get a ba.Lstr for the Achievement's description when completed."""
|
||||
from ba._language import Lstr
|
||||
|
||||
def description_complete(self) -> babase.Lstr:
|
||||
"""Get a babase.Lstr for the Achievement's description when complete."""
|
||||
if (
|
||||
'descriptionComplete'
|
||||
in _ba.app.lang.get_resource('achievements')[self._name]
|
||||
in babase.app.lang.get_resource('achievements')[self._name]
|
||||
):
|
||||
return Lstr(
|
||||
return babase.Lstr(
|
||||
resource='achievements.' + self._name + '.descriptionComplete'
|
||||
)
|
||||
return Lstr(
|
||||
return babase.Lstr(
|
||||
resource='achievements.' + self._name + '.descriptionFullComplete'
|
||||
)
|
||||
|
||||
@property
|
||||
def description_full(self) -> ba.Lstr:
|
||||
"""Get a ba.Lstr for the Achievement's full description."""
|
||||
from ba._language import Lstr
|
||||
|
||||
return Lstr(
|
||||
def description_full(self) -> babase.Lstr:
|
||||
"""Get a babase.Lstr for the Achievement's full description."""
|
||||
return babase.Lstr(
|
||||
resource='achievements.' + self._name + '.descriptionFull',
|
||||
subs=[
|
||||
(
|
||||
'${LEVEL}',
|
||||
Lstr(
|
||||
babase.Lstr(
|
||||
translate=(
|
||||
'coopLevelNames',
|
||||
ACH_LEVEL_NAMES.get(self._name, '?'),
|
||||
|
|
@ -810,16 +825,14 @@ class Achievement:
|
|||
)
|
||||
|
||||
@property
|
||||
def description_full_complete(self) -> ba.Lstr:
|
||||
"""Get a ba.Lstr for the Achievement's full desc. when completed."""
|
||||
from ba._language import Lstr
|
||||
|
||||
return Lstr(
|
||||
def description_full_complete(self) -> babase.Lstr:
|
||||
"""Get a babase.Lstr for the Achievement's full desc. when completed."""
|
||||
return babase.Lstr(
|
||||
resource='achievements.' + self._name + '.descriptionFullComplete',
|
||||
subs=[
|
||||
(
|
||||
'${LEVEL}',
|
||||
Lstr(
|
||||
babase.Lstr(
|
||||
translate=(
|
||||
'coopLevelNames',
|
||||
ACH_LEVEL_NAMES.get(self._name, '?'),
|
||||
|
|
@ -831,7 +844,10 @@ class Achievement:
|
|||
|
||||
def get_award_ticket_value(self, include_pro_bonus: bool = False) -> int:
|
||||
"""Get the ticket award value for this achievement."""
|
||||
val: int = _internal.get_v1_account_misc_read_val(
|
||||
plus = babase.app.plus
|
||||
if plus is None:
|
||||
return 0
|
||||
val: int = plus.get_v1_account_misc_read_val(
|
||||
'achAward.' + self._name, self._award
|
||||
) * _get_ach_mult(include_pro_bonus)
|
||||
assert isinstance(val, int)
|
||||
|
|
@ -840,7 +856,10 @@ class Achievement:
|
|||
@property
|
||||
def power_ranking_value(self) -> int:
|
||||
"""Get the power-ranking award value for this achievement."""
|
||||
val: int = _internal.get_v1_account_misc_read_val(
|
||||
plus = babase.app.plus
|
||||
if plus is None:
|
||||
return 0
|
||||
val: int = plus.get_v1_account_misc_read_val(
|
||||
'achLeaguePoints.' + self._name, self._award
|
||||
)
|
||||
assert isinstance(val, int)
|
||||
|
|
@ -854,17 +873,15 @@ class Achievement:
|
|||
outdelay: float | None = None,
|
||||
color: Sequence[float] | None = None,
|
||||
style: str = 'post_game',
|
||||
) -> list[ba.Actor]:
|
||||
) -> list[bascenev1.Actor]:
|
||||
"""Create a display for the Achievement.
|
||||
|
||||
Shows the Achievement icon, name, and description.
|
||||
"""
|
||||
# pylint: disable=cyclic-import
|
||||
from ba._language import Lstr
|
||||
from ba._generated.enums import SpecialChar
|
||||
from ba._coopsession import CoopSession
|
||||
from bastd.actor.image import Image
|
||||
from bastd.actor.text import Text
|
||||
from bascenev1 import CoopSession
|
||||
from bascenev1lib.actor.image import Image
|
||||
from bascenev1lib.actor.text import Text
|
||||
|
||||
# Yeah this needs cleaning up.
|
||||
if style == 'post_game':
|
||||
|
|
@ -894,7 +911,7 @@ class Achievement:
|
|||
hmo = False
|
||||
else:
|
||||
try:
|
||||
session = _ba.getsession()
|
||||
session = bascenev1.getsession()
|
||||
if isinstance(session, CoopSession):
|
||||
campaign = session.campaign
|
||||
assert campaign is not None
|
||||
|
|
@ -902,10 +919,10 @@ class Achievement:
|
|||
else:
|
||||
hmo = False
|
||||
except Exception:
|
||||
print_exception('Error determining campaign.')
|
||||
logging.exception('Error determining campaign.')
|
||||
hmo = False
|
||||
|
||||
objs: list[ba.Actor]
|
||||
objs: list[bascenev1.Actor]
|
||||
|
||||
if in_game_colors:
|
||||
objs = []
|
||||
|
|
@ -978,7 +995,7 @@ class Achievement:
|
|||
|
||||
if hmo:
|
||||
txtactor = Text(
|
||||
Lstr(resource='difficultyHardOnlyText'),
|
||||
babase.Lstr(resource='difficultyHardOnlyText'),
|
||||
host_only=True,
|
||||
maxwidth=txt2_max_w * 0.7,
|
||||
position=(x + 60, y + 5),
|
||||
|
|
@ -1002,7 +1019,7 @@ class Achievement:
|
|||
award_x = -100
|
||||
objs.append(
|
||||
Text(
|
||||
_ba.charstr(SpecialChar.TICKET),
|
||||
babase.charstr(babase.SpecialChar.TICKET),
|
||||
host_only=True,
|
||||
position=(x + award_x + 33, y + 7),
|
||||
transition=Text.Transition.FADE_IN,
|
||||
|
|
@ -1057,9 +1074,11 @@ class Achievement:
|
|||
if complete:
|
||||
objs.append(
|
||||
Image(
|
||||
_ba.gettexture('achievementOutline'),
|
||||
bascenev1.gettexture('achievementOutline'),
|
||||
host_only=True,
|
||||
model_transparent=_ba.getmodel('achievementOutline'),
|
||||
mesh_transparent=bascenev1.getmesh(
|
||||
'achievementOutline'
|
||||
),
|
||||
color=(2, 1.4, 0.4, 1),
|
||||
vr_depth=8,
|
||||
position=(x - 25, y + 5),
|
||||
|
|
@ -1075,7 +1094,7 @@ class Achievement:
|
|||
award_x = -100
|
||||
objs.append(
|
||||
Text(
|
||||
_ba.charstr(SpecialChar.TICKET),
|
||||
babase.charstr(babase.SpecialChar.TICKET),
|
||||
host_only=True,
|
||||
position=(x + award_x + 33, y + 7),
|
||||
transition=Text.Transition.IN_RIGHT,
|
||||
|
|
@ -1084,9 +1103,7 @@ class Achievement:
|
|||
v_attach=v_attach,
|
||||
h_align=Text.HAlign.CENTER,
|
||||
v_align=Text.VAlign.CENTER,
|
||||
color=(1, 1, 1, 0.4)
|
||||
if complete
|
||||
else (1, 1, 1, (0.1 if hmo else 0.2)),
|
||||
color=(1, 1, 1, (0.1 if hmo else 0.2)),
|
||||
transition_delay=delay + 0.05,
|
||||
transition_out_delay=None,
|
||||
).autoretain()
|
||||
|
|
@ -1103,11 +1120,7 @@ class Achievement:
|
|||
v_attach=v_attach,
|
||||
h_align=Text.HAlign.CENTER,
|
||||
v_align=Text.VAlign.CENTER,
|
||||
color=(
|
||||
(0.8, 0.93, 0.8, 1.0)
|
||||
if complete
|
||||
else (0.6, 0.6, 0.6, (0.2 if hmo else 0.4))
|
||||
),
|
||||
color=(0.6, 0.6, 0.6, (0.2 if hmo else 0.4)),
|
||||
transition_delay=delay + 0.05,
|
||||
transition_out_delay=None,
|
||||
).autoretain()
|
||||
|
|
@ -1117,7 +1130,7 @@ class Achievement:
|
|||
# when that's the case.
|
||||
if hmo:
|
||||
txtactor = Text(
|
||||
Lstr(resource='difficultyHardOnlyText'),
|
||||
babase.Lstr(resource='difficultyHardOnlyText'),
|
||||
host_only=True,
|
||||
maxwidth=300 * 0.7,
|
||||
position=(x + 60, y + 5),
|
||||
|
|
@ -1186,35 +1199,33 @@ class Achievement:
|
|||
Return the sub-dict in settings where this achievement's
|
||||
state is stored, creating it if need be.
|
||||
"""
|
||||
val: dict[str, Any] = _ba.app.config.setdefault(
|
||||
val: dict[str, Any] = babase.app.config.setdefault(
|
||||
'Achievements', {}
|
||||
).setdefault(self._name, {'Complete': False})
|
||||
assert isinstance(val, dict)
|
||||
return val
|
||||
|
||||
def _remove_banner_slot(self) -> None:
|
||||
classic = babase.app.classic
|
||||
assert classic is not None
|
||||
assert self._completion_banner_slot is not None
|
||||
_ba.app.ach.achievement_completion_banner_slots.remove(
|
||||
classic.ach.achievement_completion_banner_slots.remove(
|
||||
self._completion_banner_slot
|
||||
)
|
||||
self._completion_banner_slot = None
|
||||
|
||||
def show_completion_banner(self, sound: bool = True) -> None:
|
||||
"""Create the banner/sound for an acquired achievement announcement."""
|
||||
from ba import _gameutils
|
||||
from bastd.actor.text import Text
|
||||
from bastd.actor.image import Image
|
||||
from ba._general import WeakCall
|
||||
from ba._language import Lstr
|
||||
from ba._messages import DieMessage
|
||||
from ba._generated.enums import TimeType, SpecialChar
|
||||
from bascenev1lib.actor.text import Text
|
||||
from bascenev1lib.actor.image import Image
|
||||
|
||||
app = _ba.app
|
||||
app.ach.last_achievement_display_time = _ba.time(TimeType.REAL)
|
||||
app = babase.app
|
||||
assert app.classic is not None
|
||||
app.classic.ach.last_achievement_display_time = babase.apptime()
|
||||
|
||||
# Just piggy-back onto any current activity
|
||||
# (should we use the session instead?..)
|
||||
activity = _ba.getactivity(doraise=False)
|
||||
activity = bascenev1.getactivity(doraise=False)
|
||||
|
||||
# If this gets called while this achievement is occupying a slot
|
||||
# already, ignore it. (probably should never happen in real
|
||||
|
|
@ -1227,10 +1238,10 @@ class Achievement:
|
|||
return
|
||||
|
||||
if sound:
|
||||
_ba.playsound(_ba.getsound('achievement'), host_only=True)
|
||||
bascenev1.getsound('achievement').play(host_only=True)
|
||||
else:
|
||||
_ba.timer(
|
||||
0.5, lambda: _ba.playsound(_ba.getsound('ding'), host_only=True)
|
||||
bascenev1.timer(
|
||||
0.5, lambda: bascenev1.getsound('ding').play(host_only=True)
|
||||
)
|
||||
|
||||
in_time = 0.300
|
||||
|
|
@ -1241,26 +1252,24 @@ class Achievement:
|
|||
# Find the first free slot.
|
||||
i = 0
|
||||
while True:
|
||||
if i not in app.ach.achievement_completion_banner_slots:
|
||||
app.ach.achievement_completion_banner_slots.add(i)
|
||||
if i not in app.classic.ach.achievement_completion_banner_slots:
|
||||
app.classic.ach.achievement_completion_banner_slots.add(i)
|
||||
self._completion_banner_slot = i
|
||||
|
||||
# Remove us from that slot when we close.
|
||||
# Use a real-timer in the UI context so the removal runs even
|
||||
# if our activity/session dies.
|
||||
with _ba.Context('ui'):
|
||||
_ba.timer(
|
||||
in_time + out_time,
|
||||
self._remove_banner_slot,
|
||||
timetype=TimeType.REAL,
|
||||
# Use an app-timer in an empty context so the removal
|
||||
# runs even if our activity/session dies.
|
||||
with babase.ContextRef.empty():
|
||||
babase.apptimer(
|
||||
in_time + out_time, self._remove_banner_slot
|
||||
)
|
||||
break
|
||||
i += 1
|
||||
assert self._completion_banner_slot is not None
|
||||
y_offs = 110 * self._completion_banner_slot
|
||||
objs: list[ba.Actor] = []
|
||||
objs: list[bascenev1.Actor] = []
|
||||
obj = Image(
|
||||
_ba.gettexture('shadow'),
|
||||
bascenev1.gettexture('shadow'),
|
||||
position=(-30, 30 + y_offs),
|
||||
front=True,
|
||||
attach=Image.Attach.BOTTOM_CENTER,
|
||||
|
|
@ -1275,7 +1284,7 @@ class Achievement:
|
|||
assert obj.node
|
||||
obj.node.host_only = True
|
||||
obj = Image(
|
||||
_ba.gettexture('light'),
|
||||
bascenev1.gettexture('light'),
|
||||
position=(-180, 60 + y_offs),
|
||||
front=True,
|
||||
attach=Image.Attach.BOTTOM_CENTER,
|
||||
|
|
@ -1290,8 +1299,10 @@ class Achievement:
|
|||
assert obj.node
|
||||
obj.node.host_only = True
|
||||
obj.node.premultiplied = True
|
||||
combine = _ba.newnode('combine', owner=obj.node, attrs={'size': 2})
|
||||
_gameutils.animate(
|
||||
combine = bascenev1.newnode(
|
||||
'combine', owner=obj.node, attrs={'size': 2}
|
||||
)
|
||||
bascenev1.animate(
|
||||
combine,
|
||||
'input0',
|
||||
{
|
||||
|
|
@ -1302,7 +1313,7 @@ class Achievement:
|
|||
in_time + 2.0: 0,
|
||||
},
|
||||
)
|
||||
_gameutils.animate(
|
||||
bascenev1.animate(
|
||||
combine,
|
||||
'input1',
|
||||
{
|
||||
|
|
@ -1314,7 +1325,7 @@ class Achievement:
|
|||
},
|
||||
)
|
||||
combine.connectattr('output', obj.node, 'scale')
|
||||
_gameutils.animate(obj.node, 'rotate', {0: 0.0, 0.35: 360.0}, loop=True)
|
||||
bascenev1.animate(obj.node, 'rotate', {0: 0.0, 0.35: 360.0}, loop=True)
|
||||
obj = Image(
|
||||
self.get_icon_texture(True),
|
||||
position=(-180, 60 + y_offs),
|
||||
|
|
@ -1332,7 +1343,9 @@ class Achievement:
|
|||
|
||||
# Flash.
|
||||
color = self.get_icon_color(True)
|
||||
combine = _ba.newnode('combine', owner=obj.node, attrs={'size': 3})
|
||||
combine = bascenev1.newnode(
|
||||
'combine', owner=obj.node, attrs={'size': 3}
|
||||
)
|
||||
keys = {
|
||||
in_time: 1.0 * color[0],
|
||||
in_time + 0.4: 1.5 * color[0],
|
||||
|
|
@ -1340,7 +1353,7 @@ class Achievement:
|
|||
in_time + 0.6: 1.5 * color[0],
|
||||
in_time + 2.0: 1.0 * color[0],
|
||||
}
|
||||
_gameutils.animate(combine, 'input0', keys)
|
||||
bascenev1.animate(combine, 'input0', keys)
|
||||
keys = {
|
||||
in_time: 1.0 * color[1],
|
||||
in_time + 0.4: 1.5 * color[1],
|
||||
|
|
@ -1348,7 +1361,7 @@ class Achievement:
|
|||
in_time + 0.6: 1.5 * color[1],
|
||||
in_time + 2.0: 1.0 * color[1],
|
||||
}
|
||||
_gameutils.animate(combine, 'input1', keys)
|
||||
bascenev1.animate(combine, 'input1', keys)
|
||||
keys = {
|
||||
in_time: 1.0 * color[2],
|
||||
in_time + 0.4: 1.5 * color[2],
|
||||
|
|
@ -1356,12 +1369,12 @@ class Achievement:
|
|||
in_time + 0.6: 1.5 * color[2],
|
||||
in_time + 2.0: 1.0 * color[2],
|
||||
}
|
||||
_gameutils.animate(combine, 'input2', keys)
|
||||
bascenev1.animate(combine, 'input2', keys)
|
||||
combine.connectattr('output', obj.node, 'color')
|
||||
|
||||
obj = Image(
|
||||
_ba.gettexture('achievementOutline'),
|
||||
model_transparent=_ba.getmodel('achievementOutline'),
|
||||
bascenev1.gettexture('achievementOutline'),
|
||||
mesh_transparent=bascenev1.getmesh('achievementOutline'),
|
||||
position=(-180, 60 + y_offs),
|
||||
front=True,
|
||||
attach=Image.Attach.BOTTOM_CENTER,
|
||||
|
|
@ -1376,7 +1389,9 @@ class Achievement:
|
|||
|
||||
# Flash.
|
||||
color = (2, 1.4, 0.4, 1)
|
||||
combine = _ba.newnode('combine', owner=obj.node, attrs={'size': 3})
|
||||
combine = bascenev1.newnode(
|
||||
'combine', owner=obj.node, attrs={'size': 3}
|
||||
)
|
||||
keys = {
|
||||
in_time: 1.0 * color[0],
|
||||
in_time + 0.4: 1.5 * color[0],
|
||||
|
|
@ -1384,7 +1399,7 @@ class Achievement:
|
|||
in_time + 0.6: 1.5 * color[0],
|
||||
in_time + 2.0: 1.0 * color[0],
|
||||
}
|
||||
_gameutils.animate(combine, 'input0', keys)
|
||||
bascenev1.animate(combine, 'input0', keys)
|
||||
keys = {
|
||||
in_time: 1.0 * color[1],
|
||||
in_time + 0.4: 1.5 * color[1],
|
||||
|
|
@ -1392,7 +1407,7 @@ class Achievement:
|
|||
in_time + 0.6: 1.5 * color[1],
|
||||
in_time + 2.0: 1.0 * color[1],
|
||||
}
|
||||
_gameutils.animate(combine, 'input1', keys)
|
||||
bascenev1.animate(combine, 'input1', keys)
|
||||
keys = {
|
||||
in_time: 1.0 * color[2],
|
||||
in_time + 0.4: 1.5 * color[2],
|
||||
|
|
@ -1400,13 +1415,14 @@ class Achievement:
|
|||
in_time + 0.6: 1.5 * color[2],
|
||||
in_time + 2.0: 1.0 * color[2],
|
||||
}
|
||||
_gameutils.animate(combine, 'input2', keys)
|
||||
bascenev1.animate(combine, 'input2', keys)
|
||||
combine.connectattr('output', obj.node, 'color')
|
||||
objs.append(obj)
|
||||
|
||||
objt = Text(
|
||||
Lstr(
|
||||
value='${A}:', subs=[('${A}', Lstr(resource='achievementText'))]
|
||||
babase.Lstr(
|
||||
value='${A}:',
|
||||
subs=[('${A}', babase.Lstr(resource='achievementText'))],
|
||||
),
|
||||
position=(-120, 91 + y_offs),
|
||||
front=True,
|
||||
|
|
@ -1442,7 +1458,7 @@ class Achievement:
|
|||
objt.node.host_only = True
|
||||
|
||||
objt = Text(
|
||||
_ba.charstr(SpecialChar.TICKET),
|
||||
babase.charstr(babase.SpecialChar.TICKET),
|
||||
position=(-120 - 170 + 5, 75 + y_offs - 20),
|
||||
front=True,
|
||||
v_attach=Text.VAttach.BOTTOM,
|
||||
|
|
@ -1482,7 +1498,7 @@ class Achievement:
|
|||
objt.node.host_only = True
|
||||
|
||||
# Add the 'x 2' if we've got pro.
|
||||
if app.accounts_v1.have_pro():
|
||||
if app.classic.accounts.have_pro():
|
||||
objt = Text(
|
||||
'x 2',
|
||||
position=(-120 - 180 + 45, 80 + y_offs - 50),
|
||||
|
|
@ -1522,6 +1538,7 @@ class Achievement:
|
|||
objt.node.host_only = True
|
||||
|
||||
for actor in objs:
|
||||
_ba.timer(
|
||||
out_time + 1.000, WeakCall(actor.handlemessage, DieMessage())
|
||||
bascenev1.timer(
|
||||
out_time + 1.000,
|
||||
babase.WeakCall(actor.handlemessage, bascenev1.DieMessage()),
|
||||
)
|
||||
|
|
@ -6,8 +6,9 @@ from __future__ import annotations
|
|||
import time
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import _ba
|
||||
from ba import _internal
|
||||
import babase
|
||||
import bauiv1
|
||||
import bascenev1
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Callable, Any
|
||||
|
|
@ -33,32 +34,34 @@ class AdsSubsystem:
|
|||
|
||||
def do_remove_in_game_ads_message(self) -> None:
|
||||
"""(internal)"""
|
||||
from ba._language import Lstr
|
||||
from ba._generated.enums import TimeType
|
||||
|
||||
# Print this message once every 10 minutes at most.
|
||||
tval = _ba.time(TimeType.REAL)
|
||||
tval = babase.apptime()
|
||||
if self.last_in_game_ad_remove_message_show_time is None or (
|
||||
tval - self.last_in_game_ad_remove_message_show_time > 60 * 10
|
||||
):
|
||||
self.last_in_game_ad_remove_message_show_time = tval
|
||||
with _ba.Context('ui'):
|
||||
_ba.timer(
|
||||
with babase.ContextRef.empty():
|
||||
babase.apptimer(
|
||||
1.0,
|
||||
lambda: _ba.screenmessage(
|
||||
Lstr(
|
||||
lambda: babase.screenmessage(
|
||||
babase.Lstr(
|
||||
resource='removeInGameAdsText',
|
||||
subs=[
|
||||
(
|
||||
'${PRO}',
|
||||
Lstr(resource='store.bombSquadProNameText'),
|
||||
babase.Lstr(
|
||||
resource='store.bombSquadProNameText'
|
||||
),
|
||||
),
|
||||
(
|
||||
'${APP_NAME}',
|
||||
babase.Lstr(resource='titleText'),
|
||||
),
|
||||
('${APP_NAME}', Lstr(resource='titleText')),
|
||||
],
|
||||
),
|
||||
color=(1, 1, 0),
|
||||
),
|
||||
timetype=TimeType.REAL,
|
||||
)
|
||||
|
||||
def show_ad(
|
||||
|
|
@ -66,7 +69,7 @@ class AdsSubsystem:
|
|||
) -> None:
|
||||
"""(internal)"""
|
||||
self.last_ad_purpose = purpose
|
||||
_ba.show_ad(purpose, on_completion_call)
|
||||
bauiv1.show_ad(purpose, on_completion_call)
|
||||
|
||||
def show_ad_2(
|
||||
self,
|
||||
|
|
@ -75,25 +78,28 @@ class AdsSubsystem:
|
|||
) -> None:
|
||||
"""(internal)"""
|
||||
self.last_ad_purpose = purpose
|
||||
_ba.show_ad_2(purpose, on_completion_call)
|
||||
bauiv1.show_ad_2(purpose, on_completion_call)
|
||||
|
||||
def call_after_ad(self, call: Callable[[], Any]) -> None:
|
||||
"""Run a call after potentially showing an ad."""
|
||||
# pylint: disable=too-many-statements
|
||||
# pylint: disable=too-many-branches
|
||||
# pylint: disable=too-many-locals
|
||||
from ba._generated.enums import TimeType
|
||||
|
||||
app = _ba.app
|
||||
app = babase.app
|
||||
plus = app.plus
|
||||
classic = app.classic
|
||||
assert plus is not None
|
||||
assert classic is not None
|
||||
show = True
|
||||
|
||||
# No ads without net-connections, etc.
|
||||
if not _ba.can_show_ad():
|
||||
if not bauiv1.can_show_ad():
|
||||
show = False
|
||||
if app.accounts_v1.have_pro():
|
||||
if classic.accounts.have_pro():
|
||||
show = False # Pro disables interstitials.
|
||||
try:
|
||||
session = _ba.get_foreground_host_session()
|
||||
session = bascenev1.get_foreground_host_session()
|
||||
assert session is not None
|
||||
is_tournament = session.tournament_id is not None
|
||||
except Exception:
|
||||
|
|
@ -107,19 +113,17 @@ class AdsSubsystem:
|
|||
|
||||
# If we're seeing short ads we may want to space them differently.
|
||||
interval_mult = (
|
||||
_internal.get_v1_account_misc_read_val(
|
||||
'ads.shortIntervalMult', 1.0
|
||||
)
|
||||
plus.get_v1_account_misc_read_val('ads.shortIntervalMult', 1.0)
|
||||
if self.last_ad_was_short
|
||||
else 1.0
|
||||
)
|
||||
if self.ad_amt is None:
|
||||
if launch_count <= 1:
|
||||
self.ad_amt = _internal.get_v1_account_misc_read_val(
|
||||
self.ad_amt = plus.get_v1_account_misc_read_val(
|
||||
'ads.startVal1', 0.99
|
||||
)
|
||||
else:
|
||||
self.ad_amt = _internal.get_v1_account_misc_read_val(
|
||||
self.ad_amt = plus.get_v1_account_misc_read_val(
|
||||
'ads.startVal2', 1.0
|
||||
)
|
||||
interval = None
|
||||
|
|
@ -128,23 +132,19 @@ class AdsSubsystem:
|
|||
# ad-show-threshold and see if we should *actually* show
|
||||
# (we reach our threshold faster the longer we've been
|
||||
# playing).
|
||||
base = 'ads' if _ba.has_video_ads() else 'ads2'
|
||||
min_lc = _internal.get_v1_account_misc_read_val(
|
||||
base + '.minLC', 0.0
|
||||
)
|
||||
max_lc = _internal.get_v1_account_misc_read_val(
|
||||
base + '.maxLC', 5.0
|
||||
)
|
||||
min_lc_scale = _internal.get_v1_account_misc_read_val(
|
||||
base = 'ads' if bauiv1.has_video_ads() else 'ads2'
|
||||
min_lc = plus.get_v1_account_misc_read_val(base + '.minLC', 0.0)
|
||||
max_lc = plus.get_v1_account_misc_read_val(base + '.maxLC', 5.0)
|
||||
min_lc_scale = plus.get_v1_account_misc_read_val(
|
||||
base + '.minLCScale', 0.25
|
||||
)
|
||||
max_lc_scale = _internal.get_v1_account_misc_read_val(
|
||||
max_lc_scale = plus.get_v1_account_misc_read_val(
|
||||
base + '.maxLCScale', 0.34
|
||||
)
|
||||
min_lc_interval = _internal.get_v1_account_misc_read_val(
|
||||
min_lc_interval = plus.get_v1_account_misc_read_val(
|
||||
base + '.minLCInterval', 360
|
||||
)
|
||||
max_lc_interval = _internal.get_v1_account_misc_read_val(
|
||||
max_lc_interval = plus.get_v1_account_misc_read_val(
|
||||
base + '.maxLCInterval', 300
|
||||
)
|
||||
if launch_count < min_lc:
|
||||
|
|
@ -170,7 +170,7 @@ class AdsSubsystem:
|
|||
self.last_ad_completion_time is None
|
||||
or (
|
||||
interval is not None
|
||||
and _ba.time(TimeType.REAL) - self.last_ad_completion_time
|
||||
and babase.apptime() - self.last_ad_completion_time
|
||||
> (interval * interval_mult)
|
||||
)
|
||||
):
|
||||
|
|
@ -192,34 +192,25 @@ class AdsSubsystem:
|
|||
|
||||
def run(self, fallback: bool = False) -> None:
|
||||
"""Run fallback call (and issue a warning about it)."""
|
||||
assert app.classic is not None
|
||||
if not self._ran:
|
||||
if fallback:
|
||||
lanst = app.classic.ads.last_ad_network_set_time
|
||||
print(
|
||||
(
|
||||
'ERROR: relying on fallback ad-callback! '
|
||||
'last network: '
|
||||
+ app.ads.last_ad_network
|
||||
+ ' (set '
|
||||
+ str(
|
||||
int(
|
||||
time.time()
|
||||
- app.ads.last_ad_network_set_time
|
||||
)
|
||||
)
|
||||
+ 's ago); purpose='
|
||||
+ app.ads.last_ad_purpose
|
||||
)
|
||||
'ERROR: relying on fallback ad-callback! '
|
||||
'last network: '
|
||||
+ app.classic.ads.last_ad_network
|
||||
+ ' (set '
|
||||
+ str(int(time.time() - lanst))
|
||||
+ 's ago); purpose='
|
||||
+ app.classic.ads.last_ad_purpose
|
||||
)
|
||||
_ba.pushcall(self._call)
|
||||
babase.pushcall(self._call)
|
||||
self._ran = True
|
||||
|
||||
payload = _Payload(call)
|
||||
with _ba.Context('ui'):
|
||||
_ba.timer(
|
||||
5.0,
|
||||
lambda: payload.run(fallback=True),
|
||||
timetype=TimeType.REAL,
|
||||
)
|
||||
with babase.ContextRef.empty():
|
||||
babase.apptimer(5.0, lambda: payload.run(fallback=True))
|
||||
self.show_ad('between_game', on_completion_call=payload.run)
|
||||
else:
|
||||
_ba.pushcall(call) # Just run the callback without the ad.
|
||||
babase.pushcall(call) # Just run the callback without the ad.
|
||||
|
|
@ -6,7 +6,8 @@ from __future__ import annotations
|
|||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import _ba
|
||||
import babase
|
||||
import bascenev1
|
||||
|
||||
if TYPE_CHECKING:
|
||||
pass
|
||||
|
|
@ -16,13 +17,17 @@ def game_begin_analytics() -> None:
|
|||
"""Update analytics events for the start of a game."""
|
||||
# pylint: disable=too-many-branches
|
||||
# pylint: disable=cyclic-import
|
||||
from ba._dualteamsession import DualTeamSession
|
||||
from ba._freeforallsession import FreeForAllSession
|
||||
from ba._coopsession import CoopSession
|
||||
from ba._gameactivity import GameActivity
|
||||
from bascenev1 import (
|
||||
DualTeamSession,
|
||||
FreeForAllSession,
|
||||
CoopSession,
|
||||
GameActivity,
|
||||
)
|
||||
|
||||
activity = _ba.getactivity(False)
|
||||
session = _ba.getsession(False)
|
||||
assert babase.app.classic is not None
|
||||
|
||||
activity = bascenev1.getactivity(False)
|
||||
session = bascenev1.getsession(False)
|
||||
|
||||
# Fail gracefully if we didn't cleanly get a session and game activity.
|
||||
if not activity or not session or not isinstance(activity, GameActivity):
|
||||
|
|
@ -31,53 +36,60 @@ def game_begin_analytics() -> None:
|
|||
if isinstance(session, CoopSession):
|
||||
campaign = session.campaign
|
||||
assert campaign is not None
|
||||
_ba.set_analytics_screen(
|
||||
babase.set_analytics_screen(
|
||||
'Coop Game: '
|
||||
+ campaign.name
|
||||
+ ' '
|
||||
+ campaign.getlevel(_ba.app.coop_session_args['level']).name
|
||||
+ campaign.getlevel(
|
||||
babase.app.classic.coop_session_args['level']
|
||||
).name
|
||||
)
|
||||
_ba.increment_analytics_count('Co-op round start')
|
||||
babase.increment_analytics_count('Co-op round start')
|
||||
if len(activity.players) == 1:
|
||||
_ba.increment_analytics_count('Co-op round start 1 human player')
|
||||
babase.increment_analytics_count('Co-op round start 1 human player')
|
||||
elif len(activity.players) == 2:
|
||||
_ba.increment_analytics_count('Co-op round start 2 human players')
|
||||
babase.increment_analytics_count(
|
||||
'Co-op round start 2 human players'
|
||||
)
|
||||
elif len(activity.players) == 3:
|
||||
_ba.increment_analytics_count('Co-op round start 3 human players')
|
||||
babase.increment_analytics_count(
|
||||
'Co-op round start 3 human players'
|
||||
)
|
||||
elif len(activity.players) >= 4:
|
||||
_ba.increment_analytics_count('Co-op round start 4+ human players')
|
||||
babase.increment_analytics_count(
|
||||
'Co-op round start 4+ human players'
|
||||
)
|
||||
|
||||
elif isinstance(session, DualTeamSession):
|
||||
_ba.set_analytics_screen('Teams Game: ' + activity.getname())
|
||||
_ba.increment_analytics_count('Teams round start')
|
||||
babase.set_analytics_screen('Teams Game: ' + activity.getname())
|
||||
babase.increment_analytics_count('Teams round start')
|
||||
if len(activity.players) == 1:
|
||||
_ba.increment_analytics_count('Teams round start 1 human player')
|
||||
babase.increment_analytics_count('Teams round start 1 human player')
|
||||
elif 1 < len(activity.players) < 8:
|
||||
_ba.increment_analytics_count(
|
||||
babase.increment_analytics_count(
|
||||
'Teams round start '
|
||||
+ str(len(activity.players))
|
||||
+ ' human players'
|
||||
)
|
||||
elif len(activity.players) >= 8:
|
||||
_ba.increment_analytics_count('Teams round start 8+ human players')
|
||||
babase.increment_analytics_count(
|
||||
'Teams round start 8+ human players'
|
||||
)
|
||||
|
||||
elif isinstance(session, FreeForAllSession):
|
||||
_ba.set_analytics_screen('FreeForAll Game: ' + activity.getname())
|
||||
_ba.increment_analytics_count('Free-for-all round start')
|
||||
babase.set_analytics_screen('FreeForAll Game: ' + activity.getname())
|
||||
babase.increment_analytics_count('Free-for-all round start')
|
||||
if len(activity.players) == 1:
|
||||
_ba.increment_analytics_count(
|
||||
babase.increment_analytics_count(
|
||||
'Free-for-all round start 1 human player'
|
||||
)
|
||||
elif 1 < len(activity.players) < 8:
|
||||
_ba.increment_analytics_count(
|
||||
babase.increment_analytics_count(
|
||||
'Free-for-all round start '
|
||||
+ str(len(activity.players))
|
||||
+ ' human players'
|
||||
)
|
||||
elif len(activity.players) >= 8:
|
||||
_ba.increment_analytics_count(
|
||||
babase.increment_analytics_count(
|
||||
'Free-for-all round start 8+ human players'
|
||||
)
|
||||
|
||||
# For some analytics tracking on the c layer.
|
||||
_ba.reset_game_activity_tracking()
|
||||
|
|
@ -5,9 +5,11 @@ from __future__ import annotations
|
|||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import babase
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Callable
|
||||
import ba
|
||||
import bascenev1
|
||||
|
||||
|
||||
class AppDelegate:
|
||||
|
|
@ -18,8 +20,8 @@ class AppDelegate:
|
|||
|
||||
def create_default_game_settings_ui(
|
||||
self,
|
||||
gameclass: type[ba.GameActivity],
|
||||
sessiontype: type[ba.Session],
|
||||
gameclass: type[bascenev1.GameActivity],
|
||||
sessiontype: type[bascenev1.Session],
|
||||
settings: dict | None,
|
||||
completion_call: Callable[[dict | None], None],
|
||||
) -> None:
|
||||
|
|
@ -28,9 +30,16 @@ class AppDelegate:
|
|||
It should manipulate the contents of config and call completion_call
|
||||
when done.
|
||||
"""
|
||||
del gameclass, sessiontype, settings, completion_call # Unused.
|
||||
from ba import _error
|
||||
# Replace the main window once we come up successfully.
|
||||
from bauiv1lib.playlist.editgame import PlaylistEditGameWindow
|
||||
|
||||
_error.print_error(
|
||||
"create_default_game_settings_ui needs to be overridden"
|
||||
assert babase.app.classic is not None
|
||||
babase.app.ui_v1.clear_main_menu_window(transition='out_left')
|
||||
babase.app.ui_v1.set_main_menu_window(
|
||||
PlaylistEditGameWindow(
|
||||
gameclass,
|
||||
sessiontype,
|
||||
settings,
|
||||
completion_call=completion_call,
|
||||
).get_root_widget()
|
||||
)
|
||||
|
|
@ -6,48 +6,45 @@ from __future__ import annotations
|
|||
import random
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import _ba
|
||||
import babase
|
||||
import bascenev1
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any, Sequence
|
||||
import ba
|
||||
|
||||
|
||||
def run_cpu_benchmark() -> None:
|
||||
"""Run a cpu benchmark."""
|
||||
# pylint: disable=cyclic-import
|
||||
from bastd import tutorial
|
||||
from ba._session import Session
|
||||
from bascenev1lib import tutorial
|
||||
|
||||
class BenchmarkSession(Session):
|
||||
class BenchmarkSession(bascenev1.Session):
|
||||
"""Session type for cpu benchmark."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
|
||||
# print('FIXME: BENCHMARK SESSION WOULD CALC DEPS.')
|
||||
depsets: Sequence[ba.DependencySet] = []
|
||||
depsets: Sequence[bascenev1.DependencySet] = []
|
||||
|
||||
super().__init__(depsets)
|
||||
|
||||
# Store old graphics settings.
|
||||
self._old_quality = _ba.app.config.resolve('Graphics Quality')
|
||||
cfg = _ba.app.config
|
||||
self._old_quality = babase.app.config.resolve('Graphics Quality')
|
||||
cfg = babase.app.config
|
||||
cfg['Graphics Quality'] = 'Low'
|
||||
cfg.apply()
|
||||
self.benchmark_type = 'cpu'
|
||||
self.setactivity(_ba.newactivity(tutorial.TutorialActivity))
|
||||
self.setactivity(bascenev1.newactivity(tutorial.TutorialActivity))
|
||||
|
||||
def __del__(self) -> None:
|
||||
|
||||
# When we're torn down, restore old graphics settings.
|
||||
cfg = _ba.app.config
|
||||
cfg = babase.app.config
|
||||
cfg['Graphics Quality'] = self._old_quality
|
||||
cfg.apply()
|
||||
|
||||
def on_player_request(self, player: ba.SessionPlayer) -> bool:
|
||||
def on_player_request(self, player: bascenev1.SessionPlayer) -> bool:
|
||||
return False
|
||||
|
||||
_ba.new_host_session(BenchmarkSession, benchmark_type='cpu')
|
||||
bascenev1.new_host_session(BenchmarkSession, benchmark_type='cpu')
|
||||
|
||||
|
||||
def run_stress_test(
|
||||
|
|
@ -57,15 +54,13 @@ def run_stress_test(
|
|||
round_duration: int = 30,
|
||||
) -> None:
|
||||
"""Run a stress test."""
|
||||
from ba import modutils
|
||||
from ba._general import Call
|
||||
from ba._generated.enums import TimeType
|
||||
from babase import modutils
|
||||
|
||||
_ba.screenmessage(
|
||||
babase.screenmessage(
|
||||
"Beginning stress test.. use 'End Test' to stop testing.",
|
||||
color=(1, 1, 0),
|
||||
)
|
||||
with _ba.Context('ui'):
|
||||
with babase.ContextRef.empty():
|
||||
start_stress_test(
|
||||
{
|
||||
'playlist_type': playlist_type,
|
||||
|
|
@ -74,46 +69,45 @@ def run_stress_test(
|
|||
'round_duration': round_duration,
|
||||
}
|
||||
)
|
||||
_ba.timer(
|
||||
babase.apptimer(
|
||||
7.0,
|
||||
Call(
|
||||
_ba.screenmessage,
|
||||
babase.Call(
|
||||
babase.screenmessage,
|
||||
(
|
||||
'stats will be written to '
|
||||
+ modutils.get_human_readable_user_scripts_path()
|
||||
+ '/stress_test_stats.csv'
|
||||
),
|
||||
),
|
||||
timetype=TimeType.REAL,
|
||||
)
|
||||
|
||||
|
||||
def stop_stress_test() -> None:
|
||||
"""End a running stress test."""
|
||||
_ba.set_stress_testing(False, 0)
|
||||
babase.set_stress_testing(False, 0)
|
||||
assert babase.app.classic is not None
|
||||
try:
|
||||
if _ba.app.stress_test_reset_timer is not None:
|
||||
_ba.screenmessage('Ending stress test...', color=(1, 1, 0))
|
||||
if babase.app.classic.stress_test_reset_timer is not None:
|
||||
babase.screenmessage('Ending stress test...', color=(1, 1, 0))
|
||||
except Exception:
|
||||
pass
|
||||
_ba.app.stress_test_reset_timer = None
|
||||
babase.app.classic.stress_test_reset_timer = None
|
||||
|
||||
|
||||
def start_stress_test(args: dict[str, Any]) -> None:
|
||||
"""(internal)"""
|
||||
from ba._general import Call
|
||||
from ba._dualteamsession import DualTeamSession
|
||||
from ba._freeforallsession import FreeForAllSession
|
||||
from ba._generated.enums import TimeType, TimeFormat
|
||||
from bascenev1 import DualTeamSession, FreeForAllSession
|
||||
|
||||
appconfig = _ba.app.config
|
||||
assert babase.app.classic is not None
|
||||
|
||||
appconfig = babase.app.config
|
||||
playlist_type = args['playlist_type']
|
||||
if playlist_type == 'Random':
|
||||
if random.random() < 0.5:
|
||||
playlist_type = 'Teams'
|
||||
else:
|
||||
playlist_type = 'Free-For-All'
|
||||
_ba.screenmessage(
|
||||
babase.screenmessage(
|
||||
'Running Stress Test (listType="'
|
||||
+ playlist_type
|
||||
+ '", listName="'
|
||||
|
|
@ -123,76 +117,67 @@ def start_stress_test(args: dict[str, Any]) -> None:
|
|||
if playlist_type == 'Teams':
|
||||
appconfig['Team Tournament Playlist Selection'] = args['playlist_name']
|
||||
appconfig['Team Tournament Playlist Randomize'] = 1
|
||||
_ba.timer(
|
||||
babase.apptimer(
|
||||
1.0,
|
||||
Call(_ba.pushcall, Call(_ba.new_host_session, DualTeamSession)),
|
||||
timetype=TimeType.REAL,
|
||||
babase.Call(
|
||||
babase.pushcall,
|
||||
babase.Call(bascenev1.new_host_session, DualTeamSession),
|
||||
),
|
||||
)
|
||||
else:
|
||||
appconfig['Free-for-All Playlist Selection'] = args['playlist_name']
|
||||
appconfig['Free-for-All Playlist Randomize'] = 1
|
||||
_ba.timer(
|
||||
babase.apptimer(
|
||||
1.0,
|
||||
Call(_ba.pushcall, Call(_ba.new_host_session, FreeForAllSession)),
|
||||
timetype=TimeType.REAL,
|
||||
babase.Call(
|
||||
babase.pushcall,
|
||||
babase.Call(bascenev1.new_host_session, FreeForAllSession),
|
||||
),
|
||||
)
|
||||
_ba.set_stress_testing(True, args['player_count'])
|
||||
_ba.app.stress_test_reset_timer = _ba.Timer(
|
||||
args['round_duration'] * 1000,
|
||||
Call(_reset_stress_test, args),
|
||||
timetype=TimeType.REAL,
|
||||
timeformat=TimeFormat.MILLISECONDS,
|
||||
babase.set_stress_testing(True, args['player_count'])
|
||||
babase.app.classic.stress_test_reset_timer = babase.AppTimer(
|
||||
args['round_duration'], babase.Call(_reset_stress_test, args)
|
||||
)
|
||||
|
||||
|
||||
def _reset_stress_test(args: dict[str, Any]) -> None:
|
||||
from ba._general import Call
|
||||
from ba._generated.enums import TimeType
|
||||
|
||||
_ba.set_stress_testing(False, args['player_count'])
|
||||
_ba.screenmessage('Resetting stress test...')
|
||||
session = _ba.get_foreground_host_session()
|
||||
babase.set_stress_testing(False, args['player_count'])
|
||||
babase.screenmessage('Resetting stress test...')
|
||||
session = bascenev1.get_foreground_host_session()
|
||||
assert session is not None
|
||||
session.end()
|
||||
_ba.timer(1.0, Call(start_stress_test, args), timetype=TimeType.REAL)
|
||||
babase.apptimer(1.0, babase.Call(start_stress_test, args))
|
||||
|
||||
|
||||
def run_gpu_benchmark() -> None:
|
||||
"""Kick off a benchmark to test gpu speeds."""
|
||||
# FIXME: Not wired up yet.
|
||||
_ba.screenmessage('Not wired up yet.', color=(1, 0, 0))
|
||||
babase.screenmessage('Not wired up yet.', color=(1, 0, 0))
|
||||
|
||||
|
||||
def run_media_reload_benchmark() -> None:
|
||||
"""Kick off a benchmark to test media reloading speeds."""
|
||||
from ba._general import Call
|
||||
from ba._generated.enums import TimeType
|
||||
|
||||
_ba.reload_media()
|
||||
_ba.show_progress_bar()
|
||||
babase.reload_media()
|
||||
babase.show_progress_bar()
|
||||
|
||||
def delay_add(start_time: float) -> None:
|
||||
def doit(start_time_2: float) -> None:
|
||||
_ba.screenmessage(
|
||||
_ba.app.lang.get_resource(
|
||||
babase.screenmessage(
|
||||
babase.app.lang.get_resource(
|
||||
'debugWindow.totalReloadTimeText'
|
||||
).replace(
|
||||
'${TIME}', str(_ba.time(TimeType.REAL) - start_time_2)
|
||||
)
|
||||
).replace('${TIME}', str(babase.apptime() - start_time_2))
|
||||
)
|
||||
_ba.print_load_info()
|
||||
if _ba.app.config.resolve('Texture Quality') != 'High':
|
||||
_ba.screenmessage(
|
||||
_ba.app.lang.get_resource(
|
||||
babase.print_load_info()
|
||||
if babase.app.config.resolve('Texture Quality') != 'High':
|
||||
babase.screenmessage(
|
||||
babase.app.lang.get_resource(
|
||||
'debugWindow.reloadBenchmarkBestResultsText'
|
||||
),
|
||||
color=(1, 1, 0),
|
||||
)
|
||||
|
||||
_ba.add_clean_frame_callback(Call(doit, start_time))
|
||||
babase.add_clean_frame_callback(babase.Call(doit, start_time))
|
||||
|
||||
# The reload starts (should add a completion callback to the
|
||||
# reload func to fix this).
|
||||
_ba.timer(
|
||||
0.05, Call(delay_add, _ba.time(TimeType.REAL)), timetype=TimeType.REAL
|
||||
)
|
||||
babase.apptimer(0.05, babase.Call(delay_add, babase.apptime()))
|
||||
|
|
@ -4,16 +4,17 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
import logging
|
||||
|
||||
import _ba
|
||||
from ba._internal import get_v1_account_display_string
|
||||
import babase
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any
|
||||
import ba
|
||||
|
||||
|
||||
def get_device_value(device: ba.InputDevice, name: str) -> Any:
|
||||
def get_input_device_mapped_value(
|
||||
devicename: str, unique_id: str, name: str
|
||||
) -> Any:
|
||||
"""Returns a mapped value for an input device.
|
||||
|
||||
This checks the user config and falls back to default values
|
||||
|
|
@ -22,16 +23,16 @@ def get_device_value(device: ba.InputDevice, name: str) -> Any:
|
|||
# pylint: disable=too-many-statements
|
||||
# pylint: disable=too-many-return-statements
|
||||
# pylint: disable=too-many-branches
|
||||
devicename = device.name
|
||||
unique_id = device.unique_identifier
|
||||
app = _ba.app
|
||||
useragentstring = app.user_agent_string
|
||||
platform = app.platform
|
||||
subplatform = app.subplatform
|
||||
appconfig = _ba.app.config
|
||||
|
||||
app = babase.app
|
||||
assert app.classic is not None
|
||||
useragentstring = app.classic.legacy_user_agent_string
|
||||
platform = app.classic.platform
|
||||
subplatform = app.classic.subplatform
|
||||
appconfig = babase.app.config
|
||||
|
||||
# iiRcade: hard-code for a/b/c/x for now...
|
||||
if _ba.app.iircade_mode:
|
||||
if babase.app.iircade_mode:
|
||||
return {
|
||||
'triggerRun2': 19,
|
||||
'unassignedButtonsRun': False,
|
||||
|
|
@ -64,7 +65,6 @@ def get_device_value(device: ba.InputDevice, name: str) -> Any:
|
|||
return mapping.get(name, -1)
|
||||
|
||||
if platform == 'windows':
|
||||
|
||||
# XInput (hopefully this mapping is consistent?...)
|
||||
if devicename.startswith('XInput Controller'):
|
||||
return {
|
||||
|
|
@ -98,7 +98,7 @@ def get_device_value(device: ba.InputDevice, name: str) -> Any:
|
|||
}.get(name, -1)
|
||||
|
||||
# Look for some exact types.
|
||||
if _ba.is_running_on_fire_tv():
|
||||
if babase.is_running_on_fire_tv():
|
||||
if devicename in ['Thunder', 'Amazon Fire Game Controller']:
|
||||
return {
|
||||
'triggerRun2': 23,
|
||||
|
|
@ -211,7 +211,6 @@ def get_device_value(device: ba.InputDevice, name: str) -> Any:
|
|||
'buttonIgnored': 17,
|
||||
}.get(name, -1)
|
||||
if devicename in ['Wireless 360 Controller', 'Controller']:
|
||||
|
||||
# Xbox360 gamepads
|
||||
return {
|
||||
'analogStickDeadZone': 1.2,
|
||||
|
|
@ -325,7 +324,6 @@ def get_device_value(device: ba.InputDevice, name: str) -> Any:
|
|||
|
||||
# Generic android...
|
||||
if platform == 'android':
|
||||
|
||||
# Steelseries stratus xl.
|
||||
if devicename == 'SteelSeries Stratus XL':
|
||||
return {
|
||||
|
|
@ -519,8 +517,7 @@ def get_device_value(device: ba.InputDevice, name: str) -> Any:
|
|||
|
||||
# Reasonable defaults.
|
||||
if platform == 'android':
|
||||
if _ba.is_running_on_fire_tv():
|
||||
|
||||
if babase.is_running_on_fire_tv():
|
||||
# Mostly same as default firetv controller.
|
||||
return {
|
||||
'triggerRun2': 23,
|
||||
|
|
@ -579,15 +576,11 @@ def _gen_android_input_hash() -> str:
|
|||
except PermissionError:
|
||||
pass
|
||||
except Exception:
|
||||
from ba import _error
|
||||
|
||||
_error.print_exception(
|
||||
'error in _gen_android_input_hash inner loop'
|
||||
)
|
||||
logging.exception('Error in _gen_android_input_hash inner loop.')
|
||||
return md5.hexdigest()
|
||||
|
||||
|
||||
def get_input_map_hash(inputdevice: ba.InputDevice) -> str:
|
||||
def get_input_device_map_hash() -> str:
|
||||
"""Given an input device, return a hash based on its raw input values.
|
||||
|
||||
This lets us avoid sharing mappings across devices that may
|
||||
|
|
@ -595,34 +588,34 @@ def get_input_map_hash(inputdevice: ba.InputDevice) -> str:
|
|||
(Different Android versions, for example, may return different
|
||||
key codes for button presses on a given type of controller)
|
||||
"""
|
||||
del inputdevice # Currently unused.
|
||||
app = _ba.app
|
||||
try:
|
||||
if app.input_map_hash is None:
|
||||
if app.platform == 'android':
|
||||
app.input_map_hash = _gen_android_input_hash()
|
||||
else:
|
||||
app.input_map_hash = ''
|
||||
return app.input_map_hash
|
||||
except Exception:
|
||||
from ba import _error
|
||||
app = babase.app
|
||||
|
||||
_error.print_exception('Exception in get_input_map_hash')
|
||||
return ''
|
||||
# Currently only using this when classic is present.
|
||||
# Need to replace with a modern equivalent.
|
||||
if app.classic is not None:
|
||||
try:
|
||||
if app.classic.input_map_hash is None:
|
||||
if app.classic.platform == 'android':
|
||||
app.classic.input_map_hash = _gen_android_input_hash()
|
||||
else:
|
||||
app.classic.input_map_hash = ''
|
||||
return app.classic.input_map_hash
|
||||
except Exception:
|
||||
logging.exception('Error in get_input_map_hash.')
|
||||
return ''
|
||||
return ''
|
||||
|
||||
|
||||
def get_input_device_config(
|
||||
device: ba.InputDevice, default: bool
|
||||
name: str, unique_id: str, default: bool
|
||||
) -> tuple[dict, str]:
|
||||
"""Given an input device, return its config dict in the app config.
|
||||
|
||||
The dict will be created if it does not exist.
|
||||
"""
|
||||
cfg = _ba.app.config
|
||||
name = device.name
|
||||
cfg = babase.app.config
|
||||
ccfgs: dict[str, Any] = cfg.setdefault('Controllers', {})
|
||||
ccfgs.setdefault(name, {})
|
||||
unique_id = device.unique_identifier
|
||||
if default:
|
||||
if unique_id in ccfgs[name]:
|
||||
del ccfgs[name][unique_id]
|
||||
|
|
@ -632,26 +625,3 @@ def get_input_device_config(
|
|||
if unique_id not in ccfgs[name]:
|
||||
ccfgs[name][unique_id] = {}
|
||||
return ccfgs[name], unique_id
|
||||
|
||||
|
||||
def get_last_player_name_from_input_device(device: ba.InputDevice) -> str:
|
||||
"""Return a reasonable player name associated with a device.
|
||||
|
||||
(generally the last one used there)
|
||||
"""
|
||||
appconfig = _ba.app.config
|
||||
|
||||
# Look for a default player profile name for them;
|
||||
# otherwise default to their current random name.
|
||||
profilename = '_random'
|
||||
key_name = device.name + ' ' + device.unique_identifier
|
||||
if (
|
||||
'Default Player Profiles' in appconfig
|
||||
and key_name in appconfig['Default Player Profiles']
|
||||
):
|
||||
profilename = appconfig['Default Player Profiles'][key_name]
|
||||
if profilename == '_random':
|
||||
profilename = device.get_default_player_name()
|
||||
if profilename == '__account__':
|
||||
profilename = get_v1_account_display_string()
|
||||
return profilename
|
||||
|
|
@ -4,49 +4,17 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
|
||||
import _ba
|
||||
import babase
|
||||
import bascenev1
|
||||
from bascenev1 import MusicType
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Callable, Any
|
||||
import ba
|
||||
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
MENU = 'Menu'
|
||||
VICTORY = 'Victory'
|
||||
CHAR_SELECT = 'CharSelect'
|
||||
RUN_AWAY = 'RunAway'
|
||||
ONSLAUGHT = 'Onslaught'
|
||||
KEEP_AWAY = 'Keep Away'
|
||||
RACE = 'Race'
|
||||
EPIC_RACE = 'Epic Race'
|
||||
SCORES = 'Scores'
|
||||
GRAND_ROMP = 'GrandRomp'
|
||||
TO_THE_DEATH = 'ToTheDeath'
|
||||
CHOSEN_ONE = 'Chosen One'
|
||||
FORWARD_MARCH = 'ForwardMarch'
|
||||
FLAG_CATCHER = 'FlagCatcher'
|
||||
SURVIVAL = 'Survival'
|
||||
EPIC = 'Epic'
|
||||
SPORTS = 'Sports'
|
||||
HOCKEY = 'Hockey'
|
||||
FOOTBALL = 'Football'
|
||||
FLYING = 'Flying'
|
||||
SCARY = 'Scary'
|
||||
MARCHING = 'Marching'
|
||||
|
||||
|
||||
class MusicPlayMode(Enum):
|
||||
|
|
@ -118,7 +86,8 @@ class MusicSubsystem:
|
|||
|
||||
def __init__(self) -> None:
|
||||
# pylint: disable=cyclic-import
|
||||
self._music_node: _ba.Node | None = None
|
||||
# self._music_node: _bascenev1.Node | None = None
|
||||
self._playing_internal_music = False
|
||||
self._music_mode: MusicPlayMode = MusicPlayMode.REGULAR
|
||||
self._music_player: MusicPlayer | None = None
|
||||
self._music_player_type: type[MusicPlayer] | None = None
|
||||
|
|
@ -133,31 +102,29 @@ class MusicSubsystem:
|
|||
# Our standard asset playback should probably just be one of them
|
||||
# instead of a special case.
|
||||
if self.supports_soundtrack_entry_type('musicFile'):
|
||||
from ba.osmusic import OSMusicPlayer
|
||||
from baclassic.osmusic import OSMusicPlayer
|
||||
|
||||
self._music_player_type = OSMusicPlayer
|
||||
elif self.supports_soundtrack_entry_type('iTunesPlaylist'):
|
||||
from ba.macmusicapp import MacMusicAppMusicPlayer
|
||||
from baclassic.macmusicapp import MacMusicAppMusicPlayer
|
||||
|
||||
self._music_player_type = MacMusicAppMusicPlayer
|
||||
|
||||
def on_app_launch(self) -> None:
|
||||
"""Should be called by app on_app_launch()."""
|
||||
def on_app_loading(self) -> None:
|
||||
"""Should be called by app on_app_loading()."""
|
||||
|
||||
# If we're using a non-default playlist, lets go ahead and get our
|
||||
# music-player going since it may hitch (better while we're faded
|
||||
# out than later).
|
||||
try:
|
||||
cfg = _ba.app.config
|
||||
cfg = babase.app.config
|
||||
if 'Soundtrack' in cfg and cfg['Soundtrack'] not in [
|
||||
'__default__',
|
||||
'Default Soundtrack',
|
||||
]:
|
||||
self.get_music_player()
|
||||
except Exception:
|
||||
from ba import _error
|
||||
|
||||
_error.print_exception('error prepping music-player')
|
||||
logging.exception('Error prepping music-player.')
|
||||
|
||||
def on_app_shutdown(self) -> None:
|
||||
"""Should be called when the app is shutting down."""
|
||||
|
|
@ -188,7 +155,6 @@ class MusicSubsystem:
|
|||
old_mode = self._music_mode
|
||||
self._music_mode = mode
|
||||
if old_mode != self._music_mode or force_restart:
|
||||
|
||||
# If we're switching into test mode we don't
|
||||
# actually play anything until its requested.
|
||||
# If we're switching *out* of test mode though
|
||||
|
|
@ -199,7 +165,7 @@ class MusicSubsystem:
|
|||
|
||||
def supports_soundtrack_entry_type(self, entry_type: str) -> bool:
|
||||
"""Return whether provided soundtrack entry type is supported here."""
|
||||
uas = _ba.env()['user_agent_string']
|
||||
uas = babase.env()['legacy_user_agent_string']
|
||||
assert isinstance(uas, str)
|
||||
|
||||
# FIXME: Generalize this.
|
||||
|
|
@ -208,7 +174,7 @@ class MusicSubsystem:
|
|||
if entry_type in ('musicFile', 'musicFolder'):
|
||||
return (
|
||||
'android' in uas
|
||||
and _ba.android_get_external_files_dir() is not None
|
||||
and babase.android_get_external_files_dir() is not None
|
||||
)
|
||||
if entry_type == 'default':
|
||||
return True
|
||||
|
|
@ -246,9 +212,7 @@ class MusicSubsystem:
|
|||
return entry_type
|
||||
raise ValueError('invalid soundtrack entry:' + str(entry))
|
||||
except Exception:
|
||||
from ba import _error
|
||||
|
||||
_error.print_exception()
|
||||
logging.exception('Error in get_soundtrack_entry_type.')
|
||||
return 'default'
|
||||
|
||||
def get_soundtrack_entry_name(self, entry: Any) -> str:
|
||||
|
|
@ -272,14 +236,12 @@ class MusicSubsystem:
|
|||
return entry['name']
|
||||
raise ValueError('invalid soundtrack entry:' + str(entry))
|
||||
except Exception:
|
||||
from ba import _error
|
||||
|
||||
_error.print_exception()
|
||||
logging.exception('Error in get_soundtrack_entry_name.')
|
||||
return 'default'
|
||||
|
||||
def on_app_resume(self) -> None:
|
||||
"""Should be run when the app resumes from a suspended state."""
|
||||
if _ba.is_os_playing_music():
|
||||
if babase.is_os_playing_music():
|
||||
self.do_play_music(None)
|
||||
|
||||
def do_play_music(
|
||||
|
|
@ -305,8 +267,7 @@ class MusicSubsystem:
|
|||
print(f"Invalid music type: '{musictype}'")
|
||||
musictype = None
|
||||
|
||||
with _ba.Context('ui'):
|
||||
|
||||
with babase.ContextRef.empty():
|
||||
# If they don't want to restart music and we're already
|
||||
# playing what's requested, we're done.
|
||||
if continuous and self.music_types[mode] is musictype:
|
||||
|
|
@ -315,7 +276,7 @@ class MusicSubsystem:
|
|||
|
||||
# If the OS tells us there's currently music playing,
|
||||
# all our operations default to playing nothing.
|
||||
if _ba.is_os_playing_music():
|
||||
if babase.is_os_playing_music():
|
||||
musictype = None
|
||||
|
||||
# If we're not in the mode this music is being set for,
|
||||
|
|
@ -346,7 +307,7 @@ class MusicSubsystem:
|
|||
|
||||
def _get_user_soundtrack(self) -> dict[str, Any]:
|
||||
"""Return current user soundtrack or empty dict otherwise."""
|
||||
cfg = _ba.app.config
|
||||
cfg = babase.app.config
|
||||
soundtrack: dict[str, Any] = {}
|
||||
soundtrackname = cfg.get('Soundtrack')
|
||||
if soundtrackname is not None and soundtrackname != '__default__':
|
||||
|
|
@ -358,44 +319,53 @@ class MusicSubsystem:
|
|||
return soundtrack
|
||||
|
||||
def _play_music_player_music(self, entry: Any) -> None:
|
||||
|
||||
# Stop any existing internal music.
|
||||
if self._music_node is not None:
|
||||
self._music_node.delete()
|
||||
self._music_node = None
|
||||
# if self._music_node is not None:
|
||||
# self._music_node.delete()
|
||||
# self._music_node = None
|
||||
if self._playing_internal_music:
|
||||
bascenev1.set_internal_music(None)
|
||||
self._playing_internal_music = False
|
||||
|
||||
# Do the thing.
|
||||
self.get_music_player().play(entry)
|
||||
|
||||
def _play_internal_music(self, musictype: MusicType | None) -> None:
|
||||
|
||||
# Stop any existing music-player playback.
|
||||
if self._music_player is not None:
|
||||
self._music_player.stop()
|
||||
|
||||
# Stop any existing internal music.
|
||||
if self._music_node:
|
||||
self._music_node.delete()
|
||||
self._music_node = None
|
||||
# if self._music_node:
|
||||
# self._music_node.delete()
|
||||
# self._music_node = None
|
||||
if self._playing_internal_music:
|
||||
bascenev1.set_internal_music(None)
|
||||
self._playing_internal_music = False
|
||||
|
||||
# Start up new internal music.
|
||||
if musictype is not None:
|
||||
|
||||
entry = ASSET_SOUNDTRACK_ENTRIES.get(musictype)
|
||||
if entry is None:
|
||||
print(f"Unknown music: '{musictype}'")
|
||||
entry = ASSET_SOUNDTRACK_ENTRIES[MusicType.FLAG_CATCHER]
|
||||
|
||||
self._music_node = _ba.newnode(
|
||||
type='sound',
|
||||
attrs={
|
||||
'sound': _ba.getsound(entry.assetname),
|
||||
'positional': False,
|
||||
'music': True,
|
||||
'volume': entry.volume * 5.0,
|
||||
'loop': entry.loop,
|
||||
},
|
||||
# self._music_node = _bascenev1.newnode(
|
||||
# type='sound',
|
||||
# attrs={
|
||||
# 'sound': _bascenev1.getsound(entry.assetname),
|
||||
# 'positional': False,
|
||||
# 'music': True,
|
||||
# 'volume': entry.volume * 5.0,
|
||||
# 'loop': entry.loop,
|
||||
# },
|
||||
# )
|
||||
bascenev1.set_internal_music(
|
||||
babase.getsimplesound(entry.assetname),
|
||||
volume=entry.volume * 5.0,
|
||||
loop=entry.loop,
|
||||
)
|
||||
self._playing_internal_music = True
|
||||
|
||||
|
||||
class MusicPlayer:
|
||||
|
|
@ -433,7 +403,7 @@ class MusicPlayer:
|
|||
def play(self, entry: Any) -> None:
|
||||
"""Play provided entry."""
|
||||
if not self._have_set_initial_volume:
|
||||
self._volume = _ba.app.config.resolve('Music Volume')
|
||||
self._volume = babase.app.config.resolve('Music Volume')
|
||||
self.on_set_volume(self._volume)
|
||||
self._have_set_initial_volume = True
|
||||
self._entry_to_play = copy.deepcopy(entry)
|
||||
|
|
@ -482,46 +452,18 @@ class MusicPlayer:
|
|||
"""Called on final app shutdown."""
|
||||
|
||||
def _update_play_state(self) -> None:
|
||||
|
||||
# If we aren't playing, should be, and have positive volume, do so.
|
||||
if not self._actually_playing:
|
||||
if self._entry_to_play is not None and self._volume > 0.0:
|
||||
self.on_play(self._entry_to_play)
|
||||
self._actually_playing = True
|
||||
else:
|
||||
if self._actually_playing and (
|
||||
self._entry_to_play is None or self._volume <= 0.0
|
||||
):
|
||||
if self._entry_to_play is None or self._volume <= 0.0:
|
||||
self.on_stop()
|
||||
self._actually_playing = False
|
||||
|
||||
|
||||
def setmusic(musictype: ba.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.
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
# All we do here now is set a few music attrs on the current globals
|
||||
# node. The foreground globals' current playing music then gets fed to
|
||||
# the do_play_music call in our music controller. This way we can
|
||||
# seamlessly support custom soundtracks in replays/etc since we're being
|
||||
# driven purely by node data.
|
||||
gnode = _ba.getactivity().globalsnode
|
||||
gnode.music_continuous = continuous
|
||||
gnode.music = '' if musictype is None else musictype.value
|
||||
gnode.music_count += 1
|
||||
|
||||
|
||||
def do_play_music(*args: Any, **keywds: Any) -> None:
|
||||
"""A passthrough used by the C++ layer."""
|
||||
_ba.app.music.do_play_music(*args, **keywds)
|
||||
assert babase.app.classic is not None
|
||||
babase.app.classic.music.do_play_music(*args, **keywds)
|
||||
|
|
@ -3,95 +3,30 @@
|
|||
"""Networking related functionality."""
|
||||
from __future__ import annotations
|
||||
|
||||
import ssl
|
||||
import copy
|
||||
import threading
|
||||
import weakref
|
||||
from enum import Enum
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import _ba
|
||||
import babase
|
||||
from babase import DEFAULT_REQUEST_TIMEOUT_SECONDS
|
||||
import bascenev1
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any, Callable
|
||||
import socket
|
||||
|
||||
MasterServerCallback = Callable[[None | dict[str, Any]], None]
|
||||
|
||||
# Timeout for standard functions talking to the master-server/etc.
|
||||
DEFAULT_REQUEST_TIMEOUT_SECONDS = 60
|
||||
|
||||
|
||||
class NetworkSubsystem:
|
||||
"""Network related app subsystem."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
|
||||
# Anyone accessing/modifying zone_pings should hold this lock,
|
||||
# as it is updated by a background thread.
|
||||
self.zone_pings_lock = threading.Lock()
|
||||
|
||||
# Zone IDs mapped to average pings. This will remain empty
|
||||
# until enough pings have been run to be reasonably certain
|
||||
# that a nearby server has been pinged.
|
||||
self.zone_pings: dict[str, float] = {}
|
||||
|
||||
self._sslcontext: ssl.SSLContext | None = None
|
||||
|
||||
# For debugging.
|
||||
self.v1_test_log: str = ''
|
||||
self.v1_ctest_results: dict[int, str] = {}
|
||||
self.server_time_offset_hours: float | None = None
|
||||
|
||||
@property
|
||||
def sslcontext(self) -> ssl.SSLContext:
|
||||
"""Create/return our shared SSLContext.
|
||||
|
||||
This can be reused for all standard urllib requests/etc.
|
||||
"""
|
||||
# Note: I've run into older Android devices taking upwards of 1 second
|
||||
# to put together a default SSLContext, so recycling one can definitely
|
||||
# be a worthwhile optimization. This was suggested to me in this
|
||||
# thread by one of Python's SSL maintainers:
|
||||
# https://github.com/python/cpython/issues/94637
|
||||
if self._sslcontext is None:
|
||||
self._sslcontext = ssl.create_default_context()
|
||||
return self._sslcontext
|
||||
|
||||
|
||||
def get_ip_address_type(addr: str) -> socket.AddressFamily:
|
||||
"""Return socket.AF_INET6 or socket.AF_INET4 for the provided address."""
|
||||
import socket
|
||||
|
||||
socket_type = None
|
||||
|
||||
# First try it as an ipv4 address.
|
||||
try:
|
||||
socket.inet_pton(socket.AF_INET, addr)
|
||||
socket_type = socket.AF_INET
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
# Hmm apparently not ipv4; try ipv6.
|
||||
if socket_type is None:
|
||||
try:
|
||||
socket.inet_pton(socket.AF_INET6, addr)
|
||||
socket_type = socket.AF_INET6
|
||||
except OSError:
|
||||
pass
|
||||
if socket_type is None:
|
||||
raise ValueError(f'addr seems to be neither v4 or v6: {addr}')
|
||||
return socket_type
|
||||
|
||||
|
||||
class MasterServerResponseType(Enum):
|
||||
"""How to interpret responses from the master-server."""
|
||||
"""How to interpret responses from the v1 master-server."""
|
||||
|
||||
JSON = 0
|
||||
|
||||
|
||||
class MasterServerCallThread(threading.Thread):
|
||||
"""Thread to communicate with the master-server."""
|
||||
class MasterServerV1CallThread(threading.Thread):
|
||||
"""Thread to communicate with the v1 master-server."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
|
@ -109,10 +44,10 @@ class MasterServerCallThread(threading.Thread):
|
|||
self._response_type = response_type
|
||||
self._data = {} if data is None else copy.deepcopy(data)
|
||||
self._callback: MasterServerCallback | None = callback
|
||||
self._context = _ba.Context('current')
|
||||
self._context = babase.ContextRef()
|
||||
|
||||
# Save and restore the context we were created from.
|
||||
activity = _ba.getactivity(doraise=False)
|
||||
activity = bascenev1.getactivity(doraise=False)
|
||||
self._activity = weakref.ref(activity) if activity is not None else None
|
||||
|
||||
def _run_callback(self, arg: None | dict[str, Any]) -> None:
|
||||
|
|
@ -140,38 +75,44 @@ class MasterServerCallThread(threading.Thread):
|
|||
import json
|
||||
|
||||
from efro.error import is_urllib_communication_error
|
||||
from ba._general import Call, utf8_all
|
||||
from ba._internal import get_master_server_address
|
||||
|
||||
plus = babase.app.plus
|
||||
assert plus is not None
|
||||
response_data: Any = None
|
||||
url: str | None = None
|
||||
try:
|
||||
self._data = utf8_all(self._data)
|
||||
_ba.set_thread_name('BA_ServerCallThread')
|
||||
classic = babase.app.classic
|
||||
assert classic is not None
|
||||
self._data = babase.utf8_all(self._data)
|
||||
babase.set_thread_name('BA_ServerCallThread')
|
||||
if self._request_type == 'get':
|
||||
url = (
|
||||
get_master_server_address()
|
||||
plus.get_master_server_address()
|
||||
+ '/'
|
||||
+ self._request
|
||||
+ '?'
|
||||
+ urllib.parse.urlencode(self._data)
|
||||
)
|
||||
assert url is not None
|
||||
response = urllib.request.urlopen(
|
||||
urllib.request.Request(
|
||||
url, None, {'User-Agent': _ba.app.user_agent_string}
|
||||
url,
|
||||
None,
|
||||
{'User-Agent': classic.legacy_user_agent_string},
|
||||
),
|
||||
context=_ba.app.net.sslcontext,
|
||||
context=babase.app.net.sslcontext,
|
||||
timeout=DEFAULT_REQUEST_TIMEOUT_SECONDS,
|
||||
)
|
||||
elif self._request_type == 'post':
|
||||
url = get_master_server_address() + '/' + self._request
|
||||
url = plus.get_master_server_address() + '/' + self._request
|
||||
assert url is not None
|
||||
response = urllib.request.urlopen(
|
||||
urllib.request.Request(
|
||||
url,
|
||||
urllib.parse.urlencode(self._data).encode(),
|
||||
{'User-Agent': _ba.app.user_agent_string},
|
||||
{'User-Agent': classic.legacy_user_agent_string},
|
||||
),
|
||||
context=_ba.app.net.sslcontext,
|
||||
context=babase.app.net.sslcontext,
|
||||
timeout=DEFAULT_REQUEST_TIMEOUT_SECONDS,
|
||||
)
|
||||
else:
|
||||
|
|
@ -192,7 +133,6 @@ class MasterServerCallThread(threading.Thread):
|
|||
raise TypeError(f'invalid responsetype: {self._response_type}')
|
||||
|
||||
except Exception as exc:
|
||||
|
||||
# Ignore common network errors; note unexpected ones.
|
||||
if not is_urllib_communication_error(exc, url=url):
|
||||
print(
|
||||
|
|
@ -208,30 +148,7 @@ class MasterServerCallThread(threading.Thread):
|
|||
response_data = None
|
||||
|
||||
if self._callback is not None:
|
||||
_ba.pushcall(
|
||||
Call(self._run_callback, response_data), from_other_thread=True
|
||||
babase.pushcall(
|
||||
babase.Call(self._run_callback, response_data),
|
||||
from_other_thread=True,
|
||||
)
|
||||
|
||||
|
||||
def master_server_get(
|
||||
request: str,
|
||||
data: dict[str, Any],
|
||||
callback: MasterServerCallback | None = None,
|
||||
response_type: MasterServerResponseType = MasterServerResponseType.JSON,
|
||||
) -> None:
|
||||
"""Make a call to the master server via a http GET."""
|
||||
MasterServerCallThread(
|
||||
request, 'get', data, callback, response_type
|
||||
).start()
|
||||
|
||||
|
||||
def master_server_post(
|
||||
request: str,
|
||||
data: dict[str, Any],
|
||||
callback: MasterServerCallback | None = None,
|
||||
response_type: MasterServerResponseType = MasterServerResponseType.JSON,
|
||||
) -> None:
|
||||
"""Make a call to the master server via a http POST."""
|
||||
MasterServerCallThread(
|
||||
request, 'post', data, callback, response_type
|
||||
).start()
|
||||
|
|
@ -19,17 +19,12 @@ from bacommon.servermanager import (
|
|||
ClientListCommand,
|
||||
KickCommand,
|
||||
)
|
||||
import _ba
|
||||
from ba._internal import add_transaction, run_transactions, get_v1_account_state
|
||||
from ba._generated.enums import TimeType
|
||||
from ba._freeforallsession import FreeForAllSession
|
||||
from ba._dualteamsession import DualTeamSession
|
||||
from ba._coopsession import CoopSession
|
||||
import babase
|
||||
import bascenev1
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any
|
||||
|
||||
import ba
|
||||
from bacommon.servermanager import ServerConfig
|
||||
|
||||
|
||||
|
|
@ -37,33 +32,35 @@ def _cmd(command_data: bytes) -> None:
|
|||
"""Handle commands coming in from our server manager parent process."""
|
||||
import pickle
|
||||
|
||||
assert babase.app.classic is not None
|
||||
|
||||
command = pickle.loads(command_data)
|
||||
assert isinstance(command, ServerCommand)
|
||||
|
||||
if isinstance(command, StartServerModeCommand):
|
||||
assert _ba.app.server is None
|
||||
_ba.app.server = ServerController(command.config)
|
||||
assert babase.app.classic.server is None
|
||||
babase.app.classic.server = ServerController(command.config)
|
||||
return
|
||||
|
||||
if isinstance(command, ShutdownCommand):
|
||||
assert _ba.app.server is not None
|
||||
_ba.app.server.shutdown(
|
||||
assert babase.app.classic.server is not None
|
||||
babase.app.classic.server.shutdown(
|
||||
reason=command.reason, immediate=command.immediate
|
||||
)
|
||||
return
|
||||
|
||||
if isinstance(command, ChatMessageCommand):
|
||||
assert _ba.app.server is not None
|
||||
_ba.chatmessage(command.message, clients=command.clients)
|
||||
assert babase.app.classic.server is not None
|
||||
bascenev1.chatmessage(command.message, clients=command.clients)
|
||||
return
|
||||
|
||||
if isinstance(command, ScreenMessageCommand):
|
||||
assert _ba.app.server is not None
|
||||
assert babase.app.classic.server is not None
|
||||
|
||||
# Note: we have to do transient messages if
|
||||
# clients is specified, so they won't show up
|
||||
# in replays.
|
||||
_ba.screenmessage(
|
||||
bascenev1.broadcastmessage(
|
||||
command.message,
|
||||
color=command.color,
|
||||
clients=command.clients,
|
||||
|
|
@ -72,13 +69,13 @@ def _cmd(command_data: bytes) -> None:
|
|||
return
|
||||
|
||||
if isinstance(command, ClientListCommand):
|
||||
assert _ba.app.server is not None
|
||||
_ba.app.server.print_client_list()
|
||||
assert babase.app.classic.server is not None
|
||||
babase.app.classic.server.print_client_list()
|
||||
return
|
||||
|
||||
if isinstance(command, KickCommand):
|
||||
assert _ba.app.server is not None
|
||||
_ba.app.server.kick(
|
||||
assert babase.app.classic.server is not None
|
||||
babase.app.classic.server.kick(
|
||||
client_id=command.client_id, ban_time=command.ban_time
|
||||
)
|
||||
return
|
||||
|
|
@ -96,11 +93,10 @@ class ServerController:
|
|||
"""
|
||||
|
||||
def __init__(self, config: ServerConfig) -> None:
|
||||
|
||||
self._config = config
|
||||
self._playlist_name = '__default__'
|
||||
self._ran_access_check = False
|
||||
self._prep_timer: ba.Timer | None = None
|
||||
self._prep_timer: babase.AppTimer | None = None
|
||||
self._next_stuck_login_warn_time = time.time() + 10.0
|
||||
self._first_run = True
|
||||
self._shutdown_reason: ShutdownReason | None = None
|
||||
|
|
@ -116,19 +112,16 @@ class ServerController:
|
|||
# Now sit around doing any pre-launch prep such as waiting for
|
||||
# account sign-in or fetching playlists; this will kick off the
|
||||
# session once done.
|
||||
with _ba.Context('ui'):
|
||||
self._prep_timer = _ba.Timer(
|
||||
0.25,
|
||||
self._prepare_to_serve,
|
||||
timetype=TimeType.REAL,
|
||||
repeat=True,
|
||||
with babase.ContextRef.empty():
|
||||
self._prep_timer = babase.AppTimer(
|
||||
0.25, self._prepare_to_serve, repeat=True
|
||||
)
|
||||
|
||||
def print_client_list(self) -> None:
|
||||
"""Print info about all connected clients."""
|
||||
import json
|
||||
|
||||
roster = _ba.get_game_roster()
|
||||
roster = bascenev1.get_game_roster()
|
||||
title1 = 'Client ID'
|
||||
title2 = 'Account Name'
|
||||
title3 = 'Players'
|
||||
|
|
@ -161,7 +154,7 @@ class ServerController:
|
|||
if ban_time is None:
|
||||
ban_time = 300
|
||||
|
||||
_ba.disconnect_client(client_id=client_id, ban_time=ban_time)
|
||||
bascenev1.disconnect_client(client_id=client_id, ban_time=ban_time)
|
||||
|
||||
def shutdown(self, reason: ShutdownReason, immediate: bool) -> None:
|
||||
"""Set the app to quit either now or at the next clean opportunity."""
|
||||
|
|
@ -177,7 +170,7 @@ class ServerController:
|
|||
)
|
||||
|
||||
def handle_transition(self) -> bool:
|
||||
"""Handle transitioning to a new ba.Session or quitting the app.
|
||||
"""Handle transitioning to a new bascenev1.Session or quitting the app.
|
||||
|
||||
Will be called once at the end of an activity that is marked as
|
||||
a good 'end-point' (such as a final score screen).
|
||||
|
|
@ -190,15 +183,13 @@ class ServerController:
|
|||
return False
|
||||
|
||||
def _execute_shutdown(self) -> None:
|
||||
from ba._language import Lstr
|
||||
|
||||
if self._executing_shutdown:
|
||||
return
|
||||
self._executing_shutdown = True
|
||||
timestrval = time.strftime('%c')
|
||||
if self._shutdown_reason is ShutdownReason.RESTARTING:
|
||||
_ba.screenmessage(
|
||||
Lstr(resource='internal.serverRestartingText'),
|
||||
bascenev1.broadcastmessage(
|
||||
babase.Lstr(resource='internal.serverRestartingText'),
|
||||
color=(1, 0.5, 0.0),
|
||||
)
|
||||
print(
|
||||
|
|
@ -206,24 +197,24 @@ class ServerController:
|
|||
f' at {timestrval}.{Clr.RST}'
|
||||
)
|
||||
else:
|
||||
_ba.screenmessage(
|
||||
Lstr(resource='internal.serverShuttingDownText'),
|
||||
bascenev1.broadcastmessage(
|
||||
babase.Lstr(resource='internal.serverShuttingDownText'),
|
||||
color=(1, 0.5, 0.0),
|
||||
)
|
||||
print(
|
||||
f'{Clr.SBLU}Exiting for server-shutdown'
|
||||
f' at {timestrval}.{Clr.RST}'
|
||||
)
|
||||
with _ba.Context('ui'):
|
||||
_ba.timer(2.0, _ba.quit, timetype=TimeType.REAL)
|
||||
with babase.ContextRef.empty():
|
||||
babase.apptimer(2.0, babase.quit)
|
||||
|
||||
def _run_access_check(self) -> None:
|
||||
"""Check with the master server to see if we're likely joinable."""
|
||||
from ba._net import master_server_get
|
||||
assert babase.app.classic is not None
|
||||
|
||||
master_server_get(
|
||||
babase.app.classic.master_server_v1_get(
|
||||
'bsAccessCheck',
|
||||
{'port': _ba.get_game_port(), 'b': _ba.app.build_number},
|
||||
{'port': bascenev1.get_game_port(), 'b': babase.app.build_number},
|
||||
callback=self._access_check_response,
|
||||
)
|
||||
|
||||
|
|
@ -235,7 +226,7 @@ class ServerController:
|
|||
else:
|
||||
addr = data['address']
|
||||
port = data['port']
|
||||
show_addr = True
|
||||
show_addr = os.environ.get('BA_ACCESS_CHECK_VERBOSE', '0') == '1'
|
||||
if show_addr:
|
||||
addrstr = f' {addr}'
|
||||
poststr = ''
|
||||
|
|
@ -250,31 +241,22 @@ class ServerController:
|
|||
f'{Clr.SBLU}Master server access check of{addrstr}'
|
||||
f' udp port {port} succeeded.\n'
|
||||
f'Your server appears to be'
|
||||
f' joinable from the internet .{poststr}{Clr.RST}'
|
||||
f' joinable from the internet.{poststr}{Clr.RST}'
|
||||
)
|
||||
if self._config.party_is_public:
|
||||
print(
|
||||
f'{Clr.SBLU}Your party {self._config.party_name}'
|
||||
f' visible in public party list.{Clr.RST}'
|
||||
)
|
||||
else:
|
||||
print(
|
||||
f'{Clr.SBLU}Your private party {self._config.party_name}'
|
||||
f'can be joined by {addrstr} {port}.{Clr.RST}'
|
||||
)
|
||||
else:
|
||||
print(
|
||||
f'{Clr.SRED}Master server access check of{addrstr}'
|
||||
f' udp port {port} failed.\n'
|
||||
f'Your server does not appear to be'
|
||||
f' joinable from the internet. Please check your firewall or instance security group.{poststr}{Clr.RST}'
|
||||
f' joinable from the internet.{poststr}{Clr.RST}'
|
||||
)
|
||||
|
||||
def _prepare_to_serve(self) -> None:
|
||||
"""Run in a timer to do prep before beginning to serve."""
|
||||
signed_in = get_v1_account_state() == 'signed_in'
|
||||
plus = babase.app.plus
|
||||
assert plus is not None
|
||||
signed_in = plus.get_v1_account_state() == 'signed_in'
|
||||
if not signed_in:
|
||||
|
||||
# Signing in to the local server account should not take long;
|
||||
# complain if it does...
|
||||
curtime = time.time()
|
||||
|
|
@ -294,7 +276,7 @@ class ServerController:
|
|||
f'{Clr.SBLU}Requesting shared-playlist'
|
||||
f' {self._config.playlist_code}...{Clr.RST}'
|
||||
)
|
||||
add_transaction(
|
||||
plus.add_v1_account_transaction(
|
||||
{
|
||||
'type': 'IMPORT_PLAYLIST',
|
||||
'code': str(self._config.playlist_code),
|
||||
|
|
@ -302,7 +284,7 @@ class ServerController:
|
|||
},
|
||||
callback=self._on_playlist_fetch_response,
|
||||
)
|
||||
run_transactions()
|
||||
plus.run_v1_account_transactions()
|
||||
self._playlist_fetch_sent_request = True
|
||||
|
||||
if self._playlist_fetch_got_response:
|
||||
|
|
@ -311,7 +293,7 @@ class ServerController:
|
|||
|
||||
if can_launch:
|
||||
self._prep_timer = None
|
||||
_ba.pushcall(self._launch_server_session)
|
||||
babase.pushcall(self._launch_server_session)
|
||||
|
||||
def _on_playlist_fetch_response(
|
||||
self,
|
||||
|
|
@ -319,8 +301,7 @@ class ServerController:
|
|||
) -> None:
|
||||
if result is None:
|
||||
print('Error fetching playlist; aborting.')
|
||||
import _ba
|
||||
_ba.quit()
|
||||
sys.exit(-1)
|
||||
|
||||
# Once we get here, simply modify our config to use this playlist.
|
||||
typename = (
|
||||
|
|
@ -336,15 +317,15 @@ class ServerController:
|
|||
self._config.session_type = typename
|
||||
self._playlist_name = result['playlistName']
|
||||
|
||||
def _get_session_type(self) -> type[ba.Session]:
|
||||
def _get_session_type(self) -> type[bascenev1.Session]:
|
||||
# Convert string session type to the class.
|
||||
# Hmm should we just keep this as a string?
|
||||
if self._config.session_type == 'ffa':
|
||||
return FreeForAllSession
|
||||
return bascenev1.FreeForAllSession
|
||||
if self._config.session_type == 'teams':
|
||||
return DualTeamSession
|
||||
return bascenev1.DualTeamSession
|
||||
if self._config.session_type == 'coop':
|
||||
return CoopSession
|
||||
return bascenev1.CoopSession
|
||||
raise RuntimeError(
|
||||
f'Invalid session_type: "{self._config.session_type}"'
|
||||
)
|
||||
|
|
@ -352,11 +333,16 @@ class ServerController:
|
|||
def _launch_server_session(self) -> None:
|
||||
"""Kick off a host-session based on the current server config."""
|
||||
# pylint: disable=too-many-branches
|
||||
app = _ba.app
|
||||
# pylint: disable=too-many-statements
|
||||
app = babase.app
|
||||
classic = app.classic
|
||||
plus = app.plus
|
||||
assert plus is not None
|
||||
assert classic is not None
|
||||
appcfg = app.config
|
||||
sessiontype = self._get_session_type()
|
||||
|
||||
if get_v1_account_state() != 'signed_in':
|
||||
if plus.get_v1_account_state() != 'signed_in':
|
||||
print(
|
||||
'WARNING: launch_server_session() expects to run '
|
||||
'with a signed in server account'
|
||||
|
|
@ -369,18 +355,18 @@ class ServerController:
|
|||
and self._config.playlist_inline is not None
|
||||
):
|
||||
self._playlist_name = 'ServerModePlaylist'
|
||||
if sessiontype is FreeForAllSession:
|
||||
if sessiontype is bascenev1.FreeForAllSession:
|
||||
ptypename = 'Free-for-All'
|
||||
elif sessiontype is DualTeamSession:
|
||||
elif sessiontype is bascenev1.DualTeamSession:
|
||||
ptypename = 'Team Tournament'
|
||||
elif sessiontype is CoopSession:
|
||||
elif sessiontype is bascenev1.CoopSession:
|
||||
ptypename = 'Coop'
|
||||
else:
|
||||
raise RuntimeError(f'Unknown session type {sessiontype}')
|
||||
|
||||
# Need to add this in a transaction instead of just setting
|
||||
# it directly or it will get overwritten by the master-server.
|
||||
add_transaction(
|
||||
plus.add_v1_account_transaction(
|
||||
{
|
||||
'type': 'ADD_PLAYLIST',
|
||||
'playlistType': ptypename,
|
||||
|
|
@ -388,67 +374,66 @@ class ServerController:
|
|||
'playlist': self._config.playlist_inline,
|
||||
}
|
||||
)
|
||||
run_transactions()
|
||||
plus.run_v1_account_transactions()
|
||||
|
||||
if self._first_run:
|
||||
curtimestr = time.strftime('%c')
|
||||
startupmsg = (
|
||||
f'{Clr.BLD}{Clr.BLU}{_ba.appnameupper()} {app.version}'
|
||||
f'{Clr.BLD}{Clr.BLU}{babase.appnameupper()} {app.version}'
|
||||
f' ({app.build_number})'
|
||||
f' entering server-mode {curtimestr}{Clr.RST}'
|
||||
)
|
||||
logging.info(startupmsg)
|
||||
|
||||
if sessiontype is FreeForAllSession:
|
||||
if sessiontype is bascenev1.FreeForAllSession:
|
||||
appcfg['Free-for-All Playlist Selection'] = self._playlist_name
|
||||
appcfg[
|
||||
'Free-for-All Playlist Randomize'
|
||||
] = self._config.playlist_shuffle
|
||||
elif sessiontype is DualTeamSession:
|
||||
elif sessiontype is bascenev1.DualTeamSession:
|
||||
appcfg['Team Tournament Playlist Selection'] = self._playlist_name
|
||||
appcfg[
|
||||
'Team Tournament Playlist Randomize'
|
||||
] = self._config.playlist_shuffle
|
||||
elif sessiontype is CoopSession:
|
||||
app.coop_session_args = {
|
||||
elif sessiontype is bascenev1.CoopSession:
|
||||
classic.coop_session_args = {
|
||||
'campaign': self._config.coop_campaign,
|
||||
'level': self._config.coop_level,
|
||||
}
|
||||
else:
|
||||
raise RuntimeError(f'Unknown session type {sessiontype}')
|
||||
|
||||
app.teams_series_length = self._config.teams_series_length
|
||||
app.ffa_series_length = self._config.ffa_series_length
|
||||
classic.teams_series_length = self._config.teams_series_length
|
||||
classic.ffa_series_length = self._config.ffa_series_length
|
||||
|
||||
_ba.set_authenticate_clients(self._config.authenticate_clients)
|
||||
bascenev1.set_authenticate_clients(self._config.authenticate_clients)
|
||||
|
||||
_ba.set_enable_default_kick_voting(
|
||||
bascenev1.set_enable_default_kick_voting(
|
||||
self._config.enable_default_kick_voting
|
||||
)
|
||||
_ba.set_admins(self._config.admins)
|
||||
bascenev1.set_admins(self._config.admins)
|
||||
|
||||
# Call set-enabled last (will push state to the cloud).
|
||||
_ba.set_public_party_max_size(self._config.max_party_size)
|
||||
_ba.set_public_party_queue_enabled(self._config.enable_queue)
|
||||
_ba.set_public_party_name(self._config.party_name)
|
||||
_ba.set_public_party_stats_url(self._config.stats_url)
|
||||
_ba.set_public_party_enabled(self._config.party_is_public)
|
||||
bascenev1.set_public_party_max_size(self._config.max_party_size)
|
||||
bascenev1.set_public_party_queue_enabled(self._config.enable_queue)
|
||||
bascenev1.set_public_party_name(self._config.party_name)
|
||||
bascenev1.set_public_party_stats_url(self._config.stats_url)
|
||||
bascenev1.set_public_party_enabled(self._config.party_is_public)
|
||||
|
||||
# And here.. we.. go.
|
||||
if self._config.stress_test_players is not None:
|
||||
# Special case: run a stress test.
|
||||
from ba.internal import run_stress_test
|
||||
|
||||
run_stress_test(
|
||||
assert babase.app.classic is not None
|
||||
babase.app.classic.run_stress_test(
|
||||
playlist_type='Random',
|
||||
playlist_name='__default__',
|
||||
player_count=self._config.stress_test_players,
|
||||
round_duration=30,
|
||||
)
|
||||
else:
|
||||
_ba.new_host_session(sessiontype)
|
||||
bascenev1.new_host_session(sessiontype)
|
||||
|
||||
# Run an access check if we're trying to make a public party.
|
||||
if not self._ran_access_check:
|
||||
if not self._ran_access_check and self._config.party_is_public:
|
||||
self._run_access_check()
|
||||
self._ran_access_check = True
|
||||
570
dist/ba_data/python/baclassic/_store.py
vendored
Normal file
570
dist/ba_data/python/baclassic/_store.py
vendored
Normal file
|
|
@ -0,0 +1,570 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Store related functionality for classic mode."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import babase
|
||||
import bascenev1
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any
|
||||
|
||||
|
||||
class StoreSubsystem:
|
||||
"""Wrangles classic store."""
|
||||
|
||||
def get_store_item(self, item: str) -> dict[str, Any]:
|
||||
"""(internal)"""
|
||||
return self.get_store_items()[item]
|
||||
|
||||
def get_store_item_name_translated(self, item_name: str) -> babase.Lstr:
|
||||
"""Return a babase.Lstr for a store item name."""
|
||||
# pylint: disable=cyclic-import
|
||||
item_info = self.get_store_item(item_name)
|
||||
if item_name.startswith('characters.'):
|
||||
return babase.Lstr(
|
||||
translate=('characterNames', item_info['character'])
|
||||
)
|
||||
if item_name in ['merch']:
|
||||
return babase.Lstr(resource='merchText')
|
||||
if item_name in ['upgrades.pro', 'pro']:
|
||||
return babase.Lstr(
|
||||
resource='store.bombSquadProNameText',
|
||||
subs=[('${APP_NAME}', babase.Lstr(resource='titleText'))],
|
||||
)
|
||||
if item_name.startswith('maps.'):
|
||||
map_type: type[bascenev1.Map] = item_info['map_type']
|
||||
return bascenev1.get_map_display_string(map_type.name)
|
||||
if item_name.startswith('games.'):
|
||||
gametype: type[bascenev1.GameActivity] = item_info['gametype']
|
||||
return gametype.get_display_string()
|
||||
if item_name.startswith('icons.'):
|
||||
return babase.Lstr(resource='editProfileWindow.iconText')
|
||||
raise ValueError('unrecognized item: ' + item_name)
|
||||
|
||||
def get_store_item_display_size(
|
||||
self, item_name: str
|
||||
) -> tuple[float, float]:
|
||||
"""(internal)"""
|
||||
if item_name.startswith('characters.'):
|
||||
return 340 * 0.6, 430 * 0.6
|
||||
if item_name in ['pro', 'upgrades.pro', 'merch']:
|
||||
assert babase.app.classic is not None
|
||||
return 650 * 0.9, 500 * (
|
||||
0.72
|
||||
if (
|
||||
babase.app.config.get('Merch Link')
|
||||
and babase.app.ui_v1.uiscale is babase.UIScale.SMALL
|
||||
)
|
||||
else 0.85
|
||||
)
|
||||
if item_name.startswith('maps.'):
|
||||
return 510 * 0.6, 450 * 0.6
|
||||
if item_name.startswith('icons.'):
|
||||
return 265 * 0.6, 250 * 0.6
|
||||
return 450 * 0.6, 450 * 0.6
|
||||
|
||||
def get_store_items(self) -> dict[str, dict]:
|
||||
"""Returns info about purchasable items.
|
||||
|
||||
(internal)
|
||||
"""
|
||||
# pylint: disable=cyclic-import
|
||||
from bascenev1lib import maps
|
||||
|
||||
assert babase.app.classic is not None
|
||||
|
||||
if babase.app.classic.store_items is None:
|
||||
from bascenev1lib.game import ninjafight
|
||||
from bascenev1lib.game import meteorshower
|
||||
from bascenev1lib.game import targetpractice
|
||||
from bascenev1lib.game import easteregghunt
|
||||
|
||||
# IMPORTANT - need to keep this synced with the master server.
|
||||
# (doing so manually for now)
|
||||
babase.app.classic.store_items = {
|
||||
'characters.kronk': {'character': 'Kronk'},
|
||||
'characters.zoe': {'character': 'Zoe'},
|
||||
'characters.jackmorgan': {'character': 'Jack Morgan'},
|
||||
'characters.mel': {'character': 'Mel'},
|
||||
'characters.snakeshadow': {'character': 'Snake Shadow'},
|
||||
'characters.bones': {'character': 'Bones'},
|
||||
'characters.bernard': {
|
||||
'character': 'Bernard',
|
||||
'highlight': (0.6, 0.5, 0.8),
|
||||
},
|
||||
'characters.pixie': {'character': 'Pixel'},
|
||||
'characters.wizard': {'character': 'Grumbledorf'},
|
||||
'characters.frosty': {'character': 'Frosty'},
|
||||
'characters.pascal': {'character': 'Pascal'},
|
||||
'characters.cyborg': {'character': 'B-9000'},
|
||||
'characters.agent': {'character': 'Agent Johnson'},
|
||||
'characters.taobaomascot': {'character': 'Taobao Mascot'},
|
||||
'characters.santa': {'character': 'Santa Claus'},
|
||||
'characters.bunny': {'character': 'Easter Bunny'},
|
||||
'merch': {},
|
||||
'pro': {},
|
||||
'maps.lake_frigid': {'map_type': maps.LakeFrigid},
|
||||
'games.ninja_fight': {
|
||||
'gametype': ninjafight.NinjaFightGame,
|
||||
'previewTex': 'courtyardPreview',
|
||||
},
|
||||
'games.meteor_shower': {
|
||||
'gametype': meteorshower.MeteorShowerGame,
|
||||
'previewTex': 'rampagePreview',
|
||||
},
|
||||
'games.target_practice': {
|
||||
'gametype': targetpractice.TargetPracticeGame,
|
||||
'previewTex': 'doomShroomPreview',
|
||||
},
|
||||
'games.easter_egg_hunt': {
|
||||
'gametype': easteregghunt.EasterEggHuntGame,
|
||||
'previewTex': 'towerDPreview',
|
||||
},
|
||||
'icons.flag_us': {
|
||||
'icon': babase.charstr(
|
||||
babase.SpecialChar.FLAG_UNITED_STATES
|
||||
)
|
||||
},
|
||||
'icons.flag_mexico': {
|
||||
'icon': babase.charstr(babase.SpecialChar.FLAG_MEXICO)
|
||||
},
|
||||
'icons.flag_germany': {
|
||||
'icon': babase.charstr(babase.SpecialChar.FLAG_GERMANY)
|
||||
},
|
||||
'icons.flag_brazil': {
|
||||
'icon': babase.charstr(babase.SpecialChar.FLAG_BRAZIL)
|
||||
},
|
||||
'icons.flag_russia': {
|
||||
'icon': babase.charstr(babase.SpecialChar.FLAG_RUSSIA)
|
||||
},
|
||||
'icons.flag_china': {
|
||||
'icon': babase.charstr(babase.SpecialChar.FLAG_CHINA)
|
||||
},
|
||||
'icons.flag_uk': {
|
||||
'icon': babase.charstr(
|
||||
babase.SpecialChar.FLAG_UNITED_KINGDOM
|
||||
)
|
||||
},
|
||||
'icons.flag_canada': {
|
||||
'icon': babase.charstr(babase.SpecialChar.FLAG_CANADA)
|
||||
},
|
||||
'icons.flag_india': {
|
||||
'icon': babase.charstr(babase.SpecialChar.FLAG_INDIA)
|
||||
},
|
||||
'icons.flag_japan': {
|
||||
'icon': babase.charstr(babase.SpecialChar.FLAG_JAPAN)
|
||||
},
|
||||
'icons.flag_france': {
|
||||
'icon': babase.charstr(babase.SpecialChar.FLAG_FRANCE)
|
||||
},
|
||||
'icons.flag_indonesia': {
|
||||
'icon': babase.charstr(babase.SpecialChar.FLAG_INDONESIA)
|
||||
},
|
||||
'icons.flag_italy': {
|
||||
'icon': babase.charstr(babase.SpecialChar.FLAG_ITALY)
|
||||
},
|
||||
'icons.flag_south_korea': {
|
||||
'icon': babase.charstr(babase.SpecialChar.FLAG_SOUTH_KOREA)
|
||||
},
|
||||
'icons.flag_netherlands': {
|
||||
'icon': babase.charstr(babase.SpecialChar.FLAG_NETHERLANDS)
|
||||
},
|
||||
'icons.flag_uae': {
|
||||
'icon': babase.charstr(
|
||||
babase.SpecialChar.FLAG_UNITED_ARAB_EMIRATES
|
||||
)
|
||||
},
|
||||
'icons.flag_qatar': {
|
||||
'icon': babase.charstr(babase.SpecialChar.FLAG_QATAR)
|
||||
},
|
||||
'icons.flag_egypt': {
|
||||
'icon': babase.charstr(babase.SpecialChar.FLAG_EGYPT)
|
||||
},
|
||||
'icons.flag_kuwait': {
|
||||
'icon': babase.charstr(babase.SpecialChar.FLAG_KUWAIT)
|
||||
},
|
||||
'icons.flag_algeria': {
|
||||
'icon': babase.charstr(babase.SpecialChar.FLAG_ALGERIA)
|
||||
},
|
||||
'icons.flag_saudi_arabia': {
|
||||
'icon': babase.charstr(babase.SpecialChar.FLAG_SAUDI_ARABIA)
|
||||
},
|
||||
'icons.flag_malaysia': {
|
||||
'icon': babase.charstr(babase.SpecialChar.FLAG_MALAYSIA)
|
||||
},
|
||||
'icons.flag_czech_republic': {
|
||||
'icon': babase.charstr(
|
||||
babase.SpecialChar.FLAG_CZECH_REPUBLIC
|
||||
)
|
||||
},
|
||||
'icons.flag_australia': {
|
||||
'icon': babase.charstr(babase.SpecialChar.FLAG_AUSTRALIA)
|
||||
},
|
||||
'icons.flag_singapore': {
|
||||
'icon': babase.charstr(babase.SpecialChar.FLAG_SINGAPORE)
|
||||
},
|
||||
'icons.flag_iran': {
|
||||
'icon': babase.charstr(babase.SpecialChar.FLAG_IRAN)
|
||||
},
|
||||
'icons.flag_poland': {
|
||||
'icon': babase.charstr(babase.SpecialChar.FLAG_POLAND)
|
||||
},
|
||||
'icons.flag_argentina': {
|
||||
'icon': babase.charstr(babase.SpecialChar.FLAG_ARGENTINA)
|
||||
},
|
||||
'icons.flag_philippines': {
|
||||
'icon': babase.charstr(babase.SpecialChar.FLAG_PHILIPPINES)
|
||||
},
|
||||
'icons.flag_chile': {
|
||||
'icon': babase.charstr(babase.SpecialChar.FLAG_CHILE)
|
||||
},
|
||||
'icons.fedora': {
|
||||
'icon': babase.charstr(babase.SpecialChar.FEDORA)
|
||||
},
|
||||
'icons.hal': {'icon': babase.charstr(babase.SpecialChar.HAL)},
|
||||
'icons.crown': {
|
||||
'icon': babase.charstr(babase.SpecialChar.CROWN)
|
||||
},
|
||||
'icons.yinyang': {
|
||||
'icon': babase.charstr(babase.SpecialChar.YIN_YANG)
|
||||
},
|
||||
'icons.eyeball': {
|
||||
'icon': babase.charstr(babase.SpecialChar.EYE_BALL)
|
||||
},
|
||||
'icons.skull': {
|
||||
'icon': babase.charstr(babase.SpecialChar.SKULL)
|
||||
},
|
||||
'icons.heart': {
|
||||
'icon': babase.charstr(babase.SpecialChar.HEART)
|
||||
},
|
||||
'icons.dragon': {
|
||||
'icon': babase.charstr(babase.SpecialChar.DRAGON)
|
||||
},
|
||||
'icons.helmet': {
|
||||
'icon': babase.charstr(babase.SpecialChar.HELMET)
|
||||
},
|
||||
'icons.mushroom': {
|
||||
'icon': babase.charstr(babase.SpecialChar.MUSHROOM)
|
||||
},
|
||||
'icons.ninja_star': {
|
||||
'icon': babase.charstr(babase.SpecialChar.NINJA_STAR)
|
||||
},
|
||||
'icons.viking_helmet': {
|
||||
'icon': babase.charstr(babase.SpecialChar.VIKING_HELMET)
|
||||
},
|
||||
'icons.moon': {'icon': babase.charstr(babase.SpecialChar.MOON)},
|
||||
'icons.spider': {
|
||||
'icon': babase.charstr(babase.SpecialChar.SPIDER)
|
||||
},
|
||||
'icons.fireball': {
|
||||
'icon': babase.charstr(babase.SpecialChar.FIREBALL)
|
||||
},
|
||||
'icons.mikirog': {
|
||||
'icon': babase.charstr(babase.SpecialChar.MIKIROG)
|
||||
},
|
||||
'icons.explodinary': {
|
||||
'icon': babase.charstr(babase.SpecialChar.EXPLODINARY_LOGO)
|
||||
},
|
||||
}
|
||||
return babase.app.classic.store_items
|
||||
|
||||
def get_store_layout(self) -> dict[str, list[dict[str, Any]]]:
|
||||
"""Return what's available in the store at a given time.
|
||||
|
||||
Categorized by tab and by section.
|
||||
"""
|
||||
plus = babase.app.plus
|
||||
classic = babase.app.classic
|
||||
|
||||
assert classic is not None
|
||||
assert plus is not None
|
||||
|
||||
if classic.store_layout is None:
|
||||
classic.store_layout = {
|
||||
'characters': [{'items': []}],
|
||||
'extras': [{'items': ['pro']}],
|
||||
'maps': [{'items': ['maps.lake_frigid']}],
|
||||
'minigames': [],
|
||||
'icons': [
|
||||
{
|
||||
'items': [
|
||||
'icons.mushroom',
|
||||
'icons.heart',
|
||||
'icons.eyeball',
|
||||
'icons.yinyang',
|
||||
'icons.hal',
|
||||
'icons.flag_us',
|
||||
'icons.flag_mexico',
|
||||
'icons.flag_germany',
|
||||
'icons.flag_brazil',
|
||||
'icons.flag_russia',
|
||||
'icons.flag_china',
|
||||
'icons.flag_uk',
|
||||
'icons.flag_canada',
|
||||
'icons.flag_india',
|
||||
'icons.flag_japan',
|
||||
'icons.flag_france',
|
||||
'icons.flag_indonesia',
|
||||
'icons.flag_italy',
|
||||
'icons.flag_south_korea',
|
||||
'icons.flag_netherlands',
|
||||
'icons.flag_uae',
|
||||
'icons.flag_qatar',
|
||||
'icons.flag_egypt',
|
||||
'icons.flag_kuwait',
|
||||
'icons.flag_algeria',
|
||||
'icons.flag_saudi_arabia',
|
||||
'icons.flag_malaysia',
|
||||
'icons.flag_czech_republic',
|
||||
'icons.flag_australia',
|
||||
'icons.flag_singapore',
|
||||
'icons.flag_iran',
|
||||
'icons.flag_poland',
|
||||
'icons.flag_argentina',
|
||||
'icons.flag_philippines',
|
||||
'icons.flag_chile',
|
||||
'icons.moon',
|
||||
'icons.fedora',
|
||||
'icons.spider',
|
||||
'icons.ninja_star',
|
||||
'icons.skull',
|
||||
'icons.dragon',
|
||||
'icons.viking_helmet',
|
||||
'icons.fireball',
|
||||
'icons.helmet',
|
||||
'icons.crown',
|
||||
]
|
||||
}
|
||||
],
|
||||
}
|
||||
store_layout = classic.store_layout
|
||||
store_layout['characters'] = [
|
||||
{
|
||||
'items': [
|
||||
'characters.kronk',
|
||||
'characters.zoe',
|
||||
'characters.jackmorgan',
|
||||
'characters.mel',
|
||||
'characters.snakeshadow',
|
||||
'characters.bones',
|
||||
'characters.bernard',
|
||||
'characters.agent',
|
||||
'characters.frosty',
|
||||
'characters.pascal',
|
||||
'characters.pixie',
|
||||
]
|
||||
}
|
||||
]
|
||||
store_layout['minigames'] = [
|
||||
{
|
||||
'items': [
|
||||
'games.ninja_fight',
|
||||
'games.meteor_shower',
|
||||
'games.target_practice',
|
||||
]
|
||||
}
|
||||
]
|
||||
if plus.get_v1_account_misc_read_val('xmas', False):
|
||||
store_layout['characters'][0]['items'].append('characters.santa')
|
||||
store_layout['characters'][0]['items'].append('characters.wizard')
|
||||
store_layout['characters'][0]['items'].append('characters.cyborg')
|
||||
if plus.get_v1_account_misc_read_val('easter', False):
|
||||
store_layout['characters'].append(
|
||||
{
|
||||
'title': 'store.holidaySpecialText',
|
||||
'items': ['characters.bunny'],
|
||||
}
|
||||
)
|
||||
store_layout['minigames'].append(
|
||||
{
|
||||
'title': 'store.holidaySpecialText',
|
||||
'items': ['games.easter_egg_hunt'],
|
||||
}
|
||||
)
|
||||
|
||||
# This will cause merch to show only if the master-server has
|
||||
# given us a link (which means merch is available in our region).
|
||||
store_layout['extras'] = [{'items': ['pro']}]
|
||||
if babase.app.config.get('Merch Link'):
|
||||
store_layout['extras'][0]['items'].append('merch')
|
||||
return store_layout
|
||||
|
||||
def get_clean_price(self, price_string: str) -> str:
|
||||
"""(internal)"""
|
||||
|
||||
# I'm not brave enough to try and do any numerical
|
||||
# manipulation on formatted price strings, but lets do a
|
||||
# few swap-outs to tidy things up a bit.
|
||||
psubs = {
|
||||
'$2.99': '$3.00',
|
||||
'$4.99': '$5.00',
|
||||
'$9.99': '$10.00',
|
||||
'$19.99': '$20.00',
|
||||
'$49.99': '$50.00',
|
||||
}
|
||||
return psubs.get(price_string, price_string)
|
||||
|
||||
def get_available_purchase_count(self, tab: str | None = None) -> int:
|
||||
"""(internal)"""
|
||||
plus = babase.app.plus
|
||||
if plus is None:
|
||||
return 0
|
||||
try:
|
||||
if plus.get_v1_account_state() != 'signed_in':
|
||||
return 0
|
||||
count = 0
|
||||
our_tickets = plus.get_v1_account_ticket_count()
|
||||
store_data = self.get_store_layout()
|
||||
if tab is not None:
|
||||
tabs = [(tab, store_data[tab])]
|
||||
else:
|
||||
tabs = list(store_data.items())
|
||||
for tab_name, tabval in tabs:
|
||||
if tab_name == 'icons':
|
||||
continue # too many of these; don't show..
|
||||
count = self._calc_count_for_tab(tabval, our_tickets, count)
|
||||
return count
|
||||
except Exception:
|
||||
logging.exception('Error calcing available purchases.')
|
||||
return 0
|
||||
|
||||
def _calc_count_for_tab(
|
||||
self, tabval: list[dict[str, Any]], our_tickets: int, count: int
|
||||
) -> int:
|
||||
plus = babase.app.plus
|
||||
assert plus
|
||||
for section in tabval:
|
||||
for item in section['items']:
|
||||
ticket_cost = plus.get_v1_account_misc_read_val(
|
||||
'price.' + item, None
|
||||
)
|
||||
if ticket_cost is not None:
|
||||
if our_tickets >= ticket_cost and not plus.get_purchased(
|
||||
item
|
||||
):
|
||||
count += 1
|
||||
return count
|
||||
|
||||
def get_available_sale_time(self, tab: str) -> int | None:
|
||||
"""(internal)"""
|
||||
# pylint: disable=too-many-branches
|
||||
# pylint: disable=too-many-nested-blocks
|
||||
plus = babase.app.plus
|
||||
assert plus is not None
|
||||
|
||||
try:
|
||||
import datetime
|
||||
|
||||
app = babase.app
|
||||
assert app.classic is not None
|
||||
sale_times: list[int | None] = []
|
||||
|
||||
# Calc time for our pro sale (old special case).
|
||||
if tab == 'extras':
|
||||
config = app.config
|
||||
if app.classic.accounts.have_pro():
|
||||
return None
|
||||
|
||||
# If we haven't calced/loaded start times yet.
|
||||
if app.classic.pro_sale_start_time is None:
|
||||
# If we've got a time-remaining in our config, start there.
|
||||
if 'PSTR' in config:
|
||||
app.classic.pro_sale_start_time = int(
|
||||
babase.apptime() * 1000
|
||||
)
|
||||
app.classic.pro_sale_start_val = config['PSTR']
|
||||
else:
|
||||
# We start the timer once we get the duration from
|
||||
# the server.
|
||||
start_duration = plus.get_v1_account_misc_read_val(
|
||||
'proSaleDurationMinutes', None
|
||||
)
|
||||
if start_duration is not None:
|
||||
app.classic.pro_sale_start_time = int(
|
||||
babase.apptime() * 1000
|
||||
)
|
||||
app.classic.pro_sale_start_val = (
|
||||
60000 * start_duration
|
||||
)
|
||||
|
||||
# If we haven't heard from the server yet, no sale..
|
||||
else:
|
||||
return None
|
||||
|
||||
assert app.classic.pro_sale_start_val is not None
|
||||
val: int | None = max(
|
||||
0,
|
||||
app.classic.pro_sale_start_val
|
||||
- (
|
||||
int(babase.apptime() * 1000.0)
|
||||
- app.classic.pro_sale_start_time
|
||||
),
|
||||
)
|
||||
|
||||
# Keep the value in the config up to date. I suppose we should
|
||||
# write the config occasionally but it should happen often
|
||||
# enough for other reasons.
|
||||
config['PSTR'] = val
|
||||
if val == 0:
|
||||
val = None
|
||||
sale_times.append(val)
|
||||
|
||||
# Now look for sales in this tab.
|
||||
sales_raw = plus.get_v1_account_misc_read_val('sales', {})
|
||||
store_layout = self.get_store_layout()
|
||||
for section in store_layout[tab]:
|
||||
for item in section['items']:
|
||||
if item in sales_raw:
|
||||
if not plus.get_purchased(item):
|
||||
to_end = (
|
||||
datetime.datetime.utcfromtimestamp(
|
||||
sales_raw[item]['e']
|
||||
)
|
||||
- datetime.datetime.utcnow()
|
||||
).total_seconds()
|
||||
if to_end > 0:
|
||||
sale_times.append(int(to_end * 1000))
|
||||
|
||||
# Return the smallest time I guess?
|
||||
sale_times_int = [t for t in sale_times if isinstance(t, int)]
|
||||
return min(sale_times_int) if sale_times_int else None
|
||||
|
||||
except Exception:
|
||||
logging.exception('Error calcing sale time.')
|
||||
return None
|
||||
|
||||
def get_unowned_maps(self) -> list[str]:
|
||||
"""Return the list of local maps not owned by the current account.
|
||||
|
||||
Category: **Asset Functions**
|
||||
"""
|
||||
plus = babase.app.plus
|
||||
unowned_maps: set[str] = set()
|
||||
if not babase.app.headless_mode:
|
||||
for map_section in self.get_store_layout()['maps']:
|
||||
for mapitem in map_section['items']:
|
||||
if plus is None or not plus.get_purchased(mapitem):
|
||||
m_info = self.get_store_item(mapitem)
|
||||
unowned_maps.add(m_info['map_type'].name)
|
||||
return sorted(unowned_maps)
|
||||
|
||||
def get_unowned_game_types(self) -> set[type[bascenev1.GameActivity]]:
|
||||
"""Return present game types not owned by the current account."""
|
||||
try:
|
||||
plus = babase.app.plus
|
||||
unowned_games: set[type[bascenev1.GameActivity]] = set()
|
||||
if not babase.app.headless_mode:
|
||||
for section in self.get_store_layout()['minigames']:
|
||||
for mname in section['items']:
|
||||
if plus is None or not plus.get_purchased(mname):
|
||||
m_info = self.get_store_item(mname)
|
||||
unowned_games.add(m_info['gametype'])
|
||||
return unowned_games
|
||||
except Exception:
|
||||
logging.exception('Error calcing un-owned games.')
|
||||
return set()
|
||||
781
dist/ba_data/python/baclassic/_subsystem.py
vendored
Normal file
781
dist/ba_data/python/baclassic/_subsystem.py
vendored
Normal file
|
|
@ -0,0 +1,781 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Provides classic app subsystem."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
import random
|
||||
import logging
|
||||
import weakref
|
||||
|
||||
from efro.dataclassio import dataclass_from_dict
|
||||
import babase
|
||||
import bauiv1
|
||||
import bascenev1
|
||||
|
||||
import _baclassic
|
||||
from baclassic._music import MusicSubsystem
|
||||
from baclassic._accountv1 import AccountV1Subsystem
|
||||
from baclassic._ads import AdsSubsystem
|
||||
from baclassic._net import MasterServerResponseType, MasterServerV1CallThread
|
||||
from baclassic._achievement import AchievementSubsystem
|
||||
from baclassic._tips import get_all_tips
|
||||
from baclassic._store import StoreSubsystem
|
||||
from baclassic import _input
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Callable, Any, Sequence
|
||||
|
||||
from bascenev1lib.actor import spazappearance
|
||||
from bauiv1lib.party import PartyWindow
|
||||
|
||||
from baclassic._appdelegate import AppDelegate
|
||||
from baclassic._servermode import ServerController
|
||||
from baclassic._net import MasterServerCallback
|
||||
|
||||
|
||||
class ClassicSubsystem(babase.AppSubsystem):
|
||||
"""Subsystem for classic functionality in the app.
|
||||
|
||||
The single shared instance of this app can be accessed at
|
||||
babase.app.classic. Note that it is possible for babase.app.classic to
|
||||
be None if the classic package is not present, and code should handle
|
||||
that case gracefully.
|
||||
"""
|
||||
|
||||
# pylint: disable=too-many-public-methods
|
||||
|
||||
# noinspection PyUnresolvedReferences
|
||||
from baclassic._music import MusicPlayMode
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self._env = babase.env()
|
||||
|
||||
self.accounts = AccountV1Subsystem()
|
||||
self.ads = AdsSubsystem()
|
||||
self.ach = AchievementSubsystem()
|
||||
self.store = StoreSubsystem()
|
||||
self.music = MusicSubsystem()
|
||||
|
||||
# Co-op Campaigns.
|
||||
self.campaigns: dict[str, bascenev1.Campaign] = {}
|
||||
self.custom_coop_practice_games: list[str] = []
|
||||
|
||||
# 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
|
||||
|
||||
# Misc.
|
||||
self.tips: list[str] = []
|
||||
self.stress_test_reset_timer: babase.AppTimer | None = None
|
||||
self.value_test_defaults: dict = {}
|
||||
self.special_offer: dict | None = None
|
||||
self.ping_thread_count = 0
|
||||
self.allow_ticket_purchases: bool = not babase.app.iircade_mode
|
||||
|
||||
# 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 = babase.AppTime(-99999.0)
|
||||
|
||||
# Server Mode.
|
||||
self.server: ServerController | None = None
|
||||
|
||||
self.log_have_new = False
|
||||
self.log_upload_timer_started = False
|
||||
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
|
||||
|
||||
# Maps.
|
||||
self.maps: dict[str, type[bascenev1.Map]] = {}
|
||||
|
||||
# Gameplay.
|
||||
self.teams_series_length = 7
|
||||
self.ffa_series_length = 24
|
||||
self.coop_session_args: dict = {}
|
||||
|
||||
# UI.
|
||||
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.invite_confirm_windows: list[Any] = [] # FIXME: Don't use Any.
|
||||
self.delegate: AppDelegate | None = None
|
||||
self.party_window: weakref.ref[PartyWindow] | None = None
|
||||
|
||||
# Store.
|
||||
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
|
||||
|
||||
@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 legacy_user_agent_string(self) -> str:
|
||||
"""String containing various bits of info about OS/device/etc."""
|
||||
assert isinstance(self._env['legacy_user_agent_string'], str)
|
||||
return self._env['legacy_user_agent_string']
|
||||
|
||||
def on_app_loading(self) -> None:
|
||||
from bascenev1lib.actor import spazappearance
|
||||
from bascenev1lib import maps as stdmaps
|
||||
|
||||
from baclassic._appdelegate import AppDelegate
|
||||
|
||||
plus = babase.app.plus
|
||||
assert plus is not None
|
||||
|
||||
cfg = babase.app.config
|
||||
|
||||
self.music.on_app_loading()
|
||||
|
||||
self.delegate = AppDelegate()
|
||||
|
||||
# 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 babase.app.debug_build
|
||||
and not babase.app.test_build
|
||||
and not plus.is_blessed()
|
||||
):
|
||||
babase.screenmessage('WARNING: NON-BLESSED BUILD', color=(1, 0, 0))
|
||||
|
||||
# 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,
|
||||
]:
|
||||
bascenev1.register_map(maptype)
|
||||
|
||||
spazappearance.register_appearances()
|
||||
bascenev1.init_campaigns()
|
||||
|
||||
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:
|
||||
assert plus is not None
|
||||
|
||||
from bauiv1lib.specialoffer import show_offer
|
||||
|
||||
if (
|
||||
'pendingSpecialOffer' in cfg
|
||||
and plus.get_v1_account_public_login_id()
|
||||
== cfg['pendingSpecialOffer']['a']
|
||||
):
|
||||
self.special_offer = cfg['pendingSpecialOffer']['o']
|
||||
show_offer()
|
||||
|
||||
if not babase.app.headless_mode:
|
||||
babase.apptimer(3.0, check_special_offer)
|
||||
|
||||
# If there's a leftover log file, attempt to upload it to the
|
||||
# master-server and/or get rid of it.
|
||||
babase.handle_leftover_v1_cloud_log_file()
|
||||
|
||||
self.accounts.on_app_loading()
|
||||
|
||||
def on_app_pause(self) -> None:
|
||||
self.accounts.on_app_pause()
|
||||
|
||||
def on_app_resume(self) -> None:
|
||||
self.accounts.on_app_resume()
|
||||
self.music.on_app_resume()
|
||||
|
||||
def on_app_shutdown(self) -> None:
|
||||
self.music.on_app_shutdown()
|
||||
|
||||
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. Note: we now no longer pause if there are connected clients.
|
||||
"""
|
||||
activity: bascenev1.Activity | None = (
|
||||
bascenev1.get_foreground_host_activity()
|
||||
)
|
||||
if (
|
||||
activity is not None
|
||||
and activity.allow_pausing
|
||||
and not bascenev1.have_connected_clients()
|
||||
):
|
||||
from babase import Lstr
|
||||
from bascenev1 import NodeActor
|
||||
|
||||
# FIXME: Shouldn't be touching scene stuff here;
|
||||
# should just pass the request on to the host-session.
|
||||
with activity.context:
|
||||
globs = activity.globalsnode
|
||||
if not globs.paused:
|
||||
bascenev1.getsound('refWhistle').play()
|
||||
globs.paused = True
|
||||
|
||||
# FIXME: This should not be an attr on Actor.
|
||||
activity.paused_text = NodeActor(
|
||||
bascenev1.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 = bascenev1.get_foreground_host_activity()
|
||||
if activity is not None:
|
||||
with activity.context:
|
||||
globs = activity.globalsnode
|
||||
if globs.paused:
|
||||
bascenev1.getsound('refWhistle').play()
|
||||
globs.paused = False
|
||||
|
||||
# FIXME: This should not be an actor attr.
|
||||
activity.paused_text = None
|
||||
|
||||
def add_coop_practice_level(self, level: bascenev1.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 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 bauiv1lib.coop.level import CoopLevelLockedWindow
|
||||
|
||||
assert babase.app.classic is not None
|
||||
|
||||
if args is None:
|
||||
args = {}
|
||||
if game == '':
|
||||
raise ValueError('empty game name')
|
||||
campaignname, levelname = game.split(':')
|
||||
campaign = babase.app.classic.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 bascenev1 import CoopSession
|
||||
|
||||
try:
|
||||
bascenev1.new_host_session(CoopSession)
|
||||
except Exception:
|
||||
logging.exception('Error creating coopsession after fade end.')
|
||||
from bascenev1lib.mainmenu import MainMenuSession
|
||||
|
||||
bascenev1.new_host_session(MainMenuSession)
|
||||
|
||||
babase.fade_screen(False, endcall=_fade_end)
|
||||
return True
|
||||
|
||||
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 baclassic import _benchmark
|
||||
from bascenev1lib.mainmenu import MainMenuSession
|
||||
|
||||
plus = babase.app.plus
|
||||
assert plus is not None
|
||||
|
||||
if reset_ui:
|
||||
babase.app.ui_v1.clear_main_menu_window()
|
||||
|
||||
if isinstance(bascenev1.get_foreground_host_session(), MainMenuSession):
|
||||
# It may be possible we're on the main menu but the screen is faded
|
||||
# so fade back in.
|
||||
babase.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: bascenev1.Session | None = (
|
||||
bascenev1.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.
|
||||
plus.add_v1_account_transaction(
|
||||
{'type': 'END_SESSION', 'sType': str(type(host_session))}
|
||||
)
|
||||
plus.run_v1_account_transactions()
|
||||
|
||||
host_session.end()
|
||||
|
||||
# Otherwise just force the issue.
|
||||
else:
|
||||
babase.pushcall(
|
||||
babase.Call(bascenev1.new_host_session, MainMenuSession)
|
||||
)
|
||||
|
||||
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.
|
||||
|
||||
Play Types:
|
||||
|
||||
'melee'
|
||||
General fighting map.
|
||||
Has one or more 'spawn' locations.
|
||||
|
||||
'team_flag'
|
||||
For games such as Capture The Flag where each team spawns by a flag.
|
||||
Has two or more 'spawn' locations, each with a corresponding 'flag'
|
||||
location (based on index).
|
||||
|
||||
'single_flag'
|
||||
For games such as King of the Hill or Keep Away where multiple teams
|
||||
are fighting over a single flag.
|
||||
Has two or more 'spawn' locations and 1 'flag_default' location.
|
||||
|
||||
'conquest'
|
||||
For games such as Conquest where flags are spread throughout the map
|
||||
- has 2+ 'flag' locations, 2+ 'spawn_by_flag' locations.
|
||||
|
||||
'king_of_the_hill' - has 2+ 'spawn' locations,
|
||||
1+ 'flag_default' locations, and 1+ 'powerup_spawn' locations
|
||||
|
||||
'hockey'
|
||||
For hockey games.
|
||||
Has two 'goal' locations, corresponding 'spawn' locations, and one
|
||||
'flag_default' location (for where puck spawns)
|
||||
|
||||
'football'
|
||||
For football games.
|
||||
Has two 'goal' locations, corresponding 'spawn' locations, and one
|
||||
'flag_default' location (for where flag/ball/etc. spawns)
|
||||
|
||||
'race'
|
||||
For racing games where players much touch each region in order.
|
||||
Has two or more 'race_point' locations.
|
||||
"""
|
||||
return sorted(
|
||||
key
|
||||
for key, val in self.maps.items()
|
||||
if playtype in val.get_play_types()
|
||||
)
|
||||
|
||||
def show_online_score_ui(
|
||||
self,
|
||||
show: str = 'general',
|
||||
game: str | None = None,
|
||||
game_version: str | None = None,
|
||||
) -> None:
|
||||
"""(internal)"""
|
||||
bauiv1.show_online_score_ui(show, game, game_version)
|
||||
|
||||
def game_begin_analytics(self) -> None:
|
||||
"""(internal)"""
|
||||
from baclassic import _analytics
|
||||
|
||||
_analytics.game_begin_analytics()
|
||||
|
||||
def master_server_v1_get(
|
||||
self,
|
||||
request: str,
|
||||
data: dict[str, Any],
|
||||
callback: MasterServerCallback | None = None,
|
||||
response_type: MasterServerResponseType = MasterServerResponseType.JSON,
|
||||
) -> None:
|
||||
"""Make a call to the master server via a http GET."""
|
||||
|
||||
MasterServerV1CallThread(
|
||||
request, 'get', data, callback, response_type
|
||||
).start()
|
||||
|
||||
def master_server_v1_post(
|
||||
self,
|
||||
request: str,
|
||||
data: dict[str, Any],
|
||||
callback: MasterServerCallback | None = None,
|
||||
response_type: MasterServerResponseType = MasterServerResponseType.JSON,
|
||||
) -> None:
|
||||
"""Make a call to the master server via a http POST."""
|
||||
MasterServerV1CallThread(
|
||||
request, 'post', data, callback, response_type
|
||||
).start()
|
||||
|
||||
def get_tournament_prize_strings(self, entry: dict[str, Any]) -> list[str]:
|
||||
"""Given a tournament entry, return strings for its prize levels."""
|
||||
from baclassic import _tournament
|
||||
|
||||
return _tournament.get_tournament_prize_strings(entry)
|
||||
|
||||
def getcampaign(self, name: str) -> bascenev1.Campaign:
|
||||
"""Return a campaign by name."""
|
||||
return self.campaigns[name]
|
||||
|
||||
def get_next_tip(self) -> str:
|
||||
"""Returns the next tip to be displayed."""
|
||||
if not self.tips:
|
||||
for tip in get_all_tips():
|
||||
self.tips.insert(random.randint(0, len(self.tips)), tip)
|
||||
tip = self.tips.pop()
|
||||
return tip
|
||||
|
||||
def run_gpu_benchmark(self) -> None:
|
||||
"""Kick off a benchmark to test gpu speeds."""
|
||||
from baclassic._benchmark import run_gpu_benchmark as run
|
||||
|
||||
run()
|
||||
|
||||
def run_cpu_benchmark(self) -> None:
|
||||
"""Kick off a benchmark to test cpu speeds."""
|
||||
from baclassic._benchmark import run_cpu_benchmark as run
|
||||
|
||||
run()
|
||||
|
||||
def run_media_reload_benchmark(self) -> None:
|
||||
"""Kick off a benchmark to test media reloading speeds."""
|
||||
from baclassic._benchmark import run_media_reload_benchmark as run
|
||||
|
||||
run()
|
||||
|
||||
def run_stress_test(
|
||||
self,
|
||||
playlist_type: str = 'Random',
|
||||
playlist_name: str = '__default__',
|
||||
player_count: int = 8,
|
||||
round_duration: int = 30,
|
||||
) -> None:
|
||||
"""Run a stress test."""
|
||||
from baclassic._benchmark import run_stress_test as run
|
||||
|
||||
run(playlist_type, playlist_name, player_count, round_duration)
|
||||
|
||||
def get_input_device_mapped_value(
|
||||
self, device: bascenev1.InputDevice, name: str
|
||||
) -> Any:
|
||||
"""Returns a mapped value for an input device.
|
||||
|
||||
This checks the user config and falls back to default values
|
||||
where available.
|
||||
"""
|
||||
return _input.get_input_device_mapped_value(
|
||||
device.name, device.unique_identifier, name
|
||||
)
|
||||
|
||||
def get_input_device_map_hash(
|
||||
self, inputdevice: bascenev1.InputDevice
|
||||
) -> str:
|
||||
"""Given an input device, return hash based on its raw input values."""
|
||||
del inputdevice # unused currently
|
||||
return _input.get_input_device_map_hash()
|
||||
|
||||
def get_input_device_config(
|
||||
self, inputdevice: bascenev1.InputDevice, default: bool
|
||||
) -> tuple[dict, str]:
|
||||
"""Given an input device, return its config dict in the app config.
|
||||
|
||||
The dict will be created if it does not exist.
|
||||
"""
|
||||
return _input.get_input_device_config(
|
||||
inputdevice.name, inputdevice.unique_identifier, default
|
||||
)
|
||||
|
||||
def get_player_colors(self) -> list[tuple[float, float, float]]:
|
||||
"""Return user-selectable player colors."""
|
||||
return bascenev1.get_player_colors()
|
||||
|
||||
def get_player_profile_icon(self, profilename: str) -> str:
|
||||
"""Given a profile name, returns an icon string for it.
|
||||
|
||||
(non-account profiles only)
|
||||
"""
|
||||
return bascenev1.get_player_profile_icon(profilename)
|
||||
|
||||
def get_player_profile_colors(
|
||||
self,
|
||||
profilename: str | None,
|
||||
profiles: dict[str, dict[str, Any]] | None = None,
|
||||
) -> tuple[tuple[float, float, float], tuple[float, float, float]]:
|
||||
"""Given a profile, return colors for them."""
|
||||
return bascenev1.get_player_profile_colors(profilename, profiles)
|
||||
|
||||
def get_foreground_host_session(self) -> bascenev1.Session | None:
|
||||
"""(internal)"""
|
||||
return bascenev1.get_foreground_host_session()
|
||||
|
||||
def get_foreground_host_activity(self) -> bascenev1.Activity | None:
|
||||
"""(internal)"""
|
||||
return bascenev1.get_foreground_host_activity()
|
||||
|
||||
def show_config_error_window(self) -> bool:
|
||||
"""(internal)"""
|
||||
if self.platform in ('mac', 'linux', 'windows'):
|
||||
from bauiv1lib.configerror import ConfigErrorWindow
|
||||
|
||||
babase.pushcall(ConfigErrorWindow)
|
||||
return True
|
||||
return False
|
||||
|
||||
def value_test(
|
||||
self,
|
||||
arg: str,
|
||||
change: float | None = None,
|
||||
absolute: float | None = None,
|
||||
) -> float:
|
||||
"""(internal)"""
|
||||
return _baclassic.value_test(arg, change, absolute)
|
||||
|
||||
def set_master_server_source(self, source: int) -> None:
|
||||
"""(internal)"""
|
||||
bascenev1.set_master_server_source(source)
|
||||
|
||||
def get_game_port(self) -> int:
|
||||
"""(internal)"""
|
||||
return bascenev1.get_game_port()
|
||||
|
||||
def v2_upgrade_window(self, login_name: str, code: str) -> None:
|
||||
"""(internal)"""
|
||||
|
||||
from bauiv1lib.v2upgrade import V2UpgradeWindow
|
||||
|
||||
V2UpgradeWindow(login_name, code)
|
||||
|
||||
def account_link_code_window(self, data: dict[str, Any]) -> None:
|
||||
"""(internal)"""
|
||||
from bauiv1lib.account.link import AccountLinkCodeWindow
|
||||
|
||||
AccountLinkCodeWindow(data)
|
||||
|
||||
def server_dialog(self, delay: float, data: dict[str, Any]) -> None:
|
||||
"""(internal)"""
|
||||
from bauiv1lib.serverdialog import (
|
||||
ServerDialogData,
|
||||
ServerDialogWindow,
|
||||
)
|
||||
|
||||
try:
|
||||
sddata = dataclass_from_dict(ServerDialogData, data)
|
||||
except Exception:
|
||||
sddata = None
|
||||
logging.warning(
|
||||
'Got malformatted ServerDialogData: %s',
|
||||
data,
|
||||
)
|
||||
if sddata is not None:
|
||||
babase.apptimer(
|
||||
delay,
|
||||
babase.Call(ServerDialogWindow, sddata),
|
||||
)
|
||||
|
||||
def ticket_icon_press(self) -> None:
|
||||
"""(internal)"""
|
||||
from bauiv1lib.resourcetypeinfo import ResourceTypeInfoWindow
|
||||
|
||||
ResourceTypeInfoWindow(
|
||||
origin_widget=bauiv1.get_special_widget('tickets_info_button')
|
||||
)
|
||||
|
||||
def show_url_window(self, address: str) -> None:
|
||||
"""(internal)"""
|
||||
from bauiv1lib.url import ShowURLWindow
|
||||
|
||||
ShowURLWindow(address)
|
||||
|
||||
def quit_window(self) -> None:
|
||||
"""(internal)"""
|
||||
from bauiv1lib.confirm import QuitWindow
|
||||
|
||||
QuitWindow()
|
||||
|
||||
def tournament_entry_window(
|
||||
self,
|
||||
tournament_id: str,
|
||||
tournament_activity: bascenev1.Activity | None = None,
|
||||
position: tuple[float, float] = (0.0, 0.0),
|
||||
delegate: Any = None,
|
||||
scale: float | None = None,
|
||||
offset: tuple[float, float] = (0.0, 0.0),
|
||||
on_close_call: Callable[[], Any] | None = None,
|
||||
) -> None:
|
||||
"""(internal)"""
|
||||
from bauiv1lib.tournamententry import TournamentEntryWindow
|
||||
|
||||
TournamentEntryWindow(
|
||||
tournament_id,
|
||||
tournament_activity,
|
||||
position,
|
||||
delegate,
|
||||
scale,
|
||||
offset,
|
||||
on_close_call,
|
||||
)
|
||||
|
||||
def get_main_menu_session(self) -> type[bascenev1.Session]:
|
||||
"""(internal)"""
|
||||
from bascenev1lib.mainmenu import MainMenuSession
|
||||
|
||||
return MainMenuSession
|
||||
|
||||
def continues_window(
|
||||
self,
|
||||
activity: bascenev1.Activity,
|
||||
cost: int,
|
||||
continue_call: Callable[[], Any],
|
||||
cancel_call: Callable[[], Any],
|
||||
) -> None:
|
||||
"""(internal)"""
|
||||
from bauiv1lib.continues import ContinuesWindow
|
||||
|
||||
ContinuesWindow(activity, cost, continue_call, cancel_call)
|
||||
|
||||
def profile_browser_window(
|
||||
self,
|
||||
transition: str = 'in_right',
|
||||
in_main_menu: bool = True,
|
||||
selected_profile: str | None = None,
|
||||
origin_widget: bauiv1.Widget | None = None,
|
||||
) -> None:
|
||||
"""(internal)"""
|
||||
from bauiv1lib.profile.browser import ProfileBrowserWindow
|
||||
|
||||
ProfileBrowserWindow(
|
||||
transition, in_main_menu, selected_profile, origin_widget
|
||||
)
|
||||
|
||||
def preload_map_preview_media(self) -> None:
|
||||
"""Preload media needed for map preview UIs.
|
||||
|
||||
Category: **Asset Functions**
|
||||
"""
|
||||
try:
|
||||
bauiv1.getmesh('level_select_button_opaque')
|
||||
bauiv1.getmesh('level_select_button_transparent')
|
||||
for maptype in list(self.maps.values()):
|
||||
map_tex_name = maptype.get_preview_texture_name()
|
||||
if map_tex_name is not None:
|
||||
bauiv1.gettexture(map_tex_name)
|
||||
except Exception:
|
||||
logging.exception('Error preloading map preview media.')
|
||||
|
||||
def party_icon_activate(self, origin: Sequence[float]) -> None:
|
||||
"""(internal)"""
|
||||
from bauiv1lib.party import PartyWindow
|
||||
from babase import app
|
||||
|
||||
assert not app.headless_mode
|
||||
|
||||
bauiv1.getsound('swish').play()
|
||||
|
||||
# If it exists, dismiss it; otherwise make a new one.
|
||||
party_window = (
|
||||
None if self.party_window is None else self.party_window()
|
||||
)
|
||||
if party_window is not None:
|
||||
party_window.close()
|
||||
else:
|
||||
self.party_window = weakref.ref(PartyWindow(origin=origin))
|
||||
|
||||
def device_menu_press(self, device_id: int | None) -> None:
|
||||
"""(internal)"""
|
||||
from bauiv1lib.mainmenu import MainMenuWindow
|
||||
from bauiv1 import set_ui_input_device
|
||||
|
||||
assert babase.app is not None
|
||||
in_main_menu = babase.app.ui_v1.has_main_menu_window()
|
||||
if not in_main_menu:
|
||||
set_ui_input_device(device_id)
|
||||
|
||||
if not babase.app.headless_mode:
|
||||
bauiv1.getsound('swish').play()
|
||||
|
||||
babase.app.ui_v1.set_main_menu_window(
|
||||
MainMenuWindow().get_root_widget()
|
||||
)
|
||||
|
|
@ -1,29 +1,18 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Functionality related to game tips.
|
||||
"""Functionality related to classic game tips.
|
||||
|
||||
These can be shown at opportune times such as between rounds."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
import random
|
||||
|
||||
import _ba
|
||||
import babase
|
||||
|
||||
if TYPE_CHECKING:
|
||||
pass
|
||||
|
||||
|
||||
def get_next_tip() -> str:
|
||||
"""Returns the next tip to be displayed."""
|
||||
app = _ba.app
|
||||
if not app.tips:
|
||||
for tip in get_all_tips():
|
||||
app.tips.insert(random.randint(0, len(app.tips)), tip)
|
||||
tip = app.tips.pop()
|
||||
return tip
|
||||
|
||||
|
||||
def get_all_tips() -> list[str]:
|
||||
"""Return the complete list of tips."""
|
||||
tips = [
|
||||
|
|
@ -116,14 +105,15 @@ def get_all_tips() -> list[str]:
|
|||
'color of sparks from its fuse: yellow..orange..red..BOOM.'
|
||||
),
|
||||
]
|
||||
app = _ba.app
|
||||
app = babase.app
|
||||
if not app.iircade_mode:
|
||||
tips += [
|
||||
'If your framerate is choppy, try turning down resolution\nor '
|
||||
'visuals in the game\'s graphics settings.'
|
||||
]
|
||||
if (
|
||||
app.platform in ('android', 'ios')
|
||||
app.classic is not None
|
||||
and app.classic.platform in ('android', 'ios')
|
||||
and not app.on_tv
|
||||
and not app.iircade_mode
|
||||
):
|
||||
|
|
@ -134,7 +124,11 @@ def get_all_tips() -> list[str]:
|
|||
'in Settings->Graphics'
|
||||
),
|
||||
]
|
||||
if app.platform in ['mac', 'android'] and not app.iircade_mode:
|
||||
if (
|
||||
app.classic is not None
|
||||
and app.classic.platform in ['mac', 'android']
|
||||
and not app.iircade_mode
|
||||
):
|
||||
tips += [
|
||||
'Tired of the soundtrack? Replace it with your own!'
|
||||
'\nSee Settings->Audio->Soundtrack'
|
||||
|
|
@ -142,7 +136,11 @@ def get_all_tips() -> list[str]:
|
|||
|
||||
# Hot-plugging is currently only on some platforms.
|
||||
# FIXME: Should add a platform entry for this so don't forget to update it.
|
||||
if app.platform in ['mac', 'android', 'windows'] and not app.iircade_mode:
|
||||
if (
|
||||
app.classic is not None
|
||||
and app.classic.platform in ['mac', 'android', 'windows']
|
||||
and not app.iircade_mode
|
||||
):
|
||||
tips += [
|
||||
'Players can join and leave in the middle of most games,\n'
|
||||
'and you can also plug and unplug controllers on the fly.',
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Functionality related to tournament play."""
|
||||
"""Functionality related to classic tournament play."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import _ba
|
||||
import babase
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any
|
||||
|
|
@ -15,8 +15,7 @@ if TYPE_CHECKING:
|
|||
def get_tournament_prize_strings(entry: dict[str, Any]) -> list[str]:
|
||||
"""Given a tournament entry, return strings for its prize levels."""
|
||||
# pylint: disable=too-many-locals
|
||||
from ba._generated.enums import SpecialChar
|
||||
from ba._gameutils import get_trophy_string
|
||||
from bascenev1 import get_trophy_string
|
||||
|
||||
range1 = entry.get('prizeRange1')
|
||||
range2 = entry.get('prizeRange2')
|
||||
|
|
@ -47,7 +46,11 @@ def get_tournament_prize_strings(entry: dict[str, Any]) -> list[str]:
|
|||
# If we've got trophies but not for this entry, throw some space
|
||||
# in to compensate so the ticket counts line up.
|
||||
if prize is not None:
|
||||
pvval = _ba.charstr(SpecialChar.TICKET_BACKING) + str(prize) + pvval
|
||||
pvval = (
|
||||
babase.charstr(babase.SpecialChar.TICKET_BACKING)
|
||||
+ str(prize)
|
||||
+ pvval
|
||||
)
|
||||
out_vals.append(prval)
|
||||
out_vals.append(pvval)
|
||||
return out_vals
|
||||
|
|
@ -3,12 +3,14 @@
|
|||
"""Music playback functionality using the Mac Music (formerly iTunes) app."""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import threading
|
||||
from collections import deque
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import _ba
|
||||
from ba._music import MusicPlayer
|
||||
import babase
|
||||
|
||||
from baclassic._music import MusicPlayer
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Callable, Any
|
||||
|
|
@ -32,7 +34,7 @@ class MacMusicAppMusicPlayer(MusicPlayer):
|
|||
selection_target_name: str,
|
||||
) -> Any:
|
||||
# pylint: disable=cyclic-import
|
||||
from bastd.ui.soundtrack import entrytypeselect as etsel
|
||||
from bauiv1lib.soundtrack import entrytypeselect as etsel
|
||||
|
||||
return etsel.SoundtrackEntryTypeSelectWindow(
|
||||
callback, current_entry, selection_target_name
|
||||
|
|
@ -46,7 +48,8 @@ class MacMusicAppMusicPlayer(MusicPlayer):
|
|||
self._thread.get_playlists(callback)
|
||||
|
||||
def on_play(self, entry: Any) -> None:
|
||||
music = _ba.app.music
|
||||
assert babase.app.classic is not None
|
||||
music = babase.app.classic.music
|
||||
entry_type = music.get_soundtrack_entry_type(entry)
|
||||
if entry_type == 'iTunesPlaylist':
|
||||
self._thread.play_playlist(music.get_soundtrack_entry_name(entry))
|
||||
|
|
@ -76,32 +79,27 @@ class _MacMusicAppThread(threading.Thread):
|
|||
|
||||
def run(self) -> None:
|
||||
"""Run the Music.app thread."""
|
||||
from ba._general import Call
|
||||
from ba._language import Lstr
|
||||
from ba._generated.enums import TimeType
|
||||
|
||||
_ba.set_thread_name('BA_MacMusicAppThread')
|
||||
_ba.mac_music_app_init()
|
||||
babase.set_thread_name('BA_MacMusicAppThread')
|
||||
babase.mac_music_app_init()
|
||||
|
||||
# Let's mention to the user we're launching Music.app in case
|
||||
# it causes any funny business (this used to background the app
|
||||
# sometimes, though I think that is fixed now)
|
||||
def do_print() -> None:
|
||||
_ba.timer(
|
||||
babase.apptimer(
|
||||
1.0,
|
||||
Call(
|
||||
_ba.screenmessage,
|
||||
Lstr(resource='usingItunesText'),
|
||||
babase.Call(
|
||||
babase.screenmessage,
|
||||
babase.Lstr(resource='usingItunesText'),
|
||||
(0, 1, 0),
|
||||
),
|
||||
timetype=TimeType.REAL,
|
||||
)
|
||||
|
||||
_ba.pushcall(do_print, from_other_thread=True)
|
||||
babase.pushcall(do_print, from_other_thread=True)
|
||||
|
||||
# Here we grab this to force the actual launch.
|
||||
_ba.mac_music_app_get_volume()
|
||||
_ba.mac_music_app_get_library_source()
|
||||
babase.mac_music_app_get_volume()
|
||||
babase.mac_music_app_get_library_source()
|
||||
done = False
|
||||
while not done:
|
||||
self._commands_available.wait()
|
||||
|
|
@ -137,16 +135,15 @@ class _MacMusicAppThread(threading.Thread):
|
|||
if old_volume > 0.0 and volume == 0.0:
|
||||
try:
|
||||
assert self._orig_volume is not None
|
||||
_ba.mac_music_app_stop()
|
||||
_ba.mac_music_app_set_volume(self._orig_volume)
|
||||
babase.mac_music_app_stop()
|
||||
babase.mac_music_app_set_volume(self._orig_volume)
|
||||
except Exception as exc:
|
||||
print('Error stopping iTunes music:', exc)
|
||||
elif self._volume > 0:
|
||||
|
||||
# If volume was zero, store pre-playing volume and start
|
||||
# playing.
|
||||
if old_volume == 0.0:
|
||||
self._orig_volume = _ba.mac_music_app_get_volume()
|
||||
self._orig_volume = babase.mac_music_app_get_volume()
|
||||
self._update_mac_music_app_volume()
|
||||
if old_volume == 0.0:
|
||||
self._play_current_playlist()
|
||||
|
|
@ -170,10 +167,8 @@ class _MacMusicAppThread(threading.Thread):
|
|||
def _handle_get_playlists_command(
|
||||
self, target: Callable[[list[str]], None]
|
||||
) -> None:
|
||||
from ba._general import Call
|
||||
|
||||
try:
|
||||
playlists = _ba.mac_music_app_get_playlists()
|
||||
playlists = babase.mac_music_app_get_playlists()
|
||||
playlists = [
|
||||
p
|
||||
for p in playlists
|
||||
|
|
@ -197,15 +192,15 @@ class _MacMusicAppThread(threading.Thread):
|
|||
except Exception as exc:
|
||||
print('Error getting iTunes playlists:', exc)
|
||||
playlists = []
|
||||
_ba.pushcall(Call(target, playlists), from_other_thread=True)
|
||||
babase.pushcall(babase.Call(target, playlists), from_other_thread=True)
|
||||
|
||||
def _handle_play_command(self, target: str | None) -> None:
|
||||
if target is None:
|
||||
if self._current_playlist is not None and self._volume > 0:
|
||||
try:
|
||||
assert self._orig_volume is not None
|
||||
_ba.mac_music_app_stop()
|
||||
_ba.mac_music_app_set_volume(self._orig_volume)
|
||||
babase.mac_music_app_stop()
|
||||
babase.mac_music_app_set_volume(self._orig_volume)
|
||||
except Exception as exc:
|
||||
print('Error stopping iTunes music:', exc)
|
||||
self._current_playlist = None
|
||||
|
|
@ -215,42 +210,39 @@ class _MacMusicAppThread(threading.Thread):
|
|||
if self._current_playlist is not None and self._volume > 0:
|
||||
try:
|
||||
assert self._orig_volume is not None
|
||||
_ba.mac_music_app_stop()
|
||||
_ba.mac_music_app_set_volume(self._orig_volume)
|
||||
babase.mac_music_app_stop()
|
||||
babase.mac_music_app_set_volume(self._orig_volume)
|
||||
except Exception as exc:
|
||||
print('Error stopping iTunes music:', exc)
|
||||
|
||||
# Set our playlist and play it if our volume is up.
|
||||
self._current_playlist = target
|
||||
if self._volume > 0:
|
||||
self._orig_volume = _ba.mac_music_app_get_volume()
|
||||
self._orig_volume = babase.mac_music_app_get_volume()
|
||||
self._update_mac_music_app_volume()
|
||||
self._play_current_playlist()
|
||||
|
||||
def _handle_die_command(self) -> None:
|
||||
|
||||
# Only stop if we've actually played something
|
||||
# (we don't want to kill music the user has playing).
|
||||
if self._current_playlist is not None and self._volume > 0:
|
||||
try:
|
||||
assert self._orig_volume is not None
|
||||
_ba.mac_music_app_stop()
|
||||
_ba.mac_music_app_set_volume(self._orig_volume)
|
||||
babase.mac_music_app_stop()
|
||||
babase.mac_music_app_set_volume(self._orig_volume)
|
||||
except Exception as exc:
|
||||
print('Error stopping iTunes music:', exc)
|
||||
|
||||
def _play_current_playlist(self) -> None:
|
||||
try:
|
||||
from ba._general import Call
|
||||
|
||||
assert self._current_playlist is not None
|
||||
if _ba.mac_music_app_play_playlist(self._current_playlist):
|
||||
if babase.mac_music_app_play_playlist(self._current_playlist):
|
||||
pass
|
||||
else:
|
||||
_ba.pushcall(
|
||||
Call(
|
||||
_ba.screenmessage,
|
||||
_ba.app.lang.get_resource('playlistNotFoundText')
|
||||
babase.pushcall(
|
||||
babase.Call(
|
||||
babase.screenmessage,
|
||||
babase.app.lang.get_resource('playlistNotFoundText')
|
||||
+ ': \''
|
||||
+ self._current_playlist
|
||||
+ '\'',
|
||||
|
|
@ -259,13 +251,11 @@ class _MacMusicAppThread(threading.Thread):
|
|||
from_other_thread=True,
|
||||
)
|
||||
except Exception:
|
||||
from ba import _error
|
||||
|
||||
_error.print_exception(
|
||||
f'error playing playlist {self._current_playlist}'
|
||||
logging.exception(
|
||||
"Error playing playlist '%s'.", self._current_playlist
|
||||
)
|
||||
|
||||
def _update_mac_music_app_volume(self) -> None:
|
||||
_ba.mac_music_app_set_volume(
|
||||
babase.mac_music_app_set_volume(
|
||||
max(0, min(100, int(100.0 * self._volume)))
|
||||
)
|
||||
|
|
@ -5,11 +5,13 @@ from __future__ import annotations
|
|||
|
||||
import os
|
||||
import random
|
||||
import logging
|
||||
import threading
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import _ba
|
||||
from ba._music import MusicPlayer
|
||||
import babase
|
||||
|
||||
from baclassic._music import MusicPlayer
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Callable, Any
|
||||
|
|
@ -38,7 +40,7 @@ class OSMusicPlayer(MusicPlayer):
|
|||
selection_target_name: str,
|
||||
) -> Any:
|
||||
# pylint: disable=cyclic-import
|
||||
from bastd.ui.soundtrack.entrytypeselect import (
|
||||
from bauiv1lib.soundtrack.entrytypeselect import (
|
||||
SoundtrackEntryTypeSelectWindow,
|
||||
)
|
||||
|
||||
|
|
@ -47,18 +49,18 @@ class OSMusicPlayer(MusicPlayer):
|
|||
)
|
||||
|
||||
def on_set_volume(self, volume: float) -> None:
|
||||
_ba.music_player_set_volume(volume)
|
||||
babase.music_player_set_volume(volume)
|
||||
|
||||
def on_play(self, entry: Any) -> None:
|
||||
music = _ba.app.music
|
||||
assert babase.app.classic is not None
|
||||
music = babase.app.classic.music
|
||||
entry_type = music.get_soundtrack_entry_type(entry)
|
||||
name = music.get_soundtrack_entry_name(entry)
|
||||
assert name is not None
|
||||
if entry_type == 'musicFile':
|
||||
self._want_to_play = self._actually_playing = True
|
||||
_ba.music_player_play(name)
|
||||
babase.music_player_play(name)
|
||||
elif entry_type == 'musicFolder':
|
||||
|
||||
# Launch a thread to scan this folder and give us a random
|
||||
# valid file within it.
|
||||
self._want_to_play = True
|
||||
|
|
@ -72,10 +74,8 @@ class OSMusicPlayer(MusicPlayer):
|
|||
def _on_play_folder_cb(
|
||||
self, result: str | list[str], error: str | None = None
|
||||
) -> None:
|
||||
from ba import _language
|
||||
|
||||
if error is not None:
|
||||
rstr = _language.Lstr(
|
||||
rstr = babase.Lstr(
|
||||
resource='internal.errorPlayingMusicText'
|
||||
).evaluate()
|
||||
if isinstance(result, str):
|
||||
|
|
@ -88,7 +88,7 @@ class OSMusicPlayer(MusicPlayer):
|
|||
err_str = (
|
||||
rstr.replace('${MUSIC}', '<multiple>') + '; ' + str(error)
|
||||
)
|
||||
_ba.screenmessage(err_str, color=(1, 0, 0))
|
||||
babase.screenmessage(err_str, color=(1, 0, 0))
|
||||
return
|
||||
|
||||
# There's a chance a stop could have been issued before our thread
|
||||
|
|
@ -97,15 +97,15 @@ class OSMusicPlayer(MusicPlayer):
|
|||
print('_on_play_folder_cb called with _want_to_play False')
|
||||
else:
|
||||
self._actually_playing = True
|
||||
_ba.music_player_play(result)
|
||||
babase.music_player_play(result)
|
||||
|
||||
def on_stop(self) -> None:
|
||||
self._want_to_play = False
|
||||
self._actually_playing = False
|
||||
_ba.music_player_stop()
|
||||
babase.music_player_stop()
|
||||
|
||||
def on_app_shutdown(self) -> None:
|
||||
_ba.music_player_shutdown()
|
||||
babase.music_player_shutdown()
|
||||
|
||||
|
||||
class _PickFolderSongThread(threading.Thread):
|
||||
|
|
@ -121,12 +121,9 @@ class _PickFolderSongThread(threading.Thread):
|
|||
self._path = path
|
||||
|
||||
def run(self) -> None:
|
||||
from ba import _language
|
||||
from ba._general import Call
|
||||
|
||||
do_print_error = True
|
||||
do_log_error = True
|
||||
try:
|
||||
_ba.set_thread_name('BA_PickFolderSongThread')
|
||||
babase.set_thread_name('BA_PickFolderSongThread')
|
||||
all_files: list[str] = []
|
||||
valid_extensions = ['.' + x for x in self._valid_extensions]
|
||||
for root, _subdirs, filenames in os.walk(self._path):
|
||||
|
|
@ -139,25 +136,24 @@ class _PickFolderSongThread(threading.Thread):
|
|||
root + '/' + fname,
|
||||
)
|
||||
if not all_files:
|
||||
do_print_error = False
|
||||
do_log_error = False
|
||||
raise RuntimeError(
|
||||
_language.Lstr(
|
||||
babase.Lstr(
|
||||
resource='internal.noMusicFilesInFolderText'
|
||||
).evaluate()
|
||||
)
|
||||
_ba.pushcall(
|
||||
Call(self._callback, all_files, None), from_other_thread=True
|
||||
babase.pushcall(
|
||||
babase.Call(self._callback, all_files, None),
|
||||
from_other_thread=True,
|
||||
)
|
||||
except Exception as exc:
|
||||
from ba import _error
|
||||
|
||||
if do_print_error:
|
||||
_error.print_exception()
|
||||
if do_log_error:
|
||||
logging.exception('Error in _PickFolderSongThread')
|
||||
try:
|
||||
err_str = str(exc)
|
||||
except Exception:
|
||||
err_str = '<ENCERR4523>'
|
||||
_ba.pushcall(
|
||||
Call(self._callback, self._path, err_str),
|
||||
babase.pushcall(
|
||||
babase.Call(self._callback, self._path, err_str),
|
||||
from_other_thread=True,
|
||||
)
|
||||
18
dist/ba_data/python/bacommon/err.py
vendored
18
dist/ba_data/python/bacommon/err.py
vendored
|
|
@ -1,18 +0,0 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Error related functionality."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
pass
|
||||
|
||||
|
||||
class RemoteError(Exception):
|
||||
"""An error occurred on the other end of some connection."""
|
||||
|
||||
def __str__(self) -> str:
|
||||
s = ''.join(str(arg) for arg in self.args)
|
||||
return f'Remote Exception Follows:\n{s}'
|
||||
2
dist/ba_data/python/bacommon/transfer.py
vendored
2
dist/ba_data/python/bacommon/transfer.py
vendored
|
|
@ -59,7 +59,7 @@ class DirectoryManifest:
|
|||
sha = hashlib.sha256()
|
||||
fullfilepath = os.path.join(pathstr, filepath)
|
||||
if not os.path.isfile(fullfilepath):
|
||||
raise Exception(f'File not found: "{fullfilepath}"')
|
||||
raise RuntimeError(f'File not found: "{fullfilepath}".')
|
||||
with open(fullfilepath, 'rb') as infile:
|
||||
filebytes = infile.read()
|
||||
filesize = len(filebytes)
|
||||
|
|
|
|||
528
dist/ba_data/python/baenv.py
vendored
Normal file
528
dist/ba_data/python/baenv.py
vendored
Normal file
|
|
@ -0,0 +1,528 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""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,
|
||||
logging, and app-dirs. Because these things are global in nature, this
|
||||
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.
|
||||
|
||||
Ballistica can be used without explicitly configuring the environment in
|
||||
order to integrate it in arbitrary Python environments, but this may
|
||||
cause some features to be disabled or behave differently than expected.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING
|
||||
import __main__
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any
|
||||
|
||||
from efro.log import LogHandler
|
||||
|
||||
# IMPORTANT - It is likely (and in some cases expected) that this
|
||||
# module's code will be exec'ed multiple times. This is because it is
|
||||
# the job of this module to set up Python paths for an engine run, and
|
||||
# that may involve modifying sys.path in such a way that this module
|
||||
# resolves to a different path afterwards (for example from
|
||||
# /abs/path/to/ba_data/scripts/babase.py to ba_data/scripts/babase.py).
|
||||
# This can result in the next import of baenv loading us from our 'new'
|
||||
# location, which may or may not actually be the same file on disk as
|
||||
# the last load. Either way, however, multiple execs will happen in some
|
||||
# form.
|
||||
#
|
||||
# So we need to do a few things to handle that situation gracefully.
|
||||
#
|
||||
# - First, we need to store any mutable global state in the __main__
|
||||
# module; not in ourself. This way, alternate versions of ourself will
|
||||
# still know if we already ran configure/etc.
|
||||
#
|
||||
# - Second, we should avoid the use of isinstance and similar calls for
|
||||
# our types. An EnvConfig we create would technically be a different
|
||||
# type than that created by an alternate baenv.
|
||||
|
||||
# Build number and version of the ballistica binary we expect to be
|
||||
# using.
|
||||
TARGET_BALLISTICA_BUILD = 21212
|
||||
TARGET_BALLISTICA_VERSION = '1.7.26'
|
||||
|
||||
|
||||
@dataclass
|
||||
class EnvConfig:
|
||||
"""Final config values we provide to the engine."""
|
||||
|
||||
# Where app config/state data lives.
|
||||
config_dir: str
|
||||
|
||||
# Directory containing ba_data and any other platform-specific data.
|
||||
data_dir: str
|
||||
|
||||
# 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.
|
||||
standard_app_python_dir: str
|
||||
|
||||
# Where the app's bundled third party Python stuff lives.
|
||||
site_python_dir: str | None
|
||||
|
||||
# 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.
|
||||
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.
|
||||
log_handler: LogHandler | None
|
||||
|
||||
# Initial data from the ballisticakit-config.json file. This is
|
||||
# passed mostly as an optimization to avoid reading the same config
|
||||
# file twice, since config data is first needed in baenv and next in
|
||||
# the engine. It will be cleared after passing it to the app's
|
||||
# config management subsystem and should not be accessed by any
|
||||
# other code.
|
||||
initial_app_config: Any
|
||||
|
||||
|
||||
@dataclass
|
||||
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).
|
||||
"""
|
||||
|
||||
config: EnvConfig | None = None
|
||||
called_configure: bool = False
|
||||
paths_set_failed: bool = False
|
||||
modular_main_called: bool = False
|
||||
|
||||
@classmethod
|
||||
def get(cls) -> _EnvGlobals:
|
||||
"""Create/return our singleton."""
|
||||
name = '_baenv_globals'
|
||||
envglobals: _EnvGlobals | None = getattr(__main__, name, None)
|
||||
if envglobals is None:
|
||||
envglobals = _EnvGlobals()
|
||||
setattr(__main__, name, envglobals)
|
||||
return envglobals
|
||||
|
||||
|
||||
def did_paths_set_fail() -> bool:
|
||||
"""Did we try to set paths and fail?"""
|
||||
return _EnvGlobals.get().paths_set_failed
|
||||
|
||||
|
||||
def config_exists() -> bool:
|
||||
"""Has a config been created?"""
|
||||
|
||||
return _EnvGlobals.get().config is not None
|
||||
|
||||
|
||||
def get_config() -> EnvConfig:
|
||||
"""Return the active config, creating a default if none exists."""
|
||||
envglobals = _EnvGlobals.get()
|
||||
|
||||
# If configure() has not been explicitly called, set up a
|
||||
# minimally-intrusive default config. We want Ballistica to default
|
||||
# to being a good citizen when imported into alien environments and
|
||||
# not blow away logging or otherwise muck with stuff. All official
|
||||
# paths to run Ballistica apps should be explicitly calling
|
||||
# configure() first to get a full featured setup.
|
||||
if not envglobals.called_configure:
|
||||
configure(setup_logging=False)
|
||||
|
||||
config = envglobals.config
|
||||
if config is None:
|
||||
raise RuntimeError(
|
||||
'baenv.configure() has been called but no config exists;'
|
||||
' perhaps it errored?'
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
def configure(
|
||||
config_dir: str | None = None,
|
||||
data_dir: str | None = None,
|
||||
user_python_dir: str | None = None,
|
||||
app_python_dir: str | None = None,
|
||||
site_python_dir: str | None = None,
|
||||
contains_python_dist: bool = False,
|
||||
setup_logging: bool = True,
|
||||
) -> None:
|
||||
"""Set up the environment for running a Ballistica app.
|
||||
|
||||
This includes things such as Python path wrangling and app directory
|
||||
creation. This must be called before any actual Ballistica modules
|
||||
are imported; the environment is locked in as soon as that happens.
|
||||
"""
|
||||
|
||||
envglobals = _EnvGlobals.get()
|
||||
|
||||
# Keep track of whether we've been *called*, not whether a config
|
||||
# has been created. Otherwise its possible to get multiple
|
||||
# overlapping configure calls going.
|
||||
if envglobals.called_configure:
|
||||
raise RuntimeError(
|
||||
'baenv.configure() has already been called;'
|
||||
' it can only be called once.'
|
||||
)
|
||||
envglobals.called_configure = True
|
||||
|
||||
# The very first thing we do is setup Python paths (while also
|
||||
# calculating some engine paths). This code needs to be bulletproof
|
||||
# since we have no logging yet at this point. We used to set up
|
||||
# logging first, but this way logging stuff will get loaded from its
|
||||
# proper final path (otherwise we might wind up using two different
|
||||
# versions of efro.logging in a single engine run).
|
||||
(
|
||||
user_python_dir,
|
||||
app_python_dir,
|
||||
site_python_dir,
|
||||
data_dir,
|
||||
config_dir,
|
||||
standard_app_python_dir,
|
||||
is_user_app_python_dir,
|
||||
) = _setup_paths(
|
||||
user_python_dir,
|
||||
app_python_dir,
|
||||
site_python_dir,
|
||||
data_dir,
|
||||
config_dir,
|
||||
)
|
||||
|
||||
# The second thing we do is set up our logging system and pipe
|
||||
# Python's stdout/stderr into it. At this point we can at least
|
||||
# debug problems on systems where native stdout/stderr is not easily
|
||||
# accessible such as Android.
|
||||
log_handler = _setup_logging() if setup_logging else None
|
||||
|
||||
# We want to always be run in UTF-8 mode; complain if we're not.
|
||||
if sys.flags.utf8_mode != 1:
|
||||
logging.warning(
|
||||
"Python's UTF-8 mode is not set. Running Ballistica without"
|
||||
' it may lead to errors.'
|
||||
)
|
||||
|
||||
# Attempt to create dirs that we'll write stuff to.
|
||||
_setup_dirs(config_dir, user_python_dir)
|
||||
|
||||
# Get ssl working if needed so we can use https and all that.
|
||||
_setup_certs(contains_python_dist)
|
||||
|
||||
# This is now the active config.
|
||||
envglobals.config = EnvConfig(
|
||||
config_dir=config_dir,
|
||||
data_dir=data_dir,
|
||||
user_python_dir=user_python_dir,
|
||||
app_python_dir=app_python_dir,
|
||||
standard_app_python_dir=standard_app_python_dir,
|
||||
site_python_dir=site_python_dir,
|
||||
log_handler=log_handler,
|
||||
is_user_app_python_dir=is_user_app_python_dir,
|
||||
initial_app_config=None,
|
||||
)
|
||||
|
||||
|
||||
def _calc_data_dir(data_dir: str | None) -> str:
|
||||
if data_dir is None:
|
||||
# To calc default data_dir, we assume this module was imported
|
||||
# from that dir's ba_data/python subdir.
|
||||
assert Path(__file__).parts[-3:-1] == ('ba_data', 'python')
|
||||
data_dir_path = Path(__file__).parents[2]
|
||||
|
||||
# Prefer tidy relative paths like './ba_data' if possible so
|
||||
# that things like stack traces are easier to read. For best
|
||||
# results, platforms where CWD doesn't matter can chdir to where
|
||||
# ba_data lives before calling configure().
|
||||
#
|
||||
# NOTE: If there's ever a case where the user is chdir'ing at
|
||||
# runtime we might want an option to use only abs paths here.
|
||||
cwd_path = Path.cwd()
|
||||
data_dir = str(
|
||||
data_dir_path.relative_to(cwd_path)
|
||||
if data_dir_path.is_relative_to(cwd_path)
|
||||
else data_dir_path
|
||||
)
|
||||
return data_dir
|
||||
|
||||
|
||||
def _setup_logging() -> LogHandler:
|
||||
from efro.log import setup_logging, LogLevel
|
||||
|
||||
log_handler = setup_logging(
|
||||
log_path=None,
|
||||
level=LogLevel.DEBUG,
|
||||
suppress_non_root_debug=True,
|
||||
log_stdout_stderr=True,
|
||||
cache_size_limit=1024 * 1024,
|
||||
)
|
||||
return log_handler
|
||||
|
||||
|
||||
def _setup_certs(contains_python_dist: bool) -> None:
|
||||
# In situations where we're bringing our own Python, let's also
|
||||
# provide our own root certs so ssl works. We can consider
|
||||
# overriding this in particular embedded cases if we can verify that
|
||||
# system certs are working. We also allow forcing this via an env
|
||||
# var if the user desires.
|
||||
if (
|
||||
contains_python_dist
|
||||
or os.environ.get('BA_USE_BUNDLED_ROOT_CERTS') == '1'
|
||||
):
|
||||
import certifi
|
||||
|
||||
# Let both OpenSSL and requests (if present) know to use this.
|
||||
os.environ['SSL_CERT_FILE'] = os.environ[
|
||||
'REQUESTS_CA_BUNDLE'
|
||||
] = certifi.where()
|
||||
|
||||
|
||||
def _setup_paths(
|
||||
user_python_dir: str | None,
|
||||
app_python_dir: str | None,
|
||||
site_python_dir: str | None,
|
||||
data_dir: str | None,
|
||||
config_dir: str | None,
|
||||
) -> tuple[str | None, str | None, str | None, str, str, str, bool]:
|
||||
# First a few paths we can ALWAYS calculate since they don't affect
|
||||
# Python imports:
|
||||
|
||||
envglobals = _EnvGlobals.get()
|
||||
|
||||
data_dir = _calc_data_dir(data_dir)
|
||||
|
||||
# Default config-dir is simply ~/.ballisticakit
|
||||
if config_dir is None:
|
||||
config_dir = str(Path(Path.home(), '.ballisticakit'))
|
||||
|
||||
# Standard app-python-dir is simply ba_data/python under data-dir.
|
||||
standard_app_python_dir = str(Path(data_dir, 'ba_data', 'python'))
|
||||
|
||||
# Whether the final app-dir we're returning is a custom user-owned one.
|
||||
is_user_app_python_dir = False
|
||||
|
||||
# If _babase has already been imported, there's not much we can do
|
||||
# at this point aside from complain and inform for next time.
|
||||
if '_babase' in sys.modules:
|
||||
app_python_dir = user_python_dir = site_python_dir = None
|
||||
|
||||
# We don't actually complain yet here; we simply take note that
|
||||
# we weren't able to set paths. Then we complain if/when the app
|
||||
# is started. This way, non-app uses of babase won't be filled
|
||||
# with unnecessary warnings.
|
||||
envglobals.paths_set_failed = True
|
||||
|
||||
else:
|
||||
# Ok; _babase hasn't been imported yet, so we can muck with
|
||||
# Python paths.
|
||||
|
||||
if app_python_dir is None:
|
||||
app_python_dir = standard_app_python_dir
|
||||
|
||||
# Likewise site-python-dir defaults to ba_data/python-site-packages.
|
||||
if site_python_dir is None:
|
||||
site_python_dir = str(
|
||||
Path(data_dir, 'ba_data', 'python-site-packages')
|
||||
)
|
||||
|
||||
# By default, user-python-dir is simply 'mods' under config-dir.
|
||||
if user_python_dir is None:
|
||||
user_python_dir = str(Path(config_dir, 'mods'))
|
||||
|
||||
# Wherever our user_python_dir is, if we find a sys/FOO dir
|
||||
# under it where FOO matches our version, use that as our
|
||||
# app_python_dir. This allows modding built-in stuff on
|
||||
# platforms where there is no write access to said built-in
|
||||
# stuff.
|
||||
check_dir = Path(user_python_dir, 'sys', TARGET_BALLISTICA_VERSION)
|
||||
if check_dir.is_dir():
|
||||
app_python_dir = str(check_dir)
|
||||
is_user_app_python_dir = True
|
||||
|
||||
# Ok, now apply these to sys.path.
|
||||
|
||||
# First off, strip out any instances of the path containing this
|
||||
# module. We will *probably* be re-adding the same path in a
|
||||
# moment so this keeps things cleaner. Though hmm should we
|
||||
# leave it in there in cases where we *don't* re-add the same
|
||||
# path?...
|
||||
our_parent_path = Path(__file__).parent.resolve()
|
||||
oldpaths: list[str] = [
|
||||
p for p in sys.path if Path(p).resolve() != our_parent_path
|
||||
]
|
||||
|
||||
# Let's place mods first (so users can override whatever they
|
||||
# want) followed by our app scripts and lastly our bundled site
|
||||
# stuff.
|
||||
|
||||
# One could make the argument that at least our bundled app &
|
||||
# site stuff should be placed at the end so actual local site
|
||||
# stuff could override it. That could be a good thing or a bad
|
||||
# thing. Maybe we could add an option for that, but for now I'm
|
||||
# prioritizing our stuff to give as consistent an environment as
|
||||
# possible.
|
||||
ourpaths = [user_python_dir, app_python_dir, site_python_dir]
|
||||
|
||||
# Special case: our modular builds will have a 'python-dylib'
|
||||
# dir alongside the 'python' scripts dir which contains our
|
||||
# binary Python modules. If we see that, add it to the path also.
|
||||
# Not sure if we'd ever have a need to customize this path.
|
||||
dylibdir = f'{app_python_dir}-dylib'
|
||||
if os.path.exists(dylibdir):
|
||||
ourpaths.append(dylibdir)
|
||||
|
||||
sys.path = ourpaths + oldpaths
|
||||
|
||||
return (
|
||||
user_python_dir,
|
||||
app_python_dir,
|
||||
site_python_dir,
|
||||
data_dir,
|
||||
config_dir,
|
||||
standard_app_python_dir,
|
||||
is_user_app_python_dir,
|
||||
)
|
||||
|
||||
|
||||
def _setup_dirs(config_dir: str | None, user_python_dir: str | None) -> None:
|
||||
create_dirs: list[tuple[str, str | None]] = [
|
||||
('config', config_dir),
|
||||
('user_python', user_python_dir),
|
||||
]
|
||||
for cdirname, cdir in create_dirs:
|
||||
if cdir is not None:
|
||||
try:
|
||||
os.makedirs(cdir, exist_ok=True)
|
||||
except Exception:
|
||||
# Not the end of the world if we can't make these dirs.
|
||||
logging.warning(
|
||||
"Unable to create %s dir at '%s'.", cdirname, cdir
|
||||
)
|
||||
|
||||
|
||||
def extract_arg(args: list[str], names: list[str], is_dir: bool) -> str | None:
|
||||
"""Given a list of args and an arg name, returns a value.
|
||||
|
||||
The arg flag and value are removed from the arg list. We also check
|
||||
to make sure the path exists.
|
||||
|
||||
raises CleanErrors on any problems.
|
||||
"""
|
||||
from efro.error import CleanError
|
||||
|
||||
count = sum(args.count(n) for n in names)
|
||||
if not count:
|
||||
return None
|
||||
|
||||
if count > 1:
|
||||
raise CleanError(f'Arg {names} passed multiple times.')
|
||||
|
||||
for name in names:
|
||||
if name not in args:
|
||||
continue
|
||||
argindex = args.index(name)
|
||||
if argindex + 1 >= len(args):
|
||||
raise CleanError(f'No value passed after {name} arg.')
|
||||
|
||||
val = args[argindex + 1]
|
||||
del args[argindex : argindex + 2]
|
||||
|
||||
if is_dir and not os.path.isdir(val):
|
||||
namepretty = names[0].removeprefix('--')
|
||||
raise CleanError(
|
||||
f"Provided {namepretty} path '{val}' is not a directory."
|
||||
)
|
||||
return val
|
||||
|
||||
raise RuntimeError(f'Expected arg name not found from {names}')
|
||||
|
||||
|
||||
def _modular_main() -> None:
|
||||
from efro.error import CleanError
|
||||
|
||||
# Fundamentally, running a Ballistica app consists of the following:
|
||||
# import baenv; baenv.configure(); import babase; babase.app.run()
|
||||
#
|
||||
# First baenv sets up things like Python paths the way the engine
|
||||
# needs them, and then we import and run the engine.
|
||||
#
|
||||
# Below we're doing a slightly fancier version of that. Namely we do
|
||||
# some processing of command line args to allow overriding of paths
|
||||
# or running explicit commands or whatever else. Our goal is that
|
||||
# this modular form of the app should be basically indistinguishable
|
||||
# from the monolithic form when used from the command line.
|
||||
|
||||
try:
|
||||
# Take note that we're running via modular-main. The native
|
||||
# layer can key off this to know whether it should apply
|
||||
# sys.argv or not.
|
||||
_EnvGlobals.get().modular_main_called = True
|
||||
|
||||
# Deal with a few key things here ourself before even running
|
||||
# configure.
|
||||
|
||||
# Extract stuff below modifies this so work with a copy.
|
||||
args = sys.argv.copy()
|
||||
|
||||
# NOTE: We need to keep these arg long/short arg versions synced
|
||||
# to those in core_config.cc. That code parses these same args
|
||||
# (even if it doesn't handle them in our case) and will complain
|
||||
# if unrecognized args come through.
|
||||
|
||||
# Our -c arg basically mirrors Python's -c arg. If we get that,
|
||||
# simply exec it and return; no engine stuff.
|
||||
command = extract_arg(args, ['--command', '-c'], is_dir=False)
|
||||
if command is not None:
|
||||
exec(command) # pylint: disable=exec-used
|
||||
return
|
||||
|
||||
config_dir = extract_arg(args, ['--config-dir', '-C'], is_dir=True)
|
||||
data_dir = extract_arg(args, ['--data-dir', '-d'], is_dir=True)
|
||||
mods_dir = extract_arg(args, ['--mods-dir', '-m'], is_dir=True)
|
||||
|
||||
# We run configure() BEFORE importing babase. (part of its job
|
||||
# is to wrangle paths to determine where babase and everything
|
||||
# else gets loaded from).
|
||||
configure(
|
||||
config_dir=config_dir,
|
||||
data_dir=data_dir,
|
||||
user_python_dir=mods_dir,
|
||||
)
|
||||
|
||||
import babase
|
||||
|
||||
# The engine will have parsed and processed all other args as
|
||||
# part of the above import. If there were errors or args such as
|
||||
# --help which should lead to us immediately returning, do so.
|
||||
code = babase.get_immediate_return_code()
|
||||
if code is not None:
|
||||
sys.exit(code)
|
||||
|
||||
# Aaaand we're off!
|
||||
babase.app.run()
|
||||
|
||||
# Code wanting us to die with a clean error message instead of an
|
||||
# ugly stack trace can raise one of these.
|
||||
except CleanError as clean_exc:
|
||||
clean_exc.pretty_print()
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
# Exec'ing this module directly will do a standard app run.
|
||||
if __name__ == '__main__':
|
||||
_modular_main()
|
||||
37
dist/ba_data/python/baplus/__init__.py
vendored
Normal file
37
dist/ba_data/python/baplus/__init__.py
vendored
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""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
|
||||
precompiled static libraries of this portion are provided for those who
|
||||
want to compile the rest of the engine, and a fully open-source engine
|
||||
can also be built by removing this 'plus' feature-set.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
# Note: there's not much here.
|
||||
# All comms with this feature-set should go through app.plus.
|
||||
|
||||
import logging
|
||||
|
||||
from baplus._subsystem import PlusSubsystem
|
||||
|
||||
__all__ = [
|
||||
'PlusSubsystem',
|
||||
]
|
||||
|
||||
# Sanity check: we want to keep ballistica's dependencies and
|
||||
# bootstrapping order clearly defined; let's check a few particular
|
||||
# modules to make sure they never directly or indirectly import us
|
||||
# before their own execs complete.
|
||||
if __debug__:
|
||||
for _mdl in 'babase', '_babase':
|
||||
if not hasattr(__import__(_mdl), '_REACHED_END_OF_MODULE'):
|
||||
logging.warning(
|
||||
'%s was imported before %s finished importing;'
|
||||
' should not happen.',
|
||||
__name__,
|
||||
_mdl,
|
||||
)
|
||||
15
dist/ba_data/python/baplus/_hooks.py
vendored
Normal file
15
dist/ba_data/python/baplus/_hooks.py
vendored
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Snippets of code for use by the c++ layer."""
|
||||
# (most of these are self-explanatory)
|
||||
# pylint: disable=missing-function-docstring
|
||||
from __future__ import annotations
|
||||
|
||||
import _baplus
|
||||
|
||||
|
||||
def submit_analytics_counts(sval: str) -> None:
|
||||
_baplus.add_v1_account_transaction(
|
||||
{'type': 'ANALYTICS_COUNTS', 'values': sval}
|
||||
)
|
||||
_baplus.run_v1_account_transactions()
|
||||
251
dist/ba_data/python/baplus/_subsystem.py
vendored
Normal file
251
dist/ba_data/python/baplus/_subsystem.py
vendored
Normal file
|
|
@ -0,0 +1,251 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Provides classic app subsystem."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import _baplus
|
||||
from babase import AppSubsystem
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Callable, Any
|
||||
|
||||
from babase import CloudSubsystem, AccountV2Subsystem
|
||||
|
||||
|
||||
class PlusSubsystem(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.
|
||||
"""
|
||||
|
||||
# pylint: disable=too-many-public-methods
|
||||
|
||||
# Note: this is basically just a wrapper around _baplus for
|
||||
# type-checking purposes. Maybe there's some smart way we could skip
|
||||
# the overhead of this wrapper at runtime.
|
||||
|
||||
accounts: AccountV2Subsystem
|
||||
cloud: CloudSubsystem
|
||||
|
||||
def on_app_loading(self) -> None:
|
||||
_baplus.on_app_loading()
|
||||
self.accounts.on_app_loading()
|
||||
|
||||
# noinspection PyUnresolvedReferences
|
||||
@staticmethod
|
||||
def add_v1_account_transaction(
|
||||
transaction: dict, callback: Callable | None = None
|
||||
) -> None:
|
||||
"""(internal)"""
|
||||
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.
|
||||
"""
|
||||
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 _baplus.get_master_server_address(source, version)
|
||||
|
||||
@staticmethod
|
||||
def get_news_show() -> str:
|
||||
"""(internal)"""
|
||||
return _baplus.get_news_show()
|
||||
|
||||
@staticmethod
|
||||
def get_price(item: str) -> str | None:
|
||||
"""(internal)"""
|
||||
return _baplus.get_price(item)
|
||||
|
||||
@staticmethod
|
||||
def get_purchased(item: str) -> bool:
|
||||
"""(internal)"""
|
||||
return _baplus.get_purchased(item)
|
||||
|
||||
@staticmethod
|
||||
def get_purchases_state() -> int:
|
||||
"""(internal)"""
|
||||
return _baplus.get_purchases_state()
|
||||
|
||||
@staticmethod
|
||||
def get_v1_account_display_string(full: bool = True) -> str:
|
||||
"""(internal)"""
|
||||
return _baplus.get_v1_account_display_string(full)
|
||||
|
||||
@staticmethod
|
||||
def get_v1_account_misc_read_val(name: str, default_value: Any) -> Any:
|
||||
"""(internal)"""
|
||||
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)"""
|
||||
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)"""
|
||||
return _baplus.get_v1_account_misc_val(name, default_value)
|
||||
|
||||
@staticmethod
|
||||
def get_v1_account_name() -> str:
|
||||
"""(internal)"""
|
||||
return _baplus.get_v1_account_name()
|
||||
|
||||
@staticmethod
|
||||
def get_v1_account_public_login_id() -> str | None:
|
||||
"""(internal)"""
|
||||
return _baplus.get_v1_account_public_login_id()
|
||||
|
||||
@staticmethod
|
||||
def get_v1_account_state() -> str:
|
||||
"""(internal)"""
|
||||
return _baplus.get_v1_account_state()
|
||||
|
||||
@staticmethod
|
||||
def get_v1_account_state_num() -> int:
|
||||
"""(internal)"""
|
||||
return _baplus.get_v1_account_state_num()
|
||||
|
||||
@staticmethod
|
||||
def get_v1_account_ticket_count() -> int:
|
||||
"""(internal)
|
||||
|
||||
Returns the number of tickets for the current account.
|
||||
"""
|
||||
return _baplus.get_v1_account_ticket_count()
|
||||
|
||||
@staticmethod
|
||||
def get_v1_account_type() -> str:
|
||||
"""(internal)"""
|
||||
return _baplus.get_v1_account_type()
|
||||
|
||||
@staticmethod
|
||||
def get_v2_fleet() -> str:
|
||||
"""(internal)"""
|
||||
return _baplus.get_v2_fleet()
|
||||
|
||||
@staticmethod
|
||||
def have_outstanding_v1_account_transactions() -> bool:
|
||||
"""(internal)"""
|
||||
return _baplus.have_outstanding_v1_account_transactions()
|
||||
|
||||
@staticmethod
|
||||
def in_game_purchase(item: str, price: int) -> None:
|
||||
"""(internal)"""
|
||||
return _baplus.in_game_purchase(item, price)
|
||||
|
||||
@staticmethod
|
||||
def is_blessed() -> bool:
|
||||
"""(internal)"""
|
||||
return _baplus.is_blessed()
|
||||
|
||||
@staticmethod
|
||||
def mark_config_dirty() -> None:
|
||||
"""(internal)
|
||||
|
||||
Category: General Utility Functions
|
||||
"""
|
||||
return _baplus.mark_config_dirty()
|
||||
|
||||
@staticmethod
|
||||
def power_ranking_query(callback: Callable, season: Any = None) -> None:
|
||||
"""(internal)"""
|
||||
return _baplus.power_ranking_query(callback, season)
|
||||
|
||||
@staticmethod
|
||||
def purchase(item: str) -> None:
|
||||
"""(internal)"""
|
||||
return _baplus.purchase(item)
|
||||
|
||||
@staticmethod
|
||||
def report_achievement(
|
||||
achievement: str, pass_to_account: bool = True
|
||||
) -> None:
|
||||
"""(internal)"""
|
||||
return _baplus.report_achievement(achievement, pass_to_account)
|
||||
|
||||
@staticmethod
|
||||
def reset_achievements() -> None:
|
||||
"""(internal)"""
|
||||
return _baplus.reset_achievements()
|
||||
|
||||
@staticmethod
|
||||
def restore_purchases() -> None:
|
||||
"""(internal)"""
|
||||
return _baplus.restore_purchases()
|
||||
|
||||
@staticmethod
|
||||
def run_v1_account_transactions() -> None:
|
||||
"""(internal)"""
|
||||
return _baplus.run_v1_account_transactions()
|
||||
|
||||
@staticmethod
|
||||
def sign_in_v1(account_type: str) -> None:
|
||||
"""(internal)
|
||||
|
||||
Category: General Utility Functions
|
||||
"""
|
||||
return _baplus.sign_in_v1(account_type)
|
||||
|
||||
@staticmethod
|
||||
def sign_out_v1(v2_embedded: bool = False) -> None:
|
||||
"""(internal)
|
||||
|
||||
Category: General Utility Functions
|
||||
"""
|
||||
return _baplus.sign_out_v1(v2_embedded)
|
||||
|
||||
@staticmethod
|
||||
def submit_score(
|
||||
game: str,
|
||||
config: str,
|
||||
name: Any,
|
||||
score: int | None,
|
||||
callback: Callable,
|
||||
order: str = 'increasing',
|
||||
tournament_id: str | None = None,
|
||||
score_type: str = 'points',
|
||||
campaign: str | None = None,
|
||||
level: str | None = None,
|
||||
) -> None:
|
||||
"""(internal)
|
||||
|
||||
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.
|
||||
"""
|
||||
return _baplus.submit_score(
|
||||
game,
|
||||
config,
|
||||
name,
|
||||
score,
|
||||
callback,
|
||||
order,
|
||||
tournament_id,
|
||||
score_type,
|
||||
campaign,
|
||||
level,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def tournament_query(
|
||||
callback: Callable[[dict | None], None], args: dict
|
||||
) -> None:
|
||||
"""(internal)"""
|
||||
return _baplus.tournament_query(callback, args)
|
||||
459
dist/ba_data/python/bascenev1/__init__.py
vendored
Normal file
459
dist/ba_data/python/bascenev1/__init__.py
vendored
Normal file
|
|
@ -0,0 +1,459 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Ballistica scene api version 1. Basically all gameplay related code."""
|
||||
|
||||
# ba_meta require api 8
|
||||
|
||||
# The stuff we expose here at the top level is our 'public' api for use
|
||||
# from other modules/packages. Code *within* this package should import
|
||||
# things from this package's submodules directly to reduce the chance of
|
||||
# dependency loops. The exception is TYPE_CHECKING blocks and
|
||||
# annotations since those aren't evaluated at runtime.
|
||||
|
||||
import logging
|
||||
|
||||
# Aside from our own stuff, we also bundle a number of things from ba or
|
||||
# other modules; the goal is to let most simple mods rely solely on this
|
||||
# module to keep things simple.
|
||||
|
||||
from efro.util import set_canonical_module_names
|
||||
from babase import (
|
||||
app,
|
||||
AppIntent,
|
||||
AppIntentDefault,
|
||||
AppIntentExec,
|
||||
AppMode,
|
||||
apptime,
|
||||
AppTime,
|
||||
apptimer,
|
||||
AppTimer,
|
||||
Call,
|
||||
ContextError,
|
||||
ContextRef,
|
||||
displaytime,
|
||||
DisplayTime,
|
||||
displaytimer,
|
||||
DisplayTimer,
|
||||
existing,
|
||||
fade_screen,
|
||||
get_remote_app_name,
|
||||
increment_analytics_count,
|
||||
InputType,
|
||||
is_point_in_box,
|
||||
lock_all_input,
|
||||
Lstr,
|
||||
NodeNotFoundError,
|
||||
normalized_color,
|
||||
NotFoundError,
|
||||
PlayerNotFoundError,
|
||||
Plugin,
|
||||
pushcall,
|
||||
safecolor,
|
||||
screenmessage,
|
||||
set_analytics_screen,
|
||||
storagename,
|
||||
timestring,
|
||||
UIScale,
|
||||
unlock_all_input,
|
||||
Vec3,
|
||||
WeakCall,
|
||||
)
|
||||
|
||||
from _bascenev1 import (
|
||||
ActivityData,
|
||||
basetime,
|
||||
basetimer,
|
||||
BaseTimer,
|
||||
camerashake,
|
||||
capture_gamepad_input,
|
||||
capture_keyboard_input,
|
||||
chatmessage,
|
||||
client_info_query_response,
|
||||
CollisionMesh,
|
||||
connect_to_party,
|
||||
Data,
|
||||
disconnect_client,
|
||||
disconnect_from_host,
|
||||
emitfx,
|
||||
end_host_scanning,
|
||||
get_chat_messages,
|
||||
get_connection_to_host_info,
|
||||
get_foreground_host_activity,
|
||||
get_foreground_host_session,
|
||||
get_game_port,
|
||||
get_game_roster,
|
||||
get_local_active_input_devices_count,
|
||||
get_public_party_enabled,
|
||||
get_public_party_max_size,
|
||||
get_random_names,
|
||||
get_replay_speed_exponent,
|
||||
get_ui_input_device,
|
||||
getactivity,
|
||||
getcollisionmesh,
|
||||
getdata,
|
||||
getinputdevice,
|
||||
getmesh,
|
||||
getnodes,
|
||||
getsession,
|
||||
getsound,
|
||||
gettexture,
|
||||
have_connected_clients,
|
||||
have_touchscreen_input,
|
||||
host_scan_cycle,
|
||||
InputDevice,
|
||||
is_in_replay,
|
||||
ls_input_devices,
|
||||
ls_objects,
|
||||
Material,
|
||||
Mesh,
|
||||
new_host_session,
|
||||
new_replay_session,
|
||||
newactivity,
|
||||
newnode,
|
||||
Node,
|
||||
printnodes,
|
||||
release_gamepad_input,
|
||||
release_keyboard_input,
|
||||
reset_random_player_names,
|
||||
broadcastmessage,
|
||||
SessionData,
|
||||
SessionPlayer,
|
||||
set_admins,
|
||||
set_authenticate_clients,
|
||||
set_debug_speed_exponent,
|
||||
set_enable_default_kick_voting,
|
||||
set_internal_music,
|
||||
set_map_bounds,
|
||||
set_master_server_source,
|
||||
set_public_party_enabled,
|
||||
set_public_party_max_size,
|
||||
set_public_party_name,
|
||||
set_public_party_queue_enabled,
|
||||
set_public_party_stats_url,
|
||||
set_replay_speed_exponent,
|
||||
set_touchscreen_editing,
|
||||
Sound,
|
||||
Texture,
|
||||
time,
|
||||
timer,
|
||||
Timer,
|
||||
)
|
||||
from bascenev1._activity import Activity
|
||||
from bascenev1._activitytypes import JoinActivity, ScoreScreenActivity
|
||||
from bascenev1._actor import Actor
|
||||
from bascenev1._appmode import SceneV1AppMode
|
||||
from bascenev1._campaign import init_campaigns, Campaign
|
||||
from bascenev1._collision import Collision, getcollision
|
||||
from bascenev1._coopgame import CoopGameActivity
|
||||
from bascenev1._coopsession import CoopSession
|
||||
from bascenev1._debug import print_live_object_warnings
|
||||
from bascenev1._dependency import (
|
||||
Dependency,
|
||||
DependencyComponent,
|
||||
DependencySet,
|
||||
AssetPackage,
|
||||
)
|
||||
from bascenev1._dualteamsession import DualTeamSession
|
||||
from bascenev1._freeforallsession import FreeForAllSession
|
||||
from bascenev1._gameactivity import GameActivity
|
||||
from bascenev1._gameresults import GameResults
|
||||
from bascenev1._gameutils import (
|
||||
animate,
|
||||
animate_array,
|
||||
BaseTime,
|
||||
cameraflash,
|
||||
GameTip,
|
||||
get_trophy_string,
|
||||
show_damage_count,
|
||||
Time,
|
||||
)
|
||||
from bascenev1._level import Level
|
||||
from bascenev1._lobby import Lobby, Chooser
|
||||
from bascenev1._map import (
|
||||
get_filtered_map_name,
|
||||
get_map_class,
|
||||
get_map_display_string,
|
||||
Map,
|
||||
register_map,
|
||||
)
|
||||
from bascenev1._messages import (
|
||||
CelebrateMessage,
|
||||
DeathType,
|
||||
DieMessage,
|
||||
DropMessage,
|
||||
DroppedMessage,
|
||||
FreezeMessage,
|
||||
HitMessage,
|
||||
ImpactDamageMessage,
|
||||
OutOfBoundsMessage,
|
||||
PickedUpMessage,
|
||||
PickUpMessage,
|
||||
PlayerDiedMessage,
|
||||
PlayerProfilesChangedMessage,
|
||||
ShouldShatterMessage,
|
||||
StandMessage,
|
||||
ThawMessage,
|
||||
UNHANDLED,
|
||||
)
|
||||
from bascenev1._multiteamsession import (
|
||||
MultiTeamSession,
|
||||
DEFAULT_TEAM_COLORS,
|
||||
DEFAULT_TEAM_NAMES,
|
||||
)
|
||||
from bascenev1._music import MusicType, setmusic
|
||||
from bascenev1._nodeactor import NodeActor
|
||||
from bascenev1._powerup import get_default_powerup_distribution
|
||||
from bascenev1._profile import (
|
||||
get_player_colors,
|
||||
get_player_profile_icon,
|
||||
get_player_profile_colors,
|
||||
)
|
||||
from bascenev1._player import PlayerInfo, Player, EmptyPlayer, StandLocation
|
||||
from bascenev1._playlist import (
|
||||
get_default_free_for_all_playlist,
|
||||
get_default_teams_playlist,
|
||||
filter_playlist,
|
||||
)
|
||||
from bascenev1._powerup import PowerupMessage, PowerupAcceptMessage
|
||||
from bascenev1._score import ScoreType, ScoreConfig
|
||||
from bascenev1._settings import (
|
||||
BoolSetting,
|
||||
ChoiceSetting,
|
||||
FloatChoiceSetting,
|
||||
FloatSetting,
|
||||
IntChoiceSetting,
|
||||
IntSetting,
|
||||
Setting,
|
||||
)
|
||||
from bascenev1._session import Session
|
||||
from bascenev1._stats import PlayerScoredMessage, PlayerRecord, Stats
|
||||
from bascenev1._team import SessionTeam, Team, EmptyTeam
|
||||
from bascenev1._teamgame import TeamGameActivity
|
||||
|
||||
__all__ = [
|
||||
'Activity',
|
||||
'ActivityData',
|
||||
'Actor',
|
||||
'animate',
|
||||
'animate_array',
|
||||
'app',
|
||||
'AppIntent',
|
||||
'AppIntentDefault',
|
||||
'AppIntentExec',
|
||||
'AppMode',
|
||||
'AppTime',
|
||||
'apptime',
|
||||
'apptimer',
|
||||
'AppTimer',
|
||||
'AssetPackage',
|
||||
'basetime',
|
||||
'BaseTime',
|
||||
'basetimer',
|
||||
'BaseTimer',
|
||||
'BoolSetting',
|
||||
'Call',
|
||||
'cameraflash',
|
||||
'camerashake',
|
||||
'Campaign',
|
||||
'capture_gamepad_input',
|
||||
'capture_keyboard_input',
|
||||
'CelebrateMessage',
|
||||
'chatmessage',
|
||||
'ChoiceSetting',
|
||||
'Chooser',
|
||||
'client_info_query_response',
|
||||
'Collision',
|
||||
'CollisionMesh',
|
||||
'connect_to_party',
|
||||
'ContextError',
|
||||
'ContextRef',
|
||||
'CoopGameActivity',
|
||||
'CoopSession',
|
||||
'Data',
|
||||
'DeathType',
|
||||
'DEFAULT_TEAM_COLORS',
|
||||
'DEFAULT_TEAM_NAMES',
|
||||
'Dependency',
|
||||
'DependencyComponent',
|
||||
'DependencySet',
|
||||
'DieMessage',
|
||||
'disconnect_client',
|
||||
'disconnect_from_host',
|
||||
'displaytime',
|
||||
'DisplayTime',
|
||||
'displaytimer',
|
||||
'DisplayTimer',
|
||||
'DropMessage',
|
||||
'DroppedMessage',
|
||||
'DualTeamSession',
|
||||
'emitfx',
|
||||
'EmptyPlayer',
|
||||
'EmptyTeam',
|
||||
'end_host_scanning',
|
||||
'existing',
|
||||
'fade_screen',
|
||||
'filter_playlist',
|
||||
'FloatChoiceSetting',
|
||||
'FloatSetting',
|
||||
'FreeForAllSession',
|
||||
'FreezeMessage',
|
||||
'GameActivity',
|
||||
'GameResults',
|
||||
'GameTip',
|
||||
'get_chat_messages',
|
||||
'get_connection_to_host_info',
|
||||
'get_default_free_for_all_playlist',
|
||||
'get_default_teams_playlist',
|
||||
'get_default_powerup_distribution',
|
||||
'get_filtered_map_name',
|
||||
'get_foreground_host_activity',
|
||||
'get_foreground_host_session',
|
||||
'get_game_port',
|
||||
'get_game_roster',
|
||||
'get_game_roster',
|
||||
'get_local_active_input_devices_count',
|
||||
'get_map_class',
|
||||
'get_map_display_string',
|
||||
'get_player_colors',
|
||||
'get_player_profile_colors',
|
||||
'get_player_profile_icon',
|
||||
'get_public_party_enabled',
|
||||
'get_public_party_max_size',
|
||||
'get_random_names',
|
||||
'get_remote_app_name',
|
||||
'get_replay_speed_exponent',
|
||||
'get_trophy_string',
|
||||
'get_ui_input_device',
|
||||
'getactivity',
|
||||
'getcollision',
|
||||
'getcollisionmesh',
|
||||
'getdata',
|
||||
'getinputdevice',
|
||||
'getmesh',
|
||||
'getnodes',
|
||||
'getsession',
|
||||
'getsound',
|
||||
'gettexture',
|
||||
'have_connected_clients',
|
||||
'have_touchscreen_input',
|
||||
'HitMessage',
|
||||
'host_scan_cycle',
|
||||
'ImpactDamageMessage',
|
||||
'increment_analytics_count',
|
||||
'init_campaigns',
|
||||
'InputDevice',
|
||||
'InputType',
|
||||
'IntChoiceSetting',
|
||||
'IntSetting',
|
||||
'is_in_replay',
|
||||
'is_point_in_box',
|
||||
'JoinActivity',
|
||||
'Level',
|
||||
'Lobby',
|
||||
'lock_all_input',
|
||||
'ls_input_devices',
|
||||
'ls_objects',
|
||||
'Lstr',
|
||||
'Map',
|
||||
'Material',
|
||||
'Mesh',
|
||||
'MultiTeamSession',
|
||||
'MusicType',
|
||||
'new_host_session',
|
||||
'new_replay_session',
|
||||
'newactivity',
|
||||
'newnode',
|
||||
'Node',
|
||||
'NodeActor',
|
||||
'NodeNotFoundError',
|
||||
'normalized_color',
|
||||
'NotFoundError',
|
||||
'OutOfBoundsMessage',
|
||||
'PickedUpMessage',
|
||||
'PickUpMessage',
|
||||
'Player',
|
||||
'PlayerDiedMessage',
|
||||
'PlayerProfilesChangedMessage',
|
||||
'PlayerInfo',
|
||||
'PlayerNotFoundError',
|
||||
'PlayerRecord',
|
||||
'PlayerScoredMessage',
|
||||
'Plugin',
|
||||
'PowerupAcceptMessage',
|
||||
'PowerupMessage',
|
||||
'print_live_object_warnings',
|
||||
'printnodes',
|
||||
'pushcall',
|
||||
'register_map',
|
||||
'release_gamepad_input',
|
||||
'release_keyboard_input',
|
||||
'reset_random_player_names',
|
||||
'safecolor',
|
||||
'screenmessage',
|
||||
'SceneV1AppMode',
|
||||
'ScoreConfig',
|
||||
'ScoreScreenActivity',
|
||||
'ScoreType',
|
||||
'broadcastmessage',
|
||||
'Session',
|
||||
'SessionData',
|
||||
'SessionPlayer',
|
||||
'SessionTeam',
|
||||
'set_admins',
|
||||
'set_analytics_screen',
|
||||
'set_authenticate_clients',
|
||||
'set_debug_speed_exponent',
|
||||
'set_debug_speed_exponent',
|
||||
'set_enable_default_kick_voting',
|
||||
'set_internal_music',
|
||||
'set_map_bounds',
|
||||
'set_master_server_source',
|
||||
'set_public_party_enabled',
|
||||
'set_public_party_max_size',
|
||||
'set_public_party_name',
|
||||
'set_public_party_queue_enabled',
|
||||
'set_public_party_stats_url',
|
||||
'set_replay_speed_exponent',
|
||||
'set_touchscreen_editing',
|
||||
'setmusic',
|
||||
'Setting',
|
||||
'ShouldShatterMessage',
|
||||
'show_damage_count',
|
||||
'Sound',
|
||||
'StandLocation',
|
||||
'StandMessage',
|
||||
'Stats',
|
||||
'storagename',
|
||||
'Team',
|
||||
'TeamGameActivity',
|
||||
'Texture',
|
||||
'ThawMessage',
|
||||
'time',
|
||||
'Time',
|
||||
'timer',
|
||||
'Timer',
|
||||
'timestring',
|
||||
'UIScale',
|
||||
'UNHANDLED',
|
||||
'unlock_all_input',
|
||||
'Vec3',
|
||||
'WeakCall',
|
||||
]
|
||||
|
||||
# We want stuff here to show up as bascenev1.Foo instead of
|
||||
# bascenev1._submodule.Foo.
|
||||
set_canonical_module_names(globals())
|
||||
|
||||
# Sanity check: we want to keep ballistica's dependencies and
|
||||
# bootstrapping order clearly defined; let's check a few particular
|
||||
# modules to make sure they never directly or indirectly import us
|
||||
# before their own execs complete.
|
||||
if __debug__:
|
||||
for _mdl in 'babase', '_babase':
|
||||
if not hasattr(__import__(_mdl), '_REACHED_END_OF_MODULE'):
|
||||
logging.warning(
|
||||
'%s was imported before %s finished importing;'
|
||||
' should not happen.',
|
||||
__name__,
|
||||
_mdl,
|
||||
)
|
||||
|
|
@ -4,39 +4,32 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import weakref
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Generic, TypeVar
|
||||
|
||||
import _ba
|
||||
from ba._team import Team
|
||||
from ba._player import Player
|
||||
from ba._error import (
|
||||
print_exception,
|
||||
SessionTeamNotFoundError,
|
||||
SessionPlayerNotFoundError,
|
||||
NodeNotFoundError,
|
||||
)
|
||||
from ba._dependency import DependencyComponent
|
||||
from ba._general import Call, verify_object_death
|
||||
from ba._messages import UNHANDLED
|
||||
import babase
|
||||
import _bascenev1
|
||||
from bascenev1._dependency import DependencyComponent
|
||||
from bascenev1._team import Team
|
||||
from bascenev1._messages import UNHANDLED
|
||||
from bascenev1._player import Player
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any
|
||||
import ba
|
||||
import bascenev1
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
PlayerType = TypeVar('PlayerType', bound=Player)
|
||||
TeamType = TypeVar('TeamType', bound=Team)
|
||||
# pylint: enable=invalid-name
|
||||
PlayerT = TypeVar('PlayerT', bound=Player)
|
||||
TeamT = TypeVar('TeamT', bound=Team)
|
||||
|
||||
|
||||
class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
|
||||
"""Units of execution wrangled by a ba.Session.
|
||||
class Activity(DependencyComponent, Generic[PlayerT, TeamT]):
|
||||
"""Units of execution wrangled by a bascenev1.Session.
|
||||
|
||||
Category: Gameplay Classes
|
||||
|
||||
Examples of Activities include games, score-screens, cutscenes, etc.
|
||||
A ba.Session has one 'current' Activity at any time, though their existence
|
||||
can overlap during transitions.
|
||||
A bascenev1.Session has one 'current' Activity at any time, though
|
||||
their existence can overlap during transitions.
|
||||
"""
|
||||
|
||||
# pylint: disable=too-many-public-methods
|
||||
|
|
@ -47,17 +40,17 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
|
|||
activities should pull all values they need from the 'settings' arg
|
||||
passed to the Activity __init__ call."""
|
||||
|
||||
teams: list[TeamType]
|
||||
"""The list of ba.Team-s in the Activity. This gets populated just
|
||||
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)."""
|
||||
|
||||
players: list[PlayerType]
|
||||
"""The list of ba.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."""
|
||||
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."""
|
||||
|
||||
announce_player_deaths = False
|
||||
"""Whether to print every time a player dies. This can be pertinent
|
||||
|
|
@ -125,11 +118,11 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
|
|||
the next activity?"""
|
||||
|
||||
def __init__(self, settings: dict):
|
||||
"""Creates an Activity in the current ba.Session.
|
||||
"""Creates an Activity in the current bascenev1.Session.
|
||||
|
||||
The activity will not be actually run until ba.Session.setactivity
|
||||
is called. 'settings' should be a dict of key/value pairs specific
|
||||
to the activity.
|
||||
The activity will not be actually run until
|
||||
bascenev1.Session.setactivity is called. 'settings' should be a
|
||||
dict of key/value pairs specific to the activity.
|
||||
|
||||
Activities should preload as much of their media/etc as possible in
|
||||
their constructor, but none of it should actually be used until they
|
||||
|
|
@ -138,23 +131,23 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
|
|||
super().__init__()
|
||||
|
||||
# Create our internal engine data.
|
||||
self._activity_data = _ba.register_activity(self)
|
||||
self._activity_data = _bascenev1.register_activity(self)
|
||||
|
||||
assert isinstance(settings, dict)
|
||||
assert _ba.getactivity() is self
|
||||
assert _bascenev1.getactivity() is self
|
||||
|
||||
self._globalsnode: ba.Node | None = None
|
||||
self._globalsnode: bascenev1.Node | None = None
|
||||
|
||||
# Player/Team types should have been specified as type args;
|
||||
# grab those.
|
||||
self._playertype: type[PlayerType]
|
||||
self._teamtype: type[TeamType]
|
||||
self._playertype: type[PlayerT]
|
||||
self._teamtype: type[TeamT]
|
||||
self._setup_player_and_team_types()
|
||||
|
||||
# FIXME: Relocate or remove the need for this stuff.
|
||||
self.paused_text: ba.Actor | None = None
|
||||
self.paused_text: bascenev1.Actor | None = None
|
||||
|
||||
self._session = weakref.ref(_ba.getsession())
|
||||
self._session = weakref.ref(_bascenev1.getsession())
|
||||
|
||||
# Preloaded data for actors, maps, etc; indexed by type.
|
||||
self.preloads: dict[type, Any] = {}
|
||||
|
|
@ -167,68 +160,71 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
|
|||
self._has_transitioned_in = False
|
||||
self._has_begun = False
|
||||
self._has_ended = False
|
||||
self._activity_death_check_timer: ba.Timer | None = None
|
||||
self._activity_death_check_timer: bascenev1.AppTimer | None = None
|
||||
self._expired = False
|
||||
self._delay_delete_players: list[PlayerType] = []
|
||||
self._delay_delete_teams: list[TeamType] = []
|
||||
self._players_that_left: list[weakref.ref[PlayerType]] = []
|
||||
self._teams_that_left: list[weakref.ref[TeamType]] = []
|
||||
self._delay_delete_players: list[PlayerT] = []
|
||||
self._delay_delete_teams: list[TeamT] = []
|
||||
self._players_that_left: list[weakref.ref[PlayerT]] = []
|
||||
self._teams_that_left: list[weakref.ref[TeamT]] = []
|
||||
self._transitioning_out = False
|
||||
|
||||
# A handy place to put most actors; this list is pruned of dead
|
||||
# actors regularly and these actors are insta-killed as the activity
|
||||
# is dying.
|
||||
self._actor_refs: list[ba.Actor] = []
|
||||
self._actor_weak_refs: list[weakref.ref[ba.Actor]] = []
|
||||
self._last_prune_dead_actors_time = _ba.time()
|
||||
self._prune_dead_actors_timer: ba.Timer | None = None
|
||||
self._actor_refs: list[bascenev1.Actor] = []
|
||||
self._actor_weak_refs: list[weakref.ref[bascenev1.Actor]] = []
|
||||
self._last_prune_dead_actors_time = babase.apptime()
|
||||
self._prune_dead_actors_timer: bascenev1.Timer | None = None
|
||||
|
||||
self.teams = []
|
||||
self.players = []
|
||||
|
||||
self.lobby = None
|
||||
self._stats: ba.Stats | None = None
|
||||
self._stats: bascenev1.Stats | None = None
|
||||
self._customdata: dict | None = {}
|
||||
|
||||
def __del__(self) -> None:
|
||||
|
||||
# If the activity has been run then we should have already cleaned
|
||||
# it up, but we still need to run expire calls for un-run activities.
|
||||
if not self._expired:
|
||||
with _ba.Context('empty'):
|
||||
with babase.ContextRef.empty():
|
||||
self._expire()
|
||||
|
||||
# Inform our owner that we officially kicked the bucket.
|
||||
if self._transitioning_out:
|
||||
session = self._session()
|
||||
if session is not None:
|
||||
_ba.pushcall(
|
||||
Call(
|
||||
babase.pushcall(
|
||||
babase.Call(
|
||||
session.transitioning_out_activity_was_freed,
|
||||
self.can_show_ad_on_death,
|
||||
)
|
||||
)
|
||||
|
||||
@property
|
||||
def globalsnode(self) -> ba.Node:
|
||||
"""The 'globals' ba.Node for the activity. This contains various
|
||||
def context(self) -> bascenev1.ContextRef:
|
||||
"""A context-ref pointing at this activity."""
|
||||
return self._activity_data.context()
|
||||
|
||||
@property
|
||||
def globalsnode(self) -> bascenev1.Node:
|
||||
"""The 'globals' bascenev1.Node for the activity. This contains various
|
||||
global controls and values.
|
||||
"""
|
||||
node = self._globalsnode
|
||||
if not node:
|
||||
raise NodeNotFoundError()
|
||||
raise babase.NodeNotFoundError()
|
||||
return node
|
||||
|
||||
@property
|
||||
def stats(self) -> ba.Stats:
|
||||
def stats(self) -> bascenev1.Stats:
|
||||
"""The stats instance accessible while the activity is running.
|
||||
|
||||
If access is attempted before or after, raises a ba.NotFoundError.
|
||||
If access is attempted before or after, raises a
|
||||
bascenev1.NotFoundError.
|
||||
"""
|
||||
if self._stats is None:
|
||||
from ba._error import NotFoundError
|
||||
|
||||
raise NotFoundError()
|
||||
raise babase.NotFoundError()
|
||||
return self._stats
|
||||
|
||||
def on_expire(self) -> None:
|
||||
|
|
@ -263,13 +259,13 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
|
|||
return self._expired
|
||||
|
||||
@property
|
||||
def playertype(self) -> type[PlayerType]:
|
||||
"""The type of ba.Player this Activity is using."""
|
||||
def playertype(self) -> type[PlayerT]:
|
||||
"""The type of bascenev1.Player this Activity is using."""
|
||||
return self._playertype
|
||||
|
||||
@property
|
||||
def teamtype(self) -> type[TeamType]:
|
||||
"""The type of ba.Team this Activity is using."""
|
||||
def teamtype(self) -> type[TeamT]:
|
||||
"""The type of bascenev1.Team this Activity is using."""
|
||||
return self._teamtype
|
||||
|
||||
def set_has_ended(self, val: bool) -> None:
|
||||
|
|
@ -281,19 +277,17 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
|
|||
|
||||
(internal)
|
||||
"""
|
||||
from ba._generated.enums import TimeType
|
||||
|
||||
# Create a real-timer that watches a weak-ref of this activity
|
||||
# Create an app-timer that watches a weak-ref of this activity
|
||||
# and reports any lingering references keeping it alive.
|
||||
# We store the timer on the activity so as soon as the activity dies
|
||||
# it gets cleaned up.
|
||||
with _ba.Context('ui'):
|
||||
with babase.ContextRef.empty():
|
||||
ref = weakref.ref(self)
|
||||
self._activity_death_check_timer = _ba.Timer(
|
||||
self._activity_death_check_timer = babase.AppTimer(
|
||||
5.0,
|
||||
Call(self._check_activity_death, ref, [0]),
|
||||
babase.Call(self._check_activity_death, ref, [0]),
|
||||
repeat=True,
|
||||
timetype=TimeType.REAL,
|
||||
)
|
||||
|
||||
# Run _expire in an empty context; nothing should be happening in
|
||||
|
|
@ -302,67 +296,65 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
|
|||
# and we can't properly provide context in that situation anyway; might
|
||||
# as well be consistent).
|
||||
if not self._expired:
|
||||
with _ba.Context('empty'):
|
||||
with babase.ContextRef.empty():
|
||||
self._expire()
|
||||
else:
|
||||
raise RuntimeError(
|
||||
f'destroy() called when' f' already expired for {self}'
|
||||
f'destroy() called when already expired for {self}.'
|
||||
)
|
||||
|
||||
def retain_actor(self, actor: ba.Actor) -> None:
|
||||
"""Add a strong-reference to a ba.Actor to this Activity.
|
||||
def retain_actor(self, actor: bascenev1.Actor) -> None:
|
||||
"""Add a strong-reference to a bascenev1.Actor to this Activity.
|
||||
|
||||
The reference will be lazily released once ba.Actor.exists()
|
||||
returns False for the Actor. The ba.Actor.autoretain() method
|
||||
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.
|
||||
"""
|
||||
if __debug__:
|
||||
from ba._actor import Actor
|
||||
from bascenev1._actor import Actor
|
||||
|
||||
assert isinstance(actor, Actor)
|
||||
self._actor_refs.append(actor)
|
||||
|
||||
def add_actor_weak_ref(self, actor: ba.Actor) -> None:
|
||||
"""Add a weak-reference to a ba.Actor to the ba.Activity.
|
||||
def add_actor_weak_ref(self, actor: bascenev1.Actor) -> None:
|
||||
"""Add a weak-reference to a bascenev1.Actor to the bascenev1.Activity.
|
||||
|
||||
(called by the ba.Actor base class)
|
||||
(called by the bascenev1.Actor base class)
|
||||
"""
|
||||
if __debug__:
|
||||
from ba._actor import Actor
|
||||
from bascenev1._actor import Actor
|
||||
|
||||
assert isinstance(actor, Actor)
|
||||
self._actor_weak_refs.append(weakref.ref(actor))
|
||||
|
||||
@property
|
||||
def session(self) -> ba.Session:
|
||||
"""The ba.Session this ba.Activity belongs go.
|
||||
def session(self) -> bascenev1.Session:
|
||||
"""The bascenev1.Session this bascenev1.Activity belongs to.
|
||||
|
||||
Raises a ba.SessionNotFoundError if the Session no longer exists.
|
||||
Raises a babase.SessionNotFoundError if the Session no longer exists.
|
||||
"""
|
||||
session = self._session()
|
||||
if session is None:
|
||||
from ba._error import SessionNotFoundError
|
||||
|
||||
raise SessionNotFoundError()
|
||||
raise babase.SessionNotFoundError()
|
||||
return session
|
||||
|
||||
def on_player_join(self, player: PlayerType) -> None:
|
||||
"""Called when a new ba.Player has joined the Activity.
|
||||
def on_player_join(self, player: PlayerT) -> None:
|
||||
"""Called when a new bascenev1.Player has joined the Activity.
|
||||
|
||||
(including the initial set of Players)
|
||||
"""
|
||||
|
||||
def on_player_leave(self, player: PlayerType) -> None:
|
||||
"""Called when a ba.Player is leaving the Activity."""
|
||||
def on_player_leave(self, player: PlayerT) -> None:
|
||||
"""Called when a bascenev1.Player is leaving the Activity."""
|
||||
|
||||
def on_team_join(self, team: TeamType) -> None:
|
||||
"""Called when a new ba.Team joins the Activity.
|
||||
def on_team_join(self, team: TeamT) -> None:
|
||||
"""Called when a new bascenev1.Team joins the Activity.
|
||||
|
||||
(including the initial set of Teams)
|
||||
"""
|
||||
|
||||
def on_team_leave(self, team: TeamType) -> None:
|
||||
"""Called when a ba.Team leaves the Activity."""
|
||||
def on_team_leave(self, team: TeamT) -> None:
|
||||
"""Called when a bascenev1.Team leaves the Activity."""
|
||||
|
||||
def on_transition_in(self) -> None:
|
||||
"""Called when the Activity is first becoming visible.
|
||||
|
|
@ -370,18 +362,18 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
|
|||
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 ba.Activity.on_begin() is called.
|
||||
up until 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 ba.Activity.end() has
|
||||
not been called.
|
||||
Note that this may happen at any time even if bascenev1.Activity.end()
|
||||
has not been called.
|
||||
"""
|
||||
|
||||
def on_begin(self) -> None:
|
||||
"""Called once the previous ba.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.
|
||||
|
|
@ -393,12 +385,11 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
|
|||
return UNHANDLED
|
||||
|
||||
def has_transitioned_in(self) -> bool:
|
||||
"""Return whether ba.Activity.on_transition_in()
|
||||
has been called."""
|
||||
"""Return whether bascenev1.Activity.on_transition_in() has run."""
|
||||
return self._has_transitioned_in
|
||||
|
||||
def has_begun(self) -> bool:
|
||||
"""Return whether ba.Activity.on_begin() has been called."""
|
||||
"""Return whether bascenev1.Activity.on_begin() has run."""
|
||||
return self._has_begun
|
||||
|
||||
def has_ended(self) -> bool:
|
||||
|
|
@ -406,10 +397,10 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
|
|||
return self._has_ended
|
||||
|
||||
def is_transitioning_out(self) -> bool:
|
||||
"""Return whether ba.Activity.on_transition_out() has been called."""
|
||||
"""Return whether bascenev1.Activity.on_transition_out() has run."""
|
||||
return self._transitioning_out
|
||||
|
||||
def transition_in(self, prev_globals: ba.Node | None) -> None:
|
||||
def transition_in(self, prev_globals: bascenev1.Node | None) -> None:
|
||||
"""Called by Session to kick off transition-in.
|
||||
|
||||
(internal)
|
||||
|
|
@ -418,8 +409,8 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
|
|||
self._has_transitioned_in = True
|
||||
|
||||
# Set up the globals node based on our settings.
|
||||
with _ba.Context(self):
|
||||
glb = self._globalsnode = _ba.newnode('globals')
|
||||
with self.context:
|
||||
glb = self._globalsnode = _bascenev1.newnode('globals')
|
||||
|
||||
# Now that it's going to be front and center,
|
||||
# set some global values based on what the activity wants.
|
||||
|
|
@ -449,11 +440,11 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
|
|||
|
||||
# Start pruning our various things periodically.
|
||||
self._prune_dead_actors()
|
||||
self._prune_dead_actors_timer = _ba.Timer(
|
||||
self._prune_dead_actors_timer = _bascenev1.Timer(
|
||||
5.17, self._prune_dead_actors, repeat=True
|
||||
)
|
||||
|
||||
_ba.timer(13.3, self._prune_delay_deletes, repeat=True)
|
||||
_bascenev1.timer(13.3, self._prune_delay_deletes, repeat=True)
|
||||
|
||||
# Also start our low-level scene running.
|
||||
self._activity_data.start()
|
||||
|
|
@ -461,7 +452,7 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
|
|||
try:
|
||||
self.on_transition_in()
|
||||
except Exception:
|
||||
print_exception(f'Error in on_transition_in for {self}.')
|
||||
logging.exception('Error in on_transition_in for %s.', self)
|
||||
|
||||
# Tell the C++ layer that this activity is the main one, so it uses
|
||||
# settings from our globals, directs various events to us, etc.
|
||||
|
|
@ -471,13 +462,13 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
|
|||
"""Called by the Session to start us transitioning out."""
|
||||
assert not self._transitioning_out
|
||||
self._transitioning_out = True
|
||||
with _ba.Context(self):
|
||||
with self.context:
|
||||
try:
|
||||
self.on_transition_out()
|
||||
except Exception:
|
||||
print_exception(f'Error in on_transition_out for {self}.')
|
||||
logging.exception('Error in on_transition_out for %s.', self)
|
||||
|
||||
def begin(self, session: ba.Session) -> None:
|
||||
def begin(self, session: bascenev1.Session) -> None:
|
||||
"""Begin the activity.
|
||||
|
||||
(internal)
|
||||
|
|
@ -499,7 +490,7 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
|
|||
self._has_begun = True
|
||||
|
||||
# Let the activity do its thing.
|
||||
with _ba.Context(self):
|
||||
with self.context:
|
||||
# Note: do we want to catch errors here?
|
||||
# Currently I believe we wind up canceling the
|
||||
# activity launch; just wanna be sure that is intentional.
|
||||
|
|
@ -508,7 +499,7 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
|
|||
def end(
|
||||
self, results: Any = None, delay: float = 0.0, force: bool = False
|
||||
) -> None:
|
||||
"""Commences Activity shutdown and delivers results to the ba.Session.
|
||||
"""Commences 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
|
||||
|
|
@ -519,20 +510,20 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
|
|||
# Ask the session to end us.
|
||||
self.session.end_activity(self, results, delay, force)
|
||||
|
||||
def create_player(self, sessionplayer: ba.SessionPlayer) -> PlayerType:
|
||||
def create_player(self, sessionplayer: bascenev1.SessionPlayer) -> PlayerT:
|
||||
"""Create the 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
|
||||
ba.Activity.on_player_join() for that.
|
||||
bascenev1.Activity.on_player_join() for that.
|
||||
"""
|
||||
del sessionplayer # Unused.
|
||||
player = self._playertype()
|
||||
return player
|
||||
|
||||
def create_team(self, sessionteam: ba.SessionTeam) -> TeamType:
|
||||
def create_team(self, sessionteam: bascenev1.SessionTeam) -> TeamT:
|
||||
"""Create the Team instance for this Activity.
|
||||
|
||||
Subclasses can override this if the activity's team class
|
||||
|
|
@ -545,7 +536,7 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
|
|||
team = self._teamtype()
|
||||
return team
|
||||
|
||||
def add_player(self, sessionplayer: ba.SessionPlayer) -> None:
|
||||
def add_player(self, sessionplayer: bascenev1.SessionPlayer) -> None:
|
||||
"""(internal)"""
|
||||
assert sessionplayer.sessionteam is not None
|
||||
sessionplayer.resetinput()
|
||||
|
|
@ -554,7 +545,7 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
|
|||
team = sessionteam.activityteam
|
||||
assert team is not None
|
||||
sessionplayer.setactivity(self)
|
||||
with _ba.Context(self):
|
||||
with self.context:
|
||||
sessionplayer.activityplayer = player = self.create_player(
|
||||
sessionplayer
|
||||
)
|
||||
|
|
@ -571,9 +562,9 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
|
|||
try:
|
||||
self.on_player_join(player)
|
||||
except Exception:
|
||||
print_exception(f'Error in on_player_join for {self}.')
|
||||
logging.exception('Error in on_player_join for %s.', self)
|
||||
|
||||
def remove_player(self, sessionplayer: ba.SessionPlayer) -> None:
|
||||
def remove_player(self, sessionplayer: bascenev1.SessionPlayer) -> None:
|
||||
"""Remove a player from the Activity while it is running.
|
||||
|
||||
(internal)
|
||||
|
|
@ -593,19 +584,19 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
|
|||
self.players.remove(player)
|
||||
assert player not in self.players
|
||||
|
||||
# This should allow our ba.Player instance to die.
|
||||
# This should allow our bascenev1.Player instance to die.
|
||||
# Complain if that doesn't happen.
|
||||
# verify_object_death(player)
|
||||
|
||||
with _ba.Context(self):
|
||||
with self.context:
|
||||
try:
|
||||
self.on_player_leave(player)
|
||||
except Exception:
|
||||
print_exception(f'Error in on_player_leave for {self}.')
|
||||
logging.exception('Error in on_player_leave for %s.', self)
|
||||
try:
|
||||
player.leave()
|
||||
except Exception:
|
||||
print_exception(f'Error on leave for {player} in {self}.')
|
||||
logging.exception('Error on leave for %s in %s.', player, self)
|
||||
|
||||
self._reset_session_player_for_no_activity(sessionplayer)
|
||||
|
||||
|
|
@ -616,23 +607,23 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
|
|||
self._delay_delete_players.append(player)
|
||||
self._players_that_left.append(weakref.ref(player))
|
||||
|
||||
def add_team(self, sessionteam: ba.SessionTeam) -> None:
|
||||
def add_team(self, sessionteam: bascenev1.SessionTeam) -> None:
|
||||
"""Add a team to the Activity
|
||||
|
||||
(internal)
|
||||
"""
|
||||
assert not self.expired
|
||||
|
||||
with _ba.Context(self):
|
||||
with self.context:
|
||||
sessionteam.activityteam = team = self.create_team(sessionteam)
|
||||
team.postinit(sessionteam)
|
||||
self.teams.append(team)
|
||||
try:
|
||||
self.on_team_join(team)
|
||||
except Exception:
|
||||
print_exception(f'Error in on_team_join for {self}.')
|
||||
logging.exception('Error in on_team_join for %s.', self)
|
||||
|
||||
def remove_team(self, sessionteam: ba.SessionTeam) -> None:
|
||||
def remove_team(self, sessionteam: bascenev1.SessionTeam) -> None:
|
||||
"""Remove a team from a Running Activity
|
||||
|
||||
(internal)
|
||||
|
|
@ -647,16 +638,16 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
|
|||
self.teams.remove(team)
|
||||
assert team not in self.teams
|
||||
|
||||
with _ba.Context(self):
|
||||
with self.context:
|
||||
# Make a decent attempt to persevere if user code breaks.
|
||||
try:
|
||||
self.on_team_leave(team)
|
||||
except Exception:
|
||||
print_exception(f'Error in on_team_leave for {self}.')
|
||||
logging.exception('Error in on_team_leave for %s.', self)
|
||||
try:
|
||||
team.leave()
|
||||
except Exception:
|
||||
print_exception(f'Error on leave for {team} in {self}.')
|
||||
logging.exception('Error on leave for %s in %s.', team, self)
|
||||
|
||||
sessionteam.activityteam = None
|
||||
|
||||
|
|
@ -668,25 +659,26 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
|
|||
self._teams_that_left.append(weakref.ref(team))
|
||||
|
||||
def _reset_session_player_for_no_activity(
|
||||
self, sessionplayer: ba.SessionPlayer
|
||||
self, sessionplayer: bascenev1.SessionPlayer
|
||||
) -> None:
|
||||
|
||||
# Let's be extra-defensive here: killing a node/input-call/etc
|
||||
# could trigger user-code resulting in errors, but we would still
|
||||
# like to complete the reset if possible.
|
||||
try:
|
||||
sessionplayer.setnode(None)
|
||||
except Exception:
|
||||
print_exception(
|
||||
f'Error resetting SessionPlayer node on {sessionplayer}'
|
||||
f' for {self}.'
|
||||
logging.exception(
|
||||
'Error resetting SessionPlayer node on %s for %s.',
|
||||
sessionplayer,
|
||||
self,
|
||||
)
|
||||
try:
|
||||
sessionplayer.resetinput()
|
||||
except Exception:
|
||||
print_exception(
|
||||
f'Error resetting SessionPlayer input on {sessionplayer}'
|
||||
f' for {self}.'
|
||||
logging.exception(
|
||||
'Error resetting SessionPlayer input on %s for %s.',
|
||||
sessionplayer,
|
||||
self,
|
||||
)
|
||||
|
||||
# These should never fail I think...
|
||||
|
|
@ -699,7 +691,7 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
|
|||
|
||||
# TODO: There are proper calls for pulling these in Python 3.8;
|
||||
# should update this code when we adopt that.
|
||||
# NOTE: If we get Any as PlayerType or TeamType (generally due
|
||||
# NOTE: If we get Any as PlayerT or TeamT (generally due
|
||||
# to no generic params being passed) we automatically use the
|
||||
# base class types, but also warn the user since this will mean
|
||||
# less type safety for that class. (its better to pass the base
|
||||
|
|
@ -710,7 +702,7 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
|
|||
self._playertype = Player
|
||||
print(
|
||||
f'ERROR: {type(self)} was not passed a Player'
|
||||
f' type argument; please explicitly pass ba.Player'
|
||||
f' type argument; please explicitly pass bascenev1.Player'
|
||||
f' if you do not want to override it.'
|
||||
)
|
||||
self._teamtype = type(self).__orig_bases__[-1].__args__[1]
|
||||
|
|
@ -718,7 +710,7 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
|
|||
self._teamtype = Team
|
||||
print(
|
||||
f'ERROR: {type(self)} was not passed a Team'
|
||||
f' type argument; please explicitly pass ba.Team'
|
||||
f' type argument; please explicitly pass bascenev1.Team'
|
||||
f' if you do not want to override it.'
|
||||
)
|
||||
assert issubclass(self._playertype, Player)
|
||||
|
|
@ -730,8 +722,8 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
|
|||
) -> None:
|
||||
"""Sanity check to make sure an Activity was destroyed properly.
|
||||
|
||||
Receives a weakref to a ba.Activity which should have torn itself
|
||||
down due to no longer being referenced anywhere. Will complain
|
||||
Receives a weakref to a bascenev1.Activity which should have torn
|
||||
itself down due to no longer being referenced anywhere. Will complain
|
||||
and/or print debugging info if the Activity still exists.
|
||||
"""
|
||||
try:
|
||||
|
|
@ -751,10 +743,10 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
|
|||
counter[0] += 1
|
||||
if counter[0] == 4:
|
||||
print('Killing app due to stuck activity... :-(')
|
||||
_ba.quit()
|
||||
babase.quit()
|
||||
|
||||
except Exception:
|
||||
print_exception('Error on _check_activity_death/')
|
||||
logging.exception('Error on _check_activity_death.')
|
||||
|
||||
def _expire(self) -> None:
|
||||
"""Put the activity in a state where it can be garbage-collected.
|
||||
|
|
@ -768,12 +760,12 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
|
|||
try:
|
||||
self.on_expire()
|
||||
except Exception:
|
||||
print_exception(f'Error in Activity on_expire() for {self}.')
|
||||
logging.exception('Error in Activity on_expire() for %s.', self)
|
||||
|
||||
try:
|
||||
self._customdata = None
|
||||
except Exception:
|
||||
print_exception(f'Error clearing customdata for {self}.')
|
||||
logging.exception('Error clearing customdata for %s.', self)
|
||||
|
||||
# Don't want to be holding any delay-delete refs at this point.
|
||||
self._prune_delay_deletes()
|
||||
|
|
@ -788,79 +780,77 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
|
|||
try:
|
||||
self._activity_data.expire()
|
||||
except Exception:
|
||||
print_exception(f'Error expiring _activity_data for {self}.')
|
||||
logging.exception('Error expiring _activity_data for %s.', self)
|
||||
|
||||
def _expire_actors(self) -> None:
|
||||
# Expire all Actors.
|
||||
for actor_ref in self._actor_weak_refs:
|
||||
actor = actor_ref()
|
||||
if actor is not None:
|
||||
verify_object_death(actor)
|
||||
babase.verify_object_death(actor)
|
||||
try:
|
||||
actor.on_expire()
|
||||
except Exception:
|
||||
print_exception(
|
||||
f'Error in Actor.on_expire()' f' for {actor_ref()}.'
|
||||
logging.exception(
|
||||
'Error in Actor.on_expire() for %s.', actor_ref()
|
||||
)
|
||||
|
||||
def _expire_players(self) -> None:
|
||||
|
||||
# Issue warnings for any players that left the game but don't
|
||||
# get freed soon.
|
||||
for ex_player in (p() for p in self._players_that_left):
|
||||
if ex_player is not None:
|
||||
verify_object_death(ex_player)
|
||||
babase.verify_object_death(ex_player)
|
||||
|
||||
for player in self.players:
|
||||
# This should allow our ba.Player instance to be freed.
|
||||
# This should allow our bascenev1.Player instance to be freed.
|
||||
# Complain if that doesn't happen.
|
||||
verify_object_death(player)
|
||||
babase.verify_object_death(player)
|
||||
|
||||
try:
|
||||
player.expire()
|
||||
except Exception:
|
||||
print_exception(f'Error expiring {player}')
|
||||
logging.exception('Error expiring %s.', player)
|
||||
|
||||
# Reset the SessionPlayer to a not-in-an-activity state.
|
||||
try:
|
||||
sessionplayer = player.sessionplayer
|
||||
self._reset_session_player_for_no_activity(sessionplayer)
|
||||
except SessionPlayerNotFoundError:
|
||||
except babase.SessionPlayerNotFoundError:
|
||||
# Conceivably, someone could have held on to a Player object
|
||||
# until now whos underlying SessionPlayer left long ago...
|
||||
pass
|
||||
except Exception:
|
||||
print_exception(f'Error expiring {player}.')
|
||||
logging.exception('Error expiring %s.', player)
|
||||
|
||||
def _expire_teams(self) -> None:
|
||||
|
||||
# Issue warnings for any teams that left the game but don't
|
||||
# get freed soon.
|
||||
for ex_team in (p() for p in self._teams_that_left):
|
||||
if ex_team is not None:
|
||||
verify_object_death(ex_team)
|
||||
babase.verify_object_death(ex_team)
|
||||
|
||||
for team in self.teams:
|
||||
# This should allow our ba.Team instance to die.
|
||||
# This should allow our bascenev1.Team instance to die.
|
||||
# Complain if that doesn't happen.
|
||||
verify_object_death(team)
|
||||
babase.verify_object_death(team)
|
||||
|
||||
try:
|
||||
team.expire()
|
||||
except Exception:
|
||||
print_exception(f'Error expiring {team}')
|
||||
logging.exception('Error expiring %s.', team)
|
||||
|
||||
try:
|
||||
sessionteam = team.sessionteam
|
||||
sessionteam.activityteam = None
|
||||
except SessionTeamNotFoundError:
|
||||
except babase.SessionTeamNotFoundError:
|
||||
# It is expected that Team objects may last longer than
|
||||
# the SessionTeam they came from (game objects may hold
|
||||
# team references past the point at which the underlying
|
||||
# player/team has left the game)
|
||||
pass
|
||||
except Exception:
|
||||
print_exception(f'Error expiring Team {team}.')
|
||||
logging.exception('Error expiring Team %s.', team)
|
||||
|
||||
def _prune_delay_deletes(self) -> None:
|
||||
self._delay_delete_players.clear()
|
||||
|
|
@ -875,7 +865,7 @@ class Activity(DependencyComponent, Generic[PlayerType, TeamType]):
|
|||
]
|
||||
|
||||
def _prune_dead_actors(self) -> None:
|
||||
self._last_prune_dead_actors_time = _ba.time()
|
||||
self._last_prune_dead_actors_time = babase.apptime()
|
||||
|
||||
# Prune our strong refs when the Actor's exists() call gives False
|
||||
self._actor_refs = [a for a in self._actor_refs if a.exists()]
|
||||
|
|
@ -5,22 +5,24 @@ from __future__ import annotations
|
|||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import _ba
|
||||
from ba._activity import Activity
|
||||
from ba._music import setmusic, MusicType
|
||||
from ba._generated.enums import InputType, UIScale
|
||||
import babase
|
||||
|
||||
import _bascenev1
|
||||
from bascenev1._activity import Activity
|
||||
|
||||
# False-positive from pylint due to our class-generics-filter.
|
||||
from ba._player import EmptyPlayer # pylint: disable=W0611
|
||||
from ba._team import EmptyTeam # pylint: disable=W0611
|
||||
from bascenev1._player import EmptyPlayer # pylint: disable=W0611
|
||||
from bascenev1._team import EmptyTeam # pylint: disable=W0611
|
||||
from bascenev1._music import MusicType, setmusic
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import ba
|
||||
from ba._lobby import JoinInfo
|
||||
import bascenev1
|
||||
from bascenev1._lobby import JoinInfo
|
||||
|
||||
|
||||
class EndSessionActivity(Activity[EmptyPlayer, EmptyTeam]):
|
||||
"""Special ba.Activity to fade out and end the current ba.Session."""
|
||||
"""Special Activity to fade out and end the current Session."""
|
||||
|
||||
def __init__(self, settings: dict):
|
||||
super().__init__(settings)
|
||||
|
|
@ -34,17 +36,22 @@ class EndSessionActivity(Activity[EmptyPlayer, EmptyTeam]):
|
|||
|
||||
def on_transition_in(self) -> None:
|
||||
super().on_transition_in()
|
||||
_ba.fade_screen(False)
|
||||
_ba.lock_all_input()
|
||||
babase.fade_screen(False)
|
||||
babase.lock_all_input()
|
||||
|
||||
def on_begin(self) -> None:
|
||||
# pylint: disable=cyclic-import
|
||||
from bastd.mainmenu import MainMenuSession
|
||||
from ba._general import Call
|
||||
|
||||
assert babase.app.classic is not None
|
||||
|
||||
main_menu_session = babase.app.classic.get_main_menu_session()
|
||||
|
||||
super().on_begin()
|
||||
_ba.unlock_all_input()
|
||||
_ba.app.ads.call_after_ad(Call(_ba.new_host_session, MainMenuSession))
|
||||
babase.unlock_all_input()
|
||||
assert babase.app.classic is not None
|
||||
babase.app.classic.ads.call_after_ad(
|
||||
babase.Call(_bascenev1.new_host_session, main_menu_session)
|
||||
)
|
||||
|
||||
|
||||
class JoinActivity(Activity[EmptyPlayer, EmptyTeam]):
|
||||
|
|
@ -66,14 +73,14 @@ class JoinActivity(Activity[EmptyPlayer, EmptyTeam]):
|
|||
# In vr mode we don't want stuff moving around.
|
||||
self.use_fixed_vr_overlay = True
|
||||
|
||||
self._background: ba.Actor | None = None
|
||||
self._tips_text: ba.Actor | None = None
|
||||
self._background: bascenev1.Actor | None = None
|
||||
self._tips_text: bascenev1.Actor | None = None
|
||||
self._join_info: JoinInfo | None = None
|
||||
|
||||
def on_transition_in(self) -> None:
|
||||
# pylint: disable=cyclic-import
|
||||
from bastd.actor.tipstext import TipsText
|
||||
from bastd.actor.background import Background
|
||||
from bascenev1lib.actor.tipstext import TipsText
|
||||
from bascenev1lib.actor.background import Background
|
||||
|
||||
super().on_transition_in()
|
||||
self._background = Background(
|
||||
|
|
@ -82,7 +89,7 @@ class JoinActivity(Activity[EmptyPlayer, EmptyTeam]):
|
|||
self._tips_text = TipsText()
|
||||
setmusic(MusicType.CHAR_SELECT)
|
||||
self._join_info = self.session.lobby.create_join_info()
|
||||
_ba.set_analytics_screen('Joining Screen')
|
||||
babase.set_analytics_screen('Joining Screen')
|
||||
|
||||
|
||||
class TransitionActivity(Activity[EmptyPlayer, EmptyTeam]):
|
||||
|
|
@ -101,14 +108,14 @@ class TransitionActivity(Activity[EmptyPlayer, EmptyTeam]):
|
|||
|
||||
def __init__(self, settings: dict):
|
||||
super().__init__(settings)
|
||||
self._background: ba.Actor | None = None
|
||||
self._background: bascenev1.Actor | None = None
|
||||
|
||||
def on_transition_in(self) -> None:
|
||||
# pylint: disable=cyclic-import
|
||||
from bastd.actor import background # FIXME: Don't use bastd from ba.
|
||||
from bascenev1lib.actor.background import Background
|
||||
|
||||
super().on_transition_in()
|
||||
self._background = background.Background(
|
||||
self._background = Background(
|
||||
fade_time=0.5, start_faded=False, show_logo=False
|
||||
)
|
||||
|
||||
|
|
@ -116,7 +123,7 @@ class TransitionActivity(Activity[EmptyPlayer, EmptyTeam]):
|
|||
super().on_begin()
|
||||
|
||||
# Die almost immediately.
|
||||
_ba.timer(0.1, self.end)
|
||||
_bascenev1.timer(0.1, self.end)
|
||||
|
||||
|
||||
class ScoreScreenActivity(Activity[EmptyPlayer, EmptyTeam]):
|
||||
|
|
@ -134,32 +141,32 @@ class ScoreScreenActivity(Activity[EmptyPlayer, EmptyTeam]):
|
|||
|
||||
def __init__(self, settings: dict):
|
||||
super().__init__(settings)
|
||||
self._birth_time = _ba.time()
|
||||
self._birth_time = babase.apptime()
|
||||
self._min_view_time = 5.0
|
||||
self._allow_server_transition = False
|
||||
self._background: ba.Actor | None = None
|
||||
self._tips_text: ba.Actor | None = None
|
||||
self._background: bascenev1.Actor | None = None
|
||||
self._tips_text: bascenev1.Actor | None = None
|
||||
self._kicked_off_server_shutdown = False
|
||||
self._kicked_off_server_restart = False
|
||||
self._default_show_tips = True
|
||||
self._custom_continue_message: ba.Lstr | None = None
|
||||
self._custom_continue_message: babase.Lstr | None = None
|
||||
self._server_transitioning: bool | None = None
|
||||
|
||||
def on_player_join(self, player: EmptyPlayer) -> None:
|
||||
from ba._general import WeakCall
|
||||
|
||||
super().on_player_join(player)
|
||||
time_till_assign = max(
|
||||
0, self._birth_time + self._min_view_time - _ba.time()
|
||||
0, self._birth_time + self._min_view_time - babase.apptime()
|
||||
)
|
||||
|
||||
# If we're still kicking at the end of our assign-delay, assign this
|
||||
# guy's input to trigger us.
|
||||
_ba.timer(time_till_assign, WeakCall(self._safe_assign, player))
|
||||
_bascenev1.timer(
|
||||
time_till_assign, babase.WeakCall(self._safe_assign, player)
|
||||
)
|
||||
|
||||
def on_transition_in(self) -> None:
|
||||
from bastd.actor.tipstext import TipsText
|
||||
from bastd.actor.background import Background
|
||||
from bascenev1lib.actor.tipstext import TipsText
|
||||
from bascenev1lib.actor.background import Background
|
||||
|
||||
super().on_transition_in()
|
||||
self._background = Background(
|
||||
|
|
@ -171,22 +178,20 @@ class ScoreScreenActivity(Activity[EmptyPlayer, EmptyTeam]):
|
|||
|
||||
def on_begin(self) -> None:
|
||||
# pylint: disable=cyclic-import
|
||||
from bastd.actor.text import Text
|
||||
from ba import _language
|
||||
from bascenev1lib.actor.text import Text
|
||||
|
||||
super().on_begin()
|
||||
import custom_hooks
|
||||
custom_hooks.score_screen_on_begin(self._stats)
|
||||
|
||||
# Pop up a 'press any button to continue' statement after our
|
||||
# min-view-time show a 'press any button to continue..'
|
||||
# thing after a bit.
|
||||
if _ba.app.ui.uiscale is UIScale.LARGE:
|
||||
assert babase.app.classic is not None
|
||||
if babase.app.ui_v1.uiscale is babase.UIScale.LARGE:
|
||||
# FIXME: Need a better way to determine whether we've probably
|
||||
# got a keyboard.
|
||||
sval = _language.Lstr(resource='pressAnyKeyButtonText')
|
||||
sval = babase.Lstr(resource='pressAnyKeyButtonText')
|
||||
else:
|
||||
sval = _language.Lstr(resource='pressAnyButtonText')
|
||||
sval = babase.Lstr(resource='pressAnyButtonText')
|
||||
|
||||
Text(
|
||||
self._custom_continue_message
|
||||
|
|
@ -204,15 +209,17 @@ class ScoreScreenActivity(Activity[EmptyPlayer, EmptyTeam]):
|
|||
).autoretain()
|
||||
|
||||
def _player_press(self) -> None:
|
||||
|
||||
# If this activity is a good 'end point', ask server-mode just once if
|
||||
# it wants to do anything special like switch sessions or kill the app.
|
||||
if (
|
||||
self._allow_server_transition
|
||||
and _ba.app.server is not None
|
||||
and babase.app.classic is not None
|
||||
and babase.app.classic.server is not None
|
||||
and self._server_transitioning is None
|
||||
):
|
||||
self._server_transitioning = _ba.app.server.handle_transition()
|
||||
self._server_transitioning = (
|
||||
babase.app.classic.server.handle_transition()
|
||||
)
|
||||
assert isinstance(self._server_transitioning, bool)
|
||||
|
||||
# If server-mode is handling this, don't do anything ourself.
|
||||
|
|
@ -223,16 +230,15 @@ class ScoreScreenActivity(Activity[EmptyPlayer, EmptyTeam]):
|
|||
self.end()
|
||||
|
||||
def _safe_assign(self, player: EmptyPlayer) -> None:
|
||||
|
||||
# Just to be extra careful, don't assign if we're transitioning out.
|
||||
# (though theoretically that should be ok).
|
||||
if not self.is_transitioning_out() and player:
|
||||
player.assigninput(
|
||||
(
|
||||
InputType.JUMP_PRESS,
|
||||
InputType.PUNCH_PRESS,
|
||||
InputType.BOMB_PRESS,
|
||||
InputType.PICK_UP_PRESS,
|
||||
babase.InputType.JUMP_PRESS,
|
||||
babase.InputType.PUNCH_PRESS,
|
||||
babase.InputType.BOMB_PRESS,
|
||||
babase.InputType.PICK_UP_PRESS,
|
||||
),
|
||||
self._player_press,
|
||||
)
|
||||
|
|
@ -5,29 +5,37 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import weakref
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, TypeVar, overload
|
||||
|
||||
import _ba
|
||||
from ba._messages import DieMessage, DeathType, OutOfBoundsMessage, UNHANDLED
|
||||
from ba._error import print_exception, ActivityNotFoundError
|
||||
import babase
|
||||
|
||||
import _bascenev1
|
||||
from bascenev1._messages import (
|
||||
DieMessage,
|
||||
DeathType,
|
||||
OutOfBoundsMessage,
|
||||
UNHANDLED,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any, Literal
|
||||
import ba
|
||||
|
||||
import bascenev1
|
||||
|
||||
ActorT = TypeVar('ActorT', bound='Actor')
|
||||
|
||||
|
||||
class Actor:
|
||||
"""High level logical entities in a ba.Activity.
|
||||
"""High level logical entities in a bascenev1.Activity.
|
||||
|
||||
Category: **Gameplay Classes**
|
||||
|
||||
Actors act as controllers, combining some number of ba.Nodes,
|
||||
ba.Textures, ba.Sounds, etc. 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 Bomb, Flag, and Spaz classes that
|
||||
live in the bastd.actor.* modules.
|
||||
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
|
||||
|
|
@ -35,7 +43,7 @@ class Actor:
|
|||
|
||||
##### Example
|
||||
>>> # Create a flag Actor in our game activity:
|
||||
... from bastd.actor.flag import Flag
|
||||
... from bascenev1lib.actor.flag import Flag
|
||||
... self.flag = Flag(position=(0, 10, 0))
|
||||
...
|
||||
... # Later, destroy the flag.
|
||||
|
|
@ -44,34 +52,36 @@ class Actor:
|
|||
... # Either way, the old flag disappears.
|
||||
... self.flag = None
|
||||
|
||||
This is in contrast to the behavior of the more low level ba.Nodes,
|
||||
which are always explicitly created and destroyed and don't care
|
||||
how many Python references to them exist.
|
||||
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.
|
||||
|
||||
Note, however, that you can use the ba.Actor.autoretain() method
|
||||
Note, however, that you can use the bascenev1.Actor.autoretain() method
|
||||
if you want an Actor to stick around until explicitly killed
|
||||
regardless of references.
|
||||
|
||||
Another key feature of ba.Actor is its ba.Actor.handlemessage() method,
|
||||
which takes a single arbitrary object as an argument. This provides a safe
|
||||
way to communicate between ba.Actor, ba.Activity, ba.Session, and any other
|
||||
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 ba.DieMessage.
|
||||
message type for Actors is the bascenev1.DieMessage.
|
||||
|
||||
Another way to kill the flag from the example above:
|
||||
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
|
||||
ba.Actor.exists() and ba.Actor.is_alive() methods will both return False.
|
||||
>>> self.flag.handlemessage(ba.DieMessage())
|
||||
bascenev1.Actor.exists() and bascenev1.Actor.is_alive() methods will
|
||||
both return False.
|
||||
>>> self.flag.handlemessage(bascenev1.DieMessage())
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Instantiates an Actor in the current ba.Activity."""
|
||||
"""Instantiates an Actor in the current bascenev1.Activity."""
|
||||
|
||||
if __debug__:
|
||||
self._root_actor_init_called = True
|
||||
activity = _ba.getactivity()
|
||||
activity = _bascenev1.getactivity()
|
||||
self._activity = weakref.ref(activity)
|
||||
activity.add_actor_weak_ref(self)
|
||||
|
||||
|
|
@ -83,7 +93,9 @@ class Actor:
|
|||
if not self.expired:
|
||||
self.handlemessage(DieMessage())
|
||||
except Exception:
|
||||
print_exception('exception in ba.Actor.__del__() for', self)
|
||||
logging.exception(
|
||||
'Error in bascenev1.Actor.__del__() for %s.', self
|
||||
)
|
||||
|
||||
def handlemessage(self, msg: Any) -> Any:
|
||||
"""General message handling; can be passed any message object."""
|
||||
|
|
@ -98,30 +110,32 @@ class Actor:
|
|||
def autoretain(self: ActorT) -> ActorT:
|
||||
"""Keep this Actor alive without needing to hold a reference to it.
|
||||
|
||||
This keeps the ba.Actor in existence by storing a reference to it
|
||||
with the ba.Activity it was created in. The reference is lazily
|
||||
released once ba.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 ba.Actor from dying.
|
||||
For convenience, this method returns the ba.Actor it is called with,
|
||||
enabling chained statements such as: myflag = ba.Flag().autoretain()
|
||||
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()
|
||||
"""
|
||||
activity = self._activity()
|
||||
if activity is None:
|
||||
raise ActivityNotFoundError()
|
||||
raise babase.ActivityNotFoundError()
|
||||
activity.retain_actor(self)
|
||||
return self
|
||||
|
||||
def on_expire(self) -> None:
|
||||
"""Called for remaining `ba.Actor`s when their ba.Activity shuts down.
|
||||
"""Called for remaining `bascenev1.Actor`s when their activity dies.
|
||||
|
||||
Actors can use this opportunity to clear callbacks or other
|
||||
references which have the potential of keeping the ba.Activity
|
||||
references which have the potential of keeping the bascenev1.Activity
|
||||
alive inadvertently (Activities can not exit cleanly while
|
||||
any Python references to them remain.)
|
||||
|
||||
Once an actor is expired (see ba.Actor.is_expired()) it should no
|
||||
longer perform any game-affecting operations (creating, modifying,
|
||||
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.
|
||||
"""
|
||||
|
|
@ -130,7 +144,7 @@ class Actor:
|
|||
def expired(self) -> bool:
|
||||
"""Whether the Actor is expired.
|
||||
|
||||
(see ba.Actor.on_expire())
|
||||
(see bascenev1.Actor.on_expire())
|
||||
"""
|
||||
activity = self.getactivity(doraise=False)
|
||||
return True if activity is None else activity.expired
|
||||
|
|
@ -140,11 +154,11 @@ class Actor:
|
|||
|
||||
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 ba.Actor.is_alive() for that).
|
||||
(see 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 ba.Actor.autoretain()
|
||||
when pruning lists of Actors, such as with bascenev1.Actor.autoretain()
|
||||
|
||||
The default implementation of this method always return True.
|
||||
|
||||
|
|
@ -170,33 +184,35 @@ class Actor:
|
|||
return True
|
||||
|
||||
@property
|
||||
def activity(self) -> ba.Activity:
|
||||
def activity(self) -> bascenev1.Activity:
|
||||
"""The Activity this Actor was created in.
|
||||
|
||||
Raises a ba.ActivityNotFoundError if the Activity no longer exists.
|
||||
Raises a bascenev1.ActivityNotFoundError if the Activity no longer
|
||||
exists.
|
||||
"""
|
||||
activity = self._activity()
|
||||
if activity is None:
|
||||
raise ActivityNotFoundError()
|
||||
raise babase.ActivityNotFoundError()
|
||||
return activity
|
||||
|
||||
# Overloads to convey our exact return type depending on 'doraise' value.
|
||||
|
||||
@overload
|
||||
def getactivity(self, doraise: Literal[True] = True) -> ba.Activity:
|
||||
def getactivity(self, doraise: Literal[True] = True) -> bascenev1.Activity:
|
||||
...
|
||||
|
||||
@overload
|
||||
def getactivity(self, doraise: Literal[False]) -> ba.Activity | None:
|
||||
def getactivity(self, doraise: Literal[False]) -> bascenev1.Activity | None:
|
||||
...
|
||||
|
||||
def getactivity(self, doraise: bool = True) -> ba.Activity | None:
|
||||
"""Return the ba.Activity this Actor is associated with.
|
||||
def getactivity(self, doraise: bool = True) -> bascenev1.Activity | None:
|
||||
"""Return the bascenev1.Activity this Actor is associated with.
|
||||
|
||||
If the Activity no longer exists, raises a ba.ActivityNotFoundError
|
||||
or returns None depending on whether 'doraise' is True.
|
||||
If the Activity no longer exists, raises a
|
||||
bascenev1.ActivityNotFoundError or returns None depending on whether
|
||||
'doraise' is True.
|
||||
"""
|
||||
activity = self._activity()
|
||||
if activity is None and doraise:
|
||||
raise ActivityNotFoundError()
|
||||
raise babase.ActivityNotFoundError()
|
||||
return activity
|
||||
36
dist/ba_data/python/bascenev1/_appmode.py
vendored
Normal file
36
dist/ba_data/python/bascenev1/_appmode.py
vendored
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Provides AppMode functionality."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from babase import AppMode, AppIntentExec, AppIntentDefault
|
||||
import _bascenev1
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from babase import AppIntent
|
||||
|
||||
|
||||
class SceneV1AppMode(AppMode):
|
||||
"""Our app-mode."""
|
||||
|
||||
@classmethod
|
||||
def supports_intent(cls, intent: AppIntent) -> bool:
|
||||
# We support default and exec intents currently.
|
||||
return isinstance(intent, AppIntentExec | AppIntentDefault)
|
||||
|
||||
def handle_intent(self, intent: AppIntent) -> None:
|
||||
if isinstance(intent, AppIntentExec):
|
||||
_bascenev1.handle_app_intent_exec(intent.code)
|
||||
return
|
||||
assert isinstance(intent, AppIntentDefault)
|
||||
_bascenev1.handle_app_intent_default()
|
||||
|
||||
def on_activate(self) -> None:
|
||||
# Let the native layer do its thing.
|
||||
_bascenev1.app_mode_activate()
|
||||
|
||||
def on_deactivate(self) -> None:
|
||||
# Let the native layer do its thing.
|
||||
_bascenev1.app_mode_deactivate()
|
||||
|
|
@ -5,25 +5,21 @@ from __future__ import annotations
|
|||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import _ba
|
||||
import babase
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any
|
||||
import ba
|
||||
import bascenev1
|
||||
|
||||
|
||||
def register_campaign(campaign: ba.Campaign) -> None:
|
||||
def register_campaign(campaign: bascenev1.Campaign) -> None:
|
||||
"""Register a new campaign."""
|
||||
_ba.app.campaigns[campaign.name] = campaign
|
||||
|
||||
|
||||
def getcampaign(name: str) -> ba.Campaign:
|
||||
"""Return a campaign by name."""
|
||||
return _ba.app.campaigns[name]
|
||||
assert babase.app.classic is not None
|
||||
babase.app.classic.campaigns[campaign.name] = campaign
|
||||
|
||||
|
||||
class Campaign:
|
||||
"""Represents a unique set or series of ba.Level-s.
|
||||
"""Represents a unique set or series of baclassic.Level-s.
|
||||
|
||||
Category: **App Classes**
|
||||
"""
|
||||
|
|
@ -32,11 +28,11 @@ class Campaign:
|
|||
self,
|
||||
name: str,
|
||||
sequential: bool = True,
|
||||
levels: list[ba.Level] | None = None,
|
||||
levels: list[bascenev1.Level] | None = None,
|
||||
):
|
||||
self._name = name
|
||||
self._sequential = sequential
|
||||
self._levels: list[ba.Level] = []
|
||||
self._levels: list[bascenev1.Level] = []
|
||||
if levels is not None:
|
||||
for level in levels:
|
||||
self.addlevel(level)
|
||||
|
|
@ -51,8 +47,10 @@ class Campaign:
|
|||
"""Whether this Campaign's levels must be played in sequence."""
|
||||
return self._sequential
|
||||
|
||||
def addlevel(self, level: ba.Level, index: int | None = None) -> None:
|
||||
"""Adds a ba.Level to the Campaign."""
|
||||
def addlevel(
|
||||
self, level: bascenev1.Level, index: int | None = None
|
||||
) -> None:
|
||||
"""Adds a baclassic.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))
|
||||
|
|
@ -62,30 +60,30 @@ class Campaign:
|
|||
self._levels.insert(index, level)
|
||||
|
||||
@property
|
||||
def levels(self) -> list[ba.Level]:
|
||||
"""The list of ba.Level-s in the Campaign."""
|
||||
def levels(self) -> list[bascenev1.Level]:
|
||||
"""The list of baclassic.Level-s in the Campaign."""
|
||||
return self._levels
|
||||
|
||||
def getlevel(self, name: str) -> ba.Level:
|
||||
"""Return a contained ba.Level by name."""
|
||||
from ba import _error
|
||||
def getlevel(self, name: str) -> bascenev1.Level:
|
||||
"""Return a contained baclassic.Level by name."""
|
||||
|
||||
for level in self._levels:
|
||||
if level.name == name:
|
||||
return level
|
||||
raise _error.NotFoundError(
|
||||
raise babase.NotFoundError(
|
||||
"Level '" + name + "' not found in campaign '" + self.name + "'"
|
||||
)
|
||||
|
||||
def reset(self) -> None:
|
||||
"""Reset state for the Campaign."""
|
||||
_ba.app.config.setdefault('Campaigns', {})[self._name] = {}
|
||||
babase.app.config.setdefault('Campaigns', {})[self._name] = {}
|
||||
|
||||
# FIXME should these give/take ba.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
|
||||
_ba.app.config.commit()
|
||||
babase.app.config.commit()
|
||||
|
||||
def get_selected_level(self) -> str:
|
||||
"""Return the name of the Level currently selected in the UI."""
|
||||
|
|
@ -94,7 +92,7 @@ class Campaign:
|
|||
@property
|
||||
def configdict(self) -> dict[str, Any]:
|
||||
"""Return the live config dict for this campaign."""
|
||||
val: dict[str, Any] = _ba.app.config.setdefault(
|
||||
val: dict[str, Any] = babase.app.config.setdefault(
|
||||
'Campaigns', {}
|
||||
).setdefault(self._name, {})
|
||||
assert isinstance(val, dict)
|
||||
|
|
@ -104,19 +102,19 @@ class Campaign:
|
|||
def init_campaigns() -> None:
|
||||
"""Fill out initial default Campaigns."""
|
||||
# pylint: disable=cyclic-import
|
||||
from ba._level import Level
|
||||
from bastd.game.onslaught import OnslaughtGame
|
||||
from bastd.game.football import FootballCoopGame
|
||||
from bastd.game.runaround import RunaroundGame
|
||||
from bastd.game.thelaststand import TheLastStandGame
|
||||
from bastd.game.race import RaceGame
|
||||
from bastd.game.targetpractice import TargetPracticeGame
|
||||
from bastd.game.meteorshower import MeteorShowerGame
|
||||
from bastd.game.easteregghunt import EasterEggHuntGame
|
||||
from bastd.game.ninjafight import NinjaFightGame
|
||||
from bascenev1._level import Level
|
||||
from bascenev1lib.game.onslaught import OnslaughtGame
|
||||
from bascenev1lib.game.football import FootballCoopGame
|
||||
from bascenev1lib.game.runaround import RunaroundGame
|
||||
from bascenev1lib.game.thelaststand import TheLastStandGame
|
||||
from bascenev1lib.game.race import RaceGame
|
||||
from bascenev1lib.game.targetpractice import TargetPracticeGame
|
||||
from bascenev1lib.game.meteorshower import MeteorShowerGame
|
||||
from bascenev1lib.game.easteregghunt import EasterEggHuntGame
|
||||
from bascenev1lib.game.ninjafight import NinjaFightGame
|
||||
|
||||
# TODO: Campaigns should be load-on-demand; not all imported at launch
|
||||
# like this.
|
||||
# like this.
|
||||
|
||||
# FIXME: Once translations catch up, we can convert these to use the
|
||||
# generic display-name '${GAME} Training' type stuff.
|
||||
|
|
@ -6,11 +6,11 @@ from __future__ import annotations
|
|||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import _ba
|
||||
from ba._error import NodeNotFoundError
|
||||
import babase
|
||||
import _bascenev1
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import ba
|
||||
import bascenev1
|
||||
|
||||
|
||||
class Collision:
|
||||
|
|
@ -20,42 +20,42 @@ class Collision:
|
|||
"""
|
||||
|
||||
@property
|
||||
def position(self) -> ba.Vec3:
|
||||
def position(self) -> bascenev1.Vec3:
|
||||
"""The position of the current collision."""
|
||||
return _ba.Vec3(_ba.get_collision_info('position'))
|
||||
return babase.Vec3(_bascenev1.get_collision_info('position'))
|
||||
|
||||
@property
|
||||
def sourcenode(self) -> ba.Node:
|
||||
def sourcenode(self) -> bascenev1.Node:
|
||||
"""The node containing the material triggering the current callback.
|
||||
|
||||
Throws a ba.NodeNotFoundError if the node does not exist, though
|
||||
the node should always exist (at least at the start of the collision
|
||||
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).
|
||||
"""
|
||||
node = _ba.get_collision_info('sourcenode')
|
||||
assert isinstance(node, (_ba.Node, type(None)))
|
||||
node = _bascenev1.get_collision_info('sourcenode')
|
||||
assert isinstance(node, (_bascenev1.Node, type(None)))
|
||||
if not node:
|
||||
raise NodeNotFoundError()
|
||||
raise babase.NodeNotFoundError()
|
||||
return node
|
||||
|
||||
@property
|
||||
def opposingnode(self) -> ba.Node:
|
||||
def opposingnode(self) -> bascenev1.Node:
|
||||
"""The node the current callback material node is hitting.
|
||||
|
||||
Throws a ba.NodeNotFoundError if the node does not exist.
|
||||
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.
|
||||
"""
|
||||
node = _ba.get_collision_info('opposingnode')
|
||||
assert isinstance(node, (_ba.Node, type(None)))
|
||||
node = _bascenev1.get_collision_info('opposingnode')
|
||||
assert isinstance(node, (_bascenev1.Node, type(None)))
|
||||
if not node:
|
||||
raise NodeNotFoundError()
|
||||
raise babase.NodeNotFoundError()
|
||||
return node
|
||||
|
||||
@property
|
||||
def opposingbody(self) -> int:
|
||||
"""The body index on the opposing node in the current collision."""
|
||||
body = _ba.get_collision_info('opposingbody')
|
||||
body = _bascenev1.get_collision_info('opposingbody')
|
||||
assert isinstance(body, int)
|
||||
return body
|
||||
|
||||
|
|
@ -3,36 +3,39 @@
|
|||
"""Functionality related to co-op games."""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, TypeVar
|
||||
|
||||
import _ba
|
||||
from ba import _internal
|
||||
from ba._gameactivity import GameActivity
|
||||
from ba._general import WeakCall
|
||||
import babase
|
||||
|
||||
import _bascenev1
|
||||
from bascenev1._gameactivity import GameActivity
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Sequence
|
||||
from bastd.actor.playerspaz import PlayerSpaz
|
||||
import ba
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
PlayerType = TypeVar('PlayerType', bound='ba.Player')
|
||||
TeamType = TypeVar('TeamType', bound='ba.Team')
|
||||
# pylint: enable=invalid-name
|
||||
from bascenev1lib.actor.playerspaz import PlayerSpaz
|
||||
|
||||
import bascenev1
|
||||
|
||||
PlayerT = TypeVar('PlayerT', bound='bascenev1.Player')
|
||||
TeamT = TypeVar('TeamT', bound='bascenev1.Team')
|
||||
|
||||
|
||||
class CoopGameActivity(GameActivity[PlayerType, TeamType]):
|
||||
class CoopGameActivity(GameActivity[PlayerT, TeamT]):
|
||||
"""Base class for cooperative-mode games.
|
||||
|
||||
Category: **Gameplay Classes**
|
||||
"""
|
||||
|
||||
# We can assume our session is a CoopSession.
|
||||
session: ba.CoopSession
|
||||
session: bascenev1.CoopSession
|
||||
|
||||
@classmethod
|
||||
def supports_session_type(cls, sessiontype: type[ba.Session]) -> bool:
|
||||
from ba._coopsession import CoopSession
|
||||
def supports_session_type(
|
||||
cls, sessiontype: type[bascenev1.Session]
|
||||
) -> bool:
|
||||
from bascenev1._coopsession import CoopSession
|
||||
|
||||
return issubclass(sessiontype, CoopSession)
|
||||
|
||||
|
|
@ -42,19 +45,21 @@ class CoopGameActivity(GameActivity[PlayerType, TeamType]):
|
|||
# Cache these for efficiency.
|
||||
self._achievements_awarded: set[str] = set()
|
||||
|
||||
self._life_warning_beep: ba.Actor | None = None
|
||||
self._life_warning_beep_timer: ba.Timer | None = None
|
||||
self._warn_beeps_sound = _ba.getsound('warnBeeps')
|
||||
self._life_warning_beep: bascenev1.Actor | None = None
|
||||
self._life_warning_beep_timer: bascenev1.Timer | None = None
|
||||
self._warn_beeps_sound = _bascenev1.getsound('warnBeeps')
|
||||
|
||||
def on_begin(self) -> None:
|
||||
super().on_begin()
|
||||
|
||||
# Show achievements remaining.
|
||||
if not (_ba.app.demo_mode or _ba.app.arcade_mode):
|
||||
_ba.timer(3.8, WeakCall(self._show_remaining_achievements))
|
||||
if not (babase.app.demo_mode or babase.app.arcade_mode):
|
||||
_bascenev1.timer(
|
||||
3.8, babase.WeakCall(self._show_remaining_achievements)
|
||||
)
|
||||
|
||||
# Preload achievement images in case we get some.
|
||||
_ba.timer(2.0, WeakCall(self._preload_achievements))
|
||||
_bascenev1.timer(2.0, babase.WeakCall(self._preload_achievements))
|
||||
|
||||
# FIXME: this is now redundant with activityutils.getscoreconfig();
|
||||
# need to kill this.
|
||||
|
|
@ -75,14 +80,15 @@ class CoopGameActivity(GameActivity[PlayerType, TeamType]):
|
|||
a wave.
|
||||
duration is given in seconds.
|
||||
"""
|
||||
from ba._messages import CelebrateMessage
|
||||
from bascenev1._messages import CelebrateMessage
|
||||
|
||||
for player in self.players:
|
||||
if player.actor:
|
||||
player.actor.handlemessage(CelebrateMessage(duration))
|
||||
|
||||
def _preload_achievements(self) -> None:
|
||||
achievements = _ba.app.ach.achievements_for_coop_level(
|
||||
assert babase.app.classic is not None
|
||||
achievements = babase.app.classic.ach.achievements_for_coop_level(
|
||||
self._get_coop_level_name()
|
||||
)
|
||||
for ach in achievements:
|
||||
|
|
@ -90,22 +96,22 @@ class CoopGameActivity(GameActivity[PlayerType, TeamType]):
|
|||
|
||||
def _show_remaining_achievements(self) -> None:
|
||||
# pylint: disable=cyclic-import
|
||||
from ba._language import Lstr
|
||||
from bastd.actor.text import Text
|
||||
from bascenev1lib.actor.text import Text
|
||||
|
||||
assert babase.app.classic is not None
|
||||
ts_h_offs = 30
|
||||
v_offs = -200
|
||||
achievements = [
|
||||
a
|
||||
for a in _ba.app.ach.achievements_for_coop_level(
|
||||
for a in babase.app.classic.ach.achievements_for_coop_level(
|
||||
self._get_coop_level_name()
|
||||
)
|
||||
if not a.complete
|
||||
]
|
||||
vrmode = _ba.app.vr_mode
|
||||
vrmode = babase.app.vr_mode
|
||||
if achievements:
|
||||
Text(
|
||||
Lstr(resource='achievementsRemainingText'),
|
||||
babase.Lstr(resource='achievementsRemainingText'),
|
||||
host_only=True,
|
||||
position=(ts_h_offs - 10 + 40, v_offs - 10),
|
||||
transition=Text.Transition.FADE_IN,
|
||||
|
|
@ -134,7 +140,7 @@ class CoopGameActivity(GameActivity[PlayerType, TeamType]):
|
|||
|
||||
def spawn_player_spaz(
|
||||
self,
|
||||
player: PlayerType,
|
||||
player: PlayerT,
|
||||
position: Sequence[float] = (0.0, 0.0, 0.0),
|
||||
angle: float | None = None,
|
||||
) -> PlayerSpaz:
|
||||
|
|
@ -154,10 +160,18 @@ class CoopGameActivity(GameActivity[PlayerType, TeamType]):
|
|||
False otherwise
|
||||
"""
|
||||
|
||||
classic = babase.app.classic
|
||||
plus = babase.app.plus
|
||||
if classic is None or plus is None:
|
||||
logging.warning(
|
||||
'_award_achievement is a no-op without classic and plus.'
|
||||
)
|
||||
return
|
||||
|
||||
if achievement_name in self._achievements_awarded:
|
||||
return
|
||||
|
||||
ach = _ba.app.ach.get_achievement(achievement_name)
|
||||
ach = classic.ach.get_achievement(achievement_name)
|
||||
|
||||
# If we're in the easy campaign and this achievement is hard-mode-only,
|
||||
# ignore it.
|
||||
|
|
@ -167,9 +181,7 @@ class CoopGameActivity(GameActivity[PlayerType, TeamType]):
|
|||
if ach.hard_mode_only and campaign.name == 'Easy':
|
||||
return
|
||||
except Exception:
|
||||
from ba._error import print_exception
|
||||
|
||||
print_exception()
|
||||
logging.exception('Error in _award_achievement.')
|
||||
|
||||
# If we haven't awarded this one, check to see if we've got it.
|
||||
# If not, set it through the game service *and* add a transaction
|
||||
|
|
@ -178,10 +190,10 @@ class CoopGameActivity(GameActivity[PlayerType, TeamType]):
|
|||
self._achievements_awarded.add(achievement_name)
|
||||
|
||||
# Report new achievements to the game-service.
|
||||
_internal.report_achievement(achievement_name)
|
||||
plus.report_achievement(achievement_name)
|
||||
|
||||
# ...and to our account.
|
||||
_internal.add_transaction(
|
||||
plus.add_v1_account_transaction(
|
||||
{'type': 'ACHIEVEMENT', 'name': achievement_name}
|
||||
)
|
||||
|
||||
|
|
@ -190,10 +202,10 @@ class CoopGameActivity(GameActivity[PlayerType, TeamType]):
|
|||
|
||||
def fade_to_red(self) -> None:
|
||||
"""Fade the screen to red; (such as when the good guys have lost)."""
|
||||
from ba import _gameutils
|
||||
from bascenev1 import _gameutils
|
||||
|
||||
c_existing = self.globalsnode.tint
|
||||
cnode = _ba.newnode(
|
||||
cnode = _bascenev1.newnode(
|
||||
'combine',
|
||||
attrs={
|
||||
'input0': c_existing[0],
|
||||
|
|
@ -209,8 +221,8 @@ class CoopGameActivity(GameActivity[PlayerType, TeamType]):
|
|||
def setup_low_life_warning_sound(self) -> None:
|
||||
"""Set up a beeping noise to play when any players are near death."""
|
||||
self._life_warning_beep = None
|
||||
self._life_warning_beep_timer = _ba.Timer(
|
||||
1.0, WeakCall(self._update_life_warning), repeat=True
|
||||
self._life_warning_beep_timer = _bascenev1.Timer(
|
||||
1.0, babase.WeakCall(self._update_life_warning), repeat=True
|
||||
)
|
||||
|
||||
def _update_life_warning(self) -> None:
|
||||
|
|
@ -224,10 +236,10 @@ class CoopGameActivity(GameActivity[PlayerType, TeamType]):
|
|||
should_beep = True
|
||||
break
|
||||
if should_beep and self._life_warning_beep is None:
|
||||
from ba._nodeactor import NodeActor
|
||||
from bascenev1._nodeactor import NodeActor
|
||||
|
||||
self._life_warning_beep = NodeActor(
|
||||
_ba.newnode(
|
||||
_bascenev1.newnode(
|
||||
'sound',
|
||||
attrs={
|
||||
'sound': self._warn_beeps_sound,
|
||||
|
|
@ -5,65 +5,64 @@ from __future__ import annotations
|
|||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import _ba
|
||||
from ba._session import Session
|
||||
import babase
|
||||
|
||||
import _bascenev1
|
||||
from bascenev1._session import Session
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any, Callable, Sequence
|
||||
import ba
|
||||
|
||||
import bascenev1
|
||||
|
||||
TEAM_COLORS = [(0.2, 0.4, 1.6)]
|
||||
TEAM_NAMES = ['Humans']
|
||||
TEAM_NAMES = ['Good Guys']
|
||||
|
||||
|
||||
class CoopSession(Session):
|
||||
"""A ba.Session which runs cooperative-mode games.
|
||||
"""A bascenev1.Session which runs cooperative-mode games.
|
||||
|
||||
Category: Gameplay Classes
|
||||
Category: **Gameplay Classes**
|
||||
|
||||
These generally consist of 1-4 players against
|
||||
the computer and include functionality such as
|
||||
high score lists.
|
||||
|
||||
Attributes:
|
||||
|
||||
campaign
|
||||
The ba.Campaign instance this Session represents, or None if
|
||||
there is no associated Campaign.
|
||||
"""
|
||||
|
||||
use_teams = True
|
||||
use_team_colors = False
|
||||
allow_mid_activity_joins = True
|
||||
allow_mid_activity_joins = False
|
||||
|
||||
# Note: even though these are instance vars, we annotate them at the
|
||||
# class level so that docs generation can access their types.
|
||||
|
||||
campaign: ba.Campaign | None
|
||||
"""The ba.Campaign instance this Session represents, or None if
|
||||
campaign: bascenev1.Campaign | None
|
||||
"""The baclassic.Campaign instance this Session represents, or None if
|
||||
there is no associated Campaign."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Instantiate a co-op mode session."""
|
||||
# pylint: disable=cyclic-import
|
||||
from ba._campaign import getcampaign
|
||||
from bastd.activity.coopjoin import CoopJoinActivity
|
||||
from bascenev1lib.activity.coopjoin import CoopJoinActivity
|
||||
|
||||
_ba.increment_analytics_count('Co-op session start')
|
||||
app = _ba.app
|
||||
babase.increment_analytics_count('Co-op session start')
|
||||
app = babase.app
|
||||
classic = app.classic
|
||||
assert classic is not None
|
||||
|
||||
# If they passed in explicit min/max, honor that.
|
||||
# Otherwise defer to user overrides or defaults.
|
||||
if 'min_players' in app.coop_session_args:
|
||||
min_players = app.coop_session_args['min_players']
|
||||
if 'min_players' in classic.coop_session_args:
|
||||
min_players = classic.coop_session_args['min_players']
|
||||
else:
|
||||
min_players = 1
|
||||
if 'max_players' in app.coop_session_args:
|
||||
max_players = app.coop_session_args['max_players']
|
||||
if 'max_players' in classic.coop_session_args:
|
||||
max_players = classic.coop_session_args['max_players']
|
||||
else:
|
||||
max_players = app.config.get('Coop Game Max Players', 4)
|
||||
|
||||
# print('FIXME: COOP SESSION WOULD CALC DEPS.')
|
||||
depsets: Sequence[ba.DependencySet] = []
|
||||
depsets: Sequence[bascenev1.DependencySet] = []
|
||||
|
||||
super().__init__(
|
||||
depsets,
|
||||
|
|
@ -74,41 +73,48 @@ class CoopSession(Session):
|
|||
)
|
||||
|
||||
# Tournament-ID if we correspond to a co-op tournament (otherwise None)
|
||||
self.tournament_id: str | None = app.coop_session_args.get(
|
||||
self.tournament_id: str | None = classic.coop_session_args.get(
|
||||
'tournament_id'
|
||||
)
|
||||
|
||||
self.campaign = getcampaign(app.coop_session_args['campaign'])
|
||||
self.campaign_level_name: str = app.coop_session_args['level']
|
||||
self.campaign = classic.getcampaign(
|
||||
classic.coop_session_args['campaign']
|
||||
)
|
||||
self.campaign_level_name: str = classic.coop_session_args['level']
|
||||
|
||||
self._ran_tutorial_activity = False
|
||||
self._tutorial_activity: ba.Activity | None = None
|
||||
self._tutorial_activity: bascenev1.Activity | None = None
|
||||
self._custom_menu_ui: list[dict[str, Any]] = []
|
||||
|
||||
# Start our joining screen.
|
||||
self.setactivity(_ba.newactivity(CoopJoinActivity))
|
||||
self.setactivity(_bascenev1.newactivity(CoopJoinActivity))
|
||||
|
||||
self._next_game_instance: ba.GameActivity | None = None
|
||||
self._next_game_instance: bascenev1.GameActivity | None = None
|
||||
self._next_game_level_name: str | None = None
|
||||
self._update_on_deck_game_instances()
|
||||
|
||||
def get_current_game_instance(self) -> ba.GameActivity:
|
||||
def get_current_game_instance(self) -> bascenev1.GameActivity:
|
||||
"""Get the game instance currently being played."""
|
||||
return self._current_game_instance
|
||||
|
||||
def should_allow_mid_activity_joins(self, activity: ba.Activity) -> bool:
|
||||
def should_allow_mid_activity_joins(
|
||||
self, activity: bascenev1.Activity
|
||||
) -> bool:
|
||||
# pylint: disable=cyclic-import
|
||||
# from ba._gameactivity import GameActivity
|
||||
from bascenev1._gameactivity import GameActivity
|
||||
|
||||
# Disallow any joins in the middle of the game.
|
||||
# if isinstance(activity, GameActivity):
|
||||
# return False
|
||||
if isinstance(activity, GameActivity):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def _update_on_deck_game_instances(self) -> None:
|
||||
# pylint: disable=cyclic-import
|
||||
from ba._gameactivity import GameActivity
|
||||
from bascenev1._gameactivity import GameActivity
|
||||
|
||||
classic = babase.app.classic
|
||||
assert classic is not None
|
||||
|
||||
# Instantiate levels we may be running soon to let them load in the bg.
|
||||
|
||||
|
|
@ -124,7 +130,7 @@ class CoopSession(Session):
|
|||
if setting.name not in settings:
|
||||
settings[setting.name] = setting.default
|
||||
|
||||
newactivity = _ba.newactivity(gametype, settings)
|
||||
newactivity = _bascenev1.newactivity(gametype, settings)
|
||||
assert isinstance(newactivity, GameActivity)
|
||||
self._current_game_instance: GameActivity = newactivity
|
||||
|
||||
|
|
@ -132,14 +138,11 @@ class CoopSession(Session):
|
|||
levels = self.campaign.levels
|
||||
level = self.campaign.getlevel(self.campaign_level_name)
|
||||
|
||||
|
||||
nextlevel: ba.Level | None
|
||||
# if level.index < len(levels) - 1:
|
||||
# nextlevel = levels[level.index + 1]
|
||||
# else:
|
||||
# nextlevel = None
|
||||
nextlevel=levels[(level.index+1)%len(levels)]
|
||||
|
||||
nextlevel: bascenev1.Level | None
|
||||
if level.index < len(levels) - 1:
|
||||
nextlevel = levels[level.index + 1]
|
||||
else:
|
||||
nextlevel = None
|
||||
if nextlevel:
|
||||
gametype = nextlevel.gametype
|
||||
settings = nextlevel.get_settings()
|
||||
|
|
@ -151,7 +154,7 @@ class CoopSession(Session):
|
|||
settings[setting.name] = setting.default
|
||||
|
||||
# We wanna be in the activity's context while taking it down.
|
||||
newactivity = _ba.newactivity(gametype, settings)
|
||||
newactivity = _bascenev1.newactivity(gametype, settings)
|
||||
assert isinstance(newactivity, GameActivity)
|
||||
self._next_game_instance = newactivity
|
||||
self._next_game_level_name = nextlevel.name
|
||||
|
|
@ -167,24 +170,22 @@ class CoopSession(Session):
|
|||
and self._tutorial_activity is None
|
||||
and not self._ran_tutorial_activity
|
||||
):
|
||||
from bastd.tutorial import TutorialActivity
|
||||
from bascenev1lib.tutorial import TutorialActivity
|
||||
|
||||
self._tutorial_activity = _ba.newactivity(TutorialActivity)
|
||||
self._tutorial_activity = _bascenev1.newactivity(TutorialActivity)
|
||||
|
||||
def get_custom_menu_entries(self) -> list[dict[str, Any]]:
|
||||
return self._custom_menu_ui
|
||||
|
||||
def on_player_leave(self, sessionplayer: ba.SessionPlayer) -> None:
|
||||
from ba._general import WeakCall
|
||||
|
||||
def on_player_leave(self, sessionplayer: bascenev1.SessionPlayer) -> None:
|
||||
super().on_player_leave(sessionplayer)
|
||||
|
||||
_ba.timer(2.0, WeakCall(self._handle_empty_activity))
|
||||
_bascenev1.timer(2.0, babase.WeakCall(self._handle_empty_activity))
|
||||
|
||||
def _handle_empty_activity(self) -> None:
|
||||
"""Handle cases where all players have left the current activity."""
|
||||
|
||||
from ba._gameactivity import GameActivity
|
||||
from bascenev1._gameactivity import GameActivity
|
||||
|
||||
activity = self.getactivity()
|
||||
if activity is None:
|
||||
|
|
@ -197,11 +198,9 @@ class CoopSession(Session):
|
|||
# If there are *not* players in the current activity but there
|
||||
# *are* in the session:
|
||||
if not activity.players and self.sessionplayers:
|
||||
|
||||
# If we're in a game, we should restart to pull in players
|
||||
# currently waiting in the session.
|
||||
if isinstance(activity, GameActivity):
|
||||
|
||||
# Never restart tourney games however; just end the session
|
||||
# if all players are gone.
|
||||
if self.tournament_id is not None:
|
||||
|
|
@ -212,25 +211,25 @@ class CoopSession(Session):
|
|||
# Hmm; no players anywhere. Let's end the entire session if we're
|
||||
# running a GUI (or just the current game if we're running headless).
|
||||
else:
|
||||
if not _ba.app.headless_mode:
|
||||
if not babase.app.headless_mode:
|
||||
self.end()
|
||||
else:
|
||||
if isinstance(activity, GameActivity):
|
||||
with _ba.Context(activity):
|
||||
with activity.context:
|
||||
activity.end_game()
|
||||
|
||||
def _on_tournament_restart_menu_press(
|
||||
self, resume_callback: Callable[[], Any]
|
||||
) -> None:
|
||||
# pylint: disable=cyclic-import
|
||||
from bastd.ui.tournamententry import TournamentEntryWindow
|
||||
from ba._gameactivity import GameActivity
|
||||
from bascenev1._gameactivity import GameActivity
|
||||
|
||||
assert babase.app.classic is not None
|
||||
activity = self.getactivity()
|
||||
if activity is not None and not activity.expired:
|
||||
assert self.tournament_id is not None
|
||||
assert isinstance(activity, GameActivity)
|
||||
TournamentEntryWindow(
|
||||
babase.app.classic.tournament_entry_window(
|
||||
tournament_id=self.tournament_id,
|
||||
tournament_activity=activity,
|
||||
on_close_call=resume_callback,
|
||||
|
|
@ -253,10 +252,13 @@ class CoopSession(Session):
|
|||
activity = self.getactivity()
|
||||
if activity is not None and not activity.expired:
|
||||
activity.can_show_ad_on_death = True
|
||||
with _ba.Context(activity):
|
||||
with activity.context:
|
||||
activity.end(results={'outcome': 'restart'}, force=True)
|
||||
|
||||
def on_activity_end(self, activity: ba.Activity, results: Any) -> None:
|
||||
# noinspection PyUnresolvedReferences
|
||||
def on_activity_end(
|
||||
self, activity: bascenev1.Activity, results: Any
|
||||
) -> None:
|
||||
"""Method override for co-op sessions.
|
||||
|
||||
Jumps between co-op games and score screens.
|
||||
|
|
@ -265,17 +267,18 @@ class CoopSession(Session):
|
|||
# pylint: disable=too-many-locals
|
||||
# pylint: disable=too-many-statements
|
||||
# pylint: disable=cyclic-import
|
||||
from ba._activitytypes import JoinActivity, TransitionActivity
|
||||
from ba._language import Lstr
|
||||
from ba._general import WeakCall
|
||||
from ba._coopgame import CoopGameActivity
|
||||
from ba._gameresults import GameResults
|
||||
from ba._score import ScoreType
|
||||
from ba._player import PlayerInfo
|
||||
from bastd.tutorial import TutorialActivity
|
||||
from bastd.activity.coopscore import CoopScoreScreen
|
||||
from bascenev1lib.activity.coopscore import CoopScoreScreen
|
||||
from bascenev1lib.tutorial import TutorialActivity
|
||||
|
||||
app = _ba.app
|
||||
from bascenev1._gameresults import GameResults
|
||||
from bascenev1._player import PlayerInfo
|
||||
from bascenev1._activitytypes import JoinActivity, TransitionActivity
|
||||
from bascenev1._coopgame import CoopGameActivity
|
||||
from bascenev1._score import ScoreType
|
||||
|
||||
app = babase.app
|
||||
classic = app.classic
|
||||
assert classic is not None
|
||||
|
||||
# If we're running a TeamGameActivity we'll have a GameResults
|
||||
# as results. Otherwise its an old CoopGameActivity so its giving
|
||||
|
|
@ -288,7 +291,7 @@ class CoopSession(Session):
|
|||
# If we're running with a gui and at any point we have no
|
||||
# in-game players, quit out of the session (this can happen if
|
||||
# someone leaves in the tutorial for instance).
|
||||
if not _ba.app.headless_mode:
|
||||
if not babase.app.headless_mode:
|
||||
active_players = [p for p in self.sessionplayers if p.in_game]
|
||||
if not active_players:
|
||||
self.end()
|
||||
|
|
@ -296,19 +299,9 @@ class CoopSession(Session):
|
|||
|
||||
# If we're in a between-round activity or a restart-activity,
|
||||
# hop into a round.
|
||||
|
||||
|
||||
if outcome=="victory" or outcome=="restart" or outcome=="defeat":
|
||||
outcome = 'next_level'
|
||||
|
||||
|
||||
|
||||
if (isinstance(activity,
|
||||
(JoinActivity, CoopScoreScreen, TransitionActivity))) or True:
|
||||
|
||||
from features import team_balancer
|
||||
team_balancer.checkToExitCoop()
|
||||
|
||||
if isinstance(
|
||||
activity, (JoinActivity, CoopScoreScreen, TransitionActivity)
|
||||
):
|
||||
if outcome == 'next_level':
|
||||
if self._next_game_instance is None:
|
||||
raise RuntimeError()
|
||||
|
|
@ -335,11 +328,9 @@ class CoopSession(Session):
|
|||
|
||||
# Normal case; launch the next round.
|
||||
else:
|
||||
|
||||
# Reset stats for the new activity.
|
||||
self.stats.reset()
|
||||
for player in self.sessionplayers:
|
||||
|
||||
# Skip players that are still choosing a team.
|
||||
if player.in_game:
|
||||
self.stats.register_sessionplayer(player)
|
||||
|
|
@ -352,9 +343,9 @@ class CoopSession(Session):
|
|||
if self.tournament_id is not None:
|
||||
self._custom_menu_ui = [
|
||||
{
|
||||
'label': Lstr(resource='restartText'),
|
||||
'label': babase.Lstr(resource='restartText'),
|
||||
'resume_on_call': False,
|
||||
'call': WeakCall(
|
||||
'call': babase.WeakCall(
|
||||
self._on_tournament_restart_menu_press
|
||||
),
|
||||
}
|
||||
|
|
@ -362,22 +353,17 @@ class CoopSession(Session):
|
|||
else:
|
||||
self._custom_menu_ui = [
|
||||
{
|
||||
'label': Lstr(resource='restartText'),
|
||||
'call': WeakCall(self.restart),
|
||||
'label': babase.Lstr(resource='restartText'),
|
||||
'call': babase.WeakCall(self.restart),
|
||||
}
|
||||
]
|
||||
|
||||
# If we were in a tutorial, just pop a transition to get to the
|
||||
# actual round.
|
||||
elif isinstance(activity, TutorialActivity):
|
||||
self.setactivity(_ba.newactivity(TransitionActivity))
|
||||
|
||||
self.setactivity(_bascenev1.newactivity(TransitionActivity))
|
||||
else:
|
||||
pass
|
||||
if False:
|
||||
|
||||
|
||||
playerinfos: list[ba.PlayerInfo]
|
||||
playerinfos: list[bascenev1.PlayerInfo]
|
||||
|
||||
# Generic team games.
|
||||
if isinstance(results, GameResults):
|
||||
|
|
@ -431,17 +417,16 @@ class CoopSession(Session):
|
|||
# Validate types.
|
||||
if playerinfos is not None:
|
||||
assert isinstance(playerinfos, list)
|
||||
assert (isinstance(i, PlayerInfo) for i in playerinfos)
|
||||
assert all(isinstance(i, PlayerInfo) for i in playerinfos)
|
||||
|
||||
# Looks like we were in a round - check the outcome and
|
||||
# go from there.
|
||||
if outcome == 'restart':
|
||||
|
||||
# This will pop up back in the same round.
|
||||
self.setactivity(_ba.newactivity(TransitionActivity))
|
||||
self.setactivity(_bascenev1.newactivity(TransitionActivity))
|
||||
else:
|
||||
self.setactivity(
|
||||
_ba.newactivity(
|
||||
_bascenev1.newactivity(
|
||||
CoopScoreScreen,
|
||||
{
|
||||
'playerinfos': playerinfos,
|
||||
68
dist/ba_data/python/bascenev1/_debug.py
vendored
Normal file
68
dist/ba_data/python/bascenev1/_debug.py
vendored
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Debugging functionality."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import babase
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any
|
||||
|
||||
import bascenev1
|
||||
|
||||
|
||||
def print_live_object_warnings(
|
||||
when: Any,
|
||||
ignore_session: bascenev1.Session | None = None,
|
||||
ignore_activity: bascenev1.Activity | None = None,
|
||||
) -> None:
|
||||
"""Print warnings for remaining objects in the current context.
|
||||
|
||||
IMPORTANT - don't call this in production; usage of gc.get_objects()
|
||||
can bork Python. See notes at top of efro.debug module.
|
||||
"""
|
||||
# pylint: disable=cyclic-import
|
||||
import gc
|
||||
|
||||
from bascenev1._session import Session
|
||||
from bascenev1._actor import Actor
|
||||
from bascenev1._activity import Activity
|
||||
|
||||
assert babase.app.classic is not None
|
||||
|
||||
sessions: list[bascenev1.Session] = []
|
||||
activities: list[bascenev1.Activity] = []
|
||||
actors: list[bascenev1.Actor] = []
|
||||
|
||||
# Once we come across leaked stuff, printing again is probably
|
||||
# redundant.
|
||||
if babase.app.classic.printed_live_object_warning:
|
||||
return
|
||||
for obj in gc.get_objects():
|
||||
if isinstance(obj, Actor):
|
||||
actors.append(obj)
|
||||
elif isinstance(obj, Session):
|
||||
sessions.append(obj)
|
||||
elif isinstance(obj, Activity):
|
||||
activities.append(obj)
|
||||
|
||||
# Complain about any remaining sessions.
|
||||
for session in sessions:
|
||||
if session is ignore_session:
|
||||
continue
|
||||
babase.app.classic.printed_live_object_warning = True
|
||||
print(f'ERROR: Session found {when}: {session}')
|
||||
|
||||
# Complain about any remaining activities.
|
||||
for activity in activities:
|
||||
if activity is ignore_activity:
|
||||
continue
|
||||
babase.app.classic.printed_live_object_warning = True
|
||||
print(f'ERROR: Activity found {when}: {activity}')
|
||||
|
||||
# Complain about any remaining actors.
|
||||
for actor in actors:
|
||||
babase.app.classic.printed_live_object_warning = True
|
||||
print(f'ERROR: Actor found {when}: {actor}')
|
||||
|
|
@ -7,11 +7,14 @@ from __future__ import annotations
|
|||
import weakref
|
||||
from typing import Generic, TypeVar, TYPE_CHECKING
|
||||
|
||||
import _ba
|
||||
import babase
|
||||
|
||||
import _bascenev1
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any
|
||||
import ba
|
||||
|
||||
import bascenev1
|
||||
|
||||
T = TypeVar('T', bound='DependencyComponent')
|
||||
|
||||
|
|
@ -26,13 +29,13 @@ class Dependency(Generic[T]):
|
|||
The class functions as a descriptor, allowing dependencies to
|
||||
be added at a class level much the same as properties or methods
|
||||
and then used with class instances to access those dependencies.
|
||||
For instance, if you do 'floofcls = ba.Dependency(FloofClass)' you
|
||||
would then be able to instantiate a FloofClass in your class's
|
||||
For instance, if you do 'floofcls = bascenev1.Dependency(FloofClass)'
|
||||
you would then be able to instantiate a FloofClass in your class's
|
||||
methods via self.floofcls().
|
||||
"""
|
||||
|
||||
def __init__(self, cls: type[T], config: Any = None):
|
||||
"""Instantiate a Dependency given a ba.DependencyComponent type.
|
||||
"""Instantiate a Dependency given a bascenev1.DependencyComponent type.
|
||||
|
||||
Optionally, an arbitrary object can be passed as 'config' to
|
||||
influence dependency calculation for the target class.
|
||||
|
|
@ -57,7 +60,7 @@ class Dependency(Generic[T]):
|
|||
)
|
||||
raise TypeError(
|
||||
f'Dependency cannot be added to class of type {type(obj)}'
|
||||
' (class must inherit from ba.DependencyComponent).'
|
||||
' (class must inherit from bascenev1.DependencyComponent).'
|
||||
)
|
||||
|
||||
# We expect to be instantiated from an already living
|
||||
|
|
@ -125,7 +128,7 @@ class DependencyComponent:
|
|||
|
||||
|
||||
class DependencyEntry:
|
||||
"""Data associated with a dependency/config pair in a ba.DependencySet."""
|
||||
"""Data associated with a dep/config pair in bascenev1.DependencySet."""
|
||||
|
||||
# def __del__(self) -> None:
|
||||
# print('~DepEntry()', self.cls)
|
||||
|
|
@ -154,7 +157,6 @@ class DependencyEntry:
|
|||
instance._dep_entry = weakref.ref(self)
|
||||
instance.__init__() # type: ignore
|
||||
|
||||
assert self.depset
|
||||
depset = self.depset()
|
||||
assert depset is not None
|
||||
self.component = instance
|
||||
|
|
@ -193,12 +195,12 @@ class DependencySet(Generic[T]):
|
|||
def resolve(self) -> None:
|
||||
"""Resolve the complete set of required dependencies for this set.
|
||||
|
||||
Raises a ba.DependencyError if dependencies are missing (or other
|
||||
Exception types on other errors).
|
||||
Raises a bascenev1.DependencyError if dependencies are missing (or
|
||||
other Exception types on other errors).
|
||||
"""
|
||||
|
||||
if self._resolved:
|
||||
raise Exception('DependencySet has already been resolved.')
|
||||
raise RuntimeError('DependencySet has already been resolved.')
|
||||
|
||||
# print('RESOLVING DEP SET')
|
||||
|
||||
|
|
@ -214,8 +216,6 @@ class DependencySet(Generic[T]):
|
|||
if not entry.cls.dep_is_present(entry.config)
|
||||
]
|
||||
if missing:
|
||||
from ba._error import DependencyError
|
||||
|
||||
raise DependencyError(missing)
|
||||
|
||||
self._resolved = True
|
||||
|
|
@ -233,7 +233,7 @@ class DependencySet(Generic[T]):
|
|||
"""
|
||||
ids: set[str] = set()
|
||||
if not self._resolved:
|
||||
raise Exception('Must be called on a resolved dep-set.')
|
||||
raise RuntimeError('Must be called on a resolved dep-set.')
|
||||
for entry in self.entries.values():
|
||||
if issubclass(entry.cls, AssetPackage):
|
||||
assert isinstance(entry.config, str)
|
||||
|
|
@ -268,7 +268,6 @@ class DependencySet(Generic[T]):
|
|||
return rootdata
|
||||
|
||||
def _resolve(self, dep: Dependency[T], recursion: int) -> None:
|
||||
|
||||
# Watch for wacky infinite dep loops.
|
||||
if recursion > 10:
|
||||
raise RecursionError('Max recursion reached')
|
||||
|
|
@ -297,7 +296,7 @@ class DependencySet(Generic[T]):
|
|||
|
||||
|
||||
class AssetPackage(DependencyComponent):
|
||||
"""ba.DependencyComponent representing a bundled package of game assets.
|
||||
"""bascenev1.DependencyComponent representing a bundled package of assets.
|
||||
|
||||
Category: **Asset Classes**
|
||||
"""
|
||||
|
|
@ -306,7 +305,7 @@ class AssetPackage(DependencyComponent):
|
|||
super().__init__()
|
||||
|
||||
# This is used internally by the get_package_xxx calls.
|
||||
self.context = _ba.Context('current')
|
||||
self.context = babase.ContextRef()
|
||||
|
||||
entry = self._dep_entry()
|
||||
assert entry is not None
|
||||
|
|
@ -323,40 +322,40 @@ class AssetPackage(DependencyComponent):
|
|||
return True
|
||||
return False
|
||||
|
||||
def gettexture(self, name: str) -> ba.Texture:
|
||||
"""Load a named ba.Texture from the AssetPackage.
|
||||
def gettexture(self, name: str) -> bascenev1.Texture:
|
||||
"""Load a named bascenev1.Texture from the AssetPackage.
|
||||
|
||||
Behavior is similar to ba.gettexture()
|
||||
Behavior is similar to bascenev1.gettexture()
|
||||
"""
|
||||
return _ba.get_package_texture(self, name)
|
||||
return _bascenev1.get_package_texture(self, name)
|
||||
|
||||
def getmodel(self, name: str) -> ba.Model:
|
||||
"""Load a named ba.Model from the AssetPackage.
|
||||
def getmesh(self, name: str) -> bascenev1.Mesh:
|
||||
"""Load a named bascenev1.Mesh from the AssetPackage.
|
||||
|
||||
Behavior is similar to ba.getmodel()
|
||||
Behavior is similar to bascenev1.getmesh()
|
||||
"""
|
||||
return _ba.get_package_model(self, name)
|
||||
return _bascenev1.get_package_mesh(self, name)
|
||||
|
||||
def getcollidemodel(self, name: str) -> ba.CollideModel:
|
||||
"""Load a named ba.CollideModel from the AssetPackage.
|
||||
def getcollisionmesh(self, name: str) -> bascenev1.CollisionMesh:
|
||||
"""Load a named bascenev1.CollisionMesh from the AssetPackage.
|
||||
|
||||
Behavior is similar to ba.getcollideModel()
|
||||
Behavior is similar to bascenev1.getcollisionmesh()
|
||||
"""
|
||||
return _ba.get_package_collide_model(self, name)
|
||||
return _bascenev1.get_package_collision_mesh(self, name)
|
||||
|
||||
def getsound(self, name: str) -> ba.Sound:
|
||||
"""Load a named ba.Sound from the AssetPackage.
|
||||
def getsound(self, name: str) -> bascenev1.Sound:
|
||||
"""Load a named bascenev1.Sound from the AssetPackage.
|
||||
|
||||
Behavior is similar to ba.getsound()
|
||||
Behavior is similar to bascenev1.getsound()
|
||||
"""
|
||||
return _ba.get_package_sound(self, name)
|
||||
return _bascenev1.get_package_sound(self, name)
|
||||
|
||||
def getdata(self, name: str) -> ba.Data:
|
||||
"""Load a named ba.Data from the AssetPackage.
|
||||
def getdata(self, name: str) -> bascenev1.Data:
|
||||
"""Load a named bascenev1.Data from the AssetPackage.
|
||||
|
||||
Behavior is similar to ba.getdata()
|
||||
Behavior is similar to bascenev1.getdata()
|
||||
"""
|
||||
return _ba.get_package_data(self, name)
|
||||
return _bascenev1.get_package_data(self, name)
|
||||
|
||||
|
||||
class TestClassFactory(DependencyComponent):
|
||||
|
|
@ -368,7 +367,7 @@ class TestClassFactory(DependencyComponent):
|
|||
super().__init__()
|
||||
print('Instantiating TestClassFactory')
|
||||
self.tex = self._assets.gettexture('black')
|
||||
self.model = self._assets.getmodel('landMine')
|
||||
self.mesh = self._assets.getmesh('landMine')
|
||||
self.sound = self._assets.getsound('error')
|
||||
self.data = self._assets.getdata('langdata')
|
||||
|
||||
|
|
@ -402,8 +401,6 @@ def test_depset() -> None:
|
|||
print('running test_depset()...')
|
||||
|
||||
def doit() -> None:
|
||||
from ba._error import DependencyError
|
||||
|
||||
depset = DependencySet(Dependency(TestClass))
|
||||
try:
|
||||
depset.resolve()
|
||||
|
|
@ -428,4 +425,22 @@ def test_depset() -> None:
|
|||
# To test this, add prints on __del__ for stuff used above;
|
||||
# everything should be dead at this point if we have no cycles.
|
||||
print('everything should be cleaned up...')
|
||||
_ba.quit()
|
||||
babase.quit()
|
||||
|
||||
|
||||
class DependencyError(Exception):
|
||||
"""Exception raised when one or more bascenev1.Dependency items are missing.
|
||||
|
||||
Category: **Exception Classes**
|
||||
|
||||
(this will generally be missing assets).
|
||||
"""
|
||||
|
||||
def __init__(self, deps: list[bascenev1.Dependency]):
|
||||
super().__init__()
|
||||
self._deps = deps
|
||||
|
||||
@property
|
||||
def deps(self) -> list[bascenev1.Dependency]:
|
||||
"""The list of missing dependencies causing this error."""
|
||||
return self._deps
|
||||
|
|
@ -5,15 +5,17 @@ from __future__ import annotations
|
|||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import _ba
|
||||
from ba._multiteamsession import MultiTeamSession
|
||||
import babase
|
||||
|
||||
import _bascenev1
|
||||
from bascenev1._multiteamsession import MultiTeamSession
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import ba
|
||||
import bascenev1
|
||||
|
||||
|
||||
class DualTeamSession(MultiTeamSession):
|
||||
"""ba.Session type for teams mode games.
|
||||
"""bascenev1.Session type for teams mode games.
|
||||
|
||||
Category: **Gameplay Classes**
|
||||
"""
|
||||
|
|
@ -27,22 +29,24 @@ class DualTeamSession(MultiTeamSession):
|
|||
_playlists_var = 'Team Tournament Playlists'
|
||||
|
||||
def __init__(self) -> None:
|
||||
_ba.increment_analytics_count('Teams session start')
|
||||
babase.increment_analytics_count('Teams session start')
|
||||
super().__init__()
|
||||
|
||||
def _switch_to_score_screen(self, results: ba.GameResults) -> None:
|
||||
def _switch_to_score_screen(self, results: bascenev1.GameResults) -> None:
|
||||
# pylint: disable=cyclic-import
|
||||
from bastd.activity.drawscore import DrawScoreScreenActivity
|
||||
from bastd.activity.dualteamscore import TeamVictoryScoreScreenActivity
|
||||
from bastd.activity.multiteamvictory import (
|
||||
from bascenev1lib.activity.multiteamvictory import (
|
||||
TeamSeriesVictoryScoreScreenActivity,
|
||||
)
|
||||
from bascenev1lib.activity.dualteamscore import (
|
||||
TeamVictoryScoreScreenActivity,
|
||||
)
|
||||
from bascenev1lib.activity.drawscore import DrawScoreScreenActivity
|
||||
|
||||
winnergroups = results.winnergroups
|
||||
|
||||
# If everyone has the same score, call it a draw.
|
||||
if len(winnergroups) < 2:
|
||||
self.setactivity(_ba.newactivity(DrawScoreScreenActivity))
|
||||
self.setactivity(_bascenev1.newactivity(DrawScoreScreenActivity))
|
||||
else:
|
||||
winner = winnergroups[0].teams[0]
|
||||
winner.customdata['score'] += 1
|
||||
|
|
@ -50,13 +54,14 @@ class DualTeamSession(MultiTeamSession):
|
|||
# If a team has won, show final victory screen.
|
||||
if winner.customdata['score'] >= (self._series_length - 1) / 2 + 1:
|
||||
self.setactivity(
|
||||
_ba.newactivity(
|
||||
TeamSeriesVictoryScoreScreenActivity, {'winner': winner}
|
||||
_bascenev1.newactivity(
|
||||
TeamSeriesVictoryScoreScreenActivity,
|
||||
{'winner': winner},
|
||||
)
|
||||
)
|
||||
else:
|
||||
self.setactivity(
|
||||
_ba.newactivity(
|
||||
_bascenev1.newactivity(
|
||||
TeamVictoryScoreScreenActivity, {'winner': winner}
|
||||
)
|
||||
)
|
||||
8
dist/ba_data/python/bascenev1/_featureset.py
vendored
Normal file
8
dist/ba_data/python/bascenev1/_featureset.py
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Gather our feature-set functionality from the _babase binary module."""
|
||||
|
||||
# This module only exists to imports/rename the parts of our feature-set
|
||||
# contained in _babase. This allows our internal modules to access this
|
||||
# functionality through us instead of requiring long-winded
|
||||
# feature-set-specific names in _babase.
|
||||
|
|
@ -6,15 +6,17 @@ from __future__ import annotations
|
|||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import _ba
|
||||
from ba._multiteamsession import MultiTeamSession
|
||||
import babase
|
||||
|
||||
import _bascenev1
|
||||
from bascenev1._multiteamsession import MultiTeamSession
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import ba
|
||||
import bascenev1
|
||||
|
||||
|
||||
class FreeForAllSession(MultiTeamSession):
|
||||
"""ba.Session type for free-for-all mode games.
|
||||
"""bascenev1.Session type for free-for-all mode games.
|
||||
|
||||
Category: **Gameplay Classes**
|
||||
"""
|
||||
|
|
@ -48,19 +50,19 @@ class FreeForAllSession(MultiTeamSession):
|
|||
return point_awards
|
||||
|
||||
def __init__(self) -> None:
|
||||
_ba.increment_analytics_count('Free-for-all session start')
|
||||
babase.increment_analytics_count('Free-for-all session start')
|
||||
super().__init__()
|
||||
|
||||
def _switch_to_score_screen(self, results: ba.GameResults) -> None:
|
||||
def _switch_to_score_screen(self, results: bascenev1.GameResults) -> None:
|
||||
# pylint: disable=cyclic-import
|
||||
from efro.util import asserttype
|
||||
from bastd.activity.drawscore import DrawScoreScreenActivity
|
||||
from bastd.activity.multiteamvictory import (
|
||||
from bascenev1lib.activity.multiteamvictory import (
|
||||
TeamSeriesVictoryScoreScreenActivity,
|
||||
)
|
||||
from bastd.activity.freeforallvictory import (
|
||||
from bascenev1lib.activity.freeforallvictory import (
|
||||
FreeForAllVictoryScoreScreenActivity,
|
||||
)
|
||||
from bascenev1lib.activity.drawscore import DrawScoreScreenActivity
|
||||
|
||||
winners = results.winnergroups
|
||||
|
||||
|
|
@ -68,7 +70,9 @@ class FreeForAllSession(MultiTeamSession):
|
|||
# call it a draw.
|
||||
if len(self.sessionplayers) > 1 and len(winners) < 2:
|
||||
self.setactivity(
|
||||
_ba.newactivity(DrawScoreScreenActivity, {'results': results})
|
||||
_bascenev1.newactivity(
|
||||
DrawScoreScreenActivity, {'results': results}
|
||||
)
|
||||
)
|
||||
else:
|
||||
# Award different point amounts based on number of players.
|
||||
|
|
@ -95,14 +99,14 @@ class FreeForAllSession(MultiTeamSession):
|
|||
!= series_winners[1].customdata['score']
|
||||
):
|
||||
self.setactivity(
|
||||
_ba.newactivity(
|
||||
_bascenev1.newactivity(
|
||||
TeamSeriesVictoryScoreScreenActivity,
|
||||
{'winner': series_winners[0]},
|
||||
)
|
||||
)
|
||||
else:
|
||||
self.setactivity(
|
||||
_ba.newactivity(
|
||||
_bascenev1.newactivity(
|
||||
FreeForAllVictoryScoreScreenActivity,
|
||||
{'results': results},
|
||||
)
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -9,11 +9,14 @@ from dataclasses import dataclass
|
|||
from typing import TYPE_CHECKING
|
||||
|
||||
from efro.util import asserttype
|
||||
from ba._team import Team, SessionTeam
|
||||
import babase
|
||||
|
||||
from bascenev1._team import Team, SessionTeam
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Sequence
|
||||
import ba
|
||||
|
||||
import bascenev1
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
@ -21,7 +24,7 @@ class WinnerGroup:
|
|||
"""Entry for a winning team or teams calculated by game-results."""
|
||||
|
||||
score: int | None
|
||||
teams: Sequence[ba.SessionTeam]
|
||||
teams: Sequence[bascenev1.SessionTeam]
|
||||
|
||||
|
||||
class GameResults:
|
||||
|
|
@ -31,22 +34,24 @@ class GameResults:
|
|||
Category: **Gameplay Classes**
|
||||
|
||||
Upon completion, a game should fill one of these out and pass it to its
|
||||
ba.Activity.end call.
|
||||
bascenev1.Activity.end call.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._game_set = False
|
||||
self._scores: dict[
|
||||
int, tuple[weakref.ref[ba.SessionTeam], int | None]
|
||||
int, tuple[weakref.ref[bascenev1.SessionTeam], int | None]
|
||||
] = {}
|
||||
self._sessionteams: list[weakref.ref[ba.SessionTeam]] | None = None
|
||||
self._playerinfos: list[ba.PlayerInfo] | None = None
|
||||
self._sessionteams: list[
|
||||
weakref.ref[bascenev1.SessionTeam]
|
||||
] | None = None
|
||||
self._playerinfos: list[bascenev1.PlayerInfo] | None = None
|
||||
self._lower_is_better: bool | None = None
|
||||
self._score_label: str | None = None
|
||||
self._none_is_winner: bool | None = None
|
||||
self._scoretype: ba.ScoreType | None = None
|
||||
self._scoretype: bascenev1.ScoreType | None = None
|
||||
|
||||
def set_game(self, game: ba.GameActivity) -> None:
|
||||
def set_game(self, game: bascenev1.GameActivity) -> None:
|
||||
"""Set the game instance these results are applying to."""
|
||||
if self._game_set:
|
||||
raise RuntimeError('Game set twice for GameResults.')
|
||||
|
|
@ -61,7 +66,7 @@ class GameResults:
|
|||
self._none_is_winner = scoreconfig.none_is_winner
|
||||
self._scoretype = scoreconfig.scoretype
|
||||
|
||||
def set_team_score(self, team: ba.Team, score: int | None) -> None:
|
||||
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.
|
||||
|
|
@ -71,8 +76,10 @@ class GameResults:
|
|||
sessionteam = team.sessionteam
|
||||
self._scores[sessionteam.id] = (weakref.ref(sessionteam), score)
|
||||
|
||||
def get_sessionteam_score(self, sessionteam: ba.SessionTeam) -> int | None:
|
||||
"""Return the score for a given ba.SessionTeam."""
|
||||
def get_sessionteam_score(
|
||||
self, sessionteam: bascenev1.SessionTeam
|
||||
) -> int | None:
|
||||
"""Return the score for a given bascenev1.SessionTeam."""
|
||||
assert isinstance(sessionteam, SessionTeam)
|
||||
for score in list(self._scores.values()):
|
||||
if score[0]() is sessionteam:
|
||||
|
|
@ -82,8 +89,8 @@ class GameResults:
|
|||
return None
|
||||
|
||||
@property
|
||||
def sessionteams(self) -> list[ba.SessionTeam]:
|
||||
"""Return all ba.SessionTeams in the results."""
|
||||
def sessionteams(self) -> list[bascenev1.SessionTeam]:
|
||||
"""Return all bascenev1.SessionTeams in the results."""
|
||||
if not self._game_set:
|
||||
raise RuntimeError("Can't get teams until game is set.")
|
||||
teams = []
|
||||
|
|
@ -94,41 +101,36 @@ class GameResults:
|
|||
teams.append(team)
|
||||
return teams
|
||||
|
||||
def has_score_for_sessionteam(self, sessionteam: ba.SessionTeam) -> bool:
|
||||
def has_score_for_sessionteam(
|
||||
self, sessionteam: bascenev1.SessionTeam
|
||||
) -> bool:
|
||||
"""Return whether there is a score for a given session-team."""
|
||||
return any(s[0]() is sessionteam for s in self._scores.values())
|
||||
|
||||
def get_sessionteam_score_str(self, sessionteam: ba.SessionTeam) -> ba.Lstr:
|
||||
def get_sessionteam_score_str(
|
||||
self, sessionteam: bascenev1.SessionTeam
|
||||
) -> babase.Lstr:
|
||||
"""Return the score for the given session-team as an Lstr.
|
||||
|
||||
(properly formatted for the score type.)
|
||||
"""
|
||||
from ba._gameutils import timestring
|
||||
from ba._language import Lstr
|
||||
from ba._generated.enums import TimeFormat
|
||||
from ba._score import ScoreType
|
||||
from bascenev1._score import ScoreType
|
||||
|
||||
if not self._game_set:
|
||||
raise RuntimeError("Can't get team-score-str until game is set.")
|
||||
for score in list(self._scores.values()):
|
||||
if score[0]() is sessionteam:
|
||||
if score[1] is None:
|
||||
return Lstr(value='-')
|
||||
return babase.Lstr(value='-')
|
||||
if self._scoretype is ScoreType.SECONDS:
|
||||
return timestring(
|
||||
score[1] * 1000,
|
||||
centi=False,
|
||||
timeformat=TimeFormat.MILLISECONDS,
|
||||
)
|
||||
return babase.timestring(score[1], centi=False)
|
||||
if self._scoretype is ScoreType.MILLISECONDS:
|
||||
return timestring(
|
||||
score[1], centi=True, timeformat=TimeFormat.MILLISECONDS
|
||||
)
|
||||
return Lstr(value=str(score[1]))
|
||||
return Lstr(value='-')
|
||||
return babase.timestring(score[1] / 1000.0, centi=True)
|
||||
return babase.Lstr(value=str(score[1]))
|
||||
return babase.Lstr(value='-')
|
||||
|
||||
@property
|
||||
def playerinfos(self) -> list[ba.PlayerInfo]:
|
||||
def playerinfos(self) -> list[bascenev1.PlayerInfo]:
|
||||
"""Get info about the players represented by the results."""
|
||||
if not self._game_set:
|
||||
raise RuntimeError("Can't get player-info until game is set.")
|
||||
|
|
@ -136,7 +138,7 @@ class GameResults:
|
|||
return self._playerinfos
|
||||
|
||||
@property
|
||||
def scoretype(self) -> ba.ScoreType:
|
||||
def scoretype(self) -> bascenev1.ScoreType:
|
||||
"""The type of score."""
|
||||
if not self._game_set:
|
||||
raise RuntimeError("Can't get score-type until game is set.")
|
||||
|
|
@ -160,8 +162,8 @@ class GameResults:
|
|||
return self._lower_is_better
|
||||
|
||||
@property
|
||||
def winning_sessionteam(self) -> ba.SessionTeam | None:
|
||||
"""The winning ba.SessionTeam if there is exactly one, or else None."""
|
||||
def winning_sessionteam(self) -> bascenev1.SessionTeam | None:
|
||||
"""The winning SessionTeam 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
|
||||
|
|
@ -176,7 +178,7 @@ class GameResults:
|
|||
raise RuntimeError("Can't get winners until game is set.")
|
||||
|
||||
# Group by best scoring teams.
|
||||
winners: dict[int, list[ba.SessionTeam]] = {}
|
||||
winners: dict[int, list[bascenev1.SessionTeam]] = {}
|
||||
scores = [
|
||||
score
|
||||
for score in self._scores.values()
|
||||
|
|
@ -188,7 +190,7 @@ class GameResults:
|
|||
team = score[0]()
|
||||
assert team is not None
|
||||
sval.append(team)
|
||||
results: list[tuple[int | None, list[ba.SessionTeam]]] = list(
|
||||
results: list[tuple[int | None, list[bascenev1.SessionTeam]]] = list(
|
||||
winners.items()
|
||||
)
|
||||
results.sort(
|
||||
|
|
@ -197,7 +199,7 @@ class GameResults:
|
|||
)
|
||||
|
||||
# Also group the 'None' scores.
|
||||
none_sessionteams: list[ba.SessionTeam] = []
|
||||
none_sessionteams: list[bascenev1.SessionTeam] = []
|
||||
for score in self._scores.values():
|
||||
scoreteam = score[0]()
|
||||
if scoreteam is not None and score[1] is None:
|
||||
|
|
@ -206,7 +208,7 @@ class GameResults:
|
|||
# Add the Nones to the list (either as winners or losers
|
||||
# depending on the rules).
|
||||
if none_sessionteams:
|
||||
nones: list[tuple[int | None, list[ba.SessionTeam]]] = [
|
||||
nones: list[tuple[int | None, list[bascenev1.SessionTeam]]] = [
|
||||
(None, none_sessionteams)
|
||||
]
|
||||
if self._none_is_winner:
|
||||
|
|
@ -5,23 +5,26 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, NewType
|
||||
|
||||
import _ba
|
||||
from ba._generated.enums import TimeType, TimeFormat, SpecialChar, UIScale
|
||||
from ba._error import ActivityNotFoundError
|
||||
import babase
|
||||
import _bascenev1
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Sequence
|
||||
import ba
|
||||
|
||||
import bascenev1
|
||||
|
||||
Time = NewType('Time', float)
|
||||
BaseTime = NewType('BaseTime', float)
|
||||
|
||||
TROPHY_CHARS = {
|
||||
'1': SpecialChar.TROPHY1,
|
||||
'2': SpecialChar.TROPHY2,
|
||||
'3': SpecialChar.TROPHY3,
|
||||
'0a': SpecialChar.TROPHY0A,
|
||||
'0b': SpecialChar.TROPHY0B,
|
||||
'4': SpecialChar.TROPHY4,
|
||||
'1': babase.SpecialChar.TROPHY1,
|
||||
'2': babase.SpecialChar.TROPHY2,
|
||||
'3': babase.SpecialChar.TROPHY3,
|
||||
'0a': babase.SpecialChar.TROPHY0A,
|
||||
'0b': babase.SpecialChar.TROPHY0B,
|
||||
'4': babase.SpecialChar.TROPHY4,
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -33,28 +36,25 @@ class GameTip:
|
|||
"""
|
||||
|
||||
text: str
|
||||
icon: ba.Texture | None = None
|
||||
sound: ba.Sound | None = None
|
||||
icon: bascenev1.Texture | None = None
|
||||
sound: bascenev1.Sound | None = None
|
||||
|
||||
|
||||
def get_trophy_string(trophy_id: str) -> str:
|
||||
"""Given a trophy id, returns a string to visualize it."""
|
||||
if trophy_id in TROPHY_CHARS:
|
||||
return _ba.charstr(TROPHY_CHARS[trophy_id])
|
||||
return babase.charstr(TROPHY_CHARS[trophy_id])
|
||||
return '?'
|
||||
|
||||
|
||||
def animate(
|
||||
node: ba.Node,
|
||||
node: bascenev1.Node,
|
||||
attr: str,
|
||||
keys: dict[float, float],
|
||||
loop: bool = False,
|
||||
offset: float = 0,
|
||||
timetype: ba.TimeType = TimeType.SIM,
|
||||
timeformat: ba.TimeFormat = TimeFormat.SECONDS,
|
||||
suppress_format_warning: bool = False,
|
||||
) -> ba.Node:
|
||||
"""Animate values on a target ba.Node.
|
||||
) -> bascenev1.Node:
|
||||
"""Animate values on a target bascenev1.Node.
|
||||
|
||||
Category: **Gameplay Functions**
|
||||
|
||||
|
|
@ -65,37 +65,20 @@ def animate(
|
|||
but timeformat can also be set to MILLISECONDS to recreate the old behavior
|
||||
(prior to ba 1.5) of taking milliseconds. Returns the animcurve node.
|
||||
"""
|
||||
if timetype is TimeType.SIM:
|
||||
driver = 'time'
|
||||
else:
|
||||
raise Exception('FIXME; only SIM timetype is supported currently.')
|
||||
items = list(keys.items())
|
||||
items.sort()
|
||||
|
||||
# Temp sanity check while we transition from milliseconds to seconds
|
||||
# based time values.
|
||||
if __debug__:
|
||||
if not suppress_format_warning:
|
||||
for item in items:
|
||||
_ba.time_format_check(timeformat, item[0])
|
||||
|
||||
curve = _ba.newnode(
|
||||
curve = _bascenev1.newnode(
|
||||
'animcurve',
|
||||
owner=node,
|
||||
name='Driving ' + str(node) + ' \'' + attr + '\'',
|
||||
)
|
||||
|
||||
if timeformat is TimeFormat.SECONDS:
|
||||
mult = 1000
|
||||
elif timeformat is TimeFormat.MILLISECONDS:
|
||||
mult = 1
|
||||
else:
|
||||
raise ValueError(f'invalid timeformat value: {timeformat}')
|
||||
# We take seconds but operate on milliseconds internally.
|
||||
mult = 1000
|
||||
|
||||
curve.times = [int(mult * time) for time, val in items]
|
||||
curve.offset = _ba.time(timeformat=TimeFormat.MILLISECONDS) + int(
|
||||
mult * offset
|
||||
)
|
||||
curve.offset = int(_bascenev1.time() * 1000.0) + int(mult * offset)
|
||||
curve.values = [val for time, val in items]
|
||||
curve.loop = loop
|
||||
|
||||
|
|
@ -105,10 +88,8 @@ def animate(
|
|||
# get disconnected.
|
||||
if not loop:
|
||||
# noinspection PyUnresolvedReferences
|
||||
_ba.timer(
|
||||
int(mult * items[-1][0]) + 1000,
|
||||
curve.delete,
|
||||
timeformat=TimeFormat.MILLISECONDS,
|
||||
_bascenev1.timer(
|
||||
(int(mult * items[-1][0]) + 1000) / 1000.0, curve.delete
|
||||
)
|
||||
|
||||
# Do the connects last so all our attrs are in place when we push initial
|
||||
|
|
@ -116,76 +97,54 @@ def animate(
|
|||
|
||||
# We operate in either activities or sessions..
|
||||
try:
|
||||
globalsnode = _ba.getactivity().globalsnode
|
||||
except ActivityNotFoundError:
|
||||
globalsnode = _ba.getsession().sessionglobalsnode
|
||||
globalsnode = _bascenev1.getactivity().globalsnode
|
||||
except babase.ActivityNotFoundError:
|
||||
globalsnode = _bascenev1.getsession().sessionglobalsnode
|
||||
|
||||
globalsnode.connectattr(driver, curve, 'in')
|
||||
globalsnode.connectattr('time', curve, 'in')
|
||||
curve.connectattr('out', node, attr)
|
||||
return curve
|
||||
|
||||
|
||||
def animate_array(
|
||||
node: ba.Node,
|
||||
node: bascenev1.Node,
|
||||
attr: str,
|
||||
size: int,
|
||||
keys: dict[float, Sequence[float]],
|
||||
loop: bool = False,
|
||||
offset: float = 0,
|
||||
timetype: ba.TimeType = TimeType.SIM,
|
||||
timeformat: ba.TimeFormat = TimeFormat.SECONDS,
|
||||
suppress_format_warning: bool = False,
|
||||
) -> None:
|
||||
"""Animate an array of values on a target ba.Node.
|
||||
"""Animate an array of values on a target bascenev1.Node.
|
||||
|
||||
Category: **Gameplay Functions**
|
||||
|
||||
Like ba.animate, but operates on array attributes.
|
||||
Like bs.animate, but operates on array attributes.
|
||||
"""
|
||||
# pylint: disable=too-many-locals
|
||||
combine = _ba.newnode('combine', owner=node, attrs={'size': size})
|
||||
if timetype is TimeType.SIM:
|
||||
driver = 'time'
|
||||
else:
|
||||
raise Exception('FIXME: Only SIM timetype is supported currently.')
|
||||
combine = _bascenev1.newnode('combine', owner=node, attrs={'size': size})
|
||||
items = list(keys.items())
|
||||
items.sort()
|
||||
|
||||
# Temp sanity check while we transition from milliseconds to seconds
|
||||
# based time values.
|
||||
if __debug__:
|
||||
if not suppress_format_warning:
|
||||
for item in items:
|
||||
# (PyCharm seems to think item is a float, not a tuple)
|
||||
_ba.time_format_check(timeformat, item[0])
|
||||
|
||||
if timeformat is TimeFormat.SECONDS:
|
||||
mult = 1000
|
||||
elif timeformat is TimeFormat.MILLISECONDS:
|
||||
mult = 1
|
||||
else:
|
||||
raise ValueError('invalid timeformat value: "' + str(timeformat) + '"')
|
||||
# We take seconds but operate on milliseconds internally.
|
||||
mult = 1000
|
||||
|
||||
# We operate in either activities or sessions..
|
||||
try:
|
||||
globalsnode = _ba.getactivity().globalsnode
|
||||
except ActivityNotFoundError:
|
||||
globalsnode = _ba.getsession().sessionglobalsnode
|
||||
globalsnode = _bascenev1.getactivity().globalsnode
|
||||
except babase.ActivityNotFoundError:
|
||||
globalsnode = _bascenev1.getsession().sessionglobalsnode
|
||||
|
||||
for i in range(size):
|
||||
curve = _ba.newnode(
|
||||
curve = _bascenev1.newnode(
|
||||
'animcurve',
|
||||
owner=node,
|
||||
name=(
|
||||
'Driving ' + str(node) + ' \'' + attr + '\' member ' + str(i)
|
||||
),
|
||||
)
|
||||
globalsnode.connectattr(driver, curve, 'in')
|
||||
globalsnode.connectattr('time', curve, 'in')
|
||||
curve.times = [int(mult * time) for time, val in items]
|
||||
curve.values = [val[i] for time, val in items]
|
||||
curve.offset = _ba.time(timeformat=TimeFormat.MILLISECONDS) + int(
|
||||
mult * offset
|
||||
)
|
||||
curve.offset = int(_bascenev1.time() * 1000.0) + int(mult * offset)
|
||||
curve.loop = loop
|
||||
curve.connectattr('out', combine, 'input' + str(i))
|
||||
|
||||
|
|
@ -194,10 +153,9 @@ def animate_array(
|
|||
if not loop:
|
||||
# (PyCharm seems to think item is a float, not a tuple)
|
||||
# noinspection PyUnresolvedReferences
|
||||
_ba.timer(
|
||||
int(mult * items[-1][0]) + 1000,
|
||||
_bascenev1.timer(
|
||||
(int(mult * items[-1][0]) + 1000) / 1000.0,
|
||||
curve.delete,
|
||||
timeformat=TimeFormat.MILLISECONDS,
|
||||
)
|
||||
combine.connectattr('output', node, attr)
|
||||
|
||||
|
|
@ -208,10 +166,8 @@ def animate_array(
|
|||
if not loop:
|
||||
# (PyCharm seems to think item is a float, not a tuple)
|
||||
# noinspection PyUnresolvedReferences
|
||||
_ba.timer(
|
||||
int(mult * items[-1][0]) + 1000,
|
||||
combine.delete,
|
||||
timeformat=TimeFormat.MILLISECONDS,
|
||||
_bascenev1.timer(
|
||||
(int(mult * items[-1][0]) + 1000) / 1000.0, combine.delete
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -223,13 +179,14 @@ def show_damage_count(
|
|||
Category: **Gameplay Functions**
|
||||
"""
|
||||
lifespan = 1.0
|
||||
app = _ba.app
|
||||
app = babase.app
|
||||
|
||||
# FIXME: Should never vary game elements based on local config.
|
||||
# (connected clients may have differing configs so they won't
|
||||
# get the intended results).
|
||||
do_big = app.ui.uiscale is UIScale.SMALL or app.vr_mode
|
||||
txtnode = _ba.newnode(
|
||||
assert app.classic is not None
|
||||
do_big = app.ui_v1.uiscale is babase.UIScale.SMALL or app.vr_mode
|
||||
txtnode = _bascenev1.newnode(
|
||||
'text',
|
||||
attrs={
|
||||
'text': damage,
|
||||
|
|
@ -242,7 +199,7 @@ def show_damage_count(
|
|||
},
|
||||
)
|
||||
# Translate upward.
|
||||
tcombine = _ba.newnode('combine', owner=txtnode, attrs={'size': 3})
|
||||
tcombine = _bascenev1.newnode('combine', owner=txtnode, attrs={'size': 3})
|
||||
tcombine.connectattr('output', txtnode, 'position')
|
||||
v_vals = []
|
||||
pval = 0.0
|
||||
|
|
@ -274,104 +231,7 @@ def show_damage_count(
|
|||
{i[0] * lifespan: p_start + p_dir * i[1] for i in v_vals},
|
||||
)
|
||||
animate(txtnode, 'opacity', {0.7 * lifespan: 1.0, lifespan: 0.0})
|
||||
_ba.timer(lifespan, txtnode.delete)
|
||||
|
||||
|
||||
def timestring(
|
||||
timeval: float | int,
|
||||
centi: bool = True,
|
||||
timeformat: ba.TimeFormat = TimeFormat.SECONDS,
|
||||
suppress_format_warning: bool = False,
|
||||
) -> ba.Lstr:
|
||||
"""Generate a ba.Lstr for displaying a time value.
|
||||
|
||||
Category: **General Utility Functions**
|
||||
|
||||
Given a time value, returns a ba.Lstr with:
|
||||
(hours if > 0 ) : minutes : seconds : (centiseconds if centi=True).
|
||||
|
||||
Time 'timeval' is specified in seconds by default, or 'timeformat' can
|
||||
be set to ba.TimeFormat.MILLISECONDS to accept milliseconds instead.
|
||||
|
||||
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.
|
||||
|
||||
"""
|
||||
from ba._language import Lstr
|
||||
|
||||
# Temp sanity check while we transition from milliseconds to seconds
|
||||
# based time values.
|
||||
if __debug__:
|
||||
if not suppress_format_warning:
|
||||
_ba.time_format_check(timeformat, timeval)
|
||||
|
||||
# We operate on milliseconds internally.
|
||||
if timeformat is TimeFormat.SECONDS:
|
||||
timeval = int(1000 * timeval)
|
||||
elif timeformat is TimeFormat.MILLISECONDS:
|
||||
pass
|
||||
else:
|
||||
raise ValueError(f'invalid timeformat: {timeformat}')
|
||||
if not isinstance(timeval, int):
|
||||
timeval = int(timeval)
|
||||
bits = []
|
||||
subs = []
|
||||
hval = (timeval // 1000) // (60 * 60)
|
||||
if hval != 0:
|
||||
bits.append('${H}')
|
||||
subs.append(
|
||||
(
|
||||
'${H}',
|
||||
Lstr(
|
||||
resource='timeSuffixHoursText',
|
||||
subs=[('${COUNT}', str(hval))],
|
||||
),
|
||||
)
|
||||
)
|
||||
mval = ((timeval // 1000) // 60) % 60
|
||||
if mval != 0:
|
||||
bits.append('${M}')
|
||||
subs.append(
|
||||
(
|
||||
'${M}',
|
||||
Lstr(
|
||||
resource='timeSuffixMinutesText',
|
||||
subs=[('${COUNT}', str(mval))],
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
# We add seconds if its non-zero *or* we haven't added anything else.
|
||||
if centi:
|
||||
# pylint: disable=consider-using-f-string
|
||||
sval = timeval / 1000.0 % 60.0
|
||||
if sval >= 0.005 or not bits:
|
||||
bits.append('${S}')
|
||||
subs.append(
|
||||
(
|
||||
'${S}',
|
||||
Lstr(
|
||||
resource='timeSuffixSecondsText',
|
||||
subs=[('${COUNT}', ('%.2f' % sval))],
|
||||
),
|
||||
)
|
||||
)
|
||||
else:
|
||||
sval = timeval // 1000 % 60
|
||||
if sval != 0 or not bits:
|
||||
bits.append('${S}')
|
||||
subs.append(
|
||||
(
|
||||
'${S}',
|
||||
Lstr(
|
||||
resource='timeSuffixSecondsText',
|
||||
subs=[('${COUNT}', str(sval))],
|
||||
),
|
||||
)
|
||||
)
|
||||
return Lstr(value=' '.join(bits), subs=subs)
|
||||
_bascenev1.timer(lifespan, txtnode.delete)
|
||||
|
||||
|
||||
def cameraflash(duration: float = 999.0) -> None:
|
||||
|
|
@ -384,7 +244,7 @@ def cameraflash(duration: float = 999.0) -> None:
|
|||
"""
|
||||
# pylint: disable=too-many-locals
|
||||
import random
|
||||
from ba._nodeactor import NodeActor
|
||||
from bascenev1._nodeactor import NodeActor
|
||||
|
||||
x_spread = 10
|
||||
y_spread = 5
|
||||
|
|
@ -400,11 +260,11 @@ def cameraflash(duration: float = 999.0) -> None:
|
|||
|
||||
# Store this on the current activity so we only have one at a time.
|
||||
# FIXME: Need a type safe way to do this.
|
||||
activity = _ba.getactivity()
|
||||
activity = _bascenev1.getactivity()
|
||||
activity.camera_flash_data = [] # type: ignore
|
||||
for i in range(6):
|
||||
light = NodeActor(
|
||||
_ba.newnode(
|
||||
_bascenev1.newnode(
|
||||
'light',
|
||||
attrs={
|
||||
'position': (positions[i][0], 0, positions[i][1]),
|
||||
|
|
@ -417,7 +277,7 @@ def cameraflash(duration: float = 999.0) -> None:
|
|||
)
|
||||
sval = 1.87
|
||||
iscale = 1.3
|
||||
tcombine = _ba.newnode(
|
||||
tcombine = _bascenev1.newnode(
|
||||
'combine',
|
||||
owner=light.node,
|
||||
attrs={
|
||||
|
|
@ -468,9 +328,8 @@ def cameraflash(duration: float = 999.0) -> None:
|
|||
loop=True,
|
||||
offset=times[i],
|
||||
)
|
||||
_ba.timer(
|
||||
(times[i] + random.randint(1, int(duration)) * 40 * sval),
|
||||
_bascenev1.timer(
|
||||
(times[i] + random.randint(1, int(duration)) * 40 * sval) / 1000.0,
|
||||
light.node.delete,
|
||||
timeformat=TimeFormat.MILLISECONDS,
|
||||
)
|
||||
activity.camera_flash_data.append(light) # type: ignore
|
||||
56
dist/ba_data/python/bascenev1/_hooks.py
vendored
Normal file
56
dist/ba_data/python/bascenev1/_hooks.py
vendored
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Snippets of code for use by the c++ layer."""
|
||||
# (most of these are self-explanatory)
|
||||
# pylint: disable=missing-function-docstring
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import babase
|
||||
|
||||
import _bascenev1
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any
|
||||
|
||||
import bascenev1
|
||||
|
||||
|
||||
def launch_main_menu_session() -> None:
|
||||
assert babase.app.classic is not None
|
||||
|
||||
_bascenev1.new_host_session(babase.app.classic.get_main_menu_session())
|
||||
|
||||
|
||||
def get_player_icon(sessionplayer: bascenev1.SessionPlayer) -> dict[str, Any]:
|
||||
info = sessionplayer.get_icon_info()
|
||||
return {
|
||||
'texture': _bascenev1.gettexture(info['texture']),
|
||||
'tint_texture': _bascenev1.gettexture(info['tint_texture']),
|
||||
'tint_color': info['tint_color'],
|
||||
'tint2_color': info['tint2_color'],
|
||||
}
|
||||
|
||||
|
||||
def filter_chat_message(msg: str, client_id: int) -> str | None:
|
||||
"""Intercept/filter chat messages.
|
||||
|
||||
Called for all chat messages while hosting.
|
||||
Messages originating from the host will have clientID -1.
|
||||
Should filter and return the string to be displayed, or return None
|
||||
to ignore the message.
|
||||
"""
|
||||
del client_id # Unused by default.
|
||||
return msg
|
||||
|
||||
|
||||
def local_chat_message(msg: str) -> None:
|
||||
classic = babase.app.classic
|
||||
assert classic is not None
|
||||
party_window = (
|
||||
None if classic.party_window is None else classic.party_window()
|
||||
)
|
||||
|
||||
if party_window is not None:
|
||||
party_window.on_chat_message(msg)
|
||||
|
|
@ -7,15 +7,16 @@ import copy
|
|||
import weakref
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import _ba
|
||||
import babase
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any
|
||||
import ba
|
||||
|
||||
import bascenev1
|
||||
|
||||
|
||||
class Level:
|
||||
"""An entry in a ba.Campaign consisting of a name, game type, and settings.
|
||||
"""An entry in a bascenev1.Campaign.
|
||||
|
||||
Category: **Gameplay Classes**
|
||||
"""
|
||||
|
|
@ -23,7 +24,7 @@ class Level:
|
|||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
gametype: type[ba.GameActivity],
|
||||
gametype: type[bascenev1.GameActivity],
|
||||
settings: dict,
|
||||
preview_texture_name: str,
|
||||
displayname: str | None = None,
|
||||
|
|
@ -33,7 +34,7 @@ class Level:
|
|||
self._settings = settings
|
||||
self._preview_texture_name = preview_texture_name
|
||||
self._displayname = displayname
|
||||
self._campaign: weakref.ref[ba.Campaign] | None = None
|
||||
self._campaign: weakref.ref[bascenev1.Campaign] | None = None
|
||||
self._index: int | None = None
|
||||
self._score_version_string: str | None = None
|
||||
|
||||
|
|
@ -60,16 +61,14 @@ class Level:
|
|||
"""The preview texture name for this Level."""
|
||||
return self._preview_texture_name
|
||||
|
||||
def get_preview_texture(self) -> ba.Texture:
|
||||
"""Load/return the preview Texture for this Level."""
|
||||
return _ba.gettexture(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) -> ba.Lstr:
|
||||
def displayname(self) -> bascenev1.Lstr:
|
||||
"""The localized name for this Level."""
|
||||
from ba import _language
|
||||
|
||||
return _language.Lstr(
|
||||
return babase.Lstr(
|
||||
translate=(
|
||||
'coopLevelNames',
|
||||
self._displayname
|
||||
|
|
@ -82,18 +81,18 @@ class Level:
|
|||
)
|
||||
|
||||
@property
|
||||
def gametype(self) -> type[ba.GameActivity]:
|
||||
def gametype(self) -> type[bascenev1.GameActivity]:
|
||||
"""The type of game used for this Level."""
|
||||
return self._gametype
|
||||
|
||||
@property
|
||||
def campaign(self) -> ba.Campaign | None:
|
||||
"""The ba.Campaign this Level is associated with, or None."""
|
||||
def campaign(self) -> bascenev1.Campaign | None:
|
||||
"""The baclassic.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 ba.Campaign.
|
||||
"""The zero-based index of this Level in its baclassic.Campaign.
|
||||
|
||||
Access results in a RuntimeError if the Level is not assigned to a
|
||||
Campaign.
|
||||
|
|
@ -171,8 +170,8 @@ class Level:
|
|||
assert isinstance(val, dict)
|
||||
return val
|
||||
|
||||
def set_campaign(self, campaign: ba.Campaign, index: int) -> None:
|
||||
"""For use by ba.Campaign when adding levels to itself.
|
||||
def set_campaign(self, campaign: bascenev1.Campaign, index: int) -> None:
|
||||
"""For use by baclassic.Campaign when adding levels to itself.
|
||||
|
||||
(internal)"""
|
||||
self._campaign = weakref.ref(campaign)
|
||||
|
|
@ -5,20 +5,20 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import weakref
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import _ba
|
||||
from ba._error import print_exception, print_error, NotFoundError
|
||||
from ba._gameutils import animate, animate_array
|
||||
from ba._language import Lstr
|
||||
from ba._generated.enums import SpecialChar, InputType
|
||||
from ba._profile import get_player_profile_colors
|
||||
import babase
|
||||
import _bascenev1
|
||||
from bascenev1._profile import get_player_profile_colors
|
||||
from bascenev1._gameutils import animate, animate_array
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any, Sequence
|
||||
import ba
|
||||
|
||||
import bascenev1
|
||||
|
||||
MAX_QUICK_CHANGE_COUNT = 30
|
||||
QUICK_CHANGE_INTERVAL = 0.05
|
||||
|
|
@ -29,34 +29,33 @@ QUICK_CHANGE_RESET_INTERVAL = 1.0
|
|||
class JoinInfo:
|
||||
"""Display useful info for joiners."""
|
||||
|
||||
def __init__(self, lobby: ba.Lobby):
|
||||
from ba._nodeactor import NodeActor
|
||||
from ba._general import WeakCall
|
||||
def __init__(self, lobby: bascenev1.Lobby):
|
||||
from bascenev1._nodeactor import NodeActor
|
||||
|
||||
self._state = 0
|
||||
self._press_to_punch: str | ba.Lstr = (
|
||||
self._press_to_punch: str | bascenev1.Lstr = (
|
||||
'C'
|
||||
if _ba.app.iircade_mode
|
||||
else _ba.charstr(SpecialChar.LEFT_BUTTON)
|
||||
if babase.app.iircade_mode
|
||||
else babase.charstr(babase.SpecialChar.LEFT_BUTTON)
|
||||
)
|
||||
self._press_to_bomb: str | ba.Lstr = (
|
||||
self._press_to_bomb: str | bascenev1.Lstr = (
|
||||
'B'
|
||||
if _ba.app.iircade_mode
|
||||
else _ba.charstr(SpecialChar.RIGHT_BUTTON)
|
||||
if babase.app.iircade_mode
|
||||
else babase.charstr(babase.SpecialChar.RIGHT_BUTTON)
|
||||
)
|
||||
self._joinmsg = Lstr(resource='pressAnyButtonToJoinText')
|
||||
self._joinmsg = babase.Lstr(resource='pressAnyButtonToJoinText')
|
||||
can_switch_teams = len(lobby.sessionteams) > 1
|
||||
|
||||
# If we have a keyboard, grab keys for punch and pickup.
|
||||
# FIXME: This of course is only correct on the local device;
|
||||
# Should change this for net games.
|
||||
keyboard = _ba.getinputdevice('Keyboard', '#1', doraise=False)
|
||||
keyboard = _bascenev1.getinputdevice('Keyboard', '#1', doraise=False)
|
||||
if keyboard is not None:
|
||||
self._update_for_keyboard(keyboard)
|
||||
|
||||
flatness = 1.0 if _ba.app.vr_mode else 0.0
|
||||
flatness = 1.0 if babase.app.vr_mode else 0.0
|
||||
self._text = NodeActor(
|
||||
_ba.newnode(
|
||||
_bascenev1.newnode(
|
||||
'text',
|
||||
attrs={
|
||||
'position': (0, -40),
|
||||
|
|
@ -70,39 +69,43 @@ class JoinInfo:
|
|||
)
|
||||
)
|
||||
|
||||
if _ba.app.demo_mode or _ba.app.arcade_mode:
|
||||
if babase.app.demo_mode or babase.app.arcade_mode:
|
||||
self._messages = [self._joinmsg]
|
||||
else:
|
||||
msg1 = Lstr(
|
||||
msg1 = babase.Lstr(
|
||||
resource='pressToSelectProfileText',
|
||||
subs=[
|
||||
(
|
||||
'${BUTTONS}',
|
||||
_ba.charstr(SpecialChar.UP_ARROW)
|
||||
babase.charstr(babase.SpecialChar.UP_ARROW)
|
||||
+ ' '
|
||||
+ _ba.charstr(SpecialChar.DOWN_ARROW),
|
||||
+ babase.charstr(babase.SpecialChar.DOWN_ARROW),
|
||||
)
|
||||
],
|
||||
)
|
||||
msg2 = Lstr(
|
||||
msg2 = babase.Lstr(
|
||||
resource='pressToOverrideCharacterText',
|
||||
subs=[('${BUTTONS}', Lstr(resource='bombBoldText'))],
|
||||
subs=[('${BUTTONS}', babase.Lstr(resource='bombBoldText'))],
|
||||
)
|
||||
msg3 = Lstr(
|
||||
msg3 = babase.Lstr(
|
||||
value='${A} < ${B} >',
|
||||
subs=[('${A}', msg2), ('${B}', self._press_to_bomb)],
|
||||
)
|
||||
self._messages = (
|
||||
(
|
||||
[
|
||||
Lstr(
|
||||
babase.Lstr(
|
||||
resource='pressToSelectTeamText',
|
||||
subs=[
|
||||
(
|
||||
'${BUTTONS}',
|
||||
_ba.charstr(SpecialChar.LEFT_ARROW)
|
||||
babase.charstr(
|
||||
babase.SpecialChar.LEFT_ARROW
|
||||
)
|
||||
+ ' '
|
||||
+ _ba.charstr(SpecialChar.RIGHT_ARROW),
|
||||
+ babase.charstr(
|
||||
babase.SpecialChar.RIGHT_ARROW
|
||||
),
|
||||
)
|
||||
],
|
||||
)
|
||||
|
|
@ -115,35 +118,44 @@ class JoinInfo:
|
|||
+ [self._joinmsg]
|
||||
)
|
||||
|
||||
self._timer = _ba.Timer(4.0, WeakCall(self._update), repeat=True)
|
||||
self._timer = _bascenev1.Timer(
|
||||
4.0, babase.WeakCall(self._update), repeat=True
|
||||
)
|
||||
|
||||
def _update_for_keyboard(self, keyboard: ba.InputDevice) -> None:
|
||||
from ba import _input
|
||||
def _update_for_keyboard(self, keyboard: bascenev1.InputDevice) -> None:
|
||||
classic = babase.app.classic
|
||||
assert classic is not None
|
||||
|
||||
punch_key = keyboard.get_button_name(
|
||||
_input.get_device_value(keyboard, 'buttonPunch')
|
||||
classic.get_input_device_mapped_value(keyboard, 'buttonPunch')
|
||||
)
|
||||
self._press_to_punch = Lstr(
|
||||
self._press_to_punch = babase.Lstr(
|
||||
resource='orText',
|
||||
subs=[
|
||||
('${A}', Lstr(value='\'${K}\'', subs=[('${K}', punch_key)])),
|
||||
(
|
||||
'${A}',
|
||||
babase.Lstr(value='\'${K}\'', subs=[('${K}', punch_key)]),
|
||||
),
|
||||
('${B}', self._press_to_punch),
|
||||
],
|
||||
)
|
||||
bomb_key = keyboard.get_button_name(
|
||||
_input.get_device_value(keyboard, 'buttonBomb')
|
||||
classic.get_input_device_mapped_value(keyboard, 'buttonBomb')
|
||||
)
|
||||
self._press_to_bomb = Lstr(
|
||||
self._press_to_bomb = babase.Lstr(
|
||||
resource='orText',
|
||||
subs=[
|
||||
('${A}', Lstr(value='\'${K}\'', subs=[('${K}', bomb_key)])),
|
||||
(
|
||||
'${A}',
|
||||
babase.Lstr(value='\'${K}\'', subs=[('${K}', bomb_key)]),
|
||||
),
|
||||
('${B}', self._press_to_bomb),
|
||||
],
|
||||
)
|
||||
self._joinmsg = Lstr(
|
||||
self._joinmsg = babase.Lstr(
|
||||
value='${A} < ${B} >',
|
||||
subs=[
|
||||
('${A}', Lstr(resource='pressPunchToJoinText')),
|
||||
('${A}', babase.Lstr(resource='pressPunchToJoinText')),
|
||||
('${B}', self._press_to_punch),
|
||||
],
|
||||
)
|
||||
|
|
@ -158,7 +170,7 @@ class JoinInfo:
|
|||
class PlayerReadyMessage:
|
||||
"""Tells an object a player has been selected from the given chooser."""
|
||||
|
||||
chooser: ba.Chooser
|
||||
chooser: bascenev1.Chooser
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
@ -170,32 +182,34 @@ class ChangeMessage:
|
|||
|
||||
|
||||
class Chooser:
|
||||
"""A character/team selector for a ba.Player.
|
||||
"""A character/team selector for a bascenev1.Player.
|
||||
|
||||
Category: Gameplay Classes
|
||||
"""
|
||||
|
||||
def __del__(self) -> None:
|
||||
|
||||
# Just kill off our base node; the rest should go down with it.
|
||||
if self._text_node:
|
||||
self._text_node.delete()
|
||||
|
||||
def __init__(
|
||||
self, vpos: float, sessionplayer: _ba.SessionPlayer, lobby: 'Lobby'
|
||||
self,
|
||||
vpos: float,
|
||||
sessionplayer: bascenev1.SessionPlayer,
|
||||
lobby: 'Lobby',
|
||||
) -> None:
|
||||
self._deek_sound = _ba.getsound('deek')
|
||||
self._click_sound = _ba.getsound('click01')
|
||||
self._punchsound = _ba.getsound('punch01')
|
||||
self._swish_sound = _ba.getsound('punchSwish')
|
||||
self._errorsound = _ba.getsound('error')
|
||||
self._mask_texture = _ba.gettexture('characterIconMask')
|
||||
self._deek_sound = _bascenev1.getsound('deek')
|
||||
self._click_sound = _bascenev1.getsound('click01')
|
||||
self._punchsound = _bascenev1.getsound('punch01')
|
||||
self._swish_sound = _bascenev1.getsound('punchSwish')
|
||||
self._errorsound = _bascenev1.getsound('error')
|
||||
self._mask_texture = _bascenev1.gettexture('characterIconMask')
|
||||
self._vpos = vpos
|
||||
self._lobby = weakref.ref(lobby)
|
||||
self._sessionplayer = sessionplayer
|
||||
self._inited = False
|
||||
self._dead = False
|
||||
self._text_node: ba.Node | None = None
|
||||
self._text_node: bascenev1.Node | None = None
|
||||
self._profilename = ''
|
||||
self._profilenames: list[str] = []
|
||||
self._ready: bool = False
|
||||
|
|
@ -203,7 +217,8 @@ class Chooser:
|
|||
self._last_change: Sequence[float | int] = (0, 0)
|
||||
self._profiles: dict[str, dict[str, Any]] = {}
|
||||
|
||||
app = _ba.app
|
||||
app = babase.app
|
||||
assert app.classic is not None
|
||||
|
||||
# Load available player profiles either from the local config or
|
||||
# from the remote device.
|
||||
|
|
@ -224,7 +239,7 @@ class Chooser:
|
|||
# To calc our random character we pick a random one out of our
|
||||
# unlocked list and then locate that character's index in the full
|
||||
# list.
|
||||
char_index_offset = app.lobby_random_char_index_offset
|
||||
char_index_offset: int = app.classic.lobby_random_char_index_offset
|
||||
self._random_character_index = (
|
||||
sessionplayer.inputdevice.id + char_index_offset
|
||||
) % len(self._character_names)
|
||||
|
|
@ -234,7 +249,7 @@ class Chooser:
|
|||
self._profileindex = self._select_initial_profile()
|
||||
self._profilename = self._profilenames[self._profileindex]
|
||||
|
||||
self._text_node = _ba.newnode(
|
||||
self._text_node = _bascenev1.newnode(
|
||||
'text',
|
||||
delegate=self,
|
||||
attrs={
|
||||
|
|
@ -248,7 +263,7 @@ class Chooser:
|
|||
},
|
||||
)
|
||||
animate(self._text_node, 'scale', {0: 0, 0.1: 1.0})
|
||||
self.icon = _ba.newnode(
|
||||
self.icon = _bascenev1.newnode(
|
||||
'image',
|
||||
owner=self._text_node,
|
||||
attrs={
|
||||
|
|
@ -263,7 +278,7 @@ class Chooser:
|
|||
|
||||
# Set our initial name to '<choosing player>' in case anyone asks.
|
||||
self._sessionplayer.setname(
|
||||
Lstr(resource='choosingPlayerText').evaluate(), real=False
|
||||
babase.Lstr(resource='choosingPlayerText').evaluate(), real=False
|
||||
)
|
||||
|
||||
# Init these to our rando but they should get switched to the
|
||||
|
|
@ -279,7 +294,8 @@ class Chooser:
|
|||
self._set_ready(False)
|
||||
|
||||
def _select_initial_profile(self) -> int:
|
||||
app = _ba.app
|
||||
app = babase.app
|
||||
assert app.classic is not None
|
||||
profilenames = self._profilenames
|
||||
inputdevice = self._sessionplayer.inputdevice
|
||||
|
||||
|
|
@ -296,9 +312,9 @@ class Chooser:
|
|||
if (
|
||||
dprofilename == '__account__'
|
||||
and not inputdevice.is_remote_client
|
||||
and app.lobby_account_profile_device_id is None
|
||||
and app.classic.lobby_account_profile_device_id is None
|
||||
):
|
||||
app.lobby_account_profile_device_id = inputdevice.id
|
||||
app.classic.lobby_account_profile_device_id = inputdevice.id
|
||||
return profilenames.index(dprofilename)
|
||||
|
||||
# We want to mark the first local input-device in the game
|
||||
|
|
@ -308,15 +324,15 @@ class Chooser:
|
|||
and not inputdevice.is_controller_app
|
||||
):
|
||||
if (
|
||||
app.lobby_account_profile_device_id is None
|
||||
app.classic.lobby_account_profile_device_id is None
|
||||
and '__account__' in profilenames
|
||||
):
|
||||
app.lobby_account_profile_device_id = inputdevice.id
|
||||
app.classic.lobby_account_profile_device_id = inputdevice.id
|
||||
|
||||
# If this is the designated account-profile-device, try to default
|
||||
# to the account profile.
|
||||
if (
|
||||
inputdevice.id == app.lobby_account_profile_device_id
|
||||
inputdevice.id == app.classic.lobby_account_profile_device_id
|
||||
and '__account__' in profilenames
|
||||
):
|
||||
return profilenames.index('__account__')
|
||||
|
|
@ -335,24 +351,24 @@ class Chooser:
|
|||
|
||||
# Cycle through our non-random profiles once; after
|
||||
# that, everyone gets random.
|
||||
while app.lobby_random_profile_index < len(
|
||||
while app.classic.lobby_random_profile_index < len(
|
||||
profilenames
|
||||
) and profilenames[app.lobby_random_profile_index] in (
|
||||
) and profilenames[app.classic.lobby_random_profile_index] in (
|
||||
'_random',
|
||||
'__account__',
|
||||
'_edit',
|
||||
):
|
||||
app.lobby_random_profile_index += 1
|
||||
if app.lobby_random_profile_index < len(profilenames):
|
||||
profileindex = app.lobby_random_profile_index
|
||||
app.lobby_random_profile_index += 1
|
||||
app.classic.lobby_random_profile_index += 1
|
||||
if app.classic.lobby_random_profile_index < len(profilenames):
|
||||
profileindex: int = app.classic.lobby_random_profile_index
|
||||
app.classic.lobby_random_profile_index += 1
|
||||
return profileindex
|
||||
assert '_random' in profilenames
|
||||
return profilenames.index('_random')
|
||||
|
||||
@property
|
||||
def sessionplayer(self) -> ba.SessionPlayer:
|
||||
"""The ba.SessionPlayer associated with this chooser."""
|
||||
def sessionplayer(self) -> bascenev1.SessionPlayer:
|
||||
"""The bascenev1.SessionPlayer associated with this chooser."""
|
||||
return self._sessionplayer
|
||||
|
||||
@property
|
||||
|
|
@ -369,24 +385,25 @@ class Chooser:
|
|||
self._dead = val
|
||||
|
||||
@property
|
||||
def sessionteam(self) -> ba.SessionTeam:
|
||||
"""Return this chooser's currently selected ba.SessionTeam."""
|
||||
def sessionteam(self) -> bascenev1.SessionTeam:
|
||||
"""Return this chooser's currently selected bascenev1.SessionTeam."""
|
||||
return self.lobby.sessionteams[self._selected_team_index]
|
||||
|
||||
@property
|
||||
def lobby(self) -> ba.Lobby:
|
||||
"""The chooser's ba.Lobby."""
|
||||
def lobby(self) -> bascenev1.Lobby:
|
||||
"""The chooser's baclassic.Lobby."""
|
||||
lobby = self._lobby()
|
||||
if lobby is None:
|
||||
raise NotFoundError('Lobby does not exist.')
|
||||
raise babase.NotFoundError('Lobby does not exist.')
|
||||
return lobby
|
||||
|
||||
def get_lobby(self) -> ba.Lobby | None:
|
||||
def get_lobby(self) -> bascenev1.Lobby | None:
|
||||
"""Return this chooser's lobby if it still exists; otherwise None."""
|
||||
return self._lobby()
|
||||
|
||||
def update_from_profile(self) -> None:
|
||||
"""Set character/colors based on the current profile."""
|
||||
assert babase.app.classic is not None
|
||||
self._profilename = self._profilenames[self._profileindex]
|
||||
if self._profilename == '_edit':
|
||||
pass
|
||||
|
|
@ -407,7 +424,7 @@ class Chooser:
|
|||
# so no exploit opportunities)
|
||||
if (
|
||||
character not in self._character_names
|
||||
and character in _ba.app.spaz_appearances
|
||||
and character in babase.app.classic.spaz_appearances
|
||||
):
|
||||
self._character_names.append(character)
|
||||
self._character_index = self._character_names.index(character)
|
||||
|
|
@ -419,9 +436,9 @@ class Chooser:
|
|||
|
||||
def reload_profiles(self) -> None:
|
||||
"""Reload all player profiles."""
|
||||
from ba._general import json_prep
|
||||
|
||||
app = _ba.app
|
||||
app = babase.app
|
||||
assert app.classic is not None
|
||||
|
||||
# Re-construct our profile index and other stuff since the profile
|
||||
# list might have changed.
|
||||
|
|
@ -448,11 +465,14 @@ class Chooser:
|
|||
# (non-unicode/non-json) version.
|
||||
# Make sure they conform to our standards
|
||||
# (unicode strings, no tuples, etc)
|
||||
self._profiles = json_prep(self._profiles)
|
||||
self._profiles = babase.json_prep(self._profiles)
|
||||
|
||||
# Filter out any characters we're unaware of.
|
||||
for profile in list(self._profiles.items()):
|
||||
if profile[1].get('character', '') not in app.spaz_appearances:
|
||||
if (
|
||||
profile[1].get('character', '')
|
||||
not in app.classic.spaz_appearances
|
||||
):
|
||||
profile[1]['character'] = 'Spaz'
|
||||
|
||||
# Add in a random one so we're ok even if there's no user profiles.
|
||||
|
|
@ -523,20 +543,20 @@ class Chooser:
|
|||
try:
|
||||
name = self._sessionplayer.inputdevice.get_default_player_name()
|
||||
except Exception:
|
||||
print_exception('Error getting _random chooser name.')
|
||||
logging.exception('Error getting _random chooser name.')
|
||||
name = 'Invalid'
|
||||
clamp = not full
|
||||
elif name == '__account__':
|
||||
try:
|
||||
name = self._sessionplayer.inputdevice.get_v1_account_name(full)
|
||||
except Exception:
|
||||
print_exception('Error getting account name for chooser.')
|
||||
logging.exception('Error getting account name for chooser.')
|
||||
name = 'Invalid'
|
||||
clamp = not full
|
||||
elif name == '_edit':
|
||||
# Explicitly flattening this to a str; it's only relevant on
|
||||
# the host so that's ok.
|
||||
name = Lstr(
|
||||
name = babase.Lstr(
|
||||
resource='createEditPlayerText',
|
||||
fallback_resource='editProfileWindow.titleNewText',
|
||||
).evaluate()
|
||||
|
|
@ -549,11 +569,11 @@ class Chooser:
|
|||
icon = (
|
||||
self._profiles[name_raw]['icon']
|
||||
if 'icon' in self._profiles[name_raw]
|
||||
else _ba.charstr(SpecialChar.LOGO)
|
||||
else babase.charstr(babase.SpecialChar.LOGO)
|
||||
)
|
||||
name = icon + name
|
||||
except Exception:
|
||||
print_exception('Error applying global icon.')
|
||||
logging.exception('Error applying global icon.')
|
||||
else:
|
||||
# We now clamp non-full versions of names so there's at
|
||||
# least some hope of reading them in-game.
|
||||
|
|
@ -566,49 +586,54 @@ class Chooser:
|
|||
|
||||
def _set_ready(self, ready: bool) -> None:
|
||||
# pylint: disable=cyclic-import
|
||||
from bastd.ui.profile import browser as pbrowser
|
||||
from ba._general import Call
|
||||
|
||||
classic = babase.app.classic
|
||||
assert classic is not None
|
||||
|
||||
profilename = self._profilenames[self._profileindex]
|
||||
|
||||
# Handle '_edit' as a special case.
|
||||
if profilename == '_edit' and ready:
|
||||
with _ba.Context('ui'):
|
||||
pbrowser.ProfileBrowserWindow(in_main_menu=False)
|
||||
with babase.ContextRef.empty():
|
||||
classic.profile_browser_window(in_main_menu=False)
|
||||
|
||||
# Give their input-device UI ownership too
|
||||
# (prevent someone else from snatching it in crowded games)
|
||||
_ba.set_ui_input_device(self._sessionplayer.inputdevice)
|
||||
babase.set_ui_input_device(self._sessionplayer.inputdevice.id)
|
||||
return
|
||||
|
||||
if not ready:
|
||||
self._sessionplayer.assigninput(
|
||||
InputType.LEFT_PRESS,
|
||||
Call(self.handlemessage, ChangeMessage('team', -1)),
|
||||
babase.InputType.LEFT_PRESS,
|
||||
babase.Call(self.handlemessage, ChangeMessage('team', -1)),
|
||||
)
|
||||
self._sessionplayer.assigninput(
|
||||
InputType.RIGHT_PRESS,
|
||||
Call(self.handlemessage, ChangeMessage('team', 1)),
|
||||
babase.InputType.RIGHT_PRESS,
|
||||
babase.Call(self.handlemessage, ChangeMessage('team', 1)),
|
||||
)
|
||||
self._sessionplayer.assigninput(
|
||||
InputType.BOMB_PRESS,
|
||||
Call(self.handlemessage, ChangeMessage('character', 1)),
|
||||
babase.InputType.BOMB_PRESS,
|
||||
babase.Call(self.handlemessage, ChangeMessage('character', 1)),
|
||||
)
|
||||
self._sessionplayer.assigninput(
|
||||
InputType.UP_PRESS,
|
||||
Call(self.handlemessage, ChangeMessage('profileindex', -1)),
|
||||
babase.InputType.UP_PRESS,
|
||||
babase.Call(
|
||||
self.handlemessage, ChangeMessage('profileindex', -1)
|
||||
),
|
||||
)
|
||||
self._sessionplayer.assigninput(
|
||||
InputType.DOWN_PRESS,
|
||||
Call(self.handlemessage, ChangeMessage('profileindex', 1)),
|
||||
babase.InputType.DOWN_PRESS,
|
||||
babase.Call(
|
||||
self.handlemessage, ChangeMessage('profileindex', 1)
|
||||
),
|
||||
)
|
||||
self._sessionplayer.assigninput(
|
||||
(
|
||||
InputType.JUMP_PRESS,
|
||||
InputType.PICK_UP_PRESS,
|
||||
InputType.PUNCH_PRESS,
|
||||
babase.InputType.JUMP_PRESS,
|
||||
babase.InputType.PICK_UP_PRESS,
|
||||
babase.InputType.PUNCH_PRESS,
|
||||
),
|
||||
Call(self.handlemessage, ChangeMessage('ready', 1)),
|
||||
babase.Call(self.handlemessage, ChangeMessage('ready', 1)),
|
||||
)
|
||||
self._ready = False
|
||||
self._update_text()
|
||||
|
|
@ -616,31 +641,31 @@ class Chooser:
|
|||
else:
|
||||
self._sessionplayer.assigninput(
|
||||
(
|
||||
InputType.LEFT_PRESS,
|
||||
InputType.RIGHT_PRESS,
|
||||
InputType.UP_PRESS,
|
||||
InputType.DOWN_PRESS,
|
||||
InputType.JUMP_PRESS,
|
||||
InputType.BOMB_PRESS,
|
||||
InputType.PICK_UP_PRESS,
|
||||
babase.InputType.LEFT_PRESS,
|
||||
babase.InputType.RIGHT_PRESS,
|
||||
babase.InputType.UP_PRESS,
|
||||
babase.InputType.DOWN_PRESS,
|
||||
babase.InputType.JUMP_PRESS,
|
||||
babase.InputType.BOMB_PRESS,
|
||||
babase.InputType.PICK_UP_PRESS,
|
||||
),
|
||||
self._do_nothing,
|
||||
)
|
||||
self._sessionplayer.assigninput(
|
||||
(
|
||||
InputType.JUMP_PRESS,
|
||||
InputType.BOMB_PRESS,
|
||||
InputType.PICK_UP_PRESS,
|
||||
InputType.PUNCH_PRESS,
|
||||
babase.InputType.JUMP_PRESS,
|
||||
babase.InputType.BOMB_PRESS,
|
||||
babase.InputType.PICK_UP_PRESS,
|
||||
babase.InputType.PUNCH_PRESS,
|
||||
),
|
||||
Call(self.handlemessage, ChangeMessage('ready', 0)),
|
||||
babase.Call(self.handlemessage, ChangeMessage('ready', 0)),
|
||||
)
|
||||
|
||||
# Store the last profile picked by this input for reuse.
|
||||
input_device = self._sessionplayer.inputdevice
|
||||
name = input_device.name
|
||||
unique_id = input_device.unique_identifier
|
||||
device_profiles = _ba.app.config.setdefault(
|
||||
device_profiles = babase.app.config.setdefault(
|
||||
'Default Player Profiles', {}
|
||||
)
|
||||
|
||||
|
|
@ -656,7 +681,7 @@ class Chooser:
|
|||
del device_profiles[profilekey]
|
||||
else:
|
||||
device_profiles[profilekey] = profilename
|
||||
_ba.app.config.commit()
|
||||
babase.app.config.commit()
|
||||
|
||||
# Set this player's short and full name.
|
||||
self._sessionplayer.setname(
|
||||
|
|
@ -666,7 +691,7 @@ class Chooser:
|
|||
self._update_text()
|
||||
|
||||
# Inform the session that this player is ready.
|
||||
_ba.getsession().handlemessage(PlayerReadyMessage(self))
|
||||
_bascenev1.getsession().handlemessage(PlayerReadyMessage(self))
|
||||
|
||||
def _handle_ready_msg(self, ready: bool) -> None:
|
||||
force_team_switch = False
|
||||
|
|
@ -674,11 +699,10 @@ class Chooser:
|
|||
# Team auto-balance kicks us to another team if we try to
|
||||
# join the team with the most players.
|
||||
if not self._ready:
|
||||
if _ba.app.config.get('Auto Balance Teams', False):
|
||||
if babase.app.config.get('Auto Balance Teams', False):
|
||||
lobby = self.lobby
|
||||
sessionteams = lobby.sessionteams
|
||||
if len(sessionteams) > 1:
|
||||
|
||||
# First, calc how many players are on each team
|
||||
# ..we need to count both active players and
|
||||
# choosers that have been marked as ready.
|
||||
|
|
@ -704,20 +728,22 @@ class Chooser:
|
|||
|
||||
# Either force switch teams, or actually for realsies do the set-ready.
|
||||
if force_team_switch:
|
||||
_ba.playsound(self._errorsound)
|
||||
self._errorsound.play()
|
||||
self.handlemessage(ChangeMessage('team', 1))
|
||||
else:
|
||||
_ba.playsound(self._punchsound)
|
||||
self._punchsound.play()
|
||||
self._set_ready(ready)
|
||||
|
||||
# TODO: should handle this at the engine layer so this is unnecessary.
|
||||
def _handle_repeat_message_attack(self) -> None:
|
||||
now = _ba.time()
|
||||
now = babase.apptime()
|
||||
count = self._last_change[1]
|
||||
if now - self._last_change[0] < QUICK_CHANGE_INTERVAL:
|
||||
count += 1
|
||||
if count > MAX_QUICK_CHANGE_COUNT:
|
||||
_ba.disconnect_client(self._sessionplayer.inputdevice.client_id)
|
||||
_bascenev1.disconnect_client(
|
||||
self._sessionplayer.inputdevice.client_id
|
||||
)
|
||||
elif now - self._last_change[0] > QUICK_CHANGE_RESET_INTERVAL:
|
||||
count = 0
|
||||
self._last_change = (now, count)
|
||||
|
|
@ -730,17 +756,17 @@ class Chooser:
|
|||
|
||||
# If we've been removed from the lobby, ignore this stuff.
|
||||
if self._dead:
|
||||
print_error('chooser got ChangeMessage after dying')
|
||||
logging.error('chooser got ChangeMessage after dying')
|
||||
return
|
||||
|
||||
if not self._text_node:
|
||||
print_error('got ChangeMessage after nodes died')
|
||||
logging.error('got ChangeMessage after nodes died')
|
||||
return
|
||||
|
||||
if msg.what == 'team':
|
||||
sessionteams = self.lobby.sessionteams
|
||||
if len(sessionteams) > 1:
|
||||
_ba.playsound(self._swish_sound)
|
||||
self._swish_sound.play()
|
||||
self._selected_team_index = (
|
||||
self._selected_team_index + msg.value
|
||||
) % len(sessionteams)
|
||||
|
|
@ -750,22 +776,20 @@ class Chooser:
|
|||
|
||||
elif msg.what == 'profileindex':
|
||||
if len(self._profilenames) == 1:
|
||||
|
||||
# This should be pretty hard to hit now with
|
||||
# automatic local accounts.
|
||||
_ba.playsound(_ba.getsound('error'))
|
||||
_bascenev1.getsound('error').play()
|
||||
else:
|
||||
|
||||
# Pick the next player profile and assign our name
|
||||
# and character based on that.
|
||||
_ba.playsound(self._deek_sound)
|
||||
self._deek_sound.play()
|
||||
self._profileindex = (self._profileindex + msg.value) % len(
|
||||
self._profilenames
|
||||
)
|
||||
self.update_from_profile()
|
||||
|
||||
elif msg.what == 'character':
|
||||
_ba.playsound(self._click_sound)
|
||||
self._click_sound.play()
|
||||
# update our index in our local list of characters
|
||||
self._character_index = (
|
||||
self._character_index + msg.value
|
||||
|
|
@ -779,21 +803,23 @@ class Chooser:
|
|||
def _update_text(self) -> None:
|
||||
assert self._text_node is not None
|
||||
if self._ready:
|
||||
|
||||
# Once we're ready, we've saved the name, so lets ask the system
|
||||
# for it so we get appended numbers and stuff.
|
||||
text = Lstr(value=self._sessionplayer.getname(full=True))
|
||||
text = Lstr(
|
||||
text = babase.Lstr(value=self._sessionplayer.getname(full=True))
|
||||
text = babase.Lstr(
|
||||
value='${A} (${B})',
|
||||
subs=[('${A}', text), ('${B}', Lstr(resource='readyText'))],
|
||||
subs=[
|
||||
('${A}', text),
|
||||
('${B}', babase.Lstr(resource='readyText')),
|
||||
],
|
||||
)
|
||||
else:
|
||||
text = Lstr(value=self._getname(full=True))
|
||||
text = babase.Lstr(value=self._getname(full=True))
|
||||
|
||||
can_switch_teams = len(self.lobby.sessionteams) > 1
|
||||
|
||||
# Flash as we're coming in.
|
||||
fin_color = _ba.safecolor(self.get_color()) + (1,)
|
||||
fin_color = babase.safecolor(self.get_color()) + (1,)
|
||||
if not self._inited:
|
||||
animate_array(
|
||||
self._text_node,
|
||||
|
|
@ -802,7 +828,6 @@ class Chooser:
|
|||
{0.15: fin_color, 0.25: (2, 2, 2, 1), 0.35: fin_color},
|
||||
)
|
||||
else:
|
||||
|
||||
# Blend if we're in teams mode; switch instantly otherwise.
|
||||
if can_switch_teams:
|
||||
animate_array(
|
||||
|
|
@ -839,7 +864,6 @@ class Chooser:
|
|||
if self.lobby.use_team_colors:
|
||||
for i, sessionteam in enumerate(self.lobby.sessionteams):
|
||||
if i != self._selected_team_index:
|
||||
|
||||
# Find the dominant component of this sessionteam's color
|
||||
# and adjust ours so that the component is
|
||||
# not super-dominant.
|
||||
|
|
@ -861,14 +885,15 @@ class Chooser:
|
|||
highlight[(max_index + 2) % 3] += diff * 0.2
|
||||
return highlight
|
||||
|
||||
def getplayer(self) -> ba.SessionPlayer:
|
||||
def getplayer(self) -> bascenev1.SessionPlayer:
|
||||
"""Return the player associated with this chooser."""
|
||||
return self._sessionplayer
|
||||
|
||||
def _update_icon(self) -> None:
|
||||
assert babase.app.classic is not None
|
||||
if self._profilenames[self._profileindex] == '_edit':
|
||||
tex = _ba.gettexture('black')
|
||||
tint_tex = _ba.gettexture('black')
|
||||
tex = _bascenev1.gettexture('black')
|
||||
tint_tex = _bascenev1.gettexture('black')
|
||||
self.icon.color = (1, 1, 1)
|
||||
self.icon.texture = tex
|
||||
self.icon.tint_texture = tint_tex
|
||||
|
|
@ -876,19 +901,19 @@ class Chooser:
|
|||
return
|
||||
|
||||
try:
|
||||
tex_name = _ba.app.spaz_appearances[
|
||||
tex_name = babase.app.classic.spaz_appearances[
|
||||
self._character_names[self._character_index]
|
||||
].icon_texture
|
||||
tint_tex_name = _ba.app.spaz_appearances[
|
||||
tint_tex_name = babase.app.classic.spaz_appearances[
|
||||
self._character_names[self._character_index]
|
||||
].icon_mask_texture
|
||||
except Exception:
|
||||
print_exception('Error updating char icon list')
|
||||
logging.exception('Error updating char icon list')
|
||||
tex_name = 'neoSpazIcon'
|
||||
tint_tex_name = 'neoSpazIconColorMask'
|
||||
|
||||
tex = _ba.gettexture(tex_name)
|
||||
tint_tex = _ba.gettexture(tint_tex_name)
|
||||
tex = _bascenev1.gettexture(tex_name)
|
||||
tint_tex = _bascenev1.gettexture(tint_tex_name)
|
||||
|
||||
self.icon.color = (1, 1, 1)
|
||||
self.icon.texture = tex
|
||||
|
|
@ -921,13 +946,12 @@ class Chooser:
|
|||
|
||||
|
||||
class Lobby:
|
||||
"""Container for ba.Choosers.
|
||||
"""Container for baclassic.Choosers.
|
||||
|
||||
Category: Gameplay Classes
|
||||
"""
|
||||
|
||||
def __del__(self) -> None:
|
||||
|
||||
# Reset any players that still have a chooser in us.
|
||||
# (should allow the choosers to die).
|
||||
sessionplayers = [
|
||||
|
|
@ -937,10 +961,10 @@ class Lobby:
|
|||
sessionplayer.resetinput()
|
||||
|
||||
def __init__(self) -> None:
|
||||
from ba._team import SessionTeam
|
||||
from ba._coopsession import CoopSession
|
||||
from bascenev1._team import SessionTeam
|
||||
from bascenev1._coopsession import CoopSession
|
||||
|
||||
session = _ba.getsession()
|
||||
session = _bascenev1.getsession()
|
||||
self._use_team_colors = session.use_team_colors
|
||||
if session.use_teams:
|
||||
self._sessionteams = [
|
||||
|
|
@ -976,8 +1000,8 @@ class Lobby:
|
|||
return self._use_team_colors
|
||||
|
||||
@property
|
||||
def sessionteams(self) -> list[ba.SessionTeam]:
|
||||
"""ba.SessionTeams available in this lobby."""
|
||||
def sessionteams(self) -> list[bascenev1.SessionTeam]:
|
||||
"""bascenev1.SessionTeams available in this lobby."""
|
||||
allteams = []
|
||||
for tref in self._sessionteams:
|
||||
team = tref()
|
||||
|
|
@ -1000,7 +1024,9 @@ class Lobby:
|
|||
def reload_profiles(self) -> None:
|
||||
"""Reload available player profiles."""
|
||||
# pylint: disable=cyclic-import
|
||||
from bastd.actor.spazappearance import get_appearances
|
||||
from bascenev1lib.actor.spazappearance import get_appearances
|
||||
|
||||
assert babase.app.classic is not None
|
||||
|
||||
# We may have gained or lost character names if the user
|
||||
# bought something; reload these too.
|
||||
|
|
@ -1008,13 +1034,13 @@ class Lobby:
|
|||
self.character_names_local_unlocked.sort(key=lambda x: x.lower())
|
||||
|
||||
# Do any overall prep we need to such as creating account profile.
|
||||
_ba.app.accounts_v1.ensure_have_account_player_profile()
|
||||
babase.app.classic.accounts.ensure_have_account_player_profile()
|
||||
for chooser in self.choosers:
|
||||
try:
|
||||
chooser.reload_profiles()
|
||||
chooser.update_from_profile()
|
||||
except Exception:
|
||||
print_exception('Error reloading profiles.')
|
||||
logging.exception('Error reloading profiles.')
|
||||
|
||||
def update_positions(self) -> None:
|
||||
"""Update positions for all choosers."""
|
||||
|
|
@ -1028,7 +1054,7 @@ class Lobby:
|
|||
"""Return whether all choosers are marked ready."""
|
||||
return all(chooser.ready for chooser in self.choosers)
|
||||
|
||||
def add_chooser(self, sessionplayer: ba.SessionPlayer) -> None:
|
||||
def add_chooser(self, sessionplayer: bascenev1.SessionPlayer) -> None:
|
||||
"""Add a chooser to the lobby for the provided player."""
|
||||
self.choosers.append(
|
||||
Chooser(vpos=self._vpos, sessionplayer=sessionplayer, lobby=self)
|
||||
|
|
@ -1038,7 +1064,7 @@ class Lobby:
|
|||
)
|
||||
self._vpos -= 48
|
||||
|
||||
def remove_chooser(self, player: ba.SessionPlayer) -> None:
|
||||
def remove_chooser(self, player: bascenev1.SessionPlayer) -> None:
|
||||
"""Remove a single player's chooser; does not kick them.
|
||||
|
||||
This is used when a player enters the game and no longer
|
||||
|
|
@ -1056,9 +1082,9 @@ class Lobby:
|
|||
self.choosers.remove(chooser)
|
||||
break
|
||||
if not found:
|
||||
print_error(f'remove_chooser did not find player {player}')
|
||||
logging.exception('remove_chooser did not find player %s.', player)
|
||||
elif chooser in self.choosers:
|
||||
print_error(f'chooser remains after removal for {player}')
|
||||
logging.exception('chooser remains after removal for %s.', player)
|
||||
self.update_positions()
|
||||
|
||||
def remove_all_choosers(self) -> None:
|
||||
|
|
@ -6,26 +6,15 @@ from __future__ import annotations
|
|||
import random
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import _ba
|
||||
from ba import _math
|
||||
from ba._actor import Actor
|
||||
import babase
|
||||
|
||||
import _bascenev1
|
||||
from bascenev1._actor import Actor
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Sequence, Any
|
||||
import ba
|
||||
|
||||
|
||||
def preload_map_preview_media() -> None:
|
||||
"""Preload media needed for map preview UIs.
|
||||
|
||||
Category: **Asset Functions**
|
||||
"""
|
||||
_ba.getmodel('level_select_button_opaque')
|
||||
_ba.getmodel('level_select_button_transparent')
|
||||
for maptype in list(_ba.app.maps.values()):
|
||||
map_tex_name = maptype.get_preview_texture_name()
|
||||
if map_tex_name is not None:
|
||||
_ba.gettexture(map_tex_name)
|
||||
import bascenev1
|
||||
|
||||
|
||||
def get_filtered_map_name(name: str) -> str:
|
||||
|
|
@ -43,80 +32,26 @@ def get_filtered_map_name(name: str) -> str:
|
|||
return name
|
||||
|
||||
|
||||
def get_map_display_string(name: str) -> ba.Lstr:
|
||||
"""Return a ba.Lstr for displaying a given map\'s name.
|
||||
def get_map_display_string(name: str) -> babase.Lstr:
|
||||
"""Return a babase.Lstr for displaying a given map\'s name.
|
||||
|
||||
Category: **Asset Functions**
|
||||
"""
|
||||
from ba import _language
|
||||
|
||||
return _language.Lstr(translate=('mapsNames', name))
|
||||
return babase.Lstr(translate=('mapsNames', name))
|
||||
|
||||
|
||||
def getmaps(playtype: str) -> list[str]:
|
||||
"""Return a list of ba.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.
|
||||
|
||||
Play Types:
|
||||
|
||||
'melee'
|
||||
General fighting map.
|
||||
Has one or more 'spawn' locations.
|
||||
|
||||
'team_flag'
|
||||
For games such as Capture The Flag where each team spawns by a flag.
|
||||
Has two or more 'spawn' locations, each with a corresponding 'flag'
|
||||
location (based on index).
|
||||
|
||||
'single_flag'
|
||||
For games such as King of the Hill or Keep Away where multiple teams
|
||||
are fighting over a single flag.
|
||||
Has two or more 'spawn' locations and 1 'flag_default' location.
|
||||
|
||||
'conquest'
|
||||
For games such as Conquest where flags are spread throughout the map
|
||||
- has 2+ 'flag' locations, 2+ 'spawn_by_flag' locations.
|
||||
|
||||
'king_of_the_hill' - has 2+ 'spawn' locations, 1+ 'flag_default' locations,
|
||||
and 1+ 'powerup_spawn' locations
|
||||
|
||||
'hockey'
|
||||
For hockey games.
|
||||
Has two 'goal' locations, corresponding 'spawn' locations, and one
|
||||
'flag_default' location (for where puck spawns)
|
||||
|
||||
'football'
|
||||
For football games.
|
||||
Has two 'goal' locations, corresponding 'spawn' locations, and one
|
||||
'flag_default' location (for where flag/ball/etc. spawns)
|
||||
|
||||
'race'
|
||||
For racing games where players much touch each region in order.
|
||||
Has two or more 'race_point' locations.
|
||||
"""
|
||||
return sorted(
|
||||
key
|
||||
for key, val in _ba.app.maps.items()
|
||||
if playtype in val.get_play_types()
|
||||
)
|
||||
|
||||
|
||||
def get_map_class(name: str) -> type[ba.Map]:
|
||||
def get_map_class(name: str) -> type[Map]:
|
||||
"""Return a map type given a name.
|
||||
|
||||
Category: **Asset Functions**
|
||||
"""
|
||||
assert babase.app.classic is not None
|
||||
name = get_filtered_map_name(name)
|
||||
try:
|
||||
return _ba.app.maps[name]
|
||||
mapclass: type[Map] = babase.app.classic.maps[name]
|
||||
return mapclass
|
||||
except KeyError:
|
||||
from ba import _error
|
||||
|
||||
raise _error.NotFoundError(f"Map not found: '{name}'") from None
|
||||
raise babase.NotFoundError(f"Map not found: '{name}'") from None
|
||||
|
||||
|
||||
class Map(Actor):
|
||||
|
|
@ -137,11 +72,12 @@ class Map(Actor):
|
|||
"""Preload map media.
|
||||
|
||||
This runs the class's on_preload() method as needed to prep it to run.
|
||||
Preloading should generally be done in a ba.Activity's __init__ method.
|
||||
Note that this is a classmethod since it is not operate on map
|
||||
instances but rather on the class itself before instances are made
|
||||
Preloading should generally be done in a bascenev1.Activity's
|
||||
__init__ method. Note that this is a classmethod since it is not
|
||||
operate on map instances but rather on the class itself before
|
||||
instances are made
|
||||
"""
|
||||
activity = _ba.getactivity()
|
||||
activity = _bascenev1.getactivity()
|
||||
if cls not in activity.preloads:
|
||||
activity.preloads[cls] = cls.on_preload()
|
||||
|
||||
|
|
@ -169,7 +105,7 @@ class Map(Actor):
|
|||
return cls.name
|
||||
|
||||
@classmethod
|
||||
def get_music_type(cls) -> ba.MusicType | None:
|
||||
def get_music_type(cls) -> bascenev1.MusicType | None:
|
||||
"""Return a music-type string that should be played on this map.
|
||||
|
||||
If None is returned, default music will be used.
|
||||
|
|
@ -182,18 +118,17 @@ class Map(Actor):
|
|||
"""Instantiate a map."""
|
||||
super().__init__()
|
||||
|
||||
# This is expected to always be a ba.Node object (whether valid or not)
|
||||
# should be set to something meaningful by child classes.
|
||||
self.node: _ba.Node | None = None
|
||||
# This is expected to always be a bascenev1.Node object
|
||||
# (whether valid or not) should be set to something meaningful
|
||||
# by child classes.
|
||||
self.node: _bascenev1.Node | None = None
|
||||
|
||||
# Make our class' preload-data available to us
|
||||
# (and instruct the user if we weren't preloaded properly).
|
||||
try:
|
||||
self.preloaddata = _ba.getactivity().preloads[type(self)]
|
||||
self.preloaddata = _bascenev1.getactivity().preloads[type(self)]
|
||||
except Exception as exc:
|
||||
from ba import _error
|
||||
|
||||
raise _error.NotFoundError(
|
||||
raise babase.NotFoundError(
|
||||
'Preload data not found for '
|
||||
+ str(type(self))
|
||||
+ '; make sure to call the type\'s preload()'
|
||||
|
|
@ -201,11 +136,7 @@ class Map(Actor):
|
|||
) from exc
|
||||
|
||||
# Set various globals.
|
||||
gnode = _ba.getactivity().globalsnode
|
||||
import ba
|
||||
import custom_hooks
|
||||
custom_hooks.on_map_init()
|
||||
|
||||
gnode = _bascenev1.getactivity().globalsnode
|
||||
|
||||
# Set area-of-interest bounds.
|
||||
aoi_bounds = self.get_def_bound_box('area_of_interest_bounds')
|
||||
|
|
@ -219,7 +150,7 @@ class Map(Actor):
|
|||
if map_bounds is None:
|
||||
print('WARNING: no "map_bounds" found for map:', self.getname())
|
||||
map_bounds = (-30, -10, -30, 30, 100, 30)
|
||||
_ba.set_map_bounds(map_bounds)
|
||||
_bascenev1.set_map_bounds(map_bounds)
|
||||
|
||||
# Set shadow ranges.
|
||||
try:
|
||||
|
|
@ -288,7 +219,9 @@ class Map(Actor):
|
|||
len(self.ffa_spawn_points)
|
||||
)
|
||||
|
||||
def is_point_near_edge(self, point: ba.Vec3, running: bool = False) -> bool:
|
||||
def is_point_near_edge(
|
||||
self, point: babase.Vec3, running: bool = False
|
||||
) -> bool:
|
||||
"""Return whether the provided point is near an edge of the map.
|
||||
|
||||
Simple bot logic uses this call to determine if they
|
||||
|
|
@ -322,7 +255,7 @@ class Map(Actor):
|
|||
return (
|
||||
None
|
||||
if val is None
|
||||
else _math.vec3validate(val)
|
||||
else babase.vec3validate(val)
|
||||
if __debug__
|
||||
else val
|
||||
)
|
||||
|
|
@ -360,12 +293,12 @@ class Map(Actor):
|
|||
return pnt
|
||||
|
||||
def get_ffa_start_position(
|
||||
self, players: Sequence[ba.Player]
|
||||
self, players: Sequence[bascenev1.Player]
|
||||
) -> Sequence[float]:
|
||||
"""Return a random starting position in one of the FFA spawn areas.
|
||||
|
||||
If a list of ba.Player-s is provided; the returned points will be
|
||||
as far from these players as possible.
|
||||
If a list of bascenev1.Player-s is provided; the returned points
|
||||
will be as far from these players as possible.
|
||||
"""
|
||||
|
||||
# Get positions for existing players.
|
||||
|
|
@ -396,7 +329,7 @@ class Map(Actor):
|
|||
farthestpt_dist = -1.0
|
||||
farthestpt = None
|
||||
for _i in range(10):
|
||||
testpt = _ba.Vec3(_getpt())
|
||||
testpt = babase.Vec3(_getpt())
|
||||
closest_player_dist = 9999.0
|
||||
for ppt in player_pts:
|
||||
dist = (ppt - testpt).length()
|
||||
|
|
@ -424,7 +357,7 @@ class Map(Actor):
|
|||
return bool(self.node)
|
||||
|
||||
def handlemessage(self, msg: Any) -> Any:
|
||||
from ba import _messages
|
||||
from bascenev1 import _messages
|
||||
|
||||
if isinstance(msg, _messages.DieMessage):
|
||||
if self.node:
|
||||
|
|
@ -436,6 +369,7 @@ class Map(Actor):
|
|||
|
||||
def register_map(maptype: type[Map]) -> None:
|
||||
"""Register a map class with the game."""
|
||||
if maptype.name in _ba.app.maps:
|
||||
assert babase.app.classic is not None
|
||||
if maptype.name in babase.app.classic.maps:
|
||||
raise RuntimeError('map "' + maptype.name + '" already registered')
|
||||
_ba.app.maps[maptype.name] = maptype
|
||||
babase.app.classic.maps[maptype.name] = maptype
|
||||
|
|
@ -8,11 +8,12 @@ from dataclasses import dataclass
|
|||
from typing import TYPE_CHECKING, TypeVar
|
||||
from enum import Enum
|
||||
|
||||
import _ba
|
||||
import babase
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Sequence, Any
|
||||
import ba
|
||||
|
||||
import bascenev1
|
||||
|
||||
|
||||
class _UnhandledType:
|
||||
|
|
@ -53,7 +54,7 @@ class DieMessage:
|
|||
|
||||
Category: **Message Classes**
|
||||
|
||||
Most ba.Actor-s respond to this.
|
||||
Most bascenev1.Actor-s respond to this.
|
||||
"""
|
||||
|
||||
immediate: bool = False
|
||||
|
|
@ -66,13 +67,11 @@ class DieMessage:
|
|||
"""The particular reason for death."""
|
||||
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
PlayerType = TypeVar('PlayerType', bound='ba.Player')
|
||||
# pylint: enable=invalid-name
|
||||
PlayerT = TypeVar('PlayerT', bound='bascenev1.Player')
|
||||
|
||||
|
||||
class PlayerDiedMessage:
|
||||
"""A message saying a ba.Player has died.
|
||||
"""A message saying a bascenev1.Player has died.
|
||||
|
||||
Category: **Message Classes**
|
||||
"""
|
||||
|
|
@ -81,15 +80,15 @@ class PlayerDiedMessage:
|
|||
"""If True, the player was killed;
|
||||
If False, they left the game or the round ended."""
|
||||
|
||||
how: ba.DeathType
|
||||
how: DeathType
|
||||
"""The particular type of death."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
player: ba.Player,
|
||||
player: bascenev1.Player,
|
||||
was_killed: bool,
|
||||
killerplayer: ba.Player | None,
|
||||
how: ba.DeathType,
|
||||
killerplayer: bascenev1.Player | None,
|
||||
how: DeathType,
|
||||
):
|
||||
"""Instantiate a message with the given values."""
|
||||
|
||||
|
|
@ -103,18 +102,16 @@ class PlayerDiedMessage:
|
|||
self.killed = was_killed
|
||||
self.how = how
|
||||
|
||||
def getkillerplayer(
|
||||
self, playertype: type[PlayerType]
|
||||
) -> PlayerType | None:
|
||||
"""Return the ba.Player responsible for the killing, if any.
|
||||
def getkillerplayer(self, playertype: type[PlayerT]) -> PlayerT | None:
|
||||
"""Return the bascenev1.Player responsible for the killing, if any.
|
||||
|
||||
Pass the Player type being used by the current game.
|
||||
"""
|
||||
assert isinstance(self._killerplayer, (playertype, type(None)))
|
||||
return self._killerplayer
|
||||
|
||||
def getplayer(self, playertype: type[PlayerType]) -> PlayerType:
|
||||
"""Return the ba.Player that died.
|
||||
def getplayer(self, playertype: type[PlayerT]) -> PlayerT:
|
||||
"""Return the bascenev1.Player that died.
|
||||
|
||||
The type of player for the current activity should be passed so that
|
||||
the type-checker properly identifies the returned value as one.
|
||||
|
|
@ -151,8 +148,8 @@ class PickUpMessage:
|
|||
Category: **Message Classes**
|
||||
"""
|
||||
|
||||
node: ba.Node
|
||||
"""The ba.Node that is getting picked up."""
|
||||
node: bascenev1.Node
|
||||
"""The bascenev1.Node that is getting picked up."""
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
@ -170,8 +167,8 @@ class PickedUpMessage:
|
|||
Category: **Message Classes**
|
||||
"""
|
||||
|
||||
node: ba.Node
|
||||
"""The ba.Node doing the picking up."""
|
||||
node: bascenev1.Node
|
||||
"""The bascenev1.Node doing the picking up."""
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
@ -181,8 +178,8 @@ class DroppedMessage:
|
|||
Category: **Message Classes**
|
||||
"""
|
||||
|
||||
node: ba.Node
|
||||
"""The ba.Node doing the dropping."""
|
||||
node: bascenev1.Node
|
||||
"""The bascenev1.Node doing the dropping."""
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
@ -210,7 +207,7 @@ class FreezeMessage:
|
|||
|
||||
Category: **Message Classes**
|
||||
|
||||
As seen in the effects of an ice ba.Bomb.
|
||||
As seen in the effects of an ice bascenev1.Bomb.
|
||||
"""
|
||||
|
||||
|
||||
|
|
@ -244,13 +241,13 @@ class HitMessage:
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
srcnode: ba.Node | None = None,
|
||||
srcnode: bascenev1.Node | None = None,
|
||||
pos: Sequence[float] | None = None,
|
||||
velocity: Sequence[float] | None = None,
|
||||
magnitude: float = 1.0,
|
||||
velocity_magnitude: float = 0.0,
|
||||
radius: float = 1.0,
|
||||
source_player: ba.Player | None = None,
|
||||
source_player: bascenev1.Player | None = None,
|
||||
kick_back: float = 1.0,
|
||||
flat_damage: float | None = None,
|
||||
hit_type: str = 'generic',
|
||||
|
|
@ -260,8 +257,8 @@ class HitMessage:
|
|||
"""Instantiate a message with given values."""
|
||||
|
||||
self.srcnode = srcnode
|
||||
self.pos = pos if pos is not None else _ba.Vec3()
|
||||
self.velocity = velocity if velocity is not None else _ba.Vec3()
|
||||
self.pos = pos if pos is not None else babase.Vec3()
|
||||
self.velocity = velocity if velocity is not None else babase.Vec3()
|
||||
self.magnitude = magnitude
|
||||
self.velocity_magnitude = velocity_magnitude
|
||||
self.radius = radius
|
||||
|
|
@ -277,9 +274,7 @@ class HitMessage:
|
|||
force_direction if force_direction is not None else velocity
|
||||
)
|
||||
|
||||
def get_source_player(
|
||||
self, playertype: type[PlayerType]
|
||||
) -> PlayerType | None:
|
||||
def get_source_player(self, playertype: type[PlayerT]) -> PlayerT | None:
|
||||
"""Return the source-player if one exists and is the provided type."""
|
||||
player: Any = self._source_player
|
||||
|
||||
|
|
@ -5,27 +5,30 @@ from __future__ import annotations
|
|||
|
||||
import copy
|
||||
import random
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import _ba
|
||||
from ba._session import Session
|
||||
from ba._error import NotFoundError, print_error
|
||||
import babase
|
||||
import _bascenev1
|
||||
from bascenev1._session import Session
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any, Sequence
|
||||
import ba
|
||||
|
||||
import bascenev1
|
||||
|
||||
DEFAULT_TEAM_COLORS = ((0.1, 0.25, 1.0), (1.0, 0.25, 0.2))
|
||||
DEFAULT_TEAM_NAMES = ('Blue', 'Red')
|
||||
|
||||
|
||||
class MultiTeamSession(Session):
|
||||
"""Common base class for ba.DualTeamSession and ba.FreeForAllSession.
|
||||
"""Common base for DualTeamSession and FreeForAllSession.
|
||||
|
||||
Category: **Gameplay Classes**
|
||||
|
||||
Free-for-all-mode is essentially just teams-mode with each ba.Player having
|
||||
their own ba.Team, so there is much overlap in functionality.
|
||||
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.
|
||||
"""
|
||||
|
||||
# These should be overridden.
|
||||
|
|
@ -34,12 +37,14 @@ class MultiTeamSession(Session):
|
|||
_playlists_var = 'UNSET Playlists'
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Set up playlists and launches a ba.Activity to accept joiners."""
|
||||
"""Set up playlists & launch a bascenev1.Activity to accept joiners."""
|
||||
# pylint: disable=cyclic-import
|
||||
from ba import _playlist
|
||||
from bastd.activity.multiteamjoin import MultiTeamJoinActivity
|
||||
from bascenev1 import _playlist
|
||||
from bascenev1lib.activity.multiteamjoin import MultiTeamJoinActivity
|
||||
|
||||
app = _ba.app
|
||||
app = babase.app
|
||||
classic = app.classic
|
||||
assert classic is not None
|
||||
cfg = app.config
|
||||
|
||||
if self.use_teams:
|
||||
|
|
@ -50,7 +55,7 @@ class MultiTeamSession(Session):
|
|||
team_colors = None
|
||||
|
||||
# print('FIXME: TEAM BASE SESSION WOULD CALC DEPS.')
|
||||
depsets: Sequence[ba.DependencySet] = []
|
||||
depsets: Sequence[bascenev1.DependencySet] = []
|
||||
|
||||
super().__init__(
|
||||
depsets,
|
||||
|
|
@ -60,17 +65,21 @@ class MultiTeamSession(Session):
|
|||
max_players=self.get_max_players(),
|
||||
)
|
||||
|
||||
self._series_length = app.teams_series_length
|
||||
self._ffa_series_length = app.ffa_series_length
|
||||
self._series_length: int = classic.teams_series_length
|
||||
self._ffa_series_length: int = classic.ffa_series_length
|
||||
|
||||
show_tutorial = cfg.get('Show Tutorial', True)
|
||||
|
||||
self._tutorial_activity_instance: ba.Activity | None
|
||||
self._tutorial_activity_instance: bascenev1.Activity | None
|
||||
if show_tutorial:
|
||||
from bastd.tutorial import TutorialActivity
|
||||
from bascenev1lib.tutorial import TutorialActivity
|
||||
|
||||
tutorial_activity = TutorialActivity
|
||||
|
||||
# Get this loading.
|
||||
self._tutorial_activity_instance = _ba.newactivity(TutorialActivity)
|
||||
self._tutorial_activity_instance = _bascenev1.newactivity(
|
||||
tutorial_activity
|
||||
)
|
||||
else:
|
||||
self._tutorial_activity_instance = None
|
||||
|
||||
|
|
@ -88,7 +97,6 @@ class MultiTeamSession(Session):
|
|||
self._playlist_name != '__default__'
|
||||
and self._playlist_name in playlists
|
||||
):
|
||||
|
||||
# Make sure to copy this, as we muck with it in place once we've
|
||||
# got it and we don't want that to affect our config.
|
||||
playlist = copy.deepcopy(playlists[self._playlist_name])
|
||||
|
|
@ -105,16 +113,9 @@ class MultiTeamSession(Session):
|
|||
add_resolved_type=True,
|
||||
name='default teams' if self.use_teams else 'default ffa',
|
||||
)
|
||||
default_playlist_resolved = _playlist.filter_playlist(
|
||||
_playlist.get_default_teams_playlist(),
|
||||
sessiontype=type(self),
|
||||
add_resolved_type=True,
|
||||
name='default teams' if self.use_teams else 'default ffa',
|
||||
)
|
||||
|
||||
if not playlist_resolved:
|
||||
print("PLAYLIST CONTAINS NO VALID GAMES , FALLING BACK TO DEFAULT TEAM PLAYLIST")
|
||||
playlist_resolved = default_playlist_resolved
|
||||
# raise RuntimeError('Playlist contains no valid games.')
|
||||
raise RuntimeError('Playlist contains no valid games.')
|
||||
|
||||
self._playlist = ShuffleList(
|
||||
playlist_resolved, shuffle=self._playlist_randomize
|
||||
|
|
@ -123,7 +124,7 @@ class MultiTeamSession(Session):
|
|||
# Get a game on deck ready to go.
|
||||
self._current_game_spec: dict[str, Any] | None = None
|
||||
self._next_game_spec: dict[str, Any] = self._playlist.pull_next()
|
||||
self._next_game: type[ba.GameActivity] = self._next_game_spec[
|
||||
self._next_game: type[bascenev1.GameActivity] = self._next_game_spec[
|
||||
'resolved_type'
|
||||
]
|
||||
|
||||
|
|
@ -132,7 +133,7 @@ class MultiTeamSession(Session):
|
|||
self._instantiate_next_game()
|
||||
|
||||
# Start in our custom join screen.
|
||||
self.setactivity(_ba.newactivity(MultiTeamJoinActivity))
|
||||
self.setactivity(_bascenev1.newactivity(MultiTeamJoinActivity))
|
||||
|
||||
def get_ffa_series_length(self) -> int:
|
||||
"""Return free-for-all series length."""
|
||||
|
|
@ -142,10 +143,10 @@ class MultiTeamSession(Session):
|
|||
"""Return teams series length."""
|
||||
return self._series_length
|
||||
|
||||
def get_next_game_description(self) -> ba.Lstr:
|
||||
def get_next_game_description(self) -> babase.Lstr:
|
||||
"""Returns a description of the next game on deck."""
|
||||
# pylint: disable=cyclic-import
|
||||
from ba._gameactivity import GameActivity
|
||||
from bascenev1._gameactivity import GameActivity
|
||||
|
||||
gametype: type[GameActivity] = self._next_game_spec['resolved_type']
|
||||
assert issubclass(gametype, GameActivity)
|
||||
|
|
@ -155,28 +156,30 @@ class MultiTeamSession(Session):
|
|||
"""Returns which game in the series is currently being played."""
|
||||
return self._game_number
|
||||
|
||||
def on_team_join(self, team: ba.SessionTeam) -> None:
|
||||
def on_team_join(self, team: bascenev1.SessionTeam) -> None:
|
||||
team.customdata['previous_score'] = team.customdata['score'] = 0
|
||||
|
||||
def get_max_players(self) -> int:
|
||||
"""Return max number of ba.Player-s allowed to join the game at once"""
|
||||
"""Return max number of Players allowed to join the game at once."""
|
||||
if self.use_teams:
|
||||
return _ba.app.config.get('Team Game Max Players', 8)
|
||||
return _ba.app.config.get('Free-for-All Max Players', 8)
|
||||
return babase.app.config.get('Team Game Max Players', 8)
|
||||
return babase.app.config.get('Free-for-All Max Players', 8)
|
||||
|
||||
def _instantiate_next_game(self) -> None:
|
||||
self._next_game_instance = _ba.newactivity(
|
||||
self._next_game_instance = _bascenev1.newactivity(
|
||||
self._next_game_spec['resolved_type'],
|
||||
self._next_game_spec['settings'],
|
||||
)
|
||||
|
||||
def on_activity_end(self, activity: ba.Activity, results: Any) -> None:
|
||||
def on_activity_end(
|
||||
self, activity: bascenev1.Activity, results: Any
|
||||
) -> None:
|
||||
# pylint: disable=cyclic-import
|
||||
from bastd.tutorial import TutorialActivity
|
||||
from bastd.activity.multiteamvictory import (
|
||||
from bascenev1lib.tutorial import TutorialActivity
|
||||
from bascenev1lib.activity.multiteamvictory import (
|
||||
TeamSeriesVictoryScoreScreenActivity,
|
||||
)
|
||||
from ba._activitytypes import (
|
||||
from bascenev1._activitytypes import (
|
||||
TransitionActivity,
|
||||
JoinActivity,
|
||||
ScoreScreenActivity,
|
||||
|
|
@ -192,14 +195,13 @@ class MultiTeamSession(Session):
|
|||
# to transition us into a round gracefully (otherwise we'd snap from
|
||||
# one terrain to another instantly).
|
||||
elif isinstance(activity, TutorialActivity):
|
||||
self.setactivity(_ba.newactivity(TransitionActivity))
|
||||
self.setactivity(_bascenev1.newactivity(TransitionActivity))
|
||||
|
||||
# If we're in a between-round activity or a restart-activity, hop
|
||||
# into a round.
|
||||
elif isinstance(
|
||||
activity, (JoinActivity, TransitionActivity, ScoreScreenActivity)
|
||||
):
|
||||
|
||||
# If we're coming from a series-end activity, reset scores.
|
||||
if isinstance(activity, TeamSeriesVictoryScoreScreenActivity):
|
||||
self.stats.reset()
|
||||
|
|
@ -226,7 +228,7 @@ class MultiTeamSession(Session):
|
|||
# (ie: no longer sitting in the lobby).
|
||||
try:
|
||||
has_team = player.sessionteam is not None
|
||||
except NotFoundError:
|
||||
except babase.NotFoundError:
|
||||
has_team = False
|
||||
if has_team:
|
||||
self.stats.register_sessionplayer(player)
|
||||
|
|
@ -242,12 +244,12 @@ class MultiTeamSession(Session):
|
|||
def _switch_to_score_screen(self, results: Any) -> None:
|
||||
"""Switch to a score screen after leaving a round."""
|
||||
del results # Unused arg.
|
||||
print_error('this should be overridden')
|
||||
logging.error('This should be overridden.', stack_info=True)
|
||||
|
||||
def announce_game_results(
|
||||
self,
|
||||
activity: ba.GameActivity,
|
||||
results: ba.GameResults,
|
||||
activity: bascenev1.GameActivity,
|
||||
results: bascenev1.GameResults,
|
||||
delay: float,
|
||||
announce_winning_team: bool = True,
|
||||
) -> None:
|
||||
|
|
@ -259,15 +261,11 @@ class MultiTeamSession(Session):
|
|||
announcement of the same.
|
||||
"""
|
||||
# pylint: disable=cyclic-import
|
||||
# pylint: disable=too-many-locals
|
||||
from ba._math import normalized_color
|
||||
from ba._general import Call
|
||||
from ba._gameutils import cameraflash
|
||||
from ba._language import Lstr
|
||||
from ba._freeforallsession import FreeForAllSession
|
||||
from ba._messages import CelebrateMessage
|
||||
from bascenev1._gameutils import cameraflash
|
||||
from bascenev1._freeforallsession import FreeForAllSession
|
||||
from bascenev1._messages import CelebrateMessage
|
||||
|
||||
_ba.timer(delay, Call(_ba.playsound, _ba.getsound('boxingBell')))
|
||||
_bascenev1.timer(delay, _bascenev1.getsound('boxingBell').play)
|
||||
|
||||
if announce_winning_team:
|
||||
winning_sessionteam = results.winning_sessionteam
|
||||
|
|
@ -285,14 +283,14 @@ class MultiTeamSession(Session):
|
|||
wins_resource = 'winsPlayerText'
|
||||
else:
|
||||
wins_resource = 'winsTeamText'
|
||||
wins_text = Lstr(
|
||||
wins_text = babase.Lstr(
|
||||
resource=wins_resource,
|
||||
subs=[('${NAME}', winning_sessionteam.name)],
|
||||
)
|
||||
activity.show_zoom_message(
|
||||
wins_text,
|
||||
scale=0.85,
|
||||
color=normalized_color(winning_sessionteam.color),
|
||||
color=babase.normalized_color(winning_sessionteam.color),
|
||||
)
|
||||
|
||||
|
||||
73
dist/ba_data/python/bascenev1/_music.py
vendored
Normal file
73
dist/ba_data/python/bascenev1/_music.py
vendored
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Music related bits."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import Enum
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import _bascenev1
|
||||
|
||||
if TYPE_CHECKING:
|
||||
pass
|
||||
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
MENU = 'Menu'
|
||||
VICTORY = 'Victory'
|
||||
CHAR_SELECT = 'CharSelect'
|
||||
RUN_AWAY = 'RunAway'
|
||||
ONSLAUGHT = 'Onslaught'
|
||||
KEEP_AWAY = 'Keep Away'
|
||||
RACE = 'Race'
|
||||
EPIC_RACE = 'Epic Race'
|
||||
SCORES = 'Scores'
|
||||
GRAND_ROMP = 'GrandRomp'
|
||||
TO_THE_DEATH = 'ToTheDeath'
|
||||
CHOSEN_ONE = 'Chosen One'
|
||||
FORWARD_MARCH = 'ForwardMarch'
|
||||
FLAG_CATCHER = 'FlagCatcher'
|
||||
SURVIVAL = 'Survival'
|
||||
EPIC = 'Epic'
|
||||
SPORTS = 'Sports'
|
||||
HOCKEY = 'Hockey'
|
||||
FOOTBALL = 'Football'
|
||||
FLYING = 'Flying'
|
||||
SCARY = 'Scary'
|
||||
MARCHING = 'Marching'
|
||||
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
# All we do here now is set a few music attrs on the current globals
|
||||
# node. The foreground globals' current playing music then gets fed to
|
||||
# the do_play_music call in our music controller. This way we can
|
||||
# seamlessly support custom soundtracks in replays/etc since we're being
|
||||
# driven purely by node data.
|
||||
gnode = _bascenev1.getactivity().globalsnode
|
||||
gnode.music_continuous = continuous
|
||||
gnode.music = '' if musictype is None else musictype.value
|
||||
gnode.music_count += 1
|
||||
|
|
@ -6,16 +6,17 @@ from __future__ import annotations
|
|||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from ba._messages import DieMessage
|
||||
from ba._actor import Actor
|
||||
from bascenev1._messages import DieMessage
|
||||
from bascenev1._actor import Actor
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import ba
|
||||
from typing import Any
|
||||
|
||||
import bascenev1
|
||||
|
||||
|
||||
class NodeActor(Actor):
|
||||
"""A simple ba.Actor type that wraps a single ba.Node.
|
||||
"""A simple bascenev1.Actor type that wraps a single bascenev1.Node.
|
||||
|
||||
Category: **Gameplay Classes**
|
||||
|
||||
|
|
@ -23,7 +24,7 @@ class NodeActor(Actor):
|
|||
exists() call will return whether the Node still exists or not.
|
||||
"""
|
||||
|
||||
def __init__(self, node: ba.Node):
|
||||
def __init__(self, node: bascenev1.Node):
|
||||
super().__init__()
|
||||
self.node = node
|
||||
|
||||
|
|
@ -4,25 +4,22 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, TypeVar, Generic, cast
|
||||
|
||||
import _ba
|
||||
from ba._error import (
|
||||
SessionPlayerNotFoundError,
|
||||
print_exception,
|
||||
ActorNotFoundError,
|
||||
)
|
||||
from ba._messages import DeathType, DieMessage
|
||||
import babase
|
||||
|
||||
import _bascenev1
|
||||
from bascenev1._messages import DeathType, DieMessage
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Sequence, Any, Callable
|
||||
import ba
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
PlayerType = TypeVar('PlayerType', bound='ba.Player')
|
||||
TeamType = TypeVar('TeamType', bound='ba.Team')
|
||||
# pylint: enable=invalid-name
|
||||
import bascenev1
|
||||
|
||||
PlayerT = TypeVar('PlayerT', bound='bascenev1.Player')
|
||||
TeamT = TypeVar('TeamT', bound='bascenev1.Team')
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
@ -43,33 +40,33 @@ class StandLocation:
|
|||
Category: Gameplay Classes
|
||||
"""
|
||||
|
||||
position: ba.Vec3
|
||||
position: babase.Vec3
|
||||
angle: float | None = None
|
||||
|
||||
|
||||
class Player(Generic[TeamType]):
|
||||
"""A player in a specific ba.Activity.
|
||||
class Player(Generic[TeamT]):
|
||||
"""A player in a specific bascenev1.Activity.
|
||||
|
||||
Category: Gameplay Classes
|
||||
|
||||
These correspond to ba.SessionPlayer objects, but are associated with a
|
||||
single ba.Activity instance. This allows activities to specify their
|
||||
own custom ba.Player types.
|
||||
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.
|
||||
"""
|
||||
|
||||
# These are instance attrs but we define them at the type level so
|
||||
# their type annotations are introspectable (for docs generation).
|
||||
character: str
|
||||
|
||||
actor: ba.Actor | None
|
||||
"""The ba.Actor associated with the player."""
|
||||
actor: bascenev1.Actor | None
|
||||
"""The bascenev1.Actor associated with the player."""
|
||||
|
||||
color: Sequence[float]
|
||||
highlight: Sequence[float]
|
||||
|
||||
_team: TeamType
|
||||
_sessionplayer: ba.SessionPlayer
|
||||
_nodeactor: ba.NodeActor | None
|
||||
_team: TeamT
|
||||
_sessionplayer: bascenev1.SessionPlayer
|
||||
_nodeactor: bascenev1.NodeActor | None
|
||||
_expired: bool
|
||||
_postinited: bool
|
||||
_customdata: dict
|
||||
|
|
@ -79,12 +76,12 @@ class Player(Generic[TeamType]):
|
|||
# This also lets us keep trivial player classes cleaner by skipping
|
||||
# the super().__init__() line.
|
||||
|
||||
def postinit(self, sessionplayer: ba.SessionPlayer) -> None:
|
||||
def postinit(self, sessionplayer: bascenev1.SessionPlayer) -> None:
|
||||
"""Wire up a newly created player.
|
||||
|
||||
(internal)
|
||||
"""
|
||||
from ba._nodeactor import NodeActor
|
||||
from bascenev1._nodeactor import NodeActor
|
||||
|
||||
# Sanity check; if a dataclass is created that inherits from us,
|
||||
# it will define an equality operator by default which will break
|
||||
|
|
@ -100,17 +97,19 @@ class Player(Generic[TeamType]):
|
|||
|
||||
self.actor = None
|
||||
self.character = ''
|
||||
self._nodeactor: ba.NodeActor | None = None
|
||||
self._nodeactor: bascenev1.NodeActor | None = None
|
||||
self._sessionplayer = sessionplayer
|
||||
self.character = sessionplayer.character
|
||||
self.color = sessionplayer.color
|
||||
self.highlight = sessionplayer.highlight
|
||||
self._team = cast(TeamType, sessionplayer.sessionteam.activityteam)
|
||||
self._team = cast(TeamT, sessionplayer.sessionteam.activityteam)
|
||||
assert self._team is not None
|
||||
self._customdata = {}
|
||||
self._expired = False
|
||||
self._postinited = True
|
||||
node = _ba.newnode('player', attrs={'playerID': sessionplayer.id})
|
||||
node = _bascenev1.newnode(
|
||||
'player', attrs={'playerID': sessionplayer.id}
|
||||
)
|
||||
self._nodeactor = NodeActor(node)
|
||||
sessionplayer.setnode(node)
|
||||
|
||||
|
|
@ -127,7 +126,7 @@ class Player(Generic[TeamType]):
|
|||
self.actor.handlemessage(DieMessage(how=DeathType.LEFT_GAME))
|
||||
self.actor = None
|
||||
except Exception:
|
||||
print_exception(f'Error killing actor on leave for {self}')
|
||||
logging.exception('Error killing actor on leave for %s.', self)
|
||||
self._nodeactor = None
|
||||
del self._team
|
||||
del self._customdata
|
||||
|
|
@ -144,7 +143,7 @@ class Player(Generic[TeamType]):
|
|||
try:
|
||||
self.on_expire()
|
||||
except Exception:
|
||||
print_exception(f'Error in on_expire for {self}.')
|
||||
logging.exception('Error in on_expire for %s.', self)
|
||||
|
||||
self._nodeactor = None
|
||||
self.actor = None
|
||||
|
|
@ -161,8 +160,8 @@ class Player(Generic[TeamType]):
|
|||
"""
|
||||
|
||||
@property
|
||||
def team(self) -> TeamType:
|
||||
"""The ba.Team for this player."""
|
||||
def team(self) -> TeamT:
|
||||
"""The bascenev1.Team for this player."""
|
||||
assert self._postinited
|
||||
assert not self._expired
|
||||
return self._team
|
||||
|
|
@ -171,7 +170,7 @@ class Player(Generic[TeamType]):
|
|||
def customdata(self) -> dict:
|
||||
"""Arbitrary values associated with the player.
|
||||
Though it is encouraged that most player values be properly defined
|
||||
on the ba.Player subclass, it may be useful for player-agnostic
|
||||
on the bascenev1.Player subclass, it may be useful for player-agnostic
|
||||
objects to store values here. This dict is cleared when the player
|
||||
leaves or expires so objects stored here will be disposed of at
|
||||
the expected time, unlike the Player instance itself which may
|
||||
|
|
@ -182,19 +181,19 @@ class Player(Generic[TeamType]):
|
|||
return self._customdata
|
||||
|
||||
@property
|
||||
def sessionplayer(self) -> ba.SessionPlayer:
|
||||
"""Return the ba.SessionPlayer corresponding to this Player.
|
||||
def sessionplayer(self) -> bascenev1.SessionPlayer:
|
||||
"""Return the bascenev1.SessionPlayer corresponding to this Player.
|
||||
|
||||
Throws a ba.SessionPlayerNotFoundError if it does not exist.
|
||||
Throws a bascenev1.SessionPlayerNotFoundError if it does not exist.
|
||||
"""
|
||||
assert self._postinited
|
||||
if bool(self._sessionplayer):
|
||||
return self._sessionplayer
|
||||
raise SessionPlayerNotFoundError()
|
||||
raise babase.SessionPlayerNotFoundError()
|
||||
|
||||
@property
|
||||
def node(self) -> ba.Node:
|
||||
"""A ba.Node of type 'player' associated with this Player.
|
||||
def node(self) -> bascenev1.Node:
|
||||
"""A bascenev1.Node of type 'player' associated with this Player.
|
||||
|
||||
This node can be used to get a generic player position/etc.
|
||||
"""
|
||||
|
|
@ -204,23 +203,24 @@ class Player(Generic[TeamType]):
|
|||
return self._nodeactor.node
|
||||
|
||||
@property
|
||||
def position(self) -> ba.Vec3:
|
||||
"""The position of the player, as defined by its current ba.Actor.
|
||||
def position(self) -> babase.Vec3:
|
||||
"""The position of the player, as defined by its bascenev1.Actor.
|
||||
|
||||
If the player currently has no actor, raises a ba.ActorNotFoundError.
|
||||
If the player currently has no actor, raises a
|
||||
babase.ActorNotFoundError.
|
||||
"""
|
||||
assert self._postinited
|
||||
assert not self._expired
|
||||
if self.actor is None:
|
||||
raise ActorNotFoundError
|
||||
return _ba.Vec3(self.node.position)
|
||||
raise babase.ActorNotFoundError
|
||||
return babase.Vec3(self.node.position)
|
||||
|
||||
def exists(self) -> bool:
|
||||
"""Whether the underlying player still exists.
|
||||
|
||||
This will return False if the underlying ba.SessionPlayer has
|
||||
left the game or if the ba.Activity this player was associated
|
||||
with has ended.
|
||||
This will return False if the underlying bascenev1.SessionPlayer has
|
||||
left the game or if the bascenev1.Activity this player was
|
||||
associated with has ended.
|
||||
Most functionality will fail on a nonexistent player.
|
||||
Note that you can also use the boolean operator for this same
|
||||
functionality, so a statement such as "if player" will do
|
||||
|
|
@ -240,7 +240,7 @@ class Player(Generic[TeamType]):
|
|||
|
||||
def is_alive(self) -> bool:
|
||||
"""
|
||||
Returns True if the player has a ba.Actor assigned and its
|
||||
Returns True if the player has a bascenev1.Actor assigned and its
|
||||
is_alive() method return True. False is returned otherwise.
|
||||
"""
|
||||
assert self._postinited
|
||||
|
|
@ -256,7 +256,9 @@ class Player(Generic[TeamType]):
|
|||
return self._sessionplayer.get_icon()
|
||||
|
||||
def assigninput(
|
||||
self, inputtype: ba.InputType | tuple[ba.InputType, ...], call: Callable
|
||||
self,
|
||||
inputtype: babase.InputType | tuple[babase.InputType, ...],
|
||||
call: Callable,
|
||||
) -> None:
|
||||
"""
|
||||
Set the python callable to be run for one or more types of input.
|
||||
|
|
@ -277,16 +279,17 @@ class Player(Generic[TeamType]):
|
|||
return self.exists()
|
||||
|
||||
|
||||
class EmptyPlayer(Player['ba.EmptyTeam']):
|
||||
class EmptyPlayer(Player['bascenev1.EmptyTeam']):
|
||||
"""An empty player for use by Activities that don't need to define one.
|
||||
|
||||
Category: Gameplay Classes
|
||||
|
||||
ba.Player and ba.Team are 'Generic' types, and so passing those top level
|
||||
classes as type arguments when defining a ba.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 ba.Activity that does not need custom types of its own.
|
||||
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
|
||||
|
|
@ -300,13 +303,13 @@ class EmptyPlayer(Player['ba.EmptyTeam']):
|
|||
# instead of requiring extra work by them.
|
||||
|
||||
|
||||
def playercast(totype: type[PlayerType], player: ba.Player) -> PlayerType:
|
||||
"""Cast a ba.Player to a specific ba.Player subclass.
|
||||
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
|
||||
ba.Player objects which need to be cast back to the game's actual
|
||||
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
|
||||
is a safe way to do so. It ensures that Optional values are not cast
|
||||
into Non-Optional, etc.
|
||||
|
|
@ -319,9 +322,9 @@ def playercast(totype: type[PlayerType], player: ba.Player) -> PlayerType:
|
|||
# for the optional variety, but that currently seems to not be working.
|
||||
# See: https://github.com/python/mypy/issues/8800
|
||||
def playercast_o(
|
||||
totype: type[PlayerType], player: ba.Player | None
|
||||
) -> PlayerType | None:
|
||||
"""A variant of ba.playercast() for use with optional ba.Player values.
|
||||
totype: type[PlayerT], player: bascenev1.Player | None
|
||||
) -> PlayerT | None:
|
||||
"""A variant of bascenev1.playercast() for use with optional Player values.
|
||||
|
||||
Category: Gameplay Functions
|
||||
"""
|
||||
|
|
@ -4,23 +4,23 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
import _ba
|
||||
import copy
|
||||
import logging
|
||||
from typing import Any, TYPE_CHECKING
|
||||
|
||||
import _ba
|
||||
import babase
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Sequence
|
||||
from ba import _session
|
||||
|
||||
from bascenev1._session import Session
|
||||
|
||||
PlaylistType = list[dict[str, Any]]
|
||||
|
||||
|
||||
def filter_playlist(
|
||||
playlist: PlaylistType,
|
||||
sessiontype: type[_session.Session],
|
||||
sessiontype: type[Session],
|
||||
add_resolved_type: bool = False,
|
||||
remove_unowned: bool = True,
|
||||
mark_unowned: bool = False,
|
||||
|
|
@ -35,18 +35,17 @@ def filter_playlist(
|
|||
# pylint: disable=too-many-locals
|
||||
# pylint: disable=too-many-branches
|
||||
# pylint: disable=too-many-statements
|
||||
from ba._map import get_filtered_map_name
|
||||
from ba._error import MapNotFoundError
|
||||
from ba._store import get_unowned_maps, get_unowned_game_types
|
||||
from ba._general import getclass
|
||||
from ba._gameactivity import GameActivity
|
||||
from bascenev1._map import get_filtered_map_name
|
||||
from bascenev1._gameactivity import GameActivity
|
||||
|
||||
assert babase.app.classic is not None
|
||||
|
||||
goodlist: list[dict] = []
|
||||
unowned_maps: Sequence[str]
|
||||
available_maps: list[str] = list(_ba.app.maps.keys())
|
||||
if remove_unowned or mark_unowned:
|
||||
unowned_maps = get_unowned_maps()
|
||||
unowned_game_types = get_unowned_game_types()
|
||||
available_maps: list[str] = list(babase.app.classic.maps.keys())
|
||||
if (remove_unowned or mark_unowned) and babase.app.classic is not None:
|
||||
unowned_maps = babase.app.classic.store.get_unowned_maps()
|
||||
unowned_game_types = babase.app.classic.store.get_unowned_game_types()
|
||||
else:
|
||||
unowned_maps = []
|
||||
unowned_game_types = set()
|
||||
|
|
@ -81,89 +80,113 @@ def filter_playlist(
|
|||
'Happy_Thoughts.HappyThoughtsGame',
|
||||
'bsAssault.AssaultGame',
|
||||
'bs_assault.AssaultGame',
|
||||
'bastd.game.assault.AssaultGame',
|
||||
):
|
||||
entry['type'] = 'bastd.game.assault.AssaultGame'
|
||||
entry['type'] = 'bascenev1lib.game.assault.AssaultGame'
|
||||
if entry['type'] in (
|
||||
'King_of_the_Hill.KingOfTheHillGame',
|
||||
'bsKingOfTheHill.KingOfTheHillGame',
|
||||
'bs_king_of_the_hill.KingOfTheHillGame',
|
||||
'bastd.game.kingofthehill.KingOfTheHillGame',
|
||||
):
|
||||
entry['type'] = 'bastd.game.kingofthehill.KingOfTheHillGame'
|
||||
entry[
|
||||
'type'
|
||||
] = 'bascenev1lib.game.kingofthehill.KingOfTheHillGame'
|
||||
if entry['type'] in (
|
||||
'Capture_the_Flag.CTFGame',
|
||||
'bsCaptureTheFlag.CTFGame',
|
||||
'bs_capture_the_flag.CTFGame',
|
||||
'bastd.game.capturetheflag.CaptureTheFlagGame',
|
||||
):
|
||||
entry['type'] = 'bastd.game.capturetheflag.CaptureTheFlagGame'
|
||||
entry[
|
||||
'type'
|
||||
] = 'bascenev1lib.game.capturetheflag.CaptureTheFlagGame'
|
||||
if entry['type'] in (
|
||||
'Death_Match.DeathMatchGame',
|
||||
'bsDeathMatch.DeathMatchGame',
|
||||
'bs_death_match.DeathMatchGame',
|
||||
'bastd.game.deathmatch.DeathMatchGame',
|
||||
):
|
||||
entry['type'] = 'bastd.game.deathmatch.DeathMatchGame'
|
||||
entry['type'] = 'bascenev1lib.game.deathmatch.DeathMatchGame'
|
||||
if entry['type'] in (
|
||||
'ChosenOne.ChosenOneGame',
|
||||
'bsChosenOne.ChosenOneGame',
|
||||
'bs_chosen_one.ChosenOneGame',
|
||||
'bastd.game.chosenone.ChosenOneGame',
|
||||
):
|
||||
entry['type'] = 'bastd.game.chosenone.ChosenOneGame'
|
||||
entry['type'] = 'bascenev1lib.game.chosenone.ChosenOneGame'
|
||||
if entry['type'] in (
|
||||
'Conquest.Conquest',
|
||||
'Conquest.ConquestGame',
|
||||
'bsConquest.ConquestGame',
|
||||
'bs_conquest.ConquestGame',
|
||||
'bastd.game.conquest.ConquestGame',
|
||||
):
|
||||
entry['type'] = 'bastd.game.conquest.ConquestGame'
|
||||
entry['type'] = 'bascenev1lib.game.conquest.ConquestGame'
|
||||
if entry['type'] in (
|
||||
'Elimination.EliminationGame',
|
||||
'bsElimination.EliminationGame',
|
||||
'bs_elimination.EliminationGame',
|
||||
'bastd.game.elimination.EliminationGame',
|
||||
):
|
||||
entry['type'] = 'bastd.game.elimination.EliminationGame'
|
||||
entry['type'] = 'bascenev1lib.game.elimination.EliminationGame'
|
||||
if entry['type'] in (
|
||||
'Football.FootballGame',
|
||||
'bsFootball.FootballTeamGame',
|
||||
'bs_football.FootballTeamGame',
|
||||
'bastd.game.football.FootballTeamGame',
|
||||
):
|
||||
entry['type'] = 'bastd.game.football.FootballTeamGame'
|
||||
entry['type'] = 'bascenev1lib.game.football.FootballTeamGame'
|
||||
if entry['type'] in (
|
||||
'Hockey.HockeyGame',
|
||||
'bsHockey.HockeyGame',
|
||||
'bs_hockey.HockeyGame',
|
||||
'bastd.game.hockey.HockeyGame',
|
||||
):
|
||||
entry['type'] = 'bastd.game.hockey.HockeyGame'
|
||||
entry['type'] = 'bascenev1lib.game.hockey.HockeyGame'
|
||||
if entry['type'] in (
|
||||
'Keep_Away.KeepAwayGame',
|
||||
'bsKeepAway.KeepAwayGame',
|
||||
'bs_keep_away.KeepAwayGame',
|
||||
'bastd.game.keepaway.KeepAwayGame',
|
||||
):
|
||||
entry['type'] = 'bastd.game.keepaway.KeepAwayGame'
|
||||
entry['type'] = 'bascenev1lib.game.keepaway.KeepAwayGame'
|
||||
if entry['type'] in (
|
||||
'Race.RaceGame',
|
||||
'bsRace.RaceGame',
|
||||
'bs_race.RaceGame',
|
||||
'bastd.game.race.RaceGame',
|
||||
):
|
||||
entry['type'] = 'bastd.game.race.RaceGame'
|
||||
entry['type'] = 'bascenev1lib.game.race.RaceGame'
|
||||
if entry['type'] in (
|
||||
'bsEasterEggHunt.EasterEggHuntGame',
|
||||
'bs_easter_egg_hunt.EasterEggHuntGame',
|
||||
'bastd.game.easteregghunt.EasterEggHuntGame',
|
||||
):
|
||||
entry['type'] = 'bastd.game.easteregghunt.EasterEggHuntGame'
|
||||
entry[
|
||||
'type'
|
||||
] = 'bascenev1lib.game.easteregghunt.EasterEggHuntGame'
|
||||
if entry['type'] in (
|
||||
'bsMeteorShower.MeteorShowerGame',
|
||||
'bs_meteor_shower.MeteorShowerGame',
|
||||
'bastd.game.meteorshower.MeteorShowerGame',
|
||||
):
|
||||
entry['type'] = 'bastd.game.meteorshower.MeteorShowerGame'
|
||||
entry[
|
||||
'type'
|
||||
] = 'bascenev1lib.game.meteorshower.MeteorShowerGame'
|
||||
if entry['type'] in (
|
||||
'bsTargetPractice.TargetPracticeGame',
|
||||
'bs_target_practice.TargetPracticeGame',
|
||||
'bastd.game.targetpractice.TargetPracticeGame',
|
||||
):
|
||||
entry['type'] = 'bastd.game.targetpractice.TargetPracticeGame'
|
||||
entry[
|
||||
'type'
|
||||
] = 'bascenev1lib.game.targetpractice.TargetPracticeGame'
|
||||
|
||||
gameclass = getclass(entry['type'], GameActivity)
|
||||
gameclass = babase.getclass(entry['type'], GameActivity)
|
||||
|
||||
if entry['settings']['map'] not in available_maps:
|
||||
raise MapNotFoundError()
|
||||
raise babase.MapNotFoundError()
|
||||
|
||||
if remove_unowned and gameclass in unowned_game_types:
|
||||
continue
|
||||
|
|
@ -182,11 +205,11 @@ def filter_playlist(
|
|||
|
||||
goodlist.append(entry)
|
||||
|
||||
except MapNotFoundError:
|
||||
except babase.MapNotFoundError:
|
||||
logging.warning(
|
||||
'Map \'%s\' not found while scanning playlist \'%s\'.',
|
||||
name,
|
||||
entry['settings']['map'],
|
||||
name,
|
||||
)
|
||||
except ImportError as exc:
|
||||
logging.warning(
|
||||
|
|
@ -9,7 +9,8 @@ from dataclasses import dataclass
|
|||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Sequence
|
||||
import ba
|
||||
|
||||
import bascenev1
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
@ -18,27 +19,28 @@ class PowerupMessage:
|
|||
|
||||
Category: **Message Classes**
|
||||
|
||||
This message is normally received by touching a ba.PowerupBox.
|
||||
This message is normally received by touching a bascenev1.PowerupBox.
|
||||
"""
|
||||
|
||||
poweruptype: str
|
||||
"""The type of powerup to be granted (a string).
|
||||
See ba.Powerup.poweruptype for available type values."""
|
||||
See bascenev1.Powerup.poweruptype for available type values."""
|
||||
|
||||
sourcenode: ba.Node | None = None
|
||||
sourcenode: bascenev1.Node | None = None
|
||||
"""The node the powerup game from, or None otherwise.
|
||||
If a powerup is accepted, a ba.PowerupAcceptMessage should be sent
|
||||
back to the sourcenode to inform it of the fact. This will generally
|
||||
cause the powerup box to make a sound and disappear or whatnot."""
|
||||
If a powerup is accepted, a bascenev1.PowerupAcceptMessage should be
|
||||
sent back to the sourcenode to inform it of the fact. This will
|
||||
generally cause the powerup box to make a sound and disappear or
|
||||
whatnot."""
|
||||
|
||||
|
||||
@dataclass
|
||||
class PowerupAcceptMessage:
|
||||
"""A message informing a ba.Powerup that it was accepted.
|
||||
"""A message informing a bascenev1.Powerup that it was accepted.
|
||||
|
||||
Category: **Message Classes**
|
||||
|
||||
This is generally sent in response to a ba.PowerupMessage
|
||||
This is generally sent in response to a bascenev1.PowerupMessage
|
||||
to inform the box (or whoever granted it) that it can go away.
|
||||
"""
|
||||
|
||||
|
|
@ -6,7 +6,7 @@ from __future__ import annotations
|
|||
import random
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import _ba
|
||||
import babase
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any
|
||||
|
|
@ -43,9 +43,7 @@ def get_player_profile_icon(profilename: str) -> str:
|
|||
|
||||
(non-account profiles only)
|
||||
"""
|
||||
from ba._generated.enums import SpecialChar
|
||||
|
||||
appconfig = _ba.app.config
|
||||
appconfig = babase.app.config
|
||||
icon: str
|
||||
try:
|
||||
is_global = appconfig['Player Profiles'][profilename]['global']
|
||||
|
|
@ -55,7 +53,7 @@ def get_player_profile_icon(profilename: str) -> str:
|
|||
try:
|
||||
icon = appconfig['Player Profiles'][profilename]['icon']
|
||||
except KeyError:
|
||||
icon = _ba.charstr(SpecialChar.LOGO)
|
||||
icon = babase.charstr(babase.SpecialChar.LOGO)
|
||||
else:
|
||||
icon = ''
|
||||
return icon
|
||||
|
|
@ -65,13 +63,13 @@ def get_player_profile_colors(
|
|||
profilename: str | None, profiles: dict[str, dict[str, Any]] | None = None
|
||||
) -> tuple[tuple[float, float, float], tuple[float, float, float]]:
|
||||
"""Given a profile, return colors for them."""
|
||||
appconfig = _ba.app.config
|
||||
appconfig = babase.app.config
|
||||
if profiles is None:
|
||||
profiles = appconfig['Player Profiles']
|
||||
|
||||
# Special case: when being asked for a random color in kiosk mode,
|
||||
# always return default purple.
|
||||
if (_ba.app.demo_mode or _ba.app.arcade_mode) and profilename is None:
|
||||
if (babase.app.demo_mode or babase.app.arcade_mode) and profilename is None:
|
||||
color = (0.5, 0.4, 1.0)
|
||||
highlight = (0.4, 0.4, 0.5)
|
||||
else:
|
||||
|
|
@ -9,7 +9,7 @@ from dataclasses import dataclass
|
|||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import ba
|
||||
import bascenev1
|
||||
|
||||
|
||||
@unique
|
||||
|
|
@ -34,7 +34,7 @@ class ScoreConfig:
|
|||
label: str = 'Score'
|
||||
"""A label show to the user for scores; 'Score', 'Time Survived', etc."""
|
||||
|
||||
scoretype: ba.ScoreType = ScoreType.POINTS
|
||||
scoretype: bascenev1.ScoreType = ScoreType.POINTS
|
||||
"""How the score value should be displayed."""
|
||||
|
||||
lower_is_better: bool = False
|
||||
|
|
@ -4,28 +4,30 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import weakref
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import _ba
|
||||
from ba._error import print_error, print_exception, NodeNotFoundError
|
||||
from ba._language import Lstr
|
||||
from ba._player import Player
|
||||
import babase
|
||||
|
||||
import _bascenev1
|
||||
from bascenev1._player import Player
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Sequence, Any
|
||||
import ba
|
||||
|
||||
import bascenev1
|
||||
|
||||
|
||||
class Session:
|
||||
"""Defines a high level series of ba.Activity-es with a common purpose.
|
||||
"""Defines a high level series of bascenev1.Activity-es.
|
||||
|
||||
Category: **Gameplay Classes**
|
||||
|
||||
Examples of sessions are ba.FreeForAllSession, ba.DualTeamSession, and
|
||||
ba.CoopSession.
|
||||
Examples of sessions are bascenev1.FreeForAllSession,
|
||||
bascenev1.DualTeamSession, and bascenev1.CoopSession.
|
||||
|
||||
A Session is responsible for wrangling and transitioning between various
|
||||
ba.Activity instances such as mini-games and score-screens, and for
|
||||
bascenev1.Activity instances such as mini-games and score-screens, and for
|
||||
maintaining state between them (players, teams, score tallies, etc).
|
||||
"""
|
||||
|
||||
|
|
@ -43,9 +45,9 @@ class Session:
|
|||
# at the class level so that looks better and nobody get lost while
|
||||
# reading large __init__
|
||||
|
||||
lobby: ba.Lobby
|
||||
"""The ba.Lobby instance where new ba.Player-s go to select a
|
||||
Profile/Team/etc. before being added to games.
|
||||
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."""
|
||||
|
||||
|
|
@ -56,23 +58,23 @@ class Session:
|
|||
"""The minimum number of players who must be present for the Session
|
||||
to proceed past the initial joining screen"""
|
||||
|
||||
sessionplayers: list[ba.SessionPlayer]
|
||||
"""All ba.SessionPlayers in the Session. Most things should use the
|
||||
list of ba.Player-s in ba.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."""
|
||||
|
||||
customdata: dict
|
||||
"""A shared dictionary for objects to use as storage on this session.
|
||||
Ensure that keys here are unique to avoid collisions."""
|
||||
|
||||
sessionteams: list[ba.SessionTeam]
|
||||
"""All the ba.SessionTeams in the Session. Most things should use the
|
||||
list of ba.Team-s in ba.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,
|
||||
depsets: Sequence[ba.DependencySet],
|
||||
depsets: Sequence[bascenev1.DependencySet],
|
||||
team_names: Sequence[str] | None = None,
|
||||
team_colors: Sequence[Sequence[float]] | None = None,
|
||||
min_players: int = 1,
|
||||
|
|
@ -80,21 +82,25 @@ class Session:
|
|||
):
|
||||
"""Instantiate a session.
|
||||
|
||||
depsets should be a sequence of successfully resolved ba.DependencySet
|
||||
instances; one for each ba.Activity the session may potentially run.
|
||||
depsets should be a sequence of successfully resolved
|
||||
bascenev1.DependencySet instances; one for each bascenev1.Activity
|
||||
the session may potentially run.
|
||||
"""
|
||||
# pylint: disable=too-many-statements
|
||||
# pylint: disable=too-many-locals
|
||||
# pylint: disable=cyclic-import
|
||||
# pylint: disable=too-many-branches
|
||||
from ba._lobby import Lobby
|
||||
from ba._stats import Stats
|
||||
from ba._gameactivity import GameActivity
|
||||
from ba._activity import Activity
|
||||
from ba._team import SessionTeam
|
||||
from ba._error import DependencyError
|
||||
from ba._dependency import Dependency, AssetPackage
|
||||
from efro.util import empty_weakref
|
||||
from bascenev1._dependency import (
|
||||
Dependency,
|
||||
AssetPackage,
|
||||
DependencyError,
|
||||
)
|
||||
from bascenev1._lobby import Lobby
|
||||
from bascenev1._stats import Stats
|
||||
from bascenev1._gameactivity import GameActivity
|
||||
from bascenev1._activity import Activity
|
||||
from bascenev1._team import SessionTeam
|
||||
|
||||
# First off, resolve all dependency-sets we were passed.
|
||||
# If things are missing, we'll try to gather them into a single
|
||||
|
|
@ -135,7 +141,7 @@ class Session:
|
|||
# required_asset_packages)
|
||||
|
||||
# Init our C++ layer data.
|
||||
self._sessiondata = _ba.register_session(self)
|
||||
self._sessiondata = _bascenev1.register_session(self)
|
||||
|
||||
# Should remove this if possible.
|
||||
self.tournament_id: str | None = None
|
||||
|
|
@ -148,16 +154,16 @@ class Session:
|
|||
self.customdata = {}
|
||||
self._in_set_activity = False
|
||||
self._next_team_id = 0
|
||||
self._activity_retained: ba.Activity | None = None
|
||||
self._activity_retained: bascenev1.Activity | None = None
|
||||
self._launch_end_session_activity_time: float | None = None
|
||||
self._activity_end_timer: ba.Timer | None = None
|
||||
self._activity_end_timer: bascenev1.BaseTimer | None = None
|
||||
self._activity_weak = empty_weakref(Activity)
|
||||
self._next_activity: ba.Activity | None = None
|
||||
self._next_activity: bascenev1.Activity | None = None
|
||||
self._wants_to_end = False
|
||||
self._ending = False
|
||||
self._activity_should_end_immediately = False
|
||||
self._activity_should_end_immediately_results: (
|
||||
ba.GameResults | None
|
||||
bascenev1.GameResults | None
|
||||
) = None
|
||||
self._activity_should_end_immediately_delay = 0.0
|
||||
|
||||
|
|
@ -186,26 +192,33 @@ class Session:
|
|||
self.sessionteams.append(team)
|
||||
self._next_team_id += 1
|
||||
try:
|
||||
with _ba.Context(self):
|
||||
with self.context:
|
||||
self.on_team_join(team)
|
||||
except Exception:
|
||||
print_exception(f'Error in on_team_join for {self}.')
|
||||
logging.exception('Error in on_team_join for %s.', self)
|
||||
|
||||
self.lobby = Lobby()
|
||||
self.stats = Stats()
|
||||
|
||||
# Instantiate our session globals node which will apply its settings.
|
||||
self._sessionglobalsnode = _ba.newnode('sessionglobals')
|
||||
self._sessionglobalsnode = _bascenev1.newnode('sessionglobals')
|
||||
|
||||
@property
|
||||
def sessionglobalsnode(self) -> ba.Node:
|
||||
"""The sessionglobals ba.Node for the session."""
|
||||
def context(self) -> bascenev1.ContextRef:
|
||||
"""A context-ref pointing at this activity."""
|
||||
return self._sessiondata.context()
|
||||
|
||||
@property
|
||||
def sessionglobalsnode(self) -> bascenev1.Node:
|
||||
"""The sessionglobals bascenev1.Node for the session."""
|
||||
node = self._sessionglobalsnode
|
||||
if not node:
|
||||
raise NodeNotFoundError()
|
||||
raise babase.NodeNotFoundError()
|
||||
return node
|
||||
|
||||
def should_allow_mid_activity_joins(self, activity: ba.Activity) -> bool:
|
||||
def should_allow_mid_activity_joins(
|
||||
self, activity: bascenev1.Activity
|
||||
) -> bool:
|
||||
"""Ask ourself if we should allow joins during an Activity.
|
||||
|
||||
Note that for a join to be allowed, both the Session and Activity
|
||||
|
|
@ -215,21 +228,22 @@ class Session:
|
|||
del activity # Unused.
|
||||
return True
|
||||
|
||||
def on_player_request(self, player: ba.SessionPlayer) -> bool:
|
||||
"""Called when a new ba.Player wants to join the Session.
|
||||
def on_player_request(self, player: bascenev1.SessionPlayer) -> bool:
|
||||
"""Called when a new bascenev1.Player wants to join the Session.
|
||||
|
||||
This should return True or False to accept/reject.
|
||||
"""
|
||||
|
||||
# Limit player counts *unless* we're in a stress test.
|
||||
if _ba.app.stress_test_reset_timer is None:
|
||||
|
||||
if (
|
||||
babase.app.classic is not None
|
||||
and babase.app.classic.stress_test_reset_timer is None
|
||||
):
|
||||
if len(self.sessionplayers) >= self.max_players:
|
||||
# Print a rejection message *only* to the client trying to
|
||||
# join (prevents spamming everyone else in the game).
|
||||
_ba.playsound(_ba.getsound('error'))
|
||||
_ba.screenmessage(
|
||||
Lstr(
|
||||
_bascenev1.getsound('error').play()
|
||||
_bascenev1.broadcastmessage(
|
||||
babase.Lstr(
|
||||
resource='playerLimitReachedText',
|
||||
subs=[('${COUNT}', str(self.max_players))],
|
||||
),
|
||||
|
|
@ -239,11 +253,11 @@ class Session:
|
|||
)
|
||||
return False
|
||||
|
||||
_ba.playsound(_ba.getsound('dripity'))
|
||||
_bascenev1.getsound('dripity').play()
|
||||
return True
|
||||
|
||||
def on_player_leave(self, sessionplayer: ba.SessionPlayer) -> None:
|
||||
"""Called when a previously-accepted ba.SessionPlayer leaves."""
|
||||
def on_player_leave(self, sessionplayer: bascenev1.SessionPlayer) -> None:
|
||||
"""Called when a previously-accepted bascenev1.SessionPlayer leaves."""
|
||||
|
||||
if sessionplayer not in self.sessionplayers:
|
||||
print(
|
||||
|
|
@ -252,26 +266,25 @@ class Session:
|
|||
)
|
||||
return
|
||||
|
||||
_ba.playsound(_ba.getsound('playerLeft'))
|
||||
_bascenev1.getsound('playerLeft').play()
|
||||
|
||||
activity = self._activity_weak()
|
||||
|
||||
if not sessionplayer.in_game:
|
||||
|
||||
# Ok, the player is still in the lobby; simply remove them.
|
||||
with _ba.Context(self):
|
||||
with self.context:
|
||||
try:
|
||||
self.lobby.remove_chooser(sessionplayer)
|
||||
except Exception:
|
||||
print_exception('Error in Lobby.remove_chooser().')
|
||||
logging.exception('Error in Lobby.remove_chooser().')
|
||||
else:
|
||||
# Ok, they've already entered the game. Remove them from
|
||||
# teams/activities/etc.
|
||||
sessionteam = sessionplayer.sessionteam
|
||||
assert sessionteam is not None
|
||||
|
||||
_ba.screenmessage(
|
||||
Lstr(
|
||||
_bascenev1.broadcastmessage(
|
||||
babase.Lstr(
|
||||
resource='playerLeftText',
|
||||
subs=[('${PLAYER}', sessionplayer.getname(full=True))],
|
||||
)
|
||||
|
|
@ -305,7 +318,9 @@ class Session:
|
|||
self.sessionplayers.remove(sessionplayer)
|
||||
|
||||
def _remove_player_team(
|
||||
self, sessionteam: ba.SessionTeam, activity: ba.Activity | None
|
||||
self,
|
||||
sessionteam: bascenev1.SessionTeam,
|
||||
activity: bascenev1.Activity | None,
|
||||
) -> None:
|
||||
"""Remove the player-specific team in non-teams mode."""
|
||||
|
||||
|
|
@ -320,23 +335,24 @@ class Session:
|
|||
print('Team not found in Activity in on_player_leave.')
|
||||
|
||||
# And then from the Session.
|
||||
with _ba.Context(self):
|
||||
with self.context:
|
||||
if sessionteam in self.sessionteams:
|
||||
try:
|
||||
self.sessionteams.remove(sessionteam)
|
||||
self.on_team_leave(sessionteam)
|
||||
except Exception:
|
||||
print_exception(
|
||||
f'Error in on_team_leave for Session {self}.'
|
||||
logging.exception(
|
||||
'Error in on_team_leave for Session %s.', self
|
||||
)
|
||||
else:
|
||||
print('Team no in Session teams in on_player_leave.')
|
||||
try:
|
||||
sessionteam.leave()
|
||||
except Exception:
|
||||
print_exception(
|
||||
f'Error clearing sessiondata'
|
||||
f' for team {sessionteam} in session {self}.'
|
||||
logging.exception(
|
||||
'Error clearing sessiondata for team %s in session %s.',
|
||||
sessionteam,
|
||||
self,
|
||||
)
|
||||
|
||||
def end(self) -> None:
|
||||
|
|
@ -351,46 +367,45 @@ class Session:
|
|||
|
||||
def _launch_end_session_activity(self) -> None:
|
||||
"""(internal)"""
|
||||
from ba._activitytypes import EndSessionActivity
|
||||
from ba._generated.enums import TimeType
|
||||
from bascenev1._activitytypes import EndSessionActivity
|
||||
|
||||
with _ba.Context(self):
|
||||
curtime = _ba.time(TimeType.REAL)
|
||||
with self.context:
|
||||
curtime = babase.apptime()
|
||||
if self._ending:
|
||||
# Ignore repeats unless its been a while.
|
||||
assert self._launch_end_session_activity_time is not None
|
||||
since_last = curtime - self._launch_end_session_activity_time
|
||||
if since_last < 30.0:
|
||||
return
|
||||
print_error(
|
||||
'_launch_end_session_activity called twice (since_last='
|
||||
+ str(since_last)
|
||||
+ ')'
|
||||
logging.error(
|
||||
'_launch_end_session_activity called twice (since_last=%s)',
|
||||
since_last,
|
||||
)
|
||||
self._launch_end_session_activity_time = curtime
|
||||
self.setactivity(_ba.newactivity(EndSessionActivity))
|
||||
self.setactivity(_bascenev1.newactivity(EndSessionActivity))
|
||||
self._wants_to_end = False
|
||||
self._ending = True # Prevent further actions.
|
||||
|
||||
def on_team_join(self, team: ba.SessionTeam) -> None:
|
||||
"""Called when a new ba.Team joins the session."""
|
||||
def on_team_join(self, team: bascenev1.SessionTeam) -> None:
|
||||
"""Called when a new bascenev1.Team joins the session."""
|
||||
|
||||
def on_team_leave(self, team: ba.SessionTeam) -> None:
|
||||
"""Called when a ba.Team is leaving the session."""
|
||||
def on_team_leave(self, team: bascenev1.SessionTeam) -> None:
|
||||
"""Called when a bascenev1.Team is leaving the session."""
|
||||
|
||||
def end_activity(
|
||||
self, activity: ba.Activity, results: Any, delay: float, force: bool
|
||||
self,
|
||||
activity: bascenev1.Activity,
|
||||
results: Any,
|
||||
delay: float,
|
||||
force: bool,
|
||||
) -> None:
|
||||
"""Commence shutdown of a ba.Activity (if not already occurring).
|
||||
"""Commence shutdown of a bascenev1.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.
|
||||
"""
|
||||
from ba._general import Call
|
||||
from ba._generated.enums import TimeType
|
||||
|
||||
# Only pay attention if this is coming from our current activity.
|
||||
if activity is not self._activity_retained:
|
||||
return
|
||||
|
|
@ -410,16 +425,15 @@ class Session:
|
|||
activity.set_has_ended(True)
|
||||
|
||||
# Set a timer to set in motion this activity's demise.
|
||||
self._activity_end_timer = _ba.Timer(
|
||||
self._activity_end_timer = _bascenev1.BaseTimer(
|
||||
delay,
|
||||
Call(self._complete_end_activity, activity, results),
|
||||
timetype=TimeType.BASE,
|
||||
babase.Call(self._complete_end_activity, activity, results),
|
||||
)
|
||||
|
||||
def handlemessage(self, msg: Any) -> Any:
|
||||
"""General message handling; can be passed any message object."""
|
||||
from ba._lobby import PlayerReadyMessage
|
||||
from ba._messages import PlayerProfilesChangedMessage, UNHANDLED
|
||||
from bascenev1._lobby import PlayerReadyMessage
|
||||
from bascenev1._messages import PlayerProfilesChangedMessage, UNHANDLED
|
||||
|
||||
if isinstance(msg, PlayerReadyMessage):
|
||||
self._on_player_ready(msg.chooser)
|
||||
|
|
@ -427,7 +441,7 @@ class Session:
|
|||
elif isinstance(msg, PlayerProfilesChangedMessage):
|
||||
# If we have a current activity with a lobby, ask it to reload
|
||||
# profiles.
|
||||
with _ba.Context(self):
|
||||
with self.context:
|
||||
self.lobby.reload_profiles()
|
||||
return None
|
||||
|
||||
|
|
@ -436,7 +450,7 @@ class Session:
|
|||
return None
|
||||
|
||||
class _SetActivityScopedLock:
|
||||
def __init__(self, session: ba.Session) -> None:
|
||||
def __init__(self, session: Session) -> None:
|
||||
self._session = session
|
||||
if session._in_set_activity:
|
||||
raise RuntimeError('Session.setactivity() called recursively.')
|
||||
|
|
@ -445,20 +459,20 @@ class Session:
|
|||
def __del__(self) -> None:
|
||||
self._session._in_set_activity = False
|
||||
|
||||
def setactivity(self, activity: ba.Activity) -> None:
|
||||
"""Assign a new current ba.Activity for the session.
|
||||
def setactivity(self, activity: bascenev1.Activity) -> None:
|
||||
"""Assign a new current bascenev1.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 ba.newnode() to add a node to foo)
|
||||
session.setactivity(foo) and then bascenev1.newnode() to add a node
|
||||
to foo)
|
||||
"""
|
||||
from ba._generated.enums import TimeType
|
||||
|
||||
# Make sure we don't get called recursively.
|
||||
_rlock = self._SetActivityScopedLock(self)
|
||||
|
||||
if activity.session is not _ba.getsession():
|
||||
if activity.session is not _bascenev1.getsession():
|
||||
raise RuntimeError("Provided Activity's Session is not current.")
|
||||
|
||||
# Quietly ignore this if the whole session is going down.
|
||||
|
|
@ -466,7 +480,7 @@ class Session:
|
|||
return
|
||||
|
||||
if activity is self._activity_retained:
|
||||
print_error('Activity set to already-current activity.')
|
||||
logging.error('Activity set to already-current activity.')
|
||||
return
|
||||
|
||||
if self._next_activity is not None:
|
||||
|
|
@ -507,15 +521,13 @@ class Session:
|
|||
# the activity should have no refs left to it and should die (which
|
||||
# will trigger the next activity to run).
|
||||
if prev_activity is not None:
|
||||
with _ba.Context('ui'):
|
||||
_ba.timer(
|
||||
max(0.0, activity.transition_time),
|
||||
prev_activity.expire,
|
||||
timetype=TimeType.REAL,
|
||||
with babase.ContextRef.empty():
|
||||
babase.apptimer(
|
||||
max(0.0, activity.transition_time), prev_activity.expire
|
||||
)
|
||||
self._in_set_activity = False
|
||||
|
||||
def getactivity(self) -> ba.Activity | None:
|
||||
def getactivity(self) -> bascenev1.Activity | None:
|
||||
"""Return the current foreground activity for this session."""
|
||||
return self._activity_weak()
|
||||
|
||||
|
|
@ -530,49 +542,54 @@ class Session:
|
|||
return []
|
||||
|
||||
def _complete_end_activity(
|
||||
self, activity: ba.Activity, results: Any
|
||||
self, activity: bascenev1.Activity, results: Any
|
||||
) -> None:
|
||||
# Run the subclass callback in the session context.
|
||||
try:
|
||||
with _ba.Context(self):
|
||||
with self.context:
|
||||
self.on_activity_end(activity, results)
|
||||
except Exception:
|
||||
print_exception(
|
||||
f'Error in on_activity_end() for session {self}'
|
||||
f' activity {activity} with results {results}'
|
||||
logging.error(
|
||||
'Error in on_activity_end() for session %s'
|
||||
' activity %s with results %s',
|
||||
self,
|
||||
activity,
|
||||
results,
|
||||
)
|
||||
|
||||
def _request_player(self, sessionplayer: ba.SessionPlayer) -> bool:
|
||||
def _request_player(self, sessionplayer: bascenev1.SessionPlayer) -> bool:
|
||||
"""Called by the native layer when a player wants to join."""
|
||||
|
||||
# If we're ending, allow no new players.
|
||||
if self._ending:
|
||||
return False
|
||||
|
||||
# Ask the ba.Session subclass to approve/deny this request.
|
||||
# Ask the bascenev1.Session subclass to approve/deny this request.
|
||||
try:
|
||||
with _ba.Context(self):
|
||||
with self.context:
|
||||
result = self.on_player_request(sessionplayer)
|
||||
except Exception:
|
||||
print_exception(f'Error in on_player_request for {self}')
|
||||
logging.exception('Error in on_player_request for %s.', self)
|
||||
result = False
|
||||
|
||||
# If they said yes, add the player to the lobby.
|
||||
if result:
|
||||
self.sessionplayers.append(sessionplayer)
|
||||
with _ba.Context(self):
|
||||
with self.context:
|
||||
try:
|
||||
self.lobby.add_chooser(sessionplayer)
|
||||
except Exception:
|
||||
print_exception('Error in lobby.add_chooser().')
|
||||
logging.exception('Error in lobby.add_chooser().')
|
||||
|
||||
return result
|
||||
|
||||
def on_activity_end(self, activity: ba.Activity, results: Any) -> None:
|
||||
"""Called when the current ba.Activity has ended.
|
||||
def on_activity_end(
|
||||
self, activity: bascenev1.Activity, results: Any
|
||||
) -> None:
|
||||
"""Called when the current bascenev1.Activity has ended.
|
||||
|
||||
The ba.Session should look at the results and start
|
||||
another ba.Activity.
|
||||
The bascenev1.Session should look at the results and start
|
||||
another bascenev1.Activity.
|
||||
"""
|
||||
|
||||
def begin_next_activity(self) -> None:
|
||||
|
|
@ -582,7 +599,7 @@ class Session:
|
|||
"""
|
||||
if self._next_activity is None:
|
||||
# Should this ever happen?
|
||||
print_error('begin_next_activity() called with no _next_activity')
|
||||
logging.error('begin_next_activity() called with no _next_activity')
|
||||
return
|
||||
|
||||
# We store both a weak and a strong ref to the new activity;
|
||||
|
|
@ -611,8 +628,8 @@ class Session:
|
|||
self._activity_should_end_immediately_delay,
|
||||
)
|
||||
|
||||
def _on_player_ready(self, chooser: ba.Chooser) -> None:
|
||||
"""Called when a ba.Player has checked themself ready."""
|
||||
def _on_player_ready(self, chooser: bascenev1.Chooser) -> None:
|
||||
"""Called when a bascenev1.Player has checked themself ready."""
|
||||
lobby = chooser.lobby
|
||||
activity = self._activity_weak()
|
||||
|
||||
|
|
@ -638,14 +655,14 @@ class Session:
|
|||
# Get our next activity going.
|
||||
self._complete_end_activity(activity, {})
|
||||
else:
|
||||
_ba.screenmessage(
|
||||
Lstr(
|
||||
_bascenev1.broadcastmessage(
|
||||
babase.Lstr(
|
||||
resource='notEnoughPlayersText',
|
||||
subs=[('${COUNT}', str(min_players))],
|
||||
),
|
||||
color=(1, 1, 0),
|
||||
)
|
||||
_ba.playsound(_ba.getsound('error'))
|
||||
_bascenev1.getsound('error').play()
|
||||
|
||||
# Otherwise just add players on the fly.
|
||||
else:
|
||||
|
|
@ -657,22 +674,24 @@ class Session:
|
|||
) -> None:
|
||||
"""(internal)"""
|
||||
# pylint: disable=cyclic-import
|
||||
from ba._apputils import garbage_collect
|
||||
|
||||
# Since things should be generally still right now, it's a good time
|
||||
# to run garbage collection to clear out any circular dependency
|
||||
# loops. We keep this disabled normally to avoid non-deterministic
|
||||
# hitches.
|
||||
garbage_collect()
|
||||
babase.garbage_collect()
|
||||
|
||||
with _ba.Context(self):
|
||||
assert babase.app.classic is not None
|
||||
with self.context:
|
||||
if can_show_ad_on_death:
|
||||
_ba.app.ads.call_after_ad(self.begin_next_activity)
|
||||
babase.app.classic.ads.call_after_ad(self.begin_next_activity)
|
||||
else:
|
||||
_ba.pushcall(self.begin_next_activity)
|
||||
babase.pushcall(self.begin_next_activity)
|
||||
|
||||
def _add_chosen_player(self, chooser: ba.Chooser) -> ba.SessionPlayer:
|
||||
from ba._team import SessionTeam
|
||||
def _add_chosen_player(
|
||||
self, chooser: bascenev1.Chooser
|
||||
) -> bascenev1.SessionPlayer:
|
||||
from bascenev1._team import SessionTeam
|
||||
|
||||
sessionplayer = chooser.getplayer()
|
||||
assert sessionplayer in self.sessionplayers, (
|
||||
|
|
@ -701,9 +720,9 @@ class Session:
|
|||
and self.should_allow_mid_activity_joins(activity)
|
||||
):
|
||||
pass_to_activity = False
|
||||
with _ba.Context(self):
|
||||
_ba.screenmessage(
|
||||
Lstr(
|
||||
with self.context:
|
||||
_bascenev1.broadcastmessage(
|
||||
babase.Lstr(
|
||||
resource='playerDelayedJoinText',
|
||||
subs=[
|
||||
('${PLAYER}', sessionplayer.getname(full=True))
|
||||
|
|
@ -728,11 +747,11 @@ class Session:
|
|||
# Add player's team to the Session.
|
||||
self.sessionteams.append(sessionteam)
|
||||
|
||||
with _ba.Context(self):
|
||||
with self.context:
|
||||
try:
|
||||
self.on_team_join(sessionteam)
|
||||
except Exception:
|
||||
print_exception(f'Error in on_team_join for {self}.')
|
||||
logging.exception('Error in on_team_join for %s.', self)
|
||||
|
||||
# Add player's team to the Activity.
|
||||
if pass_to_activity:
|
||||
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