diff --git a/torchlight_changes_unloze/systemd/torchlight_restart.service b/torchlight_changes_unloze/systemd/torchlight_restart.service new file mode 100644 index 00000000..fd4e7e7b --- /dev/null +++ b/torchlight_changes_unloze/systemd/torchlight_restart.service @@ -0,0 +1,11 @@ +[Unit] +Description=Restarting torch on ze every 30 minutes + +[Service] +Type=simple +User=gameservers +WorkingDirectory=/home/gameservers/css_ze/torchlight3 +ExecStart=/home/gameservers/css_ze/torchlight3/_start.sh +Restart=always +RuntimeMaxSec=1800 + diff --git a/torchlight_changes_unloze/systemd/torchlight_restart_mg.service b/torchlight_changes_unloze/systemd/torchlight_restart_mg.service new file mode 100644 index 00000000..0a0b1005 --- /dev/null +++ b/torchlight_changes_unloze/systemd/torchlight_restart_mg.service @@ -0,0 +1,11 @@ +[Unit] +Description=Restarting torch on mg every 30 minutes + +[Service] +Type=simple +User=gameservers +WorkingDirectory=/home/gameservers/css_ze/torchlight3 +ExecStart=/home/gameservers/css_ze/torchlight3/_start_mg.sh +Restart=always +RuntimeMaxSec=1800 + diff --git a/torchlight_changes_unloze/torchlight3/README.md b/torchlight_changes_unloze/torchlight3/README.md new file mode 100755 index 00000000..4b7dcaf4 --- /dev/null +++ b/torchlight_changes_unloze/torchlight3/README.md @@ -0,0 +1,27 @@ +# Torchlight3 + +## 0. Requirements + * Python3.6 + * FFMPEG + * youtube-dl + * On game server: + * custom sourcemod + * sm-ext-AsyncSocket extension + * smjansson extension + * SMJSONAPI plugin + * sm-ext-Voice extension + +## 1. Install + * Install python3 and python-virtualenv + * Create a virtualenv: `virtualenv venv` + * Activate the virtualenv: `. venv/bin/activate` + * Install all dependencies: `pip install -r requirements.txt` + +## 2. Usage +Set up game server stuff. +Adapt config.json. + +##### Make sure you are in the virtualenv! (`. venv/bin/activate`) +Run: `python main.py` + +Glacius was here. xd diff --git a/torchlight_changes_unloze/torchlight3/Torchlight/AccessManager.py b/torchlight_changes_unloze/torchlight3/Torchlight/AccessManager.py new file mode 100755 index 00000000..9b52f975 --- /dev/null +++ b/torchlight_changes_unloze/torchlight3/Torchlight/AccessManager.py @@ -0,0 +1,43 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +import logging +import json +from collections import OrderedDict + +class AccessManager(): + ACCESS_FILE = "access.json" + def __init__(self): + self.Logger = logging.getLogger(__class__.__name__) + self.AccessDict = OrderedDict() + + def Load(self): + self.Logger.info("Loading access from {0}".format(self.ACCESS_FILE)) + + with open(self.ACCESS_FILE, "r") as fp: + self.AccessDict = json.load(fp, object_pairs_hook = OrderedDict) + + def Save(self): + self.Logger.info("Saving access to {0}".format(self.ACCESS_FILE)) + + self.AccessDict = OrderedDict( + sorted(self.AccessDict.items(), key = lambda x: x[1]["level"], reverse = True)) + + with open(self.ACCESS_FILE, "w") as fp: + json.dump(self.AccessDict, fp, indent = '\t') + + def __len__(self): + return len(self.AccessDict) + + def __getitem__(self, key): + if key in self.AccessDict: + return self.AccessDict[key] + + def __setitem__(self, key, value): + self.AccessDict[key] = value + + def __delitem__(self, key): + if key in self.AccessDict: + del self.AccessDict[key] + + def __iter__(self): + return self.AccessDict.items().__iter__() diff --git a/torchlight_changes_unloze/torchlight3/Torchlight/AsyncClient.py b/torchlight_changes_unloze/torchlight3/Torchlight/AsyncClient.py new file mode 100755 index 00000000..16e1baf1 --- /dev/null +++ b/torchlight_changes_unloze/torchlight3/Torchlight/AsyncClient.py @@ -0,0 +1,96 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +import asyncio +import logging +import json + +class ClientProtocol(asyncio.Protocol): + def __init__(self, loop, master): + self.Loop = loop + self.Master = master + self.Transport = None + self.Buffer = bytearray() + + def connection_made(self, transport): + self.Transport = transport + + def data_received(self, data): + self.Buffer += data + + chunks = self.Buffer.split(b'\0') + if data[-1] == b'\0': + chunks = chunks[:-1] + self.Buffer = bytearray() + else: + self.Buffer = bytearray(chunks[-1]) + chunks = chunks[:-1] + + for chunk in chunks: + self.Master.OnReceive(chunk) + + def connection_lost(self, exc): + self.Transport.close() + self.Transport = None + self.Master.OnDisconnect(exc) + + def Send(self, data): + if self.Transport: + self.Transport.write(data) + +class AsyncClient(): + def __init__(self, loop, host, port, master): + self.Logger = logging.getLogger(__class__.__name__) + self.Loop = loop + self.Host = host + self.Port = port + self.Master = master + + self.Protocol = None + self.SendLock = asyncio.Lock() + self.RecvFuture = None + + async def Connect(self): + while True: + self.Logger.warn("Reconnecting...") + try: + _, self.Protocol = await self.Loop.create_connection(lambda: ClientProtocol(self.Loop, self), host = self.Host, port = self.Port) + break + except: + await asyncio.sleep(1.0) + + def OnReceive(self, data): + Obj = json.loads(data) + + if "method" in Obj and Obj["method"] == "publish": + self.Master.OnPublish(Obj) + else: + if self.RecvFuture: + self.RecvFuture.set_result(Obj) + + def OnDisconnect(self, exc): + self.Protocol = None + if self.RecvFuture: + self.RecvFuture.cancel() + self.Master.OnDisconnect(exc) + + async def Send(self, obj): + if not self.Protocol: + return None + + Data = json.dumps(obj, ensure_ascii = False, separators = (',', ':')).encode("UTF-8") + + with (await self.SendLock): + if not self.Protocol: + return None + + self.RecvFuture = asyncio.Future() + self.Protocol.Send(Data) + await self.RecvFuture + + if self.RecvFuture.done(): + Obj = self.RecvFuture.result() + else: + Obj = None + + self.RecvFuture = None + return Obj diff --git a/torchlight_changes_unloze/torchlight3/Torchlight/AudioManager.py b/torchlight_changes_unloze/torchlight3/Torchlight/AudioManager.py new file mode 100755 index 00000000..f56d97e1 --- /dev/null +++ b/torchlight_changes_unloze/torchlight3/Torchlight/AudioManager.py @@ -0,0 +1,351 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +import logging +import sys +import io +import math +from .FFmpegAudioPlayer import FFmpegAudioPlayerFactory + +class AudioPlayerFactory(): + AUDIOPLAYER_FFMPEG = 1 + + def __init__(self, master): + self.Logger = logging.getLogger(__class__.__name__) + self.Master = master + self.Torchlight = self.Master.Torchlight + + self.FFmpegAudioPlayerFactory = FFmpegAudioPlayerFactory(self) + + def __del__(self): + self.Logger.info("~AudioPlayerFactory()") + + def NewPlayer(self, _type): + if _type == self.AUDIOPLAYER_FFMPEG: + return self.FFmpegAudioPlayerFactory.NewPlayer() + + +class AntiSpam(): + def __init__(self, master): + self.Logger = logging.getLogger(__class__.__name__) + self.Master = master + self.Torchlight = self.Master.Torchlight + + self.LastClips = dict() + self.DisabledTime = None + self.SaidHint = False + + def CheckAntiSpam(self, player): + if self.DisabledTime and self.DisabledTime > self.Torchlight().Master.Loop.time() and \ + not (player.Access and player.Access["level"] >= self.Torchlight().Config["AntiSpam"]["ImmunityLevel"]): + + self.Torchlight().SayPrivate(player, "Torchlight is currently on cooldown! ({0} seconds left)".format( + math.ceil(self.DisabledTime - self.Torchlight().Master.Loop.time()))) + return False + + return True + + def SpamCheck(self, Delta): + Now = self.Torchlight().Master.Loop.time() + Duration = 0.0 + + for Key, Clip in list(self.LastClips.items()): + if not Clip["timestamp"]: + continue + + if Clip["timestamp"] + Clip["duration"] + self.Torchlight().Config["AntiSpam"]["MaxUsageSpan"] < Now: + if not Clip["active"]: + del self.LastClips[Key] + continue + + Duration += Clip["duration"] + + if Duration > self.Torchlight().Config["AntiSpam"]["MaxUsageTime"]: + self.DisabledTime = self.Torchlight().Master.Loop.time() + self.Torchlight().Config["AntiSpam"]["PunishDelay"] + self.Torchlight().SayChat("Blocked voice commands for the next {0} seconds. Used {1} seconds within {2} seconds.".format( + self.Torchlight().Config["AntiSpam"]["PunishDelay"], self.Torchlight().Config["AntiSpam"]["MaxUsageTime"], self.Torchlight().Config["AntiSpam"]["MaxUsageSpan"])) + + # Make a copy of the list since AudioClip.Stop() will change the list + for AudioClip in self.Master.AudioClips[:]: + if AudioClip.Level < self.Torchlight().Config["AntiSpam"]["ImmunityLevel"]: + AudioClip.Stop() + + self.LastClips.clear() + + def OnPlay(self, clip): + Now = self.Torchlight().Master.Loop.time() + self.LastClips[hash(clip)] = dict({"timestamp": Now, "duration": 0.0, "dominant": False, "active": True}) + + HasDominant = False + for Key, Clip in self.LastClips.items(): + if Clip["dominant"]: + HasDominant = True + break + + self.LastClips[hash(clip)]["dominant"] = not HasDominant + + def OnStop(self, clip): + if hash(clip) not in self.LastClips: + return + + self.LastClips[hash(clip)]["active"] = False + + if self.LastClips[hash(clip)]["dominant"]: + for Key, Clip in self.LastClips.items(): + if Clip["active"]: + Clip["dominant"] = True + break + + self.LastClips[hash(clip)]["dominant"] = False + + def OnUpdate(self, clip, old_position, new_position): + Delta = new_position - old_position + Clip = self.LastClips[hash(clip)] + + if not Clip["dominant"]: + return + + Clip["duration"] += Delta + self.SpamCheck(Delta) + + +class Advertiser(): + def __init__(self, master): + self.Logger = logging.getLogger(__class__.__name__) + self.Master = master + self.Torchlight = self.Master.Torchlight + + self.LastClips = dict() + self.AdStop = 0 + self.NextAdStop = 0 + + def Think(self, Delta): + Now = self.Torchlight().Master.Loop.time() + Duration = 0.0 + + for Key, Clip in list(self.LastClips.items()): + if not Clip["timestamp"]: + continue + + if Clip["timestamp"] + Clip["duration"] + self.Torchlight().Config["Advertiser"]["MaxSpan"] < Now: + if not Clip["active"]: + del self.LastClips[Key] + continue + + Duration += Clip["duration"] + + self.NextAdStop -= Delta + CeilDur = math.ceil(Duration) + if CeilDur > self.AdStop and self.NextAdStop <= 0 and CeilDur % self.Torchlight().Config["Advertiser"]["AdStop"] == 0: + self.Torchlight().SayChat("Hint: Type \x07FF0000!stop(ze) !pls(mg)\x01 to stop all currently playing sounds.") + self.AdStop = CeilDur + self.NextAdStop = 0 + elif CeilDur < self.AdStop: + self.AdStop = 0 + self.NextAdStop = self.Torchlight().Config["Advertiser"]["AdStop"] / 2 + + def OnPlay(self, clip): + Now = self.Torchlight().Master.Loop.time() + self.LastClips[hash(clip)] = dict({"timestamp": Now, "duration": 0.0, "dominant": False, "active": True}) + + HasDominant = False + for Key, Clip in self.LastClips.items(): + if Clip["dominant"]: + HasDominant = True + break + + self.LastClips[hash(clip)]["dominant"] = not HasDominant + + def OnStop(self, clip): + if hash(clip) not in self.LastClips: + return + + self.LastClips[hash(clip)]["active"] = False + + if self.LastClips[hash(clip)]["dominant"]: + for Key, Clip in self.LastClips.items(): + if Clip["active"]: + Clip["dominant"] = True + break + + self.LastClips[hash(clip)]["dominant"] = False + + def OnUpdate(self, clip, old_position, new_position): + Delta = new_position - old_position + Clip = self.LastClips[hash(clip)] + + if not Clip["dominant"]: + return + + Clip["duration"] += Delta + self.Think(Delta) + + +class AudioManager(): + def __init__(self, torchlight): + self.Logger = logging.getLogger(__class__.__name__) + self.Torchlight = torchlight + self.AntiSpam = AntiSpam(self) + self.Advertiser = Advertiser(self) + self.AudioPlayerFactory = AudioPlayerFactory(self) + self.AudioClips = [] + + def __del__(self): + self.Logger.info("~AudioManager()") + + def CheckLimits(self, player): + Level = 0 + if player.Access: + Level = player.Access["level"] + + if str(Level) in self.Torchlight().Config["AudioLimits"]: + if self.Torchlight().Config["AudioLimits"][str(Level)]["Uses"] >= 0 and \ + player.Storage["Audio"]["Uses"] >= self.Torchlight().Config["AudioLimits"][str(Level)]["Uses"]: + + self.Torchlight().SayPrivate(player, "You have used up all of your free uses! ({0} uses)".format( + self.Torchlight().Config["AudioLimits"][str(Level)]["Uses"])) + return False + + if player.Storage["Audio"]["TimeUsed"] >= self.Torchlight().Config["AudioLimits"][str(Level)]["TotalTime"]: + self.Torchlight().SayPrivate(player, "You have used up all of your free time! ({0} seconds)".format( + self.Torchlight().Config["AudioLimits"][str(Level)]["TotalTime"])) + return False + + TimeElapsed = self.Torchlight().Master.Loop.time() - player.Storage["Audio"]["LastUse"] + UseDelay = player.Storage["Audio"]["LastUseLength"] * self.Torchlight().Config["AudioLimits"][str(Level)]["DelayFactor"] + + if TimeElapsed < UseDelay: + self.Torchlight().SayPrivate(player, "You are currently on cooldown! ({0} seconds left)".format( + round(UseDelay - TimeElapsed))) + return False + + return True + + def Stop(self, player, extra): + Level = 0 + if player.Access: + Level = player.Access["level"] + + for AudioClip in self.AudioClips[:]: + if extra and not extra.lower() in AudioClip.Player.Name.lower(): + continue + + if not Level or (Level < AudioClip.Level and Level < self.Torchlight().Config["AntiSpam"]["StopLevel"]): + AudioClip.Stops.add(player.UserID) + + if len(AudioClip.Stops) >= 3: + AudioClip.Stop() + self.Torchlight().SayPrivate(AudioClip.Player, "Your audio clip was stopped.") + if player != AudioClip.Player: + self.Torchlight().SayPrivate(player, "Stopped \"{0}\"({1}) audio clip.".format(AudioClip.Player.Name, AudioClip.Player.UserID)) + else: + self.Torchlight().SayPrivate(player, "This audio clip needs {0} more !stop's.".format(3 - len(AudioClip.Stops))) + else: + AudioClip.Stop() + self.Torchlight().SayPrivate(AudioClip.Player, "Your audio clip was stopped.") + if player != AudioClip.Player: + self.Torchlight().SayPrivate(player, "Stopped \"{0}\"({1}) audio clip.".format(AudioClip.Player.Name, AudioClip.Player.UserID)) + + def AudioClip(self, player, uri, _type = AudioPlayerFactory.AUDIOPLAYER_FFMPEG): + Level = 0 + if player.Access: + Level = player.Access["level"] + + if self.Torchlight().Disabled and self.Torchlight().Disabled > Level: + self.Torchlight().SayPrivate(player, "Torchlight is currently disabled!") + return None + + if not self.AntiSpam.CheckAntiSpam(player): + return None + + if not self.CheckLimits(player): + return None + + Clip = AudioClip(self, player, uri, _type) + self.AudioClips.append(Clip) + + if not player.Access or player.Access["level"] < self.Torchlight().Config["AntiSpam"]["ImmunityLevel"]: + Clip.AudioPlayer.AddCallback("Play", lambda *args: self.AntiSpam.OnPlay(Clip, *args)) + Clip.AudioPlayer.AddCallback("Stop", lambda *args: self.AntiSpam.OnStop(Clip, *args)) + Clip.AudioPlayer.AddCallback("Update", lambda *args: self.AntiSpam.OnUpdate(Clip, *args)) + + Clip.AudioPlayer.AddCallback("Play", lambda *args: self.Advertiser.OnPlay(Clip, *args)) + Clip.AudioPlayer.AddCallback("Stop", lambda *args: self.Advertiser.OnStop(Clip, *args)) + Clip.AudioPlayer.AddCallback("Update", lambda *args: self.Advertiser.OnUpdate(Clip, *args)) + + return Clip + + def OnDisconnect(self, player): + for AudioClip in self.AudioClips[:]: + if AudioClip.Player == player: + AudioClip.Stop() + + +class AudioClip(): + def __init__(self, master, player, uri, _type): + self.Logger = logging.getLogger(__class__.__name__) + self.Master = master + self.Torchlight = self.Master.Torchlight + self.Player = player + self.Type = _type + self.URI = uri + self.LastPosition = None + self.Stops = set() + + self.Level = 0 + if self.Player.Access: + self.Level = self.Player.Access["level"] + + self.AudioPlayer = self.Master.AudioPlayerFactory.NewPlayer(self.Type) + self.AudioPlayer.AddCallback("Play", self.OnPlay) + self.AudioPlayer.AddCallback("Stop", self.OnStop) + self.AudioPlayer.AddCallback("Update", self.OnUpdate) + + def __del__(self): + self.Logger.info("~AudioClip()") + + def Play(self, seconds = None, *args): + return self.AudioPlayer.PlayURI(self.URI, seconds, *args) + + def Stop(self): + return self.AudioPlayer.Stop() + + def OnPlay(self): + self.Logger.debug(sys._getframe().f_code.co_name + ' ' + self.URI) + + self.Player.Storage["Audio"]["Uses"] += 1 + self.Player.Storage["Audio"]["LastUse"] = self.Torchlight().Master.Loop.time() + self.Player.Storage["Audio"]["LastUseLength"] = 0.0 + + def OnStop(self): + self.Logger.debug(sys._getframe().f_code.co_name + ' ' + self.URI) + self.Master.AudioClips.remove(self) + + if self.AudioPlayer.Playing: + Delta = self.AudioPlayer.Position - self.LastPosition + self.Player.Storage["Audio"]["TimeUsed"] += Delta + self.Player.Storage["Audio"]["LastUseLength"] += Delta + + if str(self.Level) in self.Torchlight().Config["AudioLimits"]: + if self.Player.Storage: + if self.Player.Storage["Audio"]["TimeUsed"] >= self.Torchlight().Config["AudioLimits"][str(self.Level)]["TotalTime"]: + self.Torchlight().SayPrivate(self.Player, "You have used up all of your free time! ({0} seconds)".format( + self.Torchlight().Config["AudioLimits"][str(self.Level)]["TotalTime"])) + elif self.Player.Storage["Audio"]["LastUseLength"] >= self.Torchlight().Config["AudioLimits"][str(self.Level)]["MaxLength"]: + self.Torchlight().SayPrivate(self.Player, "Your audio clip exceeded the maximum length! ({0} seconds)".format( + self.Torchlight().Config["AudioLimits"][str(self.Level)]["MaxLength"])) + + del self.AudioPlayer + + def OnUpdate(self, old_position, new_position): + Delta = new_position - old_position + self.LastPosition = new_position + + self.Player.Storage["Audio"]["TimeUsed"] += Delta + self.Player.Storage["Audio"]["LastUseLength"] += Delta + + if not str(self.Level) in self.Torchlight().Config["AudioLimits"]: + return + + if (self.Player.Storage["Audio"]["TimeUsed"] >= self.Torchlight().Config["AudioLimits"][str(self.Level)]["TotalTime"] or + self.Player.Storage["Audio"]["LastUseLength"] >= self.Torchlight().Config["AudioLimits"][str(self.Level)]["MaxLength"]): + self.Stop() diff --git a/torchlight_changes_unloze/torchlight3/Torchlight/CommandHandler.py b/torchlight_changes_unloze/torchlight3/Torchlight/CommandHandler.py new file mode 100755 index 00000000..e9b73ffa --- /dev/null +++ b/torchlight_changes_unloze/torchlight3/Torchlight/CommandHandler.py @@ -0,0 +1,115 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +import logging +import asyncio +import sys +import re +import traceback +import math +from importlib import reload +from . import Commands + +class CommandHandler(): + def __init__(self, Torchlight): + self.Logger = logging.getLogger(__class__.__name__) + self.Torchlight = Torchlight + self.Commands = [] + self.NeedsReload = False + + def Setup(self): + Counter = len(self.Commands) + self.Commands.clear() + if Counter: + self.Logger.info(sys._getframe().f_code.co_name + " Unloaded {0} commands!".format(Counter)) + + Counter = 0 + for subklass in sorted(Commands.BaseCommand.__subclasses__(), key = lambda x: x.Order, reverse = True): + try: + Command = subklass(self.Torchlight) + if hasattr(Command, "_setup"): + Command._setup() + except Exception as e: + self.Logger.error(traceback.format_exc()) + else: + self.Commands.append(Command) + Counter += 1 + + self.Logger.info(sys._getframe().f_code.co_name + " Loaded {0} commands!".format(Counter)) + + def Reload(self): + try: + reload(Commands) + except Exception as e: + self.Logger.error(traceback.format_exc()) + else: + self.Setup() + + async def HandleCommand(self, line, player): + Message = line.split(sep = ' ', maxsplit = 1) + if len(Message) < 2: + Message.append("") + Message[1] = Message[1].strip() + + if Message[1] and self.Torchlight().LastUrl: + Message[1] = Message[1].replace("!last", self.Torchlight().LastUrl) + line = Message[0] + ' ' + Message[1] + + Level = 0 + if player.Access: + Level = player.Access["level"] + + RetMessage = None + Ret = None + for Command in self.Commands: + for Trigger in Command.Triggers: + Match = False + RMatch = None + if isinstance(Trigger, tuple): + if Message[0].lower().startswith(Trigger[0], 0, Trigger[1]): + Match = True + elif isinstance(Trigger, str): + if Message[0].lower() == Trigger.lower(): + Match = True + else: # compiled regex + RMatch = Trigger.search(line) + if RMatch: + Match = True + + if not Match: + continue + + self.Logger.debug(sys._getframe().f_code.co_name + " \"{0}\" Match -> {1} | {2}".format(player.Name, Command.__class__.__name__, Trigger)) + + if Level < Command.Level: + RetMessage = "You do not have access to this command! (You: {0} | Required: {1})".format(Level, Command.Level) + continue + + try: + if RMatch: + Ret = await Command._rfunc(line, RMatch, player) + else: + Ret = await Command._func(Message, player) + except Exception as e: + self.Logger.error(traceback.format_exc()) + self.Torchlight().SayChat("Error: {0}".format(str(e))) + + RetMessage = None + + if isinstance(Ret, str): + Message = Ret.split(sep = ' ', maxsplit = 1) + Ret = None + + if Ret != None and Ret > 0: + break + + if Ret != None and Ret >= 0: + break + + if RetMessage: + self.Torchlight().SayPrivate(player, RetMessage) + + if self.NeedsReload: + self.NeedsReload = False + self.Reload() + + return Ret diff --git a/torchlight_changes_unloze/torchlight3/Torchlight/CommandHandlermg.py b/torchlight_changes_unloze/torchlight3/Torchlight/CommandHandlermg.py new file mode 100755 index 00000000..bf00d8f2 --- /dev/null +++ b/torchlight_changes_unloze/torchlight3/Torchlight/CommandHandlermg.py @@ -0,0 +1,116 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +import logging +import asyncio +import sys +import re +import traceback +import math +from importlib import reload +from . import Commandsmg + +class CommandHandlermg(): + def __init__(self, Torchlight): + self.Logger = logging.getLogger(__class__.__name__) + self.Torchlight = Torchlight + self.Commands = [] + self.NeedsReload = False + + def Setup(self): + Counter = len(self.Commands) + self.Commands.clear() + if Counter: + self.Logger.info(sys._getframe().f_code.co_name + " Unloaded {0} commands!".format(Counter)) + + Counter = 0 + for subklass in sorted(Commandsmg.BaseCommand.__subclasses__(), key = lambda x: x.Order, reverse = True): + try: + Command = subklass(self.Torchlight) + if hasattr(Command, "_setup"): + Command._setup() + except Exception as e: + self.Logger.error(traceback.format_exc()) + else: + self.Commands.append(Command) + Counter += 1 + + self.Logger.info(sys._getframe().f_code.co_name + " Loaded {0} commands!".format(Counter)) + + def Reload(self): + try: + reload(Commandsmg) + except Exception as e: + self.Logger.error(traceback.format_exc()) + else: + self.Setup() + + async def HandleCommand(self, line, player): + Message = line.split(sep = ' ', maxsplit = 1) + if len(Message) < 2: + Message.append("") + Message[1] = Message[1].strip() + + if Message[1] and self.Torchlight().LastUrl: + Message[1] = Message[1].replace("!last", self.Torchlight().LastUrl) + line = Message[0] + ' ' + Message[1] + + Level = 0 + if player.Access: + Level = player.Access["level"] + + RetMessage = None + Ret = None + for Command in self.Commands: + for Trigger in Command.Triggers: + Match = False + RMatch = None + if isinstance(Trigger, tuple): + if Message[0].lower().startswith(Trigger[0], 0, Trigger[1]): + Match = True + elif isinstance(Trigger, str): + if Message[0].lower() == Trigger.lower(): + Match = True + else: # compiled regex + RMatch = Trigger.search(line) + if RMatch: + Match = True + + if not Match: + continue + + self.Logger.debug(sys._getframe().f_code.co_name + " \"{0}\" Match -> {1} | {2}".format(player.Name, Command.__class__.__name__, Trigger)) + + if Level < Command.Level: + RetMessage = "You do not have access to this command! (You: {0} | Required: {1})".format(Level, Command.Level) + continue + + try: + if RMatch: + Ret = await Command._rfunc(line, RMatch, player) + else: + Ret = await Command._func(Message, player) + except Exception as e: + self.Logger.error(traceback.format_exc()) + self.Torchlight().SayChat("Error: {0}".format(str(e))) + + RetMessage = None + + if isinstance(Ret, str): + Message = Ret.split(sep = ' ', maxsplit = 1) + Ret = None + + if Ret != None and Ret > 0: + break + + if Ret != None and Ret >= 0: + break + + if RetMessage: + self.Torchlight().SayPrivate(player, RetMessage) + + if self.NeedsReload: + self.NeedsReload = False + self.Reload() + + return Ret + diff --git a/torchlight_changes_unloze/torchlight3/Torchlight/Commands.py b/torchlight_changes_unloze/torchlight3/Torchlight/Commands.py new file mode 100755 index 00000000..5f76e5cc --- /dev/null +++ b/torchlight_changes_unloze/torchlight3/Torchlight/Commands.py @@ -0,0 +1,928 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +import asyncio +import os +import sys +import logging +import math +from .Utils import Utils, DataHolder +import traceback + +class BaseCommand(): + Order = 0 + def __init__(self, torchlight): + self.Logger = logging.getLogger(__class__.__name__) + self.Torchlight = torchlight + self.Triggers = [] + self.Level = 0 + + def check_chat_cooldown(self, player): + if player.ChatCooldown > self.Torchlight().Master.Loop.time(): + cooldown = player.ChatCooldown - self.Torchlight().Master.Loop.time() + self.Torchlight().SayPrivate(player, "You're on cooldown for the next {0:.1f} seconds.".format(cooldown)) + return True + + def check_disabled(self, player): + Level = 0 + if player.Access: + Level = player.Access["level"] + + Disabled = self.Torchlight().Disabled + if Disabled and (Disabled > Level or Disabled == Level and Level < self.Torchlight().Config["AntiSpam"]["ImmunityLevel"]): + self.Torchlight().SayPrivate(player, "Torchlight is currently disabled!") + return True + + async def _func(self, message, player): + self.Logger.debug(sys._getframe().f_code.co_name) + + +class URLFilter(BaseCommand): + Order = 1 + import re + import aiohttp + import magic + import datetime + import json + import io + from bs4 import BeautifulSoup + from PIL import Image + def __init__(self, torchlight): + super().__init__(torchlight) + self.Triggers = [self.re.compile(r'''(?i)\b((?:https?://|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’]))''', self.re.IGNORECASE)] + self.Level = 10 + self.re_youtube = self.re.compile(r'.*?(?:youtube\.com\/\S*(?:(?:\/e(?:mbed))?\/|watch\?(?:\S*?&?v\=))|youtu\.be\/)([a-zA-Z0-9_-]{6,11}).*?') + + async def URLInfo(self, url, yt = False): + Text = None + Info = None + match = self.re_youtube.search(url) + if match or yt: + Temp = DataHolder() + Time = None + + if Temp(url.find("&t=")) != -1 or Temp(url.find("?t=")) != -1 or Temp(url.find("#t=")) != -1: + TimeStr = url[Temp.value + 3:].split('&')[0].split('?')[0].split('#')[0] + if TimeStr: + Time = Utils.ParseTime(TimeStr) + + Proc = await asyncio.create_subprocess_exec("youtube-dl", "--dump-json", "-g", url, + stdout = asyncio.subprocess.PIPE) + Out, _ = await Proc.communicate() + + parts = Out.split(b'\n') + parts.pop() # trailing new line + + Info = parts.pop() + url = parts.pop() + + url = url.strip().decode("ascii") + Info = self.json.loads(Info) + + if Info["extractor_key"] == "Youtube": + self.Torchlight().SayChat("\x07E52D27[YouTube]\x01 {0} | {1} | {2}/5.00 | {3:,}".format( + Info["title"], str(self.datetime.timedelta(seconds = Info["duration"])), round(Info["average_rating"], 2), int(Info["view_count"]))) + else: + match = None + + if Time: + url += "#t={0}".format(Time) + + else: + try: + async with self.aiohttp.ClientSession() as session: + Response = await asyncio.wait_for(session.get(url), 5) + if Response: + ContentType = Response.headers.get("Content-Type") + ContentLength = Response.headers.get("Content-Length") + Content = await asyncio.wait_for(Response.content.read(65536), 5) + + if not ContentLength: + ContentLength = -1 + + if ContentType.startswith("text"): + if ContentType.startswith("text/plain"): + Text = Content.decode("utf-8", errors = "ignore") + else: + Soup = self.BeautifulSoup(Content.decode("utf-8", errors = "ignore"), "lxml") + if Soup.title: + self.Torchlight().SayChat("[URL] {0}".format(Soup.title.string)) + elif ContentType.startswith("image"): + fp = self.io.BytesIO(Content) + im = self.Image.open(fp) + self.Torchlight().SayChat("[IMAGE] {0} | Width: {1} | Height: {2} | Size: {3}".format(im.format, im.size[0], im.size[1], Utils.HumanSize(ContentLength))) + fp.close() + else: + Filetype = self.magic.from_buffer(bytes(Content)) + self.Torchlight().SayChat("[FILE] {0} | Size: {1}".format(Filetype, Utils.HumanSize(ContentLength))) + + Response.close() + except Exception as e: + self.Torchlight().SayChat("Error: {0}".format(str(e))) + self.Logger.error(traceback.format_exc()) + + self.Torchlight().LastUrl = url + return url, Text + + async def _rfunc(self, line, match, player): + Url = match.groups()[0] + if not Url.startswith("http") and not Url.startswith("ftp"): + Url = "http://" + Url + + if line.startswith("!yt "): + URL, _ = await self.URLInfo(Url, True) + return "!yt " + URL + + if line.startswith("!dec "): + _, text = await self.URLInfo(Url, False) + if text: + return "!dec " + text + + asyncio.ensure_future(self.URLInfo(Url)) + return -1 + + +def FormatAccess(Torchlight, player): + Answer = "#{0} \"{1}\"({2}) is ".format(player.UserID, player.Name, player.UniqueID) + Level = str(0) + if player.Access: + Level = str(player.Access["level"]) + Answer += "level {0!s} as {1}.".format(Level, player.Access["name"]) + else: + Answer += "not authenticated." + + if Level in Torchlight().Config["AudioLimits"]: + Uses = Torchlight().Config["AudioLimits"][Level]["Uses"] + TotalTime = Torchlight().Config["AudioLimits"][Level]["TotalTime"] + + if Uses >= 0: + Answer += " Uses: {0}/{1}".format(player.Storage["Audio"]["Uses"], Uses) + if TotalTime >= 0: + Answer += " Time: {0}/{1}".format(round(player.Storage["Audio"]["TimeUsed"], 2), round(TotalTime, 2)) + + return Answer + +class Access(BaseCommand): + def __init__(self, torchlight): + super().__init__(torchlight) + self.Triggers = ["!access"] + self.Level = 0 + + async def _func(self, message, player): + self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message)) + + if self.check_chat_cooldown(player): + return -1 + + Count = 0 + if message[0] == "!access": + if message[1]: + return -1 + + self.Torchlight().SayChat(FormatAccess(self.Torchlight, player), player) + + return 0 + +class Who(BaseCommand): + def __init__(self, torchlight): + super().__init__(torchlight) + self.Triggers = ["!who", "!whois"] + self.Level = 1 + + async def _func(self, message, player): + self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message)) + + Count = 0 + if message[0] == "!who": + for Player in self.Torchlight().Players: + if Player.Name.lower().find(message[1].lower()) != -1: + self.Torchlight().SayChat(FormatAccess(self.Torchlight, Player)) + + Count += 1 + if Count >= 3: + break + + elif message[0] == "!whois": + for UniqueID, Access in self.Torchlight().Access: + if Access["name"].lower().find(message[1].lower()) != -1: + Player = self.Torchlight().Players.FindUniqueID(UniqueID) + if Player: + self.Torchlight().SayChat(FormatAccess(self.Torchlight, Player)) + else: + self.Torchlight().SayChat("#? \"{0}\"({1}) is level {2!s} is currently offline.".format(Access["name"], UniqueID, Access["level"])) + + Count += 1 + if Count >= 3: + break + return 0 + + +class WolframAlpha(BaseCommand): + import urllib.parse + import aiohttp + import xml.etree.ElementTree as etree + import re + def __init__(self, torchlight): + super().__init__(torchlight) + self.Triggers = ["!cc"] + self.Level = 10 + + def Clean(self, Text): + return self.re.sub("[ ]{2,}", " ", Text.replace(' | ', ': ').replace('\n', ' | ').replace('~~', ' ≈ ')).strip() + + async def Calculate(self, Params, player): + async with self.aiohttp.ClientSession() as session: + Response = await asyncio.wait_for(session.get("http://api.wolframalpha.com/v2/query", params=Params), 10) + if not Response: + return 1 + + Data = await asyncio.wait_for(Response.text(), 5) + if not Data: + return 2 + + Root = self.etree.fromstring(Data) + + + # Find all pods with plaintext answers + # Filter out None -answers, strip strings and filter out the empty ones + Pods = list(filter(None, [p.text.strip() for p in Root.findall('.//subpod/plaintext') if p is not None and p.text is not None])) + + # no answer pods found, check if there are didyoumeans-elements + if not Pods: + Didyoumeans = Root.find("didyoumeans") + # no support for future stuff yet, TODO? + if not Didyoumeans: + # If there's no pods, the question clearly wasn't understood + self.Torchlight().SayChat("Sorry, couldn't understand the question.", player) + return 3 + + Options = [] + for Didyoumean in Didyoumeans: + Options.append("\"{0}\"".format(Didyoumean.text)) + Line = " or ".join(Options) + Line = "Did you mean {0}?".format(Line) + self.Torchlight().SayChat(Line, player) + return 0 + + # If there's only one pod with text, it's probably the answer + # example: "integral x²" + if len(Pods) == 1: + Answer = self.Clean(Pods[0]) + self.Torchlight().SayChat(Answer, player) + return 0 + + # If there's multiple pods, first is the question interpretation + Question = self.Clean(Pods[0].replace(' | ', ' ').replace('\n', ' ')) + # and second is the best answer + Answer = self.Clean(Pods[1]) + self.Torchlight().SayChat("{0} = {1}".format(Question, Answer), player) + return 0 + + async def _func(self, message, player): + self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message)) + + if self.check_chat_cooldown(player): + return -1 + + if self.check_disabled(player): + return -1 + + Params = dict({"input": message[1], "appid": self.Torchlight().Config["WolframAPIKey"]}) + Ret = await self.Calculate(Params, player) + return Ret + + +class UrbanDictionary(BaseCommand): + import aiohttp + def __init__(self, torchlight): + super().__init__(torchlight) + self.Triggers = ["!define", "!ud"] + self.Level = 10 + + async def _func(self, message, player): + self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message)) + + if self.check_chat_cooldown(player): + return -1 + + if self.check_disabled(player): + return -1 + + async with self.aiohttp.ClientSession() as session: + Response = await asyncio.wait_for(session.get("https://api.urbandictionary.com/v0/define?term={0}".format(message[1])), 5) + if not Response: + return 1 + + Data = await asyncio.wait_for(Response.json(), 5) + if not Data: + return 3 + + if not 'list' in Data or not Data["list"]: + self.Torchlight().SayChat("[UB] No definition found for: {}".format(message[1]), player) + return 4 + + def print_item(item): + self.Torchlight().SayChat("[UD] {word} ({thumbs_up}/{thumbs_down}): {definition}\n{example}".format(**item), player) + + print_item(Data["list"][0]) + + +class OpenWeather(BaseCommand): + import aiohttp + import geoip2.database + def __init__(self, torchlight): + super().__init__(torchlight) + self.GeoIP = self.geoip2.database.Reader("/usr/share/GeoIP/GeoLite2-City.mmdb") + self.Triggers = ["!w", "!vv"] + self.Level = 10 + + async def _func(self, message, player): + self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message)) + + if self.check_chat_cooldown(player): + return -1 + + if self.check_disabled(player): + return -1 + + if not message[1]: + # Use GeoIP location + info = self.GeoIP.city(player.Address.split(":")[0]) + Search = "lat={}&lon={}".format(info.location.latitude, info.location.longitude) + else: + Search = "q={}".format(message[1]) + + async with self.aiohttp.ClientSession() as session: + Response = await asyncio.wait_for(session.get("https://api.openweathermap.org/data/2.5/weather?APPID={0}&units=metric&{1}".format( + self.Torchlight().Config["OpenWeatherAPIKey"], Search)), 5) + if not Response: + return 2 + + Data = await asyncio.wait_for(Response.json(), 5) + if not Data: + return 3 + + if Data["cod"] != 200: + self.Torchlight().SayPrivate(player, "[OW] {0}".format(Data["message"])) + return 5 + + degToCardinal = lambda d: ["N", "NE", "E", "SE", "S", "SW", "W", "NW"][int(((d + 22.5)/45.0) % 8)] + if "deg" in Data["wind"]: + windDir = degToCardinal(Data["wind"]["deg"]) + else: + windDir = "?" + + timezone = "{}{}".format('+' if Data["timezone"] > 0 else '', int(Data["timezone"] / 3600)) + if Data["timezone"] % 3600 != 0: + timezone += ":{}".format((Data["timezone"] % 3600) / 60) + + self.Torchlight().SayChat("[{}, {}](UTC{}) {}°C ({}/{}) {}: {} | Wind {} {}kph | Clouds: {}%% | Humidity: {}%%".format(Data["name"], Data["sys"]["country"], timezone, + Data["main"]["temp"], Data["main"]["temp_min"], Data["main"]["temp_max"], Data["weather"][0]["main"], Data["weather"][0]["description"], + windDir, Data["wind"]["speed"], Data["clouds"]["all"], Data["main"]["humidity"]), player) + + return 0 + +''' +class WUnderground(BaseCommand): + import aiohttp + def __init__(self, torchlight): + super().__init__(torchlight) + self.Triggers = ["!w"] + self.Level = 0 + + async def _func(self, message, player): + if not message[1]: + # Use IP address + Search = "autoip" + Additional = "?geo_ip={0}".format(player.Address.split(":")[0]) + else: + async with self.aiohttp.ClientSession() as session: + Response = await asyncio.wait_for(session.get("http://autocomplete.wunderground.com/aq?format=JSON&query={0}".format(message[1])), 5) + if not Response: + return 2 + + Data = await asyncio.wait_for(Response.json(), 5) + if not Data: + return 3 + + if not Data["RESULTS"]: + self.Torchlight().SayPrivate(player, "[WU] No cities match your search query.") + return 4 + + Search = Data["RESULTS"][0]["name"] + Additional = "" + + async with self.aiohttp.ClientSession() as session: + Response = await asyncio.wait_for(session.get("http://api.wunderground.com/api/{0}/conditions/q/{1}.json{2}".format( + self.Torchlight().Config["WundergroundAPIKey"], Search, Additional)), 5) + if not Response: + return 2 + + Data = await asyncio.wait_for(Response.json(), 5) + if not Data: + return 3 + + if "error" in Data["response"]: + self.Torchlight().SayPrivate(player, "[WU] {0}.".format(Data["response"]["error"]["description"])) + return 5 + + if not "current_observation" in Data: + Choices = str() + NumResults = len(Data["response"]["results"]) + for i, Result in enumerate(Data["response"]["results"]): + Choices += "{0}, {1}".format(Result["city"], + Result["state"] if Result["state"] else Result ["country_iso3166"]) + + if i < NumResults - 1: + Choices += " | " + + self.Torchlight().SayPrivate(player, "[WU] Did you mean: {0}".format(Choices)) + return 6 + + Observation = Data["current_observation"] + + self.Torchlight().SayChat("[{0}, {1}] {2}°C ({3}F) {4} | Wind {5} {6}kph ({7}mph) | Humidity: {8}".format(Observation["display_location"]["city"], + Observation["display_location"]["state"] if Observation["display_location"]["state"] else Observation["display_location"]["country_iso3166"], + Observation["temp_c"], Observation["temp_f"], Observation["weather"], + Observation["wind_dir"], Observation["wind_kph"], Observation["wind_mph"], + Observation["relative_humidity"])) + + return 0 +''' + +class VoteDisable(BaseCommand): + def __init__(self, torchlight): + super().__init__(torchlight) + self.Triggers = ["!votedisable", "!disablevote"] + self.Level = 0 + + async def _func(self, message, player): + self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message)) + + if self.Torchlight().Disabled: + self.Torchlight().SayPrivate(player, "Torchlight is already disabled for the duration of this map.") + return + + self.Torchlight().DisableVotes.add(player.UniqueID) + + have = len(self.Torchlight().DisableVotes) + needed = len(self.Torchlight().Players) // 5 + if have >= needed: + self.Torchlight().SayChat("Torchlight has been disabled for the duration of this map.") + self.Torchlight().Disabled = 6 + else: + self.Torchlight().SayPrivate(player, "Torchlight needs {0} more disable votes to be disabled.".format(needed - have)) + + +class VoiceCommands(BaseCommand): + import json + import random + def __init__(self, torchlight): + super().__init__(torchlight) + self.Triggers = ["!random", "!search"] + self.Level = 0 + + def LoadTriggers(self): + try: + with open("triggers.json", "r") as fp: + Triggers = self.json.load(fp) + except ValueError as e: + self.Logger.error(sys._getframe().f_code.co_name + ' ' + str(e)) + self.Torchlight().SayChat(str(e)) + + self.VoiceTriggers = dict() + for Line in Triggers: + for Trigger in Line["names"]: + self.VoiceTriggers[Trigger] = Line["sound"] + + def _setup(self): + self.Logger.debug(sys._getframe().f_code.co_name) + self.LoadTriggers() + for Trigger in self.VoiceTriggers.keys(): + self.Triggers.append(Trigger) + + async def _func(self, message, player): + self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message)) + + if self.check_disabled(player): + return -1 + + Level = 0 + if player.Access: + Level = player.Access["level"] + + message[0] = message[0].lower() + message[1] = message[1].lower() + if message[0][0] != '!' and Level < 2: + return 1 + + if message[0] == "!search": + res = [] + for key in self.VoiceTriggers.keys(): + if message[1] in key.lower(): + res.append(key) + self.Torchlight().SayPrivate(player, "{} results: {}".format(len(res), ", ".join(res))) + return 0 + elif Level < 2: + return 0 + + if message[0] == "!random": + Trigger = self.random.choice(list(self.VoiceTriggers.values())) + if isinstance(Trigger, list): + Sound = self.random.choice(Trigger) + else: + Sound = Trigger + else: + Sounds = self.VoiceTriggers[message[0]] + + try: + Num = int(message[1]) + except ValueError: + Num = None + + if isinstance(Sounds, list): + if Num and Num > 0 and Num <= len(Sounds): + Sound = Sounds[Num - 1] + + elif message[1]: + searching = message[1].startswith('?') + search = message[1][1:] if searching else message[1] + Sound = None + names = [] + matches = [] + for sound in Sounds: + name = os.path.splitext(os.path.basename(sound))[0] + names.append(name) + + if search and search in name.lower(): + matches.append((name, sound)) + + if matches: + matches.sort(key=lambda t: len(t[0])) + mlist = [t[0] for t in matches] + if searching: + self.Torchlight().SayPrivate(player, "{} results: {}".format(len(mlist), ", ".join(mlist))) + return 0 + + Sound = matches[0][1] + if len(matches) > 1: + self.Torchlight().SayPrivate(player, "Multiple matches: {}".format(", ".join(mlist))) + + if not Sound and not Num: + if not searching: + self.Torchlight().SayPrivate(player, "Couldn't find {} in list of sounds.".format(message[1])) + self.Torchlight().SayPrivate(player, ", ".join(names)) + return 1 + + elif Num: + self.Torchlight().SayPrivate(player, "Number {} is out of bounds, max {}.".format(Num, len(Sounds))) + return 1 + + else: + Sound = self.random.choice(Sounds) + else: + Sound = Sounds + + if not Sound: + return 1 + + Path = os.path.abspath(os.path.join("sounds", Sound)) + AudioClip = self.Torchlight().AudioManager.AudioClip(player, "file://" + Path) + if not AudioClip: + return 1 + + return AudioClip.Play() + + +class YouTube(BaseCommand): + def __init__(self, torchlight): + super().__init__(torchlight) + self.Triggers = ["!yt"] + self.Level = 3 + + async def _func(self, message, player): + self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message)) + + if self.check_disabled(player): + return -1 + + if self.Torchlight().LastUrl: + message[1] = message[1].replace("!last", self.Torchlight().LastUrl) + + Temp = DataHolder() + Time = None + + if Temp(message[1].find("&t=")) != -1 or Temp(message[1].find("?t=")) != -1 or Temp(message[1].find("#t=")) != -1: + TimeStr = message[1][Temp.value + 3:].split('&')[0].split('?')[0].split('#')[0] + if TimeStr: + Time = Utils.ParseTime(TimeStr) + + AudioClip = self.Torchlight().AudioManager.AudioClip(player, message[1]) + if not AudioClip: + return 1 + + return AudioClip.Play(Time) + +class YouTubeSearch(BaseCommand): + import json + import datetime + def __init__(self, torchlight): + super().__init__(torchlight) + self.Triggers = ["!yts"] + self.Level = 3 + + async def _func(self, message, player): + self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message)) + + if self.check_disabled(player): + return -1 + + Temp = DataHolder() + Time = None + + if Temp(message[1].find("&t=")) != -1 or Temp(message[1].find("?t=")) != -1 or Temp(message[1].find("#t=")) != -1: + TimeStr = message[1][Temp.value + 3:].split('&')[0].split('?')[0].split('#')[0] + if TimeStr: + Time = Utils.ParseTime(TimeStr) + message[1] = message[1][:Temp.value] + + Proc = await asyncio.create_subprocess_exec("youtube-dl", "--dump-json", "-xg", "ytsearch:" + message[1], + stdout = asyncio.subprocess.PIPE) + Out, _ = await Proc.communicate() + + print('out value: ', Out) + url, Info = Out.split(b'\n', maxsplit = 1) + url = url.strip().decode("ascii") + Info = self.json.loads(Info) + + if Info["extractor_key"] == "Youtube": + self.Torchlight().SayChat("\x07E52D27[YouTube]\x01 {0} | {1} | {2}/5.00 | {3:,}".format( + Info["title"], str(self.datetime.timedelta(seconds = Info["duration"])), round(Info["average_rating"] or 0, 2), int(Info["view_count"]))) + AudioClip = self.Torchlight().AudioManager.AudioClip(player, url) + if not AudioClip: + return 1 + self.Torchlight().LastUrl = url + return AudioClip.Play(Time) + + +class Say(BaseCommand): + import gtts + import tempfile + VALID_LANGUAGES = [lang for lang in gtts.lang.tts_langs().keys()] + def __init__(self, torchlight): + super().__init__(torchlight) + self.Triggers = [("!say", 4)] + self.Level = 2 + + async def Say(self, player, language, message): + GTTS = self.gtts.gTTS(text = message, lang = language) + + TempFile = self.tempfile.NamedTemporaryFile(delete = False) + GTTS.write_to_fp(TempFile) + TempFile.close() + + AudioClip = self.Torchlight().AudioManager.AudioClip(player, "file://" + TempFile.name) + if not AudioClip: + os.unlink(TempFile.name) + return 1 + + if AudioClip.Play(): + AudioClip.AudioPlayer.AddCallback("Stop", lambda: os.unlink(TempFile.name)) + return 0 + else: + os.unlink(TempFile.name) + return 1 + + async def _func(self, message, player): + self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message)) + + if self.check_disabled(player): + return -1 + + if not message[1]: + return 1 + + Language = "en" + if len(message[0]) > 4: + Language = message[0][4:] + + if not Language in self.VALID_LANGUAGES: + return 1 + + asyncio.ensure_future(self.Say(player, Language, message[1])) + return 0 + +''' +class DECTalk(BaseCommand): + import tempfile + def __init__(self, torchlight): + super().__init__(torchlight) + self.Triggers = ["!dec"] + self.Level = 0 + + async def Say(self, player, message): + message = "[:phoneme on]" + message + TempFile = self.tempfile.NamedTemporaryFile(delete = False) + TempFile.close() + + Proc = await asyncio.create_subprocess_exec("wine", "say.exe", "-w", TempFile.name, + cwd = "dectalk", stdin = asyncio.subprocess.PIPE) + await Proc.communicate(message.encode('utf-8', errors='ignore')) + + AudioClip = self.Torchlight().AudioManager.AudioClip(player, "file://" + TempFile.name) + if not AudioClip: + os.unlink(TempFile.name) + return 1 + + if AudioClip.Play(None, "-af", "volume=10dB"): + AudioClip.AudioPlayer.AddCallback("Stop", lambda: os.unlink(TempFile.name)) + return 0 + else: + os.unlink(TempFile.name) + return 1 + + async def _func(self, message, player): + self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message)) + + if self.check_disabled(player): + return -1 + + if not message[1]: + return 1 + + asyncio.ensure_future(self.Say(player, message[1])) + return 0 +''' + +class Stop(BaseCommand): + def __init__(self, torchlight): + super().__init__(torchlight) + self.Triggers = ["!stop"] + self.Level = 0 + + async def _func(self, message, player): + self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message)) + + self.Torchlight().AudioManager.Stop(player, message[1]) + return True + + +class EnableDisable(BaseCommand): + def __init__(self, torchlight): + super().__init__(torchlight) + self.Triggers = ["!enable", "!disable"] + self.Level = 3 + + async def _func(self, message, player): + self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message)) + + if message[0] == "!enable": + if self.Torchlight().Disabled: + if self.Torchlight().Disabled > player.Access["level"]: + self.Torchlight().SayPrivate(player, "You don't have access to enable torchlight, since it was disabled by a higher level user.") + return 1 + self.Torchlight().SayChat("Torchlight has been enabled for the duration of this map - Type !disable to disable it again.") + + self.Torchlight().Disabled = False + + elif message[0] == "!disable": + if self.Torchlight().Disabled > player.Access["level"]: + self.Torchlight().SayPrivate(player, "You don't have access to disable torchlight, since it was already disabled by a higher level user.") + return 1 + self.Torchlight().SayChat("Torchlight has been disabled for the duration of this map - Type !enable to enable it again.") + self.Torchlight().Disabled = player.Access["level"] + + +class AdminAccess(BaseCommand): + from collections import OrderedDict + def __init__(self, torchlight): + super().__init__(torchlight) + self.Triggers = ["!access"] + self.Level = 4 + + def ReloadValidUsers(self): + self.Torchlight().Access.Load() + for Player in self.Torchlight().Players: + Access = self.Torchlight().Access[Player.UniqueID] + Player.Access = Access + + async def _func(self, message, player): + self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message)) + if not message[1]: + return -1 + + if message[1].lower() == "reload": + self.ReloadValidUsers() + self.Torchlight().SayChat("Loaded access list with {0} users".format(len(self.Torchlight().Access))) + + elif message[1].lower() == "save": + self.Torchlight().Access.Save() + self.Torchlight().SayChat("Saved access list with {0} users".format(len(self.Torchlight().Access))) + + # Modify access + else: + Player = None + Buf = message[1] + Temp = Buf.find(" as ") + if Temp != -1: + try: + Regname, Level = Buf[Temp + 4:].rsplit(' ', 1) + except ValueError as e: + self.Torchlight().SayChat(str(e)) + return 1 + + Regname = Regname.strip() + Level = Level.strip() + Buf = Buf[:Temp].strip() + else: + try: + Buf, Level = Buf.rsplit(' ', 1) + except ValueError as e: + self.Torchlight().SayChat(str(e)) + return 2 + + Buf = Buf.strip() + Level = Level.strip() + + # Find user by User ID + if Buf[0] == '#' and Buf[1:].isnumeric(): + Player = self.Torchlight().Players.FindUserID(int(Buf[1:])) + # Search user by name + else: + for Player_ in self.Torchlight().Players: + if Player_.Name.lower().find(Buf.lower()) != -1: + Player = Player_ + break + + if not Player: + self.Torchlight().SayChat("Couldn't find user: {0}".format(Buf)) + return 3 + + if Level.isnumeric() or (Level.startswith('-') and Level[1:].isdigit()): + Level = int(Level) + + if Level >= player.Access["level"] and player.Access["level"] < 10: + self.Torchlight().SayChat("Trying to assign level {0}, which is higher or equal than your level ({1})".format(Level, player.Access["level"])) + return 4 + + if Player.Access: + if Player.Access["level"] >= player.Access["level"] and player.Access["level"] < 10: + self.Torchlight().SayChat("Trying to modify level {0}, which is higher or equal than your level ({1})".format(Player.Access["level"], player.Access["level"])) + return 5 + + if "Regname" in locals(): + self.Torchlight().SayChat("Changed \"{0}\"({1}) as {2} level/name from {3} to {4} as {5}".format( + Player.Name, Player.UniqueID, Player.Access["name"], Player.Access["level"], Level, Regname)) + Player.Access["name"] = Regname + else: + self.Torchlight().SayChat("Changed \"{0}\"({1}) as {2} level from {3} to {4}".format( + Player.Name, Player.UniqueID, Player.Access["name"], Player.Access["level"], Level)) + + Player.Access["level"] = Level + self.Torchlight().Access[Player.UniqueID] = Player.Access + else: + if not "Regname" in locals(): + Regname = Player.Name + + self.Torchlight().Access[Player.UniqueID] = self.OrderedDict([("name", Regname), ("level", Level)]) + Player.Access = self.Torchlight().Access[Player.UniqueID] + self.Torchlight().SayChat("Added \"{0}\"({1}) to access list as {2} with level {3}".format(Player.Name, Player.UniqueID, Regname, Level)) + else: + if Level == "revoke" and Player.Access: + if Player.Access["level"] >= player.Access["level"] and player.Access["level"] < 10: + self.Torchlight().SayChat("Trying to revoke level {0}, which is higher or equal than your level ({1})".format(Player.Access["level"], player.Access["level"])) + return 6 + + self.Torchlight().SayChat("Removed \"{0}\"({1}) from access list (was {2} with level {3})".format( + Player.Name, Player.UniqueID, Player.Access["name"], Player.Access["level"])) + del self.Torchlight().Access[Player.UniqueID] + Player.Access = None + return 0 + +class Reload(BaseCommand): + def __init__(self, torchlight): + super().__init__(torchlight) + self.Triggers = ["!reload"] + self.Level = 4 + + async def _func(self, message, player): + self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message)) + self.Torchlight().Reload() + return 0 + +""" +class Exec(BaseCommand): + def __init__(self, torchlight): + super().__init__(torchlight) + self.Triggers = ["!exec"] + self.Level = 100 + + async def _func(self, message, player): + self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message)) + try: + Response = eval(message[1]) + except Exception as e: + self.Torchlight().SayChat("Error: {0}".format(str(e))) + return 1 + self.Torchlight().SayChat(str(Response)) + return 0 +""" diff --git a/torchlight_changes_unloze/torchlight3/Torchlight/Commandsmg.py b/torchlight_changes_unloze/torchlight3/Torchlight/Commandsmg.py new file mode 100755 index 00000000..7b954de6 --- /dev/null +++ b/torchlight_changes_unloze/torchlight3/Torchlight/Commandsmg.py @@ -0,0 +1,930 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +import asyncio +import os +import sys +import logging +import math +from .Utils import Utils, DataHolder +import traceback + +class BaseCommand(): + Order = 0 + def __init__(self, torchlight): + self.Logger = logging.getLogger(__class__.__name__) + self.Torchlight = torchlight + self.Triggers = [] + self.Level = 0 + + def check_chat_cooldown(self, player): + if player.ChatCooldown > self.Torchlight().Master.Loop.time(): + cooldown = player.ChatCooldown - self.Torchlight().Master.Loop.time() + self.Torchlight().SayPrivate(player, "You're on cooldown for the next {0:.1f} seconds.".format(cooldown)) + return True + + def check_disabled(self, player): + Level = 0 + if player.Access: + Level = player.Access["level"] + + Disabled = self.Torchlight().Disabled + if Disabled and (Disabled > Level or Disabled == Level and Level < self.Torchlight().Config["AntiSpam"]["ImmunityLevel"]): + self.Torchlight().SayPrivate(player, "Torchlight is currently disabled!") + return True + + async def _func(self, message, player): + self.Logger.debug(sys._getframe().f_code.co_name) + + +class URLFilter(BaseCommand): + Order = 1 + import re + import aiohttp + import magic + import datetime + import json + import io + from bs4 import BeautifulSoup + from PIL import Image + def __init__(self, torchlight): + super().__init__(torchlight) + self.Triggers = [self.re.compile(r'''(?i)\b((?:https?://|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’]))''', self.re.IGNORECASE)] + self.Level = 10 + self.re_youtube = self.re.compile(r'.*?(?:youtube\.com\/\S*(?:(?:\/e(?:mbed))?\/|watch\?(?:\S*?&?v\=))|youtu\.be\/)([a-zA-Z0-9_-]{6,11}).*?') + + async def URLInfo(self, url, yt = False): + Text = None + Info = None + match = self.re_youtube.search(url) + if match or yt: + Temp = DataHolder() + Time = None + + if Temp(url.find("&t=")) != -1 or Temp(url.find("?t=")) != -1 or Temp(url.find("#t=")) != -1: + TimeStr = url[Temp.value + 3:].split('&')[0].split('?')[0].split('#')[0] + if TimeStr: + Time = Utils.ParseTime(TimeStr) + + Proc = await asyncio.create_subprocess_exec("youtube-dl", "--dump-json", "-g", url, + stdout = asyncio.subprocess.PIPE) + Out, _ = await Proc.communicate() + + parts = Out.split(b'\n') + parts.pop() # trailing new line + + Info = parts.pop() + url = parts.pop() + + url = url.strip().decode("ascii") + Info = self.json.loads(Info) + + if Info["extractor_key"] == "Youtube": + self.Torchlight().SayChat("\x07E52D27[YouTube]\x01 {0} | {1} | {2}/5.00 | {3:,}".format( + Info["title"], str(self.datetime.timedelta(seconds = Info["duration"])), round(Info["average_rating"], 2), int(Info["view_count"]))) + else: + match = None + + if Time: + url += "#t={0}".format(Time) + + else: + try: + async with self.aiohttp.ClientSession() as session: + Response = await asyncio.wait_for(session.get(url), 5) + if Response: + ContentType = Response.headers.get("Content-Type") + ContentLength = Response.headers.get("Content-Length") + Content = await asyncio.wait_for(Response.content.read(65536), 5) + + if not ContentLength: + ContentLength = -1 + + if ContentType.startswith("text"): + if ContentType.startswith("text/plain"): + Text = Content.decode("utf-8", errors = "ignore") + else: + Soup = self.BeautifulSoup(Content.decode("utf-8", errors = "ignore"), "lxml") + if Soup.title: + self.Torchlight().SayChat("[URL] {0}".format(Soup.title.string)) + elif ContentType.startswith("image"): + fp = self.io.BytesIO(Content) + im = self.Image.open(fp) + self.Torchlight().SayChat("[IMAGE] {0} | Width: {1} | Height: {2} | Size: {3}".format(im.format, im.size[0], im.size[1], Utils.HumanSize(ContentLength))) + fp.close() + else: + Filetype = self.magic.from_buffer(bytes(Content)) + self.Torchlight().SayChat("[FILE] {0} | Size: {1}".format(Filetype, Utils.HumanSize(ContentLength))) + + Response.close() + except Exception as e: + self.Torchlight().SayChat("Error: {0}".format(str(e))) + self.Logger.error(traceback.format_exc()) + + self.Torchlight().LastUrl = url + return url, Text + + async def _rfunc(self, line, match, player): + Url = match.groups()[0] + if not Url.startswith("http") and not Url.startswith("ftp"): + Url = "http://" + Url + + if line.startswith("!yt "): + return + #URL, _ = await self.URLInfo(Url, True) + #return "!yt " + URL + + if line.startswith("!dec "): + _, text = await self.URLInfo(Url, False) + if text: + return "!dec " + text + + asyncio.ensure_future(self.URLInfo(Url)) + return -1 + + +def FormatAccess(Torchlight, player): + Answer = "#{0} \"{1}\"({2}) is ".format(player.UserID, player.Name, player.UniqueID) + Level = str(0) + if player.Access: + Level = str(player.Access["level"]) + Answer += "level {0!s} as {1}.".format(Level, player.Access["name"]) + else: + Answer += "not authenticated." + + if Level in Torchlight().Config["AudioLimits"]: + Uses = Torchlight().Config["AudioLimits"][Level]["Uses"] + TotalTime = Torchlight().Config["AudioLimits"][Level]["TotalTime"] + + if Uses >= 0: + Answer += " Uses: {0}/{1}".format(player.Storage["Audio"]["Uses"], Uses) + if TotalTime >= 0: + Answer += " Time: {0}/{1}".format(round(player.Storage["Audio"]["TimeUsed"], 2), round(TotalTime, 2)) + + return Answer + +class Access(BaseCommand): + def __init__(self, torchlight): + super().__init__(torchlight) + self.Triggers = ["!access"] + self.Level = 0 + + async def _func(self, message, player): + self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message)) + + if self.check_chat_cooldown(player): + return -1 + + Count = 0 + if message[0] == "!access": + if message[1]: + return -1 + + self.Torchlight().SayChat(FormatAccess(self.Torchlight, player), player) + + return 0 + +class Who(BaseCommand): + def __init__(self, torchlight): + super().__init__(torchlight) + self.Triggers = ["!who", "!whois"] + self.Level = 1 + + async def _func(self, message, player): + self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message)) + + Count = 0 + if message[0] == "!who": + for Player in self.Torchlight().Players: + if Player.Name.lower().find(message[1].lower()) != -1: + self.Torchlight().SayChat(FormatAccess(self.Torchlight, Player)) + + Count += 1 + if Count >= 3: + break + + elif message[0] == "!whois": + for UniqueID, Access in self.Torchlight().Access: + if Access["name"].lower().find(message[1].lower()) != -1: + Player = self.Torchlight().Players.FindUniqueID(UniqueID) + if Player: + self.Torchlight().SayChat(FormatAccess(self.Torchlight, Player)) + else: + self.Torchlight().SayChat("#? \"{0}\"({1}) is level {2!s} is currently offline.".format(Access["name"], UniqueID, Access["level"])) + + Count += 1 + if Count >= 3: + break + return 0 + + +class WolframAlpha(BaseCommand): + import urllib.parse + import aiohttp + import xml.etree.ElementTree as etree + import re + def __init__(self, torchlight): + super().__init__(torchlight) + self.Triggers = ["!cc"] + self.Level = 10 + + def Clean(self, Text): + return self.re.sub("[ ]{2,}", " ", Text.replace(' | ', ': ').replace('\n', ' | ').replace('~~', ' ≈ ')).strip() + + async def Calculate(self, Params, player): + async with self.aiohttp.ClientSession() as session: + Response = await asyncio.wait_for(session.get("http://api.wolframalpha.com/v2/query", params=Params), 10) + if not Response: + return 1 + + Data = await asyncio.wait_for(Response.text(), 5) + if not Data: + return 2 + + Root = self.etree.fromstring(Data) + + + # Find all pods with plaintext answers + # Filter out None -answers, strip strings and filter out the empty ones + Pods = list(filter(None, [p.text.strip() for p in Root.findall('.//subpod/plaintext') if p is not None and p.text is not None])) + + # no answer pods found, check if there are didyoumeans-elements + if not Pods: + Didyoumeans = Root.find("didyoumeans") + # no support for future stuff yet, TODO? + if not Didyoumeans: + # If there's no pods, the question clearly wasn't understood + self.Torchlight().SayChat("Sorry, couldn't understand the question.", player) + return 3 + + Options = [] + for Didyoumean in Didyoumeans: + Options.append("\"{0}\"".format(Didyoumean.text)) + Line = " or ".join(Options) + Line = "Did you mean {0}?".format(Line) + self.Torchlight().SayChat(Line, player) + return 0 + + # If there's only one pod with text, it's probably the answer + # example: "integral x²" + if len(Pods) == 1: + Answer = self.Clean(Pods[0]) + self.Torchlight().SayChat(Answer, player) + return 0 + + # If there's multiple pods, first is the question interpretation + Question = self.Clean(Pods[0].replace(' | ', ' ').replace('\n', ' ')) + # and second is the best answer + Answer = self.Clean(Pods[1]) + self.Torchlight().SayChat("{0} = {1}".format(Question, Answer), player) + return 0 + + async def _func(self, message, player): + self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message)) + + if self.check_chat_cooldown(player): + return -1 + + if self.check_disabled(player): + return -1 + + Params = dict({"input": message[1], "appid": self.Torchlight().Config["WolframAPIKey"]}) + Ret = await self.Calculate(Params, player) + return Ret + + +class UrbanDictionary(BaseCommand): + import aiohttp + def __init__(self, torchlight): + super().__init__(torchlight) + self.Triggers = ["!define", "!ud"] + self.Level = 10 + + async def _func(self, message, player): + self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message)) + + if self.check_chat_cooldown(player): + return -1 + + if self.check_disabled(player): + return -1 + + async with self.aiohttp.ClientSession() as session: + Response = await asyncio.wait_for(session.get("https://api.urbandictionary.com/v0/define?term={0}".format(message[1])), 5) + if not Response: + return 1 + + Data = await asyncio.wait_for(Response.json(), 5) + if not Data: + return 3 + + if not 'list' in Data or not Data["list"]: + self.Torchlight().SayChat("[UB] No definition found for: {}".format(message[1]), player) + return 4 + + def print_item(item): + self.Torchlight().SayChat("[UD] {word} ({thumbs_up}/{thumbs_down}): {definition}\n{example}".format(**item), player) + + print_item(Data["list"][0]) + + +class OpenWeather(BaseCommand): + import aiohttp + import geoip2.database + def __init__(self, torchlight): + super().__init__(torchlight) + self.GeoIP = self.geoip2.database.Reader("/usr/share/GeoIP/GeoLite2-City.mmdb") + self.Triggers = ["!w", "!vv"] + self.Level = 10 + + async def _func(self, message, player): + self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message)) + + if self.check_chat_cooldown(player): + return -1 + + if self.check_disabled(player): + return -1 + + if not message[1]: + # Use GeoIP location + info = self.GeoIP.city(player.Address.split(":")[0]) + Search = "lat={}&lon={}".format(info.location.latitude, info.location.longitude) + else: + Search = "q={}".format(message[1]) + + async with self.aiohttp.ClientSession() as session: + Response = await asyncio.wait_for(session.get("https://api.openweathermap.org/data/2.5/weather?APPID={0}&units=metric&{1}".format( + self.Torchlight().Config["OpenWeatherAPIKey"], Search)), 5) + if not Response: + return 2 + + Data = await asyncio.wait_for(Response.json(), 5) + if not Data: + return 3 + + if Data["cod"] != 200: + self.Torchlight().SayPrivate(player, "[OW] {0}".format(Data["message"])) + return 5 + + degToCardinal = lambda d: ["N", "NE", "E", "SE", "S", "SW", "W", "NW"][int(((d + 22.5)/45.0) % 8)] + if "deg" in Data["wind"]: + windDir = degToCardinal(Data["wind"]["deg"]) + else: + windDir = "?" + + timezone = "{}{}".format('+' if Data["timezone"] > 0 else '', int(Data["timezone"] / 3600)) + if Data["timezone"] % 3600 != 0: + timezone += ":{}".format((Data["timezone"] % 3600) / 60) + + self.Torchlight().SayChat("[{}, {}](UTC{}) {}°C ({}/{}) {}: {} | Wind {} {}kph | Clouds: {}%% | Humidity: {}%%".format(Data["name"], Data["sys"]["country"], timezone, + Data["main"]["temp"], Data["main"]["temp_min"], Data["main"]["temp_max"], Data["weather"][0]["main"], Data["weather"][0]["description"], + windDir, Data["wind"]["speed"], Data["clouds"]["all"], Data["main"]["humidity"]), player) + + return 0 + +''' +class WUnderground(BaseCommand): + import aiohttp + def __init__(self, torchlight): + super().__init__(torchlight) + self.Triggers = ["!w"] + self.Level = 0 + + async def _func(self, message, player): + if not message[1]: + # Use IP address + Search = "autoip" + Additional = "?geo_ip={0}".format(player.Address.split(":")[0]) + else: + async with self.aiohttp.ClientSession() as session: + Response = await asyncio.wait_for(session.get("http://autocomplete.wunderground.com/aq?format=JSON&query={0}".format(message[1])), 5) + if not Response: + return 2 + + Data = await asyncio.wait_for(Response.json(), 5) + if not Data: + return 3 + + if not Data["RESULTS"]: + self.Torchlight().SayPrivate(player, "[WU] No cities match your search query.") + return 4 + + Search = Data["RESULTS"][0]["name"] + Additional = "" + + async with self.aiohttp.ClientSession() as session: + Response = await asyncio.wait_for(session.get("http://api.wunderground.com/api/{0}/conditions/q/{1}.json{2}".format( + self.Torchlight().Config["WundergroundAPIKey"], Search, Additional)), 5) + if not Response: + return 2 + + Data = await asyncio.wait_for(Response.json(), 5) + if not Data: + return 3 + + if "error" in Data["response"]: + self.Torchlight().SayPrivate(player, "[WU] {0}.".format(Data["response"]["error"]["description"])) + return 5 + + if not "current_observation" in Data: + Choices = str() + NumResults = len(Data["response"]["results"]) + for i, Result in enumerate(Data["response"]["results"]): + Choices += "{0}, {1}".format(Result["city"], + Result["state"] if Result["state"] else Result ["country_iso3166"]) + + if i < NumResults - 1: + Choices += " | " + + self.Torchlight().SayPrivate(player, "[WU] Did you mean: {0}".format(Choices)) + return 6 + + Observation = Data["current_observation"] + + self.Torchlight().SayChat("[{0}, {1}] {2}°C ({3}F) {4} | Wind {5} {6}kph ({7}mph) | Humidity: {8}".format(Observation["display_location"]["city"], + Observation["display_location"]["state"] if Observation["display_location"]["state"] else Observation["display_location"]["country_iso3166"], + Observation["temp_c"], Observation["temp_f"], Observation["weather"], + Observation["wind_dir"], Observation["wind_kph"], Observation["wind_mph"], + Observation["relative_humidity"])) + + return 0 +''' + +class VoteDisable(BaseCommand): + def __init__(self, torchlight): + super().__init__(torchlight) + self.Triggers = ["!votedisable", "!disablevote"] + self.Level = 0 + + async def _func(self, message, player): + self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message)) + + if self.Torchlight().Disabled: + self.Torchlight().SayPrivate(player, "Torchlight is already disabled for the duration of this map.") + return + + self.Torchlight().DisableVotes.add(player.UniqueID) + + have = len(self.Torchlight().DisableVotes) + needed = len(self.Torchlight().Players) // 5 + if have >= needed: + self.Torchlight().SayChat("Torchlight has been disabled for the duration of this map.") + self.Torchlight().Disabled = 6 + else: + self.Torchlight().SayPrivate(player, "Torchlight needs {0} more disable votes to be disabled.".format(needed - have)) + + +class VoiceCommands(BaseCommand): + import json + import random + def __init__(self, torchlight): + super().__init__(torchlight) + self.Triggers = ["!random", "!search"] + self.Level = 0 + + def LoadTriggers(self): + try: + with open("triggers.json", "r") as fp: + Triggers = self.json.load(fp) + except ValueError as e: + self.Logger.error(sys._getframe().f_code.co_name + ' ' + str(e)) + self.Torchlight().SayChat(str(e)) + + self.VoiceTriggers = dict() + for Line in Triggers: + for Trigger in Line["names"]: + self.VoiceTriggers[Trigger] = Line["sound"] + + def _setup(self): + self.Logger.debug(sys._getframe().f_code.co_name) + self.LoadTriggers() + for Trigger in self.VoiceTriggers.keys(): + self.Triggers.append(Trigger) + + async def _func(self, message, player): + self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message)) + + if self.check_disabled(player): + return -1 + + Level = 0 + if player.Access: + Level = player.Access["level"] + + message[0] = message[0].lower() + message[1] = message[1].lower() + if message[0][0] != '!' and Level < 2: + return 1 + + if message[0] == "!search": + res = [] + for key in self.VoiceTriggers.keys(): + if message[1] in key.lower(): + res.append(key) + self.Torchlight().SayPrivate(player, "{} results: {}".format(len(res), ", ".join(res))) + return 0 + elif Level < 2: + return 0 + + if message[0] == "!random": + Trigger = self.random.choice(list(self.VoiceTriggers.values())) + if isinstance(Trigger, list): + Sound = self.random.choice(Trigger) + else: + Sound = Trigger + else: + Sounds = self.VoiceTriggers[message[0]] + + try: + Num = int(message[1]) + except ValueError: + Num = None + + if isinstance(Sounds, list): + if Num and Num > 0 and Num <= len(Sounds): + Sound = Sounds[Num - 1] + + elif message[1]: + searching = message[1].startswith('?') + search = message[1][1:] if searching else message[1] + Sound = None + names = [] + matches = [] + for sound in Sounds: + name = os.path.splitext(os.path.basename(sound))[0] + names.append(name) + + if search and search in name.lower(): + matches.append((name, sound)) + + if matches: + matches.sort(key=lambda t: len(t[0])) + mlist = [t[0] for t in matches] + if searching: + self.Torchlight().SayPrivate(player, "{} results: {}".format(len(mlist), ", ".join(mlist))) + return 0 + + Sound = matches[0][1] + if len(matches) > 1: + self.Torchlight().SayPrivate(player, "Multiple matches: {}".format(", ".join(mlist))) + + if not Sound and not Num: + if not searching: + self.Torchlight().SayPrivate(player, "Couldn't find {} in list of sounds.".format(message[1])) + self.Torchlight().SayPrivate(player, ", ".join(names)) + return 1 + + elif Num: + self.Torchlight().SayPrivate(player, "Number {} is out of bounds, max {}.".format(Num, len(Sounds))) + return 1 + + else: + Sound = self.random.choice(Sounds) + else: + Sound = Sounds + + if not Sound: + return 1 + + Path = os.path.abspath(os.path.join("sounds", Sound)) + AudioClip = self.Torchlight().AudioManager.AudioClip(player, "file://" + Path) + if not AudioClip: + return 1 + + return AudioClip.Play() + + +class YouTube(BaseCommand): + def __init__(self, torchlight): + super().__init__(torchlight) + self.Triggers = ["!yt"] + self.Level = 3 + + async def _func(self, message, player): + self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message)) + + if self.check_disabled(player): + return -1 + + if self.Torchlight().LastUrl: + message[1] = message[1].replace("!last", self.Torchlight().LastUrl) + + Temp = DataHolder() + Time = None + + if Temp(message[1].find("&t=")) != -1 or Temp(message[1].find("?t=")) != -1 or Temp(message[1].find("#t=")) != -1: + TimeStr = message[1][Temp.value + 3:].split('&')[0].split('?')[0].split('#')[0] + if TimeStr: + Time = Utils.ParseTime(TimeStr) + + AudioClip = self.Torchlight().AudioManager.AudioClip(player, message[1]) + if not AudioClip: + return 1 + + return AudioClip.Play(Time) + +class YouTubeSearch(BaseCommand): + import json + import datetime + def __init__(self, torchlight): + super().__init__(torchlight) + self.Triggers = ["!yts"] + self.Level = 3 + + async def _func(self, message, player): + return -1 + self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message)) + + if self.check_disabled(player): + return -1 + + Temp = DataHolder() + Time = None + + if Temp(message[1].find("&t=")) != -1 or Temp(message[1].find("?t=")) != -1 or Temp(message[1].find("#t=")) != -1: + TimeStr = message[1][Temp.value + 3:].split('&')[0].split('?')[0].split('#')[0] + if TimeStr: + Time = Utils.ParseTime(TimeStr) + message[1] = message[1][:Temp.value] + + Proc = await asyncio.create_subprocess_exec("youtube-dl", "--dump-json", "-xg", "ytsearch:" + message[1], + stdout = asyncio.subprocess.PIPE) + Out, _ = await Proc.communicate() + + print('out value: ', Out) + url, Info = Out.split(b'\n', maxsplit = 1) + url = url.strip().decode("ascii") + Info = self.json.loads(Info) + + if Info["extractor_key"] == "Youtube": + self.Torchlight().SayChat("\x07E52D27[YouTube]\x01 {0} | {1} | {2}/5.00 | {3:,}".format( + Info["title"], str(self.datetime.timedelta(seconds = Info["duration"])), round(Info["average_rating"] or 0, 2), int(Info["view_count"]))) + AudioClip = self.Torchlight().AudioManager.AudioClip(player, url) + if not AudioClip: + return 1 + self.Torchlight().LastUrl = url + return AudioClip.Play(Time) + + +class Say(BaseCommand): + import gtts + import tempfile + VALID_LANGUAGES = [lang for lang in gtts.lang.tts_langs().keys()] + def __init__(self, torchlight): + super().__init__(torchlight) + self.Triggers = [("!say", 4)] + self.Level = 2 + + async def Say(self, player, language, message): + GTTS = self.gtts.gTTS(text = message, lang = language) + + TempFile = self.tempfile.NamedTemporaryFile(delete = False) + GTTS.write_to_fp(TempFile) + TempFile.close() + + AudioClip = self.Torchlight().AudioManager.AudioClip(player, "file://" + TempFile.name) + if not AudioClip: + os.unlink(TempFile.name) + return 1 + + if AudioClip.Play(): + AudioClip.AudioPlayer.AddCallback("Stop", lambda: os.unlink(TempFile.name)) + return 0 + else: + os.unlink(TempFile.name) + return 1 + + async def _func(self, message, player): + self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message)) + + if self.check_disabled(player): + return -1 + + if not message[1]: + return 1 + + Language = "en" + if len(message[0]) > 4: + Language = message[0][4:] + + if not Language in self.VALID_LANGUAGES: + return 1 + + asyncio.ensure_future(self.Say(player, Language, message[1])) + return 0 + +''' +class DECTalk(BaseCommand): + import tempfile + def __init__(self, torchlight): + super().__init__(torchlight) + self.Triggers = ["!dec"] + self.Level = 0 + + async def Say(self, player, message): + message = "[:phoneme on]" + message + TempFile = self.tempfile.NamedTemporaryFile(delete = False) + TempFile.close() + + Proc = await asyncio.create_subprocess_exec("wine", "say.exe", "-w", TempFile.name, + cwd = "dectalk", stdin = asyncio.subprocess.PIPE) + await Proc.communicate(message.encode('utf-8', errors='ignore')) + + AudioClip = self.Torchlight().AudioManager.AudioClip(player, "file://" + TempFile.name) + if not AudioClip: + os.unlink(TempFile.name) + return 1 + + if AudioClip.Play(None, "-af", "volume=10dB"): + AudioClip.AudioPlayer.AddCallback("Stop", lambda: os.unlink(TempFile.name)) + return 0 + else: + os.unlink(TempFile.name) + return 1 + + async def _func(self, message, player): + self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message)) + + if self.check_disabled(player): + return -1 + + if not message[1]: + return 1 + + asyncio.ensure_future(self.Say(player, message[1])) + return 0 +''' + +class Stop(BaseCommand): + def __init__(self, torchlight): + super().__init__(torchlight) + self.Triggers = ["!pls"] + self.Level = 0 + + async def _func(self, message, player): + self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message)) + + self.Torchlight().AudioManager.Stop(player, message[1]) + return True + + +class EnableDisable(BaseCommand): + def __init__(self, torchlight): + super().__init__(torchlight) + self.Triggers = ["!enable", "!disable"] + self.Level = 3 + + async def _func(self, message, player): + self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message)) + + if message[0] == "!enable": + if self.Torchlight().Disabled: + if self.Torchlight().Disabled > player.Access["level"]: + self.Torchlight().SayPrivate(player, "You don't have access to enable torchlight, since it was disabled by a higher level user.") + return 1 + self.Torchlight().SayChat("Torchlight has been enabled for the duration of this map - Type !disable to disable it again.") + + self.Torchlight().Disabled = False + + elif message[0] == "!disable": + if self.Torchlight().Disabled > player.Access["level"]: + self.Torchlight().SayPrivate(player, "You don't have access to disable torchlight, since it was already disabled by a higher level user.") + return 1 + self.Torchlight().SayChat("Torchlight has been disabled for the duration of this map - Type !enable to enable it again.") + self.Torchlight().Disabled = player.Access["level"] + + +class AdminAccess(BaseCommand): + from collections import OrderedDict + def __init__(self, torchlight): + super().__init__(torchlight) + self.Triggers = ["!access"] + self.Level = 4 + + def ReloadValidUsers(self): + self.Torchlight().Access.Load() + for Player in self.Torchlight().Players: + Access = self.Torchlight().Access[Player.UniqueID] + Player.Access = Access + + async def _func(self, message, player): + self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message)) + if not message[1]: + return -1 + + if message[1].lower() == "reload": + self.ReloadValidUsers() + self.Torchlight().SayChat("Loaded access list with {0} users".format(len(self.Torchlight().Access))) + + elif message[1].lower() == "save": + self.Torchlight().Access.Save() + self.Torchlight().SayChat("Saved access list with {0} users".format(len(self.Torchlight().Access))) + + # Modify access + else: + Player = None + Buf = message[1] + Temp = Buf.find(" as ") + if Temp != -1: + try: + Regname, Level = Buf[Temp + 4:].rsplit(' ', 1) + except ValueError as e: + self.Torchlight().SayChat(str(e)) + return 1 + + Regname = Regname.strip() + Level = Level.strip() + Buf = Buf[:Temp].strip() + else: + try: + Buf, Level = Buf.rsplit(' ', 1) + except ValueError as e: + self.Torchlight().SayChat(str(e)) + return 2 + + Buf = Buf.strip() + Level = Level.strip() + + # Find user by User ID + if Buf[0] == '#' and Buf[1:].isnumeric(): + Player = self.Torchlight().Players.FindUserID(int(Buf[1:])) + # Search user by name + else: + for Player_ in self.Torchlight().Players: + if Player_.Name.lower().find(Buf.lower()) != -1: + Player = Player_ + break + + if not Player: + self.Torchlight().SayChat("Couldn't find user: {0}".format(Buf)) + return 3 + + if Level.isnumeric() or (Level.startswith('-') and Level[1:].isdigit()): + Level = int(Level) + + if Level >= player.Access["level"] and player.Access["level"] < 10: + self.Torchlight().SayChat("Trying to assign level {0}, which is higher or equal than your level ({1})".format(Level, player.Access["level"])) + return 4 + + if Player.Access: + if Player.Access["level"] >= player.Access["level"] and player.Access["level"] < 10: + self.Torchlight().SayChat("Trying to modify level {0}, which is higher or equal than your level ({1})".format(Player.Access["level"], player.Access["level"])) + return 5 + + if "Regname" in locals(): + self.Torchlight().SayChat("Changed \"{0}\"({1}) as {2} level/name from {3} to {4} as {5}".format( + Player.Name, Player.UniqueID, Player.Access["name"], Player.Access["level"], Level, Regname)) + Player.Access["name"] = Regname + else: + self.Torchlight().SayChat("Changed \"{0}\"({1}) as {2} level from {3} to {4}".format( + Player.Name, Player.UniqueID, Player.Access["name"], Player.Access["level"], Level)) + + Player.Access["level"] = Level + self.Torchlight().Access[Player.UniqueID] = Player.Access + else: + if not "Regname" in locals(): + Regname = Player.Name + + self.Torchlight().Access[Player.UniqueID] = self.OrderedDict([("name", Regname), ("level", Level)]) + Player.Access = self.Torchlight().Access[Player.UniqueID] + self.Torchlight().SayChat("Added \"{0}\"({1}) to access list as {2} with level {3}".format(Player.Name, Player.UniqueID, Regname, Level)) + else: + if Level == "revoke" and Player.Access: + if Player.Access["level"] >= player.Access["level"] and player.Access["level"] < 10: + self.Torchlight().SayChat("Trying to revoke level {0}, which is higher or equal than your level ({1})".format(Player.Access["level"], player.Access["level"])) + return 6 + + self.Torchlight().SayChat("Removed \"{0}\"({1}) from access list (was {2} with level {3})".format( + Player.Name, Player.UniqueID, Player.Access["name"], Player.Access["level"])) + del self.Torchlight().Access[Player.UniqueID] + Player.Access = None + return 0 + +class Reload(BaseCommand): + def __init__(self, torchlight): + super().__init__(torchlight) + self.Triggers = ["!reload"] + self.Level = 4 + + async def _func(self, message, player): + self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message)) + self.Torchlight().Reload() + return 0 + +""" +class Exec(BaseCommand): + def __init__(self, torchlight): + super().__init__(torchlight) + self.Triggers = ["!exec"] + self.Level = 100 + + async def _func(self, message, player): + self.Logger.debug(sys._getframe().f_code.co_name + ' ' + str(message)) + try: + Response = eval(message[1]) + except Exception as e: + self.Torchlight().SayChat("Error: {0}".format(str(e))) + return 1 + self.Torchlight().SayChat(str(Response)) + return 0 +""" diff --git a/torchlight_changes_unloze/torchlight3/Torchlight/Config.py b/torchlight_changes_unloze/torchlight3/Torchlight/Config.py new file mode 100755 index 00000000..9686bb38 --- /dev/null +++ b/torchlight_changes_unloze/torchlight3/Torchlight/Config.py @@ -0,0 +1,29 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +import logging +import json +import sys + +class Config(): + def __init__(self): + self.Logger = logging.getLogger(__class__.__name__) + self.Config = dict() + if len(sys.argv) >= 2: + self.ConfigPath = sys.argv[1] + else: + self.ConfigPath = "config.json" + self.Load() + + def Load(self): + try: + with open(self.ConfigPath, "r") as fp: + self.Config = json.load(fp) + except ValueError as e: + self.Logger.error(sys._getframe().f_code.co_name + ' ' + str(e)) + return 1 + return 0 + + def __getitem__(self, key): + if key in self.Config: + return self.Config[key] + return None diff --git a/torchlight_changes_unloze/torchlight3/Torchlight/Constants.py b/torchlight_changes_unloze/torchlight3/Torchlight/Constants.py new file mode 100755 index 00000000..95477aa9 --- /dev/null +++ b/torchlight_changes_unloze/torchlight3/Torchlight/Constants.py @@ -0,0 +1,26 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- + +MAXPLAYERS = 65 + +ADMFLAG_RESERVATION = (1<<0) +ADMFLAG_GENERIC = (1<<1) +ADMFLAG_KICK = (1<<2) +ADMFLAG_BAN = (1<<3) +ADMFLAG_UNBAN = (1<<4) +ADMFLAG_SLAY = (1<<5) +ADMFLAG_CHANGEMAP = (1<<6) +ADMFLAG_CONVARS = (1<<7) +ADMFLAG_CONFIG = (1<<8) +ADMFLAG_CHAT = (1<<9) +ADMFLAG_VOTE = (1<<10) +ADMFLAG_PASSWORD = (1<<11) +ADMFLAG_RCON = (1<<12) +ADMFLAG_CHEATS = (1<<13) +ADMFLAG_ROOT = (1<<14) +ADMFLAG_CUSTOM1 = (1<<15) +ADMFLAG_CUSTOM2 = (1<<16) +ADMFLAG_CUSTOM3 = (1<<17) +ADMFLAG_CUSTOM4 = (1<<18) +ADMFLAG_CUSTOM5 = (1<<19) +ADMFLAG_CUSTOM6 = (1<<20) diff --git a/torchlight_changes_unloze/torchlight3/Torchlight/FFmpegAudioPlayer.py b/torchlight_changes_unloze/torchlight3/Torchlight/FFmpegAudioPlayer.py new file mode 100755 index 00000000..8599f170 --- /dev/null +++ b/torchlight_changes_unloze/torchlight3/Torchlight/FFmpegAudioPlayer.py @@ -0,0 +1,180 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +import logging +import traceback +import asyncio +import datetime +import time +import socket +import struct +import sys + +SAMPLEBYTES = 2 + +class FFmpegAudioPlayerFactory(): + VALID_CALLBACKS = ["Play", "Stop", "Update"] + + def __init__(self, master): + self.Logger = logging.getLogger(__class__.__name__) + self.Master = master + self.Torchlight = self.Master.Torchlight + + def __del__(self): + self.Master.Logger.info("~FFmpegAudioPlayerFactory()") + self.Quit() + + def NewPlayer(self): + self.Logger.debug(sys._getframe().f_code.co_name) + Player = FFmpegAudioPlayer(self) + return Player + + def Quit(self): + self.Master.Logger.info("FFmpegAudioPlayerFactory->Quit()") + + +class FFmpegAudioPlayer(): + def __init__(self, master): + self.Master = master + self.Torchlight = self.Master.Torchlight + self.Playing = False + + self.Host = ( + self.Torchlight().Config["VoiceServer"]["Host"], + self.Torchlight().Config["VoiceServer"]["Port"] + ) + self.SampleRate = float(self.Torchlight().Config["VoiceServer"]["SampleRate"]) + + self.StartedPlaying = None + self.StoppedPlaying = None + self.Seconds = 0.0 + + self.Writer = None + self.Process = None + + self.Callbacks = [] + + def __del__(self): + self.Master.Logger.debug("~FFmpegAudioPlayer()") + self.Stop() + + def PlayURI(self, uri, position, *args): + if position: + PosStr = str(datetime.timedelta(seconds = position)) + Command = ["/usr/bin/ffmpeg", "-ss", PosStr, "-i", uri, "-acodec", "pcm_s16le", "-ac", "1", "-ar", str(int(self.SampleRate)), "-f", "s16le", "-vn", *args, "-"] + else: + Command = ["/usr/bin/ffmpeg", "-i", uri, "-acodec", "pcm_s16le", "-ac", "1", "-ar", str(int(self.SampleRate)), "-f", "s16le", "-vn", *args, "-"] + + print(Command) + + self.Playing = True + asyncio.ensure_future(self._stream_subprocess(Command)) + return True + + def Stop(self, force = True): + if not self.Playing: + return False + + if self.Process: + try: + self.Process.terminate() + self.Process.kill() + self.Process = None + except ProcessLookupError: + pass + + if self.Writer: + if force: + Socket = self.Writer.transport.get_extra_info("socket") + if Socket: + Socket.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, + struct.pack("ii", 1, 0)) + + self.Writer.transport.abort() + + self.Writer.close() + + self.Playing = False + + self.Callback("Stop") + del self.Callbacks + + return True + + def AddCallback(self, cbtype, cbfunc): + if not cbtype in FFmpegAudioPlayerFactory.VALID_CALLBACKS: + return False + + self.Callbacks.append((cbtype, cbfunc)) + return True + + def Callback(self, cbtype, *args, **kwargs): + for Callback in self.Callbacks: + if Callback[0] == cbtype: + try: + Callback[1](*args, **kwargs) + except Exception as e: + self.Master.Logger.error(traceback.format_exc()) + + async def _updater(self): + LastSecondsElapsed = 0.0 + + while self.Playing: + SecondsElapsed = time.time() - self.StartedPlaying + + if SecondsElapsed > self.Seconds: + SecondsElapsed = self.Seconds + + self.Callback("Update", LastSecondsElapsed, SecondsElapsed) + + if SecondsElapsed >= self.Seconds: + if not self.StoppedPlaying: + print("BUFFER UNDERRUN!") + self.Stop(False) + return + + LastSecondsElapsed = SecondsElapsed + + await asyncio.sleep(0.1) + + async def _read_stream(self, stream, writer): + Started = False + + while stream and self.Playing: + Data = await stream.read(65536) + + if Data: + writer.write(Data) + await writer.drain() + + Bytes = len(Data) + Samples = Bytes / SAMPLEBYTES + Seconds = Samples / self.SampleRate + + self.Seconds += Seconds + + if not Started: + Started = True + self.Callback("Play") + self.StartedPlaying = time.time() + asyncio.ensure_future(self._updater()) + else: + self.Process = None + break + + self.StoppedPlaying = time.time() + + async def _stream_subprocess(self, cmd): + if not self.Playing: + return + + _, self.Writer = await asyncio.open_connection(self.Host[0], self.Host[1]) + + Process = await asyncio.create_subprocess_exec(*cmd, + stdout = asyncio.subprocess.PIPE, stderr = asyncio.subprocess.DEVNULL) + self.Process = Process + + await self._read_stream(Process.stdout, self.Writer) + await Process.wait() + + if self.Seconds == 0.0: + self.Stop() diff --git a/torchlight_changes_unloze/torchlight3/Torchlight/GameEvents.py b/torchlight_changes_unloze/torchlight3/Torchlight/GameEvents.py new file mode 100755 index 00000000..d7a8828e --- /dev/null +++ b/torchlight_changes_unloze/torchlight3/Torchlight/GameEvents.py @@ -0,0 +1,149 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +import asyncio +import logging +import traceback + +class GameEvents(): + def __init__(self, master): + self.Logger = logging.getLogger(__class__.__name__) + self.Torchlight = master + + self.Callbacks = {} + + def __del__(self): + if not len(self.Callbacks) or not self.Torchlight(): + return + + Obj = { + "method": "unsubscribe", + "module": "gameevents", + "events": self.Callbacks.keys() + } + + asyncio.ensure_future(self.Torchlight().Send(Obj)) + + async def _Register(self, events): + if type(events) is not list: + events = [ events ] + + Obj = { + "method": "subscribe", + "module": "gameevents", + "events": events + } + + Res = await self.Torchlight().Send(Obj) + + Ret = [] + for i, ret in enumerate(Res["events"]): + if ret >= 0: + Ret.append(True) + if not events[i] in self.Callbacks: + self.Callbacks[events[i]] = set() + else: + Ret.append(False) + + if len(Ret) == 1: + Ret = Ret[0] + return Ret + + async def _Unregister(self, events): + if type(events) is not list: + events = [ events ] + + Obj = { + "method": "unsubscribe", + "module": "gameevents", + "events": events + } + + Res = await self.Torchlight().Send(Obj) + + Ret = [] + for i, ret in enumerate(Res["events"]): + if ret >= 0: + Ret.append(True) + if events[i] in self.Callbacks: + del self.Callbacks[events[i]] + else: + Ret.append(False) + + if len(Ret) == 1: + Ret = Ret[0] + return Ret + + def HookEx(self, event, callback): + asyncio.ensure_future(self.Hook(event, callback)) + + def UnhookEx(self, event, callback): + asyncio.ensure_future(self.Unhook(event, callback)) + + def ReplayEx(self, events): + asyncio.ensure_future(self.Replay(events)) + + async def Hook(self, event, callback): + if not event in self.Callbacks: + if not await self._Register(event): + return False + + self.Callbacks[event].add(callback) + return True + + async def Unhook(self, event, callback): + if not event in self.Callbacks: + return True + + if not callback in self.Callbacks[event]: + return True + + self.Callbacks[event].discard(callback) + + if len(a) == 0: + return await self._Unregister(event) + + return True + + async def Replay(self, events): + if type(events) is not list: + events = [ events ] + + for event in events[:]: + if not event in self.Callbacks: + events.remove(event) + + Obj = { + "method": "replay", + "module": "gameevents", + "events": events + } + + Res = await self.Torchlight().Send(Obj) + + Ret = [] + for i, ret in enumerate(Res["events"]): + if ret >= 0: + Ret.append(True) + else: + Ret.append(False) + + if len(Ret) == 1: + Ret = Ret[0] + return Ret + + def OnPublish(self, obj): + Event = obj["event"] + + if not Event["name"] in self.Callbacks: + return False + + Callbacks = self.Callbacks[Event["name"]] + + for Callback in Callbacks: + try: + Callback(**Event["data"]) + except Exception as e: + self.Logger.error(traceback.format_exc()) + self.Logger.error(Event) + + return True diff --git a/torchlight_changes_unloze/torchlight3/Torchlight/PlayerManager.py b/torchlight_changes_unloze/torchlight3/Torchlight/PlayerManager.py new file mode 100755 index 00000000..37357c5b --- /dev/null +++ b/torchlight_changes_unloze/torchlight3/Torchlight/PlayerManager.py @@ -0,0 +1,201 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +import asyncio +import logging +import numpy +from .Constants import * + +class PlayerManager(): + def __init__(self, master): + self.Logger = logging.getLogger(__class__.__name__) + self.Torchlight = master + + self.Players = numpy.empty(MAXPLAYERS + 1, dtype = object) + self.Storage = self.StorageManager(self) + + self.Torchlight().GameEvents.HookEx("player_connect", self.Event_PlayerConnect) + self.Torchlight().GameEvents.HookEx("player_activate", self.Event_PlayerActivate) + self.Torchlight().Forwards.HookEx("OnClientPostAdminCheck", self.OnClientPostAdminCheck) + self.Torchlight().GameEvents.HookEx("player_info", self.Event_PlayerInfo) + self.Torchlight().GameEvents.HookEx("player_disconnect", self.Event_PlayerDisconnect) + self.Torchlight().GameEvents.HookEx("server_spawn", self.Event_ServerSpawn) + + def Event_PlayerConnect(self, name, index, userid, networkid, address, bot): + index += 1 + self.Logger.info("OnConnect(name={0}, index={1}, userid={2}, networkid={3}, address={4}, bot={5})" + .format(name, index, userid, networkid, address, bot)) + if self.Players[index] != None: + self.Logger.error("!!! Player already exists, overwriting !!!") + + self.Players[index] = self.Player(self, index, userid, networkid, address, name) + self.Players[index].OnConnect() + + def Event_PlayerActivate(self, userid): + self.Logger.info("Pre_OnActivate(userid={0})".format(userid)) + index = self.FindUserID(userid).Index + self.Logger.info("OnActivate(index={0}, userid={1})".format(index, userid)) + + self.Players[index].OnActivate() + + def OnClientPostAdminCheck(self, client): + self.Logger.info("OnClientPostAdminCheck(client={0})".format(client)) + + asyncio.ensure_future(self.Players[client].OnClientPostAdminCheck()) + + def Event_PlayerInfo(self, name, index, userid, networkid, bot): + index += 1 + self.Logger.info("OnInfo(name={0}, index={1}, userid={2}, networkid={3}, bot={4})" + .format(name, index, userid, networkid, bot)) + + # We've connected to the server and receive info events about the already connected players + # Emulate connect message + if not self.Players[index]: + self.Event_PlayerConnect(name, index - 1, userid, networkid, bot) + else: + self.Players[index].OnInfo(name) + + def Event_PlayerDisconnect(self, userid, reason, name, networkid, bot): + index = self.FindUserID(userid).Index + self.Logger.info("OnDisconnect(index={0}, userid={1}, reason={2}, name={3}, networkid={4}, bot={5})" + .format(index, userid, reason, name, networkid, bot)) + + self.Players[index].OnDisconnect(reason) + self.Players[index] = None + + def Event_ServerSpawn(self, hostname, address, ip, port, game, mapname, maxplayers, os, dedicated, password): + self.Logger.info("ServerSpawn(mapname={0})" + .format(mapname)) + + self.Storage.Reset() + + for i in range(1, self.Players.size): + if self.Players[i]: + self.Players[i].OnDisconnect("mapchange") + self.Players[i].OnConnect() + + def FindUniqueID(self, uniqueid): + for Player in self.Players: + if Player and Player.UniqueID == uniqueid: + return Player + + def FindUserID(self, userid): + for Player in self.Players: + if Player and Player.UserID == userid: + return Player + + def FindName(self, name): + for Player in self.Players: + if Player and Player.Name == name: + return Player + + def __len__(self): + Count = 0 + for i in range(1, self.Players.size): + if self.Players[i]: + Count += 1 + return Count + + def __setitem__(self, key, value): + if key > 0 and key <= MAXPLAYERS: + self.Players[key] = value + + def __getitem__(self, key): + if key > 0 and key <= MAXPLAYERS: + return self.Players[key] + + def __iter__(self): + for i in range(1, self.Players.size): + if self.Players[i]: + yield self.Players[i] + + class StorageManager(): + def __init__(self, master): + self.PlayerManager = master + self.Storage = dict() + + def Reset(self): + self.Storage = dict() + + def __getitem__(self, key): + if not key in self.Storage: + self.Storage[key] = dict() + + return self.Storage[key] + + class Admin(): + def __init__(self): + self._FlagBits = 0 + + def FlagBits(self): + return self._FlagBits + + def Reservation(self): return (self._FlagBits & ADMFLAG_RESERVATION) + def Generic(self): return (self._FlagBits & ADMFLAG_GENERIC) + def Kick(self): return (self._FlagBits & ADMFLAG_KICK) + def Ban(self): return (self._FlagBits & ADMFLAG_BAN) + def Unban(self): return (self._FlagBits & ADMFLAG_UNBAN) + def Slay(self): return (self._FlagBits & ADMFLAG_SLAY) + def Changemap(self): return (self._FlagBits & ADMFLAG_CHANGEMAP) + def Convars(self): return (self._FlagBits & ADMFLAG_CONVARS) + def Config(self): return (self._FlagBits & ADMFLAG_CONFIG) + def Chat(self): return (self._FlagBits & ADMFLAG_CHAT) + def Vote(self): return (self._FlagBits & ADMFLAG_VOTE) + def Password(self): return (self._FlagBits & ADMFLAG_PASSWORD) + def RCON(self): return (self._FlagBits & ADMFLAG_RCON) + def Cheats(self): return (self._FlagBits & ADMFLAG_CHEATS) + def Root(self): return (self._FlagBits & ADMFLAG_ROOT) + def Custom1(self): return (self._FlagBits & ADMFLAG_CUSTOM1) + def Custom2(self): return (self._FlagBits & ADMFLAG_CUSTOM2) + def Custom3(self): return (self._FlagBits & ADMFLAG_CUSTOM3) + def Custom4(self): return (self._FlagBits & ADMFLAG_CUSTOM4) + def Custom5(self): return (self._FlagBits & ADMFLAG_CUSTOM5) + def Custom6(self): return (self._FlagBits & ADMFLAG_CUSTOM6) + + class Player(): + def __init__(self, master, index, userid, uniqueid, address, name): + self.PlayerManager = master + self.Torchlight = self.PlayerManager.Torchlight + self.Index = index + self.UserID = userid + self.UniqueID = uniqueid + self.Address = address + self.Name = name + self.Access = None + self.Admin = self.PlayerManager.Admin() + self.Storage = None + self.Active = False + self.ChatCooldown = 0 + + def OnConnect(self): + self.Storage = self.PlayerManager.Storage[self.UniqueID] + + if not "Audio" in self.Storage: + self.Storage["Audio"] = dict({"Uses": 0, "LastUse": 0.0, "LastUseLength": 0.0, "TimeUsed": 0.0}) + + self.Access = self.Torchlight().Access[self.UniqueID] + + def OnActivate(self): + self.Active = True + + async def OnClientPostAdminCheck(self): + self.Admin._FlagBits = (await self.Torchlight().API.GetUserFlagBits(self.Index))["result"] + self.PlayerManager.Logger.info("#{0} \"{1}\"({2}) FlagBits: {3}".format(self.UserID, self.Name, self.UniqueID, self.Admin._FlagBits)) + if not self.Access: + if self.Admin.RCON(): + self.Access = dict({"level": 6, "name": "SAdmin"}) + elif self.Admin.Generic(): + self.Access = dict({"level": 3, "name": "Admin"}) + elif self.Admin.Custom1(): + self.Access = dict({"level": 1, "name": "VIP"}) + + if self.PlayerManager.Torchlight().Config["DefaultLevel"]: + if self.Access and self.Access["level"] < self.PlayerManager.Torchlight().Config["DefaultLevel"]: + self.Access = dict({"level": self.PlayerManager.Torchlight().Config["DefaultLevel"], "name": "Default"}) + + def OnInfo(self, name): + self.Name = name + + def OnDisconnect(self, message): + self.Active = False + self.Storage = None + self.Torchlight().AudioManager.OnDisconnect(self) diff --git a/torchlight_changes_unloze/torchlight3/Torchlight/SourceModAPI.py b/torchlight_changes_unloze/torchlight3/Torchlight/SourceModAPI.py new file mode 100755 index 00000000..64f83792 --- /dev/null +++ b/torchlight_changes_unloze/torchlight3/Torchlight/SourceModAPI.py @@ -0,0 +1,27 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +import functools + +class SourceModAPI: + def __init__(self, master): + self.Torchlight = master + + def __getattr__(self, attr): + try: + return super(SourceModAPI, self).__getattr__(attr) + except AttributeError: + return functools.partial(self._MakeCall, attr) + + async def _MakeCall(self, function, *args, **kwargs): + Obj = { + "method": "function", + "function": function, + "args": args + } + + Res = await self.Torchlight().Send(Obj) + + if Res["error"]: + raise Exception("{0}({1})\n{2}".format(function, args, Res["error"])) + + return Res diff --git a/torchlight_changes_unloze/torchlight3/Torchlight/SourceRCONServer.py b/torchlight_changes_unloze/torchlight3/Torchlight/SourceRCONServer.py new file mode 100755 index 00000000..882c7687 --- /dev/null +++ b/torchlight_changes_unloze/torchlight3/Torchlight/SourceRCONServer.py @@ -0,0 +1,106 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +import logging +import asyncio +import sys +import socket +import struct +import time +import traceback +from importlib import reload +from .PlayerManager import PlayerManager + +class SourceRCONServer(): + class SourceRCONClient(): + def __init__(self, Server, Socket, Name): + self.Loop = Server.Loop + self.Server = Server + self._sock = Socket + self.Name = Name + self.Authenticated = False + asyncio.Task(self._peer_handler()) + + def send(self, data): + return self.Loop.sock_sendall(self._sock, data) + + @asyncio.coroutine + def _peer_handler(self): + try: + yield from self._peer_loop() + except IOError: + pass + finally: + self.Server.Remove(self) + + @asyncio.coroutine + def _peer_loop(self): + while True: + Data = yield from self.Loop.sock_recv(self._sock, 1024) + if Data == b'': + break + + while Data: + p_size = struct.unpack("= 0: + Ret.append(True) + if not events[i] in self.Callbacks: + self.Callbacks[events[i]] = set() + else: + Ret.append(False) + + if len(Ret) == 1: + Ret = Ret[0] + return Ret + + async def _Unregister(self, events): + if type(events) is not list: + events = [ events ] + + Obj = { + "method": "unsubscribe", + "module": self.Module, + "events": events + } + + Res = await self.Torchlight().Send(Obj) + + Ret = [] + for i, ret in enumerate(Res["events"]): + if ret >= 0: + Ret.append(True) + if events[i] in self.Callbacks: + del self.Callbacks[events[i]] + else: + Ret.append(False) + + if len(Ret) == 1: + Ret = Ret[0] + return Ret + + def HookEx(self, event, callback): + asyncio.ensure_future(self.Hook(event, callback)) + + def UnhookEx(self, event, callback): + asyncio.ensure_future(self.Unhook(event, callback)) + + def ReplayEx(self, events): + asyncio.ensure_future(self.Replay(events)) + + async def Hook(self, event, callback): + if not event in self.Callbacks: + if not await self._Register(event): + return False + + self.Callbacks[event].add(callback) + return True + + async def Unhook(self, event, callback): + if not event in self.Callbacks: + return True + + if not callback in self.Callbacks[event]: + return True + + self.Callbacks[event].discard(callback) + + if len(a) == 0: + return await self._Unregister(event) + + return True + + async def Replay(self, events): + if type(events) is not list: + events = [ events ] + + for event in events[:]: + if not event in self.Callbacks: + events.remove(event) + + Obj = { + "method": "replay", + "module": self.Module, + "events": events + } + + Res = await self.Torchlight().Send(Obj) + + Ret = [] + for i, ret in enumerate(Res["events"]): + if ret >= 0: + Ret.append(True) + else: + Ret.append(False) + + if len(Ret) == 1: + Ret = Ret[0] + return Ret + + def OnPublish(self, obj): + Event = obj["event"] + + if not Event["name"] in self.Callbacks: + return False + + Callbacks = self.Callbacks[Event["name"]] + + for Callback in Callbacks: + try: + Callback(**Event["data"]) + except Exception as e: + self.Logger.error(traceback.format_exc()) + self.Logger.error(Event) + + return True + + +class GameEvents(SubscribeBase): + def __init__(self, master): + super().__init__(master, "gameevents") + +class Forwards(SubscribeBase): + def __init__(self, master): + super().__init__(master, "forwards") diff --git a/torchlight_changes_unloze/torchlight3/Torchlight/Torchlight.py b/torchlight_changes_unloze/torchlight3/Torchlight/Torchlight.py new file mode 100755 index 00000000..0096204c --- /dev/null +++ b/torchlight_changes_unloze/torchlight3/Torchlight/Torchlight.py @@ -0,0 +1,149 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +import logging +import asyncio +import sys +import json +import time +import weakref +import traceback +import textwrap + +from .AsyncClient import AsyncClient + +from .SourceModAPI import SourceModAPI +from .Subscribe import GameEvents, Forwards + +from .Utils import Utils +from .Config import Config +from .CommandHandler import CommandHandler +from .AccessManager import AccessManager +from .PlayerManager import PlayerManager +from .AudioManager import AudioManager + +class Torchlight(): + def __init__(self, master): + self.Logger = logging.getLogger(__class__.__name__) + self.Master = master + self.Config = self.Master.Config + self.WeakSelf = weakref.ref(self) + + self.API = SourceModAPI(self.WeakSelf) + self.GameEvents = GameEvents(self.WeakSelf) + self.Forwards = Forwards(self.WeakSelf) + + self.DisableVotes = set() + self.Disabled = 0 + self.LastUrl = None + + def InitModules(self): + self.Access = AccessManager() + self.Access.Load() + + self.Players = PlayerManager(self.WeakSelf) + + self.AudioManager = AudioManager(self.WeakSelf) + + self.CommandHandler = CommandHandler(self.WeakSelf) + self.CommandHandler.Setup() + + self.GameEvents.HookEx("server_spawn", self.Event_ServerSpawn) + self.GameEvents.HookEx("player_say", self.Event_PlayerSay) + + def SayChat(self, message, player=None): + message = "\x0700FFFA[Torchlight]: \x01{0}".format(message) + if len(message) > 976: + message = message[:973] + "..." + lines = textwrap.wrap(message, 244, break_long_words = True) + for line in lines: + asyncio.ensure_future(self.API.PrintToChatAll(line)) + + if player: + Level = 0 + if player.Access: + Level = player.Access["level"] + + if Level < self.Config["AntiSpam"]["ImmunityLevel"]: + cooldown = len(lines) * self.Config["AntiSpam"]["ChatCooldown"] + if player.ChatCooldown > self.Master.Loop.time(): + player.ChatCooldown += cooldown + else: + player.ChatCooldown = self.Master.Loop.time() + cooldown + + def SayPrivate(self, player, message): + message = "\x0700FFFA[Torchlight]: \x01{0}".format(message) + if len(message) > 976: + message = message[:973] + "..." + lines = textwrap.wrap(message, 244, break_long_words = True) + for line in lines: + asyncio.ensure_future(self.API.PrintToChat(player.Index, line)) + + def Reload(self): + self.Config.Load() + self.CommandHandler.NeedsReload = True + + async def Send(self, data): + return await self.Master.Send(data) + + def OnPublish(self, obj): + if obj["module"] == "gameevents": + self.GameEvents.OnPublish(obj) + elif obj["module"] == "forwards": + self.Forwards.OnPublish(obj) + + def Event_ServerSpawn(self, hostname, address, ip, port, game, mapname, maxplayers, os, dedicated, password): + self.DisableVotes = set() + self.Disabled = 0 + + def Event_PlayerSay(self, userid, text): + if userid == 0: + return + + Player = self.Players.FindUserID(userid) + asyncio.ensure_future(self.CommandHandler.HandleCommand(text, Player)) + + def __del__(self): + self.Logger.debug("~Torchlight()") + + +class TorchlightHandler(): + def __init__(self, loop): + self.Logger = logging.getLogger(__class__.__name__) + self.Loop = loop if loop else asyncio.get_event_loop() + self._Client = None + self.Torchlight = None + self.Config = Config() + + asyncio.ensure_future(self._Connect(), loop = self.Loop) + + async def _Connect(self): + # Connect to API + self._Client = AsyncClient(self.Loop, self.Config["SMAPIServer"]["Host"], self.Config["SMAPIServer"]["Port"], self) + await self._Client.Connect() + + self.Torchlight = Torchlight(self) + + # Pre Hook for late load + await self.Torchlight.GameEvents._Register(["player_connect", "player_activate"]) + await self.Torchlight.Forwards._Register(["OnClientPostAdminCheck"]) + + self.Torchlight.InitModules() + + # Late load + await self.Torchlight.GameEvents.Replay(["player_connect", "player_activate"]) + await self.Torchlight.Forwards.Replay(["OnClientPostAdminCheck"]) + + async def Send(self, data): + return await self._Client.Send(data) + + def OnPublish(self, obj): + self.Torchlight.OnPublish(obj) + + def OnDisconnect(self, exc): + self.Logger.info("OnDisconnect({0})".format(exc)) + self.Torchlight = None + + asyncio.ensure_future(self._Connect(), loop = self.Loop) + + def __del__(self): + self.Logger.debug("~TorchlightHandler()") diff --git a/torchlight_changes_unloze/torchlight3/Torchlight/Torchlightmg.py b/torchlight_changes_unloze/torchlight3/Torchlight/Torchlightmg.py new file mode 100755 index 00000000..f1c21372 --- /dev/null +++ b/torchlight_changes_unloze/torchlight3/Torchlight/Torchlightmg.py @@ -0,0 +1,150 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +import logging +import asyncio +import sys +import json +import time +import weakref +import traceback +import textwrap + +from .AsyncClient import AsyncClient + +from .SourceModAPI import SourceModAPI +from .Subscribe import GameEvents, Forwards + +from .Utils import Utils +from .Config import Config +from .CommandHandlermg import CommandHandlermg +from .AccessManager import AccessManager +from .PlayerManager import PlayerManager +from .AudioManager import AudioManager + +class Torchlight(): + def __init__(self, master): + self.Logger = logging.getLogger(__class__.__name__) + self.Master = master + self.Config = self.Master.Config + self.WeakSelf = weakref.ref(self) + + self.API = SourceModAPI(self.WeakSelf) + self.GameEvents = GameEvents(self.WeakSelf) + self.Forwards = Forwards(self.WeakSelf) + + self.DisableVotes = set() + self.Disabled = 0 + self.LastUrl = None + + def InitModules(self): + self.Access = AccessManager() + self.Access.Load() + + self.Players = PlayerManager(self.WeakSelf) + + self.AudioManager = AudioManager(self.WeakSelf) + + self.CommandHandler = CommandHandlermg(self.WeakSelf) + self.CommandHandler.Setup() + + self.GameEvents.HookEx("server_spawn", self.Event_ServerSpawn) + self.GameEvents.HookEx("player_say", self.Event_PlayerSay) + + def SayChat(self, message, player=None): + message = "\x0700FFFA[Torchlight]: \x01{0}".format(message) + if len(message) > 976: + message = message[:973] + "..." + lines = textwrap.wrap(message, 244, break_long_words = True) + for line in lines: + asyncio.ensure_future(self.API.PrintToChatAll(line)) + + if player: + Level = 0 + if player.Access: + Level = player.Access["level"] + + if Level < self.Config["AntiSpam"]["ImmunityLevel"]: + cooldown = len(lines) * self.Config["AntiSpam"]["ChatCooldown"] + if player.ChatCooldown > self.Master.Loop.time(): + player.ChatCooldown += cooldown + else: + player.ChatCooldown = self.Master.Loop.time() + cooldown + + def SayPrivate(self, player, message): + message = "\x0700FFFA[Torchlight]: \x01{0}".format(message) + if len(message) > 976: + message = message[:973] + "..." + lines = textwrap.wrap(message, 244, break_long_words = True) + for line in lines: + asyncio.ensure_future(self.API.PrintToChat(player.Index, line)) + + def Reload(self): + self.Config.Load() + self.CommandHandler.NeedsReload = True + + async def Send(self, data): + return await self.Master.Send(data) + + def OnPublish(self, obj): + if obj["module"] == "gameevents": + self.GameEvents.OnPublish(obj) + elif obj["module"] == "forwards": + self.Forwards.OnPublish(obj) + + def Event_ServerSpawn(self, hostname, address, ip, port, game, mapname, maxplayers, os, dedicated, password): + self.DisableVotes = set() + self.Disabled = 0 + + def Event_PlayerSay(self, userid, text): + if userid == 0: + return + + Player = self.Players.FindUserID(userid) + asyncio.ensure_future(self.CommandHandler.HandleCommand(text, Player)) + + def __del__(self): + self.Logger.debug("~Torchlight()") + + +class TorchlightHandler(): + def __init__(self, loop): + self.Logger = logging.getLogger(__class__.__name__) + self.Loop = loop if loop else asyncio.get_event_loop() + self._Client = None + self.Torchlight = None + self.Config = Config() + + asyncio.ensure_future(self._Connect(), loop = self.Loop) + + async def _Connect(self): + # Connect to API + self._Client = AsyncClient(self.Loop, self.Config["SMAPIServer"]["Host"], self.Config["SMAPIServer"]["Port"], self) + await self._Client.Connect() + + self.Torchlight = Torchlight(self) + + # Pre Hook for late load + await self.Torchlight.GameEvents._Register(["player_connect", "player_activate"]) + await self.Torchlight.Forwards._Register(["OnClientPostAdminCheck"]) + + self.Torchlight.InitModules() + + # Late load + await self.Torchlight.GameEvents.Replay(["player_connect", "player_activate"]) + await self.Torchlight.Forwards.Replay(["OnClientPostAdminCheck"]) + + async def Send(self, data): + return await self._Client.Send(data) + + def OnPublish(self, obj): + self.Torchlight.OnPublish(obj) + + def OnDisconnect(self, exc): + self.Logger.info("OnDisconnect({0})".format(exc)) + self.Torchlight = None + + asyncio.ensure_future(self._Connect(), loop = self.Loop) + + def __del__(self): + self.Logger.debug("~TorchlightHandler()") + diff --git a/torchlight_changes_unloze/torchlight3/Torchlight/Utils.py b/torchlight_changes_unloze/torchlight3/Torchlight/Utils.py new file mode 100755 index 00000000..f335768e --- /dev/null +++ b/torchlight_changes_unloze/torchlight3/Torchlight/Utils.py @@ -0,0 +1,94 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +import math + +class DataHolder: + def __init__(self, value=None, attr_name='value'): + self._attr_name = attr_name + self.set(value) + def __call__(self, value): + return self.set(value) + def set(self, value): + setattr(self, self._attr_name, value) + return value + def get(self): + return getattr(self, self._attr_name) + +class Utils(): + @staticmethod + def GetNum(Text): + Ret = '' + for c in Text: + if c.isdigit(): + Ret += c + elif Ret: + break + elif c == '-': + Ret += c + + return Ret + + @staticmethod + def ParseTime(TimeStr): + Negative = False + Time = 0 + + while TimeStr: + Val = Utils.GetNum(TimeStr) + if not Val: + break + + Val = int(Val) + if not Val: + break + + if Val < 0: + TimeStr = TimeStr[1:] + if Time == 0: + Negative = True + Val = abs(Val) + + ValLen = int(math.log10(Val)) + 1 + if len(TimeStr) > ValLen: + Mult = TimeStr[ValLen].lower() + TimeStr = TimeStr[ValLen + 1:] + if Mult == 'h': + Val *= 3600 + elif Mult == 'm': + Val *= 60 + else: + TimeStr = None + + Time += Val + + if Negative: + return -Time + else: + return Time + + + @staticmethod + def HumanSize(size_bytes): + """ + format a size in bytes into a 'human' file size, e.g. bytes, KB, MB, GB, TB, PB + Note that bytes/KB will be reported in whole numbers but MB and above will have greater precision + e.g. 1 byte, 43 bytes, 443 KB, 4.3 MB, 4.43 GB, etc + """ + if size_bytes == 1: + # because I really hate unnecessary plurals + return "1 byte" + + suffixes_table = [('bytes', 0),('KB', 0),('MB', 1),('GB', 2),('TB', 2), ('PB', 2)] + + num = float(size_bytes) + for suffix, precision in suffixes_table: + if num < 1024.0: + break + num /= 1024.0 + + if precision == 0: + formatted_size = str(int(num)) + else: + formatted_size = str(round(num, ndigits=precision)) + + return "{0}{1}".format(formatted_size, suffix) diff --git a/torchlight_changes_unloze/torchlight3/Torchlight/__init__.py b/torchlight_changes_unloze/torchlight3/Torchlight/__init__.py new file mode 100755 index 00000000..f9664561 --- /dev/null +++ b/torchlight_changes_unloze/torchlight3/Torchlight/__init__.py @@ -0,0 +1,2 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- diff --git a/torchlight_changes_unloze/torchlight3/Torchlight/__pycache__/AccessManager.cpython-37.pyc b/torchlight_changes_unloze/torchlight3/Torchlight/__pycache__/AccessManager.cpython-37.pyc new file mode 100755 index 00000000..ebc20f26 Binary files /dev/null and b/torchlight_changes_unloze/torchlight3/Torchlight/__pycache__/AccessManager.cpython-37.pyc differ diff --git a/torchlight_changes_unloze/torchlight3/Torchlight/__pycache__/AsyncClient.cpython-37.pyc b/torchlight_changes_unloze/torchlight3/Torchlight/__pycache__/AsyncClient.cpython-37.pyc new file mode 100755 index 00000000..221f74bd Binary files /dev/null and b/torchlight_changes_unloze/torchlight3/Torchlight/__pycache__/AsyncClient.cpython-37.pyc differ diff --git a/torchlight_changes_unloze/torchlight3/Torchlight/__pycache__/AudioManager.cpython-37.pyc b/torchlight_changes_unloze/torchlight3/Torchlight/__pycache__/AudioManager.cpython-37.pyc new file mode 100755 index 00000000..1be52bd1 Binary files /dev/null and b/torchlight_changes_unloze/torchlight3/Torchlight/__pycache__/AudioManager.cpython-37.pyc differ diff --git a/torchlight_changes_unloze/torchlight3/Torchlight/__pycache__/CommandHandler.cpython-37.pyc b/torchlight_changes_unloze/torchlight3/Torchlight/__pycache__/CommandHandler.cpython-37.pyc new file mode 100755 index 00000000..a90e10e7 Binary files /dev/null and b/torchlight_changes_unloze/torchlight3/Torchlight/__pycache__/CommandHandler.cpython-37.pyc differ diff --git a/torchlight_changes_unloze/torchlight3/Torchlight/__pycache__/CommandHandlermg.cpython-37.pyc b/torchlight_changes_unloze/torchlight3/Torchlight/__pycache__/CommandHandlermg.cpython-37.pyc new file mode 100644 index 00000000..5895f339 Binary files /dev/null and b/torchlight_changes_unloze/torchlight3/Torchlight/__pycache__/CommandHandlermg.cpython-37.pyc differ diff --git a/torchlight_changes_unloze/torchlight3/Torchlight/__pycache__/Commands.cpython-37.pyc b/torchlight_changes_unloze/torchlight3/Torchlight/__pycache__/Commands.cpython-37.pyc new file mode 100644 index 00000000..8b8bd5ae Binary files /dev/null and b/torchlight_changes_unloze/torchlight3/Torchlight/__pycache__/Commands.cpython-37.pyc differ diff --git a/torchlight_changes_unloze/torchlight3/Torchlight/__pycache__/Commandsmg.cpython-37.pyc b/torchlight_changes_unloze/torchlight3/Torchlight/__pycache__/Commandsmg.cpython-37.pyc new file mode 100644 index 00000000..be29893f Binary files /dev/null and b/torchlight_changes_unloze/torchlight3/Torchlight/__pycache__/Commandsmg.cpython-37.pyc differ diff --git a/torchlight_changes_unloze/torchlight3/Torchlight/__pycache__/Config.cpython-37.pyc b/torchlight_changes_unloze/torchlight3/Torchlight/__pycache__/Config.cpython-37.pyc new file mode 100755 index 00000000..b0394d68 Binary files /dev/null and b/torchlight_changes_unloze/torchlight3/Torchlight/__pycache__/Config.cpython-37.pyc differ diff --git a/torchlight_changes_unloze/torchlight3/Torchlight/__pycache__/Constants.cpython-37.pyc b/torchlight_changes_unloze/torchlight3/Torchlight/__pycache__/Constants.cpython-37.pyc new file mode 100755 index 00000000..0bb0da7e Binary files /dev/null and b/torchlight_changes_unloze/torchlight3/Torchlight/__pycache__/Constants.cpython-37.pyc differ diff --git a/torchlight_changes_unloze/torchlight3/Torchlight/__pycache__/FFmpegAudioPlayer.cpython-37.pyc b/torchlight_changes_unloze/torchlight3/Torchlight/__pycache__/FFmpegAudioPlayer.cpython-37.pyc new file mode 100755 index 00000000..deebb4c4 Binary files /dev/null and b/torchlight_changes_unloze/torchlight3/Torchlight/__pycache__/FFmpegAudioPlayer.cpython-37.pyc differ diff --git a/torchlight_changes_unloze/torchlight3/Torchlight/__pycache__/PlayerManager.cpython-37.pyc b/torchlight_changes_unloze/torchlight3/Torchlight/__pycache__/PlayerManager.cpython-37.pyc new file mode 100755 index 00000000..6eeffd0d Binary files /dev/null and b/torchlight_changes_unloze/torchlight3/Torchlight/__pycache__/PlayerManager.cpython-37.pyc differ diff --git a/torchlight_changes_unloze/torchlight3/Torchlight/__pycache__/SourceModAPI.cpython-37.pyc b/torchlight_changes_unloze/torchlight3/Torchlight/__pycache__/SourceModAPI.cpython-37.pyc new file mode 100755 index 00000000..7f15ffe8 Binary files /dev/null and b/torchlight_changes_unloze/torchlight3/Torchlight/__pycache__/SourceModAPI.cpython-37.pyc differ diff --git a/torchlight_changes_unloze/torchlight3/Torchlight/__pycache__/SourceRCONServer.cpython-37.pyc b/torchlight_changes_unloze/torchlight3/Torchlight/__pycache__/SourceRCONServer.cpython-37.pyc new file mode 100755 index 00000000..3294fce3 Binary files /dev/null and b/torchlight_changes_unloze/torchlight3/Torchlight/__pycache__/SourceRCONServer.cpython-37.pyc differ diff --git a/torchlight_changes_unloze/torchlight3/Torchlight/__pycache__/Subscribe.cpython-37.pyc b/torchlight_changes_unloze/torchlight3/Torchlight/__pycache__/Subscribe.cpython-37.pyc new file mode 100755 index 00000000..032d56bf Binary files /dev/null and b/torchlight_changes_unloze/torchlight3/Torchlight/__pycache__/Subscribe.cpython-37.pyc differ diff --git a/torchlight_changes_unloze/torchlight3/Torchlight/__pycache__/Torchlight.cpython-37.pyc b/torchlight_changes_unloze/torchlight3/Torchlight/__pycache__/Torchlight.cpython-37.pyc new file mode 100755 index 00000000..114cdf55 Binary files /dev/null and b/torchlight_changes_unloze/torchlight3/Torchlight/__pycache__/Torchlight.cpython-37.pyc differ diff --git a/torchlight_changes_unloze/torchlight3/Torchlight/__pycache__/Torchlightmg.cpython-37.pyc b/torchlight_changes_unloze/torchlight3/Torchlight/__pycache__/Torchlightmg.cpython-37.pyc new file mode 100644 index 00000000..f89f00c9 Binary files /dev/null and b/torchlight_changes_unloze/torchlight3/Torchlight/__pycache__/Torchlightmg.cpython-37.pyc differ diff --git a/torchlight_changes_unloze/torchlight3/Torchlight/__pycache__/Utils.cpython-37.pyc b/torchlight_changes_unloze/torchlight3/Torchlight/__pycache__/Utils.cpython-37.pyc new file mode 100755 index 00000000..d01d4834 Binary files /dev/null and b/torchlight_changes_unloze/torchlight3/Torchlight/__pycache__/Utils.cpython-37.pyc differ diff --git a/torchlight_changes_unloze/torchlight3/Torchlight/__pycache__/__init__.cpython-37.pyc b/torchlight_changes_unloze/torchlight3/Torchlight/__pycache__/__init__.cpython-37.pyc new file mode 100755 index 00000000..5f225dfb Binary files /dev/null and b/torchlight_changes_unloze/torchlight3/Torchlight/__pycache__/__init__.cpython-37.pyc differ diff --git a/torchlight_changes_unloze/torchlight3/_start.sh b/torchlight_changes_unloze/torchlight3/_start.sh new file mode 100755 index 00000000..9403c544 --- /dev/null +++ b/torchlight_changes_unloze/torchlight3/_start.sh @@ -0,0 +1,4 @@ +#!/bin/bash +cd "$(dirname "$0")" +source venv/bin/activate +python3 main.py diff --git a/torchlight_changes_unloze/torchlight3/_start_mg.sh b/torchlight_changes_unloze/torchlight3/_start_mg.sh new file mode 100755 index 00000000..76a3fdd3 --- /dev/null +++ b/torchlight_changes_unloze/torchlight3/_start_mg.sh @@ -0,0 +1,4 @@ +#!/bin/bash +cd "$(dirname "$0")" +source venv/bin/activate +python3 mainmg.py config-mg.json diff --git a/torchlight_changes_unloze/torchlight3/access.json b/torchlight_changes_unloze/torchlight3/access.json new file mode 100755 index 00000000..66379d74 --- /dev/null +++ b/torchlight_changes_unloze/torchlight3/access.json @@ -0,0 +1,22 @@ +{ + "[U:1:69566635]": { + "level": 100, + "name": "Jenz" + }, + "[U:1:64494019]": { + "level": 100, + "name": "Neon" + }, + "[U:1:126555404]": { + "level": 3, + "name": "Migza" + }, + "[U:1:20383465]": { + "name": "WASD", + "level": 3 + }, + "[U:1:28627906]": { + "name": "Berry", + "level": 1337 + } +} diff --git a/torchlight_changes_unloze/torchlight3/main.py b/torchlight_changes_unloze/torchlight3/main.py new file mode 100755 index 00000000..b8f6daf4 --- /dev/null +++ b/torchlight_changes_unloze/torchlight3/main.py @@ -0,0 +1,37 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +import logging +import asyncio +import os +import sys +import threading +import traceback +import gc +from importlib import reload + +global TorchMaster + +import Torchlight.Torchlight +from Torchlight.SourceRCONServer import SourceRCONServer + +if __name__ == '__main__': + logging.basicConfig( + level = logging.DEBUG, + format = "[%(asctime)s] %(levelname)s [%(name)s.%(funcName)s:%(lineno)d] %(message)s", + datefmt = "%H:%M:%S" + ) + + Loop = asyncio.get_event_loop() + + global TorchMaster + TorchMaster = Torchlight.Torchlight.TorchlightHandler(Loop) + + # Handles new connections on 0.0.0.0:27015 + RCONConfig = TorchMaster.Config["TorchRCON"] + """RCONServer = SourceRCONServer(Loop, TorchMaster, + Host = RCONConfig["Host"], + Port = RCONConfig["Port"], + Password = RCONConfig["Password"])""" + + # Run! + Loop.run_forever() diff --git a/torchlight_changes_unloze/torchlight3/mainmg.py b/torchlight_changes_unloze/torchlight3/mainmg.py new file mode 100755 index 00000000..27a8b3f0 --- /dev/null +++ b/torchlight_changes_unloze/torchlight3/mainmg.py @@ -0,0 +1,37 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +import logging +import asyncio +import os +import sys +import threading +import traceback +import gc +from importlib import reload + +global TorchMaster + +import Torchlight.Torchlightmg +from Torchlight.SourceRCONServer import SourceRCONServer + +if __name__ == '__main__': + logging.basicConfig( + level = logging.DEBUG, + format = "[%(asctime)s] %(levelname)s [%(name)s.%(funcName)s:%(lineno)d] %(message)s", + datefmt = "%H:%M:%S" + ) + + Loop = asyncio.get_event_loop() + + global TorchMaster + TorchMaster = Torchlight.Torchlightmg.TorchlightHandler(Loop) + + # Handles new connections on 0.0.0.0:27015 + RCONConfig = TorchMaster.Config["TorchRCON"] + """RCONServer = SourceRCONServer(Loop, TorchMaster, + Host = RCONConfig["Host"], + Port = RCONConfig["Port"], + Password = RCONConfig["Password"])""" + + # Run! + Loop.run_forever() diff --git a/torchlight_changes_unloze/torchlight3/requirements.txt b/torchlight_changes_unloze/torchlight3/requirements.txt new file mode 100755 index 00000000..f69a03d7 --- /dev/null +++ b/torchlight_changes_unloze/torchlight3/requirements.txt @@ -0,0 +1,23 @@ +aiohttp +appdirs +async-timeout +beautifulsoup4 +certifi +chardet +gTTS +gTTS-token +idna +lxml +multidict +numpy +olefile +packaging +Pillow +pyparsing +python-magic +requests +six +urllib3 +yarl +cython +geoip2 diff --git a/torchlight_changes_unloze/torchlight3/run.sh b/torchlight_changes_unloze/torchlight3/run.sh new file mode 100755 index 00000000..7553eb37 --- /dev/null +++ b/torchlight_changes_unloze/torchlight3/run.sh @@ -0,0 +1,5 @@ +#!/bin/bash +screen kill torchlight +screen kill torchlight-noct +screen -L -A -m -d -S torchlight ./_start.sh +screen -L -A -m -d -S torchlight-mg ./_start_mg.sh diff --git a/torchlight_changes_unloze/torchlight3/triggers.json b/torchlight_changes_unloze/torchlight3/triggers.json new file mode 100755 index 00000000..a2986e12 --- /dev/null +++ b/torchlight_changes_unloze/torchlight3/triggers.json @@ -0,0 +1,405 @@ +[ + {"names": ["!pyramid"], "sound": "ascream7.mp3"}, + {"names": ["!applause"], "sound": "applause.wav"}, + {"names": ["!seeyou", "!seeyouagain"], "sound": "sephiseeyou.mp3"}, + {"names": ["!chosen"], "sound": "sephonlythechosen.mp3"}, + {"names": ["!laser"], "sound": "bladeout.wav"}, + {"names": ["!prepare", "!battle"], "sound": "prepareforbattle.mp3"}, + {"names": ["!tuturu"], "sound": "tuturu.mp3"}, + {"names": ["!baka"], "sound": "baka.wav"}, + {"names": ["!omae"], "sound": "Omae.mp3"}, + {"names": ["!poi"], "sound": "poi.wav"}, + {"names": ["!shit"], "sound": "shit.wav"}, + {"names": ["!sugoi"], "sound": "sugoi.wav"}, + {"names": ["!allahu", "!akbar"], "sound": "allahu_akbar1.mp3"}, + {"names": ["!allahu2", "!akbar2"], "sound": "AllahuAkbarDubstep.mp3"}, + {"names": ["!quack"], "sound": "quack.mp3"}, + {"names": ["!english"], "sound": "english.wav"}, + {"names": ["!squad", "!suicidesquad"], "sound": "suicidesquad.mp3"}, + {"names": ["!cena"], "sound": "john.mp3"}, + {"names": ["!solo"], "sound": "solo.mp3"}, + {"names": ["!popopo"], "sound": "popopoguys1.mp3"}, + {"names": ["!tageule"], "sound": "tagueule.wav"}, + {"names": ["!idiot"], "sound": "omguidiot.mp3"}, + {"names": ["!game"], "sound": "isonly1.mp3"}, + {"names": ["!wah"], "sound": "wah wah sound effect.mp3"}, + {"names": ["!yes"], "sound": ["ohyes.mp3", "yes.mp3", "m-bison-yes-yes.mp3", "yesyesyesjojo.mp3"]}, + {"names": ["!overconfidence"], "sound": "Monster_Kill.wav"}, + {"names": ["!beer", "!pussy"], "sound": "grababeer.mp3"}, + {"names": ["!nein"], "sound": "Nein nein nein2.mp3"}, + {"names": ["!ayy", "!lmao"], "sound": "AyyyLmao_louer.mp3"}, + {"names": ["!blyat"], "sound": "BLYAT_louder.mp3"}, + {"names": ["!monkey", "!language"], "sound": "NomnkyLanguage_louder.mp3"}, + {"names": ["!wanker"], "sound": "OIYAWANKER_louder.mp3"}, + {"names": ["!vortex"], "sound": "WAAAAhByVortexx_louder.mp3"}, + {"names": ["!slutty"], "sound": "sluttyrainbow.wav"}, + {"names": ["!hello"], "sound": "Hello2.mp3"}, + {"names": ["!george"], "sound": "George.mp3"}, + {"names": ["!george2"], "sound": "George2.mp3"}, + {"names": ["!erebus"], "sound": "erebus.wav"}, + {"names": ["!kabzor", "!neon"], "sound": "kabzorrage.wav"}, + {"names": ["!otominas"], "sound": "Otominas.mp3"}, + {"names": ["!airhorn"], "sound": "AIRHORN.mp3"}, + {"names": ["!reee", "!reeee"], "sound": "reeeeeeee_v2.mp3"}, + {"names": ["!wombo"], "sound": "wombo combo2.mp3"}, + {"names": ["!vaperwave"], "sound": "Vaperwave.mp3"}, + {"names": ["!roasted"], "sound": "roasted2.mp3"}, + {"names": ["!ogre"], "sound": "its all ogre now.mp3"}, + {"names": ["!failed"], "sound": "Mission Failed.mp3"}, + {"names": ["!youmad"], "sound": "meshlem_arrive2_good.mp3"}, + {"names": ["!daniel"], "sound": "daniel.mp3"}, + {"names": ["!action"], "sound": "Action is Coming.mp3"}, + {"names": ["!brucelee"], "sound": "Ugandan Bruce Lee.mp3"}, + {"names": ["!gwegwe"], "sound": "Gwe Gwe Gwe.mp3"}, + {"names": ["!bruceu"], "sound": "We Call Him Bruce U.mp3"}, + {"names": ["!commando"], "sound": "COMMAND COMMANDO PagChomp.wav"}, + {"names": ["!nuffin"], "sound": "DINDU NUFFIN.wav"}, + {"names": ["!dinosaurs"], "sound": "DINOSAURS ZULUL.wav"}, + {"names": ["!deadlyactions"], "sound": "NON STOP DEADLY ACTIONS PagChomp.wav"}, + {"names": ["!gimme"], "sound": "GIMME GIMME GIMME GIMME.wav"}, + {"names": ["!skicker"], "sound": "SUPA KICKER.wav"}, + {"names": ["!sbiker"], "sound": "SUPA BIKER.wav"}, + {"names": ["!sfighter"], "sound": "SUPA FIGHTER.wav"}, + {"names": ["!smafia"], "sound": "SUPA MAFIA.wav"}, + {"names": ["!tigercage"], "sound": "THE TIGER IS IN THE CAGE.wav"}, + {"names": ["!serious"], "sound": "THIS IS SERIOUS.wav"}, + {"names": ["!hehehe"], "sound": "HE HE HE.wav"}, + {"names": ["!tigermafia"], "sound": "TIGER MAFIA.wav"}, + {"names": ["!cheeki"], "sound": "AH NU CHEEKI BREEKI IV DAMKE.mp3"}, + {"names": ["_legend"], "sound": "legend.mp3"}, + {"names": ["!admun_pls"], "sound": "Admun Please Electro (Short).mp3"}, + {"names": ["!dolphin"], "sound": "Dolfin - Song.mp3"}, + {"names": ["!omg_remix"], "sound": "O my Gah (Remix #3).mp3"}, + {"names": ["!rainbow_panda"], "sound": "ITS RAINBOW PANDA.mp3"}, + {"names": ["!dong"], "sound": "Ding Dong.mp3"}, + {"names": ["!ching_club"], "sound": "Ching Chong (Remix #9) Club.mp3"}, + {"names": ["!tonguetwister"], "sound": "tongue-twister.mp3"}, + {"names": ["!cooler"], "sound": "cooler.mp3"}, + {"names": ["!fatality"], "sound": "fatality.mp3"}, + {"names": ["!inception"], "sound": "inceptionbutton.mp3"}, + {"names": ["!leroy"], "sound": "leroy.mp3"}, + {"names": ["!ecchi"], "sound": "ecchidarling.mp3"}, + {"names": ["!moe"], "sound": "moe.mp3"}, + {"names": ["!nants"], "sound": "nants_ingonyama_bagithi_baba_-_the_circle_of_life-mp3cut.mp3"}, + {"names": ["!nyanpasu"], "sound": "nyanpass_2.mp3"}, + {"names": ["!oww"], "sound": "oww.mp3"}, + {"names": ["!wololo"], "sound": "wololo.mp3"}, + {"names": ["!yuuta"], "sound": "yuuuta_itaiiii.mp3"}, + {"names": ["!zawarudo"], "sound": "za-warudo-stop-time-sound.mp3"}, + {"names": ["!slow"], "sound": "seph_slow.wav"}, + {"names": ["!tuturu"], "sound": "Tutturuu_v1.wav"}, + {"names": ["!baka"], "sound": ["baka.wav", "Baka.mp3"]}, + {"names": ["!buhi"], "sound": "buhi.wav"}, + {"names": ["!nyan"], "sound": "nyanpass.wav"}, + {"names": ["!trial"], "sound": "trial.wav"}, + {"names": ["!mail"], "sound": "yuu_new_mail.wav"}, + {"names": ["!balrog"], "sound": "balrog_scream.wav"}, + {"names": ["!bluescreen", "!error"], "sound": "error.wav"}, + {"names": ["!hallelujah"], "sound": "hallelujah.wav"}, + {"names": ["!trap"], "sound": "itsatrap.wav"}, + {"names": ["!whores"], "sound": "whores.wav"}, + {"names": ["!gandalf"], "sound": "vozyoucannotpass.wav"}, + {"names": ["!cat"], "sound": "gato.wav"}, + {"names": ["!turtle", "!turtles"], "sound": ["iliketurtles.wav", "wherearetheturtles.mp3"]}, + {"names": ["!laser"], "sound": "blade_out.wav"}, + {"names": ["!king", "!kingoftheworld"], "sound": "vulcan.wav"}, + {"names": ["!seeyou", "!seeyouagain"], "sound": "seph_iseeyou.wav"}, + {"names": ["!chosen"], "sound": "seph_onlythechosen.wav"}, + {"names": ["!goodbye", "!saygoodbye"], "sound": "seph_saygoodbye.wav"}, + {"names": ["!slow"], "sound": "seph_slow.wav"}, + {"names": ["!late", "!toolate"], "sound": "seph_toolate.wav"}, + {"names": ["!growl"], "sound": "bahamut_growl2.wav"}, + {"names": ["!buyspins", "!buy_spins"], "sound": "BUYSPINS1.wav"}, + {"names": ["!death", "!deathscream"], "sound": "d_death_scream.wav"}, + {"names": ["!attack", "!meetyourend"], "sound": "z_godend.wav"}, + {"names": ["!die", "!guardian"], "sound": "z_guardianend.wav"}, + {"names": ["!applause"], "sound": "applause.wav"}, + {"names": ["!retreat"], "sound": "stage_x_gandalf_retreat-1-0.wav"}, + {"names": ["!idiot"], "sound": "idiot.wav"}, + {"names": ["!duck", "!quack"], "sound": "quack.wav"}, + {"names": ["!triple"], "sound": "triple.wav"}, + {"names": ["!solo"], "sound": "solo.wav"}, + {"names": ["!oniichan", "!onii-chan"], "sound": "Onii-chan.wav"}, + {"names": ["!gameover"], "sound": "Gameover.wav"}, + {"names": ["!wombo"], "sound": "WomboCombo.wav"}, + {"names": ["!camera"], "sound": "MOM GET THE CAMERA.wav"}, + {"names": ["!hihi"], "sound": "skullz.wav"}, + {"names": ["!yatta"], "sound": "Hiro Yatta.wav"}, + {"names": ["!over"], "sound": "over.wav"}, + {"names": ["!cena"], "sound": "cena.wav"}, + {"names": ["!victory"], "sound": "ffvii_victory.wav"}, + {"names": ["!2012"], "sound": ["2012_1.wav", "2012_2.wav", "2012_3.wav", "2012_4.wav"]}, + {"names": ["!nein"], "sound": "hitler_nein.wav"}, + {"names": ["!allahu", "!akbar"], "sound": "allahu_akbar.wav"}, + {"names": ["!chopper"], "sound": "gettothechopper.wav"}, + {"names": ["!payback"], "sound": "blain - payback time.wav"}, + {"names": ["!scotland"], "sound": "scotland.wav"}, + {"names": ["!happening"], "sound": "happening.wav"}, + {"names": ["!topkek"], "sound": "topkek.wav"}, + {"names": ["!welcome", "!ricefields"], "sound": "welcome_to_the_ricefields.wav"}, + {"names": ["!inception"], "sound": "inception.wav"}, + {"names": ["!x"], "sound": "xfiles.wav"}, + {"names": ["!stfu"], "sound": ["stfu.wav", "stfu2.mp3"]}, + {"names": ["!goat"], "sound": ["goat1.wav", "goat2.wav", "goat3.wav", "goat4.wav", "goat5.wav", "goat6.wav"]}, + {"names": ["!doit"], "sound": "doit.wav"}, + {"names": ["!benis"], "sound": "grossenbenis.wav"}, + {"names": ["!omg"], "sound": "omg.wav"}, + {"names": ["!wow"], "sound": ["wow.wav", "rexy_wow.wav"]}, + {"names": ["!prepare", "!battle"], "sound": "stage_2_preparaos-0-0.wav"}, + {"names": ["!meme"], "sound": "nicememe.wav"}, + {"names": ["!nyaa"], "sound": ["nyaa_1.wav", "nyaa_2.wav", "nyaa_3.wav", "nyaa_4.wav", "nyaa_5.wav", "nyaa_6.wav", "nyaa-3.mp3", "nyaa4.mp3"]}, + {"names": ["!ah"], "sound": "FFVII_Cry.wav"}, + {"names": ["!feelsbad"], "sound": "feelsbadman.wav"}, + {"names": ["!admun"], "sound": "admun_please.wav"}, + {"names": ["!population"], "sound": "population.wav"}, + {"names": ["!why", "!immigration"], "sound": "immigration.wav"}, + {"names": ["!tutury"], "sound": "tutury.wav"}, + {"names": ["!lew"], "sound": "Bewlewlewlew.wav"}, + {"names": ["!pudi"], "sound": "Pudi Pudi (Short).wav"}, + {"names": ["!hey", "!listen"], "sound": "Hey Listen.wav"}, + {"names": ["!comeon"], "sound": "Come on.mp3"}, + {"names": ["!stopp"], "sound": "itstimetostop.mp3"}, + {"names": ["!cancer"], "sound": "cancer.mp3"}, + {"names": ["!heil"], "sound": "heil.mp3"}, + {"names": ["!nico"], "sound": "niconiconi.mp3"}, + {"names": ["!cock"], "sound": ["cock1.mp3", "cock2.mp3", "cock3.mp3", "cock4.mp3", "cock5.mp3", "cock6.mp3"]}, + {"names": ["!rock"], "sound": "LetsRock.mp3"}, + {"names": ["!fmlaugh"], "sound": "fmlaugh.mp3"}, + {"names": ["!monopoly"], "sound": "monopoly.mp3"}, + {"names": ["!kiddin", "!kidding"], "sound": "rexy r u kiddin me.mp3"}, + {"names": ["!noice"], "sound": "Noice.mp3"}, + {"names": ["!rexy"], "sound": "rexywaah.mp3"}, + {"names": ["!kaboom"], "sound": "kaboom.mp3"}, + {"names": ["!honk"], "sound": "honk.wav"}, + {"names": ["!spam"], "sound": "No_Spammerino_In_The_Chatterino.mp3"}, + {"names": ["!ohoho"], "sound": ["ohoho_1.mp3", "ohoho_3.mp3", "ohoho_4.mp3", "ohoho_5.mp3", "ohoho_6.mp3", "ohoho_7.mp3", "ohoho_8.mp3", "ohoho_9.mp3", "ohoho_10.mp3", "ohoho_11.mp3", "ohoho_12.mp3", "ohoho_13.mp3", "ohoho_14.mp3", "ohoho_15.mp3", "ohoho_16.mp3", "ohoho_17.mp3", "ohoho_18.mp3", "ohoho_19.mp3", "ohoho_20.mp3", "ohoho_21.mp3", "ohoho_22.mp3", "ohoho_23.mp3", "ohoho_24.mp3", "ohoho_25.mp3", "ohoho_26.mp3", "ohoho_27.mp3", "ohoho_28.mp3", "ohoho_29.mp3", "ohoho_30.mp3", "ohoho_31.mp3", "ohoho_32.mp3", "ohoho_33.mp3", "ohoho_34.mp3", "ohoho_35.mp3", "ohoho_36.mp3", "ohoho_37.mp3", "ohoho_38.mp3", "ohoho_39.mp3", "ohoho_40.mp3", "ohoho_41.mp3", "ohoho_42.mp3", "ohoho_43.mp3", "ohoho_44.mp3", "ohoho_45.mp3", "ohoho_46.mp3", "ohoho_47.mp3", "ohoho_48.mp3", "ohoho_49.mp3", "ohoho_50.mp3", "ohoho_51.mp3", "ohoho_52.mp3", "ohoho_53.mp3", "ohoho_54.mp3", "ohoho_55.mp3", "ohoho_56.mp3", "ohoho_57.mp3", "ohoho_58.mp3", "ohoho_59.mp3", "ohoho_60.mp3", "ohoho_61.mp3", "ohoho_62.mp3", "ohoho_63.mp3", "ohoho_64.mp3", "ohoho_65.mp3", "ohoho_66.mp3", "ohoho_67.mp3", "ohoho_68.mp3", "ohoho_69.mp3", "ohoho_70.mp3", "ohoho_71.mp3", "ohoho_72.mp3", "ohoho_73.mp3", "ohoho_74.mp3", "ohoho_75.mp3", "ohoho_76.mp3", "ohoho_77.mp3", "ohoho_78.mp3", "ohoho_79.mp3", "ohoho_80.mp3", "ohoho_81.mp3", "ohoho_82.mp3", "ohoho_83.mp3", "ohoho_84.mp3", "ohoho_85.mp3", "ohoho_86.mp3", "ohoho_87.mp3", "ohoho_88.mp3", "ohoho_89.mp3"]}, + {"names": ["!sugoi"], "sound": "sugoi_sugoi.mp3"}, + {"names": ["!cry"], "sound": ["cry1.wav", "cry2.wav", "cry3.wav", "cry4.wav", "cry5.wav", "cry6.wav", "cry7.wav"]}, + {"names": ["!hehe", "!giggle"], "sound": "Giggle.mp3"}, + {"names": ["!monkey"], "sound": ["chimp1.wav", "chimp2.wav"]}, + {"names": ["!ka", "!kaka"], "sound": ["Nisemonogatari-Shinobu-Kaka.ogg", "Nisemonogatari-Shinobu-K-ka.ogg"]}, + {"names": ["!jodel"], "sound": "jodel.mp3"}, + {"names": ["!nyaaa"], "sound": "nyaaa.mp3"}, + {"names": ["!run"], "sound": "run.wav"}, + {"names": ["!goodbye"], "sound": "goodbye.wav"}, + {"names": ["!noo"], "sound": "LOTR_Noooooo.wav"}, + {"names": ["!dayum"], "sound": "daaamn.mp3"}, + {"names": ["!goddammit", "!goddamnit"], "sound": "goddammit.mp3"}, + {"names": ["!surprise"], "sound": "surprisemotherfucker1.mp3"}, + {"names": ["!csi"], "sound": "yeeaah.mp3"}, + {"names": ["!nope"], "sound": "engineer_no01.mp3"}, + {"names": ["!joke"], "sound": "rimshot.mp3"}, + {"names": ["!weed"], "sound": "smokeweederryday.mp3"}, + {"names": ["!toasty"], "sound": "toasty.mp3"}, + {"names": ["!damn"], "sound": "wheredyoufindthis.mp3"}, + {"names": ["!nuts"], "sound": "suckmynuts.mp3"}, + {"names": ["!wake"], "sound": "wakemeup.mp3"}, + {"names": ["!bye"], "sound": "bye.mp3"}, + {"names": ["!ilikeit"], "sound": "ilikeit.mp3"}, + {"names": ["!milk"], "sound": "milk.mp3"}, + {"names": ["!pussy"], "sound": "pussy.mp3"}, + {"names": ["!retard"], "sound": ["retard.mp3", "retard2.mp3"]}, + {"names": ["!sorry"], "sound": "sry.mp3"}, + {"names": ["!wtf"], "sound": "wtf.mp3"}, + {"names": ["!brb"], "sound": "brb.mp3"}, + {"names": ["!cricket"], "sound": "cricket.mp3"}, + {"names": ["!hax"], "sound": "hax.mp3"}, + {"names": ["!hi"], "sound": "hi.mp3"}, + {"names": ["!moo"], "sound": "moo.mp3"}, + {"names": ["!rape"], "sound": "rape.mp3"}, + {"names": ["!tada"], "sound": "tada.mp3"}, + {"names": ["!yay"], "sound": "yay.mp3"}, + {"names": ["!cyka"], "sound": "cyka.mp3"}, + {"names": ["!racist"], "sound": "racist.mp3"}, + {"names": ["!roger"], "sound": "roger.mp3"}, + {"names": ["!tooslow"], "sound": "tooslow.mp3"}, + {"names": ["!steam"], "sound": "steam.wav"}, + {"names": ["!good"], "sound": "that_s_pretty_good.mp3"}, + {"names": ["!crawling"], "sound": "crawling.mp3"}, + {"names": ["!nukyun"], "sound": ["nukyun1.mp3", "nukyun2.mp3", "nukyun3.mp3", "nukyun4.mp3", "nukyun5.mp3", "nukyun6.mp3", "nukyun7.mp3", "nukyun8.mp3", "nukyun9.mp3", "nukyun10.mp3", "nukyun11.mp3", "nukyun12.mp3", "nukyun13.mp3", "nukyun14.mp3", "nukyun15.mp3", "nukyun16.mp3", "nukyun17.mp3", "nukyun18.mp3", "nukyun19.mp3", "nukyun20.mp3", "nukyun21.mp3", "nukyun22.mp3", "nukyun23.mp3", "nukyun24.mp3", "nukyun25.mp3", "nukyun26.mp3", "nukyun27.mp3"]}, + {"names": ["!harambe"], "sound": "harambe.mp3"}, + {"names": ["!horn"], "sound": "vu_horn_quick.wav"}, + {"names": ["!hood"], "sound": "hood.mp3"}, + {"names": ["!gtfo"], "sound": ["gtfo.mp3", "gtfo2.mp3"]}, + {"names": ["!pomf"], "sound": "pomf.mp3"}, + {"names": ["!gay"], "sound": "putingay.mp3"}, + {"names": ["!pedo"], "sound": "pedobear.mp3"}, + {"names": ["!kys"], "sound": "kys.mp3"}, + {"names": ["!english"], "sound": "englishonly.mp3"}, + {"names": ["!knowledge"], "sound": "Knowledge.m4a"}, + {"names": ["!mana"], "sound": "mana.mp3"}, + {"names": ["!dodge"], "sound": ["dodge1.mp3", "dodge2.mp3"]}, + {"names": ["!love"], "sound": "sheep.mp3"}, + {"names": ["!timotei"], "sound": "Timotei.wav"}, + {"names": ["!daniel"], "sound": "daniel.mp3"}, + {"names": ["!cne"], "sound": ["it_is_may.mp3", "cnelaugh.mp3"]}, + {"names": ["!avocados"], "sound": "avocados.mp3"}, + {"names": ["!tutu", "!papa"], "sound": "papatutuwawa.mp3"}, + {"names": ["!nani"], "sound": "nani.mp3"}, + {"names": ["!squee"], "sound": "squee.mp3"}, + {"names": ["!ptb"], "sound": "07ptb.wav"}, + {"names": ["!wall"], "sound": "wall.mp3"}, + {"names": ["!bomb"], "sound": "bomb.mp3"}, + {"names": ["!wrong"], "sound": "wrong.mp3"}, + {"names": ["!china"], "sound": ["china1.mp3", "china2.mp3", "china3.mp3", "china4.mp3", "china5.mp3", "china6.mp3", "china7.mp3"]}, + {"names": ["!oof"], "sound": "oof.mp3"}, + {"names": ["!pan"], "sound": "panpakapan.mp3"}, + {"names": ["!shoulder"], "sound": "oh_my_shoulder.mp3"}, + {"names": ["!kizuna"], "sound": ["kizuna_fucku.mp3", "kizuna_fucku2.mp3", "kizuna_omg.mp3"]}, + {"names": ["!zegawa"], "sound": ["moaning_leBuP3p.mp3","1caae40.mp3","ara_ara.mp3","bndarling.mp3","watashi_hotto_jaate.mp3"]}, + {"names": ["!screenshot"], "sound": "camera1.wav"}, + {"names": ["!badro"], "sound": "vip.mp3"}, + {"names": ["!omarlaser"], "sound": "omarlaser.mp3"}, + {"names": ["!omarlenny"], "sound": "omarlenny.mp3"}, + {"names": ["!silent"], "sound": ["silent_scream.mp3" , "silent_laught.mp3"]}, + {"names": ["!willy"], "sound": "rippr_lotr_plz.mp3"}, + {"names": ["!anone"], "sound": "anone.mp3"}, + {"names": ["!jayz"], "sound": "jayz.wav"}, + {"names": ["!succ"], "sound": "succ.mp3"}, + {"names": ["!uguu"], "sound": "uguu_1.mp3"}, + {"names": ["!doot"], "sound": "skullsound2.mp3"}, + {"names": ["!widow"], "sound": "widowmaker_-_americans-1.mp3"}, + {"names": ["!winston"], "sound": "i-wanna-be-winston.mp3"}, + {"names": ["!try"], "sound": "try.mp3"}, + {"names": ["!smug"], "sound": "smug.mp3"}, + {"names": ["!skullz_tw"], "sound": "tw_skullzrage.mp3"}, + {"names": ["!genesis"], "sound": "genesis.mp3"}, + {"names": ["!borealis"], "sound": ["borealis1.mp3", "borealis2.mp3"]}, + {"names": ["!clams"], "sound": ["clams1.mp3", "clams2.mp3", "clams3.mp3"]}, + {"names": ["!hams"], "sound": ["hams1.mp3", "hams2.mp3"]}, + {"names": ["!expression"], "sound": "expression.mp3"}, + {"names": ["!albany"], "sound": "expression.mp3"}, + {"names": ["!steamedyes"], "sound": "steamedyes.mp3"}, + {"names": ["!steamedno"], "sound": "steamedno.mp3"}, + {"names": ["!luncheon"], "sound": "luncheon.mp3"}, + {"names": ["!devilish"], "sound": "devilish.mp3"}, + {"names": ["!roast"], "sound": "roast.mp3"}, + {"names": ["!goodlord"], "sound": "lord_unloze.mp3"}, + {"names": ["!time"], "sound": "time.mp3"}, + {"names": ["!dialect"], "sound": "dialect.mp3"}, + {"names": ["!attention"], "sound": "attention.mp3"}, + {"names": ["!great"], "sound": "great.mp3"}, + {"names": ["!epicsolo"], "sound": "epicsolo.mp3"}, + {"names": ["!everyonertv"], "sound": "everyonertv.mp3"}, + {"names": ["!esk"], "sound": "esk.mp3"}, + {"names": ["!epc"], "sound": "epc.mp3"}, + {"names": ["!ges"], "sound": "ges.mp3"}, + {"names": ["!eze"], "sound": "eze.mp3"}, + {"names": ["!eod"], "sound": "eod.mp3"}, + {"names": ["!tce"], "sound": "tce.mp3"}, + {"names": ["!trouble"], "sound": "trouble.mp3"}, + {"names": ["!teamwin"], "sound": "teamwin.mp3"}, + {"names": ["!nide"], "sound": ["nide.mp3", "nide2.mp3"]}, + {"names": ["!heaven"], "sound": "heaven.mp3"}, + {"names": ["!unskilled"], "sound": "unskilled.mp3"}, + {"names": ["!russia"], "sound": "russia.mp3"}, + {"names": ["!gg"], "sound": "gg.mp3"}, + {"names": ["!notrigger"], "sound": "notrig.wav"}, + {"names": ["!enemy"], "sound": ["enemee1.wav", "enemee2.wav", "enemee3.wav", "enemee4.wav", "enemee5.wav"]}, + {"names": ["!glacius"], "sound": "Glacius.m4a"}, + {"names": ["!ayaya"], "sound": ["ayayakaren1.mp3", "ayayakaren2.mp3"]}, + {"names": ["!dogan"], "sound": "dogan_im_going_to_fucking_fuck_you.mp3"}, + {"names": ["!destroy"], "sound": "dickstodestroy.mp3"}, + {"names": ["!boi"], "sound": "ainsley_harriott_and_his_spicy_meatconverttoaudio.mp3"}, + {"names": ["!gunga", "!ginga"], "sound": "gunga_ginga.mp3"}, + {"names": ["!leaders"], "sound": "leaders.mp3"}, + {"names": ["!never"], "sound": "never.mp3"}, + {"names": ["!hl1", "!blackmesa"], "sound": ["hl1_whatyoudoing.wav", "hl1_otis_talkmuch.wav", "hl1_pain3.wav", "hl1_otis_virgin.wav", "hl1_pain4.wav", "hl1_otis_die.wav", "hl1_pain1.wav", "hl1_otis_mom.wav", "hl1_pain2.wav", "hl1_scream05.wav", "hl1_iwounded.wav", "hl1_fear12.wav", "hl1_fear14.wav", "hl1_fear15.wav", "hl1_fear6.wav", "hl1_fear7.wav", "hl1_fear8.wav", "hl1_fear11.wav", "hl1_scream07.wav", "hl1_scream01.wav"]}, + {"names": ["!lügen"], "sound": "lügen.mp3"}, + {"names": ["!lachen"], "sound": "lachen.mp3"}, + {"names": ["!incoming"], "sound": "phalanx_incoming.mp3"}, + {"names": ["!address"], "sound": "humanz_send_me_your_address.wav"}, + {"names": ["!abuse"], "sound": "humanz_abuse.wav"}, + {"names": ["!killme"], "sound": "doit_comeon_killme.mp3"}, + {"names": ["!sickening"], "sound": "sickening.mp3"}, + {"names": ["!disgusting"], "sound": "disgusting_scuffed.mp3"}, + {"names": ["!icq"], "sound": "icq_old_sound.wav"}, + {"names": ["!discord"], "sound": "discord-notification.mp3"}, + {"names": ["!law", "!order"], "sound": "Law_&_Order_Sound.mp3"}, + {"names": ["!deutschland"], "sound": "VorUnsLiegtDeutschland.mp3"}, + {"names": ["!qwerpi"], "sound": ["qwerpi_aotsu/answer_the_question1.mp3", "qwerpi_aotsu/answer_the_question2.mp3", "qwerpi_aotsu/answer_the_question3.mp3", "qwerpi_aotsu/answer_the_question4.mp3", "qwerpi_aotsu/answer_the_question5.mp3", "qwerpi_aotsu/answer_the_question6.mp3", "qwerpi_aotsu/answer_the_question7.mp3", "qwerpi_aotsu/aotsuki_autism.mp3", "qwerpi_aotsu/aotsuki_come_on_stop.mp3", "qwerpi_aotsu/aotsuki_kouya.mp3", "qwerpi_aotsu/aotsuki_pls.mp3", "qwerpi_aotsu/aotsukitsukitsuki1.mp3", "qwerpi_aotsu/aotsukitsukitsuki2.mp3", "qwerpi_aotsu/aotsukyaotsukyaotsuky1.mp3", "qwerpi_aotsu/aotsukyaotsukyaotsuky2.mp3", "qwerpi_aotsu/can_you_pls_tell_me_your_opinion_takeshima.mp3", "qwerpi_aotsu/come_on_aotsukyyy.mp3", "qwerpi_aotsu/faking_faking_faking.mp3", "qwerpi_aotsu/i_changed_my_name_he_can_hear_me.mp3", "qwerpi_aotsu/japan_or_america1.mp3", "qwerpi_aotsu/japan_or_america10.mp3", "qwerpi_aotsu/japan_or_america11.mp3", "qwerpi_aotsu/japan_or_america12.mp3", "qwerpi_aotsu/japan_or_america2.mp3", "qwerpi_aotsu/japan_or_america3.mp3", "qwerpi_aotsu/japan_or_america4.mp3", "qwerpi_aotsu/japan_or_america5.mp3", "qwerpi_aotsu/japan_or_america6.mp3", "qwerpi_aotsu/japan_or_america7.mp3", "qwerpi_aotsu/japan_or_america8.mp3", "qwerpi_aotsu/japan_or_america9.mp3", "qwerpi_aotsu/onegaishimasu.mp3", "qwerpi_aotsu/pls_win_round_aotsuki.mp3", "qwerpi_aotsu/takeshima1.mp3", "qwerpi_aotsu/takeshima2.mp3", "qwerpi_aotsu/takeshima3.mp3", "qwerpi_aotsu/takeshima4.mp3", "qwerpi_aotsu/takeshima5.mp3", "qwerpi_aotsu/takeshima6.mp3", "qwerpi_aotsu/takeshima7.mp3", "qwerpi_aotsu/unnamed.mp3", "qwerpi_aotsu/why_do_you_do_this_to_me.mp3", "qwerpi_aotsu/why_dont_you_just_answer1.mp3", "qwerpi_aotsu/why_dont_you_just_answer2.mp3", "qwerpi_aotsu/why_dont_you_just_answer3.mp3", "qwerpi_aotsu/why_dont_you_just_answer4.mp3", "qwerpi_aotsu/why_dont_you_just_answer5.mp3", "qwerpi_aotsu/why_dont_you_just_answer6.mp3", "qwerpi_aotsu/you_not_japanese1.mp3", "qwerpi_aotsu/you_not_japanese2.mp3", "qwerpi_aotsu/you_not_japanese3.mp3", "qwerpi_aotsu/you_will_never_get_rid_of_me_aotsuki.mp3"]}, + {"names": ["!panic"], "sound": "greta_panic.mp3"}, + {"names": ["!gachi"], "sound": ["gachi/300.mp3", "gachi/ah.mp3", "gachi/amazing.mp3", "gachi/anal.mp3", "gachi/artist.mp3", "gachi/asswecan.mp3", "gachi/attention.mp3", "gachi/bigger.mp3", "gachi/boss.mp3", "gachi/boynextdoor.mp3", "gachi/collegeboy.mp3", "gachi/comeon.mp3", "gachi/daddy.mp3", "gachi/deep.mp3", "gachi/door.mp3", "gachi/doyoulike.mp3", "gachi/dungeon.mp3", "gachi/embarrassing.mp3", "gachi/fantasies.mp3", "gachi/fuckyou.mp3", "gachi/fuckyou2.mp3", "gachi/fuckyou3.mp3", "gachi/gangingup.mp3", "gachi/happy.mp3", "gachi/interruption.mp3", "gachi/jabroni.mp3", "gachi/lash.mp3", "gachi/likethat.mp3", "gachi/loads.mp3", "gachi/lube.mp3", "gachi/mmh.mp3", "gachi/pants.mp3", "gachi/power.mp3", "gachi/rip.mp3", "gachi/slaves.mp3", "gachi/sorry.mp3", "gachi/sorry2.mp3", "gachi/spank.mp3", "gachi/suction.mp3", "gachi/takeit.mp3", "gachi/website.mp3", "gachi/woo.mp3", "gachi/wth.mp3"]}, + {"names": ["!ali", "!alia"], "sound": "AliA.mp3"}, + {"names": ["!dropit"], "sound": "dropit.mp3"}, + {"names": ["!murica"], "sound": "MURICA.mp3"}, + {"names": ["!ocean", "!oceanman"], "sound": "oceanman.mp3"}, + {"names": ["!thanos"], "sound": "thanos_snaps_fingers_sfx.mp3"}, + {"names": ["!greta"], "sound": ["greta_1.mp3", "greta_2.mp3", "greta_3.mp3"]}, + {"names": ["!playb"], "sound": ["PlayB_-_back_back_back.mp3", "PlayB_-_back_doorhug.mp3", "PlayB_-_dont_stay_fkng_nubs.mp3", "PlayB_-_not_FF"]}, + {"names": ["!alarm"], "sound": "iphone_alarm.mp3"}, + {"names": ["!iphone", "!ringtone"], "sound": "iphone_ringtone.mp3"}, + {"names": ["!gman"], "sound": ["gman_choose1.wav", "gman_mono34.wav", "Gman_wellseeaboutthat.ogg", "gman_choose1.wav", "gman_choose2.wav", "gman_wisely_done.wav", "gman_wisely_done_freeman.wav", "gman_well_shit.wav"]}, + {"names": ["!steamedjenz"], "sound": "jenz_yes.mp3"}, + {"names": ["!sinaknife"], "sound": "sinaknife.mp3"}, + {"names": ["!sans"], "sound": ["undertale_sans.mp3", "megalovania_1.mp3", "megalovania_2.mp3"]}, + {"names": ["!what", "!youwhat"], "sound": "you-what-spongebob.mp3"}, + {"names": ["!pufferfish", "!augh"], "sound": "pufferfish_augh.mp3"}, + {"names": ["!gotcha"], "sound": "Gotcha Bitch2.mp3"}, + {"names": ["!palpatine"], "sound": "Palp_doit.mp3"}, + {"names": ["!chosenone"], "sound": "ChosenOne.mp3"}, + {"names": ["!anakinfun"], "sound": "AnakinFun.mp3"}, + {"names": ["!anakinliar"], "sound": "AnakinLiar.mp3"}, + {"names": ["!hellothere"], "sound": "HelloThere.mp3"}, + {"names": ["!because"], "sound": "BecauseObiWan.mp3"}, + {"names": ["!outofhand"], "sound": "OutofHand.mp3"}, + {"names": ["!problem"], "sound": "problem.mp3"}, + {"names": ["!pizza"], "sound": ["pizza.mp3", "pizza2.mp3"]}, + {"names": ["!train"], "sound": ["train.mp3", "train2.mp3", "train4.mp3"]}, + {"names": ["!screech"], "sound": "screech.mp3"}, + {"names": ["!simp"], "sound": "simp.mp3"}, + {"names": ["!tadadada"], "sound": "tadadada.mp3"}, + {"names": ["!kroaat"], "sound": "kroaat_rage.mp3"}, + {"names": ["!passed"], "sound": "GTA San Andreas - Mission passed sound.mp3"}, + {"names": ["!ahshit"], "sound": "GTA San Andreas - Ah shit, here we go again.mp3"}, + {"names": ["!no"], "sound": "nononojojo.mp3"}, + + {"names": ["_amigos"], "sound": "10-60/amigos.mp3"}, + {"names": ["_sad"], "sound": "10-60/2sad4me2.mp3"}, + {"names": ["_minasplease"], "sound": "10-60/MINASPLEASESONG.mp3"}, + {"names": ["_jobs"], "sound": "10-60/Jobs.mp3"}, + {"names": ["_praise"], "sound": "10-60/Praise-The-Sun-420Yolo2.mp3"}, + {"names": ["_brainpower"], "sound": "10-60/Brain Power V2.mp3"}, + {"names": ["_cotton"], "sound": "10-60/Cotton Eye Akbar.mp3"}, + {"names": ["_suckmycock", "_stopshooting"], "sound": "10-60/meshlem_arrive.mp3"}, + {"names": ["_hacker"], "sound": "10-60/Meshlem_die_good.mp3"}, + {"names": ["_how", "_happen"], "sound": "10-60/how_could_this_happen_to_me.mp3"}, + {"names": ["_tuturemix"], "sound": "10-60/tuturemix.mp3"}, + {"names": ["_pirate"], "sound": "10-60/pirate.wav"}, + {"names": ["_legend"], "sound": "10-60/legend.mp3"}, + {"names": ["_pirates"], "sound": "10-60/The - best.mp3"}, + {"names": ["_pegboard"], "sound": "10-60/Pendulum - Witchcraft Pegboard.mp3"}, + {"names": ["_saxoo"], "sound": "10-60/saxo.mp3"}, + {"names": ["_no"], "sound": "10-60/NOOOOOOOOOOO.wav"}, + {"names": ["_crabrave"], "sound": "10-60/crabrave.mp3"}, + {"names": ["_takemeon"], "sound": "10-60/a-ha-take-on-me-cut-mp3.mp3"}, + {"names": ["_shake"], "sound": "10-60/ShakeRockstar.ogg"}, + {"names": ["_hitormiss"], "sound": "10-60/hom_sans.mp3"}, + {"names": ["_johnny"], "sound": "10-60/Johnny Daepp.mp3"}, + {"names": ["_theo"], "sound": "10-60/theobald_lower.mp3"}, + {"names": ["_chinamotor"], "sound": "10-60/chinesemotorcycle.mp3"}, + {"names": ["_ftarflute"], "sound": "10-60/pend_taran_flute.mp3"}, + {"names": ["_neon_remix"], "sound": "10-60/neon_remix.mp3"}, + {"names": ["_lickies"], "sound": "10-60/lickies.mp3"}, + {"names": ["_jenzlem"], "sound": "10-60/jenzlem.m4a"}, + {"names": ["_playb"], "sound": "10-60/DontfallbackdefendPlayB.mp3"}, + {"names": ["_awaken"], "sound": "10-60/awaken.mp3"}, + {"names": ["_dudu"], "sound": "10-60/dudu.mp3"}, + {"names": ["_ice"], "sound": "10-60/levshoot.mp3"}, + {"names": ["_gunga", "_ginga"], "sound": "10-60/gunga_ginga.mp3"}, + {"names": ["_makoplease"], "sound": "10-60/makoplease.mp3"}, + {"names": ["_willrip"], "sound": "10-60/fullwilly.mp3"}, + {"names": ["_rage"], "sound": "tense1983_rage.mp3"}, + {"names": ["_france"], "sound": "france.mp3"}, + {"names": ["_cne"], "sound": "Cne_goes_apocalypse_mode.mp3"}, + + {"names": ["#tempest"], "sound": "60+/tempest.wav"}, + {"names": ["#pendulum_knife"], "sound": "60+/Pendulum_KnifeParty.mp3"}, + {"names": ["#nostar"], "sound": "60+/minassong.mp3"}, + {"names": ["#pika"], "sound": "60+/pika.wav"}, + {"names": ["#tripoloski"], "sound": "60+/tripoloski.mp3"}, + {"names": ["#tunak"], "sound": "60+/tunak.mp3"}, + {"names": ["#crabman"], "sound": "60+/CrabMan.ogg"}, + {"names": ["#trump"], "sound": "Donald_Trump_-_Shooting_Stars.mp3"}, + {"names": ["#tri_remix"], "sound": "60+/tree_poloskee.mp3"} +]