1
0
Fork 0
forked from aly/qstbak
qstbak/Questionable/Questionable.Windows.SmartNavComponents/DataProbesPanel.cs
2026-08-17 20:29:32 +10:00

862 lines
33 KiB
C#

using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Numerics;
using System.Text;
using System.Text.Json;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game;
using Lumina.Data.Files;
using Lumina.Data.Parsing.Layer;
using Lumina.Excel;
using Lumina.Excel.Sheets;
using Microsoft.Extensions.Logging;
using Questionable.Data;
using SmartNav;
using SmartNav.Data;
using SmartNav.Graph;
using SmartNav.Model.Navigation;
namespace Questionable.Windows.SmartNavComponents;
internal sealed class DataProbesPanel
{
private static readonly string[] PlanFiles = new string[4] { "planmap.lgb", "planevent.lgb", "planner.lgb", "planlive.lgb" };
private static readonly JsonSerializerOptions IndentedJson = new JsonSerializerOptions
{
WriteIndented = true
};
private readonly NavRouter _navRouter;
private readonly WarpDataService _warpDataService;
private readonly TerritoryData _territoryData;
private readonly IDataManager _dataManager;
private readonly IClientState _clientState;
private readonly SmartNavDataOptions _dataOptions;
private readonly ILogger<DataProbesPanel> _logger;
private string? _multiQuestReport;
private bool _popRangeOnlyDestinationless = true;
private string? _popRangeReport;
private int _lvbTerritoryId;
private string? _lvbReport;
private string? _arrivalExportReport;
public DataProbesPanel(NavRouter navRouter, WarpDataService warpDataService, TerritoryData territoryData, IDataManager dataManager, IClientState clientState, SmartNavDataOptions dataOptions, ILogger<DataProbesPanel> logger)
{
_navRouter = navRouter;
_warpDataService = warpDataService;
_territoryData = territoryData;
_dataManager = dataManager;
_clientState = clientState;
_dataOptions = dataOptions;
_logger = logger;
}
public void Draw()
{
DrawMultiQuestWarpAudit();
ImGui.Separator();
DrawPopRangeProbe();
ImGui.Separator();
DrawDerivedArrivalExport();
ImGui.Separator();
DrawLayerSetProbe();
}
private static void DrawReport(string id, ref string? report)
{
if (report == null)
{
return;
}
ImGui.SameLine();
ImU8String label = new ImU8String(6, 1);
label.AppendLiteral("Copy##");
label.AppendFormatted(id);
if (ImGui.Button(label))
{
ImGui.SetClipboardText(report);
}
ImGui.SameLine();
ImU8String label2 = new ImU8String(7, 1);
label2.AppendLiteral("Clear##");
label2.AppendFormatted(id);
if (ImGui.Button(label2))
{
report = null;
return;
}
ImU8String strId = new ImU8String(7, 1);
strId.AppendLiteral("##");
strId.AppendFormatted(id);
strId.AppendLiteral("child");
using ImRaii.ChildDisposable childDisposable = ImRaii.Child(strId, new Vector2(-1f, 300f), border: true);
if ((bool)childDisposable)
{
ImGui.TextUnformatted(report);
}
}
private void DrawMultiQuestWarpAudit()
{
if (ImGui.CollapsingHeader("Multi-Quest Warp Audit (CompleteParam)###multiquestaudit", ImGuiTreeNodeFlags.DefaultOpen))
{
ImGui.TextWrapped("Warps whose WarpCondition names 2+ quests. Verifies the CompleteParam any-of fix: conds 3/9/10 (airships, ferry) should read any-of, cond 34 all-of. 'FIX CHANGES THIS' marks warps the pre-fix all-of admission got wrong for this character.");
if (ImGui.Button("Run Audit##multiquest"))
{
_multiQuestReport = RunGuarded(BuildMultiQuestReport);
}
DrawReport("multiquest", ref _multiQuestReport);
}
}
private string BuildMultiQuestReport()
{
StringBuilder stringBuilder = new StringBuilder();
ExcelSheet<Warp> excelSheet = _dataManager.GetExcelSheet<Warp>();
ExcelSheet<Quest> excelSheet2 = _dataManager.GetExcelSheet<Quest>();
Dictionary<uint, int> dictionary = (from n in _navRouter.StaticGraph.Nodes.Values.OfType<NavNode.WarpNode>()
group n by n.WarpRowId).ToDictionary((IGrouping<uint, NavNode.WarpNode> g) => g.Key, (IGrouping<uint, NavNode.WarpNode> g) => g.Count());
int num = 0;
StringBuilder stringBuilder2;
IFormatProvider invariantCulture;
StringBuilder.AppendInterpolatedStringHandler handler;
foreach (WarpDataService.WarpInfo item in from w in _warpDataService.Warps
group w by w.RowId into g
select g.First())
{
if (item.RequiredQuestIds.Length < 2)
{
continue;
}
num++;
Warp? rowOrDefault = excelSheet.GetRowOrDefault(item.RowId);
uint value = rowOrDefault?.WarpCondition.RowId ?? 0;
byte? b = rowOrDefault?.WarpCondition.ValueNullable?.CompleteParam;
stringBuilder2 = stringBuilder;
StringBuilder stringBuilder3 = stringBuilder2;
invariantCulture = CultureInfo.InvariantCulture;
IFormatProvider provider = invariantCulture;
handler = new StringBuilder.AppendInterpolatedStringHandler(12, 3, stringBuilder2, invariantCulture);
handler.AppendLiteral("warp ");
handler.AppendFormatted(item.RowId);
handler.AppendLiteral(" '");
handler.AppendFormatted(item.Name ?? item.Question ?? "?");
handler.AppendLiteral("' ");
handler.AppendLiteral("-> ");
handler.AppendFormatted(_territoryData.GetNameAndId(item.DestTerritoryId));
stringBuilder3.AppendLine(provider, ref handler);
stringBuilder2 = stringBuilder;
StringBuilder stringBuilder4 = stringBuilder2;
invariantCulture = CultureInfo.InvariantCulture;
IFormatProvider provider2 = invariantCulture;
handler = new StringBuilder.AppendInterpolatedStringHandler(47, 3, stringBuilder2, invariantCulture);
handler.AppendLiteral(" condition ");
handler.AppendFormatted(value);
handler.AppendLiteral(", CompleteParam=");
handler.AppendFormatted(b?.ToString(CultureInfo.InvariantCulture) ?? "?");
handler.AppendLiteral(", ");
handler.AppendLiteral("RequireAllQuests=");
handler.AppendFormatted(item.RequireAllQuests);
stringBuilder4.AppendLine(provider2, ref handler);
stringBuilder2 = stringBuilder;
StringBuilder stringBuilder5 = stringBuilder2;
invariantCulture = CultureInfo.InvariantCulture;
IFormatProvider provider3 = invariantCulture;
handler = new StringBuilder.AppendInterpolatedStringHandler(37, 4, stringBuilder2, invariantCulture);
handler.AppendLiteral(" source: territory ");
handler.AppendFormatted(item.SourceTerritoryId);
handler.AppendLiteral(" ");
handler.AppendFormatted(item.SourcePosition);
handler.AppendLiteral(", ");
handler.AppendLiteral("npc ");
handler.AppendFormatted(item.NpcDataId?.ToString(CultureInfo.InvariantCulture) ?? "-");
handler.AppendLiteral(", derived=");
handler.AppendFormatted(item.DerivedSource);
stringBuilder5.AppendLine(provider3, ref handler);
stringBuilder2 = stringBuilder;
StringBuilder stringBuilder6 = stringBuilder2;
invariantCulture = CultureInfo.InvariantCulture;
IFormatProvider provider4 = invariantCulture;
handler = new StringBuilder.AppendInterpolatedStringHandler(35, 1, stringBuilder2, invariantCulture);
handler.AppendLiteral(" static graph nodes for this row: ");
handler.AppendFormatted(dictionary.GetValueOrDefault(item.RowId));
stringBuilder6.AppendLine(provider4, ref handler);
int num2 = 0;
uint[] requiredQuestIds = item.RequiredQuestIds;
foreach (uint num4 in requiredQuestIds)
{
bool flag = QuestManager.IsQuestComplete((ushort)(num4 & 0xFFFF));
if (flag)
{
num2++;
}
string value2 = excelSheet2.GetRowOrDefault(num4)?.Name.ExtractText() ?? "?";
stringBuilder2 = stringBuilder;
StringBuilder stringBuilder7 = stringBuilder2;
invariantCulture = CultureInfo.InvariantCulture;
IFormatProvider provider5 = invariantCulture;
handler = new StringBuilder.AppendInterpolatedStringHandler(13, 3, stringBuilder2, invariantCulture);
handler.AppendLiteral(" quest ");
handler.AppendFormatted(num4);
handler.AppendLiteral(" '");
handler.AppendFormatted(value2);
handler.AppendLiteral("': ");
handler.AppendFormatted(flag ? "COMPLETE" : "not complete");
stringBuilder7.AppendLine(provider5, ref handler);
}
bool flag2 = (item.RequireAllQuests ? (num2 == item.RequiredQuestIds.Length) : (num2 > 0));
bool flag3 = num2 == item.RequiredQuestIds.Length;
stringBuilder2 = stringBuilder;
StringBuilder stringBuilder8 = stringBuilder2;
invariantCulture = CultureInfo.InvariantCulture;
IFormatProvider provider6 = invariantCulture;
handler = new StringBuilder.AppendInterpolatedStringHandler(34, 3, stringBuilder2, invariantCulture);
handler.AppendLiteral(" admission: now=");
handler.AppendFormatted(flag2);
handler.AppendLiteral(", pre-fix all-of=");
handler.AppendFormatted(flag3);
handler.AppendFormatted((flag2 != flag3) ? " <-- FIX CHANGES THIS" : "");
stringBuilder8.AppendLine(provider6, ref handler);
}
stringBuilder2 = stringBuilder;
StringBuilder stringBuilder9 = stringBuilder2;
invariantCulture = CultureInfo.InvariantCulture;
IFormatProvider provider7 = invariantCulture;
handler = new StringBuilder.AppendInterpolatedStringHandler(26, 1, stringBuilder2, invariantCulture);
handler.AppendFormatted(num);
handler.AppendLiteral(" multi-quest warps audited");
stringBuilder9.AppendLine(provider7, ref handler);
return stringBuilder.ToString();
}
private void DrawPopRangeProbe()
{
if (ImGui.CollapsingHeader("Arrival PopRange Plan-File Probe###poprangeprobe", ImGuiTreeNodeFlags.DefaultOpen))
{
ImGui.TextWrapped("For each warp, reads Warp.PopRange both ways: as a Level sheet row (SmartNav's source resolution) and as an LGB PopRange instance id in the destination territory's plan files (NoireLib's arrival claim: planevent holds most arrivals, planlive holds switched-content arrivals found nowhere else). Decides whether warp-destination seeding shrinks to 'extend the scan'. Unchecked, warps with a recorded arrival get a position delta (LGB PopRange translation vs recorded DestPosition) - the control for using LGB positions as arrivals.");
ImGui.Checkbox("Only destination-less warps (no recorded arrival)", ref _popRangeOnlyDestinationless);
if (ImGui.Button("Run Probe##poprange"))
{
_popRangeReport = RunGuarded(BuildPopRangeReport);
}
DrawReport("poprange", ref _popRangeReport);
}
}
private string BuildPopRangeReport()
{
StringBuilder stringBuilder = new StringBuilder();
ExcelSheet<Warp> excelSheet = _dataManager.GetExcelSheet<Warp>();
ExcelSheet<Level> excelSheet2 = _dataManager.GetExcelSheet<Level>();
Dictionary<uint, Dictionary<string, Dictionary<uint, Vector3>>> cache = new Dictionary<uint, Dictionary<string, Dictionary<uint, Vector3>>>();
Dictionary<string, int> dictionary = PlanFiles.ToDictionary((string f) => f, (string _) => 0);
List<(uint, float)> list = new List<(uint, float)>();
int num = 0;
int num2 = 0;
int num3 = 0;
StringBuilder stringBuilder2;
IFormatProvider invariantCulture;
StringBuilder.AppendInterpolatedStringHandler handler;
foreach (WarpDataService.WarpInfo item4 in from w in _warpDataService.Warps
group w by w.RowId into g
select g.First())
{
if (_popRangeOnlyDestinationless && item4.DestPosition.HasValue)
{
continue;
}
Warp? rowOrDefault = excelSheet.GetRowOrDefault(item4.RowId);
if (!rowOrDefault.HasValue)
{
continue;
}
num++;
uint rowId = rowOrDefault.Value.PopRange.RowId;
uint rowId2 = rowOrDefault.Value.TerritoryType.RowId;
bool flag = rowId != 0 && excelSheet2.HasRow(rowId);
if (flag)
{
num3++;
}
List<string> list2 = new List<string>();
Vector3? vector = null;
foreach (KeyValuePair<string, Dictionary<uint, Vector3>> item5 in ScanPopRanges(cache, rowId2))
{
item5.Deconstruct(out var key, out var value);
string text = key;
if (value.TryGetValue(rowId, out var value2))
{
list2.Add(text);
key = text;
dictionary[key]++;
Vector3 valueOrDefault = vector.GetValueOrDefault();
if (!vector.HasValue)
{
valueOrDefault = value2;
vector = valueOrDefault;
}
}
}
if (list2.Count == 0)
{
num2++;
}
string value3 = "";
if (vector.HasValue && !item4.DerivedArrival)
{
Vector3? destPosition = item4.DestPosition;
if (destPosition.HasValue)
{
Vector3 valueOrDefault2 = destPosition.GetValueOrDefault();
float item = Vector3.Distance(vector.Value, valueOrDefault2);
list.Add((item4.RowId, item));
value3 = ", delta=" + item.ToString("F1", CultureInfo.InvariantCulture) + "y";
goto IL_02c9;
}
}
if (item4.DerivedArrival)
{
value3 = ", derived-arrival";
}
goto IL_02c9;
IL_02c9:
stringBuilder2 = stringBuilder;
StringBuilder stringBuilder3 = stringBuilder2;
invariantCulture = CultureInfo.InvariantCulture;
IFormatProvider provider = invariantCulture;
handler = new StringBuilder.AppendInterpolatedStringHandler(39, 8, stringBuilder2, invariantCulture);
handler.AppendLiteral("warp ");
handler.AppendFormatted(item4.RowId);
handler.AppendLiteral(" '");
handler.AppendFormatted(item4.Name ?? item4.Question ?? "?");
handler.AppendLiteral("' popId=");
handler.AppendFormatted(rowId);
handler.AppendLiteral(" ");
handler.AppendLiteral("dest=");
handler.AppendFormatted(_territoryData.GetNameAndId(rowId2));
handler.AppendLiteral(": ");
handler.AppendLiteral("level-row=");
handler.AppendFormatted(flag);
handler.AppendLiteral(", lgb=");
handler.AppendFormatted((list2.Count > 0) ? string.Join("+", list2) : "NOT FOUND");
handler.AppendFormatted(vector.HasValue ? $" pos={vector.Value}" : "");
handler.AppendFormatted(value3);
stringBuilder3.AppendLine(provider, ref handler);
}
stringBuilder.AppendLine();
stringBuilder2 = stringBuilder;
StringBuilder stringBuilder4 = stringBuilder2;
invariantCulture = CultureInfo.InvariantCulture;
IFormatProvider provider2 = invariantCulture;
handler = new StringBuilder.AppendInterpolatedStringHandler(65, 3, stringBuilder2, invariantCulture);
handler.AppendFormatted(num);
handler.AppendLiteral(" warps probed, ");
handler.AppendFormatted(num3);
handler.AppendLiteral(" with a Level sheet row, ");
handler.AppendFormatted(num2);
handler.AppendLiteral(" with no LGB PopRange hit");
stringBuilder4.AppendLine(provider2, ref handler);
string[] planFiles = PlanFiles;
foreach (string text2 in planFiles)
{
stringBuilder2 = stringBuilder;
StringBuilder stringBuilder5 = stringBuilder2;
invariantCulture = CultureInfo.InvariantCulture;
IFormatProvider provider3 = invariantCulture;
handler = new StringBuilder.AppendInterpolatedStringHandler(17, 2, stringBuilder2, invariantCulture);
handler.AppendLiteral(" ");
handler.AppendFormatted(text2);
handler.AppendLiteral(": ");
handler.AppendFormatted(dictionary[text2]);
handler.AppendLiteral(" arrival hits");
stringBuilder5.AppendLine(provider3, ref handler);
}
if (list.Count > 0)
{
List<(uint, float)> list3 = list.OrderBy<(uint, float), float>(((uint RowId, float Delta) d) => d.Delta).ToList();
float item2 = list3[list3.Count / 2].Item2;
float item3 = list3[Math.Min(list3.Count - 1, (int)((double)list3.Count * 0.9))].Item2;
List<(uint, float)> list4 = list3.Where<(uint, float)>(((uint RowId, float Delta) d) => d.Delta > 20f).ToList();
stringBuilder2 = stringBuilder;
StringBuilder stringBuilder6 = stringBuilder2;
invariantCulture = CultureInfo.InvariantCulture;
IFormatProvider provider4 = invariantCulture;
handler = new StringBuilder.AppendInterpolatedStringHandler(112, 5, stringBuilder2, invariantCulture);
handler.AppendLiteral("recorded-arrival delta (LGB PopRange pos vs recorded DestPosition): ");
handler.AppendFormatted(list3.Count);
handler.AppendLiteral(" compared, ");
handler.AppendLiteral("median ");
handler.AppendFormatted(item2, "F1");
handler.AppendLiteral("y, p90 ");
handler.AppendFormatted(item3, "F1");
handler.AppendLiteral("y, max ");
handler.AppendFormatted(list3[list3.Count - 1].Item2, "F1");
handler.AppendLiteral("y, ");
handler.AppendFormatted(list4.Count);
handler.AppendLiteral(" over 20y");
stringBuilder6.AppendLine(provider4, ref handler);
if (list4.Count > 0)
{
stringBuilder2 = stringBuilder;
StringBuilder stringBuilder7 = stringBuilder2;
invariantCulture = CultureInfo.InvariantCulture;
IFormatProvider provider5 = invariantCulture;
handler = new StringBuilder.AppendInterpolatedStringHandler(17, 1, stringBuilder2, invariantCulture);
handler.AppendLiteral(" >20y outliers: ");
handler.AppendFormatted(string.Join(", ", list4.Select<(uint, float), string>(((uint RowId, float Delta) d) => $"{d.RowId} ({d.Delta.ToString("F1", CultureInfo.InvariantCulture)}y)")));
stringBuilder7.AppendLine(provider5, ref handler);
}
}
return stringBuilder.ToString();
}
private Dictionary<string, Dictionary<uint, Vector3>> ScanPopRanges(Dictionary<uint, Dictionary<string, Dictionary<uint, Vector3>>> cache, uint territoryId)
{
if (cache.TryGetValue(territoryId, out Dictionary<string, Dictionary<uint, Vector3>> value))
{
return value;
}
Dictionary<string, Dictionary<uint, Vector3>> dictionary = (cache[territoryId] = new Dictionary<string, Dictionary<uint, Vector3>>());
string text = _dataManager.GetExcelSheet<TerritoryType>().GetRowOrDefault(territoryId)?.Bg.ExtractText();
if (string.IsNullOrEmpty(text))
{
return dictionary;
}
int num = text.IndexOf("/level/", StringComparison.Ordinal);
if (num < 0)
{
return dictionary;
}
string[] planFiles = PlanFiles;
foreach (string text2 in planFiles)
{
Dictionary<uint, Vector3> dictionary3 = (dictionary[text2] = new Dictionary<uint, Vector3>());
string text3 = "bg/" + text.Substring(0, num + 1) + "level/" + text2;
try
{
LgbFile file = _dataManager.GetFile<LgbFile>(text3);
if (file == null)
{
continue;
}
LayerCommon.Layer[] layers = file.Layers;
for (int j = 0; j < layers.Length; j++)
{
LayerCommon.InstanceObject[] instanceObjects = layers[j].InstanceObjects;
for (int k = 0; k < instanceObjects.Length; k++)
{
LayerCommon.InstanceObject instanceObject = instanceObjects[k];
if (instanceObject.AssetType == LayerEntryType.PopRange)
{
dictionary3[instanceObject.InstanceId] = new Vector3(instanceObject.Transform.Translation.X, instanceObject.Transform.Translation.Y, instanceObject.Transform.Translation.Z);
}
}
}
}
catch (Exception exception)
{
_logger.LogTrace(exception, "Failed to scan {Path}", text3);
}
}
return dictionary;
}
private void DrawDerivedArrivalExport()
{
if (!ImGui.CollapsingHeader("Derived Arrival Export (authoring)###arrivalexport"))
{
return;
}
ImGui.TextWrapped("Scans every Warp row's destination territory for its arrival PopRange and writes derived-arrivals.json into the SmartNav.Data checkout (DevSourceDirectory) for bundling - runtime never scans LGB itself. Rerun after game patches. The game FREEZES for the scan (~1-2 min on a Debug build) - expected, wait it out.");
if (_dataOptions.DevSourceDirectory == null)
{
ImGui.TextDisabled("DevSourceDirectory not set - available in DEBUG builds only.");
return;
}
if (ImGui.Button("Scan + Export##arrivalexportrun"))
{
_arrivalExportReport = RunGuarded(BuildDerivedArrivalExport);
}
DrawReport("arrivalexport", ref _arrivalExportReport);
}
private string BuildDerivedArrivalExport()
{
StringBuilder stringBuilder = new StringBuilder();
ExcelSheet<Warp> excelSheet = _dataManager.GetExcelSheet<Warp>();
Dictionary<uint, Dictionary<string, Dictionary<uint, Vector3>>> cache = new Dictionary<uint, Dictionary<string, Dictionary<uint, Vector3>>>();
Dictionary<string, int> dictionary = PlanFiles.ToDictionary((string f) => f, (string _) => 0);
Dictionary<uint, DerivedArrivalEntry> dictionary2 = new Dictionary<uint, DerivedArrivalEntry>();
List<string> list = new List<string>();
int num = 0;
foreach (Warp item in excelSheet.Where((Warp x) => x.RowId != 0))
{
uint rowId = item.PopRange.RowId;
ushort num2 = (ushort)item.TerritoryType.RowId;
if (rowId == 0 || num2 == 0)
{
continue;
}
num++;
Vector3? vector = null;
foreach (KeyValuePair<string, Dictionary<uint, Vector3>> item2 in ScanPopRanges(cache, num2))
{
item2.Deconstruct(out var key, out var value);
string text = key;
if (value.TryGetValue(rowId, out var value2))
{
key = text;
dictionary[key]++;
Vector3 valueOrDefault = vector.GetValueOrDefault();
if (!vector.HasValue)
{
valueOrDefault = value2;
vector = valueOrDefault;
}
}
}
if (vector.HasValue)
{
dictionary2[item.RowId] = new DerivedArrivalEntry
{
PopRangeId = rowId,
TerritoryId = num2,
X = vector.Value.X,
Y = vector.Value.Y,
Z = vector.Value.Z
};
continue;
}
string value3 = item.Name.ExtractText();
if (string.IsNullOrEmpty(value3))
{
value3 = item.Question.ExtractText();
}
list.Add($" {item.RowId} '{value3}' popId={rowId} dest={_territoryData.GetNameAndId(num2)}");
}
DerivedArrivalFile derivedArrivalFile = new DerivedArrivalFile
{
GameVersion = _dataOptions.GameVersion,
Arrivals = dictionary2
};
string text2 = Path.Combine(_dataOptions.DevSourceDirectory.FullName, "derived-arrivals.json");
File.WriteAllText(text2, JsonSerializer.Serialize(derivedArrivalFile, IndentedJson));
StringBuilder stringBuilder2 = stringBuilder;
StringBuilder stringBuilder3 = stringBuilder2;
IFormatProvider invariantCulture = CultureInfo.InvariantCulture;
IFormatProvider provider = invariantCulture;
StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(57, 2, stringBuilder2, invariantCulture);
handler.AppendFormatted(num);
handler.AppendLiteral(" warp rows with PopRange + destination, ");
handler.AppendFormatted(dictionary2.Count);
handler.AppendLiteral(" arrivals derived");
stringBuilder3.AppendLine(provider, ref handler);
string[] planFiles = PlanFiles;
foreach (string text3 in planFiles)
{
stringBuilder2 = stringBuilder;
StringBuilder stringBuilder4 = stringBuilder2;
invariantCulture = CultureInfo.InvariantCulture;
IFormatProvider provider2 = invariantCulture;
handler = new StringBuilder.AppendInterpolatedStringHandler(9, 2, stringBuilder2, invariantCulture);
handler.AppendLiteral(" ");
handler.AppendFormatted(text3);
handler.AppendLiteral(": ");
handler.AppendFormatted(dictionary[text3]);
handler.AppendLiteral(" hits");
stringBuilder4.AppendLine(provider2, ref handler);
}
stringBuilder2 = stringBuilder;
StringBuilder stringBuilder5 = stringBuilder2;
invariantCulture = CultureInfo.InvariantCulture;
IFormatProvider provider3 = invariantCulture;
handler = new StringBuilder.AppendInterpolatedStringHandler(98, 1, stringBuilder2, invariantCulture);
handler.AppendFormatted(list.Count);
handler.AppendLiteral(" unresolved (no plan file holds the pop id; Level-sheet fallback may still cover some at runtime):");
stringBuilder5.AppendLine(provider3, ref handler);
foreach (string item3 in list)
{
stringBuilder.AppendLine(item3);
}
stringBuilder.AppendLine();
stringBuilder2 = stringBuilder;
StringBuilder stringBuilder6 = stringBuilder2;
invariantCulture = CultureInfo.InvariantCulture;
IFormatProvider provider4 = invariantCulture;
handler = new StringBuilder.AppendInterpolatedStringHandler(25, 2, stringBuilder2, invariantCulture);
handler.AppendLiteral("written: ");
handler.AppendFormatted(text2);
handler.AppendLiteral(" (game version ");
handler.AppendFormatted(derivedArrivalFile.GameVersion ?? "?");
handler.AppendLiteral(")");
stringBuilder6.AppendLine(provider4, ref handler);
stringBuilder.AppendLine("DEBUG builds read this file directly via DevSourceDirectory - toggle the plugin to see the new arrivals; commit it in SmartNav to ship.");
return stringBuilder.ToString();
}
private void DrawLayerSetProbe()
{
if (!ImGui.CollapsingHeader("Layer-Set Attribution Probe (.lvb)###lvbprobe", ImGuiTreeNodeFlags.DefaultOpen))
{
return;
}
ImGui.TextWrapped("Parses the territory's .lvb layer-set table per the offsets claimed in NoireLib's GameData docs (0x1C-byte records, TerritoryType row at +0x0C) and each plan file's per-layer layer-set references. Verify against a known shared-level pair, e.g. Mist 339 vs s1h1_test 136: attributed territory ids must all appear in the 'territories sharing this Bg' line.");
ImGui.SetNextItemWidth(120f);
ImGui.InputInt("Territory id (0 = current)##lvbterritory", ref _lvbTerritoryId);
if (ImGui.Button("Parse##lvb"))
{
uint territoryId = ((_lvbTerritoryId > 0) ? ((uint)_lvbTerritoryId) : _clientState.TerritoryType);
_lvbReport = RunGuarded(() => BuildLayerSetReport(territoryId));
}
DrawReport("lvb", ref _lvbReport);
}
private string BuildLayerSetReport(uint territoryId)
{
StringBuilder stringBuilder = new StringBuilder();
ExcelSheet<TerritoryType> excelSheet = _dataManager.GetExcelSheet<TerritoryType>();
string bg = excelSheet.GetRowOrDefault(territoryId)?.Bg.ExtractText();
if (string.IsNullOrEmpty(bg))
{
return $"territory {territoryId} has no Bg path";
}
int num = bg.IndexOf("/level/", StringComparison.Ordinal);
if (num < 0)
{
return $"territory {territoryId} Bg '{bg}' has no /level/ segment";
}
string text = "bg/" + bg.Substring(0, num + 1) + "level/";
string text2 = bg.Substring(bg.LastIndexOf('/') + 1);
StringBuilder stringBuilder2 = stringBuilder;
StringBuilder stringBuilder3 = stringBuilder2;
IFormatProvider invariantCulture = CultureInfo.InvariantCulture;
IFormatProvider provider = invariantCulture;
StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(17, 2, stringBuilder2, invariantCulture);
handler.AppendLiteral("territory ");
handler.AppendFormatted(_territoryData.GetNameAndId(territoryId));
handler.AppendLiteral(", Bg '");
handler.AppendFormatted(bg);
handler.AppendLiteral("'");
stringBuilder3.AppendLine(provider, ref handler);
List<uint> list = (from t in excelSheet
where t.RowId != 0 && t.Bg.ExtractText() == bg
select t.RowId).ToList();
stringBuilder2 = stringBuilder;
StringBuilder stringBuilder4 = stringBuilder2;
invariantCulture = CultureInfo.InvariantCulture;
IFormatProvider provider2 = invariantCulture;
handler = new StringBuilder.AppendInterpolatedStringHandler(29, 1, stringBuilder2, invariantCulture);
handler.AppendLiteral("territories sharing this Bg: ");
handler.AppendFormatted(string.Join(", ", list));
stringBuilder4.AppendLine(provider2, ref handler);
List<(uint SetId, uint TerritoryId)> list2 = ParseLvbLayerSets(ReadFileBytes(text + text2 + ".lvb"), stringBuilder);
Dictionary<uint, List<uint>> owners = new Dictionary<uint, List<uint>>();
foreach (var item3 in list2)
{
uint item = item3.SetId;
uint item2 = item3.TerritoryId;
stringBuilder2 = stringBuilder;
StringBuilder stringBuilder5 = stringBuilder2;
invariantCulture = CultureInfo.InvariantCulture;
IFormatProvider provider3 = invariantCulture;
handler = new StringBuilder.AppendInterpolatedStringHandler(26, 3, stringBuilder2, invariantCulture);
handler.AppendLiteral(" layer set ");
handler.AppendFormatted(item);
handler.AppendLiteral(" -> territory ");
handler.AppendFormatted(item2);
handler.AppendFormatted(list.Contains(item2) ? "" : " <-- NOT IN SHARING SET (offsets suspect)");
stringBuilder5.AppendLine(provider3, ref handler);
if (item2 != 0)
{
if (!owners.TryGetValue(item, out List<uint> value))
{
value = (owners[item] = new List<uint>());
}
value.Add(item2);
}
}
string[] planFiles = PlanFiles;
foreach (string text3 in planFiles)
{
List<List<uint>> list4 = ParseLgbLayerSetReferences(ReadFileBytes(text + text3));
if (list4.Count == 0)
{
stringBuilder2 = stringBuilder;
StringBuilder stringBuilder6 = stringBuilder2;
invariantCulture = CultureInfo.InvariantCulture;
IFormatProvider provider4 = invariantCulture;
handler = new StringBuilder.AppendInterpolatedStringHandler(18, 1, stringBuilder2, invariantCulture);
handler.AppendFormatted(text3);
handler.AppendLiteral(": no layers parsed");
stringBuilder6.AppendLine(provider4, ref handler);
continue;
}
int value2 = list4.Count((List<uint> refs) => refs.Count > 0);
List<uint> value3;
List<string> list5 = (from setId in list4.SelectMany((List<uint> refs) => refs).Distinct()
select (!owners.TryGetValue(setId, out value3)) ? $"set {setId} unknown" : string.Join("/", value3)).Distinct().ToList();
stringBuilder2 = stringBuilder;
StringBuilder stringBuilder7 = stringBuilder2;
invariantCulture = CultureInfo.InvariantCulture;
IFormatProvider provider5 = invariantCulture;
handler = new StringBuilder.AppendInterpolatedStringHandler(48, 4, stringBuilder2, invariantCulture);
handler.AppendFormatted(text3);
handler.AppendLiteral(": ");
handler.AppendFormatted(list4.Count);
handler.AppendLiteral(" layers, ");
handler.AppendFormatted(value2);
handler.AppendLiteral(" with layer-set refs; ");
handler.AppendLiteral("attributed to: ");
handler.AppendFormatted((list5.Count > 0) ? string.Join(", ", list5) : "-");
stringBuilder7.AppendLine(provider5, ref handler);
}
return stringBuilder.ToString();
}
private byte[]? ReadFileBytes(string path)
{
try
{
return _dataManager.GetFile(path)?.Data;
}
catch (Exception exception)
{
_logger.LogTrace(exception, "Failed to read {Path}", path);
return null;
}
}
private static List<(uint SetId, uint TerritoryId)> ParseLvbLayerSets(byte[]? data, StringBuilder sb)
{
List<(uint, uint)> list = new List<(uint, uint)>();
if (data == null || data.Length < 36 || !HasMagic(data, 0, "LVB1") || !HasMagic(data, 12, "SCN1"))
{
sb.AppendLine(".lvb: missing or not LVB1/SCN1");
return list;
}
int num = 20 + BitConverter.ToInt32(data, 32);
if (num < 0 || num + 12 > data.Length)
{
sb.AppendLine(".lvb: layer-set folder offset out of range");
return list;
}
int num2 = BitConverter.ToInt32(data, num + 4);
int num3 = num + 12;
bool flag = ((num2 < 0 || num2 > 4096) ? true : false);
StringBuilder stringBuilder;
IFormatProvider invariantCulture;
StringBuilder.AppendInterpolatedStringHandler handler;
if (flag || num3 + num2 * 28 > data.Length)
{
stringBuilder = sb;
StringBuilder stringBuilder2 = stringBuilder;
invariantCulture = CultureInfo.InvariantCulture;
IFormatProvider provider = invariantCulture;
handler = new StringBuilder.AppendInterpolatedStringHandler(34, 1, stringBuilder, invariantCulture);
handler.AppendLiteral(".lvb: implausible layer-set count ");
handler.AppendFormatted(num2);
stringBuilder2.AppendLine(provider, ref handler);
return list;
}
for (int i = 0; i < num2; i++)
{
int num4 = num3 + i * 28;
list.Add((BitConverter.ToUInt32(data, num4), BitConverter.ToUInt32(data, num4 + 12)));
}
stringBuilder = sb;
StringBuilder stringBuilder3 = stringBuilder;
invariantCulture = CultureInfo.InvariantCulture;
IFormatProvider provider2 = invariantCulture;
handler = new StringBuilder.AppendInterpolatedStringHandler(17, 1, stringBuilder, invariantCulture);
handler.AppendLiteral(".lvb: ");
handler.AppendFormatted(num2);
handler.AppendLiteral(" layer sets");
stringBuilder3.AppendLine(provider2, ref handler);
return list;
}
private static List<List<uint>> ParseLgbLayerSetReferences(byte[]? data)
{
List<List<uint>> list = new List<List<uint>>();
if (data == null || data.Length < 36 || !HasMagic(data, 0, "LGB1") || !HasMagic(data, 12, "LGP1"))
{
return list;
}
int num = 20 + BitConverter.ToInt32(data, 28);
int num2 = BitConverter.ToInt32(data, 32);
bool flag = ((num2 < 0 || num2 > 100000) ? true : false);
if (flag || num < 0 || num + num2 * 4 > data.Length)
{
return list;
}
for (int i = 0; i < num2; i++)
{
List<uint> list2 = new List<uint>();
list.Add(list2);
int num3 = num + BitConverter.ToInt32(data, num + i * 4);
if (num3 < 0 || num3 + 36 > data.Length)
{
continue;
}
int num4 = num3 + BitConverter.ToInt32(data, num3 + 20);
if (num4 <= num3 || num4 + 12 > data.Length)
{
continue;
}
int num5 = num4 + BitConverter.ToInt32(data, num4 + 4);
int num6 = BitConverter.ToInt32(data, num4 + 8);
flag = ((num6 < 1 || num6 > 4096) ? true : false);
if (!flag && num5 >= 0 && num5 + num6 * 4 <= data.Length)
{
for (int j = 0; j < num6; j++)
{
list2.Add(BitConverter.ToUInt32(data, num5 + j * 4));
}
}
}
return list;
}
private static bool HasMagic(byte[] data, int at, string magic)
{
for (int i = 0; i < magic.Length; i++)
{
if (data[at + i] != (byte)magic[i])
{
return false;
}
}
return true;
}
private string RunGuarded(Func<string> probe)
{
try
{
return probe();
}
catch (Exception ex)
{
_logger.LogError(ex, "Data probe failed");
return $"probe failed: {ex}";
}
}
}