Запись повторов сервера
Игровой сервер может автоматически записывать полный повтор каждого сыгранного раунда. Повторы выводятся в виде отдельных zip-файлов, которые могут быть загружены игрой и лаунчером.
Конфигурация сервера
Конфигурация сборки
Ваш сервер должен иметь настроенную полную конфигурацию сборки/CDN (а не только ACZ). Иначе старые повторы перестанут работать, так как игру больше нельзя будет скачать лаунчером.
CVar’ы повторов
Вам нужно будет задать следующие CVar’ы:
replay.auto_record: Установите в true, чтобы включить систему!
replay.max_compressed_size: Приблизительный максимальный размер повтора в килобайтах, после которого запись автоматически останавливается. По умолчанию 256 MB, но в зависимости от ваших ожиданий вы можете захотеть немного поднять его.
replay.directory: Каталог в данных сервера, в который сохраняются повторы. По умолчанию replays/.
replay.auto_record_name: Имя файла в каталоге повторов, в которое будут сохраняться повторы. По умолчанию {year}_{month}_{day}-{hour}_{minute}-round_{round}.zip; значения в фигурных скобках автоматически подставляются при начале записи.
replay.auto_record_temp_dir Временный каталог, в котором находятся повторы во время записи. Когда запись завершена (в конце раунда), файл будет перемещён в конечное расположение из replay.auto_record_name. По умолчанию пустое значение, которое отключает эту часть системы (повторы будут записываться напрямую в конечное расположение).
Это все CVar’ы, которые нужны для настройки. В вашей папке повторов появится куча zip-файлов. Вы можете разместить эти zip-файлы на сервере статических файлов, и оно должно™️ работать. Учтите, что если вы используете сервер статических файлов, вам, возможно, стоит использовать replay.auto_record_temp_dir, чтобы повторы не были доступны для скачивания, пока раунд идёт вживую.
Конфигурация Wizard’s Den
Это будет обзор того, как мы настроили эту систему на игровых серверах Wizard’s Den. Вы можете использовать это как вдохновение или полностью проигнорировать.
Для начала мы не хотим хранить повторы на самих машинах игровых серверов. У нас большой объём хранилища на нашем центральном сервере (centcomm), так что держим их там. Также ради надёжности игровые серверы не имеют возможности удалять повторы после их завершения.
На высоком уровне мы экспортируем NFS-ресурс с нашего центрального сервера, в который пишут игровые серверы. Когда запись завершена, игровой сервер перемещает её в другой каталог на NFS-ресурсе, а скрипт на сервере перемещает повторы в их конечное расположение (доступное nginx, недоступное игровому серверу)
Структура (хранилища) папок выглядит так:
/hdd/replays
├── permanent
│ └── lizard
│ └── 2023
│ ├── 07
│ │ └── 20
│ │ └── lizard-2023_07_20-01_09-round_12345.zip
│ └── 08
│ └── 20
│ └── lizard-2023_08_20-01_09-round_12345.zip
└── temp
├── done
└── recording
NFS-ресурс выглядит так на centcomm (с использованием bind mounts для экспорта папки):
/nfs/ss14_server/replays
├── done
└── recording
На игровых серверах это смонтировано как /nfs/replays. Мы делаем символическую ссылку data/replays в экземплярах игрового сервера НА /nfs/replays, так что игровой сервер будет писать туда вместо этого. У нас replay.auto_record_temp_dir задан как "recording", а replay.auto_record_name — как "done/lizard-{year}_{month}_{day}-{hour}_{minute}-round_{round}.zip" (префикс имени сервера настраивается по-разному для каждого сервера).
Повторы уже находятся на сервере, когда запись прекращается, поэтому когда игровой сервер переименовывает файлы в конечное расположение, это происходит довольно быстро (по сравнению с тем, если бы ему пришлось копировать с локального диска перед переименованием).
Такая настройка даст вам повторы в папке done/, но это всё ещё оставляет их доступными игровым серверам впоследствии. Для немного большей изоляции мы используем systemd path unit, чтобы запускать скрипт, когда файл перемещается в папку done/, и снова перемещать его в конечное место в permanent/.
Задействованные systemd-юниты и Python-скрипт довольно просты и выглядят так:
# ss14-transfer-replays.path
[Unit]
Description=Обнаружение новых завершённых файлов повторов.
After=hdd-replays-temp.mount hdd-replays-permanent.mount
Requires=hdd-replays-temp.mount hdd-replays-permanent.mount
[Path]
PathExistsGlob=/hdd/replays/temp/done/*.zip
[Install]
WantedBy=paths.target
# ss14-transfer-replays.service
[Unit]
Description=Перенос повторов игры SS14 в конечное расположение.
[Service]
Environment=PYTHONUNBUFFERED=1
ExecStart=/opt/transfer_replays.py
User=ss14_server
Group=ss14_server
#!/usr/bin/env python3
# /opt/transfer_replays.py
import os
import os.path
import re
import shutil
SOURCE_DIR = "/hdd/replays/temp/done/"
DEST_DIR = "/hdd/replays/permanent/"
REPLAY_FILE_NAME_RE = re.compile(r"([^-]+)-(\d{4})_(\d{2})_(\d{2})-\d{2}_\d{2}-round_\d+\.zip")
def main():
for file in os.listdir(SOURCE_DIR):
src_replay = os.path.join(SOURCE_DIR, file)
final_rel_path = calculate_final_replay_path(file)
final_path = os.path.join(DEST_DIR, final_rel_path)
dir = os.path.dirname(final_path)
os.makedirs(dir, exist_ok=True)
print(f"{src_replay} -> {final_path}")
shutil.move(src_replay, final_path)
def calculate_final_replay_path(name: str) -> str:
"""
Принимает имя повтора вроде "lizard-2023_07_20-01_09-round_12345.zip"
Возвращает относительный путь, в который его переместить, вроде
"lizard/2023/07/20/01/09/lizard-2023_07_20-01_09-round_12345.zip"
"""
match = REPLAY_FILE_NAME_RE.match(name)
if not match:
# Нельзя давать таким файлам просто лежать, потому что тогда скрипт будет крутиться бесконечно. Перемещаем их в lost+found.
print(f"Warning: unable to parse file name '{name}'. Moving to lost+found")
return f"lost+found/{name}"
server = match.group(1)
year = match.group(2)
month = match.group(3)
day = match.group(4)
return f"{server}/{year}/{month}/{day}/{name}"
if __name__ == "__main__":
main()
И наконец, у нас есть небольшая конфигурация nginx для раздачи статических файлов.