<|start_filename|>src/utility/Utility.java<|end_filename|>
package utility;

import java.awt.Point;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringReader;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public final class Utility
{

	public static boolean toBool(int i)
	{
		return (i != 0);
	}

	public static String boxText(String text)
	{
		StringBuilder boxedText = new StringBuilder();
		boxedText.append(
				"\u250C" + Utility.fillWithChar(text.length() + 2, '\u2500') + "\u2510" + System.lineSeparator());
		boxedText.append("\u2502 " + text + " \u2502 " + System.lineSeparator());
		boxedText.append("\u2514" + Utility.fillWithChar(text.length() + 2, '\u2500') + "\u2518");
		return boxedText.toString();
	}

	public static void writeToFile(String s, File f) throws IOException
	{
		try (BufferedReader getter = new BufferedReader(new StringReader(s));
				PrintWriter writer = new PrintWriter(f, "UTF-8");)
		{
			getter.lines().forEach(line -> writer.println(line));
			writer.close();
		}
	}

	public static void recursiveFileSearch(File dir, List<Path> files)
	{
		File[] file_array = dir.listFiles();
		for (File f : file_array)
		{
			if (f.isFile() && f.toString().endsWith(".evtc") || f.toString().endsWith(".zip"))
			{
				files.add(f.toPath());
			}
			else if (f.isDirectory())
			{
				recursiveFileSearch(f, files);
			}
		}
	}

	public static <K, V extends Comparable<? super V>> Map<K, V> sortByValue(Map<K, V> map)
	{
		return map.entrySet().stream().sorted(Map.Entry.comparingByValue(Collections.reverseOrder()))
				.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (e1, e2) -> e1, LinkedHashMap::new));
	}

	public static String centerString(String text, int len)
	{
		String output = String.format("%" + len + "s%s%" + len + "s", "", text, "");
		float start = (output.length() / 2) - (len / 2);
		return output.substring((int) start, (int) (start + len));
	}

	public static boolean isNumeric(String str)
	{
		try
		{
			Double.parseDouble(str);
		} catch (NumberFormatException nfe)
		{
			return false;
		}
		return true;
	}

	public static String rightAlignString(String text, int len)
	{
		while (text.length() < len)
		{
			text = " " + text;

		}
		return text;
	}

	public static String leftAlignString(String text, int len)
	{
		while (text.length() < len)
		{
			text += " ";

		}
		return text;
	}

	public static String fillWithChar(int len, char c)
	{
		if (len > 0)
		{
			char[] array = new char[len];
			Arrays.fill(array, c);
			return new String(array);
		}
		return "";
	}

	public static List<Point> mergeIntervals(List<Point> intervals)
	{

		if (intervals.size() == 1)
		{
			return intervals;
		}

		List<Point> merged = new ArrayList<Point>();
		int x = intervals.get(0).x;
		int y = intervals.get(0).y;

		for (int i = 1; i < intervals.size(); i++)
		{
			Point current = intervals.get(i);
			if (current.x <= y)
			{
				y = Math.max(current.y, y);
			}
			else
			{
				merged.add(new Point(x, y));
				x = current.x;
				y = current.y;
			}
		}

		merged.add(new Point(x, y));

		return merged;
	}

	public static String[] concatStringArray(String[] a, String[] b)
	{
		int i = a.length;
		int j = b.length;
		String[] output = new String[i + j];
		System.arraycopy(a, 0, output, 0, i);
		System.arraycopy(b, 0, output, i, j);
		return output;
	}

}

<|start_filename|>src/player/BoonLog.java<|end_filename|>
package player;

public class BoonLog {

	// Fields
	private int time = 0;
	private int value = 0;

	// Constructor
	public BoonLog(int time, int value) {
		this.time = time;
		this.value = value;
	}

	// Getters
	public int getTime() {
		return time;
	}

	public int getValue() {
		return value;
	}

}


<|start_filename|>src/player/DamageLog.java<|end_filename|>
package player;

import enums.Result;

public class DamageLog
{

	// Fields
	private int time;
	private int damage;
	private int skill_id;
	private int buff;
	private Result result;
	private int is_ninety;
	private int is_moving;
	private int is_flanking;

	// Constructor
	public DamageLog(int time, int damage, int skill_id, int buff, Result result, int is_ninety, int is_moving,
			int is_flanking)
	{
		this.time = time;
		this.damage = damage;
		this.skill_id = skill_id;
		this.buff = buff;
		this.result = result;
		this.is_ninety = is_ninety;
		this.is_moving = is_moving;
		this.is_flanking = is_flanking;
	}

	// Getters
	public int getTime()
	{
		return time;
	}

	public int getDamage()
	{
		return damage;
	}

	public int getID()
	{
		return skill_id;
	}

	public int isCondi()
	{
		return buff;
	}

	public Result getResult()
	{
		return result;
	}

	public int isNinety()
	{
		return is_ninety;
	}

	public int isMoving()
	{
		return is_moving;
	}

	public int isFlanking()
	{
		return is_flanking;
	}

}


<|start_filename|>src/boon/Duration.java<|end_filename|>
package boon;

import java.util.List;

public class Duration extends AbstractBoon {

	// Constructor
	public Duration(int capacity) {
		super(capacity);
	}

	// Public Methods
	@Override
	public int getStackValue() {
		return boon_stack.stream().mapToInt(Integer::intValue).sum();
	}

	@Override
	public void update(int time_passed) {

		if (!boon_stack.isEmpty()) {
			// Clear stack
			if (time_passed >= getStackValue()) {
				boon_stack.clear();
				return;
			}
			// Remove from the longest duration
			else {
				boon_stack.set(0, boon_stack.get(0) - time_passed);
				if (boon_stack.get(0) <= 0) {
					// Spend leftover time
					time_passed = Math.abs(boon_stack.get(0));
					boon_stack.remove(0);
					update(time_passed);
				}
			}
		}
	}

	@Override
	public void addStacksBetween(List<Integer> boon_stacks, int time_between) {
	}

}


<|start_filename|>src/data/SkillItem.java<|end_filename|>
package data;

public class SkillItem {

	// Fields
	private int ID;
	private String name;

	// Constructor
	public SkillItem(int ID, String name) {
		this.ID = ID;
		this.name = name;
	}

	// Public Methods
	public String[] toStringArray() {
		String[] array = new String[2];
		array[0] = String.valueOf(ID);
		array[1] = String.valueOf(name);
		return array;
	}

	// Getters
	public int getID() {
		return ID;
	}

	public String getName() {
		return name;
	}

}


<|start_filename|>src/enums/Agent.java<|end_filename|>
package enums;

public enum Agent
{

	// Constants
	NPC(-1, "NPC"),
	GADGET(0, "GDG"),
	GUARDIAN(1, "Guardian"),
	WARRIOR(2, "Warrior"),
	ENGINEER(3, "Engineer"),
	RANGER(4, "Ranger"),
	THIEF(5, "Thief"),
	ELEMENTALIST(6, "Elementalist"),
	MESMER(7, "Mesmer"),
	NECROMANCER(8, "Necromancer"),
	REVENANT(9, "Revenant"),
	DRAGONHUNTER(10, "Dragonhunter"),
	BERSERKER(11, "Berserker"),
	SCRAPPER(12, "Scrapper"),
	DRUID(13, "Druid"),
	DAREDEVIL(14, "Daredevil"),
	TEMPEST(15, "Tempest"),
	CHRONOMANCER(16, "Chronomancer"),
	REAPER(17, "Reaper"),
	HERALD(18, "Herald");

	// Fields
	private String name;
	private int ID;

	// Constructor
	Agent(int ID, String name)
	{
		this.name = name;
		this.ID = ID;
	}

	// Public Methods
	public static Agent getEnum(int ID, int is_elite)
	{
		for (Agent p : values())
		{
			if (is_elite == -1)
			{
				if ((ID & 0xffff0000) == 0xffff0000)
				{
					return Agent.GADGET;
				}
				else
				{
					return Agent.NPC;
				}
			}
			else if (is_elite == 0)
			{
				if (p.getID() == ID)
				{
					return p;
				}
			}
			else if (is_elite == 1)
			{
				if (p.getID() == ID + 9)
				{
					return p;
				}
			}
		}
		return null;
	}

	// Getters
	public String getName()
	{
		return name;
	}

	public int getID()
	{
		return ID;
	}

}


<|start_filename|>src/enums/IFF.java<|end_filename|>
package enums;

public enum IFF {

	// Constants
	FRIEND(0),
	FOE(1),
	UNKNOWN(2);

	// Fields
	private int ID;

	// Constructors
	private IFF(int ID) {
		this.ID = ID;
	}

	// Public Methods
	public static IFF getEnum(int ID) {
		for (IFF i : values()) {
			if (i.getID() == ID) {
				return i;
			}
		}
		return null;
	}

	// Getters
	public int getID() {
		return ID;
	}
}


<|start_filename|>src/enums/CustomSkill.java<|end_filename|>
package enums;

public enum CustomSkill
{

	// Constants
	RESURRECT(1066, "Resurrect"),
	BANDAGE(1175, "Bandage"),
	DODGE(65001, "Dodge");

	// Fields
	private int ID;
	private String name;

	// Constructors
	private CustomSkill(int ID, String name)
	{
		this.ID = ID;
		this.name = name;
	}

	// Public Methods
	public static CustomSkill getEnum(int ID)
	{
		for (CustomSkill c : values())
		{
			if (c.getID() == ID)
			{
				return c;
			}
		}
		return null;
	}

	// Getters
	public int getID()
	{
		return ID;
	}

	public String getName()
	{
		return name;
	}

}


<|start_filename|>src/data/SkillData.java<|end_filename|>
package data;

import java.util.ArrayList;
import java.util.List;

import enums.CustomSkill;

public class SkillData
{

	// Fields
	private List<SkillItem> skill_list;

	// Constructors
	public SkillData()
	{
		this.skill_list = new ArrayList<SkillItem>();
	}

	// Public Methods
	public void addItem(SkillItem item)
	{
		skill_list.add(item);
	}

	public String getName(int ID)
	{

		// Custom
		CustomSkill custom_skill = CustomSkill.getEnum(ID);
		if (custom_skill != null)
		{
			return custom_skill.name();
		}

		// Normal
		for (SkillItem s : skill_list)
		{
			if (s.getID() == ID)
			{
				return s.getName();
			}
		}

		// Unknown
		return "uid: " + String.valueOf(ID);
	}

	// Getters
	public List<SkillItem> getSkillList()
	{
		return skill_list;
	}

}


<|start_filename|>src/data/BossData.java<|end_filename|>
package data;

public class BossData
{

	// Fields
	private long agent = 0;
	private int instid = 0;
	private int first_aware = 0;
	private int last_aware = Integer.MAX_VALUE;
	private int id;
	private String name = "UNKNOWN";
	private int health = -1;

	// Constructors
	public BossData(int id)
	{
		this.id = id;
	}

	// Public Methods
	public String[] getPhaseNames()
	{
		if (name.equals("<NAME>"))
		{
			return new String[] { "100% - 66%", "66% - 33%", "33% - 0%" };
		}
		else if (name.equals("<NAME>"))
		{
			return new String[] { "100% - 66%", "66% - 33%", "33% - 0%" };
		}
		else if (name.equals("<NAME>"))
		{
			return new String[] { "100% - 75%", "75% - 50%", "50% - 25%", "25% - 0%" };
		}
		else if (name.equals("Slothasor"))
		{
			return new String[] { "100% - 80%", "80% - 60%", "60% - 40%", "40% - 20%", "20% - 10%", "10% - 0%" };
		}
		else if (name.equals("<NAME>"))
		{
			return new String[] { "100% - 80%", "80% - 60%", "60% - 40%", "40% - 0%" };
		}
		else if (name.equals("<NAME>"))
		{
			return new String[] { "100% - 66%", "66% - 33%", "33% - 0%" };
		}
		else if (name.equals("Xera"))
		{
			return new String[] { "100% - 50%", "50% - 0%" };
		}
		else if (name.equals("<NAME>domitable"))
		{
			return new String[] { "100% - 75%", "75% - 50%", "50% - 25%", "25% - 0%" };
		}
		else if (name.equals("<NAME>"))
		{
			return new String[] { "100% - 75%", "75% - 50%", "50% - 25%", "25% - 0%" };
		}
		else if (name.equals("Samarog"))
		{
			return new String[] { "100% - 66%", "66% - 33%", "33% - 0%" };
		}
		else if (name.equals("Deimos"))
		{
			return new String[] { "100% - 75%", "75% - 50%", "50% - 25%", "25% - 10%" };
		}
		return new String[] { "100% - 0%" };
	}

	public String[] toStringArray()
	{
		String[] array = new String[7];
		array[0] = Long.toHexString(agent);
		array[1] = String.valueOf(instid);
		array[2] = String.valueOf(first_aware);
		array[3] = String.valueOf(last_aware);
		array[4] = String.valueOf(id);
		array[5] = name;
		array[6] = String.valueOf(health);
		return array;
	}

	// Getters
	public long getAgent()
	{
		return agent;
	}

	public int getInstid()
	{
		return instid;
	}

	public int getFirstAware()
	{
		return first_aware;
	}

	public int getLastAware()
	{
		return last_aware;
	}

	public int getID()
	{
		return id;
	}

	public String getName()
	{
		return name;
	}

	public int getHealth()
	{
		return health;
	}

	// Setters
	public void setAgent(long agent)
	{
		this.agent = agent;
	}

	public void setInstid(int instid)
	{
		this.instid = instid;
	}

	public void setFirstAware(int first_aware)
	{
		this.first_aware = first_aware;
	}

	public void setLastAware(int last_aware)
	{
		this.last_aware = last_aware;
	}

	public void setName(String name)
	{
		this.name = name;
	}

	public void setHealth(int health)
	{
		this.health = health;
	}

}


<|start_filename|>src/enums/Result.java<|end_filename|>
package enums;

public enum Result {

	// Constants
	NORMAL(0),
	CRIT(1),
	GLANCE(2),
	BLOCK(3),
	EVADE(4),
	INTERRUPT(5),
	ABSORB(6),
	BLIND(7),
	KILLING_BLOW(8);

	// Fields
	private int ID;

	// Constructors
	private Result(int ID) {
		this.ID = ID;
	}

	// Public Methods
	public static Result getEnum(int ID) {
		for (Result r : values()) {
			if (r.getID() == ID) {
				return r;
			}
		}
		return null;
	}

	// Getters
	public int getID() {
		return ID;
	}

}


<|start_filename|>src/enums/MenuChoice.java<|end_filename|>
package enums;

public enum MenuChoice {

	// Constants
	DUMP_EVTC(0, false),
	FINAL_DPS(1, true),
	PHASE_DPS(2, true),
	DMG_DIST(3, true),
	G_TOTAL_DMG(4, false),
	MISC_STATS(5, true),
	FINAL_BOONS(6, true),
	PHASE_BOONS(7, true),
	DUMP_TABLES(8, false),
	QUIT(9, false);

	// Fields
	private int ID;
	private boolean can_be_associated;

	// Constructor
	MenuChoice(int ID, boolean can_be_associated) {
		this.ID = ID;
		this.can_be_associated = can_be_associated;
	}

	// Public Methods
	public static MenuChoice getEnum(int ID) {
		for (MenuChoice c : values()) {
			if (c.getID() == ID) {
				return c;
			}
		}
		return null;
	}

	// Getters
	public int getID() {
		return ID;
	}

	public boolean canBeAssociated() {
		return can_be_associated;
	}

}


<|start_filename|>src/enums/Boon.java<|end_filename|>
package enums;

import java.util.ArrayList;
import java.util.List;

public enum Boon
{
	// Boon
	MIGHT("Might", "MGHT", "intensity", 25),
	QUICKNESS("Quickness", "QCKN", "duration", 5),
	FURY("Fury", "FURY", "duration", 9),
	PROTECTION("Protection", "PROT", "duration", 5),

	// Mesmer (also Ventari Revenant o.o)
	ALACRITY("Alacrity", "ALAC", "duration", 9),

	// Ranger
	SPOTTER("Spotter", "SPOT", "duration", 1),
	SPIRIT_OF_FROST("Spirit of Frost", "FRST", "duration", 1),
	SUN_SPIRIT("Sun Spirit", "SUNS", "duration", 1),
	STONE_SPIRIT("Stone Spirit", "STNE", "duration", 1),
	STORM_SPIRIT("Storm Spirit", "STRM", "duration", 1),
	GLYPH_OF_EMPOWERMENT("Glyph of Empowerment", "GOFE", "duration", 1),
	GRACE_OF_THE_LAND("Grace of the Land", "GOTL", "intensity", 5),

	// Warrior
	EMPOWER_ALLIES("Empower Allies", "EALL", "duration", 1),
	BANNER_OF_STRENGTH("Banner of Strength", "STRB", "duration", 1),
	BANNER_OF_DISCIPLINE("Banner of Discipline", "DISC", "duration", 1),
	BANNER_OF_TACTICS("Banner of Tactics", "TACT", "duration", 1),
	BANNER_OF_DEFENCE("Banner of Defence", "DEFN", "duration", 1),

	// Revenant
	ASSASSINS_PRESENCE("Assassin's Presence", "ASNP", "duration", 1),
	NATURALISTIC_RESONANCE("Naturalistic Resonance", "NATR", "duration", 1),

	// Engineer
	PINPOINT_PRECISION("Pinpoint Distribution", "PIND", "duration", 1),

	// Elementalist
	SOOTHING_MIST("Soothing Mist", "MIST", "duration", 1),

	// Necro
	VAMPIRIC_PRESENCE("Vampiric Presence", "VAMP", "duration", 1),

	// Thief
	LEAD_ATTACKS("Lead Attacks", "LEAD", "intensity", 15),
	LOTUS_TRAINING("Lotus Training", "LOTS", "duration", 1),
	BOUNDING_DODGER("Bounding Dodger", "BDOG", "duration", 1),

	// Equipment
	MASTERFUL_CONCENTRATION("Masterful Concentration", "CONC", "duration", 1);
	// THORNS_RUNE("Thorns", "THRN", "intensity", 5);

	// Fields
	private String name;
	private String abrv;
	private String type;
	private int capacity;

	// Constructor
	private Boon(String name, String abrv, String type, int capacity)
	{
		this.name = name;
		this.abrv = abrv;
		this.type = type;
		this.capacity = capacity;
	}

	// Public Methods
	public static Boon getEnum(String name)
	{
		for (Boon b : values())
		{
			if (b.getName() == name)
			{
				return b;
			}
		}
		return null;
	}

	public static String[] getArray()
	{
		List<String> boonList = new ArrayList<String>();
		for (Boon b : values())
		{
			boonList.add(b.getAbrv());
		}
		return boonList.toArray(new String[boonList.size()]);
	}

	public static List<String> getList()
	{
		List<String> boonList = new ArrayList<String>();
		for (Boon b : values())
		{
			boonList.add(b.getName());
		}
		return boonList;
	}

	// Getters
	public String getName()
	{
		return this.name;
	}

	public String getAbrv()
	{
		return this.abrv;
	}

	public String getType()
	{
		return this.type;
	}

	public int getCapacity()
	{
		return this.capacity;
	}
}


<|start_filename|>src/enums/Activation.java<|end_filename|>
package enums;

public enum Activation
{
	// Constants
	NONE(0),
	NORMAL(1),
	QUICKNESS(2),
	CANCEL_FIRE(3),
	CANCEL_CANCEL(4),
	RESET(5);

	// Fields
	private int ID;

	// Constructors
	private Activation(int ID)
	{
		this.ID = ID;
	}

	// Public Methods
	public static Activation getEnum(int ID)
	{
		for (Activation a : values())
		{
			if (a.getID() == ID)
			{
				return a;
			}
		}
		return null;
	}

	// Getters
	public int getID()
	{
		return ID;
	}
}


<|start_filename|>src/boon/BoonFactory.java<|end_filename|>
package boon;

import enums.Boon;

public class BoonFactory {

	// Factory
	public AbstractBoon makeBoon(Boon boon) {
		if (boon.getType().equals("intensity")) {
			return new Intensity(boon.getCapacity());
		} else if (boon.getType().equals("duration")) {
			return new Duration(boon.getCapacity());
		} else {
			return null;
		}
	}
}

<|start_filename|>src/boon/AbstractBoon.java<|end_filename|>
package boon;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public abstract class AbstractBoon {

	// Fields
	protected List<Integer> boon_stack = new ArrayList<Integer>();
	protected int capacity;

	// Constructor
	public AbstractBoon(int capacity) {
		this.capacity = capacity;
	}

	// Abstract Methods
	public abstract int getStackValue();

	public abstract void update(int time_passed);

	public abstract void addStacksBetween(List<Integer> boon_stacks, int time_between);

	// Public Methods
	public void add(int boon_duration) {
		// Find empty slot
		if (!isFull()) {
			boon_stack.add(boon_duration);
			sort();
		}
		// Replace lowest value
		else {
			int index = boon_stack.size() - 1;
			if (boon_stack.get(index) < boon_duration) {
				boon_stack.set(index, boon_duration);
				sort();
			}
		}
	}

	// Protected Methods
	protected boolean isFull() {
		return boon_stack.size() >= capacity;
	}

	protected void sort() {
		Collections.sort(boon_stack, Collections.reverseOrder());
	}

}


<|start_filename|>src/boon/Intensity.java<|end_filename|>
package boon;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;

public class Intensity extends AbstractBoon {

	// Constructor
	public Intensity(int capacity) {
		super(capacity);
	}

	// Public Methods
	@Override
	public int getStackValue() {
		return boon_stack.size();
	}

	@Override
	public void update(int time_passed) {

		// Subtract from each
		for (int i = 0; i < boon_stack.size(); i++) {
			boon_stack.set(i, boon_stack.get(i) - time_passed);
		}
		// Remove negatives
		for (Iterator<Integer> iter = boon_stack.listIterator(); iter.hasNext();) {
			Integer stack = iter.next();
			if (stack <= 0) {
				iter.remove();
			}
		}
	}

	@Override
	public void addStacksBetween(List<Integer> boon_stacks, int time_between) {

		// Create copy of the boon
		Intensity boon_copy = new Intensity(this.capacity);
		boon_copy.boon_stack = new ArrayList<Integer>(this.boon_stack);
		List<Integer> stacks = boon_copy.boon_stack;

		// Simulate the boon stack decreasing
		if (!stacks.isEmpty()) {

			int time_passed = 0;
			int min_duration = Collections.min(stacks);

			// Remove minimum duration from stack
			for (int i = 1; i < time_between; i++) {
				if ((i - time_passed) >= min_duration) {
					boon_copy.update(i - time_passed);
					if (!stacks.isEmpty()) {
						min_duration = Collections.min(stacks);
					}
					time_passed = i;
				}
				boon_stacks.add(boon_copy.getStackValue());
			}
		}
		// Fill in remaining time with 0 values
		else {
			for (int i = 1; i < time_between; i++) {
				boon_stacks.add(0);
			}
		}
	}

}


<|start_filename|>src/player/Player.java<|end_filename|>
package player;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import data.AgentItem;
import data.BossData;
import data.CombatItem;
import data.SkillData;
import enums.Boon;
import enums.IFF;
import enums.StateChange;
import statistics.Statistics;

public class Player
{
	// Fields
	private int instid;
	private String account;
	private String character;
	private String group;
	private String prof;
	private int toughness;
	private int healing;
	private int condition;
	private List<DamageLog> damage_logs = new ArrayList<DamageLog>();
	private Map<String, List<BoonLog>> boon_map = new HashMap<>();

	// Constructors
	public Player(AgentItem agent)
	{
		this.instid = agent.getInstid();
		String[] name = agent.getName().split(Character.toString('\0'));
		this.character = name[0];
		this.account = name[1];
		this.group = name[2];
		if (Statistics.hiding_players)
		{
			this.character = "P:" + String.format("%04d", instid);
			this.account = ":A." + String.format("%04d", instid);
		}
		this.prof = agent.getProf();
		this.toughness = agent.getToughness();
		this.healing = agent.getHealing();
		this.condition = agent.getCondition();
	}

	// Getters
	public int getInstid()
	{
		return instid;
	}

	public String getAccount()
	{
		return account;
	}

	public String getCharacter()
	{
		return character;
	}

	public String getGroup()
	{
		return group;
	}

	public String getProf()
	{
		return prof;
	}

	public int getToughness()
	{
		return toughness;
	}

	public int getHealing()
	{
		return healing;
	}

	public int getCondition()
	{
		return condition;
	}

	public List<DamageLog> getDamageLogs(BossData bossData, List<CombatItem> combatList)
	{
		if (damage_logs.isEmpty())
		{
			setDamageLogs(bossData, combatList);
		}
		return damage_logs;
	}

	public Map<String, List<BoonLog>> getBoonMap(BossData bossData, SkillData skillData, List<CombatItem> combatList)
	{
		if (boon_map.isEmpty())
		{
			setBoonMap(bossData, skillData, combatList);
		}
		return boon_map;
	}

	// Private Methods
	private void setDamageLogs(BossData bossData, List<CombatItem> combatList)
	{
		int time_start = bossData.getFirstAware();
		for (CombatItem c : combatList)
		{
			if (instid == c.getSrcInstid() || instid == c.getSrcMasterInstid())
			{
				StateChange state = c.isStateChange();
				int time = c.getTime() - time_start;
				if (bossData.getInstid() == c.getDstInstid() && c.getIFF().equals(IFF.FOE))
				{
					if (state.equals(StateChange.NORMAL))
					{
						if (c.isBuff() == 1 && c.getBuffDmg() != 0)
						{
							damage_logs.add(new DamageLog(time, c.getBuffDmg(), c.getSkillID(), c.isBuff(),
									c.getResult(), c.isNinety(), c.isMoving(), c.isFlanking()));
						}
						else if (c.isBuff() == 0 && c.getValue() != 0)
						{
							damage_logs.add(new DamageLog(time, c.getValue(), c.getSkillID(), c.isBuff(),
									c.getResult(), c.isNinety(), c.isMoving(), c.isFlanking()));
						}
					}
				}
			}
		}
	}

	public void setBoonMap(BossData bossData, SkillData skillData, List<CombatItem> combatList)
	{

		// Initialize Boon Map with every Boon
		for (Boon boon : Boon.values())
		{
			boon_map.put(boon.getName(), new ArrayList<BoonLog>());
		}

		// Fill in Boon Map
		int time_start = bossData.getFirstAware();
		int fight_duration = bossData.getLastAware() - time_start;
		for (CombatItem c : combatList)
		{
			if (instid == c.getDstInstid())
			{
				String skill_name = skillData.getName(c.getSkillID());
				if (c.isBuff() == 1 && c.getValue() > 0)
				{
					if (boon_map.containsKey(skill_name))
					{
						int time = c.getTime() - time_start;
						if (time < fight_duration)
						{
							boon_map.get(skill_name).add(new BoonLog(time, c.getValue()));
						}
						else
						{
							break;
						}
					}
				}
			}
		}

	}

}


<|start_filename|>src/data/AgentItem.java<|end_filename|>
package data;

public class AgentItem {

	// Fields
	private long agent;
	private int instid = 0;
	private int first_aware = 0;
	private int last_aware = Integer.MAX_VALUE;
	private String name;
	private String prof;
	private int toughness = 0;
	private int healing = 0;
	private int condition = 0;

	// Constructors
	public AgentItem(long agent, String name, String prof) {
		this.agent = agent;
		this.name = name;
		this.prof = prof;
	}

	public AgentItem(long agent, String name, String prof, int toughness, int healing, int condition) {
		this.agent = agent;
		this.name = name;
		this.prof = prof;
		this.toughness = toughness;
		this.healing = healing;
		this.condition = condition;
	}

	// Public Methods
	public String[] toStringArray() {
		String[] array = new String[9];
		array[0] = Long.toHexString(agent);
		array[1] = String.valueOf(instid);
		array[2] = String.valueOf(first_aware);
		array[3] = String.valueOf(last_aware);
		array[4] = name;
		array[5] = prof;
		array[6] = String.valueOf(toughness);
		array[7] = String.valueOf(healing);
		array[8] = String.valueOf(condition);
		return array;
	}

	// Getters
	public long getAgent() {
		return agent;
	}

	public int getInstid() {
		return instid;
	}

	public int getFirstAware() {
		return first_aware;
	}

	public int getLastAware() {
		return last_aware;
	}

	public String getName() {
		return name;
	}

	public String getProf() {
		return prof;
	}

	public int getToughness() {
		return toughness;
	}

	public int getHealing() {
		return healing;
	}

	public int getCondition() {
		return condition;
	}

	// Setters
	public void setInstid(int instid) {
		this.instid = instid;
	}

	public void setFirstAware(int first_aware) {
		this.first_aware = first_aware;
	}

	public void setLastAware(int last_aware) {
		this.last_aware = last_aware;
	}

}


<|start_filename|>src/data/LogData.java<|end_filename|>
package data;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.TimeZone;

public class LogData
{

	// Fields
	private String build_version;
	private String pov = "N/A";
	private String log_start = "yyyy-MM-dd HH:mm:ss z";
	private String log_end = "yyyy-MM-dd HH:mm:ss z";
	private SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss z");

	// Constructors
	public LogData(String build_version)
	{
		this.build_version = build_version;
		this.sdf.setTimeZone(TimeZone.getDefault());
	}

	// Public Methods
	public String[] toStringArray()
	{
		String[] array = new String[4];
		array[0] = String.valueOf(build_version);
		array[1] = String.valueOf(pov);
		array[2] = String.valueOf(log_start);
		array[3] = String.valueOf(log_end);
		return array;
	}

	// Getters
	public String getBuildVersion()
	{
		return build_version;
	}

	public String getPOV()
	{
		return pov;
	}

	public String getLogStart()
	{
		return log_start;
	}

	public String getLogEnd()
	{
		return log_end;
	}

	// Setters
	public void setPOV(String pov)
	{
		this.pov = pov.substring(0, pov.lastIndexOf('\0'));
	}

	public void setLogStart(int unix_seconds)
	{
		this.log_start = sdf.format(new Date(unix_seconds * 1000L));
	}

	public void setLogEnd(int unix_seconds)
	{
		this.log_end = sdf.format(new Date(unix_seconds * 1000L));
	}

}


<|start_filename|>src/data/CombatItem.java<|end_filename|>
package data;

import enums.Activation;
import enums.BuffRemove;
import enums.IFF;
import enums.Result;
import enums.StateChange;

public class CombatItem
{

	// Fields
	private int time;
	private long src_agent;
	private long dst_agent;
	private int value;
	private int buff_dmg;
	private int overstack_value;
	private int skill_id;
	private int src_instid;
	private int dst_instid;
	private int src_master_instid;
	private IFF iff;
	private int is_buff;
	private Result result;
	private Activation is_activation;
	private BuffRemove is_buffremove;
	private int is_ninety;
	private int is_fifty;
	private int is_moving;
	private StateChange is_statechange;
	private int is_flanking;

	// Constructor
	public CombatItem(int time, long src_agent, long dst_agent, int value, int buff_dmg, int overstack_value,
			int skill_id, int src_instid, int dst_instid, int src_master_instid, IFF iff, int buff, Result result,
			Activation is_activation, BuffRemove is_buffremove, int is_ninety, int is_fifty, int is_moving,
			StateChange is_statechange, int is_flanking)
	{
		this.time = time;
		this.src_agent = src_agent;
		this.dst_agent = dst_agent;
		this.value = value;
		this.buff_dmg = buff_dmg;
		this.overstack_value = overstack_value;
		this.skill_id = skill_id;
		this.src_instid = src_instid;
		this.dst_instid = dst_instid;
		this.src_master_instid = src_master_instid;
		this.iff = iff;
		this.is_buff = buff;
		this.result = result;
		this.is_activation = is_activation;
		this.is_buffremove = is_buffremove;
		this.is_ninety = is_ninety;
		this.is_fifty = is_fifty;
		this.is_moving = is_moving;
		this.is_statechange = is_statechange;
		this.is_flanking = is_flanking;
	}

	// Public Methods
	public String[] toStringArray()
	{
		String[] array = new String[20];
		array[0] = String.valueOf(time);
		array[1] = Long.toHexString(src_agent);
		array[2] = Long.toHexString(dst_agent);
		array[3] = String.valueOf(value);
		array[4] = String.valueOf(buff_dmg);
		array[5] = String.valueOf(overstack_value);
		array[6] = String.valueOf(skill_id);
		array[7] = String.valueOf(src_instid);
		array[8] = String.valueOf(dst_instid);
		array[9] = String.valueOf(src_master_instid);
		array[10] = String.valueOf(iff);
		array[11] = String.valueOf(is_buff);
		array[12] = String.valueOf(result);
		array[13] = String.valueOf(is_activation);
		array[14] = String.valueOf(is_buffremove);
		array[15] = String.valueOf(is_ninety);
		array[16] = String.valueOf(is_fifty);
		array[17] = String.valueOf(is_moving);
		array[18] = String.valueOf(is_statechange);
		array[19] = String.valueOf(is_flanking);
		return array;
	}

	// Getters
	public int getTime()
	{
		return time;
	}

	public long getSrcAgent()
	{
		return src_agent;
	}

	public long getDstAgent()
	{
		return dst_agent;
	}

	public int getValue()
	{
		return value;
	}

	public int getBuffDmg()
	{
		return buff_dmg;
	}

	public int getOverstackValue()
	{
		return overstack_value;
	}

	public int getSkillID()
	{
		return skill_id;
	}

	public int getSrcInstid()
	{
		return src_instid;
	}

	public int getDstInstid()
	{
		return dst_instid;
	}

	public int getSrcMasterInstid()
	{
		return src_master_instid;
	}

	public IFF getIFF()
	{
		return iff;
	}

	public int isBuff()
	{
		return is_buff;
	}

	public Result getResult()
	{
		return result;
	}

	public Activation isActivation()
	{
		return is_activation;
	}

	public BuffRemove isBuffremove()
	{
		return is_buffremove;
	}

	public int isNinety()
	{
		return is_ninety;
	}

	public int isFifty()
	{
		return is_fifty;
	}

	public int isMoving()
	{
		return is_moving;
	}

	public int isFlanking()
	{
		return is_flanking;
	}

	public StateChange isStateChange()
	{
		return is_statechange;
	}

	// Setters
	public void setSrcAgent(long src_agent)
	{
		this.src_agent = src_agent;
	}

	public void setDstAgent(long dst_agent)
	{
		this.dst_agent = dst_agent;
	}

	public void setSrcInstid(int src_instid)
	{
		this.src_instid = src_instid;
	}

	public void setDstInstid(int dst_instid)
	{
		this.dst_instid = dst_instid;
	}

}

<|start_filename|>src/data/AgentData.java<|end_filename|>
package data;

import java.util.ArrayList;
import java.util.List;

import enums.Agent;

public class AgentData {

	// Fields
	private List<AgentItem> player_agent_list = new ArrayList<AgentItem>();
	private List<AgentItem> NPC_agent_list = new ArrayList<AgentItem>();
	private List<AgentItem> gadget_agent_list = new ArrayList<AgentItem>();
	private List<AgentItem> all_agents_list = new ArrayList<AgentItem>();

	// Constructors
	public AgentData() {
	}

	// Public Methods
	public void addItem(Agent agent, AgentItem item) {
		if (agent.equals(Agent.NPC)) {
			NPC_agent_list.add(item);
		} else if (agent.equals(Agent.GADGET)) {
			gadget_agent_list.add(item);
		} else {
			player_agent_list.add(item);
		}
		all_agents_list.add(item);
	}

	// Getters
	public List<AgentItem> getPlayerAgentList() {
		return player_agent_list;
	}

	public List<AgentItem> getNPCAgentList() {
		return NPC_agent_list;
	}

	public List<AgentItem> getGadgetAgentList() {
		return gadget_agent_list;
	}

	public List<AgentItem> getAllAgentsList() {
		return all_agents_list;
	}

}


<|start_filename|>src/Main.java<|end_filename|>
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Scanner;

import enums.MenuChoice;
import statistics.Parse;
import statistics.Statistics;
import utility.TableBuilder;
import utility.Utility;

public class Main
{

	// Fields
	private static boolean will_quit = false;
	private static boolean displaying = true;
	private static Map<String, String> argument_map = new HashMap<>();
	private static String old_version = Utility.boxText("ERROR   : This only supports versions 20170218 and onwards");
	private static String current_file;
	private static Parse parsed_file;
	private static Statistics statistics;

	// Main
	public static void main(String[] args)
	{
		// Scanner
		Scanner scan = null;
		try
		{
			scan = new Scanner(System.in);

			// Read arguments
			for (String arg : args)
			{
				if (arg.contains("="))
				{
					argument_map.put(arg.substring(0, arg.indexOf('=')), arg.substring(arg.indexOf('=') + 1));
				}
			}
			String is_anon = argument_map.get("is_anon");
			String is_displaying = argument_map.get("is_displaying");
			String file_path = argument_map.get("file_path");
			String options = argument_map.get("options");

			// Handle arguments
			if (is_anon != null)
			{
				Statistics.hiding_players = Utility.toBool(Integer.valueOf(is_anon));
			}
			if (is_displaying != null)
			{
				displaying = Utility.toBool(Integer.valueOf(is_displaying));
			}

			// File Association
			if (file_path != null && !file_path.isEmpty() && options != null)
			{
				int[] choices = options.chars().map(x -> x - '0').toArray();

				StringBuilder output = new StringBuilder();
				if (displaying)
				{
					System.out.println((Utility.boxText("Log Data")));
				}
				for (int i : choices)
				{
					MenuChoice c = MenuChoice.getEnum(i);
					if (c != null && c.canBeAssociated())
					{
						String result = parseFileByChoice(c, Paths.get(file_path));
						if (result.contains("ERROR"))
						{
							System.out.println(old_version);
							scan.nextLine();
							return;
						}
						else
						{
							output.append(result + System.lineSeparator());
						}
					}
				}
				System.out.println(output.toString());
				scan.nextLine();
				return;
			}

			// Menu
			else
			{
				// Create required directories
				new File("./logs").mkdir();
				new File("./graphs").mkdirs();
				new File("./tables").mkdirs();

				// Obtain list of .evtc files in /logs/
				List<Path> log_files = new ArrayList<Path>();
				File log_folder = new File("./logs");
				Utility.recursiveFileSearch(log_folder, log_files);

				// No logs to process
				if (log_files.isEmpty())
				{
					try
					{
						System.out.println(Utility.boxText("ERROR : No log files found at \""
								+ log_folder.getCanonicalPath().toString() + "\" ... press Enter to exit"));
					} catch (IOException e)
					{
						e.printStackTrace();
					}
					scan.nextLine();
					return;
				}
				// There are logs to process
				else
				{
					while (!will_quit)
					{
						// Display menu
						System.out.println("\u250C" + Utility.fillWithChar(24, '\u2500') + "\u2510");
						System.out.println("\u2502    EVTC Log Parser     \u2502");
						System.out.println("\u251C" + Utility.fillWithChar(24, '\u2500') + "\u2524");
						System.out.println("\u2502 0. Dump EVTC           \u2502");
						System.out.println("\u2502 1. Final DPS           \u2502");
						System.out.println("\u2502 2. Phase DPS           \u2502");
						System.out.println("\u2502 3. Damage Distribution \u2502");
						System.out.println("\u2502 4. Graph Total Damage  \u2502");
						System.out.println("\u2502 5. Misc. Combat Stats  \u2502");
						System.out.println("\u2502 6. Final Boon Rates    \u2502");
						System.out.println("\u2502 7. Phase Boon Rates    \u2502");
						System.out.println("\u2502 8. Dump All Tables     \u2502");
						System.out.println("\u2502 9. Quit                \u2502");
						System.out.println("\u2514" + Utility.fillWithChar(24, '\u2500') + "\u2518");

						// Read user input
						MenuChoice choice = null;
						System.out.println(Utility.boxText("Enter an option by number"));
						System.out.print(" >> ");
						if (scan.hasNextInt())
						{
							System.out.println(System.lineSeparator() + Utility.fillWithChar(50, '\u2500')
									+ System.lineSeparator());
							choice = MenuChoice.getEnum(scan.nextInt());
						}
						scan.nextLine();

						// Invalid option
						if (choice == null)
						{
							System.out.println(Utility.boxText("WARNING : Invalid option"));
						}
						// Quit
						else if (choice.equals(MenuChoice.QUIT))
						{
							will_quit = true;
						}
						// Valid option
						else
						{
							// Apply option to all logs
							for (Path log : log_files)
							{
								System.out.println(Utility.boxText("INPUT  : " + log.getFileName().toString()));
								String output = parseFileByChoice(choice, log);
								System.out.println(output + System.lineSeparator() + System.lineSeparator()
										+ Utility.fillWithChar(50, '\u2500') + System.lineSeparator());
							}
						}
					}
				}
			}
		}

		// Close scanner
		finally
		{
			if (scan != null)
			{
				scan.close();
			}
		}
		return;
	}

	private static String parseFileByChoice(MenuChoice choice, Path path)
	{
		// Parse a new file
		if (current_file == null || !current_file.equals(path.getFileName().toString().split("\\.")[0]))
		{
			try
			{
				parsed_file = new Parse(path.toString());
				statistics = new Statistics(parsed_file);
				current_file = path.getFileName().toString().split("\\.")[0];
				if (displaying)
				{
					TableBuilder table = new TableBuilder();
					table.addRow("Build Version", "Point of View", "Start Date", "End Date");
					table.addRow(parsed_file.getLogData().toStringArray());
					System.out.println(table.toString());
				}
				if (Integer.valueOf(parsed_file.getLogData().getBuildVersion().replaceAll("EVTC", "")) < 20170218)
				{
					return old_version;
				}
			} catch (IOException e)
			{
				e.printStackTrace();
			}
		}

		// Apply option
		if (choice.equals(MenuChoice.FINAL_DPS))
		{
			return statistics.getFinalDPS();
		}
		else if (choice.equals(MenuChoice.PHASE_DPS))
		{
			return statistics.getPhaseDPS();
		}
		else if (choice.equals(MenuChoice.DMG_DIST))
		{
			return statistics.getDamageDistribution();
		}
		else if (choice.equals(MenuChoice.G_TOTAL_DMG))
		{
			return Utility.boxText("OUTPUT  : " + statistics.getTotalDamageGraph(current_file));
		}
		else if (choice.equals(MenuChoice.MISC_STATS))
		{
			return statistics.getCombatStatistics();
		}
		else if (choice.equals(MenuChoice.FINAL_BOONS))
		{
			return statistics.getFinalBoons();
		}
		else if (choice.equals(MenuChoice.PHASE_BOONS))
		{
			return statistics.getPhaseBoons();
		}
		else if (choice.equals(MenuChoice.DUMP_EVTC))
		{
			File evtc_dump = new File(
					"./tables/" + current_file + "_" + parsed_file.getBossData().getName() + "_evtc-dump.txt");
			try
			{
				Utility.writeToFile(parsed_file.toString(), evtc_dump);
			} catch (IOException e)
			{
				e.printStackTrace();
			}
			return Utility.boxText("OUTPUT : " + evtc_dump.getName());
		}
		else if (choice.equals(MenuChoice.DUMP_TABLES))
		{
			File text_dump = new File(
					"./tables/" + current_file + "_" + parsed_file.getBossData().getName() + "_all-tables.txt");
			try
			{
				Utility.writeToFile(statistics.getFinalDPS() + System.lineSeparator() + statistics.getPhaseDPS()
						+ System.lineSeparator() + statistics.getCombatStatistics() + System.lineSeparator()
						+ statistics.getFinalBoons() + System.lineSeparator() + statistics.getPhaseBoons()
						+ System.lineSeparator() + statistics.getDamageDistribution(), text_dump);
			} catch (IOException e)
			{
				e.printStackTrace();
			}
			return Utility.boxText("OUTPUT : " + text_dump.getName());
		}
		return "";
	}
}


<|start_filename|>src/data/CombatData.java<|end_filename|>
package data;

import java.awt.Point;
import java.util.ArrayList;
import java.util.List;

import enums.StateChange;

public class CombatData
{

	// Fields
	private List<CombatItem> combat_list;

	// Constructors
	public CombatData()
	{
		this.combat_list = new ArrayList<CombatItem>();
	}

	// Public Methods
	public void addItem(CombatItem item)
	{
		combat_list.add(item);
	}

	public List<Point> getStates(int src_instid, StateChange change)
	{
		List<Point> states = new ArrayList<Point>();
		for (CombatItem c : combat_list)
		{
			if (c.getSrcInstid() == src_instid && c.isStateChange().equals(change))
			{
				states.add(new Point(c.getTime(), (int) c.getDstAgent()));
			}
		}
		return states;
	}

	public int getSkillCount(int src_instid, int skill_id)
	{
		int count = 0;
		for (CombatItem c : combat_list)
		{
			if (c.getSrcInstid() == src_instid && c.getSkillID() == skill_id)
			{
				count++;
			}
		}
		return count;
	}

	// Getters
	public List<CombatItem> getCombatList()
	{
		return combat_list;
	}

}


<|start_filename|>src/enums/StateChange.java<|end_filename|>
package enums;

public enum StateChange
{

	// Constants
	NORMAL(0),
	ENTER_COMBAT(1),
	EXIT_COMBAT(2),
	CHANGE_UP(3),
	CHANGE_DEAD(4),
	CHANGE_DOWN(5),
	SPAWN(6),
	DESPAWN(7),
	HEALTH_UPDATE(8),
	LOG_START(9),
	LOG_END(10),
	WEAPON_SWAP(11),
	MAX_HEALTH_UPDATE(12),
	POINT_OF_VIEW(13),
	CBTS_LANGUAGE(14);

	// Fields
	private int ID;

	// Constructors
	private StateChange(int ID)
	{
		this.ID = ID;
	}

	// Public Methods
	public static StateChange getEnum(int ID)
	{
		for (StateChange s : values())
		{
			if (s.getID() == ID)
			{
				return s;
			}
		}
		return null;
	}

	// Getters
	public int getID()
	{
		return ID;
	}

}


<|start_filename|>src/enums/BuffRemove.java<|end_filename|>
package enums;

public enum BuffRemove
{
	// Constants
	NONE(0),
	ALL(1),
	SINGLE(2),
	MANUAL(3);

	// Fields
	private int ID;

	// Constructors
	private BuffRemove(int ID)
	{
		this.ID = ID;
	}

	// Public Methods
	public static BuffRemove getEnum(int ID)
	{
		for (BuffRemove b : values())
		{
			if (b.getID() == ID)
			{
				return b;
			}
		}
		return null;
	}

	// Getters
	public int getID()
	{
		return ID;
	}

}


<|start_filename|>src/statistics/Parse.java<|end_filename|>
package statistics;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.List;
import java.util.Scanner;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

import data.AgentData;
import data.AgentItem;
import data.BossData;
import data.CombatData;
import data.CombatItem;
import data.LogData;
import data.SkillData;
import data.SkillItem;
import enums.Activation;
import enums.Agent;
import enums.BuffRemove;
import enums.IFF;
import enums.Result;
import enums.StateChange;
import utility.TableBuilder;
import utility.Utility;

public class Parse
{
	// Fields
	private BufferedInputStream f = null;
	private LogData log_data;
	private BossData boss_data;
	private AgentData agent_data = new AgentData();
	private SkillData skill_data = new SkillData();
	private CombatData combat_data = new CombatData();

	// Constructor
	public Parse(String file_path) throws IOException
	{

		// Read .evtc file into buffered input stream
		ZipFile zip_file = null;
		if (file_path.endsWith(".zip"))
		{
			zip_file = new ZipFile(file_path);
			ZipEntry evtc_file = zip_file.entries().nextElement();
			f = new BufferedInputStream(zip_file.getInputStream(evtc_file));
		}
		else if (file_path.endsWith(".evtc"))
		{
			f = new BufferedInputStream(new FileInputStream(new File(file_path)));
		}

		// Parse file
		try
		{
			parseBossData();
			parseAgentData();
			parseSkillData();
			parseCombatList();
			fillMissingData();
		}

		// Close streams
		finally
		{
			try
			{
				if (zip_file != null)
				{
					zip_file.close();
				}
				f.close();
			} catch (IOException e)
			{
				e.printStackTrace();
			}
		}
	}

	// Public Methods
	public LogData getLogData()
	{
		return log_data;
	}

	public BossData getBossData()
	{
		return boss_data;
	}

	public AgentData getAgentData()
	{
		return agent_data;
	}

	public SkillData getSkillData()
	{
		return skill_data;
	}

	public CombatData getCombatData()
	{
		return combat_data;
	}

	// Private Methods
	private void parseBossData() throws IOException
	{
		// 12 bytes: arc build version
		String build_version = getString(12);
		this.log_data = new LogData(build_version);

		// 1 byte: skip
		safeSkip(1);

		// 2 bytes: boss instance ID
		int instid = getShort();

		// 1 byte: position
		safeSkip(1);

		// BossData
		this.boss_data = new BossData(instid);
	}

	private void parseAgentData() throws IOException
	{
		// 4 bytes: player count
		int player_count = getInt();

		// 96 bytes: each player
		for (int i = 0; i < player_count; i++)
		{
			// 8 bytes: agent
			long agent = getLong();

			// 4 bytes: profession
			int prof = getInt();

			// 4 bytes: is_elite
			int is_elite = getInt();

			// 4 bytes: toughness
			int toughness = getInt();

			// 4 bytes: healing
			int healing = getInt();

			// 4 bytes: condition
			int condition = getInt();

			// 68 bytes: name
			String name = getString(68);

			// Agent
			Agent a = Agent.getEnum(prof, is_elite);

			// Add an agent
			if (a != null)
			{
				// NPC
				if (a.equals(Agent.NPC))
				{
					agent_data.addItem(a, new AgentItem(agent, name, a.getName() + ":" + String.format("%05d", prof)));
				}
				// Gadget
				else if (a.equals(Agent.GADGET))
				{
					agent_data.addItem(a,
							new AgentItem(agent, name, a.getName() + ":" + String.format("%05d", prof & 0x0000ffff)));
				}
				// Player
				else
				{
					agent_data.addItem(a, new AgentItem(agent, name, a.getName(), toughness, healing, condition));
				}
			}
			// Unknown
			else
			{
				agent_data.addItem(a, new AgentItem(agent, name, String.valueOf(prof), toughness, healing, condition));
			}
		}
	}

	private void parseSkillData() throws IOException
	{
		// 4 bytes: player count
		int skill_count = getInt();

		// 68 bytes: each skill
		for (int i = 0; i < skill_count; i++)
		{
			// 4 bytes: skill ID
			int skill_id = getInt();

			// 64 bytes: name
			String name = getString(64);

			// Add skill
			skill_data.addItem(new SkillItem(skill_id, name));
		}
	}

	private void parseCombatList() throws IOException
	{
		// 64 bytes: each combat
		while (f.available() >= 64)
		{
			// 8 bytes: time
			int time = (int) getLong();

			// 8 bytes: src_agent
			long src_agent = getLong();

			// 8 bytes: dst_agent
			long dst_agent = getLong();

			// 4 bytes: value
			int value = getInt();

			// 4 bytes: buff_dmg
			int buff_dmg = getInt();

			// 2 bytes: overstack_value
			int overstack_value = getShort();

			// 2 bytes: skill_id
			int skill_id = getShort();

			// 2 bytes: src_instid
			int src_instid = getShort();

			// 2 bytes: dst_instid
			int dst_instid = getShort();

			// 2 bytes: src_master_instid
			int src_master_instid = getShort();

			// 9 bytes: garbage
			safeSkip(9);

			// 1 byte: iff
			IFF iff = IFF.getEnum(f.read());

			// 1 byte: buff
			int buff = f.read();

			// 1 byte: result
			Result result = Result.getEnum(f.read());

			// 1 byte: is_activation
			Activation is_activation = Activation.getEnum(f.read());

			// 1 byte: is_buffremove
			BuffRemove is_buffremove = BuffRemove.getEnum(f.read());

			// 1 byte: is_ninety
			int is_ninety = f.read();

			// 1 byte: is_fifty
			int is_fifty = f.read();

			// 1 byte: is_moving
			int is_moving = f.read();

			// 1 byte: is_statechange
			StateChange is_statechange = StateChange.getEnum(f.read());

			// 1 byte: is_flanking
			int is_flanking = f.read();

			// 3 bytes: garbage
			safeSkip(3);

			// Add combat
			combat_data.addItem(new CombatItem(time, src_agent, dst_agent, value, buff_dmg, overstack_value, skill_id,
					src_instid, dst_instid, src_master_instid, iff, buff, result, is_activation, is_buffremove,
					is_ninety, is_fifty, is_moving, is_statechange, is_flanking));
		}
	}

	public void fillMissingData()
	{
		// Set Agent instid, first_aware and last_aware
		List<AgentItem> player_list = agent_data.getPlayerAgentList();
		List<AgentItem> agent_list = agent_data.getAllAgentsList();
		List<CombatItem> combat_list = combat_data.getCombatList();
		for (AgentItem a : agent_list)
		{
			boolean assigned_first = false;
			for (CombatItem c : combat_list)
			{
				if (a.getAgent() == c.getSrcAgent() && c.getSrcInstid() != 0)
				{
					if (!assigned_first)
					{
						a.setInstid(c.getSrcInstid());
						a.setFirstAware(c.getTime());
						assigned_first = true;
					}
					a.setLastAware(c.getTime());
				}
				else if (a.getAgent() == c.getDstAgent() && c.getDstInstid() != 0)
				{
					if (!assigned_first)
					{
						a.setInstid(c.getDstInstid());
						a.setFirstAware(c.getTime());
						assigned_first = true;
					}
					a.setLastAware(c.getTime());
				}
				else if (c.isStateChange() == StateChange.POINT_OF_VIEW)
				{
					int pov_instid = c.getSrcInstid();
					for (AgentItem p : player_list)
					{
						if (pov_instid == p.getInstid())
						{
							log_data.setPOV(p.getName());
						}
					}

				}
				else if (c.isStateChange() == StateChange.LOG_START)
				{
					log_data.setLogStart(c.getValue());
				}
				else if (c.isStateChange() == StateChange.LOG_END)
				{
					log_data.setLogEnd(c.getValue());
				}
			}
		}

		// Manual log target selection
		if (boss_data.getID() == 1)
		{
			targetSelection();
		}

		// Set Boss data agent, instid, first_aware, last_aware and name
		List<AgentItem> NPC_list = agent_data.getNPCAgentList();
		for (AgentItem NPC : NPC_list)
		{
			if (NPC.getProf().endsWith(String.valueOf(boss_data.getID())))
			{
				if (boss_data.getAgent() == 0)
				{
					boss_data.setAgent(NPC.getAgent());
					boss_data.setInstid(NPC.getInstid());
					boss_data.setFirstAware(NPC.getFirstAware());
					boss_data.setName(NPC.getName());
				}
				boss_data.setLastAware(NPC.getLastAware());
			}
		}

		// Set Boss health
		for (CombatItem c : combat_list)
		{
			if (c.getSrcInstid() == boss_data.getInstid() && c.isStateChange().equals(StateChange.MAX_HEALTH_UPDATE))
			{
				boss_data.setHealth((int) c.getDstAgent());
				break;
			}
		}

		// Dealing with second half of Xera | ((22611300 * 0.5) + (25560600 *
		// 0.5)
		int xera_2_instid = 0;
		for (AgentItem NPC : NPC_list)
		{
			if (NPC.getProf().contains("16286"))
			{
				xera_2_instid = NPC.getInstid();
				boss_data.setHealth(24085950);
				boss_data.setLastAware(NPC.getLastAware());
				for (CombatItem c : combat_list)
				{
					if (c.getSrcInstid() == xera_2_instid)
					{
						c.setSrcInstid(boss_data.getInstid());
					}
					if (c.getDstInstid() == xera_2_instid)
					{
						c.setDstInstid(boss_data.getInstid());
					}
				}
				break;
			}
		}

	}

	@SuppressWarnings("resource")
	private void targetSelection()
	{
		List<AgentItem> NPC_list = agent_data.getNPCAgentList();
		TableBuilder target_table = new TableBuilder();
		target_table.addTitle("NPC List");
		target_table.addRow("ID", "Name", "Species");

		for (AgentItem NPC : NPC_list)
		{
			target_table.addRow(String.valueOf(NPC.getInstid()), NPC.getName(), NPC.getProf().substring(4));
		}

		System.out.println(target_table.toString());

		// Read user input
		Scanner scan = null;
		scan = new Scanner(System.in);
		boolean quitting = false;
		while (!quitting)
		{
			System.out.println(Utility.boxText("Select an NPC to target by ID"));
			System.out.print(" >> ");
			// A number
			if (scan.hasNextInt())
			{
				int target_id = scan.nextInt();
				for (AgentItem NPC : NPC_list)
				{
					// Input matches an ID
					if (target_id == NPC.getInstid())
					{
						boss_data.setAgent(NPC.getAgent());
						boss_data.setInstid(NPC.getInstid());
						boss_data.setFirstAware(NPC.getFirstAware());
						boss_data.setName(NPC.getName());
						boss_data.setLastAware(NPC.getLastAware());
						quitting = true;
						break;
					}
				}
				if (!quitting)
				{
					System.out.println(Utility.boxText("WARNING : Invalid NPC ID"));
				}
			}
			else
			{
				System.out.println(Utility.boxText("WARNING : Invalid NPC ID"));
			}
			scan.nextLine();
		}
	}

	// Override
	@Override
	public String toString()
	{
		// Build tables
		StringBuilder output = new StringBuilder();
		TableBuilder table = new TableBuilder();

		// Log Data Table
		table.addTitle("LOG DATA");
		table.addRow("build_version", "point_of_view", "log_start", "log_end");
		table.addRow(log_data.toStringArray());
		output.append(table.toString() + System.lineSeparator());
		table.clear();

		// Boss Data Table
		table.addTitle("BOSS DATA");
		table.addRow("agent", "instid", "first_aware", "last_aware", "id", "name", "health");
		table.addRow(boss_data.toStringArray());
		output.append(table.toString() + System.lineSeparator());
		table.clear();

		// Player Data
		List<AgentItem> playerAgents = agent_data.getPlayerAgentList();
		List<AgentItem> NPCAgents = agent_data.getNPCAgentList();
		List<AgentItem> gadgetAgents = agent_data.getGadgetAgentList();
		table.addTitle("AGENT DATA");
		table.addRow("agent", "instid", "first_aware", "last_aware", "name", "prof", "toughness", "healing",
				"condition");
		for (AgentItem player : playerAgents)
		{
			table.addRow(player.toStringArray());
		}
		for (AgentItem npc : NPCAgents)
		{
			table.addRow(npc.toStringArray());
		}
		for (AgentItem gadget : gadgetAgents)
		{
			table.addRow(gadget.toStringArray());
		}
		output.append(table.toString() + System.lineSeparator());
		table.clear();

		// Skill Data
		List<SkillItem> skillList = skill_data.getSkillList();
		table.addTitle("SKILL DATA");
		table.addRow("ID", "name");
		for (SkillItem s : skillList)
		{
			table.addRow(s.toStringArray());
		}
		output.append(table.toString() + System.lineSeparator());
		table.clear();

		// Combat Data Table
		List<CombatItem> combatList = combat_data.getCombatList();
		table.addTitle("COMBAT DATA");
		table.addRow("time", "src_agent", "dst_agent", "value", "buff_dmg", "overstack_value", "skill_id", "src_instid",
				"dst_instid", "src_master_instid", "iff", "buff", "is_crit", "is_activation", "is_buffremove",
				"is_ninety", "is_fifty", "is_moving", "is_statechange", "is_flanking");
		for (CombatItem c : combatList)
		{
			table.addRow(c.toStringArray());
		}
		output.append(table.toString() + System.lineSeparator());

		return output.toString();
	}

	// Private Methods
	private void safeSkip(long bytes_to_skip) throws IOException
	{
		while (bytes_to_skip > 0)
		{
			long bytes_actually_skipped = f.skip(bytes_to_skip);
			if (bytes_actually_skipped > 0)
			{
				bytes_to_skip -= bytes_actually_skipped;
			}
			else if (bytes_actually_skipped == 0)
			{
				if (f.read() == -1)
				{
					break;
				}
				else
				{
					bytes_to_skip--;
				}
			}
		}
	}

	private int getShort() throws IOException
	{
		byte[] bytes = new byte[2];
		f.read(bytes);
		return Short.toUnsignedInt(ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN).getShort());
	}

	private int getInt() throws IOException
	{
		byte[] bytes = new byte[4];
		f.read(bytes);
		return ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN).getInt();
	}

	private long getLong() throws IOException
	{
		byte[] bytes = new byte[8];
		f.read(bytes);
		return ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN).getLong();
	}

	private String getString(int length) throws IOException
	{
		byte[] bytes = new byte[length];
		f.read(bytes);
		try
		{
			return new String(bytes, "UTF-8").trim();
		} catch (UnsupportedEncodingException e)
		{
			e.printStackTrace();
		}
		return "UNKNOWN";
	}

}

<|start_filename|>src/utility/TableBuilder.java<|end_filename|>
package utility;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.ListIterator;

public class TableBuilder
{

	// Fields
	private String title = "";
	private List<String[]> rows = new ArrayList<String[]>();
	private String nl = System.lineSeparator();

	// Public Methods
	public void addTitle(String title)
	{
		this.title = ' ' + title + ' ';
	}

	public void addRow(String... cols)
	{
		rows.add(cols);
		String[] row = rows.get(rows.size() - 1);
		for (int i = 0; i < row.length; i++)
		{
			row[i] = ' ' + row[i] + ' ';
		}
	}

	public void addSeparator()
	{
		if (!rows.isEmpty())
		{
			String[] separator = new String[rows.get(0).length];
			Arrays.fill(separator, "");
			rows.add(separator);
		}
	}

	public void sortAsDouble(int col)
	{
		// Get body
		int separator = 0;
		for (String[] row : rows)
		{
			if (row[0].equals(""))
			{
				break;
			}
			separator++;
		}
		List<String[]> body = rows.subList(1, separator);

		// Sort by column
		Collections.sort(body, new Comparator<String[]>()
		{
			@Override
			public int compare(String[] x, String[] y)
			{
				double x_dbl = Double.parseDouble(x[col].replaceAll("\\s+", ""));
				double y_dbl = Double.parseDouble(y[col].replaceAll("\\s+", ""));
				if (x_dbl > y_dbl)
				{
					return -1;
				}
				else if (x_dbl == y_dbl)
				{
					return 0;
				}
				else
				{
					return 1;
				}
			}
		});
	}

	public void clear()
	{
		title = "";
		rows = new ArrayList<String[]>();
	}

	@Override
	public String toString()
	{
		// Initialize
		removeEmptyColumns();
		StringBuilder output = new StringBuilder();
		int[] colWidths = getWidths();
		int numCols = rows.get(0).length;

		// Title
		if (!title.equals(""))
		{
			output.append('\u250C' + Utility.fillWithChar(title.length(), '\u2500') + '\u2510' + nl);
			output.append('\u2502' + title + '\u2502' + nl);
			output.append('\u2514' + Utility.fillWithChar(title.length(), '\u2500') + '\u2518' + nl);
		}

		// Empty
		if (rows.size() <= 1)
		{
			return output.toString();
		}

		// Header
		output.append('\u250C');
		for (int colNum = 0; colNum < rows.get(0).length; colNum++)
		{
			output.append(Utility.fillWithChar(colWidths[colNum], '\u2500'));
			if (colNum != numCols - 1)
			{
				output.append('\u252C');
			}
			else
			{
				output.append('\u2510' + nl + '\u2502');
			}
		}
		for (int colNum = 0; colNum < numCols; colNum++)
		{
			output.append(Utility.centerString(rows.get(0)[colNum], colWidths[colNum]) + '\u2502');
		}
		output.append(nl + '\u251C');
		for (int colNum = 0; colNum < rows.get(0).length; colNum++)
		{
			output.append(Utility.fillWithChar(colWidths[colNum], '\u2550'));
			if (colNum != numCols - 1)
			{
				output.append('\u253C');
			}
			else
			{
				output.append('\u2524' + nl);
			}
		}

		// Body
		boolean footer = false;
		for (ListIterator<String[]> iter = rows.listIterator(1); iter.hasNext();)
		{
			String[] row = iter.next();
			String text;

			for (int colNum = 0; colNum < row.length; colNum++)
			{
				if (row[0].equals(""))
				{

					footer = true;
					if (colNum == 0)
					{
						output.append('\u251C');
					}
					else
					{
						output.append('\u253C');
					}
					output.append(Utility.fillWithChar(colWidths[colNum], '\u2500'));
				}
				else
				{
					text = row[colNum];
					if (!footer)
					{
						if (Utility.isNumeric(text))
						{
							output.append('\u2502' + Utility.rightAlignString(text, colWidths[colNum]));
						}
						else
						{
							output.append('\u2502' + Utility.leftAlignString(text, colWidths[colNum]));
						}
					}
					else
					{
						output.append('\u2502' + Utility.centerString(text, colWidths[colNum]));
					}
				}
			}
			if (row[0].equals(""))
			{
				output.append('\u2524' + nl);
			}
			else
			{
				output.append('\u2502' + nl);
			}
		}
		output.append('\u2514');
		for (int colNum = 0; colNum < rows.get(0).length; colNum++)
		{
			output.append(Utility.fillWithChar(colWidths[colNum], '\u2500'));
			if (colNum != numCols - 1)
			{
				output.append('\u2534');
			}
			else
			{
				output.append('\u2518');
			}
		}

		return output.toString();
	}

	// Private Methods
	private int[] getWidths()
	{
		int cols = 0;
		for (String[] row : rows)
			cols = Math.max(cols, row.length);
		int[] widths = new int[cols];
		for (String[] row : rows)
		{
			for (int i = 0; i < row.length; i++)
			{
				widths[i] = Math.max(widths[i], row[i].length());
			}
		}
		return widths;
	}

	private void removeEmptyColumns()
	{

		// Columns that contain all "0.00" will have existance[i] = false
		int cols = rows.get(0).length;
		boolean[] existance = new boolean[cols];
		for (ListIterator<String[]> iter = rows.listIterator(1); iter.hasNext();)
		{
			String[] row = iter.next();
			for (int i = 0; i < cols; i++)
			{
				if (!existance[i] && !row[i].equals(" 0.00 "))
				{
					existance[i] = true;
				}
			}
		}

		// Check if there are any false values
		int exist_count = 0;
		for (boolean exists : existance)
		{
			if (exists)
			{
				exist_count++;
			}
		}
		if (exist_count == cols)
		{
			return;
		}

		// Create new table with no empty columns
		List<String[]> new_rows = new ArrayList<String[]>();
		for (String[] row : rows)
		{
			int i = 0;
			int j = 0;
			String[] new_row = new String[exist_count];
			for (boolean exists : existance)
			{
				if (exists)
				{
					new_row[i] = row[j];
					i++;
				}
				j++;
			}
			new_rows.add(new_row);
		}
		rows = new_rows;

	}

}

<|start_filename|>src/statistics/Statistics.java<|end_filename|>
package statistics;

import java.awt.Font;
import java.awt.Point;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;

import org.knowm.xchart.BitmapEncoder;
import org.knowm.xchart.BitmapEncoder.BitmapFormat;
import org.knowm.xchart.XYChart;
import org.knowm.xchart.XYChartBuilder;
import org.knowm.xchart.style.Styler.LegendPosition;

import boon.AbstractBoon;
import boon.BoonFactory;
import data.AgentItem;
import data.BossData;
import data.CombatData;
import data.SkillData;
import enums.Boon;
import enums.CustomSkill;
import enums.Result;
import enums.StateChange;
import player.BoonLog;
import player.DamageLog;
import player.Player;
import utility.TableBuilder;
import utility.Utility;

public class Statistics
{
	// Fields
	public static boolean hiding_players;
	BossData b_data;
	SkillData s_data;
	CombatData c_data;
	List<Player> p_list;

	// Constructor
	public Statistics(Parse parsed)
	{
		// Data
		this.b_data = parsed.getBossData();
		this.s_data = parsed.getSkillData();
		this.c_data = parsed.getCombatData();

		// Players
		p_list = new ArrayList<Player>();
		List<AgentItem> playerAgentList = parsed.getAgentData().getPlayerAgentList();
		for (AgentItem playerAgent : playerAgentList)
		{
			this.p_list.add(new Player(playerAgent));
		}

		// Sort
		p_list.sort((a, b) -> Integer.parseInt(a.getGroup()) - Integer.parseInt(b.getGroup()));
	}

	// Final DPS
	public String getFinalDPS()
	{
		double total_dps = 0.0;
		double total_damage = 0.0;
		double fight_duration = (b_data.getLastAware() - b_data.getFirstAware()) / 1000.0;

		// Table
		TableBuilder table = new TableBuilder();
		table.addTitle("Final DPS - " + b_data.getName());

		// Header
		table.addRow("Name", "Profession", "DPS", "Damage");

		// Body
		for (Player p : p_list)
		{
			// Damage and DPS
			double damage = p.getDamageLogs(b_data, c_data.getCombatList()).stream().mapToDouble(DamageLog::getDamage)
					.sum();
			double dps = 0.0;
			if (fight_duration > 0)
			{
				dps = damage / fight_duration;
			}

			// Totals
			total_dps += dps;
			total_damage += damage;

			// Add Row
			table.addRow(p.getCharacter(), p.getProf(), String.format("%.2f", dps), String.format("%.0f", damage));
		}

		// Sort by DPS
		table.sortAsDouble(2);

		// Footer
		table.addSeparator();
		table.addRow("GROUP TOTAL", "-", String.format("%.2f", total_dps), String.format("%.0f", total_damage));
		table.addRow("TARGET HEALTH", "-", "-", String.format("%d", b_data.getHealth()));

		return table.toString();
	}

	// Phase DPS
	public String getPhaseDPS()
	{
		double total_time = 0.0;
		List<Point> fight_intervals = getPhaseIntervals();
		int n = fight_intervals.size();
		String[] phase_names = b_data.getPhaseNames();

		// Table
		TableBuilder table = new TableBuilder();
		table.addTitle("Phase DPS - " + b_data.getName());

		// Header
		String[] header = new String[n + 3];
		header[0] = "Name";
		header[1] = "Profession";
		for (int i = 2; i < n + 2; i++)
		{
			header[i] = phase_names[i - 2];
		}
		header[header.length - 1] = "Summary";
		table.addRow(header);

		// Body
		for (Player p : p_list)
		{
			total_time = 0.0;
			double total_damage = 0.0;
			String[] phase_dps = new String[n + 1];

			for (int i = 0; i < n; i++)
			{
				List<DamageLog> damage_logs = p.getDamageLogs(b_data, c_data.getCombatList());
				Point interval = fight_intervals.get(i);

				// Damage and DPS
				double phase_damage = 0.0;
				for (DamageLog log : damage_logs)
				{
					if ((log.getTime() >= interval.x) && (log.getTime() <= interval.y))
					{
						phase_damage += log.getDamage();
					}
				}
				total_time += (interval.getY() - interval.getX());
				total_damage += phase_damage;
				double dps = (phase_damage / (interval.getY() - interval.getX())) * 1000.0;
				phase_dps[i] = String.format("%.2f", dps);

			}
			// Row
			phase_dps[n] = String.format("%.2f", (total_damage / total_time) * 1000.0);
			table.addRow(Utility.concatStringArray(new String[] { p.getCharacter(), p.getProf() }, phase_dps));
		}

		// Sort
		table.sortAsDouble(n + 2);

		// Footer
		table.addSeparator();
		String[] durations = new String[n + 3];
		durations[0] = "PHASE DURATION";
		durations[1] = "-";
		for (int i = 2; i < n + 2; i++)
		{
			Point p = fight_intervals.get(i - 2);
			double time = (p.getY() - p.getX()) / 1000.0;
			durations[i] = String.format("%.3f", time);
		}
		durations[durations.length - 1] = String.format("%.3f", total_time / 1000.0);
		table.addRow(durations);

		String[] intervals = new String[n + 3];
		intervals[0] = "PHASE INTERVAL";
		intervals[1] = "-";
		for (int i = 2; i < n + 2; i++)
		{
			Point p = fight_intervals.get(i - 2);
			intervals[i] = String.format("%.3f", p.getX() / 1000.0) + " - " + String.format("%.3f", p.getY() / 1000.0);
		}
		intervals[intervals.length - 1] = String.format("%.3f", fight_intervals.get(0).getX() / 1000.0) + " - "
				+ String.format("%.3f", fight_intervals.get(fight_intervals.size() - 1).getY() / 1000.0);
		table.addRow(intervals);
		return table.toString();
	}

	// Damage Distribution
	public String getDamageDistribution()
	{
		// Heading
		StringBuilder output = new StringBuilder();
		String title = " Damage Distribution - " + b_data.getName() + ' ';
		output.append('\u250C' + Utility.fillWithChar(title.length(), '\u2500') + '\u2510' + System.lineSeparator());
		output.append('\u2502' + title + '\u2502' + System.lineSeparator());
		output.append('\u2514' + Utility.fillWithChar(title.length(), '\u2500') + '\u2518');

		// Table
		TableBuilder table = new TableBuilder();

		// Body
		for (Player p : p_list)
		{
			List<DamageLog> damage_logs = p.getDamageLogs(b_data, c_data.getCombatList());

			// Skill Damage Map
			Map<Integer, Integer> skill_damage = new HashMap<Integer, Integer>();
			for (DamageLog log : damage_logs)
			{
				if (skill_damage.containsKey(log.getID()))
				{
					skill_damage.put(log.getID(), skill_damage.get(log.getID()) + log.getDamage());
				}
				else
				{
					skill_damage.put(log.getID(), log.getDamage());
				}
			}

			// Title and Header
			table.clear();
			table.addTitle(p.getCharacter() + " - " + p.getProf());
			table.addRow("Skill", "Damage", "%");

			// Sort
			skill_damage = Utility.sortByValue(skill_damage);

			// Calculate distribution
			double total_damage = skill_damage.values().stream().reduce(0, Integer::sum);
			for (Map.Entry<Integer, Integer> entry : skill_damage.entrySet())
			{
				String skill_name = s_data.getName(entry.getKey());
				double damage = entry.getValue();
				table.addRow(skill_name, String.format("%.0f", damage),
						String.format("%.2f", (damage / total_damage * 100.0)));
			}

			// Add table
			output.append(System.lineSeparator());
			output.append(table.toString());
		}
		return output.toString();
	}

	// Total Damage Graph
	public String getTotalDamageGraph(String base)
	{
		// Build
		XYChartBuilder chartBuilder = new XYChartBuilder().width(1600).height(900);
		chartBuilder.title("Total Damage - " + b_data.getName());
		chartBuilder.xAxisTitle("Time (seconds)").yAxisTitle("Damage (K)").build();
		XYChart chart = chartBuilder.build();

		// Style
		chart.getStyler().setLegendPosition(LegendPosition.InsideNW);
		chart.getStyler().setMarkerSize(1);
		chart.getStyler().setXAxisMin(0.0);
		chart.getStyler().setYAxisMin(0.0);
		chart.getStyler().setLegendFont(new Font("Dialog", Font.PLAIN, 16));

		// Series
		for (Player p : p_list)
		{
			List<DamageLog> damage_logs = p.getDamageLogs(b_data, c_data.getCombatList());
			int n = damage_logs.size();
			if (n > 0)
			{
				double[] x = new double[n];
				double[] y = new double[n];
				double total_damage = 0.0;
				for (int i = 0; i < n; i++)
				{
					total_damage += damage_logs.get(i).getDamage();
					x[i] = damage_logs.get(i).getTime() / 1000.0;
					y[i] = total_damage / 1000.0;
				}
				chart.addSeries(p.getCharacter() + " - " + p.getProf(), x, y);
			}
		}

		// Write
		try
		{
			String file_name = "./graphs/" + base + "_" + b_data.getName() + "_TDG.png";
			BitmapEncoder.saveBitmapWithDPI(chart, file_name, BitmapFormat.PNG, 300);
		} catch (IOException e)
		{
			e.printStackTrace();
		}
		return base + "_" + b_data.getName() + "_TDG.png";
	}

	// Combat Statistics
	public String getCombatStatistics()
	{
		// Table
		TableBuilder table = new TableBuilder();
		table.addTitle("Combat Statistics - " + b_data.getName());

		// Header
		table.addRow("Account", "Character", "Profession", "SUBG", "CRIT", "SCHL", "MOVE", "FLNK", "TGHN", "HEAL",
				"COND", "SWAP", "DOGE", "RESS", "DOWN", "DIED");

		// Body
		for (Player p : p_list)
		{
			List<DamageLog> damage_logs = p.getDamageLogs(b_data, c_data.getCombatList());
			int instid = p.getInstid();

			// Rates
			double power_loop_count = 0.0;
			double critical_rate = 0.0;
			double scholar_rate = 0.0;
			double moving_rate = 0.0;
			double flanking_rate = 0.0;
			for (DamageLog log : damage_logs)
			{
				if (log.isCondi() == 0)
				{
					critical_rate += (log.getResult().equals(Result.CRIT)) ? 1 : 0;
					scholar_rate += log.isNinety();
					moving_rate += log.isMoving();
					flanking_rate += log.isFlanking();
					power_loop_count++;
				}
			}
			power_loop_count = (power_loop_count == 0) ? 1 : power_loop_count;

			// Counts
			int swap = c_data.getStates(instid, StateChange.WEAPON_SWAP).size();
			int down = c_data.getStates(instid, StateChange.CHANGE_DOWN).size();
			int dodge = c_data.getSkillCount(instid, CustomSkill.DODGE.getID());
			int ress = c_data.getSkillCount(instid, CustomSkill.RESURRECT.getID());

			// R.I.P
			List<Point> dead = c_data.getStates(instid, StateChange.CHANGE_DEAD);
			double died = 0.0;
			if (!dead.isEmpty())
			{
				died = dead.get(0).getX() - b_data.getFirstAware();
			}

			// Add row
			table.addRow(new String[] { p.getAccount(), p.getCharacter(), p.getProf(), p.getGroup(),
					String.format("%.2f", critical_rate / power_loop_count),
					String.format("%.2f", scholar_rate / power_loop_count),
					String.format("%.2f", moving_rate / power_loop_count),
					String.format("%.2f", flanking_rate / power_loop_count), String.valueOf(p.getToughness()),
					String.valueOf(p.getHealing()), String.valueOf(p.getCondition()), String.valueOf(swap),
					String.valueOf(dodge), String.valueOf(ress), String.valueOf(down),
					String.format("%.2f", died / 1000.0) });
		}
		return table.toString();
	}

	// Final Boons
	public String getFinalBoons()
	{
		// Table
		TableBuilder table = new TableBuilder();
		table.addTitle("Final Boon Rates - " + b_data.getName());

		// Header
		String[] boon_array = Boon.getArray();
		table.addRow(Utility.concatStringArray(new String[] { "Name", "Profession" }, boon_array));

		// Body
		List<String> boon_list = Boon.getList();
		int n = boon_list.size();
		BoonFactory boonFactory = new BoonFactory();

		for (Player p : p_list)
		{
			Map<String, List<BoonLog>> boon_logs = p.getBoonMap(b_data, s_data, c_data.getCombatList());
			String[] rates = new String[n];
			for (int i = 0; i < n; i++)
			{
				Boon boon = Boon.getEnum(boon_list.get(i));
				AbstractBoon boon_object = boonFactory.makeBoon(boon);
				List<BoonLog> logs = boon_logs.get(boon.getName());
				String rate = "0.00";
				if (!logs.isEmpty())
				{
					if (boon.getType().equals("duration"))
					{
						rate = getBoonDuration(getBoonIntervalsList(boon_object, logs));
					}
					else if (boon.getType().equals("intensity"))
					{
						rate = getAverageStacks(getBoonStacksList(boon_object, logs));
					}
				}
				rates[i] = rate;
			}
			table.addRow(Utility.concatStringArray(new String[] { p.getCharacter(), p.getProf() }, rates));
		}
		return table.toString();
	}

	// Phase Boons
	public String getPhaseBoons()
	{
		BoonFactory boonFactory = new BoonFactory();
		List<String[][]> all_rates = new ArrayList<String[][]>();
		List<String> boon_list = Boon.getList();
		List<Point> fight_intervals = getPhaseIntervals();
		int n = fight_intervals.size();
		int m = boon_list.size();

		for (Player p : p_list)
		{
			Map<String, List<BoonLog>> boon_logs = p.getBoonMap(b_data, s_data, c_data.getCombatList());
			String[][] rates = new String[m][];
			for (int j = 0; j < m; j++)
			{
				Boon boon = Boon.getEnum(boon_list.get(j));
				String[] rate = new String[fight_intervals.size()];
				Arrays.fill(rate, "0.00");
				List<BoonLog> logs = boon_logs.get(boon.getName());
				if (!logs.isEmpty())
				{
					AbstractBoon boon_object = boonFactory.makeBoon(boon);
					if (boon.getType().equals("duration"))
					{
						List<Point> boon_intervals = getBoonIntervalsList(boon_object, logs);
						rate = getBoonDuration(boon_intervals, fight_intervals);
					}
					else if (boon.getType().equals("intensity"))
					{
						List<Integer> boon_stacks = getBoonStacksList(boon_object, logs);
						rate = getAverageStacks(boon_stacks, fight_intervals);
					}
				}
				rates[j] = rate;
			}
			all_rates.add(rates);
		}

		// Heading
		StringBuilder output = new StringBuilder();
		String title = " Phase Boon Rates - " + b_data.getName() + ' ';
		output.append('\u250C' + Utility.fillWithChar(title.length(), '\u2500') + '\u2510' + System.lineSeparator());
		output.append('\u2502' + title + '\u2502' + System.lineSeparator());
		output.append('\u2514' + Utility.fillWithChar(title.length(), '\u2500') + '\u2518');

		// Table
		TableBuilder table = new TableBuilder();

		// Body
		String[] boon_array = Boon.getArray();
		String[] phase_names = b_data.getPhaseNames();
		for (int i = 0; i < n; i++)
		{
			table.clear();
			table.addTitle(phase_names[i]);
			table.addRow(Utility.concatStringArray(new String[] { "Name", "Profession" }, boon_array));
			int l = p_list.size();
			for (int j = 0; j < l; j++)
			{
				Player p = p_list.get(j);
				String[][] player_rates = all_rates.get(j);
				String[] row_rates = new String[m];
				for (int k = 0; k < m; k++)
				{
					row_rates[k] = player_rates[k][i];
				}
				table.addRow(Utility.concatStringArray(new String[] { p.getCharacter(), p.getProf() }, row_rates));
			}
			output.append(System.lineSeparator() + table.toString());
		}
		return output.toString();
	}

	// Private Methods
	private List<Point> getPhaseIntervals()
	{
		List<Point> fight_intervals = new ArrayList<Point>();
		int log_start = c_data.getCombatList().get(0).getTime();
		int time_start = b_data.getFirstAware() - log_start;
		int time_end = b_data.getLastAware() - log_start;

		// Thresholds
		int time_threshold = 0;
		int[] health_thresholds = null;
		if (b_data.getName().equals("Vale Guardian"))
		{
			time_threshold = 20000;
			health_thresholds = new int[] { 6600, 3300 };
		}
		else if (b_data.getName().equals("Gorseval the Multifarious"))
		{
			time_threshold = 20000;
			health_thresholds = new int[] { 6600, 3300 };
		}
		else if (b_data.getName().equals("S<NAME>ur"))
		{
			time_threshold = 20000;
			health_thresholds = new int[] { 7500, 5000, 2500 };
		}
		else if (b_data.getName().equals("Slothasor"))
		{
			health_thresholds = new int[] { 8000, 6000, 4000, 2000, 1000 };
		}
		else if (b_data.getName().equals("<NAME>"))
		{
			health_thresholds = new int[] { 8000, 6000, 4000 };
		}
		else if (b_data.getName().equals("Keep Construct"))
		{
			time_threshold = 20000;
			health_thresholds = new int[] { 6600, 3300 };
		}
		else if (b_data.getName().equals("Xera"))
		{
			time_threshold = 20000;
			health_thresholds = new int[] { 5000 };
		}
		else if (b_data.getName().equals("Cairn the Indomitable"))
		{
			health_thresholds = new int[] { 7500, 5000, 2500 };
		}
		else if (b_data.getName().equals("<NAME>"))
		{
			health_thresholds = new int[] { 7500, 5000, 2500 };
		}
		else if (b_data.getName().equals("Samarog"))
		{
			time_threshold = 20000;
			health_thresholds = new int[] { 6600, 3300 };
		}
		else if (b_data.getName().equals("Deimos"))
		{
			health_thresholds = new int[] { 7500, 5000, 2500 };
		}
		else
		{
			fight_intervals.add(new Point(time_start, time_end));
			return fight_intervals;
		}

		// Generate intervals with health updates
		ListIterator<Point> iter = c_data.getStates(b_data.getInstid(), StateChange.HEALTH_UPDATE).listIterator();
		Point previous_update = null;
		if (iter.hasNext())
		{
			previous_update = iter.next();
		}
		if (previous_update != null)
		{
			main: for (int threshold : health_thresholds)
			{
				while (iter.hasNext())
				{
					Point current_update = iter.next();
					if ((current_update.y <= threshold) && (time_threshold == 0))
					{
						fight_intervals.add(new Point(time_start, previous_update.x - log_start));
						time_start = previous_update.x - log_start;
						previous_update = current_update;
						continue main;
					}
					else if ((current_update.y <= threshold)
							&& ((current_update.x - previous_update.x) > time_threshold))
					{
						fight_intervals.add(new Point(time_start, previous_update.x - log_start));
						time_start = current_update.x - log_start;
						previous_update = current_update;
						continue main;

					}
					previous_update = current_update;
				}
			}
		}
		fight_intervals.add(new Point(time_start, time_end));
		return fight_intervals;
	}

	private List<Point> getBoonIntervalsList(AbstractBoon boon, List<BoonLog> boon_logs)
	{
		// Initialise variables
		int t_prev = 0;
		int t_curr = 0;
		List<Point> boon_intervals = new ArrayList<Point>();

		// Loop: update then add durations
		for (BoonLog log : boon_logs)
		{
			t_curr = log.getTime();
			boon.update(t_curr - t_prev);
			boon.add(log.getValue());
			boon_intervals.add(new Point(t_curr, t_curr + boon.getStackValue()));
			t_prev = t_curr;
		}

		// Merge intervals
		boon_intervals = Utility.mergeIntervals(boon_intervals);

		// Trim duration overflow
		int fight_duration = b_data.getLastAware() - b_data.getFirstAware();
		int last = boon_intervals.size() - 1;
		if (boon_intervals.get(last).getY() > fight_duration)
		{
			boon_intervals.get(last).y = fight_duration;
		}

		return boon_intervals;
	}

	private String getBoonDuration(List<Point> boon_intervals)
	{
		// Calculate average duration
		double average_duration = 0.0;
		for (Point p : boon_intervals)
		{
			average_duration = average_duration + (p.getY() - p.getX());
		}
		return String.format("%.2f", (average_duration / (b_data.getLastAware() - b_data.getFirstAware())));
	}

	private String[] getBoonDuration(List<Point> boon_intervals, List<Point> fight_intervals)
	{
		// Phase durations
		String[] phase_durations = new String[fight_intervals.size()];

		// Loop: add intervals in between, merge, calculate duration
		for (int i = 0; i < fight_intervals.size(); i++)
		{
			Point p = fight_intervals.get(i);
			List<Point> boons_intervals_during_phase = new ArrayList<Point>();
			for (Point b : boon_intervals)
			{
				if (b.x < p.y && p.x < b.y)
				{
					if (p.x <= b.x && b.y <= p.y)
					{
						boons_intervals_during_phase.add(b);
					}
					else if (b.x < p.x && p.y < b.y)
					{
						boons_intervals_during_phase.add(p);
					}
					else if (b.x < p.x && b.y <= p.y)
					{
						boons_intervals_during_phase.add(new Point(p.x, b.y));
					}
					else if (p.x <= b.x && p.y < b.y)
					{
						boons_intervals_during_phase.add(new Point(b.x, p.y));
					}
				}
			}
			double duration = 0.0;
			for (Point b : boons_intervals_during_phase)
			{
				duration = duration + (b.getY() - b.getX());
			}
			phase_durations[i] = String.format("%.2f", (duration / (p.getY() - p.getX())));
		}
		return phase_durations;
	}

	private List<Integer> getBoonStacksList(AbstractBoon boon, List<BoonLog> boon_logs)
	{
		// Initialise variables
		int t_prev = 0;
		int t_curr = 0;
		List<Integer> boon_stacks = new ArrayList<Integer>();
		boon_stacks.add(0);

		// Loop: fill, update, and add to stacks
		for (BoonLog log : boon_logs)
		{
			t_curr = log.getTime();
			boon.addStacksBetween(boon_stacks, t_curr - t_prev);
			boon.update(t_curr - t_prev);
			boon.add(log.getValue());
			if (t_curr != t_prev)
			{
				boon_stacks.add(boon.getStackValue());
			}
			else
			{
				boon_stacks.set(boon_stacks.size() - 1, boon.getStackValue());
			}
			t_prev = t_curr;
		}

		// Fill in remaining stacks
		boon.addStacksBetween(boon_stacks, b_data.getLastAware() - b_data.getFirstAware() - t_prev);
		boon.update(1);
		boon_stacks.add(boon.getStackValue());
		return boon_stacks;
	}

	private String getAverageStacks(List<Integer> boon_stacks)
	{
		// Calculate average stacks
		double average_stacks = boon_stacks.stream().mapToInt(Integer::intValue).sum();
		double average = average_stacks / boon_stacks.size();
		if (average > 10.0)
		{
			return String.format("%.1f", average);
		}
		else
		{
			return String.format("%.2f", average);
		}
	}

	private String[] getAverageStacks(List<Integer> boon_stacks, List<Point> fight_intervals)
	{
		// Phase stacks
		String[] phase_stacks = new String[fight_intervals.size()];

		// Loop: get sublist and calculate average stacks
		for (int i = 0; i < fight_intervals.size(); i++)
		{
			Point p = fight_intervals.get(i);
			List<Integer> phase_boon_stacks = new ArrayList<Integer>(boon_stacks.subList(p.x, p.y));
			double average_stacks = phase_boon_stacks.stream().mapToInt(Integer::intValue).sum();
			double average = average_stacks / phase_boon_stacks.size();
			if (average > 10.0)
			{
				phase_stacks[i] = String.format("%.1f", average);
			}
			else
			{
				phase_stacks[i] = String.format("%.2f", average);
			}
		}
		return phase_stacks;
	}
}
