FModel/FModel/ViewModels/ApiEndpoints/ValorantApiEndpoint.cs
Marlon 4be7d4c3fb
small rework
removed DotNetZip
make use of Oodle.NET & Zlib-ng.NET
updated EpicManifestParser
minor optimizations
2024-04-04 06:18:49 +02:00

312 lines
10 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using CUE4Parse.Compression;
using CUE4Parse.UE4.Exceptions;
using CUE4Parse.UE4.Readers;
using FModel.Framework;
using FModel.Settings;
using RestSharp;
namespace FModel.ViewModels.ApiEndpoints;
public class ValorantApiEndpoint : AbstractApiProvider
{
private const string _URL = "https://fmodel.fortnite-api.com/valorant/v2/manifest";
public ValorantApiEndpoint(RestClient client) : base(client) { }
public async Task<VManifest> GetManifestAsync(CancellationToken token)
{
var request = new FRestRequest(_URL);
var response = await _client.ExecuteAsync(request, token).ConfigureAwait(false);
return new VManifest(response.RawBytes);
}
public VManifest GetManifest(CancellationToken token) => GetManifestAsync(token).GetAwaiter().GetResult();
}
public class VManifest
{
private readonly HttpClient _client;
public readonly VHeader Header;
public readonly VChunk[] Chunks;
public readonly VPak[] Paks;
public VManifest(byte[] data) : this(new FByteArchive("CompressedValorantManifest", data)) { }
private VManifest(FArchive Ar)
{
using (Ar)
{
Header = new VHeader(Ar);
var compressedBuffer = Ar.ReadBytes((int) Header.CompressedSize);
var uncompressedBuffer = new byte[(int)Header.UncompressedSize];
ZlibHelper.Decompress(compressedBuffer, 0, compressedBuffer.Length, uncompressedBuffer, 0, uncompressedBuffer.Length);
var manifestAr = new FByteArchive("UncompressedValorantManifest", uncompressedBuffer);
Chunks = manifestAr.ReadArray<VChunk>((int) Header.ChunkCount);
Paks = manifestAr.ReadArray((int) Header.PakCount, () => new VPak(manifestAr));
if (manifestAr.Position != manifestAr.Length)
throw new ParserException(manifestAr, $"Parsing failed, {manifestAr.Position} != {manifestAr.Length}");
}
_client = new HttpClient(new HttpClientHandler
{
UseProxy = false,
UseCookies = false,
AutomaticDecompression = DecompressionMethods.All,
CheckCertificateRevocationList = false,
PreAuthenticate = false,
MaxConnectionsPerServer = 1337,
UseDefaultCredentials = false,
AllowAutoRedirect = false
});
}
public async ValueTask PrefetchChunk(VChunk chunk, CancellationToken cancellationToken)
{
var chunkPath = Path.Combine(UserSettings.Default.OutputDirectory, ".data", $"{chunk.Id}.chunk");
if (File.Exists(chunkPath)) return;
using var response = await _client.GetAsync(chunk.GetUrl(), cancellationToken).ConfigureAwait(false);
if (response.StatusCode == HttpStatusCode.OK)
{
await using var fileStream = new FileStream(chunkPath, FileMode.Create, FileAccess.Write, FileShare.Read);
await response.Content.CopyToAsync(fileStream, cancellationToken).ConfigureAwait(false);
}
}
public async Task<byte[]> GetChunkBytes(VChunk chunk, CancellationToken cancellationToken)
{
var chunkPath = Path.Combine(UserSettings.Default.OutputDirectory, ".data", $"{chunk.Id}.chunk");
byte[] chunkBytes;
if (File.Exists(chunkPath))
{
chunkBytes = new byte[chunk.Size];
await using var fs = new FileStream(chunkPath, FileMode.Open, FileAccess.Read, FileShare.Read);
await fs.ReadAsync(chunkBytes, cancellationToken).ConfigureAwait(false);
}
else
{
using var response = await _client.GetAsync(chunk.GetUrl(), cancellationToken).ConfigureAwait(false);
if (response.StatusCode == HttpStatusCode.OK)
{
chunkBytes = await response.Content.ReadAsByteArrayAsync(cancellationToken).ConfigureAwait(false);
await using var fs = new FileStream(chunkPath, FileMode.Create, FileAccess.Write, FileShare.Read);
await response.Content.CopyToAsync(fs, cancellationToken).ConfigureAwait(false);
}
else
{
chunkBytes = null; // Maybe add logging?
}
}
return chunkBytes;
}
public Stream GetPakStream(int index) => new VPakStream(this, index);
}
public readonly struct VHeader
{
private const uint _MAGIC = 0xC3D088F7u;
public readonly uint Magic;
public readonly uint HeaderSize;
public readonly ulong ManifestId;
public readonly uint UncompressedSize;
public readonly uint CompressedSize;
public readonly uint ChunkCount;
public readonly uint PakCount;
public readonly string GameVersion;
public VHeader(FArchive Ar)
{
Magic = Ar.Read<uint>();
if (Magic != _MAGIC)
throw new ParserException(Ar, "Invalid manifest magic");
HeaderSize = Ar.Read<uint>();
ManifestId = Ar.Read<ulong>();
UncompressedSize = Ar.Read<uint>();
CompressedSize = Ar.Read<uint>();
ChunkCount = Ar.Read<uint>();
PakCount = Ar.Read<uint>();
var gameVersionLength = Ar.ReadByte();
GameVersion = gameVersionLength == 0 ? null : Encoding.ASCII.GetString(Ar.ReadBytes(gameVersionLength));
Ar.Position = HeaderSize;
}
}
public readonly struct VPak
{
public readonly ulong Id;
public readonly uint Size;
public readonly uint[] ChunkIndices;
public readonly string Name;
public VPak(FArchive Ar)
{
Id = Ar.Read<ulong>();
Size = Ar.Read<uint>();
ChunkIndices = Ar.ReadArray<uint>(Ar.Read<int>());
Name = Encoding.ASCII.GetString(Ar.ReadBytes(Ar.ReadByte()));
}
public string GetFullName() => $"ValorantLive/ShooterGame/Content/Paks/{Name}";
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public readonly struct VChunk
{
public readonly ulong Id;
public readonly uint Size;
public string GetUrl() => $"https://fmodel.fortnite-api.com/valorant/v2/chunks/{Id}";
}
public class VPakStream : Stream, ICloneable
{
private readonly VManifest _manifest;
private readonly int _pakIndex;
private readonly VChunk[] _chunks;
public VPakStream(VManifest manifest, int pakIndex, long position = 0L)
{
_manifest = manifest;
_pakIndex = pakIndex;
_position = position;
var pak = manifest.Paks[pakIndex];
_chunks = new VChunk[pak.ChunkIndices.Length];
for (var i = 0; i < _chunks.Length; i++)
{
_chunks[i] = manifest.Chunks[pak.ChunkIndices[i]];
}
Length = pak.Size;
}
public object Clone() => new VPakStream(_manifest, _pakIndex, _position);
public override int Read(byte[] buffer, int offset, int count) => ReadAsync(buffer, offset, count, CancellationToken.None).GetAwaiter().GetResult();
public override async Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
{
var (i, startPos) = GetChunkIndex(_position);
if (i == -1) return 0;
await PrefetchAsync(i, startPos, count, cancellationToken).ConfigureAwait(false);
var bytesRead = 0;
while (true)
{
var chunk = _chunks[i];
var chunkData = await _manifest.GetChunkBytes(chunk, cancellationToken).ConfigureAwait(false);
var chunkBytes = chunk.Size - startPos;
var bytesLeft = count - bytesRead;
if (bytesLeft <= chunkBytes)
{
Unsafe.CopyBlockUnaligned(ref buffer[bytesRead + offset], ref chunkData[startPos], (uint) bytesLeft);
bytesRead += bytesLeft;
break;
}
Unsafe.CopyBlockUnaligned(ref buffer[bytesRead + offset], ref chunkData[startPos], chunkBytes);
bytesRead += (int) chunkBytes;
startPos = 0u;
if (++i == _chunks.Length) break;
}
_position += bytesRead;
return bytesRead;
}
private async Task PrefetchAsync(int i, uint startPos, long count, CancellationToken cancellationToken, int concurrentDownloads = 4)
{
var tasks = new List<Task>();
var s = new SemaphoreSlim(concurrentDownloads);
while (count > 0)
{
await s.WaitAsync(cancellationToken).ConfigureAwait(false);
var chunk = _chunks[i++];
tasks.Add(PrefetchChunkAsync(chunk));
if (i == _chunks.Length) break;
count -= chunk.Size - startPos;
startPos = 0u;
}
await Task.WhenAll(tasks).ConfigureAwait(false);
s.Dispose();
async Task PrefetchChunkAsync(VChunk chunk)
{
await _manifest.PrefetchChunk(chunk, cancellationToken).ConfigureAwait(false);
s.Release(); // This is intended
}
}
private (int Index, uint ChunkPos) GetChunkIndex(long position)
{
for (var i = 0; i < _chunks.Length; i++)
{
var size = _chunks[i].Size;
if (position < size) return (i, (uint) position);
position -= size;
}
return (-1, 0u);
}
private long _position;
public override long Position
{
get => _position;
set
{
if (value >= Length || value < 0)
throw new ArgumentOutOfRangeException(nameof(value));
_position = value;
}
}
public override long Seek(long offset, SeekOrigin origin)
{
Position = origin switch
{
SeekOrigin.Begin => offset,
SeekOrigin.Current => offset + _position,
SeekOrigin.End => Length + offset,
_ => throw new ArgumentOutOfRangeException(nameof(offset))
};
return _position;
}
public override long Length { get; }
public override bool CanRead => true;
public override bool CanSeek => true;
public override bool CanWrite => false;
public override void Flush() => throw new NotSupportedException();
public override void SetLength(long value) => throw new NotSupportedException();
public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException();
}