Quantcast

Some structure cleanup.

Peter Eliasson [01-16-15 - 11:02]
Some structure cleanup.

* Moved source files to /src.
* Added a README file.
Filename
GuildSkadaHighScore.toc
README.md
gui.lua
highscore.lua
inspect.lua
lib/lib-include.xml
lib/lib.xml
main.lua
main.xml
src/gui.lua
src/highscore.lua
src/inspect.lua
src/main.lua
src/src-include.xml
diff --git a/GuildSkadaHighScore.toc b/GuildSkadaHighScore.toc
index 661c429..90a9b82 100644
--- a/GuildSkadaHighScore.toc
+++ b/GuildSkadaHighScore.toc
@@ -2,9 +2,9 @@
 ## Author: Verath (Peter Eliasson)
 ## Version: 0.0.2
 ## Title: Guild Skada High Score
-## Notes: Ranking for dps/hps for raid bosses in a guild.
+## Notes: Tracking and ranking of dps/hps in a guild.
 ## SavedVariables: GuildSkadaHighScoreDB
 ## Dependencies: Skada

-lib/lib.xml
-main.xml
+lib/lib-include.xml
+src/src-include.xml
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..f99ea58
--- /dev/null
+++ b/README.md
@@ -0,0 +1,3 @@
+# Guild Skada High Score
+A World of Warcraft addon for tracking and ranking dps/hps for encounters defeated by a guild.
+
diff --git a/gui.lua b/gui.lua
deleted file mode 100644
index 797ff80..0000000
--- a/gui.lua
+++ /dev/null
@@ -1,396 +0,0 @@
-local addonName, addonTable = ...
-
--- Global functions for faster access
-local tinsert = tinsert;
-local tContains = tContains;
-
--- Set up module
-local addon = addonTable[1];
-local gui = addon:NewModule("gui", "AceHook-3.0")
-addon.gui = gui;
-
--- AceGUI
-local AceGUI = LibStub("AceGUI-3.0");
-
-
-function gui:CreateHighScoreParseEntry(parse, role, rank)
-	local entryWidget = AceGUI:Create("SimpleGroup");
-	entryWidget:SetFullWidth(true);
-	entryWidget:SetLayout("Flow");
-
-	local rankLabel = AceGUI:Create("Label");
-	rankLabel:SetText(rank);
-	rankLabel:SetRelativeWidth(0.14);
-
-	local dpsHpsLabel = AceGUI:Create("Label");
-	local dpsHps = Skada:FormatNumber((role == "HEALER") and parse.hps or parse.dps);
-	dpsHpsLabel:SetText(dpsHps);
-	dpsHpsLabel:SetRelativeWidth(0.14);
-
-	local nameLabel = AceGUI:Create("Label");
-	nameLabel:SetText(parse.name);
-	nameLabel:SetRelativeWidth(0.14);
-
-	local classLabel = AceGUI:Create("Label");
-	classLabel:SetText(parse.class);
-	classLabel:SetRelativeWidth(0.14);
-
-	local specLabel = AceGUI:Create("Label");
-	specLabel:SetText(parse.specName or "");
-	specLabel:SetRelativeWidth(0.14);
-
-	local ilvlLabel = AceGUI:Create("Label");
-	ilvlLabel:SetText(parse.itemLevel or "");
-	ilvlLabel:SetRelativeWidth(0.14);
-
-	local dateLabel = AceGUI:Create("Label");
-	dateLabel:SetText(date("%m/%d/%y", parse.startTime));
-	dateLabel:SetRelativeWidth(0.14);
-
-	entryWidget:AddChild(rankLabel);
-	entryWidget:AddChild(dpsHpsLabel);
-	entryWidget:AddChild(nameLabel);
-	entryWidget:AddChild(classLabel);
-	entryWidget:AddChild(specLabel);
-	entryWidget:AddChild(ilvlLabel);
-	entryWidget:AddChild(dateLabel);
-
-	return entryWidget;
-end
-
-function gui:CreateGuildDropdown()
-	local dropdown = AceGUI:Create("Dropdown");
-	self.guildDropdown = dropdown;
-
-	dropdown:SetLabel("Guild");
-	dropdown:SetRelativeWidth(0.25);
-	dropdown:SetCallback("OnValueChanged", function(widget, evt, guildId)
-		gui:SetSelectedGuild(guildId);
-	end)
-
-	local guilds, numGuilds = addon.highscore:GetGuilds();
-	if numGuilds > 0 then
-		dropdown:SetList(guilds);
-	else
-		dropdown:SetDisabled(true);
-		dropdown:SetText("No Guilds.");
-	end
-
-	return dropdown;
-end
-
-function gui:CreateZoneDropdown()
-	local dropdown = AceGUI:Create("Dropdown");
-	self.zoneDropdown = dropdown;
-
-	dropdown:SetLabel("Zone");
-	dropdown:SetRelativeWidth(0.25);
-	dropdown:SetList(nil);
-	dropdown:SetDisabled(true);
-	dropdown:SetCallback("OnValueChanged", function(widget, evt, zoneId)
-		gui:SetSelectedZone(zoneId);
-	end)
-
-	return dropdown;
-end
-
-function gui:CreateDifficultyDropdown()
-	local dropdown = AceGUI:Create("Dropdown");
-	self.difficultyDropdown = dropdown;
-
-	dropdown:SetLabel("Difficulty");
-	dropdown:SetRelativeWidth(0.25);
-	dropdown:SetList(nil);
-	dropdown:SetDisabled(true);
-	dropdown:SetCallback("OnValueChanged", function(widget, evt, difficultyId)
-		gui:SetSelectedDifficulty(difficultyId);
-	end)
-
-	return dropdown;
-end
-
-function gui:CreateEncounterDropdown()
-	local dropdown = AceGUI:Create("Dropdown");
-	self.encounterDropdown = dropdown;
-
-	dropdown:SetLabel("Encounter");
-	dropdown:SetRelativeWidth(0.25);
-	dropdown:SetList(nil);
-	dropdown:SetDisabled(true);
-	dropdown:SetCallback("OnValueChanged", function(widget, evt, encounterId)
-		gui:SetSelectedEncounter(encounterId);
-	end)
-
-	return dropdown;
-end
-
-function gui:CreateHighScoreScrollFrame()
-	local scrollFrame = AceGUI:Create("ScrollFrame");
-	scrollFrame:SetLayout("Flow");
-	scrollFrame:SetFullWidth(true);
-	scrollFrame:SetFullHeight(true);
-
-	-- Header:
-	-- Rank | DPS/HPS | Name | Class | Spec | Item Level | Date
-	local headerContainer = AceGUI:Create("SimpleGroup");
-	headerContainer:SetFullWidth(true);
-	headerContainer:SetLayout("Flow");
-
-	local rankLabel = AceGUI:Create("Label");
-	rankLabel:SetText("Rank");
-	rankLabel:SetRelativeWidth(0.14);
-	local dpsHpsLabel = AceGUI:Create("Label");
-	dpsHpsLabel:SetText("DPS/HPS");
-	dpsHpsLabel:SetRelativeWidth(0.14);
-	local nameLabel = AceGUI:Create("Label");
-	nameLabel:SetText("Name");
-	nameLabel:SetRelativeWidth(0.14);
-	local classLabel = AceGUI:Create("Label");
-	classLabel:SetText("Class");
-	classLabel:SetRelativeWidth(0.14);
-	local specLabel = AceGUI:Create("Label");
-	specLabel:SetText("Spec");
-	specLabel:SetRelativeWidth(0.14);
-	local ilvlLabel = AceGUI:Create("Label");
-	ilvlLabel:SetText("Item Level");
-	ilvlLabel:SetRelativeWidth(0.14);
-	local dateLabel = AceGUI:Create("Label");
-	dateLabel:SetText("Date");
-	dateLabel:SetRelativeWidth(0.14);
-
-	headerContainer:AddChild(rankLabel);
-	headerContainer:AddChild(dpsHpsLabel);
-	headerContainer:AddChild(nameLabel);
-	headerContainer:AddChild(classLabel);
-	headerContainer:AddChild(specLabel);
-	headerContainer:AddChild(ilvlLabel);
-	headerContainer:AddChild(dateLabel);
-
-	local parsesContainer = AceGUI:Create("SimpleGroup");
-	self.highScoreParsesContainer = parsesContainer;
-	parsesContainer:SetFullWidth(true);
-	parsesContainer:SetLayout("Flow");
-
-	scrollFrame:AddChild(headerContainer);
-	scrollFrame:AddChild(parsesContainer);
-
-	return scrollFrame;
-end
-
-function gui:CreateHighScoreTabGroup()
-	local container = AceGUI:Create("TabGroup");
-	self.highScoreTabGroup = container;
-
-	container:SetFullWidth(true);
-	container:SetFullHeight(true);
-	container:SetLayout("Fill");
-	container:SetTabs({
-		{value = "DAMAGER", text = "DPSers"},
-		{value = "HEALER", text = "Healers"},
-		{value = "TANK", text = "Tanks"}
-	});
-	container:SetCallback("OnGroupSelected", function(widget, evt, roleId)
-		gui:SetSelectedRole(roleId);
-	end)
-
-	container:AddChild(self:CreateHighScoreScrollFrame());
-
-	return container;
-end
-
-function gui:CreateMainFrame()
-	local frame = AceGUI:Create("Frame")
-	self.mainFrame = frame;
-
-	frame:Hide()
-	frame:SetWidth(800)
-	frame:SetHeight(600)
-	frame:SetTitle("Guild Skada High Score")
-	frame:SetLayout("Flow")
-	frame:SetCallback("OnClose", function()
-		gui:HideMainFrame()
-	end)
-
-	frame:AddChild(self:CreateGuildDropdown());
-	frame:AddChild(self:CreateZoneDropdown());
-	frame:AddChild(self:CreateDifficultyDropdown());
-	frame:AddChild(self:CreateEncounterDropdown());
-	frame:AddChild(self:CreateHighScoreTabGroup());
-
-	return frame;
-end
-
-function gui:DisplayParses()
-	local guildName = self.selectedGuild;
-	local zoneId = self.selectedZone;
-	local difficultyId = self.selectedDifficulty;
-	local encounter = self.selectedEncounter;
-	local roleId = self.selectedRole;
-
-	local parsesContainer = self.highScoreParsesContainer;
-	parsesContainer:ReleaseChildren();
-
-	if guildName and zoneId and difficultyId and encounter and roleId then
-		local parses, numParses = addon.highscore:GetParses(guildName,
-			zoneId, difficultyId, encounter, roleId);
-		if numParses > 0 then
-			for rank, parse in ipairs(parses) do
-				local entryWidget = self:CreateHighScoreParseEntry(parse, roleId, rank);
-				parsesContainer:AddChild(entryWidget);
-			end
-			return;
-		end
-	end
-
-	local noParsesLabel = AceGUI:Create("Label");
-	noParsesLabel:SetText("No parses found.");
-	parsesContainer:AddChild(noParsesLabel);
-end
-
-function gui:SetSelectedRole(roleId, noPropagation)
-	-- SelectTab, unlike SetValue for dropdowns, triggers the callback
-	if self.selectedRole ~= roleId then
-		self.selectedRole = roleId;
-		self.highScoreTabGroup:SelectTab(roleId);
-		self:DisplayParses();
-	end
-end
-
-function gui:SetSelectedEncounter(encounterId, noPropagation)
-	self.selectedEncounter = encounterId;
-	self.encounterDropdown:SetValue(encounterId);
-	self:DisplayParses();
-end
-
-function gui:SetSelectedDifficulty(difficultyId, noPropagation)
-	self.selectedDifficulty = difficultyId;
-	self.difficultyDropdown:SetValue(difficultyId);
-
-	-- Update encounter dropdown with new guild, zone, difficulty
-	local encounters, numEncounters = addon.highscore:GetEncounters(self.selectedGuild, self.selectedZone, self.selectedDifficulty);
-	if numEncounters > 0 then
-		self.encounterDropdown:SetDisabled(false);
-		self.encounterDropdown:SetList(encounters);
-	else
-		self.encounterDropdown:SetDisabled(true);
-		self.encounterDropdown:SetList(nil);
-		self.encounterDropdown:SetText(nil);
-	end
-
-	if not noPropagation then
-		if numEncounters == 1 then
-			-- If only one option, select it.
-			local encounterId, _ = next(encounters);
-			self:SetSelectedEncounter(encounterId);
-		else
-			self:SetSelectedEncounter(nil);
-		end
-	end
-end
-
-function gui:SetSelectedZone(zoneId, noPropagation)
-	self.selectedZone = zoneId;
-	self.zoneDropdown:SetValue(zoneId);
-
-	-- Update difficulty dropdown with new guild, zone
-	local difficulties, numDifficulties = addon.highscore:GetDifficulties(self.selectedGuild, self.selectedZone);
-	if numDifficulties > 0 then
-		self.difficultyDropdown:SetDisabled(false);
-		self.difficultyDropdown:SetList(difficulties);
-	else
-		self.difficultyDropdown:SetDisabled(true);
-		self.difficultyDropdown:SetList(nil);
-		self.difficultyDropdown:SetText(nil);
-	end
-
-	if not noPropagation then
-		if numDifficulties == 1 then
-			-- If only one option, select it.
-			local difficultyId, _ = next(difficulties);
-			self:SetSelectedDifficulty(difficultyId);
-		else
-			self:SetSelectedDifficulty(nil);
-		end
-	end
-end
-
-function gui:SetSelectedGuild(guildId, noPropagation)
-	self.selectedGuild = guildId;
-	self.guildDropdown:SetValue(guildId);
-
-	-- Update zone dropdown for the new guild
-	local zones, numZones = addon.highscore:GetZones(guildId);
-	if numZones > 0 then
-		self.zoneDropdown:SetDisabled(false);
-		self.zoneDropdown:SetList(zones);
-	else
-		self.zoneDropdown:SetDisabled(true);
-		self.zoneDropdown:SetList(nil);
-		self.zoneDropdown:SetText(nil);
-	end
-
-	if not noPropagation then
-		if numZones == 1 then
-			-- If only one option, select it.
-			local zoneId, _ = next(zones);
-			self:SetSelectedZone(zoneId);
-		else
-			self:SetSelectedZone(nil);
-		end
-	end
-end
-
-function gui:ShowMainFrame()
-	if not self.mainFrame then
-		-- Only show if not already shown
-		self:CreateMainFrame():Show();
-
-		if self.selectedGuild then
-			-- Try to restore to same values as before
-			gui:SetSelectedGuild(self.selectedGuild, true);
-			gui:SetSelectedZone(self.selectedZone, true);
-			gui:SetSelectedDifficulty(self.selectedDifficulty, true);
-			gui:SetSelectedEncounter(self.selectedEncounter, true);
-			gui:SetSelectedRole(self.selectedRole, true);
-		elseif addon.guildName then
-			-- Try pre-selecting own guild if has one.
-			gui:SetSelectedGuild(addon.guildName);
-		end
-
-		-- Have to do special for our tab group as it is never disabled
-		gui:SetSelectedRole(self.selectedRole or "DAMAGER");
-	end
-end
-
-function gui:HideMainFrame()
-	if self.mainFrame then
-		self.mainFrame:Release();
-
-		-- Unset references
-		self.mainFrame = nil;
-		self.guildDropdown = nil;
-		self.zoneDropdown = nil;
-		self.difficultyDropdown = nil;
-		self.encounterDropdown = nil;
-		self.highScoreTabGroup = nil;
-	end
-end
-
-function gui:OnCloseSpecialWindows()
-	if self.mainFrame then
-		self:HideMainFrame()
-		return true
-	else
-		return self.hooks["CloseSpecialWindows"]();
-	end
-end
-
-
-function gui:OnEnable()
-	self:RawHook("CloseSpecialWindows", "OnCloseSpecialWindows");
-end
-
-function gui:OnDisable()
-	self:HideMainFrame();
-	self:UnHook("CloseSpecialWindows");
-end
\ No newline at end of file
diff --git a/highscore.lua b/highscore.lua
deleted file mode 100644
index ccd109c..0000000
--- a/highscore.lua
+++ /dev/null
@@ -1,326 +0,0 @@
-local addonName, addonTable = ...
-
--- Global functions for faster access
-local tinsert = tinsert;
-local tContains = tContains;
-local sort = sort;
-local random = random;
-local format = format;
-
--- Set up module
-local addon = addonTable[1];
-local highscore = addon:NewModule("highscore", "AceEvent-3.0", "AceTimer-3.0")
-addon.highscore = highscore;
-
--- db defaults
-addon.dbDefaults.realm.modules["highscore"] = {
-	["guilds"] = {
-		["*"] = { -- Guild Name
-			["zones"] = {
-				["*"] = { -- zoneId
-					["difficulties"] = {
-						["*"] = { -- difficultyId
-							["encounters"] = {
-								["*"] = { -- encounterId
-									--[[
-										playerParses is a list of objects:
-										{
-											playerId 	 = "",
-											role 		 = "",
-											specName 	 = "",
-											itemLevel 	 = 0,
-											damage 		 = 0,
-											healing 	 = 0,
-											groupParseId = ""
-										}
-									--]]
-									playerParses = {}
-								}
-							}
-						}
-					}
-				}
-			}
-		}
-	},
-	["zones"] = {
-		["*"] = { -- zoneId
-			zoneName = nil
-		}
-	},
-	["difficulties"] = {
-		["*"] = { -- difficultyId
-			difficultyName = nil
-		}
-	},
-	["encounters"] = {
-		["*"] = { -- encounterId
-			encounterName = nil
-		}
-	},
-	["players"] = {
-		["*"] = { -- playerId
-			name 	= nil,
-			class 	= nil
-		}
-	},
-	["groupParses"] = {
-		--[[
-		["*"] = { -- groupParseId
-			startTime 	= 0,
-			duration 	= 0,
-		}
-		--]]
-	}
-}
-
-addon.dbVersion = addon.dbVersion + 4
-
--- Constants
-local TRACKED_ZONE_IDS = {
-	994 -- Highmaul
-}
-
-
--- Function for convering a database representation of
--- a parse to a parse that can be returned to users.
--- Copies all values and adds calculated once (like dps)
-local function getReturnableParse(db, parse)
-	local parseCopy = {};
-	for key, val in pairs(parse) do
-		parseCopy[key] = val;
-	end
-
-	-- Get duration, statTime from the group parse
-	local groupParse = db.groupParses[parse["groupParseId"]]
-	parseCopy["duration"] = groupParse.duration;
-	parseCopy["startTime"] = groupParse.startTime;
-	parseCopy["groupParseId"] = nil
-
-	-- Get player name and class
-	parseCopy["name"] = db.players[parse["playerId"]].name;
-	parseCopy["class"] = db.players[parse["playerId"]].class;
-
-	-- Calculate dps/hps
-	parseCopy["dps"] = 0;
-	parseCopy["hps"] = 0;
-	if parseCopy["duration"] > 0 then
-		parseCopy["dps"] = parseCopy["damage"] / parseCopy["duration"];
-		parseCopy["hps"] = parseCopy["healing"] / parseCopy["duration"];
-	end
-
-	return parseCopy;
-end
-
-local function generateRandomKey()
-	local r1 = random(0, 1000);
-	local r2 = random(0, 1000);
-	local r3 = random(0, 1000);
-	-- 1000^3 = 1´000´000´000, should be enough for now...
-	return format("%x-%x-%x", r1, r2, r3);
-end
-
-local function addGroupParse(db, startTime, duration)
-	-- Find a new unique key for the raid parse
-	local key = generateRandomKey();
-	while db.groupParses[key] do
-		key = generateRandomKey();
-	end
-
-	db.groupParses[key] = {
-		startTime 	= startTime,
-		duration 	= duration
-	};
-
-	return key
-end
-
-local function addZone(db, zoneId, zoneName)
-	db.zones[zoneId].zoneName = zoneName;
-end
-
-local function addDifficulty(db, difficultyId, difficultyName)
-	db.difficulties[difficultyId].difficultyName = difficultyName;
-end
-
-local function addEncounter(db, encounterId, encounterName)
-	db.encounters[encounterId].encounterName = encounterName;
-end
-
-local function addPlayer(db, playerId, playerName, playerClass)
-	db.players[playerId].name = playerName;
-	db.players[playerId].class = playerClass;
-end
-
-local function getParsesTable(db, guildName, zoneId, difficultyId, encounterId)
-	return db
-		.guilds[guildName]
-		.zones[zoneId]
-		.difficulties[difficultyId]
-		.encounters[encounterId]
-		.playerParses;
-end
-
-local function addEncounterParseForPlayer(parsesTable, player, groupParseId)
-	local parse = {
-		playerId 	 = player.id,
-		role 		 = player.role,
-		specName 	 = player.specName,
-		itemLevel 	 = player.itemLevel,
-		damage 		 = player.damage,
-		healing 	 = player.healing,
-		groupParseId = groupParseId
-	}
-	tinsert(parsesTable, parse);
-end
-
-function highscore:AddEncounterParsesForPlayers(guildName, encounter, players)
-	local zoneId = encounter.zoneId;
-	local zoneName = encounter.zoneName;
-	local encounterId = encounter.id;
-	local encounterName = encounter.name;
-	local difficultyId = encounter.difficultyId;
-	local difficultyName = encounter.difficultyName;
-	local startTime = encounter.startTime;
-	local duration = encounter.duration;
-
-	assert(guildName)
-	assert(zoneId)
-	assert(zoneName)
-	assert(encounterId)
-	assert(encounterName)
-	assert(difficultyId)
-	assert(difficultyName)
-	assert(startTime)
-	assert(duration)
-	assert(players)
-
-	if not tContains(TRACKED_ZONE_IDS, zoneId) then
-		self:Debug("AddEncounterParsesForPlayers: Current zone not not in tracked zones");
-		return
-	end
-
-	-- Add zone, difficulty and encounter info
-	addZone(self.db, zoneId, zoneName);
-	addDifficulty(self.db, difficultyId, difficultyName);
-	addEncounter(self.db, encounterId, encounterName);
-
-	-- Add a group parse entry, holding data shared between all players
-	local groupParseId = addGroupParse(self.db, startTime, duration);
-
-	local parsesTable = getParsesTable(self.db, guildName, zoneId, difficultyId, encounterId);
-
-	for _, player in ipairs(players) do
-		self:Debug(format("addEncounterParseForPlayer: %s", player.name));
-
-		addPlayer(self.db, player.id, player.name, player.class);
-		addEncounterParseForPlayer(parsesTable, player, groupParseId)
-	end
-
-end
-
--- Returns (array of parses, numParses)
-function highscore:GetParses(guildName, zoneId, difficultyId, encounterId, role, sortBy)
-	if (role ~= "TANK" and role ~= "HEALER" and role ~= "DAMAGER") then
-		return {}, 0;
-	end
-
-	if not sortBy then
-		if role == "TANK" or role == "DAMAGER" then
-			sortBy = "dps"
-		elseif role == "HEALER" then
-			sortBy = "hps"
-		end
-	end
-
-	local parsesTable = getParsesTable(self.db, guildName, zoneId, difficultyId, encounterId);
-
-	-- Get a *copy* of all parses for the specified role
-	local parses = {};
-	local numParses = 0;
-	for _, parse in ipairs(parsesTable) do
-		if parse.role == role then
-			local parseCopy = getReturnableParse(self.db, parse);
-			tinsert(parses, parseCopy);
-			numParses = numParses + 1;
-		end
-	end
-
-	sort(parses, function(a, b)
-		return a[sortBy] > b[sortBy];
-	end)
-	return parses, numParses;
-end
-
--- Returns (array of {encounterId => encounterName}, numEncounters)
-function highscore:GetEncounters(guildName, zoneId, difficultyId)
-	if not guildName or not zoneId or not difficultyId then
-		return {}, 0;
-	end
-
-	local encounters = {};
-	local numEncounters = 0;
-	local difficultiesTable = self.db.guilds[guildName].zones[zoneId].difficulties;
-
-	for encounterId, _ in pairs(difficultiesTable[difficultyId].encounters) do
-		local encounterName = self.db.encounters[encounterId].encounterName;
-		encounters[encounterId] = encounterName;
-		numEncounters = numEncounters + 1;
-	end
-	return encounters, numEncounters;
-end
-
--- Returns (array of {difficultyId => difficultyName}, numDifficulties)
-function highscore:GetDifficulties(guildName, zoneId)
-	if not guildName or not zoneId then
-		return {}, 0;
-	end
-
-	local difficulties = {};
-	local numDifficulties = 0;
-	local difficultiesTable = self.db.guilds[guildName].zones[zoneId].difficulties;
-
-	for difficultyId, _ in pairs(difficultiesTable) do
-		local difficultyName = self.db.difficulties[difficultyId].difficultyName;
-		difficulties[difficultyId] = difficultyName;
-		numDifficulties = numDifficulties + 1;
-	end
-	return difficulties, numDifficulties;
-end
-
--- Returns (array of {zoneId => zoneName}, numZones)
-function highscore:GetZones(guildName)
-	if not guildName then
-		return {}, 0;
-	end
-
-	local zones = {};
-	local numZones = 0;
-	for zoneId, _ in pairs(self.db.guilds[guildName].zones) do
-		local zoneName = self.db.zones[zoneId].zoneName;
-		zones[zoneId] = zoneName;
-		numZones = numZones + 1;
-	end
-	return zones, numZones;
-end
-
--- Returns (array of {guildId => guildName}, numGuilds)
-function highscore:GetGuilds()
-	local guildNames = {};
-	local numGuilds = 0;
-	for guildName, _ in pairs(self.db.guilds) do
-		-- Actually guildId == guildName
-		guildNames[guildName] = guildName;
-		numGuilds = numGuilds + 1;
-	end
-	return guildNames, numGuilds;
-end
-
-
-function highscore:OnEnable()
-	self.db = addon.db.realm.modules["highscore"];
-end
-
-function highscore:OnDisable()
-	self.db = nil;
-end
diff --git a/inspect.lua b/inspect.lua
deleted file mode 100644
index dce5075..0000000
--- a/inspect.lua
+++ /dev/null
@@ -1,275 +0,0 @@
-local addonName, addonTable = ...
-
--- Global functions for faster access
-local tinsert = tinsert;
-local floor = floor;
-local wipe = wipe;
-
--- Set up module
-local addon = addonTable[1];
-local inspect = addon:NewModule("inspect", "AceEvent-3.0", "AceTimer-3.0")
-addon.inspect = inspect;
-
--- Constants
-local INSPECT_CACHE_TIMEOUT = 900;
-local INVENTORY_SLOT_NAMES = {
-	"HeadSlot","NeckSlot","ShoulderSlot","BackSlot","ChestSlot","WristSlot",
-	"HandsSlot","WaistSlot","LegsSlot","FeetSlot","Finger0Slot","Finger1Slot",
-	"Trinket0Slot","Trinket1Slot","MainHandSlot","SecondaryHandSlot"
-}
-local NOOP = function() end
-
-local playerGUID = UnitGUID("player");
-local inspectCache = {};
-
-
-
-local function isInPVEInstance()
-	local isInstance, instanceType = IsInInstance();
-	return inInstance and (instanceType == "party" or instanceType == "raid")
-end
-
-local function getTalentSpec(unitName)
-	if unitName == "player" then
-		local spec = GetSpecialization();
-		if spec and spec > 0 then
-			local _, name = GetSpecializationInfo(spec);
-			return name;
-		end
-	else
-		local spec = GetInspectSpecialization(unitName)
-		if spec and spec > 0 then
-			local role = GetSpecializationRoleByID(spec);
-			if role then
-				local _, name = GetSpecializationInfoByID(spec);
-				return name
-			end
-		end
-	end
-end
-
-local function getItemLevel(unitName)
-	if unitName == "player" then
-		local _, equipped = GetAverageItemLevel();
-		return floor(equipped);
-	else
-		local total, numItems = 0, 0;
-		for i = 1, #INVENTORY_SLOT_NAMES do
-			local slotName = INVENTORY_SLOT_NAMES[i];
-			local slotId = GetInventorySlotInfo(slotName);
-			local itemLink = GetInventoryItemLink(unitName, slotId);
-
-			if itemLink then
-				local _, _, _, itemLevel = GetItemInfo(itemLink)
-				if itemLevel and itemLevel > 0 then
-					numItems = numItems + 1;
-					total = total + itemLevel;
-				end
-			end
-		end
-		if total < 1 or numItems < 15 then
-			return nil
-		else
-			return floor(total / numItems)
-		end
-	end
-end
-
-local function hasPlayerInspectCache(playerId, ignoreExpired)
-	if inspectCache[playerId] and ignoreExpired then
-		return true -- We have a cache, might be expired
-	elseif inspectCache[playerId] and inspectCache[playerId].time then
-		local isExpired = (GetTime() - inspectCache[playerId].time) > INSPECT_CACHE_TIMEOUT
-		local hasAllAttributes = inspectCache[playerId].specName and inspectCache[playerId].itemLevel;
-
-		return (not isExpired and hasAllAttributes)
-	else
-		return false; -- No entry for player id
-	end
-end
-
-local function setPlayerInspectCache(playerId, specName, itemLevel)
-	inspectCache[playerId] = {};
-	inspectCache[playerId].time = GetTime();
-	inspectCache[playerId].specName = specName;
-	inspectCache[playerId].itemLevel = itemLevel;
-end
-
-function inspect:StartNotifyInspectTimer()
-	if not self.notifyInspectTimer then
-		self:Debug("Starting notifyInspectTimer");
-
-		self.notifyInspectTimer = self:ScheduleRepeatingTimer(function()
-			self:NOTIFY_INSPECT_TIMER_DONE()
-		end, 1);
-	end
-end
-
-function inspect:StopNotifyInspectTimer()
-	if self.notifyInspectTimer then
-		self:Debug("Stopping notifyInspectTimer");
-
-		self:CancelTimer(self.notifyInspectTimer);
-		self.notifyInspectTimer = nil;
-	end
-end
-
-function inspect:QueueInspect(player, callback)
-	self:Debug("QueueInspect", player.name)
-
-	if not self.inspectQueue[player.id] then
-		self.inspectQueue[player.id] = {player = player, callbacks = {}};
-	end
-	tinsert(self.inspectQueue[player.id].callbacks, callback);
-
-	self:StartNotifyInspectTimer();
-end
-
-function inspect:SetCachedInspectDataForPlayer(player)
-	if player.id == playerGUID then
-		player.specName = getTalentSpec("player")
-		player.itemLevel = getItemLevel("player");
-		return true
-	elseif hasPlayerInspectCache(player.id, false) then
-		player.specName = inspectCache[player.id].specName;
-		player.itemLevel = inspectCache[player.id].itemLevel;
-		return true
-	elseif hasPlayerInspectCache(player.id, true) then
-		-- Expired cache, but better than nothing
-		player.specName = inspectCache[player.id].specName;
-		player.itemLevel = inspectCache[player.id].itemLevel;
-		return false
-	else
-		return false
-	end
-end
-
-function inspect:GetInspectDataForPlayer(player, callback)
-	-- Make sure we always have a callback
-	callback = callback and callback or NOOP;
-
-	if self:SetCachedInspectDataForPlayer(player) then
-		return callback()
-	elseif not CanInspect(player.name, false) then
-		return callback()
-	else
-		self:QueueInspect(player, callback);
-	end
-end
-
-function inspect:GetInspectDataForPlayers(players, callback)
-	local totalCallbacks = #players;
-	local doneCallbacks = 0;
-
-	for _, player in ipairs(players) do
-		self:GetInspectDataForPlayer(player, function()
-			doneCallbacks = doneCallbacks + 1;
-			if doneCallbacks == totalCallbacks then
-				callback();
-			end
-		end)
-	end
-end
-
-function inspect:ResolveInspect(playerId, success)
-	if not self.inspectQueue[playerId] then
-		return
-	end
-
-	local player = self.inspectQueue[playerId].player;
-	local callbacks = self.inspectQueue[playerId].callbacks;
-	self.inspectQueue[playerId] = nil;
-
-	self:Debug("ResolveInspect", player.name, (success and "success" or "fail"))
-
-	if success then
-		if not hasPlayerInspectCache(player.id) then
-			local specName = getTalentSpec(player.name);
-			local itemLevel = getItemLevel(player.name);
-			setPlayerInspectCache(player.id, specName, itemLevel);
-		end
-
-		player.specName = inspectCache[player.id].specName;
-		player.itemLevel = inspectCache[player.id].itemLevel;
-	end
-
-	for _,callback in ipairs(callbacks) do
-		callback();
-	end
-end
-
-function inspect:PreInspectGroup()
-	for i=1, GetNumGroupMembers() do
-
-		local playerName = GetRaidRosterInfo(i);
-		if playerName and addon:IsInMyGuild(playerName) then
-
-			local playerId = UnitGUID(playerName)
-			if playerId and playerId ~= playerGUID and not hasPlayerInspectCache(playerId) then
-				local player = {name=playerName, id=playerId}
-				self:QueueInspect(player, NOOP);
-			end
-		end
-	end
-end
-
-function inspect:NOTIFY_INSPECT_TIMER_DONE()
-	-- Timeout any current inspection
-	if self.currentInspectPlayerId then
-		self:ResolveInspect(self.currentInspectPlayerId, false);
-		self.currentInspectPlayerId = nil;
-	end
-
-	local playerId, inspectData = next(self.inspectQueue);
-	if playerId then
-		NotifyInspect(inspectData.player.name);
-		self.currentInspectPlayerId = playerId;
-	else
-		self:StopNotifyInspectTimer();
-	end
-end
-
-function inspect:INSPECT_READY(evt, GUID)
-	if self.currentInspectPlayerId == GUID then
-		self:ResolveInspect(self.currentInspectPlayerId, true)
-		self.currentInspectPlayerId = nil;
-	end
-end
-
-function inspect:GROUP_ROSTER_UPDATE(evt)
-	if not IsInGroup() then
-		self:Debug("Left group, wiping inspect cache");
-		wipe(inspectCache);
-		wipe(inspect.inspectQueue);
-		inspect.currentInspectPlayerId = nil;
-	end
-end
-
-function inspect:ZONE_CHANGED_NEW_AREA(evt)
-	if isInPVEInstance() then
-		-- We just zoned into an instance, try pre-inspecting the group
-		self:PreInspectGroup();
-	end
-end
-
-function inspect:OnEnable()
-	inspect.inspectQueue = {};
-	inspect.notifyInspectTimer = nil;
-	inspect.currentInspectPlayerId = nil;
-
-	self:RegisterEvent("INSPECT_READY");
-	self:RegisterEvent("GROUP_ROSTER_UPDATE");
-	self:RegisterEvent("ZONE_CHANGED_NEW_AREA");
-end
-
-function inspect:OnDisable()
-	self:UnregisterEvent("INSPECT_READY");
-	self:UnregisterEvent("GROUP_ROSTER_UPDATE");
-	self:UnregisterEvent("ZONE_CHANGED_NEW_AREA");
-
-	self:StopNotifyInspectTimer();
-
-	wipe(inspectCache);
-	wipe(inspect.inspectQueue);
-	inspect.currentInspectPlayerId = nil;
-end
\ No newline at end of file
diff --git a/lib/lib-include.xml b/lib/lib-include.xml
new file mode 100644
index 0000000..973e258
--- /dev/null
+++ b/lib/lib-include.xml
@@ -0,0 +1,11 @@
+<Ui xsi:schemaLocation="http://www.blizzard.com/wow/ui/ ..\..\FrameXML\UI.xsd">
+    <Script file="Ace3\LibStub\LibStub.lua"/>
+
+    <Include file="Ace3\AceAddon-3.0\AceAddon-3.0.xml"/>
+    <Include file="Ace3\AceConsole-3.0\AceConsole-3.0.xml"/>
+    <Include file="Ace3\AceGUI-3.0\AceGUI-3.0.xml"/>
+    <Include file="Ace3\AceHook-3.0\AceHook-3.0.xml"/>
+    <Include file="Ace3\AceTimer-3.0\AceTimer-3.0.xml"/>
+    <Include file="Ace3\AceDB-3.0\AceDB-3.0.xml"/>
+    <Include file="Ace3\AceGUI-3.0\AceGUI-3.0.xml"/>
+</Ui>
\ No newline at end of file
diff --git a/lib/lib.xml b/lib/lib.xml
deleted file mode 100644
index 973e258..0000000
--- a/lib/lib.xml
+++ /dev/null
@@ -1,11 +0,0 @@
-<Ui xsi:schemaLocation="http://www.blizzard.com/wow/ui/ ..\..\FrameXML\UI.xsd">
-    <Script file="Ace3\LibStub\LibStub.lua"/>
-
-    <Include file="Ace3\AceAddon-3.0\AceAddon-3.0.xml"/>
-    <Include file="Ace3\AceConsole-3.0\AceConsole-3.0.xml"/>
-    <Include file="Ace3\AceGUI-3.0\AceGUI-3.0.xml"/>
-    <Include file="Ace3\AceHook-3.0\AceHook-3.0.xml"/>
-    <Include file="Ace3\AceTimer-3.0\AceTimer-3.0.xml"/>
-    <Include file="Ace3\AceDB-3.0\AceDB-3.0.xml"/>
-    <Include file="Ace3\AceGUI-3.0\AceGUI-3.0.xml"/>
-</Ui>
\ No newline at end of file
diff --git a/main.lua b/main.lua
deleted file mode 100644
index 91ab007..0000000
--- a/main.lua
+++ /dev/null
@@ -1,263 +0,0 @@
-local addonName, addonTable = ...
-
-local tinsert = tinsert;
-local tremove = tremove;
-
--- Create ACE3 addon
-local addon = LibStub("AceAddon-3.0"):NewAddon(addonName, "AceConsole-3.0", "AceEvent-3.0", "AceHook-3.0")
-
-tinsert(addonTable, addon);
-_G[addonName] = addon
-
--- Set up a default prototype for all modules
-local modPrototype = { Debug = function(self, ...) addon:Debug(...) end }
-addon:SetDefaultModulePrototype(modPrototype)
-
--- Db default settings
-addon.dbDefaults = {
-	realm = {
-		modules = {}
-	},
-	global = {
-		dbVersion = 1,
-		debugLog = {}
-	}
-}
-
--- The current db version. Clear (migrate?) the database if
--- version of database doesn't match this version.
-addon.dbVersion = 2
-
--- Constants
-DEBUG_PRINT = true;
-DEBUG_LOG = false;
-MAX_NUM_DEBUG_LOG_ENTRIES = 300;
-
-local function getDifficultyNameById(difficultyId)
-	if difficultyId == 7 or difficultyId == 17 then
-		return "LFR";
-	elseif difficultyId == 1 or difficultyId == 3 or difficultyId == 4 or difficultyId == 14 then
-		return "Normal";
-	elseif difficultyId == 2 or difficultyId == 5 or difficultyId == 6 or difficultyId == 15 then
-		return "Heroic";
-	elseif difficultyId == 16 then
-		return "Mythic";
-	end
-
-	return nil
-end
-
-function addon:Debug(...)
-	if DEBUG_PRINT then
-		self:Print(...)
-	end
-	if DEBUG_LOG then
-		local logData = {date("%d/%m %H:%M:%S")};
-		for i=1, select('#', ...) do
-			local v = select(i, ...)
-			tinsert(logData, v);
-		end
-		tinsert(self.db.global.debugLog, logData);
-	end
-end
-
-function addon:UpdateMyGuildName()
-	if IsInGuild() then
-		local guildName, _, _ = GetGuildInfo("player")
-		if guildName ~= nil then
-			self.guildName = guildName
-		end
-	else
-		self.guildName = nil
-	end
-end
-
-function addon:UpdateCurrentZone()
-	local zoneId, _ = GetCurrentMapAreaID()
-	local zoneName = GetRealZoneText();
-	self.currentZone = {id = zoneId, name = zoneName};
-
-	if IsInInstance() then
-		self:Debug("UpdateCurrentZone", zoneId, zoneName)
-	else
-		self:UnsetCurrentEncounter();
-	end
-end
-
-function addon:SetCurrentEncounter(encounterId, encounterName, difficultyId, raidSize)
-	local difficultyName = getDifficultyNameById(difficultyId);
-
-	self:Debug("SetCurrentEncounter", encounterId, encounterName, difficultyId, raidSize, difficultyName);
-
-	if difficultyName then
-		self.currentEncounter = {
-			zoneId = self.currentZone.id,
-			zoneName = self.currentZone.name,
-			id = encounterId,
-			name = encounterName,
-			difficultyId = difficultyId,
-			difficultyName = difficultyName,
-			raidSize = raidSize
-		}
-	end
-end
-
-function addon:UnsetCurrentEncounter()
-	if self.currentEncounter then
-		self:Debug("UnsetCurrentEncounter");
-		self.currentEncounter = nil
-	end
-end
-
-function addon:IsInMyGuild(playerName)
-	if self.guildName then
-		local guildName, _, _ = GetGuildInfo(playerName)
-		return guildName == self.guildName
-	else
-		return false
-	end
-end
-
-function addon:GetGuildPlayersFromSet(skadaSet)
-	local players = {}
-	for i, player in ipairs(skadaSet.players) do
-		local playerData;
-		if self:IsInMyGuild(player.name) then
-			playerData = {id = player.id, name = player.name, damage = player.damage, healing = player.healing};
-			tinsert(players, playerData);
-		end
-	end
-	return players
-end
-
-function addon:SetRoleForPlayers(players)
-	for _, player in ipairs(players) do
-		player.role = UnitGroupRolesAssigned(player.name);
-	end
-end
-
-function addon:SetClassForPlayers(players)
-	for _, player in ipairs(players) do
-		player.class = UnitClass(player.name);
-	end
-end
-
-function addon:PLAYER_GUILD_UPDATE(evt, unitId)
-	if unitId == "player" then
-		self:UpdateMyGuildName()
-	end
-end
-
-function addon:ENCOUNTER_START(evt, encounterId, encounterName, difficultyId, raidSize)
-	self:Debug("ENCOUNTER_START", encounterId, encounterName, difficultyId, raidSize)
-	self:SetCurrentEncounter(encounterId, encounterName, difficultyId, raidSize)
-end
-
-function addon:ENCOUNTER_END(evt, encounterId, encounterName, difficultyId, raidSize, endStatus)
-	self:Debug("ENCOUNTER_END", encounterId, encounterName, difficultyId, raidSize, endStatus)
-	if endStatus == 1 then -- Success
-		self:SetCurrentEncounter(encounterId, encounterName, difficultyId, raidSize)
-	else
-		self:UnsetCurrentEncounter()
-	end
-end
-
-function addon:ZONE_CHANGED_NEW_AREA(evt)
-	self:UpdateCurrentZone();
-end
-
-function addon:EndSegment()
-	self:Debug("EndSegment")
-
-	-- Find the skada set matching the encounter
-	-- Looking only at the lastest set should make sense,
-	-- as that set should be the boss segment we just ended.
-	local _, skadaSet = next(Skada:GetSets());
-	local encounter = self.currentEncounter;
-
-	if not self.guildName then
-		self:Debug("Not in a guild");
-		return;
-	end
-
-	if not encounter then
-		self:Debug("No current encounter");
-		return
-	end
-
-	if not skadaSet or not skadaSet.gotboss or skadaSet.mobname ~= encounter.name then
-		self:Debug("No Skada set found for boss");
-		return;
-	end
-
-	encounter.duration = skadaSet.time;
-	encounter.startTime = skadaSet.starttime;
-
-	local players = self:GetGuildPlayersFromSet(skadaSet);
-	self:SetRoleForPlayers(players);
-	self:SetClassForPlayers(players);
-	self.inspect:GetInspectDataForPlayers(players, function()
-		self.highscore:AddEncounterParsesForPlayers(self.guildName, encounter, players);
-	end)
-
-	self:UnsetCurrentEncounter();
-end
-
-function addon:OnInitialize()
-	self.db = LibStub("AceDB-3.0"):New("GuildSkadaHighScoreDB", addon.dbDefaults, true)
-
-	-- Make sure db version is in sync
-	if self.db.global.dbVersion ~= self.dbVersion then
-		self:Debug(format("Found not matching db versions: db=%d, addon=%d",
-			self.db.global.dbVersion, self.dbVersion));
-		self:Debug("Resetting db");
-		self.db:ResetDB();
-		self.db.global.dbVersion = self.dbVersion;
-	end
-
-	-- Purge old logs
-	if DEBUG_LOG then
-		local numLogsToPurge = (#self.db.global.debugLog - MAX_NUM_DEBUG_LOG_ENTRIES);
-		while numLogsToPurge >= 0 do
-			tremove(self.db.global.debugLog, 1)
-			numLogsToPurge = numLogsToPurge - 1;
-		end
-	else
-		wipe(self.db.global.debugLog);
-	end
-end
-
-function addon:OnEnable()
-	self.currentEncounter = nil;
-	self.currentZone = {};
-	self.guildName = nil;
-
-	self:RegisterEvent("ENCOUNTER_START")
-	self:RegisterEvent("ENCOUNTER_END")
-	self:RegisterEvent("PLAYER_GUILD_UPDATE")
-	self:RegisterEvent("ZONE_CHANGED_NEW_AREA")
-
-	self:SecureHook(Skada, "EndSegment")
-
-	self:RegisterChatCommand("gshs", function()
-		self.gui:ShowMainFrame();
-	end)
-
-	self:UpdateMyGuildName();
-	self:UpdateCurrentZone();
-end
-
-function addon:OnDisable()
-	self.currentEncounter = nil;
-	self.currentZone = {};
-	self.guildName = nil;
-
-	self:UnregisterEvent("ENCOUNTER_START")
-	self:UnregisterEvent("ENCOUNTER_END")
-	self:UnregisterEvent("PLAYER_GUILD_UPDATE")
-	self:UnregisterEvent("ZONE_CHANGED_NEW_AREA")
-
-    self:UnHook(Skada, "EndSegment");
-
-    self:UnregisterChatCommand("gshs");
-end
diff --git a/main.xml b/main.xml
deleted file mode 100644
index 6cdd3d7..0000000
--- a/main.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-<Ui xsi:schemaLocation="http://www.blizzard.com/wow/ui/ ..\..\FrameXML\UI.xsd">
-    <Script file="main.lua"/>
-    <Script file="inspect.lua"/>
-    <Script file="highscore.lua"/>
-    <Script file="gui.lua"/>
-</Ui>
\ No newline at end of file
diff --git a/src/gui.lua b/src/gui.lua
new file mode 100644
index 0000000..797ff80
--- /dev/null
+++ b/src/gui.lua
@@ -0,0 +1,396 @@
+local addonName, addonTable = ...
+
+-- Global functions for faster access
+local tinsert = tinsert;
+local tContains = tContains;
+
+-- Set up module
+local addon = addonTable[1];
+local gui = addon:NewModule("gui", "AceHook-3.0")
+addon.gui = gui;
+
+-- AceGUI
+local AceGUI = LibStub("AceGUI-3.0");
+
+
+function gui:CreateHighScoreParseEntry(parse, role, rank)
+	local entryWidget = AceGUI:Create("SimpleGroup");
+	entryWidget:SetFullWidth(true);
+	entryWidget:SetLayout("Flow");
+
+	local rankLabel = AceGUI:Create("Label");
+	rankLabel:SetText(rank);
+	rankLabel:SetRelativeWidth(0.14);
+
+	local dpsHpsLabel = AceGUI:Create("Label");
+	local dpsHps = Skada:FormatNumber((role == "HEALER") and parse.hps or parse.dps);
+	dpsHpsLabel:SetText(dpsHps);
+	dpsHpsLabel:SetRelativeWidth(0.14);
+
+	local nameLabel = AceGUI:Create("Label");
+	nameLabel:SetText(parse.name);
+	nameLabel:SetRelativeWidth(0.14);
+
+	local classLabel = AceGUI:Create("Label");
+	classLabel:SetText(parse.class);
+	classLabel:SetRelativeWidth(0.14);
+
+	local specLabel = AceGUI:Create("Label");
+	specLabel:SetText(parse.specName or "");
+	specLabel:SetRelativeWidth(0.14);
+
+	local ilvlLabel = AceGUI:Create("Label");
+	ilvlLabel:SetText(parse.itemLevel or "");
+	ilvlLabel:SetRelativeWidth(0.14);
+
+	local dateLabel = AceGUI:Create("Label");
+	dateLabel:SetText(date("%m/%d/%y", parse.startTime));
+	dateLabel:SetRelativeWidth(0.14);
+
+	entryWidget:AddChild(rankLabel);
+	entryWidget:AddChild(dpsHpsLabel);
+	entryWidget:AddChild(nameLabel);
+	entryWidget:AddChild(classLabel);
+	entryWidget:AddChild(specLabel);
+	entryWidget:AddChild(ilvlLabel);
+	entryWidget:AddChild(dateLabel);
+
+	return entryWidget;
+end
+
+function gui:CreateGuildDropdown()
+	local dropdown = AceGUI:Create("Dropdown");
+	self.guildDropdown = dropdown;
+
+	dropdown:SetLabel("Guild");
+	dropdown:SetRelativeWidth(0.25);
+	dropdown:SetCallback("OnValueChanged", function(widget, evt, guildId)
+		gui:SetSelectedGuild(guildId);
+	end)
+
+	local guilds, numGuilds = addon.highscore:GetGuilds();
+	if numGuilds > 0 then
+		dropdown:SetList(guilds);
+	else
+		dropdown:SetDisabled(true);
+		dropdown:SetText("No Guilds.");
+	end
+
+	return dropdown;
+end
+
+function gui:CreateZoneDropdown()
+	local dropdown = AceGUI:Create("Dropdown");
+	self.zoneDropdown = dropdown;
+
+	dropdown:SetLabel("Zone");
+	dropdown:SetRelativeWidth(0.25);
+	dropdown:SetList(nil);
+	dropdown:SetDisabled(true);
+	dropdown:SetCallback("OnValueChanged", function(widget, evt, zoneId)
+		gui:SetSelectedZone(zoneId);
+	end)
+
+	return dropdown;
+end
+
+function gui:CreateDifficultyDropdown()
+	local dropdown = AceGUI:Create("Dropdown");
+	self.difficultyDropdown = dropdown;
+
+	dropdown:SetLabel("Difficulty");
+	dropdown:SetRelativeWidth(0.25);
+	dropdown:SetList(nil);
+	dropdown:SetDisabled(true);
+	dropdown:SetCallback("OnValueChanged", function(widget, evt, difficultyId)
+		gui:SetSelectedDifficulty(difficultyId);
+	end)
+
+	return dropdown;
+end
+
+function gui:CreateEncounterDropdown()
+	local dropdown = AceGUI:Create("Dropdown");
+	self.encounterDropdown = dropdown;
+
+	dropdown:SetLabel("Encounter");
+	dropdown:SetRelativeWidth(0.25);
+	dropdown:SetList(nil);
+	dropdown:SetDisabled(true);
+	dropdown:SetCallback("OnValueChanged", function(widget, evt, encounterId)
+		gui:SetSelectedEncounter(encounterId);
+	end)
+
+	return dropdown;
+end
+
+function gui:CreateHighScoreScrollFrame()
+	local scrollFrame = AceGUI:Create("ScrollFrame");
+	scrollFrame:SetLayout("Flow");
+	scrollFrame:SetFullWidth(true);
+	scrollFrame:SetFullHeight(true);
+
+	-- Header:
+	-- Rank | DPS/HPS | Name | Class | Spec | Item Level | Date
+	local headerContainer = AceGUI:Create("SimpleGroup");
+	headerContainer:SetFullWidth(true);
+	headerContainer:SetLayout("Flow");
+
+	local rankLabel = AceGUI:Create("Label");
+	rankLabel:SetText("Rank");
+	rankLabel:SetRelativeWidth(0.14);
+	local dpsHpsLabel = AceGUI:Create("Label");
+	dpsHpsLabel:SetText("DPS/HPS");
+	dpsHpsLabel:SetRelativeWidth(0.14);
+	local nameLabel = AceGUI:Create("Label");
+	nameLabel:SetText("Name");
+	nameLabel:SetRelativeWidth(0.14);
+	local classLabel = AceGUI:Create("Label");
+	classLabel:SetText("Class");
+	classLabel:SetRelativeWidth(0.14);
+	local specLabel = AceGUI:Create("Label");
+	specLabel:SetText("Spec");
+	specLabel:SetRelativeWidth(0.14);
+	local ilvlLabel = AceGUI:Create("Label");
+	ilvlLabel:SetText("Item Level");
+	ilvlLabel:SetRelativeWidth(0.14);
+	local dateLabel = AceGUI:Create("Label");
+	dateLabel:SetText("Date");
+	dateLabel:SetRelativeWidth(0.14);
+
+	headerContainer:AddChild(rankLabel);
+	headerContainer:AddChild(dpsHpsLabel);
+	headerContainer:AddChild(nameLabel);
+	headerContainer:AddChild(classLabel);
+	headerContainer:AddChild(specLabel);
+	headerContainer:AddChild(ilvlLabel);
+	headerContainer:AddChild(dateLabel);
+
+	local parsesContainer = AceGUI:Create("SimpleGroup");
+	self.highScoreParsesContainer = parsesContainer;
+	parsesContainer:SetFullWidth(true);
+	parsesContainer:SetLayout("Flow");
+
+	scrollFrame:AddChild(headerContainer);
+	scrollFrame:AddChild(parsesContainer);
+
+	return scrollFrame;
+end
+
+function gui:CreateHighScoreTabGroup()
+	local container = AceGUI:Create("TabGroup");
+	self.highScoreTabGroup = container;
+
+	container:SetFullWidth(true);
+	container:SetFullHeight(true);
+	container:SetLayout("Fill");
+	container:SetTabs({
+		{value = "DAMAGER", text = "DPSers"},
+		{value = "HEALER", text = "Healers"},
+		{value = "TANK", text = "Tanks"}
+	});
+	container:SetCallback("OnGroupSelected", function(widget, evt, roleId)
+		gui:SetSelectedRole(roleId);
+	end)
+
+	container:AddChild(self:CreateHighScoreScrollFrame());
+
+	return container;
+end
+
+function gui:CreateMainFrame()
+	local frame = AceGUI:Create("Frame")
+	self.mainFrame = frame;
+
+	frame:Hide()
+	frame:SetWidth(800)
+	frame:SetHeight(600)
+	frame:SetTitle("Guild Skada High Score")
+	frame:SetLayout("Flow")
+	frame:SetCallback("OnClose", function()
+		gui:HideMainFrame()
+	end)
+
+	frame:AddChild(self:CreateGuildDropdown());
+	frame:AddChild(self:CreateZoneDropdown());
+	frame:AddChild(self:CreateDifficultyDropdown());
+	frame:AddChild(self:CreateEncounterDropdown());
+	frame:AddChild(self:CreateHighScoreTabGroup());
+
+	return frame;
+end
+
+function gui:DisplayParses()
+	local guildName = self.selectedGuild;
+	local zoneId = self.selectedZone;
+	local difficultyId = self.selectedDifficulty;
+	local encounter = self.selectedEncounter;
+	local roleId = self.selectedRole;
+
+	local parsesContainer = self.highScoreParsesContainer;
+	parsesContainer:ReleaseChildren();
+
+	if guildName and zoneId and difficultyId and encounter and roleId then
+		local parses, numParses = addon.highscore:GetParses(guildName,
+			zoneId, difficultyId, encounter, roleId);
+		if numParses > 0 then
+			for rank, parse in ipairs(parses) do
+				local entryWidget = self:CreateHighScoreParseEntry(parse, roleId, rank);
+				parsesContainer:AddChild(entryWidget);
+			end
+			return;
+		end
+	end
+
+	local noParsesLabel = AceGUI:Create("Label");
+	noParsesLabel:SetText("No parses found.");
+	parsesContainer:AddChild(noParsesLabel);
+end
+
+function gui:SetSelectedRole(roleId, noPropagation)
+	-- SelectTab, unlike SetValue for dropdowns, triggers the callback
+	if self.selectedRole ~= roleId then
+		self.selectedRole = roleId;
+		self.highScoreTabGroup:SelectTab(roleId);
+		self:DisplayParses();
+	end
+end
+
+function gui:SetSelectedEncounter(encounterId, noPropagation)
+	self.selectedEncounter = encounterId;
+	self.encounterDropdown:SetValue(encounterId);
+	self:DisplayParses();
+end
+
+function gui:SetSelectedDifficulty(difficultyId, noPropagation)
+	self.selectedDifficulty = difficultyId;
+	self.difficultyDropdown:SetValue(difficultyId);
+
+	-- Update encounter dropdown with new guild, zone, difficulty
+	local encounters, numEncounters = addon.highscore:GetEncounters(self.selectedGuild, self.selectedZone, self.selectedDifficulty);
+	if numEncounters > 0 then
+		self.encounterDropdown:SetDisabled(false);
+		self.encounterDropdown:SetList(encounters);
+	else
+		self.encounterDropdown:SetDisabled(true);
+		self.encounterDropdown:SetList(nil);
+		self.encounterDropdown:SetText(nil);
+	end
+
+	if not noPropagation then
+		if numEncounters == 1 then
+			-- If only one option, select it.
+			local encounterId, _ = next(encounters);
+			self:SetSelectedEncounter(encounterId);
+		else
+			self:SetSelectedEncounter(nil);
+		end
+	end
+end
+
+function gui:SetSelectedZone(zoneId, noPropagation)
+	self.selectedZone = zoneId;
+	self.zoneDropdown:SetValue(zoneId);
+
+	-- Update difficulty dropdown with new guild, zone
+	local difficulties, numDifficulties = addon.highscore:GetDifficulties(self.selectedGuild, self.selectedZone);
+	if numDifficulties > 0 then
+		self.difficultyDropdown:SetDisabled(false);
+		self.difficultyDropdown:SetList(difficulties);
+	else
+		self.difficultyDropdown:SetDisabled(true);
+		self.difficultyDropdown:SetList(nil);
+		self.difficultyDropdown:SetText(nil);
+	end
+
+	if not noPropagation then
+		if numDifficulties == 1 then
+			-- If only one option, select it.
+			local difficultyId, _ = next(difficulties);
+			self:SetSelectedDifficulty(difficultyId);
+		else
+			self:SetSelectedDifficulty(nil);
+		end
+	end
+end
+
+function gui:SetSelectedGuild(guildId, noPropagation)
+	self.selectedGuild = guildId;
+	self.guildDropdown:SetValue(guildId);
+
+	-- Update zone dropdown for the new guild
+	local zones, numZones = addon.highscore:GetZones(guildId);
+	if numZones > 0 then
+		self.zoneDropdown:SetDisabled(false);
+		self.zoneDropdown:SetList(zones);
+	else
+		self.zoneDropdown:SetDisabled(true);
+		self.zoneDropdown:SetList(nil);
+		self.zoneDropdown:SetText(nil);
+	end
+
+	if not noPropagation then
+		if numZones == 1 then
+			-- If only one option, select it.
+			local zoneId, _ = next(zones);
+			self:SetSelectedZone(zoneId);
+		else
+			self:SetSelectedZone(nil);
+		end
+	end
+end
+
+function gui:ShowMainFrame()
+	if not self.mainFrame then
+		-- Only show if not already shown
+		self:CreateMainFrame():Show();
+
+		if self.selectedGuild then
+			-- Try to restore to same values as before
+			gui:SetSelectedGuild(self.selectedGuild, true);
+			gui:SetSelectedZone(self.selectedZone, true);
+			gui:SetSelectedDifficulty(self.selectedDifficulty, true);
+			gui:SetSelectedEncounter(self.selectedEncounter, true);
+			gui:SetSelectedRole(self.selectedRole, true);
+		elseif addon.guildName then
+			-- Try pre-selecting own guild if has one.
+			gui:SetSelectedGuild(addon.guildName);
+		end
+
+		-- Have to do special for our tab group as it is never disabled
+		gui:SetSelectedRole(self.selectedRole or "DAMAGER");
+	end
+end
+
+function gui:HideMainFrame()
+	if self.mainFrame then
+		self.mainFrame:Release();
+
+		-- Unset references
+		self.mainFrame = nil;
+		self.guildDropdown = nil;
+		self.zoneDropdown = nil;
+		self.difficultyDropdown = nil;
+		self.encounterDropdown = nil;
+		self.highScoreTabGroup = nil;
+	end
+end
+
+function gui:OnCloseSpecialWindows()
+	if self.mainFrame then
+		self:HideMainFrame()
+		return true
+	else
+		return self.hooks["CloseSpecialWindows"]();
+	end
+end
+
+
+function gui:OnEnable()
+	self:RawHook("CloseSpecialWindows", "OnCloseSpecialWindows");
+end
+
+function gui:OnDisable()
+	self:HideMainFrame();
+	self:UnHook("CloseSpecialWindows");
+end
\ No newline at end of file
diff --git a/src/highscore.lua b/src/highscore.lua
new file mode 100644
index 0000000..ccd109c
--- /dev/null
+++ b/src/highscore.lua
@@ -0,0 +1,326 @@
+local addonName, addonTable = ...
+
+-- Global functions for faster access
+local tinsert = tinsert;
+local tContains = tContains;
+local sort = sort;
+local random = random;
+local format = format;
+
+-- Set up module
+local addon = addonTable[1];
+local highscore = addon:NewModule("highscore", "AceEvent-3.0", "AceTimer-3.0")
+addon.highscore = highscore;
+
+-- db defaults
+addon.dbDefaults.realm.modules["highscore"] = {
+	["guilds"] = {
+		["*"] = { -- Guild Name
+			["zones"] = {
+				["*"] = { -- zoneId
+					["difficulties"] = {
+						["*"] = { -- difficultyId
+							["encounters"] = {
+								["*"] = { -- encounterId
+									--[[
+										playerParses is a list of objects:
+										{
+											playerId 	 = "",
+											role 		 = "",
+											specName 	 = "",
+											itemLevel 	 = 0,
+											damage 		 = 0,
+											healing 	 = 0,
+											groupParseId = ""
+										}
+									--]]
+									playerParses = {}
+								}
+							}
+						}
+					}
+				}
+			}
+		}
+	},
+	["zones"] = {
+		["*"] = { -- zoneId
+			zoneName = nil
+		}
+	},
+	["difficulties"] = {
+		["*"] = { -- difficultyId
+			difficultyName = nil
+		}
+	},
+	["encounters"] = {
+		["*"] = { -- encounterId
+			encounterName = nil
+		}
+	},
+	["players"] = {
+		["*"] = { -- playerId
+			name 	= nil,
+			class 	= nil
+		}
+	},
+	["groupParses"] = {
+		--[[
+		["*"] = { -- groupParseId
+			startTime 	= 0,
+			duration 	= 0,
+		}
+		--]]
+	}
+}
+
+addon.dbVersion = addon.dbVersion + 4
+
+-- Constants
+local TRACKED_ZONE_IDS = {
+	994 -- Highmaul
+}
+
+
+-- Function for convering a database representation of
+-- a parse to a parse that can be returned to users.
+-- Copies all values and adds calculated once (like dps)
+local function getReturnableParse(db, parse)
+	local parseCopy = {};
+	for key, val in pairs(parse) do
+		parseCopy[key] = val;
+	end
+
+	-- Get duration, statTime from the group parse
+	local groupParse = db.groupParses[parse["groupParseId"]]
+	parseCopy["duration"] = groupParse.duration;
+	parseCopy["startTime"] = groupParse.startTime;
+	parseCopy["groupParseId"] = nil
+
+	-- Get player name and class
+	parseCopy["name"] = db.players[parse["playerId"]].name;
+	parseCopy["class"] = db.players[parse["playerId"]].class;
+
+	-- Calculate dps/hps
+	parseCopy["dps"] = 0;
+	parseCopy["hps"] = 0;
+	if parseCopy["duration"] > 0 then
+		parseCopy["dps"] = parseCopy["damage"] / parseCopy["duration"];
+		parseCopy["hps"] = parseCopy["healing"] / parseCopy["duration"];
+	end
+
+	return parseCopy;
+end
+
+local function generateRandomKey()
+	local r1 = random(0, 1000);
+	local r2 = random(0, 1000);
+	local r3 = random(0, 1000);
+	-- 1000^3 = 1´000´000´000, should be enough for now...
+	return format("%x-%x-%x", r1, r2, r3);
+end
+
+local function addGroupParse(db, startTime, duration)
+	-- Find a new unique key for the raid parse
+	local key = generateRandomKey();
+	while db.groupParses[key] do
+		key = generateRandomKey();
+	end
+
+	db.groupParses[key] = {
+		startTime 	= startTime,
+		duration 	= duration
+	};
+
+	return key
+end
+
+local function addZone(db, zoneId, zoneName)
+	db.zones[zoneId].zoneName = zoneName;
+end
+
+local function addDifficulty(db, difficultyId, difficultyName)
+	db.difficulties[difficultyId].difficultyName = difficultyName;
+end
+
+local function addEncounter(db, encounterId, encounterName)
+	db.encounters[encounterId].encounterName = encounterName;
+end
+
+local function addPlayer(db, playerId, playerName, playerClass)
+	db.players[playerId].name = playerName;
+	db.players[playerId].class = playerClass;
+end
+
+local function getParsesTable(db, guildName, zoneId, difficultyId, encounterId)
+	return db
+		.guilds[guildName]
+		.zones[zoneId]
+		.difficulties[difficultyId]
+		.encounters[encounterId]
+		.playerParses;
+end
+
+local function addEncounterParseForPlayer(parsesTable, player, groupParseId)
+	local parse = {
+		playerId 	 = player.id,
+		role 		 = player.role,
+		specName 	 = player.specName,
+		itemLevel 	 = player.itemLevel,
+		damage 		 = player.damage,
+		healing 	 = player.healing,
+		groupParseId = groupParseId
+	}
+	tinsert(parsesTable, parse);
+end
+
+function highscore:AddEncounterParsesForPlayers(guildName, encounter, players)
+	local zoneId = encounter.zoneId;
+	local zoneName = encounter.zoneName;
+	local encounterId = encounter.id;
+	local encounterName = encounter.name;
+	local difficultyId = encounter.difficultyId;
+	local difficultyName = encounter.difficultyName;
+	local startTime = encounter.startTime;
+	local duration = encounter.duration;
+
+	assert(guildName)
+	assert(zoneId)
+	assert(zoneName)
+	assert(encounterId)
+	assert(encounterName)
+	assert(difficultyId)
+	assert(difficultyName)
+	assert(startTime)
+	assert(duration)
+	assert(players)
+
+	if not tContains(TRACKED_ZONE_IDS, zoneId) then
+		self:Debug("AddEncounterParsesForPlayers: Current zone not not in tracked zones");
+		return
+	end
+
+	-- Add zone, difficulty and encounter info
+	addZone(self.db, zoneId, zoneName);
+	addDifficulty(self.db, difficultyId, difficultyName);
+	addEncounter(self.db, encounterId, encounterName);
+
+	-- Add a group parse entry, holding data shared between all players
+	local groupParseId = addGroupParse(self.db, startTime, duration);
+
+	local parsesTable = getParsesTable(self.db, guildName, zoneId, difficultyId, encounterId);
+
+	for _, player in ipairs(players) do
+		self:Debug(format("addEncounterParseForPlayer: %s", player.name));
+
+		addPlayer(self.db, player.id, player.name, player.class);
+		addEncounterParseForPlayer(parsesTable, player, groupParseId)
+	end
+
+end
+
+-- Returns (array of parses, numParses)
+function highscore:GetParses(guildName, zoneId, difficultyId, encounterId, role, sortBy)
+	if (role ~= "TANK" and role ~= "HEALER" and role ~= "DAMAGER") then
+		return {}, 0;
+	end
+
+	if not sortBy then
+		if role == "TANK" or role == "DAMAGER" then
+			sortBy = "dps"
+		elseif role == "HEALER" then
+			sortBy = "hps"
+		end
+	end
+
+	local parsesTable = getParsesTable(self.db, guildName, zoneId, difficultyId, encounterId);
+
+	-- Get a *copy* of all parses for the specified role
+	local parses = {};
+	local numParses = 0;
+	for _, parse in ipairs(parsesTable) do
+		if parse.role == role then
+			local parseCopy = getReturnableParse(self.db, parse);
+			tinsert(parses, parseCopy);
+			numParses = numParses + 1;
+		end
+	end
+
+	sort(parses, function(a, b)
+		return a[sortBy] > b[sortBy];
+	end)
+	return parses, numParses;
+end
+
+-- Returns (array of {encounterId => encounterName}, numEncounters)
+function highscore:GetEncounters(guildName, zoneId, difficultyId)
+	if not guildName or not zoneId or not difficultyId then
+		return {}, 0;
+	end
+
+	local encounters = {};
+	local numEncounters = 0;
+	local difficultiesTable = self.db.guilds[guildName].zones[zoneId].difficulties;
+
+	for encounterId, _ in pairs(difficultiesTable[difficultyId].encounters) do
+		local encounterName = self.db.encounters[encounterId].encounterName;
+		encounters[encounterId] = encounterName;
+		numEncounters = numEncounters + 1;
+	end
+	return encounters, numEncounters;
+end
+
+-- Returns (array of {difficultyId => difficultyName}, numDifficulties)
+function highscore:GetDifficulties(guildName, zoneId)
+	if not guildName or not zoneId then
+		return {}, 0;
+	end
+
+	local difficulties = {};
+	local numDifficulties = 0;
+	local difficultiesTable = self.db.guilds[guildName].zones[zoneId].difficulties;
+
+	for difficultyId, _ in pairs(difficultiesTable) do
+		local difficultyName = self.db.difficulties[difficultyId].difficultyName;
+		difficulties[difficultyId] = difficultyName;
+		numDifficulties = numDifficulties + 1;
+	end
+	return difficulties, numDifficulties;
+end
+
+-- Returns (array of {zoneId => zoneName}, numZones)
+function highscore:GetZones(guildName)
+	if not guildName then
+		return {}, 0;
+	end
+
+	local zones = {};
+	local numZones = 0;
+	for zoneId, _ in pairs(self.db.guilds[guildName].zones) do
+		local zoneName = self.db.zones[zoneId].zoneName;
+		zones[zoneId] = zoneName;
+		numZones = numZones + 1;
+	end
+	return zones, numZones;
+end
+
+-- Returns (array of {guildId => guildName}, numGuilds)
+function highscore:GetGuilds()
+	local guildNames = {};
+	local numGuilds = 0;
+	for guildName, _ in pairs(self.db.guilds) do
+		-- Actually guildId == guildName
+		guildNames[guildName] = guildName;
+		numGuilds = numGuilds + 1;
+	end
+	return guildNames, numGuilds;
+end
+
+
+function highscore:OnEnable()
+	self.db = addon.db.realm.modules["highscore"];
+end
+
+function highscore:OnDisable()
+	self.db = nil;
+end
diff --git a/src/inspect.lua b/src/inspect.lua
new file mode 100644
index 0000000..dce5075
--- /dev/null
+++ b/src/inspect.lua
@@ -0,0 +1,275 @@
+local addonName, addonTable = ...
+
+-- Global functions for faster access
+local tinsert = tinsert;
+local floor = floor;
+local wipe = wipe;
+
+-- Set up module
+local addon = addonTable[1];
+local inspect = addon:NewModule("inspect", "AceEvent-3.0", "AceTimer-3.0")
+addon.inspect = inspect;
+
+-- Constants
+local INSPECT_CACHE_TIMEOUT = 900;
+local INVENTORY_SLOT_NAMES = {
+	"HeadSlot","NeckSlot","ShoulderSlot","BackSlot","ChestSlot","WristSlot",
+	"HandsSlot","WaistSlot","LegsSlot","FeetSlot","Finger0Slot","Finger1Slot",
+	"Trinket0Slot","Trinket1Slot","MainHandSlot","SecondaryHandSlot"
+}
+local NOOP = function() end
+
+local playerGUID = UnitGUID("player");
+local inspectCache = {};
+
+
+
+local function isInPVEInstance()
+	local isInstance, instanceType = IsInInstance();
+	return inInstance and (instanceType == "party" or instanceType == "raid")
+end
+
+local function getTalentSpec(unitName)
+	if unitName == "player" then
+		local spec = GetSpecialization();
+		if spec and spec > 0 then
+			local _, name = GetSpecializationInfo(spec);
+			return name;
+		end
+	else
+		local spec = GetInspectSpecialization(unitName)
+		if spec and spec > 0 then
+			local role = GetSpecializationRoleByID(spec);
+			if role then
+				local _, name = GetSpecializationInfoByID(spec);
+				return name
+			end
+		end
+	end
+end
+
+local function getItemLevel(unitName)
+	if unitName == "player" then
+		local _, equipped = GetAverageItemLevel();
+		return floor(equipped);
+	else
+		local total, numItems = 0, 0;
+		for i = 1, #INVENTORY_SLOT_NAMES do
+			local slotName = INVENTORY_SLOT_NAMES[i];
+			local slotId = GetInventorySlotInfo(slotName);
+			local itemLink = GetInventoryItemLink(unitName, slotId);
+
+			if itemLink then
+				local _, _, _, itemLevel = GetItemInfo(itemLink)
+				if itemLevel and itemLevel > 0 then
+					numItems = numItems + 1;
+					total = total + itemLevel;
+				end
+			end
+		end
+		if total < 1 or numItems < 15 then
+			return nil
+		else
+			return floor(total / numItems)
+		end
+	end
+end
+
+local function hasPlayerInspectCache(playerId, ignoreExpired)
+	if inspectCache[playerId] and ignoreExpired then
+		return true -- We have a cache, might be expired
+	elseif inspectCache[playerId] and inspectCache[playerId].time then
+		local isExpired = (GetTime() - inspectCache[playerId].time) > INSPECT_CACHE_TIMEOUT
+		local hasAllAttributes = inspectCache[playerId].specName and inspectCache[playerId].itemLevel;
+
+		return (not isExpired and hasAllAttributes)
+	else
+		return false; -- No entry for player id
+	end
+end
+
+local function setPlayerInspectCache(playerId, specName, itemLevel)
+	inspectCache[playerId] = {};
+	inspectCache[playerId].time = GetTime();
+	inspectCache[playerId].specName = specName;
+	inspectCache[playerId].itemLevel = itemLevel;
+end
+
+function inspect:StartNotifyInspectTimer()
+	if not self.notifyInspectTimer then
+		self:Debug("Starting notifyInspectTimer");
+
+		self.notifyInspectTimer = self:ScheduleRepeatingTimer(function()
+			self:NOTIFY_INSPECT_TIMER_DONE()
+		end, 1);
+	end
+end
+
+function inspect:StopNotifyInspectTimer()
+	if self.notifyInspectTimer then
+		self:Debug("Stopping notifyInspectTimer");
+
+		self:CancelTimer(self.notifyInspectTimer);
+		self.notifyInspectTimer = nil;
+	end
+end
+
+function inspect:QueueInspect(player, callback)
+	self:Debug("QueueInspect", player.name)
+
+	if not self.inspectQueue[player.id] then
+		self.inspectQueue[player.id] = {player = player, callbacks = {}};
+	end
+	tinsert(self.inspectQueue[player.id].callbacks, callback);
+
+	self:StartNotifyInspectTimer();
+end
+
+function inspect:SetCachedInspectDataForPlayer(player)
+	if player.id == playerGUID then
+		player.specName = getTalentSpec("player")
+		player.itemLevel = getItemLevel("player");
+		return true
+	elseif hasPlayerInspectCache(player.id, false) then
+		player.specName = inspectCache[player.id].specName;
+		player.itemLevel = inspectCache[player.id].itemLevel;
+		return true
+	elseif hasPlayerInspectCache(player.id, true) then
+		-- Expired cache, but better than nothing
+		player.specName = inspectCache[player.id].specName;
+		player.itemLevel = inspectCache[player.id].itemLevel;
+		return false
+	else
+		return false
+	end
+end
+
+function inspect:GetInspectDataForPlayer(player, callback)
+	-- Make sure we always have a callback
+	callback = callback and callback or NOOP;
+
+	if self:SetCachedInspectDataForPlayer(player) then
+		return callback()
+	elseif not CanInspect(player.name, false) then
+		return callback()
+	else
+		self:QueueInspect(player, callback);
+	end
+end
+
+function inspect:GetInspectDataForPlayers(players, callback)
+	local totalCallbacks = #players;
+	local doneCallbacks = 0;
+
+	for _, player in ipairs(players) do
+		self:GetInspectDataForPlayer(player, function()
+			doneCallbacks = doneCallbacks + 1;
+			if doneCallbacks == totalCallbacks then
+				callback();
+			end
+		end)
+	end
+end
+
+function inspect:ResolveInspect(playerId, success)
+	if not self.inspectQueue[playerId] then
+		return
+	end
+
+	local player = self.inspectQueue[playerId].player;
+	local callbacks = self.inspectQueue[playerId].callbacks;
+	self.inspectQueue[playerId] = nil;
+
+	self:Debug("ResolveInspect", player.name, (success and "success" or "fail"))
+
+	if success then
+		if not hasPlayerInspectCache(player.id) then
+			local specName = getTalentSpec(player.name);
+			local itemLevel = getItemLevel(player.name);
+			setPlayerInspectCache(player.id, specName, itemLevel);
+		end
+
+		player.specName = inspectCache[player.id].specName;
+		player.itemLevel = inspectCache[player.id].itemLevel;
+	end
+
+	for _,callback in ipairs(callbacks) do
+		callback();
+	end
+end
+
+function inspect:PreInspectGroup()
+	for i=1, GetNumGroupMembers() do
+
+		local playerName = GetRaidRosterInfo(i);
+		if playerName and addon:IsInMyGuild(playerName) then
+
+			local playerId = UnitGUID(playerName)
+			if playerId and playerId ~= playerGUID and not hasPlayerInspectCache(playerId) then
+				local player = {name=playerName, id=playerId}
+				self:QueueInspect(player, NOOP);
+			end
+		end
+	end
+end
+
+function inspect:NOTIFY_INSPECT_TIMER_DONE()
+	-- Timeout any current inspection
+	if self.currentInspectPlayerId then
+		self:ResolveInspect(self.currentInspectPlayerId, false);
+		self.currentInspectPlayerId = nil;
+	end
+
+	local playerId, inspectData = next(self.inspectQueue);
+	if playerId then
+		NotifyInspect(inspectData.player.name);
+		self.currentInspectPlayerId = playerId;
+	else
+		self:StopNotifyInspectTimer();
+	end
+end
+
+function inspect:INSPECT_READY(evt, GUID)
+	if self.currentInspectPlayerId == GUID then
+		self:ResolveInspect(self.currentInspectPlayerId, true)
+		self.currentInspectPlayerId = nil;
+	end
+end
+
+function inspect:GROUP_ROSTER_UPDATE(evt)
+	if not IsInGroup() then
+		self:Debug("Left group, wiping inspect cache");
+		wipe(inspectCache);
+		wipe(inspect.inspectQueue);
+		inspect.currentInspectPlayerId = nil;
+	end
+end
+
+function inspect:ZONE_CHANGED_NEW_AREA(evt)
+	if isInPVEInstance() then
+		-- We just zoned into an instance, try pre-inspecting the group
+		self:PreInspectGroup();
+	end
+end
+
+function inspect:OnEnable()
+	inspect.inspectQueue = {};
+	inspect.notifyInspectTimer = nil;
+	inspect.currentInspectPlayerId = nil;
+
+	self:RegisterEvent("INSPECT_READY");
+	self:RegisterEvent("GROUP_ROSTER_UPDATE");
+	self:RegisterEvent("ZONE_CHANGED_NEW_AREA");
+end
+
+function inspect:OnDisable()
+	self:UnregisterEvent("INSPECT_READY");
+	self:UnregisterEvent("GROUP_ROSTER_UPDATE");
+	self:UnregisterEvent("ZONE_CHANGED_NEW_AREA");
+
+	self:StopNotifyInspectTimer();
+
+	wipe(inspectCache);
+	wipe(inspect.inspectQueue);
+	inspect.currentInspectPlayerId = nil;
+end
\ No newline at end of file
diff --git a/src/main.lua b/src/main.lua
new file mode 100644
index 0000000..91ab007
--- /dev/null
+++ b/src/main.lua
@@ -0,0 +1,263 @@
+local addonName, addonTable = ...
+
+local tinsert = tinsert;
+local tremove = tremove;
+
+-- Create ACE3 addon
+local addon = LibStub("AceAddon-3.0"):NewAddon(addonName, "AceConsole-3.0", "AceEvent-3.0", "AceHook-3.0")
+
+tinsert(addonTable, addon);
+_G[addonName] = addon
+
+-- Set up a default prototype for all modules
+local modPrototype = { Debug = function(self, ...) addon:Debug(...) end }
+addon:SetDefaultModulePrototype(modPrototype)
+
+-- Db default settings
+addon.dbDefaults = {
+	realm = {
+		modules = {}
+	},
+	global = {
+		dbVersion = 1,
+		debugLog = {}
+	}
+}
+
+-- The current db version. Clear (migrate?) the database if
+-- version of database doesn't match this version.
+addon.dbVersion = 2
+
+-- Constants
+DEBUG_PRINT = true;
+DEBUG_LOG = false;
+MAX_NUM_DEBUG_LOG_ENTRIES = 300;
+
+local function getDifficultyNameById(difficultyId)
+	if difficultyId == 7 or difficultyId == 17 then
+		return "LFR";
+	elseif difficultyId == 1 or difficultyId == 3 or difficultyId == 4 or difficultyId == 14 then
+		return "Normal";
+	elseif difficultyId == 2 or difficultyId == 5 or difficultyId == 6 or difficultyId == 15 then
+		return "Heroic";
+	elseif difficultyId == 16 then
+		return "Mythic";
+	end
+
+	return nil
+end
+
+function addon:Debug(...)
+	if DEBUG_PRINT then
+		self:Print(...)
+	end
+	if DEBUG_LOG then
+		local logData = {date("%d/%m %H:%M:%S")};
+		for i=1, select('#', ...) do
+			local v = select(i, ...)
+			tinsert(logData, v);
+		end
+		tinsert(self.db.global.debugLog, logData);
+	end
+end
+
+function addon:UpdateMyGuildName()
+	if IsInGuild() then
+		local guildName, _, _ = GetGuildInfo("player")
+		if guildName ~= nil then
+			self.guildName = guildName
+		end
+	else
+		self.guildName = nil
+	end
+end
+
+function addon:UpdateCurrentZone()
+	local zoneId, _ = GetCurrentMapAreaID()
+	local zoneName = GetRealZoneText();
+	self.currentZone = {id = zoneId, name = zoneName};
+
+	if IsInInstance() then
+		self:Debug("UpdateCurrentZone", zoneId, zoneName)
+	else
+		self:UnsetCurrentEncounter();
+	end
+end
+
+function addon:SetCurrentEncounter(encounterId, encounterName, difficultyId, raidSize)
+	local difficultyName = getDifficultyNameById(difficultyId);
+
+	self:Debug("SetCurrentEncounter", encounterId, encounterName, difficultyId, raidSize, difficultyName);
+
+	if difficultyName then
+		self.currentEncounter = {
+			zoneId = self.currentZone.id,
+			zoneName = self.currentZone.name,
+			id = encounterId,
+			name = encounterName,
+			difficultyId = difficultyId,
+			difficultyName = difficultyName,
+			raidSize = raidSize
+		}
+	end
+end
+
+function addon:UnsetCurrentEncounter()
+	if self.currentEncounter then
+		self:Debug("UnsetCurrentEncounter");
+		self.currentEncounter = nil
+	end
+end
+
+function addon:IsInMyGuild(playerName)
+	if self.guildName then
+		local guildName, _, _ = GetGuildInfo(playerName)
+		return guildName == self.guildName
+	else
+		return false
+	end
+end
+
+function addon:GetGuildPlayersFromSet(skadaSet)
+	local players = {}
+	for i, player in ipairs(skadaSet.players) do
+		local playerData;
+		if self:IsInMyGuild(player.name) then
+			playerData = {id = player.id, name = player.name, damage = player.damage, healing = player.healing};
+			tinsert(players, playerData);
+		end
+	end
+	return players
+end
+
+function addon:SetRoleForPlayers(players)
+	for _, player in ipairs(players) do
+		player.role = UnitGroupRolesAssigned(player.name);
+	end
+end
+
+function addon:SetClassForPlayers(players)
+	for _, player in ipairs(players) do
+		player.class = UnitClass(player.name);
+	end
+end
+
+function addon:PLAYER_GUILD_UPDATE(evt, unitId)
+	if unitId == "player" then
+		self:UpdateMyGuildName()
+	end
+end
+
+function addon:ENCOUNTER_START(evt, encounterId, encounterName, difficultyId, raidSize)
+	self:Debug("ENCOUNTER_START", encounterId, encounterName, difficultyId, raidSize)
+	self:SetCurrentEncounter(encounterId, encounterName, difficultyId, raidSize)
+end
+
+function addon:ENCOUNTER_END(evt, encounterId, encounterName, difficultyId, raidSize, endStatus)
+	self:Debug("ENCOUNTER_END", encounterId, encounterName, difficultyId, raidSize, endStatus)
+	if endStatus == 1 then -- Success
+		self:SetCurrentEncounter(encounterId, encounterName, difficultyId, raidSize)
+	else
+		self:UnsetCurrentEncounter()
+	end
+end
+
+function addon:ZONE_CHANGED_NEW_AREA(evt)
+	self:UpdateCurrentZone();
+end
+
+function addon:EndSegment()
+	self:Debug("EndSegment")
+
+	-- Find the skada set matching the encounter
+	-- Looking only at the lastest set should make sense,
+	-- as that set should be the boss segment we just ended.
+	local _, skadaSet = next(Skada:GetSets());
+	local encounter = self.currentEncounter;
+
+	if not self.guildName then
+		self:Debug("Not in a guild");
+		return;
+	end
+
+	if not encounter then
+		self:Debug("No current encounter");
+		return
+	end
+
+	if not skadaSet or not skadaSet.gotboss or skadaSet.mobname ~= encounter.name then
+		self:Debug("No Skada set found for boss");
+		return;
+	end
+
+	encounter.duration = skadaSet.time;
+	encounter.startTime = skadaSet.starttime;
+
+	local players = self:GetGuildPlayersFromSet(skadaSet);
+	self:SetRoleForPlayers(players);
+	self:SetClassForPlayers(players);
+	self.inspect:GetInspectDataForPlayers(players, function()
+		self.highscore:AddEncounterParsesForPlayers(self.guildName, encounter, players);
+	end)
+
+	self:UnsetCurrentEncounter();
+end
+
+function addon:OnInitialize()
+	self.db = LibStub("AceDB-3.0"):New("GuildSkadaHighScoreDB", addon.dbDefaults, true)
+
+	-- Make sure db version is in sync
+	if self.db.global.dbVersion ~= self.dbVersion then
+		self:Debug(format("Found not matching db versions: db=%d, addon=%d",
+			self.db.global.dbVersion, self.dbVersion));
+		self:Debug("Resetting db");
+		self.db:ResetDB();
+		self.db.global.dbVersion = self.dbVersion;
+	end
+
+	-- Purge old logs
+	if DEBUG_LOG then
+		local numLogsToPurge = (#self.db.global.debugLog - MAX_NUM_DEBUG_LOG_ENTRIES);
+		while numLogsToPurge >= 0 do
+			tremove(self.db.global.debugLog, 1)
+			numLogsToPurge = numLogsToPurge - 1;
+		end
+	else
+		wipe(self.db.global.debugLog);
+	end
+end
+
+function addon:OnEnable()
+	self.currentEncounter = nil;
+	self.currentZone = {};
+	self.guildName = nil;
+
+	self:RegisterEvent("ENCOUNTER_START")
+	self:RegisterEvent("ENCOUNTER_END")
+	self:RegisterEvent("PLAYER_GUILD_UPDATE")
+	self:RegisterEvent("ZONE_CHANGED_NEW_AREA")
+
+	self:SecureHook(Skada, "EndSegment")
+
+	self:RegisterChatCommand("gshs", function()
+		self.gui:ShowMainFrame();
+	end)
+
+	self:UpdateMyGuildName();
+	self:UpdateCurrentZone();
+end
+
+function addon:OnDisable()
+	self.currentEncounter = nil;
+	self.currentZone = {};
+	self.guildName = nil;
+
+	self:UnregisterEvent("ENCOUNTER_START")
+	self:UnregisterEvent("ENCOUNTER_END")
+	self:UnregisterEvent("PLAYER_GUILD_UPDATE")
+	self:UnregisterEvent("ZONE_CHANGED_NEW_AREA")
+
+    self:UnHook(Skada, "EndSegment");
+
+    self:UnregisterChatCommand("gshs");
+end
diff --git a/src/src-include.xml b/src/src-include.xml
new file mode 100644
index 0000000..6cdd3d7
--- /dev/null
+++ b/src/src-include.xml
@@ -0,0 +1,6 @@
+<Ui xsi:schemaLocation="http://www.blizzard.com/wow/ui/ ..\..\FrameXML\UI.xsd">
+    <Script file="main.lua"/>
+    <Script file="inspect.lua"/>
+    <Script file="highscore.lua"/>
+    <Script file="gui.lua"/>
+</Ui>
\ No newline at end of file