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

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, 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)
party_name: str = 'FFA'
party_is_public: bool = True
authenticate_clients: bool = True
admins: list[str]
enable_default_kick_voting: bool = True
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
class ServerCommand:
169class ServerCommand:
170    """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):
173@dataclass
174class StartServerModeCommand(ServerCommand):
175    """Tells the app to switch into 'server' mode."""
176
177    config: ServerConfig

Tells the app to switch into 'server' mode.

StartServerModeCommand(config: ServerConfig)
config: ServerConfig
class ShutdownReason(enum.Enum):
180class ShutdownReason(Enum):
181    """Reason a server is shutting down."""
182
183    NONE = 'none'
184    RESTARTING = 'restarting'

Reason a server is shutting down.

NONE = <ShutdownReason.NONE: 'none'>
RESTARTING = <ShutdownReason.RESTARTING: 'restarting'>
Inherited Members
enum.Enum
name
value
@dataclass
class ShutdownCommand(ServerCommand):
187@dataclass
188class ShutdownCommand(ServerCommand):
189    """Tells the server to shut down."""
190
191    reason: ShutdownReason
192    immediate: bool

Tells the server to shut down.

ShutdownCommand(reason: ShutdownReason, immediate: bool)
reason: ShutdownReason
immediate: bool
@dataclass
class ChatMessageCommand(ServerCommand):
195@dataclass
196class ChatMessageCommand(ServerCommand):
197    """Chat message from the server."""
198
199    message: str
200    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):
203@dataclass
204class ScreenMessageCommand(ServerCommand):
205    """Screen-message from the server."""
206
207    message: str
208    color: tuple[float, float, float] | None
209    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):
212@dataclass
213class ClientListCommand(ServerCommand):
214    """Print a list of clients."""

Print a list of clients.

@dataclass
class KickCommand(ServerCommand):
217@dataclass
218class KickCommand(ServerCommand):
219    """Kick a client."""
220
221    client_id: int
222    ban_time: int | None

Kick a client.

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