mirror of
https://github.com/hypervortex/VH-Bombsquad-Modded-Server-Files
synced 2025-10-16 12:02:51 +00:00
updated files
This commit is contained in:
parent
5ba4986d59
commit
7a5e7698c0
1269 changed files with 551814 additions and 0 deletions
456
dist/ba_data/python/ba/_servermode.py
vendored
Normal file
456
dist/ba_data/python/ba/_servermode.py
vendored
Normal file
|
|
@ -0,0 +1,456 @@
|
|||
# Released under the MIT License. See LICENSE for details.
|
||||
#
|
||||
"""Functionality related to running the game in server-mode."""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import time
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from efro.terminal import Clr
|
||||
from bacommon.servermanager import (
|
||||
ServerCommand,
|
||||
StartServerModeCommand,
|
||||
ShutdownCommand,
|
||||
ShutdownReason,
|
||||
ChatMessageCommand,
|
||||
ScreenMessageCommand,
|
||||
ClientListCommand,
|
||||
KickCommand,
|
||||
)
|
||||
import _ba
|
||||
from ba._internal import add_transaction, run_transactions, get_v1_account_state
|
||||
from ba._generated.enums import TimeType
|
||||
from ba._freeforallsession import FreeForAllSession
|
||||
from ba._dualteamsession import DualTeamSession
|
||||
from ba._coopsession import CoopSession
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any
|
||||
|
||||
import ba
|
||||
from bacommon.servermanager import ServerConfig
|
||||
|
||||
|
||||
def _cmd(command_data: bytes) -> None:
|
||||
"""Handle commands coming in from our server manager parent process."""
|
||||
import pickle
|
||||
|
||||
command = pickle.loads(command_data)
|
||||
assert isinstance(command, ServerCommand)
|
||||
|
||||
if isinstance(command, StartServerModeCommand):
|
||||
assert _ba.app.server is None
|
||||
_ba.app.server = ServerController(command.config)
|
||||
return
|
||||
|
||||
if isinstance(command, ShutdownCommand):
|
||||
assert _ba.app.server is not None
|
||||
_ba.app.server.shutdown(
|
||||
reason=command.reason, immediate=command.immediate
|
||||
)
|
||||
return
|
||||
|
||||
if isinstance(command, ChatMessageCommand):
|
||||
assert _ba.app.server is not None
|
||||
_ba.chatmessage(command.message, clients=command.clients)
|
||||
return
|
||||
|
||||
if isinstance(command, ScreenMessageCommand):
|
||||
assert _ba.app.server is not None
|
||||
|
||||
# Note: we have to do transient messages if
|
||||
# clients is specified, so they won't show up
|
||||
# in replays.
|
||||
_ba.screenmessage(
|
||||
command.message,
|
||||
color=command.color,
|
||||
clients=command.clients,
|
||||
transient=command.clients is not None,
|
||||
)
|
||||
return
|
||||
|
||||
if isinstance(command, ClientListCommand):
|
||||
assert _ba.app.server is not None
|
||||
_ba.app.server.print_client_list()
|
||||
return
|
||||
|
||||
if isinstance(command, KickCommand):
|
||||
assert _ba.app.server is not None
|
||||
_ba.app.server.kick(
|
||||
client_id=command.client_id, ban_time=command.ban_time
|
||||
)
|
||||
return
|
||||
|
||||
print(
|
||||
f'{Clr.SRED}ERROR: server process'
|
||||
f' got unknown command: {type(command)}{Clr.RST}'
|
||||
)
|
||||
|
||||
|
||||
class ServerController:
|
||||
"""Overall controller for the app in server mode.
|
||||
|
||||
Category: **App Classes**
|
||||
"""
|
||||
|
||||
def __init__(self, config: ServerConfig) -> None:
|
||||
|
||||
self._config = config
|
||||
self._playlist_name = '__default__'
|
||||
self._ran_access_check = False
|
||||
self._prep_timer: ba.Timer | None = None
|
||||
self._next_stuck_login_warn_time = time.time() + 10.0
|
||||
self._first_run = True
|
||||
self._shutdown_reason: ShutdownReason | None = None
|
||||
self._executing_shutdown = False
|
||||
|
||||
# Make note if they want us to import a playlist;
|
||||
# we'll need to do that first if so.
|
||||
self._playlist_fetch_running = self._config.playlist_code is not None
|
||||
self._playlist_fetch_sent_request = False
|
||||
self._playlist_fetch_got_response = False
|
||||
self._playlist_fetch_code = -1
|
||||
|
||||
# Now sit around doing any pre-launch prep such as waiting for
|
||||
# account sign-in or fetching playlists; this will kick off the
|
||||
# session once done.
|
||||
with _ba.Context('ui'):
|
||||
self._prep_timer = _ba.Timer(
|
||||
0.25,
|
||||
self._prepare_to_serve,
|
||||
timetype=TimeType.REAL,
|
||||
repeat=True,
|
||||
)
|
||||
|
||||
def print_client_list(self) -> None:
|
||||
"""Print info about all connected clients."""
|
||||
import json
|
||||
|
||||
roster = _ba.get_game_roster()
|
||||
title1 = 'Client ID'
|
||||
title2 = 'Account Name'
|
||||
title3 = 'Players'
|
||||
col1 = 10
|
||||
col2 = 16
|
||||
out = (
|
||||
f'{Clr.BLD}'
|
||||
f'{title1:<{col1}} {title2:<{col2}} {title3}'
|
||||
f'{Clr.RST}'
|
||||
)
|
||||
for client in roster:
|
||||
if client['client_id'] == -1:
|
||||
continue
|
||||
spec = json.loads(client['spec_string'])
|
||||
name = spec['n']
|
||||
players = ', '.join(n['name'] for n in client['players'])
|
||||
clientid = client['client_id']
|
||||
out += f'\n{clientid:<{col1}} {name:<{col2}} {players}'
|
||||
print(out)
|
||||
|
||||
def kick(self, client_id: int, ban_time: int | None) -> None:
|
||||
"""Kick the provided client id.
|
||||
|
||||
ban_time is provided in seconds.
|
||||
If ban_time is None, ban duration will be determined automatically.
|
||||
Pass 0 or a negative number for no ban time.
|
||||
"""
|
||||
|
||||
# FIXME: this case should be handled under the hood.
|
||||
if ban_time is None:
|
||||
ban_time = 300
|
||||
|
||||
_ba.disconnect_client(client_id=client_id, ban_time=ban_time)
|
||||
|
||||
def shutdown(self, reason: ShutdownReason, immediate: bool) -> None:
|
||||
"""Set the app to quit either now or at the next clean opportunity."""
|
||||
self._shutdown_reason = reason
|
||||
if immediate:
|
||||
print(f'{Clr.SBLU}Immediate shutdown initiated.{Clr.RST}')
|
||||
self._execute_shutdown()
|
||||
else:
|
||||
print(
|
||||
f'{Clr.SBLU}Shutdown initiated;'
|
||||
f' server process will exit at the next clean opportunity.'
|
||||
f'{Clr.RST}'
|
||||
)
|
||||
|
||||
def handle_transition(self) -> bool:
|
||||
"""Handle transitioning to a new ba.Session or quitting the app.
|
||||
|
||||
Will be called once at the end of an activity that is marked as
|
||||
a good 'end-point' (such as a final score screen).
|
||||
Should return True if action will be handled by us; False if the
|
||||
session should just continue on it's merry way.
|
||||
"""
|
||||
if self._shutdown_reason is not None:
|
||||
self._execute_shutdown()
|
||||
return True
|
||||
return False
|
||||
|
||||
def _execute_shutdown(self) -> None:
|
||||
from ba._language import Lstr
|
||||
|
||||
if self._executing_shutdown:
|
||||
return
|
||||
self._executing_shutdown = True
|
||||
timestrval = time.strftime('%c')
|
||||
if self._shutdown_reason is ShutdownReason.RESTARTING:
|
||||
_ba.screenmessage(
|
||||
Lstr(resource='internal.serverRestartingText'),
|
||||
color=(1, 0.5, 0.0),
|
||||
)
|
||||
print(
|
||||
f'{Clr.SBLU}Exiting for server-restart'
|
||||
f' at {timestrval}.{Clr.RST}'
|
||||
)
|
||||
else:
|
||||
_ba.screenmessage(
|
||||
Lstr(resource='internal.serverShuttingDownText'),
|
||||
color=(1, 0.5, 0.0),
|
||||
)
|
||||
print(
|
||||
f'{Clr.SBLU}Exiting for server-shutdown'
|
||||
f' at {timestrval}.{Clr.RST}'
|
||||
)
|
||||
with _ba.Context('ui'):
|
||||
_ba.timer(2.0, _ba.quit, timetype=TimeType.REAL)
|
||||
|
||||
def _run_access_check(self) -> None:
|
||||
"""Check with the master server to see if we're likely joinable."""
|
||||
from ba._net import master_server_get
|
||||
|
||||
master_server_get(
|
||||
'bsAccessCheck',
|
||||
{'port': _ba.get_game_port(), 'b': _ba.app.build_number},
|
||||
callback=self._access_check_response,
|
||||
)
|
||||
|
||||
def _access_check_response(self, data: dict[str, Any] | None) -> None:
|
||||
import os
|
||||
|
||||
if data is None:
|
||||
print('error on UDP port access check (internet down?)')
|
||||
else:
|
||||
addr = data['address']
|
||||
port = data['port']
|
||||
show_addr = True
|
||||
if show_addr:
|
||||
addrstr = f' {addr}'
|
||||
poststr = ''
|
||||
else:
|
||||
addrstr = ''
|
||||
poststr = (
|
||||
'\nSet environment variable BA_ACCESS_CHECK_VERBOSE=1'
|
||||
' for more info.'
|
||||
)
|
||||
if data['accessible']:
|
||||
print(
|
||||
f'{Clr.SBLU}Master server access check of{addrstr}'
|
||||
f' udp port {port} succeeded.\n'
|
||||
f'Your server appears to be'
|
||||
f' joinable from the internet .{poststr}{Clr.RST}'
|
||||
)
|
||||
if self._config.party_is_public:
|
||||
print(
|
||||
f'{Clr.SBLU}Your party {self._config.party_name}'
|
||||
f' visible in public party list.{Clr.RST}'
|
||||
)
|
||||
else:
|
||||
print(
|
||||
f'{Clr.SBLU}Your private party {self._config.party_name}'
|
||||
f'can be joined by {addrstr} {port}.{Clr.RST}'
|
||||
)
|
||||
else:
|
||||
print(
|
||||
f'{Clr.SRED}Master server access check of{addrstr}'
|
||||
f' udp port {port} failed.\n'
|
||||
f'Your server does not appear to be'
|
||||
f' joinable from the internet. Please check your firewall or instance security group.{poststr}{Clr.RST}'
|
||||
)
|
||||
|
||||
def _prepare_to_serve(self) -> None:
|
||||
"""Run in a timer to do prep before beginning to serve."""
|
||||
signed_in = get_v1_account_state() == 'signed_in'
|
||||
if not signed_in:
|
||||
|
||||
# Signing in to the local server account should not take long;
|
||||
# complain if it does...
|
||||
curtime = time.time()
|
||||
if curtime > self._next_stuck_login_warn_time:
|
||||
print('Still waiting for account sign-in...')
|
||||
self._next_stuck_login_warn_time = curtime + 10.0
|
||||
return
|
||||
|
||||
can_launch = False
|
||||
|
||||
# If we're fetching a playlist, we need to do that first.
|
||||
if not self._playlist_fetch_running:
|
||||
can_launch = True
|
||||
else:
|
||||
if not self._playlist_fetch_sent_request:
|
||||
print(
|
||||
f'{Clr.SBLU}Requesting shared-playlist'
|
||||
f' {self._config.playlist_code}...{Clr.RST}'
|
||||
)
|
||||
add_transaction(
|
||||
{
|
||||
'type': 'IMPORT_PLAYLIST',
|
||||
'code': str(self._config.playlist_code),
|
||||
'overwrite': True,
|
||||
},
|
||||
callback=self._on_playlist_fetch_response,
|
||||
)
|
||||
run_transactions()
|
||||
self._playlist_fetch_sent_request = True
|
||||
|
||||
if self._playlist_fetch_got_response:
|
||||
self._playlist_fetch_running = False
|
||||
can_launch = True
|
||||
|
||||
if can_launch:
|
||||
self._prep_timer = None
|
||||
_ba.pushcall(self._launch_server_session)
|
||||
|
||||
def _on_playlist_fetch_response(
|
||||
self,
|
||||
result: dict[str, Any] | None,
|
||||
) -> None:
|
||||
if result is None:
|
||||
print('Error fetching playlist; aborting.')
|
||||
print('Falling back to use default playlist')
|
||||
self._config.session_type = 'teams'
|
||||
self._prep_timer = None
|
||||
_ba.pushcall(self._launch_server_session)
|
||||
return
|
||||
# Once we get here, simply modify our config to use this playlist.
|
||||
typename = (
|
||||
'teams'
|
||||
if result['playlistType'] == 'Team Tournament'
|
||||
else 'ffa'
|
||||
if result['playlistType'] == 'Free-for-All'
|
||||
else '??'
|
||||
)
|
||||
plistname = result['playlistName']
|
||||
print(f'{Clr.SBLU}Got playlist: "{plistname}" ({typename}).{Clr.RST}')
|
||||
self._playlist_fetch_got_response = True
|
||||
self._config.session_type = typename
|
||||
self._playlist_name = result['playlistName']
|
||||
|
||||
def _get_session_type(self) -> type[ba.Session]:
|
||||
# Convert string session type to the class.
|
||||
# Hmm should we just keep this as a string?
|
||||
if self._config.session_type == 'ffa':
|
||||
return FreeForAllSession
|
||||
if self._config.session_type == 'teams':
|
||||
return DualTeamSession
|
||||
if self._config.session_type == 'coop':
|
||||
return CoopSession
|
||||
raise RuntimeError(
|
||||
f'Invalid session_type: "{self._config.session_type}"'
|
||||
)
|
||||
|
||||
def _launch_server_session(self) -> None:
|
||||
"""Kick off a host-session based on the current server config."""
|
||||
# pylint: disable=too-many-branches
|
||||
app = _ba.app
|
||||
appcfg = app.config
|
||||
sessiontype = self._get_session_type()
|
||||
|
||||
if get_v1_account_state() != 'signed_in':
|
||||
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'
|
||||
elif sessiontype is CoopSession:
|
||||
ptypename = 'Coop'
|
||||
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.
|
||||
add_transaction(
|
||||
{
|
||||
'type': 'ADD_PLAYLIST',
|
||||
'playlistType': ptypename,
|
||||
'playlistName': self._playlist_name,
|
||||
'playlist': self._config.playlist_inline,
|
||||
}
|
||||
)
|
||||
run_transactions()
|
||||
|
||||
if self._first_run:
|
||||
curtimestr = time.strftime('%c')
|
||||
startupmsg = (
|
||||
f'{Clr.BLD}{Clr.BLU}{_ba.appnameupper()} {app.version}'
|
||||
f' ({app.build_number})'
|
||||
f' entering server-mode {curtimestr}{Clr.RST}'
|
||||
)
|
||||
logging.info(startupmsg)
|
||||
|
||||
if sessiontype is FreeForAllSession:
|
||||
appcfg['Free-for-All Playlist Selection'] = self._playlist_name
|
||||
appcfg[
|
||||
'Free-for-All Playlist Randomize'
|
||||
] = self._config.playlist_shuffle
|
||||
elif sessiontype is DualTeamSession:
|
||||
appcfg['Team Tournament Playlist Selection'] = self._playlist_name
|
||||
appcfg[
|
||||
'Team Tournament Playlist Randomize'
|
||||
] = self._config.playlist_shuffle
|
||||
elif sessiontype is CoopSession:
|
||||
app.coop_session_args = {
|
||||
'campaign': self._config.coop_campaign,
|
||||
'level': self._config.coop_level,
|
||||
}
|
||||
else:
|
||||
raise RuntimeError(f'Unknown session type {sessiontype}')
|
||||
|
||||
app.teams_series_length = self._config.teams_series_length
|
||||
app.ffa_series_length = self._config.ffa_series_length
|
||||
|
||||
_ba.set_authenticate_clients(self._config.authenticate_clients)
|
||||
|
||||
_ba.set_enable_default_kick_voting(
|
||||
self._config.enable_default_kick_voting
|
||||
)
|
||||
_ba.set_admins(self._config.admins)
|
||||
|
||||
# Call set-enabled last (will push state to the cloud).
|
||||
_ba.set_public_party_max_size(self._config.max_party_size)
|
||||
_ba.set_public_party_queue_enabled(self._config.enable_queue)
|
||||
_ba.set_public_party_name(self._config.party_name)
|
||||
_ba.set_public_party_stats_url(self._config.stats_url)
|
||||
_ba.set_public_party_enabled(self._config.party_is_public)
|
||||
|
||||
# And here.. we.. go.
|
||||
if self._config.stress_test_players is not None:
|
||||
# Special case: run a stress test.
|
||||
from ba.internal import run_stress_test
|
||||
|
||||
run_stress_test(
|
||||
playlist_type='Random',
|
||||
playlist_name='__default__',
|
||||
player_count=self._config.stress_test_players,
|
||||
round_duration=30,
|
||||
)
|
||||
else:
|
||||
_ba.new_host_session(sessiontype)
|
||||
|
||||
# Run an access check if we're trying to make a public party.
|
||||
if not self._ran_access_check:
|
||||
self._run_access_check()
|
||||
self._ran_access_check = True
|
||||
Loading…
Add table
Add a link
Reference in a new issue