From 4b3ecba38d72df9eb07bb2506f15cd75b35b97b7 Mon Sep 17 00:00:00 2001 From: Eric Froemling Date: Wed, 5 May 2021 14:11:16 -0500 Subject: [PATCH] v1.6 --- .efrocachemap | 56 +++++++++---------- .idea/dictionaries/ericf.xml | 4 ++ CHANGELOG.md | 7 ++- assets/src/ba_data/python/ba/_servermode.py | 23 ++++++++ assets/src/ba_data/python/ba/_session.py | 13 ++++- .../python/bastd/ui/gather/privatetab.py | 32 +++++++++-- assets/src/ba_data/python/bastd/ui/party.py | 2 +- assets/src/server/ballisticacore_server.py | 26 +++++++-- .../.idea/dictionaries/ericf.xml | 4 ++ docs/ba_module.md | 2 +- src/ballistica/ballistica.cc | 2 +- src/ballistica/input/std_input_module.cc | 12 ++-- src/ballistica/input/std_input_module.h | 3 + tests/test_efro/test_dataclassio.py | 47 ++++++++-------- tools/bacommon/net.py | 17 +++--- tools/bacommon/servermanager.py | 37 ++++++++---- tools/batools/build.py | 28 +++++++++- tools/efro/dataclassio.py | 52 ++++++++--------- tools/efrotools/pylintplugins.py | 38 ++++++++++++- 19 files changed, 279 insertions(+), 126 deletions(-) diff --git a/.efrocachemap b/.efrocachemap index 04d00178..ac8b4d5f 100644 --- a/.efrocachemap +++ b/.efrocachemap @@ -3932,26 +3932,26 @@ "assets/build/windows/Win32/ucrtbased.dll": "https://files.ballistica.net/cache/ba1/b5/85/f8b6d0558ddb87267f34254b1450", "assets/build/windows/Win32/vc_redist.x86.exe": "https://files.ballistica.net/cache/ba1/1c/e1/4a1a2eddda2f4aebd5f8b64ab08e", "assets/build/windows/Win32/vcruntime140d.dll": "https://files.ballistica.net/cache/ba1/50/8d/bc2600ac9491f1b14d659709451f", - "build/prefab/full/linux_arm64/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/75/4c/8525e8379d532892f3210bd7c007", - "build/prefab/full/linux_arm64/release/ballisticacore": "https://files.ballistica.net/cache/ba1/f1/5d/1e6f8c35114957c82a4b3a56b6a4", - "build/prefab/full/linux_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/22/e2/522fd2f2cc6fd0e387550f2cbe0f", - "build/prefab/full/linux_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/8b/e7/2fba831a7c3e3fb4c8b63ca0423f", - "build/prefab/full/linux_x86_64/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/34/bd/4c971cfc7b576c58d904a63d86a1", - "build/prefab/full/linux_x86_64/release/ballisticacore": "https://files.ballistica.net/cache/ba1/8b/8b/b8f8a75b3ded113231265f61da9d", - "build/prefab/full/linux_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/14/4e/bd10863753f44c7612ef697c4693", - "build/prefab/full/linux_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/34/a0/883c84cb130780bb8bc8a2185604", - "build/prefab/full/mac_arm64/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/e0/ee/e6b94bf4149530e412cfe27e5cb4", - "build/prefab/full/mac_arm64/release/ballisticacore": "https://files.ballistica.net/cache/ba1/97/62/0944cd3ab34d681cd7c9bfaa0b11", - "build/prefab/full/mac_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/dd/a5/c73d18aba833987ff3e713bbf981", - "build/prefab/full/mac_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/27/e1/de75aac52e10bebae81fc12aa030", - "build/prefab/full/mac_x86_64/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/a6/81/ee957a5bfd1be45c0e865f8a27ba", - "build/prefab/full/mac_x86_64/release/ballisticacore": "https://files.ballistica.net/cache/ba1/03/29/abffbfc56fd981915253f1d4ed96", - "build/prefab/full/mac_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/04/35/104446e5f91b9fe35fa413be02ca", - "build/prefab/full/mac_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/ed/c0/68b44a693639308981b5214f02c1", - "build/prefab/full/windows_x86/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/b5/c4/5935bc59cc237c42e8ac9be47ce5", - "build/prefab/full/windows_x86/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/35/3d/f04ebd4a7d066088595b8ed4bbc5", - "build/prefab/full/windows_x86_server/debug/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/72/eb/25cf435771601aeec273a92ffb50", - "build/prefab/full/windows_x86_server/release/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/45/fb/166e932a6235613935ccf2e51d00", + "build/prefab/full/linux_arm64/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/7f/59/a431e0217c157c7077241df2c365", + "build/prefab/full/linux_arm64/release/ballisticacore": "https://files.ballistica.net/cache/ba1/1d/6a/68e8714a86171f9a2be4a036491e", + "build/prefab/full/linux_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/ed/ed/1387a6c9ce1e0cea307f6ab095e7", + "build/prefab/full/linux_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/66/f8/636eeec7e4b5b1b753820dd22c04", + "build/prefab/full/linux_x86_64/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/3d/17/07ac1ff591b036ccbe3262fed5f5", + "build/prefab/full/linux_x86_64/release/ballisticacore": "https://files.ballistica.net/cache/ba1/60/e9/5fc4a69d21eba0e499940ebbc7fb", + "build/prefab/full/linux_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/43/a0/4fb77c76baaa300b3689c67bd772", + "build/prefab/full/linux_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/ac/48/53ae96cef471c3c3754ff3100636", + "build/prefab/full/mac_arm64/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/c2/df/31db000cc83b10b2202ba95919d7", + "build/prefab/full/mac_arm64/release/ballisticacore": "https://files.ballistica.net/cache/ba1/70/15/10385cf2b4c90e344ed6f5c083d1", + "build/prefab/full/mac_arm64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/fd/ee/c73314b7dbb23db05e64837c11a0", + "build/prefab/full/mac_arm64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/cf/09/c64df4e9c9eb2dddf2f60d735042", + "build/prefab/full/mac_x86_64/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/47/51/18a5f474d0a877b72389a435f3ec", + "build/prefab/full/mac_x86_64/release/ballisticacore": "https://files.ballistica.net/cache/ba1/e7/6c/81eeee17d5b116b12089bc7bb0f9", + "build/prefab/full/mac_x86_64_server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/35/11/985c2fcc38cc8c149fbc03352a0f", + "build/prefab/full/mac_x86_64_server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/5d/22/586c8cfed938a04a609a5b95945a", + "build/prefab/full/windows_x86/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/6c/e2/4dfb793c1a03adf694bde2c6f701", + "build/prefab/full/windows_x86/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/10/56/1bd7d8a57b9fdfd29a0be99c92ba", + "build/prefab/full/windows_x86_server/debug/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/f6/47/2398d24c31a2ebc0de61114eca55", + "build/prefab/full/windows_x86_server/release/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/52/2f/989db5f34be56b1950abe7b0485f", "build/prefab/lib/linux_arm64/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/f4/59/16c01f646c16bb480a197aa9e6e3", "build/prefab/lib/linux_arm64/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/81/73/a321fe4aa721d07f2a44257967cf", "build/prefab/lib/linux_arm64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/99/99/2adf5a22923eed3f8b5667ee8220", @@ -3960,12 +3960,12 @@ "build/prefab/lib/linux_x86_64/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/3d/3a/c747993afbf4c1ed1c5e8b0e5d5a", "build/prefab/lib/linux_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/46/21/31fe300afbf7d9da766c04064919", "build/prefab/lib/linux_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/f8/bd/2b05dbfd98cd55cedf924b639765", - "build/prefab/lib/mac_arm64/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/5b/6d/877a5a015eb3d705795a2db7fa74", - "build/prefab/lib/mac_arm64/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/58/a4/3d2a1e4a47c51e883ef29b5e280d", - "build/prefab/lib/mac_arm64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/a7/ca/91cfd7dc2ccc8c996daf9aaeb9fd", - "build/prefab/lib/mac_arm64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/93/cb/de559dd0ea55e12a228ea1cd8974", - "build/prefab/lib/mac_x86_64/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/c9/e1/132123edad385ac0a977337f044c", - "build/prefab/lib/mac_x86_64/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/f2/e3/a82f88651f2ffef8c52a20da42a6", - "build/prefab/lib/mac_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/d4/9c/95e358c6e929bcb046e40fe5ad2b", - "build/prefab/lib/mac_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/8e/2d/81ce3feefa50c283cd7a8398ac72" + "build/prefab/lib/mac_arm64/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/8f/3f/9bc329e012cba8c7d3132f99ee3d", + "build/prefab/lib/mac_arm64/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/95/e8/61d947fe80839edf500755fde7c6", + "build/prefab/lib/mac_arm64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/1c/52/1970d5b7d7e53145443cdab9f694", + "build/prefab/lib/mac_arm64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/0f/3f/dfecf86a39bb00bb908a32f531ee", + "build/prefab/lib/mac_x86_64/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/23/fb/1e37cdfd0d67b396e526d7f404b4", + "build/prefab/lib/mac_x86_64/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/2b/7b/a189604b95bc0620d040b251d36b", + "build/prefab/lib/mac_x86_64_server/debug/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/df/f1/82e8e63feabc82d881453f00af5c", + "build/prefab/lib/mac_x86_64_server/release/libballisticacore_internal.a": "https://files.ballistica.net/cache/ba1/96/33/f76e9abe2e28429061bf0ff22cf0" } \ No newline at end of file diff --git a/.idea/dictionaries/ericf.xml b/.idea/dictionaries/ericf.xml index 7e939da7..f2ac58b7 100644 --- a/.idea/dictionaries/ericf.xml +++ b/.idea/dictionaries/ericf.xml @@ -143,6 +143,7 @@ audiobooks audioop autodesk + autodetected autogenerate autonoassets autopoint @@ -1085,6 +1086,8 @@ introspectable intstr iobj + ioprep + ioprepped ipaddress ipos iprof @@ -1712,6 +1715,7 @@ pthreads ptrans ptype + ptypename publictab pubsync pucknode diff --git a/CHANGELOG.md b/CHANGELOG.md index a13f3155..5eda29cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -### 1.6.0 (20353) +### 1.6.0 (20357) - Revamped netcode significantly. We still don't have client-prediction, but things should (hopefully) feel much lower latency now. - Added network debug graphs accessible by hitting F8. - Added private parties functionality (cloud hosted parties with associated codes making it easier to play with friends) @@ -21,6 +21,11 @@ - Plugins can now register to be called for pause, resume, and shutdown events in addition to launch - Added ba.app.state holding the overall state of the app (running, paused, etc) - renamed the efro.dataclasses module to efro.dataclassio and added significant functionality +- command-line input no longer errors on commands longer than 4k bytes. +- added show-tutorial option to the server wrapper config +- added custom-team-names option to the server wrapper config +- added custom-team-colors option to the server wrapper config +- added inline-playlist option to the server wrapper config ### 1.5.29 (20246) - Exposed ba method/class initing in public C++ layer. diff --git a/assets/src/ba_data/python/ba/_servermode.py b/assets/src/ba_data/python/ba/_servermode.py index 245fd9e0..fab3915f 100644 --- a/assets/src/ba_data/python/ba/_servermode.py +++ b/assets/src/ba_data/python/ba/_servermode.py @@ -19,6 +19,7 @@ from ba._dualteamsession import DualTeamSession if TYPE_CHECKING: from typing import Optional, Dict, Any, Type + import ba from bacommon.servermanager import ServerConfig @@ -301,6 +302,28 @@ class ServerController: print('WARNING: launch_server_session() expects to run ' 'with a signed in server account') + # If we didn't fetch a playlist but there's an inline one in the + # server-config, pull it in to the game config and use it. + if (self._config.playlist_code is None + and self._config.playlist_inline is not None): + self._playlist_name = 'ServerModePlaylist' + if sessiontype is FreeForAllSession: + ptypename = 'Free-for-All' + elif sessiontype is DualTeamSession: + ptypename = 'Team Tournament' + else: + raise RuntimeError(f'Unknown session type {sessiontype}') + + # Need to add this in a transaction instead of just setting + # it directly or it will get overwritten by the master-server. + _ba.add_transaction({ + 'type': 'ADD_PLAYLIST', + 'playlistType': ptypename, + 'playlistName': self._playlist_name, + 'playlist': self._config.playlist_inline + }) + _ba.run_transactions() + if self._first_run: curtimestr = time.strftime('%c') _ba.log( diff --git a/assets/src/ba_data/python/ba/_session.py b/assets/src/ba_data/python/ba/_session.py index 9405ec48..f35a1318 100644 --- a/assets/src/ba_data/python/ba/_session.py +++ b/assets/src/ba_data/python/ba/_session.py @@ -99,6 +99,7 @@ class Session: # pylint: disable=too-many-statements # pylint: disable=too-many-locals # pylint: disable=cyclic-import + # pylint: disable=too-many-branches from ba._lobby import Lobby from ba._stats import Stats from ba._gameactivity import GameActivity @@ -172,8 +173,16 @@ class Session: # Create static teams if we're using them. if self.use_teams: - assert team_names is not None - assert team_colors is not None + if team_names is None: + raise RuntimeError( + 'use_teams is True but team_names not provided.') + if team_colors is None: + raise RuntimeError( + 'use_teams is True but team_colors not provided.') + if len(team_colors) != len(team_names): + raise RuntimeError(f'Got {len(team_names)} team_names' + f' and {len(team_colors)} team_colors;' + f' these numbers must match.') for i, color in enumerate(team_colors): team = SessionTeam(team_id=self._next_team_id, name=GameActivity.get_team_display_string( diff --git a/assets/src/ba_data/python/bastd/ui/gather/privatetab.py b/assets/src/ba_data/python/bastd/ui/gather/privatetab.py index c87778b0..78525556 100644 --- a/assets/src/ba_data/python/bastd/ui/gather/privatetab.py +++ b/assets/src/ba_data/python/bastd/ui/gather/privatetab.py @@ -8,12 +8,12 @@ import os import copy import time from enum import Enum -from dataclasses import dataclass, asdict +from dataclasses import dataclass from typing import TYPE_CHECKING, cast import ba import _ba -from efro.dataclassio import dataclass_from_dict +from efro.dataclassio import dataclass_from_dict, dataclass_to_dict from bacommon.net import (PrivateHostingState, PrivateHostingConfig, PrivatePartyConnectResult) from bastd.ui.gather import GatherTab @@ -150,6 +150,7 @@ class PrivateGatherTab(GatherTab): return self._container def _build_hosting_config(self) -> PrivateHostingConfig: + # pylint: disable=too-many-branches from bastd.ui.playlist import PlaylistTypeVars from ba.internal import filter_playlist hcfg = PrivateHostingConfig() @@ -192,12 +193,29 @@ class PrivateGatherTab(GatherTab): tutorial = cfg.get('Show Tutorial') if not isinstance(tutorial, bool): - tutorial = False + tutorial = True hcfg.tutorial = tutorial if hcfg.session_type == 'teams': - hcfg.custom_team_names = copy.copy(cfg.get('Custom Team Names')) - hcfg.custom_team_colors = copy.copy(cfg.get('Custom Team Colors')) + ctn: Optional[List[str]] = cfg.get('Custom Team Names') + if ctn is not None: + if (isinstance(ctn, (list, tuple)) and len(ctn) == 2 + and all(isinstance(x, str) for x in ctn)): + hcfg.custom_team_names = (ctn[0], ctn[1]) + else: + print(f'Found invalid custom-team-names data: {ctn}') + + ctc: Optional[List[List[float]]] = cfg.get('Custom Team Colors') + if ctc is not None: + if (isinstance(ctc, (list, tuple)) and len(ctc) == 2 + and all(isinstance(x, (list, tuple)) for x in ctc) + and all(len(x) == 3 for x in ctc)): + hcfg.custom_team_colors = ((ctc[0][0], ctc[0][1], + ctc[0][2]), + (ctc[1][0], ctc[1][1], + ctc[1][2])) + else: + print(f'Found invalid custom-team-colors data: {ctc}') return hcfg @@ -764,6 +782,8 @@ class PrivateGatherTab(GatherTab): ba.playsound(ba.getsound('error')) return + ba.playsound(ba.getsound('click01')) + # If we're not hosting, start. if self._hostingstate.party_code is None: @@ -784,7 +804,7 @@ class PrivateGatherTab(GatherTab): _ba.add_transaction( { 'type': 'PRIVATE_PARTY_START', - 'config': asdict(self._hostingconfig), + 'config': dataclass_to_dict(self._hostingconfig), 'region_pings': ba.app.net.region_pings, }, callback=ba.WeakCall(self._hosting_state_response)) diff --git a/assets/src/ba_data/python/bastd/ui/party.py b/assets/src/ba_data/python/bastd/ui/party.py index 99fb7abd..041fc4f7 100644 --- a/assets/src/ba_data/python/bastd/ui/party.py +++ b/assets/src/ba_data/python/bastd/ui/party.py @@ -372,7 +372,7 @@ class PartyWindow(ba.Window): cfg.apply_and_commit() self._update() else: - print('unhandled popup type: ' + str(self._popup_type)) + print(f'unhandled popup type: {self._popup_type}') def popup_menu_closing(self, popup_window: popup.PopupWindow) -> None: """Called when the popup is closing.""" diff --git a/assets/src/server/ballisticacore_server.py b/assets/src/server/ballisticacore_server.py index 2944bd31..1f0ae343 100755 --- a/assets/src/server/ballisticacore_server.py +++ b/assets/src/server/ballisticacore_server.py @@ -32,14 +32,19 @@ if TYPE_CHECKING: from types import FrameType from bacommon.servermanager import ServerCommand -VERSION_STR = '1.2' +VERSION_STR = '1.3' # Version history: +# 1.3: +# Added show_tutorial config option +# Added team_names config option +# Added team_colors config option +# Added playlist_inline config option # 1.2: # Added optional --help arg -# Added --config arg for setting config path and --root for ba_root path +# Added --config arg for specifying config file and --root for ba_root path # Added noninteractive mode and --interactive/--noninteractive args to -# explicitly specify +# explicitly enable/disable it (it is autodetected by default) # Added explicit control for auto-restart: --no-auto-restart # Config file is now reloaded each time server binary is restarted; no more # need to bring down server wrapper to pick up changes @@ -413,7 +418,7 @@ class ServerManagerApp: f'{Clr.BLD}--root [path]{Clr.RST}\n' + cls._par( 'Set the ballistica root directory. This is where the server' ' binary will read and write its caches, state files,' - ' downloaded assets, etc. It needs to be a writable' + ' downloaded assets to, etc. It needs to be a writable' ' directory. If not specified, the script will use the' ' \'dist/ba_root\' directory relative to itself.') + '\n' f'{Clr.BLD}--interactive{Clr.RST}\n' @@ -648,7 +653,18 @@ class ServerManagerApp: # ballisticacore config file; the rest we pass at runtime. bincfg['Port'] = self._config.port bincfg['Auto Balance Teams'] = self._config.auto_balance_teams - bincfg['Show Tutorial'] = False + bincfg['Show Tutorial'] = self._config.show_tutorial + + if self._config.team_names is not None: + bincfg['Custom Team Names'] = self._config.team_names + elif 'Custom Team Names' in bincfg: + del bincfg['Custom Team Names'] + + if self._config.team_colors is not None: + bincfg['Custom Team Colors'] = self._config.team_colors + elif 'Custom Team Colors' in bincfg: + del bincfg['Custom Team Colors'] + bincfg['Idle Exit Minutes'] = self._config.idle_exit_minutes with open(cfgpath, 'w') as outfile: outfile.write(json.dumps(bincfg)) diff --git a/ballisticacore-cmake/.idea/dictionaries/ericf.xml b/ballisticacore-cmake/.idea/dictionaries/ericf.xml index c416647c..f9e0c68c 100644 --- a/ballisticacore-cmake/.idea/dictionaries/ericf.xml +++ b/ballisticacore-cmake/.idea/dictionaries/ericf.xml @@ -57,6 +57,7 @@ athome attrobj audiocache + autodetected automagically autoselect availmins @@ -485,6 +486,8 @@ intstr invote iobj + ioprep + ioprepped iserverget iserverput isinst @@ -758,6 +761,7 @@ pton ptrs ptype + ptypename publictab pulseaudio punchmomentumlinear diff --git a/docs/ba_module.md b/docs/ba_module.md index ea0c52d8..cd73497f 100644 --- a/docs/ba_module.md +++ b/docs/ba_module.md @@ -1,5 +1,5 @@ -

last updated on 2021-05-03 for Ballistica version 1.6.0 build 20355

+

last updated on 2021-05-05 for Ballistica version 1.6.0 build 20357

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!


diff --git a/src/ballistica/ballistica.cc b/src/ballistica/ballistica.cc index f55cc570..61c340f4 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 change here. -const int kAppBuildNumber = 20355; +const int kAppBuildNumber = 20358; const char* kAppVersion = "1.6.0"; // Our standalone globals. diff --git a/src/ballistica/input/std_input_module.cc b/src/ballistica/input/std_input_module.cc index b7fb37a5..382a0ac9 100644 --- a/src/ballistica/input/std_input_module.cc +++ b/src/ballistica/input/std_input_module.cc @@ -43,13 +43,15 @@ void StdInputModule::PushBeginReadCall() { char buffer[4096]; char* val = fgets(buffer, sizeof(buffer), stdin); if (val) { - int last_char = static_cast(strlen(buffer) - 1); + pending_input_ += val; - // Clip off our last char if its a newline (just to keep things tidier). - if (last_char >= 0 && buffer[last_char] == '\n') { - buffer[last_char] = 0; + if (!pending_input_.empty() + && pending_input_[pending_input_.size() - 1] == '\n') { + // Get rid of the last newline and ship it to the game. + pending_input_.pop_back(); + g_game->PushStdinScriptCommand(pending_input_); + pending_input_.clear(); } - g_game->PushStdinScriptCommand(buffer); } else { // At the moment we bail on any read error. if (feof(stdin)) { diff --git a/src/ballistica/input/std_input_module.h b/src/ballistica/input/std_input_module.h index 74a1e66d..2a8137c9 100644 --- a/src/ballistica/input/std_input_module.h +++ b/src/ballistica/input/std_input_module.h @@ -12,6 +12,9 @@ class StdInputModule : public Module { explicit StdInputModule(Thread* thread); ~StdInputModule() override; void PushBeginReadCall(); + + private: + std::string pending_input_; }; } // namespace ballistica diff --git a/tests/test_efro/test_dataclassio.py b/tests/test_efro/test_dataclassio.py index 43fed197..c4c11cc8 100644 --- a/tests/test_efro/test_dataclassio.py +++ b/tests/test_efro/test_dataclassio.py @@ -7,16 +7,17 @@ from __future__ import annotations from enum import Enum import datetime from dataclasses import field, dataclass -from typing import TYPE_CHECKING +from typing import (TYPE_CHECKING, Optional, List, Set, Any, Dict, Sequence, + Union, Tuple) import pytest from efro.util import utc_now from efro.dataclassio import (dataclass_validate, dataclass_from_dict, - dataclass_to_dict, prepped) + dataclass_to_dict, ioprepped) if TYPE_CHECKING: - from typing import Optional, List, Set, Any, Dict, Sequence, Union, Tuple + pass class _EnumTest(Enum): @@ -55,7 +56,7 @@ def test_assign() -> None: # pylint: disable=too-many-statements - @prepped + @ioprepped @dataclass class _TestClass: ival: int = 0 @@ -276,7 +277,7 @@ def test_assign() -> None: def test_coerce() -> None: """Test value coercion.""" - @prepped + @ioprepped @dataclass class _TestClass: ival: int = 0 @@ -318,7 +319,7 @@ def test_prep() -> None: # a strong use case. with pytest.raises(TypeError): - @prepped + @ioprepped @dataclass class _TestClass: ival: Sequence[int] @@ -328,31 +329,31 @@ def test_prep() -> None: # get_type_hints() so we need to support at least that). with pytest.raises(TypeError): - @prepped + @ioprepped @dataclass class _TestClass2: ival: Union[int, str] - @prepped + @ioprepped @dataclass class _TestClass3: uval: Union[int, None] with pytest.raises(TypeError): - @prepped + @ioprepped @dataclass class _TestClass4: ival: Union[int, str] # This will get simplified down to simply int by get_type_hints so is ok. - @prepped + @ioprepped @dataclass class _TestClass5: ival: Union[int] # This will get simplified down to a valid 2 member union so is ok - @prepped + @ioprepped @dataclass class _TestClass6: ival: Union[int, None, int, None] @@ -361,36 +362,36 @@ def test_prep() -> None: # having those value types. with pytest.raises(TypeError): - @prepped + @ioprepped @dataclass class _TestClass7: dval: Dict[float, int] - @prepped + @ioprepped @dataclass class _TestClass8: dval: Dict[str, int] - @prepped + @ioprepped @dataclass class _TestClass9: dval: Dict[_GoodEnum, int] - @prepped + @ioprepped @dataclass class _TestClass10: dval: Dict[_GoodEnum2, int] with pytest.raises(TypeError): - @prepped + @ioprepped @dataclass class _TestClass11: dval: Dict[_BadEnum1, int] with pytest.raises(TypeError): - @prepped + @ioprepped @dataclass class _TestClass12: dval: Dict[_BadEnum2, int] @@ -399,7 +400,7 @@ def test_prep() -> None: def test_validate() -> None: """Testing validation.""" - @prepped + @ioprepped @dataclass class _TestClass: ival: int = 0 @@ -434,7 +435,7 @@ def test_validate() -> None: def test_extra_data() -> None: """Test handling of data that doesn't map to dataclass attrs.""" - @prepped + @ioprepped @dataclass class _TestClass: ival: int = 0 @@ -462,7 +463,7 @@ def test_extra_data() -> None: def test_dict() -> None: """Test various dict related bits.""" - @prepped + @ioprepped @dataclass class _TestClass: dval: dict @@ -481,7 +482,7 @@ def test_dict() -> None: # Int dict-keys should actually be stored as strings internally # (for json compatibility). - @prepped + @ioprepped @dataclass class _TestClass2: dval: Dict[int, float] @@ -496,7 +497,7 @@ def test_dict() -> None: assert obj2.dval[1] == 2.35 # Same with enum keys (we support enums with str and int values) - @prepped + @ioprepped @dataclass class _TestClass3: dval: Dict[_GoodEnum, int] @@ -508,7 +509,7 @@ def test_dict() -> None: obj3 = dataclass_from_dict(_TestClass3, out) assert obj3.dval[_GoodEnum.VAL1] == 124 - @prepped + @ioprepped @dataclass class _TestClass4: dval: Dict[_GoodEnum2, int] diff --git a/tools/bacommon/net.py b/tools/bacommon/net.py index dd60e270..13a2d455 100644 --- a/tools/bacommon/net.py +++ b/tools/bacommon/net.py @@ -4,14 +4,14 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Optional, List, Dict, Any, Tuple from dataclasses import dataclass from efro import entity -from efro.dataclassio import prepped +from efro.dataclassio import ioprepped if TYPE_CHECKING: - from typing import Optional, Any, List, Dict + pass class ServerNodeEntry(entity.CompoundValue): @@ -33,7 +33,7 @@ class ServerNodeQueryResponse(entity.Entity): store_default=False) -@prepped +@ioprepped @dataclass class PrivateHostingState: """Combined state of whether we're hosting, whether we can, etc.""" @@ -45,7 +45,7 @@ class PrivateHostingState: free_host_minutes_remaining: Optional[float] = None -@prepped +@ioprepped @dataclass class PrivateHostingConfig: """Config provided when hosting a private party.""" @@ -53,12 +53,13 @@ class PrivateHostingConfig: playlist_name: str = 'Unknown' randomize: bool = False tutorial: bool = False - custom_team_names: Optional[List[str]] = None - custom_team_colors: Optional[List[List[float]]] = None + custom_team_names: Optional[Tuple[str, str]] = None + custom_team_colors: Optional[Tuple[Tuple[float, float, float], + Tuple[float, float, float]]] = None playlist: Optional[List[Dict[str, Any]]] = None -@prepped +@ioprepped @dataclass class PrivatePartyConnectResult: """Info about a server we get back when connecting.""" diff --git a/tools/bacommon/servermanager.py b/tools/bacommon/servermanager.py index b8ff9cb0..c4a22464 100644 --- a/tools/bacommon/servermanager.py +++ b/tools/bacommon/servermanager.py @@ -5,15 +5,15 @@ from __future__ import annotations from enum import Enum from dataclasses import field, dataclass -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, List, Optional, Tuple, Dict, Any -from efro.dataclassio import prepped +from efro.dataclassio import ioprepped if TYPE_CHECKING: - from typing import Optional, Tuple, List + pass -@prepped +@ioprepped @dataclass class ServerConfig: """Configuration for the server manager app (_server).""" @@ -52,8 +52,7 @@ class ServerConfig: max_party_size: int = 6 # Options here are 'ffa' (free-for-all) and 'teams' - # This value is only used if you do not supply a playlist_code (see below). - # In that case the default teams or free-for-all playlist gets used. + # This value is ignored if you supply a playlist_code (see below). session_type: str = 'ffa' # To host your own custom playlists, use the 'share' functionality in the @@ -62,6 +61,10 @@ class ServerConfig: # playlist. playlist_code: Optional[int] = None + # Alternately, you can embed playlist data here instead of using codes. + # Make sure to set session_type to the correct type for the data here. + playlist_inline: Optional[List[Dict[str, Any]]] = None + # Whether to shuffle the playlist or play its games in designated order. playlist_shuffle: bool = True @@ -85,12 +88,12 @@ class ServerConfig: # performance) ffa_series_length: int = 24 - # If you provide a custom stats webpage for your server, you can use - # this to provide a convenient in-game link to it in the server-browser - # beside the server name. + # If you have a custom stats webpage for your server, you can use this + # to provide a convenient in-game link to it in the server-browser + # alongside the server name. # if ${ACCOUNT} is present in the string, it will be replaced by the # currently-signed-in account's id. To fetch info about an account, - # your backend server can use the following url: + # your back-end server can use the following url: # http://bombsquadgame.com/accountquery?id=ACCOUNT_ID_HERE stats_url: Optional[str] = None @@ -110,10 +113,20 @@ class ServerConfig: # If present, the server subprocess will shut down immediately if this # amount of time passes with no activity from any players. The server - # manager will then spin up a fresh server subprocess if - # auto-restart is enabled (the default). + # manager will then spin up a fresh server subprocess if auto-restart is + # enabled (the default). idle_exit_minutes: Optional[float] = None + # Should the tutorial be shown at the beginning of games? + show_tutorial: bool = False + + # Team names (teams mode only). + team_names: Optional[Tuple[str, str]] = None + + # Team colors (teams mode only). + team_colors: Optional[Tuple[Tuple[float, float, float], + Tuple[float, float, float]]] = None + # (internal) stress-testing mode. stress_test_players: Optional[int] = None diff --git a/tools/batools/build.py b/tools/batools/build.py index 43c11c24..32f305bf 100644 --- a/tools/batools/build.py +++ b/tools/batools/build.py @@ -657,8 +657,32 @@ def _get_server_config_template_yaml(projroot: str) -> str: ignore_vars = {'stress_test_players'} for line in lines_in: if any(line.startswith(f'{var}:') for var in ignore_vars): - pass - elif line != '' and not line.startswith('#'): + continue + if line.startswith(' '): + # Ignore indented lines (our few multi-line special cases). + continue + + if line.startswith('team_names:'): + lines_out += [ + '#team_names:', + '#- Blue', + '#- Red', + ] + continue + + if line.startswith('team_colors:'): + lines_out += [ + '#team_colors:', + '#- [0.1, 0.25, 1.0]', + '#- [1.0, 0.25, 0.2]', + ] + continue + + if line.startswith('playlist_inline:'): + lines_out += ['#playlist_inline: []'] + continue + + if line != '' and not line.startswith('#'): vname, _vtype, veq, vval_raw = line.split() assert vname.endswith(':') vname = vname[:-1] diff --git a/tools/efro/dataclassio.py b/tools/efro/dataclassio.py index 4b1fed96..1ccde936 100644 --- a/tools/efro/dataclassio.py +++ b/tools/efro/dataclassio.py @@ -147,7 +147,7 @@ def dataclass_validate(obj: Any, coerce_to_float: bool = True) -> None: _Outputter(obj, create=False, coerce_to_float=coerce_to_float).run() -def dataclass_prep(cls: Type, extra_types: Dict[str, Type] = None) -> None: +def ioprep(cls: Type) -> None: """Prep a dataclass type for use with this module's functionality. Prepping ensures that all types contained in a data class as well as @@ -167,19 +167,18 @@ def dataclass_prep(cls: Type, extra_types: Dict[str, Type] = None) -> None: conditional and thus not available at runtime, so are explicitly made available during annotation evaluation. """ - PrepSession(explicit=True, - extra_types=extra_types).prep_dataclass(cls, recursion_level=0) + PrepSession(explicit=True).prep_dataclass(cls, recursion_level=0) -def prepped(cls: Type[T]) -> Type[T]: - """Class decorator to easily prep a dataclass at definition time. +def ioprepped(cls: Type[T]) -> Type[T]: + """Class decorator for easily prepping a dataclass at definition time. Note that in some cases it may not be possible to prep a dataclass immediately (such as when its type annotations refer to forward-declared types). In these cases, dataclass_prep() should be explicitly called for the class once it is safe to do so. """ - dataclass_prep(cls) + ioprep(cls) return cls @@ -197,9 +196,8 @@ class PrepData: class PrepSession: """Context for a prep.""" - def __init__(self, explicit: bool, extra_types: Optional[Dict[str, Type]]): + def __init__(self, explicit: bool): self.explicit = explicit - self.extra_types = extra_types def prep_dataclass(self, cls: Type, recursion_level: int) -> PrepData: """Run prep on a dataclass if necessary and return its prep data.""" @@ -228,27 +226,18 @@ class PrepSession: ' efro.dataclassio.dataclass_prep() or the' ' @efro.dataclassio.prepped decorator).', cls) - localns: Dict[str, Any] = { - 'Optional': typing.Optional, - 'Union': typing.Union, - 'List': typing.List, - 'Tuple': typing.Tuple, - 'Sequence': typing.Sequence, - 'Set': typing.Set, - 'Any': typing.Any, - 'Dict': typing.Dict, - } - if self.extra_types is not None: - localns.update(self.extra_types) - try: # Use default globalns which should be the class' module, # but provide our own locals to cover things like typing.* # which are generally not actually present at runtime for us. - resolved_annotations = get_type_hints(cls, localns=localns) + # resolved_annotations = get_type_hints(cls, localns=localns) + resolved_annotations = get_type_hints(cls) except Exception as exc: raise RuntimeError( - f'Dataclass prep failed with error: {exc}.') from exc + f'dataclassio prep for {cls} failed with error: {exc}.' + f' Make sure all types used in annotations are defined' + f' at the module level or add them as part of an explicit' + f' prep call.') from exc # Ok; we've resolved actual types for this dataclass. # now recurse through them, verifying that we support all contained @@ -478,9 +467,8 @@ class _Outputter: return self._process_dataclass(type(self._obj), self._obj, '') def _process_dataclass(self, cls: Type, obj: Any, fieldpath: str) -> Any: - prep = PrepSession(explicit=False, - extra_types=None).prep_dataclass(type(obj), - recursion_level=0) + prep = PrepSession(explicit=False).prep_dataclass(type(obj), + recursion_level=0) fields = dataclasses.fields(obj) out: Optional[Dict[str, Any]] = {} if self._create else None for field in fields: @@ -823,9 +811,8 @@ class _Inputter(Generic[T]): if not isinstance(values, dict): raise TypeError("Expected a dict for 'values' arg.") - prep = PrepSession(explicit=False, - extra_types=None).prep_dataclass(cls, - recursion_level=0) + prep = PrepSession(explicit=False).prep_dataclass(cls, + recursion_level=0) extra_attrs = {} @@ -858,7 +845,12 @@ class _Inputter(Generic[T]): if fieldpath else fieldname) args[key] = self._value_from_input(cls, subfieldpath, fieldtype, value) - out = cls(**args) + try: + out = cls(**args) + except Exception as exc: + raise RuntimeError( + f'Error instantiating class {cls} at {fieldpath}: {exc}' + ) from exc if extra_attrs: setattr(out, EXTRA_ATTRS_ATTR, extra_attrs) return out diff --git a/tools/efrotools/pylintplugins.py b/tools/efrotools/pylintplugins.py index 490df001..16ff7e2f 100644 --- a/tools/efrotools/pylintplugins.py +++ b/tools/efrotools/pylintplugins.py @@ -153,10 +153,46 @@ def var_annotations_filter(node: nc.NodeNG) -> nc.NodeNG: This accounts for deferred evaluation. """ + # pylint: disable=too-many-branches + # pylint: disable=too-many-nested-blocks + if using_future_annotations(node): + # Future behavior: - # Annotations are never evaluated. + # Annotated assigns under functions are not evaluated. + # Class and module vars are normally not either. However we + # do evaluate if we come across an 'ioprepped' dataclass + # decorator. (the 'ioprepped' decorator explicitly evaluates + # dataclass annotations). + + fnode = node willeval = False + while fnode is not None: + if isinstance(fnode, astroid.ClassDef): + if fnode.decorators is not None: + found_ioprepped = False + for dec in fnode.decorators.nodes: + + # Look for dataclassio.ioprepped. + if (isinstance(dec, astroid.nodes.Attribute) + and dec.attrname == 'ioprepped' + and isinstance(dec.expr, astroid.nodes.Name) + and dec.expr.name == 'dataclassio'): + found_ioprepped = True + break + + # Look for simply 'ioprepped'. + if (isinstance(dec, astroid.nodes.Name) + and dec.name == 'ioprepped'): + found_ioprepped = True + break + + if found_ioprepped: + willeval = True + break + + fnode = fnode.parent + else: # Legacy behavior: # Annotated assigns under functions are not evaluated,