forked from aly/qstbak
239 lines
6.7 KiB
C#
239 lines
6.7 KiB
C#
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<LgbWorkerSupervisor> _logger;
|
|
|
|
public LgbWorkerSupervisor(IDataManager dataManager, ILogger<LgbWorkerSupervisor> 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<TerritoryEntry> list = BuildTerritoryList();
|
|
List<LevelEntry> 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<TerritoryEntry> BuildTerritoryList()
|
|
{
|
|
HashSet<uint> hashSet = new HashSet<uint>();
|
|
foreach (Aetheryte item in _dataManager.GetExcelSheet<Aetheryte>())
|
|
{
|
|
hashSet.Add(item.Territory.RowId);
|
|
}
|
|
List<TerritoryEntry> list = new List<TerritoryEntry>();
|
|
List<TerritoryEntry> list2 = new List<TerritoryEntry>();
|
|
foreach (TerritoryType item2 in _dataManager.GetExcelSheet<TerritoryType>())
|
|
{
|
|
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<LevelEntry> BuildLevelEntries()
|
|
{
|
|
ExcelSheet<ENpcBase> excelSheet = _dataManager.GetExcelSheet<ENpcBase>();
|
|
ExcelSheet<EObj> excelSheet2 = _dataManager.GetExcelSheet<EObj>();
|
|
List<LevelEntry> list = new List<LevelEntry>();
|
|
foreach (Level item in _dataManager.GetExcelSheet<Level>())
|
|
{
|
|
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
|
|
{
|
|
}
|
|
}
|
|
}
|