using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.IO.Pipes; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Dalamud.Plugin.Services; using Lumina.Excel; using Lumina.Excel.Sheets; using Microsoft.Extensions.Logging; using Questionable.LgbWorker; namespace Questionable.Data; internal sealed class LgbWorkerSupervisor { private const string WorkerExeName = "Questionable.LgbWorker.exe"; private const string WorkerSubDirectory = "lgb-worker"; private const string ManifestFileName = "lgb-worker-manifest.json"; private const string NpcCacheFileName = "npc-position-cache.bin"; private const string BoundaryCacheFileName = "zone-boundary-cache.bin"; private readonly IDataManager _dataManager; private readonly ILogger _logger; public LgbWorkerSupervisor(IDataManager dataManager, ILogger logger) { _dataManager = dataManager; _logger = logger; } public bool RunWorker(string sqpackPath, string gameVersion, string cacheDirectory, string pluginAssemblyDirectory, CancellationToken ct) { string text = Path.Combine(pluginAssemblyDirectory, "lgb-worker", "Questionable.LgbWorker.exe"); if (!File.Exists(text)) { _logger.LogWarning("LGB worker not found at {Path}, falling back to in-process scan", text); return false; } string npcCachePath = Path.Combine(cacheDirectory, "npc-position-cache.bin"); string boundaryCachePath = Path.Combine(cacheDirectory, "zone-boundary-cache.bin"); string text2 = Path.Combine(cacheDirectory, "lgb-worker-manifest.json"); string text3 = $"Questionable.LgbWorker.{Guid.NewGuid():N}"; Directory.CreateDirectory(cacheDirectory); _logger.LogDebug("Building LGB worker manifest from Excel sheets..."); List list = BuildTerritoryList(); List list2 = BuildLevelEntries(); _logger.LogDebug("Manifest: {TerritoryCount} territories, {LevelCount} level entries", list.Count, list2.Count); LgbWorkerManifest value = new LgbWorkerManifest { SqpackPath = sqpackPath, GameVersion = gameVersion, NpcCachePath = npcCachePath, BoundaryCachePath = boundaryCachePath, Territories = list, LevelEntries = list2 }; File.WriteAllText(text2, JsonSerializer.Serialize(value)); using NamedPipeServerStream namedPipeServerStream = new NamedPipeServerStream(text3, PipeDirection.In, 1, PipeTransmissionMode.Byte, PipeOptions.Asynchronous); int processId = Environment.ProcessId; ProcessStartInfo startInfo = new ProcessStartInfo { FileName = text, Arguments = $"--pipe {text3} --manifest \"{text2}\" --parent-pid {processId.ToString(CultureInfo.InvariantCulture)}", UseShellExecute = false, CreateNoWindow = true, RedirectStandardError = true }; Process process = Process.Start(startInfo); try { if (process == null) { _logger.LogError("Failed to start LGB worker process"); return false; } _logger.LogInformation("LGB worker started (PID {Pid})", process.Id); Task.Run(delegate { try { while (true) { string text4 = process.StandardError.ReadLine(); if (text4 == null) { break; } _logger.LogDebug("[LgbWorker] {Line}", text4); } } catch { } }, ct); _logger.LogDebug("Waiting for LGB worker to connect..."); try { namedPipeServerStream.WaitForConnection(); } catch (IOException exception) { _logger.LogError(exception, "LGB worker pipe connection failed"); process.Kill(); return false; } _logger.LogDebug("LGB worker connected, reading messages..."); try { while (true) { ct.ThrowIfCancellationRequested(); LgbWorkerMessage lgbWorkerMessage = LgbWorkerFraming.Read(namedPipeServerStream); switch (lgbWorkerMessage.Type) { case LgbMessageType.Progress: if (lgbWorkerMessage.Total > 0 && lgbWorkerMessage.Done % (lgbWorkerMessage.Total / 10 + 1) == 0) { _logger.LogDebug("LGB worker progress: {Done}/{Total}", lgbWorkerMessage.Done, lgbWorkerMessage.Total); } break; case LgbMessageType.Final: if (lgbWorkerMessage.Success) { _logger.LogInformation("LGB worker completed in {Duration}ms", lgbWorkerMessage.DurationMs); process.WaitForExit(5000); CleanupManifest(text2); return true; } _logger.LogError("LGB worker reported failure: {Error}", lgbWorkerMessage.Error); process.WaitForExit(5000); CleanupManifest(text2); return false; } } } catch (EndOfStreamException) { process.WaitForExit(5000); _logger.LogError("LGB worker exited without a final message (exit code {Code})", process.ExitCode); CleanupManifest(text2); return false; } catch (OperationCanceledException) { process.Kill(); CleanupManifest(text2); throw; } } finally { if (process != null) { ((IDisposable)process).Dispose(); } } } private List BuildTerritoryList() { HashSet hashSet = new HashSet(); foreach (Aetheryte item in _dataManager.GetExcelSheet()) { hashSet.Add(item.Territory.RowId); } List list = new List(); List list2 = new List(); foreach (TerritoryType item2 in _dataManager.GetExcelSheet()) { if (item2.RowId == 0) { continue; } string text = item2.Bg.ExtractText(); if (!string.IsNullOrEmpty(text) && text.Contains("/level/", StringComparison.Ordinal)) { TerritoryEntry territoryEntry = new TerritoryEntry { RowId = item2.RowId, BgPath = text, IntendedUse = (byte)item2.TerritoryIntendedUse.RowId, HasAetheryte = hashSet.Contains(item2.RowId) }; if (territoryEntry.HasAetheryte) { list.Add(territoryEntry); } else { list2.Add(territoryEntry); } } } list.AddRange(list2); return list; } private List BuildLevelEntries() { ExcelSheet excelSheet = _dataManager.GetExcelSheet(); ExcelSheet excelSheet2 = _dataManager.GetExcelSheet(); List list = new List(); foreach (Level item in _dataManager.GetExcelSheet()) { if (item.RowId != 0 && item.Object.RowId != 0 && item.Territory.IsValid && (excelSheet.HasRow(item.Object.RowId) || excelSheet2.HasRow(item.Object.RowId))) { list.Add(new LevelEntry { ObjectId = item.Object.RowId, TerritoryId = (ushort)item.Territory.RowId, X = item.X, Y = item.Y, Z = item.Z }); } } return list; } private static void CleanupManifest(string path) { try { File.Delete(path); } catch { } } }