فهرست منبع

Initial commit

Dragon 4 سال پیش
کامیت
efb00f6d6d

+ 5 - 0
.gitignore

@@ -0,0 +1,5 @@
+bin/
+obj/
+/packages/
+riderModule.iml
+/_ReSharper.Caches/

+ 15 - 0
Attributes/AliasesAttribute.cs

@@ -0,0 +1,15 @@
+using System;
+
+namespace HuntBuddy.Attributes
+{
+	[AttributeUsage(AttributeTargets.Method)]
+	public class AliasesAttribute : Attribute
+	{
+		public string[] Aliases { get; }
+
+		public AliasesAttribute(params string[] aliases)
+		{
+			Aliases = aliases;
+		}
+	}
+}

+ 15 - 0
Attributes/CommandAttribute.cs

@@ -0,0 +1,15 @@
+using System;
+
+namespace HuntBuddy.Attributes
+{
+	[AttributeUsage(AttributeTargets.Method)]
+	public class CommandAttribute : Attribute
+	{
+		public string Command { get; }
+
+		public CommandAttribute(string command)
+		{
+			Command = command;
+		}
+	}
+}

+ 9 - 0
Attributes/DoNotShowInHelpAttribute.cs

@@ -0,0 +1,9 @@
+using System;
+
+namespace HuntBuddy.Attributes
+{
+	[AttributeUsage(AttributeTargets.Method)]
+	public class DoNotShowInHelpAttribute : Attribute
+	{
+	}
+}

+ 15 - 0
Attributes/HelpMessageAttribute.cs

@@ -0,0 +1,15 @@
+using System;
+
+namespace HuntBuddy.Attributes
+{
+	[AttributeUsage(AttributeTargets.Method)]
+	public class HelpMessageAttribute : Attribute
+	{
+		public string HelpMessage { get; }
+
+		public HelpMessageAttribute(string helpMessage)
+		{
+			HelpMessage = helpMessage;
+		}
+	}
+}

+ 24 - 0
Configuration.cs

@@ -0,0 +1,24 @@
+using System.Numerics;
+using System.Text.Json.Serialization;
+using Dalamud.Configuration;
+
+namespace HuntBuddy
+{
+	public class Configuration : IPluginConfiguration
+	{
+		public int Version { get; set; }
+
+		public bool ShowLocalHunts;
+		public bool ShowLocalHuntIcons;
+		public bool HideLocalHuntBackground;
+		public bool HideCompletedHunts;
+		public Vector4 IconBackgroundColour = new(0.76f, 0.75f, 0.76f, 0.8f);
+
+		[JsonIgnore] public uint IconBackgroundColourU32;
+
+		public void Save()
+		{
+			Plugin.PluginInterface.SavePluginConfig(this);
+		}
+	}
+}

+ 10 - 0
DalamudPackager.targets

@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project>
+    <Target Name="PackagePlugin" AfterTargets="Build" Condition="'$(Configuration)' == 'Release'">
+        <DalamudPackager
+                ProjectDir="$(ProjectDir)"
+                OutputPath="$(OutputPath)"
+                AssemblyName="$(AssemblyName)"
+                MakeZip="true"/>
+    </Target>
+</Project>

+ 48 - 0
HuntBuddy.csproj

@@ -0,0 +1,48 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+    <PropertyGroup>
+        <TargetFramework>net5.0-windows</TargetFramework>
+        <Nullable>enable</Nullable>
+        <AssemblyVersion>0.1.0.0</AssemblyVersion>
+        <FileVersion>0.1.0.0</FileVersion>
+        <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
+        <RootNamespace>HuntBuddy</RootNamespace>
+        <IsPackable>false</IsPackable>
+    </PropertyGroup>
+    <PropertyGroup>
+        <ProduceReferenceAssembly>false</ProduceReferenceAssembly>
+    </PropertyGroup>
+    <ItemGroup>
+        <Reference Include="Dalamud">
+            <HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\Dalamud.dll</HintPath>
+            <Private>False</Private>
+        </Reference>
+        <Reference Include="FFXIVClientStructs">
+            <HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\FFXIVClientStructs.dll</HintPath>
+            <Private>False</Private>
+        </Reference>
+        <Reference Include="ImGui.NET">
+            <HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\ImGui.NET.dll</HintPath>
+            <Private>False</Private>
+        </Reference>
+        <Reference Include="ImGuiScene">
+            <HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\ImGuiScene.dll</HintPath>
+            <Private>False</Private>
+        </Reference>
+        <Reference Include="Lumina">
+            <HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\Lumina.dll</HintPath>
+            <Private>False</Private>
+        </Reference>
+        <Reference Include="Lumina.Excel">
+            <HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\Lumina.Excel.dll</HintPath>
+            <Private>False</Private>
+        </Reference>
+        <Reference Include="Newtonsoft.Json">
+            <HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\Newtonsoft.Json.dll</HintPath>
+            <Private>False</Private>
+        </Reference>
+    </ItemGroup>
+    <ItemGroup>
+      <PackageReference Include="DalamudPackager" Version="2.1.5" />
+    </ItemGroup>
+</Project>

+ 8 - 0
HuntBuddy.json

@@ -0,0 +1,8 @@
+{
+  "Name": "Hunt Buddy",
+  "Author": "Dragon",
+  "Description": "A daily hunt bill tracker",
+  "RepoUrl": "https://github.com/SheepGoMeh/HuntBuddy",
+  "Tags": ["Hunt", "Daily", "Utility"],
+  "Punchline": "Helps you track your daily hunt bills"
+}

+ 16 - 0
HuntBuddy.sln

@@ -0,0 +1,16 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HuntBuddy", "HuntBuddy.csproj", "{69CDED6C-0BD1-47F3-905C-3F87B0D20789}"
+EndProject
+Global
+	GlobalSection(SolutionConfigurationPlatforms) = preSolution
+		Debug|Any CPU = Debug|Any CPU
+		Release|Any CPU = Release|Any CPU
+	EndGlobalSection
+	GlobalSection(ProjectConfigurationPlatforms) = postSolution
+		{69CDED6C-0BD1-47F3-905C-3F87B0D20789}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{69CDED6C-0BD1-47F3-905C-3F87B0D20789}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{69CDED6C-0BD1-47F3-905C-3F87B0D20789}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{69CDED6C-0BD1-47F3-905C-3F87B0D20789}.Release|Any CPU.Build.0 = Release|Any CPU
+	EndGlobalSection
+EndGlobal

+ 274 - 0
Interface.cs

@@ -0,0 +1,274 @@
+using System;
+using System.Linq;
+using System.Numerics;
+using System.Threading.Tasks;
+using Dalamud.Game.Text.SeStringHandling.Payloads;
+using Dalamud.Interface;
+using Dalamud.Logging;
+using HuntBuddy.Structs;
+using ImGuiNET;
+using Lumina.Excel.GeneratedSheets;
+
+namespace HuntBuddy
+{
+	public class Interface
+	{
+		private readonly Plugin _plugin;
+
+		public bool DrawInterface;
+		public bool DrawLocalHuntsInterface = true;
+		private bool _drawConfigurationInterface;
+
+		public unsafe Interface(Plugin plugin)
+		{
+			_plugin = plugin;
+		}
+
+		public unsafe bool Draw()
+		{
+			var draw = true;
+
+			var fontGlobalScale = ImGui.GetIO().FontGlobalScale;
+
+			ImGui.SetNextWindowSize(new Vector2(400 * ImGui.GetIO().FontGlobalScale, 500), ImGuiCond.Always);
+
+			if (!ImGui.Begin($"{_plugin.Name}", ref draw))
+			{
+				return draw;
+			}
+
+			if (!_plugin.MobHuntEntriesReady)
+			{
+				ImGui.Text("Reloading data ...");
+				ImGui.End();
+				return draw;
+			}
+
+			if (IconButton(FontAwesomeIcon.Redo, "Reload"))
+			{
+				ImGui.End();
+				_plugin.MobHuntEntriesReady = false;
+				Task.Run(() => _plugin.ReloadData());
+				return draw;
+			}
+
+			if (ImGui.IsItemHovered())
+			{
+				ImGui.BeginTooltip();
+				ImGui.Text("Click this button to reload daily hunt data");
+				ImGui.EndTooltip();
+			}
+			
+			ImGui.SameLine();
+
+			if (IconButton(FontAwesomeIcon.Cog, "Config"))
+			{
+				this._drawConfigurationInterface = !this._drawConfigurationInterface;
+			}
+
+			foreach (var expansionEntry in _plugin.MobHuntEntries.Where(expansionEntry =>
+				         ImGui.TreeNode(expansionEntry.Key)))
+			{
+				foreach (var entry in expansionEntry.Value.Where(entry =>
+					         ImGui.TreeNode(
+						         $"{entry.Key.Value} ({entry.Value.Count(x => _plugin.MobHuntStruct->CurrentKills[x.CurrentKillsOffset] == x.NeededKills)}/{entry.Value.Count})")))
+				{
+					ImGui.Indent();
+					foreach (var mobHuntEntry in entry.Value)
+					{
+						if (LocationDb.Database.ContainsKey(mobHuntEntry.MobHuntId))
+						{
+							if (IconButton(FontAwesomeIcon.Search, $"##{mobHuntEntry.MobHuntId}"))
+							{
+								LocationDb.OpenMapLink(mobHuntEntry.TerritoryType, mobHuntEntry.MapId,
+									mobHuntEntry.MobHuntId);
+							}
+
+							ImGui.SameLine();
+						}
+
+						var currentKills = _plugin.MobHuntStruct->CurrentKills[mobHuntEntry.CurrentKillsOffset];
+						ImGui.Text(mobHuntEntry.Name);
+						if (ImGui.IsItemHovered())
+						{
+							ImGui.PushStyleColor(ImGuiCol.PopupBg, Vector4.Zero);
+							ImGui.BeginTooltip();
+							var imageSize = 256f * fontGlobalScale;
+							var cursorPos = ImGui.GetCursorScreenPos();
+							ImGui.InvisibleButton("canvas", new Vector2(imageSize));
+
+							var drawList = ImGui.GetWindowDrawList();
+							if (mobHuntEntry.ExpansionId == 4 &&
+							    mobHuntEntry.MobHuntType == 1) // Endwalker uses circle for non elite mobs
+							{
+								drawList.AddCircleFilled(cursorPos + new Vector2(imageSize / 2f), imageSize / 2f,
+									_plugin.Configuration.IconBackgroundColourU32);
+							}
+							else
+							{
+								drawList.AddRectFilled(cursorPos, cursorPos + new Vector2(imageSize), _plugin.Configuration.IconBackgroundColourU32);
+							}
+
+							drawList.AddImage(mobHuntEntry.Icon.ImGuiHandle, cursorPos,
+								new Vector2(cursorPos.X + imageSize, cursorPos.Y + imageSize));
+							ImGui.PopStyleColor();
+							ImGui.EndTooltip();
+						}
+
+						ImGui.SameLine();
+						if (currentKills == mobHuntEntry.NeededKills)
+						{
+							ImGui.Text($"({currentKills}/{mobHuntEntry.NeededKills})");
+						}
+						else
+						{
+							ImGui.TextColored(new Vector4(0f, 1f, 0f, 1f),
+								$"({currentKills}/{mobHuntEntry.NeededKills})");
+						}
+					}
+
+					ImGui.Unindent();
+					ImGui.TreePop();
+				}
+
+				ImGui.TreePop();
+			}
+
+			ImGui.End();
+
+			if (_drawConfigurationInterface)
+			{
+				this.DrawConfiguration();
+			}
+
+			return draw;
+		}
+
+		public unsafe void DrawLocalHunts()
+		{
+			if (!_plugin.Configuration.ShowLocalHunts ||
+			    _plugin.CurrentAreaMobHuntEntries.IsEmpty ||
+			    _plugin.CurrentAreaMobHuntEntries.Count(x =>
+				    _plugin.MobHuntStruct->CurrentKills[x.CurrentKillsOffset] == x.NeededKills) ==
+			    _plugin.CurrentAreaMobHuntEntries.Count)
+			{
+				return;
+			}
+
+			var fontGlobalScale = ImGui.GetIO().FontGlobalScale;
+
+			ImGui.SetNextWindowSize(new Vector2(0, 0), ImGuiCond.Always);
+
+			var windowFlags = ImGuiWindowFlags.NoNavInputs;
+
+			if (_plugin.Configuration.HideLocalHuntBackground)
+			{
+				windowFlags |= ImGuiWindowFlags.NoBackground;
+			}
+
+			if (!ImGui.Begin("Hunts in current area", windowFlags))
+			{
+				return;
+			}
+
+			foreach (var mobHuntEntry in _plugin.CurrentAreaMobHuntEntries)
+			{
+				var currentKills = _plugin.MobHuntStruct->CurrentKills[mobHuntEntry.CurrentKillsOffset];
+
+				if (_plugin.Configuration.HideCompletedHunts && currentKills == mobHuntEntry.NeededKills)
+				{
+					continue;
+				}
+
+				if (LocationDb.Database.ContainsKey(mobHuntEntry.MobHuntId))
+				{
+					if (IconButton(FontAwesomeIcon.Search, $"##{mobHuntEntry.MobHuntId}"))
+					{
+						LocationDb.OpenMapLink(mobHuntEntry.TerritoryType, mobHuntEntry.MapId,
+							mobHuntEntry.MobHuntId);
+					}
+
+					ImGui.SameLine();
+				}
+
+				ImGui.Text($"{mobHuntEntry.Name} ({currentKills}/{mobHuntEntry.NeededKills})");
+
+				if (!_plugin.Configuration.ShowLocalHuntIcons)
+				{
+					continue;
+				}
+
+				var imageSize = 128f * fontGlobalScale;
+				ImGui.SetCursorPosX(ImGui.GetWindowWidth() / 2f * fontGlobalScale - imageSize / 2f);
+				var cursorPos = ImGui.GetCursorScreenPos();
+				ImGui.InvisibleButton("canvas", new Vector2(imageSize));
+
+				var drawList = ImGui.GetWindowDrawList();
+				if (mobHuntEntry.ExpansionId == 4 &&
+				    mobHuntEntry.MobHuntType == 1) // Endwalker uses circle for non elite mobs
+				{
+					drawList.AddCircleFilled(cursorPos + new Vector2(imageSize / 2f), imageSize / 2f, _plugin.Configuration.IconBackgroundColourU32);
+				}
+				else
+				{
+					drawList.AddRectFilled(cursorPos, cursorPos + new Vector2(imageSize), _plugin.Configuration.IconBackgroundColourU32);
+				}
+
+				drawList.AddImage(mobHuntEntry.Icon.ImGuiHandle, cursorPos,
+					new Vector2(cursorPos.X + imageSize, cursorPos.Y + imageSize));
+			}
+
+			ImGui.End();
+		}
+
+		private void DrawConfiguration()
+		{
+			ImGui.SetNextWindowSize(new Vector2(0, 0), ImGuiCond.Always);
+
+			if (!ImGui.Begin($"{_plugin.Name} settings"))
+			{
+				return;
+			}
+
+			var save = false;
+
+			save |= ImGui.Checkbox("Show hunts in local area", ref _plugin.Configuration.ShowLocalHunts);
+			save |= ImGui.Checkbox("Show icons of hunts in local area", ref _plugin.Configuration.ShowLocalHuntIcons);
+			save |= ImGui.Checkbox("Hide background of local hunts window",
+				ref _plugin.Configuration.HideLocalHuntBackground);
+			save |= ImGui.Checkbox("Hide completed targets in local hunts window",
+				ref _plugin.Configuration.HideCompletedHunts);
+			if (ImGui.ColorEdit4("Hunt icon background colour", ref _plugin.Configuration.IconBackgroundColour))
+			{
+				_plugin.Configuration.IconBackgroundColourU32 =
+					ImGui.ColorConvertFloat4ToU32(_plugin.Configuration.IconBackgroundColour);
+				save = true;
+			}
+
+			if (save)
+			{
+				_plugin.Configuration.Save();
+			}
+
+
+			ImGui.End();
+		}
+
+		private static bool IconButton(FontAwesomeIcon icon, string? id = null)
+		{
+			ImGui.PushFont(UiBuilder.IconFont);
+
+			var text = icon.ToIconString();
+			if (id != null)
+			{
+				text += $"##{id}";
+			}
+
+			var result = ImGui.Button(text);
+
+			ImGui.PopFont();
+
+			return result;
+		}
+	}
+}

+ 225 - 0
LocationDb.cs

@@ -0,0 +1,225 @@
+using System.Collections.Generic;
+using Dalamud.Game.Text.SeStringHandling.Payloads;
+
+namespace HuntBuddy
+{
+	public static class LocationDb
+	{
+		public class PositionInfo
+		{
+			public float X { get; set; }
+			public float Y { get; set; }
+		}
+
+		// MobHuntId as key
+		public static readonly Dictionary<uint, PositionInfo> Database = new()
+		{
+			// Shadowbringers
+			// Lakeland
+			{ 08498, new PositionInfo { X = 19.0f, Y = 09.0f } }, // Chiliad Cama
+			{ 08502, new PositionInfo { X = 28.0f, Y = 23.2f } }, // Violet Triffid
+			{ 08503, new PositionInfo { X = 14.0f, Y = 16.5f } }, // Gnole
+			{ 08504, new PositionInfo { X = 24.4f, Y = 23.9f } }, // Wetland Warg
+			{ 08505, new PositionInfo { X = 33.2f, Y = 10.0f } }, // White Gremlin
+			{ 08507, new PositionInfo { X = 25.8f, Y = 23.3f } }, // Hoptrap
+			{ 08508, new PositionInfo { X = 28.5f, Y = 36.7f } }, // Wolverine
+			{ 08511, new PositionInfo { X = 11.3f, Y = 11.0f } }, // Smilodon
+			{ 08514, new PositionInfo { X = 34.2f, Y = 17.0f } }, // Ya-te-veo
+			{ 08515, new PositionInfo { X = 29.0f, Y = 17.6f } }, // Proterosuchus
+			{ 08786, new PositionInfo { X = 20.5f, Y = 25.3f } }, // Lake Viper
+
+			// Kholusia
+			{ 08517, new PositionInfo { X = 31.9f, Y = 18.9f } }, // Ironbeard
+			{ 08518, new PositionInfo { X = 36.4f, Y = 28.7f } }, // Hobgoblin
+			{ 08520, new PositionInfo { X = 17.0f, Y = 18.0f } }, // Defective Talos
+			{ 08522, new PositionInfo { X = 34.8f, Y = 10.5f } }, // Sulfur Byrgen
+			{ 08523, new PositionInfo { X = 35.4f, Y = 29.2f } }, // Maultasche
+			{ 08524, new PositionInfo { X = 14.3f, Y = 11.4f } }, // Huldu
+			{ 08525, new PositionInfo { X = 14.3f, Y = 27.1f } }, // Island Rail
+			{ 08527, new PositionInfo { X = 17.0f, Y = 11.0f } }, // Cliffkite
+			{ 08528, new PositionInfo { X = 27.1f, Y = 13.8f } }, // Cliffmole
+			{ 08529, new PositionInfo { X = 08.0f, Y = 18.0f } }, // Scree Gnome
+			{ 08532, new PositionInfo { X = 17.8f, Y = 26.5f } }, // Wood Eyes
+			{ 08533, new PositionInfo { X = 25.0f, Y = 23.5f } }, // Island Wolf
+			{ 08534, new PositionInfo { X = 10.1f, Y = 29.6f } }, // Kholusian Bison
+			{ 08536, new PositionInfo { X = 32.5f, Y = 26.2f } }, // Whiptail
+			{ 08538, new PositionInfo { X = 22.5f, Y = 09.6f } }, // Highland Hyssop
+			{ 08539, new PositionInfo { X = 19.9f, Y = 33.0f } }, // Tragopan
+			{ 08540, new PositionInfo { X = 13.0f, Y = 15.0f } }, // Saichania
+			{ 08541, new PositionInfo { X = 21.0f, Y = 08.7f } }, // Gulgnu
+			{ 08542, new PositionInfo { X = 21.6f, Y = 32.0f } }, // Germinant
+
+			// Amh Araeng
+			{ 08544, new PositionInfo { X = 11.4f, Y = 30.4f } }, // Masterless Talos
+			{ 08545, new PositionInfo { X = 19.1f, Y = 20.9f } }, // Evil Weapon
+			{ 08547, new PositionInfo { X = 30.4f, Y = 12.3f } }, // Gigantender
+			{ 08550, new PositionInfo { X = 30.1f, Y = 27.2f } }, // Ancient Lizard
+			{ 08556, new PositionInfo { X = 29.4f, Y = 21.7f } }, // Sand Mole
+			{ 08557, new PositionInfo { X = 12.7f, Y = 19.0f } }, // Thistle Mole
+			{ 08558, new PositionInfo { X = 30.9f, Y = 27.3f } }, // Scissorjaws
+			{ 08559, new PositionInfo { X = 21.5f, Y = 09.7f } }, // Gnome
+			{ 08561, new PositionInfo { X = 13.9f, Y = 18.2f } }, // Debitage
+			{ 08562, new PositionInfo { X = 27.1f, Y = 29.6f } }, // Ghilman
+			{ 08563, new PositionInfo { X = 25.0f, Y = 34.3f } }, // Flame Zonure
+			{ 08565, new PositionInfo { X = 15.2f, Y = 16.7f } }, // Phorusrhacos
+			{ 08566, new PositionInfo { X = 21.7f, Y = 09.8f } }, // Desert Coyote
+			{ 08567, new PositionInfo { X = 23.9f, Y = 31.8f } }, // Molamander
+
+			// Il Mheg
+			{ 08155, new PositionInfo { X = 08.4f, Y = 30.0f } }, // Flower Basket
+			{ 08569, new PositionInfo { X = 18.0f, Y = 31.0f } }, // Echevore
+			{ 08574, new PositionInfo { X = 31.0f, Y = 14.3f } }, // Garden Porxie
+			{ 08575, new PositionInfo { X = 19.9f, Y = 16.3f } }, // Phooka
+			{ 08576, new PositionInfo { X = 11.1f, Y = 26.0f } }, // Etainmoth
+			{ 08577, new PositionInfo { X = 29.4f, Y = 12.7f } }, // Green Glider
+			{ 08578, new PositionInfo { X = 21.0f, Y = 08.8f } }, // Moss Fungus
+			{ 08581, new PositionInfo { X = 07.8f, Y = 18.7f } }, // Hawker
+			{ 08582, new PositionInfo { X = 25.0f, Y = 11.0f } }, // Rainbow Lorikeet
+			{ 08583, new PositionInfo { X = 29.5f, Y = 11.4f } }, // Tot Aevis
+			{ 08584, new PositionInfo { X = 30.4f, Y = 10.6f } }, // Rabbit's Tail
+			{ 08585, new PositionInfo { X = 19.0f, Y = 32.0f } }, // Rosebear
+			{ 08586, new PositionInfo { X = 31.6f, Y = 06.4f } }, // Garden Crocota
+			{ 08587, new PositionInfo { X = 32.0f, Y = 05.8f } }, // Werewood
+			{ 08590, new PositionInfo { X = 09.4f, Y = 15.0f } }, // Killer Bee
+
+			// The Rak'tika Greatwood
+			{ 08596, new PositionInfo { X = 08.8f, Y = 35.6f } }, // Tomatl
+			{ 08597, new PositionInfo { X = 27.3f, Y = 25.6f } }, // Forest Echo
+			{ 08598, new PositionInfo { X = 25.1f, Y = 14.2f } }, // Cracked Ronkan Doll
+			{ 08599, new PositionInfo { X = 23.0f, Y = 14.0f } }, // Cracked Ronkan Thorn
+			{ 08600, new PositionInfo { X = 16.0f, Y = 32.0f } }, // Vampire Vine
+			{ 08601, new PositionInfo { X = 23.4f, Y = 07.6f } }, // Greatwood Rail
+			{ 08603, new PositionInfo { X = 29.4f, Y = 21.7f } }, // Snapweed
+			{ 08604, new PositionInfo { X = 12.0f, Y = 34.0f } }, // Atrociraptor
+			{ 08606, new PositionInfo { X = 27.7f, Y = 23.2f } }, // Gizamaluk
+			{ 08609, new PositionInfo { X = 16.9f, Y = 33.3f } }, // Helm Beetle
+			{ 08610, new PositionInfo { X = 34.1f, Y = 16.5f } }, // Floor Mandrill
+			{ 08611, new PositionInfo { X = 15.0f, Y = 19.4f } }, // Wild Swine
+			{ 08612, new PositionInfo { X = 24.9f, Y = 30.2f } }, // Caracal
+			{ 08614, new PositionInfo { X = 25.0f, Y = 07.2f } }, // Woodbat
+			{ 08616, new PositionInfo { X = 27.9f, Y = 21.2f } }, // Tarichuk
+			{ 08789, new PositionInfo { X = 21.1f, Y = 13.2f } }, // Cracked Ronkan Vessel
+
+			// The Tempest
+			{ 08618, new PositionInfo { X = 28.6f, Y = 06.2f } }, // Clinoid
+			{ 08619, new PositionInfo { X = 28.2f, Y = 18.3f } }, // Dagon
+			{ 08621, new PositionInfo { X = 22.6f, Y = 31.7f } }, // Cubus
+			{ 08622, new PositionInfo { X = 25.1f, Y = 18.6f } }, // Sea Anemone
+			{ 08623, new PositionInfo { X = 32.1f, Y = 11.7f } }, // Amphisbaena
+			{ 08625, new PositionInfo { X = 32.5f, Y = 21.5f } }, // Morgawr
+			{ 08626, new PositionInfo { X = 36.6f, Y = 16.6f } }, // Trilobite
+			{ 08629, new PositionInfo { X = 27.7f, Y = 08.7f } }, // Sea Gelatin
+			{ 08630, new PositionInfo { X = 29.0f, Y = 21.0f } }, // Tempest Swallow
+			{ 08631, new PositionInfo { X = 35.8f, Y = 07.2f } }, // Blue Swimmer
+
+
+			// Endwalker
+			// Labyrinthos
+			{ 10668, new PositionInfo { X = 28.8f, Y = 08.8f } }, // Troll
+			{ 10669, new PositionInfo { X = 31.0f, Y = 25.5f } }, // Genomos
+			{ 10670, new PositionInfo { X = 15.0f, Y = 06.5f } }, // Caribou
+			{ 10672, new PositionInfo { X = 32.0f, Y = 08.8f } }, // Limascabra
+			{ 10673, new PositionInfo { X = 21.5f, Y = 13.5f } }, // Luncheon Toad
+			{ 10674, new PositionInfo { X = 17.0f, Y = 12.0f } }, // Yakow
+			{ 10677, new PositionInfo { X = 34.0f, Y = 15.0f } }, // Labyrinth Screamer
+			{ 10678, new PositionInfo { X = 24.0f, Y = 10.7f } }, // Northern Snapweed
+			{ 10679, new PositionInfo { X = 26.0f, Y = 14.5f } }, // Pephredo
+			{ 10683, new PositionInfo { X = 37.5f, Y = 19.5f } }, // Mythrilcap
+
+			// Thavnair
+			{ 10697, new PositionInfo { X = 19.0f, Y = 23.9f } }, // Pisaca
+			{ 10698, new PositionInfo { X = 13.8f, Y = 18.5f } }, // Vajralangula
+			{ 10699, new PositionInfo { X = 19.2f, Y = 32.6f } }, // Kacchapa
+			{ 10700, new PositionInfo { X = 18.4f, Y = 26.7f } }, // Hamsa
+			{ 10701, new PositionInfo { X = 29.1f, Y = 12.2f } }, // Asvattha
+			{ 10702, new PositionInfo { X = 27.1f, Y = 27.8f } }, // Guhasaya
+			{ 10703, new PositionInfo { X = 27.0f, Y = 17.4f } }, // Bhujamga
+			{ 10704, new PositionInfo { X = 17.6f, Y = 17.8f } }, // Sotormurg
+			{ 10705, new PositionInfo { X = 22.7f, Y = 30.4f } }, // Gaja
+			{ 10706, new PositionInfo { X = 19.1f, Y = 11.7f } }, // Thavnairian Jhammel
+			{ 10707, new PositionInfo { X = 25.9f, Y = 19.0f } }, // Ufiti
+			{ 10709, new PositionInfo { X = 09.2f, Y = 12.8f } }, // Chamrosh
+			{ 10711, new PositionInfo { X = 16.1f, Y = 09.2f } }, // Starmite
+			{ 10712, new PositionInfo { X = 14.3f, Y = 12.7f } }, // Manjusaka
+			{ 10713, new PositionInfo { X = 23.3f, Y = 19.9f } }, // Odqan
+			{ 10715, new PositionInfo { X = 13.4f, Y = 28.5f } }, // Akyaali Crab
+			{ 10716, new PositionInfo { X = 08.2f, Y = 16.2f } }, // Valras
+
+			// Garlemald
+			{ 10648, new PositionInfo { X = 18.8f, Y = 09.8f } }, // Automated Satellite
+			{ 10649, new PositionInfo { X = 25.5f, Y = 17.5f } }, // Automated Death Machine
+			{ 10650, new PositionInfo { X = 15.5f, Y = 29.5f } }, // Automated Cavalry
+			{ 10651, new PositionInfo { X = 21.8f, Y = 17.4f } }, // Automated Bit
+			{ 10652, new PositionInfo { X = 15.7f, Y = 09.8f } }, // Automated Roader
+			{ 10653, new PositionInfo { X = 29.5f, Y = 13.7f } }, // Automated Slasher
+			{ 10654, new PositionInfo { X = 24.3f, Y = 14.9f } }, // Automated Colossus
+			{ 10655, new PositionInfo { X = 12.9f, Y = 11.7f } }, // Automated Avenger
+			{ 10656, new PositionInfo { X = 29.6f, Y = 30.3f } }, // Almasty
+			{ 10657, new PositionInfo { X = 14.6f, Y = 26.1f } }, // Eblan Bear
+			{ 10658, new PositionInfo { X = 31.3f, Y = 17.4f } }, // Eblan Icetrap
+			{ 10659, new PositionInfo { X = 19.8f, Y = 29.1f } }, // Ovibos
+			{ 10660, new PositionInfo { X = 22.3f, Y = 24.9f } }, // Jotunn
+			{ 10661, new PositionInfo { X = 28.4f, Y = 33.0f } }, // Ceruleum Zoblyn
+			{ 10662, new PositionInfo { X = 25.4f, Y = 31.5f } }, // Ilsabardian Tursus
+			{ 10663, new PositionInfo { X = 18.7f, Y = 24.8f } }, // Canis Lupinus
+			{ 10664, new PositionInfo { X = 26.1f, Y = 26.5f } }, // Overgrown Rose
+
+			// Mare Lamentorum
+			{ 10458, new PositionInfo { X = 23.9f, Y = 20.0f } }, // Daphnia
+			{ 10459, new PositionInfo { X = 23.7f, Y = 20.3f } }, // Osculator
+			{ 10460, new PositionInfo { X = 08.6f, Y = 35.5f } }, // Sweeper
+			{ 10461, new PositionInfo { X = 27.3f, Y = 26.0f } }, // Wanderer
+			{ 10462, new PositionInfo { X = 31.1f, Y = 32.2f } }, // Weeper
+			{ 10463, new PositionInfo { X = 19.8f, Y = 22.5f } }, // Thinker
+			{ 10464, new PositionInfo { X = 26.0f, Y = 34.0f } }, // Regolith
+			{ 10465, new PositionInfo { X = 21.4f, Y = 32.2f } }, // Trimmer
+			{ 10467, new PositionInfo { X = 12.0f, Y = 36.7f } }, // Panopt
+			{ 10468, new PositionInfo { X = 11.5f, Y = 22.3f } }, // Dynamite
+			{ 10469, new PositionInfo { X = 16.7f, Y = 31.8f } }, // Armalcolite
+			{ 10470, new PositionInfo { X = 12.9f, Y = 09.6f } }, // Caretaker
+			{ 10471, new PositionInfo { X = 16.1f, Y = 24.9f } }, // Mousse
+			{ 10473, new PositionInfo { X = 31.2f, Y = 27.0f } }, // Downfall Alarum
+			{ 10474, new PositionInfo { X = 33.6f, Y = 26.2f } }, // Downfall Droid
+			{ 10475, new PositionInfo { X = 34.5f, Y = 28.0f } }, // Downfall Hunter
+			{ 10476, new PositionInfo { X = 13.0f, Y = 10.0f } }, // Supporter
+			{ 10477, new PositionInfo { X = 30.1f, Y = 11.0f } }, // Scraper
+
+			// Elpis
+			{ 10590, new PositionInfo { X = 25.7f, Y = 33.9f } }, // Ophion
+			{ 10591, new PositionInfo { X = 16.5f, Y = 29.9f } }, // Yggdreant
+			{ 10592, new PositionInfo { X = 22.6f, Y = 20.0f } }, // Okyupete
+			{ 10594, new PositionInfo { X = 12.4f, Y = 31.8f } }, // Gryps
+			{ 10595, new PositionInfo { X = 26.6f, Y = 29.7f } }, // Monoceros
+			{ 10596, new PositionInfo { X = 10.1f, Y = 14.1f } }, // Pegasos
+			{ 10597, new PositionInfo { X = 28.7f, Y = 25.6f } }, // Bird of Elpis
+			{ 10599, new PositionInfo { X = 33.4f, Y = 14.3f } }, // Hippe
+			{ 10600, new PositionInfo { X = 14.1f, Y = 09.9f } }, // Harpuia
+			{ 10601, new PositionInfo { X = 25.0f, Y = 10.0f } }, // Morbol Marquis
+			{ 10602, new PositionInfo { X = 29.2f, Y = 09.3f } }, // Akantha
+			{ 10603, new PositionInfo { X = 24.4f, Y = 14.3f } }, // Lykopersikon
+			{ 10606, new PositionInfo { X = 21.5f, Y = 06.3f } }, // Lotis
+			{ 10607, new PositionInfo { X = 10.2f, Y = 34.6f } }, // Phanopsyche
+			{ 10608, new PositionInfo { X = 12.9f, Y = 23.4f } }, // Melanion
+			{ 10609, new PositionInfo { X = 12.9f, Y = 08.7f } }, // Ophiotauros
+			{ 10610, new PositionInfo { X = 13.3f, Y = 15.7f } }, // Elpis Minotaur
+			{ 10611, new PositionInfo { X = 30.7f, Y = 17.1f } }, // Remora
+
+			// Ultima Thule
+			{ 10419, new PositionInfo { X = 30.1f, Y = 25.9f } }, // Broken Omicron
+			{ 10420, new PositionInfo { X = 19.3f, Y = 11.8f } }, // Drifting Ea
+			{ 10421, new PositionInfo { X = 34.8f, Y = 28.8f } }, // Beta
+			{ 10422, new PositionInfo { X = 32.9f, Y = 28.8f } }, // Delta
+			{ 10423, new PositionInfo { X = 36.5f, Y = 25.9f } }, // Lambda
+			{ 10424, new PositionInfo { X = 32.1f, Y = 26.6f } }, // Level Tricker
+			{ 10427, new PositionInfo { X = 10.0f, Y = 30.0f } }, // Stellar Amphiptere
+			{ 10430, new PositionInfo { X = 14.4f, Y = 28.2f } }, // Stellar Brobinyak
+			{ 10435, new PositionInfo { X = 16.3f, Y = 14.1f } }, // Other One
+		};
+
+		public static void OpenMapLink(uint territoryType, uint mapId, uint mobHuntId)
+		{
+			var mapLinkPayload = new MapLinkPayload(territoryType, mapId, Database[mobHuntId].X, Database[mobHuntId].Y);
+			Plugin.GameGui.OpenMapWithMapLink(mapLinkPayload);
+		}
+	}
+}

+ 25 - 0
MobHuntEntry.cs

@@ -0,0 +1,25 @@
+using System;
+using ImGuiScene;
+
+namespace HuntBuddy
+{
+	public class MobHuntEntry : IDisposable
+	{
+		public string? Name { get; init; }
+		public string? TerritoryName { get; init; }
+		public string? ExpansionName { get; init; }
+		public uint ExpansionId { get; init; }
+		public uint MapId { get; init; }
+		public uint TerritoryType { get; init; }
+		public uint MobHuntId { get; init; }
+		public byte MobHuntType { get; init; }
+		public uint CurrentKillsOffset { get; init; }
+		public uint NeededKills { get; set; }
+		public TextureWrap Icon { get; init; } = null!;
+
+		public void Dispose()
+		{
+			Icon.Dispose();
+		}
+	}
+}

+ 265 - 0
Plugin.cs

@@ -0,0 +1,265 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using Dalamud.Data;
+using Dalamud.Game;
+using Dalamud.Game.ClientState;
+using Dalamud.Game.Command;
+using Dalamud.Game.Gui;
+using Dalamud.IoC;
+using Dalamud.Logging;
+using Dalamud.Plugin;
+using Dalamud.Utility;
+using Lumina.Excel.GeneratedSheets;
+using FFXIVClientStructs.FFXIV.Client.Game;
+using HuntBuddy.Attributes;
+using HuntBuddy.Structs;
+using ImGuiNET;
+using ImGuiScene;
+using Lumina.Data.Files;
+
+namespace HuntBuddy
+{
+	public class Plugin : IDalamudPlugin
+	{
+		public string Name => "Hunt Buddy";
+
+		[PluginService]
+		[RequiredVersion("1.0")]
+		public static DalamudPluginInterface PluginInterface { get; set; } = null!;
+
+		[PluginService]
+		[RequiredVersion("1.0")]
+		public static CommandManager Commands { get; set; } = null!;
+
+		[PluginService]
+		[RequiredVersion("1.0")]
+		public static ChatGui Chat { get; set; } = null!;
+
+		[PluginService]
+		[RequiredVersion("1.0")]
+		public static DataManager DataManager { get; set; } = null!;
+		
+		[PluginService]
+		[RequiredVersion("1.0")]
+		public static SigScanner SigScanner { get; set; } = null!;
+		
+		[PluginService]
+		[RequiredVersion("1.0")]
+		public static GameGui GameGui { get; set; } = null!;
+		
+		[PluginService]
+		[RequiredVersion("1.0")]
+		public static ClientState ClientState { get; set; } = null!;
+
+		private readonly PluginCommandManager<Plugin> _commandManager;
+		private readonly Interface _interface;
+		public readonly Dictionary<string, Dictionary<KeyValuePair<uint, string>, List<MobHuntEntry>>> MobHuntEntries;
+		public readonly ConcurrentBag<MobHuntEntry> CurrentAreaMobHuntEntries;
+		public bool MobHuntEntriesReady = true;
+		public readonly unsafe MobHuntStruct* MobHuntStruct;
+		public readonly Configuration Configuration;
+
+		public Plugin()
+		{
+			this._commandManager = new PluginCommandManager<Plugin>(this, Commands);
+			this._interface = new Interface(this);
+			this.MobHuntEntries = new Dictionary<string, Dictionary<KeyValuePair<uint, string>, List<MobHuntEntry>>>();
+			this.CurrentAreaMobHuntEntries = new ConcurrentBag<MobHuntEntry>();
+			this.Configuration = (Configuration)(PluginInterface.GetPluginConfig() ?? new Configuration());
+			this.Configuration.IconBackgroundColourU32 =
+				ImGui.ColorConvertFloat4ToU32(this.Configuration.IconBackgroundColour);
+
+			unsafe
+			{
+				this.MobHuntStruct =
+					(MobHuntStruct*)SigScanner.GetStaticAddressFromSig(
+						"D1 48 8D 0D ?? ?? ?? ?? 48 83 C4 20 5F E9 ?? ?? ?? ??");
+			}
+
+			ClientState.TerritoryChanged += ClientStateOnTerritoryChanged;
+			PluginInterface.UiBuilder.Draw += DrawInterface;
+			PluginInterface.UiBuilder.Draw += _interface.DrawLocalHunts;
+			PluginInterface.UiBuilder.OpenConfigUi += OpenConfigUi;
+		}
+
+		private void ClientStateOnTerritoryChanged(object? sender, ushort e)
+		{
+			CurrentAreaMobHuntEntries.Clear();
+
+			foreach (var mobHuntEntry in MobHuntEntries.SelectMany(expansionEntry => expansionEntry.Value
+				         .Where(entry => entry.Key.Key == ClientState.TerritoryType)
+				         .SelectMany(entry => entry.Value)))
+			{
+				CurrentAreaMobHuntEntries.Add(mobHuntEntry);
+			}
+		}
+
+		private void OpenConfigUi()
+		{
+			_interface.DrawInterface = !_interface.DrawInterface;
+		}
+
+		private void DrawInterface()
+		{
+			_interface.DrawInterface = _interface.DrawInterface && _interface.Draw();
+		}
+
+		private void Dispose(bool disposing)
+		{
+			if (!disposing)
+			{
+				return;
+			}
+
+			MobHuntEntriesReady = false;
+			PluginInterface.UiBuilder.Draw -= DrawInterface;
+			PluginInterface.UiBuilder.Draw -= _interface.DrawLocalHunts;
+			PluginInterface.UiBuilder.OpenConfigUi -= OpenConfigUi;
+
+			_commandManager.Dispose();
+		}
+
+		[Command("/phb")]
+		[HelpMessage("Toggles UI")]
+		public void PluginCommand(string command, string args)
+		{
+			OpenConfigUi();
+		}
+
+		public unsafe void ReloadData()
+		{
+			var inventoryContainer = InventoryManager.Instance()->GetInventoryContainer(InventoryType.KeyItems);
+
+			if (inventoryContainer->Loaded == 0)
+			{
+				PluginLog.Log("Container not loaded!");
+				return;
+			}
+
+			if (inventoryContainer->Size == 0)
+			{
+				PluginLog.Log("Container is empty!");
+				return;
+			}
+
+			var huntBills = new List<KeyValuePair<byte,uint>>();
+
+			for (var i = 0; i != inventoryContainer->Size; ++i)
+			{
+				var inventoryItemPtr = inventoryContainer->GetInventorySlot(i);
+
+				if (inventoryItemPtr == null || (*inventoryItemPtr).ItemID == 0)
+				{
+					continue;
+				}
+
+				var inventoryItem = *inventoryItemPtr;
+
+				foreach (var row in DataManager.Excel.GetSheet<MobHuntOrderType>()!)
+				{
+					if (inventoryItem.ItemID == row.EventItem.Row)
+					{
+						huntBills.Add(new KeyValuePair<byte, uint>(row.Type, row.RowId));
+						break;
+					}
+				}
+			}
+			
+			MobHuntEntries.Clear();
+			var mobHuntList = new List<MobHuntEntry>();
+			var mobHuntOrderSheet = DataManager.Excel.GetSheet<MobHuntOrder>()!;
+			
+			foreach (var (mobHuntType, billNumber) in huntBills)
+			{
+				var mobHuntOrderTypeRow = DataManager.Excel.GetSheet<MobHuntOrderType>()!.GetRow(billNumber)!;
+					
+				var rowId = mobHuntOrderTypeRow.OrderStart.Value!.RowId +
+				            (uint)(this.MobHuntStruct->BillOffset[mobHuntOrderTypeRow.RowId] - 1);
+
+				if (rowId > mobHuntOrderSheet.RowCount)
+				{
+					continue;
+				}
+
+				var mobHuntOrderRows = mobHuntOrderSheet.Where(x => x.RowId == rowId);
+
+				foreach (var mobHuntOrderRow in mobHuntOrderRows)
+				{
+					var mobHuntEntry =
+						mobHuntList.FirstOrDefault(x => x.MobHuntId == mobHuntOrderRow.Target.Value!.Name.Row);
+
+					if (mobHuntEntry == null)
+					{
+						mobHuntList.Add(new MobHuntEntry
+						{
+							Name = CultureInfo.InvariantCulture.TextInfo.ToTitleCase(mobHuntOrderRow.Target.Value!.Name.Value!.Singular),
+							TerritoryName = mobHuntOrderRow.Target.Value!.TerritoryType.Value!.PlaceName.Value!.Name,
+							ExpansionName = mobHuntOrderRow.Target.Value!.TerritoryType.Value.TerritoryType.Value!.ExVersion.Value!.Name,
+							ExpansionId = mobHuntOrderRow.Target.Value!.TerritoryType.Value.TerritoryType.Value!.ExVersion.Row,
+							MapId = mobHuntOrderRow.Target.Value!.TerritoryType.Row,
+							TerritoryType = mobHuntOrderRow.Target.Value!.TerritoryType.Value.TerritoryType.Row,
+							MobHuntId = mobHuntOrderRow.Target.Value!.Name.Row,
+							MobHuntType = mobHuntType,
+							CurrentKillsOffset = 5 * billNumber + mobHuntOrderRow.SubRowId,
+							NeededKills = mobHuntOrderRow.NeededKills,
+							Icon = LoadIcon(mobHuntOrderRow.Target.Value.Icon)
+						});
+					}
+					else
+					{
+						if (mobHuntEntry.NeededKills < mobHuntOrderRow.NeededKills)
+						{
+							mobHuntEntry.NeededKills = mobHuntOrderRow.NeededKills;
+						}
+					}
+				}
+			}
+			
+			foreach (var entry in mobHuntList)
+			{
+				var key = entry.ExpansionName ?? "Unknown";
+				var subKey = new KeyValuePair<uint, string>(entry.TerritoryType, entry.TerritoryName ?? "Unknown");
+					
+				if (!MobHuntEntries.ContainsKey(key))
+				{
+					MobHuntEntries[key] = new Dictionary<KeyValuePair<uint, string>, List<MobHuntEntry>>();
+				}
+
+				if (!MobHuntEntries[key].ContainsKey(subKey))
+				{
+					MobHuntEntries[key][subKey] = new List<MobHuntEntry>();
+				}
+					
+				MobHuntEntries[key][subKey].Add(entry);
+			}
+
+			ClientStateOnTerritoryChanged(null, 0);
+
+			MobHuntEntriesReady = true;
+			_interface.DrawInterface = true;
+		}
+		
+		private static TexFile? GetHdIcon(uint id)
+		{
+			var path = $"ui/icon/{id / 1000 * 1000:000000}/{id:000000}_hr1.tex";
+			return DataManager.GetFile<TexFile>(path);
+		}
+
+		private static TextureWrap LoadIcon(uint id)
+		{
+			var icon     = GetHdIcon(id) ?? DataManager.GetIcon(id)!;
+			var iconData = icon.GetRgbaImageData();
+
+			return PluginInterface.UiBuilder.LoadImageRaw(iconData, icon.Header.Width, icon.Header.Height, 4);
+		}
+
+		public void Dispose()
+		{
+			Dispose(true);
+			GC.SuppressFinalize(this);
+		}
+	}
+}

+ 87 - 0
PluginCommandManager.cs

@@ -0,0 +1,87 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using Dalamud.Game.Command;
+using HuntBuddy.Attributes;
+
+namespace HuntBuddy
+{
+	public class PluginCommandManager<THost> : IDisposable
+	{
+		private readonly CommandManager _commandManager;
+		private readonly (string, CommandInfo)[] _pluginCommands;
+		private readonly THost _host;
+
+		public PluginCommandManager(THost host, CommandManager commandManager)
+		{
+			this._commandManager = commandManager;
+			this._host = host;
+
+			this._pluginCommands = host!.GetType().GetMethods(BindingFlags.NonPublic | BindingFlags.Public |
+			                                                BindingFlags.Static | BindingFlags.Instance)
+				.Where(method => method.GetCustomAttribute<CommandAttribute>() != null)
+				.SelectMany(GetCommandInfoTuple)
+				.ToArray();
+
+			AddCommandHandlers();
+		}
+
+		// http://codebetter.com/patricksmacchia/2008/11/19/an-easy-and-efficient-way-to-improve-net-code-performances/
+		// Benchmarking this myself gave similar results, so I'm doing this to somewhat counteract using reflection to access command attributes.
+		// I like the convenience of attributes, but in principle it's a bit slower to use them as opposed to just initializing CommandInfos directly.
+		// It's usually sub-1 millisecond anyways, though. It probably doesn't matter at all.
+		private void AddCommandHandlers()
+		{
+			for (var i = 0; i < this._pluginCommands.Length; i++)
+			{
+				var (command, commandInfo) = this._pluginCommands[i];
+				this._commandManager.AddHandler(command, commandInfo);
+			}
+		}
+
+		private void RemoveCommandHandlers()
+		{
+			for (var i = 0; i < this._pluginCommands.Length; i++)
+			{
+				var (command, _) = this._pluginCommands[i];
+				this._commandManager.RemoveHandler(command);
+			}
+		}
+
+		private IEnumerable<(string, CommandInfo)> GetCommandInfoTuple(MethodInfo method)
+		{
+			var handlerDelegate = (CommandInfo.HandlerDelegate)Delegate.CreateDelegate(typeof(CommandInfo.HandlerDelegate), this._host, method);
+
+			var command = handlerDelegate.Method.GetCustomAttribute<CommandAttribute>();
+			var aliases = handlerDelegate.Method.GetCustomAttribute<AliasesAttribute>();
+			var helpMessage = handlerDelegate.Method.GetCustomAttribute<HelpMessageAttribute>();
+			var doNotShowInHelp = handlerDelegate.Method.GetCustomAttribute<DoNotShowInHelpAttribute>();
+
+			var commandInfo = new CommandInfo(handlerDelegate)
+			{
+				HelpMessage = helpMessage?.HelpMessage ?? string.Empty,
+				ShowInHelp = doNotShowInHelp == null,
+			};
+
+			// Create list of tuples that will be filled with one tuple per alias, in addition to the base command tuple.
+			var commandInfoTuples = new List<(string, CommandInfo)> { (command!.Command, commandInfo) };
+			if (aliases != null)
+			{
+				// ReSharper disable once LoopCanBeConvertedToQuery
+				for (var i = 0; i < aliases.Aliases.Length; i++)
+				{
+					commandInfoTuples.Add((aliases.Aliases[i], commandInfo));
+				}
+			}
+
+			return commandInfoTuples;
+		}
+
+		public void Dispose()
+		{
+			RemoveCommandHandlers();
+			GC.SuppressFinalize(this);
+		}
+	}
+}

+ 14 - 0
Structs/MobHuntStruct.cs

@@ -0,0 +1,14 @@
+using System;
+using System.Runtime.InteropServices;
+
+namespace HuntBuddy.Structs
+{
+	// Signature to get struct address
+	// D1 48 8D 0D ? ? ? ? 48 83 C4 20 5F E9 ? ? ? ?
+	[StructLayout(LayoutKind.Explicit, Size = 0x198)]
+	public unsafe struct MobHuntStruct
+	{
+		[FieldOffset(0x1A)] public fixed byte BillOffset[18];
+		[FieldOffset(0x2C)] public fixed int CurrentKills[5 * 18];
+	}
+}