mirror of
https://github.com/RYDE-WORK/ballistica.git
synced 2026-01-23 07:23:19 +08:00
Added 'clientlist' and 'kick' server commands
This commit is contained in:
parent
c4f06f5f96
commit
acaa9a90eb
@ -4132,16 +4132,16 @@
|
||||
"assets/build/windows/x64/python.exe": "https://files.ballistica.net/cache/ba1/25/a7/dc87c1be41605eb6fefd0145144c",
|
||||
"assets/build/windows/x64/python37.dll": "https://files.ballistica.net/cache/ba1/b9/e4/d912f56e42e9991bcbb4c804cfcb",
|
||||
"assets/build/windows/x64/pythonw.exe": "https://files.ballistica.net/cache/ba1/6c/bb/b6f52c306aa4e88061510e96cefe",
|
||||
"build/prefab/linux-server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/fe/a7/f1a153619272b4882dc0d1fcf228",
|
||||
"build/prefab/linux-server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/bd/08/3a948bec6a0553109361ab201b7f",
|
||||
"build/prefab/linux/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/69/4e/34a3913539ae323f32c6029aee61",
|
||||
"build/prefab/linux/release/ballisticacore": "https://files.ballistica.net/cache/ba1/c7/2c/3b0f126c249753469779ac7abd99",
|
||||
"build/prefab/mac-server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/9a/f5/252523aff43024c6ced461abcc04",
|
||||
"build/prefab/mac-server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/12/f5/35f0e8d8259c37389fe2d60becbc",
|
||||
"build/prefab/mac/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/ca/ad/c4e22ab2e7de1356f47aa4f8dc07",
|
||||
"build/prefab/mac/release/ballisticacore": "https://files.ballistica.net/cache/ba1/02/dc/bea2f3a3a862b9e587043f6bd73f",
|
||||
"build/prefab/windows-server/debug/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/d7/df/80ceb60dc041e2b9a69f0a3da9ea",
|
||||
"build/prefab/windows-server/release/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/b1/86/dddc66545c917e3a028fc6a6caea",
|
||||
"build/prefab/windows/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/a2/69/b183a77cd9e8a91ef550c4e7bcec",
|
||||
"build/prefab/windows/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/a0/17/d7aca9e4c6e604993328cd389356"
|
||||
"build/prefab/linux-server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/a2/5b/8598da1884bbe060f495d0273d7d",
|
||||
"build/prefab/linux-server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/83/95/5e30fd47e6befa8f9a370a52be21",
|
||||
"build/prefab/linux/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/60/3a/1edfbfc7adcbda3f6734f9282756",
|
||||
"build/prefab/linux/release/ballisticacore": "https://files.ballistica.net/cache/ba1/34/a1/f7d0bf6b709a757418825a1cc24f",
|
||||
"build/prefab/mac-server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/59/e9/8a29cf17f4434f783fab73afc349",
|
||||
"build/prefab/mac-server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/71/9d/02c9e2ba7c91941991552228c66c",
|
||||
"build/prefab/mac/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/9b/35/42e8f879ac5b07ba2d8caee897dc",
|
||||
"build/prefab/mac/release/ballisticacore": "https://files.ballistica.net/cache/ba1/3f/3e/20eb6ac1043ac0417c2b7728f7bd",
|
||||
"build/prefab/windows-server/debug/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/b9/75/6b1523ec9b0d510870b4695a6613",
|
||||
"build/prefab/windows-server/release/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/7d/04/b882bbfd0197248bca49016356c1",
|
||||
"build/prefab/windows/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/13/50/1fb612a251431229703317bd81e5",
|
||||
"build/prefab/windows/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/ec/3c/ce84db1040251fb5a11b0c7dfd7b"
|
||||
}
|
||||
2
.idea/dictionaries/ericf.xml
generated
2
.idea/dictionaries/ericf.xml
generated
@ -296,6 +296,7 @@
|
||||
<w>cleancheck</w>
|
||||
<w>cleanlist</w>
|
||||
<w>clientid</w>
|
||||
<w>clientlist</w>
|
||||
<w>clionbin</w>
|
||||
<w>clioncode</w>
|
||||
<w>clionroot</w>
|
||||
@ -1956,6 +1957,7 @@
|
||||
<w>vartype</w>
|
||||
<w>vcxproj</w>
|
||||
<w>venv</w>
|
||||
<w>versioning</w>
|
||||
<w>versionpredicate</w>
|
||||
<w>vert</w>
|
||||
<w>verts</w>
|
||||
|
||||
@ -30,7 +30,9 @@ from ba._enums import TimeType
|
||||
from ba._freeforallsession import FreeForAllSession
|
||||
from ba._dualteamsession import DualTeamSession
|
||||
from bacommon.servermanager import (ServerCommand, StartServerModeCommand,
|
||||
ShutdownCommand, ShutdownReason)
|
||||
ShutdownCommand, ShutdownReason,
|
||||
BroadcastCommand, ClientListCommand,
|
||||
KickCommand)
|
||||
import _ba
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@ -56,6 +58,22 @@ def _cmd(command_data: bytes) -> None:
|
||||
immediate=command.immediate)
|
||||
return
|
||||
|
||||
if isinstance(command, BroadcastCommand):
|
||||
assert _ba.app.server is not None
|
||||
_ba.app.server.broadcast_message(command.message)
|
||||
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}')
|
||||
|
||||
@ -93,6 +111,48 @@ class ServerController:
|
||||
timetype=TimeType.REAL,
|
||||
repeat=True)
|
||||
|
||||
def broadcast_message(self, message: str) -> None:
|
||||
"""Broadcast a message to all connected clients."""
|
||||
# FIXME: Should add a proper call for this, which would allow
|
||||
# us to use Lstr values and colors and whatnot.
|
||||
_ba.chat_message(message)
|
||||
|
||||
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['specString'])
|
||||
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: Optional[int]) -> 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
|
||||
@ -124,19 +184,15 @@ class ServerController:
|
||||
self._executing_shutdown = True
|
||||
timestrval = time.strftime('%c')
|
||||
if self._shutdown_reason is ShutdownReason.RESTARTING:
|
||||
# FIXME: Should add a server-screen-message call.
|
||||
# (so we could send this an an Lstr)
|
||||
_ba.chat_message(
|
||||
self.broadcast_message(
|
||||
Lstr(resource='internal.serverRestartingText').evaluate())
|
||||
print(f'{Clr.SBLU}Exiting for server-restart'
|
||||
f' at {timestrval}{Clr.RST}')
|
||||
else:
|
||||
# FIXME: Should add a server-screen-message call.
|
||||
# (so we could send this an an Lstr)
|
||||
self.broadcast_message(
|
||||
Lstr(resource='internal.serverShuttingDownText').evaluate())
|
||||
print(f'{Clr.SBLU}Exiting for server-shutdown'
|
||||
f' at {timestrval}{Clr.RST}')
|
||||
_ba.chat_message(
|
||||
Lstr(resource='internal.serverShuttingDownText').evaluate())
|
||||
with _ba.Context('ui'):
|
||||
_ba.timer(2.0, _ba.quit, timetype=TimeType.REAL)
|
||||
|
||||
@ -153,18 +209,22 @@ class ServerController:
|
||||
)
|
||||
|
||||
def _access_check_response(self, data: Optional[Dict[str, Any]]) -> None:
|
||||
gameport = _ba.get_game_port()
|
||||
# gameport = _ba.get_game_port()
|
||||
if data is None:
|
||||
print('error on UDP port access check (internet down?)')
|
||||
else:
|
||||
addr = data['address']
|
||||
port = data['port']
|
||||
if data['accessible']:
|
||||
print(f'{Clr.SBLU}UDP port {gameport} access check successful.'
|
||||
f' Your server appears to be joinable from the'
|
||||
f' internet.{Clr.RST}')
|
||||
print(f'{Clr.SBLU}Master server access check of {addr}'
|
||||
f' udp port {port} succeeded.\n'
|
||||
f'Your server appears to be'
|
||||
f' joinable from the internet.{Clr.RST}')
|
||||
else:
|
||||
print(f'{Clr.SRED}UDP port {gameport} access check failed.'
|
||||
f' Your server does not appear to be joinable'
|
||||
f' from the internet.{Clr.RST}')
|
||||
print(f'{Clr.SRED}Master server access check of {addr}'
|
||||
f' udp port {port} failed.\n'
|
||||
f'Your server does not appear to be'
|
||||
f' joinable from the internet.{Clr.RST}')
|
||||
|
||||
def _prepare_to_serve(self) -> None:
|
||||
signed_in = _ba.get_account_state() == 'signed_in'
|
||||
|
||||
@ -48,6 +48,10 @@ if TYPE_CHECKING:
|
||||
from types import FrameType
|
||||
from bacommon.servermanager import ServerCommand
|
||||
|
||||
# Not sure how much versioning we'll do with this, but this will get
|
||||
# printed at startup in case we need it.
|
||||
VERSION_STR = '1.0'
|
||||
|
||||
|
||||
class ServerManagerApp:
|
||||
"""An app which manages BallisticaCore server execution.
|
||||
@ -101,7 +105,8 @@ class ServerManagerApp:
|
||||
|
||||
# Print basic usage info in interactive mode.
|
||||
if sys.stdin.isatty():
|
||||
print(f'{Clr.SMAG}BallisticaCore server manager starting up...\n'
|
||||
print(f'{Clr.SMAG}BallisticaCore server manager {VERSION_STR}'
|
||||
f' starting up...\n'
|
||||
f'Use the "mgr" object to make live server adjustments.\n'
|
||||
f'Type "help(mgr)" for more information.{Clr.RST}')
|
||||
|
||||
@ -115,22 +120,19 @@ class ServerManagerApp:
|
||||
self._process_thread = Thread(target=self._bg_thread_main)
|
||||
self._process_thread.start()
|
||||
|
||||
# According to Python docs, default locals dict has __name__ set
|
||||
# to __console__ and __doc__ set to None; using that as start point.
|
||||
# https://docs.python.org/3/library/code.html
|
||||
locs = {'__name__': '__console__', '__doc__': None, 'mgr': self}
|
||||
context = {'__name__': '__console__', '__doc__': None, 'mgr': self}
|
||||
|
||||
# Enable tab-completion if possible.
|
||||
self._enable_tab_completion(locs)
|
||||
self._enable_tab_completion(context)
|
||||
|
||||
# Now just sit in an interpreter.
|
||||
# TODO: make it possible to use IPython if the user has it available.
|
||||
try:
|
||||
code.interact(local=locs, banner='', exitmsg='')
|
||||
code.interact(local=context, banner='', exitmsg='')
|
||||
except SystemExit:
|
||||
# We get this from the builtin quit(), etc.
|
||||
# Need to catch this so we can clean up, otherwise we'll be
|
||||
# left in limbo with our BG thread still running.
|
||||
# left in limbo with our process thread still running.
|
||||
pass
|
||||
except BaseException as exc:
|
||||
print(f'{Clr.SRED}Unexpected interpreter exception:'
|
||||
@ -150,7 +152,9 @@ class ServerManagerApp:
|
||||
raise TypeError(f'Expected a string arg; got {type(statement)}')
|
||||
with self._process_commands_lock:
|
||||
self._process_commands.append(statement)
|
||||
self._block_for_command_completion()
|
||||
|
||||
def _block_for_command_completion(self) -> None:
|
||||
# Ideally we'd block here until the command was run so our prompt would
|
||||
# print after it's results. We currently don't get any response from
|
||||
# the app so the best we can do is block until our bg thread has sent
|
||||
@ -166,6 +170,29 @@ class ServerManagerApp:
|
||||
# we'll hopefully still give it enough time to process/print.
|
||||
time.sleep(0.1)
|
||||
|
||||
def broadcast(self, message: str) -> None:
|
||||
"""Broadcast a message to all connected clients."""
|
||||
from bacommon.servermanager import BroadcastCommand
|
||||
self._enqueue_server_command(BroadcastCommand(message=message))
|
||||
|
||||
def clientlist(self) -> None:
|
||||
"""Print a list of connected clients."""
|
||||
from bacommon.servermanager import ClientListCommand
|
||||
self._enqueue_server_command(ClientListCommand())
|
||||
self._block_for_command_completion()
|
||||
|
||||
def kick(self, client_id: int, ban_time: Optional[int] = None) -> None:
|
||||
"""Kick the client with the provided id.
|
||||
|
||||
If ban_time is provided, the client will be banned for that
|
||||
length of time in seconds. If it is None, ban duration will
|
||||
be determined automatically. Pass 0 or a negative number for no
|
||||
ban time.
|
||||
"""
|
||||
from bacommon.servermanager import KickCommand
|
||||
self._enqueue_server_command(
|
||||
KickCommand(client_id=client_id, ban_time=ban_time))
|
||||
|
||||
def restart(self, immediate: bool = False) -> None:
|
||||
"""Restart the server child-process.
|
||||
|
||||
@ -360,10 +387,15 @@ class ServerManagerApp:
|
||||
print(f'{Clr.SMAG}Server process stopped.{Clr.RST}')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
def main() -> None:
|
||||
"""Run a BallisticaCore server manager in interactive mode."""
|
||||
try:
|
||||
ServerManagerApp().run_interactive()
|
||||
except CleanError as clean_exc:
|
||||
except CleanError as exc:
|
||||
# For clean errors, do a simple print and fail; no tracebacks/etc.
|
||||
clean_exc.pretty_print()
|
||||
exc.pretty_print()
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
@ -3988,11 +3988,17 @@ cause the powerup box to make a sound and disappear or whatnot.</p>
|
||||
</p>
|
||||
|
||||
<h3>Methods:</h3>
|
||||
<h5><a href="#method_ba_ServerController____init__"><constructor></a>, <a href="#method_ba_ServerController__handle_transition">handle_transition()</a>, <a href="#method_ba_ServerController__shutdown">shutdown()</a></h5>
|
||||
<h5><a href="#method_ba_ServerController____init__"><constructor></a>, <a href="#method_ba_ServerController__broadcast_message">broadcast_message()</a>, <a href="#method_ba_ServerController__handle_transition">handle_transition()</a>, <a href="#method_ba_ServerController__kick">kick()</a>, <a href="#method_ba_ServerController__print_client_list">print_client_list()</a>, <a href="#method_ba_ServerController__shutdown">shutdown()</a></h5>
|
||||
<dl>
|
||||
<dt><h4><a name="method_ba_ServerController____init__"><constructor></a></dt></h4><dd>
|
||||
<p><span>ba.ServerController(config: ServerConfig)</span></p>
|
||||
|
||||
</dd>
|
||||
<dt><h4><a name="method_ba_ServerController__broadcast_message">broadcast_message()</a></dt></h4><dd>
|
||||
<p><span>broadcast_message(self, message: str) -> None</span></p>
|
||||
|
||||
<p>Broadcast a message to all connected clients.</p>
|
||||
|
||||
</dd>
|
||||
<dt><h4><a name="method_ba_ServerController__handle_transition">handle_transition()</a></dt></h4><dd>
|
||||
<p><span>handle_transition(self) -> bool</span></p>
|
||||
@ -4004,6 +4010,22 @@ 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.</p>
|
||||
|
||||
</dd>
|
||||
<dt><h4><a name="method_ba_ServerController__kick">kick()</a></dt></h4><dd>
|
||||
<p><span>kick(self, client_id: int, ban_time: Optional[int]) -> None</span></p>
|
||||
|
||||
<p>Kick the provided client id.</p>
|
||||
|
||||
<p>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.</p>
|
||||
|
||||
</dd>
|
||||
<dt><h4><a name="method_ba_ServerController__print_client_list">print_client_list()</a></dt></h4><dd>
|
||||
<p><span>print_client_list(self) -> None</span></p>
|
||||
|
||||
<p>Print info about all connected clients.</p>
|
||||
|
||||
</dd>
|
||||
<dt><h4><a name="method_ba_ServerController__shutdown">shutdown()</a></dt></h4><dd>
|
||||
<p><span>shutdown(self, reason: ShutdownReason, immediate: bool) -> None</span></p>
|
||||
|
||||
@ -121,3 +121,21 @@ class ShutdownCommand(ServerCommand):
|
||||
"""Tells the server to shut down."""
|
||||
reason: ShutdownReason
|
||||
immediate: bool
|
||||
|
||||
|
||||
@dataclass
|
||||
class BroadcastCommand(ServerCommand):
|
||||
"""Broadcast a message to all clients."""
|
||||
message: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class ClientListCommand(ServerCommand):
|
||||
"""Print a list of clients."""
|
||||
|
||||
|
||||
@dataclass
|
||||
class KickCommand(ServerCommand):
|
||||
"""Kick a client."""
|
||||
client_id: int
|
||||
ban_time: Optional[int]
|
||||
|
||||
@ -69,7 +69,7 @@ def dataclass_assign(instance: Any, values: Dict[str, Any]) -> None:
|
||||
for key, value in values.items():
|
||||
if key not in fieldsdict:
|
||||
raise AttributeError(
|
||||
f"'{type(instance).__name__}' has no '{key}' field")
|
||||
f"'{type(instance).__name__}' has no '{key}' field.")
|
||||
field = fieldsdict[key]
|
||||
|
||||
# We expect to be operating under 'from __future__ import annotations'
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user