From 5d7c72c3659c25db5810aa91abeee2579f8e4f47 Mon Sep 17 00:00:00 2001
From: Eric Froemling This page documents the Python classes and functions in the 'ba' module,
which are the ones most relevant to modding in Ballistica. If you come across something you feel should be included here or could be better explained, please let me know. Happy modding!last updated on 2020-05-31 for Ballistica version 1.5.0 build 20036
+last updated on 2020-06-01 for Ballistica version 1.5.0 build 20037
@@ -1079,8 +1079,8 @@ manually.
create_default_game_config_ui(self, gameclass: Type[ba.GameActivity], sessionclass: Type[ba.Session], config: Optional[Dict[str, Any]], completion_call: Callable[[Optional[Dict[str, Any]]], None]) -> None
+create_default_game_settings_ui(self, gameclass: Type[ba.GameActivity], sessionclass: Type[ba.Session], config: Optional[Dict[str, Any]], completion_call: Callable[[Optional[Dict[str, Any]]], None]) -> None
Launch a UI to configure the given game config.
diff --git a/tests/test_efro/test_dataclasses.py b/tests/test_efro/test_dataclasses.py index a2bf51f5..f00c4aa9 100644 --- a/tests/test_efro/test_dataclasses.py +++ b/tests/test_efro/test_dataclasses.py @@ -22,7 +22,7 @@ from __future__ import annotations -from dataclasses import dataclass +from dataclasses import dataclass, field from typing import TYPE_CHECKING import pytest @@ -30,11 +30,12 @@ import pytest from efro.dataclasses import dataclass_assign, dataclass_validate if TYPE_CHECKING: - from typing import Optional + from typing import Optional, List def test_assign() -> None: """Testing various assignments.""" + # pylint: disable=too-many-statements @dataclass class _TestClass: @@ -46,6 +47,10 @@ def test_assign() -> None: osval: Optional[str] = None obval: Optional[bool] = None ofval: Optional[float] = None + lsval: List[str] = field(default_factory=list) + lival: List[int] = field(default_factory=list) + lbval: List[bool] = field(default_factory=list) + lfval: List[float] = field(default_factory=list) tclass = _TestClass() @@ -66,24 +71,39 @@ def test_assign() -> None: dataclass_assign(tclass, {'nonexistent': 'foo'}) # Correct types. - dataclass_assign(tclass, { - 'ival': 1, - 'sval': 'foo', - 'bval': True, - 'fval': 2.0, - }) - dataclass_assign(tclass, { - 'oival': None, - 'osval': None, - 'obval': None, - 'ofval': None, - }) - dataclass_assign(tclass, { - 'oival': 1, - 'osval': 'foo', - 'obval': True, - 'ofval': 2.0, - }) + dataclass_assign( + tclass, { + 'ival': 1, + 'sval': 'foo', + 'bval': True, + 'fval': 2.0, + 'lsval': ['foo'], + 'lival': [10], + 'lbval': [False], + 'lfval': [1.0] + }) + dataclass_assign( + tclass, { + 'oival': None, + 'osval': None, + 'obval': None, + 'ofval': None, + 'lsval': [], + 'lival': [], + 'lbval': [], + 'lfval': [] + }) + dataclass_assign( + tclass, { + 'oival': 1, + 'osval': 'foo', + 'obval': True, + 'ofval': 2.0, + 'lsval': ['foo', 'bar', 'eep'], + 'lival': [10, 11, 12], + 'lbval': [False, True], + 'lfval': [1.0, 2.0, 3.0] + }) # Type mismatches. with pytest.raises(TypeError): @@ -107,6 +127,21 @@ def test_assign() -> None: with pytest.raises(TypeError): dataclass_assign(tclass, {'ofval': 'blah'}) + with pytest.raises(TypeError): + dataclass_assign(tclass, {'lsval': 'blah'}) + + with pytest.raises(TypeError): + dataclass_assign(tclass, {'lsval': [1]}) + + with pytest.raises(TypeError): + dataclass_assign(tclass, {'lbval': [None]}) + + with pytest.raises(TypeError): + dataclass_assign(tclass, {'lival': ['foo']}) + + with pytest.raises(TypeError): + dataclass_assign(tclass, {'lfval': [True]}) + # More subtle ones (we currently require EXACT type matches) with pytest.raises(TypeError): dataclass_assign(tclass, {'ival': True}) @@ -120,6 +155,9 @@ def test_assign() -> None: with pytest.raises(TypeError): dataclass_assign(tclass, {'ofval': 1}) + with pytest.raises(TypeError): + dataclass_assign(tclass, {'lfval': [1]}) + def test_validate() -> None: """Testing validation.""" diff --git a/tools/bacommon/servermanager.py b/tools/bacommon/servermanager.py index c00d2db8..afa1d87d 100644 --- a/tools/bacommon/servermanager.py +++ b/tools/bacommon/servermanager.py @@ -22,7 +22,7 @@ from __future__ import annotations from enum import Enum -from dataclasses import dataclass +from dataclasses import dataclass, field from typing import TYPE_CHECKING if TYPE_CHECKING: @@ -45,6 +45,14 @@ class ServerConfig: # be enabled unless you are hosting on a LAN with no internet connection. authenticate_clients: bool = True + # IDs of server admins. Server admins are not kickable through the default + # kick vote system and they are able to kick players without a vote. To get + # your account id, enter 'getaccountid' in settings->advanced->enter-code. + admins: List[str] = field(default_factory=list) + + # Whether the default kick-voting system is enabled. + enable_default_kick_voting: bool = True + # UDP port to host on. Change this to work around firewalls or run multiple # servers on one machine. # 43210 is the default and the only port that will show up in the LAN diff --git a/tools/batools/build.py b/tools/batools/build.py index db1c366e..c4fa9561 100644 --- a/tools/batools/build.py +++ b/tools/batools/build.py @@ -33,7 +33,7 @@ from typing import TYPE_CHECKING from efro.terminal import Clr if TYPE_CHECKING: - from typing import List, Sequence, Optional + from typing import List, Sequence, Optional, Any # Python pip packages we require for this project. @@ -612,7 +612,11 @@ def _get_server_config_template_yaml(projroot: str) -> str: assert vname.endswith(':') vname = vname[:-1] assert veq == '=' - vval = eval(vval_raw) # pylint: disable=eval-used + vval: Any + if vval_raw == 'field(default_factory=list)': + vval = [] + else: + vval = eval(vval_raw) # pylint: disable=eval-used # Filter/override a few things. if vname == 'playlist_code': @@ -621,7 +625,13 @@ def _get_server_config_template_yaml(projroot: str) -> str: elif vname == 'stats_url': vval = ('https://mystatssite.com/' 'showstats?player=${ACCOUNT}') - lines_out.append('#' + yaml.dump({vname: vval}).strip()) + elif vname == 'admins': + vval = ['pb-yOuRAccOuNtIdHErE', 'pb-aNdMayBeAnotherHeRE'] + lines_out += [ + '#' + l for l in yaml.dump({ + vname: vval + }).strip().splitlines() + ] else: # Convert comments referring to python bools to yaml bools. line = line.replace('True', 'true').replace('False', 'false') diff --git a/tools/efro/dataclasses.py b/tools/efro/dataclasses.py index c313fb2f..38edc2d2 100644 --- a/tools/efro/dataclasses.py +++ b/tools/efro/dataclasses.py @@ -30,9 +30,9 @@ if TYPE_CHECKING: # For fields with these string types, we require a passed value's type # to exactly match one of the tuple values to consider the assignment valid. _SIMPLE_ASSIGN_TYPES: Dict[str, Tuple[Type, ...]] = { - 'bool': (bool, ), - 'str': (str, ), 'int': (int, ), + 'str': (str, ), + 'bool': (bool, ), 'float': (float, ), 'Optional[int]': (int, type(None)), 'Optional[str]': (str, type(None)), @@ -40,6 +40,13 @@ _SIMPLE_ASSIGN_TYPES: Dict[str, Tuple[Type, ...]] = { 'Optional[float]': (float, type(None)), } +_LIST_ASSIGN_TYPES: Dict[str, Tuple[Type, ...]] = { + 'List[int]': (int, ), + 'List[str]': (str, ), + 'List[bool]': (bool, ), + 'List[float]': (float, ), +} + def dataclass_assign(instance: Any, values: Dict[str, Any]) -> None: """Safely assign values from a dict to a dataclass instance. @@ -60,6 +67,22 @@ def dataclass_assign(instance: Any, values: Dict[str, Any]) -> None: the increased safety checks may be worth the speed tradeoff in some cases. """ + _dataclass_validate(instance, values) + for key, value in values.items(): + setattr(instance, key, value) + + +def dataclass_validate(instance: Any) -> None: + """Ensure values in a dataclass are correct types. + + Note that this will always fail if a dataclass contains field types + not supported by this module. + """ + _dataclass_validate(instance, dataclasses.asdict(instance)) + + +def _dataclass_validate(instance: Any, values: Dict[str, Any]) -> None: + # pylint: disable=too-many-branches if not dataclasses.is_dataclass(instance): raise TypeError(f'Passed instance {instance} is not a dataclass.') if not isinstance(values, dict): @@ -82,10 +105,10 @@ def dataclass_assign(instance: Any, values: Dict[str, Any]) -> None: f' been created without "from __future__ import annotations";' f' those dataclasses are unsupported here.') - reqtypes = _SIMPLE_ASSIGN_TYPES.get(fieldtype) - if reqtypes is not None: - # pylint: disable=unidiomatic-typecheck - if not any(type(value) is t for t in reqtypes): + if fieldtype in _SIMPLE_ASSIGN_TYPES: + reqtypes = _SIMPLE_ASSIGN_TYPES[fieldtype] + valuetype = type(value) + if not any(valuetype is t for t in reqtypes): if len(reqtypes) == 1: expected = reqtypes[0].__name__ else: @@ -93,23 +116,25 @@ def dataclass_assign(instance: Any, values: Dict[str, Any]) -> None: expected = f'Union[{names}]' raise TypeError(f'Invalid value type for "{key}";' f' expected "{expected}", got' - f' "{type(value).__name__}".') + f' "{valuetype.__name__}".') + + elif fieldtype in _LIST_ASSIGN_TYPES: + reqtypes = _LIST_ASSIGN_TYPES[fieldtype] + if not isinstance(value, list): + raise TypeError( + f'Invalid value for "{key}";' + f' expected a list, got a "{type(value).__name__}"') + for subvalue in value: + subvaluetype = type(subvalue) + if not any(subvaluetype is t for t in reqtypes): + if len(reqtypes) == 1: + expected = reqtypes[0].__name__ + else: + names = ', '.join(t.__name__ for t in reqtypes) + expected = f'Union[{names}]' + raise TypeError(f'Invalid value type for "{key}";' + f' expected list of "{expected}", found' + f' "{subvaluetype.__name__}".') + else: raise TypeError(f'Field type "{fieldtype}" is unsupported here.') - - # Ok, if we made it here, the value is kosher. Do the assign. - setattr(instance, key, value) - - -def dataclass_validate(instance: Any) -> None: - """Ensure values in a dataclass are correct types. - - Note that this will always fail if a dataclass contains field types - not supported by this module. - """ - # We currently simply operate by grabbing dataclass values as a dict - # and passing them through dataclass_assign(). - # It would be possible to write slightly more efficient custom code, - # this, but this keeps things simple and will allow us to easily - # incorporate things like value coercion later if we add that. - dataclass_assign(instance, dataclasses.asdict(instance)) diff --git a/tools/efrotools/code.py b/tools/efrotools/code.py index bc6a4eb7..aea849f8 100644 --- a/tools/efrotools/code.py +++ b/tools/efrotools/code.py @@ -82,15 +82,19 @@ def formatcode(projroot: Path, full: bool) -> None: def cpplint(projroot: Path, full: bool) -> None: """Run lint-checking on all code deemed lint-able.""" + # pylint: disable=too-many-locals + import tempfile from concurrent.futures import ThreadPoolExecutor from multiprocessing import cpu_count from efrotools import get_config from efro.terminal import Clr + from efro.error import CleanError os.chdir(projroot) filenames = get_code_filenames(projroot) - if any(' ' in name for name in filenames): - raise Exception('found space in path; unexpected') + for fpath in filenames: + if ' ' in fpath: + raise Exception(f'Found space in path {fpath}; unexpected.') # Check the config for a list of ones to ignore. code_blacklist: List[str] = get_config(projroot).get( @@ -114,14 +118,47 @@ def cpplint(projroot: Path, full: bool) -> None: print(f'{Clr.BLU}CppLint checking' f' {len(dirtyfiles)} file(s)...{Clr.RST}') - def lint_file(filename: str) -> None: - result = subprocess.call(['cpplint', '--root=src', filename]) - if result != 0: - raise Exception(f'Linting failed for {filename}') + # We want to do a few custom modifications to the cpplint module... + try: + import cpplint as cpplintmodule + except Exception: + raise CleanError('Unable to import cpplint') + with open(cpplintmodule.__file__) as infile: + codelines = infile.read().splitlines() + cheadersline = codelines.index('_C_HEADERS = frozenset([') - with ThreadPoolExecutor(max_workers=cpu_count()) as executor: - # Converting this to a list will propagate any errors. - list(executor.map(lint_file, dirtyfiles)) + # Extra headers we consider as valid C system headers. + c_headers = [ + 'malloc.h', 'tchar.h', 'jni.h', 'android/log.h', 'EGL/egl.h', + 'libgen.h', 'linux/netlink.h', 'linux/rtnetlink.h', 'android/bitmap.h', + 'android/log.h', 'uuid/uuid.h', 'cxxabi.h', 'direct.h', 'shellapi.h', + 'rpc.h', 'io.h' + ] + codelines.insert(cheadersline + 1, ''.join(f"'{h}'," for h in c_headers)) + + # Skip unapproved C++ headers check (it flags