added load_exported_classes function to meta system

This commit is contained in:
Eric Froemling 2022-08-11 16:06:15 -07:00
parent 0a25b07ec8
commit 9c63b8b4f9
No known key found for this signature in database
GPG Key ID: 89C93F0F8D6D5A98
16 changed files with 160 additions and 94 deletions

View File

@ -3995,26 +3995,26 @@
"assets/src/ba_data/python/ba/_generated/__init__.py": "https://files.ballistica.net/cache/ba1/ee/e8/cad05aa531c7faf7ff7b96db7f6e",
"assets/src/ba_data/python/ba/_generated/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/16/55/a14a247c0db04420117a8ed371f6",
"build/prefab/full/linux_arm64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/00/ce/c04891665cbcfbdcd40befef320e",
"build/prefab/full/linux_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/b9/47/7ec5ce0741537a0a63bb812519e9",
"build/prefab/full/linux_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/21/67/16065d61c11709286cbf58a8c1d3",
"build/prefab/full/linux_x86_64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/ef/0a/ac07d9d822e90baa8f7342041017",
"build/prefab/full/linux_x86_64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/f6/55/75ec6378098ac6c398432c4b2abc",
"build/prefab/full/linux_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/11/b0/2e4b8b67b4ed827e6c70c5d4566a",
"build/prefab/full/linux_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/c2/fd/849f092a3abf28486999b5393552",
"build/prefab/full/mac_arm64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/05/91/4ebfc45bc0b5da7384ae8f93c921",
"build/prefab/full/mac_arm64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/09/d7/12317ddd52d8c59ed93423aaed46",
"build/prefab/full/mac_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/16/b6/2f8c82eb93716e8aaad8613a2b6a",
"build/prefab/full/mac_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/b1/17/2893e78c6482b5d0e8cd3c267f41",
"build/prefab/full/mac_x86_64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/71/18/ca3952e50e1b4961dac32174daef",
"build/prefab/full/mac_x86_64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/cf/85/5e1fb710aa4eabbf4d54d2fd5e43",
"build/prefab/full/mac_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/ad/c9/9fa020ee678e12074f5d1f848055",
"build/prefab/full/mac_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/25/fe/5084d63800be13121bbce5e2b718",
"build/prefab/full/windows_x86_gui/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/67/52/13872d8fedb93a23be3931ab4ac8",
"build/prefab/full/windows_x86_gui/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/82/7a/0eff468145e4ae32e9de2f564ede",
"build/prefab/full/windows_x86_server/debug/dist/BallisticaCoreHeadless.exe": "https://files.ballistica.net/cache/ba1/7f/1b/4853e57aa94b004b8dd301ac0482",
"build/prefab/full/windows_x86_server/release/dist/BallisticaCoreHeadless.exe": "https://files.ballistica.net/cache/ba1/20/e6/9f3058021a438a944da4511fce3f",
"build/prefab/full/linux_arm64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/c7/0e/47a1cc6413aaad0c0afe0884b716",
"build/prefab/full/linux_arm64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/53/6f/5daf49965bace69f17f7b4ed2e55",
"build/prefab/full/linux_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/9a/35/4e8366fc775df35f7633d7a224ae",
"build/prefab/full/linux_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/93/f2/0d4333951f150ea0aadfeaaae852",
"build/prefab/full/linux_x86_64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/e7/01/2bec5299c6ff6bc9722f51b7839e",
"build/prefab/full/linux_x86_64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/b3/70/90c234a390e49a1eff61a0de9eca",
"build/prefab/full/linux_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/2d/33/2ef1276ec090ed01884623346dd7",
"build/prefab/full/linux_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/49/21/89e770f7339c8fa9782c7280c0af",
"build/prefab/full/mac_arm64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/62/15/699580dbe1d45625e063e056a547",
"build/prefab/full/mac_arm64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/4b/45/d857357c0c226d768434ade291d3",
"build/prefab/full/mac_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/f5/53/e0a72c39bbc8980f6cf3309e8074",
"build/prefab/full/mac_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/8b/6d/7c315ab5f44211be0f025d7b2c68",
"build/prefab/full/mac_x86_64_gui/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/32/ad/5f826af4b9773a900b2709298943",
"build/prefab/full/mac_x86_64_gui/release/ballisticacore": "https://files.ballistica.net/cache/ba1/82/31/94fef121e7a83af30760e550d800",
"build/prefab/full/mac_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/9a/12/e77b1a75fd08a625755d862700c4",
"build/prefab/full/mac_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/3d/4a/a0a222d942d7b36d4a58325c3f95",
"build/prefab/full/windows_x86_gui/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/50/e8/001aa174b93d5ee0f076390add7a",
"build/prefab/full/windows_x86_gui/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/15/2d/f1bc569de45c97fd5c1a769a4cb2",
"build/prefab/full/windows_x86_server/debug/dist/BallisticaCoreHeadless.exe": "https://files.ballistica.net/cache/ba1/14/4f/6729d832b56076aeeb5b62db0b18",
"build/prefab/full/windows_x86_server/release/dist/BallisticaCoreHeadless.exe": "https://files.ballistica.net/cache/ba1/fc/ac/fdeac05ecc7a4ff5d4a0e33b4cd4",
"build/prefab/lib/linux_arm64_gui/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/13/6a/baff53fa7c286e51469b0b32f05b",
"build/prefab/lib/linux_arm64_gui/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/57/24/9f56b62f3d49ffcab8813e155311",
"build/prefab/lib/linux_arm64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/db/15/191884a40ea1128a47058b19da6b",

View File

@ -1,3 +1,9 @@
### 1.7.7 (build 20690, api 7, 2022-08-11)
- added `ba.app.meta.load_exported_classes()` for loading classes discovered by the meta subsystem cleanly in a background thread
- improved logging of missing playlist game types
- some ba.Lstr functionality can now be used in background threads
### 1.7.6 (build 20687, api 7, 2022-08-11)
- Cleaned up da 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.

View File

@ -1 +1 @@
105185559858403296763219852162674470262
58062653605658487107918437258195542763

View File

@ -399,24 +399,3 @@ 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]

View File

@ -12,6 +12,7 @@ from pathlib import Path
from typing import TYPE_CHECKING, TypeVar
from dataclasses import dataclass, field
from efro.call import tpartial
import _ba
if TYPE_CHECKING:
@ -81,23 +82,73 @@ class MetadataSubsystem:
self._scan = DirectoryScan(
[_ba.app.python_directory_app, _ba.app.python_directory_user])
Thread(target=self._do_scan_dirs, daemon=True).start()
Thread(target=self._run_scan_in_bg, daemon=True).start()
def start_extra_scan(self) -> None:
"""Provide extra dirs to be scanned (namely Workspace dirs).
"""Proceed to the extra_scan_dirs portion of the scan.
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.
This is for parts of the scan that must be delayed until
workspace sync completion or other such events. This must be
called exactly once.
"""
assert self._scan is not None
self._scan.set_extras(self.extra_scan_dirs)
def wait_for_scan_results(self) -> ScanResults:
def load_exported_classes(
self,
cls: type[T],
completion_cb: Callable[[list[type[T]]], None],
completion_cb_in_bg_thread: bool = False,
) -> None:
"""High level function to load meta-exported classes.
Will wait for scanning to complete if necessary, and will load all
registered classes of a particular type in a background thread before
calling the passed callback in the logic thread. Errors may be logged
to messaged to the user in some way but the callback will be called
regardless.
To run the completion callback directly in the bg thread where the
loading work happens, pass completion_cb_in_bg_thread=True.
"""
Thread(
target=tpartial(self._load_exported_classes, cls, completion_cb,
completion_cb_in_bg_thread),
daemon=True,
).start()
def _load_exported_classes(
self,
cls: type[T],
completion_cb: Callable[[list[type[T]]], None],
completion_cb_in_bg_thread: bool,
) -> None:
from ba._general import getclass
classes: list[type[T]] = []
try:
classnames = self._wait_for_scan_results().exports_of_class(cls)
for classname in classnames:
try:
classes.append(getclass(classname, cls))
except Exception:
logging.exception('error importing %s', classname)
except Exception:
logging.exception('Error loading exported classes.')
completion_call = tpartial(completion_cb, classes)
if completion_cb_in_bg_thread:
completion_call()
else:
_ba.pushcall(completion_call, from_other_thread=True)
def _wait_for_scan_results(self) -> ScanResults:
"""Return scan results, blocking if the scan is not yet complete."""
if self.scanresults is None:
logging.warning('ba.meta.wait_for_scan_results()'
' called before scan completed;'
' this can cause hitches.')
if _ba.in_game_thread():
logging.warning(
'ba.meta._wait_for_scan_results()'
' called in logic thread before scan completed;'
' this can cause hitches.')
# Now wait a bit for the scan to complete.
# Eventually error though if it doesn't.
@ -109,6 +160,20 @@ class MetadataSubsystem:
'timeout waiting for meta scan to complete.')
return self.scanresults
def _run_scan_in_bg(self) -> None:
"""Runs a scan (for use in background thread)."""
try:
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}'])
# Place results and tell the logic thread they're ready.
self.scanresults = results
_ba.pushcall(self._handle_scan_results, from_other_thread=True)
def _handle_scan_results(self) -> None:
"""Called in the logic thread with results of a completed scan."""
from ba._language import Lstr
@ -139,20 +204,6 @@ class MetadataSubsystem:
assert self._scan_complete_cb is not None
self._scan_complete_cb()
def _do_scan_dirs(self) -> None:
"""Runs a scan (for use in background thread)."""
try:
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}'])
# 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:
"""Scans directories for metadata."""

View File

@ -94,9 +94,11 @@ class MultiTeamSession(Session):
playlist = _playlist.get_default_free_for_all_playlist()
# Resolve types and whatnot to get our final playlist.
playlist_resolved = _playlist.filter_playlist(playlist,
sessiontype=type(self),
add_resolved_type=True)
playlist_resolved = _playlist.filter_playlist(
playlist,
sessiontype=type(self),
add_resolved_type=True,
name='default teams' if self.use_teams else 'default ffa')
if not playlist_resolved:
raise RuntimeError('Playlist contains no valid games.')

View File

@ -18,7 +18,8 @@ def filter_playlist(playlist: PlaylistType,
sessiontype: type[_session.Session],
add_resolved_type: bool = False,
remove_unowned: bool = True,
mark_unowned: bool = False) -> PlaylistType:
mark_unowned: bool = False,
name: str = '?') -> PlaylistType:
"""Return a filtered version of a playlist.
Strips out or replaces invalid or unowned game types, makes sure all
@ -139,7 +140,7 @@ def filter_playlist(playlist: PlaylistType,
entry['settings'][setting.name] = setting.default
goodlist.append(entry)
except ImportError as exc:
_ba.log(f'Import failed while scanning playlist: {exc}',
_ba.log(f'Import failed while scanning playlist \'{name}\': {exc}',
to_server=False)
except Exception:
from ba import _error

View File

@ -36,7 +36,7 @@ from ba._store import (get_available_sale_time, get_available_purchase_count,
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, get_game_types
from ba._gameutils import get_trophy_string
__all__ = [
'get_unowned_maps', 'get_unowned_game_types', 'get_map_class',
@ -56,6 +56,5 @@ __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_game_types'
'get_clean_price', 'get_tournament_prize_strings', 'get_trophy_string'
]

View File

@ -184,7 +184,9 @@ class PrivateGatherTab(GatherTab):
if playlist is None:
playlist = pvars.get_default_list_call()
hcfg.playlist = filter_playlist(playlist, sessiontype)
hcfg.playlist = filter_playlist(playlist,
sessiontype,
name=playlist_name)
randomize = cfg.get(f'{pvars.config_name} Playlist Randomize')
if not isinstance(randomize, bool):

View File

@ -116,10 +116,39 @@ class PlaylistAddGameWindow(ba.Window):
ba.containerwidget(edit=self._root_widget,
selected_child=self._scrollwidget)
self._game_types: list[type[ba.GameActivity]] = []
# Get actual games loading in the bg.
ba.app.meta.load_exported_classes(ba.GameActivity,
self._on_game_types_loaded,
completion_cb_in_bg_thread=True)
# Refresh with our initial empty list. We'll refresh again once
# game loading is complete.
self._refresh()
def _on_game_types_loaded(self,
gametypes: list[type[ba.GameActivity]]) -> None:
from ba.internal import get_unowned_game_types
# We asked for a bg thread completion cb so we can do some
# filtering here in the bg thread where its not gonna cause hitches.
assert not _ba.in_game_thread()
sessiontype = self._editcontroller.get_session_type()
unowned = get_unowned_game_types()
self._game_types = [
gt for gt in gametypes
if gt not in unowned and gt.supports_session_type(sessiontype)
]
# Sort in the current language.
self._game_types.sort(key=lambda g: g.get_display_string().evaluate())
# Tell ourself to refresh back in the logic thread.
ba.pushcall(self._refresh, from_other_thread=True)
def _refresh(self, select_get_more_games_button: bool = False) -> None:
from ba.internal import get_game_types
# from ba.internal import get_game_types
if self._column is not None:
self._column.delete()
@ -128,15 +157,7 @@ class PlaylistAddGameWindow(ba.Window):
border=2,
margin=0)
gametypes = [
gt for gt in get_game_types() if gt.supports_session_type(
self._editcontroller.get_session_type())
]
# Sort in the current language.
gametypes.sort(key=lambda g: g.get_display_string().evaluate())
for i, gametype in enumerate(gametypes):
for i, gametype in enumerate(self._game_types):
def _doit() -> None:
if self._select_button:

View File

@ -429,7 +429,8 @@ class PlaylistBrowserWindow(ba.Window):
playlist = filter_playlist(playlist,
self._sessiontype,
remove_unowned=False,
mark_unowned=True)
mark_unowned=True,
name=name)
for entry in playlist:
mapname = entry['settings']['map']
maptype: type[ba.Map] | None

View File

@ -52,7 +52,8 @@ class PlaylistEditController:
appconfig[self._pvars.config_name +
' Playlists'][existing_playlist_name],
sessiontype=sessiontype,
remove_unowned=False)
remove_unowned=False,
name=existing_playlist_name)
self._edit_ui_selection = None
else:
if playlist is not None:

View File

@ -86,7 +86,8 @@ class PlayOptionsWindow(popup.PopupWindow):
plst = filter_playlist(plst,
self._sessiontype,
remove_unowned=False,
mark_unowned=True)
mark_unowned=True,
name=name)
game_count = len(plst)
for entry in plst:
mapname = entry['settings']['map']

View File

@ -21,8 +21,8 @@
namespace ballistica {
// These are set automatically via script; don't modify them here.
const int kAppBuildNumber = 20687;
const char* kAppVersion = "1.7.6";
const int kAppBuildNumber = 20690;
const char* kAppVersion = "1.7.7";
// Our standalone globals.
// These are separated out for easy access.

View File

@ -1569,7 +1569,10 @@ void Game::SetLanguageKeys(
}
auto DoCompileResourceString(cJSON* obj) -> std::string {
assert(InGameThread());
// NOTE: We currently talk to Python here so need to be sure
// we're holding the GIL. Perhaps in the future we could handle this
// stuff completely in C++ and be free of this limitation.
assert(Python::HaveGIL());
assert(obj != nullptr);
std::string result;
@ -1707,8 +1710,8 @@ auto DoCompileResourceString(cJSON* obj) -> std::string {
if (subs->type != cJSON_Array) {
throw Exception("expected an array for 'subs'");
}
int subsCount = cJSON_GetArraySize(subs);
for (int i = 0; i < subsCount; i++) {
int subs_count = cJSON_GetArraySize(subs);
for (int i = 0; i < subs_count; i++) {
cJSON* sub = cJSON_GetArrayItem(subs, i);
if (sub->type != cJSON_Array || cJSON_GetArraySize(sub) != 2) {
throw Exception(
@ -1753,7 +1756,6 @@ auto DoCompileResourceString(cJSON* obj) -> std::string {
auto Game::CompileResourceString(const std::string& s, const std::string& loc,
bool* valid) -> std::string {
assert(InGameThread());
assert(g_python != nullptr);
bool dummyvalid;

View File

@ -1078,7 +1078,7 @@ Python::~Python() { Reset(false); }
auto Python::GetResource(const char* key, const char* fallback_resource,
const char* fallback_value) -> std::string {
assert(InGameThread());
assert(HaveGIL());
PythonRef results;
BA_PRECONDITION(key != nullptr);
const PythonRef& get_resource_call(obj(ObjID::kGetResourceCall));
@ -1130,7 +1130,7 @@ auto Python::GetResource(const char* key, const char* fallback_resource,
auto Python::GetTranslation(const char* category, const char* s)
-> std::string {
assert(InGameThread());
assert(HaveGIL());
PythonRef results;
PythonRef args(Py_BuildValue("(ss)", category, s), PythonRef::kSteal);
// Don't print errors.