mirror of
https://github.com/RYDE-WORK/ballistica.git
synced 2026-01-19 21:37:57 +08:00
meta improvements
This commit is contained in:
parent
ee5f5eef1d
commit
92ab8db91f
@ -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"
|
||||
}
|
||||
4
.idea/dictionaries/ericf.xml
generated
4
.idea/dictionaries/ericf.xml
generated
@ -799,9 +799,11 @@
|
||||
<w>expectedsig</w>
|
||||
<w>explodable</w>
|
||||
<w>explodey</w>
|
||||
<w>exportlist</w>
|
||||
<w>exportoptions</w>
|
||||
<w>exportoptionspath</w>
|
||||
<w>exporttype</w>
|
||||
<w>exporttypestr</w>
|
||||
<w>extradata</w>
|
||||
<w>extraflagmat</w>
|
||||
<w>extrahash</w>
|
||||
@ -1266,6 +1268,7 @@
|
||||
<w>jsonutils</w>
|
||||
<w>juleskie</w>
|
||||
<w>kbclass</w>
|
||||
<w>kbexports</w>
|
||||
<w>kbytecount</w>
|
||||
<w>keepalive</w>
|
||||
<w>keepalives</w>
|
||||
@ -1782,6 +1785,7 @@
|
||||
<w>pathcapture</w>
|
||||
<w>pathdst</w>
|
||||
<w>pathlib</w>
|
||||
<w>pathlist</w>
|
||||
<w>pathnames</w>
|
||||
<w>pathparts</w>
|
||||
<w>pathsrc</w>
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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.
|
||||
|
||||
|
||||
@ -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 <NUM>" lines found;'
|
||||
' ignoring module.\n')
|
||||
self.results.warnings.append(
|
||||
f'Warning: {subpath}: multiple'
|
||||
' "# ba_meta require api <NUM>" 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 <NUM>" line found;'
|
||||
' ignoring module.\n')
|
||||
self.results.warnings.append(
|
||||
f'Warning: {subpath}:'
|
||||
' no valid "# ba_meta require api <NUM>" line found;'
|
||||
' ignoring module.')
|
||||
return None
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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'
|
||||
]
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -412,6 +412,8 @@
|
||||
<w>expbool</w>
|
||||
<w>expectedsig</w>
|
||||
<w>expl</w>
|
||||
<w>exportlist</w>
|
||||
<w>exporttypestr</w>
|
||||
<w>extradata</w>
|
||||
<w>extrahash</w>
|
||||
<w>extrascale</w>
|
||||
@ -653,6 +655,7 @@
|
||||
<w>jnames</w>
|
||||
<w>json's</w>
|
||||
<w>juleskie</w>
|
||||
<w>kbexports</w>
|
||||
<w>keepalives</w>
|
||||
<w>kerploople</w>
|
||||
<w>keyanntype</w>
|
||||
@ -931,6 +934,7 @@
|
||||
<w>passcode</w>
|
||||
<w>pathcapture</w>
|
||||
<w>pathdst</w>
|
||||
<w>pathlist</w>
|
||||
<w>pathparts</w>
|
||||
<w>pathsrc</w>
|
||||
<w>pausable</w>
|
||||
|
||||
@ -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.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user