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. Max-players is not currently
 51    # exposed but I'll try to add that soon.
 52    max_party_size: int = 6
 53
 54    # Options here are 'ffa' (free-for-all), 'teams' and 'coop' (cooperative)
 55    # This value is ignored if you supply a playlist_code (see below).
 56    session_type: str = 'ffa'
 57
 58    # Playlist-code for teams or free-for-all mode sessions.
 59    # To host your own custom playlists, use the 'share' functionality in the
 60    # playlist editor in the regular version of the game.
 61    # This will give you a numeric code you can enter here to host that
 62    # playlist.
 63    playlist_code: int | None = None
 64
 65    # Alternately, you can embed playlist data here instead of using codes.
 66    # Make sure to set session_type to the correct type for the data here.
 67    playlist_inline: list[dict[str, Any]] | None = None
 68
 69    # Whether to shuffle the playlist or play its games in designated order.
 70    playlist_shuffle: bool = True
 71
 72    # If True, keeps team sizes equal by disallowing joining the largest team
 73    # (teams mode only).
 74    auto_balance_teams: bool = True
 75
 76    # The campaign used when in co-op session mode.
 77    # Do print(ba.app.campaigns) to see available campaign names.
 78    coop_campaign: str = 'Easy'
 79
 80    # The level name within the campaign used in co-op session mode.
 81    # For campaign name FOO, do print(ba.app.campaigns['FOO'].levels) to see
 82    # available level names.
 83    coop_level: str = 'Onslaught Training'
 84
 85    # Whether to enable telnet access.
 86    # IMPORTANT: This option is no longer available, as it was being used
 87    # for exploits. Live access to the running server is still possible through
 88    # the mgr.cmd() function in the server script. Run your server through
 89    # tools such as 'screen' or 'tmux' and you can reconnect to it remotely
 90    # over a secure ssh connection.
 91    enable_telnet: bool = False
 92
 93    # Series length in teams mode (7 == 'best-of-7' series; a team must
 94    # get 4 wins)
 95    teams_series_length: int = 7
 96
 97    # Points to win in free-for-all mode (Points are awarded per game based on
 98    # performance)
 99    ffa_series_length: int = 24
100
101    # If you have a custom stats webpage for your server, you can use this
102    # to provide a convenient in-game link to it in the server-browser
103    # alongside the server name.
104    # if ${ACCOUNT} is present in the string, it will be replaced by the
105    # currently-signed-in account's id. To fetch info about an account,
106    # your back-end server can use the following url:
107    # https://legacy.ballistica.net/accountquery?id=ACCOUNT_ID_HERE
108    stats_url: str | None = None
109
110    # If present, the server subprocess will attempt to gracefully exit after
111    # this amount of time. A graceful exit can occur at the end of a series
112    # or other opportune time. Server-managers set to auto-restart (the
113    # default) will then spin up a fresh subprocess. This mechanism can be
114    # useful to clear out any memory leaks or other accumulated bad state
115    # in the server subprocess.
116    clean_exit_minutes: float | None = None
117
118    # If present, the server subprocess will shut down immediately after this
119    # amount of time. This can be useful as a fallback for clean_exit_time.
120    # The server manager will then spin up a fresh server subprocess if
121    # auto-restart is enabled (the default).
122    unclean_exit_minutes: float | None = None
123
124    # If present, the server subprocess will shut down immediately if this
125    # amount of time passes with no activity from any players. The server
126    # manager will then spin up a fresh server subprocess if auto-restart is
127    # enabled (the default).
128    idle_exit_minutes: float | None = None
129
130    # Should the tutorial be shown at the beginning of games?
131    show_tutorial: bool = False
132
133    # Team names (teams mode only).
134    team_names: tuple[str, str] | None = None
135
136    # Team colors (teams mode only).
137    team_colors: tuple[
138        tuple[float, float, float], tuple[float, float, float]
139    ] | None = None
140
141    # Whether to enable the queue where players can line up before entering
142    # your server. Disabling this can be used as a workaround to deal with
143    # queue spamming attacks.
144    enable_queue: bool = True
145
146    # (internal) stress-testing mode.
147    stress_test_players: int | None = None
148
149
150# NOTE: as much as possible, communication from the server-manager to the
151# child-process should go through these and not ad-hoc Python string commands
152# since this way is type safe.
153class ServerCommand:
154    """Base class for commands that can be sent to the server."""
155
156
157@dataclass
158class StartServerModeCommand(ServerCommand):
159    """Tells the app to switch into 'server' mode."""
160
161    config: ServerConfig
162
163
164class ShutdownReason(Enum):
165    """Reason a server is shutting down."""
166
167    NONE = 'none'
168    RESTARTING = 'restarting'
169
170
171@dataclass
172class ShutdownCommand(ServerCommand):
173    """Tells the server to shut down."""
174
175    reason: ShutdownReason
176    immediate: bool
177
178
179@dataclass
180class ChatMessageCommand(ServerCommand):
181    """Chat message from the server."""
182
183    message: str
184    clients: list[int] | None
185
186
187@dataclass
188class ScreenMessageCommand(ServerCommand):
189    """Screen-message from the server."""
190
191    message: str
192    color: tuple[float, float, float] | None
193    clients: list[int] | None
194
195
196@dataclass
197class ClientListCommand(ServerCommand):
198    """Print a list of clients."""
199
200
201@dataclass
202class KickCommand(ServerCommand):
203    """Kick a client."""
204
205    client_id: int
206    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. Max-players is not currently
 52    # exposed but I'll try to add that soon.
 53    max_party_size: int = 6
 54
 55    # Options here are 'ffa' (free-for-all), 'teams' and 'coop' (cooperative)
 56    # This value is ignored if you supply a playlist_code (see below).
 57    session_type: str = 'ffa'
 58
 59    # Playlist-code for teams or free-for-all mode sessions.
 60    # To host your own custom playlists, use the 'share' functionality in the
 61    # playlist editor in the regular version of the game.
 62    # This will give you a numeric code you can enter here to host that
 63    # playlist.
 64    playlist_code: int | None = None
 65
 66    # Alternately, you can embed playlist data here instead of using codes.
 67    # Make sure to set session_type to the correct type for the data here.
 68    playlist_inline: list[dict[str, Any]] | None = None
 69
 70    # Whether to shuffle the playlist or play its games in designated order.
 71    playlist_shuffle: bool = True
 72
 73    # If True, keeps team sizes equal by disallowing joining the largest team
 74    # (teams mode only).
 75    auto_balance_teams: bool = True
 76
 77    # The campaign used when in co-op session mode.
 78    # Do print(ba.app.campaigns) to see available campaign names.
 79    coop_campaign: str = 'Easy'
 80
 81    # The level name within the campaign used in co-op session mode.
 82    # For campaign name FOO, do print(ba.app.campaigns['FOO'].levels) to see
 83    # available level names.
 84    coop_level: str = 'Onslaught Training'
 85
 86    # Whether to enable telnet access.
 87    # IMPORTANT: This option is no longer available, as it was being used
 88    # for exploits. Live access to the running server is still possible through
 89    # the mgr.cmd() function in the server script. Run your server through
 90    # tools such as 'screen' or 'tmux' and you can reconnect to it remotely
 91    # over a secure ssh connection.
 92    enable_telnet: bool = False
 93
 94    # Series length in teams mode (7 == 'best-of-7' series; a team must
 95    # get 4 wins)
 96    teams_series_length: int = 7
 97
 98    # Points to win in free-for-all mode (Points are awarded per game based on
 99    # performance)
100    ffa_series_length: int = 24
101
102    # If you have a custom stats webpage for your server, you can use this
103    # to provide a convenient in-game link to it in the server-browser
104    # alongside the server name.
105    # if ${ACCOUNT} is present in the string, it will be replaced by the
106    # currently-signed-in account's id. To fetch info about an account,
107    # your back-end server can use the following url:
108    # https://legacy.ballistica.net/accountquery?id=ACCOUNT_ID_HERE
109    stats_url: str | None = None
110
111    # If present, the server subprocess will attempt to gracefully exit after
112    # this amount of time. A graceful exit can occur at the end of a series
113    # or other opportune time. Server-managers set to auto-restart (the
114    # default) will then spin up a fresh subprocess. This mechanism can be
115    # useful to clear out any memory leaks or other accumulated bad state
116    # in the server subprocess.
117    clean_exit_minutes: float | None = None
118
119    # If present, the server subprocess will shut down immediately after this
120    # amount of time. This can be useful as a fallback for clean_exit_time.
121    # The server manager will then spin up a fresh server subprocess if
122    # auto-restart is enabled (the default).
123    unclean_exit_minutes: float | None = None
124
125    # If present, the server subprocess will shut down immediately if this
126    # amount of time passes with no activity from any players. The server
127    # manager will then spin up a fresh server subprocess if auto-restart is
128    # enabled (the default).
129    idle_exit_minutes: float | None = None
130
131    # Should the tutorial be shown at the beginning of games?
132    show_tutorial: bool = False
133
134    # Team names (teams mode only).
135    team_names: tuple[str, str] | None = None
136
137    # Team colors (teams mode only).
138    team_colors: tuple[
139        tuple[float, float, float], tuple[float, float, float]
140    ] | None = None
141
142    # Whether to enable the queue where players can line up before entering
143    # your server. Disabling this can be used as a workaround to deal with
144    # queue spamming attacks.
145    enable_queue: bool = True
146
147    # (internal) stress-testing mode.
148    stress_test_players: int | 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, port: int = 43210, max_party_size: int = 6, 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, stress_test_players: int | None = None)
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_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
stress_test_players: int | None = None
class ServerCommand:
154class ServerCommand:
155    """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):
158@dataclass
159class StartServerModeCommand(ServerCommand):
160    """Tells the app to switch into 'server' mode."""
161
162    config: ServerConfig

Tells the app to switch into 'server' mode.

StartServerModeCommand(config: ServerConfig)
config: ServerConfig
class ShutdownReason(enum.Enum):
165class ShutdownReason(Enum):
166    """Reason a server is shutting down."""
167
168    NONE = 'none'
169    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):
172@dataclass
173class ShutdownCommand(ServerCommand):
174    """Tells the server to shut down."""
175
176    reason: ShutdownReason
177    immediate: bool

Tells the server to shut down.

ShutdownCommand(reason: ShutdownReason, immediate: bool)
reason: ShutdownReason
immediate: bool
@dataclass
class ChatMessageCommand(ServerCommand):
180@dataclass
181class ChatMessageCommand(ServerCommand):
182    """Chat message from the server."""
183
184    message: str
185    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):
188@dataclass
189class ScreenMessageCommand(ServerCommand):
190    """Screen-message from the server."""
191
192    message: str
193    color: tuple[float, float, float] | None
194    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):
197@dataclass
198class ClientListCommand(ServerCommand):
199    """Print a list of clients."""

Print a list of clients.

@dataclass
class KickCommand(ServerCommand):
202@dataclass
203class KickCommand(ServerCommand):
204    """Kick a client."""
205
206    client_id: int
207    ban_time: int | None

Kick a client.

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