using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using Dalamud.Bindings.ImGui; using Dalamud.Interface; using Dalamud.Interface.Utility.Raii; using Dalamud.Plugin; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game; using LLib.GameData; using Lumina.Excel; using Lumina.Excel.Sheets; using Questionable.Controller; using Questionable.Model; using Questionable.Model.Gathering; namespace Questionable.Windows.JournalComponents; internal sealed class GatheringJournalComponent { private enum RowType : byte { Expansion, Territory, GatheringPoint, Item } private readonly record struct GatheringCounts(string FormattedSupported, string FormattedCollected, bool IsSupportedComplete, bool IsCollectedComplete, bool IsSupportedEmpty, bool IsCollectedEmpty) { public static GatheringCounts Empty => new GatheringCounts("-", "-", IsSupportedComplete: false, IsCollectedComplete: false, IsSupportedEmpty: true, IsCollectedEmpty: true); private static readonly int PadWidth = 9999.ToString(CultureInfo.CurrentCulture).Length; public static GatheringCounts Create(int supported, int totalSupported, int collected, int totalCollected) { return new GatheringCounts(FormatPair(supported, totalSupported), FormatPair(collected, totalCollected), supported == totalSupported && totalSupported > 0, collected == totalCollected && totalCollected > 0, totalSupported == 0, totalCollected == 0); } private static string FormatPair(int n, int d) { if (d != 0) { return n.ToString(CultureInfo.CurrentCulture).PadLeft(PadWidth) + " / " + d.ToString(CultureInfo.CurrentCulture).PadLeft(PadWidth); } return "-".PadLeft(PadWidth) + " / " + "-".PadLeft(PadWidth); } } private readonly record struct FlatRow(RowType Type, int Depth, int GroupIndex, string Label, GatheringCounts Counts, bool IsPointComplete, ushort ItemId, bool IsItemGathered); private delegate byte GetIsGatheringItemGatheredDelegate(ushort item); private sealed record ExpansionPoints(EExpansionVersion ExpansionVersion, List PointsByTerritories) { public int TotalItems { get; set; } public int TotalPoints { get; set; } public int CompletedItems { get; set; } public int CompletedPoints { get; set; } } private sealed record TerritoryPoints(ushort TerritoryType, string TerritoryName, List Points) { public int TotalItems { get; set; } public int TotalPoints => Points.Count; public int CompletedItems { get; set; } public int CompletedPoints { get; set; } public string ToFriendlyString() { if (string.IsNullOrEmpty(TerritoryName)) { return $"??? ({TerritoryType})"; } return TerritoryName; } } private sealed record DefaultGatheringPoint(GatheringPointId Id, EClassJob ClassJob, byte Level, List GatheringItemIds, EExpansionVersion Expansion, ushort TerritoryType, string? TerritoryName, string? PlaceName) { public int TotalItems { get; set; } public int CompletedItems { get; set; } public bool IsComplete { get; set; } } private sealed record FilteredExpansion(ExpansionPoints Expansion, List Territories); private sealed record FilteredTerritory(TerritoryPoints Territory, List GatheringPoints); private sealed record FilteredGatheringPoint(DefaultGatheringPoint Point, List GatheringItemIds); private readonly IDalamudPluginInterface _pluginInterface; private readonly UiUtils _uiUtils; private readonly GatheringPointRegistry _gatheringPointRegistry; private readonly Dictionary _gatheringItems; private readonly List _gatheringPointsByExpansion; private readonly List _gatheredItems = new List(); private List _filteredExpansions = new List(); private List _flatRows = new List(); private readonly HashSet<(RowType Type, int GroupIndex)> _expandedGroups = new HashSet<(RowType, int)>(); private string _searchText = string.Empty; private static bool IsGatheringItemGathered(uint item) { return QuestManager.IsGatheringItemGathered((ushort)item); } public GatheringJournalComponent(IDataManager dataManager, IDalamudPluginInterface pluginInterface, UiUtils uiUtils, IGameInteropProvider gameInteropProvider, GatheringPointRegistry gatheringPointRegistry) { GatheringJournalComponent gatheringJournalComponent = this; _pluginInterface = pluginInterface; _uiUtils = uiUtils; _gatheringPointRegistry = gatheringPointRegistry; Dictionary> routeToGatheringPoint = (from x in (from x in dataManager.GetExcelSheet() where x.GatheringPoint[0].RowId != 0 select x).SelectMany((GatheringLeveRoute x) => from y in x.GatheringPoint where y.RowId != 0 select new { RouteId = x.RowId, GatheringPointId = y.RowId }) group x by x.RouteId).ToDictionary(x => x.Key, x => x.Select(y => y.GatheringPointId).ToList()); ExcelSheet gatheringLeveSheet = dataManager.GetExcelSheet(); ExcelSheet territoryTypeSheet = dataManager.GetExcelSheet(); HashSet leveGatheringPoints = (from y in (from x in dataManager.GetExcelSheet() where x.RowId != 0 select gatheringLeveSheet.GetRowOrDefault(x.DataId.RowId) into x where x.HasValue select x).Cast().SelectMany((GatheringLeve x) => x.Route) where y.RowId != 0 select y).SelectMany((RowRef y) => routeToGatheringPoint[y.RowId]).Distinct().ToHashSet(); ExcelSheet itemSheet = dataManager.GetExcelSheet(); _gatheringItems = (from x in dataManager.GetExcelSheet() where x.RowId != 0 && x.GatheringItemLevel.RowId != 0 select new { GatheringItemId = (int)x.RowId, Name = itemSheet.GetRowOrDefault(x.Item.RowId)?.Name.ToString() } into x where !string.IsNullOrEmpty(x.Name) select x).ToDictionary(x => x.GatheringItemId, x => x.Name); _gatheringPointsByExpansion = (from x in (from DefaultGatheringPoint x in from x in (from x in (from x in dataManager.GetExcelSheet() where x.GatheringPointBase.RowId != 0 select x).Where(delegate(GatheringPoint x) { uint rowId = x.GatheringPointBase.RowId; return (rowId < 653 || rowId > 680) ? true : false; }).DistinctBy((GatheringPoint x) => x.GatheringPointBase.RowId).Select(delegate(GatheringPoint x) { uint rowId = x.RowId; GatheringPointId id = new GatheringPointId((ushort)x.GatheringPointBase.RowId); EClassJob classJob; switch (x.GatheringPointBase.Value.GatheringType.RowId) { case 0u: case 1u: classJob = EClassJob.Miner; break; case 2u: case 3u: classJob = EClassJob.Botanist; break; default: classJob = EClassJob.Fisher; break; } return new { GatheringPointId = rowId, Point = new DefaultGatheringPoint(id, classJob, x.GatheringPointBase.Value.GatheringLevel, (from y in x.GatheringPointBase.Value.Item where y.RowId != 0 select (ushort)y.RowId).ToList(), (EExpansionVersion)(((byte?)x.TerritoryType.ValueNullable?.ExVersion.RowId) ?? byte.MaxValue), (ushort)x.TerritoryType.RowId, x.TerritoryType.ValueNullable?.PlaceName.ValueNullable?.Name.ToString(), $"{x.GatheringPointBase.RowId} - {x.PlaceName.ValueNullable?.Name}") }; }) where x.Point.ClassJob != EClassJob.Fisher select x).Select(x => { if (leveGatheringPoints.Contains(x.GatheringPointId)) { return (DefaultGatheringPoint)null; } if (x.Point.TerritoryType == 1 && gatheringJournalComponent._gatheringPointRegistry.TryGetGatheringPoint(x.Point.Id, out GatheringRoot gatheringRoot)) { TerritoryType row = territoryTypeSheet.GetRow(gatheringRoot.Steps.Last().TerritoryId); return x.Point with { Expansion = (EExpansionVersion)row.ExVersion.RowId, TerritoryType = (ushort)row.RowId, TerritoryName = row.PlaceName.ValueNullable?.Name.ToString() }; } return x.Point; }) where x != null select x where x.Expansion != (EExpansionVersion)255 where x.GatheringItemIds.Count > 0 select x).Where(delegate(DefaultGatheringPoint x) { ushort territoryType = x.TerritoryType; return territoryType != 901 && territoryType != 929; }) group x by x.Expansion into x select new ExpansionPoints(x.Key, (from y in x group y by new { TerritoryType = y.TerritoryType, TerritoryName = $"{((!string.IsNullOrEmpty(y.TerritoryName)) ? y.TerritoryName : "???")} ({y.TerritoryType})" } into y select new TerritoryPoints(y.Key.TerritoryType, y.Key.TerritoryName, y.ToList()) into y where y.Points.Count > 0 select y).ToList()) into x orderby x.ExpansionVersion select x).ToList(); gameInteropProvider.InitializeFromAttributes(this); } public unsafe void DrawGatheringItems() { if (UiThemeUtils.SearchInput("##GatheringSearch", ref _searchText, -1E-45f, "Search areas, gathering points and items")) { UpdateFilter(); } ImGui.Separator(); if (_flatRows.Count > 0) { float x; using (_pluginInterface.UiBuilder.IconFontFixedWidthHandle.Push()) { x = ImGui.CalcTextSize(FontAwesomeIcon.Check.ToIconString()).X; } float fontGlobalScale = ImGui.GetIO().FontGlobalScale; using ImRaii.TableDisposable tableDisposable = UiThemeUtils.BeginThemedTable("GatheringPoints", 3, ImGuiTableFlags.NoSavedSettings | ImGuiTableFlags.ScrollY); if (!tableDisposable) { return; } ImGui.TableSetupColumn("Name", ImGuiTableColumnFlags.NoHide); ImGui.TableSetupColumn("Supported", ImGuiTableColumnFlags.WidthFixed, 120f * fontGlobalScale); ImGui.TableSetupColumn("Collected", ImGuiTableColumnFlags.WidthFixed, 120f * fontGlobalScale); ImGui.TableHeadersRow(); float textLineHeightWithSpacing = ImGui.GetTextLineHeightWithSpacing(); bool flag = false; ImGuiListClipper* intPtr = ImGuiNative.ImGuiListClipper(); ImGuiNative.Begin(intPtr, _flatRows.Count, textLineHeightWithSpacing); ImGuiListClipperPtr imGuiListClipperPtr = intPtr; try { while (imGuiListClipperPtr.Step()) { for (int i = imGuiListClipperPtr.DisplayStart; i < imGuiListClipperPtr.DisplayEnd; i++) { if (DrawFlatRow(_flatRows[i], x)) { flag = true; } } } } finally { imGuiListClipperPtr.Destroy(); } if (flag) { RebuildFlatRows(); } return; } UiThemeUtils.EmptyState(FontAwesomeIcon.Leaf, "No area, gathering point or item matches your search text."); } private bool DrawFlatRow(FlatRow row, float iconSpacing) { ImGui.TableNextRow(); ImGui.TableNextColumn(); bool result = false; float num = (float)row.Depth * ImGui.GetStyle().IndentSpacing; if (row.Type == RowType.Item) { ImGui.SetCursorPosX(ImGui.GetCursorPosX() + num); ImGui.TreeNodeEx(row.Label, ImGuiTreeNodeFlags.NoTreePushOnOpen | ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.SpanFullWidth); ImGui.TableNextColumn(); ImGui.TableNextColumn(); float num2 = ImGui.GetColumnWidth() / 2f - iconSpacing; ImGui.SetCursorPosX(ImGui.GetCursorPosX() + num2); if (row.ItemId < 10000) { _uiUtils.ChecklistItem(string.Empty, row.IsItemGathered); } else { _uiUtils.ChecklistItem(string.Empty, UiThemeUtils.StatusUnobtainable, FontAwesomeIcon.Minus); } } else if (row.Type == RowType.GatheringPoint) { (RowType, int) item = (row.Type, row.GroupIndex); bool flag = _expandedGroups.Contains(item); ImGui.SetCursorPosX(ImGui.GetCursorPosX() + num); UiThemeUtils.DrawTrackerGroupCaret(_pluginInterface.UiBuilder.IconFontFixedWidthHandle, flag); ImGui.SameLine(); ImU8String label = new ImU8String(6, 3); label.AppendFormatted(row.Label); label.AppendLiteral("##grp"); label.AppendFormatted(row.Type); label.AppendLiteral("_"); label.AppendFormatted(row.GroupIndex); if (ImGui.Selectable(label)) { if (flag) { _expandedGroups.Remove(item); } else { _expandedGroups.Add(item); } result = true; } ImGui.TableNextColumn(); float num3 = ImGui.GetColumnWidth() / 2f - iconSpacing; ImGui.SetCursorPosX(ImGui.GetCursorPosX() + num3); _uiUtils.ChecklistItem(string.Empty, row.IsPointComplete); ImGui.TableNextColumn(); UiThemeUtils.DrawTrackerCount(row.Counts.FormattedCollected, row.Counts.IsCollectedComplete, row.Counts.IsCollectedEmpty); } else { (RowType, int) item2 = (row.Type, row.GroupIndex); bool flag2 = _expandedGroups.Contains(item2); ImGui.SetCursorPosX(ImGui.GetCursorPosX() + num); UiThemeUtils.DrawTrackerGroupCaret(_pluginInterface.UiBuilder.IconFontFixedWidthHandle, flag2); ImGui.SameLine(); ImU8String label2 = new ImU8String(6, 3); label2.AppendFormatted(row.Label); label2.AppendLiteral("##grp"); label2.AppendFormatted(row.Type); label2.AppendLiteral("_"); label2.AppendFormatted(row.GroupIndex); if (ImGui.Selectable(label2)) { if (flag2) { _expandedGroups.Remove(item2); } else { _expandedGroups.Add(item2); } result = true; } ImGui.TableNextColumn(); UiThemeUtils.DrawTrackerCount(row.Counts.FormattedSupported, row.Counts.IsSupportedComplete, row.Counts.IsSupportedEmpty); ImGui.TableNextColumn(); UiThemeUtils.DrawTrackerCount(row.Counts.FormattedCollected, row.Counts.IsCollectedComplete, row.Counts.IsCollectedEmpty); } return result; } private void RebuildFlatRows() { List list = new List(); foreach (FilteredExpansion filteredExpansion in _filteredExpansions) { ExpansionPoints expansion = filteredExpansion.Expansion; int expansionVersion = (int)expansion.ExpansionVersion; GatheringCounts counts = GatheringCounts.Create(expansion.CompletedPoints, expansion.TotalPoints, expansion.CompletedItems, expansion.TotalItems); list.Add(new FlatRow(RowType.Expansion, 0, expansionVersion, expansion.ExpansionVersion.ToFriendlyString(), counts, IsPointComplete: false, 0, IsItemGathered: false)); if (!_expandedGroups.Contains((RowType.Expansion, expansionVersion))) { continue; } foreach (FilteredTerritory territory2 in filteredExpansion.Territories) { TerritoryPoints territory = territory2.Territory; int territoryType = territory.TerritoryType; GatheringCounts counts2 = GatheringCounts.Create(territory.CompletedPoints, territory.TotalPoints, territory.CompletedItems, territory.TotalItems); list.Add(new FlatRow(RowType.Territory, 1, territoryType, territory.ToFriendlyString(), counts2, IsPointComplete: false, 0, IsItemGathered: false)); if (!_expandedGroups.Contains((RowType.Territory, territoryType))) { continue; } foreach (FilteredGatheringPoint gatheringPoint in territory2.GatheringPoints) { DefaultGatheringPoint point = gatheringPoint.Point; int value = point.Id.Value; GatheringCounts counts3 = GatheringCounts.Create(0, 0, point.CompletedItems, point.TotalItems); list.Add(new FlatRow(RowType.GatheringPoint, 2, value, $"{point.PlaceName} ({point.ClassJob} Lv. {point.Level})", counts3, point.IsComplete, 0, IsItemGathered: false)); if (!_expandedGroups.Contains((RowType.GatheringPoint, value))) { continue; } foreach (ushort gatheringItemId in gatheringPoint.GatheringItemIds) { string valueOrDefault = _gatheringItems.GetValueOrDefault(gatheringItemId, "???"); bool isItemGathered = _gatheredItems.Contains(gatheringItemId); list.Add(new FlatRow(RowType.Item, 3, 0, valueOrDefault, default(GatheringCounts), IsPointComplete: false, gatheringItemId, isItemGathered)); } } } } _flatRows = list; } public void UpdateFilter() { Predicate match; if (string.IsNullOrWhiteSpace(_searchText)) { match = (string _) => true; } else { match = (string x) => x.Contains(_searchText, StringComparison.CurrentCultureIgnoreCase); } _filteredExpansions = (from section in _gatheringPointsByExpansion select FilterExpansion(section, match) into x where x != null select x).Cast().ToList(); RebuildFlatRows(); } private FilteredExpansion? FilterExpansion(ExpansionPoints expansion, Predicate match) { List list = (from x in expansion.PointsByTerritories select FilterTerritory(x, match) into x where x != null select (x)).ToList(); if (list.Count > 0) { return new FilteredExpansion(expansion, list); } return null; } private FilteredTerritory? FilterTerritory(TerritoryPoints territory, Predicate match) { if (match(territory.TerritoryName)) { return new FilteredTerritory(territory, territory.Points.Select((DefaultGatheringPoint x) => FilterGatheringPoint(x, (string _) => true)).ToList()); } List list = (from x in territory.Points select FilterGatheringPoint(x, match) into x where x != null select (x)).ToList(); if (list.Count > 0) { return new FilteredTerritory(territory, list); } return null; } private FilteredGatheringPoint? FilterGatheringPoint(DefaultGatheringPoint gatheringPoint, Predicate match) { if (match(gatheringPoint.PlaceName ?? string.Empty)) { return new FilteredGatheringPoint(gatheringPoint, gatheringPoint.GatheringItemIds); } List list = gatheringPoint.GatheringItemIds.Where((ushort x) => match(_gatheringItems.GetValueOrDefault(x, string.Empty))).ToList(); if (list.Count > 0) { return new FilteredGatheringPoint(gatheringPoint, list); } return null; } internal void RefreshCounts() { _gatheredItems.Clear(); foreach (int key in _gatheringItems.Keys) { ushort item = (ushort)key; if (IsGatheringItemGathered(item)) { _gatheredItems.Add(item); } } foreach (ExpansionPoints item2 in _gatheringPointsByExpansion) { foreach (TerritoryPoints pointsByTerritory in item2.PointsByTerritories) { foreach (DefaultGatheringPoint point in pointsByTerritory.Points) { point.TotalItems = point.GatheringItemIds.Count((ushort x) => x < 10000); point.CompletedItems = point.GatheringItemIds.Count(_gatheredItems.Contains); point.IsComplete = _gatheringPointRegistry.TryGetGatheringPoint(point.Id, out GatheringRoot _); } pointsByTerritory.TotalItems = pointsByTerritory.Points.Sum((DefaultGatheringPoint x) => x.TotalItems); pointsByTerritory.CompletedItems = pointsByTerritory.Points.Sum((DefaultGatheringPoint x) => x.CompletedItems); pointsByTerritory.CompletedPoints = pointsByTerritory.Points.Count((DefaultGatheringPoint x) => x.IsComplete); } item2.TotalItems = item2.PointsByTerritories.Sum((TerritoryPoints x) => x.TotalItems); item2.CompletedItems = item2.PointsByTerritories.Sum((TerritoryPoints x) => x.CompletedItems); item2.TotalPoints = item2.PointsByTerritories.Sum((TerritoryPoints x) => x.TotalPoints); item2.CompletedPoints = item2.PointsByTerritories.Sum((TerritoryPoints x) => x.CompletedPoints); } RebuildFlatRows(); } public void ClearCounts(int type, int code) { foreach (ExpansionPoints item in _gatheringPointsByExpansion) { item.CompletedItems = 0; item.CompletedPoints = 0; foreach (TerritoryPoints pointsByTerritory in item.PointsByTerritories) { pointsByTerritory.CompletedItems = 0; pointsByTerritory.CompletedPoints = 0; foreach (DefaultGatheringPoint point in pointsByTerritory.Points) { point.IsComplete = false; } } } RebuildFlatRows(); } }