mirror of
https://github.com/RYDE-WORK/ballistica.git
synced 2026-01-23 23:49:47 +08:00
v1.6
This commit is contained in:
parent
c10cf5c2fe
commit
4b3ecba38d
@ -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"
|
||||
}
|
||||
4
.idea/dictionaries/ericf.xml
generated
4
.idea/dictionaries/ericf.xml
generated
@ -143,6 +143,7 @@
|
||||
<w>audiobooks</w>
|
||||
<w>audioop</w>
|
||||
<w>autodesk</w>
|
||||
<w>autodetected</w>
|
||||
<w>autogenerate</w>
|
||||
<w>autonoassets</w>
|
||||
<w>autopoint</w>
|
||||
@ -1085,6 +1086,8 @@
|
||||
<w>introspectable</w>
|
||||
<w>intstr</w>
|
||||
<w>iobj</w>
|
||||
<w>ioprep</w>
|
||||
<w>ioprepped</w>
|
||||
<w>ipaddress</w>
|
||||
<w>ipos</w>
|
||||
<w>iprof</w>
|
||||
@ -1712,6 +1715,7 @@
|
||||
<w>pthreads</w>
|
||||
<w>ptrans</w>
|
||||
<w>ptype</w>
|
||||
<w>ptypename</w>
|
||||
<w>publictab</w>
|
||||
<w>pubsync</w>
|
||||
<w>pucknode</w>
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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."""
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -57,6 +57,7 @@
|
||||
<w>athome</w>
|
||||
<w>attrobj</w>
|
||||
<w>audiocache</w>
|
||||
<w>autodetected</w>
|
||||
<w>automagically</w>
|
||||
<w>autoselect</w>
|
||||
<w>availmins</w>
|
||||
@ -485,6 +486,8 @@
|
||||
<w>intstr</w>
|
||||
<w>invote</w>
|
||||
<w>iobj</w>
|
||||
<w>ioprep</w>
|
||||
<w>ioprepped</w>
|
||||
<w>iserverget</w>
|
||||
<w>iserverput</w>
|
||||
<w>isinst</w>
|
||||
@ -758,6 +761,7 @@
|
||||
<w>pton</w>
|
||||
<w>ptrs</w>
|
||||
<w>ptype</w>
|
||||
<w>ptypename</w>
|
||||
<w>publictab</w>
|
||||
<w>pulseaudio</w>
|
||||
<w>punchmomentumlinear</w>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<!-- THIS FILE IS AUTO GENERATED; DO NOT EDIT BY HAND -->
|
||||
<h4><em>last updated on 2021-05-03 for Ballistica version 1.6.0 build 20355</em></h4>
|
||||
<h4><em>last updated on 2021-05-05 for Ballistica version 1.6.0 build 20357</em></h4>
|
||||
<p>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 <a href="mailto:support@froemling.net">let me know</a>. Happy modding!</p>
|
||||
<hr>
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -43,13 +43,15 @@ void StdInputModule::PushBeginReadCall() {
|
||||
char buffer[4096];
|
||||
char* val = fgets(buffer, sizeof(buffer), stdin);
|
||||
if (val) {
|
||||
int last_char = static_cast<int>(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)) {
|
||||
|
||||
@ -12,6 +12,9 @@ class StdInputModule : public Module {
|
||||
explicit StdInputModule(Thread* thread);
|
||||
~StdInputModule() override;
|
||||
void PushBeginReadCall();
|
||||
|
||||
private:
|
||||
std::string pending_input_;
|
||||
};
|
||||
|
||||
} // namespace ballistica
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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."""
|
||||
|
||||
@ -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 (<appname>_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
|
||||
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user