Netplay for libretro/RetroArch can be activated in order to play with friends or random players over the Internet. The method consists roughly in starting a game, enabling netplay and waiting for clients to connect. Clients would then have to scan the lobby for public games and join the game that a server has made. Unfortunately, once a player connects to the server, RetroArch does emit a notification on the server screen but the notification does not stick, is very difficult to see and the client might go unnoticed on the host machine.
The following program adds a way to forward netplay activity to an MQTT server thereby allowing further processing and automation with packages such as node-red.
config.yml
to adjust the path to the RetroArch log file and set the MQTT parameters,Settings
→Logging
→Logging Verbosity
→On, Settings
→Logging
→Frontend Logging Level
→1 (Info)
, Settings
→Logging
→Core Logging Level
→1 (Info)
, Settings
→Logging
→Log To File
→OnnetplayMQTT.py
executable by issuing: chmod +x netplayMQTT.py
,netplayMQTT.py
by issuing: netplayMQTT.py –daemon
(more options available by running netplayMQTT.py
with no command-line options)netplayMQTT.py
on boot (or when RetroArch starts), depending on the distribution..config/autostart.sh
and add a line such as:(sleep 10 && /storage/.local/opt/netplayMQTT/netplayMQTT.py --daemon) &
depending on where the files from this project are located in order to make sure that netplayMQTT.py
is started on boot.
If RetroArch is running on Lakka, then further steps are necessary:
pip
python package installer by issuing the following commands:wget https://bootstrap.pypa.io/get-pip.py
pip install pyyaml pip install watchdog pip install paho-mqtt pip install daemonize
The commands will install modules under /storage/.local/
and will not interfere with Lakka.
The following events, defined in constants.py
will be published to the MQTT broker:
Event | Parameters | Description | RetroArch Role |
---|---|---|---|
announcing | RetroArch has started hosting and is currently announcing the game. | Host | |
waiting | RetroArch has started hosting and is waiting for a client. | Host | |
playerjoined | nick , controls | A player with nickname nick has successfully joined the host and has been allocated slot controls . | Host |
playerdisconnected | nick | A player with nickname nick has disconnected from the netplay session. | Host |
connecting | RetroArch is attempting to connect to a host. | Client | |
joined | controls | RetroArch has joined a host and has been allocated controls controls . | Client |
in JSON format where the event and all parameters for the event are encoded as top-level key-value pairs.
Leveraging alexatts from a former TTS project and combining with Node Red, Alexa can be made to announce whenever a player has joined the game. A Node Red flow seems trivial in this case:
The flow performs the following actions, in order, whenever netplayMQTT.py
publishes an event to the MQTT broker (the green msg.payload
node is just for debugging purposes and has no effect on the flow):
json
),msg.payload.event
is set to playerjoined
(switch
),msg.payload
to a message such as A player has joined the netplay session.
(set msg.payload
),
and then re-publishes the message to the same MQTT broker with a topic corresponding to how alexatts has been set up. Alexatts is then expected to speak out the message A player has joined the netplay session.
. Note that the parameters for playerjoined
, namely nick
and controls
are not used in this flow - nick
would be problematic since it may include non-speakable characters which a TTS engine will have no choice except to spell them out individually.
All files have to be placed under the same directory.
#!/usr/bin/env python ########################################################################### ## Copyright (C) Wizardry and Steamworks 2019 - License: GNU GPLv3 ## ########################################################################### # Reads RetroArch logs looking for NetPlay events and publishes to MQTT # ########################################################################### # # wget https://bootstrap.pypa.io/get-pip.py # python get-pip.py # # pip install pyyaml # pip install watchdog # pip install paho-mqtt # pip install daemonize import yaml, subprocess, os, argparse, signal, sys, time, mmap, re, json import paho.mqtt.client as mqtt from watchdog.observers import Observer from watchdog.events import FileSystemEventHandler from os import path from daemonize import Daemonize import constants # Called when the file has been modified. class RetroArchLogUpdated(FileSystemEventHandler): def __init__(self, client): self.client = client # Called when the monitored file is modified. def on_modified(self, event): line = self.readlast(event.src_path) for event in logevents: match = logevents[event].match(line) if match: #print list(match.groups()) payload = {} payload['event'] = event if event == 'announcing': pass if event == 'waiting': pass if event == 'playerjoined': payload['nick'] = match.group(1) payload['controls'] = match.group(2) if event == 'playerdisconnected': payload['nick'] = match.groups(1) if event == 'connecting': pass if event == 'joined': payload['controls'] = match.groups(1) #print json.dumps(payload, ensure_ascii=False) # Publishes to MQTT. self.client.publish(cfg['mqtt']['topic'], json.dumps(payload, ensure_ascii=False)) # Gets the last line of a file. @staticmethod def readlast(filename): with open(filename) as f: mapping = mmap.mmap(f.fileno(), 0, prot=mmap.PROT_READ) i = mapping.rfind('\n') j = mapping.rfind('\n', 0, i) return mapping[j + 1:i] # The callback for when the client receives a CONNACK response from the server. def on_connect(client, userdata, flags, rc): client.subscribe(cfg['mqtt']['topic']) # Callback when a message is received on the MQTT bus. def on_message(client, userdata, msg): pass def main(): # Set up MQTT client. client = mqtt.Client() client.on_connect = on_connect client.on_message = on_message client.connect(cfg['mqtt']['host'], cfg['mqtt']['port'], 60) # Set up log watcher. event_handler = RetroArchLogUpdated(client) observer = Observer() observer.schedule(event_handler, path=cfg['retroarch']['logdir'], recursive=False) observer.start() # Blocking call. client.loop_forever() def daemon(): try: if path.exists(cfg['pid']): with open(cfg['pid'], 'r') as pid_file: pid = pid_file.read() os.kill(int(pid), signal.SIGHUP) os.remove(cfg['pid']) except Exception, e: print 'Unable to kill previous process: ', str(e) return daemon = Daemonize(app='mqttSwitch', pid=cfg['pid'], action=main) daemon.start() with open(os.path.join(os.path.dirname(sys.argv[0]), 'config.yml'), 'r') as ymlfile: cfg = yaml.load(ymlfile, Loader=yaml.FullLoader) logevents = {} for event in constants.NETPLAY_PATTERNS: logevents[event] = re.compile(constants.NETPLAY_PATTERNS[event], re.IGNORECASE) parser = argparse.ArgumentParser() parser.add_argument('--daemon', action='store_true', help='run as a daemon') parser.add_argument('--foreground', action='store_true', help='run as a daemon') args = parser.parse_args() if(args.daemon): daemon() elif(args.foreground): main() parser.print_help()
NETPLAY_PATTERNS = { "announcing": "^\[INFO\] \[netplay\] announcing netplay game.*?$", "waiting": "\[INFO\] \[netplay\] Waiting for client", "playerjoined": "\[INFO\] \[netplay\] ([^\s]+?) has joined as player ([0-9])+?", "playerdisconnected": "\[INFO\] \[netplay\] ([^\s]+?) has disconnected", "connecting": "\[INFO\] \[netplay\] Connecting to netplay host", "joined": "\[INFO\] \[netplay\] You have joined as player ([0-9])+?", }
pid: '/var/run/mqttNetplay.pid' retroarch: logdir: '/storage/.config/retroarch/logs/' mqtt: host: 192.168.1.1 port: 1883 topic: 'retroarch'