Serverside Demo Recording Options - Notavi - 08-18-2018
So, I'm trying to figure out a way to have a votable (i.e. "recordgame" / "recordduel") or something that allows people to have the current game recorded (ideally as a MVD recorded server side, but if that's not on the cards maybe by having 'spectator bots' join and record).
I'd ideally also like to find a way to trigger a script serverside at the end of the match, so it can pick up the finished demo and do something with it (save it somewhere, upload and link it on IRC, etc).
Is there anything people could point me towards that would help out with this?
RE: Serverside Demo Recording Options - BuddyFriendGuy - 08-19-2018
I used to record every single game on the server side, but then after a few months, I realized that I never touched any of them so I stopped doing so (and my very basic machine started performing better for the actual game). I was gonna serve the demo files on the same server to browsers.
I remember hacking together the following script at some point. It injects a programmable layer between user and Xonotic console. Originally I just used it to pause the server when there's no player (so my log doesn't keep getting bigger and bigger), but you can modify it to do some server tasks. For example, you can record all the demos, and watch the console message for demo-voting messages, and decide whether to keep the demo, move it to web server directory, or delete the demo.
Code: #!/usr/bin/python -u
#from __future__ import absolute_import
#from __future__ import print_function
#from __future__ import unicode_literals
import struct
import fcntl
import termios
import signal
import sys
#sys.path.append("./pexpect")
import pexpect
import re
class XonoticServerConsoleMonitor:
"""
launches Xonotic and watches the console message,
sends out appropriate commands when necessary
"""
xonoticSpawn = None
activePlayers = set()
xonotic_startup = "/home/bfg/bin/xonotic-server.pl"
xonotic_params = ["regular"]
regex_output = re.compile(r"^\[(\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d)\] (.*)[\s\n\r]*$")
# variables to keep track of active players
who_mode = False # who_mode is set to true when this monitor script issues a "who" command
old_numActivePlayers = 0 # the number of active players before a "who" command
game_paused = False # is the game in timeout?
# helper functions
def sigwinch_passthrough (self, sig, data):
"""support terminal windows size change"""
s = struct.pack("HHHH", 0, 0, 0, 0)
a = struct.unpack('hhhh', fcntl.ioctl(sys.stdout.fileno(),
termios.TIOCGWINSZ , s))
self.xonoticSpawn.setwinsize(a[0],a[1])
def b2s(self, b):
return bytearray(b).decode('UTF-8', errors='ignore')
def s2b(self, s):
return bytearray(s,'UTF-8')
class MatchStore:
match = None
def search(self, pattern, s, store):
match = re.search(pattern, s)
store.match = match
return match is not None
def input_filter(self, b):
"""transform input (bytearray to bytearray)"""
return b
def output_filter(self, b):
"""transform output (bytearray to bytearray)"""
s = self.b2s(b)
for line in s.splitlines():
# strip color
line = re.sub(r"\x1b\[.*?m", "", line)
# strip space
line = line.strip()
# match
result = self.regex_output.search(line)
if result:
timestamp, message = result.group(1,2)
#print("==>"+message+"\r\n")
where = self.MatchStore()
# the who commands returns something like this
#who
#[2015-09-14 23:33:37] List of client information:
#[2015-09-14 23:33:37] ent nickname ping pl time ip crypto_id
#[2015-09-14 23:33:37] #1 BuddyFriendGuy 52 0 00:00:32 71.58.222.11 dIeNu8297adfhkjl4Dspj9ie=
#[2015-09-14 23:33:37] #2 {bfgbot} Hellfire 0 0 00:00:32 null/botclient null/botclient
#[2015-09-14 23:33:37] #3 {bfgbot} Dominator 0 0 00:00:32 null/botclient null/botclient
#[2015-09-14 23:33:37] Finished listing 3 client(s) out of 16 slots.
if self.who_mode:
if self.search(r"^List of client information:$", message, where):
self.old_numActivePlayers = len(self.activePlayers)
self.activePlayers = set()
elif self.search(r"^\s*#(\d+)\s+(.+)\s+(\d+)\s+(\d+)\s+(\d\d:\d\d:\d\d)\s+(\S+)\s+(\S+)\s*$", message, where):
ent, nickname, ping, pl, time, ip, crypto_id = where.match.group(1,2,3,4,5,6,7)
if crypto_id != "null/botclient":
self.activePlayers.add(crypto_id)
elif self.search(r"^Finished listing \d+ client\(s\) out of \d+ slots.$", message, where):
# do necessary processing according to user number change
# if all users are gone,
if len(self.activePlayers) == 0 and not self.game_paused:
# we only do time out at the beginning of the game, since it's guaranteed to be pausable (e.g. voting is not pausable)
self.xonoticSpawn.sendline("echo No players. Pause the game.")
self.xonoticSpawn.sendline("pause")
self.game_paused = True
elif self.old_numActivePlayers == 0 and len(self.activePlayers) > 0 and self.game_paused:
# first player comes
self.xonoticSpawn.sendline("echo First player joined. Resume the game.")
self.xonoticSpawn.sendline("pause") # pause is a toggle
self.game_paused = False
# update old records
self.old_numActivePlayers = len(self.activePlayers)
self.who_mode = False
elif self.search(r"^BuddyFriendGuy Fun Server \[git\] paused the game$", message, where):
self.game_paused = True
elif self.search(r"^BuddyFriendGuy Fun Server \[git\] unpaused the game$", message, where):
self.game_paused = False
elif self.search(r"^Client \"(.+)\" dropped$", message, where):
playerName = where.match.group(1)
if not self.search(r"^{bfgbot} ", playerName, where):
self.xonoticSpawn.sendline("who")
self.who_mode = True
elif self.search(r"^(.+) connected$", message, where):
playerName = where.match.group(1)
if not self.search(r"^{bfgbot} ", playerName, where):
self.xonoticSpawn.sendline("who")
self.who_mode = True
elif self.search(r"^Map ready!$", message, where):
if not self.game_paused:
self.xonoticSpawn.sendline("who")
self.who_mode = True
return b
def __init__(self):
# Note that, for Python 3 compatibility reasons, we are using spawnu and
# importing unicode_literals (above). spawnu accepts Unicode input and
# unicode_literals makes all string literals in this script Unicode by default.
self.xonoticSpawn = pexpect.spawn(self.xonotic_startup, self.xonotic_params)
signal.signal(signal.SIGWINCH, self.sigwinch_passthrough)
self.xonoticSpawn.interact(escape_character=chr(29), input_filter=None, output_filter=self.output_filter)
def __del__(self):
self.xonoticSpawn.kill(1)
print('\nXonotic server still alive:', self.xonoticSpawn.isalive())
if __name__ == "__main__":
XonoticServerConsoleMonitor()
RE: Serverside Demo Recording Options - Notavi - 08-19-2018
Ahh, that's handy. I'll have to take a closer look later and see how I can build on it.
Looking at the documentation for sv_autodemo_perclient, it has to be on at the start of the match - so presumably the best option would be a votable ('recordmatch') that sets that then restarts the current match.
Availability of that vote command will probably be restricted to duel / tourney game-modes (since those seem like the only ones I'd want to keep / share).
Will have to play with it a little and check it out.
RE: Serverside Demo Recording Options - BuddyFriendGuy - 08-21-2018
With this script, I imagine the easiest way is to vote, and the command to run if passed is basically for the server to say something. Your script then captures that special phrase, and then keep the recording. I think most people realize they want to record after playing a while.
|