meta improvements

This commit is contained in:
Eric Froemling 2022-07-26 16:50:13 -07:00
parent ee5f5eef1d
commit 92ab8db91f
No known key found for this signature in database
GPG Key ID: 89C93F0F8D6D5A98
16 changed files with 355 additions and 284 deletions

View File

@ -3995,26 +3995,26 @@
"assets/src/ba_data/python/ba/_generated/__init__.py": "https://files.ballistica.net/cache/ba1/ee/e8/cad05aa531c7faf7ff7b96db7f6e", "assets/src/ba_data/python/ba/_generated/__init__.py": "https://files.ballistica.net/cache/ba1/ee/e8/cad05aa531c7faf7ff7b96db7f6e",
"assets/src/ba_data/python/ba/_generated/enums.py": "https://files.ballistica.net/cache/ba1/b2/e5/0ee0561e16257a32830645239f34", "assets/src/ba_data/python/ba/_generated/enums.py": "https://files.ballistica.net/cache/ba1/b2/e5/0ee0561e16257a32830645239f34",
"ballisticacore-windows/Generic/BallisticaCore.ico": "https://files.ballistica.net/cache/ba1/89/c0/e32c7d2a35dc9aef57cc73b0911a", "ballisticacore-windows/Generic/BallisticaCore.ico": "https://files.ballistica.net/cache/ba1/89/c0/e32c7d2a35dc9aef57cc73b0911a",
"build/prefab/full/linux_arm64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/36/14/efd9145a5e1562b04f6ff560e00d", "build/prefab/full/linux_arm64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/79/7a/c383c0a5aa5cd3a4e998ae2d607f",
"build/prefab/full/linux_arm64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/68/f3/376aecc59b69d288c276327f0a99", "build/prefab/full/linux_arm64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/52/36/3db8e70d87bc3399bdc39a26dea4",
"build/prefab/full/linux_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/a7/c1/24da478bac1eb0d9e5090a37e2af", "build/prefab/full/linux_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/26/d4/3209d789bbf8a73f4914aa7ce4d3",
"build/prefab/full/linux_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/a6/e3/a453b14a443de5dc69e32d62b425", "build/prefab/full/linux_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/59/e3/c2bc89f60961f4ed0708a5340ec8",
"build/prefab/full/linux_x86_64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/d2/1d/b1a59e26f6899e7779abb70e7c69", "build/prefab/full/linux_x86_64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/d4/8e/f7cb156fb91af56adeb9ed08a76d",
"build/prefab/full/linux_x86_64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/62/f8/3798837a711a1e488f97e9d30fea", "build/prefab/full/linux_x86_64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/44/88/634e154803570f4b977a51114706",
"build/prefab/full/linux_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/ee/11/e183b40452ced610c0ca90f211b2", "build/prefab/full/linux_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/85/fd/5afc22c9dfe62bdbec27694b0076",
"build/prefab/full/linux_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/2c/d1/2732b76fd513d1aed1ca0a9b9597", "build/prefab/full/linux_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/a1/d0/0c906dd30d70314b13d4bbc6b5bf",
"build/prefab/full/mac_arm64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/92/26/bb135305ca78c79f03cc54aa8ed6", "build/prefab/full/mac_arm64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/32/2d/80933cdd46b3f8f5a41e431a8ac1",
"build/prefab/full/mac_arm64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/03/8a/6fb8aa21f23580e331079c285bbf", "build/prefab/full/mac_arm64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/e3/1c/2cc790a44ed8abe0d7cab40a4fba",
"build/prefab/full/mac_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/67/66/c70b8602a431689381516385ef4c", "build/prefab/full/mac_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/df/ab/9222758826b0a48a87abcaa1d4a6",
"build/prefab/full/mac_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/e6/e4/cad1d7bf7347a8f505fc9ed3d483", "build/prefab/full/mac_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/77/f4/52b8e2abd145a276e614a9286dbe",
"build/prefab/full/mac_x86_64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/75/a6/f2926b1c3083cde654e744fa7081", "build/prefab/full/mac_x86_64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/95/9b/58a4d68b9eaafefa0a108a5cf293",
"build/prefab/full/mac_x86_64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/2c/71/b8587bf758d8774dc5363e99c25b", "build/prefab/full/mac_x86_64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/cf/ab/572963d01007626cfa067ad750b4",
"build/prefab/full/mac_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/78/a8/eea0ff02bd7dfdac45f5e8bb1b91", "build/prefab/full/mac_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/c5/6b/3a003ef79f24b9d4d7b2c2b71be2",
"build/prefab/full/mac_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/2e/cb/c100875d6d87b162c5ee5d8e7623", "build/prefab/full/mac_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/23/1c/4027cf14f282242f2ea8a11c684d",
"build/prefab/full/windows_x86_gui/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/51/06/f5d5e4167caf3c1920e041ce79ae", "build/prefab/full/windows_x86_gui/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/3b/b9/d897246697814e97bd241909d78c",
"build/prefab/full/windows_x86_gui/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/89/fb/53cbead744939cbef30386a74b0b", "build/prefab/full/windows_x86_gui/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/11/2e/5019b997e6703cc65033a5ad9aaa",
"build/prefab/full/windows_x86_server/debug/dist/BallisticaCoreHeadless.exe": "https://files.ballistica.net/cache/ba1/ec/f4/45308c7323f909964fb81ce0b2cc", "build/prefab/full/windows_x86_server/debug/dist/BallisticaCoreHeadless.exe": "https://files.ballistica.net/cache/ba1/9c/1f/c045bc85e0d062fbe496279ecea8",
"build/prefab/full/windows_x86_server/release/dist/BallisticaCoreHeadless.exe": "https://files.ballistica.net/cache/ba1/b0/f1/0e699be629f7936a67e7b336a383", "build/prefab/full/windows_x86_server/release/dist/BallisticaCoreHeadless.exe": "https://files.ballistica.net/cache/ba1/e2/c2/2bcbf1dcf557a21b82a8b91dc0a1",
"build/prefab/lib/linux_arm64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/1a/6c/0c89987987ba2394da51b13d48cb", "build/prefab/lib/linux_arm64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/1a/6c/0c89987987ba2394da51b13d48cb",
"build/prefab/lib/linux_arm64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/b8/b1/7ce57441f44a48185d1842cc954e", "build/prefab/lib/linux_arm64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/b8/b1/7ce57441f44a48185d1842cc954e",
"build/prefab/lib/linux_arm64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/0d/3e/77ed4e2948754ceb60353ab90091", "build/prefab/lib/linux_arm64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/0d/3e/77ed4e2948754ceb60353ab90091",
@ -4031,14 +4031,14 @@
"build/prefab/lib/mac_x86_64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/82/b5/e8a3fa193cff285aa20afeeae35f", "build/prefab/lib/mac_x86_64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/82/b5/e8a3fa193cff285aa20afeeae35f",
"build/prefab/lib/mac_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/b5/a5/aba084a4026ebb1cd8ef8c141bc4", "build/prefab/lib/mac_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/b5/a5/aba084a4026ebb1cd8ef8c141bc4",
"build/prefab/lib/mac_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/1c/17/d35f66f340e9cddd948dcec5453d", "build/prefab/lib/mac_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/1c/17/d35f66f340e9cddd948dcec5453d",
"build/prefab/lib/windows/Debug_Win32/BallisticaCoreGenericInternal.lib": "https://files.ballistica.net/cache/ba1/b5/cd/789085462ab13214d6addbe06921", "build/prefab/lib/windows/Debug_Win32/BallisticaCoreGenericInternal.lib": "https://files.ballistica.net/cache/ba1/f1/c6/8faa5f4940ea61876f3e8e8dfb06",
"build/prefab/lib/windows/Debug_Win32/BallisticaCoreGenericInternal.pdb": "https://files.ballistica.net/cache/ba1/5a/98/cbca8986e08d5fcfdc7aeaef25e2", "build/prefab/lib/windows/Debug_Win32/BallisticaCoreGenericInternal.pdb": "https://files.ballistica.net/cache/ba1/ed/c8/c72049370c7fa1f8ef703526b818",
"build/prefab/lib/windows/Debug_Win32/BallisticaCoreHeadlessInternal.lib": "https://files.ballistica.net/cache/ba1/7a/2d/cef593252fbe3fddc22f6e788130", "build/prefab/lib/windows/Debug_Win32/BallisticaCoreHeadlessInternal.lib": "https://files.ballistica.net/cache/ba1/88/74/1404c5895de5eb6da05c32be5cd5",
"build/prefab/lib/windows/Debug_Win32/BallisticaCoreHeadlessInternal.pdb": "https://files.ballistica.net/cache/ba1/0a/30/b9b0d8629eb372e48cb0bc2d0241", "build/prefab/lib/windows/Debug_Win32/BallisticaCoreHeadlessInternal.pdb": "https://files.ballistica.net/cache/ba1/36/1b/2710d1d788bf5a6518f6092a9829",
"build/prefab/lib/windows/Release_Win32/BallisticaCoreGenericInternal.lib": "https://files.ballistica.net/cache/ba1/5e/1f/86c5af445fe4c21e58ebcf5c7f39", "build/prefab/lib/windows/Release_Win32/BallisticaCoreGenericInternal.lib": "https://files.ballistica.net/cache/ba1/5e/8f/53b5139a47bd6d8b637b3e8c97c2",
"build/prefab/lib/windows/Release_Win32/BallisticaCoreGenericInternal.pdb": "https://files.ballistica.net/cache/ba1/b5/38/ac11b6c33d404a421e8adf354202", "build/prefab/lib/windows/Release_Win32/BallisticaCoreGenericInternal.pdb": "https://files.ballistica.net/cache/ba1/84/e3/f5b80f011508edfabc2e042ea356",
"build/prefab/lib/windows/Release_Win32/BallisticaCoreHeadlessInternal.lib": "https://files.ballistica.net/cache/ba1/64/2d/2833633e1277af02c072b1cbc03c", "build/prefab/lib/windows/Release_Win32/BallisticaCoreHeadlessInternal.lib": "https://files.ballistica.net/cache/ba1/a0/4b/f177f48bb3a0125c072079c561d6",
"build/prefab/lib/windows/Release_Win32/BallisticaCoreHeadlessInternal.pdb": "https://files.ballistica.net/cache/ba1/bd/1c/36e1ee0184d510d548f002ddacfe", "build/prefab/lib/windows/Release_Win32/BallisticaCoreHeadlessInternal.pdb": "https://files.ballistica.net/cache/ba1/3c/57/39092393925cb999582a15023275",
"src/ballistica/generated/python_embedded/binding.inc": "https://files.ballistica.net/cache/ba1/7d/3e/229a581cb2454ed856f1d8b564a7", "src/ballistica/generated/python_embedded/binding.inc": "https://files.ballistica.net/cache/ba1/7d/3e/229a581cb2454ed856f1d8b564a7",
"src/ballistica/generated/python_embedded/bootstrap.inc": "https://files.ballistica.net/cache/ba1/d3/db/e73d4dcf1280d5f677c3cf8b47c3" "src/ballistica/generated/python_embedded/bootstrap.inc": "https://files.ballistica.net/cache/ba1/d3/db/e73d4dcf1280d5f677c3cf8b47c3"
} }

View File

@ -799,9 +799,11 @@
<w>expectedsig</w> <w>expectedsig</w>
<w>explodable</w> <w>explodable</w>
<w>explodey</w> <w>explodey</w>
<w>exportlist</w>
<w>exportoptions</w> <w>exportoptions</w>
<w>exportoptionspath</w> <w>exportoptionspath</w>
<w>exporttype</w> <w>exporttype</w>
<w>exporttypestr</w>
<w>extradata</w> <w>extradata</w>
<w>extraflagmat</w> <w>extraflagmat</w>
<w>extrahash</w> <w>extrahash</w>
@ -1266,6 +1268,7 @@
<w>jsonutils</w> <w>jsonutils</w>
<w>juleskie</w> <w>juleskie</w>
<w>kbclass</w> <w>kbclass</w>
<w>kbexports</w>
<w>kbytecount</w> <w>kbytecount</w>
<w>keepalive</w> <w>keepalive</w>
<w>keepalives</w> <w>keepalives</w>
@ -1782,6 +1785,7 @@
<w>pathcapture</w> <w>pathcapture</w>
<w>pathdst</w> <w>pathdst</w>
<w>pathlib</w> <w>pathlib</w>
<w>pathlist</w>
<w>pathnames</w> <w>pathnames</w>
<w>pathparts</w> <w>pathparts</w>
<w>pathsrc</w> <w>pathsrc</w>

View File

@ -1,4 +1,7 @@
### 1.7.6 (build 20674, api 7, 2022-07-25) ### 1.7.6 (build 20676, api 7, 2022-07-26)
- Cleaned up MetaSubsystem code
- It is now possible to tell the meta system about arbitrary classes (ba_meta export foo.bar.Class) instead of just the preset types 'plugin', 'game', etc.
- Newly discovered plugins are now activated immediately instead of requiring a restart.
### 1.7.5 (build 20672, api 7, 2022-07-25) ### 1.7.5 (build 20672, api 7, 2022-07-25)
- Android build now uses the ReLinker library to load the native main.so, which will (hopefully) avoid some random load failures on older Android versions. - Android build now uses the ReLinker library to load the native main.so, which will (hopefully) avoid some random load failures on older Android versions.

View File

@ -223,6 +223,7 @@ class App:
self._launch_completed = False self._launch_completed = False
self._initial_login_completed = False self._initial_login_completed = False
self._meta_scan_completed = False
self._called_on_app_running = False self._called_on_app_running = False
self._app_paused = False self._app_paused = False
@ -344,6 +345,8 @@ class App:
from bastd.actor import spazappearance from bastd.actor import spazappearance
from ba._generated.enums import TimeType from ba._generated.enums import TimeType
assert _ba.in_game_thread()
self._aioloop = _asyncio.setup_asyncio() self._aioloop = _asyncio.setup_asyncio()
cfg = self.config cfg = self.config
@ -415,6 +418,9 @@ class App:
if not self.headless_mode: if not self.headless_mode:
_ba.timer(3.0, check_special_offer, timetype=TimeType.REAL) _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_v2.on_app_launch()
self.accounts_v1.on_app_launch() self.accounts_v1.on_app_launch()
@ -429,17 +435,27 @@ class App:
def on_app_running(self) -> None: def on_app_running(self) -> None:
"""Called when initially entering the running state.""" """Called when initially entering the running state."""
self.meta.on_app_running()
self.plugins.on_app_running() self.plugins.on_app_running()
# from ba._dependency import test_depset # from ba._dependency import test_depset
# test_depset() # test_depset()
def on_meta_scan_complete(self) -> None:
"""Called by meta-scan when it is done doing its thing."""
assert _ba.in_game_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: def _update_state(self) -> None:
assert _ba.in_game_thread()
if self._app_paused: if self._app_paused:
self.state = self.State.PAUSED self.state = self.State.PAUSED
else: else:
if self._initial_login_completed: if self._initial_login_completed and self._meta_scan_completed:
self.state = self.State.RUNNING self.state = self.State.RUNNING
if not self._called_on_app_running: if not self._called_on_app_running:
self._called_on_app_running = True self._called_on_app_running = True
@ -650,5 +666,9 @@ class App:
This should also run after a short amount of time if no login This should also run after a short amount of time if no login
has occurred. has occurred.
""" """
# Tell meta it can start scanning extra stuff that just showed up
# (account workspaces).
self.meta.start_extra_scan()
self._initial_login_completed = True self._initial_login_completed = True
self._update_state() self._update_state()

View File

@ -17,6 +17,7 @@ from ba._error import NotFoundError, print_error, print_exception
from ba._general import Call, WeakCall from ba._general import Call, WeakCall
from ba._player import PlayerInfo from ba._player import PlayerInfo
from ba import _map from ba import _map
from ba import _store
if TYPE_CHECKING: if TYPE_CHECKING:
from typing import Any, Callable, Sequence from typing import Any, Callable, Sequence
@ -1159,7 +1160,7 @@ class GameActivity(Activity[PlayerType, TeamType]):
else: else:
# If settings doesn't specify a map, pick a random one from the # If settings doesn't specify a map, pick a random one from the
# list of supported ones. # list of supported ones.
unowned_maps = _map.get_unowned_maps() unowned_maps = _store.get_unowned_maps()
valid_maps: list[str] = [ valid_maps: list[str] = [
m for m in self.get_supported_maps(type(self.session)) m for m in self.get_supported_maps(type(self.session))
if m not in unowned_maps if m not in unowned_maps

View File

@ -399,3 +399,24 @@ def cameraflash(duration: float = 999.0) -> None:
light.node.delete, light.node.delete,
timeformat=TimeFormat.MILLISECONDS) timeformat=TimeFormat.MILLISECONDS)
activity.camera_flash_data.append(light) # type: ignore activity.camera_flash_data.append(light) # type: ignore
def get_game_types() -> list[type[ba.GameActivity]]:
"""Return all available game types."""
# pylint: disable=cyclic-import
from ba._general import getclass
from ba._gameactivity import GameActivity
from ba._store import get_unowned_game_types
gameclassnames = _ba.app.meta.wait_for_scan_results().exports_of_class(
GameActivity)
gameclasses = []
for gameclassname in gameclassnames:
try:
cls = getclass(gameclassname, GameActivity)
gameclasses.append(cls)
except Exception:
from ba import _error
_error.print_exception('error importing ' + str(gameclassname))
unowned = get_unowned_game_types()
return [cls for cls in gameclasses if cls not in unowned]

View File

@ -101,22 +101,6 @@ def getmaps(playtype: str) -> list[str]:
if playtype in val.get_play_types()) if playtype in val.get_play_types())
def get_unowned_maps() -> list[str]:
"""Return the list of local maps not owned by the current account.
Category: **Asset Functions**
"""
from ba import _store
unowned_maps: set[str] = set()
if not _ba.app.headless_mode:
for map_section in _store.get_store_layout()['maps']:
for mapitem in map_section['items']:
if not _ba.get_purchased(mapitem):
m_info = _store.get_store_item(mapitem)
unowned_maps.add(m_info['map_type'].name)
return sorted(unowned_maps)
def get_map_class(name: str) -> type[ba.Map]: def get_map_class(name: str) -> type[ba.Map]:
"""Return a map type given a name. """Return a map type given a name.

View File

@ -6,15 +6,16 @@ from __future__ import annotations
import os import os
import time import time
import threading import logging
from threading import Thread
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING from typing import TYPE_CHECKING, TypeVar
from dataclasses import dataclass, field from dataclasses import dataclass, field
import _ba import _ba
if TYPE_CHECKING: if TYPE_CHECKING:
import ba from typing import Callable
# The meta api version of this build of the game. # The meta api version of this build of the game.
# Only packages and modules requiring this exact api version # Only packages and modules requiring this exact api version
@ -22,15 +23,28 @@ if TYPE_CHECKING:
# See: https://ballistica.net/wiki/Meta-Tags # See: https://ballistica.net/wiki/Meta-Tags
CURRENT_API_VERSION = 7 CURRENT_API_VERSION = 7
# 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',
}
T = TypeVar('T')
@dataclass @dataclass
class ScanResults: class ScanResults:
"""Final results from a metadata scan.""" """Final results from a meta-scan."""
games: list[str] = field(default_factory=list) exports: dict[str, list[str]] = field(default_factory=dict)
plugins: list[str] = field(default_factory=list) errors: list[str] = field(default_factory=list)
keyboards: list[str] = field(default_factory=list) warnings: list[str] = field(default_factory=list)
errors: str = ''
warnings: str = '' def exports_of_class(self, cls: type) -> list[str]:
"""Return exports of a given class."""
return self.exports.get(f'{cls.__module__}.{cls.__qualname__}', [])
class MetadataSubsystem: class MetadataSubsystem:
@ -42,99 +56,52 @@ class MetadataSubsystem:
""" """
def __init__(self) -> None: def __init__(self) -> None:
self.scanresults: ScanResults | None = None
self._scan: DirectoryScan | None = None
# Can be populated before starting the scan.
self.extra_scan_dirs: list[str] = [] self.extra_scan_dirs: list[str] = []
def on_app_running(self) -> None: # Results populated once scan is complete.
"""Should be called when the app enters the running state.""" self.scanresults: ScanResults | None = None
# Start scanning for things exposed via ba_meta. self._scan_complete_cb: Callable[[], None] | None = None
self.start_scan()
def start_scan(self) -> None: def start_scan(self, scan_complete_cb: Callable[[], None]) -> None:
"""Begin scanning script directories for scripts containing metadata. """Begin the overall scan.
Should be called only once at launch.""" This will start scanning built in directories (which for vanilla
app = _ba.app installs should be the vast majority of the work). This should only
if self.scanresults is not None: be called once.
print('WARNING: meta scan run more than once.') """
pythondirs = ([app.python_directory_app, app.python_directory_user] + assert self._scan_complete_cb is None
self.extra_scan_dirs) assert self._scan is None
thread = ScanThread(pythondirs)
thread.start()
def handle_scan_results(self, results: ScanResults) -> None: self._scan_complete_cb = scan_complete_cb
"""Called in the game thread with results of a completed scan.""" self._scan = DirectoryScan(
[_ba.app.python_directory_app, _ba.app.python_directory_user])
from ba._language import Lstr Thread(target=self._do_scan_dirs, daemon=True).start()
from ba._plugin import PotentialPlugin
# Warnings generally only get printed locally for users' benefit def start_extra_scan(self) -> None:
# (things like out-of-date scripts being ignored, etc.) """Provide extra dirs to be scanned (namely Workspace dirs).
# Errors are more serious and will get included in the regular log
# warnings = results.get('warnings', '')
# errors = results.get('errors', '')
if results.warnings != '' or results.errors != '':
import textwrap
_ba.screenmessage(Lstr(resource='scanScriptsErrorText'),
color=(1, 0, 0))
_ba.playsound(_ba.getsound('error'))
if results.warnings != '':
_ba.log(textwrap.indent(results.warnings,
'Warning (meta-scan): '),
to_server=False)
if results.errors != '':
_ba.log(textwrap.indent(results.errors, 'Error (meta-scan): '))
# Handle plugins. This is the bare minimum part of the scan that must be delayed until
plugs = _ba.app.plugins workspaces have been synced/etc. This must be called exactly once.
config_changed = False """
found_new = False assert self._scan is not None
plugstates: dict[str, dict] = _ba.app.config.setdefault('Plugins', {}) self._scan.set_extras(self.extra_scan_dirs)
assert isinstance(plugstates, dict)
# Create a potential-plugin for each class we found in the scan. # Let the game know we're done.
for class_path in results.plugins: assert self._scan_complete_cb is not None
plugs.potential_plugins.append( self._scan_complete_cb()
PotentialPlugin(display_name=Lstr(value=class_path),
class_path=class_path,
available=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
# Also add a special one for any plugins set to load but *not* found def wait_for_scan_results(self) -> ScanResults:
# in the scan (this way they will show up in the UI so we can disable """Return scan results, blocking if the scan is not yet complete."""
# them)
for class_path, plugstate in plugstates.items():
enabled = plugstate.get('enabled', False)
assert isinstance(enabled, bool)
if enabled and class_path not in results.plugins:
plugs.potential_plugins.append(
PotentialPlugin(display_name=Lstr(value=class_path),
class_path=class_path,
available=False))
plugs.potential_plugins.sort(key=lambda p: p.class_path)
if found_new:
_ba.screenmessage(Lstr(resource='pluginsDetectedText'),
color=(0, 1, 0))
_ba.playsound(_ba.getsound('ding'))
if config_changed:
_ba.app.config.commit()
def get_scan_results(self) -> ScanResults:
"""Return meta scan results; block if the scan is not yet complete."""
if self.scanresults is None: if self.scanresults is None:
print('WARNING: ba.meta.get_scan_results()' logging.warning('ba.meta.wait_for_scan_results()'
' called before scan completed.' ' called before scan completed;'
' This can cause hitches.') ' this can cause hitches.')
# Now wait a bit for the scan to complete. # Now wait a bit for the scan to complete.
# Eventually error though if it doesn't. # Eventually error though if it doesn't.
@ -146,69 +113,49 @@ class MetadataSubsystem:
'timeout waiting for meta scan to complete.') 'timeout waiting for meta scan to complete.')
return self.scanresults return self.scanresults
def get_game_types(self) -> list[type[ba.GameActivity]]: def _handle_scan_results(self) -> None:
"""Return available game types.""" """Called in the logic thread with results of a completed scan."""
from ba._general import getclass from ba._language import Lstr
from ba._gameactivity import GameActivity assert _ba.in_game_thread()
gameclassnames = self.get_scan_results().games
gameclasses = []
for gameclassname in gameclassnames:
try:
cls = getclass(gameclassname, GameActivity)
gameclasses.append(cls)
except Exception:
from ba import _error
_error.print_exception('error importing ' + str(gameclassname))
unowned = self.get_unowned_game_types()
return [cls for cls in gameclasses if cls not in unowned]
def get_unowned_game_types(self) -> set[type[ba.GameActivity]]: results = self.scanresults
"""Return present game types not owned by the current account.""" 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
_ba.screenmessage(Lstr(resource='scanScriptsErrorText'),
color=(1, 0, 0))
_ba.playsound(_ba.getsound('error'))
if results.warnings:
_ba.log(textwrap.indent('\n'.join(results.warnings),
'Warning (meta-scan): '),
to_server=False)
if results.errors:
_ba.log(
textwrap.indent('\n'.join(results.errors),
'Error (meta-scan): '))
def _do_scan_dirs(self) -> None:
"""Runs a scan (for use in background thread)."""
try: try:
from ba import _store assert self._scan is not None
unowned_games: set[type[ba.GameActivity]] = set() self._scan.run()
if not _ba.app.headless_mode: results = self._scan.results
for section in _store.get_store_layout()['minigames']: self._scan = None
for mname in section['items']:
if not _ba.get_purchased(mname):
m_info = _store.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()
class ScanThread(threading.Thread):
"""Thread to scan script dirs for metadata."""
def __init__(self, dirs: list[str]):
super().__init__()
self._dirs = dirs
def run(self) -> None:
from ba._general import Call
try:
scan = DirectoryScan(self._dirs)
scan.scan()
results = scan.results
except Exception as exc: except Exception as exc:
results = ScanResults(errors=f'Scan exception: {exc}') results = ScanResults(errors=[f'Scan exception: {exc}'])
# Push a call to the game thread to print warnings/errors # Place results and tell the logic thread they're ready.
# or otherwise deal with scan results. self.scanresults = results
_ba.pushcall(Call(_ba.app.meta.handle_scan_results, results), _ba.pushcall(self._handle_scan_results, from_other_thread=True)
from_other_thread=True)
# We also, however, immediately make results available.
# This is because the game thread may be blocked waiting
# for them so we can't push a call or we'd get deadlock.
_ba.app.meta.scanresults = results
class DirectoryScan: class DirectoryScan:
"""Handles scanning directories for metadata.""" """Scans directories for metadata."""
def __init__(self, paths: list[str]): def __init__(self, paths: list[str]):
"""Given one or more paths, parses available meta information. """Given one or more paths, parses available meta information.
@ -218,9 +165,42 @@ class DirectoryScan:
""" """
# Skip non-existent paths completely. # Skip non-existent paths completely.
self.paths = [Path(p) for p in paths if os.path.isdir(p)] self.base_paths = [Path(p) for p in paths if os.path.isdir(p)]
self.extra_paths: list[Path] = []
self.extra_paths_set = False
self.results = ScanResults() self.results = ScanResults()
def set_extras(self, paths: list[str]) -> None:
"""Set extra portion."""
# Skip non-existent paths completely.
self.extra_paths += [Path(p) for p in paths if os.path.isdir(p)]
self.extra_paths_set = True
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:
time.sleep(0.001)
modules: list[tuple[Path, Path]] = []
for path in pathlist:
self._get_path_module_entries(path, '', modules)
for moduledir, subpath in modules:
try:
self._scan_module(moduledir, subpath)
except Exception:
import traceback
self.results.warnings.append(
f"Error scanning '{subpath}': " +
traceback.format_exc())
# Sort our results
for exportlist in self.results.exports.values():
exportlist.sort()
def _get_path_module_entries(self, path: Path, subpath: str | Path, def _get_path_module_entries(self, path: Path, subpath: str | Path,
modules: list[tuple[Path, Path]]) -> None: modules: list[tuple[Path, Path]]) -> None:
"""Scan provided path and add module entries to provided list.""" """Scan provided path and add module entries to provided list."""
@ -235,7 +215,7 @@ class DirectoryScan:
entries = [] entries = []
except Exception as exc: except Exception as exc:
# Unexpected; report this. # Unexpected; report this.
self.results.errors += f'{exc}\n' self.results.errors.append(str(exc))
entries = [] entries = []
# Now identify python packages/modules out of what we found. # Now identify python packages/modules out of what we found.
@ -246,24 +226,7 @@ class DirectoryScan:
and Path(entry[0], entry[1], '__init__.py').is_file()): and Path(entry[0], entry[1], '__init__.py').is_file()):
modules.append(entry) modules.append(entry)
def scan(self) -> None: def _scan_module(self, moduledir: Path, subpath: Path) -> None:
"""Scan provided paths."""
modules: list[tuple[Path, Path]] = []
for path in self.paths:
self._get_path_module_entries(path, '', modules)
for moduledir, subpath in modules:
try:
self.scan_module(moduledir, subpath)
except Exception:
import traceback
self.results.warnings += ("Error scanning '" + str(subpath) +
"': " + traceback.format_exc() +
'\n')
# Sort our results
self.results.games.sort()
self.results.plugins.sort()
def scan_module(self, moduledir: Path, subpath: Path) -> None:
"""Scan an individual module and add the findings to results.""" """Scan an individual module and add the findings to results."""
if subpath.name.endswith('.py'): if subpath.name.endswith('.py'):
fpath = Path(moduledir, subpath) fpath = Path(moduledir, subpath)
@ -277,19 +240,20 @@ class DirectoryScan:
lnum: l[1:].split() lnum: l[1:].split()
for lnum, l in enumerate(flines) if '# ba_meta ' in l for lnum, l in enumerate(flines) if '# ba_meta ' in l
} }
toplevel = len(subpath.parts) <= 1 is_top_level = len(subpath.parts) <= 1
required_api = self.get_api_requirement(subpath, meta_lines, toplevel) required_api = self._get_api_requirement(subpath, meta_lines,
is_top_level)
# Top level modules with no discernible api version get ignored. # Top level modules with no discernible api version get ignored.
if toplevel and required_api is None: if is_top_level and required_api is None:
return return
# If we find a module requiring a different api version, warn # If we find a module requiring a different api version, warn
# and ignore. # and ignore.
if required_api is not None and required_api != CURRENT_API_VERSION: if required_api is not None and required_api != CURRENT_API_VERSION:
self.results.warnings += ( self.results.warnings.append(
f'Warning: {subpath} requires api {required_api} but' f'Warning: {subpath} requires api {required_api} but'
f' we are running {CURRENT_API_VERSION}; ignoring module.\n') f' we are running {CURRENT_API_VERSION}; ignoring module.')
return return
# Ok; can proceed with a full scan of this module. # Ok; can proceed with a full scan of this module.
@ -302,11 +266,11 @@ class DirectoryScan:
self._get_path_module_entries(moduledir, subpath, submodules) self._get_path_module_entries(moduledir, subpath, submodules)
for submodule in submodules: for submodule in submodules:
if submodule[1].name != '__init__.py': if submodule[1].name != '__init__.py':
self.scan_module(submodule[0], submodule[1]) self._scan_module(submodule[0], submodule[1])
except Exception: except Exception:
import traceback import traceback
self.results.warnings += ( self.results.warnings.append(
f"Error scanning '{subpath}': {traceback.format_exc()}\n") f"Error scanning '{subpath}': {traceback.format_exc()}")
def _process_module_meta_tags(self, subpath: Path, flines: list[str], def _process_module_meta_tags(self, subpath: Path, flines: list[str],
meta_lines: dict[int, list[str]]) -> None: meta_lines: dict[int, list[str]]) -> None:
@ -315,10 +279,9 @@ class DirectoryScan:
# meta_lines is just anything containing '# ba_meta '; make sure # meta_lines is just anything containing '# ba_meta '; make sure
# the ba_meta is in the right place. # the ba_meta is in the right place.
if mline[0] != 'ba_meta': if mline[0] != 'ba_meta':
self.results.warnings += ( self.results.warnings.append(
'Warning: ' + str(subpath) + f'Warning: {subpath}:'
': malformed ba_meta statement on line ' + f' malformed ba_meta statement on line {lindex + 1}.')
str(lindex + 1) + '.\n')
elif (len(mline) == 4 and mline[1] == 'require' elif (len(mline) == 4 and mline[1] == 'require'
and mline[2] == 'api'): and mline[2] == 'api'):
# Ignore 'require api X' lines in this pass. # Ignore 'require api X' lines in this pass.
@ -326,31 +289,28 @@ class DirectoryScan:
elif len(mline) != 3 or mline[1] != 'export': elif len(mline) != 3 or mline[1] != 'export':
# Currently we only support 'ba_meta export FOO'; # Currently we only support 'ba_meta export FOO';
# complain for anything else we see. # complain for anything else we see.
self.results.warnings += ( self.results.warnings.append(
'Warning: ' + str(subpath) + f'Warning: {subpath}'
': unrecognized ba_meta statement on line ' + f': unrecognized ba_meta statement on line {lindex + 1}.')
str(lindex + 1) + '.\n')
else: else:
# Looks like we've got a valid export line! # Looks like we've got a valid export line!
modulename = '.'.join(subpath.parts) modulename = '.'.join(subpath.parts)
if subpath.name.endswith('.py'): if subpath.name.endswith('.py'):
modulename = modulename[:-3] modulename = modulename[:-3]
exporttype = mline[2] exporttypestr = mline[2]
export_class_name = self._get_export_class_name( export_class_name = self._get_export_class_name(
subpath, flines, lindex) subpath, flines, lindex)
if export_class_name is not None: if export_class_name is not None:
classname = modulename + '.' + export_class_name classname = modulename + '.' + export_class_name
if exporttype == 'game':
self.results.games.append(classname) # If export type is one of our shortcuts, sub in the
elif exporttype == 'plugin': # actual class path. Otherwise assume its a classpath
self.results.plugins.append(classname) # itself.
elif exporttype == 'keyboard': exporttype = EXPORT_CLASS_NAME_SHORTCUTS.get(exporttypestr)
self.results.keyboards.append(classname) if exporttype is None:
else: exporttype = exporttypestr
self.results.warnings += ( self.results.exports.setdefault(exporttype,
'Warning: ' + str(subpath) + []).append(classname)
': unrecognized export type "' + exporttype +
'" on line ' + str(lindex + 1) + '.\n')
def _get_export_class_name(self, subpath: Path, lines: list[str], def _get_export_class_name(self, subpath: Path, lines: list[str],
lindex: int) -> str | None: lindex: int) -> str | None:
@ -372,13 +332,12 @@ class DirectoryScan:
classname = cbits[0] classname = cbits[0]
break # Success! break # Success!
if classname is None: if classname is None:
self.results.warnings += ( self.results.warnings.append(
'Warning: ' + str(subpath) + ': class definition not found' f'Warning: {subpath}: class definition not found below'
' below "ba_meta export" statement on line ' + f' "ba_meta export" statement on line {lindexorig + 1}.')
str(lindexorig + 1) + '.\n')
return classname return classname
def get_api_requirement( def _get_api_requirement(
self, self,
subpath: Path, subpath: Path,
meta_lines: dict[int, list[str]], meta_lines: dict[int, list[str]],
@ -399,15 +358,15 @@ class DirectoryScan:
# Ok; not successful. lets issue warnings for a few error cases. # Ok; not successful. lets issue warnings for a few error cases.
if len(lines) > 1: if len(lines) > 1:
self.results.warnings += ( self.results.warnings.append(
'Warning: ' + str(subpath) + f'Warning: {subpath}: multiple'
': multiple "# ba_meta require api <NUM>" lines found;' ' "# ba_meta require api <NUM>" lines found;'
' ignoring module.\n') ' ignoring module.')
elif not lines and toplevel and meta_lines: elif not lines and toplevel and meta_lines:
# If we're a top-level module containing meta lines but # If we're a top-level module containing meta lines but
# no valid "require api" line found, complain. # no valid "require api" line found, complain.
self.results.warnings += ( self.results.warnings.append(
'Warning: ' + str(subpath) + f'Warning: {subpath}:'
': no valid "# ba_meta require api <NUM>" line found;' ' no valid "# ba_meta require api <NUM>" line found;'
' ignoring module.\n') ' ignoring module.')
return None return None

View File

@ -29,14 +29,15 @@ def filter_playlist(playlist: PlaylistType,
# pylint: disable=too-many-branches # pylint: disable=too-many-branches
# pylint: disable=too-many-statements # pylint: disable=too-many-statements
import _ba import _ba
from ba import _map from ba._map import get_filtered_map_name
from ba import _general from ba._store import get_unowned_maps, get_unowned_game_types
from ba import _gameactivity from ba._general import getclass
from ba._gameactivity import GameActivity
goodlist: list[dict] = [] goodlist: list[dict] = []
unowned_maps: Sequence[str] unowned_maps: Sequence[str]
if remove_unowned or mark_unowned: if remove_unowned or mark_unowned:
unowned_maps = _map.get_unowned_maps() unowned_maps = get_unowned_maps()
unowned_game_types = _ba.app.meta.get_unowned_game_types() unowned_game_types = get_unowned_game_types()
else: else:
unowned_maps = [] unowned_maps = []
unowned_game_types = set() unowned_game_types = set()
@ -53,7 +54,7 @@ def filter_playlist(playlist: PlaylistType,
del entry['map'] del entry['map']
# Update old map names to new ones. # Update old map names to new ones.
entry['settings']['map'] = _map.get_filtered_map_name( entry['settings']['map'] = get_filtered_map_name(
entry['settings']['map']) entry['settings']['map'])
if remove_unowned and entry['settings']['map'] in unowned_maps: if remove_unowned and entry['settings']['map'] in unowned_maps:
continue continue
@ -120,8 +121,7 @@ def filter_playlist(playlist: PlaylistType,
entry['type'] = ( entry['type'] = (
'bastd.game.targetpractice.TargetPracticeGame') 'bastd.game.targetpractice.TargetPracticeGame')
gameclass = _general.getclass(entry['type'], gameclass = getclass(entry['type'], GameActivity)
_gameactivity.GameActivity)
if remove_unowned and gameclass in unowned_game_types: if remove_unowned and gameclass in unowned_game_types:
continue continue

View File

@ -25,6 +25,46 @@ class PluginSubsystem:
self.potential_plugins: list[ba.PotentialPlugin] = [] self.potential_plugins: list[ba.PotentialPlugin] = []
self.active_plugins: dict[str, ba.Plugin] = {} 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 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: def on_app_running(self) -> None:
"""Should be called when the app reaches the running state.""" """Should be called when the app reaches the running state."""
# Load up our plugins and go ahead and call their on_app_running calls. # Load up our plugins and go ahead and call their on_app_running calls.
@ -69,10 +109,7 @@ class PluginSubsystem:
from ba._language import Lstr from ba._language import Lstr
# Note: the plugins we load is purely based on what's enabled # Note: the plugins we load is purely based on what's enabled
# in the app config. Our meta-scan gives us a list of available # in the app config. Its not our job to look at meta stuff here.
# plugins, but that is only used to give the user a list of plugins
# that they can enable. (we wouldn't want to look at meta-scan here
# anyway because it may not be done yet at this point in the launch)
plugstates: dict[str, dict] = _ba.app.config.get('Plugins', {}) plugstates: dict[str, dict] = _ba.app.config.get('Plugins', {})
assert isinstance(plugstates, dict) assert isinstance(plugstates, dict)
plugkeys: list[str] = sorted(key for key, val in plugstates.items() plugkeys: list[str] = sorted(key for key, val in plugstates.items()

View File

@ -509,3 +509,35 @@ def get_available_sale_time(tab: str) -> int | None:
from ba import _error from ba import _error
_error.print_exception('error calcing sale time') _error.print_exception('error calcing sale time')
return None 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 _ba.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 _ba.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()

View File

@ -7,9 +7,8 @@ or disappear without warning, so should be avoided (or used sparingly and
defensively) in mods. defensively) in mods.
""" """
from ba._map import (get_unowned_maps, get_map_class, register_map, from ba._map import (get_map_class, register_map, preload_map_preview_media,
preload_map_preview_media, get_map_display_string, get_map_display_string, get_filtered_map_name)
get_filtered_map_name)
from ba._appconfig import commit_app_config from ba._appconfig import commit_app_config
from ba._input import (get_device_value, get_input_map_hash, from ba._input import (get_device_value, get_input_map_hash,
get_input_device_config) get_input_device_config)
@ -34,13 +33,14 @@ from ba._playlist import (get_default_free_for_all_playlist,
from ba._store import (get_available_sale_time, get_available_purchase_count, from ba._store import (get_available_sale_time, get_available_purchase_count,
get_store_item_name_translated, get_store_item_name_translated,
get_store_item_display_size, get_store_layout, get_store_item_display_size, get_store_layout,
get_store_item, get_clean_price) get_store_item, get_clean_price, get_unowned_maps,
get_unowned_game_types)
from ba._tournament import get_tournament_prize_strings from ba._tournament import get_tournament_prize_strings
from ba._gameutils import get_trophy_string from ba._gameutils import get_trophy_string, get_game_types
__all__ = [ __all__ = [
'get_unowned_maps', 'get_map_class', 'register_map', 'get_unowned_maps', 'get_unowned_game_types', 'get_map_class',
'preload_map_preview_media', 'get_map_display_string', 'register_map', 'preload_map_preview_media', 'get_map_display_string',
'get_filtered_map_name', 'commit_app_config', 'get_device_value', 'get_filtered_map_name', 'commit_app_config', 'get_device_value',
'get_input_map_hash', 'get_input_device_config', 'getclass', 'json_prep', 'get_input_map_hash', 'get_input_device_config', 'getclass', 'json_prep',
'get_type_name', 'JoinActivity', 'ScoreScreenActivity', 'get_type_name', 'JoinActivity', 'ScoreScreenActivity',
@ -56,5 +56,6 @@ __all__ = [
'get_default_teams_playlist', 'filter_playlist', 'get_available_sale_time', 'get_default_teams_playlist', 'filter_playlist', 'get_available_sale_time',
'get_available_purchase_count', 'get_store_item_name_translated', 'get_available_purchase_count', 'get_store_item_name_translated',
'get_store_item_display_size', 'get_store_layout', 'get_store_item', 'get_store_item_display_size', 'get_store_layout', 'get_store_item',
'get_clean_price', 'get_tournament_prize_strings', 'get_trophy_string' 'get_clean_price', 'get_tournament_prize_strings', 'get_trophy_string',
'get_game_types'
] ]

View File

@ -213,8 +213,10 @@ class OnScreenKeyboardWindow(ba.Window):
# Show change instructions only if we have more than one # Show change instructions only if we have more than one
# keyboard option. # keyboard option.
if (ba.app.meta.scanresults is not None keyboards = (ba.app.meta.scanresults.exports_of_class(
and len(ba.app.meta.scanresults.keyboards) > 1): ba.Keyboard) if ba.app.meta.scanresults is not None
else [])
if len(keyboards) > 1:
ba.textwidget( ba.textwidget(
parent=self._root_widget, parent=self._root_widget,
h_align='center', h_align='center',
@ -239,7 +241,8 @@ class OnScreenKeyboardWindow(ba.Window):
def _get_keyboard(self) -> ba.Keyboard: def _get_keyboard(self) -> ba.Keyboard:
assert ba.app.meta.scanresults is not None assert ba.app.meta.scanresults is not None
classname = ba.app.meta.scanresults.keyboards[self._keyboard_index] classname = ba.app.meta.scanresults.exports_of_class(
ba.Keyboard)[self._keyboard_index]
kbclass = ba.getclass(classname, ba.Keyboard) kbclass = ba.getclass(classname, ba.Keyboard)
return kbclass() return kbclass()
@ -318,10 +321,11 @@ class OnScreenKeyboardWindow(ba.Window):
def _next_keyboard(self) -> None: def _next_keyboard(self) -> None:
assert ba.app.meta.scanresults is not None assert ba.app.meta.scanresults is not None
self._keyboard_index = (self._keyboard_index + 1) % len( kbexports = ba.app.meta.scanresults.exports_of_class(ba.Keyboard)
ba.app.meta.scanresults.keyboards) self._keyboard_index = (self._keyboard_index + 1) % len(kbexports)
self._load_keyboard() self._load_keyboard()
if len(ba.app.meta.scanresults.keyboards) < 2: if len(kbexports) < 2:
ba.playsound(ba.getsound('error')) ba.playsound(ba.getsound('error'))
ba.screenmessage(ba.Lstr(resource='keyboardNoOthersAvailableText'), ba.screenmessage(ba.Lstr(resource='keyboardNoOthersAvailableText'),
color=(1, 0, 0)) color=(1, 0, 0))

View File

@ -119,6 +119,7 @@ class PlaylistAddGameWindow(ba.Window):
self._refresh() self._refresh()
def _refresh(self, select_get_more_games_button: bool = False) -> None: def _refresh(self, select_get_more_games_button: bool = False) -> None:
from ba.internal import get_game_types
if self._column is not None: if self._column is not None:
self._column.delete() self._column.delete()
@ -128,8 +129,8 @@ class PlaylistAddGameWindow(ba.Window):
margin=0) margin=0)
gametypes = [ gametypes = [
gt for gt in ba.app.meta.get_game_types() if gt for gt in get_game_types() if gt.supports_session_type(
gt.supports_session_type(self._editcontroller.get_session_type()) self._editcontroller.get_session_type())
] ]
# Sort in the current language. # Sort in the current language.

View File

@ -412,6 +412,8 @@
<w>expbool</w> <w>expbool</w>
<w>expectedsig</w> <w>expectedsig</w>
<w>expl</w> <w>expl</w>
<w>exportlist</w>
<w>exporttypestr</w>
<w>extradata</w> <w>extradata</w>
<w>extrahash</w> <w>extrahash</w>
<w>extrascale</w> <w>extrascale</w>
@ -653,6 +655,7 @@
<w>jnames</w> <w>jnames</w>
<w>json's</w> <w>json's</w>
<w>juleskie</w> <w>juleskie</w>
<w>kbexports</w>
<w>keepalives</w> <w>keepalives</w>
<w>kerploople</w> <w>kerploople</w>
<w>keyanntype</w> <w>keyanntype</w>
@ -931,6 +934,7 @@
<w>passcode</w> <w>passcode</w>
<w>pathcapture</w> <w>pathcapture</w>
<w>pathdst</w> <w>pathdst</w>
<w>pathlist</w>
<w>pathparts</w> <w>pathparts</w>
<w>pathsrc</w> <w>pathsrc</w>
<w>pausable</w> <w>pausable</w>

View File

@ -21,7 +21,7 @@
namespace ballistica { namespace ballistica {
// These are set automatically via script; don't modify them here. // These are set automatically via script; don't modify them here.
const int kAppBuildNumber = 20674; const int kAppBuildNumber = 20676;
const char* kAppVersion = "1.7.6"; const char* kAppVersion = "1.7.6";
// Our standalone globals. // Our standalone globals.