168 lines
5.6 KiB
C#
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);
|
|
}
|
|
}
|