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 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 _logger; private readonly string? _overrideDirectory; private readonly string? _gameVersion; private readonly object _buildLock = new object(); private IReadOnlyList? _boundaries; public LgbZoneBoundarySource(ILogger logger, SmartNavDataOptions? dataOptions = null) { _logger = logger; _overrideDirectory = dataOptions?.UserOverrideDirectory?.FullName; _gameVersion = dataOptions?.GameVersion; } public IReadOnlyList GetBoundaries() { lock (_buildLock) { return _boundaries ?? (_boundaries = Build()); } } public void Invalidate() { lock (_buildLock) { _boundaries = null; } } private IReadOnlyList Build() { try { var (lgbData, text) = LoadRawData(); if (lgbData == null) { return Array.Empty(); } List 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(); } } 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 list = new List(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); } }