|
| 1 | +using System.Buffers.Binary; |
| 2 | +using System.Numerics; |
| 3 | +using EnumsNET; |
| 4 | +using Microsoft.Extensions.FileProviders; |
| 5 | +using Microsoft.Extensions.Logging; |
| 6 | +using QuantumCore.API; |
| 7 | +using QuantumCore.API.Core.Models; |
| 8 | +using QuantumCore.Core.Types; |
| 9 | +using QuantumCore.Game.World; |
| 10 | + |
| 11 | +namespace QuantumCore.Game.Services; |
| 12 | + |
| 13 | +internal sealed class MapAttributeProvider : IMapAttributeProvider |
| 14 | +{ |
| 15 | + private readonly IFileProvider _fileProvider; |
| 16 | + private readonly ILogger<MapAttributeProvider> _logger; |
| 17 | + |
| 18 | + public MapAttributeProvider(IFileProvider fileProvider, ILogger<MapAttributeProvider> logger) |
| 19 | + { |
| 20 | + _fileProvider = fileProvider; |
| 21 | + _logger = logger; |
| 22 | + } |
| 23 | + |
| 24 | + public async Task<IMapAttributeSet?> GetAttributesAsync(string mapName, Coordinates position, uint mapWidth, |
| 25 | + uint mapHeight, CancellationToken cancellationToken = default) |
| 26 | + { |
| 27 | + var attrPath = $"maps/{mapName}/server_attr"; |
| 28 | + var file = _fileProvider.GetFileInfo(attrPath); |
| 29 | + if (!file.Exists) |
| 30 | + { |
| 31 | + _logger.LogDebug("No server_attr file found for map {Map}", mapName); |
| 32 | + return null; |
| 33 | + } |
| 34 | + |
| 35 | + _logger.LogDebug("Loading cell attributes from file {ServerAttrPath}", attrPath); |
| 36 | + |
| 37 | + await using var stream = file.CreateReadStream(); |
| 38 | + |
| 39 | + // header contains length and width in sectree units (which are squares of 128x128 attr cells) |
| 40 | + var headerBuffer = new byte[2 * sizeof(int)]; |
| 41 | + await stream.ReadExactlyAsync(headerBuffer, cancellationToken); |
| 42 | + var sectreesWidth = BinaryPrimitives.ReadInt32LittleEndian(headerBuffer.AsSpan(0 * sizeof(int), sizeof(int))); |
| 43 | + var sectreesHeight = BinaryPrimitives.ReadInt32LittleEndian(headerBuffer.AsSpan(1 * sizeof(int), sizeof(int))); |
| 44 | + |
| 45 | + var expectedSectreesWidth = (int)(mapWidth * Map.MapUnit / MapAttributeSet.SectreeSize); |
| 46 | + var expectedSectreesHeight = (int)(mapHeight * Map.MapUnit / MapAttributeSet.SectreeSize); |
| 47 | + if (sectreesWidth != expectedSectreesWidth || sectreesHeight != expectedSectreesHeight) |
| 48 | + { |
| 49 | + _logger.LogWarning( |
| 50 | + "{ServerAttrPath} dimensions ({SectreeWidth}x{SectreeHeight}) do not match atlasinfo.txt dimensions ({ExpectedWidth}x{ExpectedHeight})", |
| 51 | + attrPath, sectreesWidth, sectreesHeight, expectedSectreesWidth, expectedSectreesHeight); |
| 52 | + } |
| 53 | + |
| 54 | + var sectrees = new MapAttributeSectree?[sectreesHeight, sectreesWidth]; |
| 55 | + var lzoDecompressor = new Lzo(MapAttributeSet.CellsPerSectree * sizeof(uint)); |
| 56 | + var intBuffer = new byte[4]; |
| 57 | + |
| 58 | + // info for debug logging only |
| 59 | + long cellsWithKnownFlags = 0; |
| 60 | + var unknownFlagCounts = new Dictionary<int, long>(); |
| 61 | + var flagCountsDebugInfo = Enum.GetValues<EMapAttribute>() |
| 62 | + .Where(x => x != EMapAttribute.None) |
| 63 | + .ToDictionary(attr => attr, _ => 0L); |
| 64 | + |
| 65 | + for (var y = 0; y < sectreesHeight; y++) |
| 66 | + { |
| 67 | + for (var x = 0; x < sectreesWidth; x++) |
| 68 | + { |
| 69 | + await stream.ReadExactlyAsync(intBuffer, cancellationToken); |
| 70 | + var blockSize = BinaryPrimitives.ReadInt32LittleEndian(intBuffer); |
| 71 | + |
| 72 | + var compressedSectree = new byte[blockSize]; |
| 73 | + await stream.ReadExactlyAsync(compressedSectree, cancellationToken); |
| 74 | + |
| 75 | + var decompressedSectree = lzoDecompressor.DecodeRaw(compressedSectree); |
| 76 | + if (decompressedSectree.Length != MapAttributeSet.CellsPerSectree * sizeof(uint)) |
| 77 | + { |
| 78 | + _logger.LogWarning("{ServerAttrPath} failed to decode sectree ({X}, {Y}): unexpected size {DecompressedSectorLength}", attrPath, x, y, decompressedSectree.Length); |
| 79 | + sectrees[y, x] = MapAttributeSectree.Empty; |
| 80 | + continue; |
| 81 | + } |
| 82 | + |
| 83 | + var allCellsAttrs = new EMapAttribute[MapAttributeSet.CellsPerSectree]; |
| 84 | + for (var i = 0; i < allCellsAttrs.Length; i++) |
| 85 | + { |
| 86 | + var rawCellFlags = BinaryPrimitives.ReadUInt32LittleEndian( |
| 87 | + decompressedSectree.AsSpan(i * sizeof(uint), sizeof(uint)) |
| 88 | + ); |
| 89 | + // all set bits of `rawCellFlags` are preserved when cast to enum - individual flags can be extracted with HasFlag() |
| 90 | + var cellAttrFlags = (EMapAttribute)rawCellFlags; |
| 91 | + allCellsAttrs[i] = cellAttrFlags; |
| 92 | + |
| 93 | + // saving debug info if server_attr is corrupted or wrong format |
| 94 | + if (_logger.IsEnabled(LogLevel.Debug)) { |
| 95 | + var cellHasValidFlag = false; |
| 96 | + foreach (var flag in flagCountsDebugInfo.Keys.ToList()) |
| 97 | + { |
| 98 | + if (cellAttrFlags.HasFlag(flag)) |
| 99 | + { |
| 100 | + flagCountsDebugInfo[flag]++; |
| 101 | + cellHasValidFlag = true; |
| 102 | + } |
| 103 | + } |
| 104 | + |
| 105 | + if (cellHasValidFlag) cellsWithKnownFlags++; |
| 106 | + |
| 107 | + var unknownFlags = cellAttrFlags & |
| 108 | + ~flagCountsDebugInfo.Keys.Aggregate(EMapAttribute.None, |
| 109 | + (acc, attr) => acc | attr); |
| 110 | + if (unknownFlags != EMapAttribute.None) |
| 111 | + { |
| 112 | + var remaining = (uint)unknownFlags; |
| 113 | + while (remaining != 0) |
| 114 | + { |
| 115 | + var lsb = remaining & ~(remaining - 1); |
| 116 | + var bitPosition = BitOperations.TrailingZeroCount(lsb); |
| 117 | + unknownFlagCounts.TryGetValue(bitPosition, out var count); |
| 118 | + unknownFlagCounts[bitPosition] = count + 1; |
| 119 | + remaining &= remaining - 1; |
| 120 | + } |
| 121 | + } |
| 122 | + } |
| 123 | + } |
| 124 | + |
| 125 | + sectrees[y, x] = new MapAttributeSectree(allCellsAttrs); |
| 126 | + } |
| 127 | + } |
| 128 | + |
| 129 | + if (_logger.IsEnabled(LogLevel.Debug)) |
| 130 | + { |
| 131 | + var totalCells = (long)sectreesHeight * sectreesWidth * MapAttributeSet.CellsPerSectree; |
| 132 | + var percentage = 100.0 * cellsWithKnownFlags / totalCells; |
| 133 | + _logger.LogDebug( "Loaded {ServerAttrPath} {CellSize}x{CellSize2} cell attributes: {Summary} (cells with known flags: {NonZero}/{Total} = {Percentage:F2}%)", |
| 134 | + attrPath, MapAttributeSet.CellSize, MapAttributeSet.CellSize, |
| 135 | + string.Join(" ", flagCountsDebugInfo.Select(kv => $"{kv.Key}={kv.Value}")), |
| 136 | + cellsWithKnownFlags, totalCells, percentage); |
| 137 | + |
| 138 | + if (unknownFlagCounts.Count > 0) |
| 139 | + { |
| 140 | + _logger.LogWarning("{ServerAttrPath} contains cells with unknown flag bits (LSB being bit 0): {UnknownSummary}", |
| 141 | + attrPath, string.Join(", ", unknownFlagCounts.Select(kv => $"bit {kv.Key} found in {kv.Value} cells"))); |
| 142 | + } |
| 143 | + } |
| 144 | + |
| 145 | + return new MapAttributeSet(sectreesWidth, sectreesHeight, position, sectrees); |
| 146 | + } |
| 147 | + |
| 148 | + private sealed class MapAttributeSet : IMapAttributeSet |
| 149 | + { |
| 150 | + internal const int SectreeSize = 6400; // 64m x 64m |
| 151 | + internal const int CellSize = 50; // 50cm x 50cm - basically a quarter of the size of a full world cell (2m x 2m) |
| 152 | + internal const int CellsPerAxis = SectreeSize / CellSize; |
| 153 | + internal const int CellsPerSectree = CellsPerAxis * CellsPerAxis; |
| 154 | + |
| 155 | + private readonly MapAttributeSectree?[,] _sectreesAttrs; |
| 156 | + private readonly int _sectreesWidth; |
| 157 | + private readonly int _sectreesHeight; |
| 158 | + private readonly Coordinates _baseCoords; |
| 159 | + |
| 160 | + public MapAttributeSet(int sectreesWidth, int sectreesHeight, Coordinates basePosition, |
| 161 | + MapAttributeSectree?[,] sectreesAttrs) |
| 162 | + { |
| 163 | + _sectreesWidth = sectreesWidth; |
| 164 | + _sectreesHeight = sectreesHeight; |
| 165 | + _baseCoords = basePosition; |
| 166 | + _sectreesAttrs = sectreesAttrs; |
| 167 | + } |
| 168 | + |
| 169 | + public EMapAttribute GetAttribute(Coordinates coords) |
| 170 | + { |
| 171 | + if (!TryLocate(coords, out var locatedSectreeAttrs, out var cellX, out var cellY) || locatedSectreeAttrs is null) |
| 172 | + { |
| 173 | + return EMapAttribute.None; |
| 174 | + } |
| 175 | + |
| 176 | + return locatedSectreeAttrs.Get(cellX, cellY); |
| 177 | + } |
| 178 | + |
| 179 | + private bool TryLocate(Coordinates coords, out MapAttributeSectree? sectreeAttrs, out int cellX, out int cellY) |
| 180 | + { |
| 181 | + sectreeAttrs = null; |
| 182 | + cellX = 0; |
| 183 | + cellY = 0; |
| 184 | + |
| 185 | + var relativeCoords = coords - _baseCoords; |
| 186 | + |
| 187 | + // find which sectree covers the x y relative map coords |
| 188 | + var sectreeIndexX = relativeCoords.X / SectreeSize; |
| 189 | + var sectreeIndexY = relativeCoords.Y / SectreeSize; |
| 190 | + if (sectreeIndexX >= _sectreesWidth || sectreeIndexY >= _sectreesHeight) |
| 191 | + { |
| 192 | + return false; |
| 193 | + } |
| 194 | + sectreeAttrs = _sectreesAttrs[sectreeIndexY, sectreeIndexX]; |
| 195 | + |
| 196 | + // find which cell of the sectree covers the x y relative map coords |
| 197 | + cellX = (int)((relativeCoords.X % SectreeSize) / CellSize); |
| 198 | + cellY = (int)((relativeCoords.Y % SectreeSize) / CellSize); |
| 199 | + |
| 200 | + return cellX < CellsPerAxis && cellY < CellsPerAxis; |
| 201 | + } |
| 202 | + } |
| 203 | + |
| 204 | + private sealed class MapAttributeSectree(EMapAttribute[] values) |
| 205 | + { |
| 206 | + public static MapAttributeSectree Empty { get; } = new(new EMapAttribute[MapAttributeSet.CellsPerSectree]); |
| 207 | + |
| 208 | + public EMapAttribute Get(int x, int y) |
| 209 | + { |
| 210 | + return values[y * MapAttributeSet.CellsPerAxis + x]; |
| 211 | + } |
| 212 | + } |
| 213 | +} |
0 commit comments