Xonotic Forums
Serverside Demo Recording Options - Printable Version

+- Xonotic Forums (https://forums.xonotic.org)
+-- Forum: Support (https://forums.xonotic.org/forumdisplay.php?fid=3)
+--- Forum: Xonotic - Server Administration (https://forums.xonotic.org/forumdisplay.php?fid=16)
+--- Thread: Serverside Demo Recording Options (/showthread.php?tid=7802)



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.