> commandsByRole, boolean delimitSection)
+ {
+ // Formatting variables
+ final String headerStart = "";
+ final String headerEnd = "
\n";
+ final String sectionStart = "\n";
+ final String sectionEnd = "
";
+ final String indent = " ";
+ final String leadStart = "";
+ final String leadEnd = "";
+ final String leadFollow = ": ";
+ final String lineDelimiter = "
\n";
+ final String sectionDelimiter = "\n\n";
+
+ String accumulated = "";
+ ArrayList commands = commandsByRole.get(role);
+ ArrayList commandsSoFar = new ArrayList();
+ if(commands.size() > 0)
+ {
+ String roleStr = role.toString();
+ roleStr = roleStr.substring(0, 1).toUpperCase() + roleStr.toLowerCase().substring(1);
+
+ // Section header
+ accumulated += headerStart + roleStr + " commands" + headerEnd;
+ accumulated += sectionStart;
+
+ // Section content
+ for(int i = 0; i < commands.size(); ++i)
+ {
+ accumulated += indent;
+ Command command = commands.get(i);
+
+ // Skip multiples
+ if(commandsSoFar.contains(command))
+ continue;
+ else
+ commandsSoFar.add(command);
+
+ // Write out all the keys and the help text
+ ArrayList keys = command.RegisteredNames();
+ for(int j = 0; j < keys.size(); ++j)
+ {
+ if(j != 0)
+ accumulated += ", ";
+
+ accumulated += leadStart + keys.get(j) + leadEnd;
+ }
+
+ accumulated += leadFollow + commands.get(i).HelpText() + lineDelimiter;;
+ }
+
+ accumulated += sectionEnd;
+
+ // Add spaces after if needed
+ if(delimitSection)
+ accumulated += sectionDelimiter;
+ }
+
+ return accumulated;
+ }
+}
\ No newline at end of file
diff --git a/src/commands/CommandInfo.java b/src/commands/CommandInfo.java
new file mode 100644
index 0000000..99ae8b8
--- /dev/null
+++ b/src/commands/CommandInfo.java
@@ -0,0 +1,26 @@
+package commands;
+
+import core.Command;
+import dataStructures.KittyChannel;
+import dataStructures.KittyGuild;
+import dataStructures.KittyRating;
+import dataStructures.KittyRole;
+import dataStructures.KittyUser;
+import dataStructures.Response;
+import dataStructures.UserInput;
+
+public class CommandInfo extends Command
+{
+ public CommandInfo(KittyRole level, KittyRating rating) { super(level, rating); }
+
+ @Override
+ public String HelpText() { return "Provides author info and a link to Kitty's website"; }
+
+ @Override
+ public void OnRun(KittyGuild guild, KittyChannel channel, KittyUser user, UserInput input, Response res)
+ {
+ String info = "I'm made by `Rin#8904` and `Reverie Wisp#3703`!\n"
+ + "You can find more info about me along with a Patreon link to support us and GitHub link for filing bugs https://www.rinsnowmew.com/bot/" ;
+ res.Call(info);
+ }
+}
\ No newline at end of file
diff --git a/src/commands/CommandInvite.java b/src/commands/CommandInvite.java
new file mode 100644
index 0000000..3bcb6a4
--- /dev/null
+++ b/src/commands/CommandInvite.java
@@ -0,0 +1,20 @@
+package commands;
+
+import core.Command;
+import dataStructures.*;
+import offline.Ref;
+
+public class CommandInvite extends Command
+{
+ public CommandInvite(KittyRole level, KittyRating rating) { super(level, rating); }
+
+ @Override
+ public String HelpText() { return "Provies a direct invite link for KittyBot"; }
+
+ @Override
+ public void OnRun(KittyGuild guild, KittyChannel channel, KittyUser user, UserInput input, Response res)
+ {
+ res.Call("https://discordapp.com/oauth2/authorize?&client_id="+ Ref.CliID
+ +"&scope=bot&permissions=8");
+ }
+}
\ No newline at end of file
diff --git a/src/commands/CommandJDoodle.java b/src/commands/CommandJDoodle.java
new file mode 100644
index 0000000..a2add42
--- /dev/null
+++ b/src/commands/CommandJDoodle.java
@@ -0,0 +1,21 @@
+package commands;
+
+import core.Command;
+import dataStructures.*;
+import network.NetworkJDoodle;
+
+public class CommandJDoodle extends Command
+{
+ NetworkJDoodle compiler = new NetworkJDoodle();
+
+ public CommandJDoodle(KittyRole level, KittyRating rating) { super(level, rating); }
+
+ @Override
+ public String HelpText() { return "Will compile any java code you put in! Supports Java 1.8"; }
+
+ @Override
+ public void OnRun(KittyGuild guild, KittyChannel channel, KittyUser user, UserInput input, Response res)
+ {
+ res.Call(compiler.compileJava(input.args));
+ }
+}
diff --git a/src/commands/CommandMap.java b/src/commands/CommandMap.java
new file mode 100644
index 0000000..d4fb4a9
--- /dev/null
+++ b/src/commands/CommandMap.java
@@ -0,0 +1,362 @@
+package commands;
+
+import java.util.Random;
+
+import core.Command;
+import dataStructures.KittyChannel;
+import dataStructures.KittyGuild;
+import dataStructures.KittyRating;
+import dataStructures.KittyRole;
+import dataStructures.KittyUser;
+import dataStructures.Response;
+import dataStructures.UserInput;
+import utils.OptionParser;
+
+// This command is a port of worldGen command line program by Matthew Cech (https://www.matthewcech.com/)
+// Permissions were granted for use and modification in this bot.
+public class CommandMap extends Command
+{
+ final int MaxWidth = 50;
+ final int MaxHeight = 35;
+
+ public CommandMap(KittyRole roleLevel, KittyRating contentRating) { super(roleLevel, contentRating); }
+
+ @Override
+ public String HelpText() { return "Generates a map! You can pass additional information if you want with the flags `-s -w -h`. If one of the fields isn't provided, its default will be used. Note that adjusting the width and height impacts the map outcomes.\n\nDefault seed: Random,\nDefault Width: 35(max "+ MaxWidth + "),\nDefault Height: 25(max " + MaxHeight + ")"; }
+
+ @Override
+ public void OnRun(KittyGuild guild, KittyChannel channel, KittyUser user, UserInput input, Response res)
+ {
+ // Variables
+ int width = 35;
+ int height = 25;
+ long seed = 0;
+ String header = "";
+ String body = "";
+ Random randGenerator = null;
+
+ // Parse out a seed if one was provided
+ boolean hasSeed = false;
+ if(input.args != null && input.args.length() > 0)
+ try
+ {
+ OptionParser parser = new OptionParser(input.args);
+
+ String seedStr = parser.GetOption("-s", true);
+ if(seedStr != null)
+ {
+ seed = Long.parseLong(seedStr);
+ hasSeed = true;
+ }
+
+ String widthStr = parser.GetOption("-w", true);
+ if(widthStr != null)
+ width = ValidateSize(Integer.parseInt(widthStr), MaxWidth);
+
+ String heightStr = parser.GetOption("-h", true);
+ if(heightStr != null)
+ height = ValidateSize(Integer.parseInt(heightStr), MaxHeight);
+ }
+ catch(NumberFormatException e)
+ {
+ res.Call("Invalid arguments provided!");
+ return;
+ }
+
+ // Set up the random generator, creating a seed if needed.
+ if(hasSeed)
+ {
+ randGenerator = new Random(seed);
+ }
+ else
+ {
+ seed = new Random().nextLong();
+ randGenerator = new Random(seed);
+ }
+
+ // Response header creation
+ header += "Using mapgen v0.1\n";
+ header += "Seed: `" + seed + "`, Width: `"+ width +"`, Height: `"+ height + "`\n";
+
+ // Response body creation
+ body += "```\n";
+ body += GenerateMap(width, height, randGenerator);
+ body += "\n```";
+
+ // Send back the map
+ res.Call(header + body);
+ }
+
+ // Verifies and appropriately caps input as necessary
+ int ValidateSize(int input, int max)
+ {
+ if(input < 0)
+ throw new NumberFormatException();
+
+ if(input > max)
+ input = max;
+
+ return input;
+ }
+
+
+ // Does the map generation
+ String GenerateMap(int width, int height, Random gen)
+ {
+ String landscape[][] = new String[width][height];
+ int r1 = rand(gen);
+
+ // Variables to determine various feature quantities.
+ int LandScarcity = (r1 % 200) + 100; // Higher = more water.
+ int landPasses = 7; // Higher = larger land chunks.
+ int MountainScarcity = 50; // Higher = fewer mountains;
+ int mountainPasses = r1 % 8 + 2; // Times to plant mountains.
+ int marshPasses = r1 % 3 + 10; // Times to plant marshes.
+ int lavaPasses = 1; // Times to plant lava.
+ int beachPasses = 1;
+ int i, j;
+ int sizeX = height;
+ int sizeY = width;
+
+ // Previously defines
+ String LAND = "\u02F6"; // Used to be "
+ String WATER = " "; // Used to be ~
+ String MOUNTAIN = "\u25B2"; // Used to be ^
+ String BEACH = "="; // Used to be ~
+ String MARSH = "\u00A5"; // Used to be ,
+ String LAVA = "#"; // Used to be *
+ //String SPACE = " ";
+
+ ////////////////////
+ //GENERATION LOOPS//
+ ////////////////////
+
+ // Populate grid with water.
+ for(i = 0; i < sizeY; i++)
+ for(j = 0; j < sizeX; j++)
+ landscape[i][j] = WATER;
+
+ // land initial distribution.
+ for(i = 0; i < sizeY; i++)
+ for(j = 0; j < sizeX; j++)
+ {
+ r1 = rand(gen) % LandScarcity;
+ if(r1 == 0)
+ landscape[i][j] = LAND;
+ }
+
+ // Land expansion, standard land.
+ while(landPasses-- > 0)
+ for(i = 0; i < sizeY; i++)
+ for(j = 0; j < sizeX; j++)
+ if(landscape[i][j] == LAND)
+ {
+ if((rand(gen) % 3) == 0)
+ expandLand(sizeX, sizeY, landscape, i, j, LAND);
+ }
+
+ //Beach creation based on land
+ while(beachPasses-- > 0)
+ for(i = 0; i < sizeY; i++)
+ for(j = 0; j < sizeX; j++)
+ if(landscape[i][j] == LAND)
+ {
+ Surround beachQuals = new Surround(new String[]{WATER, WATER, WATER, WATER});
+
+ if((rand(gen) % 3) == 0)
+ replaceIf(sizeX, sizeY, landscape, beachQuals, i, j, BEACH);
+ }
+
+ //Mountain creation based on land
+ for(i = 0; i < sizeY; i++)
+ for(j = 0; j < sizeX; j++)
+ if(landscape[i][j] == LAND)
+ {
+ Surround MountainQuals = new Surround(new String[]{LAND, LAND, LAND, LAND});
+
+ if((rand(gen) % MountainScarcity) == 0)
+ {
+ replaceIf(sizeX, sizeY, landscape, MountainQuals, i, j, MOUNTAIN);
+ landscape[i][j] = MOUNTAIN;
+ }
+ }
+
+ //Mountain creation based on Mountain
+ while(mountainPasses-- > 0)
+ for(i = 0; i < sizeY; i++)
+ for(j = 0; j < sizeX; j++)
+ if(landscape[i][j] == LAND)
+ {
+ Surround MountainQuals = new Surround(new String[]{MOUNTAIN, MOUNTAIN, MOUNTAIN, MOUNTAIN});
+
+ int scalar = 0;
+ scalar = replaceIf(sizeX, sizeY, landscape, MountainQuals, i, j, MOUNTAIN);
+ if(scalar > 0)
+ if((rand(gen) % (30 / scalar)) == 0)
+ landscape[i][j] = MOUNTAIN;
+
+ }
+
+ //Marsh creation based on land
+ while(marshPasses-- > 0)
+ for(i = 0; i < sizeY; i++)
+ for(j = 0; j < sizeX; j++)
+ if( landscape[i][j] == LAND)
+ {
+ Surround ForestQuals = new Surround();
+ ForestQuals.left = BEACH;
+ ForestQuals.right = BEACH;
+ ForestQuals.up = BEACH;
+ ForestQuals.down = BEACH;
+
+ if((rand(gen) % 80) == 0)
+ {
+ replaceIf(sizeX, sizeY, landscape, ForestQuals, i, j, MARSH);
+ landscape[i][j] = MARSH;
+ }
+ }
+
+ //Lava creation based on land
+ while(lavaPasses-- > 0)
+ for(i = 0; i < sizeY; i++)
+ for(j = 0; j < sizeX; j++)
+ if( landscape[i][j] == MOUNTAIN)
+ {
+ Surround Mountains = new Surround(new String[]{MOUNTAIN, MOUNTAIN, MOUNTAIN, MOUNTAIN});
+
+ if((rand(gen) % 5) == 0)
+ if(replaceIf(sizeX, sizeY, landscape, Mountains, i, j, MOUNTAIN) == 4)
+ landscape[i][j] = LAVA;
+ }
+
+ ////////////
+ //CLEAN UP//
+ ////////////
+
+ //Remove lone islands, with the exception of things on the edge.
+ for(i = 0; i < sizeY; i++)
+ for(j = 0; j < sizeX; j++)
+ {
+ Surround water = new Surround(new String[]{WATER, WATER, WATER, WATER});
+ if(replaceIf(sizeX, sizeY, landscape, water, i, j, WATER) == 4)
+ landscape[i][j] = WATER;
+ }
+
+
+
+
+ // Build the map
+ String output = "";
+ for(int y = 0; y < height; ++y)
+ {
+ if(y != 0)
+ output += '\n';
+
+ for(int x = 0; x < width; ++x)
+ output += landscape[x][y];
+ }
+
+ // Return the map
+ return output;
+ }
+
+ // Standardized application of rand for the purposes of this generaton
+ int rand(Random rand)
+ {
+ return rand.nextInt();
+ }
+
+ // Ported from C struct
+ class Surround
+ {
+ public String left;
+ public String right;
+ public String up;
+ public String down;
+
+ public Surround()
+ {
+ left = null;
+ right = null;
+ up = null;
+ down = null;
+ }
+
+ public Surround(String[] args)
+ {
+ if(args.length > 0)
+ left = args[0];
+
+ if(args.length > 1)
+ right = args[1];
+
+ if(args.length > 2)
+ up = args[2];
+
+ if(args.length > 3)
+ down = args[3];
+ }
+ }
+
+ // Expands the current land block
+ static void expandLand(int x, int y, String landscape[][], int i, int j, String toAdd)
+ {
+ // Four checks, one for each block in the 4 cardinal directions.
+ if(i - 1 > -1)
+ landscape[i - 1][j] = toAdd;
+
+ if(i + 1 < y)
+ landscape[i + 1][j] = toAdd;
+
+ if(j - 1 > -1)
+ landscape[i][j - 1] = toAdd;
+
+ if(j + 1 < x)
+ landscape[i][j + 1] = toAdd;
+ }
+
+ // Replaces surrounding stuff
+ int replaceIf(int x, int y, String[][] landscape, Surround spaces, int i, int j, String toAdd)
+ {
+ // Running total of tiles changed.
+ int tilesChanged = 0;
+
+ // Check square left of me.
+ if(spaces.left != null)
+ if(j - 1 > -1)
+ if(landscape[i][j - 1] == spaces.left)
+ {
+ landscape[i][j - 1] = toAdd;
+ tilesChanged++;
+ }
+
+ // Check square right of me.
+ if(spaces.right != null)
+ if(j + 1 < x)
+ if(landscape[i][j + 1] == spaces.right)
+ {
+ landscape[i][j + 1] = toAdd;
+ tilesChanged++;
+ }
+
+ // Check square above me.
+ if(spaces.up != null)
+ if(i - 1 > -1)
+ if(landscape[i - 1][j] == spaces.up)
+ {
+ landscape[i - 1][j] = toAdd;
+ tilesChanged++;
+ }
+
+ // Check square below me.
+ if(spaces.down != null)
+ if(i + 1 < y)
+ if(landscape[i + 1][j] == spaces.down)
+ {
+ landscape[i + 1][j] = toAdd;
+ tilesChanged++;
+ }
+
+ return tilesChanged;
+ }
+}
diff --git a/src/commands/CommandPerish.java b/src/commands/CommandPerish.java
new file mode 100644
index 0000000..475c099
--- /dev/null
+++ b/src/commands/CommandPerish.java
@@ -0,0 +1,104 @@
+package commands;
+
+import java.awt.Color;
+import java.awt.image.BufferedImage;
+import java.io.File;
+import java.io.IOException;
+
+import javax.imageio.ImageIO;
+
+import core.Command;
+import dataStructures.KittyChannel;
+import dataStructures.KittyGuild;
+import dataStructures.KittyRating;
+import dataStructures.KittyRole;
+import dataStructures.KittyUser;
+import dataStructures.Response;
+import dataStructures.UserInput;
+import utils.ImageUtils;
+
+public class CommandPerish extends Command
+{
+ public CommandPerish(KittyRole level, KittyRating rating) { super(level, rating); }
+
+ @Override
+ public String HelpText() { return "Adds a red, 'a n g e r y' overlay to your icon or the icon of a friend you mentioned"; };
+
+ private static Long num = 0l;
+
+ @Override
+ public void OnRun(KittyGuild guild, KittyChannel channel, KittyUser user, UserInput input, Response res)
+ {
+ String name = null;
+ String filename = null;
+ File preProcessed = null;
+ File postProcessed = null;
+
+ synchronized(num)
+ {
+ name = "perish_" + num + ".png";
+ ++num;
+ }
+
+ try
+ {
+ try
+ {
+ filename = ImageUtils.DownloadFromURL(input.args.split(" ")[0], ".png");
+ preProcessed = new File(filename);
+ }
+ catch(Exception e)
+ {
+ KittyUser person = user;
+ if(input.mentions != null)
+ person = input.mentions[0];
+
+ filename = ImageUtils.DownloadFromURL(person.avatarID, ".png");
+ if(filename == null)
+ return;
+ }
+ preProcessed = new File(filename);
+ ApplyTintEffect(ImageIO.read(preProcessed), name);
+ }
+ catch (IOException e)
+ {
+ e.printStackTrace();
+ }
+
+ postProcessed = new File(name);
+ res.CallFile(postProcessed, "png");
+
+ ImageUtils.BlockingFileDelete(preProcessed);
+ ImageUtils.BlockingFileDelete(postProcessed);
+ }
+
+ private static void ApplyTintEffect(BufferedImage image, String name) throws IOException
+ {
+ // Iterate over each column left to right and touch up each pixel
+ for(int x = 0; x < image.getWidth(); ++x)
+ {
+ for(int y = 0; y < image.getHeight(); ++y)
+ {
+ Color c = new Color(image.getRGB(x, y), true);
+
+ int red = c.getRed();
+ red += 100;
+ if(red > 255)
+ red = 255;
+
+ int green = c.getGreen();
+
+ int blue = c.getBlue();
+ blue += 20;
+ if(blue > 255)
+ blue = 255;
+
+ Color tinted = new Color(red, green, blue, c.getAlpha());
+ image.setRGB(x, y, tinted.getRGB());
+ }
+ }
+
+ File outputfile = new File(name);
+ ImageIO.write(image, "png", outputfile);
+ }
+}
diff --git a/src/commands/CommandPing.java b/src/commands/CommandPing.java
new file mode 100644
index 0000000..137d3a9
--- /dev/null
+++ b/src/commands/CommandPing.java
@@ -0,0 +1,26 @@
+package commands;
+
+import core.*;
+import dataStructures.KittyChannel;
+import dataStructures.KittyGuild;
+import dataStructures.KittyRating;
+import dataStructures.KittyRole;
+import dataStructures.KittyUser;
+import dataStructures.Response;
+import dataStructures.UserInput;
+
+// A ping! Good example command.
+public class CommandPing extends Command
+{
+ public CommandPing(KittyRole level, KittyRating rating) { super(level, rating); }
+
+ @Override
+ public String HelpText() { return "Will respond with Pong!"; }
+
+ // Called when the command is run!
+ @Override
+ public void OnRun(KittyGuild guild, KittyChannel channel, KittyUser user, UserInput input, Response res)
+ {
+ res.Call("Pong!");
+ }
+}
diff --git a/src/commands/CommandPollManage.java b/src/commands/CommandPollManage.java
new file mode 100644
index 0000000..33230a0
--- /dev/null
+++ b/src/commands/CommandPollManage.java
@@ -0,0 +1,40 @@
+package commands;
+
+import core.*;
+import dataStructures.KittyChannel;
+import dataStructures.KittyGuild;
+import dataStructures.KittyRating;
+import dataStructures.KittyRole;
+import dataStructures.KittyUser;
+import dataStructures.Response;
+import dataStructures.UserInput;
+
+public class CommandPollManage extends Command
+{
+ public CommandPollManage(KittyRole level, KittyRating rating) { super(level, rating); }
+
+ @Override
+ public String HelpText() { return "'start' will start a new poll with the query of the line you put after, 'choice' will add a choice to the poll, 'stop' will end the poll"; }
+
+ @Override
+ public void OnRun(KittyGuild guild, KittyChannel channel, KittyUser user, UserInput input, Response res)
+ {
+ switch(input.args.split(" ")[0].toLowerCase())
+ {
+ case "start":
+ res.Call(guild.startPoll(input.args.substring(input.args.indexOf(' ')).trim()));
+ break;
+
+ case "choice":
+ res.Call(guild.addChoiceToPoll(input.args.substring(input.args.indexOf(' ')).trim()));
+ break;
+
+ case "stop":
+ res.Call(guild.endPoll());
+ break;
+
+ default:
+ break;
+ }
+ }
+}
diff --git a/src/commands/CommandPollResults.java b/src/commands/CommandPollResults.java
new file mode 100644
index 0000000..8178955
--- /dev/null
+++ b/src/commands/CommandPollResults.java
@@ -0,0 +1,35 @@
+package commands;
+
+import java.util.ArrayList;
+
+import core.*;
+import dataStructures.*;
+
+public class CommandPollResults extends Command
+{
+ public CommandPollResults(KittyRole level, KittyRating rating) { super(level, rating); }
+
+ @Override
+ public String HelpText() { return "Will show current results of a poll and percentages of votes per choice"; }
+
+ @Override
+ public void OnRun(KittyGuild guild, KittyChannel channel, KittyUser user, UserInput input, Response res)
+ {
+ String results = "";
+ int totalVotes = 0;
+ ArrayList votes = guild.choices;
+
+ for(int i = 0; i < votes.size(); i++)
+ {
+ totalVotes += votes.get(i).votes;
+ }
+
+ for(int i = 0; i < votes.size(); i++)
+ {
+
+ results += votes.get(i).votes + " people voted for `" + votes.get(i).choice + "` with `" + (int)(((double)votes.get(i).votes) / ((double)totalVotes) * 100) + "%`!\n";
+ }
+
+ res.Call(results);
+ }
+}
diff --git a/src/commands/CommandPollShow.java b/src/commands/CommandPollShow.java
new file mode 100644
index 0000000..46df883
--- /dev/null
+++ b/src/commands/CommandPollShow.java
@@ -0,0 +1,30 @@
+package commands;
+
+import core.*;
+import dataStructures.*;
+
+public class CommandPollShow extends Command
+{
+ public CommandPollShow(KittyRole level, KittyRating rating) { super(level, rating); }
+
+ @Override
+ public String HelpText() { return "Will show the current poll running"; }
+
+ @Override
+ public void OnRun(KittyGuild guild, KittyChannel channel, KittyUser user, UserInput input, Response res)
+ {
+ if(!guild.polling)
+ {
+ res.Call("There is no poll running!");
+ return;
+ }
+ String poll = "The current poll is `" + guild.poll + "`\n";
+ poll += "And the choices are:\n";
+ for(int i = 0; i < guild.choices.size(); i++)
+ {
+ poll += (i+1) + ": `" + guild.choices.get(i).choice + "`\n";
+ }
+
+ res.Call(poll);
+ }
+}
diff --git a/src/commands/CommandPollVote.java b/src/commands/CommandPollVote.java
new file mode 100644
index 0000000..120b371
--- /dev/null
+++ b/src/commands/CommandPollVote.java
@@ -0,0 +1,45 @@
+package commands;
+
+import core.*;
+import dataStructures.*;
+
+public class CommandPollVote extends Command
+{
+ public CommandPollVote(KittyRole level, KittyRating rating) { super(level, rating); }
+
+ @Override
+ public String HelpText() { return "Vote in a poll with the choice number, won't work if no poll is running, you can't change your vote once you have cast it! Be careful!"; }
+
+ @Override
+ public void OnRun(KittyGuild guild, KittyChannel channel, KittyUser user, UserInput input, Response res)
+ {
+ if(guild.polling)
+ {
+ if(guild.hasVoted.contains(user.uniqueID))
+ {
+ res.Call("You already voted");
+ return;
+ }
+ try
+ {
+ int voteNum = Integer.parseInt(input.args)-1;
+ if(voteNum >= guild.choices.size() || voteNum < 0)
+ {
+ res.Call(voteNum + " That's not a vaild vote!");
+ return;
+ }
+ KittyPoll polled = guild.choices.get(voteNum);
+ polled.votes++;
+ guild.hasVoted.add(user.uniqueID);
+ res.Call("You successfully voted for `" + polled.choice + "`!");
+ return;
+ }
+ catch (NumberFormatException e)
+ {
+ res.Call("That's not a vaild number!");
+ return;
+ }
+ }
+ res.Call("There is no poll running!");
+ }
+}
diff --git a/src/commands/CommandRPEnd.java b/src/commands/CommandRPEnd.java
new file mode 100644
index 0000000..1f97c5f
--- /dev/null
+++ b/src/commands/CommandRPEnd.java
@@ -0,0 +1,36 @@
+package commands;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.UnsupportedEncodingException;
+
+import core.Command;
+import core.RPManager;
+import dataStructures.*;
+
+public class CommandRPEnd extends Command
+{
+ public CommandRPEnd (KittyRole level, KittyRating rating) { super(level, rating); }
+
+ @Override
+ public String HelpText() { return "Ends RP in channel"; }
+
+ @Override
+ public void OnRun(KittyGuild guild, KittyChannel channel, KittyUser user, UserInput input, Response res)
+ {
+ File sending = null;
+ try {
+ sending = RPManager.instance.endRP(channel, user);
+ } catch (FileNotFoundException | UnsupportedEncodingException e)
+ {
+ System.out.println("I don't know how you got here");
+ }
+ if(sending != null)
+ {
+ res.CallFile(sending, "txt");
+ res.Call("Here's your file!");
+ }
+ else
+ res.Call("You can't end this RP!");
+ }
+}
diff --git a/src/commands/CommandRPG.java b/src/commands/CommandRPG.java
new file mode 100644
index 0000000..dd2273c
--- /dev/null
+++ b/src/commands/CommandRPG.java
@@ -0,0 +1,51 @@
+package commands;
+
+import core.Command;
+import core.rpg.RPGFramework;
+import dataStructures.KittyChannel;
+import dataStructures.KittyGuild;
+import dataStructures.KittyRating;
+import dataStructures.KittyRole;
+import dataStructures.KittyUser;
+import dataStructures.Response;
+import dataStructures.UserInput;
+
+
+public class CommandRPG extends Command
+{
+ private RPGFramework framework;
+
+ public CommandRPG(KittyRole role, KittyRating rating)
+ {
+ super(role, rating);
+ framework = new RPGFramework();
+ }
+
+ @Override
+ public String HelpText() { return "Admin+ command only right now - experimental text RPG! Format is !rpg "; };
+
+
+ @Override
+ public void OnRun(KittyGuild guild, KittyChannel channel, KittyUser user, UserInput input, Response res)
+ {
+ if(input.args == null || input.args.length() == 0)
+ {
+ res.Call(HelpText());
+ return;
+ }
+
+ String result = null;
+ synchronized(framework)
+ {
+ result = framework.Run(user.uniqueID, input.args.trim());
+ }
+
+ if(result == null)
+ {
+ res.Call("Invalid RPG Command!");
+ return;
+ }
+
+ res.Call(result);
+ }
+}
diff --git a/src/commands/CommandRPStart.java b/src/commands/CommandRPStart.java
new file mode 100644
index 0000000..dce9b3b
--- /dev/null
+++ b/src/commands/CommandRPStart.java
@@ -0,0 +1,31 @@
+package commands;
+
+import java.util.ArrayList;
+
+import core.Command;
+import core.RPManager;
+import dataStructures.*;
+
+public class CommandRPStart extends Command
+{
+ public CommandRPStart (KittyRole level, KittyRating rating) { super(level, rating); }
+
+ @Override
+ public String HelpText() { return "Starts an rp, add people to it by mentioning them!"; }
+
+ @Override
+ public void OnRun(KittyGuild guild, KittyChannel channel, KittyUser user, UserInput input, Response res)
+ {
+ ArrayList users = new ArrayList();
+ users.add(user);
+ if(input.mentions != null)
+ {
+ for(int i = 0; i < input.mentions.length; i++)
+ {
+ users.add(input.mentions[i]);
+ }
+ }
+
+ res.Call(RPManager.instance.newRP(channel, users));
+ }
+}
diff --git a/src/commands/CommandRating.java b/src/commands/CommandRating.java
new file mode 100644
index 0000000..c2a09c2
--- /dev/null
+++ b/src/commands/CommandRating.java
@@ -0,0 +1,64 @@
+package commands;
+
+import core.Command;
+import dataStructures.KittyChannel;
+import dataStructures.KittyGuild;
+import dataStructures.KittyRating;
+import dataStructures.KittyRole;
+import dataStructures.KittyUser;
+import dataStructures.Response;
+import dataStructures.UserInput;
+
+public class CommandRating extends Command
+{
+ public CommandRating(KittyRole level, KittyRating rating) { super(level, rating); }
+
+ @Override
+ public String HelpText() { return "'0' is fully sfw (Derpi and e621 searches are disabled), '1' is filtered (kitty auto appends a sfw tag on any searches), '2' is nsfw (any search will go through). Some other words are supported for setting filter as well."; }
+
+ // Called when the command is run!
+ @Override
+ public void OnRun(KittyGuild guild, KittyChannel channel, KittyUser user, UserInput input, Response res)
+ {
+ String newRating = null;
+ switch(input.args.toLowerCase().trim())
+ {
+ case "0":
+ case "sfw":
+ case "safe":
+ case "clean":
+ case "work":
+ case "safeforwork":
+ guild.contentRating = KittyRating.Safe;
+ newRating = "Safe";
+ break;
+ case "1":
+ case "questionable":
+ case "intermediate":
+ case "middle":
+ case "search filter":
+ case "filter":
+ case "filtered":
+ guild.contentRating = KittyRating.Filtered;
+ newRating = "Filtered";
+ break;
+ case "2":
+ case "rude":
+ case "nsfw":
+ case "explicit":
+ case "lewd":
+ case "notsafeforwork":
+ guild.contentRating = KittyRating.Explicit;
+ newRating = "Explicit";
+ break;
+ }
+
+ if(newRating != null)
+ res.Call("Kittybot content set to " + newRating);
+ else
+ res.Call("Invalid content rating `" + input.args + "`");
+
+ if(newRating.equals("Filtered"))
+ res.Call("Warning: NSFW may slip through, images are only based on tags on their respective sites!");
+ }
+}
\ No newline at end of file
diff --git a/src/commands/CommandRole.java b/src/commands/CommandRole.java
new file mode 100644
index 0000000..196ddda
--- /dev/null
+++ b/src/commands/CommandRole.java
@@ -0,0 +1,60 @@
+package commands;
+
+import core.Command;
+import dataStructures.*;
+
+public class CommandRole extends Command
+{
+ public CommandRole (KittyRole level, KittyRating rating) { super(level, rating); }
+
+ @Override
+ public String HelpText() { return "Will show the current role you have, Admins can change others roles with input of 'role x @y' with x being blacklist, general, mod, or admin. Blacklist will not allow the user to interact with kitty anymore, general mod and admin will give the user access to those commands."; }
+
+ @Override
+ public void OnRun(KittyGuild guild, KittyChannel channel, KittyUser user, UserInput input, Response res)
+ {
+ if(input.args.isEmpty())
+ {
+ res.Call("Your role is " + user.GetRole().name() + "!");
+ return;
+ }
+
+ if(user.GetRole().getValue() < KittyRole.Admin.getValue())
+ {
+ res.Call("You aren't allowed to do that! You must have the KittyRole '" + KittyRole.Admin.toString() + "' or higher!");
+ return;
+ }
+
+ KittyRole newRole;
+ switch(input.args.split(" ")[0].toLowerCase())
+ {
+ case "blacklist":
+ newRole = KittyRole.Blacklisted;
+ break;
+
+ case "general":
+ newRole = KittyRole.General;
+ break;
+
+ case "mod":
+ newRole = KittyRole.Mod;
+ break;
+
+ case "admin":
+ newRole = KittyRole.Admin;
+ break;
+
+ default:
+ res.Call("Please enter `general`, `mod`, or `admin`!");
+ return;
+ }
+ String users = "";
+ for(int i = 0; i < input.mentions.length; i++)
+ {
+ input.mentions[i].ChangeRole(newRole);
+ users += input.mentions[i].name + " ";
+ }
+
+ res.Call("Changed " + users + " to role `" + newRole.name() + "`!");
+ }
+}
diff --git a/src/commands/CommandRoll.java b/src/commands/CommandRoll.java
new file mode 100644
index 0000000..488d570
--- /dev/null
+++ b/src/commands/CommandRoll.java
@@ -0,0 +1,89 @@
+package commands;
+
+import core.*;
+import dataStructures.KittyChannel;
+import dataStructures.KittyGuild;
+import dataStructures.KittyRating;
+import dataStructures.KittyRole;
+import dataStructures.KittyUser;
+import dataStructures.Response;
+import dataStructures.UserInput;
+
+public class CommandRoll extends Command
+{
+ public CommandRoll(KittyRole level, KittyRating rating) { super(level, rating); }
+
+ @Override
+ public String HelpText() { return "Based on input of xdy where x is number of dice and y is faces kitty will roll that amount of dice, display the individual rolls and total"; }
+
+ @Override
+ public void OnRun(KittyGuild guild, KittyChannel channel, KittyUser user, UserInput input, Response res)
+ {
+ int dice;
+ int sides;
+ String dicenum [] = input.args.split("d");
+
+ try
+ {
+ dice = Integer.parseInt(dicenum[0]);
+ sides = Integer.parseInt(dicenum[1]);
+ }
+ catch (NumberFormatException e)
+ {
+ res.Call("Rude.");
+ return;
+ }
+
+ res.Call(rollDice(dice,sides));
+ }
+
+ private String rollDice(int dice, int sides)
+ {
+ int total = 0;
+ int roll = 0;
+ String nums = "";
+
+ //checks for lower than 1 dice or dice size
+ if(dice > 100 || sides > 100)
+ {
+ return ("*drowns in dice*");
+ }
+
+ if(dice < 1 || sides < 1)
+ {
+ return("I can't do that!");
+ }
+
+ for(int i = 0; dice > i; i ++)
+ {
+ roll = (int)(Math.random() * sides) + 1;
+ nums += roll + " ";
+ total += roll;
+ }
+
+ //makes output look nicer
+ String prettyNums = "";
+ String[] splitNums = nums.split(" ");
+ if(splitNums.length > 10)
+ {
+ final int len = 5;
+ for(int i = 0; i < splitNums.length; i += len)
+ {
+ prettyNums += "\n";
+ for(int j = 0; j < len; ++j)
+ {
+ if(j != 0)
+ prettyNums += " ";
+
+ prettyNums += splitNums[i + j];
+ }
+ }
+ }
+ else
+ {
+ prettyNums = nums;
+ }
+
+ return ("Rolls are\n" + prettyNums + "\n" + "Total is: " + total);
+ }
+}
diff --git a/src/commands/CommandShutdown.java b/src/commands/CommandShutdown.java
new file mode 100644
index 0000000..7339c51
--- /dev/null
+++ b/src/commands/CommandShutdown.java
@@ -0,0 +1,53 @@
+package commands;
+
+import core.Command;
+import core.DatabaseManager;
+import core.Stats;
+import dataStructures.KittyChannel;
+import dataStructures.KittyGuild;
+import dataStructures.KittyRating;
+import dataStructures.KittyRole;
+import dataStructures.KittyUser;
+import dataStructures.Response;
+import dataStructures.UserInput;
+
+// NOTE(wisp): This is a sort of special command.
+public class CommandShutdown extends Command
+{
+ public CommandShutdown(KittyRole level, KittyRating rating) { super(level, rating); }
+
+ @Override
+ public String HelpText() { return "Stops kitty. `-s` or `safe` as an argument attempts to sync off the database before shutdown."; }
+
+ // Called when the command is run!
+ @Override
+ public void OnRun(KittyGuild guild, KittyChannel channel, KittyUser user, UserInput input, Response res)
+ {
+ // Flag the shutdown immediately.
+ Stats.instance.IndicateShutdown();
+
+ boolean isSafe = false;
+ switch(input.args.toLowerCase().trim())
+ {
+ case "s":
+ case "safe":
+ case "-s":
+ case "-safe":
+ case "snafe":
+ isSafe = true;
+ break;
+ }
+
+ if(isSafe)
+ {
+ DatabaseManager.instance.Upkeep(); // Force upkeep, this works so long as on main thread.
+ res.CallImmediate("Forced shutdown, database synced before abandoning threads.");
+ System.exit(0);
+ }
+ else
+ {
+ res.CallImmediate("Forced immediate shutdown, threads abandoned without sync.");
+ System.exit(0);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/commands/CommandStark.java b/src/commands/CommandStark.java
new file mode 100644
index 0000000..d27fa04
--- /dev/null
+++ b/src/commands/CommandStark.java
@@ -0,0 +1,92 @@
+package commands;
+
+import java.awt.Color;
+import java.awt.image.BufferedImage;
+import java.io.File;
+import java.io.IOException;
+
+import javax.imageio.ImageIO;
+
+import core.Command;
+import dataStructures.*;
+import utils.ImageUtils;
+
+public class CommandStark extends Command
+{
+ public CommandStark(KittyRole level, KittyRating rating) { super(level, rating); }
+
+ @Override
+ public String HelpText() { return "Snaps your icon or the icon of a friend you mentioned"; };
+
+ private static Long num = 0l;
+
+ @Override
+ public void OnRun(KittyGuild guild, KittyChannel channel, KittyUser user, UserInput input, Response res)
+ {
+ String name = null;
+ String filename = null;
+ File preProcessed = null;
+ File postProcessed = null;
+
+ synchronized(num)
+ {
+ name = "snapped_" + num + ".png";
+ ++num;
+ }
+
+ try
+ {
+ try
+ {
+ filename = ImageUtils.DownloadFromURL(input.args.split(" ")[0], ".png");
+ preProcessed = new File(filename);
+ }
+ catch(Exception e)
+ {
+ KittyUser person = user;
+ if(input.mentions != null)
+ person = input.mentions[0];
+
+ filename = ImageUtils.DownloadFromURL(person.avatarID, ".png");
+ if(filename == null)
+ return;
+ }
+ preProcessed = new File(filename);
+ ApplySnap(ImageIO.read(preProcessed), name);
+ }
+ catch (IOException e)
+ {
+ e.printStackTrace();
+ }
+
+ postProcessed = new File(name);
+ res.CallFile(postProcessed, "png");
+
+ ImageUtils.BlockingFileDelete(preProcessed);
+ ImageUtils.BlockingFileDelete(postProcessed);
+ }
+
+ private static void ApplySnap(BufferedImage image, String name) throws IOException
+ {
+ BufferedImage snap = ImageUtils.copyImage(image);
+ // Iterate over each column left to right and touch up each pixel
+ for(int x = 0; x < snap.getWidth(); ++x)
+ {
+ for(int y = 0; y < snap.getHeight(); ++y)
+ {
+ Color c = new Color(snap.getRGB(x, y), true);
+
+ int alpha = c.getAlpha();
+ if(x * Math.random() > 25)
+ alpha = (int)((1 - (x / ((float)snap.getWidth()))) * 255);
+
+
+ Color snapped = new Color(c.getRed(),c.getGreen(), c.getBlue(), alpha);
+ snap.setRGB(x, y, snapped.getRGB());
+ }
+ }
+
+ File outputfile = new File(name);
+ ImageIO.write(snap, "png", outputfile);
+ }
+}
diff --git a/src/commands/CommandStats.java b/src/commands/CommandStats.java
new file mode 100644
index 0000000..fe865a3
--- /dev/null
+++ b/src/commands/CommandStats.java
@@ -0,0 +1,66 @@
+package commands;
+
+import core.Command;
+import core.CommandManager.ThreadData;
+import dataStructures.KittyChannel;
+import dataStructures.KittyGuild;
+import dataStructures.KittyRating;
+import dataStructures.KittyRole;
+import dataStructures.KittyUser;
+import dataStructures.Response;
+import dataStructures.UserInput;
+import core.Stats;
+
+public class CommandStats extends Command
+{
+ public CommandStats(KittyRole level, KittyRating rating) { super(level, rating); }
+
+ @Override
+ public String HelpText() { return "Displays the actively running KittyBot application information"; }
+
+ // Called when the command is run!
+ @Override
+ public void OnRun(KittyGuild guild, KittyChannel channel, KittyUser user, UserInput input, Response res)
+ {
+ String out = "```\n";
+ Stats stats = Stats.instance;
+
+ out += "----- [General] -----\n";
+ out += " Messages observed: " + stats.GetMessagesSeen() + "\n";
+ out += "Commands processed: " + stats.GetCommandsProcessed() + "\n";
+ out += " KittyBot uptime: " + stats.GetFormattedUptime() + "\n";
+ out += "\n";
+ out += "----- [Health] -----\n";
+ out += " SMT cores: " + stats.GetCPUAvailable() + "\n";
+
+ // If CPU load works on this OS, list it.
+ double CPULoad = stats.GetSystemCPULoad();
+ if(CPULoad > -0.9) // -1.0 is the error state
+ out += "System CPU Load: " + (CPULoad * 100) + "%\n";
+
+ ThreadData data = stats.GetThreadData();
+ Integer terminated = data.states.get(Thread.State.TERMINATED);
+ Integer runnable = data.states.get(Thread.State.RUNNABLE);
+ Integer blocked = data.states.get(Thread.State.BLOCKED);
+ Integer timed_waiting = data.states.get(Thread.State.TIMED_WAITING);
+ Integer waiting = data.states.get(Thread.State.WAITING);
+
+ out += " Child threads:"
+ + " run: " + (runnable == null ? 0 : runnable)
+ + " block: " + (blocked == null ? 0 : blocked)
+ + " sleep: " + (timed_waiting == null ? 0 : timed_waiting)
+ + " wait: " + (waiting == null ? 0 : waiting)
+ + " term: " + (terminated == null ? 0 : terminated)
+ + "\n";
+
+ Integer guildCount = stats.GetGuildCount();
+ Integer userCount = stats.GetUserCount();
+
+ out += "\n----- [Counts] -----\n"
+ + "Guilds: " + guildCount + "\n"
+ + " Users: " + userCount;
+ out += "\n```";
+
+ res.Call(out);
+ }
+}
diff --git a/src/commands/CommandWolfram.java b/src/commands/CommandWolfram.java
new file mode 100644
index 0000000..8f48c2e
--- /dev/null
+++ b/src/commands/CommandWolfram.java
@@ -0,0 +1,36 @@
+package commands;
+
+import java.io.File;
+import java.io.IOException;
+import core.Command;
+import dataStructures.*;
+import network.*;
+import utils.ImageUtils;
+
+public class CommandWolfram extends Command
+{
+ NetworkWolfram searcher = new NetworkWolfram();
+
+ public CommandWolfram(KittyRole level, KittyRating rating) { super(level, rating);}
+
+ @Override
+ public String HelpText() { return "Will query wolframalpha with your question and give a full image output of the answer"; }
+
+ @Override
+ public void OnRun(KittyGuild guild, KittyChannel channel, KittyUser user, UserInput input, Response res)
+ {
+ if(input.args == null || input.args.trim().length() == 0)
+ res.Call("You need to provide some arguments!");
+
+ try
+ {
+ File pic = new File(searcher.getWolfram(input.args));
+ res.CallFile(pic, "png");
+ ImageUtils.BlockingFileDelete(pic);
+ }
+ catch (IOException e)
+ {
+ res.Call("Something went wrong!");
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/commands/CommandYeet.java b/src/commands/CommandYeet.java
new file mode 100644
index 0000000..85a83e9
--- /dev/null
+++ b/src/commands/CommandYeet.java
@@ -0,0 +1,336 @@
+package commands;
+
+import java.awt.image.BufferedImage;
+import java.awt.image.DataBufferByte;
+import java.awt.image.RenderedImage;
+import java.io.File;
+import java.io.IOException;
+import java.util.Calendar;
+import java.util.Iterator;
+
+import javax.imageio.IIOException;
+import javax.imageio.IIOImage;
+import javax.imageio.ImageIO;
+import javax.imageio.ImageTypeSpecifier;
+import javax.imageio.ImageWriteParam;
+import javax.imageio.ImageWriter;
+import javax.imageio.metadata.IIOMetadata;
+import javax.imageio.metadata.IIOMetadataNode;
+import javax.imageio.stream.FileImageOutputStream;
+import javax.imageio.stream.ImageOutputStream;
+
+import core.Command;
+import dataStructures.KittyChannel;
+import dataStructures.KittyGuild;
+import dataStructures.KittyRating;
+import dataStructures.KittyRole;
+import dataStructures.KittyUser;
+import dataStructures.Response;
+import dataStructures.UserInput;
+import utils.ImageUtils;
+
+public class CommandYeet extends Command
+{
+
+ // Required constructor
+ public CommandYeet(KittyRole level, KittyRating rating) { super(level, rating); }
+
+ private static Long num = 0l;
+
+ @Override
+ public String HelpText() { return "Yeet yourself or yeet a friend with @!"; }
+
+ // Called when the command is run!
+ @Override
+ public void OnRun(KittyGuild guild, KittyChannel channel, KittyUser user, UserInput input, Response res)
+ {
+ String name = null;
+ File yeetFile = null;
+ File yeeteeFile = null;
+
+ synchronized(num)
+ {
+ name = "yeet_" + num + ".gif";
+ ++num;
+ }
+
+ try
+ {
+ KittyUser person = null;
+
+ if(input.mentions == null)
+ {
+ person = user;
+ }
+ else
+ {
+ person = input.mentions[0];
+ }
+
+ String yeeteeFilename = ImageUtils.DownloadFromURL(person.avatarID, ".png");
+ if(yeeteeFilename == null)
+ return;
+
+ yeeteeFile = new File(yeeteeFilename);
+ YEET(ImageIO.read(yeeteeFile), name);
+ }
+ catch (IOException e)
+ {
+ e.printStackTrace();
+ }
+
+ yeetFile = new File (name);
+ res.CallFile(yeetFile, "gif");
+
+ // Thread cleanup...
+ ImageUtils.BlockingFileDelete(yeetFile);
+ ImageUtils.BlockingFileDelete(yeeteeFile);
+ }
+
+ // Lets get some trash established for the yeet
+ public static final boolean VERBOSE = false;
+ public static final int NONE = -1;
+ public static final int PIXEL_BYTE_LENGTH = 4; // 32-bit PNG, RGBA
+ public static final String YEET_BASE_PATH = "yeet/frames/";
+ public static final int YEET_BASE_SIZE = 24;
+ public static final int YEET_FPS = 18;
+
+ // Some small logging functions... cause we're professionals...
+ public static void Verbose(String str) { if(VERBOSE) Log("[Verbose] " + str); }
+ public static void Log(String str) { System.out.println("[Log] " + str); }
+ public static void Warn(String str) { System.out.println("[Warn] " + str); }
+ public static void Error(String str) { System.out.println("[Error] " + str); }
+
+ // Performs the Y E E T (image overlay per frame specified, catches IO errors as a note.)
+ public static void YEET(BufferedImage overlay, String outpath)
+ {
+ try
+ {
+ long start = Calendar.getInstance().getTimeInMillis();
+
+ ProcessYeet(overlay, outpath);
+
+ long end = Calendar.getInstance().getTimeInMillis();
+ Log("Took " + (end - start) + "ms");
+ }
+ catch (IOException e)
+ {
+ e.printStackTrace();
+ }
+ }
+
+ // Processing the image frames to construct a gif. Relies on (0,255,0) pixel for image center specification.
+ public static void ProcessYeet(BufferedImage overlay, String outfileName) throws IOException
+ {
+ BufferedImage[] frames = new BufferedImage[YEET_BASE_SIZE];
+
+ for(int i = 0; i < YEET_BASE_SIZE; ++i)
+ {
+ BufferedImage out = CombineImages(YEET_BASE_PATH + "yeet " + i + ".png", overlay);
+
+ if(out != null)
+ frames[i] = out;
+ else
+ Error("There was an issue with frame " + i);
+ }
+
+ Verbose("Processed " + YEET_BASE_SIZE + " frames, writing...");
+ ImageOutputStream output = new FileImageOutputStream(new File(outfileName));
+ GifSequenceWriter writer = new GifSequenceWriter(output, frames[0].getType(), YEET_FPS, true);
+
+ for(int i = 0; i < YEET_BASE_SIZE; ++i)
+ writer.writeToSequence(frames[i]);
+
+ writer.close();
+ output.close();
+ }
+
+ // Performs an overlay at a pixel location. We're making some assumptions here, mostly that there is going
+ // to be an RGBA-32-bit encoded PNG image read in for our parsing purposes. We then look for a 0, 255, 0
+ // green pixel on the base frame to apply the overlay buffer to, centered.
+ public static BufferedImage CombineImages(String pathBase, BufferedImage overlay) throws IOException
+ {
+ // Acquire images
+ BufferedImage imageBase = null;
+ BufferedImage imageOverlay = overlay;
+
+ imageBase = ImageIO.read(new File(pathBase));
+
+ // Grab data we know won't change. As a side note, I discovered you can get specific pixels a bit
+ // differently later, but it was too late, I wrote the pixel specific code already.
+ final byte[] pixelsBase = ((DataBufferByte) imageBase.getRaster().getDataBuffer()).getData();
+ final int baseWidth = imageBase.getWidth();
+ final int overlayWidth = imageOverlay.getWidth();
+ final int baseHeight = imageBase.getHeight();
+ final int overlayHeight = imageOverlay.getHeight();
+
+ // Warn about odd sizing when applicable
+ if(overlayHeight > baseHeight || overlayWidth > baseWidth)
+ Warn("Size mismatch, overlay is larger. May function, but not supported.");
+
+ // Skim base picture for solid green pixel to use as target center
+ int targetX = NONE;
+ int targetY = NONE;
+ for(int i = 0; i < pixelsBase.length; i += PIXEL_BYTE_LENGTH)
+ {
+ final int x = ((i / PIXEL_BYTE_LENGTH) % baseWidth);
+ final int y = ((i / PIXEL_BYTE_LENGTH) / baseWidth);
+
+ final int r = (pixelsBase[i]);
+ final int g = (pixelsBase[i + 1]);
+ final int b = (pixelsBase[i + 2]);
+ //final int a = (pixelsBase[i + 3]);
+
+ // Max contrast in 32-bit PNG for green is no red no blue, max green (ratio), max alpha (implied 0 here)
+ if(r == -1 && g == 0 && b == -1)
+ {
+ //Log("Found target pixel at x: " + x + " y: " + y);
+ targetX = x;
+ targetY = y;
+ break;
+ }
+ }
+
+ // Skip overlay if we have no target location
+ if(targetX == NONE || targetY == NONE)
+ {
+ Verbose("No overlay required for this frame.");
+ return imageBase;
+ }
+
+ // Re-create a new byte array and establish overlay bounds
+ final int left = targetX - overlayWidth / 2;
+ final int right = targetX + overlayWidth / 2;
+ final int top = targetY - overlayHeight / 2;
+ final int bottom = targetY + overlayHeight / 2;
+
+ // Apply overlay when appropriate
+ for(int i = 0; i < pixelsBase.length; i += PIXEL_BYTE_LENGTH)
+ {
+ final int x = ((i / PIXEL_BYTE_LENGTH) % baseWidth);
+ final int y = ((i / PIXEL_BYTE_LENGTH) / baseWidth);
+
+ // If we're within the bounds of where the overlay picture should go...
+ if(x > left && x < right && y > top && y < bottom)
+ {
+ final int overlayX = x - left;
+ final int overlayY = y - top;
+
+ // Skip out of bounds pixels
+ if(overlayX < 0 || overlayY < 0 || overlayX >= baseWidth || overlayY >= baseHeight)
+ continue;
+
+ // Re-write bytes as overlay
+ imageBase.setRGB(x, y, imageOverlay.getRGB(overlayX, overlayY));
+ }
+ }
+
+ // Give the frame back
+ return imageBase;
+ }
+
+
+ // GifSequenceWriter.java
+ //
+ // Created by Elliot Kroo on 2009-04-25,
+ // (Small modifications by wisp in 2018)
+ //
+ // This work is licensed under the Creative Commons Attribution 3.0 Unported License.
+ // To view a copy of this license visit http://creativecommons.org/licenses/by/3.0/
+ // NOTE(wisp): This is Elliot's original license
+ public static class GifSequenceWriter
+ {
+ protected ImageWriter gifWriter;
+ protected ImageWriteParam imageWriteParam;
+ protected IIOMetadata imageMetaData;
+
+ // NOTE: This is limited to things that divide into 1000 cleanly as multiples of 10.
+ public GifSequenceWriter(ImageOutputStream outputStream, int imageType, int framesPerSecond, boolean isLooping) throws IIOException, IOException
+ {
+ gifWriter = getWriter();
+ imageWriteParam = gifWriter.getDefaultWriteParam();
+ ImageTypeSpecifier imageTypeSpecifier = ImageTypeSpecifier.createFromBufferedImageType(imageType);
+
+ imageMetaData = gifWriter.getDefaultImageMetadata(imageTypeSpecifier, imageWriteParam);
+
+ String metaFormatName = imageMetaData.getNativeMetadataFormatName();
+
+ IIOMetadataNode root = (IIOMetadataNode) imageMetaData.getAsTree(metaFormatName);
+ IIOMetadataNode graphicsControlExtensionNode = getNode(root, "GraphicControlExtension");
+ graphicsControlExtensionNode.setAttribute("disposalMethod", "none");
+ graphicsControlExtensionNode.setAttribute("userInputFlag", "FALSE");
+ graphicsControlExtensionNode.setAttribute("transparentColorFlag", "FALSE");
+ graphicsControlExtensionNode.setAttribute("delayTime", "" + (int) ((1000.0 / framesPerSecond) / 10.0)); // THis line isn't super well honored
+ graphicsControlExtensionNode.setAttribute("transparentColorIndex","0");
+
+ IIOMetadataNode appEntensionsNode = getNode( root, "ApplicationExtensions");
+ IIOMetadataNode child = new IIOMetadataNode("ApplicationExtension");
+ child.setAttribute("applicationID", "NETSCAPE");
+ child.setAttribute("authenticationCode", "2.0");
+
+ int loopValue = isLooping ? 0 : 1;
+ child.setUserObject(new byte[]{ 0x1, (byte) (loopValue & 0xFF), (byte) ((loopValue >> 8) & 0xFF)});
+ appEntensionsNode.appendChild(child);
+
+ imageMetaData.setFromTree(metaFormatName, root);
+
+ gifWriter.setOutput(outputStream);
+ gifWriter.prepareWriteSequence(null);
+ }
+
+ public void writeToSequence(RenderedImage img) throws IOException
+ {
+ gifWriter.writeToSequence(new IIOImage(img, null, imageMetaData), imageWriteParam);
+ }
+
+ /**
+ * Close this GifSequenceWriter object. This does not close the underlying
+ * stream, just finishes off the GIF.
+ */
+ public void close() throws IOException
+ {
+ gifWriter.endWriteSequence();
+ }
+
+ /**
+ * Returns the first available GIF ImageWriter using
+ * ImageIO.getImageWritersBySuffix("gif").
+ *
+ * @return a GIF ImageWriter object
+ * @throws IIOException if no GIF image writers are returned
+ */
+ private static ImageWriter getWriter() throws IIOException
+ {
+ Iterator iter = ImageIO.getImageWritersBySuffix("gif");
+
+ if(!iter.hasNext())
+ throw new IIOException("No GIF Image Writers Exist");
+ else
+ return iter.next();
+ }
+
+ /**
+ * Returns an existing child node, or creates and returns a new child node (if
+ * the requested node does not exist).
+ *
+ * @param rootNode the IIOMetadataNode to search for the child node.
+ * @param nodeName the name of the child node.
+ *
+ * @return the child node, if found or a new node created with the given name.
+ */
+ private static IIOMetadataNode getNode(IIOMetadataNode rootNode, String nodeName)
+ {
+ int nodeCount = rootNode.getLength();
+ for (int i = 0; i < nodeCount; i++)
+ {
+ if (rootNode.item(i).getNodeName().compareToIgnoreCase(nodeName) == 0)
+ return((IIOMetadataNode) rootNode.item(i));
+ }
+
+ IIOMetadataNode node = new IIOMetadataNode(nodeName);
+ rootNode.appendChild(node);
+
+ return(node);
+ }
+ }
+}
diff --git a/src/commands/rpg/RPGCommandBattleFight.java b/src/commands/rpg/RPGCommandBattleFight.java
new file mode 100644
index 0000000..6efcbb7
--- /dev/null
+++ b/src/commands/rpg/RPGCommandBattleFight.java
@@ -0,0 +1,38 @@
+package commands.rpg;
+
+import core.rpg.RPGCommand;
+import core.rpg.RPGInput;
+import core.rpg.RPGState;
+
+public class RPGCommandBattleFight extends RPGCommand
+{
+
+ @Override
+ public String OnRun(RPGState state, RPGInput input)
+ {
+ if(state.battleContext == null)
+ return "```There's nothing to fight!```";
+
+ String out = null;
+ String reward = null;
+
+ out = "No one ever trained you to fight, so you do a silly dance and it weirds your opponent out and they leave. (i need to implement this still)";
+ reward = "+150xp";
+
+ state.battleContext = null;
+ state.player.ApplyEXP(150);
+
+ if(out != null && reward != null)
+ {
+ out += "\n";
+ out += "\n";
+ out += "[" + reward + "]";
+ }
+
+ if(out != null)
+ out = "```\n" + out + "```";
+
+ return out;
+ }
+
+}
diff --git a/src/commands/rpg/RPGCommandBattleRun.java b/src/commands/rpg/RPGCommandBattleRun.java
new file mode 100644
index 0000000..b01a593
--- /dev/null
+++ b/src/commands/rpg/RPGCommandBattleRun.java
@@ -0,0 +1,36 @@
+package commands.rpg;
+
+import core.rpg.RPGCommand;
+import core.rpg.RPGInput;
+import core.rpg.RPGState;
+
+public class RPGCommandBattleRun extends RPGCommand
+{
+ @Override
+ public String OnRun(RPGState state, RPGInput input)
+ {
+ if(state.battleContext == null)
+ return "```There's nothing to run from!```";
+
+ long lostGold = (long)(state.player.GetGold() * .1);
+ String out = null;
+
+ state.player.SpendGold(lostGold);
+ state.battleContext = null;
+
+ out = "You duck behind a tree and manage to escape from encounter just barely!";
+ if(lostGold > 0)
+ {
+ out += " As you make your final move to escape, you accidentally drop some of your gold behind. You don't dare try to go back and pick it up.";
+ out += "\n";
+ out += "\n";
+ out += "[-" + lostGold + "gp]";
+ }
+
+ if(out != null)
+ out = "```\n" + out + "```";
+
+ return out;
+ }
+
+}
diff --git a/src/commands/rpg/RPGCommandExplore.java b/src/commands/rpg/RPGCommandExplore.java
new file mode 100644
index 0000000..55e38f9
--- /dev/null
+++ b/src/commands/rpg/RPGCommandExplore.java
@@ -0,0 +1,97 @@
+package commands.rpg;
+
+import java.util.Random;
+
+import core.rpg.RPGBattleContext;
+import core.rpg.RPGCommand;
+import core.rpg.RPGInput;
+import core.rpg.RPGState;
+
+public class RPGCommandExplore extends RPGCommand
+{
+ private class Chance
+ {
+ double rand;
+ float lastSection;
+
+ public Chance(float percentChanceTotal)
+ {
+ rand = new Random().nextDouble() * percentChanceTotal;
+ lastSection = 0;
+ }
+
+ public boolean Next(float amount)
+ {
+ lastSection += amount;
+ return rand < lastSection;
+ }
+ }
+
+ @Override
+ public String OnRun(RPGState state, RPGInput input)
+ {
+ Chance chance = new Chance(100);
+
+ String out = null;
+ String reward = null;
+
+ if(state.battleContext != null)
+ return "```You can't explore right now, you're in a fight! Try either 'fight' or 'run'!```";
+
+ if(chance.Next(20))
+ {
+ out = "A creature jumps out of a bush at you!";
+ reward = "Prepare to fight!";
+ state.battleContext = new RPGBattleContext(state);
+ }
+ else if(chance.Next(20))
+ {
+ final int gp = 1;
+
+ out = "As you take a stroll, you spy a small shiny glint from a bush and decide to investigate! Looks like it's your lucky day!";
+ reward = "+" + gp + "gp";
+ state.player.GiveGold(gp);
+ }
+ else if(chance.Next(20))
+ {
+ final int healing = 1;
+ final int xp = 25;
+
+ out = "You head out on a lovely stroll down a familiar path - the sun is out and the birds are chirping! Nothing much comes of it, but you feel refreshed.";
+ reward = "+" + xp + "xp, +" + healing + "hp";
+ state.player.ApplyHealing(healing);
+ state.player.ApplyEXP(xp);
+ }
+ else if(chance.Next(20))
+ {
+ final int xp = 75;
+ final int damage = 1;
+
+ out = "Today's the day you head out on a new path. You find a lot of little nicknacks and trinkets on the trail, but leave them be. The trail gets really steep, the rocks jagged, but you keep going. Eventually, you make it out to the other side into a small but pleasant town, and collapse on a bench to catch your breath.";
+ reward = "+" + xp + "xp, -" + damage + "hp";
+ state.player.ApplyEXP(xp);
+ state.player.ApplyDamage(damage);
+ }
+ else
+ {
+ final int hp = 2;
+
+ out = "You step outside but it starts to rain. You decide not to do much today, and hang about the inn and the tavern.";
+ reward = "+" + hp + "hp";
+ state.player.ApplyHealing(hp);
+ }
+
+ if(out != null && reward != null)
+ {
+ out += "\n";
+ out += "\n";
+ out += "[" + reward + "]";
+ }
+
+ if(out != null)
+ out = "```\n" + out + "```";
+
+ return out;
+ }
+
+}
diff --git a/src/commands/rpg/RPGCommandInfo.java b/src/commands/rpg/RPGCommandInfo.java
new file mode 100644
index 0000000..b668757
--- /dev/null
+++ b/src/commands/rpg/RPGCommandInfo.java
@@ -0,0 +1,92 @@
+package commands.rpg;
+
+import core.rpg.RPGArmor;
+import core.rpg.RPGCommand;
+import core.rpg.RPGInput;
+import core.rpg.RPGState;
+import core.rpg.RPGWeapon;
+
+public class RPGCommandInfo extends RPGCommand
+{
+
+ @Override
+ public String OnRun(RPGState state, RPGInput input)
+ {
+ String out = null;
+ switch(input.value.trim().toLowerCase())
+ {
+ case "weapon":
+ case "weapo":
+ case "weap":
+ case "wea":
+ case "we":
+ case "w":
+ case "wepon":
+ case "wepo":
+ case "wep":
+ case "hand":
+ case "att":
+ case "attack":
+ out = WeaponStats(state.player.GetWeapon());
+ break;
+
+ case "armour":
+ case "armou":
+ case "armor":
+ case "armr":
+ case "armo":
+ case "arm":
+ case "ar":
+ case "a":
+ case "def":
+ case "defense":
+ case "body":
+ case "dress":
+ case "wear":
+ case "outfit":
+ out = ArmorStats(state.player.GetArmor());
+ break;
+ }
+
+ if(out != null)
+ out = "```\n" + out + "```";
+
+ return out;
+ }
+
+ private String WeaponStats(RPGWeapon weapon)
+ {
+ if(weapon == null)
+ return null;
+
+ String out = "";
+ out += "[" + weapon.GetName() + "]";
+ out += "\n";
+ out += " att: " + weapon.GetAttack();
+ out += "\n";
+ out += "value: " + weapon.GetValue() + "gp";
+ out += "\n";
+ out += "\n";
+ out += weapon.GetDescription();
+
+ return out;
+ }
+
+ private String ArmorStats(RPGArmor armor)
+ {
+ if(armor == null)
+ return null;
+
+ String out = "";
+ out += "[" + armor.GetName() + "]";
+ out += "\n";
+ out += " def: " + armor.GetDefense();
+ out += "\n";
+ out += "value: " + armor.GetValue() + "gp";
+ out += "\n";
+ out += "\n";
+ out += armor.GetDescription();
+
+ return out;
+ }
+}
diff --git a/src/commands/rpg/RPGCommandStats.java b/src/commands/rpg/RPGCommandStats.java
new file mode 100644
index 0000000..221d038
--- /dev/null
+++ b/src/commands/rpg/RPGCommandStats.java
@@ -0,0 +1,61 @@
+package commands.rpg;
+
+import core.rpg.RPGArmor;
+import core.rpg.RPGCommand;
+import core.rpg.RPGExpTable;
+import core.rpg.RPGInput;
+import core.rpg.RPGPlayer;
+import core.rpg.RPGState;
+import core.rpg.RPGWeapon;
+
+public class RPGCommandStats extends RPGCommand
+{
+ @Override
+ public String OnRun(RPGState state, RPGInput input)
+ {
+ RPGPlayer player = state.player;
+ long exp = player.GetEXP();
+ long level = RPGExpTable.LevelFromEXP(player.GetEXP());
+ long ceil = RPGExpTable.EXPCeil(level);
+
+ String indent = "";
+ String linebreak = "\n";
+
+ String out = "";
+ out += indent + "[" + player.GetName() + ", lv. " + level + "]";
+ out += indent + linebreak;
+ out += indent + "_______________";
+ out += indent + linebreak;
+
+ out += indent + " EXP: " + exp;
+ out += indent + " (until next: " + (ceil - exp) + ")";
+ out += indent + linebreak;
+
+ out += indent + " Gold: " + player.GetGold();
+ out += indent + linebreak;
+
+ out += indent + "Health: " + player.GetHealthCurrent() + "/" + player.GetHealthMax();
+ out += indent + linebreak;
+
+ RPGWeapon weapon = player.GetWeapon();
+ out += indent + "_____";
+ out += indent + linebreak;
+ out += indent + "Weapon: " + weapon.GetName();
+ out += indent + linebreak;
+ out += indent + " att: " + weapon.GetAttack();
+ out += indent + linebreak;
+
+ RPGArmor armor = player.GetArmor();
+ out += indent + "_____";
+ out += indent + linebreak;
+ out += indent + "Armor: " + armor.GetName();
+ out += indent + linebreak;
+ out += indent + " def: " + armor.GetDefense() ;
+ out += indent + linebreak;
+
+ out += indent + linebreak;
+ out += "Use 'info armor' / 'info weapon' for details!";
+
+ return "```\n" + out + "```";
+ }
+}
diff --git a/src/core/Command.java b/src/core/Command.java
new file mode 100644
index 0000000..e252f9c
--- /dev/null
+++ b/src/core/Command.java
@@ -0,0 +1,91 @@
+package core;
+
+import java.util.ArrayList;
+
+import dataStructures.KittyChannel;
+import dataStructures.KittyGuild;
+import dataStructures.KittyRating;
+import dataStructures.KittyRole;
+import dataStructures.KittyUser;
+import dataStructures.Response;
+import dataStructures.UserInput;
+import utils.GlobalLog;
+import utils.LogFilter;
+
+// One note about string[] ... We can't change whitelist
+// on the command without re-registering it.
+public abstract class Command
+{
+ public ArrayList registeredNames;
+ private KittyRole roleLevel;
+ private KittyRating contentRating;
+
+ public Command(KittyRole roleLevel, KittyRating contentRating)
+ {
+ this.registeredNames = new ArrayList();
+ this.roleLevel = roleLevel;
+ this.contentRating = contentRating;
+ }
+
+ private void Reject(KittyUser user, String reason)
+ {
+ GlobalLog.Warn(LogFilter.Command, this.getClass().getSimpleName() + " from " + user.name + " rejected due to command's " + reason);
+ }
+
+ // Determine if we're exclusive enough for this command and
+ // if the command is permitted by the guild we're in
+ private boolean CanCall(KittyGuild guild, KittyChannel channel, KittyUser user)
+ {
+ if(guild.contentRating.getValue() < contentRating.getValue())
+ {
+ Reject(user, "content rating");
+ return false;
+ }
+
+ //TODO(wisp, rin): ADD CHANNEL CHECK HERE
+
+ if(user.GetRole().getValue() >= roleLevel.getValue())
+ {
+ return true;
+ }
+
+ Reject(user, "permissions");
+ return false;
+ }
+
+ // Called by the Command manager - this will run the command
+ // if the issuing user has the permission to do so!
+ protected final void Invoke(KittyGuild guild, KittyChannel channel, KittyUser user, UserInput input, Response res)
+ {
+ if(!CanCall(guild, channel, user))
+ return;
+
+ OnRun(guild, channel, user, input, res);
+ }
+
+ public ArrayList RegisteredNames()
+ {
+ return registeredNames;
+ }
+
+ public KittyRating Rating()
+ {
+ return contentRating;
+ }
+
+ public KittyRole RequiredRole()
+ {
+ return roleLevel;
+ }
+
+ // OVERRIDE ME! (This is not required but advised!)
+ // Returns if the command succeeded or not.
+ public String HelpText()
+ {
+ return "No help text has been added yet for " + this.getClass().getSimpleName() + "!";
+ }
+
+ // OVERRIDE ME! (This is required)
+ // Returns if the command succeeded or not.
+ public abstract void OnRun(KittyGuild guild, KittyChannel channel, KittyUser user, UserInput input, Response res);
+}
diff --git a/src/core/CommandManager.java b/src/core/CommandManager.java
new file mode 100644
index 0000000..e3e8970
--- /dev/null
+++ b/src/core/CommandManager.java
@@ -0,0 +1,138 @@
+package core;
+
+import java.util.*;
+import java.util.Map.Entry;
+
+import dataStructures.KittyChannel;
+import dataStructures.KittyGuild;
+import dataStructures.KittyUser;
+import dataStructures.Response;
+import dataStructures.UserInput;
+import utils.GlobalLog;
+import utils.LogFilter;
+
+public class CommandManager
+{
+ // Variables
+ private HashMap commands;
+ private ArrayList threadAccumulator;
+ private long invokeCount;
+
+
+ // Default Constructor
+ public CommandManager()
+ {
+ commands = new HashMap();
+ threadAccumulator = new ArrayList();
+ invokeCount = 0;
+ }
+
+ // Allows the command manager to keep track of a command.
+ public void Register(String key, Command command)
+ {
+ if(key == null)
+ return;
+
+ key = key.toLowerCase();
+ command.registeredNames.add(key);
+
+ Command old = commands.put(key, command);
+
+ if(old != null)
+ {
+ GlobalLog.Warn(LogFilter.Core, "Writing over a command with the key " + key);
+ return;
+ }
+
+ GlobalLog.Log(LogFilter.Core, "Command registered under key " + key);
+ }
+
+ // Registers a command under multiple names!
+ public void Register(String[] keys, Command command)
+ {
+ for(int i = 0; i < keys.length; ++i)
+ Register(keys[i], command);
+ }
+
+ // Calls the command but on a whole new thread!
+ public void InvokeOnNewThread(KittyGuild guild, KittyChannel channel, KittyUser user, UserInput input, Response responseContext)
+ {
+ // This is here to prevent spinning up a thread if this wasn't even a command.
+ if(input == null || !input.IsValid())
+ return;
+
+ // Spin up a thread and begin it. The thread carries the info needed to invoke the command.
+ CommandThread newThread = new CommandThread(this, input, guild, channel, user, responseContext);
+ threadAccumulator.add(newThread);
+ newThread.start();
+ }
+
+ // Calls the command specified with the key, providing user information arguments, etc.
+ public void Invoke(KittyGuild guild, KittyChannel channel, KittyUser user, UserInput input, Response responseContext)
+ {
+ if(input == null || !input.IsValid())
+ return;
+
+ Command command = commands.get(input.key);
+
+ if(command != null)
+ {
+ ++invokeCount;
+ command.Invoke(guild, channel, user, input, responseContext);
+ }
+ else
+ {
+ GlobalLog.Warn(LogFilter.Command, "User " + user.name + " tried to invoke command that doesn't exist: " + input.key);
+ }
+ }
+
+ // Looks up command by name. If it exists, dumps help text, otherwise returns null.
+ public String GetCommandHelpText(String lookup)
+ {
+ Command command = commands.get(lookup);
+
+ if(command != null)
+ return command.HelpText();
+
+ return null;
+ }
+
+ // Returns the number of commands sent so far during this program run
+ public long GetInvokeCount()
+ {
+ return invokeCount;
+ }
+
+ // Returns all commands by name
+ public ArrayList GetAllRegisteredCommands()
+ {
+ ArrayList cmds = new ArrayList();
+ for(Entry entry : commands.entrySet())
+ cmds.add(entry.getValue());
+
+ return cmds;
+ }
+
+ // Info about threads running packaged into an object
+ public class ThreadData
+ {
+ public HashMap states = new HashMap();
+ }
+ public ThreadData DumpThreadData()
+ {
+ ThreadData data = new ThreadData();
+
+ for(int i = 0; i < threadAccumulator.size(); ++i)
+ {
+ Thread.State state = threadAccumulator.get(i).getState();
+ Integer num = data.states.get(state);
+
+ if(num == null)
+ num = 0;
+
+ data.states.put(state, num + 1);
+ }
+
+ return data;
+ }
+}
diff --git a/src/core/CommandThread.java b/src/core/CommandThread.java
new file mode 100644
index 0000000..eb2d848
--- /dev/null
+++ b/src/core/CommandThread.java
@@ -0,0 +1,56 @@
+package core;
+
+import dataStructures.KittyChannel;
+import dataStructures.KittyGuild;
+import dataStructures.KittyUser;
+import dataStructures.Response;
+import dataStructures.UserInput;
+
+public class CommandThread extends Thread
+{
+ // Pile of variables
+ // TODO(wisp): Consider packaging this up into a thread arguments object potentially...?
+ CommandManager manager;
+ UserInput input;
+ KittyGuild guild;
+ KittyChannel channel;
+ KittyUser user;
+ Response response;
+
+ // Constructor
+ public CommandThread(CommandManager manager, UserInput input, KittyGuild guild, KittyChannel channel, KittyUser user, Response response)
+ {
+ this.manager = manager;
+ this.input = input;
+ this.guild = guild;
+ this.channel = channel;
+ this.user = user;
+ this.response = response;
+ }
+
+ // Method called when spawned as a thread
+ @Override
+ public void run()
+ {
+ InvokeCommand();
+ }
+
+ private void InvokeCommand()
+ {
+ manager.Invoke(guild, channel, user, input, response);
+ }
+
+ // Handles the try catch requirement java has for sleeping
+ @SuppressWarnings("unused")
+ private void ThreadSleep(int ms)
+ {
+ try
+ {
+ Thread.sleep(ms);
+ }
+ catch (InterruptedException e)
+ {
+ e.printStackTrace();
+ }
+ }
+}
diff --git a/src/core/DatabaseDriver.java b/src/core/DatabaseDriver.java
new file mode 100644
index 0000000..3d98d33
--- /dev/null
+++ b/src/core/DatabaseDriver.java
@@ -0,0 +1,132 @@
+package core;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+import network.JDBCDriver;
+import network.JDBCDriverSQLite;
+import utils.GlobalLog;
+import utils.LogFilter;
+
+// Java DataBase Connection Driver - the generic version.
+// This is where we put everything and swap out the moving parts,
+// ie MySQL, PostgreSQL, SQLite, etc...
+//
+// TODO(wisp): Right now this can be messed up with SQL injection stuff if users
+// are allowed to directly touch data. Leaving it like this temporarily.
+public class DatabaseDriver
+{
+ private JDBCDriver driver;
+
+ // Note: Changing these values can mess up the database...
+ private final String globalTableName = "kitty_globals";
+ private final String globalKeyName = "GlobalKey";
+ private final String globalValueName = "GlobalValue";
+
+ public DatabaseDriver()
+ {
+ driver = null;
+ }
+
+ public boolean Connect()
+ {
+ driver = new JDBCDriverSQLite();
+ driver.Connect();
+
+ // Require a global table if it doesn't exist already
+ driver.ExecuteStatement("CREATE TABLE IF NOT EXISTS " + globalTableName + " (" + globalKeyName + " text PRIMARY KEY, " + globalValueName + " text);");
+
+ return true;
+ }
+
+ // The key will be created if it doesn't exist and the value specified will be stored.
+ public void CreateSetKey(String key, String value)
+ {
+ GlobalLog.Log(LogFilter.Database, "CreateSetKey: Key-" + key + " value-" + value);
+
+ if(HasKey(key))
+ {
+ UpdateKey(key, value);
+ }
+ else
+ {
+ CreateKey(key, value);
+ }
+ }
+
+ // the key will be created if it doesn't exist, and the default value returned.
+ public String CreateGetKey(String key)
+ {
+ GlobalLog.Log(LogFilter.Database, "CreateGeyKey: key-" + key);
+
+ if(HasKey(key))
+ {
+ return GetKey(key);
+ }
+ else
+ {
+ String newValue = "";
+ CreateKey(key, newValue);
+ return newValue;
+ }
+ }
+
+ private void UpdateKey(String key, String value)
+ {
+ //GlobalLog.Log(LogFilter.Database, "Update key " + key);
+ String command = "UPDATE " + globalTableName + " SET " + globalValueName + " = '" + value + "' WHERE " + globalKeyName + "= '" + key + "';";
+ //GlobalLog.Log(LogFilter.Database, "Composed command: " + command);
+ driver.ExecuteStatement(command);
+ }
+
+ private boolean HasKey(String key)
+ {
+ //GlobalLog.Log(LogFilter.Database, "Has key " + key);
+ String command = "SELECT COUNT(1) as count FROM " + globalTableName + " WHERE " + globalKeyName + " = '" + key + "';";
+ //GlobalLog.Log(LogFilter.Database, "Composed command: " + command);
+ ResultSet set = driver.ExecuteReturningStatement(command);
+ String out = ResultAsString(set, "count");
+ return out.charAt(0) == '1';
+ }
+
+ private String GetKey(String key)
+ {
+ //GlobalLog.Log(LogFilter.Database, "Getting key " + key);
+ String command = "SELECT " + globalValueName + " as searchedKey FROM " + globalTableName +" WHERE " + globalKeyName + "= \'" + key + "\';";
+ //GlobalLog.Log(LogFilter.Database, "Composed command: " + command);
+ ResultSet set = driver.ExecuteReturningStatement(command);
+ return ResultAsString(set, "searchedKey");
+ }
+
+ private void CreateKey(String key, String value)
+ {
+ //GlobalLog.Log(LogFilter.Database, "Creating Key " + key);
+ String command = "INSERT INTO " + globalTableName + " (GlobalKey, GlobalValue) VALUES ('" + key + "', '" + value + "');";
+ //GlobalLog.Log(LogFilter.Database, "Composed command: " + command);
+ driver.ExecuteStatement(command);
+ }
+
+ private String ResultAsString(ResultSet rs, String key)
+ {
+ if(rs == null)
+ return "";
+
+ try
+ {
+ boolean hasKey = rs.next();
+ if(hasKey)
+ {
+ String val = rs.getString(key);
+ return val;
+ }
+ else
+ return null;
+
+ }
+ catch (SQLException e)
+ {
+ GlobalLog.Error(LogFilter.Database, e.toString());
+ return null;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/core/DatabaseManager.java b/src/core/DatabaseManager.java
new file mode 100644
index 0000000..c3649d9
--- /dev/null
+++ b/src/core/DatabaseManager.java
@@ -0,0 +1,67 @@
+package core;
+
+import java.util.ArrayList;
+
+import utils.GlobalLog;
+import utils.LogFilter;
+
+public class DatabaseManager
+{
+ // Singleton accessor
+ public static DatabaseManager instance = null;
+
+ // Private internal variables
+ private ArrayList trackedObjects;
+ private DatabaseDriver driver;
+
+ public DatabaseManager()
+ {
+ if(instance == null)
+ {
+ instance = this;
+ }
+ else
+ {
+ GlobalLog.Error(LogFilter.Database, "Attempted to register a second DataBase manager!");
+ return;
+ }
+
+ trackedObjects = new ArrayList();
+ driver = new DatabaseDriver();
+ driver.Connect();
+ }
+
+ // Thumbs through registered objects and syncs them with the database.
+ // TODO(wisp) Right now this just syncs on the main thread, but we will
+ // want to have upkeep commands queue up for a dedicated database thread
+ // in the future to offload the wait times.
+ public void Upkeep()
+ {
+ for(int i = 0 ; i < trackedObjects.size(); ++i)
+ {
+ DatabaseTrackedObject dto = trackedObjects.get(i);
+
+ if(dto.IsDirty())
+ {
+ SetRemoteValue(dto.identifier, dto.Serialize());
+ dto.Resolve();
+ }
+ }
+ }
+
+ public void Register(DatabaseTrackedObject tracked)
+ {
+ trackedObjects.add(tracked);
+ tracked.DeSerialzie(GetRemoteValue(tracked.identifier));
+ }
+
+ public String GetRemoteValue(String key)
+ {
+ return driver.CreateGetKey(key);
+ }
+
+ public void SetRemoteValue(String key, String value)
+ {
+ driver.CreateSetKey(key, value);
+ }
+}
diff --git a/src/core/DatabaseTrackedObject.java b/src/core/DatabaseTrackedObject.java
new file mode 100644
index 0000000..9d5dfa7
--- /dev/null
+++ b/src/core/DatabaseTrackedObject.java
@@ -0,0 +1,35 @@
+package core;
+
+public abstract class DatabaseTrackedObject
+{
+ private boolean isDirty;
+ public final String identifier;
+
+ public DatabaseTrackedObject(String identifier)
+ {
+ this.isDirty = false;
+ this.identifier = identifier;
+ }
+
+ public final boolean IsDirty()
+ {
+ return isDirty;
+ }
+
+ public final void MarkDirty()
+ {
+ isDirty = true;
+ }
+
+ public final void Resolve()
+ {
+ isDirty = false;
+ }
+
+ // TODO(wisp): Not the best way to handle this, potentially consider
+ // using an object factory that looks up how to serialize and
+ // deserialize based on the type of the thing being tracked.
+ // For now, this is fine.
+ public abstract String Serialize();
+ public abstract void DeSerialzie(String string);
+}
diff --git a/src/core/GenericImage.java b/src/core/GenericImage.java
new file mode 100644
index 0000000..ecfaa58
--- /dev/null
+++ b/src/core/GenericImage.java
@@ -0,0 +1,39 @@
+package core;
+
+public class GenericImage
+{
+ private String artist;
+ private String postURL;
+ private String imageURL;
+
+ public GenericImage(String artist, String postURL, String imageURL)
+ {
+ this.artist = artist;
+ this.postURL = postURL;
+ this.imageURL = imageURL;
+ }
+
+ public void editArtist(String artist)
+ {
+ this.artist = artist;
+ }
+
+ public void editPostURL(String postURL)
+ {
+ this.postURL = postURL;
+ }
+
+ public void editImageURL(String imageURL)
+ {
+ this.imageURL = imageURL;
+ }
+
+ public String toString()
+ {
+ if(imageURL.isEmpty())
+ {
+ return "I couldn't find anything! Please try again!";
+ }
+ return "Artist: " + artist + "\n<" + postURL.trim() + ">\n" + imageURL;
+ }
+}
diff --git a/src/core/ObjectBuilderFactory.java b/src/core/ObjectBuilderFactory.java
new file mode 100644
index 0000000..bf8ad63
--- /dev/null
+++ b/src/core/ObjectBuilderFactory.java
@@ -0,0 +1,380 @@
+package core;
+
+import java.util.HashMap;
+import java.util.concurrent.Semaphore;
+
+import commands.*;
+import dataStructures.*;
+import net.dv8tion.jda.core.entities.Member;
+import net.dv8tion.jda.core.events.message.guild.GuildMessageReceivedEvent;
+import utils.GlobalLog;
+import utils.LogFilter;
+
+// NOTE(wisp): Isolated factory to assist with storage and caching if needed.
+// This also minimizes the number of places JDA interacts with our codebase.
+// As it stands, if the object name begins with Kitty, it's constructed here.
+// TODO: Make all methods ID based instead of event based
+public class ObjectBuilderFactory
+{
+ // Key: guild string id, Value: guild information
+ private static HashMap guildCache;
+
+ // Key: channel string id, Value: channel information
+ private static HashMap channelCache;
+
+ // Key: guild string id + user string id, Value: user information
+ private static HashMap userCache;
+
+ // For tracking and managing object sync
+ private static DatabaseManager database;
+
+ // Stats tracking and whatnot... for setting stats too internally potentially
+ private static Stats stats;
+
+ //RPManger for tracking RP system
+ private static RPManager rpManager;
+
+ // NOTE(wisp): This is designed to initialize at the last possible second.
+ // The idea behind this is that there may be other things that may need
+ // time to initialize before the factory can use them, and this guarantees
+ // they get the time they need.
+ private static boolean hasInitialized;
+ private static Semaphore initMutex = new Semaphore(1);
+ private static void LazyInit()
+ {
+ if(hasInitialized)
+ return;
+
+ try
+ {
+ initMutex.acquire();
+ try
+ {
+ // NOTE(wisp): Actually put all the init code here.
+ // In the future, this is where we would read from something external.
+ guildCache = new HashMap();
+ userCache = new HashMap();
+ channelCache = new HashMap();
+ database = null;
+ stats = null;
+ }
+ finally
+ {
+ initMutex.release();
+ }
+ }
+ catch(InterruptedException ie)
+ {
+ GlobalLog.Error(LogFilter.Core, "Issue during object builder lazy initialization."
+ + " The factory was not initialized, "
+ + "and kitty will not be able to continue functionally.");
+ }
+
+ hasInitialized = true;
+ }
+
+ // Explicitly locks: guildCache
+ public static KittyGuild ExtractGuild(GuildMessageReceivedEvent event)
+ {
+ LazyInit();
+
+ // Look up the guild. This process can only happen in a single-threaded way
+ // because of the nature of the cache. We wait until the last second to
+ // look up the guild.
+ String uid = event.getGuild().getId();
+
+ // ince we're lazily initialized, we can synchronize w/ the
+ // guildCache object now instead of having to use a mutex.
+ KittyGuild guild = null;
+ synchronized (guildCache)
+ {
+ KittyGuild cachedGuild = guildCache.get(uid);
+ if(cachedGuild != null)
+ {
+ guild = cachedGuild;
+ }
+ else
+ {
+ // Construct a new guild with defaults
+ guild = new KittyGuild(uid);
+ DatabaseManager.instance.Register(guild);
+ guildCache.put(uid, guild);
+ }
+ }
+
+ return guild;
+ }
+
+ // Explicitly locks: guildCache
+ public static KittyRole ExtractRole(GuildMessageReceivedEvent event)
+ {
+ LazyInit();
+
+ // Looks up the user role. If none is found we check to see if they own
+ // the guild if not, they're assumed to be
+ // allowed to use the bot at a general level.
+ KittyRole role = KittyRole.General;
+
+ if(event.getAuthor().getId() == event.getGuild().getOwner().getUser().getId())
+ {
+ role = KittyRole.Admin;
+ }
+
+ String uid = event.getGuild().getId() + event.getAuthor().getId();
+ synchronized(userCache)
+ {
+ KittyUser cachedUser = userCache.get(uid);
+ if(cachedUser != null)
+ role = cachedUser.GetRole();
+ }
+
+ return role;
+ }
+
+ // Explicitly locks: guildCache
+ // Extracts the content rating information it can from the provided event.
+ public static KittyRating ExtractContentRating(GuildMessageReceivedEvent event)
+ {
+ LazyInit();
+
+ // Look up content rating of the guild, returns a safe content rating.
+ KittyRating contentRating = KittyRating.Safe;
+ String uid = event.getGuild().getId();
+ synchronized(guildCache)
+ {
+ KittyGuild cachedGuild = guildCache.get(uid);
+ if(cachedGuild != null)
+ contentRating = cachedGuild.contentRating;
+ }
+
+ return contentRating;
+ }
+
+ // Implicitly locks guild cache by calling ExtractGuild
+ public static KittyChannel ExtractChannel(GuildMessageReceivedEvent event)
+ {
+ LazyInit();
+
+ String channelID = event.getChannel().getId();
+ String guildID = event.getGuild().getId();
+ KittyChannel channel = null;
+
+ synchronized(channelCache)
+ {
+ KittyChannel cachedChannel = channelCache.get(channelID);
+
+ if(cachedChannel != null)
+ {
+ channel = cachedChannel;
+ }
+ else
+ {
+ KittyGuild cachedGuild = guildCache.get(guildID);
+ channel = new KittyChannel(channelID, cachedGuild);
+ channelCache.put(channelID, channel);
+ }
+ }
+
+ return channel;
+ }
+
+ // Implicitly locks guild cache by calling ExtractRole and ExtractGuild
+ public static KittyUser ExtractUser(GuildMessageReceivedEvent event)
+ {
+ LazyInit();
+
+ String uid = event.getGuild().getId() + event.getAuthor().getId();
+ KittyUser user = null;
+ synchronized(userCache)
+ {
+ KittyUser cachedUser = userCache.get(uid);
+ if(cachedUser != null)
+ {
+ updateUser(cachedUser, event.getMember());
+ user = cachedUser;
+ }
+ else
+ {
+ KittyRole role = ExtractRole(event);
+ KittyGuild guild = ExtractGuild(event);
+
+ String name;
+ if(event.getMember().getNickname() == null)
+ name = event.getAuthor().getName();
+ else
+ name = event.getMember().getNickname();
+
+ String discordID = event.getMember().getUser().getId();
+ String avatarID = event.getAuthor().getAvatarUrl();
+ user = new KittyUser(name, guild, role, uid, avatarID, discordID);
+ DatabaseManager.instance.Register(user);
+ userCache.put(uid, user);
+ }
+ }
+
+ if(event.getMessage().getMentionedMembers().isEmpty())
+ return user;
+
+ Member mentioned;
+ for(int i = 0; i < event.getMessage().getMentionedMembers().size(); i++)
+ {
+ mentioned = event.getMessage().getMentionedMembers().get(i);
+ if(mentioned.getNickname() != null)
+ ExtractUserByJDAUser(event.getGuild().getId(), mentioned.getNickname(),
+ mentioned.getUser().getId(), mentioned.getUser().getAvatarUrl(), mentioned.getUser().getId());
+ else
+ ExtractUserByJDAUser(event.getGuild().getId(), mentioned.getUser().getName(),
+ mentioned.getUser().getId(), mentioned.getUser().getAvatarUrl(), mentioned.getUser().getId());
+ }
+
+ return user;
+ }
+
+ public static KittyUser ExtractUserByJDAUser(String guildID, String name, String userID, String avatarID, String discordID)
+ {
+ LazyInit();
+
+ String uid = guildID + userID;
+ KittyUser user = null;
+ synchronized(userCache)
+ {
+ KittyUser cachedUser = userCache.get(uid);
+ if(cachedUser != null)
+ {
+ if(name != null)
+ cachedUser.name = name;
+ cachedUser.avatarID = avatarID;
+ user = cachedUser;
+ }
+ else
+ {
+ KittyRole role = KittyRole.General;
+ KittyGuild guild = guildCache.get(guildID);
+ user = new KittyUser(name, guild, role, uid, avatarID, discordID);
+ DatabaseManager.instance.Register(user);
+ userCache.put(uid, user);
+ }
+ }
+
+ return user;
+ }
+
+ public static KittyUser getCachedUser(String guildID, String userID)
+ {
+ String uid = guildID + userID;
+ KittyUser user = null;
+ synchronized(userCache)
+ {
+ user = userCache.get(uid);
+ }
+ return user;
+ }
+
+ public static void updateUser(KittyUser user, Member member)
+ {
+ if(member.getNickname() == null)
+ user.name = member.getUser().getName();
+ else
+ user.name = member.getNickname();
+
+ user.avatarID = member.getUser().getAvatarUrl();
+ }
+
+ // Default construction of the command manager.
+ // TODO(wisp): We want to be able to keep all this data
+ // stored off in a file at some point, so we can reflect it onto the
+ // project and build it per-guild. That's for later now tho.
+ public static CommandManager ConstructCommandManager()
+ {
+ LazyInit();
+
+ CommandManager manager = new CommandManager();
+
+ manager.Register("test", new CommandTesting(KittyRole.Dev, KittyRating.Safe));
+ manager.Register("work", new CommandDoWork(KittyRole.Dev, KittyRating.Safe));
+ manager.Register("shutdown", new CommandShutdown(KittyRole.Dev, KittyRating.Safe));
+ manager.Register("stats", new CommandStats(KittyRole.Dev, KittyRating.Safe));
+ manager.Register("invite", new CommandInvite(KittyRole.Dev, KittyRating.Safe));
+ manager.Register("buildHelp", new CommandHelpBuilder(KittyRole.Dev, KittyRating.Safe));
+
+ manager.Register("rating", new CommandRating(KittyRole.Admin, KittyRating.Safe));
+ manager.Register("indicator", new CommandChangeIndicator(KittyRole.Admin, KittyRating.Safe));
+
+ manager.Register("poll", new CommandPollManage(KittyRole.Mod, KittyRating.Safe));
+ manager.Register("givebeans", new CommandGiveBeans(KittyRole.Mod, KittyRating.Safe));
+ manager.Register("rpg", new CommandRPG(KittyRole.Mod, KittyRating.Safe));
+
+ manager.Register(new String[]{"perish", "thenperish"}, new CommandPerish(KittyRole.General, KittyRating.Safe));
+ manager.Register("yeet", new CommandYeet(KittyRole.General, KittyRating.Safe));
+ manager.Register("ping", new CommandPing(KittyRole.General, KittyRating.Safe));
+ manager.Register("boop", new CommandBoop(KittyRole.General, KittyRating.Safe));
+ manager.Register("roll", new CommandRoll(KittyRole.General, KittyRating.Safe));
+ manager.Register("choose", new CommandChoose(KittyRole.General, KittyRating.Safe));
+ manager.Register("help", new CommandHelp(KittyRole.General, KittyRating.Safe));
+ manager.Register(new String[] {"info", "about"}, new CommandInfo(KittyRole.General, KittyRating.Safe));
+ manager.Register("vote", new CommandPollVote(KittyRole.General, KittyRating.Safe));
+ manager.Register("results", new CommandPollResults(KittyRole.General, KittyRating.Safe));
+ manager.Register("showpoll", new CommandPollShow(KittyRole.General, KittyRating.Safe));
+ manager.Register("wolfram", new CommandWolfram(KittyRole.General, KittyRating.Safe));
+ manager.Register(new String[] {"c++", "g++", "cplus",}, new CommandColiru(KittyRole.General, KittyRating.Safe));
+ manager.Register(new String[] {"java", "jdoodle" }, new CommandJDoodle(KittyRole.General, KittyRating.Safe));
+ manager.Register("beans", new CommandBeansShow(KittyRole.General, KittyRating.Safe));
+ manager.Register("role", new CommandRole(KittyRole.General, KittyRating.Safe));
+ manager.Register("bet", new CommandBetBeans(KittyRole.General, KittyRating.Safe));
+ manager.Register("map", new CommandMap(KittyRole.General, KittyRating.Safe));
+ manager.Register("rpstart", new CommandRPStart(KittyRole.General, KittyRating.Safe));
+ manager.Register("rpend", new CommandRPEnd(KittyRole.General, KittyRating.Safe));
+ manager.Register(new String[] {"tony", "stark", "dontfeelgood", "dontfeelsogood"}, new CommandStark(KittyRole.General, KittyRating.Safe));
+ manager.Register("blur", new CommandBlurry(KittyRole.General, KittyRating.Safe));
+
+ return manager;
+ }
+
+ // NOTE(wisp): Default database manager construction. It can be constructed
+ // in different ways, and so we construct it outside of the constructor for
+ // the factory since it doesn't have to be present / can be elsewhere.
+ // Effectively we cache the database here.
+ public static DatabaseManager ConstructDatabaseManager()
+ {
+ LazyInit();
+
+ if(database == null)
+ database = new DatabaseManager();
+
+ return database;
+ }
+
+ public static Stats ConstructStats(CommandManager manager)
+ {
+ LazyInit();
+
+ if(stats == null)
+ stats = new Stats(manager);
+
+ return stats;
+ }
+
+ public static RPManager ConstructRPManager()
+ {
+ LazyInit();
+
+ if(rpManager == null)
+ rpManager = new RPManager();
+
+ return rpManager;
+ }
+
+ public static Integer GetGuildCount()
+ { synchronized(guildCache)
+ {
+ return guildCache.size();
+ }
+ }
+
+ public static Integer GetUserCount()
+ { synchronized(userCache)
+ {
+ return userCache.size();
+ }
+ }
+}
diff --git a/src/core/RPManager.java b/src/core/RPManager.java
new file mode 100644
index 0000000..7209d10
--- /dev/null
+++ b/src/core/RPManager.java
@@ -0,0 +1,83 @@
+package core;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.UnsupportedEncodingException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map.Entry;
+
+import dataStructures.*;
+import net.dv8tion.jda.core.JDA;
+import utils.GlobalLog;
+import utils.LogFilter;
+
+public class RPManager
+{
+ static HashMap logs = new HashMap ();
+ public static RPManager instance = null;
+
+ public RPManager()
+ {
+ if(instance == null)
+ {
+ instance = this;
+ }
+ else
+ {
+ GlobalLog.Error(LogFilter.Core, "Attempted to create a second RP Manager!");
+ return;
+ }
+ }
+
+ public String newRP(KittyChannel channel, ArrayList users)
+ {
+ if(logs.containsKey(Long.parseLong(channel.uniqueID)))
+ return "You can't have 2 RP's running at the same time!";
+ else
+ logs.put(Long.parseLong(channel.uniqueID), new KittyRP(users, channel));
+ return "RP started!";
+ }
+
+ public void addLine(KittyChannel channel, KittyUser user, UserInput input)
+ {
+ if(logs.containsKey(Long.parseLong(channel.uniqueID)) && !input.IsValid())
+ logs.get(Long.parseLong(channel.uniqueID)).addLine(user, input.message);
+ }
+
+ public File endRP(KittyChannel channel, KittyUser user) throws FileNotFoundException, UnsupportedEncodingException
+ {
+ if(logs.containsKey(Long.parseLong(channel.uniqueID)))
+ {
+ return null;
+ }
+ File log = logs.get(Long.parseLong(channel.uniqueID)).endRP(user);
+ if(log != null)
+ logs.remove(Long.parseLong(channel.uniqueID));
+ return log;
+ }
+
+ public static void Upkeep(JDA kitty)
+ {
+ Response res = new Response(null, kitty);
+ String reminder = "";
+ ArrayList users;
+ long currentTime = System.currentTimeMillis();
+ for (Entry entry : logs.entrySet())
+ {
+ if(currentTime > entry.getValue().getTimer() + 1000 * 60 * 30)
+ {
+ reminder = "Don't forget about your RP log!";
+ users = entry.getValue().getUsers();
+ for(int i = 0; i < users.size(); i ++)
+ {
+ reminder += " <@" + users.get(i) + ">";
+ }
+ res.CallToChannel(reminder, entry.getValue().getChannel().uniqueID);
+ reminder = "";
+
+ entry.getValue().resetTimer();
+ }
+ }
+ }
+}
diff --git a/src/core/Stats.java b/src/core/Stats.java
new file mode 100644
index 0000000..82f5f8e
--- /dev/null
+++ b/src/core/Stats.java
@@ -0,0 +1,126 @@
+package core;
+
+import java.lang.management.ManagementFactory;
+import java.lang.management.OperatingSystemMXBean;
+import java.util.ArrayList;
+import java.util.concurrent.TimeUnit;
+import core.CommandManager.ThreadData;
+import utils.GlobalLog;
+import utils.LogFilter;
+
+// NOTE(wisp): This is a class designed to be asked about various kittybot stats
+public class Stats
+{
+ public static String botName = "KittyBot";
+ public static Stats instance = null;
+
+ // Internal
+ private boolean isShuttingDown;
+ private long messagesSeen;
+ private long initTimeMS;
+
+ private CommandManager commandManager;
+ private OperatingSystemMXBean osBean;
+
+ public Stats(CommandManager manager)
+ {
+ if(instance == null)
+ {
+ instance = this;
+ }
+ else
+ {
+ GlobalLog.Error(LogFilter.Core, "Attempted to create a second Stats singleton!");
+ return;
+ }
+
+ isShuttingDown = false;
+ messagesSeen = 0;
+ initTimeMS = System.currentTimeMillis();
+
+ commandManager = manager;
+ osBean = ManagementFactory.getPlatformMXBean(OperatingSystemMXBean.class);
+ }
+
+ public void NoteMessageEvent()
+ {
+ ++messagesSeen;
+ }
+
+ public void IndicateShutdown()
+ {
+ synchronized(instance)
+ {
+ isShuttingDown = true;
+ }
+ }
+
+ public boolean GetIsShuttingDown()
+ {
+ synchronized(instance)
+ {
+ return isShuttingDown;
+ }
+ }
+
+ /////////////////////////////////////////
+ // All the functions to look up stats! //
+ /////////////////////////////////////////
+
+ // Formatted as HH:MM:SS
+ public String GetFormattedUptime()
+ {
+ long dif = System.currentTimeMillis() - initTimeMS;
+
+ return String.format("%02d:%02d:%02d",
+ TimeUnit.MILLISECONDS.toHours(dif),
+ TimeUnit.MILLISECONDS.toMinutes(dif) - TimeUnit.HOURS.toMinutes(TimeUnit.MILLISECONDS.toHours(dif)),
+ TimeUnit.MILLISECONDS.toSeconds(dif) - TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes(dif)));
+ }
+
+ // Get number of commands that kitty has run!
+ public long GetCommandsProcessed()
+ {
+ return commandManager.GetInvokeCount();
+ }
+
+ public long GetMessagesSeen()
+ {
+ return messagesSeen;
+ }
+
+ public double GetSystemCPULoad()
+ {
+ return osBean.getSystemLoadAverage();
+ }
+
+ public long GetCPUAvailable()
+ {
+ return osBean.getAvailableProcessors();
+ }
+
+ public ThreadData GetThreadData()
+ {
+ return commandManager.DumpThreadData();
+ }
+
+ public int GetGuildCount()
+ {
+ return ObjectBuilderFactory.GetGuildCount();
+ }
+
+ public int GetUserCount()
+ {
+ return ObjectBuilderFactory.GetUserCount();
+ }
+
+ public ArrayList GetAllCommands()
+ {
+ return commandManager.GetAllRegisteredCommands();
+ }
+
+ public String GetHelpText(String commandName)
+ {
+ return commandManager.GetCommandHelpText(commandName);
+ }
+}
diff --git a/src/core/rpg/RPGArmor.java b/src/core/rpg/RPGArmor.java
new file mode 100644
index 0000000..62b957d
--- /dev/null
+++ b/src/core/rpg/RPGArmor.java
@@ -0,0 +1,14 @@
+package core.rpg;
+
+public class RPGArmor extends RPGItem
+{
+ int defense;
+
+ public RPGArmor()
+ {
+ super("Tattered Clothes", "The remains of your first sewing project. You did a pretty good job!", 5);
+ defense = 1;
+ }
+
+ public long GetDefense() { return defense; }
+}
diff --git a/src/core/rpg/RPGBattleContext.java b/src/core/rpg/RPGBattleContext.java
new file mode 100644
index 0000000..72c82ca
--- /dev/null
+++ b/src/core/rpg/RPGBattleContext.java
@@ -0,0 +1,11 @@
+package core.rpg;
+
+public class RPGBattleContext
+{
+ public RPGState stateRef;
+
+ public RPGBattleContext(RPGState state)
+ {
+ this.stateRef = state;
+ }
+}
diff --git a/src/core/rpg/RPGCommand.java b/src/core/rpg/RPGCommand.java
new file mode 100644
index 0000000..d22448c
--- /dev/null
+++ b/src/core/rpg/RPGCommand.java
@@ -0,0 +1,9 @@
+package core.rpg;
+
+public abstract class RPGCommand
+{
+ public RPGCommand() { }
+
+ // OVERRIDE ME
+ public abstract String OnRun(RPGState state, RPGInput input);
+}
diff --git a/src/core/rpg/RPGEnemy.java b/src/core/rpg/RPGEnemy.java
new file mode 100644
index 0000000..41a9a52
--- /dev/null
+++ b/src/core/rpg/RPGEnemy.java
@@ -0,0 +1,9 @@
+package core.rpg;
+
+public class RPGEnemy extends RPGUnit
+{
+ RPGWeapon attack;
+ RPGArmor armor;
+
+ long expValue;
+}
diff --git a/src/core/rpg/RPGExpTable.java b/src/core/rpg/RPGExpTable.java
new file mode 100644
index 0000000..5a1f60a
--- /dev/null
+++ b/src/core/rpg/RPGExpTable.java
@@ -0,0 +1,65 @@
+package core.rpg;
+
+public final class RPGExpTable
+{
+ public static long EXPFloor(long level)
+ {
+ if(level > Levels.length - 1)
+ level = Levels.length - 1;
+
+ else if (level <= 0)
+ level = 1;
+
+ return Levels[(int) level];
+ }
+
+ public static long EXPCeil(long level)
+ {
+ if(level > Levels.length - 1)
+ level = Levels.length - 1;
+
+ else if (level <= 0)
+ level = 1;
+
+ return Levels[(int) (level + 1)];
+ }
+ public static long LevelFromEXP(long exp)
+ {
+ if(exp < 0)
+ exp = 0;
+
+ for(int i = 0; i < Levels.length; ++i)
+ {
+ if(Levels[i] > exp)
+ return i - 1;
+ }
+
+ return Levels.length - 1;
+ }
+
+ // Based on D&D Pathfinder levels
+ public static long Levels[] =
+ {
+ 0, // level 0
+ 0, // level 1
+ 3000, // level 2
+ 7500, // level 3
+ 14000, // level 4
+ 23000, // level 5
+ 35000, // level 6
+ 53000, // level 7
+ 77000, // level 8
+ 115000, // level 9
+ 160000, // level 10
+ 235000, // level 11
+ 330000, // level 12
+ 475000, // level 13
+ 665000, // level 14
+ 955000, // level 15
+ 1350000, // level 16
+ 1900000, // level 17
+ 2700000, // level 18
+ 3850000, // level 19
+ 5350000 // level 20
+ };
+}
diff --git a/src/core/rpg/RPGFramework.java b/src/core/rpg/RPGFramework.java
new file mode 100644
index 0000000..21f8312
--- /dev/null
+++ b/src/core/rpg/RPGFramework.java
@@ -0,0 +1,82 @@
+package core.rpg;
+
+import java.util.HashMap;
+
+import commands.rpg.RPGCommandBattleFight;
+import commands.rpg.RPGCommandBattleRun;
+import commands.rpg.RPGCommandExplore;
+import commands.rpg.RPGCommandInfo;
+import commands.rpg.RPGCommandStats;
+import core.rpg.RPGInput;
+
+// Holds the framework for the text RPG, for any number of users
+public class RPGFramework
+{
+ // User ID to user state. Users are conceptually just an ID.
+ public HashMap gameStates;
+ public HashMap gameCommands;
+
+ // Ctor
+ public RPGFramework()
+ {
+ this.gameStates = new HashMap();
+ this.gameCommands = new HashMap();
+
+ RegisterCommand("stats", new RPGCommandStats());
+ RegisterCommand("about", new RPGCommandInfo());
+ RegisterCommand("info", new RPGCommandInfo());
+ RegisterCommand("explore", new RPGCommandExplore());
+ RegisterCommand("run", new RPGCommandBattleRun());
+ RegisterCommand("fight", new RPGCommandBattleFight());
+ }
+
+ // Primary external
+ public String Run(String userID, String inputRaw)
+ {
+ RPGState state = LookupState(userID);
+ RPGInput input = new RPGInput(inputRaw);
+ return ExecuteCommand(input.key, state, input);
+ }
+
+ // Get state for executing a command
+ public RPGState LookupState(String userID)
+ {
+ RPGState state;
+ synchronized(gameStates)
+ {
+ // Note: Hardcoded right now. Later: Extract.
+ state = gameStates.get(userID);
+
+ if(state == null)
+ {
+ state = new RPGState(userID);
+ gameStates.put(userID, state);
+ }
+ }
+
+ return state;
+ }
+
+ // Registers a command
+ private void RegisterCommand(String commandName, RPGCommand command)
+ {
+ commandName = commandName.toLowerCase();
+ if(gameCommands.put(commandName, command) != null)
+ RPGLog.Log("Managed to register the same RPG command twice! Not ideal!");
+
+ RPGLog.Log("Registered " + commandName);
+ }
+
+ private String ExecuteCommand(String name, RPGState state, RPGInput input)
+ {
+ synchronized(gameCommands)
+ {
+ RPGCommand command = gameCommands.get(name.toLowerCase());
+
+ if(command != null && state != null)
+ return command.OnRun(state, input);
+ }
+
+ return null;
+ }
+}
diff --git a/src/core/rpg/RPGInput.java b/src/core/rpg/RPGInput.java
new file mode 100644
index 0000000..e3c3cec
--- /dev/null
+++ b/src/core/rpg/RPGInput.java
@@ -0,0 +1,42 @@
+package core.rpg;
+
+public class RPGInput
+{
+ public String raw;
+ public String key;
+ public String value;
+
+ public RPGInput(String raw)
+ {
+ this.raw = raw;
+ this.key = "";
+ this.value = "";
+
+ if(raw == null || raw.length() == 0)
+ return;
+
+ raw = raw.trim();
+ int whitespacePos = FindFirstWhitespace(raw);
+
+ if(whitespacePos == -1)
+ {
+ key = raw;
+ return;
+ }
+
+ key = raw.substring(0, whitespacePos).trim();
+ value = raw.substring(whitespacePos).trim();
+ }
+
+ // Finds first whitespace in the string
+ private int FindFirstWhitespace(String str)
+ {
+ for (int i = 0; i < str.length(); ++i)
+ {
+ if (Character.isWhitespace(str.charAt(i)))
+ return i;
+ }
+
+ return -1;
+ }
+}
diff --git a/src/core/rpg/RPGItem.java b/src/core/rpg/RPGItem.java
new file mode 100644
index 0000000..e7dd749
--- /dev/null
+++ b/src/core/rpg/RPGItem.java
@@ -0,0 +1,25 @@
+package core.rpg;
+
+public abstract class RPGItem
+{
+ protected String name;
+ protected String description;
+ protected int value;
+
+ // Defaults
+ public RPGItem() { this("unknown"); }
+ public RPGItem(String name) { this(name, "nothig is known about this item"); }
+ public RPGItem(String name, String description) { this(name, description, 1); }
+
+ // Ctor
+ public RPGItem(String name, String description, int value)
+ {
+ this.name = name;
+ this.description = description;
+ this.value = value;
+ }
+
+ public String GetName() { return name; }
+ public String GetDescription() { return description; }
+ public long GetValue() { return value; }
+}
diff --git a/src/core/rpg/RPGLog.java b/src/core/rpg/RPGLog.java
new file mode 100644
index 0000000..0ee2bf8
--- /dev/null
+++ b/src/core/rpg/RPGLog.java
@@ -0,0 +1,12 @@
+package core.rpg;
+
+import utils.GlobalLog;
+import utils.LogFilter;
+
+public class RPGLog
+{
+ public static void Log(String toWrite)
+ {
+ GlobalLog.Log(LogFilter.Command, "[RPG] " + toWrite);
+ }
+}
diff --git a/src/core/rpg/RPGMain.java b/src/core/rpg/RPGMain.java
new file mode 100644
index 0000000..19ed553
--- /dev/null
+++ b/src/core/rpg/RPGMain.java
@@ -0,0 +1,38 @@
+package core.rpg;
+
+import java.util.Scanner;
+
+public class RPGMain
+{
+ private static RPGFramework framework;
+ private static Scanner scanner;
+
+ public static void RPGmain(String[] args)
+ {
+ Init();
+ while(Run()) { };
+ Shutdown();
+ }
+
+ // Initializes stuff (small factory, ish)
+ private static void Init()
+ {
+ framework = new RPGFramework();
+ scanner = new Scanner(System.in);
+ }
+
+ // Main loop
+ private static boolean Run()
+ {
+ // move outside
+ String out = framework.Run("Test", scanner.nextLine());
+ RPGLog.Log(out);
+ return true;
+ }
+
+ // Cleanup
+ private static void Shutdown()
+ {
+ scanner.close();
+ }
+}
\ No newline at end of file
diff --git a/src/core/rpg/RPGPlayer.java b/src/core/rpg/RPGPlayer.java
new file mode 100644
index 0000000..86d61be
--- /dev/null
+++ b/src/core/rpg/RPGPlayer.java
@@ -0,0 +1,58 @@
+package core.rpg;
+
+public class RPGPlayer extends RPGUnit
+{
+ private String name;
+ private long gold;
+ private long exp;
+
+ RPGWeapon weapon;
+ RPGArmor armor;
+
+ public RPGPlayer()
+ {
+ super();
+
+ name = "Wanderer";
+ healthCurrent = 5;
+ healthMax = 5;
+ gold = 0;
+ weapon = new RPGWeapon();
+ armor = new RPGArmor();
+ }
+
+ // Getters
+ public long GetEXP() { return exp; }
+ public long GetGold() { return gold; }
+ public String GetName() { return name; }
+ public RPGArmor GetArmor() { return armor; };
+ public RPGWeapon GetWeapon() { return weapon; };
+
+ // Setters
+ public void SetName(String name) { this.name = name; }
+
+ // Interactions
+ public void ApplyEXP(int expToGive)
+ {
+ if(expToGive < 0)
+ expToGive = 0;
+
+ exp += expToGive;
+ }
+
+ public void GiveGold(long amount)
+ {
+ if(amount < 0)
+ amount = 0;
+
+ gold += amount;
+ }
+
+ public void SpendGold(long amount)
+ {
+ if(amount < 0)
+ amount = 0;
+
+ gold -= amount;
+ }
+}
diff --git a/src/core/rpg/RPGState.java b/src/core/rpg/RPGState.java
new file mode 100644
index 0000000..f6fb1a7
--- /dev/null
+++ b/src/core/rpg/RPGState.java
@@ -0,0 +1,19 @@
+package core.rpg;
+
+// Holds specific state information for a given user and their world
+public class RPGState
+{
+ // General
+ public String userID;
+
+ // Stats and gameplay
+ public RPGPlayer player;
+ public RPGBattleContext battleContext;
+
+ public RPGState(String userID)
+ {
+ this.userID = userID;
+ this.player = new RPGPlayer();
+ this.battleContext = null;
+ }
+}
\ No newline at end of file
diff --git a/src/core/rpg/RPGUnit.java b/src/core/rpg/RPGUnit.java
new file mode 100644
index 0000000..cb2cb99
--- /dev/null
+++ b/src/core/rpg/RPGUnit.java
@@ -0,0 +1,34 @@
+package core.rpg;
+
+public abstract class RPGUnit
+{
+ protected int healthMax;
+ protected int healthCurrent;
+
+ public int GetHealthMax() { return healthMax; }
+ public int GetHealthCurrent() { return healthCurrent; }
+ public boolean IsAlive() { return healthCurrent > 0; }
+
+ // Generic implementation. Consider implementing armor in
+ // derived classes, for example.
+ public void ApplyDamage(int value)
+ {
+ if(value < 0)
+ value = 0;
+
+ healthCurrent -= value;
+ }
+
+ // Generic implementation. Consider applying boosts in
+ // derived classes, for example.
+ public void ApplyHealing(int value)
+ {
+ if(value < 0)
+ value = 0;
+
+ healthCurrent += value;
+
+ if(healthCurrent > healthMax)
+ healthCurrent = healthMax;
+ }
+}
diff --git a/src/core/rpg/RPGWeapon.java b/src/core/rpg/RPGWeapon.java
new file mode 100644
index 0000000..7b390ad
--- /dev/null
+++ b/src/core/rpg/RPGWeapon.java
@@ -0,0 +1,17 @@
+package core.rpg;
+
+public class RPGWeapon extends RPGItem
+{
+ private long attack;
+ private double accuracy;
+
+ RPGWeapon()
+ {
+ super("Singed Stick", "A really cool stick that you found! You poked at your campfire last night with it a bit, so the end is a bit toasty.", 2);
+ attack = 1;
+ accuracy = 0.8;
+ }
+
+ public long GetAttack() { return attack; }
+ public double GetAccuracy() { return accuracy; }
+}
diff --git a/src/dataStructures/KittyChannel.java b/src/dataStructures/KittyChannel.java
new file mode 100644
index 0000000..fb1a225
--- /dev/null
+++ b/src/dataStructures/KittyChannel.java
@@ -0,0 +1,31 @@
+package dataStructures;
+
+//import core.DatabaseTrackedObject;
+
+public class KittyChannel //extends DatabaseTrackedObject
+{
+ public String uniqueID;
+ public KittyGuild guild;
+
+ //public KittyRating rating;
+ //public bool isVoice;
+
+ public KittyChannel(String uniqueID, KittyGuild guild)
+ {
+ //super(uniqueID);
+ this.uniqueID = uniqueID;
+ this.guild = guild;
+ }
+
+// @Override
+// public String Serialize()
+// {
+// return "";
+// }
+//
+// @Override
+// public void DeSerialzie(String string)
+// {
+//
+// }
+}
diff --git a/src/dataStructures/KittyGuild.java b/src/dataStructures/KittyGuild.java
new file mode 100644
index 0000000..c35791e
--- /dev/null
+++ b/src/dataStructures/KittyGuild.java
@@ -0,0 +1,91 @@
+package dataStructures;
+
+import java.util.ArrayList;
+import core.DatabaseTrackedObject;
+
+// Context for a given guild for kittybot. Primarily designed to hold guild-specific settings.
+public class KittyGuild extends DatabaseTrackedObject
+{
+ public String uniqueID;
+ public KittyRating contentRating;
+ public KittyUser guildOwner;
+ public boolean polling;
+ public String poll;
+ public ArrayList hasVoted = new ArrayList();
+ public ArrayList choices = new ArrayList();
+
+ private String commandIndicator;
+
+ // Default content for a guild
+ public KittyGuild(String uniqueID)
+ {
+ super(uniqueID);
+
+ this.uniqueID = uniqueID;
+ this.contentRating = KittyRating.Safe;
+ this.polling = false;
+ SetCommandIndicator("!");
+ }
+
+ // Explicit constructor
+ public KittyGuild(String commandIndicator, KittyRating contentRating, KittyUser guildOwner, String uniqueID)
+ {
+ super(uniqueID);
+
+ SetCommandIndicator(commandIndicator);
+ this.contentRating = contentRating;
+ this.polling = false;
+ this.guildOwner = guildOwner;
+ }
+
+ public String startPoll(String poll)
+ {
+ polling = true;
+ this.poll = poll;
+ return "Poll `" + poll + "` started! Don't forget to add choices with `!poll choice`~";
+ }
+
+ public String addChoiceToPoll(String choice)
+ {
+ choices.add(new KittyPoll(choice));
+ return "Added `" + choice + "` to poll!";
+ }
+
+ public String endPoll()
+ {
+ choices.clear();
+ hasVoted.clear();
+ this.polling = false;
+ poll = null;
+ return "Poll ended!";
+ }
+
+ @Override
+ public String Serialize()
+ {
+ return commandIndicator;
+ }
+
+ @Override
+ public void DeSerialzie(String string)
+ {
+ if(string == null || string.length() == 0)
+ SetCommandIndicator("!");
+ else
+ SetCommandIndicator(string);
+ }
+
+ public String GetCommandIndicator()
+ {
+ return commandIndicator;
+ }
+
+ public void SetCommandIndicator(String newIndicator)
+ {
+ if(newIndicator != commandIndicator)
+ {
+ commandIndicator = newIndicator;
+ MarkDirty();
+ }
+ }
+}
diff --git a/src/dataStructures/KittyPoll.java b/src/dataStructures/KittyPoll.java
new file mode 100644
index 0000000..e47e277
--- /dev/null
+++ b/src/dataStructures/KittyPoll.java
@@ -0,0 +1,13 @@
+package dataStructures;
+
+public class KittyPoll
+{
+ public String choice;
+ public int votes;
+
+ public KittyPoll(String choice)
+ {
+ this.choice = choice;
+ votes = 0;
+ }
+}
diff --git a/src/dataStructures/KittyRP.java b/src/dataStructures/KittyRP.java
new file mode 100644
index 0000000..138d308
--- /dev/null
+++ b/src/dataStructures/KittyRP.java
@@ -0,0 +1,81 @@
+package dataStructures;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.PrintWriter;
+import java.io.UnsupportedEncodingException;
+import java.util.ArrayList;
+
+public class KittyRP
+{
+ ArrayList users = new ArrayList();
+ ArrayList log = new ArrayList ();
+ KittyChannel channel;
+ long timer;
+
+ public KittyRP(ArrayList users, KittyChannel channel)
+ {
+ for(int i = 0; i < users.size(); i++)
+ {
+ this.users.add(Long.parseLong(users.get(i).discordID));
+ }
+ this.channel = channel;
+ timer = System.currentTimeMillis();
+ }
+
+ public void addLine(KittyUser user, String line)
+ {
+ if(users.contains(Long.parseLong(user.discordID)))
+ {
+ log.add(user.name + " " + formatString(line));
+ resetTimer();
+ }
+ }
+
+ private String formatString(String line)
+ {
+ if((line.charAt(0) == '_' && line.charAt(line.length()-1) == '_')
+ ||(line.charAt(0) == '*' && line.charAt(line.length()-1) == '*'))
+ {
+ line = line.substring(1,line.length()-1);
+ }
+
+ return line;
+ }
+
+ public File endRP(KittyUser user) throws FileNotFoundException, UnsupportedEncodingException
+ {
+ if(!users.contains(Long.parseLong(user.discordID)) && user.GetRole().getValue() < 2)
+ {
+ return null;
+ }
+ PrintWriter writer = new PrintWriter("RP.txt", "UTF-8");
+ while(!log.isEmpty())
+ {
+ writer.println(log.get(0));
+ log.remove(0);
+ }
+ writer.close();
+ return new File("RP.txt");
+ }
+
+ public void resetTimer()
+ {
+ timer = System.currentTimeMillis();
+ }
+
+ public long getTimer()
+ {
+ return timer;
+ }
+
+ public KittyChannel getChannel()
+ {
+ return channel;
+ }
+
+ public ArrayList getUsers()
+ {
+ return users;
+ }
+}
\ No newline at end of file
diff --git a/src/dataStructures/KittyRating.java b/src/dataStructures/KittyRating.java
new file mode 100644
index 0000000..5e0ba79
--- /dev/null
+++ b/src/dataStructures/KittyRating.java
@@ -0,0 +1,17 @@
+package dataStructures;
+
+public enum KittyRating
+{
+ Safe(0), Filtered(1), Explicit(2);
+
+ private final int value;
+ private KittyRating(int value)
+ {
+ this.value = value;
+ }
+
+ public int getValue()
+ {
+ return value;
+ }
+}
diff --git a/src/dataStructures/KittyRole.java b/src/dataStructures/KittyRole.java
new file mode 100644
index 0000000..baec91c
--- /dev/null
+++ b/src/dataStructures/KittyRole.java
@@ -0,0 +1,25 @@
+package dataStructures;
+
+import java.util.Arrays;
+import java.util.Optional;
+
+public enum KittyRole
+{
+ Blacklisted (0), General(1), Mod(2), Admin(3), Dev(4);
+
+ private final int value;
+ private KittyRole(int value)
+ {
+ this.value = value;
+ }
+
+ public int getValue()
+ {
+ return value;
+ }
+
+ public static Optional valueOf(int value)
+ {
+ return Arrays.stream(values()).filter(role -> role.value == value).findFirst();
+ }
+}
diff --git a/src/dataStructures/KittyUser.java b/src/dataStructures/KittyUser.java
new file mode 100644
index 0000000..1523a8a
--- /dev/null
+++ b/src/dataStructures/KittyUser.java
@@ -0,0 +1,96 @@
+package dataStructures;
+
+import core.DatabaseTrackedObject;
+import utils.GlobalLog;
+import utils.LogFilter;
+
+// NOTE(wisp): A user is a specific instance of a discord user on a given
+// discord server. This means that if a user is on two servers, they have
+// two unique user objects associated with them.
+public class KittyUser extends DatabaseTrackedObject
+{
+ public KittyGuild guild;
+ public String name;
+ public String uniqueID;
+ public String discordID;
+ public String avatarID;
+ private KittyRole role;
+ private long beans;
+
+ // Explicit Constructor
+ public KittyUser(String name, KittyGuild guild, KittyRole role, String uniqueID, String avatarID, String discordID)
+ {
+ super(uniqueID);
+ this.name = name;
+ this.role = role;
+ this.guild = guild;
+ this.uniqueID = uniqueID;
+ this.beans = 0;
+ this.avatarID = avatarID;
+ this.discordID = discordID;
+ }
+
+ // Can be positive or negative
+ public void ChangeBeans(int amount)
+ {
+ beans += amount;
+
+ if(amount != 0)
+ MarkDirty();
+ }
+
+ public void ChangeRole(KittyRole newRole)
+ {
+ if(newRole != role)
+ {
+ role = newRole;
+ MarkDirty();
+ }
+ }
+
+ public long GetBeans()
+ {
+ return beans;
+ }
+
+ public KittyRole GetRole()
+ {
+ return role;
+ }
+
+ @Override
+ public String Serialize()
+ {
+ return beans + "," + role.getValue();
+ }
+
+ @Override
+ public void DeSerialzie(String string)
+ {
+ try
+ {
+ String[] strings = string.split(",");
+ beans = Integer.parseInt(strings[0]);
+ if(strings.length > 1)
+ {
+ role = KittyRole.valueOf(Integer.parseInt(strings[1])).get();
+ }
+ else
+ {
+ GlobalLog.Log(LogFilter.Database, "Upgrading user " + name + " to include 'role' in DB");
+ // Mark ourselves dirty to re-write the role information stored in the user.
+ // Just uses defaults from earlier again.
+ MarkDirty();
+ }
+ }
+ catch (NumberFormatException e)
+ {
+ GlobalLog.Warn(LogFilter.Database, "Invalid user data for user " + name + "! "
+ + "Starting over at 0 beans with a general role!");
+ // We don't need to specify the role at this point because it is set at this point.
+ // We use what the user was created with whatever defaults were in the factory.
+ // Beans are maintained too, just in case there's some in cache.
+ MarkDirty();
+ }
+ }
+}
diff --git a/src/dataStructures/Response.java b/src/dataStructures/Response.java
new file mode 100644
index 0000000..f25ba36
--- /dev/null
+++ b/src/dataStructures/Response.java
@@ -0,0 +1,80 @@
+package dataStructures;
+
+import java.io.File;
+import java.io.InputStream;
+import net.dv8tion.jda.core.JDA;
+import net.dv8tion.jda.core.entities.TextChannel;
+import net.dv8tion.jda.core.events.message.guild.*;
+import utils.GlobalLog;
+import utils.LogFilter;
+
+// NOTE(wisp): This isn't constructed with the factory at this time, mostly
+// because we need it to handle all the response queue behavior and everything
+// internally. So long as commands aren't exposed to the GuildMessageReceivedEvent
+// then we should be fine.
+public class Response
+{
+ private GuildMessageReceivedEvent event;
+ private JDA kitty;
+ private final int discordMessageMax = 2000;
+ private final int kittyMessageMax = 1950;
+
+ public Response(GuildMessageReceivedEvent event, JDA kitty)
+ {
+ this.event = event;
+ this.kitty = kitty;
+ }
+
+ public void Call(String toRespondWith)
+ {
+ GlobalLog.Log(LogFilter.Response, "Sending response: " + toRespondWith);
+ if(toRespondWith.length() > discordMessageMax)
+ {
+ event.getChannel().sendMessage(toRespondWith.substring(0, kittyMessageMax) + "\n\nI think that's enough!").queue();
+ }
+ else
+ {
+ event.getChannel().sendMessage(toRespondWith).queue();
+ }
+ }
+
+ public void CallToChannel(String toRespondWith, String channelID)
+ {
+ TextChannel channel;
+ channel = kitty.getTextChannelById(Long.parseLong(channelID));
+ if(toRespondWith.length() > discordMessageMax)
+ {
+ channel.sendMessage(toRespondWith.substring(0, kittyMessageMax) + "\n\nI think that's enough!").queue();
+ }
+ else
+ {
+ channel.sendMessage(toRespondWith).queue();
+ }
+ }
+
+ public void CallImmediate(String toRespondWith)
+ {
+ GlobalLog.Log(LogFilter.Response, "Sending immediate response: " + toRespondWith);
+ if(toRespondWith.length() > discordMessageMax)
+ {
+ event.getChannel().sendMessage(toRespondWith.substring(0, kittyMessageMax) + "\n\nI think that's enough!");
+ }
+ else
+ {
+ event.getChannel().sendMessage(toRespondWith);
+ }
+ }
+
+ // This is for responding with a file rather than a String
+ public void CallFile(File toRespondWith, String extension)
+ {
+ GlobalLog.Log(LogFilter.Response, "Sending file response");
+ event.getChannel().sendFile(toRespondWith, "return." + extension).queue();
+ }
+
+ public void CallInput(InputStream in, String extension)
+ {
+ GlobalLog.Log(LogFilter.Response, "Sending input stream response");
+ event.getChannel().sendFile(in, "return." + extension).queue();
+ }
+}
diff --git a/src/dataStructures/UserInput.java b/src/dataStructures/UserInput.java
new file mode 100644
index 0000000..21fa03a
--- /dev/null
+++ b/src/dataStructures/UserInput.java
@@ -0,0 +1,91 @@
+package dataStructures;
+
+import java.util.List;
+
+import core.ObjectBuilderFactory;
+import net.dv8tion.jda.core.entities.Member;
+import net.dv8tion.jda.core.events.message.guild.GuildMessageReceivedEvent;
+
+public class UserInput
+{
+ // Variables
+ public String key;
+ public String args;
+ public KittyUser [] mentions;
+ private boolean isValid;
+ public String message;
+
+ // NOTE(wisp): Designed to parse a string as it's constructed.
+ // If a user enters the command "!ping some stuff here :D"
+ // and if the command indicator is !, then...
+ //
+ // key=ping
+ // args=some stuff here :D
+ //
+ // The CommandIndicator and spaces between the key and args are dropped.
+ public UserInput(GuildMessageReceivedEvent event, KittyGuild guildContext)
+ {
+ String toParse = event.getMessage().getContentRaw();
+ message = toParse;
+
+ key = "";
+ args = "";
+
+ if(toParse.length() <= 0)
+ return;
+
+ if(!event.getMessage().getMentionedMembers().isEmpty())
+ mentions = FindMentionedUsers(event);
+
+ toParse = toParse.trim();
+ String commandIndicator = guildContext.GetCommandIndicator();
+ if(toParse.startsWith(commandIndicator))
+ {
+ int loc = FindFirstWhitespace(toParse);
+
+ if(loc <= 0)
+ {
+ key = toParse.substring(commandIndicator.length()).trim().toLowerCase();
+ }
+ else
+ {
+ key = toParse.substring(commandIndicator.length(), loc).trim().toLowerCase();
+ args = toParse.substring(loc).trim();
+ }
+
+ isValid = true;
+ }
+ else
+ {
+ isValid = false;
+ }
+ }
+
+ public boolean IsValid()
+ {
+ return isValid;
+ }
+
+ // Finds first whitespace in the string
+ private int FindFirstWhitespace(String str)
+ {
+ for (int i = 0; i < str.length(); ++i)
+ {
+ if (Character.isWhitespace(str.charAt(i)))
+ return i;
+ }
+
+ return -1;
+ }
+
+ private KittyUser[] FindMentionedUsers(GuildMessageReceivedEvent event)
+ {
+ List JDAMentions = event.getMessage().getMentionedMembers();
+ KittyUser [] KittyMentions = new KittyUser[JDAMentions.size()];
+ for(int i = 0; i < KittyMentions.length; i ++)
+ {
+ KittyMentions [i] = ObjectBuilderFactory.getCachedUser(event.getGuild().getId(), JDAMentions.get(i).getUser().getId());
+ }
+ return KittyMentions;
+ }
+}
diff --git a/src/main/Main.java b/src/main/Main.java
new file mode 100644
index 0000000..1faa78a
--- /dev/null
+++ b/src/main/Main.java
@@ -0,0 +1,136 @@
+package main;
+
+import javax.security.auth.login.LoginException;
+
+import core.*;
+import dataStructures.KittyChannel;
+import dataStructures.KittyGuild;
+import dataStructures.KittyRole;
+import dataStructures.KittyUser;
+import dataStructures.Response;
+import dataStructures.UserInput;
+import net.dv8tion.jda.core.AccountType;
+import net.dv8tion.jda.core.entities.*;
+import net.dv8tion.jda.core.events.message.guild.*;
+import net.dv8tion.jda.core.hooks.ListenerAdapter;
+import offline.*;
+import utils.GlobalLog;
+import net.dv8tion.jda.core.*;
+
+// NOTE(wisp): http://www.slf4j.org/ - this JDA logging tool has been disabled by specifying NOP implementation.
+// NOTE(wisp): Application entry point!
+public class Main extends ListenerAdapter
+{
+ // Variables and stuff
+ private static JDA kitty;
+ private static CommandManager commandManager;
+ private static DatabaseManager databaseManager;
+ private static Stats stats;
+
+ // Main test location
+ public static void main(String[] args) throws InterruptedException, LoginException, Exception
+ {
+ databaseManager = ObjectBuilderFactory.ConstructDatabaseManager();
+ commandManager = ObjectBuilderFactory.ConstructCommandManager();
+ ObjectBuilderFactory.ConstructRPManager();
+ stats = ObjectBuilderFactory.ConstructStats(commandManager);
+
+ kitty = new JDABuilder(AccountType.BOT).setToken(Ref.TestToken).buildBlocking();
+ kitty.getPresence().setGame(Game.playing("with a new build"));
+ kitty.addEventListener(new Main());
+ }
+
+ @Override
+ public void onGuildMessageReceived(GuildMessageReceivedEvent event)
+ {
+ // Tweak the event as necessary
+ if(!PreProcessSetup(event))
+ return;
+
+ GlobalLog.Log("Parseable message recieved!");
+
+ // Factory objects
+ KittyUser user = ObjectBuilderFactory.ExtractUser(event);
+ KittyGuild guild = ObjectBuilderFactory.ExtractGuild(event);
+ KittyChannel channel = ObjectBuilderFactory.ExtractChannel(event);
+
+ // Specialized uncached objects
+ Response response = new Response(event, kitty);
+ UserInput input = new UserInput(event, guild);
+
+ // Tweak object construction as necessary
+ if(!PostProcessSetup(event, user, guild, channel, response, input))
+ return;
+
+ // Track beans!
+ user.ChangeBeans(1);
+
+ //RP logging system
+ RPManager.instance.addLine(channel, user, input);
+
+ // Issue the command
+ commandManager.InvokeOnNewThread(guild, channel, user, input, response);
+
+ // Run any upkeep we need to
+ PerCommandUpkeep();
+ }
+
+ // This is run on the JDA GuildMessageReceivedEvent before anything else happens.
+ public static boolean PreProcessSetup(GuildMessageReceivedEvent event)
+ {
+ // Verify we have an event
+ if(event == null)
+ return false;
+
+ // Track number of messages seen
+ stats.NoteMessageEvent();
+
+ // If we're mid-shutdown, no more commands.
+ if(stats.GetIsShuttingDown())
+ return false;
+
+ // Swat down highest ban level immediately before even hitting command parsing.
+ for(int i = 0; i < Ref.alwaysIgnore.length; ++i)
+ {
+ String id = event.getAuthor().getId();
+ if(id.equals(Ref.alwaysIgnore[i]))
+ return false;
+ }
+
+ return true;
+ }
+
+ // This runs after all the objects have been constructed!
+ // Modifications to objects here stick!
+ public static boolean PostProcessSetup(GuildMessageReceivedEvent event, KittyUser user, KittyGuild guild, KittyChannel channel, Response res, UserInput input)
+ {
+ // Verify we have everything. From here out, we promise we have that all.
+ if(event == null || user == null || guild == null || channel == null || res == null || input == null)
+ return false;
+
+ // If the guild member is the owner, give them admin role by default.
+ if(event.getMember().isOwner())
+ user.ChangeRole(KittyRole.Admin);
+
+ // Give devs the dev role
+ for(int i = 0; i < Ref.devIDs.length; ++i)
+ {
+ if(event.getAuthor().getId().equals(Ref.devIDs[i]))
+ {
+ user.ChangeRole(KittyRole.Dev);
+ break;
+ }
+ }
+
+ return true;
+ }
+
+ // This is for stuff that we need to do on a regular basis, but don't
+ // necessarily want running at all points in time.
+ private static boolean PerCommandUpkeep()
+ {
+ databaseManager.Upkeep();
+ RPManager.Upkeep(kitty);
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/src/network/JDBCDriver.java b/src/network/JDBCDriver.java
new file mode 100644
index 0000000..9058b9e
--- /dev/null
+++ b/src/network/JDBCDriver.java
@@ -0,0 +1,19 @@
+package network;
+
+import java.sql.ResultSet;
+
+// NOTE: This outlines Java DataBase Connection Driver
+// requirements that cater to our specific needs.
+public abstract class JDBCDriver
+{
+ // Connects to the database, returns a bool if it succeeded.
+ public abstract boolean Connect();
+
+ // Disconnects the driver. Returns if the driver is disconnected now, regardless of connection status.
+ public abstract boolean Disconnect();
+
+ // Executes a SQL command with the database. Returns if it was executed successfully, or in the
+ // case of the returning statement, returns the ResultSet.
+ public abstract boolean ExecuteStatement(String statement);
+ public abstract ResultSet ExecuteReturningStatement(String statement);
+}
diff --git a/src/network/JDBCDriverMySQL.java b/src/network/JDBCDriverMySQL.java
new file mode 100644
index 0000000..2c3addf
--- /dev/null
+++ b/src/network/JDBCDriverMySQL.java
@@ -0,0 +1,30 @@
+package network;
+
+import java.sql.ResultSet;
+
+public class JDBCDriverMySQL extends JDBCDriver
+{
+ @Override
+ public boolean Connect() {
+ // TODO Auto-generated method stub
+ return false;
+ }
+
+ @Override
+ public boolean Disconnect() {
+ // TODO Auto-generated method stub
+ return false;
+ }
+
+ @Override
+ public boolean ExecuteStatement(String statement) {
+ // TODO Auto-generated method stub
+ return false;
+ }
+
+ @Override
+ public ResultSet ExecuteReturningStatement(String statement) {
+ // TODO Auto-generated method stub
+ return null;
+ }
+}
diff --git a/src/network/JDBCDriverPostgreSQL.java b/src/network/JDBCDriverPostgreSQL.java
new file mode 100644
index 0000000..960ca35
--- /dev/null
+++ b/src/network/JDBCDriverPostgreSQL.java
@@ -0,0 +1,30 @@
+package network;
+
+import java.sql.ResultSet;
+
+public class JDBCDriverPostgreSQL extends JDBCDriver
+{
+ @Override
+ public boolean Connect() {
+ // TODO Auto-generated method stub
+ return false;
+ }
+
+ @Override
+ public boolean Disconnect() {
+ // TODO Auto-generated method stub
+ return false;
+ }
+
+ @Override
+ public boolean ExecuteStatement(String statement) {
+ // TODO Auto-generated method stub
+ return false;
+ }
+
+ @Override
+ public ResultSet ExecuteReturningStatement(String statement) {
+ // TODO Auto-generated method stub
+ return null;
+ }
+}
diff --git a/src/network/JDBCDriverSQLite.java b/src/network/JDBCDriverSQLite.java
new file mode 100644
index 0000000..0580cbb
--- /dev/null
+++ b/src/network/JDBCDriverSQLite.java
@@ -0,0 +1,117 @@
+package network;
+
+import java.io.File;
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import utils.GlobalLog;
+import utils.LogFilter;
+
+// Only to be called by the generic driver.
+public class JDBCDriverSQLite extends JDBCDriver
+{
+ Connection connection = null;
+ String databaseFolder = "db/";
+ String databaseName = "catfood";
+
+ @Override
+ public boolean Connect()
+ {
+ try
+ {
+ { // Scope to discard file...
+ File f = new File(databaseFolder);
+ if(!f.exists() || !f.isDirectory())
+ {
+ f.mkdir();
+ }
+ }
+
+ // db parameters
+ String url = "jdbc:sqlite:db/" + databaseName + ".db";
+
+ // create a connection to the database
+ connection = DriverManager.getConnection(url);
+
+ GlobalLog.Log(LogFilter.Database, "Connection to SQLite has been established.");
+ }
+ catch (SQLException e)
+ {
+ GlobalLog.Error(e.getMessage());
+ }
+
+ return connection != null;
+ }
+
+ @Override
+ public boolean Disconnect()
+ {
+ try
+ {
+ if (connection != null)
+ connection.close();
+
+ return true;
+ }
+ catch (SQLException ex)
+ {
+ GlobalLog.Error(ex.getMessage());
+ return false;
+ }
+ }
+
+ @Override
+ public ResultSet ExecuteReturningStatement(String sql)
+ {
+ if(connection == null)
+ return null;
+
+ if(sql == null || sql.length() == 0)
+ return null;
+
+ try
+ {
+ Statement statement = connection.createStatement();
+ ResultSet set = statement.executeQuery(sql);
+ return set;
+ }
+ catch (SQLException e)
+ {
+ try {
+ GlobalLog.Fatal(e.getMessage());
+ } catch (Exception e1) {
+ // TODO Auto-generated catch block
+ e1.printStackTrace();
+ }
+ return null;
+ }
+ }
+
+ public boolean ExecuteStatement(String sql)
+ {
+ if(connection == null)
+ return false;
+
+ if(sql == null || sql.length() == 0)
+ return false;
+
+ try
+ {
+ Statement statement = connection.createStatement();
+ boolean executed = statement.execute(sql);
+ return executed;
+ }
+ catch (SQLException e)
+ {
+ try {
+ GlobalLog.Fatal(e.getMessage());
+ } catch (Exception e1) {
+ // TODO Auto-generated catch block
+ e1.printStackTrace();
+ }
+ return false;
+ }
+ }
+}
diff --git a/src/network/NetworkColiru.java b/src/network/NetworkColiru.java
new file mode 100644
index 0000000..63d80fb
--- /dev/null
+++ b/src/network/NetworkColiru.java
@@ -0,0 +1,29 @@
+package network;
+
+import utils.HTTPUtils;
+
+public class NetworkColiru
+{
+ public String compileCPlus(String query)
+ {
+ // Escape characters that need escaping.
+ // Primarily types of whitespace.
+ query = query.replace("\\n", "\\\\n");
+ query = query.replace("\\t", "\\\\t");
+ query = query.replace("\n", "\\n");
+ query = query.replace("\"", "\\\"");
+ query = query.replace("\t", "\\t");
+
+ // Send the compilation request!
+ String result = HTTPUtils.SendPOSTRequest("http://coliru.stacked-crooked.com/compile"
+ , "{ \"cmd\": \"g++ main.cpp && ./a.out\", \"src\": \"" + query + "\" }");
+
+ // If we got a valid response...
+ if(!result.isEmpty())
+ {
+ result = "Here's what happened when I went to compiled that! \n```" + result + "```";
+ }
+ return result;
+ }
+
+}
diff --git a/src/network/NetworkDerpi.java b/src/network/NetworkDerpi.java
new file mode 100644
index 0000000..c67fa4d
--- /dev/null
+++ b/src/network/NetworkDerpi.java
@@ -0,0 +1,69 @@
+package network;
+
+import com.google.gson.Gson;
+import core.*;
+import offline.*;
+import utils.*;
+
+public class NetworkDerpi
+{
+ private final String mainURL = "https://derpibooru.org/search.json?q=";
+ private static final Gson jsonParser_ = new Gson();
+
+ private class DerpiResponseObject
+ {
+ public String image;
+ public String tags;
+ }
+
+ private class InitialRequest
+ {
+ public int id;
+ }
+
+ public GenericImage getDerpi(String query)
+ {
+ GenericImage image = new GenericImage(" ", " ", " ");
+ query = query.trim();
+ query = query.replace(" ", ",");
+ String res = HTTPUtils.SendGETRequest(mainURL + query + "&random_image=1&key=" + Ref.derpiKey);
+ if(res != null)
+ {
+ // Use class evaluation on an array of the response object to be able to hold multiple.
+ InitialRequest obj = jsonParser_.fromJson(res, InitialRequest.class);
+ if(res != null)
+ {
+
+ String res2 = HTTPUtils.SendGETRequest("https://derpibooru.org/" + obj.id + ".json");
+ image.editPostURL("");
+ DerpiResponseObject imageObj = jsonParser_.fromJson(res2, DerpiResponseObject.class);
+
+ image.editImageURL("https:" + imageObj.image.substring(0, imageObj.image.indexOf('_')) + imageObj.image.substring(imageObj.image.lastIndexOf('.')));
+ if(imageObj.tags.contains("artist:"))
+ {
+ String [] sepTags = imageObj.tags.split(",");
+ String artists = "";
+ for(int i = 0; i < sepTags.length; i++)
+ {
+ if(sepTags[i].contains("artist:"))
+ {
+ if(!artists.equals(""))
+ {
+ artists += " and ";
+ }
+ artists += sepTags[i].substring(sepTags[i].indexOf(":")+1);
+ }
+ }
+
+ image.editArtist(artists);
+ }
+ else
+ {
+ image.editArtist("Artist Unknown!");
+ }
+ }
+ }
+
+ return image;
+ }
+}
\ No newline at end of file
diff --git a/src/network/NetworkE621.java b/src/network/NetworkE621.java
new file mode 100644
index 0000000..0ce696f
--- /dev/null
+++ b/src/network/NetworkE621.java
@@ -0,0 +1,103 @@
+package network;
+
+import com.google.gson.Gson;
+import core.*;
+import utils.*;
+
+/**
+ * This is the e621 request class, designed for form and parse requests that
+ * use the e621 API, and is entirely static.
+ *
+ * If you ever find this class randomly not working,
+ * it may be a good idea to make sure the user agent string is set in
+ * HTTPUtils to something other than a browser emulating string, or the default
+ * java one.
+ *
+ * @author Wisp
+ * Edited by Rin
+ */
+public class NetworkE621
+{
+ ///////////////////////////////////////
+ // Internal JSON class and variables //
+ //////////////////////////////////.////
+ private static final Gson jsonParser_ = new Gson();
+ private static final String API_ROOT = "https://e621.net/post/index.json?";
+ private static int maxSearchResults_ = 10;
+ private static String[] blacklist = {"theallseeingeye","scat","diaper","cub"};
+ private class E621ResponseObject
+ {
+ // public varaibles matching the case and the type we want for JSON.
+ // There are many more fields, but if we don't provide some it just
+ // doesn't bother parsing them.
+ public String file_url;
+ public String id;
+ public String tags;
+ public String [] artist;
+ }
+
+
+
+ ////////////////////
+ // Static methods //
+ ////////////////////
+ // Requests a specific image, then returns a few.
+ public GenericImage getE621(String input)
+ {
+
+ GenericImage image = new GenericImage("","","");
+ boolean blacklisted;
+ // Clean up request and replace problematic characters for the query string.
+ input = input.trim();
+ input = input.replace("+", "%2B");
+ input = input.replace(" ", "%20");
+
+ // Configure and send request. Note: Random ordering added as first
+ // tag by default. User-provided tags, therefore, will override it.
+ // If order:score is provided, that will be honored over order:random.
+ String res = HTTPUtils.SendPOSTRequest(API_ROOT
+ , "tags=order:random%20" + input + "&limit=" + maxSearchResults_);
+
+
+ if(res != null)
+ {
+ // Use class evaluation on an array of the response imageObject to be able to hold multiple.
+ E621ResponseObject[] imageObj = jsonParser_.fromJson(res, E621ResponseObject[].class);
+
+ // For now, we really just wanna display images and their source.
+ // Append them all separately to a response string w/ some flavor text.
+ if(imageObj.length < 1)
+ {
+
+ }
+ else
+ {
+ for(int i = 0; i < imageObj.length; ++i)
+ {
+ blacklisted = false;
+ for(int j = 0; j < blacklist.length; j++)
+ {
+ if(imageObj[i].tags.contains(blacklist[j]))
+ {
+ blacklisted = true;
+ }
+ }
+
+ if(blacklisted)
+ {
+ continue;
+ }
+ // We will always have a file URL. That's a given.
+ image.editImageURL(imageObj[i].file_url);
+ image.editPostURL("https://e621.net/post/show/" + imageObj[i].id);
+ if(imageObj[i].artist.length > 0)
+ image.editArtist(imageObj[i].artist[0]);
+ else
+ image.editArtist("Artist Not Found!");
+ }
+ }
+
+ }
+ return image;
+ }
+}
\ No newline at end of file
diff --git a/src/network/NetworkJDoodle.java b/src/network/NetworkJDoodle.java
new file mode 100644
index 0000000..824f93b
--- /dev/null
+++ b/src/network/NetworkJDoodle.java
@@ -0,0 +1,80 @@
+package network;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URL;
+
+import com.google.gson.Gson;
+
+import offline.Ref;
+
+public class NetworkJDoodle
+{
+ private static final Gson jsonParser_ = new Gson();
+ private class JDoodleObject
+ {
+ public String output;
+ public String cpuTime;
+ }
+
+ public String compileJava(String query)
+ {
+ String result = "";
+ String lang = "java";
+ String version = "0";
+ query = query.replace("\\n", "\\\\n");
+ query = query.replace("\\t", "\\\\t");
+ query = query.replace("\n", "\\n");
+ query = query.replace("\"", "\\\"");
+ query = query.replace("\t", "\\t");
+
+ String input = "{\"clientId\": \"" + Ref.jdoodleID + "\",\"clientSecret\":\"" + Ref.jdoodleSecret + "\",\"script\":\"" + query +
+ "\",\"language\":\"" + lang + "\",\"versionIndex\":\"" + version + "\"} ";
+
+ try {
+ URL url = new URL("https://api.jdoodle.com/v1/execute");
+ HttpURLConnection connection = (HttpURLConnection) url.openConnection();
+ connection.setDoOutput(true);
+ connection.setRequestMethod("POST");
+ connection.setRequestProperty("Content-Type", "application/json");
+
+ //result += input + "\n";
+
+ OutputStream outputStream = connection.getOutputStream();
+ outputStream.write(input.getBytes());
+ outputStream.flush();
+
+ if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) {
+ result = "Please check your inputs : HTTP error code : "+ connection.getResponseCode();
+ return result;
+ }
+
+ BufferedReader bufferedReader;
+ bufferedReader = new BufferedReader(new InputStreamReader((connection.getInputStream())));
+
+ String output;
+ String fullOut = "";
+ result += "This is what happened! \n";
+ while ((output = bufferedReader.readLine()) != null)
+ {
+ fullOut += output + "\n";
+ }
+ JDoodleObject doodle = jsonParser_.fromJson(fullOut, JDoodleObject.class);
+
+ result += doodle.output + "\n";
+ result += "The CPU time was " + doodle.cpuTime + "\n";
+ connection.disconnect();
+ return result;
+ } catch (MalformedURLException e)
+ {
+ return "You probably shouldn't be seeing this";
+ } catch (IOException e)
+ {
+ return "You probably shouldn't be seeing this";
+ }
+ }
+}
diff --git a/src/network/NetworkWolfram.java b/src/network/NetworkWolfram.java
new file mode 100644
index 0000000..a76d637
--- /dev/null
+++ b/src/network/NetworkWolfram.java
@@ -0,0 +1,36 @@
+package network;
+
+import java.io.*;
+import java.net.*;
+
+import offline.Ref;
+
+public class NetworkWolfram
+{
+ private static Integer num = 1;
+
+ public String getWolfram(String input) throws IOException
+ {
+ String name = null;
+ synchronized(num)
+ {
+ name = "wolfram_" + num;
+ ++num;
+ }
+
+ URL url = new URL("http://api.wolframalpha.com/v1/simple?appid=" + Ref.wolfRamID + "&i="
+ + URLEncoder.encode(input, "UTF-8"));
+
+ InputStream in = new BufferedInputStream(url.openStream());
+ OutputStream out = new BufferedOutputStream(new FileOutputStream(name + ".png"));
+
+ for ( int i; (i = in.read()) != -1; )
+ {
+ out.write(i);
+ }
+
+ in.close();
+ out.close();
+ return name + ".png";
+ }
+}
\ No newline at end of file
diff --git a/src/utils/GlobalLog.java b/src/utils/GlobalLog.java
new file mode 100644
index 0000000..f350d2d
--- /dev/null
+++ b/src/utils/GlobalLog.java
@@ -0,0 +1,24 @@
+package utils;
+
+public class GlobalLog
+{
+ private static final String log = "Log";
+ private static final String warn = "Warning";
+ private static final String error = "ERROR";
+ private static final String fatal = "FATAL";
+
+ private static void Write(String status, LogFilter filter, String body)
+ {
+ System.out.println("[" + status + "] " + "[" + filter.name() + "] " + body);
+ }
+
+ public static void Log(String msg) { Write(log, LogFilter.Debug, msg); }
+ public static void Warn(String msg) {Write(warn, LogFilter.Debug, msg); }
+ public static void Error(String msg) { Write(error, LogFilter.Debug, msg); }
+ public static void Fatal(String msg) throws Exception { Write(fatal, LogFilter.Debug, msg); throw new Exception(msg); }
+
+ public static void Log(LogFilter filter, String msg) { Write(log, filter, msg); }
+ public static void Warn(LogFilter filter, String msg) { Write(warn, filter, msg); }
+ public static void Error(LogFilter filter, String msg) { Write(error, filter, msg); }
+ public static void Fatal(LogFilter filter, String msg) throws Exception { Write(fatal, filter, msg); throw new Exception(msg); }
+}
diff --git a/src/utils/HTTPUtils.java b/src/utils/HTTPUtils.java
new file mode 100644
index 0000000..b9355a1
--- /dev/null
+++ b/src/utils/HTTPUtils.java
@@ -0,0 +1,111 @@
+package utils;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+
+
+/**
+ * Adjustments to the excellent code provided via:
+ * https://www.journaldev.com/7148/java-httpurlconnection-example-java-http-request-get-post
+ * @author Wisp
+ */
+public class HTTPUtils
+{
+
+ // Configuration for requests
+ public static final String USER_AGENT = "KittyBot/2.0";
+
+
+ // Sends a GET request for the specified URL.
+ public static String SendGETRequest(String url)
+ {
+ try
+ {
+ // Get and open a connection, stored in connection object
+ URL obj = new URL(url);
+ HttpURLConnection con = (HttpURLConnection) obj.openConnection();
+
+ // Set ourselves as if we're "vaguely netscape"
+ con.setRequestMethod("GET");
+ con.setRequestProperty("User-Agent", USER_AGENT);
+
+ // Handle String
+ return (handleHttpResponse(con));
+ }
+ catch(IOException e)
+ {
+ return new String();
+ }
+ }
+
+
+ // Sends a POST request to the desired URL.
+ // There are two forms of this function, one of which takes
+ public static String SendPOSTRequest(String url) { return SendPOSTRequest(url, ""); }
+ public static String SendPOSTRequest(String url, String params)
+ {
+ try
+ {
+ // Get and open a connection, stored in connection object
+ URL obj = new URL(url);
+ HttpURLConnection con = (HttpURLConnection) obj.openConnection();
+
+ // Set ourselves as if we're "vaguely netscape"
+ con.setRequestMethod("POST");
+ con.setRequestProperty("User-Agent", USER_AGENT);
+
+ // For POST only - Configure parameters
+ con.setDoOutput(true);
+ OutputStream os = con.getOutputStream();
+ os.write(params.getBytes());
+ os.flush();
+ os.close();
+
+ // Handle String
+ return (handleHttpResponse(con));
+ }
+ catch(IOException e)
+ {
+ return new String();
+ }
+ }
+
+
+ // Internal static function for parsing HTTP Responses based on the
+ // connection object provided by java. Assumes you only want to parse
+ // valid connections, others are discarded.
+ private static String handleHttpResponse(HttpURLConnection con) throws IOException
+ {
+ // Check String code and act on it accordingly.
+ int responseCode = con.getResponseCode();
+
+ // If we succeeded...
+ if (responseCode == HttpURLConnection.HTTP_OK)
+ {
+ // Variable declaration
+ String inputLine;
+ StringBuffer String = new StringBuffer();
+ BufferedReader in = new BufferedReader(
+ new InputStreamReader(con.getInputStream()));
+
+ // Read all lines from the String
+ inputLine = in.readLine();
+ while (inputLine != null) {
+ String.append('\n');
+ String.append(inputLine);
+ inputLine = in.readLine();
+ }
+
+ // Close and return our data
+ in.close();
+ return new String(String.toString());
+ } else {
+ GlobalLog.Error(LogFilter.Network, con.getRequestMethod() + " responded with " + responseCode + " instead of 200.");
+ return Integer.toString(responseCode);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/utils/ImageUtils.java b/src/utils/ImageUtils.java
new file mode 100644
index 0000000..e85af12
--- /dev/null
+++ b/src/utils/ImageUtils.java
@@ -0,0 +1,94 @@
+package utils;
+
+import java.awt.Color;
+import java.awt.image.BufferedImage;
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+
+public class ImageUtils
+{
+ private static Long uniqueID = 0l;
+
+ // Downloads the file at the url and assigns it a unique local string.
+ // The string (filename) is returned, and can be used. It will have to be
+ // deleted later. By assigning a unique name to the files, this is
+ public static String DownloadFromURL(String URL, String extension)
+ {
+ String name = null;
+ synchronized(uniqueID)
+ {
+ name = "imagedownloaderutil_" + (uniqueID++) + extension;
+ }
+
+ try
+ {
+ URL yeetee = new URL(URL);
+
+ HttpURLConnection con = (HttpURLConnection) yeetee.openConnection();
+
+ con.setRequestMethod("GET");
+ con.setRequestProperty("User-Agent", HTTPUtils.USER_AGENT);
+
+ // Check String code and act on it accordingly.
+ con.getResponseCode();
+
+ InputStream in = con.getInputStream();
+ OutputStream out = new BufferedOutputStream(new FileOutputStream(name));
+ for ( int i; (i = in.read()) != -1; )
+ {
+ out.write(i);
+ }
+
+ in.close();
+ out.close();
+
+ return name;
+ }
+ catch(Exception e)
+ {
+ return null;
+ }
+ }
+
+ // Deletes the file provided, if it exists. Will continue, in a blocking way,
+ // to attempt to delete the file 10 times a second until thread termination.
+ public static void BlockingFileDelete(File file)
+ {
+ if(file == null)
+ return;
+
+ if(!file.exists())
+ return;
+
+ do
+ {
+ try { Thread.sleep(100); }
+ catch (InterruptedException e) { }
+ }
+ while(!file.delete());
+ }
+
+ public static BufferedImage copyImage(BufferedImage image)
+ {
+ int width = image.getWidth();
+ int height = image.getHeight();
+
+ BufferedImage out = new BufferedImage(width, height, BufferedImage.TYPE_4BYTE_ABGR);
+
+ for(int x = 0; x < width; ++x)
+ {
+ for(int y = 0; y < height; ++y)
+ {
+ Color c = new Color(image.getRGB(x, y), true);
+ out.setRGB(x, y, c.getRGB());
+ }
+ }
+
+ return out;
+ }
+}
diff --git a/src/utils/LogFilter.java b/src/utils/LogFilter.java
new file mode 100644
index 0000000..833c440
--- /dev/null
+++ b/src/utils/LogFilter.java
@@ -0,0 +1,17 @@
+package utils;
+
+public enum LogFilter
+{
+ Debug(0), Command(1), Core(2), Database(4), Response(8), Network(16); //8, 16, 32... (flags, so we can | together later)
+
+ private final int value;
+ private LogFilter(int value)
+ {
+ this.value = value;
+ }
+
+ public int getValue()
+ {
+ return value;
+ }
+}
diff --git a/src/utils/OptionParser.java b/src/utils/OptionParser.java
new file mode 100644
index 0000000..bc23853
--- /dev/null
+++ b/src/utils/OptionParser.java
@@ -0,0 +1,126 @@
+package utils;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+
+public class OptionParser
+{
+ public final char OptionIndicator = '-';
+
+ private class Option
+ {
+ public String optionRaw = "";
+ public String argRaw = null;
+
+ public Option(String option, String arg)
+ {
+ optionRaw = option;
+
+ if(arg != null && arg.length() == 0)
+ arg = null;
+
+ argRaw = arg;
+ }
+ }
+
+ // lowercase option to Option object
+ // -a argument -B -c would result in the following:
+ // Key: a, Value: a, argument
+ // Key: b, Value: B,
+ // Key: c, Value: c,
+ private HashMap lookup;
+
+ // Accumulates the freefloating options
+ private ArrayList floating;
+
+ // Constructor
+ public OptionParser(String toParse)
+ {
+ lookup = new HashMap();
+ floating = new ArrayList();
+
+ if(toParse != null && toParse.length() > 0)
+ Parse(toParse);
+ }
+
+ public void Parse(String vals)
+ {
+ String[] strings = vals.split("\\s+");
+
+ // Clean
+ for(int i = 0; i < strings.length; ++i)
+ strings[i] = strings[i].trim();
+
+ // Add to map
+ for(int i = 0; i < strings.length; ++i)
+ {
+ if(strings[i] != null && strings[i].length() > 0)
+ {
+ if(strings[i].charAt(0) == OptionIndicator && strings[i].length() > 1)
+ {
+ String option = "" + strings[i].charAt(1);
+
+ if(i + 1 < strings.length && strings[i + 1].charAt(0) != OptionIndicator)
+ {
+ lookup.put(option.toLowerCase(), new Option(option, strings[i + 1]));
+ ++i;
+ }
+ else
+ {
+ String val = null;
+
+ if(strings[i].length() > 2)
+ val = strings[i].substring(2, strings[i].length());
+
+ lookup.put(option.toLowerCase(), new Option(option, val));
+ }
+ }
+ else
+ {
+ floating.add(strings[i]);
+ }
+ }
+
+ }
+ }
+
+ public ArrayList GetFloating()
+ {
+ return floating;
+ }
+
+ public String GetOption(String arg)
+ {
+ return GetOption(arg, false);
+ }
+
+ public String GetOption(String option, boolean isCaseInsensitive)
+ {
+ if(option == null)
+ return null;
+
+ option.trim();
+
+ if(option.length() == 0)
+ return null;
+
+ if(option.charAt(0) == OptionIndicator && option.length() > 1)
+ option = option.substring(1, option.length());
+
+ Option parsedOption = lookup.get(option.toLowerCase());
+ if(parsedOption != null)
+ {
+ if(isCaseInsensitive)
+ {
+ return parsedOption.argRaw;
+ }
+ else
+ {
+ if(parsedOption.optionRaw == option)
+ return parsedOption.argRaw;
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/yeet/frames/yeet 0.png b/yeet/frames/yeet 0.png
new file mode 100644
index 0000000..4646793
Binary files /dev/null and b/yeet/frames/yeet 0.png differ
diff --git a/yeet/frames/yeet 1.png b/yeet/frames/yeet 1.png
new file mode 100644
index 0000000..ca4ce04
Binary files /dev/null and b/yeet/frames/yeet 1.png differ
diff --git a/yeet/frames/yeet 10.png b/yeet/frames/yeet 10.png
new file mode 100644
index 0000000..d5d540b
Binary files /dev/null and b/yeet/frames/yeet 10.png differ
diff --git a/yeet/frames/yeet 11.png b/yeet/frames/yeet 11.png
new file mode 100644
index 0000000..ff584a5
Binary files /dev/null and b/yeet/frames/yeet 11.png differ
diff --git a/yeet/frames/yeet 12.png b/yeet/frames/yeet 12.png
new file mode 100644
index 0000000..205094c
Binary files /dev/null and b/yeet/frames/yeet 12.png differ
diff --git a/yeet/frames/yeet 13.png b/yeet/frames/yeet 13.png
new file mode 100644
index 0000000..6d1ef61
Binary files /dev/null and b/yeet/frames/yeet 13.png differ
diff --git a/yeet/frames/yeet 14.png b/yeet/frames/yeet 14.png
new file mode 100644
index 0000000..e69440e
Binary files /dev/null and b/yeet/frames/yeet 14.png differ
diff --git a/yeet/frames/yeet 15.png b/yeet/frames/yeet 15.png
new file mode 100644
index 0000000..b51c7e2
Binary files /dev/null and b/yeet/frames/yeet 15.png differ
diff --git a/yeet/frames/yeet 16.png b/yeet/frames/yeet 16.png
new file mode 100644
index 0000000..683d00a
Binary files /dev/null and b/yeet/frames/yeet 16.png differ
diff --git a/yeet/frames/yeet 17.png b/yeet/frames/yeet 17.png
new file mode 100644
index 0000000..8997f49
Binary files /dev/null and b/yeet/frames/yeet 17.png differ
diff --git a/yeet/frames/yeet 18.png b/yeet/frames/yeet 18.png
new file mode 100644
index 0000000..3a19846
Binary files /dev/null and b/yeet/frames/yeet 18.png differ
diff --git a/yeet/frames/yeet 19.png b/yeet/frames/yeet 19.png
new file mode 100644
index 0000000..657b6bc
Binary files /dev/null and b/yeet/frames/yeet 19.png differ
diff --git a/yeet/frames/yeet 2.png b/yeet/frames/yeet 2.png
new file mode 100644
index 0000000..e89dc8d
Binary files /dev/null and b/yeet/frames/yeet 2.png differ
diff --git a/yeet/frames/yeet 20.png b/yeet/frames/yeet 20.png
new file mode 100644
index 0000000..2a21759
Binary files /dev/null and b/yeet/frames/yeet 20.png differ
diff --git a/yeet/frames/yeet 21.png b/yeet/frames/yeet 21.png
new file mode 100644
index 0000000..d3146fd
Binary files /dev/null and b/yeet/frames/yeet 21.png differ
diff --git a/yeet/frames/yeet 22.png b/yeet/frames/yeet 22.png
new file mode 100644
index 0000000..c2ebcae
Binary files /dev/null and b/yeet/frames/yeet 22.png differ
diff --git a/yeet/frames/yeet 23.png b/yeet/frames/yeet 23.png
new file mode 100644
index 0000000..571a7b7
Binary files /dev/null and b/yeet/frames/yeet 23.png differ
diff --git a/yeet/frames/yeet 3.png b/yeet/frames/yeet 3.png
new file mode 100644
index 0000000..b6c4089
Binary files /dev/null and b/yeet/frames/yeet 3.png differ
diff --git a/yeet/frames/yeet 4.png b/yeet/frames/yeet 4.png
new file mode 100644
index 0000000..d1eaa98
Binary files /dev/null and b/yeet/frames/yeet 4.png differ
diff --git a/yeet/frames/yeet 5.png b/yeet/frames/yeet 5.png
new file mode 100644
index 0000000..febf7f7
Binary files /dev/null and b/yeet/frames/yeet 5.png differ
diff --git a/yeet/frames/yeet 6.png b/yeet/frames/yeet 6.png
new file mode 100644
index 0000000..0959b54
Binary files /dev/null and b/yeet/frames/yeet 6.png differ
diff --git a/yeet/frames/yeet 7.png b/yeet/frames/yeet 7.png
new file mode 100644
index 0000000..9cfe7c1
Binary files /dev/null and b/yeet/frames/yeet 7.png differ
diff --git a/yeet/frames/yeet 8.png b/yeet/frames/yeet 8.png
new file mode 100644
index 0000000..68a0c93
Binary files /dev/null and b/yeet/frames/yeet 8.png differ
diff --git a/yeet/frames/yeet 9.png b/yeet/frames/yeet 9.png
new file mode 100644
index 0000000..911e2ce
Binary files /dev/null and b/yeet/frames/yeet 9.png differ
diff --git a/yeet/kirb.png b/yeet/kirb.png
new file mode 100644
index 0000000..589cc8e
Binary files /dev/null and b/yeet/kirb.png differ