diff --git a/.efrocachemap b/.efrocachemap
index ce7102c5..321c5e6f 100644
--- a/.efrocachemap
+++ b/.efrocachemap
@@ -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/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",
- "build/prefab/full/linux_arm64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/36/14/efd9145a5e1562b04f6ff560e00d",
- "build/prefab/full/linux_arm64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/68/f3/376aecc59b69d288c276327f0a99",
- "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/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/a6/e3/a453b14a443de5dc69e32d62b425",
- "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/release/ballisticacore": "https://files.ballistica.net/cache/ba1/62/f8/3798837a711a1e488f97e9d30fea",
- "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/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/2c/d1/2732b76fd513d1aed1ca0a9b9597",
- "build/prefab/full/mac_arm64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/92/26/bb135305ca78c79f03cc54aa8ed6",
- "build/prefab/full/mac_arm64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/03/8a/6fb8aa21f23580e331079c285bbf",
- "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/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/e6/e4/cad1d7bf7347a8f505fc9ed3d483",
- "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/release/ballisticacore": "https://files.ballistica.net/cache/ba1/2c/71/b8587bf758d8774dc5363e99c25b",
- "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/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/2e/cb/c100875d6d87b162c5ee5d8e7623",
- "build/prefab/full/windows_x86_gui/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/51/06/f5d5e4167caf3c1920e041ce79ae",
- "build/prefab/full/windows_x86_gui/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/89/fb/53cbead744939cbef30386a74b0b",
- "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/release/dist/BallisticaCoreHeadless.exe": "https://files.ballistica.net/cache/ba1/b0/f1/0e699be629f7936a67e7b336a383",
+ "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/52/36/3db8e70d87bc3399bdc39a26dea4",
+ "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/59/e3/c2bc89f60961f4ed0708a5340ec8",
+ "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/44/88/634e154803570f4b977a51114706",
+ "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/a1/d0/0c906dd30d70314b13d4bbc6b5bf",
+ "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/e3/1c/2cc790a44ed8abe0d7cab40a4fba",
+ "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/77/f4/52b8e2abd145a276e614a9286dbe",
+ "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/cf/ab/572963d01007626cfa067ad750b4",
+ "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/23/1c/4027cf14f282242f2ea8a11c684d",
+ "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/11/2e/5019b997e6703cc65033a5ad9aaa",
+ "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/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/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",
@@ -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_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/windows/Debug_Win32/BallisticaCoreGenericInternal.lib": "https://files.ballistica.net/cache/ba1/b5/cd/789085462ab13214d6addbe06921",
- "build/prefab/lib/windows/Debug_Win32/BallisticaCoreGenericInternal.pdb": "https://files.ballistica.net/cache/ba1/5a/98/cbca8986e08d5fcfdc7aeaef25e2",
- "build/prefab/lib/windows/Debug_Win32/BallisticaCoreHeadlessInternal.lib": "https://files.ballistica.net/cache/ba1/7a/2d/cef593252fbe3fddc22f6e788130",
- "build/prefab/lib/windows/Debug_Win32/BallisticaCoreHeadlessInternal.pdb": "https://files.ballistica.net/cache/ba1/0a/30/b9b0d8629eb372e48cb0bc2d0241",
- "build/prefab/lib/windows/Release_Win32/BallisticaCoreGenericInternal.lib": "https://files.ballistica.net/cache/ba1/5e/1f/86c5af445fe4c21e58ebcf5c7f39",
- "build/prefab/lib/windows/Release_Win32/BallisticaCoreGenericInternal.pdb": "https://files.ballistica.net/cache/ba1/b5/38/ac11b6c33d404a421e8adf354202",
- "build/prefab/lib/windows/Release_Win32/BallisticaCoreHeadlessInternal.lib": "https://files.ballistica.net/cache/ba1/64/2d/2833633e1277af02c072b1cbc03c",
- "build/prefab/lib/windows/Release_Win32/BallisticaCoreHeadlessInternal.pdb": "https://files.ballistica.net/cache/ba1/bd/1c/36e1ee0184d510d548f002ddacfe",
+ "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/ed/c8/c72049370c7fa1f8ef703526b818",
+ "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/36/1b/2710d1d788bf5a6518f6092a9829",
+ "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/84/e3/f5b80f011508edfabc2e042ea356",
+ "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/3c/57/39092393925cb999582a15023275",
"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"
}
\ No newline at end of file
diff --git a/.idea/dictionaries/ericf.xml b/.idea/dictionaries/ericf.xml
index afd796c3..93067d86 100644
--- a/.idea/dictionaries/ericf.xml
+++ b/.idea/dictionaries/ericf.xml
@@ -799,9 +799,11 @@
expectedsig
explodable
explodey
+ exportlist
exportoptions
exportoptionspath
exporttype
+ exporttypestr
extradata
extraflagmat
extrahash
@@ -1266,6 +1268,7 @@
jsonutils
juleskie
kbclass
+ kbexports
kbytecount
keepalive
keepalives
@@ -1782,6 +1785,7 @@
pathcapture
pathdst
pathlib
+ pathlist
pathnames
pathparts
pathsrc
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 71f57bc2..0f965f7c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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)
- 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.
diff --git a/assets/src/ba_data/python/ba/_app.py b/assets/src/ba_data/python/ba/_app.py
index c04b7fe9..8031d289 100644
--- a/assets/src/ba_data/python/ba/_app.py
+++ b/assets/src/ba_data/python/ba/_app.py
@@ -223,6 +223,7 @@ class App:
self._launch_completed = False
self._initial_login_completed = False
+ self._meta_scan_completed = False
self._called_on_app_running = False
self._app_paused = False
@@ -344,6 +345,8 @@ class App:
from bastd.actor import spazappearance
from ba._generated.enums import TimeType
+ assert _ba.in_game_thread()
+
self._aioloop = _asyncio.setup_asyncio()
cfg = self.config
@@ -415,6 +418,9 @@ class App:
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()
@@ -429,17 +435,27 @@ class App:
def on_app_running(self) -> None:
"""Called when initially entering the running state."""
- self.meta.on_app_running()
self.plugins.on_app_running()
# from ba._dependency import 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:
+ assert _ba.in_game_thread()
+
if self._app_paused:
self.state = self.State.PAUSED
else:
- if self._initial_login_completed:
+ if self._initial_login_completed and self._meta_scan_completed:
self.state = self.State.RUNNING
if not self._called_on_app_running:
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
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._update_state()
diff --git a/assets/src/ba_data/python/ba/_gameactivity.py b/assets/src/ba_data/python/ba/_gameactivity.py
index 0d221851..1b91f16f 100644
--- a/assets/src/ba_data/python/ba/_gameactivity.py
+++ b/assets/src/ba_data/python/ba/_gameactivity.py
@@ -17,6 +17,7 @@ from ba._error import NotFoundError, print_error, print_exception
from ba._general import Call, WeakCall
from ba._player import PlayerInfo
from ba import _map
+from ba import _store
if TYPE_CHECKING:
from typing import Any, Callable, Sequence
@@ -1159,7 +1160,7 @@ class GameActivity(Activity[PlayerType, TeamType]):
else:
# If settings doesn't specify a map, pick a random one from the
# list of supported ones.
- unowned_maps = _map.get_unowned_maps()
+ unowned_maps = _store.get_unowned_maps()
valid_maps: list[str] = [
m for m in self.get_supported_maps(type(self.session))
if m not in unowned_maps
diff --git a/assets/src/ba_data/python/ba/_gameutils.py b/assets/src/ba_data/python/ba/_gameutils.py
index 97cac63a..65d93345 100644
--- a/assets/src/ba_data/python/ba/_gameutils.py
+++ b/assets/src/ba_data/python/ba/_gameutils.py
@@ -399,3 +399,24 @@ def cameraflash(duration: float = 999.0) -> None:
light.node.delete,
timeformat=TimeFormat.MILLISECONDS)
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]
diff --git a/assets/src/ba_data/python/ba/_map.py b/assets/src/ba_data/python/ba/_map.py
index 6cbc48e0..122e4df6 100644
--- a/assets/src/ba_data/python/ba/_map.py
+++ b/assets/src/ba_data/python/ba/_map.py
@@ -101,22 +101,6 @@ def getmaps(playtype: str) -> list[str]:
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]:
"""Return a map type given a name.
diff --git a/assets/src/ba_data/python/ba/_meta.py b/assets/src/ba_data/python/ba/_meta.py
index 8b29f9e3..a008be98 100644
--- a/assets/src/ba_data/python/ba/_meta.py
+++ b/assets/src/ba_data/python/ba/_meta.py
@@ -6,15 +6,16 @@ from __future__ import annotations
import os
import time
-import threading
+import logging
+from threading import Thread
from pathlib import Path
-from typing import TYPE_CHECKING
+from typing import TYPE_CHECKING, TypeVar
from dataclasses import dataclass, field
import _ba
if TYPE_CHECKING:
- import ba
+ from typing import Callable
# The meta api version of this build of the game.
# Only packages and modules requiring this exact api version
@@ -22,15 +23,28 @@ if TYPE_CHECKING:
# See: https://ballistica.net/wiki/Meta-Tags
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
class ScanResults:
- """Final results from a metadata scan."""
- games: list[str] = field(default_factory=list)
- plugins: list[str] = field(default_factory=list)
- keyboards: list[str] = field(default_factory=list)
- errors: str = ''
- warnings: str = ''
+ """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)
+
+ 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:
@@ -42,99 +56,52 @@ class MetadataSubsystem:
"""
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] = []
- def on_app_running(self) -> None:
- """Should be called when the app enters the running state."""
+ # Results populated once scan is complete.
+ self.scanresults: ScanResults | None = None
- # Start scanning for things exposed via ba_meta.
- self.start_scan()
+ self._scan_complete_cb: Callable[[], None] | None = None
- def start_scan(self) -> None:
- """Begin scanning script directories for scripts containing metadata.
+ def start_scan(self, scan_complete_cb: Callable[[], None]) -> None:
+ """Begin the overall scan.
- Should be called only once at launch."""
- app = _ba.app
- if self.scanresults is not None:
- print('WARNING: meta scan run more than once.')
- pythondirs = ([app.python_directory_app, app.python_directory_user] +
- self.extra_scan_dirs)
- thread = ScanThread(pythondirs)
- thread.start()
+ This will start scanning built in directories (which for vanilla
+ installs should be the vast majority of the work). This should only
+ be called once.
+ """
+ assert self._scan_complete_cb is None
+ assert self._scan is None
- def handle_scan_results(self, results: ScanResults) -> None:
- """Called in the game thread with results of a completed scan."""
+ self._scan_complete_cb = scan_complete_cb
+ self._scan = DirectoryScan(
+ [_ba.app.python_directory_app, _ba.app.python_directory_user])
- from ba._language import Lstr
- from ba._plugin import PotentialPlugin
+ Thread(target=self._do_scan_dirs, daemon=True).start()
- # 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
- # 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): '))
+ def start_extra_scan(self) -> None:
+ """Provide extra dirs to be scanned (namely Workspace dirs).
- # Handle plugins.
- plugs = _ba.app.plugins
- config_changed = False
- found_new = False
- plugstates: dict[str, dict] = _ba.app.config.setdefault('Plugins', {})
- assert isinstance(plugstates, dict)
+ This is the bare minimum part of the scan that must be delayed until
+ workspaces have been synced/etc. This must be called exactly once.
+ """
+ assert self._scan is not None
+ self._scan.set_extras(self.extra_scan_dirs)
- # Create a potential-plugin for each class we found in the scan.
- for class_path in results.plugins:
- 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
+ # Let the game know we're done.
+ assert self._scan_complete_cb is not None
+ self._scan_complete_cb()
- # Also add a special one for any plugins set to load but *not* found
- # in the scan (this way they will show up in the UI so we can disable
- # 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."""
+ def wait_for_scan_results(self) -> ScanResults:
+ """Return scan results, blocking if the scan is not yet complete."""
if self.scanresults is None:
- print('WARNING: ba.meta.get_scan_results()'
- ' called before scan completed.'
- ' This can cause hitches.')
+ logging.warning('ba.meta.wait_for_scan_results()'
+ ' called before scan completed;'
+ ' this can cause hitches.')
# Now wait a bit for the scan to complete.
# Eventually error though if it doesn't.
@@ -146,69 +113,49 @@ class MetadataSubsystem:
'timeout waiting for meta scan to complete.')
return self.scanresults
- def get_game_types(self) -> list[type[ba.GameActivity]]:
- """Return available game types."""
- from ba._general import getclass
- from ba._gameactivity import GameActivity
- 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 _handle_scan_results(self) -> None:
+ """Called in the logic thread with results of a completed scan."""
+ from ba._language import Lstr
+ assert _ba.in_game_thread()
- def get_unowned_game_types(self) -> set[type[ba.GameActivity]]:
- """Return present game types not owned by the current account."""
+ 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
+ _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:
- from ba import _store
- unowned_games: set[type[ba.GameActivity]] = set()
- if not _ba.app.headless_mode:
- for section in _store.get_store_layout()['minigames']:
- 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
+ assert self._scan is not None
+ self._scan.run()
+ results = self._scan.results
+ self._scan = None
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
- # or otherwise deal with scan results.
- _ba.pushcall(Call(_ba.app.meta.handle_scan_results, results),
- 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
+ # Place results and tell the logic thread they're ready.
+ self.scanresults = results
+ _ba.pushcall(self._handle_scan_results, from_other_thread=True)
class DirectoryScan:
- """Handles scanning directories for metadata."""
+ """Scans directories for metadata."""
def __init__(self, paths: list[str]):
"""Given one or more paths, parses available meta information.
@@ -218,9 +165,42 @@ class DirectoryScan:
"""
# 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()
+ 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,
modules: list[tuple[Path, Path]]) -> None:
"""Scan provided path and add module entries to provided list."""
@@ -235,7 +215,7 @@ class DirectoryScan:
entries = []
except Exception as exc:
# Unexpected; report this.
- self.results.errors += f'{exc}\n'
+ self.results.errors.append(str(exc))
entries = []
# 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()):
modules.append(entry)
- def scan(self) -> 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:
+ def _scan_module(self, moduledir: Path, subpath: Path) -> None:
"""Scan an individual module and add the findings to results."""
if subpath.name.endswith('.py'):
fpath = Path(moduledir, subpath)
@@ -277,19 +240,20 @@ class DirectoryScan:
lnum: l[1:].split()
for lnum, l in enumerate(flines) if '# ba_meta ' in l
}
- toplevel = len(subpath.parts) <= 1
- required_api = self.get_api_requirement(subpath, meta_lines, toplevel)
+ is_top_level = len(subpath.parts) <= 1
+ required_api = self._get_api_requirement(subpath, meta_lines,
+ is_top_level)
# 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
# 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 += (
+ self.results.warnings.append(
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
# Ok; can proceed with a full scan of this module.
@@ -302,11 +266,11 @@ class DirectoryScan:
self._get_path_module_entries(moduledir, subpath, submodules)
for submodule in submodules:
if submodule[1].name != '__init__.py':
- self.scan_module(submodule[0], submodule[1])
+ self._scan_module(submodule[0], submodule[1])
except Exception:
import traceback
- self.results.warnings += (
- f"Error scanning '{subpath}': {traceback.format_exc()}\n")
+ self.results.warnings.append(
+ f"Error scanning '{subpath}': {traceback.format_exc()}")
def _process_module_meta_tags(self, subpath: Path, flines: list[str],
meta_lines: dict[int, list[str]]) -> None:
@@ -315,10 +279,9 @@ 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 += (
- 'Warning: ' + str(subpath) +
- ': malformed ba_meta statement on line ' +
- str(lindex + 1) + '.\n')
+ self.results.warnings.append(
+ f'Warning: {subpath}:'
+ f' malformed ba_meta statement on line {lindex + 1}.')
elif (len(mline) == 4 and mline[1] == 'require'
and mline[2] == 'api'):
# Ignore 'require api X' lines in this pass.
@@ -326,31 +289,28 @@ 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 += (
- 'Warning: ' + str(subpath) +
- ': unrecognized ba_meta statement on line ' +
- str(lindex + 1) + '.\n')
+ self.results.warnings.append(
+ f'Warning: {subpath}'
+ f': unrecognized ba_meta statement on line {lindex + 1}.')
else:
# Looks like we've got a valid export line!
modulename = '.'.join(subpath.parts)
if subpath.name.endswith('.py'):
modulename = modulename[:-3]
- exporttype = mline[2]
+ exporttypestr = mline[2]
export_class_name = self._get_export_class_name(
subpath, flines, lindex)
if export_class_name is not None:
classname = modulename + '.' + export_class_name
- if exporttype == 'game':
- self.results.games.append(classname)
- elif exporttype == 'plugin':
- self.results.plugins.append(classname)
- elif exporttype == 'keyboard':
- self.results.keyboards.append(classname)
- else:
- self.results.warnings += (
- 'Warning: ' + str(subpath) +
- ': unrecognized export type "' + exporttype +
- '" on line ' + str(lindex + 1) + '.\n')
+
+ # 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) -> str | None:
@@ -372,13 +332,12 @@ class DirectoryScan:
classname = cbits[0]
break # Success!
if classname is None:
- self.results.warnings += (
- 'Warning: ' + str(subpath) + ': class definition not found'
- ' below "ba_meta export" statement on line ' +
- str(lindexorig + 1) + '.\n')
+ self.results.warnings.append(
+ f'Warning: {subpath}: class definition not found below'
+ f' "ba_meta export" statement on line {lindexorig + 1}.')
return classname
- def get_api_requirement(
+ def _get_api_requirement(
self,
subpath: Path,
meta_lines: dict[int, list[str]],
@@ -399,15 +358,15 @@ class DirectoryScan:
# Ok; not successful. lets issue warnings for a few error cases.
if len(lines) > 1:
- self.results.warnings += (
- 'Warning: ' + str(subpath) +
- ': multiple "# ba_meta require api " lines found;'
- ' ignoring module.\n')
+ self.results.warnings.append(
+ f'Warning: {subpath}: multiple'
+ ' "# ba_meta require api " lines found;'
+ ' ignoring module.')
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 += (
- 'Warning: ' + str(subpath) +
- ': no valid "# ba_meta require api " line found;'
- ' ignoring module.\n')
+ self.results.warnings.append(
+ f'Warning: {subpath}:'
+ ' no valid "# ba_meta require api " line found;'
+ ' ignoring module.')
return None
diff --git a/assets/src/ba_data/python/ba/_playlist.py b/assets/src/ba_data/python/ba/_playlist.py
index f99aabe4..848259a2 100644
--- a/assets/src/ba_data/python/ba/_playlist.py
+++ b/assets/src/ba_data/python/ba/_playlist.py
@@ -29,14 +29,15 @@ def filter_playlist(playlist: PlaylistType,
# pylint: disable=too-many-branches
# pylint: disable=too-many-statements
import _ba
- from ba import _map
- from ba import _general
- from ba import _gameactivity
+ from ba._map import get_filtered_map_name
+ from ba._store import get_unowned_maps, get_unowned_game_types
+ from ba._general import getclass
+ from ba._gameactivity import GameActivity
goodlist: list[dict] = []
unowned_maps: Sequence[str]
if remove_unowned or mark_unowned:
- unowned_maps = _map.get_unowned_maps()
- unowned_game_types = _ba.app.meta.get_unowned_game_types()
+ unowned_maps = get_unowned_maps()
+ unowned_game_types = get_unowned_game_types()
else:
unowned_maps = []
unowned_game_types = set()
@@ -53,7 +54,7 @@ def filter_playlist(playlist: PlaylistType,
del entry['map']
# 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'])
if remove_unowned and entry['settings']['map'] in unowned_maps:
continue
@@ -120,8 +121,7 @@ def filter_playlist(playlist: PlaylistType,
entry['type'] = (
'bastd.game.targetpractice.TargetPracticeGame')
- gameclass = _general.getclass(entry['type'],
- _gameactivity.GameActivity)
+ gameclass = getclass(entry['type'], GameActivity)
if remove_unowned and gameclass in unowned_game_types:
continue
diff --git a/assets/src/ba_data/python/ba/_plugin.py b/assets/src/ba_data/python/ba/_plugin.py
index 9e7200f2..c4702045 100644
--- a/assets/src/ba_data/python/ba/_plugin.py
+++ b/assets/src/ba_data/python/ba/_plugin.py
@@ -25,6 +25,46 @@ class PluginSubsystem:
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 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.
@@ -69,10 +109,7 @@ class PluginSubsystem:
from ba._language import Lstr
# 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
- # 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)
+ # 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()
diff --git a/assets/src/ba_data/python/ba/_store.py b/assets/src/ba_data/python/ba/_store.py
index 81a2e21a..deeefb0d 100644
--- a/assets/src/ba_data/python/ba/_store.py
+++ b/assets/src/ba_data/python/ba/_store.py
@@ -509,3 +509,35 @@ def get_available_sale_time(tab: str) -> int | None:
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 _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()
diff --git a/assets/src/ba_data/python/ba/internal.py b/assets/src/ba_data/python/ba/internal.py
index 95a7bfc3..7bb22e6d 100644
--- a/assets/src/ba_data/python/ba/internal.py
+++ b/assets/src/ba_data/python/ba/internal.py
@@ -7,9 +7,8 @@ or disappear without warning, so should be avoided (or used sparingly and
defensively) in mods.
"""
-from ba._map import (get_unowned_maps, get_map_class, register_map,
- preload_map_preview_media, get_map_display_string,
- get_filtered_map_name)
+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)
@@ -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,
get_store_item_name_translated,
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._gameutils import get_trophy_string
+from ba._gameutils import get_trophy_string, get_game_types
__all__ = [
- 'get_unowned_maps', 'get_map_class', 'register_map',
- 'preload_map_preview_media', 'get_map_display_string',
+ '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',
@@ -56,5 +56,6 @@ __all__ = [
'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_clean_price', 'get_tournament_prize_strings', 'get_trophy_string',
+ 'get_game_types'
]
diff --git a/assets/src/ba_data/python/bastd/ui/onscreenkeyboard.py b/assets/src/ba_data/python/bastd/ui/onscreenkeyboard.py
index 69d2303a..3997cca3 100644
--- a/assets/src/ba_data/python/bastd/ui/onscreenkeyboard.py
+++ b/assets/src/ba_data/python/bastd/ui/onscreenkeyboard.py
@@ -213,8 +213,10 @@ class OnScreenKeyboardWindow(ba.Window):
# Show change instructions only if we have more than one
# keyboard option.
- if (ba.app.meta.scanresults is not None
- and len(ba.app.meta.scanresults.keyboards) > 1):
+ keyboards = (ba.app.meta.scanresults.exports_of_class(
+ ba.Keyboard) if ba.app.meta.scanresults is not None
+ else [])
+ if len(keyboards) > 1:
ba.textwidget(
parent=self._root_widget,
h_align='center',
@@ -239,7 +241,8 @@ class OnScreenKeyboardWindow(ba.Window):
def _get_keyboard(self) -> ba.Keyboard:
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)
return kbclass()
@@ -318,10 +321,11 @@ class OnScreenKeyboardWindow(ba.Window):
def _next_keyboard(self) -> None:
assert ba.app.meta.scanresults is not None
- self._keyboard_index = (self._keyboard_index + 1) % len(
- ba.app.meta.scanresults.keyboards)
+ kbexports = ba.app.meta.scanresults.exports_of_class(ba.Keyboard)
+ self._keyboard_index = (self._keyboard_index + 1) % len(kbexports)
+
self._load_keyboard()
- if len(ba.app.meta.scanresults.keyboards) < 2:
+ if len(kbexports) < 2:
ba.playsound(ba.getsound('error'))
ba.screenmessage(ba.Lstr(resource='keyboardNoOthersAvailableText'),
color=(1, 0, 0))
diff --git a/assets/src/ba_data/python/bastd/ui/playlist/addgame.py b/assets/src/ba_data/python/bastd/ui/playlist/addgame.py
index 8ca5c6c2..065749c4 100644
--- a/assets/src/ba_data/python/bastd/ui/playlist/addgame.py
+++ b/assets/src/ba_data/python/bastd/ui/playlist/addgame.py
@@ -119,6 +119,7 @@ class PlaylistAddGameWindow(ba.Window):
self._refresh()
def _refresh(self, select_get_more_games_button: bool = False) -> None:
+ from ba.internal import get_game_types
if self._column is not None:
self._column.delete()
@@ -128,8 +129,8 @@ class PlaylistAddGameWindow(ba.Window):
margin=0)
gametypes = [
- gt for gt in ba.app.meta.get_game_types() if
- gt.supports_session_type(self._editcontroller.get_session_type())
+ gt for gt in get_game_types() if gt.supports_session_type(
+ self._editcontroller.get_session_type())
]
# Sort in the current language.
diff --git a/ballisticacore-cmake/.idea/dictionaries/ericf.xml b/ballisticacore-cmake/.idea/dictionaries/ericf.xml
index 6faa8fa9..a13e3a39 100644
--- a/ballisticacore-cmake/.idea/dictionaries/ericf.xml
+++ b/ballisticacore-cmake/.idea/dictionaries/ericf.xml
@@ -412,6 +412,8 @@
expbool
expectedsig
expl
+ exportlist
+ exporttypestr
extradata
extrahash
extrascale
@@ -653,6 +655,7 @@
jnames
json's
juleskie
+ kbexports
keepalives
kerploople
keyanntype
@@ -931,6 +934,7 @@
passcode
pathcapture
pathdst
+ pathlist
pathparts
pathsrc
pausable
diff --git a/src/ballistica/ballistica.cc b/src/ballistica/ballistica.cc
index 743dc92e..08c146dd 100644
--- a/src/ballistica/ballistica.cc
+++ b/src/ballistica/ballistica.cc
@@ -21,7 +21,7 @@
namespace ballistica {
// 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";
// Our standalone globals.