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,