Jelajahi Sumber

Merge in the new dalamud window system code

Migrate interface to the Dalamud windowing system.

* Moved service out of the Plugin class into their own class.
* Introduce static instance for Plugin class.
* Removed current interface implementation.
* Introduced a new interface implementation based on Dalamud.WindowSystem
Lilith 2 tahun lalu
induk
melakukan
f109c518f4

+ 1 - 1
HuntBuddy/Configuration.cs

@@ -21,7 +21,7 @@ namespace HuntBuddy
 
 		public void Save()
 		{
-			Plugin.PluginInterface.SavePluginConfig(this);
+			Service.PluginInterface.SavePluginConfig(this);
 		}
 	}
 }

+ 0 - 423
HuntBuddy/Interface.cs

@@ -1,423 +0,0 @@
-using System.Linq;
-using System.Numerics;
-using System.Threading.Tasks;
-using Dalamud.Interface;
-using ImGuiNET;
-
-namespace HuntBuddy
-{
-	public class Interface
-	{
-		private readonly Plugin plugin;
-
-		public bool DrawInterface;
-		private bool drawConfigurationInterface;
-
-		public Interface(Plugin plugin)
-		{
-			this.plugin = plugin;
-		}
-
-		public unsafe bool Draw()
-		{
-			var draw = true;
-
-			ImGui.SetNextWindowSize(new Vector2(400 * ImGui.GetIO().FontGlobalScale, 500), ImGuiCond.Once);
-
-			var windowFlags = ImGuiWindowFlags.NoDocking;
-
-			if (this.plugin.Configuration.LockWindowPositions)
-			{
-				windowFlags |= ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoMove;
-			}
-
-			if (!ImGui.Begin($"{this.plugin.Name}", ref draw, windowFlags))
-			{
-				return draw;
-			}
-
-			if (!this.plugin.MobHuntEntriesReady)
-			{
-				ImGui.Text("Reloading data ...");
-				ImGui.End();
-				return draw;
-			}
-
-			if (Interface.IconButton(FontAwesomeIcon.Redo, "Reload"))
-			{
-				ImGui.End();
-				this.plugin.MobHuntEntriesReady = false;
-				Task.Run(() => this.plugin.ReloadData());
-				return draw;
-			}
-
-			if (ImGui.IsItemHovered())
-			{
-				ImGui.BeginTooltip();
-				ImGui.Text("Click this button to reload daily hunt data");
-				ImGui.EndTooltip();
-			}
-
-			ImGui.SameLine();
-
-			if (Interface.IconButton(FontAwesomeIcon.Cog, "Config"))
-			{
-				this.drawConfigurationInterface = !this.drawConfigurationInterface;
-			}
-
-			foreach (var expansionEntry in this.plugin.MobHuntEntries.Where(
-				         expansionEntry =>
-					         ImGui.TreeNode(expansionEntry.Key)))
-			{
-				foreach (var entry in expansionEntry.Value.Where(
-					         entry =>
-					         {
-						         var treeOpen = ImGui.TreeNodeEx(entry.Key.Value, ImGuiTreeNodeFlags.AllowItemOverlap);
-						         ImGui.SameLine();
-						         var killedCount = entry.Value.Count(
-							         x =>
-								         this.plugin.MobHuntStruct->CurrentKills[x.CurrentKillsOffset] ==
-								         x.NeededKills);
-
-						         if (killedCount != entry.Value.Count)
-						         {
-							         ImGui.Text($"({killedCount}/{entry.Value.Count})");
-						         }
-						         else
-						         {
-							         ImGui.TextColored(
-								         new Vector4(0f, 1f, 0f, 1f),
-								         $"({killedCount}/{entry.Value.Count})");
-						         }
-
-						         return treeOpen;
-					         }))
-				{
-					//ImGui.Indent();
-					foreach (var mobHuntEntry in entry.Value)
-					{
-						if (Location.Database.ContainsKey(mobHuntEntry.MobHuntId))
-						{
-							if (Interface.IconButton(FontAwesomeIcon.MapMarkerAlt, $"pin##{mobHuntEntry.MobHuntId}"))
-							{
-								Location.CreateMapMarker(
-									mobHuntEntry.TerritoryType,
-									mobHuntEntry.MapId,
-									mobHuntEntry.MobHuntId,
-									mobHuntEntry.Name,
-									Location.OpenType.None);
-							}
-
-							if (ImGui.IsItemHovered())
-							{
-								ImGui.BeginTooltip();
-								ImGui.Text("Place marker on the map");
-								ImGui.EndTooltip();
-							}
-
-							ImGui.SameLine();
-
-							if (Interface.IconButton(FontAwesomeIcon.MapMarkedAlt, $"open##{mobHuntEntry.MobHuntId}"))
-							{
-								var includeArea = this.plugin.Configuration.IncludeAreaOnMap;
-								if (ImGui.IsKeyDown(ImGuiKey.ModShift))
-								{
-									includeArea = !includeArea;
-								}
-								Location.CreateMapMarker(
-									mobHuntEntry.TerritoryType,
-									mobHuntEntry.MapId,
-									mobHuntEntry.MobHuntId,
-									mobHuntEntry.Name,
-									includeArea ? Location.OpenType.ShowOpen : Location.OpenType.MarkerOpen);
-							}
-
-							if (ImGui.IsItemHovered())
-							{
-								var color = ImGui.IsKeyDown(ImGuiKey.ModShift) ? new Vector4(0f, 0.7f, 0f, 1f) : new Vector4(0.7f, 0.7f, 0.7f, 1f);
-								ImGui.BeginTooltip();
-								if (this.plugin.Configuration.IncludeAreaOnMap)
-								{
-									ImGui.Text("Show hunt area on the map");
-									ImGui.TextColored(
-										color,
-										"Hold [SHIFT] to show the location only");
-								}
-								else
-								{
-									ImGui.Text("Show hunt location on the map");
-									ImGui.TextColored(
-										color,
-										"Hold [SHIFT] to include the area");
-								}
-								ImGui.EndTooltip();
-							}
-
-							ImGui.SameLine();
-
-							if (Plugin.TeleportConsumer?.IsAvailable == true)
-							{
-								if (Interface.IconButton(FontAwesomeIcon.StreetView, $"t##{mobHuntEntry.MobHuntId}"))
-								{
-									Location.TeleportToNearestAetheryte(
-										mobHuntEntry.TerritoryType,
-										mobHuntEntry.MapId,
-										mobHuntEntry.MobHuntId);
-								}
-
-								if (ImGui.IsItemHovered())
-								{
-									ImGui.BeginTooltip();
-									ImGui.Text("Teleport to nearest aetheryte");
-									ImGui.EndTooltip();
-								}
-
-								ImGui.SameLine();
-							}
-						}
-
-						var currentKills = this.plugin.MobHuntStruct->CurrentKills[mobHuntEntry.CurrentKillsOffset];
-						ImGui.Text(mobHuntEntry.Name);
-						if (ImGui.IsItemHovered())
-						{
-							ImGui.PushStyleColor(ImGuiCol.PopupBg, Vector4.Zero);
-							ImGui.BeginTooltip();
-							this.DrawHuntIcon(mobHuntEntry);
-							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 (this.drawConfigurationInterface)
-			{
-				this.DrawConfiguration();
-			}
-
-			return draw;
-		}
-
-		public unsafe void DrawLocalHunts()
-		{
-			if (!this.plugin.Configuration.ShowLocalHunts ||
-			    this.plugin.CurrentAreaMobHuntEntries.IsEmpty ||
-			    this.plugin.CurrentAreaMobHuntEntries.Count(
-				    x =>
-					    this.plugin.MobHuntStruct->CurrentKills[x.CurrentKillsOffset] == x.NeededKills) ==
-			    this.plugin.CurrentAreaMobHuntEntries.Count)
-			{
-				return;
-			}
-
-			ImGui.SetNextWindowSize(Vector2.Zero, ImGuiCond.Always);
-
-			var windowFlags = ImGuiWindowFlags.NoNavInputs | ImGuiWindowFlags.NoDocking;
-
-			if (this.plugin.Configuration.HideLocalHuntBackground)
-			{
-				windowFlags |= ImGuiWindowFlags.NoBackground;
-			}
-
-			if (this.plugin.Configuration.LockWindowPositions)
-			{
-				windowFlags |= ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoMove;
-			}
-
-			if (!ImGui.Begin("Hunts in current area", windowFlags))
-			{
-				return;
-			}
-
-			foreach (var mobHuntEntry in this.plugin.CurrentAreaMobHuntEntries)
-			{
-				var currentKills = this.plugin.MobHuntStruct->CurrentKills[mobHuntEntry.CurrentKillsOffset];
-
-				if (this.plugin.Configuration.HideCompletedHunts && currentKills == mobHuntEntry.NeededKills)
-				{
-					continue;
-				}
-
-				if (Location.Database.ContainsKey(mobHuntEntry.MobHuntId))
-				{
-					if (Interface.IconButton(FontAwesomeIcon.MapMarkerAlt, $"pin##{mobHuntEntry.MobHuntId}"))
-					{
-						Location.CreateMapMarker(
-							mobHuntEntry.TerritoryType,
-							mobHuntEntry.MapId,
-							mobHuntEntry.MobHuntId,
-							mobHuntEntry.Name,
-							Location.OpenType.None);
-					}
-
-					if (ImGui.IsItemHovered())
-					{
-						ImGui.BeginTooltip();
-						ImGui.Text("Place marker on the map");
-						ImGui.EndTooltip();
-					}
-
-					ImGui.SameLine();
-
-					if (Interface.IconButton(FontAwesomeIcon.MapMarkedAlt, $"open##{mobHuntEntry.MobHuntId}"))
-					{
-						var includeArea = this.plugin.Configuration.IncludeAreaOnMap;
-						if (ImGui.IsKeyDown(ImGuiKey.ModShift))
-						{
-							includeArea = !includeArea;
-						}
-						Location.CreateMapMarker(
-							mobHuntEntry.TerritoryType,
-							mobHuntEntry.MapId,
-							mobHuntEntry.MobHuntId,
-							mobHuntEntry.Name,
-							includeArea ? Location.OpenType.ShowOpen : Location.OpenType.MarkerOpen);
-					}
-
-					if (ImGui.IsItemHovered())
-					{
-						var color = ImGui.IsKeyDown(ImGuiKey.ModShift) ? new Vector4(0f, 0.7f, 0f, 1f) : new Vector4(0.7f, 0.7f, 0.7f, 1f);
-						ImGui.BeginTooltip();
-						if (this.plugin.Configuration.IncludeAreaOnMap)
-						{
-							ImGui.Text("Show hunt area on the map");
-							ImGui.TextColored(
-								color,
-								"Hold [SHIFT] to show the location only");
-						}
-						else
-						{
-							ImGui.Text("Show hunt location on the map");
-							ImGui.TextColored(
-								color,
-								"Hold [SHIFT] to include the area");
-						}
-						ImGui.EndTooltip();
-					}
-
-					ImGui.SameLine();
-				}
-
-				ImGui.Text($"{mobHuntEntry.Name} ({currentKills}/{mobHuntEntry.NeededKills})");
-
-				if (!this.plugin.Configuration.ShowLocalHuntIcons)
-				{
-					continue;
-				}
-
-				this.DrawHuntIcon(mobHuntEntry);
-			}
-
-			ImGui.End();
-		}
-
-		private void DrawConfiguration()
-		{
-			ImGui.SetNextWindowSize(Vector2.Zero, ImGuiCond.Always);
-
-			var windowFlags = ImGuiWindowFlags.NoDocking;
-
-			if (this.plugin.Configuration.LockWindowPositions)
-			{
-				windowFlags |= ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoMove;
-			}
-
-			if (!ImGui.Begin($"{this.plugin.Name} settings", windowFlags))
-			{
-				return;
-			}
-
-			var save = false;
-
-			save |= ImGui.Checkbox("Lock plugin window positions", ref this.plugin.Configuration.LockWindowPositions);
-			save |= ImGui.Checkbox("Include hunt area on map by default", ref this.plugin.Configuration.IncludeAreaOnMap);
-			save |= ImGui.Checkbox("Show hunts in local area", ref this.plugin.Configuration.ShowLocalHunts);
-			save |= ImGui.Checkbox(
-				"Show icons of hunts in local area",
-				ref this.plugin.Configuration.ShowLocalHuntIcons);
-			save |= ImGui.Checkbox(
-				"Hide background of local hunts window",
-				ref this.plugin.Configuration.HideLocalHuntBackground);
-			save |= ImGui.Checkbox(
-				"Hide completed targets in local hunts window",
-				ref this.plugin.Configuration.HideCompletedHunts);
-			save |= ImGui.SliderFloat("Hunt icon scale", ref this.plugin.Configuration.IconScale, 0.2f, 2f, "%.2f");
-			if (ImGui.ColorEdit4("Hunt icon background colour", ref this.plugin.Configuration.IconBackgroundColour))
-			{
-				this.plugin.Configuration.IconBackgroundColourU32 =
-					ImGui.ColorConvertFloat4ToU32(this.plugin.Configuration.IconBackgroundColour);
-				save = true;
-			}
-
-			if (save)
-			{
-				this.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;
-		}
-
-		private void DrawHuntIcon(MobHuntEntry mobHuntEntry)
-		{
-			var cursorPos = ImGui.GetCursorScreenPos();
-			var imageSize = mobHuntEntry.ExpansionId < 3 ? new Vector2(192f, 128f) : new Vector2(210f);
-			imageSize *= ImGui.GetIO().FontGlobalScale * this.plugin.Configuration.IconScale;
-
-			ImGui.InvisibleButton("canvas", imageSize);
-
-			var drawList = ImGui.GetWindowDrawList();
-			if (mobHuntEntry is { ExpansionId: 4, IsEliteMark: false }) // Endwalker uses circle for non elite mobs
-			{
-				drawList.AddCircleFilled(
-					cursorPos + (imageSize / 2f),
-					imageSize.X / 2f,
-					this.plugin.Configuration.IconBackgroundColourU32);
-			}
-			else
-			{
-				drawList.AddRectFilled(
-					cursorPos,
-					cursorPos + imageSize,
-					this.plugin.Configuration.IconBackgroundColourU32);
-			}
-
-			drawList.AddImage(mobHuntEntry.Icon.ImGuiHandle, cursorPos, cursorPos + imageSize);
-		}
-	}
-}

+ 4 - 5
HuntBuddy/Ipc/TeleportConsumer.cs

@@ -1,5 +1,4 @@
 using System;
-using Dalamud.Logging;
 using Dalamud.Plugin.Ipc;
 
 namespace HuntBuddy.Ipc
@@ -40,12 +39,12 @@ namespace HuntBuddy.Ipc
 		{
 			try
 			{
-				this.consumerTeleport = Plugin.PluginInterface.GetIpcSubscriber<uint, byte, bool>("Teleport");
-				this.consumerMessageSetting = Plugin.PluginInterface.GetIpcSubscriber<bool>("Teleport.ChatMessage");
+				this.consumerTeleport = Service.PluginInterface.GetIpcSubscriber<uint, byte, bool>("Teleport");
+				this.consumerMessageSetting = Service.PluginInterface.GetIpcSubscriber<bool>("Teleport.ChatMessage");
 			}
 			catch (Exception ex)
 			{
-				Plugin.PluginLog.Debug($"Failed to subscribe to Teleporter\nReason: {ex}");
+				Service.PluginLog.Debug($"Failed to subscribe to Teleporter\nReason: {ex}");
 			}
 		}
 
@@ -59,7 +58,7 @@ namespace HuntBuddy.Ipc
 			}
 			catch
 			{
-				Plugin.Chat.PrintError("Teleporter plugin is not responding");
+				Service.Chat.PrintError("Teleporter plugin is not responding");
 				return false;
 			}
 		}

+ 4 - 4
HuntBuddy/Location.cs

@@ -493,7 +493,7 @@ namespace HuntBuddy
 		
 		private static (int X, int Y) MapToWorldCoordinates(Vector2 pos, uint mapId)
 		{
-			var scale = Plugin.DataManager.GetExcelSheet<Map>()?.GetRow(mapId)?.SizeFactor ?? 100;
+			var scale = Service.DataManager.GetExcelSheet<Map>()?.GetRow(mapId)?.SizeFactor ?? 100;
 			var num = scale / 100f;
 			var x = (float)(((pos.X - 1.0) * num / 41.0 * 2048.0) - 1024.0) / num * 1000f;
 			var y = (float)(((pos.Y - 1.0) * num / 41.0 * 2048.0) - 1024.0) / num * 1000f;
@@ -519,14 +519,14 @@ namespace HuntBuddy
 
 		public static void TeleportToNearestAetheryte(uint territoryType, uint mapId, uint mobHuntId)
 		{
-			var mapRow = Plugin.DataManager.Excel.GetSheet<Map>()?.GetRow(mapId);
+			var mapRow = Service.DataManager.Excel.GetSheet<Map>()?.GetRow(mapId);
 
 			if (mapRow == null)
 			{
 				return;
 			}
 
-			var nearestAetheryteId = Plugin.DataManager.Excel.GetSheet<MapMarker>()
+			var nearestAetheryteId = Service.DataManager.Excel.GetSheet<MapMarker>()
 				?.Where(x => x.DataType == 3 && x.RowId == mapRow.MapMarkerRange)
 				.Select(
 					x => new
@@ -542,7 +542,7 @@ namespace HuntBuddy
 			var nearestAetheryte =
 				territoryType == 399 // Support the unique case of aetheryte not being in the same map
 					? mapRow.TerritoryType?.Value?.Aetheryte.Value
-					: Plugin.DataManager.Excel.GetSheet<Aetheryte>()?.FirstOrDefault(
+					: Service.DataManager.Excel.GetSheet<Aetheryte>()?.FirstOrDefault(
 						x =>
 							x.IsAetheryte && x.Territory.Row == territoryType && x.RowId == nearestAetheryteId);
 

+ 59 - 61
HuntBuddy/Plugin.cs

@@ -5,10 +5,8 @@ using System.Collections.Generic;
 using System.Globalization;
 using System.Linq;
 using System.Threading.Tasks;
-using Dalamud.Game;
 using Dalamud.Interface.Internal;
-using Dalamud.IoC;
-using Dalamud.Logging;
+using Dalamud.Interface.Windowing;
 using Dalamud.Plugin;
 using Dalamud.Plugin.Services;
 using Dalamud.Utility;
@@ -16,6 +14,7 @@ using Lumina.Excel.GeneratedSheets;
 using HuntBuddy.Attributes;
 using HuntBuddy.Ipc;
 using HuntBuddy.Structs;
+using HuntBuddy.Windows;
 using ImGuiNET;
 using Lumina.Extensions;
 
@@ -25,26 +24,7 @@ namespace HuntBuddy
 	{
 		public string Name => "Hunt Buddy";
 
-		[PluginService] public static DalamudPluginInterface PluginInterface { get; set; } = null!;
-
-		[PluginService] public static ICommandManager Commands { get; set; } = null!;
-
-		[PluginService] public static IChatGui Chat { get; set; } = null!;
-
-		[PluginService] public static IDataManager DataManager { get; set; } = null!;
-
-		[PluginService] public static ISigScanner SigScanner { get; set; } = null!;
-
-		[PluginService] public static IGameGui GameGui { get; set; } = null!;
-
-		[PluginService] public static IClientState ClientState { get; set; } = null!;
-
-		[PluginService] public static IFramework Framework { get; set; } = null!;
-
-		[PluginService] public static IPluginLog PluginLog { get; set; } = null!;
-
 		private readonly PluginCommandManager<Plugin> commandManager;
-		private readonly Interface pluginInterface;
 		private ObtainedBillEnum lastState;
 		// Dictionary<string ExpansionName, Dictionary<KeyValuePair<uint MobTerritoryType, string MobTerritoryName>, List<MobHuntEntry MobsInZone>>>
 		public readonly Dictionary<string, Dictionary<KeyValuePair<uint, string>, List<MobHuntEntry>>> MobHuntEntries;
@@ -53,30 +33,47 @@ namespace HuntBuddy
 		public readonly unsafe MobHuntStruct* MobHuntStruct;
 		public readonly Configuration Configuration;
 		public static TeleportConsumer? TeleportConsumer;
+		
+		private WindowSystem WindowSystem { get; }
+		
+		private MainWindow MainWindow { get; }
+		private ConfigurationWindow ConfigurationWindow { get; }
 
-		public Plugin()
+		public static Plugin Instance { get; internal set; } = null!;
+
+		public Plugin(DalamudPluginInterface pluginInterface)
 		{
-			this.commandManager = new PluginCommandManager<Plugin>(this, Commands);
-			this.pluginInterface = new Interface(this);
+			Instance = this;
+
+			pluginInterface.Create<Service>();
+
+			this.commandManager = new PluginCommandManager<Plugin>(this, Service.Commands);
 			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 = (Configuration)(Service.PluginInterface.GetPluginConfig() ?? new Configuration());
 			this.Configuration.IconBackgroundColourU32 =
 				ImGui.ColorConvertFloat4ToU32(this.Configuration.IconBackgroundColour);
 
 			unsafe
 			{
 				this.MobHuntStruct =
-					(MobHuntStruct*)SigScanner.GetStaticAddressFromSig(
+					(MobHuntStruct*)Service.SigScanner.GetStaticAddressFromSig(
 						"48 8D 0D ?? ?? ?? ?? 8B D8 0F B6 52");
 			}
+			
+			this.MainWindow = new MainWindow();
+			this.ConfigurationWindow = new ConfigurationWindow();
+			
+			this.WindowSystem = new WindowSystem("HuntBuddy");
+			this.WindowSystem.AddWindow(this.MainWindow);
+			this.WindowSystem.AddWindow(new LocalHuntsWindow());
+			this.WindowSystem.AddWindow(this.ConfigurationWindow);
 
 			Plugin.TeleportConsumer = new TeleportConsumer();
-			Plugin.ClientState.TerritoryChanged += this.ClientStateOnTerritoryChanged;
-			Plugin.PluginInterface.UiBuilder.Draw += this.DrawInterface;
-			Plugin.PluginInterface.UiBuilder.Draw += this.pluginInterface.DrawLocalHunts;
-			Plugin.PluginInterface.UiBuilder.OpenConfigUi += this.OpenConfigUi;
-			Plugin.Framework.Update += this.FrameworkOnUpdate;
+			Service.ClientState.TerritoryChanged += this.ClientStateOnTerritoryChanged;
+			Service.PluginInterface.UiBuilder.Draw += this.WindowSystem.Draw;
+			Service.PluginInterface.UiBuilder.OpenConfigUi += this.OpenConfigUi;
+			Service.Framework.Update += this.FrameworkOnUpdate;
 		}
 
 		private unsafe void FrameworkOnUpdate(IFramework framework)
@@ -96,21 +93,21 @@ namespace HuntBuddy
 
 			foreach (var mobHuntEntry in this.MobHuntEntries.SelectMany(
 				         expansionEntry => expansionEntry.Value
-					         .Where(entry => entry.Key.Key == Plugin.ClientState.TerritoryType)
+					         .Where(entry => entry.Key.Key == Service.ClientState.TerritoryType)
 					         .SelectMany(entry => entry.Value)))
 			{
 				this.CurrentAreaMobHuntEntries.Add(mobHuntEntry);
 			}
 		}
 
-		private void OpenConfigUi()
+		private void DrawInterface()
 		{
-			this.pluginInterface.DrawInterface = !this.pluginInterface.DrawInterface;
+			this.MainWindow.Toggle();
 		}
 
-		private void DrawInterface()
+		public void OpenConfigUi()
 		{
-			this.pluginInterface.DrawInterface = this.pluginInterface.DrawInterface && this.pluginInterface.Draw();
+			this.ConfigurationWindow.Toggle();
 		}
 
 		private void Dispose(bool disposing)
@@ -121,11 +118,12 @@ namespace HuntBuddy
 			}
 
 			this.MobHuntEntriesReady = false;
-			Plugin.ClientState.TerritoryChanged -= this.ClientStateOnTerritoryChanged;
-			Plugin.Framework.Update -= this.FrameworkOnUpdate;
-			Plugin.PluginInterface.UiBuilder.Draw -= this.DrawInterface;
-			Plugin.PluginInterface.UiBuilder.Draw -= this.pluginInterface.DrawLocalHunts;
-			Plugin.PluginInterface.UiBuilder.OpenConfigUi -= this.OpenConfigUi;
+			Service.ClientState.TerritoryChanged -= this.ClientStateOnTerritoryChanged;
+			Service.Framework.Update -= this.FrameworkOnUpdate;
+			Service.PluginInterface.UiBuilder.Draw -= this.WindowSystem.Draw;
+			Service.PluginInterface.UiBuilder.OpenConfigUi -= this.OpenConfigUi;
+			
+			this.WindowSystem.RemoveAllWindows();
 
 			this.commandManager.Dispose();
 		}
@@ -150,8 +148,8 @@ namespace HuntBuddy
 						{
 							var filterPredicate = (MobHuntEntry entry) => entry.IsEliteMark || this.MobHuntStruct->CurrentKills[entry.CurrentKillsOffset] < entry.NeededKills;
 							var openType = Location.OpenType.None;
-							var playerLocation = Plugin.ClientState.LocalPlayer!.Position;
-							var map = Plugin.DataManager.GetExcelSheet<TerritoryType>()!.GetRow(Plugin.ClientState.TerritoryType)!.Map!.Value!;
+							var playerLocation = Service.ClientState.LocalPlayer!.Position;
+							var map = Service.DataManager.GetExcelSheet<TerritoryType>()!.GetRow(Service.ClientState.TerritoryType)!.Map!.Value!;
 							var playerVec2 = MapUtil.WorldToMap(new Vector2(playerLocation.X, playerLocation.Z), map);
 							var chosen = this.CurrentAreaMobHuntEntries
 								.Where(filterPredicate)
@@ -159,10 +157,10 @@ namespace HuntBuddy
 								.FirstOrDefault();
 							if (chosen == null)
 							{
-								PluginLog.Information("No marks in current zone, looking in current expansion");
+								Service.PluginLog.Information("No marks in current zone, looking in current expansion");
 								openType = this.Configuration.IncludeAreaOnMap ? Location.OpenType.ShowOpen : Location.OpenType.MarkerOpen;
-								var expansion = Plugin.DataManager.Excel.GetSheet<TerritoryType>()!.GetRow(Plugin.ClientState.TerritoryType)!.ExVersion.Value!.Name;
-								PluginLog.Information($"Player is in a zone from {expansion}; known expansions are {string.Join(", ", this.MobHuntEntries.Keys)}");
+								var expansion = Service.DataManager.Excel.GetSheet<TerritoryType>()!.GetRow(Service.ClientState.TerritoryType)!.ExVersion.Value!.Name;
+								Service.PluginLog.Information($"Player is in a zone from {expansion}; known expansions are {string.Join(", ", this.MobHuntEntries.Keys)}");
 								var candidates = this.MobHuntEntries.ContainsKey(expansion)
 									? this.MobHuntEntries[expansion]
 										.Values
@@ -173,7 +171,7 @@ namespace HuntBuddy
 								// if we didn't find any candidates, we try a different method to fill it
 								if (candidates.Count == 0)
 								{
-									PluginLog.Information("Nothing available in current expansion, looking globally");
+									Service.PluginLog.Information("Nothing available in current expansion, looking globally");
 									candidates =
 										this.MobHuntEntries.Values
 											.SelectMany(dict => dict.Values)
@@ -186,7 +184,7 @@ namespace HuntBuddy
 								// but this block must ALWAYS run, regardless
 								if (candidates.Count >= 1)
 								{
-									PluginLog.Information($"Found {candidates.Count}");
+									Service.PluginLog.Information($"Found {candidates.Count}");
 									chosen = candidates[new Random().Next(candidates.Count)];
 								}
 							}
@@ -194,12 +192,12 @@ namespace HuntBuddy
 							{
 								if (chosen.IsEliteMark)
 								{
-									Chat.Print($"Hunting elite mark {chosen.Name} in {chosen.TerritoryName}");
+									Service.Chat.Print($"Hunting elite mark {chosen.Name} in {chosen.TerritoryName}");
 								}
 								else
 								{
 									var remaining = chosen.NeededKills - this.MobHuntStruct->CurrentKills[chosen.CurrentKillsOffset];
-									Chat.Print($"Hunting {remaining}x {chosen.Name} in {chosen.TerritoryName}");
+									Service.Chat.Print($"Hunting {remaining}x {chosen.Name} in {chosen.TerritoryName}");
 									Location.CreateMapMarker(
 										chosen.TerritoryType,
 										chosen.MapId,
@@ -210,29 +208,29 @@ namespace HuntBuddy
 							}
 							else
 							{
-								PluginLog.Information("Unable to find a hunt mark to target");
-								Chat.Print("Couldn't find any hunt marks. Either you have no bills, or this is a bug.");
+								Service.PluginLog.Information("Unable to find a hunt mark to target");
+								Service.Chat.Print("Couldn't find any hunt marks. Either you have no bills, or this is a bug.");
 							}
 						}
 						break;
 					case "ls":
 					case "list":
 						if (this.MobHuntEntries.Count < 1) {
-							Chat.Print("No hunt marks found. If this doesn't sound right, please use `/phb reload` and try again.");
+							Service.Chat.Print("No hunt marks found. If this doesn't sound right, please use `/phb reload` and try again.");
 							break;
 						}
 						foreach (string expac in this.MobHuntEntries.Keys) {
-							Chat.Print($"{expac}: {string.Join(", ", this.MobHuntEntries[expac].Values.SelectMany(e => e).OrderBy(s => s.Name).Select(m => m.Name))}");
+							Service.Chat.Print($"{expac}: {string.Join(", ", this.MobHuntEntries[expac].Values.SelectMany(e => e).OrderBy(s => s.Name).Select(m => m.Name))}");
 						}
 						break;
 					default:
-						this.OpenConfigUi();
+						this.DrawInterface();
 						break;
 				}
 			}
 			catch (Exception e)
 			{
-				PluginLog.Error("Error in command handler: " + e.ToString());
+				Service.PluginLog.Error("Error in command handler: " + e.ToString());
 			}
 		}
 
@@ -240,7 +238,7 @@ namespace HuntBuddy
 		{
 			this.MobHuntEntries.Clear();
 			var mobHuntList = new List<MobHuntEntry>();
-			var mobHuntOrderSheet = Plugin.DataManager.Excel.GetSheet<MobHuntOrder>()!;
+			var mobHuntOrderSheet = Service.DataManager.Excel.GetSheet<MobHuntOrder>()!;
 
 			foreach (var billNumber in Enum.GetValues<BillEnum>())
 			{
@@ -250,7 +248,7 @@ namespace HuntBuddy
 				}
 
 				var mobHuntOrderTypeRow =
-					Plugin.DataManager.Excel.GetSheet<MobHuntOrderType>()!.GetRow((uint)billNumber)!;
+					Service.DataManager.Excel.GetSheet<MobHuntOrderType>()!.GetRow((uint)billNumber)!;
 
 				var rowId = mobHuntOrderTypeRow.OrderStart.Value!.RowId +
 				            (uint)(this.MobHuntStruct->BillOffset[mobHuntOrderTypeRow.RowId] - 1);
@@ -325,10 +323,10 @@ namespace HuntBuddy
 
 		private static IDalamudTextureWrap LoadIcon(uint id)
 		{
-			var icon = Plugin.DataManager.GameData.GetHqIcon(id) ?? Plugin.DataManager.GameData.GetIcon(id)!;
+			var icon = Service.DataManager.GameData.GetHqIcon(id) ?? Service.DataManager.GameData.GetIcon(id)!;
 			var iconData = icon.GetRgbaImageData();
 
-			return Plugin.PluginInterface.UiBuilder.LoadImageRaw(iconData, icon.Header.Width, icon.Header.Height, 4);
+			return Service.PluginInterface.UiBuilder.LoadImageRaw(iconData, icon.Header.Width, icon.Header.Height, 4);
 		}
 
 		public void Dispose()

+ 27 - 0
HuntBuddy/Service.cs

@@ -0,0 +1,27 @@
+using Dalamud.Game;
+using Dalamud.IoC;
+using Dalamud.Plugin;
+using Dalamud.Plugin.Services;
+
+namespace HuntBuddy;
+
+public class Service
+{
+    [PluginService] public static DalamudPluginInterface PluginInterface { get; set; } = null!;
+
+    [PluginService] public static ICommandManager Commands { get; set; } = null!;
+
+    [PluginService] public static IChatGui Chat { get; set; } = null!;
+
+    [PluginService] public static IDataManager DataManager { get; set; } = null!;
+
+    [PluginService] public static ISigScanner SigScanner { get; set; } = null!;
+
+    [PluginService] public static IGameGui GameGui { get; set; } = null!;
+
+    [PluginService] public static IClientState ClientState { get; set; } = null!;
+
+    [PluginService] public static IFramework Framework { get; set; } = null!;
+
+    [PluginService] public static IPluginLog PluginLog { get; set; } = null!;
+}

+ 73 - 0
HuntBuddy/Utils/InterfaceUtil.cs

@@ -0,0 +1,73 @@
+using System.Numerics;
+using Dalamud.Interface;
+using ImGuiNET;
+
+namespace HuntBuddy.Utils;
+
+/// <summary>
+/// Interface utilities class.
+/// </summary>
+public static class InterfaceUtil
+{
+    /// <summary>
+    /// Draws hunt icons from game images.
+    /// </summary>
+    /// <param name="mobHuntEntry"><see cref="MobHuntEntry"/> containing relevant information.</param>
+    public static void DrawHuntIcon(MobHuntEntry mobHuntEntry)
+    {
+        var cursorPos = ImGui.GetCursorScreenPos();
+        var imageSize = mobHuntEntry.ExpansionId < 3 ? new Vector2(192f, 128f) : new Vector2(210f);
+        imageSize *= ImGui.GetIO().FontGlobalScale * Plugin.Instance.Configuration.IconScale;
+
+        ImGui.InvisibleButton("canvas", imageSize);
+
+        var drawList = ImGui.GetWindowDrawList();
+        if (mobHuntEntry is { ExpansionId: 4, IsEliteMark: false }) // Endwalker uses circle for non elite mobs
+        {
+            drawList.AddCircleFilled(
+                cursorPos + (imageSize / 2f),
+                imageSize.X / 2f,
+                Plugin.Instance.Configuration.IconBackgroundColourU32);
+        }
+        else
+        {
+            drawList.AddRectFilled(
+                cursorPos,
+                cursorPos + imageSize,
+                Plugin.Instance.Configuration.IconBackgroundColourU32);
+        }
+
+        drawList.AddImage(mobHuntEntry.Icon.ImGuiHandle, cursorPos, cursorPos + imageSize);
+    }
+
+    /// <summary>
+    /// Renders a button with an icon.
+    /// </summary>
+    /// <param name="icon">Desired <see cref="FontAwesomeIcon"/> to be rendered.</param>
+    /// <param name="id">Button ID.</param>
+    /// <returns>True if pressed.</returns>
+    public static bool IconButton(FontAwesomeIcon icon, string? id)
+    {
+        ImGui.PushFont(UiBuilder.IconFont);
+
+        var text = icon.ToIconString();
+
+        if (id != null)
+        {
+            text += $"##{id}";
+        }
+
+        var result = ImGui.Button(text);
+
+        ImGui.PopFont();
+
+        return result;
+    }
+
+    /// <summary>
+    /// Renders a button with an icon.
+    /// </summary>
+    /// <param name="icon">Desired <see cref="FontAwesomeIcon"/> to be rendered.</param>
+    /// <returns>True if pressed.</returns>
+    public static bool IconButton(FontAwesomeIcon icon) => IconButton(icon, null);
+}

+ 58 - 0
HuntBuddy/Windows/ConfigurationWindow.cs

@@ -0,0 +1,58 @@
+using System.Numerics;
+using Dalamud.Interface.Windowing;
+using ImGuiNET;
+
+namespace HuntBuddy.Windows;
+
+/// <summary>
+/// Configuration window.
+/// </summary>
+public class ConfigurationWindow : Window
+{
+    public ConfigurationWindow() : base(
+        $"{Plugin.Instance.Name} configuration",
+        ImGuiWindowFlags.NoDocking,
+        true)
+    {
+        this.Size = Vector2.Zero;
+        this.SizeCondition = ImGuiCond.Always;
+    }
+
+    public override void PreOpenCheck()
+    {
+        if (Plugin.Instance.Configuration.LockWindowPositions)
+        {
+            this.Flags |= ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoMove;
+        }
+    }
+
+    public override void Draw()
+    {
+        var save = false;
+
+        save |= ImGui.Checkbox("Lock plugin window positions", ref Plugin.Instance.Configuration.LockWindowPositions);
+        save |= ImGui.Checkbox("Include hunt area on map by default", ref Plugin.Instance.Configuration.IncludeAreaOnMap);
+        save |= ImGui.Checkbox("Show hunts in local area", ref Plugin.Instance.Configuration.ShowLocalHunts);
+        save |= ImGui.Checkbox(
+            "Show icons of hunts in local area",
+            ref Plugin.Instance.Configuration.ShowLocalHuntIcons);
+        save |= ImGui.Checkbox(
+            "Hide background of local hunts window",
+            ref Plugin.Instance.Configuration.HideLocalHuntBackground);
+        save |= ImGui.Checkbox(
+            "Hide completed targets in local hunts window",
+            ref Plugin.Instance.Configuration.HideCompletedHunts);
+        save |= ImGui.SliderFloat("Hunt icon scale", ref Plugin.Instance.Configuration.IconScale, 0.2f, 2f, "%.2f");
+        if (ImGui.ColorEdit4("Hunt icon background colour", ref Plugin.Instance.Configuration.IconBackgroundColour))
+        {
+            Plugin.Instance.Configuration.IconBackgroundColourU32 =
+                ImGui.ColorConvertFloat4ToU32(Plugin.Instance.Configuration.IconBackgroundColour);
+            save = true;
+        }
+
+        if (save)
+        {
+            Plugin.Instance.Configuration.Save();
+        }
+    }
+}

+ 131 - 0
HuntBuddy/Windows/LocalHuntsWindow.cs

@@ -0,0 +1,131 @@
+using System.Linq;
+using System.Numerics;
+using Dalamud.Interface;
+using Dalamud.Interface.Windowing;
+using HuntBuddy.Utils;
+using ImGuiNET;
+
+namespace HuntBuddy.Windows;
+
+/// <summary>
+/// Local hunts window.
+/// </summary>
+public class LocalHuntsWindow : Window
+{
+    public LocalHuntsWindow() : base(
+        $"{Plugin.Instance.Name}",
+        ImGuiWindowFlags.NoNavInputs | ImGuiWindowFlags.NoDocking,
+        true)
+    {
+        this.Size = Vector2.Zero;
+        this.SizeCondition = ImGuiCond.Always;
+
+        this.IsOpen = true;
+    }
+
+    public override void PreOpenCheck()
+    {
+        if (Plugin.Instance.Configuration.HideLocalHuntBackground)
+        {
+            this.Flags |= ImGuiWindowFlags.NoBackground;
+        }
+
+        if (Plugin.Instance.Configuration.LockWindowPositions)
+        {
+            this.Flags |= ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoMove;
+        }
+    }
+
+    public override unsafe bool DrawConditions() =>
+        Plugin.Instance.Configuration.ShowLocalHunts &&
+        !Plugin.Instance.CurrentAreaMobHuntEntries.IsEmpty &&
+        Plugin.Instance.CurrentAreaMobHuntEntries.Count(
+            x => Plugin.Instance.MobHuntStruct->CurrentKills[x.CurrentKillsOffset] == x.NeededKills) !=
+        Plugin.Instance.CurrentAreaMobHuntEntries.Count;
+
+    public override unsafe void Draw()
+    {
+        foreach (var mobHuntEntry in Plugin.Instance.CurrentAreaMobHuntEntries)
+        {
+            var currentKills = Plugin.Instance.MobHuntStruct->CurrentKills[mobHuntEntry.CurrentKillsOffset];
+
+            if (Plugin.Instance.Configuration.HideCompletedHunts && currentKills == mobHuntEntry.NeededKills)
+            {
+                continue;
+            }
+
+            if (Location.Database.ContainsKey(mobHuntEntry.MobHuntId))
+            {
+                if (InterfaceUtil.IconButton(FontAwesomeIcon.MapMarkerAlt, $"pin##{mobHuntEntry.MobHuntId}"))
+                {
+                    Location.CreateMapMarker(
+                        mobHuntEntry.TerritoryType,
+                        mobHuntEntry.MapId,
+                        mobHuntEntry.MobHuntId,
+                        mobHuntEntry.Name,
+                        Location.OpenType.None);
+                }
+
+                if (ImGui.IsItemHovered())
+                {
+                    ImGui.BeginTooltip();
+                    ImGui.Text("Place marker on the map");
+                    ImGui.EndTooltip();
+                }
+
+                ImGui.SameLine();
+
+                if (InterfaceUtil.IconButton(FontAwesomeIcon.MapMarkedAlt, $"open##{mobHuntEntry.MobHuntId}"))
+                {
+                    var includeArea = Plugin.Instance.Configuration.IncludeAreaOnMap;
+                    if (ImGui.IsKeyDown(ImGuiKey.ModShift))
+                    {
+                        includeArea = !includeArea;
+                    }
+
+                    Location.CreateMapMarker(
+                        mobHuntEntry.TerritoryType,
+                        mobHuntEntry.MapId,
+                        mobHuntEntry.MobHuntId,
+                        mobHuntEntry.Name,
+                        includeArea ? Location.OpenType.ShowOpen : Location.OpenType.MarkerOpen);
+                }
+
+                if (ImGui.IsItemHovered())
+                {
+                    var color = ImGui.IsKeyDown(ImGuiKey.ModShift)
+                        ? new Vector4(0f, 0.7f, 0f, 1f)
+                        : new Vector4(0.7f, 0.7f, 0.7f, 1f);
+                    ImGui.BeginTooltip();
+                    if (Plugin.Instance.Configuration.IncludeAreaOnMap)
+                    {
+                        ImGui.Text("Show hunt area on the map");
+                        ImGui.TextColored(
+                            color,
+                            "Hold [SHIFT] to show the location only");
+                    }
+                    else
+                    {
+                        ImGui.Text("Show hunt location on the map");
+                        ImGui.TextColored(
+                            color,
+                            "Hold [SHIFT] to include the area");
+                    }
+
+                    ImGui.EndTooltip();
+                }
+
+                ImGui.SameLine();
+            }
+
+            ImGui.Text($"{mobHuntEntry.Name} ({currentKills}/{mobHuntEntry.NeededKills})");
+
+            if (!Plugin.Instance.Configuration.ShowLocalHuntIcons)
+            {
+                continue;
+            }
+
+            InterfaceUtil.DrawHuntIcon(mobHuntEntry);
+        }
+    }
+}

+ 210 - 0
HuntBuddy/Windows/MainWindow.cs

@@ -0,0 +1,210 @@
+using System.Linq;
+using System.Numerics;
+using System.Threading.Tasks;
+using Dalamud.Interface;
+using Dalamud.Interface.Windowing;
+using ImGuiNET;
+using HuntBuddy.Utils;
+
+namespace HuntBuddy.Windows;
+
+/// <summary>
+/// Main plugin window.
+/// </summary>
+public class MainWindow : Window
+{
+    public MainWindow() : base(
+        $"{Plugin.Instance.Name}",
+        ImGuiWindowFlags.NoDocking,
+        true)
+    {
+        this.Size = new Vector2(400 * ImGui.GetIO().FontGlobalScale, 500);
+        this.SizeCondition = ImGuiCond.Once;
+    }
+
+    public override void PreOpenCheck()
+    {
+        if (Plugin.Instance.Configuration.LockWindowPositions)
+        {
+            this.Flags |= ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoMove;
+        }
+    }
+
+    public override unsafe void Draw()
+    {
+        if (!Plugin.Instance.MobHuntEntriesReady)
+        {
+            ImGui.Text("Reloading data ...");
+            ImGui.End();
+            return;
+        }
+
+        if (InterfaceUtil.IconButton(FontAwesomeIcon.Redo, "Reload"))
+        {
+            ImGui.End();
+            Plugin.Instance.MobHuntEntriesReady = false;
+            Task.Run(Plugin.Instance.ReloadData);
+            return;
+        }
+
+        if (ImGui.IsItemHovered())
+        {
+            ImGui.BeginTooltip();
+            ImGui.Text("Click this button to reload daily hunt data");
+            ImGui.EndTooltip();
+        }
+
+        ImGui.SameLine();
+
+        if (InterfaceUtil.IconButton(FontAwesomeIcon.Cog, "Config"))
+        {
+            Plugin.Instance.OpenConfigUi();
+        }
+
+        foreach (var expansionEntry in Plugin.Instance.MobHuntEntries.Where(
+                     expansionEntry =>
+                         ImGui.TreeNode(expansionEntry.Key)))
+        {
+            foreach (var entry in expansionEntry.Value.Where(
+                         entry =>
+                         {
+                             var treeOpen = ImGui.TreeNodeEx(entry.Key.Value, ImGuiTreeNodeFlags.AllowItemOverlap);
+                             ImGui.SameLine();
+                             var killedCount = entry.Value.Count(
+                                 x =>
+                                     Plugin.Instance.MobHuntStruct->CurrentKills[x.CurrentKillsOffset] ==
+                                     x.NeededKills);
+
+                             if (killedCount != entry.Value.Count)
+                             {
+                                 ImGui.Text($"({killedCount}/{entry.Value.Count})");
+                             }
+                             else
+                             {
+                                 ImGui.TextColored(
+                                     new Vector4(0f, 1f, 0f, 1f),
+                                     $"({killedCount}/{entry.Value.Count})");
+                             }
+
+                             return treeOpen;
+                         }))
+            {
+                foreach (var mobHuntEntry in entry.Value)
+                {
+                    if (Location.Database.ContainsKey(mobHuntEntry.MobHuntId))
+                    {
+                        if (InterfaceUtil.IconButton(FontAwesomeIcon.MapMarkerAlt, $"pin##{mobHuntEntry.MobHuntId}"))
+                        {
+                            Location.CreateMapMarker(
+                                mobHuntEntry.TerritoryType,
+                                mobHuntEntry.MapId,
+                                mobHuntEntry.MobHuntId,
+                                mobHuntEntry.Name,
+                                Location.OpenType.None);
+                        }
+
+                        if (ImGui.IsItemHovered())
+                        {
+                            ImGui.BeginTooltip();
+                            ImGui.Text("Place marker on the map");
+                            ImGui.EndTooltip();
+                        }
+
+                        ImGui.SameLine();
+
+                        if (InterfaceUtil.IconButton(FontAwesomeIcon.MapMarkedAlt, $"open##{mobHuntEntry.MobHuntId}"))
+                        {
+                            var includeArea = Plugin.Instance.Configuration.IncludeAreaOnMap;
+                            if (ImGui.IsKeyDown(ImGuiKey.ModShift))
+                            {
+                                includeArea = !includeArea;
+                            }
+
+                            Location.CreateMapMarker(
+                                mobHuntEntry.TerritoryType,
+                                mobHuntEntry.MapId,
+                                mobHuntEntry.MobHuntId,
+                                mobHuntEntry.Name,
+                                includeArea ? Location.OpenType.ShowOpen : Location.OpenType.MarkerOpen);
+                        }
+
+                        if (ImGui.IsItemHovered())
+                        {
+                            var color = ImGui.IsKeyDown(ImGuiKey.ModShift)
+                                ? new Vector4(0f, 0.7f, 0f, 1f)
+                                : new Vector4(0.7f, 0.7f, 0.7f, 1f);
+                            ImGui.BeginTooltip();
+                            if (Plugin.Instance.Configuration.IncludeAreaOnMap)
+                            {
+                                ImGui.Text("Show hunt area on the map");
+                                ImGui.TextColored(
+                                    color,
+                                    "Hold [SHIFT] to show the location only");
+                            }
+                            else
+                            {
+                                ImGui.Text("Show hunt location on the map");
+                                ImGui.TextColored(
+                                    color,
+                                    "Hold [SHIFT] to include the area");
+                            }
+
+                            ImGui.EndTooltip();
+                        }
+
+                        ImGui.SameLine();
+
+                        if (Plugin.TeleportConsumer?.IsAvailable == true)
+                        {
+                            if (InterfaceUtil.IconButton(FontAwesomeIcon.StreetView, $"t##{mobHuntEntry.MobHuntId}"))
+                            {
+                                Location.TeleportToNearestAetheryte(
+                                    mobHuntEntry.TerritoryType,
+                                    mobHuntEntry.MapId,
+                                    mobHuntEntry.MobHuntId);
+                            }
+
+                            if (ImGui.IsItemHovered())
+                            {
+                                ImGui.BeginTooltip();
+                                ImGui.Text("Teleport to nearest aetheryte");
+                                ImGui.EndTooltip();
+                            }
+
+                            ImGui.SameLine();
+                        }
+                    }
+
+                    var currentKills = Plugin.Instance.MobHuntStruct->CurrentKills[mobHuntEntry.CurrentKillsOffset];
+                    ImGui.Text(mobHuntEntry.Name);
+                    if (ImGui.IsItemHovered())
+                    {
+                        ImGui.PushStyleColor(ImGuiCol.PopupBg, Vector4.Zero);
+                        ImGui.BeginTooltip();
+                        InterfaceUtil.DrawHuntIcon(mobHuntEntry);
+                        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.TreePop();
+            }
+
+            ImGui.TreePop();
+        }
+
+        ImGui.End();
+    }
+}