mirror of
https://github.com/4sval/FModel.git
synced 2026-04-19 08:07:43 -05:00
fixed & re-added valorant live
This commit is contained in:
parent
d536df54ef
commit
9413e68b5b
|
|
@ -1,17 +1,22 @@
|
|||
using System;
|
||||
using CUE4Parse.UE4.Exceptions;
|
||||
using CUE4Parse.UE4.Readers;
|
||||
|
||||
using FModel.Settings;
|
||||
|
||||
using Ionic.Zlib;
|
||||
|
||||
using RestSharp;
|
||||
|
||||
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.UE4.Exceptions;
|
||||
using CUE4Parse.UE4.Readers;
|
||||
using FModel.Settings;
|
||||
using Ionic.Zlib;
|
||||
using RestSharp;
|
||||
|
||||
namespace FModel.ViewModels.ApiEndpoints
|
||||
{
|
||||
|
|
@ -19,9 +24,7 @@ namespace FModel.ViewModels.ApiEndpoints
|
|||
{
|
||||
private const string _URL = "https://fmodel.fortnite-api.com/valorant/v2/manifest";
|
||||
|
||||
public ValorantApiEndpoint(IRestClient client) : base(client)
|
||||
{
|
||||
}
|
||||
public ValorantApiEndpoint(IRestClient client) : base(client) { }
|
||||
|
||||
public async Task<VManifest> GetManifestAsync(CancellationToken token)
|
||||
{
|
||||
|
|
@ -30,10 +33,7 @@ namespace FModel.ViewModels.ApiEndpoints
|
|||
return new VManifest(response.RawBytes);
|
||||
}
|
||||
|
||||
public VManifest GetManifest(CancellationToken token)
|
||||
{
|
||||
return GetManifestAsync(token).GetAwaiter().GetResult();
|
||||
}
|
||||
public VManifest GetManifest(CancellationToken token) => GetManifestAsync(token).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public class VManifest
|
||||
|
|
@ -43,9 +43,7 @@ namespace FModel.ViewModels.ApiEndpoints
|
|||
public readonly VChunk[] Chunks;
|
||||
public readonly VPak[] Paks;
|
||||
|
||||
public VManifest(byte[] data) : this(new FByteArchive("CompressedValorantManifest", data))
|
||||
{
|
||||
}
|
||||
public VManifest(byte[] data) : this(new FByteArchive("CompressedValorantManifest", data)) { }
|
||||
|
||||
private VManifest(FArchive Ar)
|
||||
{
|
||||
|
|
@ -55,13 +53,14 @@ namespace FModel.ViewModels.ApiEndpoints
|
|||
var compressedBuffer = Ar.ReadBytes((int) Header.CompressedSize);
|
||||
var uncompressedBuffer = ZlibStream.UncompressBuffer(compressedBuffer);
|
||||
if (uncompressedBuffer.Length != Header.UncompressedSize)
|
||||
{
|
||||
throw new ParserException(Ar, $"Decompression failed, {uncompressedBuffer.Length} != {Header.UncompressedSize}");
|
||||
}
|
||||
|
||||
using var manifest = new FByteArchive("UncompressedValorantManifest", uncompressedBuffer);
|
||||
Chunks = manifest.ReadArray(Header.ChunkCount, manifest.Read<VChunk>);
|
||||
Paks = manifest.ReadArray(Header.PakCount, () => new VPak(manifest));
|
||||
Chunks = manifest.ReadArray<VChunk>((int) Header.ChunkCount);
|
||||
Paks = manifest.ReadArray((int) Header.PakCount, () => new VPak(manifest));
|
||||
|
||||
if (manifest.Position != manifest.Length)
|
||||
throw new ParserException(manifest, $"Parsing failed, {manifest.Position} != {manifest.Length}");
|
||||
}
|
||||
|
||||
_client = new HttpClient(new HttpClientHandler
|
||||
|
|
@ -77,42 +76,44 @@ namespace FModel.ViewModels.ApiEndpoints
|
|||
});
|
||||
}
|
||||
|
||||
public async Task PrefetchChunk(VChunk chunk, CancellationToken cancellationToken)
|
||||
public async ValueTask PrefetchChunk(VChunk chunk, CancellationToken cancellationToken)
|
||||
{
|
||||
var chunkPath = Path.Combine(UserSettings.Default.OutputDirectory, ".data", $"{chunk.Id}.chunk");
|
||||
if (File.Exists(chunkPath))
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (File.Exists(chunkPath)) return;
|
||||
using var response = await _client.GetAsync(chunk.GetUrl(), cancellationToken).ConfigureAwait(false);
|
||||
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get, chunk.GetUrl());
|
||||
using var response = await _client.SendAsync(request, cancellationToken).ConfigureAwait(false);
|
||||
if (response.StatusCode == HttpStatusCode.OK)
|
||||
{
|
||||
var chunkBytes = await response.Content.ReadAsByteArrayAsync(cancellationToken).ConfigureAwait(false);
|
||||
await using var fileStream = new FileStream(chunkPath, FileMode.Create, FileAccess.Write, FileShare.Read);
|
||||
await fileStream.WriteAsync(chunkBytes, cancellationToken).ConfigureAwait(false);
|
||||
await response.Content.CopyToAsync(fileStream, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<byte[]> GetChunkBytes(VChunk chunk, CancellationToken cancellationToken)
|
||||
{
|
||||
byte[] chunkBytes = null;
|
||||
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 fileStream = new FileStream(chunkPath, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||
await fileStream.ReadAsync(chunkBytes, cancellationToken).ConfigureAwait(false);
|
||||
await using var fs = new FileStream(chunkPath, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||
await fs.ReadAsync(chunkBytes, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get, chunk.GetUrl());
|
||||
using var response = await _client.SendAsync(request, cancellationToken).ConfigureAwait(false);
|
||||
if (response.StatusCode != HttpStatusCode.OK) return chunkBytes;
|
||||
chunkBytes = await response.Content.ReadAsByteArrayAsync(cancellationToken).ConfigureAwait(false);
|
||||
await using var fileStream = new FileStream(chunkPath, FileMode.Create, FileAccess.Write, FileShare.Read);
|
||||
await fileStream.WriteAsync(chunkBytes, cancellationToken).ConfigureAwait(false);
|
||||
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;
|
||||
|
|
@ -130,25 +131,25 @@ namespace FModel.ViewModels.ApiEndpoints
|
|||
public readonly ulong ManifestId;
|
||||
public readonly uint UncompressedSize;
|
||||
public readonly uint CompressedSize;
|
||||
public readonly int ChunkCount;
|
||||
public readonly int PakCount;
|
||||
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<int>();
|
||||
PakCount = Ar.Read<int>();
|
||||
GameVersion = Ar.ReadString();
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -165,10 +166,10 @@ namespace FModel.ViewModels.ApiEndpoints
|
|||
Id = Ar.Read<ulong>();
|
||||
Size = Ar.Read<uint>();
|
||||
ChunkIndices = Ar.ReadArray<uint>(Ar.Read<int>());
|
||||
Name = Ar.ReadString();
|
||||
Name = Encoding.ASCII.GetString(Ar.ReadBytes(Ar.ReadByte()));
|
||||
}
|
||||
|
||||
public string GetFullName() => $"E:/RiotGames/Valorant/ShooterGame/Content/Paks/{Name}";
|
||||
public string GetFullName() => $"ValorantLive/ShooterGame/Content/Paks/{Name}";
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
|
|
@ -180,14 +181,17 @@ namespace FModel.ViewModels.ApiEndpoints
|
|||
public string GetUrl() => $"https://fmodel.fortnite-api.com/valorant/v2/chunks/{Id}";
|
||||
}
|
||||
|
||||
public class VPakStream : Stream
|
||||
public class VPakStream : Stream, ICloneable
|
||||
{
|
||||
private readonly VManifest _manifest;
|
||||
private readonly int _pakIndex;
|
||||
private readonly VChunk[] _chunks;
|
||||
|
||||
public VPakStream(VManifest manifest, int pakIndex)
|
||||
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];
|
||||
|
|
@ -199,36 +203,37 @@ namespace FModel.ViewModels.ApiEndpoints
|
|||
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 < 0) return 0;
|
||||
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;
|
||||
var chunkBytes = _chunks[i].Size - startPos;
|
||||
var chunkData = await _manifest.GetChunkBytes(_chunks[i], cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (bytesLeft <= chunkBytes)
|
||||
{
|
||||
Unsafe.CopyBlockUnaligned(ref buffer[bytesRead + offset], ref chunkData[startPos], (uint) bytesLeft);
|
||||
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;
|
||||
bytesRead += (int)chunkBytes;
|
||||
startPos = 0u;
|
||||
|
||||
if (++i == _chunks.Length)
|
||||
{
|
||||
break;
|
||||
}
|
||||
if (++i == _chunks.Length) break;
|
||||
}
|
||||
|
||||
_position += bytesRead;
|
||||
|
|
@ -238,25 +243,24 @@ namespace FModel.ViewModels.ApiEndpoints
|
|||
private async Task PrefetchAsync(int i, uint startPos, long count, CancellationToken cancellationToken, int concurrentDownloads = 4)
|
||||
{
|
||||
var tasks = new List<Task>();
|
||||
using (var s = new SemaphoreSlim(concurrentDownloads))
|
||||
var s = new SemaphoreSlim(concurrentDownloads);
|
||||
while (count > 0)
|
||||
{
|
||||
while (count > 0)
|
||||
{
|
||||
await s.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
await s.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var chunk = _chunks[i++];
|
||||
tasks.Add(PrefetchChunkAsync(chunk));
|
||||
s.Release();
|
||||
var chunk = _chunks[i++];
|
||||
tasks.Add(PrefetchChunkAsync(chunk));
|
||||
|
||||
if (i == _chunks.Length) break;
|
||||
count -= chunk.Size - startPos;
|
||||
startPos = 0u;
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -265,11 +269,7 @@ namespace FModel.ViewModels.ApiEndpoints
|
|||
for (var i = 0; i < _chunks.Length; i++)
|
||||
{
|
||||
var size = _chunks[i].Size;
|
||||
if (position < size)
|
||||
{
|
||||
return (i, (uint) position);
|
||||
}
|
||||
|
||||
if (position < size) return (i, (uint) position);
|
||||
position -= size;
|
||||
}
|
||||
|
||||
|
|
@ -283,9 +283,7 @@ namespace FModel.ViewModels.ApiEndpoints
|
|||
set
|
||||
{
|
||||
if (value >= Length || value < 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
throw new ArgumentOutOfRangeException(nameof(value));
|
||||
|
||||
_position = value;
|
||||
}
|
||||
|
|
@ -298,7 +296,7 @@ namespace FModel.ViewModels.ApiEndpoints
|
|||
SeekOrigin.Begin => offset,
|
||||
SeekOrigin.Current => offset + _position,
|
||||
SeekOrigin.End => Length + offset,
|
||||
_ => throw new ArgumentOutOfRangeException()
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(offset))
|
||||
};
|
||||
return _position;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ namespace FModel.ViewModels
|
|||
yield return GetUnrealEngineGame("Snoek", "\\StateOfDecay2\\Content\\Paks");
|
||||
yield return GetUnrealEngineGame("a99769d95d8f400baad1f67ab5dfe508", "\\Core\\Platform\\Content\\Paks");
|
||||
yield return GetRiotGame("VALORANT", "ShooterGame\\Content\\Paks");
|
||||
// yield return new DetectedGame {GameName = "Valorant [LIVE]", GameDirectory = Constants._VAL_LIVE_TRIGGER};
|
||||
yield return new DetectedGame {GameName = "Valorant [LIVE]", GameDirectory = Constants._VAL_LIVE_TRIGGER};
|
||||
yield return GetMojangGame("MinecraftDungeons", "\\dungeons\\dungeons\\Dungeons\\Content\\Paks");
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user