mirror of
https://github.com/4sval/FModel.git
synced 2026-08-24 18:34:30 -05:00
Added Valorant [LIVE] & some fixes (#122)
* some fixes * added valorant live support
This commit is contained in:
@@ -136,12 +136,16 @@ namespace FModel.Creator.Bases
|
||||
int x = 20;
|
||||
foreach (var (index, reward) in BookXpSchedule)
|
||||
{
|
||||
if (index == 0 || reward.Count <= 0)
|
||||
if (index == 0 || reward.Count == 0)
|
||||
continue;
|
||||
|
||||
c.DrawText(index.ToString(), new SKPoint(x + (defaultSize / 2), y - 5), paint);
|
||||
|
||||
rarity.Color = reward[0].TheReward.RarityBackgroundColors[0];
|
||||
var theReward = reward[0].TheReward;
|
||||
if (theReward == null)
|
||||
continue;
|
||||
|
||||
rarity.Color = theReward.RarityBackgroundColors[0];
|
||||
c.DrawRect(new SKRect(x, y, x + defaultSize, y + defaultSize), bg);
|
||||
c.DrawBitmap(reward[0].RewardIcon, new SKPoint(x, y), icon);
|
||||
var pathBottom = new SKPath { FillType = SKPathFillType.EvenOdd };
|
||||
|
||||
356
FModel/Grabber/Manifests/ValorantAPIManifest.cs
Normal file
356
FModel/Grabber/Manifests/ValorantAPIManifest.cs
Normal file
@@ -0,0 +1,356 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using FModel.Utils;
|
||||
|
||||
using PakReader;
|
||||
|
||||
namespace FModel.Grabber.Manifests
|
||||
{
|
||||
public class ValorantAPIManifest
|
||||
{
|
||||
private const string _url = "https://fmodel.fortnite-api.com/valorant/v1/manifest";
|
||||
|
||||
private readonly HttpClient _client;
|
||||
private readonly DirectoryInfo _chunkDirectory;
|
||||
|
||||
public readonly ulong Id;
|
||||
public readonly Dictionary<ulong, ValorantChunk> Chunks;
|
||||
public readonly ValorantPak[] Paks;
|
||||
|
||||
public ValorantAPIManifest(byte[] data, DirectoryInfo directoryInfo) : this(new MemoryStream(data, false), directoryInfo) { }
|
||||
public ValorantAPIManifest(Stream stream, DirectoryInfo directoryInfo) : this(new BinaryReader(stream), directoryInfo) { }
|
||||
public ValorantAPIManifest(BinaryReader reader, DirectoryInfo directoryInfo)
|
||||
{
|
||||
Id = reader.ReadUInt64();
|
||||
var chunks = reader.ReadInt32();
|
||||
Chunks = new Dictionary<ulong, ValorantChunk>(chunks);
|
||||
|
||||
for (var i = 0; i < chunks; i++)
|
||||
{
|
||||
var chunk = new ValorantChunk(reader);
|
||||
Chunks.Add(chunk.Id, chunk);
|
||||
}
|
||||
|
||||
Paks = reader.ReadTArray(() => new ValorantPak(reader));
|
||||
|
||||
_client = new HttpClient(new HttpClientHandler
|
||||
{
|
||||
UseProxy = false,
|
||||
UseCookies = false,
|
||||
AutomaticDecompression = DecompressionMethods.All,
|
||||
CheckCertificateRevocationList = false,
|
||||
PreAuthenticate = false,
|
||||
MaxConnectionsPerServer = 1337
|
||||
});
|
||||
_chunkDirectory = directoryInfo;
|
||||
}
|
||||
|
||||
public Stream GetPakStream(int index)
|
||||
{
|
||||
return new ValorantPakStream(this, index);
|
||||
}
|
||||
|
||||
public async Task PrefetchChunk(ValorantChunk chunk, CancellationToken cancellationToken)
|
||||
{
|
||||
var chunkPath = Path.Combine(_chunkDirectory.FullName, chunk.Id + ".valchunk");
|
||||
|
||||
if (File.Exists(chunkPath))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get, chunk.Url);
|
||||
using var response = await _client.SendAsync(request, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (response.StatusCode == HttpStatusCode.OK)
|
||||
{
|
||||
var chunkBytes = await response.Content.ReadAsByteArrayAsync().ConfigureAwait(false);
|
||||
await using var fs = new FileStream(chunkPath, FileMode.Create, FileAccess.Write, FileShare.Read);
|
||||
await fs.WriteAsync(chunkBytes, 0, chunkBytes.Length, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
#if DEBUG
|
||||
else
|
||||
{
|
||||
var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
Debugger.Break();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
public async Task<byte[]> GetChunkBytes(ValorantChunk chunk, CancellationToken cancellationToken)
|
||||
{
|
||||
var chunkPath = Path.Combine(_chunkDirectory.FullName, chunk.Id + ".valchunk");
|
||||
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, 0, chunkBytes.Length, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get, chunk.Url);
|
||||
using var response = await _client.SendAsync(request, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (response.StatusCode == HttpStatusCode.OK)
|
||||
{
|
||||
chunkBytes = await response.Content.ReadAsByteArrayAsync().ConfigureAwait(false);
|
||||
await using var fs = new FileStream(chunkPath, FileMode.Create, FileAccess.Write, FileShare.Read);
|
||||
await fs.WriteAsync(chunkBytes, 0, chunkBytes.Length, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
#if DEBUG
|
||||
var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
Debugger.Break();
|
||||
#endif
|
||||
chunkBytes = null;
|
||||
}
|
||||
}
|
||||
|
||||
return chunkBytes;
|
||||
}
|
||||
|
||||
public static async Task<ValorantAPIManifest> DownloadAndParse(DirectoryInfo directoryInfo)
|
||||
{
|
||||
using var client = new HttpClient(new HttpClientHandler
|
||||
{
|
||||
UseProxy = false,
|
||||
UseCookies = false,
|
||||
AutomaticDecompression = DecompressionMethods.All,
|
||||
CheckCertificateRevocationList = false,
|
||||
PreAuthenticate = false
|
||||
});
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get, _url);
|
||||
using var response = await client.SendAsync(request).ConfigureAwait(false);
|
||||
|
||||
if (response.StatusCode != HttpStatusCode.OK)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var responseStream = await response.Content.ReadAsStreamAsync();
|
||||
return new ValorantAPIManifest(responseStream, directoryInfo);
|
||||
}
|
||||
}
|
||||
|
||||
public readonly struct ValorantChunk
|
||||
{
|
||||
private const string _baseUrl = "https://fmodel.fortnite-api.com/valorant/v1/chunks/";
|
||||
|
||||
public readonly ulong Id;
|
||||
public readonly uint Size;
|
||||
public string Url => _baseUrl + Id;
|
||||
|
||||
public ValorantChunk(BinaryReader reader)
|
||||
{
|
||||
Id = reader.ReadUInt64();
|
||||
Size = reader.ReadUInt32();
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{Id:X8} | {Strings.GetReadableSize(Size)}";
|
||||
}
|
||||
}
|
||||
|
||||
public readonly struct ValorantPak
|
||||
{
|
||||
public readonly ulong Id;
|
||||
public readonly uint Size;
|
||||
public readonly string Name;
|
||||
public readonly ulong[] ChunkIds;
|
||||
|
||||
public ValorantPak(BinaryReader reader)
|
||||
{
|
||||
Id = reader.ReadUInt64();
|
||||
Size = reader.ReadUInt32();
|
||||
var nameLength = reader.ReadInt32();
|
||||
var nameBytes = reader.ReadBytes(nameLength);
|
||||
Name = Encoding.ASCII.GetString(nameBytes);
|
||||
ChunkIds = new ulong[reader.ReadInt32()];
|
||||
|
||||
for (var i = 0; i < ChunkIds.Length; i++)
|
||||
{
|
||||
ChunkIds[i] = reader.ReadUInt64();
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{Name} | {Strings.GetReadableSize(Size)}";
|
||||
}
|
||||
}
|
||||
|
||||
public class ValorantPakStream : Stream
|
||||
{
|
||||
public override bool CanRead { get; } = true;
|
||||
public override bool CanSeek { get; } = true;
|
||||
public override bool CanWrite { get; } = false;
|
||||
public override long Length { get; }
|
||||
|
||||
private long _position;
|
||||
|
||||
public override long Position
|
||||
{
|
||||
get => _position;
|
||||
set
|
||||
{
|
||||
if (value >= Length || value < 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
|
||||
_position = value;
|
||||
}
|
||||
}
|
||||
|
||||
public string FileName { get; }
|
||||
private readonly ValorantAPIManifest _manifest;
|
||||
private readonly ValorantChunk[] _chunks;
|
||||
|
||||
public ValorantPakStream(ValorantAPIManifest manifest, int pakIndex)
|
||||
{
|
||||
_manifest = manifest;
|
||||
var pak = manifest.Paks[pakIndex];
|
||||
FileName = pak.Name;
|
||||
Length = pak.Size;
|
||||
_chunks = new ValorantChunk[pak.ChunkIds.Length];
|
||||
|
||||
for (var i = 0; i < _chunks.Length; i++)
|
||||
{
|
||||
_chunks[i] = manifest.Chunks[pak.ChunkIds[i]];
|
||||
}
|
||||
}
|
||||
|
||||
public override int Read(byte[] buffer, int offset, int count)
|
||||
{
|
||||
return ReadAsync(buffer, offset, count, CancellationToken.None).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public async Task PrefetchAsync(int i, uint startPos, long count, CancellationToken cancellationToken, int concurrentDownloads = 4)
|
||||
{
|
||||
var tasks = new List<Task>();
|
||||
var sem = new SemaphoreSlim(concurrentDownloads);
|
||||
|
||||
while (count > 0)
|
||||
{
|
||||
await sem.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);
|
||||
sem.Dispose();
|
||||
|
||||
async Task PrefetchChunkAsync(ValorantChunk chunk)
|
||||
{
|
||||
await _manifest.PrefetchChunk(chunk, cancellationToken).ConfigureAwait(false);
|
||||
sem.Release();
|
||||
}
|
||||
}
|
||||
|
||||
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 (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);
|
||||
}
|
||||
|
||||
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()
|
||||
};
|
||||
return _position;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -44,9 +44,15 @@ namespace FModel.Grabber.Paks
|
||||
}
|
||||
|
||||
// Add Pak Files
|
||||
if (Properties.Settings.Default.PakPath.EndsWith(".manifest"))
|
||||
if (Properties.Settings.Default.PakPath.EndsWith("-fn.manifest"))
|
||||
{
|
||||
ManifestInfo manifestInfo = await ManifestGrabber.TryGetLatestManifestInfo().ConfigureAwait(false);
|
||||
|
||||
if (manifestInfo == null)
|
||||
{
|
||||
throw new Exception("Failed to load latest manifest.");
|
||||
}
|
||||
|
||||
DirectoryInfo chunksDir = Directory.CreateDirectory(Path.Combine(Properties.Settings.Default.OutputPath, "PakChunks"));
|
||||
string manifestPath = Path.Combine(chunksDir.FullName, manifestInfo.Filename);
|
||||
byte[] manifestData;
|
||||
@@ -97,6 +103,41 @@ namespace FModel.Grabber.Paks
|
||||
});
|
||||
}
|
||||
}
|
||||
else if (Properties.Settings.Default.PakPath.EndsWith("-val.manifest"))
|
||||
{
|
||||
ValorantAPIManifest manifest = await ValorantAPIManifest.DownloadAndParse(Directory.CreateDirectory(Path.Combine(Properties.Settings.Default.OutputPath, "PakChunks"))).ConfigureAwait(false);
|
||||
|
||||
if (manifest == null)
|
||||
{
|
||||
throw new Exception("Failed to load latest manifest.");
|
||||
}
|
||||
|
||||
for (int i = 0; i < manifest.Paks.Length; i++)
|
||||
{
|
||||
ValorantPak pak = manifest.Paks[i];
|
||||
|
||||
var pakFileName = @$"ShooterGame\Content\Paks\{pak.Name}";
|
||||
PakFileReader pakFile = new PakFileReader(pakFileName, manifest.GetPakStream(i));
|
||||
|
||||
if (i == 0)
|
||||
{
|
||||
// define the current game thank to the pak path
|
||||
Folders.SetGameName(pakFileName);
|
||||
|
||||
Globals.Game.Version = pakFile.Info.Version;
|
||||
Globals.Game.SubVersion = pakFile.Info.SubVersion;
|
||||
}
|
||||
|
||||
await Application.Current.Dispatcher.InvokeAsync(delegate
|
||||
{
|
||||
MenuItems.pakFiles.Add(new PakMenuItemViewModel
|
||||
{
|
||||
PakFile = pakFile,
|
||||
IsEnabled = false
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
else if (Directory.Exists(Properties.Settings.Default.PakPath))
|
||||
{
|
||||
// define the current game thank to the pak path
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
using FModel;
|
||||
|
||||
namespace PakReader.Parsers.Objects
|
||||
{
|
||||
public readonly struct FTexturePlatformData
|
||||
@@ -27,7 +29,7 @@ namespace PakReader.Parsers.Objects
|
||||
|
||||
Mips = reader.ReadTArray(() => new FTexture2DMipMap(reader, ubulk, bulkOffset));
|
||||
|
||||
if (FModel.Globals.Game.Version > EPakVersion.FNAME_BASED_COMPRESSION_METHOD || FModel.Globals.Game.SubVersion == 1)
|
||||
if (Globals.Game.Version > EPakVersion.FNAME_BASED_COMPRESSION_METHOD || Globals.Game.SubVersion == 1 && Globals.Game.ActualGame != EGame.Fortnite)
|
||||
{
|
||||
bIsVirtual = reader.ReadInt32() != 0;
|
||||
if (bIsVirtual)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace FModel.Utils
|
||||
@@ -21,13 +22,13 @@ namespace FModel.Utils
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static string FixPath(string path)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
if (string.IsNullOrWhiteSpace(path) || path.Length < 5)
|
||||
return string.Empty;
|
||||
|
||||
string trigger;
|
||||
{
|
||||
string tempPath = path[1..];
|
||||
trigger = tempPath.Substring(0, tempPath.IndexOf("/"));
|
||||
trigger = tempPath.Substring(0, tempPath.IndexOf('/'));
|
||||
}
|
||||
|
||||
Regex regex = new Regex(trigger);
|
||||
|
||||
@@ -47,7 +47,7 @@ namespace FModel.Windows.Launcher
|
||||
ComboBoxVm.gamesCbViewModel.Add(new ComboBoxViewModel { Id = i++, Content = "Fortnite [EGL2]", Property = egl2FilesPath });
|
||||
}
|
||||
|
||||
ComboBoxVm.gamesCbViewModel.Add(new ComboBoxViewModel { Id = i++, Content = "Fortnite [LIVE]", Property = "donotedit-youcanteditanyway.manifest" });
|
||||
ComboBoxVm.gamesCbViewModel.Add(new ComboBoxViewModel { Id = i++, Content = "Fortnite [LIVE]", Property = "donotedit-youcanteditanyway-fn.manifest" });
|
||||
|
||||
string valorantFilesPath = Paks.GetValorantPakFilesPath();
|
||||
if (!string.IsNullOrEmpty(valorantFilesPath))
|
||||
@@ -57,6 +57,8 @@ namespace FModel.Windows.Launcher
|
||||
ComboBoxVm.gamesCbViewModel.Add(new ComboBoxViewModel { Id = i++, Content = "Valorant", Property = valorantFilesPath });
|
||||
}
|
||||
|
||||
ComboBoxVm.gamesCbViewModel.Add(new ComboBoxViewModel { Id = i++, Content = "Valorant [LIVE]", Property = "donotedit-youcanteditanyway-val.manifest" });
|
||||
|
||||
string borderlands3FilesPath = Paks.GetBorderlands3PakFilesPath();
|
||||
if (!string.IsNullOrEmpty(borderlands3FilesPath))
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user