qstbak/SmartNav/SmartNav.Data/LgbZoneBoundarySource.cs
2026-08-19 13:19:57 +10:00

168 lines
5.6 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Numerics;
using System.Text;
using Microsoft.Extensions.Logging;
using SmartNav.Model.Navigation;
namespace SmartNav.Data;
public sealed class LgbZoneBoundarySource : IZoneBoundarySource
{
private sealed record LgbData(List<ExitRangeEntry> ExitRanges, Dictionary<(ushort TerritoryId, uint InstanceId), PopRangeEntry> PopRangeIndex);
private const uint BoundaryFileMagic = 1112425562u;
private const int BoundaryFileFormatVersion = 1;
private const string CacheFileName = "zone-boundary-cache.bin";
private const int MaxPreallocatedEntries = 131072;
private readonly ILogger<LgbZoneBoundarySource> _logger;
private readonly string? _overrideDirectory;
private readonly string? _gameVersion;
private readonly object _buildLock = new object();
private IReadOnlyList<ZoneBoundary>? _boundaries;
public LgbZoneBoundarySource(ILogger<LgbZoneBoundarySource> logger, SmartNavDataOptions? dataOptions = null)
{
_logger = logger;
_overrideDirectory = dataOptions?.UserOverrideDirectory?.FullName;
_gameVersion = dataOptions?.GameVersion;
}
public IReadOnlyList<ZoneBoundary> GetBoundaries()
{
lock (_buildLock)
{
return _boundaries ?? (_boundaries = Build());
}
}
public void Invalidate()
{
lock (_buildLock)
{
_boundaries = null;
}
}
private IReadOnlyList<ZoneBoundary> Build()
{
try
{
var (lgbData, text) = LoadRawData();
if (lgbData == null)
{
return Array.Empty<ZoneBoundary>();
}
List<ZoneBoundary> list = ZoneBoundaryDerivation.Derive(lgbData.ExitRanges, lgbData.PopRangeIndex, _logger);
ZoneBoundaryDerivation.ApplyOverrides(list, ZoneBoundaryOverrides.FlyingPairs, ZoneBoundaryOverrides.PositionOverrides, _logger);
_logger.LogDebug("Derived {Count} zone boundaries from {ExitCount} ExitRanges ({Source})", list.Count, lgbData.ExitRanges.Count, text);
return list;
}
catch (Exception exception)
{
_logger.LogError(exception, "Zone boundary derivation failed - building graph without boundary edges");
return Array.Empty<ZoneBoundary>();
}
}
private (LgbData? Data, string Source) LoadRawData()
{
LgbData lgbData = TryLoadOverrideFile("override file");
if (lgbData != null)
{
return (Data: lgbData, Source: "override file");
}
return (Data: TryLoadEmbedded("embedded resource"), Source: "embedded resource");
}
private LgbData? TryLoadOverrideFile(string source)
{
if (string.IsNullOrEmpty(_overrideDirectory))
{
return null;
}
string text = Path.Combine(_overrideDirectory, "zone-boundary-cache.bin");
if (!File.Exists(text))
{
return null;
}
try
{
using Stream stream = File.OpenRead(text);
LgbData lgbData = ReadCache(stream, source);
if (lgbData.ExitRanges.Count == 0 && lgbData.PopRangeIndex.Count == 0)
{
_logger.LogWarning("Zone boundary {Source} {Path} holds no exits and no pops, ignoring it and using the embedded data", source, text);
return null;
}
return lgbData;
}
catch (Exception exception)
{
_logger.LogWarning(exception, "Failed to read zone boundary {Source}, falling back to the embedded data", source);
return null;
}
}
private LgbData? TryLoadEmbedded(string source)
{
try
{
using Stream stream = AssemblyLgbCacheLoader.OpenZoneBoundaryCache();
return ReadCache(stream, source);
}
catch (Exception exception)
{
_logger.LogError(exception, "Failed to read zone boundary {Source} - building graph without boundary edges", source);
return null;
}
}
private LgbData ReadCache(Stream stream, string source)
{
using BinaryReader binaryReader = new BinaryReader(stream, Encoding.UTF8, leaveOpen: true);
if (binaryReader.ReadUInt32() != 1112425562 || binaryReader.ReadInt32() != 1)
{
throw new InvalidDataException("zone boundary " + source + " has an unknown header");
}
string text = binaryReader.ReadString();
if (!string.IsNullOrEmpty(_gameVersion) && text != _gameVersion)
{
_logger.LogInformation("Zone boundary {Source} was built for game version {FileVersion}, running {GameVersion} - using it anyway", source, text, _gameVersion);
}
int num = binaryReader.ReadInt32();
if (num < 0)
{
throw new InvalidDataException($"zone boundary {source} has a negative exit count ({num})");
}
List<ExitRangeEntry> list = new List<ExitRangeEntry>(Math.Min(num, 131072));
for (int i = 0; i < num; i++)
{
list.Add(new ExitRangeEntry(binaryReader.ReadUInt16(), binaryReader.ReadUInt32(), new Vector3(binaryReader.ReadSingle(), binaryReader.ReadSingle(), binaryReader.ReadSingle()), binaryReader.ReadUInt16(), binaryReader.ReadUInt32(), binaryReader.ReadUInt32(), new Vector3(binaryReader.ReadSingle(), binaryReader.ReadSingle(), binaryReader.ReadSingle()), new Vector3(binaryReader.ReadSingle(), binaryReader.ReadSingle(), binaryReader.ReadSingle())));
}
int num2 = binaryReader.ReadInt32();
if (num2 < 0)
{
throw new InvalidDataException($"zone boundary {source} has a negative pop count ({num2})");
}
Dictionary<(ushort, uint), PopRangeEntry> dictionary = new Dictionary<(ushort, uint), PopRangeEntry>(Math.Min(num2, 131072));
for (int j = 0; j < num2; j++)
{
ushort num3 = binaryReader.ReadUInt16();
uint num4 = binaryReader.ReadUInt32();
Vector3 position = new Vector3(binaryReader.ReadSingle(), binaryReader.ReadSingle(), binaryReader.ReadSingle());
dictionary[(num3, num4)] = new PopRangeEntry(num3, num4, position);
}
_logger.LogDebug("Zone boundary data loaded from {Source} ({ExitCount} exits, {PopCount} pops)", source, list.Count, dictionary.Count);
return new LgbData(list, dictionary);
}
}