1
0
Fork 0

Bot::Client: Rename classes and move to separate namespace

Bot::Client: Remove audio commands
Delete Mod_Audio
Create Mod_DiscordLogger
master
Marrub 2016-11-20 12:13:26 -05:00
parent 41c1a365a7
commit d09924cb27
8 changed files with 320 additions and 405 deletions

View File

@ -24,38 +24,24 @@ using CommandFuncDict =
namespace ProjectGolan.Vrobot3
{
//
// BotCommand
//
// Delegate type for bot commands.
//
public delegate void BotCommand(User usr, Channel channel, String msg);
//
// CommandDict
//
// Dictionary of bot commands.
//
public class CommandDict : Dictionary<String, BotCommandStructure> {}
//
// Bot
//
public partial class Bot
{
private readonly Dictionary<ulong, String> lastLine;
private readonly IBotClient client;
private readonly Client.IChatClient client;
public List<IBotModule> modules { get; private set; }
public CommandFuncDict cmdfuncs { get; private set; }
public readonly BotInfo info;
public bool isInAudioChannel => client.isInAudioChannel();
public BotClientInfo clientInfo => client.info;
public Client.ClientInfo clientInfo => client.info;
public Client.IChatClient getClient() => client;
//
// Bot constructor
//
public Bot(BotInfo info)
{
this.info = info;
@ -65,8 +51,8 @@ namespace ProjectGolan.Vrobot3
switch(info.serverType)
{
case "IRC": this.client = new BotClientIRC(this); break;
case "Discord": this.client = new BotClientDiscord(this); break;
case "IRC": this.client = new Client.ClientIRC(this); break;
case "Discord": this.client = new Client.ClientDiscord(this); break;
default: throw new BotConfigurationException("Invalid server type.");
}
@ -84,9 +70,6 @@ namespace ProjectGolan.Vrobot3
cmdfuncs.Add(kvp.Key, Tuple.Create(mod, kvp.Value));
}
//
// client
//
public void connect() => client.connect();
public void action(Channel channel, String msg) =>
@ -109,12 +92,6 @@ namespace ProjectGolan.Vrobot3
public void reply(User usr, ulong id, String msg) =>
message(id, usr.name + ": " + msg);
public void partAudioChannel() => client.partAudioChannel();
public Task playAudioFile(String file) => client.playAudioFile(file);
//
// disconnect
//
public void disconnect()
{
cmdfuncs.Clear();
@ -122,24 +99,6 @@ namespace ProjectGolan.Vrobot3
client.disconnect();
}
//
// joinAudioChannel
//
public async Task<bool> joinAudioChannel(User user)
{
var channel = client.getAudioChannel(user);
if(channel != null)
{
await client.joinAudioChannel(channel);
return true;
}
else
return false;
}
//
// checkModPermissions
//
public bool checkModPermissions(Channel channel, Type mod)
{
String[] enables;

View File

@ -59,11 +59,11 @@ namespace ProjectGolan.Vrobot3
return false;
if(attribute is BotModuleIRCAttribute &&
!(client is BotClientIRC))
!(client is Client.ClientIRC))
return false;
if(attribute is BotModuleDiscordAttribute &&
!(client is BotClientDiscord))
!(client is Client.ClientDiscord))
return false;
if(attribute is BotModuleRequiresAudioAttribute &&

View File

@ -13,12 +13,9 @@
using System;
using System.Threading.Tasks;
namespace ProjectGolan.Vrobot3
namespace ProjectGolan.Vrobot3.Client
{
//
// BotClientInfo
//
public class BotClientInfo
public class ClientInfo
{
public bool hasAudio;
public bool hasColors;
@ -27,17 +24,14 @@ namespace ProjectGolan.Vrobot3
public bool shortMessages;
}
//
// IBotClient
//
public abstract class IBotClient
public abstract class IChatClient
{
protected Bot bot;
public BotClientInfo info { get; protected set; }
public ClientInfo info { get; protected set; }
protected IBotClient(Bot bot)
protected IChatClient(Bot bot)
{
this.info = new BotClientInfo();
this.info = new ClientInfo();
this.bot = bot;
}
@ -58,16 +52,6 @@ namespace ProjectGolan.Vrobot3
// user
public abstract bool userPermitted(User usr, BotRole role);
// audio
public virtual ChannelAudio getAudioChannel(User usr) =>
new ChannelAudio();
public virtual async Task joinAudioChannel(ChannelAudio channel) =>
await Task.FromResult(0);
public virtual void partAudioChannel() {}
public virtual bool isInAudioChannel() => false;
public virtual async Task playAudioFile(String file) =>
await Task.FromResult(0);
}
}

View File

@ -15,24 +15,15 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Diagnostics;
using Discord.Audio;
namespace ProjectGolan.Vrobot3
namespace ProjectGolan.Vrobot3.Client
{
//
// BotClientDiscord
//
public class BotClientDiscord : IBotClient
public class ClientDiscord : IChatClient
{
private Discord.DiscordClient client;
private Discord.Audio.IAudioClient audioClient;
private Discord.Server server;
public Discord.DiscordClient client;
public Discord.Server server;
//
// BotClientDiscord constructor
//
public BotClientDiscord(Bot bot) :
base(bot)
public ClientDiscord(Bot bot) : base(bot)
{
this.client = new Discord.DiscordClient();
@ -61,24 +52,16 @@ namespace ProjectGolan.Vrobot3
bot.onMessage(usr, channel, evt.Message.Text);
}
};
this.client.UsingAudio(x => x.Mode = AudioMode.Outgoing);
}
//
// getUser
//
private Discord.User getUser(User usr)
public Discord.User getUser(User usr)
{
if(server == null)
server = client.GetServer(ulong.Parse(bot.info.serverAddr));
return server.GetUser(ulong.Parse(usr.hostname));
}
//
// checkRole
//
private bool checkRole(User usr, String[] strings)
public bool checkRole(User usr, String[] strings)
{
var duser = getUser(usr);
@ -102,9 +85,6 @@ namespace ProjectGolan.Vrobot3
return false;
}
//
// connect
//
public override void connect()
{
Console.WriteLine("{0}: Creating connection.", bot.info.serverName);
@ -115,42 +95,26 @@ namespace ProjectGolan.Vrobot3
});
}
//
// disconnect
//
public override void disconnect()
{
if(client != null)
{
partAudioChannel();
client.Disconnect();
client = null;
}
}
//
// sendAction
//
public override void sendAction(Channel channel, String msg) =>
client.GetChannel(channel.id)?.SendMessage(
"_" + Discord.Format.Escape(msg) + "_");
"_" + Discord.Format.Escape(msg ?? String.Empty) + "_");
//
// sendMessage
//
public override void sendMessage(Channel channel, String msg) =>
client.GetChannel(channel.id)?.SendMessage(Discord.Format.Escape(msg));
client.GetChannel(channel.id)?.SendMessage(Discord.Format.Escape(msg ?? String.Empty));
//
// sendMessageRaw
//
public override void sendMessageRaw(Channel channel, String msg) =>
sendMessage(channel,
"```" + Discord.Format.Escape(msg ?? String.Empty) + "```");
//
// getChannel
//
public override Channel getChannel(ulong id)
{
var dchannel = client.GetChannel(id);
@ -160,104 +124,10 @@ namespace ProjectGolan.Vrobot3
return channel;
}
//
// userPermitted
//
public override bool userPermitted(User usr, BotRole role) =>
role == BotRole.User ||
(role == BotRole.HalfAdmin && checkRole(usr, bot.info.roles.halfadmin)) ||
(role == BotRole.Admin && checkRole(usr, bot.info.roles.admin));
//
// getAudioChannel
//
public override ChannelAudio getAudioChannel(User usr)
{
var dchannel = getUser(usr).VoiceChannel;
if(dchannel == null) return null;
var channel = new ChannelAudio{};
channel.id = dchannel.Id;
channel.name = dchannel.Name;
return channel;
}
//
// joinAudioChannel
//
public override async Task joinAudioChannel(ChannelAudio channel)
{
if(channel == null)
return;
var dchannel = client.GetChannel(channel.id);
if(!isInAudioChannel())
audioClient = await dchannel.JoinAudio();
else
await audioClient.Join(dchannel);
}
//
// partAudioChannel
//
public override void partAudioChannel()
{
if(isInAudioChannel())
{
audioClient.Clear();
audioClient.Wait();
audioClient.Disconnect();
}
audioClient = null;
}
//
// isInAudioChannel
//
public override bool isInAudioChannel() =>
audioClient?.State == Discord.ConnectionState.Connected;
//
// playAudioFile
//
public override async Task playAudioFile(String file)
{
if(!isInAudioChannel()) return;
var proc = Process.Start(new ProcessStartInfo{
FileName = "ffmpeg",
Arguments = $"-i {file} -f s16le -ar 48000 -ac 2 " +
"-loglevel quiet pipe:1",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = false,
CreateNoWindow = true
});
var buf = new byte[3840 * 32];
var ostream = audioClient.OutputStream;
var istream = proc?.StandardOutput.BaseStream;
try
{
int count;
while(proc?.HasExited == false &&
(count = await istream.ReadAsync(buf, 0, buf.Length)) != 0)
{
Thread.Sleep(8);
await ostream.WriteAsync(buf, 0, count);
}
}
catch(OperationCanceledException)
{
Console.WriteLine("{0}: Canceled audio stream.",
bot.info.serverName);
}
finally
{
istream?.Dispose();
ostream.Dispose();
}
}
}
}

View File

@ -12,18 +12,11 @@
using System;
namespace ProjectGolan.Vrobot3
namespace ProjectGolan.Vrobot3.Client
{
//
// BotClientIRC
//
public class BotClientIRC : IBotClient
public class ClientIRC : IChatClient
{
//
// BotClientIRC constructor
//
public BotClientIRC(Bot bot) :
base(bot)
public ClientIRC(Bot bot) : base(bot)
{
this.info.hasAudio = false;
this.info.hasColors = true;

View File

@ -1,185 +0,0 @@
//-----------------------------------------------------------------------------
//
// Copyright © 2016 Project Golan
//
// See "LICENSE" for more information.
//
//-----------------------------------------------------------------------------
//
// Audio-based commands.
//
//-----------------------------------------------------------------------------
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Generic;
namespace ProjectGolan.Vrobot3.Modules
{
//
// Mod_Audio
//
[BotModuleRequiresAudio, BotModuleDisabled]
public class Mod_Audio : IBotModule
{
//
// QueueItem
//
class QueueItem
{
private Utils.URI uri;
public QueueItem(Utils.URI uri)
{
this.uri = uri;
}
public override String ToString() => String.Empty;
}
//
// Queue
//
class Queue
{
private TimeSpan curTime;
private List<QueueItem> items;
private int pos;
public Queue()
{
this.curTime = new TimeSpan();
this.items = new List<QueueItem>();
this.pos = 0;
}
public bool addItem(Utils.URI uri)
{
var item = new QueueItem(uri);
items.Add(item);
return true;
}
}
private readonly String[] validMethods =
{ "http", "https", "ftp", "ftps" };
private Queue queue = new Queue();
//
// Mod_Audio constructor
//
public Mod_Audio(Bot bot_) :
base(bot_)
{
commands["queue"] = new BotCommandStructure{
cmd = cmdQueue,
help = "Add an item (or items) to the audio queue.\n" +
"Syntax: .queue uri...\n" +
"Example: .queue https://www.youtube.com/watch?v=13pL0TiOiHM"
};
commands["play"] = new BotCommandStructure{
cmd = cmdPlay,
help = "Set the currently playing item in the queue. " +
"If a URL is given, queues and plays that.\n" +
"Syntax: .play [number|uri]\n" +
"Example: .play 5\n" +
"Example: .play https://www.youtube.com/watch?v=13pL0TiOiHM"
};
commands["lsqueue"] = new BotCommandStructure{
cmd = cmdListQueue,
help = "Lists queue items.\n" +
"Syntax: .lsqueue"
};
commands["summon"] = new BotCommandStructure{
cmd = cmdSummon,
help = "Makes the bot join your audio channel.\n" +
"Syntax: .summon"
};
commands["vanquish"] = new BotCommandStructure{
cmd = cmdVanquish,
help = "Makes the bot leave their audio channel.\n" +
"Syntax: %vanquish",
role = BotRole.HalfAdmin
};
}
//
// summon
//
private async Task<bool> summon(User usr, Channel channel)
{
if(bot.isInAudioChannel)
return true;
if(!await bot.joinAudioChannel(usr))
{
bot.reply(usr, channel,
"Couldn't find audio channel. " +
"If you are already in an audio channel, please reconnect to " +
"it and try again.");
return false;
}
return true;
}
//
// cmdQueue
//
public void cmdQueue(User usr, Channel channel, String msg)
{
var uris = Utils.URI.Matches(msg);
if(uris == null)
throw new CommandArgumentException("no valid URIs");
int loadPass = 0;
foreach(var uri_ in uris)
{
var uri = uri_;
if(uri.method == String.Empty)
uri.method = "http";
if(validMethods.Contains(uri.method) &&
queue.addItem(uri))
loadPass++;
}
bot.reply(usr, channel,
$"Added {loadPass} item{loadPass != 1 ? "s" : ""} to the queue.");
}
//
// cmdPlay
//
public void cmdPlay(User usr, Channel channel, String msg)
{
}
//
// cmdListQueue
//
public void cmdListQueue(User usr, Channel channel, String msg)
{
}
//
// cmdSummon
//
public void cmdSummon(User usr, Channel channel, String msg) =>
summon(usr, channel);
//
// cmdVanquish
//
public void cmdVanquish(User usr, Channel channel, String msg) =>
bot.partAudioChannel();
}
}

View File

@ -0,0 +1,294 @@
//-----------------------------------------------------------------------------
//
// Copyright © 2016 Project Golan
//
// See "LICENSE" for more information.
//
//-----------------------------------------------------------------------------
//
// Logging.
//
//-----------------------------------------------------------------------------
using System;
using System.Linq;
using System.Collections.Generic;
using System.Threading.Tasks;
delegate void HaxFn(String name, String vname);
namespace ProjectGolan.Vrobot3.Modules
{
[BotModuleDiscord]
public class Mod_DiscordLogger : IBotModule
{
public Mod_DiscordLogger(Bot bot) : base(bot)
{
var client = bot.getClient() as Client.ClientDiscord;
client.client.ChannelCreated +=
catchFunc<Discord.ChannelEventArgs>(
async (_, e) =>
await logMsg(e.Server, "Channel created", getChannel(e.Channel)));
client.client.ChannelDestroyed +=
catchFunc<Discord.ChannelEventArgs>(
async (_, e) =>
await logMsg(e.Server, "Channel destroyed", getChannel(e.Channel)));
client.client.ChannelUpdated +=
catchFunc<Discord.ChannelUpdatedEventArgs>(async (_, e) =>
{
var delta = getChannelDelta(e.Before, e.After);
if(delta != String.Empty)
await logMsg(e.Server, "Channel updated", delta);
});
client.client.MessageDeleted +=
catchFunc<Discord.MessageEventArgs>(async (_, e) =>
{
if(!e.Message.User.IsBot)
await verMsg(e.Server, "Message deleted", getMessage(e.Message));
});
client.client.MessageUpdated +=
catchFunc<Discord.MessageUpdatedEventArgs>(async (_, e) =>
{
var delta = getMessageDelta(e.Before, e.After);
if(delta != String.Empty)
await verMsg(e.Server, "Message edited", delta);
});
client.client.UserBanned +=
catchFunc<Discord.UserEventArgs>(async (_, e) =>
await logMsg(e.Server, "User banned", getUser(e.User)));
client.client.UserUnbanned +=
catchFunc<Discord.UserEventArgs>(async (_, e) =>
await logMsg(e.Server, "User unbanned", getUser(e.User)));
client.client.UserJoined +=
catchFunc<Discord.UserEventArgs>(async (_, e) =>
await logMsg(e.Server, "User joined", getUser(e.User)));
client.client.UserLeft +=
catchFunc<Discord.UserEventArgs>(async (_, e) =>
await logMsg(e.Server, "User removed", getUser(e.User)));
client.client.UserUpdated +=
catchFunc<Discord.UserUpdatedEventArgs>(async (_, e) =>
{
var delta = getUserDelta(e.Before, e.After);
if(delta != String.Empty)
await verMsg(e.Server, "User updated", delta);
});
}
private EventHandler<T> catchFunc<T>(EventHandler<T> fn)
{
return (object sender, T evt) =>
{
try
{fn(sender, evt);}
catch(System.Exception)
{}
};
}
private String yesno(bool arg) => arg ? "Yes" : "No";
private String getTime(DateTime time)
{
var offs = TimeZoneInfo.Local.GetUtcOffset(time);
return $"{time.ToShortDateString()} {time.ToLongTimeString()} " +
$"UTC{offs.Hours.ToString("+#;-#;0")}:{offs.Minutes}";
}
private String discordEscape(String str) =>
Discord.Format.Escape(str ?? String.Empty);
private String getChannel(Discord.Channel channel) =>
$"`Name :` {discordEscape(channel.Name)}\n" +
$"`Topic :` {discordEscape(channel.Topic)}\n" +
$"`Id :` `{channel.Id}`\n" +
$"`Position :` `{channel.Position}`\n" +
$"`Type :` {channel.Type.ToString()}";
private String getUser(Discord.User user) =>
$"`Username :` {discordEscape(user.Name)}\n" +
$"`Nickname :` {discordEscape(user.Nickname)}\n" +
$"`Id :` `{user.Id}`\n" +
$"`Is Bot :` {yesno(user.IsBot)}\n" +
$"`Joined At :` {getTime(user.JoinedAt)}";
private String getMessage(Discord.Message msg) =>
$"`Time Sent :` {getTime(msg.Timestamp)}\n" +
$"`Channel :` {discordEscape(msg.Channel.Name)}\n" +
$"`Id :` `{msg.Id}`\n" +
$"`Content :` {msg.Text}\n" +
$"\nUser info:\n{getUser(msg.User)}";
private String getMessageDelta(Discord.Message bfr, Discord.Message aft)
{
if(bfr.Text == aft.Text) return String.Empty;
return $"`Time Sent :` {getTime(aft.Timestamp)}\n" +
$"`Channel :` {discordEscape(aft.Channel.Name)}\n" +
$"`Id :` `{aft.Id}`\n" +
$"`Old Content :` {bfr.Text}\n" +
$"`New Content :` {aft.Text}\n" +
$"\nUser info:\n{getUser(aft.User)}";
}
private String getPermDelta(Discord.ServerPermissions bfr,
Discord.ServerPermissions aft)
{
var outp = String.Empty;
if(bfr.RawValue != aft.RawValue)
{
outp += $"`Old Value :` `{bfr.RawValue}`\n";
outp += $"`New Value :` `{aft.RawValue}`\n";
HaxFn fn = (String name, String vname) => {
var a = (bool)typeof(Discord.ServerPermissions)
.GetProperty(vname).GetValue(bfr);
var b = (bool)typeof(Discord.ServerPermissions)
.GetProperty(vname).GetValue(aft);
if(a != b)
outp += $"`{name}:` {yesno(b)}\n";
};
fn("Create Invite", "CreateInstantInvite");
fn("Ban Users ", "BanMembers");
fn("Kick Users ", "KickMembers");
fn("Edit Channels", "ManageChannels");
fn("Edit Server ", "ManageServer");
fn("Join Channels", "ReadMessages");
fn("Send Messages", "SendMessages");
fn("Send TTS ", "SendTTSMessages");
fn("Edit Messages", "ManageMessages");
fn("Embed Links ", "EmbedLinks");
fn("Attach Files ", "AttachFiles");
fn("Ping Everyone", "MentionEveryone");
fn("Join Voice ", "Connect");
fn("Speak ", "Speak");
fn("Mute Users ", "MuteMembers");
fn("Deafen Users ", "DeafenMembers");
fn("Move Users ", "MoveMembers");
fn("Use VA ", "UseVoiceActivation");
fn("Change Nick ", "ChangeNickname");
fn("Edit Nicks ", "ManageNicknames");
fn("Edit Roles ", "ManageRoles");
}
return outp;
}
private String getChannelDelta(Discord.Channel bfr, Discord.Channel aft)
{
var outp1 = String.Empty;
var outp2 = String.Empty;
if(bfr.Name != aft.Name)
{
outp1 += "Name changed.\n";
if(!String.IsNullOrEmpty(bfr.Name))
outp2 += $"`Old Name :` {discordEscape(bfr.Name)}\n";
if(!String.IsNullOrEmpty(aft.Name))
outp2 += $"`New Name :` {discordEscape(aft.Name)}\n";
}
if(bfr.Id != aft.Id)
{
outp1 += "Id changed.\n";
outp2 += $"`Old Id :` `{bfr.Id}`\n";
outp2 += $"`New Id :` `{aft.Id}`\n";
}
if(bfr.Topic != aft.Topic)
{
outp1 += "Topic changed.\n";
if(!String.IsNullOrEmpty(bfr.Topic))
outp2 += $"`Old Topic :` {discordEscape(bfr.Topic)}\n";
if(!String.IsNullOrEmpty(aft.Topic))
outp2 += $"`New Topic :` {discordEscape(aft.Topic)}\n";
}
if(bfr.Position != aft.Position)
{
outp1 += "Position changed.\n";
outp2 += $"`Old Position :` `{bfr.Position}`\n";
outp2 += $"`New Position :` `{aft.Position}`\n";
}
if(outp1 != String.Empty)
outp2 += $"\nChannel info:\n{getChannel(aft)}\n";
return outp1 + outp2.Trim();
}
private String getUserDelta(Discord.User bfr, Discord.User aft)
{
var outp1 = String.Empty;
var outp2 = String.Empty;
var perm = getPermDelta(bfr.ServerPermissions, aft.ServerPermissions);
if(bfr.Name != aft.Name)
{
outp1 += "Username changed.\n";
if(!String.IsNullOrEmpty(bfr.Name))
outp2 += $"`Old Username :` {discordEscape(bfr.Name)}\n";
if(!String.IsNullOrEmpty(aft.Name))
outp2 += $"`New Username :` {discordEscape(aft.Name)}\n";
}
if(bfr.Nickname != aft.Nickname)
{
outp1 += "Nickname changed.\n";
if(!String.IsNullOrEmpty(bfr.Nickname))
outp2 += $"`Old Nickname :` {discordEscape(bfr.Nickname)}\n";
if(!String.IsNullOrEmpty(aft.Nickname))
outp2 += $"`New Nickname :` {discordEscape(aft.Nickname)}\n";
}
if(bfr.AvatarId != aft.AvatarId)
{
outp1 += "Avatar changed.\n";
outp2 += $"`Old Avatar Id:` `{bfr.AvatarId}`\n";
outp2 += $"`New Avatar Id:` `{aft.AvatarId}`\n";
outp2 += $"`Old AvatarUrl:` `{bfr.AvatarUrl}`\n";
outp2 += $"`New AvatarUrl:` `{aft.AvatarUrl}`\n";
}
if(bfr.Discriminator != aft.Discriminator)
{
outp1 += "Unique Id changed.\n";
outp2 += $"`Old Unique Id:` `{bfr.Discriminator}`\n";
outp2 += $"`New Unique Id:` `{aft.Discriminator}`\n";
}
if(bfr.Id != aft.Id)
{
outp1 += "Id changed.\n";
outp2 += $"`Old Id :` `{bfr.Id}`\n";
outp2 += $"`New Id :` `{aft.Id}`\n";
}
if(!String.IsNullOrEmpty(perm))
{
outp1 += "Permissions changed.\n";
outp2 += perm;
}
if(outp1 != String.Empty)
outp2 += $"\nUser info:\n{getUser(aft)}\n";
return outp1 + outp2.Trim();
}
private async Task sndMsg(Discord.Server server, String name,
String type, String msg)
{
if(server.Id.ToString() != bot.info.serverAddr) return;
var log = server.FindChannels(name).FirstOrDefault();
if(log == null) return;
var time = getTime(DateTime.Now);
if(msg.Length > 1800)
await log.SendMessage($"{type} at {time}\n[Omitted, too long]");
else
await log.SendMessage($"{type} at {time}\n{msg}");
}
private async Task logMsg(Discord.Server server, String type, String msg)
=> await sndMsg(server, "log", type, msg);
private async Task verMsg(Discord.Server server, String type, String msg)
=> await sndMsg(server, "log-v", type, msg);
}
}

View File

@ -79,7 +79,7 @@
<Compile Include="Source/Client/Discord.cs" />
<Compile Include="Source/Client/IRC.cs" />
<Compile Include="Source/Modules/Mod_Admin.cs" />
<Compile Include="Source/Modules/Mod_Audio.cs" />
<Compile Include="Source/Modules/Mod_DiscordLogger.cs" />
<Compile Include="Source/Modules/Mod_Fun.cs" />
<Compile Include="Source/Modules/Mod_Utils.cs" />
<Compile Include="Source/Modules/Mod_Idgames.cs" />