# ba_meta require api 7 """ Bombsquad Discord Rich Presence by Mr.Smoothy v1.1 17 march 2022 v1.2 29 November 2022 Watch : https://www.youtube.com/watch?v=RB0jzZ95gY4 Checkout : https://www.youtube.com/c/heysmoothy for more interesting mods Bombsquad 1.7 Server : https://github.com/imayushsaini/Bombsquad-Ballistica-Modded-Server Stay upto date join : https://discord.gg/ucyaesh Just install and enable this plugin and you go . only compatible with discord desktop app (Windows/Linux) (not discord in browser). Inspired by JRMP, mod requested by Firefighter1077 Join Discord BCS : https://discord.gg/ucyaesh Bombspot : https://discord.gg/2RKd9QQdQY JRMP : https://discord.gg/Zz4XsrTUpT SahilP : https://discord.gg/B9mJaQAuKv # CHANGELOG: ### v1.1 (17 match 2022) - fixed linux compatibility - display full game name, if possible - several fixes ### v1.2 - more assets(emojis) - brief Released under MIT Licence """ from __future__ import annotations import logging import socket import sys import struct import errno import uuid import time import os import json from abc import ABCMeta, abstractmethod import ba import _ba from typing import TYPE_CHECKING if TYPE_CHECKING: from typing import Any, Type, List, Dict, Tuple, Union, Sequence, Optional OP_HANDSHAKE = 0 OP_FRAME = 1 OP_CLOSE = 2 OP_PING = 3 OP_PONG = 4 logger = logging.getLogger(__name__) class DiscordIpcError(Exception): pass class DiscordIpcNotConnectedError(DiscordIpcError): pass class DiscordIpcClient(metaclass=ABCMeta): """Work with an open Discord instance via its JSON IPC for its rich presence API. In a blocking way. Classmethod `for_platform` will resolve to one of WinDiscordIpcClient or UnixDiscordIpcClient, depending on the current platform. Supports context handler protocol. """ def __init__(self, client_id: str): self.client_id = client_id self._connected = False @property def connected(self) -> bool: """Are we connected to Discord and able to communicate?""" return self._connected def connect(self) -> None: """Checks whether Discord is available and try to contact it""" self._connect() self._connected = True try: self._do_handshake() except Exception as e: self._connected = False self._close() raise e logger.info('connected via ID %s', self.client_id) @classmethod def for_platform(cls, client_id: str, platform: str = sys.platform) -> DiscordIpcClient: """Returns platform-specific Windows/Unix instance for current OS""" if platform == 'win32': return WinDiscordIpcClient(client_id) else: return UnixDiscordIpcClient(client_id) @abstractmethod def _connect(self) -> None: pass def _do_handshake(self) -> None: ret_op, ret_data = self.send_recv({ 'v': 1, 'client_id': self.client_id }, op=OP_HANDSHAKE) # yapf: disable if (ret_op == OP_FRAME and ret_data['cmd'] == 'DISPATCH' and ret_data['evt'] == 'READY'): return else: if ret_op == OP_CLOSE: self.close() raise RuntimeError(ret_data) @abstractmethod def _write(self, data: bytes) -> None: pass @abstractmethod def _recv(self, size: int) -> bytes: pass def _recv_header(self) -> Tuple[int, int]: header = self._recv_exactly(8) return struct.unpack(' bytes: buf = b'' size_remaining = size while size_remaining: chunk = self._recv(size_remaining) buf += chunk size_remaining -= len(chunk) return buf def close(self) -> None: """Close connection""" # logger.warning('closing connection') try: self.send({}, op=OP_CLOSE) except DiscordIpcError: pass finally: self._close() self._connected = False @abstractmethod def _close(self) -> None: pass def __enter__(self) -> DiscordIpcClient: return self def __exit__(self, *_: Any) -> None: self.close() def send_recv(self, data: dict[str, Any], op: int = OP_FRAME) -> Tuple[int, dict[str, Any]]: self.send(data, op) return self.recv() def send(self, data: dict[str, Any], op: int = OP_FRAME) -> None: if not self.connected: raise DiscordIpcNotConnectedError logger.debug('sending %s', data) data_str = json.dumps(data, separators=(',', ':')) data_bytes = data_str.encode('utf-8') header = struct.pack(' Tuple[int, dict[str, Any]]: """Receives a packet from discord. Returns op code and payload. """ if not self.connected: raise DiscordIpcNotConnectedError op, length = self._recv_header() payload = self._recv_exactly(length) data = json.loads(payload.decode('utf-8')) logger.debug('received %s', data) return op, data def set_activity(self, act: dict[str, Any]) -> None: data = { 'cmd': 'SET_ACTIVITY', 'args': { 'pid': os.getpid(), 'activity': act }, 'nonce': str(uuid.uuid4()) } self.send(data) class WinDiscordIpcClient(DiscordIpcClient): _pipe_pattern = R'\\?\pipe\discord-ipc-{}' def _connect(self) -> None: for i in range(10): path = self._pipe_pattern.format(i)#FIXME BUG try: self._f = open(path, 'w+b') except OSError as e: if e.errno == errno.EPIPE: pass # logger.error("Discord not running ") # logger.error("failed to open {!r}: {}".format(path, e)) else: break else: raise DiscordIpcError('Failed to connect to Discord pipe') self.path = path def _write(self, data: bytes) -> None: try: self._f.write(data) self._f.flush() except Exception: pass def _recv(self, size: int) -> bytes: try: return self._f.read(size) except Exception: return b'0' def _close(self) -> None: self._f.close() class UnixDiscordIpcClient(DiscordIpcClient): def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) self._sock = socket.socket(socket.AF_UNIX) def _connect(self) -> None: pipe_pattern = self._get_pipe_pattern() for i in range(10): path = pipe_pattern.format(i) if not os.path.exists(path): continue try: self._sock.connect(path) except OSError as e: if e.errno == errno.EPIPE: pass logger.error("failed to open %s: %s", path, e) else: break else: raise DiscordIpcError("Failed to connect to Discord pipe") @staticmethod def _get_pipe_pattern() -> str: env_keys = ('XDG_RUNTIME_DIR', 'TMPDIR', 'TMP', 'TEMP') for env_key in env_keys: dir_path = os.environ.get(env_key) if dir_path: break else: dir_path = '/tmp' return os.path.join(dir_path, 'discord-ipc-{}') def _write(self, data: bytes) -> None: self._sock.sendall(data) def _recv(self, size: int) -> bytes: return self._sock.recv(size) def _close(self) -> None: self._sock.close() # ba_meta export plugin class HeySmoothy(ba.Plugin): def __init__(self) -> None: self.state = 'Just Started' self.details = 'Just started' self.large_image = 'logo' self.small_image = 'Platfrom' self.small_text = 'Version' self.large_text = 'Join BCS(😎Mr.Smoothy)' self.laststatus = 'offline' self.starttime = time.mktime(time.localtime()) self.client_id = '1046091093785186444' self.rpclient: DiscordIpcClient = DiscordIpcClient.for_platform( self.client_id) self.timer: Optional[ba.Timer] = None def on_app_running(self) -> None: self.update_status() self.timer = ba.Timer(9.0, self.update_status, repeat=True, timetype=ba.TimeType.REAL) def on_app_shutdown(self) -> None: self.rpclient.close() def update_status(self) -> None: if not self.rpclient.connected: try: self.rpclient.connect() except DiscordIpcError: # In most cases Discord is simply closed, let's do not notice # about that. pass self.getstatus() activity = { 'state': self.state, 'details': self.details, 'timestamps': { 'start': self.starttime }, 'assets': { 'small_text': _ba.app.version, 'small_image': _ba.app.platform, 'large_text': self.large_text, 'large_image': self.large_image } } if self.rpclient.connected: self.rpclient.set_activity(activity) def getstatus(self) -> None: Players = _ba.get_public_party_max_size() currentplayers = len(_ba.get_game_roster())-1 if not _ba.get_connection_to_host_info(): act = _ba.get_foreground_host_activity() a = act.__class__.__name__.replace('Activity', '') b = a.replace('Game', '') c = b.replace('Coop', '') d = c.replace('Victory', '') if isinstance(act, ba.GameActivity): self.state = act.name elif d == 'Join'or d == 'MultiTeamJoin': self.state = 'Lobby🚭' elif d == 'NoneType': self.state = 'Watching Replay💣' else: self.state = d if not _ba.get_game_roster(): self.details = 'Local' else: self.details = f'Local ({currentplayers+1} of {Players})' if self.laststatus == 'online': self.starttime = time.mktime(time.localtime()) self.laststatus = 'offline' else: if _ba.get_connection_to_host_info()['name'] != '': self.state = _ba.get_connection_to_host_info()['name'] if currentplayers ==1: self.details = 'Online Alone 😭' else: host_spec = json.loads(_ba.get_game_roster()[0]['spec_string']) if host_spec['a'] == 'Server': self.details = f'Online ({currentplayers} of {Players})' else: self.details = f'Online ({currentplayers+1})' if self.laststatus == 'offline': self.starttime = time.mktime(time.localtime()) self.laststatus = 'online' if self.state == 'Watching Replay💣': self.large_image = "replay" elif currentplayers == 2: self.large_image = "two" elif 3 <= currentplayers <= 5: self.large_image = "twomore" elif currentplayers >= 6: self.large_image = "many" elif self.details == 'Online Alone 😭': self.large_image = 'curse' else: self.large_image = 'logo'