bacommon.servermanager

Functionality related to the server manager script.

  1# Released under the MIT License. See LICENSE for details.
  2#
  3"""Functionality related to the server manager script."""
  4from __future__ import annotations
  5
  6from enum import Enum
  7from dataclasses import field, dataclass
  8from typing import TYPE_CHECKING, Any
  9
 10from efro.dataclassio import ioprepped
 11
 12if TYPE_CHECKING:
 13    pass
 14
 15
 16@ioprepped
 17@dataclass
 18class ServerConfig:
 19    """Configuration for the server manager app (<appname>_server)."""
 20
 21    # Name of our server in the public parties list.
 22    party_name: str = 'FFA'
 23
 24    # If True, your party will show up in the global public party list
 25    # Otherwise it will still be joinable via LAN or connecting by IP
 26    # address.
 27    party_is_public: bool = True
 28
 29    # If True, all connecting clients will be authenticated through the
 30    # master server to screen for fake account info. Generally this
 31    # should always be enabled unless you are hosting on a LAN with no
 32    # internet connection.
 33    authenticate_clients: bool = True
 34
 35    # IDs of server admins. Server admins are not kickable through the
 36    # default kick vote system and they are able to kick players without
 37    # a vote. To get your account id, enter 'getaccountid' in
 38    # settings->advanced->enter-code.
 39    admins: list[str] = field(default_factory=list)
 40
 41    # Whether the default kick-voting system is enabled.
 42    enable_default_kick_voting: bool = True
 43
 44    # To be included in the public server list, your server MUST be
 45    # accessible via an ipv4 address. By default, the master server will
 46    # try to use the address your server contacts it from, but this may
 47    # be an ipv6 address these days so you may need to provide an ipv4
 48    # address explicitly.
 49    public_ipv4_address: str | None = None
 50
 51    # You can optionally provide an ipv6 address for your server for the
 52    # public server list. Unlike ipv4, a server is not required to have
 53    # an ipv6 address to appear in the list, but is still good to
 54    # provide when available since more and more devices are using ipv6
 55    # these days. Your server's ipv6 address will be autodetected if
 56    # your server uses ipv6 when communicating with the master server. You
 57    # can pass an empty string here to explicitly disable the ipv6
 58    # address.
 59    public_ipv6_address: str | None = None
 60
 61    # UDP port to host on. Change this to work around firewalls or run
 62    # multiple servers on one machine.
 63    #
 64    # 43210 is the default and the only port that will show up in the
 65    # LAN browser tab.
 66    port: int = 43210
 67
 68    # Max devices in the party. Note that this does *NOT* mean max
 69    # players. Any device in the party can have more than one player on
 70    # it if they have multiple controllers. Also, this number currently
 71    # includes the server so generally make it 1 bigger than you need.
 72    max_party_size: int = 6
 73
 74    # Max players that can join a session. If present this will override
 75    # the session's preferred max_players. if a value below 0 is given
 76    # player limit will be removed.
 77    session_max_players_override: int | None = None
 78
 79    # Options here are 'ffa' (free-for-all), 'teams' and 'coop'
 80    # (cooperative) This value is ignored if you supply a playlist_code
 81    # (see below).
 82    session_type: str = 'ffa'
 83
 84    # Playlist-code for teams or free-for-all mode sessions. To host
 85    # your own custom playlists, use the 'share' functionality in the
 86    # playlist editor in the regular version of the game. This will give
 87    # you a numeric code you can enter here to host that playlist.
 88    playlist_code: int | None = None
 89
 90    # Alternately, you can embed playlist data here instead of using
 91    # codes. Make sure to set session_type to the correct type for the
 92    # data here.
 93    playlist_inline: list[dict[str, Any]] | None = None
 94
 95    # Whether to shuffle the playlist or play its games in designated
 96    # order.
 97    playlist_shuffle: bool = True
 98
 99    # If True, keeps team sizes equal by disallowing joining the largest
100    # team (teams mode only).
101    auto_balance_teams: bool = True
102
103    # The campaign used when in co-op session mode. Do
104    # print(ba.app.campaigns) to see available campaign names.
105    coop_campaign: str = 'Easy'
106
107    # The level name within the campaign used in co-op session mode. For
108    # campaign name FOO, do print(ba.app.campaigns['FOO'].levels) to see
109    # available level names.
110    coop_level: str = 'Onslaught Training'
111
112    # Whether to enable telnet access.
113    #
114    # IMPORTANT: This option is no longer available, as it was being
115    # used for exploits. Live access to the running server is still
116    # possible through the mgr.cmd() function in the server script. Run
117    # your server through tools such as 'screen' or 'tmux' and you can
118    # reconnect to it remotely over a secure ssh connection.
119    enable_telnet: bool = False
120
121    # Series length in teams mode (7 == 'best-of-7' series; a team must
122    # get 4 wins)
123    teams_series_length: int = 7
124
125    # Points to win in free-for-all mode (Points are awarded per game
126    # based on performance)
127    ffa_series_length: int = 24
128
129    # If you have a custom stats webpage for your server, you can use
130    # this to provide a convenient in-game link to it in the
131    # server-browser alongside the server name.
132    #
133    # if ${ACCOUNT} is present in the string, it will be replaced by the
134    # currently-signed-in account's id. To fetch info about an account,
135    # your back-end server can use the following url:
136    # https://legacy.ballistica.net/accountquery?id=ACCOUNT_ID_HERE
137    stats_url: str | None = None
138
139    # If present, the server subprocess will attempt to gracefully exit
140    # after this amount of time. A graceful exit can occur at the end of
141    # a series or other opportune time. Server-managers set to
142    # auto-restart (the default) will then spin up a fresh subprocess.
143    # This mechanism can be useful to clear out any memory leaks or
144    # other accumulated bad state in the server subprocess.
145    clean_exit_minutes: float | None = None
146
147    # If present, the server subprocess will shut down immediately after
148    # this amount of time. This can be useful as a fallback for
149    # clean_exit_time. The server manager will then spin up a fresh
150    # server subprocess if auto-restart is enabled (the default).
151    unclean_exit_minutes: float | None = None
152
153    # If present, the server subprocess will shut down immediately if
154    # this amount of time passes with no activity from any players. The
155    # server manager will then spin up a fresh server subprocess if
156    # auto-restart is enabled (the default).
157    idle_exit_minutes: float | None = None
158
159    # Should the tutorial be shown at the beginning of games?
160    show_tutorial: bool = False
161
162    # Team names (teams mode only).
163    team_names: tuple[str, str] | None = None
164
165    # Team colors (teams mode only).
166    team_colors: (
167        tuple[tuple[float, float, float], tuple[float, float, float]] | None
168    ) = None
169
170    # Whether to enable the queue where players can line up before
171    # entering your server. Disabling this can be used as a workaround
172    # to deal with queue spamming attacks.
173    enable_queue: bool = True
174
175    # Protocol version we host with. Currently the default is 33 which
176    # still allows older 1.4 game clients to connect. Explicitly setting
177    # to 35 no longer allows those clients but adds/fixes a few things
178    # such as making camera shake properly work in net games.
179    protocol_version: int | None = None
180
181    # (internal) stress-testing mode.
182    stress_test_players: int | None = None
183
184    # How many seconds individual players from a given account must wait
185    # before rejoining the game. This can help suppress exploits
186    # involving leaving and rejoining or switching teams rapidly.
187    player_rejoin_cooldown: float = 10.0
188
189    # Log levels for particular loggers, overriding the engine's
190    # defaults. Valid values are NOTSET, DEBUG, INFO, WARNING, ERROR, or
191    # CRITICAL.
192    log_levels: dict[str, str] | None = None
193
194
195# NOTE: as much as possible, communication from the server-manager to
196# the child-process should go through these and not ad-hoc Python string
197# commands since this way is type safe.
198class ServerCommand:
199    """Base class for commands that can be sent to the server."""
200
201
202@dataclass
203class StartServerModeCommand(ServerCommand):
204    """Tells the app to switch into 'server' mode."""
205
206    config: ServerConfig
207
208
209class ShutdownReason(Enum):
210    """Reason a server is shutting down."""
211
212    NONE = 'none'
213    RESTARTING = 'restarting'
214
215
216@dataclass
217class ShutdownCommand(ServerCommand):
218    """Tells the server to shut down."""
219
220    reason: ShutdownReason
221    immediate: bool
222
223
224@dataclass
225class ChatMessageCommand(ServerCommand):
226    """Chat message from the server."""
227
228    message: str
229    clients: list[int] | None
230
231
232@dataclass
233class ScreenMessageCommand(ServerCommand):
234    """Screen-message from the server."""
235
236    message: str
237    color: tuple[float, float, float] | None
238    clients: list[int] | None
239
240
241@dataclass
242class ClientListCommand(ServerCommand):
243    """Print a list of clients."""
244
245
246@dataclass
247class KickCommand(ServerCommand):
248    """Kick a client."""
249
250    client_id: int
251    ban_time: int | None
@ioprepped
@dataclass
class ServerConfig:
 17@ioprepped
 18@dataclass
 19class ServerConfig:
 20    """Configuration for the server manager app (<appname>_server)."""
 21
 22    # Name of our server in the public parties list.
 23    party_name: str = 'FFA'
 24
 25    # If True, your party will show up in the global public party list
 26    # Otherwise it will still be joinable via LAN or connecting by IP
 27    # address.
 28    party_is_public: bool = True
 29
 30    # If True, all connecting clients will be authenticated through the
 31    # master server to screen for fake account info. Generally this
 32    # should always be enabled unless you are hosting on a LAN with no
 33    # internet connection.
 34    authenticate_clients: bool = True
 35
 36    # IDs of server admins. Server admins are not kickable through the
 37    # default kick vote system and they are able to kick players without
 38    # a vote. To get your account id, enter 'getaccountid' in
 39    # settings->advanced->enter-code.
 40    admins: list[str] = field(default_factory=list)
 41
 42    # Whether the default kick-voting system is enabled.
 43    enable_default_kick_voting: bool = True
 44
 45    # To be included in the public server list, your server MUST be
 46    # accessible via an ipv4 address. By default, the master server will
 47    # try to use the address your server contacts it from, but this may
 48    # be an ipv6 address these days so you may need to provide an ipv4
 49    # address explicitly.
 50    public_ipv4_address: str | None = None
 51
 52    # You can optionally provide an ipv6 address for your server for the
 53    # public server list. Unlike ipv4, a server is not required to have
 54    # an ipv6 address to appear in the list, but is still good to
 55    # provide when available since more and more devices are using ipv6
 56    # these days. Your server's ipv6 address will be autodetected if
 57    # your server uses ipv6 when communicating with the master server. You
 58    # can pass an empty string here to explicitly disable the ipv6
 59    # address.
 60    public_ipv6_address: str | None = None
 61
 62    # UDP port to host on. Change this to work around firewalls or run
 63    # multiple servers on one machine.
 64    #
 65    # 43210 is the default and the only port that will show up in the
 66    # LAN browser tab.
 67    port: int = 43210
 68
 69    # Max devices in the party. Note that this does *NOT* mean max
 70    # players. Any device in the party can have more than one player on
 71    # it if they have multiple controllers. Also, this number currently
 72    # includes the server so generally make it 1 bigger than you need.
 73    max_party_size: int = 6
 74
 75    # Max players that can join a session. If present this will override
 76    # the session's preferred max_players. if a value below 0 is given
 77    # player limit will be removed.
 78    session_max_players_override: int | None = None
 79
 80    # Options here are 'ffa' (free-for-all), 'teams' and 'coop'
 81    # (cooperative) This value is ignored if you supply a playlist_code
 82    # (see below).
 83    session_type: str = 'ffa'
 84
 85    # Playlist-code for teams or free-for-all mode sessions. To host
 86    # your own custom playlists, use the 'share' functionality in the
 87    # playlist editor in the regular version of the game. This will give
 88    # you a numeric code you can enter here to host that playlist.
 89    playlist_code: int | None = None
 90
 91    # Alternately, you can embed playlist data here instead of using
 92    # codes. Make sure to set session_type to the correct type for the
 93    # data here.
 94    playlist_inline: list[dict[str, Any]] | None = None
 95
 96    # Whether to shuffle the playlist or play its games in designated
 97    # order.
 98    playlist_shuffle: bool = True
 99
100    # If True, keeps team sizes equal by disallowing joining the largest
101    # team (teams mode only).
102    auto_balance_teams: bool = True
103
104    # The campaign used when in co-op session mode. Do
105    # print(ba.app.campaigns) to see available campaign names.
106    coop_campaign: str = 'Easy'
107
108    # The level name within the campaign used in co-op session mode. For
109    # campaign name FOO, do print(ba.app.campaigns['FOO'].levels) to see
110    # available level names.
111    coop_level: str = 'Onslaught Training'
112
113    # Whether to enable telnet access.
114    #
115    # IMPORTANT: This option is no longer available, as it was being
116    # used for exploits. Live access to the running server is still
117    # possible through the mgr.cmd() function in the server script. Run
118    # your server through tools such as 'screen' or 'tmux' and you can
119    # reconnect to it remotely over a secure ssh connection.
120    enable_telnet: bool = False
121
122    # Series length in teams mode (7 == 'best-of-7' series; a team must
123    # get 4 wins)
124    teams_series_length: int = 7
125
126    # Points to win in free-for-all mode (Points are awarded per game
127    # based on performance)
128    ffa_series_length: int = 24
129
130    # If you have a custom stats webpage for your server, you can use
131    # this to provide a convenient in-game link to it in the
132    # server-browser alongside the server name.
133    #
134    # if ${ACCOUNT} is present in the string, it will be replaced by the
135    # currently-signed-in account's id. To fetch info about an account,
136    # your back-end server can use the following url:
137    # https://legacy.ballistica.net/accountquery?id=ACCOUNT_ID_HERE
138    stats_url: str | None = None
139
140    # If present, the server subprocess will attempt to gracefully exit
141    # after this amount of time. A graceful exit can occur at the end of
142    # a series or other opportune time. Server-managers set to
143    # auto-restart (the default) will then spin up a fresh subprocess.
144    # This mechanism can be useful to clear out any memory leaks or
145    # other accumulated bad state in the server subprocess.
146    clean_exit_minutes: float | None = None
147
148    # If present, the server subprocess will shut down immediately after
149    # this amount of time. This can be useful as a fallback for
150    # clean_exit_time. The server manager will then spin up a fresh
151    # server subprocess if auto-restart is enabled (the default).
152    unclean_exit_minutes: float | None = None
153
154    # If present, the server subprocess will shut down immediately if
155    # this amount of time passes with no activity from any players. The
156    # server manager will then spin up a fresh server subprocess if
157    # auto-restart is enabled (the default).
158    idle_exit_minutes: float | None = None
159
160    # Should the tutorial be shown at the beginning of games?
161    show_tutorial: bool = False
162
163    # Team names (teams mode only).
164    team_names: tuple[str, str] | None = None
165
166    # Team colors (teams mode only).
167    team_colors: (
168        tuple[tuple[float, float, float], tuple[float, float, float]] | None
169    ) = None
170
171    # Whether to enable the queue where players can line up before
172    # entering your server. Disabling this can be used as a workaround
173    # to deal with queue spamming attacks.
174    enable_queue: bool = True
175
176    # Protocol version we host with. Currently the default is 33 which
177    # still allows older 1.4 game clients to connect. Explicitly setting
178    # to 35 no longer allows those clients but adds/fixes a few things
179    # such as making camera shake properly work in net games.
180    protocol_version: int | None = None
181
182    # (internal) stress-testing mode.
183    stress_test_players: int | None = None
184
185    # How many seconds individual players from a given account must wait
186    # before rejoining the game. This can help suppress exploits
187    # involving leaving and rejoining or switching teams rapidly.
188    player_rejoin_cooldown: float = 10.0
189
190    # Log levels for particular loggers, overriding the engine's
191    # defaults. Valid values are NOTSET, DEBUG, INFO, WARNING, ERROR, or
192    # CRITICAL.
193    log_levels: dict[str, str] | None = None

Configuration for the server manager app (_server).

ServerConfig( party_name: str = 'FFA', party_is_public: bool = True, authenticate_clients: bool = True, admins: list[str] = <factory>, enable_default_kick_voting: bool = True, public_ipv4_address: str | None = None, public_ipv6_address: str | None = None, port: int = 43210, max_party_size: int = 6, session_max_players_override: int | None = None, session_type: str = 'ffa', playlist_code: int | None = None, playlist_inline: list[dict[str, typing.Any]] | None = None, playlist_shuffle: bool = True, auto_balance_teams: bool = True, coop_campaign: str = 'Easy', coop_level: str = 'Onslaught Training', enable_telnet: bool = False, teams_series_length: int = 7, ffa_series_length: int = 24, stats_url: str | None = None, clean_exit_minutes: float | None = None, unclean_exit_minutes: float | None = None, idle_exit_minutes: float | None = None, show_tutorial: bool = False, team_names: tuple[str, str] | None = None, team_colors: tuple[tuple[float, float, float], tuple[float, float, float]] | None = None, enable_queue: bool = True, protocol_version: int | None = None, stress_test_players: int | None = None, player_rejoin_cooldown: float = 10.0, log_levels: dict[str, str] | None = None)
party_name: str = 'FFA'
party_is_public: bool = True
authenticate_clients: bool = True
admins: list[str]
enable_default_kick_voting: bool = True
public_ipv4_address: str | None = None
public_ipv6_address: str | None = None
port: int = 43210
max_party_size: int = 6
session_max_players_override: int | None = None
session_type: str = 'ffa'
playlist_code: int | None = None
playlist_inline: list[dict[str, typing.Any]] | None = None
playlist_shuffle: bool = True
auto_balance_teams: bool = True
coop_campaign: str = 'Easy'
coop_level: str = 'Onslaught Training'
enable_telnet: bool = False
teams_series_length: int = 7
ffa_series_length: int = 24
stats_url: str | None = None
clean_exit_minutes: float | None = None
unclean_exit_minutes: float | None = None
idle_exit_minutes: float | None = None
show_tutorial: bool = False
team_names: tuple[str, str] | None = None
team_colors: tuple[tuple[float, float, float], tuple[float, float, float]] | None = None
enable_queue: bool = True
protocol_version: int | None = None
stress_test_players: int | None = None
player_rejoin_cooldown: float = 10.0
log_levels: dict[str, str] | None = None
class ServerCommand:
199class ServerCommand:
200    """Base class for commands that can be sent to the server."""

Base class for commands that can be sent to the server.

@dataclass
class StartServerModeCommand(ServerCommand):
203@dataclass
204class StartServerModeCommand(ServerCommand):
205    """Tells the app to switch into 'server' mode."""
206
207    config: ServerConfig

Tells the app to switch into 'server' mode.

StartServerModeCommand(config: ServerConfig)
config: ServerConfig
class ShutdownReason(enum.Enum):
210class ShutdownReason(Enum):
211    """Reason a server is shutting down."""
212
213    NONE = 'none'
214    RESTARTING = 'restarting'

Reason a server is shutting down.

NONE = <ShutdownReason.NONE: 'none'>
RESTARTING = <ShutdownReason.RESTARTING: 'restarting'>
@dataclass
class ShutdownCommand(ServerCommand):
217@dataclass
218class ShutdownCommand(ServerCommand):
219    """Tells the server to shut down."""
220
221    reason: ShutdownReason
222    immediate: bool

Tells the server to shut down.

ShutdownCommand(reason: ShutdownReason, immediate: bool)
reason: ShutdownReason
immediate: bool
@dataclass
class ChatMessageCommand(ServerCommand):
225@dataclass
226class ChatMessageCommand(ServerCommand):
227    """Chat message from the server."""
228
229    message: str
230    clients: list[int] | None

Chat message from the server.

ChatMessageCommand(message: str, clients: list[int] | None)
message: str
clients: list[int] | None
@dataclass
class ScreenMessageCommand(ServerCommand):
233@dataclass
234class ScreenMessageCommand(ServerCommand):
235    """Screen-message from the server."""
236
237    message: str
238    color: tuple[float, float, float] | None
239    clients: list[int] | None

Screen-message from the server.

ScreenMessageCommand( message: str, color: tuple[float, float, float] | None, clients: list[int] | None)
message: str
color: tuple[float, float, float] | None
clients: list[int] | None
@dataclass
class ClientListCommand(ServerCommand):
242@dataclass
243class ClientListCommand(ServerCommand):
244    """Print a list of clients."""

Print a list of clients.

@dataclass
class KickCommand(ServerCommand):
247@dataclass
248class KickCommand(ServerCommand):
249    """Kick a client."""
250
251    client_id: int
252    ban_time: int | None

Kick a client.

KickCommand(client_id: int, ban_time: int | None)
client_id: int
ban_time: int | None