2023-11-12 01:59:17 +08:00
|
|
|
|
using CodeWalker.Core.Utils;
|
2024-01-07 02:41:10 +08:00
|
|
|
|
using CodeWalker.World;
|
|
|
|
|
using Collections.Pooled;
|
|
|
|
|
using Microsoft.Extensions.ObjectPool;
|
2023-11-12 01:59:17 +08:00
|
|
|
|
using Microsoft.IO;
|
2023-10-28 03:31:09 +08:00
|
|
|
|
using System;
|
|
|
|
|
using System.Buffers;
|
|
|
|
|
using System.Buffers.Binary;
|
2017-09-21 18:33:05 +08:00
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
using System.ComponentModel;
|
2023-10-28 03:31:09 +08:00
|
|
|
|
using System.Diagnostics;
|
2024-01-07 02:41:10 +08:00
|
|
|
|
using System.Diagnostics.CodeAnalysis;
|
|
|
|
|
using System.Drawing;
|
2017-09-21 18:33:05 +08:00
|
|
|
|
using System.IO;
|
|
|
|
|
using System.IO.Compression;
|
2023-10-28 03:31:09 +08:00
|
|
|
|
using System.IO.Pipes;
|
2017-09-21 18:33:05 +08:00
|
|
|
|
using System.Linq;
|
2024-01-07 02:41:10 +08:00
|
|
|
|
using System.Runtime.CompilerServices;
|
|
|
|
|
using System.Runtime.Intrinsics;
|
|
|
|
|
using System.Runtime.Intrinsics.Arm;
|
2023-10-28 03:31:09 +08:00
|
|
|
|
using System.Security.Cryptography.X509Certificates;
|
2017-09-21 18:33:05 +08:00
|
|
|
|
using System.Text;
|
2023-10-28 03:31:09 +08:00
|
|
|
|
using System.Threading;
|
2017-09-21 18:33:05 +08:00
|
|
|
|
using System.Threading.Tasks;
|
|
|
|
|
|
|
|
|
|
namespace CodeWalker.GameFiles
|
|
|
|
|
{
|
2024-01-08 12:00:55 +08:00
|
|
|
|
public record struct FileCounts
|
2023-11-14 23:16:59 +08:00
|
|
|
|
{
|
|
|
|
|
public uint Rpfs;
|
|
|
|
|
public uint Files;
|
|
|
|
|
public uint Folders;
|
|
|
|
|
public uint Resources;
|
|
|
|
|
public uint BinaryFiles;
|
|
|
|
|
|
2024-01-07 02:41:10 +08:00
|
|
|
|
public readonly bool Equals(in FileCounts b)
|
|
|
|
|
{
|
|
|
|
|
return
|
|
|
|
|
Rpfs == b.Rpfs
|
|
|
|
|
&& Files == b.Files
|
|
|
|
|
&& Folders == b.Folders
|
|
|
|
|
&& Resources == b.Resources
|
|
|
|
|
&& BinaryFiles == b.BinaryFiles;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static FileCounts operator +(in FileCounts a, in FileCounts b)
|
2023-11-14 23:16:59 +08:00
|
|
|
|
{
|
|
|
|
|
return new FileCounts
|
|
|
|
|
{
|
|
|
|
|
Rpfs = a.Rpfs + b.Rpfs,
|
|
|
|
|
Files = a.Files + b.Files,
|
|
|
|
|
Folders = a.Folders + b.Folders,
|
|
|
|
|
Resources = a.Resources + b.Resources,
|
|
|
|
|
BinaryFiles = a.BinaryFiles + b.BinaryFiles
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
2017-09-21 18:33:05 +08:00
|
|
|
|
|
|
|
|
|
public class RpfFile
|
|
|
|
|
{
|
|
|
|
|
public string Name { get; set; } //name of this RPF file/package
|
|
|
|
|
public string Path { get; set; } //path within the RPF structure
|
|
|
|
|
public string FilePath { get; set; } //full file path of the RPF
|
|
|
|
|
public long FileSize { get; set; }
|
|
|
|
|
public string LastError { get; set; }
|
|
|
|
|
public Exception LastException { get; set; }
|
|
|
|
|
|
|
|
|
|
public RpfDirectoryEntry Root { get; set; }
|
|
|
|
|
|
|
|
|
|
public bool IsAESEncrypted { get; set; }
|
|
|
|
|
public bool IsNGEncrypted { get; set; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//offset in the current file
|
|
|
|
|
public long StartPos { get; set; }
|
|
|
|
|
|
|
|
|
|
//header data
|
|
|
|
|
public uint Version { get; set; }
|
|
|
|
|
public uint EntryCount { get; set; }
|
|
|
|
|
public uint NamesLength { get; set; }
|
|
|
|
|
public RpfEncryption Encryption { get; set; }
|
|
|
|
|
|
|
|
|
|
//object linkage
|
2024-01-07 02:41:10 +08:00
|
|
|
|
public PooledList<RpfEntry> AllEntries { get; set; }
|
|
|
|
|
public PooledList<RpfFile> Children { get; set; }
|
2017-09-21 18:33:05 +08:00
|
|
|
|
public RpfFile Parent { get; set; }
|
2018-01-10 11:17:30 +08:00
|
|
|
|
public RpfBinaryFileEntry ParentFileEntry { get; set; }
|
2017-09-21 18:33:05 +08:00
|
|
|
|
|
|
|
|
|
public BinaryReader CurrentFileReader { get; set; } //for temporary use while reading header
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public uint TotalFileCount { get; set; }
|
|
|
|
|
|
2023-11-12 01:59:17 +08:00
|
|
|
|
public RpfFile(FileInfo fileInfo)
|
|
|
|
|
{
|
|
|
|
|
Name = fileInfo.Name;
|
|
|
|
|
FilePath = fileInfo.FullName;
|
|
|
|
|
FileSize = fileInfo.Length;
|
|
|
|
|
}
|
2017-09-21 18:33:05 +08:00
|
|
|
|
|
2018-01-10 11:17:30 +08:00
|
|
|
|
public RpfFile(string fpath, string relpath) //for a ROOT filesystem RPF
|
2017-09-21 18:33:05 +08:00
|
|
|
|
{
|
|
|
|
|
FileInfo fi = new FileInfo(fpath);
|
|
|
|
|
Name = fi.Name;
|
2023-11-12 01:59:17 +08:00
|
|
|
|
Path = relpath;
|
2017-09-21 18:33:05 +08:00
|
|
|
|
FilePath = fpath;
|
|
|
|
|
FileSize = fi.Length;
|
|
|
|
|
}
|
2018-01-10 11:17:30 +08:00
|
|
|
|
public RpfFile(string name, string path, long filesize) //for a child RPF
|
2017-09-21 18:33:05 +08:00
|
|
|
|
{
|
|
|
|
|
Name = name;
|
2023-11-12 01:59:17 +08:00
|
|
|
|
Path = path;
|
2018-01-10 11:17:30 +08:00
|
|
|
|
FilePath = path;
|
2017-09-21 18:33:05 +08:00
|
|
|
|
FileSize = filesize;
|
|
|
|
|
}
|
|
|
|
|
|
2019-06-03 15:12:52 +08:00
|
|
|
|
// Returns string to new path
|
|
|
|
|
public string CopyToModsFolder(out string status)
|
|
|
|
|
{
|
|
|
|
|
RpfFile parentFile = GetTopParent();
|
|
|
|
|
string rel_parent_path = parentFile.Path;
|
|
|
|
|
string full_parent_path = parentFile.FilePath;
|
2017-09-21 18:33:05 +08:00
|
|
|
|
|
2023-11-12 01:59:17 +08:00
|
|
|
|
if(rel_parent_path.StartsWith(@"mods\", StringComparison.OrdinalIgnoreCase))
|
2019-06-03 15:12:52 +08:00
|
|
|
|
{
|
|
|
|
|
status = "already in mods folder";
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if(!full_parent_path.EndsWith(rel_parent_path))
|
|
|
|
|
{
|
|
|
|
|
throw new DirectoryNotFoundException("Expected full parent path to end with relative path");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
string mods_base_path = full_parent_path.Replace(rel_parent_path, @"mods\");
|
|
|
|
|
string dest_path = mods_base_path + rel_parent_path;
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
File.Copy(full_parent_path, dest_path);
|
|
|
|
|
status = $"copied \"{parentFile.Name}\" from \"{full_parent_path}\" to \"{dest_path}\"";
|
|
|
|
|
return dest_path;
|
|
|
|
|
} catch (IOException e)
|
|
|
|
|
{
|
|
|
|
|
status = $"unable to copy \"{parentFile.Name}\" from \"{full_parent_path}\" to \"{dest_path}\": {e.Message}";
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public bool IsInModsFolder()
|
|
|
|
|
{
|
2023-11-12 01:59:17 +08:00
|
|
|
|
return GetTopParent().Path.StartsWith(@"mods\", StringComparison.OrdinalIgnoreCase);
|
2019-06-03 15:12:52 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public RpfFile GetTopParent()
|
2017-09-21 18:33:05 +08:00
|
|
|
|
{
|
|
|
|
|
RpfFile pfile = this;
|
|
|
|
|
while (pfile.Parent != null)
|
|
|
|
|
{
|
|
|
|
|
pfile = pfile.Parent;
|
|
|
|
|
}
|
2019-06-03 15:12:52 +08:00
|
|
|
|
return pfile;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public string GetPhysicalFilePath()
|
|
|
|
|
{
|
|
|
|
|
return GetTopParent().FilePath;
|
2017-09-21 18:33:05 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2023-11-14 23:16:59 +08:00
|
|
|
|
private void ThrowInvalidResource()
|
|
|
|
|
{
|
|
|
|
|
throw new Exception("Invalid Resource - not GTAV!");
|
|
|
|
|
}
|
2017-09-21 18:33:05 +08:00
|
|
|
|
|
2024-01-07 02:41:10 +08:00
|
|
|
|
[ThreadStatic]
|
|
|
|
|
private static Stack<RpfDirectoryEntry> cachedStack;
|
2017-09-21 18:33:05 +08:00
|
|
|
|
private void ReadHeader(BinaryReader br)
|
|
|
|
|
{
|
|
|
|
|
StartPos = br.BaseStream.Position;
|
|
|
|
|
|
|
|
|
|
Version = br.ReadUInt32(); //RPF Version - GTAV should be 0x52504637 (1380992567)
|
2024-01-07 02:41:10 +08:00
|
|
|
|
var entryCount = br.ReadUInt32(); //Number of Entries
|
|
|
|
|
EntryCount = entryCount;
|
2017-09-21 18:33:05 +08:00
|
|
|
|
NamesLength = br.ReadUInt32();
|
|
|
|
|
Encryption = (RpfEncryption)br.ReadUInt32(); //0x04E45504F (1313165391): none; 0x0ffffff9 (268435449): AES
|
|
|
|
|
|
|
|
|
|
if (Version != 0x52504637)
|
|
|
|
|
{
|
2023-11-14 23:16:59 +08:00
|
|
|
|
ThrowInvalidResource();
|
2017-09-21 18:33:05 +08:00
|
|
|
|
}
|
|
|
|
|
|
2024-01-07 02:41:10 +08:00
|
|
|
|
var entriesLength = (int)entryCount * 16;
|
2023-11-12 01:59:17 +08:00
|
|
|
|
var namesLength = (int)NamesLength;
|
|
|
|
|
byte[] entriesdata = ArrayPool<byte>.Shared.Rent(entriesLength);
|
|
|
|
|
byte[] namesdata = ArrayPool<byte>.Shared.Rent(namesLength);
|
2023-10-28 03:31:09 +08:00
|
|
|
|
|
2023-11-12 01:59:17 +08:00
|
|
|
|
br.BaseStream.Read(entriesdata, 0, entriesLength);
|
|
|
|
|
br.BaseStream.Read(namesdata, 0, namesLength);
|
2017-09-21 18:33:05 +08:00
|
|
|
|
|
|
|
|
|
switch (Encryption)
|
|
|
|
|
{
|
2018-01-10 11:17:30 +08:00
|
|
|
|
case RpfEncryption.NONE: //no encryption
|
|
|
|
|
case RpfEncryption.OPEN: //OpenIV style RPF with unencrypted TOC
|
2024-01-07 02:41:10 +08:00
|
|
|
|
case RpfEncryption.CFXP:
|
2017-09-21 18:33:05 +08:00
|
|
|
|
break;
|
|
|
|
|
case RpfEncryption.AES:
|
2023-11-12 01:59:17 +08:00
|
|
|
|
GTACrypto.DecryptAES(entriesdata, entriesLength);
|
|
|
|
|
GTACrypto.DecryptAES(namesdata, namesLength);
|
2023-10-28 03:31:09 +08:00
|
|
|
|
|
2017-09-21 18:33:05 +08:00
|
|
|
|
IsAESEncrypted = true;
|
|
|
|
|
break;
|
|
|
|
|
case RpfEncryption.NG:
|
2023-10-28 03:31:09 +08:00
|
|
|
|
default:
|
2024-01-07 02:41:10 +08:00
|
|
|
|
GTACrypto.DecryptNG(entriesdata.AsMemory(0, entriesLength), Name, (uint)FileSize);
|
|
|
|
|
GTACrypto.DecryptNG(namesdata.AsMemory(0, namesLength), Name, (uint)FileSize);
|
2023-10-28 03:31:09 +08:00
|
|
|
|
|
2017-09-21 18:33:05 +08:00
|
|
|
|
IsNGEncrypted = true;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2024-01-07 02:41:10 +08:00
|
|
|
|
var entriessequence = new ReadOnlySequence<byte>(entriesdata);
|
|
|
|
|
var entriesrdr = new SequenceReader<byte>(entriessequence);
|
|
|
|
|
|
|
|
|
|
var namessequence = new ReadOnlySequence<byte>(namesdata);
|
|
|
|
|
var namesrdr = new SequenceReader<byte>(namessequence);
|
2023-10-28 03:31:09 +08:00
|
|
|
|
|
2017-09-21 18:33:05 +08:00
|
|
|
|
|
2024-01-07 02:41:10 +08:00
|
|
|
|
var allEntries = new PooledList<RpfEntry>((int)entryCount);
|
|
|
|
|
AllEntries = allEntries;
|
|
|
|
|
var totalFileCount = 0u;
|
|
|
|
|
|
|
|
|
|
for (uint i = 0; i < entryCount; i++)
|
2017-09-21 18:33:05 +08:00
|
|
|
|
{
|
2024-01-07 02:41:10 +08:00
|
|
|
|
ulong xy = entriesrdr.ReadUInt64();
|
|
|
|
|
|
|
|
|
|
uint x = (uint)(xy >> 32);
|
2017-09-21 18:33:05 +08:00
|
|
|
|
|
|
|
|
|
RpfEntry e;
|
|
|
|
|
|
|
|
|
|
if (x == 0x7fffff00) //directory entry
|
|
|
|
|
{
|
|
|
|
|
e = new RpfDirectoryEntry();
|
|
|
|
|
}
|
|
|
|
|
else if ((x & 0x80000000) == 0) //binary file entry
|
|
|
|
|
{
|
|
|
|
|
e = new RpfBinaryFileEntry();
|
2024-01-07 02:41:10 +08:00
|
|
|
|
totalFileCount++;
|
2017-09-21 18:33:05 +08:00
|
|
|
|
}
|
|
|
|
|
else //assume resource file entry
|
|
|
|
|
{
|
|
|
|
|
e = new RpfResourceFileEntry();
|
2024-01-07 02:41:10 +08:00
|
|
|
|
totalFileCount++;
|
2017-09-21 18:33:05 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
e.File = this;
|
2024-01-07 02:41:10 +08:00
|
|
|
|
e.Header = xy;
|
2017-09-21 18:33:05 +08:00
|
|
|
|
|
2024-01-07 02:41:10 +08:00
|
|
|
|
e.Read(ref entriesrdr, br);
|
2017-09-21 18:33:05 +08:00
|
|
|
|
|
2024-01-07 02:41:10 +08:00
|
|
|
|
namesrdr.SetPosition(e.NameOffset);
|
2023-10-28 03:31:09 +08:00
|
|
|
|
|
2024-01-07 02:41:10 +08:00
|
|
|
|
namesrdr.TryReadTo(out ReadOnlySpan<byte> buffer, 0);
|
|
|
|
|
if (buffer.Length > 256)
|
|
|
|
|
{
|
|
|
|
|
buffer = buffer.Slice(0, 256);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
e.Name = Encoding.UTF8.GetStringPooled(buffer);
|
|
|
|
|
JenkIndex.EnsureLower(e.Name);
|
|
|
|
|
if (e is RpfResourceFileEntry rfe)// && string.IsNullOrEmpty(e.Name))
|
2017-09-21 18:33:05 +08:00
|
|
|
|
{
|
2023-11-12 01:59:17 +08:00
|
|
|
|
rfe.IsEncrypted = rfe.IsExtension(".ysc");//any other way to know..?
|
2017-09-21 18:33:05 +08:00
|
|
|
|
}
|
2024-01-07 02:41:10 +08:00
|
|
|
|
|
|
|
|
|
allEntries.Add(e);
|
2017-09-21 18:33:05 +08:00
|
|
|
|
}
|
|
|
|
|
|
2024-01-07 02:41:10 +08:00
|
|
|
|
TotalFileCount = totalFileCount;
|
|
|
|
|
|
|
|
|
|
Root = (RpfDirectoryEntry)allEntries[0];
|
|
|
|
|
Root.Path = Path;
|
2017-09-21 18:33:05 +08:00
|
|
|
|
|
2024-01-07 02:41:10 +08:00
|
|
|
|
var entriesSpan = AllEntries.Span;
|
|
|
|
|
|
|
|
|
|
var stack = new Stack<RpfDirectoryEntry>();;
|
|
|
|
|
stack.Push(Root);
|
|
|
|
|
while (stack.Count > 0)
|
2023-11-12 01:59:17 +08:00
|
|
|
|
{
|
2024-01-07 02:41:10 +08:00
|
|
|
|
var item = stack.Pop();
|
|
|
|
|
|
2018-01-10 11:17:30 +08:00
|
|
|
|
int starti = (int)item.EntriesIndex;
|
2024-01-07 02:41:10 +08:00
|
|
|
|
int endi = (int)(starti + item.EntriesCount);
|
2018-01-10 11:17:30 +08:00
|
|
|
|
|
|
|
|
|
for (int i = starti; i < endi; i++)
|
2017-09-21 18:33:05 +08:00
|
|
|
|
{
|
2024-01-07 02:41:10 +08:00
|
|
|
|
RpfEntry e = entriesSpan[i];
|
2018-01-10 11:17:30 +08:00
|
|
|
|
e.Parent = item;
|
2023-10-28 03:31:09 +08:00
|
|
|
|
if (e is RpfDirectoryEntry rde)
|
2017-09-21 18:33:05 +08:00
|
|
|
|
{
|
2024-01-07 02:41:10 +08:00
|
|
|
|
rde.Path = $"{item.Path}\\{rde.Name}";
|
2017-09-21 18:33:05 +08:00
|
|
|
|
item.Directories.Add(rde);
|
2024-01-07 02:41:10 +08:00
|
|
|
|
stack.Push(rde);
|
2017-09-21 18:33:05 +08:00
|
|
|
|
}
|
2023-11-12 01:59:17 +08:00
|
|
|
|
else if (e is RpfFileEntry rfe)
|
2017-09-21 18:33:05 +08:00
|
|
|
|
{
|
2024-01-07 02:41:10 +08:00
|
|
|
|
rfe.Path = $"{item.Path}\\{rfe.Name}";
|
2017-09-21 18:33:05 +08:00
|
|
|
|
item.Files.Add(rfe);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
br.BaseStream.Position = StartPos;
|
|
|
|
|
|
2023-10-28 03:31:09 +08:00
|
|
|
|
ArrayPool<byte>.Shared.Return(entriesdata);
|
|
|
|
|
ArrayPool<byte>.Shared.Return(namesdata);
|
2024-01-07 02:41:10 +08:00
|
|
|
|
stack.Clear();
|
2017-09-21 18:33:05 +08:00
|
|
|
|
}
|
|
|
|
|
|
2024-01-07 02:41:10 +08:00
|
|
|
|
public bool ScanStructure(Action<string>? updateStatus, Action<string>? errorLog, out FileCounts fileCounts)
|
2017-09-21 18:33:05 +08:00
|
|
|
|
{
|
2023-10-28 03:31:09 +08:00
|
|
|
|
|
|
|
|
|
using var fileStream = File.OpenRead(FilePath);
|
|
|
|
|
using var br = new BinaryReader(fileStream);
|
|
|
|
|
try
|
2017-09-21 18:33:05 +08:00
|
|
|
|
{
|
2024-01-07 02:41:10 +08:00
|
|
|
|
return ScanStructure(br, updateStatus, errorLog, out fileCounts);
|
2023-10-28 03:31:09 +08:00
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
LastError = ex.ToString();
|
|
|
|
|
LastException = ex;
|
2024-01-07 02:41:10 +08:00
|
|
|
|
errorLog?.Invoke($"{FilePath}: {ex}");
|
|
|
|
|
Console.WriteLine($"{FilePath}: {ex}");
|
|
|
|
|
fileCounts = default;
|
|
|
|
|
return false;
|
2017-09-21 18:33:05 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
2024-01-07 02:41:10 +08:00
|
|
|
|
private bool ScanStructure(BinaryReader br, Action<string>? updateStatus, Action<string>? errorLog, out FileCounts fileCounts)
|
2017-09-21 18:33:05 +08:00
|
|
|
|
{
|
2024-01-07 02:41:10 +08:00
|
|
|
|
if (FilePath == "update\\update.rpf\\dlc_patch\\patchday1ng\\x64\\patch\\data\\lang\\chinesesimp.rpf")
|
|
|
|
|
{
|
|
|
|
|
fileCounts = default;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2023-10-28 03:31:09 +08:00
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
ReadHeader(br);
|
2024-01-07 02:41:10 +08:00
|
|
|
|
}
|
|
|
|
|
catch
|
2023-10-28 03:31:09 +08:00
|
|
|
|
{
|
2024-01-07 02:41:10 +08:00
|
|
|
|
fileCounts = default;
|
|
|
|
|
return false;
|
2023-10-28 03:31:09 +08:00
|
|
|
|
}
|
2017-09-21 18:33:05 +08:00
|
|
|
|
|
2024-01-07 02:41:10 +08:00
|
|
|
|
fileCounts = new FileCounts
|
2023-11-14 23:16:59 +08:00
|
|
|
|
{
|
|
|
|
|
Rpfs = 1,
|
|
|
|
|
Files = 1
|
|
|
|
|
};
|
2017-09-21 18:33:05 +08:00
|
|
|
|
|
2024-01-07 02:41:10 +08:00
|
|
|
|
Children = new PooledList<RpfFile>();
|
2017-09-21 18:33:05 +08:00
|
|
|
|
|
2024-01-07 02:41:10 +08:00
|
|
|
|
updateStatus?.Invoke($"Scanning {Path}...");
|
2017-09-21 18:33:05 +08:00
|
|
|
|
|
|
|
|
|
foreach (RpfEntry entry in AllEntries)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
2023-10-28 03:31:09 +08:00
|
|
|
|
if (entry is RpfBinaryFileEntry binentry)
|
2017-09-21 18:33:05 +08:00
|
|
|
|
{
|
|
|
|
|
//search all the sub resources for YSC files. (recurse!)
|
2023-11-12 01:59:17 +08:00
|
|
|
|
if (binentry.IsExtension(".rpf") && binentry.Path.Length < 5000) // a long path is most likely an attempt to crash CW, so skip it
|
2017-09-21 18:33:05 +08:00
|
|
|
|
{
|
2018-01-10 11:17:30 +08:00
|
|
|
|
br.BaseStream.Position = StartPos + ((long)binentry.FileOffset * 512);
|
|
|
|
|
|
|
|
|
|
long l = binentry.GetFileSize();
|
2017-09-21 18:33:05 +08:00
|
|
|
|
|
2018-01-10 11:17:30 +08:00
|
|
|
|
RpfFile subfile = new RpfFile(binentry.Name, binentry.Path, l);
|
2017-09-21 18:33:05 +08:00
|
|
|
|
subfile.Parent = this;
|
2018-01-10 11:17:30 +08:00
|
|
|
|
subfile.ParentFileEntry = binentry;
|
2017-09-21 18:33:05 +08:00
|
|
|
|
|
2024-01-07 02:41:10 +08:00
|
|
|
|
var success = subfile.ScanStructure(br, updateStatus, errorLog, out var result);
|
2017-09-21 18:33:05 +08:00
|
|
|
|
|
2024-01-07 02:41:10 +08:00
|
|
|
|
if (success)
|
2023-11-14 23:16:59 +08:00
|
|
|
|
{
|
2024-01-07 02:41:10 +08:00
|
|
|
|
fileCounts += result;
|
2023-10-28 03:31:09 +08:00
|
|
|
|
Children.Add(subfile);
|
|
|
|
|
}
|
2017-09-21 18:33:05 +08:00
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
//binary file that's not an rpf...
|
2023-11-14 23:16:59 +08:00
|
|
|
|
fileCounts.BinaryFiles++;
|
|
|
|
|
fileCounts.Files++;
|
2017-09-21 18:33:05 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else if (entry is RpfResourceFileEntry)
|
|
|
|
|
{
|
2023-11-14 23:16:59 +08:00
|
|
|
|
fileCounts.Resources++;
|
|
|
|
|
fileCounts.Files++;
|
2017-09-21 18:33:05 +08:00
|
|
|
|
}
|
|
|
|
|
else if (entry is RpfDirectoryEntry)
|
|
|
|
|
{
|
2023-11-14 23:16:59 +08:00
|
|
|
|
fileCounts.Folders++;
|
2017-09-21 18:33:05 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
2024-01-07 02:41:10 +08:00
|
|
|
|
Console.WriteLine(ex);
|
|
|
|
|
errorLog?.Invoke($"{entry.Path}: {ex}");
|
2017-09-21 18:33:05 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
2024-01-07 02:41:10 +08:00
|
|
|
|
return true;
|
2017-09-21 18:33:05 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public void ExtractScripts(string outputfolder, Action<string> updateStatus)
|
|
|
|
|
{
|
|
|
|
|
FileStream fs = File.OpenRead(FilePath);
|
|
|
|
|
BinaryReader br = new BinaryReader(fs);
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
ExtractScripts(br, outputfolder, updateStatus);
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
LastError = ex.ToString();
|
|
|
|
|
LastException = ex;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
br.Close();
|
|
|
|
|
br.Dispose();
|
|
|
|
|
fs.Dispose();
|
|
|
|
|
}
|
|
|
|
|
private void ExtractScripts(BinaryReader br, string outputfolder, Action<string> updateStatus)
|
|
|
|
|
{
|
2024-01-07 02:41:10 +08:00
|
|
|
|
updateStatus?.Invoke($"Searching {Name}...");
|
2017-09-21 18:33:05 +08:00
|
|
|
|
|
|
|
|
|
ReadHeader(br);
|
|
|
|
|
|
|
|
|
|
//List<DataBlock> blocks = new List<DataBlock>();
|
|
|
|
|
foreach (RpfEntry entry in AllEntries)
|
|
|
|
|
{
|
|
|
|
|
if (entry is RpfBinaryFileEntry)
|
|
|
|
|
{
|
|
|
|
|
RpfBinaryFileEntry binentry = entry as RpfBinaryFileEntry;
|
2018-01-10 11:17:30 +08:00
|
|
|
|
long l = binentry.GetFileSize();
|
2017-09-21 18:33:05 +08:00
|
|
|
|
|
|
|
|
|
//search all the sub resources for YSC files. (recurse!)
|
2023-11-12 01:59:17 +08:00
|
|
|
|
if (binentry.IsExtension(".rpf"))
|
2017-09-21 18:33:05 +08:00
|
|
|
|
{
|
2018-01-10 11:17:30 +08:00
|
|
|
|
br.BaseStream.Position = StartPos + ((long)binentry.FileOffset * 512);
|
2017-09-21 18:33:05 +08:00
|
|
|
|
|
2018-01-10 11:17:30 +08:00
|
|
|
|
RpfFile subfile = new RpfFile(binentry.Name, binentry.Path, l);
|
2017-09-21 18:33:05 +08:00
|
|
|
|
subfile.Parent = this;
|
2018-01-10 11:17:30 +08:00
|
|
|
|
subfile.ParentFileEntry = binentry;
|
2017-09-21 18:33:05 +08:00
|
|
|
|
|
|
|
|
|
subfile.ExtractScripts(br, outputfolder, updateStatus);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
else if (entry is RpfResourceFileEntry)
|
|
|
|
|
{
|
|
|
|
|
|
|
|
|
|
RpfResourceFileEntry resentry = entry as RpfResourceFileEntry;
|
|
|
|
|
|
2023-11-12 01:59:17 +08:00
|
|
|
|
if (resentry.IsExtension(".ysc"))
|
2017-09-21 18:33:05 +08:00
|
|
|
|
{
|
2024-01-07 02:41:10 +08:00
|
|
|
|
updateStatus?.Invoke($"Extracting {resentry.Name}...");
|
2017-09-21 18:33:05 +08:00
|
|
|
|
|
|
|
|
|
//found a YSC file. extract it!
|
2024-01-07 02:41:10 +08:00
|
|
|
|
string ofpath = $"{outputfolder}\\{resentry.Name}";
|
2017-09-21 18:33:05 +08:00
|
|
|
|
|
2018-01-10 11:17:30 +08:00
|
|
|
|
br.BaseStream.Position = StartPos + ((long)resentry.FileOffset * 512);
|
2017-09-21 18:33:05 +08:00
|
|
|
|
|
|
|
|
|
if (resentry.FileSize > 0)
|
|
|
|
|
{
|
|
|
|
|
uint offset = 0x10;
|
|
|
|
|
uint totlen = resentry.FileSize - offset;
|
|
|
|
|
|
|
|
|
|
byte[] tbytes = new byte[totlen];
|
|
|
|
|
|
|
|
|
|
br.BaseStream.Position += offset;
|
|
|
|
|
|
|
|
|
|
br.Read(tbytes, 0, (int)totlen);
|
|
|
|
|
|
|
|
|
|
if (IsAESEncrypted)
|
|
|
|
|
{
|
2023-10-28 03:31:09 +08:00
|
|
|
|
GTACrypto.DecryptAES(tbytes);
|
2017-09-21 18:33:05 +08:00
|
|
|
|
|
|
|
|
|
//special case! probable duplicate pilot_school.ysc
|
2024-01-07 02:41:10 +08:00
|
|
|
|
ofpath = $"{outputfolder}\\{Name}___{resentry.Name}";
|
2017-09-21 18:33:05 +08:00
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
2024-01-07 02:41:10 +08:00
|
|
|
|
GTACrypto.DecryptNG(tbytes.AsMemory(0, (int)totlen), resentry.Name, resentry.FileSize);
|
2017-09-21 18:33:05 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
2023-10-28 03:31:09 +08:00
|
|
|
|
MemoryStream ms = new MemoryStream(tbytes);
|
2017-09-21 18:33:05 +08:00
|
|
|
|
DeflateStream ds = new DeflateStream(ms, CompressionMode.Decompress);
|
|
|
|
|
|
2023-10-28 03:31:09 +08:00
|
|
|
|
MemoryStream outstr = recyclableMemoryStreamManager.GetStream();
|
2023-11-14 23:16:59 +08:00
|
|
|
|
ds.CopyTo(outstr);
|
|
|
|
|
byte[] outbuf = outstr.ToArray();
|
2017-09-21 18:33:05 +08:00
|
|
|
|
|
|
|
|
|
bool pathok = true;
|
|
|
|
|
if (File.Exists(ofpath))
|
|
|
|
|
{
|
2024-01-07 02:41:10 +08:00
|
|
|
|
ofpath = $"{outputfolder}\\{Name}_{resentry.Name}";
|
2017-09-21 18:33:05 +08:00
|
|
|
|
if (File.Exists(ofpath))
|
|
|
|
|
{
|
2024-01-07 02:41:10 +08:00
|
|
|
|
LastError = $"Output file {ofpath} already exists!";
|
2017-09-21 18:33:05 +08:00
|
|
|
|
pathok = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (pathok)
|
|
|
|
|
{
|
|
|
|
|
File.WriteAllBytes(ofpath, outbuf);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
LastError = ex.ToString();
|
|
|
|
|
LastException = ex;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-01-08 12:00:55 +08:00
|
|
|
|
}
|
2017-09-21 18:33:05 +08:00
|
|
|
|
|
2024-01-08 12:00:55 +08:00
|
|
|
|
private Dictionary<string, RpfEntry[]>? FilesByFileType;
|
|
|
|
|
public RpfEntry[] GetFilesByFileType(string ext)
|
|
|
|
|
{
|
|
|
|
|
FilesByFileType ??= new Dictionary<string, RpfEntry[]>();
|
|
|
|
|
if (!FilesByFileType.TryGetValue(ext, out var entries))
|
|
|
|
|
{
|
|
|
|
|
entries = AllEntries.Where(p => p.IsExtension(ext)).ToArray();
|
|
|
|
|
FilesByFileType[ext] = entries;
|
|
|
|
|
}
|
|
|
|
|
return entries;
|
2017-09-21 18:33:05 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2023-10-28 03:31:09 +08:00
|
|
|
|
//public Stream ExtractFileStream(RpfFileEntry entry)
|
|
|
|
|
//{
|
|
|
|
|
// try
|
|
|
|
|
// {
|
|
|
|
|
// using (BinaryReader br = new BinaryReader(File.OpenRead(GetPhysicalFilePath())))
|
|
|
|
|
// {
|
|
|
|
|
// if (entry is RpfBinaryFileEntry binaryFileEntry)
|
|
|
|
|
// {
|
|
|
|
|
// return ExtractFileBinary(binaryFileEntry, br);
|
|
|
|
|
// }
|
|
|
|
|
// else if (entry is RpfResourceFileEntry resourceFileEntry)
|
|
|
|
|
// {
|
|
|
|
|
// return ExtractFileResource(resourceFileEntry, br);
|
|
|
|
|
// }
|
|
|
|
|
// else
|
|
|
|
|
// {
|
|
|
|
|
// return null;
|
|
|
|
|
// }
|
|
|
|
|
// }
|
|
|
|
|
// }
|
|
|
|
|
// catch (Exception ex)
|
|
|
|
|
// {
|
|
|
|
|
// LastError = ex.ToString();
|
|
|
|
|
// LastException = ex;
|
|
|
|
|
// return null;
|
|
|
|
|
// }
|
|
|
|
|
//}
|
2017-09-21 18:33:05 +08:00
|
|
|
|
|
2024-01-07 02:41:10 +08:00
|
|
|
|
public byte[]? ExtractFile(RpfFileEntry entry)
|
2017-09-21 18:33:05 +08:00
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
2024-01-07 02:41:10 +08:00
|
|
|
|
using var fileStream = new FileStream(GetPhysicalFilePath(), FileMode.Open, FileAccess.Read, FileShare.Read, 0);
|
2023-11-12 01:59:17 +08:00
|
|
|
|
using (BinaryReader br = new BinaryReader(fileStream))
|
2017-09-21 18:33:05 +08:00
|
|
|
|
{
|
2023-10-28 03:31:09 +08:00
|
|
|
|
if (entry is RpfBinaryFileEntry binaryFileEntry)
|
2017-09-21 18:33:05 +08:00
|
|
|
|
{
|
2023-10-28 03:31:09 +08:00
|
|
|
|
return ExtractFileBinary(binaryFileEntry, br);
|
2017-09-21 18:33:05 +08:00
|
|
|
|
}
|
2023-10-28 03:31:09 +08:00
|
|
|
|
else if (entry is RpfResourceFileEntry resourceFileEntry)
|
2017-09-21 18:33:05 +08:00
|
|
|
|
{
|
2023-10-28 03:31:09 +08:00
|
|
|
|
return ExtractFileResource(resourceFileEntry, br);
|
2017-09-21 18:33:05 +08:00
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
LastError = ex.ToString();
|
|
|
|
|
LastException = ex;
|
2023-11-14 23:16:59 +08:00
|
|
|
|
Console.WriteLine(ex);
|
2017-09-21 18:33:05 +08:00
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-11-12 01:59:17 +08:00
|
|
|
|
|
2024-01-07 02:41:10 +08:00
|
|
|
|
public async ValueTask<byte[]?> ExtractFileAsync(RpfFileEntry entry)
|
2023-11-12 01:59:17 +08:00
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
2024-01-07 02:41:10 +08:00
|
|
|
|
using var fileStream = new FileStream(GetPhysicalFilePath(), FileMode.Open, FileAccess.Read, FileShare.Read, 0, FileOptions.Asynchronous);
|
|
|
|
|
if (entry is RpfBinaryFileEntry binaryFileEntry)
|
2023-11-12 01:59:17 +08:00
|
|
|
|
{
|
2024-01-07 02:41:10 +08:00
|
|
|
|
return await ExtractFileBinaryAsync(binaryFileEntry, fileStream);
|
|
|
|
|
}
|
|
|
|
|
else if (entry is RpfResourceFileEntry resourceFileEntry)
|
|
|
|
|
{
|
|
|
|
|
return await ExtractFileResourceAsync(resourceFileEntry, fileStream);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
Console.WriteLine($"{entry} is not a BinaryFileEntry of ResourceFileEntry");
|
|
|
|
|
return null;
|
2023-11-12 01:59:17 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
LastError = ex.ToString();
|
|
|
|
|
LastException = ex;
|
2023-11-14 23:16:59 +08:00
|
|
|
|
return null;
|
2023-11-12 01:59:17 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-07 02:41:10 +08:00
|
|
|
|
public async ValueTask<byte[]?> ExtractFileBinaryAsync(RpfBinaryFileEntry entry, Stream stream)
|
2017-09-21 18:33:05 +08:00
|
|
|
|
{
|
2024-01-07 02:41:10 +08:00
|
|
|
|
stream.Position = StartPos + ((long)entry.FileOffset * 512);
|
2017-09-21 18:33:05 +08:00
|
|
|
|
|
2018-01-10 11:17:30 +08:00
|
|
|
|
long l = entry.GetFileSize();
|
2017-09-21 18:33:05 +08:00
|
|
|
|
|
2023-11-12 01:59:17 +08:00
|
|
|
|
if (l <= 0)
|
2017-09-21 18:33:05 +08:00
|
|
|
|
{
|
2023-11-12 01:59:17 +08:00
|
|
|
|
return null;
|
|
|
|
|
}
|
2017-09-21 18:33:05 +08:00
|
|
|
|
|
2023-11-12 01:59:17 +08:00
|
|
|
|
uint offset = 0;// 0x10;
|
|
|
|
|
uint totlen = (uint)l - offset;
|
2017-09-21 18:33:05 +08:00
|
|
|
|
|
2023-11-12 01:59:17 +08:00
|
|
|
|
byte[] tbytes = ArrayPool<byte>.Shared.Rent((int)totlen);
|
2017-09-21 18:33:05 +08:00
|
|
|
|
|
2024-01-07 02:41:10 +08:00
|
|
|
|
stream.Position += offset;
|
|
|
|
|
await stream.ReadAsync(tbytes, 0, (int)totlen).ConfigureAwait(false);
|
2023-11-12 01:59:17 +08:00
|
|
|
|
|
|
|
|
|
if (entry.IsEncrypted)
|
|
|
|
|
{
|
|
|
|
|
if (IsAESEncrypted)
|
2017-09-21 18:33:05 +08:00
|
|
|
|
{
|
2023-11-12 01:59:17 +08:00
|
|
|
|
GTACrypto.DecryptAES(tbytes, (int)totlen);
|
2017-09-21 18:33:05 +08:00
|
|
|
|
}
|
2023-11-12 01:59:17 +08:00
|
|
|
|
else //if (IsNGEncrypted) //assume the archive is set to NG encryption if not AES... (comment: fix for openIV modded files)
|
|
|
|
|
{
|
2024-01-07 02:41:10 +08:00
|
|
|
|
GTACrypto.DecryptNG(tbytes.AsMemory(0, (int)totlen), entry.Name, entry.FileUncompressedSize);
|
2023-11-12 01:59:17 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
2017-09-21 18:33:05 +08:00
|
|
|
|
|
2023-11-12 01:59:17 +08:00
|
|
|
|
byte[] defl;
|
|
|
|
|
if (entry.FileSize > 0) //apparently this means it's compressed
|
|
|
|
|
{
|
2024-01-07 02:41:10 +08:00
|
|
|
|
defl = await DecompressBytesAsync(tbytes, entry.GetUncompressedFileSize()).ConfigureAwait(false);
|
2023-11-12 01:59:17 +08:00
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
defl = new byte[(int)totlen];
|
|
|
|
|
Array.Copy(tbytes, defl, (int)totlen);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ArrayPool<byte>.Shared.Return(tbytes);
|
|
|
|
|
|
|
|
|
|
return defl;
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-07 02:41:10 +08:00
|
|
|
|
public ValueTask<byte[]?> ExtractFileBinaryAsync(RpfBinaryFileEntry entry, BinaryReader br)
|
|
|
|
|
{
|
|
|
|
|
return ExtractFileBinaryAsync(entry, br.BaseStream);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public byte[]? ExtractFileBinary(RpfBinaryFileEntry entry, BinaryReader br)
|
2023-11-12 01:59:17 +08:00
|
|
|
|
{
|
|
|
|
|
br.BaseStream.Position = StartPos + ((long)entry.FileOffset * 512);
|
2017-09-21 18:33:05 +08:00
|
|
|
|
|
2023-11-12 01:59:17 +08:00
|
|
|
|
long l = entry.GetFileSize();
|
|
|
|
|
|
|
|
|
|
if (l <= 0)
|
|
|
|
|
{
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
uint offset = 0;// 0x10;
|
|
|
|
|
uint totlen = (uint)l - offset;
|
|
|
|
|
|
|
|
|
|
byte[] tbytes = ArrayPool<byte>.Shared.Rent((int)totlen);
|
|
|
|
|
|
|
|
|
|
br.BaseStream.Position += offset;
|
|
|
|
|
br.Read(tbytes, 0, (int)totlen);
|
|
|
|
|
|
|
|
|
|
if (entry.IsEncrypted)
|
|
|
|
|
{
|
|
|
|
|
if (IsAESEncrypted)
|
2017-09-21 18:33:05 +08:00
|
|
|
|
{
|
2023-11-12 01:59:17 +08:00
|
|
|
|
GTACrypto.DecryptAES(tbytes, (int)totlen);
|
2017-09-21 18:33:05 +08:00
|
|
|
|
}
|
2023-11-12 01:59:17 +08:00
|
|
|
|
else //if (IsNGEncrypted) //assume the archive is set to NG encryption if not AES... (comment: fix for openIV modded files)
|
2017-09-21 18:33:05 +08:00
|
|
|
|
{
|
2024-01-07 02:41:10 +08:00
|
|
|
|
GTACrypto.DecryptNG(tbytes.AsMemory(0, (int)totlen), entry.Name, entry.FileUncompressedSize);
|
2017-09-21 18:33:05 +08:00
|
|
|
|
}
|
2023-11-12 01:59:17 +08:00
|
|
|
|
}
|
2017-09-21 18:33:05 +08:00
|
|
|
|
|
2023-11-12 01:59:17 +08:00
|
|
|
|
byte[] defl;
|
|
|
|
|
if (entry.FileSize > 0) //apparently this means it's compressed
|
|
|
|
|
{
|
2024-01-07 02:41:10 +08:00
|
|
|
|
defl = DecompressBytes(tbytes, entry.GetUncompressedFileSize());
|
2023-11-12 01:59:17 +08:00
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
defl = new byte[(int)totlen];
|
|
|
|
|
Buffer.BlockCopy(tbytes, 0, defl, 0, (int)totlen);
|
2017-09-21 18:33:05 +08:00
|
|
|
|
}
|
|
|
|
|
|
2023-11-12 01:59:17 +08:00
|
|
|
|
ArrayPool<byte>.Shared.Return(tbytes);
|
|
|
|
|
|
|
|
|
|
return defl;
|
2017-09-21 18:33:05 +08:00
|
|
|
|
}
|
2023-11-12 01:59:17 +08:00
|
|
|
|
|
2024-01-07 02:41:10 +08:00
|
|
|
|
public async ValueTask<byte[]?> ExtractFileResourceAsync(RpfResourceFileEntry entry, Stream stream)
|
2017-09-21 18:33:05 +08:00
|
|
|
|
{
|
2024-01-07 02:41:10 +08:00
|
|
|
|
stream.Position = StartPos + ((long)entry.FileOffset * 512);
|
2017-09-21 18:33:05 +08:00
|
|
|
|
|
2023-11-12 01:59:17 +08:00
|
|
|
|
if (entry.FileSize <= 0)
|
2017-09-21 18:33:05 +08:00
|
|
|
|
{
|
2023-11-12 01:59:17 +08:00
|
|
|
|
return null;
|
|
|
|
|
}
|
2017-09-21 18:33:05 +08:00
|
|
|
|
|
2023-11-12 01:59:17 +08:00
|
|
|
|
uint offset = 0x10;
|
|
|
|
|
uint totlen = entry.FileSize - offset;
|
2017-09-21 18:33:05 +08:00
|
|
|
|
|
2023-11-12 01:59:17 +08:00
|
|
|
|
byte[] tbytes = ArrayPool<byte>.Shared.Rent((int)totlen);
|
2017-09-21 18:33:05 +08:00
|
|
|
|
|
|
|
|
|
|
2024-01-07 02:41:10 +08:00
|
|
|
|
stream.Position += offset;
|
2017-09-21 18:33:05 +08:00
|
|
|
|
|
2024-01-07 02:41:10 +08:00
|
|
|
|
await stream.ReadAsync(tbytes, 0, (int)totlen).ConfigureAwait(false);
|
2023-11-12 01:59:17 +08:00
|
|
|
|
if (entry.IsEncrypted)
|
|
|
|
|
{
|
|
|
|
|
if (IsAESEncrypted)
|
2017-09-21 18:33:05 +08:00
|
|
|
|
{
|
2023-11-12 01:59:17 +08:00
|
|
|
|
GTACrypto.DecryptAES(tbytes, (int)totlen);
|
|
|
|
|
}
|
|
|
|
|
else //if (IsNGEncrypted) //assume the archive is set to NG encryption if not AES... (comment: fix for openIV modded files)
|
|
|
|
|
{
|
2024-01-07 02:41:10 +08:00
|
|
|
|
GTACrypto.DecryptNG(tbytes.AsMemory(0, (int)totlen), entry.Name, entry.FileSize);
|
2017-09-21 18:33:05 +08:00
|
|
|
|
}
|
2023-11-12 01:59:17 +08:00
|
|
|
|
}
|
2017-09-21 18:33:05 +08:00
|
|
|
|
|
2024-01-07 02:41:10 +08:00
|
|
|
|
byte[] deflated = await DecompressBytesAsync(tbytes, entry.GetUncompressedFileSize());
|
2017-09-21 18:33:05 +08:00
|
|
|
|
|
2023-11-12 01:59:17 +08:00
|
|
|
|
byte[] data;
|
|
|
|
|
if (deflated != null)
|
|
|
|
|
{
|
|
|
|
|
data = deflated;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
entry.FileSize -= offset;
|
2017-09-21 18:33:05 +08:00
|
|
|
|
|
2023-11-12 01:59:17 +08:00
|
|
|
|
data = new byte[(int)totlen];
|
|
|
|
|
Buffer.BlockCopy(tbytes, 0, data, 0, (int)totlen);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ArrayPool<byte>.Shared.Return(tbytes);
|
|
|
|
|
|
|
|
|
|
return data;
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-07 02:41:10 +08:00
|
|
|
|
public ValueTask<byte[]?> ExtractFileResourceAsync(RpfResourceFileEntry entry, BinaryReader br)
|
|
|
|
|
{
|
|
|
|
|
return ExtractFileResourceAsync(entry, br.BaseStream);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public byte[]? ExtractFileResource(RpfResourceFileEntry entry, BinaryReader br)
|
2023-11-12 01:59:17 +08:00
|
|
|
|
{
|
|
|
|
|
br.BaseStream.Position = StartPos + ((long)entry.FileOffset * 512);
|
|
|
|
|
|
|
|
|
|
if (entry.FileSize <= 0)
|
|
|
|
|
{
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
uint offset = 0x10;
|
|
|
|
|
uint totlen = entry.FileSize - offset;
|
|
|
|
|
|
|
|
|
|
byte[] tbytes = ArrayPool<byte>.Shared.Rent((int)totlen);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
br.BaseStream.Position += offset;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
br.Read(tbytes, 0, (int)totlen);
|
|
|
|
|
if (entry.IsEncrypted)
|
|
|
|
|
{
|
|
|
|
|
if (IsAESEncrypted)
|
2017-09-21 18:33:05 +08:00
|
|
|
|
{
|
2023-11-12 01:59:17 +08:00
|
|
|
|
GTACrypto.DecryptAES(tbytes, (int)totlen);
|
2017-09-21 18:33:05 +08:00
|
|
|
|
}
|
2023-11-12 01:59:17 +08:00
|
|
|
|
else //if (IsNGEncrypted) //assume the archive is set to NG encryption if not AES... (comment: fix for openIV modded files)
|
2017-09-21 18:33:05 +08:00
|
|
|
|
{
|
2024-01-07 02:41:10 +08:00
|
|
|
|
GTACrypto.DecryptNG(tbytes.AsMemory(0, (int)totlen), entry.Name, entry.FileSize);
|
2017-09-21 18:33:05 +08:00
|
|
|
|
}
|
2023-11-12 01:59:17 +08:00
|
|
|
|
}
|
2017-09-21 18:33:05 +08:00
|
|
|
|
|
2024-01-07 02:41:10 +08:00
|
|
|
|
byte[] deflated = DecompressBytes(tbytes, entry.GetUncompressedFileSize());
|
2017-09-21 18:33:05 +08:00
|
|
|
|
|
2023-11-12 01:59:17 +08:00
|
|
|
|
byte[] data;
|
|
|
|
|
if (deflated != null)
|
|
|
|
|
{
|
|
|
|
|
data = deflated;
|
2017-09-21 18:33:05 +08:00
|
|
|
|
}
|
2023-11-12 01:59:17 +08:00
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
entry.FileSize -= offset;
|
2017-09-21 18:33:05 +08:00
|
|
|
|
|
2023-11-12 01:59:17 +08:00
|
|
|
|
data = new byte[(int)totlen];
|
|
|
|
|
Buffer.BlockCopy(tbytes, 0, data, 0, (int)totlen);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ArrayPool<byte>.Shared.Return(tbytes);
|
|
|
|
|
|
|
|
|
|
return data;
|
2017-09-21 18:33:05 +08:00
|
|
|
|
}
|
|
|
|
|
|
2024-01-07 02:41:10 +08:00
|
|
|
|
public static T? GetFile<T>(RpfEntry e) where T : class, PackedFile, new()
|
2017-09-21 18:33:05 +08:00
|
|
|
|
{
|
2024-01-07 02:41:10 +08:00
|
|
|
|
T? file = null;
|
|
|
|
|
byte[]? data = null;
|
|
|
|
|
RpfFileEntry? entry = e as RpfFileEntry;
|
2017-09-21 18:33:05 +08:00
|
|
|
|
if (entry != null)
|
|
|
|
|
{
|
|
|
|
|
data = entry.File.ExtractFile(entry);
|
|
|
|
|
}
|
|
|
|
|
if (data != null)
|
|
|
|
|
{
|
|
|
|
|
file = new T();
|
|
|
|
|
file.Load(data, entry);
|
|
|
|
|
}
|
|
|
|
|
return file;
|
|
|
|
|
}
|
|
|
|
|
public static T GetFile<T>(RpfEntry e, byte[] data) where T : class, PackedFile, new()
|
|
|
|
|
{
|
2024-01-07 02:41:10 +08:00
|
|
|
|
ArgumentNullException.ThrowIfNull(data, nameof(data));
|
|
|
|
|
if (e is not RpfFileEntry entry)
|
2017-09-21 18:33:05 +08:00
|
|
|
|
{
|
2024-01-07 02:41:10 +08:00
|
|
|
|
entry = CreateResourceFileEntry(ref data, 0);
|
2017-09-21 18:33:05 +08:00
|
|
|
|
}
|
2024-01-07 02:41:10 +08:00
|
|
|
|
var file = new T();
|
|
|
|
|
file.Load(data, entry);
|
2017-09-21 18:33:05 +08:00
|
|
|
|
return file;
|
|
|
|
|
}
|
2018-03-08 09:15:28 +08:00
|
|
|
|
|
2023-10-28 03:31:09 +08:00
|
|
|
|
public static T GetFile<T>(RpfEntry e, Stream data) where T : class, PackedFileStream, new()
|
|
|
|
|
{
|
2024-01-07 02:41:10 +08:00
|
|
|
|
ArgumentNullException.ThrowIfNull(data, nameof(data));
|
|
|
|
|
T? file = null;
|
|
|
|
|
if (data is not null)
|
2023-10-28 03:31:09 +08:00
|
|
|
|
{
|
2024-01-07 02:41:10 +08:00
|
|
|
|
if (e is not RpfFileEntry entry)
|
2023-10-28 03:31:09 +08:00
|
|
|
|
{
|
|
|
|
|
entry = CreateResourceFileEntry(data, 0);
|
|
|
|
|
}
|
|
|
|
|
file = new T();
|
|
|
|
|
file.Load(data, entry);
|
|
|
|
|
}
|
|
|
|
|
return file;
|
|
|
|
|
}
|
|
|
|
|
|
2018-03-08 09:15:28 +08:00
|
|
|
|
|
|
|
|
|
|
2024-01-07 02:41:10 +08:00
|
|
|
|
public static T? GetResourceFile<T>(byte[] data) where T : class, PackedFile, new()
|
2017-09-21 18:33:05 +08:00
|
|
|
|
{
|
2024-01-07 02:41:10 +08:00
|
|
|
|
T? file = null;
|
2018-03-08 09:15:28 +08:00
|
|
|
|
RpfFileEntry entry = CreateResourceFileEntry(ref data, 0);
|
2024-01-07 02:41:10 +08:00
|
|
|
|
if (data != null && entry != null)
|
2017-09-21 18:33:05 +08:00
|
|
|
|
{
|
2018-05-01 11:20:39 +08:00
|
|
|
|
data = ResourceBuilder.Decompress(data);
|
2017-09-21 18:33:05 +08:00
|
|
|
|
file = new T();
|
|
|
|
|
file.Load(data, entry);
|
|
|
|
|
}
|
|
|
|
|
return file;
|
|
|
|
|
}
|
2023-10-28 03:31:09 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public static void LoadResourceFile<T>(T file, Stream data, uint ver) where T : class, PackedFileStream
|
|
|
|
|
{
|
|
|
|
|
//direct load from a raw, compressed resource file (openIV-compatible format)
|
|
|
|
|
|
|
|
|
|
RpfResourceFileEntry resentry = CreateResourceFileEntry(data, ver);
|
|
|
|
|
|
|
|
|
|
if (file is GameFile)
|
|
|
|
|
{
|
|
|
|
|
GameFile gfile = file as GameFile;
|
|
|
|
|
|
2024-01-07 02:41:10 +08:00
|
|
|
|
if (gfile.RpfFileEntry is RpfResourceFileEntry oldresentry) //update the existing entry with the new one
|
2023-10-28 03:31:09 +08:00
|
|
|
|
{
|
|
|
|
|
oldresentry.SystemFlags = resentry.SystemFlags;
|
|
|
|
|
oldresentry.GraphicsFlags = resentry.GraphicsFlags;
|
|
|
|
|
resentry.Name = oldresentry.Name;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
gfile.RpfFileEntry = resentry; //just stick it in there for later...
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
data = ResourceBuilder.Decompress(data);
|
|
|
|
|
|
|
|
|
|
file.Load(data, resentry);
|
|
|
|
|
}
|
|
|
|
|
|
2018-03-08 09:15:28 +08:00
|
|
|
|
public static void LoadResourceFile<T>(T file, byte[] data, uint ver) where T : class, PackedFile
|
|
|
|
|
{
|
|
|
|
|
//direct load from a raw, compressed resource file (openIV-compatible format)
|
2017-09-21 18:33:05 +08:00
|
|
|
|
|
2018-03-08 09:15:28 +08:00
|
|
|
|
RpfResourceFileEntry resentry = CreateResourceFileEntry(ref data, ver);
|
2017-09-21 18:33:05 +08:00
|
|
|
|
|
2024-01-07 02:41:10 +08:00
|
|
|
|
if (file is GameFile gfile)
|
2018-03-08 09:15:28 +08:00
|
|
|
|
{
|
2024-01-07 02:41:10 +08:00
|
|
|
|
if (gfile.RpfFileEntry is RpfResourceFileEntry oldresentry) //update the existing entry with the new one
|
2018-03-08 09:15:28 +08:00
|
|
|
|
{
|
|
|
|
|
oldresentry.SystemFlags = resentry.SystemFlags;
|
|
|
|
|
oldresentry.GraphicsFlags = resentry.GraphicsFlags;
|
|
|
|
|
resentry.Name = oldresentry.Name;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
gfile.RpfFileEntry = resentry; //just stick it in there for later...
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
data = ResourceBuilder.Decompress(data);
|
|
|
|
|
|
|
|
|
|
file.Load(data, resentry);
|
2024-01-07 02:41:10 +08:00
|
|
|
|
}
|
2018-03-08 09:15:28 +08:00
|
|
|
|
|
2024-01-07 02:41:10 +08:00
|
|
|
|
public static async Task LoadResourceFileAsync<T>(T file, byte[] data, uint ver) where T : class, PackedFile
|
|
|
|
|
{
|
|
|
|
|
RpfResourceFileEntry resentry = CreateResourceFileEntry(ref data, ver);
|
|
|
|
|
|
|
|
|
|
if (file is GameFile gfile)
|
|
|
|
|
{
|
|
|
|
|
if (gfile.RpfFileEntry is RpfResourceFileEntry oldresentry) //update the existing entry with the new one
|
|
|
|
|
{
|
|
|
|
|
oldresentry.SystemFlags = resentry.SystemFlags;
|
|
|
|
|
oldresentry.GraphicsFlags = resentry.GraphicsFlags;
|
|
|
|
|
resentry.Name = oldresentry.Name;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
gfile.RpfFileEntry = resentry; //just stick it in there for later...
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
data = await ResourceBuilder.DecompressAsync(data);
|
|
|
|
|
|
|
|
|
|
file.Load(data, resentry);
|
2018-03-08 09:15:28 +08:00
|
|
|
|
}
|
2023-10-28 03:31:09 +08:00
|
|
|
|
|
|
|
|
|
public static RpfResourceFileEntry CreateResourceFileEntry(Stream stream, uint ver, uint? header = null)
|
|
|
|
|
{
|
|
|
|
|
var resentry = new RpfResourceFileEntry();
|
|
|
|
|
|
|
|
|
|
using var reader = new BinaryReader(stream, Encoding.UTF8, true);
|
|
|
|
|
|
|
|
|
|
//hopefully this data has an RSC7 header...
|
|
|
|
|
uint rsc7 = header ?? reader.ReadUInt32(); // BitConverter.ToUInt32(data, 0);
|
|
|
|
|
if (rsc7 == 0x37435352) //RSC7 header present!
|
|
|
|
|
{
|
|
|
|
|
int version = reader.ReadInt32(); // BitConverter.ToInt32(data, 4);//use this instead of what was given...
|
|
|
|
|
resentry.SystemFlags = reader.ReadUInt32();// BitConverter.ToUInt32(data, 8);
|
|
|
|
|
resentry.GraphicsFlags = reader.ReadUInt32(); //BitConverter.ToUInt32(data, 12);
|
|
|
|
|
//if (stream.Length > 16)
|
|
|
|
|
//{
|
|
|
|
|
// int newlen = stream.Length - 16; //trim the header from the data passed to the next step.
|
|
|
|
|
// byte[] newdata = new byte[newlen];
|
|
|
|
|
// Buffer.BlockCopy(data, 16, newdata, 0, newlen);
|
|
|
|
|
// data = newdata;
|
|
|
|
|
//}
|
|
|
|
|
//else
|
|
|
|
|
//{
|
|
|
|
|
// data = null; //shouldn't happen... empty..
|
|
|
|
|
//}
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
//direct load from file without the rpf header..
|
|
|
|
|
//assume it's in resource meta format
|
|
|
|
|
resentry.SystemFlags = RpfResourceFileEntry.GetFlagsFromSize((int)stream.Length, 0);
|
|
|
|
|
resentry.GraphicsFlags = RpfResourceFileEntry.GetFlagsFromSize(0, ver);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
resentry.Name = "";
|
|
|
|
|
|
|
|
|
|
return resentry;
|
|
|
|
|
}
|
|
|
|
|
|
2018-03-08 09:15:28 +08:00
|
|
|
|
public static RpfResourceFileEntry CreateResourceFileEntry(ref byte[] data, uint ver)
|
2017-09-21 18:33:05 +08:00
|
|
|
|
{
|
|
|
|
|
var resentry = new RpfResourceFileEntry();
|
|
|
|
|
|
2018-03-08 09:15:28 +08:00
|
|
|
|
//hopefully this data has an RSC7 header...
|
2017-09-21 18:33:05 +08:00
|
|
|
|
uint rsc7 = BitConverter.ToUInt32(data, 0);
|
|
|
|
|
if (rsc7 == 0x37435352) //RSC7 header present!
|
|
|
|
|
{
|
|
|
|
|
int version = BitConverter.ToInt32(data, 4);//use this instead of what was given...
|
|
|
|
|
resentry.SystemFlags = BitConverter.ToUInt32(data, 8);
|
|
|
|
|
resentry.GraphicsFlags = BitConverter.ToUInt32(data, 12);
|
2018-03-08 09:15:28 +08:00
|
|
|
|
if (data.Length > 16)
|
|
|
|
|
{
|
|
|
|
|
int newlen = data.Length - 16; //trim the header from the data passed to the next step.
|
|
|
|
|
byte[] newdata = new byte[newlen];
|
|
|
|
|
Buffer.BlockCopy(data, 16, newdata, 0, newlen);
|
|
|
|
|
data = newdata;
|
|
|
|
|
}
|
2017-09-21 18:33:05 +08:00
|
|
|
|
//else
|
|
|
|
|
//{
|
|
|
|
|
// data = null; //shouldn't happen... empty..
|
|
|
|
|
//}
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
//direct load from file without the rpf header..
|
|
|
|
|
//assume it's in resource meta format
|
|
|
|
|
resentry.SystemFlags = RpfResourceFileEntry.GetFlagsFromSize(data.Length, 0);
|
2018-03-08 09:15:28 +08:00
|
|
|
|
resentry.GraphicsFlags = RpfResourceFileEntry.GetFlagsFromSize(0, ver);
|
2017-09-21 18:33:05 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
resentry.Name = "";
|
|
|
|
|
|
|
|
|
|
return resentry;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public string TestExtractAllFiles()
|
|
|
|
|
{
|
|
|
|
|
StringBuilder sb = new StringBuilder();
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
using (BinaryReader br = new BinaryReader(File.OpenRead(GetPhysicalFilePath())))
|
|
|
|
|
{
|
|
|
|
|
foreach (RpfEntry entry in AllEntries)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
LastError = string.Empty;
|
|
|
|
|
LastException = null;
|
2023-11-12 01:59:17 +08:00
|
|
|
|
if (!entry.IsExtension(".rpf")) //don't try to extract rpf's, they will be done separately..
|
2017-09-21 18:33:05 +08:00
|
|
|
|
{
|
2024-01-07 02:41:10 +08:00
|
|
|
|
if (entry is RpfBinaryFileEntry binentry)
|
2017-09-21 18:33:05 +08:00
|
|
|
|
{
|
2024-01-07 02:41:10 +08:00
|
|
|
|
try
|
2017-09-21 18:33:05 +08:00
|
|
|
|
{
|
2024-01-07 02:41:10 +08:00
|
|
|
|
byte[]? data = ExtractFileBinary(binentry, br);
|
|
|
|
|
if (data is null)
|
2017-09-21 18:33:05 +08:00
|
|
|
|
{
|
2024-01-07 02:41:10 +08:00
|
|
|
|
if (binentry.FileSize == 0)
|
|
|
|
|
{
|
|
|
|
|
sb.AppendLine($"{entry.Path} : Binary FileSize is 0.");
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
sb.AppendLine($"{entry.Path} : {LastError}");
|
|
|
|
|
}
|
2017-09-21 18:33:05 +08:00
|
|
|
|
}
|
2024-01-07 02:41:10 +08:00
|
|
|
|
else if (data.Length == 0)
|
2017-09-21 18:33:05 +08:00
|
|
|
|
{
|
2024-01-07 02:41:10 +08:00
|
|
|
|
sb.AppendLine($"{entry.Path} : Decompressed output was empty.");
|
2017-09-21 18:33:05 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
2024-01-07 02:41:10 +08:00
|
|
|
|
catch(Exception ex)
|
2017-09-21 18:33:05 +08:00
|
|
|
|
{
|
2024-01-07 02:41:10 +08:00
|
|
|
|
sb.AppendLine($"{entry.Path} : {ex}");
|
2017-09-21 18:33:05 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
2024-01-07 02:41:10 +08:00
|
|
|
|
else if (entry is RpfResourceFileEntry resentry)
|
2017-09-21 18:33:05 +08:00
|
|
|
|
{
|
2024-01-07 02:41:10 +08:00
|
|
|
|
try
|
2017-09-21 18:33:05 +08:00
|
|
|
|
{
|
2024-01-07 02:41:10 +08:00
|
|
|
|
byte[]? data = ExtractFileResource(resentry, br);
|
|
|
|
|
if (data == null)
|
2017-09-21 18:33:05 +08:00
|
|
|
|
{
|
2024-01-07 02:41:10 +08:00
|
|
|
|
if (resentry.FileSize == 0)
|
|
|
|
|
{
|
|
|
|
|
sb.AppendLine($"{entry.Path} : Resource FileSize is 0.");
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
sb.AppendLine($"{entry.Path} : {LastError}");
|
|
|
|
|
}
|
2017-09-21 18:33:05 +08:00
|
|
|
|
}
|
2024-01-07 02:41:10 +08:00
|
|
|
|
else if (data.Length == 0)
|
2017-09-21 18:33:05 +08:00
|
|
|
|
{
|
2024-01-07 02:41:10 +08:00
|
|
|
|
sb.AppendLine($"{entry.Path} : Decompressed output was empty.");
|
2017-09-21 18:33:05 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
2024-01-07 02:41:10 +08:00
|
|
|
|
catch(Exception ex)
|
2017-09-21 18:33:05 +08:00
|
|
|
|
{
|
2024-01-07 02:41:10 +08:00
|
|
|
|
sb.AppendLine($"{entry.Path} : {ex}");
|
2017-09-21 18:33:05 +08:00
|
|
|
|
}
|
2024-01-07 02:41:10 +08:00
|
|
|
|
|
2017-09-21 18:33:05 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
LastError = ex.ToString();
|
|
|
|
|
LastException = ex;
|
2024-01-07 02:41:10 +08:00
|
|
|
|
Console.WriteLine(ex);
|
|
|
|
|
sb.AppendLine($"{entry.Path} : {ex.Message}");
|
2017-09-21 18:33:05 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
LastError = ex.ToString();
|
|
|
|
|
LastException = ex;
|
2024-01-07 02:41:10 +08:00
|
|
|
|
Console.WriteLine(ex);
|
|
|
|
|
sb.AppendLine($"{Path} : {ex.Message}");
|
2017-09-21 18:33:05 +08:00
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
return sb.ToString();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2024-01-07 02:41:10 +08:00
|
|
|
|
public IReadOnlyCollection<RpfFileEntry> GetFiles(string folder, bool recurse)
|
2017-09-21 18:33:05 +08:00
|
|
|
|
{
|
2023-11-14 23:16:59 +08:00
|
|
|
|
if (Root == null)
|
2017-09-21 18:33:05 +08:00
|
|
|
|
{
|
2024-01-07 02:41:10 +08:00
|
|
|
|
return [];
|
2017-09-21 18:33:05 +08:00
|
|
|
|
}
|
2024-01-07 02:41:10 +08:00
|
|
|
|
PooledList<RpfFileEntry> result = new PooledList<RpfFileEntry>();
|
2023-11-14 23:16:59 +08:00
|
|
|
|
//folder.AsSpan().Split
|
|
|
|
|
RpfDirectoryEntry dir = Root;
|
|
|
|
|
foreach(var part in folder.EnumerateSplit('\\'))
|
2017-09-21 18:33:05 +08:00
|
|
|
|
{
|
2023-11-14 23:16:59 +08:00
|
|
|
|
if (part.Length == 0)
|
|
|
|
|
continue;
|
|
|
|
|
dir = FindSubDirectory(dir, part);
|
2024-01-07 02:41:10 +08:00
|
|
|
|
//if (dir is null)
|
|
|
|
|
//{
|
|
|
|
|
// return Array.Empty<RpfFileEntry>();
|
|
|
|
|
//}
|
2017-09-21 18:33:05 +08:00
|
|
|
|
}
|
2023-11-14 23:16:59 +08:00
|
|
|
|
|
|
|
|
|
GetFiles(dir, result, recurse);
|
|
|
|
|
|
2017-09-21 18:33:05 +08:00
|
|
|
|
return result;
|
|
|
|
|
}
|
2024-01-07 02:41:10 +08:00
|
|
|
|
public void GetFiles(RpfDirectoryEntry dir, PooledList<RpfFileEntry> result, bool recurse)
|
2017-09-21 18:33:05 +08:00
|
|
|
|
{
|
|
|
|
|
if (dir.Files != null)
|
|
|
|
|
{
|
|
|
|
|
result.AddRange(dir.Files);
|
|
|
|
|
}
|
|
|
|
|
if (recurse)
|
|
|
|
|
{
|
|
|
|
|
if (dir.Directories != null)
|
|
|
|
|
{
|
2024-01-07 02:41:10 +08:00
|
|
|
|
foreach(var dirEntry in dir.Directories)
|
2017-09-21 18:33:05 +08:00
|
|
|
|
{
|
2024-01-07 02:41:10 +08:00
|
|
|
|
GetFiles(dirEntry, result, recurse);
|
2017-09-21 18:33:05 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-07 02:41:10 +08:00
|
|
|
|
private RpfDirectoryEntry? FindSubDirectory(RpfDirectoryEntry dir, ReadOnlySpan<char> name)
|
2023-11-14 23:16:59 +08:00
|
|
|
|
{
|
|
|
|
|
if (dir == null) return null;
|
|
|
|
|
if (dir.Directories == null) return null;
|
2017-09-21 18:33:05 +08:00
|
|
|
|
|
2023-11-14 23:16:59 +08:00
|
|
|
|
for (int i = 0; i < dir.Directories.Count; i++)
|
|
|
|
|
{
|
|
|
|
|
var cdir = dir.Directories[i];
|
|
|
|
|
if (cdir.Name.AsSpan() == name || cdir.Name.AsSpan().Equals(name, StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
{
|
|
|
|
|
return cdir;
|
|
|
|
|
}
|
|
|
|
|
}
|
2017-09-21 18:33:05 +08:00
|
|
|
|
|
2023-11-14 23:16:59 +08:00
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2023-11-12 01:59:17 +08:00
|
|
|
|
public static RecyclableMemoryStreamManager recyclableMemoryStreamManager = new RecyclableMemoryStreamManager(256 * 1024, 1024 * 1024, 128 * 1024 * 1024, false, 256 * 1024 * 100, 1024 * 1024 * 128 * 4);
|
|
|
|
|
|
2024-01-07 02:41:10 +08:00
|
|
|
|
public byte[] DecompressBytes(byte[] bytes, long requiredSize = -1)
|
2017-09-21 18:33:05 +08:00
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
2024-01-07 02:41:10 +08:00
|
|
|
|
if (requiredSize == -1)
|
|
|
|
|
{
|
|
|
|
|
requiredSize = bytes.Length;
|
|
|
|
|
}
|
2023-11-12 01:59:17 +08:00
|
|
|
|
using DeflateStream ds = new DeflateStream(new MemoryStream(bytes), CompressionMode.Decompress);
|
2024-01-07 02:41:10 +08:00
|
|
|
|
using var outstr = new MemoryStream((int)requiredSize);
|
2023-11-12 01:59:17 +08:00
|
|
|
|
|
2023-11-14 23:16:59 +08:00
|
|
|
|
ds.CopyTo(outstr, 524288);
|
2024-01-07 02:41:10 +08:00
|
|
|
|
|
|
|
|
|
byte[] outbuf = Array.Empty<byte>();
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
outbuf = outstr.GetBuffer();
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
// Failed to get buffer
|
|
|
|
|
Console.WriteLine(ex);
|
|
|
|
|
outbuf = outstr.ToArray();
|
|
|
|
|
}
|
2023-11-12 01:59:17 +08:00
|
|
|
|
|
|
|
|
|
if (outbuf.Length <= bytes.Length)
|
2017-09-21 18:33:05 +08:00
|
|
|
|
{
|
2023-11-12 01:59:17 +08:00
|
|
|
|
LastError = "Warning: Decompressed data was smaller than compressed data...";
|
|
|
|
|
//return null; //could still be OK for tiny things!
|
|
|
|
|
}
|
2017-09-21 18:33:05 +08:00
|
|
|
|
|
2024-01-07 02:41:10 +08:00
|
|
|
|
if (outbuf.Length != requiredSize)
|
|
|
|
|
{
|
|
|
|
|
Console.WriteLine($"Buffer was resized for expectedSize: {requiredSize}; actualSize: {outbuf.Length}");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Ensure array is correct size (this ensures everything works even is expectedSize is incorrect, but adds allocation overhead)
|
|
|
|
|
if (outbuf.Length != outstr.Length)
|
|
|
|
|
{
|
|
|
|
|
Console.WriteLine($"Calling to array on MemoryStream");
|
|
|
|
|
outbuf = outbuf.ToArray();
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-12 01:59:17 +08:00
|
|
|
|
return outbuf;
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
LastError = "Could not decompress.";// ex.ToString();
|
|
|
|
|
LastException = ex;
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
2022-07-30 19:54:21 +08:00
|
|
|
|
|
2024-01-07 02:41:10 +08:00
|
|
|
|
public async Task<byte[]> DecompressBytesAsync(byte[] bytes, long expectedSize = -1)
|
2023-11-12 01:59:17 +08:00
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
2024-01-07 02:41:10 +08:00
|
|
|
|
if (expectedSize == -1)
|
|
|
|
|
{
|
|
|
|
|
expectedSize = bytes.Length;
|
|
|
|
|
}
|
2023-11-12 01:59:17 +08:00
|
|
|
|
using DeflateStream ds = new DeflateStream(new MemoryStream(bytes), CompressionMode.Decompress);
|
2024-01-07 02:41:10 +08:00
|
|
|
|
using var outstr = new MemoryStream((int)expectedSize);
|
2023-11-12 01:59:17 +08:00
|
|
|
|
|
2023-11-14 23:16:59 +08:00
|
|
|
|
await ds.CopyToAsync(outstr, 524288).ConfigureAwait(false);
|
2024-01-07 02:41:10 +08:00
|
|
|
|
byte[] outbuf = Array.Empty<byte>();
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
outbuf = outstr.GetBuffer();
|
|
|
|
|
}
|
|
|
|
|
catch(Exception ex)
|
|
|
|
|
{
|
|
|
|
|
Console.WriteLine(ex);
|
|
|
|
|
outbuf = outstr.ToArray();
|
|
|
|
|
}
|
2023-11-12 01:59:17 +08:00
|
|
|
|
|
|
|
|
|
if (outbuf.Length <= bytes.Length)
|
|
|
|
|
{
|
|
|
|
|
LastError = "Warning: Decompressed data was smaller than compressed data...";
|
|
|
|
|
//return null; //could still be OK for tiny things!
|
2017-09-21 18:33:05 +08:00
|
|
|
|
}
|
2023-11-12 01:59:17 +08:00
|
|
|
|
|
2024-01-07 02:41:10 +08:00
|
|
|
|
if (outbuf.Length != expectedSize)
|
|
|
|
|
{
|
|
|
|
|
Console.WriteLine($"Buffer was resized for expectedSize: {expectedSize}; actualSize: {outbuf.Length}");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Ensure array is correct size (this ensures everything works even is expectedSize is incorrect, but adds allocation overhead)
|
|
|
|
|
if (outbuf.Length != outstr.Length)
|
|
|
|
|
{
|
|
|
|
|
Console.WriteLine($"Calling to array on MemoryStream");
|
|
|
|
|
outbuf = outbuf.ToArray();
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-12 01:59:17 +08:00
|
|
|
|
return outbuf;
|
2017-09-21 18:33:05 +08:00
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
LastError = "Could not decompress.";// ex.ToString();
|
|
|
|
|
LastException = ex;
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-11-12 01:59:17 +08:00
|
|
|
|
|
2018-01-10 11:17:30 +08:00
|
|
|
|
public static byte[] CompressBytes(byte[] data) //TODO: refactor this with ResourceBuilder.Compress/Decompress
|
|
|
|
|
{
|
2024-01-07 02:41:10 +08:00
|
|
|
|
using (MemoryStream ms = recyclableMemoryStreamManager.GetStream("CompressBytes"))
|
2018-01-10 11:17:30 +08:00
|
|
|
|
{
|
2024-01-07 02:41:10 +08:00
|
|
|
|
using (var ds = new DeflateStream(ms, CompressionLevel.SmallestSize, true))
|
2022-07-30 19:54:21 +08:00
|
|
|
|
{
|
|
|
|
|
ds.Write(data, 0, data.Length);
|
|
|
|
|
ds.Close();
|
2023-11-14 23:16:59 +08:00
|
|
|
|
byte[] outbuf = ms.ToArray(); //need to copy to the right size buffer...
|
2022-07-30 19:54:21 +08:00
|
|
|
|
return outbuf;
|
|
|
|
|
}
|
2018-01-10 11:17:30 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private void WriteHeader(BinaryWriter bw)
|
|
|
|
|
{
|
|
|
|
|
var namesdata = GetHeaderNamesData();
|
|
|
|
|
NamesLength = (uint)namesdata.Length;
|
|
|
|
|
|
|
|
|
|
//ensure there's enough space for the new header, move things if necessary
|
|
|
|
|
var headersize = GetHeaderBlockCount() * 512;
|
|
|
|
|
EnsureSpace(bw, null, headersize);
|
|
|
|
|
|
|
|
|
|
//entries may have been updated, so need to do this after ensuring header space
|
|
|
|
|
var entriesdata = GetHeaderEntriesData();
|
|
|
|
|
|
|
|
|
|
//FileSize = ... //need to make sure this is updated for NG encryption...
|
|
|
|
|
switch (Encryption)
|
|
|
|
|
{
|
|
|
|
|
case RpfEncryption.NONE: //no encryption
|
|
|
|
|
case RpfEncryption.OPEN: //OpenIV style RPF with unencrypted TOC
|
2024-01-07 02:41:10 +08:00
|
|
|
|
case RpfEncryption.CFXP:
|
2018-01-10 11:17:30 +08:00
|
|
|
|
break;
|
|
|
|
|
case RpfEncryption.AES:
|
|
|
|
|
entriesdata = GTACrypto.EncryptAES(entriesdata);
|
|
|
|
|
namesdata = GTACrypto.EncryptAES(namesdata);
|
|
|
|
|
IsAESEncrypted = true;
|
|
|
|
|
break;
|
|
|
|
|
case RpfEncryption.NG:
|
|
|
|
|
entriesdata = GTACrypto.EncryptNG(entriesdata, Name, (uint)FileSize);
|
|
|
|
|
namesdata = GTACrypto.EncryptNG(namesdata, Name, (uint)FileSize);
|
|
|
|
|
IsNGEncrypted = true;
|
|
|
|
|
break;
|
|
|
|
|
default: //unknown encryption type? assume NG.. should never get here!
|
|
|
|
|
entriesdata = GTACrypto.EncryptNG(entriesdata, Name, (uint)FileSize);
|
|
|
|
|
namesdata = GTACrypto.EncryptNG(namesdata, Name, (uint)FileSize);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
2018-01-11 09:10:03 +08:00
|
|
|
|
//now there's enough space, it's safe to write the header data...
|
|
|
|
|
bw.BaseStream.Position = StartPos;
|
|
|
|
|
|
|
|
|
|
bw.Write(Version);
|
|
|
|
|
bw.Write(EntryCount);
|
|
|
|
|
bw.Write(NamesLength);
|
|
|
|
|
bw.Write((uint)Encryption);
|
2018-01-10 11:17:30 +08:00
|
|
|
|
bw.Write(entriesdata);
|
|
|
|
|
bw.Write(namesdata);
|
|
|
|
|
|
|
|
|
|
WritePadding(bw.BaseStream, StartPos + headersize); //makes sure the actual file can grow...
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private static void WritePadding(Stream s, long upto)
|
|
|
|
|
{
|
|
|
|
|
int diff = (int)(upto - s.Position);
|
|
|
|
|
if (diff > 0)
|
|
|
|
|
{
|
|
|
|
|
s.Write(new byte[diff], 0, diff);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-07 02:41:10 +08:00
|
|
|
|
[MemberNotNull(nameof(AllEntries))]
|
2018-01-10 11:17:30 +08:00
|
|
|
|
private void EnsureAllEntries()
|
|
|
|
|
{
|
|
|
|
|
if (AllEntries == null)
|
|
|
|
|
{
|
|
|
|
|
//assume this is a new RPF, create the root directory entry
|
2024-01-07 02:41:10 +08:00
|
|
|
|
AllEntries = new PooledList<RpfEntry>();
|
2018-01-10 11:17:30 +08:00
|
|
|
|
Root = new RpfDirectoryEntry();
|
|
|
|
|
Root.File = this;
|
|
|
|
|
Root.Name = string.Empty;
|
2023-11-12 01:59:17 +08:00
|
|
|
|
Root.Path = Path;
|
2018-01-10 11:17:30 +08:00
|
|
|
|
}
|
|
|
|
|
if (Children == null)
|
|
|
|
|
{
|
2024-01-07 02:41:10 +08:00
|
|
|
|
Children = new PooledList<RpfFile>();
|
2018-01-10 11:17:30 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//re-build the AllEntries list from the root node.
|
|
|
|
|
List<RpfEntry> temp = new List<RpfEntry>(); //for sorting
|
2024-01-07 02:41:10 +08:00
|
|
|
|
|
|
|
|
|
//using var tempList = new PooledList<RpfEntry>();
|
|
|
|
|
|
2018-01-10 11:17:30 +08:00
|
|
|
|
AllEntries.Clear();
|
2024-01-07 02:41:10 +08:00
|
|
|
|
//tempList.Add(Root);
|
2018-01-10 11:17:30 +08:00
|
|
|
|
AllEntries.Add(Root);
|
|
|
|
|
Stack<RpfDirectoryEntry> stack = new Stack<RpfDirectoryEntry>();
|
|
|
|
|
stack.Push(Root);
|
|
|
|
|
while (stack.Count > 0)
|
|
|
|
|
{
|
|
|
|
|
var item = stack.Pop();
|
|
|
|
|
|
|
|
|
|
item.EntriesCount = (uint)(item.Directories.Count + item.Files.Count);
|
|
|
|
|
item.EntriesIndex = (uint)AllEntries.Count;
|
|
|
|
|
|
|
|
|
|
//having items sorted by name is important for the game for some reason. (it crashes otherwise!)
|
|
|
|
|
temp.Clear();
|
|
|
|
|
temp.AddRange(item.Directories);
|
|
|
|
|
temp.AddRange(item.Files);
|
|
|
|
|
temp.Sort((a, b) => String.CompareOrdinal(a.Name, b.Name));
|
|
|
|
|
|
|
|
|
|
foreach (var entry in temp)
|
|
|
|
|
{
|
2024-01-07 02:41:10 +08:00
|
|
|
|
//tempList.Add(entry);
|
2018-01-10 11:17:30 +08:00
|
|
|
|
AllEntries.Add(entry);
|
2024-01-07 02:41:10 +08:00
|
|
|
|
if (entry is RpfDirectoryEntry dir)
|
2018-01-10 11:17:30 +08:00
|
|
|
|
{
|
|
|
|
|
stack.Push(dir);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-07 02:41:10 +08:00
|
|
|
|
//AllEntries.AddRange(tempRange);
|
|
|
|
|
|
2018-01-10 11:17:30 +08:00
|
|
|
|
EntryCount = (uint)AllEntries.Count;
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
private byte[] GetHeaderNamesData()
|
|
|
|
|
{
|
2023-11-14 23:16:59 +08:00
|
|
|
|
using MemoryStream namesstream = new MemoryStream();
|
|
|
|
|
using DataWriter nameswriter = new DataWriter(namesstream);
|
2018-01-10 11:17:30 +08:00
|
|
|
|
var namedict = new Dictionary<string, uint>();
|
|
|
|
|
foreach (var entry in AllEntries)
|
|
|
|
|
{
|
|
|
|
|
uint nameoffset;
|
|
|
|
|
string name = entry.Name ?? "";
|
|
|
|
|
if (namedict.TryGetValue(name, out nameoffset))
|
|
|
|
|
{
|
|
|
|
|
entry.NameOffset = nameoffset;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
entry.NameOffset = (uint)namesstream.Length;
|
|
|
|
|
namedict.Add(name, entry.NameOffset);
|
|
|
|
|
nameswriter.Write(name);
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-11-14 23:16:59 +08:00
|
|
|
|
var buf = new byte[Math.Max(namesstream.Length, 16)];
|
2018-01-10 11:17:30 +08:00
|
|
|
|
namesstream.Position = 0;
|
2023-11-14 23:16:59 +08:00
|
|
|
|
namesstream.Read(buf, 0, (int)namesstream.Length);
|
|
|
|
|
return buf;
|
2018-01-10 11:17:30 +08:00
|
|
|
|
}
|
|
|
|
|
private byte[] GetHeaderEntriesData()
|
|
|
|
|
{
|
|
|
|
|
MemoryStream entriesstream = new MemoryStream();
|
|
|
|
|
DataWriter entrieswriter = new DataWriter(entriesstream);
|
|
|
|
|
foreach (var entry in AllEntries)
|
|
|
|
|
{
|
|
|
|
|
entry.Write(entrieswriter);
|
|
|
|
|
}
|
|
|
|
|
var buf = new byte[entriesstream.Length];
|
|
|
|
|
entriesstream.Position = 0;
|
|
|
|
|
entriesstream.Read(buf, 0, buf.Length);
|
|
|
|
|
return buf;
|
|
|
|
|
}
|
|
|
|
|
private uint GetHeaderBlockCount()//make sure EntryCount and NamesLength are updated before calling this...
|
|
|
|
|
{
|
|
|
|
|
uint headerusedbytes = 16 + (EntryCount * 16) + NamesLength;
|
|
|
|
|
uint headerblockcount = GetBlockCount(headerusedbytes);
|
|
|
|
|
return headerblockcount;
|
|
|
|
|
}
|
|
|
|
|
private static uint PadLength(uint l, uint n)//round up to nearest n bytes
|
|
|
|
|
{
|
|
|
|
|
uint rem = l % n;
|
|
|
|
|
return l + ((rem > 0) ? (n - rem) : 0);
|
|
|
|
|
}
|
|
|
|
|
private static uint GetBlockCount(long bytecount)
|
|
|
|
|
{
|
|
|
|
|
uint b0 = (uint)(bytecount & 0x1FF); //511;
|
|
|
|
|
uint b1 = (uint)(bytecount >> 9);
|
|
|
|
|
if (b0 == 0) return b1;
|
|
|
|
|
return b1 + 1;
|
|
|
|
|
}
|
|
|
|
|
private RpfFileEntry FindFirstFileAfter(uint block)
|
|
|
|
|
{
|
|
|
|
|
RpfFileEntry nextentry = null;
|
|
|
|
|
foreach (var entry in AllEntries)
|
|
|
|
|
{
|
2024-01-07 02:41:10 +08:00
|
|
|
|
if ((entry is RpfFileEntry fe) && (fe.FileOffset > block))
|
2018-01-10 11:17:30 +08:00
|
|
|
|
{
|
|
|
|
|
if ((nextentry == null) || (fe.FileOffset < nextentry.FileOffset))
|
|
|
|
|
{
|
|
|
|
|
nextentry = fe;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return nextentry;
|
|
|
|
|
}
|
|
|
|
|
private uint FindHole(uint reqblocks, uint ignorestart, uint ignoreend)
|
|
|
|
|
{
|
|
|
|
|
//find the block index of a hole that can fit the required number of blocks.
|
|
|
|
|
//return 0 if no hole found (0 is the header block, it can't be used for files!)
|
|
|
|
|
//make sure any found hole is not within the ignore range
|
|
|
|
|
//(i.e. area where space is currently being made)
|
|
|
|
|
|
|
|
|
|
//gather and sort the list of files to allow searching for holes
|
|
|
|
|
List<RpfFileEntry> allfiles = new List<RpfFileEntry>();
|
|
|
|
|
foreach (var entry in AllEntries)
|
|
|
|
|
{
|
2024-01-07 02:41:10 +08:00
|
|
|
|
if (entry is RpfFileEntry rfe)
|
2018-01-10 11:17:30 +08:00
|
|
|
|
{
|
|
|
|
|
allfiles.Add(rfe);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
allfiles.Sort((e1, e2) => e1.FileOffset.CompareTo(e2.FileOffset));
|
|
|
|
|
|
|
|
|
|
//find the smallest available hole from the list.
|
|
|
|
|
uint found = 0;
|
|
|
|
|
uint foundsize = 0xFFFFFFFF;
|
|
|
|
|
|
|
|
|
|
for (int i = 1; i < allfiles.Count(); i++)
|
|
|
|
|
{
|
|
|
|
|
RpfFileEntry e1 = allfiles[i - 1];
|
|
|
|
|
RpfFileEntry e2 = allfiles[i];
|
|
|
|
|
|
|
|
|
|
uint e1cnt = GetBlockCount(e1.GetFileSize());
|
|
|
|
|
uint e1end = e1.FileOffset + e1cnt;
|
|
|
|
|
uint e2beg = e2.FileOffset;
|
|
|
|
|
if ((e2beg > ignorestart) && (e1end < ignoreend))
|
|
|
|
|
{
|
|
|
|
|
continue; //this space is in the ignore area.
|
|
|
|
|
}
|
|
|
|
|
if (e1end < e2beg)
|
|
|
|
|
{
|
|
|
|
|
uint space = e2beg - e1end;
|
|
|
|
|
if ((space >= reqblocks) && (space < foundsize))
|
|
|
|
|
{
|
|
|
|
|
found = e1end;
|
|
|
|
|
foundsize = space;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return found;
|
|
|
|
|
}
|
|
|
|
|
private uint FindEndBlock()
|
|
|
|
|
{
|
|
|
|
|
//find the next available block after all other files (or after header if there's no files)
|
|
|
|
|
uint endblock = 0;
|
|
|
|
|
foreach (var entry in AllEntries)
|
|
|
|
|
{
|
2024-01-07 02:41:10 +08:00
|
|
|
|
if (entry is RpfFileEntry e)
|
2018-01-10 11:17:30 +08:00
|
|
|
|
{
|
|
|
|
|
uint ecnt = GetBlockCount(e.GetFileSize());
|
|
|
|
|
uint eend = e.FileOffset + ecnt;
|
|
|
|
|
if (eend > endblock)
|
|
|
|
|
{
|
|
|
|
|
endblock = eend;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (endblock == 0)
|
|
|
|
|
{
|
|
|
|
|
//must be no files present, end block comes directly after the header.
|
|
|
|
|
endblock = GetHeaderBlockCount();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return endblock;
|
|
|
|
|
}
|
|
|
|
|
private void GrowArchive(BinaryWriter bw, uint newblockcount)
|
|
|
|
|
{
|
|
|
|
|
uint newsize = newblockcount * 512;
|
|
|
|
|
if (newsize < FileSize)
|
|
|
|
|
{
|
|
|
|
|
return;//already bigger than it needs to be, can happen if last file got moved into a hole...
|
|
|
|
|
}
|
|
|
|
|
if (FileSize == newsize)
|
|
|
|
|
{
|
|
|
|
|
return;//nothing to do... correct size already
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
FileSize = newsize;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//ensure enough space in the parent if there is one...
|
|
|
|
|
if (Parent != null)
|
|
|
|
|
{
|
|
|
|
|
if (ParentFileEntry == null)
|
|
|
|
|
{
|
|
|
|
|
throw new Exception("Can't grow archive " + Path + ": ParentFileEntry was null!");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//parent's header will be updated with these new values.
|
|
|
|
|
ParentFileEntry.FileUncompressedSize = newsize;
|
|
|
|
|
ParentFileEntry.FileSize = 0; //archives have FileSize==0 in parent...
|
|
|
|
|
|
|
|
|
|
Parent.EnsureSpace(bw, ParentFileEntry, newsize);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
private void RelocateFile(BinaryWriter bw, RpfFileEntry f, uint newblock)
|
|
|
|
|
{
|
|
|
|
|
//directly move this file. does NOT update the header!
|
|
|
|
|
//enough space should already be allocated for this move.
|
|
|
|
|
|
|
|
|
|
uint flen = GetBlockCount(f.GetFileSize());
|
|
|
|
|
uint fbeg = f.FileOffset;
|
|
|
|
|
uint fend = fbeg + flen;
|
|
|
|
|
uint nend = newblock + flen;
|
|
|
|
|
if ((nend > fbeg) && (newblock < fend))//can't move to somewhere within itself!
|
|
|
|
|
{
|
|
|
|
|
throw new Exception("Unable to relocate file " + f.Path + ": new position was inside the original!");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var stream = bw.BaseStream;
|
|
|
|
|
long origpos = stream.Position;
|
|
|
|
|
long source = StartPos + ((long)fbeg * 512);
|
|
|
|
|
long dest = StartPos + ((long)newblock * 512);
|
|
|
|
|
long newstart = dest;
|
|
|
|
|
long length = (long)flen * 512;
|
|
|
|
|
long destend = dest + length;
|
|
|
|
|
const int BUFFER_SIZE = 16384;//what buffer size is best for HDD copy?
|
|
|
|
|
var buffer = new byte[BUFFER_SIZE];
|
|
|
|
|
while (length > 0)
|
|
|
|
|
{
|
|
|
|
|
stream.Position = source;
|
|
|
|
|
int i = stream.Read(buffer, 0, (int)Math.Min(length, BUFFER_SIZE));
|
|
|
|
|
stream.Position = dest;
|
|
|
|
|
stream.Write(buffer, 0, i);
|
|
|
|
|
source += i;
|
|
|
|
|
dest += i;
|
|
|
|
|
length -= i;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
WritePadding(stream, destend);//makes sure the stream can grow if necessary
|
|
|
|
|
|
|
|
|
|
stream.Position = origpos;//reset this just to be nice
|
|
|
|
|
|
|
|
|
|
f.FileOffset = newblock;
|
|
|
|
|
|
|
|
|
|
//if this is a child RPF archive, need to update its StartPos...
|
|
|
|
|
var child = FindChildArchive(f);
|
|
|
|
|
if (child != null)
|
|
|
|
|
{
|
2018-01-12 12:19:32 +08:00
|
|
|
|
child.UpdateStartPos(newstart);
|
2018-01-10 11:17:30 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
private void EnsureSpace(BinaryWriter bw, RpfFileEntry e, long bytecount)
|
|
|
|
|
{
|
|
|
|
|
//(called with null entry for ensuring header space)
|
|
|
|
|
|
|
|
|
|
uint blockcount = GetBlockCount(bytecount);
|
|
|
|
|
uint startblock = e?.FileOffset ?? 0; //0 is always header block
|
|
|
|
|
uint endblock = startblock + blockcount;
|
|
|
|
|
|
|
|
|
|
RpfFileEntry nextentry = FindFirstFileAfter(startblock);
|
|
|
|
|
|
|
|
|
|
while (nextentry != null) //just deal with relocating one entry at a time.
|
|
|
|
|
{
|
|
|
|
|
//move this nextentry to somewhere else... preferably into a hole otherwise at the end
|
|
|
|
|
//if the RPF needs to grow, space needs to be ensured in the parent rpf (if there is one)...
|
|
|
|
|
//keep moving further entries until enough space is gained.
|
|
|
|
|
|
|
|
|
|
if (nextentry.FileOffset >= endblock)
|
|
|
|
|
{
|
|
|
|
|
break; //already enough space for this entry, don't go further.
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
uint entryblocks = GetBlockCount(nextentry.GetFileSize());
|
|
|
|
|
uint newblock = FindHole(entryblocks, startblock, endblock);
|
|
|
|
|
if (newblock == 0)
|
|
|
|
|
{
|
|
|
|
|
//no hole was found, move this entry to the end of the file.
|
|
|
|
|
newblock = FindEndBlock();
|
|
|
|
|
GrowArchive(bw, newblock + entryblocks);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//now move the file contents and update the entry's position.
|
|
|
|
|
RelocateFile(bw, nextentry, newblock);
|
|
|
|
|
|
|
|
|
|
//move on to the next file...
|
|
|
|
|
nextentry = FindFirstFileAfter(startblock);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (nextentry == null)
|
|
|
|
|
{
|
|
|
|
|
//last entry in the RPF, so just need to grow the RPF enough to fit.
|
|
|
|
|
//this could be the header (for an empty RPF)...
|
|
|
|
|
uint newblock = FindEndBlock();
|
|
|
|
|
GrowArchive(bw, newblock + ((e != null) ? blockcount : 0));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//changing a file's size (not the header size!) - need to update the header..!
|
|
|
|
|
//also, files could have been moved. so always update the header if we aren't already
|
|
|
|
|
if (e != null)
|
|
|
|
|
{
|
|
|
|
|
WriteHeader(bw);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
private void InsertFileSpace(BinaryWriter bw, RpfFileEntry entry)
|
|
|
|
|
{
|
|
|
|
|
//to insert a new entry. find space in the archive for it and assign the FileOffset.
|
|
|
|
|
|
|
|
|
|
uint blockcount = GetBlockCount(entry.GetFileSize());
|
|
|
|
|
entry.FileOffset = FindHole(blockcount, 0, 0);
|
|
|
|
|
if (entry.FileOffset == 0)
|
|
|
|
|
{
|
|
|
|
|
entry.FileOffset = FindEndBlock();
|
|
|
|
|
GrowArchive(bw, entry.FileOffset + blockcount);
|
|
|
|
|
}
|
|
|
|
|
EnsureAllEntries();
|
|
|
|
|
WriteHeader(bw);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void WriteNewArchive(BinaryWriter bw, RpfEncryption encryption)
|
|
|
|
|
{
|
|
|
|
|
var stream = bw.BaseStream;
|
|
|
|
|
Encryption = encryption;
|
|
|
|
|
Version = 0x52504637; //'RPF7'
|
|
|
|
|
IsAESEncrypted = (encryption == RpfEncryption.AES);
|
|
|
|
|
IsNGEncrypted = (encryption == RpfEncryption.NG);
|
|
|
|
|
StartPos = stream.Position;
|
|
|
|
|
EnsureAllEntries();
|
|
|
|
|
WriteHeader(bw);
|
|
|
|
|
FileSize = stream.Position - StartPos;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void UpdatePaths(RpfDirectoryEntry dir = null)
|
|
|
|
|
{
|
|
|
|
|
//recursively update paths, including in child RPFs.
|
|
|
|
|
if (dir == null)
|
|
|
|
|
{
|
2023-11-12 01:59:17 +08:00
|
|
|
|
Root.Path = Path;
|
2018-01-10 11:17:30 +08:00
|
|
|
|
dir = Root;
|
|
|
|
|
}
|
|
|
|
|
foreach (var file in dir.Files)
|
|
|
|
|
{
|
2023-11-12 01:59:17 +08:00
|
|
|
|
file.Path = dir.Path + "\\" + file.Name;
|
2018-01-10 11:17:30 +08:00
|
|
|
|
|
2024-01-07 02:41:10 +08:00
|
|
|
|
if ((file is RpfBinaryFileEntry binf) && file.IsExtension(".rpf"))
|
2018-01-10 11:17:30 +08:00
|
|
|
|
{
|
|
|
|
|
RpfFile childrpf = FindChildArchive(binf);
|
|
|
|
|
if (childrpf != null)
|
|
|
|
|
{
|
|
|
|
|
childrpf.Path = binf.Path;
|
|
|
|
|
childrpf.FilePath = binf.Path;
|
|
|
|
|
childrpf.UpdatePaths();
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{ }//couldn't find child RPF! problem..!
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
foreach (var subdir in dir.Directories)
|
|
|
|
|
{
|
2023-10-28 03:31:09 +08:00
|
|
|
|
subdir.Path = dir.Path + "\\" + subdir.Name;
|
2018-01-10 11:17:30 +08:00
|
|
|
|
UpdatePaths(subdir);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-01-11 09:10:03 +08:00
|
|
|
|
public RpfFile FindChildArchive(RpfFileEntry f)
|
2018-01-10 11:17:30 +08:00
|
|
|
|
{
|
|
|
|
|
RpfFile c = null;
|
|
|
|
|
if (Children != null)
|
|
|
|
|
{
|
|
|
|
|
foreach (var child in Children)//kinda messy, but no other option really...
|
|
|
|
|
{
|
|
|
|
|
if (child.ParentFileEntry == f)
|
|
|
|
|
{
|
|
|
|
|
c = child;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return c;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2018-01-12 12:19:32 +08:00
|
|
|
|
public long GetDefragmentedFileSize()
|
|
|
|
|
{
|
|
|
|
|
//this represents the size the file would be when fully defragmented.
|
|
|
|
|
uint blockcount = GetHeaderBlockCount();
|
|
|
|
|
|
|
|
|
|
foreach (var entry in AllEntries)
|
|
|
|
|
{
|
2024-01-07 02:41:10 +08:00
|
|
|
|
if (entry is RpfFileEntry fentry)
|
2018-01-12 12:19:32 +08:00
|
|
|
|
{
|
|
|
|
|
blockcount += GetBlockCount(fentry.GetFileSize());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (long)blockcount * 512;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private void UpdateStartPos(long newpos)
|
|
|
|
|
{
|
|
|
|
|
StartPos = newpos;
|
|
|
|
|
|
|
|
|
|
if (Children != null)
|
|
|
|
|
{
|
|
|
|
|
//make sure children also get their StartPos updated!
|
|
|
|
|
foreach (var child in Children)
|
|
|
|
|
{
|
|
|
|
|
if (child.ParentFileEntry == null) continue;//shouldn't really happen...
|
|
|
|
|
var cpos = StartPos + (long)child.ParentFileEntry.FileOffset * 512;
|
2018-02-26 20:49:06 +08:00
|
|
|
|
child.UpdateStartPos(cpos);
|
2018-01-12 12:19:32 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2018-01-10 11:17:30 +08:00
|
|
|
|
|
|
|
|
|
public static RpfFile CreateNew(string gtafolder, string relpath, RpfEncryption encryption = RpfEncryption.OPEN)
|
|
|
|
|
{
|
|
|
|
|
//create a new, empty RPF file in the filesystem
|
|
|
|
|
//this will assume that the folder the file is going into already exists!
|
|
|
|
|
|
|
|
|
|
string fpath = gtafolder;
|
|
|
|
|
fpath = fpath.EndsWith("\\") ? fpath : fpath + "\\";
|
|
|
|
|
fpath = fpath + relpath;
|
|
|
|
|
|
|
|
|
|
if (File.Exists(fpath))
|
|
|
|
|
{
|
|
|
|
|
throw new Exception("File " + fpath + " already exists!");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
File.Create(fpath).Dispose(); //just write a placeholder, will fill it out later
|
|
|
|
|
|
|
|
|
|
RpfFile file = new RpfFile(fpath, relpath);
|
|
|
|
|
|
|
|
|
|
using (var fstream = File.Open(fpath, FileMode.Open, FileAccess.ReadWrite))
|
|
|
|
|
{
|
|
|
|
|
using (var bw = new BinaryWriter(fstream))
|
|
|
|
|
{
|
|
|
|
|
file.WriteNewArchive(bw, encryption);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return file;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static RpfFile CreateNew(RpfDirectoryEntry dir, string name, RpfEncryption encryption = RpfEncryption.OPEN)
|
|
|
|
|
{
|
|
|
|
|
//create a new empty RPF inside the given parent RPF directory.
|
|
|
|
|
|
|
|
|
|
string namel = name.ToLowerInvariant();
|
|
|
|
|
RpfFile parent = dir.File;
|
|
|
|
|
string fpath = parent.GetPhysicalFilePath();
|
|
|
|
|
string rpath = dir.Path + "\\" + namel;
|
|
|
|
|
|
|
|
|
|
if (!File.Exists(fpath))
|
|
|
|
|
{
|
|
|
|
|
throw new Exception("Root RPF file " + fpath + " does not exist!");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
RpfFile file = new RpfFile(name, rpath, 512);//empty RPF is 512 bytes...
|
|
|
|
|
file.Parent = parent;
|
|
|
|
|
file.ParentFileEntry = new RpfBinaryFileEntry();
|
|
|
|
|
|
|
|
|
|
RpfBinaryFileEntry entry = file.ParentFileEntry;
|
|
|
|
|
entry.Parent = dir;
|
|
|
|
|
entry.FileOffset = 0;//InsertFileSpace will update this
|
|
|
|
|
entry.FileSize = 0;
|
|
|
|
|
entry.FileUncompressedSize = (uint)file.FileSize;
|
|
|
|
|
entry.EncryptionType = 0;
|
|
|
|
|
entry.IsEncrypted = false;
|
|
|
|
|
entry.File = parent;
|
|
|
|
|
entry.Path = rpath;
|
|
|
|
|
entry.Name = name;
|
|
|
|
|
|
|
|
|
|
dir.Files.Add(entry);
|
|
|
|
|
|
|
|
|
|
parent.Children.Add(file);
|
|
|
|
|
|
|
|
|
|
using (var fstream = File.Open(fpath, FileMode.Open, FileAccess.ReadWrite))
|
|
|
|
|
{
|
|
|
|
|
using (var bw = new BinaryWriter(fstream))
|
|
|
|
|
{
|
|
|
|
|
parent.InsertFileSpace(bw, entry);
|
|
|
|
|
|
|
|
|
|
fstream.Position = parent.StartPos + entry.FileOffset * 512;
|
|
|
|
|
|
|
|
|
|
file.WriteNewArchive(bw, encryption);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return file;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static RpfDirectoryEntry CreateDirectory(RpfDirectoryEntry dir, string name)
|
|
|
|
|
{
|
|
|
|
|
//create a new directory inside the given parent dir
|
|
|
|
|
|
|
|
|
|
RpfFile parent = dir.File;
|
|
|
|
|
string fpath = parent.GetPhysicalFilePath();
|
2023-11-12 01:59:17 +08:00
|
|
|
|
string rpath = dir.Path + "\\" + name;
|
2018-01-10 11:17:30 +08:00
|
|
|
|
|
|
|
|
|
if (!File.Exists(fpath))
|
|
|
|
|
{
|
|
|
|
|
throw new Exception("Root RPF file " + fpath + " does not exist!");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
RpfDirectoryEntry entry = new RpfDirectoryEntry();
|
|
|
|
|
entry.Parent = dir;
|
|
|
|
|
entry.File = parent;
|
|
|
|
|
entry.Path = rpath;
|
|
|
|
|
entry.Name = name;
|
|
|
|
|
|
|
|
|
|
foreach (var exdir in dir.Directories)
|
|
|
|
|
{
|
2023-10-28 03:31:09 +08:00
|
|
|
|
if (exdir.Name.Equals(entry.Name, StringComparison.OrdinalIgnoreCase))
|
2018-01-10 11:17:30 +08:00
|
|
|
|
{
|
|
|
|
|
throw new Exception("RPF Directory \"" + entry.Name + "\" already exists!");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
dir.Directories.Add(entry);
|
|
|
|
|
|
|
|
|
|
using (var fstream = File.Open(fpath, FileMode.Open, FileAccess.ReadWrite))
|
|
|
|
|
{
|
|
|
|
|
using (var bw = new BinaryWriter(fstream))
|
|
|
|
|
{
|
|
|
|
|
parent.EnsureAllEntries();
|
|
|
|
|
parent.WriteHeader(bw);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return entry;
|
|
|
|
|
}
|
|
|
|
|
|
2023-10-28 03:31:09 +08:00
|
|
|
|
public static RpfFileEntry CreateFileEntry(string name, string path, Stream data)
|
|
|
|
|
{
|
|
|
|
|
//this should only really be used when loading a file from the filesystem.
|
|
|
|
|
RpfFileEntry e;
|
|
|
|
|
using var reader = new BinaryReader(data, Encoding.UTF8, true);
|
|
|
|
|
|
|
|
|
|
uint rsc7 = (reader.BaseStream.Length > 4) ? reader.ReadUInt32() : 0;
|
|
|
|
|
if (rsc7 == 0x37435352) //RSC7 header present! create RpfResourceFileEntry and decompress data...
|
|
|
|
|
{
|
|
|
|
|
e = RpfFile.CreateResourceFileEntry(data, 0);//"version" should be loadable from the header in the data..
|
|
|
|
|
data = ResourceBuilder.Decompress(data);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
var be = new RpfBinaryFileEntry
|
|
|
|
|
{
|
|
|
|
|
FileSize = (uint)(data?.Length ?? 0),
|
|
|
|
|
FileUncompressedSize = (uint)(data?.Length ?? 0),
|
|
|
|
|
};
|
|
|
|
|
e = be;
|
|
|
|
|
}
|
|
|
|
|
e.Name = name;
|
|
|
|
|
e.Path = path;
|
|
|
|
|
return e;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static RpfFileEntry CreateFileEntry(string name, string path, ref byte[] data)
|
|
|
|
|
{
|
|
|
|
|
//this should only really be used when loading a file from the filesystem.
|
|
|
|
|
RpfFileEntry e = null;
|
|
|
|
|
uint rsc7 = (data?.Length > 4) ? BitConverter.ToUInt32(data, 0) : 0;
|
|
|
|
|
if (rsc7 == 0x37435352) //RSC7 header present! create RpfResourceFileEntry and decompress data...
|
|
|
|
|
{
|
|
|
|
|
e = RpfFile.CreateResourceFileEntry(ref data, 0);//"version" should be loadable from the header in the data..
|
|
|
|
|
data = ResourceBuilder.Decompress(data);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
var be = new RpfBinaryFileEntry();
|
|
|
|
|
be.FileSize = (uint)data?.Length;
|
|
|
|
|
be.FileUncompressedSize = be.FileSize;
|
|
|
|
|
e = be;
|
|
|
|
|
}
|
|
|
|
|
e.Name = name;
|
|
|
|
|
e.Path = path;
|
|
|
|
|
return e;
|
|
|
|
|
}
|
|
|
|
|
|
2018-02-26 20:49:06 +08:00
|
|
|
|
public static RpfFileEntry CreateFile(RpfDirectoryEntry dir, string name, byte[] data, bool overwrite = true)
|
2018-01-10 11:17:30 +08:00
|
|
|
|
{
|
2018-02-26 20:49:06 +08:00
|
|
|
|
if (overwrite)
|
|
|
|
|
{
|
|
|
|
|
foreach (var exfile in dir.Files)
|
|
|
|
|
{
|
2023-10-28 03:31:09 +08:00
|
|
|
|
if (exfile.Name.Equals(name, StringComparison.OrdinalIgnoreCase))
|
2018-02-26 20:49:06 +08:00
|
|
|
|
{
|
|
|
|
|
//file already exists. delete the existing one first!
|
|
|
|
|
//this should probably be optimised to just replace the existing one...
|
|
|
|
|
//TODO: investigate along with ExploreForm.ReplaceSelected()
|
|
|
|
|
DeleteEntry(exfile);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
//else fail if already exists..? items with the same name allowed?
|
|
|
|
|
|
|
|
|
|
RpfFile parent = dir.File;
|
2018-01-10 11:17:30 +08:00
|
|
|
|
string fpath = parent.GetPhysicalFilePath();
|
2023-10-28 03:31:09 +08:00
|
|
|
|
string namel = name.ToLowerInvariant();
|
2018-01-11 09:10:03 +08:00
|
|
|
|
string rpath = dir.Path + "\\" + namel;
|
2018-01-10 11:17:30 +08:00
|
|
|
|
if (!File.Exists(fpath))
|
|
|
|
|
{
|
|
|
|
|
throw new Exception("Root RPF file " + fpath + " does not exist!");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
RpfFileEntry entry = null;
|
|
|
|
|
uint len = (uint)data.Length;
|
|
|
|
|
|
2018-01-11 09:10:03 +08:00
|
|
|
|
|
|
|
|
|
bool isrpf = false;
|
2020-02-10 19:24:51 +08:00
|
|
|
|
bool isawc = false;
|
2018-01-11 09:10:03 +08:00
|
|
|
|
uint hdr = 0;
|
|
|
|
|
if (len >= 16)
|
|
|
|
|
{
|
|
|
|
|
hdr = BitConverter.ToUInt32(data, 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (hdr == 0x37435352) //'RSC7'
|
2018-01-10 11:17:30 +08:00
|
|
|
|
{
|
|
|
|
|
//RSC header is present... import as resource
|
|
|
|
|
var rentry = new RpfResourceFileEntry();
|
|
|
|
|
rentry.SystemFlags = BitConverter.ToUInt32(data, 8);
|
|
|
|
|
rentry.GraphicsFlags = BitConverter.ToUInt32(data, 12);
|
|
|
|
|
rentry.FileSize = len;
|
|
|
|
|
if (len >= 0xFFFFFF)
|
|
|
|
|
{
|
|
|
|
|
//just....why
|
|
|
|
|
//FileSize = (buf[7] << 0) | (buf[14] << 8) | (buf[5] << 16) | (buf[2] << 24);
|
|
|
|
|
data[7] = (byte)((len >> 0) & 0xFF);
|
|
|
|
|
data[14] = (byte)((len >> 8) & 0xFF);
|
|
|
|
|
data[5] = (byte)((len >> 16) & 0xFF);
|
|
|
|
|
data[2] = (byte)((len >> 24) & 0xFF);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
entry = rentry;
|
|
|
|
|
}
|
|
|
|
|
|
2023-10-28 03:31:09 +08:00
|
|
|
|
if ((hdr == 0x52504637) && name.EndsWith(".rpf", StringComparison.OrdinalIgnoreCase)) //'RPF7'
|
2018-01-11 09:10:03 +08:00
|
|
|
|
{
|
|
|
|
|
isrpf = true;
|
|
|
|
|
}
|
2023-10-28 03:31:09 +08:00
|
|
|
|
if (name.EndsWith(".awc", StringComparison.OrdinalIgnoreCase))
|
2020-02-10 19:24:51 +08:00
|
|
|
|
{
|
|
|
|
|
isawc = true;
|
|
|
|
|
}
|
2018-01-10 11:17:30 +08:00
|
|
|
|
|
|
|
|
|
if (entry == null)
|
|
|
|
|
{
|
|
|
|
|
//no RSC7 header present, import as a binary file.
|
2020-02-10 19:24:51 +08:00
|
|
|
|
var compressed = (isrpf||isawc) ? data : CompressBytes(data);
|
2018-01-10 11:17:30 +08:00
|
|
|
|
var bentry = new RpfBinaryFileEntry();
|
|
|
|
|
bentry.EncryptionType = 0;//TODO: binary encryption
|
|
|
|
|
bentry.IsEncrypted = false;
|
|
|
|
|
bentry.FileUncompressedSize = (uint)data.Length;
|
2020-02-10 19:24:51 +08:00
|
|
|
|
bentry.FileSize = (isrpf||isawc) ? 0 : (uint)compressed.Length;
|
2018-01-10 11:17:30 +08:00
|
|
|
|
if (bentry.FileSize > 0xFFFFFF)
|
|
|
|
|
{
|
|
|
|
|
bentry.FileSize = 0;
|
|
|
|
|
compressed = data;
|
|
|
|
|
//can't compress?? since apparently FileSize>0 means compressed...
|
|
|
|
|
}
|
|
|
|
|
data = compressed;
|
|
|
|
|
entry = bentry;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
entry.Parent = dir;
|
|
|
|
|
entry.File = parent;
|
|
|
|
|
entry.Path = rpath;
|
|
|
|
|
entry.Name = name;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
foreach (var exfile in dir.Files)
|
|
|
|
|
{
|
2023-10-28 03:31:09 +08:00
|
|
|
|
if (exfile.Name.Equals(entry.Name, StringComparison.OrdinalIgnoreCase))
|
2018-01-10 11:17:30 +08:00
|
|
|
|
{
|
|
|
|
|
throw new Exception("File \"" + entry.Name + "\" already exists!");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
dir.Files.Add(entry);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
using (var fstream = File.Open(fpath, FileMode.Open, FileAccess.ReadWrite))
|
|
|
|
|
{
|
2023-10-28 03:31:09 +08:00
|
|
|
|
using var bw = new BinaryWriter(fstream);
|
|
|
|
|
parent.InsertFileSpace(bw, entry);
|
|
|
|
|
long bbeg = parent.StartPos + (entry.FileOffset * 512);
|
|
|
|
|
long bend = bbeg + (GetBlockCount(entry.GetFileSize()) * 512);
|
|
|
|
|
fstream.Position = bbeg;
|
|
|
|
|
fstream.Write(data, 0, data.Length);
|
|
|
|
|
WritePadding(fstream, bend); //write 0's until the end of the block.
|
2018-01-10 11:17:30 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2018-01-11 09:10:03 +08:00
|
|
|
|
if (isrpf)
|
|
|
|
|
{
|
|
|
|
|
//importing a raw RPF archive. create the new RpfFile object, and read its headers etc.
|
|
|
|
|
RpfFile file = new RpfFile(name, rpath, data.LongLength);
|
|
|
|
|
file.Parent = parent;
|
|
|
|
|
file.ParentFileEntry = entry as RpfBinaryFileEntry;
|
|
|
|
|
file.StartPos = parent.StartPos + (entry.FileOffset * 512);
|
|
|
|
|
parent.Children.Add(file);
|
|
|
|
|
|
2023-10-28 03:31:09 +08:00
|
|
|
|
using var fstream = File.OpenRead(fpath);
|
|
|
|
|
using var br = new BinaryReader(fstream);
|
|
|
|
|
fstream.Position = file.StartPos;
|
2024-01-07 02:41:10 +08:00
|
|
|
|
file.ScanStructure(br, null, null, out _);
|
2018-01-11 09:10:03 +08:00
|
|
|
|
}
|
|
|
|
|
|
2018-01-10 11:17:30 +08:00
|
|
|
|
return entry;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public static void RenameArchive(RpfFile file, string newname)
|
|
|
|
|
{
|
|
|
|
|
//updates all items in the RPF with the new path - no actual file changes made here
|
|
|
|
|
//(since all the paths are generated at runtime and not stored)
|
|
|
|
|
|
|
|
|
|
file.Name = newname;
|
|
|
|
|
file.Path = GetParentPath(file.Path) + newname;
|
|
|
|
|
file.FilePath = GetParentPath(file.FilePath) + newname;
|
|
|
|
|
|
|
|
|
|
file.UpdatePaths();
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static void RenameEntry(RpfEntry entry, string newname)
|
|
|
|
|
{
|
|
|
|
|
//rename the entry in the RPF header...
|
|
|
|
|
//also make sure any relevant child paths are updated...
|
|
|
|
|
|
|
|
|
|
string dirpath = GetParentPath(entry.Path);
|
|
|
|
|
|
|
|
|
|
entry.Name = newname;
|
|
|
|
|
entry.Path = dirpath + newname;
|
|
|
|
|
|
2023-10-28 03:31:09 +08:00
|
|
|
|
JenkIndex.EnsureLower(entry.ShortName);//could be anything... but it needs to be there
|
2018-01-10 11:17:30 +08:00
|
|
|
|
|
|
|
|
|
RpfFile parent = entry.File;
|
|
|
|
|
string fpath = parent.GetPhysicalFilePath();
|
|
|
|
|
|
|
|
|
|
using (var fstream = File.Open(fpath, FileMode.Open, FileAccess.ReadWrite))
|
|
|
|
|
{
|
|
|
|
|
using (var bw = new BinaryWriter(fstream))
|
|
|
|
|
{
|
|
|
|
|
parent.EnsureAllEntries();
|
|
|
|
|
parent.WriteHeader(bw);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (entry is RpfDirectoryEntry)
|
|
|
|
|
{
|
|
|
|
|
//a folder was renamed, make sure all its children's paths get updated
|
|
|
|
|
parent.UpdatePaths(entry as RpfDirectoryEntry);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public static void DeleteEntry(RpfEntry entry)
|
|
|
|
|
{
|
|
|
|
|
//delete this entry from the RPF header.
|
|
|
|
|
//also remove any references to this item in its parent directory...
|
2018-02-28 00:46:06 +08:00
|
|
|
|
//if this is a directory entry, this will delete the contents first
|
2018-01-10 11:17:30 +08:00
|
|
|
|
|
|
|
|
|
RpfFile parent = entry.File;
|
|
|
|
|
string fpath = parent.GetPhysicalFilePath();
|
|
|
|
|
if (!File.Exists(fpath))
|
|
|
|
|
{
|
|
|
|
|
throw new Exception("Root RPF file " + fpath + " does not exist!");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
RpfDirectoryEntry entryasdir = entry as RpfDirectoryEntry;
|
2024-01-07 02:41:10 +08:00
|
|
|
|
//it has to be one or the other...
|
2018-01-10 11:17:30 +08:00
|
|
|
|
|
|
|
|
|
if (entryasdir != null)
|
|
|
|
|
{
|
2018-02-28 00:46:06 +08:00
|
|
|
|
var deldirs = entryasdir.Directories.ToArray();
|
|
|
|
|
var delfiles = entryasdir.Files.ToArray();
|
2024-01-07 02:41:10 +08:00
|
|
|
|
foreach (var deldir in deldirs)
|
2018-01-10 11:17:30 +08:00
|
|
|
|
{
|
2018-02-28 00:46:06 +08:00
|
|
|
|
DeleteEntry(deldir);
|
|
|
|
|
}
|
|
|
|
|
foreach (var delfile in delfiles)
|
|
|
|
|
{
|
|
|
|
|
DeleteEntry(delfile);
|
2018-01-10 11:17:30 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (entry.Parent == null)
|
|
|
|
|
{
|
|
|
|
|
throw new Exception("Parent directory is null! This shouldn't happen - please refresh the folder!");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (entryasdir != null)
|
|
|
|
|
{
|
|
|
|
|
entry.Parent.Directories.Remove(entryasdir);
|
|
|
|
|
}
|
2024-01-07 02:41:10 +08:00
|
|
|
|
if (entry is RpfFileEntry entryasfile)
|
2018-01-10 11:17:30 +08:00
|
|
|
|
{
|
|
|
|
|
entry.Parent.Files.Remove(entryasfile);
|
|
|
|
|
|
|
|
|
|
var child = parent.FindChildArchive(entryasfile);
|
|
|
|
|
if (child != null)
|
|
|
|
|
{
|
|
|
|
|
parent.Children.Remove(child); //RPF file being deleted...
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
using (var fstream = File.Open(fpath, FileMode.Open, FileAccess.ReadWrite))
|
|
|
|
|
{
|
|
|
|
|
using (var bw = new BinaryWriter(fstream))
|
|
|
|
|
{
|
|
|
|
|
parent.EnsureAllEntries();
|
|
|
|
|
parent.WriteHeader(bw);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2018-02-26 20:49:06 +08:00
|
|
|
|
public static bool EnsureValidEncryption(RpfFile file, Func<RpfFile, bool> confirm)
|
|
|
|
|
{
|
|
|
|
|
if (file == null) return false;
|
|
|
|
|
|
|
|
|
|
//currently assumes OPEN is the valid encryption type.
|
|
|
|
|
//TODO: support other encryption types!
|
|
|
|
|
|
|
|
|
|
bool needsupd = false;
|
|
|
|
|
var f = file;
|
|
|
|
|
List<RpfFile> files = new List<RpfFile>();
|
|
|
|
|
while (f != null)
|
|
|
|
|
{
|
|
|
|
|
if (f.Encryption != RpfEncryption.OPEN)
|
|
|
|
|
{
|
|
|
|
|
if (!confirm(f))
|
|
|
|
|
{
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
needsupd = true;
|
|
|
|
|
}
|
|
|
|
|
if (needsupd)
|
|
|
|
|
{
|
|
|
|
|
files.Add(f);
|
|
|
|
|
}
|
|
|
|
|
f = f.Parent;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//change encryption types, starting from the root rpf.
|
|
|
|
|
files.Reverse();
|
|
|
|
|
foreach (var cfile in files)
|
|
|
|
|
{
|
|
|
|
|
SetEncryptionType(cfile, RpfEncryption.OPEN);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2018-01-11 09:10:03 +08:00
|
|
|
|
public static void SetEncryptionType(RpfFile file, RpfEncryption encryption)
|
|
|
|
|
{
|
|
|
|
|
file.Encryption = encryption;
|
|
|
|
|
string fpath = file.GetPhysicalFilePath();
|
|
|
|
|
using (var fstream = File.Open(fpath, FileMode.Open, FileAccess.ReadWrite))
|
|
|
|
|
{
|
|
|
|
|
using (var bw = new BinaryWriter(fstream))
|
|
|
|
|
{
|
|
|
|
|
file.WriteHeader(bw);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-01-10 11:17:30 +08:00
|
|
|
|
|
2020-01-27 22:21:53 +08:00
|
|
|
|
public static void Defragment(RpfFile file, Action<string, float> progress = null)
|
2018-01-12 12:19:32 +08:00
|
|
|
|
{
|
|
|
|
|
if (file?.AllEntries == null) return;
|
|
|
|
|
|
|
|
|
|
string fpath = file.GetPhysicalFilePath();
|
|
|
|
|
using (var fstream = File.Open(fpath, FileMode.Open, FileAccess.ReadWrite))
|
|
|
|
|
{
|
|
|
|
|
using (var bw = new BinaryWriter(fstream))
|
|
|
|
|
{
|
|
|
|
|
uint destblock = file.GetHeaderBlockCount();
|
|
|
|
|
|
|
|
|
|
const int BUFFER_SIZE = 16384;//what buffer size is best for HDD copy?
|
|
|
|
|
var buffer = new byte[BUFFER_SIZE];
|
|
|
|
|
|
|
|
|
|
var allfiles = new List<RpfFileEntry>();
|
|
|
|
|
for (int i = 0; i < file.AllEntries.Count; i++)
|
|
|
|
|
{
|
2024-01-07 02:41:10 +08:00
|
|
|
|
if (file.AllEntries[i] is RpfFileEntry entry) allfiles.Add(entry);
|
2018-01-12 12:19:32 +08:00
|
|
|
|
}
|
|
|
|
|
//make sure we process everything in the current order that they are in the archive
|
|
|
|
|
allfiles.Sort((a, b) => { return a.FileOffset.CompareTo(b.FileOffset); });
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < allfiles.Count; i++)
|
|
|
|
|
{
|
|
|
|
|
var entry = allfiles[i];
|
|
|
|
|
float prog = (float)i / allfiles.Count;
|
|
|
|
|
string txt = "Relocating " + entry.Name + "...";
|
2020-01-27 22:21:53 +08:00
|
|
|
|
progress?.Invoke(txt, prog);
|
2018-01-12 12:19:32 +08:00
|
|
|
|
|
|
|
|
|
var sourceblock = entry.FileOffset;
|
|
|
|
|
var blockcount = GetBlockCount(entry.GetFileSize());
|
|
|
|
|
|
|
|
|
|
if (sourceblock > destblock) //should only be moving things toward the start
|
|
|
|
|
{
|
|
|
|
|
var source = file.StartPos + (long)sourceblock * 512;
|
|
|
|
|
var dest = file.StartPos + (long)destblock * 512;
|
|
|
|
|
var remlength = (long)blockcount * 512;
|
|
|
|
|
while (remlength > 0)
|
|
|
|
|
{
|
|
|
|
|
fstream.Position = source;
|
|
|
|
|
int n = fstream.Read(buffer, 0, (int)Math.Min(remlength, BUFFER_SIZE));
|
|
|
|
|
fstream.Position = dest;
|
|
|
|
|
fstream.Write(buffer, 0, n);
|
|
|
|
|
source += n;
|
|
|
|
|
dest += n;
|
|
|
|
|
remlength -= n;
|
|
|
|
|
}
|
|
|
|
|
entry.FileOffset = destblock;
|
|
|
|
|
|
|
|
|
|
var entryrpf = file.FindChildArchive(entry);
|
|
|
|
|
if (entryrpf != null)
|
|
|
|
|
{
|
|
|
|
|
entryrpf.UpdateStartPos(file.StartPos + (long)entry.FileOffset * 512);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else if (sourceblock != destblock)
|
|
|
|
|
{ }//shouldn't get here...
|
|
|
|
|
|
|
|
|
|
destblock += blockcount;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
file.FileSize = (long)destblock * 512;
|
|
|
|
|
|
|
|
|
|
file.WriteHeader(bw);
|
|
|
|
|
|
|
|
|
|
if (file.ParentFileEntry != null)
|
|
|
|
|
{
|
|
|
|
|
//make sure to also update the parent archive file entry, if there is one
|
|
|
|
|
file.ParentFileEntry.FileUncompressedSize = (uint)file.FileSize;
|
|
|
|
|
file.ParentFileEntry.FileSize = 0;
|
|
|
|
|
if (file.Parent != null)
|
|
|
|
|
{
|
|
|
|
|
file.Parent.WriteHeader(bw);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (file.Parent == null)
|
|
|
|
|
{
|
|
|
|
|
//this is a root archive, so update the file's length to the new size.
|
|
|
|
|
fstream.SetLength(file.FileSize);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2018-01-10 11:17:30 +08:00
|
|
|
|
|
|
|
|
|
private static string GetParentPath(string path)
|
|
|
|
|
{
|
|
|
|
|
string dirpath = path.Replace('/', '\\');//just to make sure..
|
|
|
|
|
int lidx = dirpath.LastIndexOf('\\');
|
|
|
|
|
if (lidx > 0)
|
|
|
|
|
{
|
|
|
|
|
dirpath = dirpath.Substring(0, lidx + 1);
|
|
|
|
|
}
|
|
|
|
|
if (!dirpath.EndsWith("\\"))
|
|
|
|
|
{
|
2023-10-28 03:31:09 +08:00
|
|
|
|
dirpath += "\\";
|
2018-01-10 11:17:30 +08:00
|
|
|
|
}
|
|
|
|
|
return dirpath;
|
|
|
|
|
}
|
2017-09-21 18:33:05 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public override string ToString()
|
|
|
|
|
{
|
|
|
|
|
return Path;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public enum RpfEncryption : uint
|
|
|
|
|
{
|
2018-01-10 11:17:30 +08:00
|
|
|
|
NONE = 0, //some modded RPF's may use this
|
|
|
|
|
OPEN = 0x4E45504F, //1313165391 "OPEN", ie. "no encryption"
|
2017-09-21 18:33:05 +08:00
|
|
|
|
AES = 0x0FFFFFF9, //268435449
|
|
|
|
|
NG = 0x0FEFFFFF, //267386879
|
2024-01-07 02:41:10 +08:00
|
|
|
|
CFXP = 0x50584643,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public enum FileHeader : uint
|
|
|
|
|
{
|
|
|
|
|
RSC7 = 0x37435352,
|
|
|
|
|
FXAP = 0x50415846,
|
2017-09-21 18:33:05 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
[TypeConverter(typeof(ExpandableObjectConverter))] public abstract class RpfEntry
|
|
|
|
|
{
|
|
|
|
|
public RpfFile File { get; set; }
|
2018-01-10 11:17:30 +08:00
|
|
|
|
public RpfDirectoryEntry Parent { get; set; }
|
2017-09-21 18:33:05 +08:00
|
|
|
|
|
2024-01-07 02:41:10 +08:00
|
|
|
|
public uint NameHash {
|
|
|
|
|
get
|
2023-10-28 03:31:09 +08:00
|
|
|
|
{
|
|
|
|
|
if (nameHash == 0 && !string.IsNullOrEmpty(Name))
|
|
|
|
|
{
|
|
|
|
|
nameHash = JenkHash.GenHashLower(Name);
|
|
|
|
|
}
|
|
|
|
|
return nameHash;
|
|
|
|
|
}
|
|
|
|
|
set
|
|
|
|
|
{
|
|
|
|
|
nameHash = value;
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-01-07 02:41:10 +08:00
|
|
|
|
public uint ShortNameHash {
|
|
|
|
|
get
|
2023-10-28 03:31:09 +08:00
|
|
|
|
{
|
2024-01-07 02:41:10 +08:00
|
|
|
|
if (shortNameHash == 0 && !ShortName.IsEmpty)
|
2023-10-28 03:31:09 +08:00
|
|
|
|
{
|
|
|
|
|
shortNameHash = JenkHash.GenHashLower(ShortName);
|
|
|
|
|
}
|
|
|
|
|
return shortNameHash;
|
|
|
|
|
}
|
|
|
|
|
set
|
|
|
|
|
{
|
|
|
|
|
shortNameHash = value;
|
|
|
|
|
}
|
|
|
|
|
}
|
2017-09-21 18:33:05 +08:00
|
|
|
|
|
2024-01-07 02:41:10 +08:00
|
|
|
|
public virtual uint NameOffset
|
|
|
|
|
{
|
|
|
|
|
get => (uint)(Header & 0xFFFF);
|
|
|
|
|
set => Header = (Header & ~0xFFFFUL) | (value & 0xFFFF);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public string Name {
|
|
|
|
|
get => name;
|
|
|
|
|
set
|
2023-10-28 03:31:09 +08:00
|
|
|
|
{
|
|
|
|
|
if (name == value)
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
name = value;
|
|
|
|
|
nameHash = 0;
|
|
|
|
|
shortNameHash = 0;
|
2024-01-07 02:41:10 +08:00
|
|
|
|
shortNameIndex = -1;
|
2023-10-28 03:31:09 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
2023-11-12 01:59:17 +08:00
|
|
|
|
|
2024-01-07 02:41:10 +08:00
|
|
|
|
public ReadOnlySpan<char> ShortName {
|
2023-10-28 03:31:09 +08:00
|
|
|
|
get
|
|
|
|
|
{
|
2024-01-07 02:41:10 +08:00
|
|
|
|
if (shortNameIndex == -1 && !string.IsNullOrEmpty(Name))
|
2023-11-12 01:59:17 +08:00
|
|
|
|
{
|
|
|
|
|
int length = Name.Length;
|
|
|
|
|
for (int i = length; --i >= 0;)
|
|
|
|
|
{
|
|
|
|
|
char ch = Name[i];
|
|
|
|
|
if (ch == '.')
|
|
|
|
|
{
|
2024-01-07 02:41:10 +08:00
|
|
|
|
shortNameIndex = i;
|
|
|
|
|
//shortName = Name.Substring(0, i);
|
2023-11-12 01:59:17 +08:00
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
if (ch == System.IO.Path.DirectorySeparatorChar || ch == System.IO.Path.AltDirectorySeparatorChar)
|
|
|
|
|
{
|
2024-01-07 02:41:10 +08:00
|
|
|
|
shortNameIndex = Name.Length;
|
2023-11-12 01:59:17 +08:00
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-07 02:41:10 +08:00
|
|
|
|
if (shortNameIndex == -1 || shortNameIndex == Name.Length)
|
2023-10-28 03:31:09 +08:00
|
|
|
|
{
|
2024-01-07 02:41:10 +08:00
|
|
|
|
return Name;
|
2023-10-28 03:31:09 +08:00
|
|
|
|
}
|
|
|
|
|
|
2024-01-07 02:41:10 +08:00
|
|
|
|
return Name.AsSpan(0, shortNameIndex);
|
2023-10-28 03:31:09 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-09-21 18:33:05 +08:00
|
|
|
|
public string Path { get; set; }
|
|
|
|
|
|
2024-01-07 02:41:10 +08:00
|
|
|
|
//public uint H1; //first 2 header values from RPF table...
|
|
|
|
|
//public uint H2;
|
|
|
|
|
public ulong Header;
|
2023-10-28 03:31:09 +08:00
|
|
|
|
private string name;
|
|
|
|
|
private uint shortNameHash;
|
|
|
|
|
private uint nameHash;
|
2017-09-21 18:33:05 +08:00
|
|
|
|
|
2024-01-07 02:41:10 +08:00
|
|
|
|
private int shortNameIndex = -1;
|
|
|
|
|
|
|
|
|
|
public abstract void Read(DataReader reader, BinaryReader br);
|
|
|
|
|
|
|
|
|
|
public abstract void Read(ref SequenceReader<byte> reader, BinaryReader br);
|
|
|
|
|
|
2017-09-21 18:33:05 +08:00
|
|
|
|
public abstract void Write(DataWriter writer);
|
|
|
|
|
|
|
|
|
|
public override string ToString()
|
|
|
|
|
{
|
|
|
|
|
return Path;
|
|
|
|
|
}
|
2018-01-10 11:17:30 +08:00
|
|
|
|
|
2023-11-12 01:59:17 +08:00
|
|
|
|
public bool IsExtension(string ext)
|
2018-01-10 11:17:30 +08:00
|
|
|
|
{
|
2024-01-07 02:41:10 +08:00
|
|
|
|
return Name.EndsWith(ext, StringComparison.OrdinalIgnoreCase);
|
2018-01-10 11:17:30 +08:00
|
|
|
|
}
|
2017-09-21 18:33:05 +08:00
|
|
|
|
}
|
|
|
|
|
|
2023-10-28 03:31:09 +08:00
|
|
|
|
[TypeConverter(typeof(ExpandableObjectConverter))]
|
|
|
|
|
public class RpfDirectoryEntry : RpfEntry
|
2017-09-21 18:33:05 +08:00
|
|
|
|
{
|
|
|
|
|
public uint EntriesIndex { get; set; }
|
|
|
|
|
public uint EntriesCount { get; set; }
|
|
|
|
|
|
2024-01-07 02:41:10 +08:00
|
|
|
|
public override uint NameOffset => (uint)(Header & 0xFFFFFFFF);
|
|
|
|
|
|
|
|
|
|
public uint Ident
|
|
|
|
|
{
|
|
|
|
|
get => (uint)((Header >> 32) & 0xFFFFFFFF);
|
|
|
|
|
set => Header = (Header & 0xFFFFFFFF) | (value << 32);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public PooledList<RpfDirectoryEntry> Directories = new PooledList<RpfDirectoryEntry>();
|
|
|
|
|
public PooledList<RpfFileEntry> Files = new PooledList<RpfFileEntry>();
|
|
|
|
|
|
2017-09-21 18:33:05 +08:00
|
|
|
|
|
2024-01-07 02:41:10 +08:00
|
|
|
|
public override void Read(DataReader reader, BinaryReader _)
|
2017-09-21 18:33:05 +08:00
|
|
|
|
{
|
2024-01-07 02:41:10 +08:00
|
|
|
|
if (Ident != 0x7FFFFF00u)
|
2017-09-21 18:33:05 +08:00
|
|
|
|
{
|
|
|
|
|
throw new Exception("Error in RPF7 directory entry.");
|
|
|
|
|
}
|
|
|
|
|
EntriesIndex = reader.ReadUInt32();
|
|
|
|
|
EntriesCount = reader.ReadUInt32();
|
|
|
|
|
}
|
2024-01-07 02:41:10 +08:00
|
|
|
|
|
|
|
|
|
public override void Read(ref SequenceReader<byte> reader, BinaryReader br)
|
|
|
|
|
{
|
|
|
|
|
if (Ident != 0x7FFFFF00u)
|
|
|
|
|
{
|
|
|
|
|
throw new Exception("Error in RPF7 directory entry.");
|
|
|
|
|
}
|
|
|
|
|
EntriesIndex = reader.ReadUInt32();
|
|
|
|
|
EntriesCount = reader.ReadUInt32();
|
|
|
|
|
}
|
|
|
|
|
|
2017-09-21 18:33:05 +08:00
|
|
|
|
public override void Write(DataWriter writer)
|
|
|
|
|
{
|
|
|
|
|
writer.Write(NameOffset);
|
|
|
|
|
writer.Write(0x7FFFFF00u);
|
|
|
|
|
writer.Write(EntriesIndex);
|
|
|
|
|
writer.Write(EntriesCount);
|
|
|
|
|
}
|
2024-01-07 02:41:10 +08:00
|
|
|
|
|
|
|
|
|
public void Dispose()
|
|
|
|
|
{
|
|
|
|
|
Directories.Dispose();
|
|
|
|
|
Files.Dispose();
|
|
|
|
|
GC.SuppressFinalize(this);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//[MemberNotNull(nameof(files), nameof(directories))]
|
|
|
|
|
//public void InitFilesAndDirectories()
|
|
|
|
|
//{
|
|
|
|
|
// int starti = (int)EntriesIndex;
|
|
|
|
|
// int endi = (int)(EntriesIndex + EntriesCount);
|
|
|
|
|
|
|
|
|
|
// var AllEntries = File.AllEntries;
|
|
|
|
|
|
|
|
|
|
// files = new List<RpfFileEntry>();
|
|
|
|
|
// directories = new List<RpfDirectoryEntry>();
|
|
|
|
|
|
|
|
|
|
// for (int i = starti; i < endi; i++)
|
|
|
|
|
// {
|
|
|
|
|
// RpfEntry e = AllEntries[i];
|
|
|
|
|
// e.Parent = this;
|
|
|
|
|
// if (e is RpfDirectoryEntry rde)
|
|
|
|
|
// {
|
|
|
|
|
// directories.Add(rde);
|
|
|
|
|
// }
|
|
|
|
|
// else if (e is RpfFileEntry rfe)
|
|
|
|
|
// {
|
|
|
|
|
// files.Add(rfe);
|
|
|
|
|
// }
|
|
|
|
|
// }
|
|
|
|
|
//}
|
|
|
|
|
|
2017-09-21 18:33:05 +08:00
|
|
|
|
public override string ToString()
|
|
|
|
|
{
|
2024-01-07 02:41:10 +08:00
|
|
|
|
return $"Directory: {Path}";
|
2017-09-21 18:33:05 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-10-28 03:31:09 +08:00
|
|
|
|
[TypeConverter(typeof(ExpandableObjectConverter))]
|
|
|
|
|
public abstract class RpfFileEntry : RpfEntry
|
2017-09-21 18:33:05 +08:00
|
|
|
|
{
|
2024-01-07 02:41:10 +08:00
|
|
|
|
public uint FileOffset {
|
|
|
|
|
get => (uint) ((Header >> 40) & 0x7FFFFFU);
|
|
|
|
|
set => Header = (Header & ~(0x7FFFFFUL << 40)) | (((value & 0x7FFFFFUL) | 0x800000UL) << 40);
|
|
|
|
|
}
|
|
|
|
|
public abstract uint FileSize { get; set; }
|
2017-09-21 18:33:05 +08:00
|
|
|
|
public bool IsEncrypted { get; set; }
|
|
|
|
|
|
2024-01-07 02:41:10 +08:00
|
|
|
|
public abstract long GetUncompressedFileSize();
|
2018-01-10 11:17:30 +08:00
|
|
|
|
public abstract long GetFileSize();
|
|
|
|
|
public abstract void SetFileSize(uint s);
|
2017-09-21 18:33:05 +08:00
|
|
|
|
}
|
|
|
|
|
|
2024-01-07 02:41:10 +08:00
|
|
|
|
[TypeConverter(typeof(ExpandableObjectConverter))]
|
|
|
|
|
public class RpfBinaryFileEntry : RpfFileEntry
|
2017-09-21 18:33:05 +08:00
|
|
|
|
{
|
|
|
|
|
public uint FileUncompressedSize { get; set; }
|
2017-12-20 07:52:50 +08:00
|
|
|
|
public uint EncryptionType { get; set; }
|
2017-09-21 18:33:05 +08:00
|
|
|
|
|
2024-01-07 02:41:10 +08:00
|
|
|
|
public override uint FileSize
|
2017-09-21 18:33:05 +08:00
|
|
|
|
{
|
2024-01-07 02:41:10 +08:00
|
|
|
|
get => (uint)((Header >> 16) & 0xFFFFFF);
|
|
|
|
|
set => Header = (Header & ~0xFFFFFF0000UL) | ((value & 0xFFFFFFUL) << 16);
|
|
|
|
|
}
|
2017-09-21 18:33:05 +08:00
|
|
|
|
|
2024-01-07 02:41:10 +08:00
|
|
|
|
public override void Read(DataReader reader, BinaryReader _)
|
|
|
|
|
{
|
2017-09-21 18:33:05 +08:00
|
|
|
|
FileUncompressedSize = reader.ReadUInt32();
|
|
|
|
|
|
2017-12-20 07:52:50 +08:00
|
|
|
|
EncryptionType = reader.ReadUInt32();
|
|
|
|
|
|
|
|
|
|
switch (EncryptionType)
|
2017-09-21 18:33:05 +08:00
|
|
|
|
{
|
|
|
|
|
case 0: IsEncrypted = false; break;
|
|
|
|
|
case 1: IsEncrypted = true; break;
|
|
|
|
|
default:
|
2023-10-28 03:31:09 +08:00
|
|
|
|
throw new Exception($"Error in RPF7 file entry. {EncryptionType}");
|
2017-09-21 18:33:05 +08:00
|
|
|
|
}
|
2018-01-10 11:17:30 +08:00
|
|
|
|
|
2017-09-21 18:33:05 +08:00
|
|
|
|
}
|
2024-01-07 02:41:10 +08:00
|
|
|
|
|
|
|
|
|
public override void Read(ref SequenceReader<byte> reader, BinaryReader _)
|
|
|
|
|
{
|
|
|
|
|
FileUncompressedSize = reader.ReadUInt32();
|
|
|
|
|
|
|
|
|
|
EncryptionType = reader.ReadUInt32();
|
|
|
|
|
|
|
|
|
|
switch (EncryptionType)
|
|
|
|
|
{
|
|
|
|
|
case 0: IsEncrypted = false; break;
|
|
|
|
|
case 1: IsEncrypted = true; break;
|
|
|
|
|
default:
|
|
|
|
|
throw new Exception($"Error in RPF7 file entry. {EncryptionType}");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
2017-09-21 18:33:05 +08:00
|
|
|
|
public override void Write(DataWriter writer)
|
|
|
|
|
{
|
|
|
|
|
writer.Write((ushort)NameOffset);
|
|
|
|
|
|
|
|
|
|
var buf1 = new byte[] {
|
|
|
|
|
(byte)((FileSize >> 0) & 0xFF),
|
|
|
|
|
(byte)((FileSize >> 8) & 0xFF),
|
|
|
|
|
(byte)((FileSize >> 16) & 0xFF)
|
|
|
|
|
};
|
|
|
|
|
writer.Write(buf1);
|
|
|
|
|
|
|
|
|
|
var buf2 = new byte[] {
|
|
|
|
|
(byte)((FileOffset >> 0) & 0xFF),
|
|
|
|
|
(byte)((FileOffset >> 8) & 0xFF),
|
|
|
|
|
(byte)((FileOffset >> 16) & 0xFF)
|
|
|
|
|
};
|
|
|
|
|
writer.Write(buf2);
|
|
|
|
|
|
|
|
|
|
writer.Write(FileUncompressedSize);
|
|
|
|
|
|
|
|
|
|
if (IsEncrypted)
|
|
|
|
|
writer.Write((uint)1);
|
|
|
|
|
else
|
|
|
|
|
writer.Write((uint)0);
|
|
|
|
|
}
|
|
|
|
|
public override string ToString()
|
|
|
|
|
{
|
2024-01-07 02:41:10 +08:00
|
|
|
|
return $"Binary file: {Path}";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public override long GetUncompressedFileSize()
|
|
|
|
|
{
|
|
|
|
|
return FileUncompressedSize;
|
2017-09-21 18:33:05 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public override long GetFileSize()
|
|
|
|
|
{
|
2018-01-10 11:17:30 +08:00
|
|
|
|
return (FileSize == 0) ? FileUncompressedSize : FileSize;
|
|
|
|
|
}
|
|
|
|
|
public override void SetFileSize(uint s)
|
|
|
|
|
{
|
|
|
|
|
//FileUncompressedSize = s;
|
|
|
|
|
FileSize = s;
|
2017-09-21 18:33:05 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[TypeConverter(typeof(ExpandableObjectConverter))] public class RpfResourceFileEntry : RpfFileEntry
|
|
|
|
|
{
|
2020-03-18 02:31:11 +08:00
|
|
|
|
public RpfResourcePageFlags SystemFlags { get; set; }
|
|
|
|
|
public RpfResourcePageFlags GraphicsFlags { get; set; }
|
2017-09-21 18:33:05 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public static int GetSizeFromFlags(uint flags)
|
|
|
|
|
{
|
|
|
|
|
//dexfx simplified version
|
2024-01-07 02:41:10 +08:00
|
|
|
|
var s0 = ((flags >> 27) & 0x1) << 0; // 1 bit - 27 (*1)
|
|
|
|
|
var s1 = ((flags >> 26) & 0x1) << 1; // 1 bit - 26 (*2)
|
|
|
|
|
var s2 = ((flags >> 25) & 0x1) << 2; // 1 bit - 25 (*4)
|
|
|
|
|
var s3 = ((flags >> 24) & 0x1) << 3; // 1 bit - 24 (*8)
|
2017-09-21 18:33:05 +08:00
|
|
|
|
var s4 = ((flags >> 17) & 0x7F) << 4; // 7 bits - 17 - 23 (*16) (max 127 * 16)
|
|
|
|
|
var s5 = ((flags >> 11) & 0x3F) << 5; // 6 bits - 11 - 16 (*32) (max 63 * 32)
|
2024-01-07 02:41:10 +08:00
|
|
|
|
var s6 = ((flags >> 7) & 0xF) << 6; // 4 bits - 7 - 10 (*64) (max 15 * 64)
|
|
|
|
|
var s7 = ((flags >> 5) & 0x3) << 7; // 2 bits - 5 - 6 (*128) (max 3 * 128)
|
|
|
|
|
var s8 = ((flags >> 4) & 0x1) << 8; // 1 bit - 4 (*256)
|
|
|
|
|
var ss = ((flags >> 0) & 0xF); // 4 bits - 0 - 3
|
2017-09-21 18:33:05 +08:00
|
|
|
|
var baseSize = 0x200 << (int)ss;
|
|
|
|
|
var size = baseSize * (s0 + s1 + s2 + s3 + s4 + s5 + s6 + s7 + s8);
|
|
|
|
|
return (int)size;
|
|
|
|
|
|
|
|
|
|
|
2020-03-18 02:31:11 +08:00
|
|
|
|
#region dexyfex testing
|
2017-09-21 18:33:05 +08:00
|
|
|
|
//var type = flags >> 28;
|
|
|
|
|
//var test = GetFlagsFromSize((int)size, type);
|
|
|
|
|
//s0 = ((test >> 27) & 0x1) << 0; // 1 bit - 27 (*1)
|
|
|
|
|
//s1 = ((test >> 26) & 0x1) << 1; // 1 bit - 26 (*2)
|
|
|
|
|
//s2 = ((test >> 25) & 0x1) << 2; // 1 bit - 25 (*4)
|
|
|
|
|
//s3 = ((test >> 24) & 0x1) << 3; // 1 bit - 24 (*8)
|
|
|
|
|
//s4 = ((test >> 17) & 0x7F) << 4; // 7 bits - 17 - 23 (*16) (max 127 * 16)
|
|
|
|
|
//s5 = ((test >> 11) & 0x3F) << 5; // 6 bits - 11 - 16 (*32) (max 63 * 32)
|
|
|
|
|
//s6 = ((test >> 7) & 0xF) << 6; // 4 bits - 7 - 10 (*64) (max 15 * 64)
|
|
|
|
|
//s7 = ((test >> 5) & 0x3) << 7; // 2 bits - 5 - 6 (*128) (max 3 * 128)
|
|
|
|
|
//s8 = ((test >> 4) & 0x1) << 8; // 1 bit - 4 (*256)
|
|
|
|
|
//ss = ((test >> 0) & 0xF); // 4 bits - 0 - 3
|
|
|
|
|
//baseSize = 0x200 << (int)ss;
|
|
|
|
|
//var tsize = baseSize * (s0 + s1 + s2 + s3 + s4 + s5 + s6 + s7 + s8);
|
|
|
|
|
//if (tsize != size)
|
|
|
|
|
//{ }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//if (s8 == 256)
|
|
|
|
|
//{ }
|
|
|
|
|
//if ((s0 != 0) || (s1 != 0) || (s2 != 0) || (s3 != 0))
|
|
|
|
|
//{ }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//return (int)size;
|
|
|
|
|
|
|
|
|
|
//examples:
|
|
|
|
|
//size:8192, ss:0, s4:1 (ytd)
|
|
|
|
|
//size:16384, ss:0, s5:1 (ytyp)
|
|
|
|
|
//size:24576, ss:0, s4:1, s5:1 (ytyp)
|
|
|
|
|
//size:40960, ss:0, s4:1, s5:2 (ytyp)
|
|
|
|
|
//size:49152, ss:0, s4:2, s5:2 (ytyp)
|
|
|
|
|
//size:237568, ss:0, s4:5, s7:1, s8:1 (yft)
|
|
|
|
|
//size:262144, ss:1, s8:1 (yft)
|
|
|
|
|
//size:589824, ss:1, s6:9 (ytd)
|
|
|
|
|
//size:663552, ss:1, s3:1, s4:12, s6:1, s7:3 (ydd)
|
|
|
|
|
//size:606208, ss:2, s3:1, s4:2, s8:1 (ydr)
|
|
|
|
|
//size:958464, ss:2, s2:1, s4:1, s6:3, s8:1 (ydr)
|
|
|
|
|
//size:966656, ss:2, s3:1, s4:1, s6:3, s8:1 (ydr)
|
|
|
|
|
//size:1695744, ss:2, s2:1, s3:1, s4:5, s5:3, s7:3, s8:1 (ydr)
|
|
|
|
|
//size:2768896, ss:3, s2:1, s4:24, s5:1, s6:4 (ydd)
|
|
|
|
|
//size:4063232, ss:4, s4:15, s7:2 (ytd)
|
|
|
|
|
//size:8650752, ss:5, s4:13, s6:5 (ytd)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2020-03-18 02:31:11 +08:00
|
|
|
|
#endregion
|
2017-09-21 18:33:05 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#region original neo version (system)
|
|
|
|
|
//const int RESOURCE_IDENT = 0x37435352;
|
|
|
|
|
//const int BASE_SIZE = 0x2000;
|
|
|
|
|
//var SystemPagesDiv16 = (int)(SystemFlags >> 27) & 0x1;
|
|
|
|
|
//var SystemPagesDiv8 = (int)(SystemFlags >> 26) & 0x1;
|
|
|
|
|
//var SystemPagesDiv4 = (int)(SystemFlags >> 25) & 0x1;
|
|
|
|
|
//var SystemPagesDiv2 = (int)(SystemFlags >> 24) & 0x1;
|
|
|
|
|
//var SystemPagesMul1 = (int)(SystemFlags >> 17) & 0x7F;
|
|
|
|
|
//var SystemPagesMul2 = (int)(SystemFlags >> 11) & 0x3F;
|
|
|
|
|
//var SystemPagesMul4 = (int)(SystemFlags >> 7) & 0xF;
|
|
|
|
|
//var SystemPagesMul8 = (int)(SystemFlags >> 5) & 0x3;
|
|
|
|
|
//var SystemPagesMul16 = (int)(SystemFlags >> 4) & 0x1;
|
|
|
|
|
//var SystemPagesSizeShift = (int)(SystemFlags >> 0) & 0xF;
|
|
|
|
|
//var systemBaseSize = BASE_SIZE << SystemPagesSizeShift;
|
|
|
|
|
//return
|
|
|
|
|
// (systemBaseSize * SystemPagesDiv16) / 16 +
|
|
|
|
|
// (systemBaseSize * SystemPagesDiv8) / 8 +
|
|
|
|
|
// (systemBaseSize * SystemPagesDiv4) / 4 +
|
|
|
|
|
// (systemBaseSize * SystemPagesDiv2) / 2 +
|
|
|
|
|
// (systemBaseSize * SystemPagesMul1) * 1 +
|
|
|
|
|
// (systemBaseSize * SystemPagesMul2) * 2 +
|
|
|
|
|
// (systemBaseSize * SystemPagesMul4) * 4 +
|
|
|
|
|
// (systemBaseSize * SystemPagesMul8) * 8 +
|
|
|
|
|
// (systemBaseSize * SystemPagesMul16) * 16;
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#region original neo version (graphics)
|
|
|
|
|
//const int RESOURCE_IDENT = 0x37435352;
|
|
|
|
|
//const int BASE_SIZE = 0x2000;
|
|
|
|
|
//var GraphicsPagesDiv16 = (int)(GraphicsFlags >> 27) & 0x1;
|
|
|
|
|
//var GraphicsPagesDiv8 = (int)(GraphicsFlags >> 26) & 0x1;
|
|
|
|
|
//var GraphicsPagesDiv4 = (int)(GraphicsFlags >> 25) & 0x1;
|
|
|
|
|
//var GraphicsPagesDiv2 = (int)(GraphicsFlags >> 24) & 0x1;
|
|
|
|
|
//var GraphicsPagesMul1 = (int)(GraphicsFlags >> 17) & 0x7F;
|
|
|
|
|
//var GraphicsPagesMul2 = (int)(GraphicsFlags >> 11) & 0x3F;
|
|
|
|
|
//var GraphicsPagesMul4 = (int)(GraphicsFlags >> 7) & 0xF;
|
|
|
|
|
//var GraphicsPagesMul8 = (int)(GraphicsFlags >> 5) & 0x3;
|
|
|
|
|
//var GraphicsPagesMul16 = (int)(GraphicsFlags >> 4) & 0x1;
|
|
|
|
|
//var GraphicsPagesSizeShift = (int)(GraphicsFlags >> 0) & 0xF;
|
|
|
|
|
//var graphicsBaseSize = BASE_SIZE << GraphicsPagesSizeShift;
|
|
|
|
|
//return
|
|
|
|
|
// graphicsBaseSize * GraphicsPagesDiv16 / 16 +
|
|
|
|
|
// graphicsBaseSize * GraphicsPagesDiv8 / 8 +
|
|
|
|
|
// graphicsBaseSize * GraphicsPagesDiv4 / 4 +
|
|
|
|
|
// graphicsBaseSize * GraphicsPagesDiv2 / 2 +
|
|
|
|
|
// graphicsBaseSize * GraphicsPagesMul1 * 1 +
|
|
|
|
|
// graphicsBaseSize * GraphicsPagesMul2 * 2 +
|
|
|
|
|
// graphicsBaseSize * GraphicsPagesMul4 * 4 +
|
|
|
|
|
// graphicsBaseSize * GraphicsPagesMul8 * 8 +
|
|
|
|
|
// graphicsBaseSize * GraphicsPagesMul16 * 16;
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
public static uint GetFlagsFromSize(int size, uint version)
|
|
|
|
|
{
|
|
|
|
|
//WIP - may make crashes :(
|
|
|
|
|
//type: see SystemSize and GraphicsSize below
|
|
|
|
|
|
|
|
|
|
//aim for s4: blocksize (0 remainder for 0x2000 block)
|
|
|
|
|
int origsize = size;
|
|
|
|
|
int remainder = size & 0x1FF;
|
|
|
|
|
int blocksize = 0x200;
|
|
|
|
|
if (remainder != 0)
|
|
|
|
|
{
|
|
|
|
|
size = (size - remainder) + blocksize; //round up to the minimum blocksize
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
uint blockcount = (uint)size >> 9; //how many blocks of the minimum size (0x200)
|
|
|
|
|
uint ss = 0;
|
|
|
|
|
while (blockcount > 1024)
|
|
|
|
|
{
|
|
|
|
|
ss++;
|
|
|
|
|
blockcount = blockcount >> 1;
|
|
|
|
|
}
|
|
|
|
|
if (ss > 0)
|
|
|
|
|
{
|
|
|
|
|
size = origsize;
|
|
|
|
|
blocksize = blocksize << (int)ss; //adjust the block size to reduce the block count.
|
|
|
|
|
remainder = size & blocksize;
|
2024-01-07 02:41:10 +08:00
|
|
|
|
if (remainder != 0)
|
2017-09-21 18:33:05 +08:00
|
|
|
|
{
|
|
|
|
|
size = (size - remainder) + blocksize; //readjust size with round-up
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var s0 = (blockcount >> 0) & 0x1; //*1 X
|
|
|
|
|
var s1 = (blockcount >> 1) & 0x1; //*2 X
|
|
|
|
|
var s2 = (blockcount >> 2) & 0x1; //*4 X
|
|
|
|
|
var s3 = (blockcount >> 3) & 0x1; //*8 X
|
|
|
|
|
var s4 = (blockcount >> 4) & 0x7F; //*16 7 bits XXXXXXX
|
|
|
|
|
var s5 = (blockcount >> 5) & 0x3F; //*32 6 bits XXXXXX
|
|
|
|
|
var s6 = (blockcount >> 6) & 0xF; //*64 4 bits XXXX
|
|
|
|
|
var s7 = (blockcount >> 7) & 0x3; //*128 2 bits XX
|
|
|
|
|
var s8 = (blockcount >> 8) & 0x1; //*256 X
|
|
|
|
|
|
|
|
|
|
if (ss > 4)
|
|
|
|
|
{ }
|
|
|
|
|
if (s4 > 0x7F)
|
|
|
|
|
{ } //too big...
|
|
|
|
|
//needs more work to include higher bits..
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
uint f = 0;
|
|
|
|
|
f |= (version & 0xF) << 28;
|
|
|
|
|
f |= (s0 & 0x1) << 27;
|
|
|
|
|
f |= (s1 & 0x1) << 26;
|
|
|
|
|
f |= (s2 & 0x1) << 25;
|
|
|
|
|
f |= (s3 & 0x1) << 24;
|
|
|
|
|
f |= (s4 & 0x7F) << 17;
|
|
|
|
|
f |= (ss & 0xF);
|
2024-01-07 02:41:10 +08:00
|
|
|
|
|
2017-09-21 18:33:05 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return f;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//var s0 = ((flags >> 27) & 0x1) << 0; // 1 bit - 27 (*1)
|
|
|
|
|
//var s1 = ((flags >> 26) & 0x1) << 1; // 1 bit - 26 (*2)
|
|
|
|
|
//var s2 = ((flags >> 25) & 0x1) << 2; // 1 bit - 25 (*4)
|
|
|
|
|
//var s3 = ((flags >> 24) & 0x1) << 3; // 1 bit - 24 (*8)
|
|
|
|
|
//var s4 = ((flags >> 17) & 0x7F) << 4; // 7 bits - 17 - 23 (*16) (max 127 * 16)
|
|
|
|
|
//var s5 = ((flags >> 11) & 0x3F) << 5; // 6 bits - 11 - 16 (*32) (max 63 * 32)
|
|
|
|
|
//var s6 = ((flags >> 7) & 0xF) << 6; // 4 bits - 7 - 10 (*64) (max 15 * 64)
|
|
|
|
|
//var s7 = ((flags >> 5) & 0x3) << 7; // 2 bits - 5 - 6 (*128) (max 3 * 128)
|
|
|
|
|
//var s8 = ((flags >> 4) & 0x1) << 8; // 1 bit - 4 (*256)
|
|
|
|
|
//var ss = ((flags >> 0) & 0xF); // 4 bits - 0 - 3
|
|
|
|
|
//var baseSize = 0x200 << (int)ss;
|
|
|
|
|
//var size = baseSize * (s0 + s1 + s2 + s3 + s4 + s5 + s6 + s7 + s8);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
public static uint GetFlagsFromBlocks(uint blockCount, uint blockSize, uint version)
|
|
|
|
|
{
|
|
|
|
|
|
|
|
|
|
//dexfx test version - seems to work mostly...
|
|
|
|
|
|
|
|
|
|
uint s0 = 0;
|
|
|
|
|
uint s1 = 0;
|
|
|
|
|
uint s2 = 0;
|
|
|
|
|
uint s3 = 0;
|
|
|
|
|
uint s4 = 0;
|
|
|
|
|
uint s5 = 0;
|
|
|
|
|
uint s6 = 0;
|
|
|
|
|
uint s7 = 0;
|
|
|
|
|
uint s8 = 0;
|
|
|
|
|
uint ss = 0;
|
|
|
|
|
|
|
|
|
|
uint bst = blockSize;
|
|
|
|
|
if (blockCount > 0)
|
|
|
|
|
{
|
|
|
|
|
while (bst > 0x200) //ss is number of bits to shift 0x200 to get blocksize...
|
|
|
|
|
{
|
|
|
|
|
ss++;
|
|
|
|
|
bst = bst >> 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
s0 = (blockCount >> 0) & 0x1; //*1 X
|
|
|
|
|
s1 = (blockCount >> 1) & 0x1; //*2 X
|
|
|
|
|
s2 = (blockCount >> 2) & 0x1; //*4 X
|
|
|
|
|
s3 = (blockCount >> 3) & 0x1; //*8 X
|
|
|
|
|
s4 = (blockCount >> 4) & 0x7F; //*16 7 bits XXXXXXX
|
|
|
|
|
//s5 = (blockCount >> 5) & 0x3F; //*32 6 bits XXXXXX
|
|
|
|
|
//s6 = (blockCount >> 6) & 0xF; //*64 4 bits XXXX
|
|
|
|
|
//s7 = (blockCount >> 7) & 0x3; //*128 2 bits XX
|
|
|
|
|
//s8 = (blockCount >> 8) & 0x1; //*256 X
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//if (blockCount > 0)
|
|
|
|
|
//{
|
|
|
|
|
// var curblocksize = 0x2000u;
|
|
|
|
|
// var totsize = blockCount * blockSize;
|
|
|
|
|
// var totcount = totsize / curblocksize;
|
|
|
|
|
// if ((totsize % curblocksize) > 0) totcount++;
|
|
|
|
|
// ss = 4;
|
|
|
|
|
// while (totcount > 0x7f)
|
|
|
|
|
// {
|
|
|
|
|
// ss++;
|
|
|
|
|
// curblocksize = curblocksize << 1;
|
|
|
|
|
// totcount = totsize / curblocksize;
|
|
|
|
|
// if ((totsize % curblocksize) > 0) totcount++;
|
|
|
|
|
// if (ss >= 16)
|
|
|
|
|
// { break; }
|
|
|
|
|
// }
|
|
|
|
|
// s4 = totcount >> 4;
|
|
|
|
|
// s3 = (totcount >> 3) & 1;
|
|
|
|
|
// s2 = (totcount >> 2) & 1;
|
|
|
|
|
// s1 = (totcount >> 1) & 1;
|
|
|
|
|
// s0 = (totcount >> 0) & 1;
|
|
|
|
|
//}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (ss > 0xF)
|
|
|
|
|
{ } //too big...
|
|
|
|
|
if (s4 > 0x7F)
|
|
|
|
|
{ } //too big...
|
|
|
|
|
//needs more work to include higher bits..
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
uint f = 0;
|
|
|
|
|
f |= (version & 0xF) << 28;
|
|
|
|
|
f |= (s0 & 0x1) << 27;
|
|
|
|
|
f |= (s1 & 0x1) << 26;
|
|
|
|
|
f |= (s2 & 0x1) << 25;
|
|
|
|
|
f |= (s3 & 0x1) << 24;
|
|
|
|
|
f |= (s4 & 0x7F) << 17;
|
|
|
|
|
f |= (s5 & 0x3F) << 11;
|
|
|
|
|
f |= (s6 & 0xF) << 7;
|
|
|
|
|
f |= (s7 & 0x3) << 5;
|
|
|
|
|
f |= (s8 & 0x1) << 4;
|
|
|
|
|
f |= (ss & 0xF);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return f;
|
|
|
|
|
}
|
|
|
|
|
public static int GetVersionFromFlags(uint sysFlags, uint gfxFlags)
|
|
|
|
|
{
|
|
|
|
|
var sv = (sysFlags >> 28) & 0xF;
|
|
|
|
|
var gv = (gfxFlags >> 28) & 0xF;
|
|
|
|
|
return (int)((sv << 4) + gv);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2024-01-07 02:41:10 +08:00
|
|
|
|
public int Version => GetVersionFromFlags(SystemFlags, GraphicsFlags);
|
2017-09-21 18:33:05 +08:00
|
|
|
|
|
2024-01-07 02:41:10 +08:00
|
|
|
|
public uint FileSizeHeader {
|
|
|
|
|
get => (uint) ((Header >> 16) & 0xFFFFFF);
|
|
|
|
|
set => Header = (Header & ~0xFFFFFF0000UL) | ((value & 0xFFFFFFUL) << 16);
|
|
|
|
|
}
|
2017-09-21 18:33:05 +08:00
|
|
|
|
|
2024-01-07 02:41:10 +08:00
|
|
|
|
public uint fileSize;
|
|
|
|
|
public override uint FileSize
|
2017-09-21 18:33:05 +08:00
|
|
|
|
{
|
2024-01-07 02:41:10 +08:00
|
|
|
|
get => fileSize;
|
|
|
|
|
set {
|
|
|
|
|
|
|
|
|
|
if (value > 0xFFFFFF)
|
|
|
|
|
{
|
|
|
|
|
FileSizeHeader = 0xFFFFFF;
|
|
|
|
|
} else
|
|
|
|
|
{
|
|
|
|
|
FileSizeHeader = value;
|
|
|
|
|
}
|
|
|
|
|
fileSize = value;
|
2017-09-21 18:33:05 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
2024-01-07 02:41:10 +08:00
|
|
|
|
|
|
|
|
|
public int SystemSize => (int)SystemFlags.Size;
|
|
|
|
|
public int GraphicsSize => (int)GraphicsFlags.Size;
|
|
|
|
|
|
|
|
|
|
[SkipLocalsInit]
|
|
|
|
|
public override void Read(DataReader reader, BinaryReader cfr)
|
2017-09-21 18:33:05 +08:00
|
|
|
|
{
|
2024-01-07 02:41:10 +08:00
|
|
|
|
fileSize = FileSizeHeader;
|
|
|
|
|
|
|
|
|
|
SystemFlags = reader.ReadUInt32();
|
|
|
|
|
GraphicsFlags = reader.ReadUInt32();
|
|
|
|
|
|
|
|
|
|
// there are sometimes resources with length=0xffffff which actually
|
|
|
|
|
// means length>=0xffffff
|
|
|
|
|
if (fileSize == 0xFFFFFF)
|
2017-09-21 18:33:05 +08:00
|
|
|
|
{
|
2024-01-07 02:41:10 +08:00
|
|
|
|
long opos = cfr.BaseStream.Position;
|
|
|
|
|
cfr.BaseStream.Position = File.StartPos + ((long)FileOffset * 512); //need to use the base offset!!
|
|
|
|
|
Span<byte> buf = stackalloc byte[16];
|
|
|
|
|
cfr.BaseStream.ReadAtLeast(buf, 16);
|
|
|
|
|
fileSize = ((uint)buf[7] << 0) | ((uint)buf[14] << 8) | ((uint)buf[5] << 16) | ((uint)buf[2] << 24);
|
|
|
|
|
cfr.BaseStream.Position = opos;
|
2017-09-21 18:33:05 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-07 02:41:10 +08:00
|
|
|
|
[SkipLocalsInit]
|
|
|
|
|
public override void Read(ref SequenceReader<byte> reader, BinaryReader cfr)
|
2017-09-21 18:33:05 +08:00
|
|
|
|
{
|
2024-01-07 02:41:10 +08:00
|
|
|
|
fileSize = FileSizeHeader;
|
2017-09-21 18:33:05 +08:00
|
|
|
|
SystemFlags = reader.ReadUInt32();
|
|
|
|
|
GraphicsFlags = reader.ReadUInt32();
|
|
|
|
|
|
|
|
|
|
// there are sometimes resources with length=0xffffff which actually
|
|
|
|
|
// means length>=0xffffff
|
2024-01-07 02:41:10 +08:00
|
|
|
|
if (fileSize == 0xFFFFFF)
|
2017-09-21 18:33:05 +08:00
|
|
|
|
{
|
|
|
|
|
long opos = cfr.BaseStream.Position;
|
|
|
|
|
cfr.BaseStream.Position = File.StartPos + ((long)FileOffset * 512); //need to use the base offset!!
|
2024-01-07 02:41:10 +08:00
|
|
|
|
Span<byte> buf = stackalloc byte[16];
|
|
|
|
|
cfr.BaseStream.ReadAtLeast(buf, 16);
|
|
|
|
|
fileSize = ((uint)buf[7] << 0) | ((uint)buf[14] << 8) | ((uint)buf[5] << 16) | ((uint)buf[2] << 24);
|
2017-09-21 18:33:05 +08:00
|
|
|
|
cfr.BaseStream.Position = opos;
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-01-07 02:41:10 +08:00
|
|
|
|
|
2017-09-21 18:33:05 +08:00
|
|
|
|
public override void Write(DataWriter writer)
|
|
|
|
|
{
|
|
|
|
|
writer.Write((ushort)NameOffset);
|
|
|
|
|
|
2018-01-10 11:17:30 +08:00
|
|
|
|
var fs = FileSize;
|
2024-01-07 02:41:10 +08:00
|
|
|
|
if (fs > 0xFFFFFF)
|
|
|
|
|
fs = 0xFFFFFF;//will also need to make sure the RSC header is updated...
|
2018-01-10 11:17:30 +08:00
|
|
|
|
|
2017-09-21 18:33:05 +08:00
|
|
|
|
var buf1 = new byte[] {
|
2018-01-10 11:17:30 +08:00
|
|
|
|
(byte)((fs >> 0) & 0xFF),
|
|
|
|
|
(byte)((fs >> 8) & 0xFF),
|
|
|
|
|
(byte)((fs >> 16) & 0xFF)
|
2017-09-21 18:33:05 +08:00
|
|
|
|
};
|
|
|
|
|
writer.Write(buf1);
|
|
|
|
|
|
|
|
|
|
var buf2 = new byte[] {
|
|
|
|
|
(byte)((FileOffset >> 0) & 0xFF),
|
|
|
|
|
(byte)((FileOffset >> 8) & 0xFF),
|
|
|
|
|
(byte)(((FileOffset >> 16) & 0xFF) | 0x80)
|
|
|
|
|
};
|
|
|
|
|
writer.Write(buf2);
|
|
|
|
|
|
|
|
|
|
writer.Write(SystemFlags);
|
|
|
|
|
writer.Write(GraphicsFlags);
|
|
|
|
|
}
|
|
|
|
|
public override string ToString()
|
|
|
|
|
{
|
2024-01-07 02:41:10 +08:00
|
|
|
|
return $"Resource file: {Path}";
|
2017-09-21 18:33:05 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public override long GetFileSize()
|
|
|
|
|
{
|
|
|
|
|
return (FileSize == 0) ? (long)(SystemSize + GraphicsSize) : FileSize;
|
|
|
|
|
}
|
2024-01-07 02:41:10 +08:00
|
|
|
|
|
|
|
|
|
public override long GetUncompressedFileSize()
|
|
|
|
|
{
|
|
|
|
|
return (long)(SystemSize + GraphicsSize);
|
|
|
|
|
}
|
|
|
|
|
|
2018-01-10 11:17:30 +08:00
|
|
|
|
public override void SetFileSize(uint s)
|
|
|
|
|
{
|
|
|
|
|
FileSize = s;
|
|
|
|
|
}
|
2017-09-21 18:33:05 +08:00
|
|
|
|
}
|
|
|
|
|
|
2024-01-07 02:41:10 +08:00
|
|
|
|
[TypeConverter(typeof(ExpandableObjectConverter))]
|
|
|
|
|
public struct RpfResourcePageFlags
|
2020-03-18 02:31:11 +08:00
|
|
|
|
{
|
|
|
|
|
public uint Value { get; set; }
|
|
|
|
|
|
2023-11-14 23:16:59 +08:00
|
|
|
|
public readonly RpfResourcePage[] Pages
|
2020-03-18 02:31:11 +08:00
|
|
|
|
{
|
|
|
|
|
get
|
|
|
|
|
{
|
|
|
|
|
var count = Count;
|
2024-01-07 02:41:10 +08:00
|
|
|
|
if (count == 0)
|
|
|
|
|
return null;
|
2020-03-18 02:31:11 +08:00
|
|
|
|
var pages = new RpfResourcePage[count];
|
|
|
|
|
var counts = PageCounts;
|
|
|
|
|
var sizes = BaseSizes;
|
|
|
|
|
int n = 0;
|
|
|
|
|
uint o = 0;
|
|
|
|
|
for (int i = 0; i < counts.Length; i++)
|
|
|
|
|
{
|
|
|
|
|
var c = counts[i];
|
|
|
|
|
var s = sizes[i];
|
|
|
|
|
for (int p = 0; p < c; p++)
|
|
|
|
|
{
|
2024-01-07 02:41:10 +08:00
|
|
|
|
pages[n] = new RpfResourcePage(s, o);
|
2020-03-18 02:31:11 +08:00
|
|
|
|
o += s;
|
|
|
|
|
n++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return pages;
|
|
|
|
|
}
|
|
|
|
|
}
|
2017-09-21 18:33:05 +08:00
|
|
|
|
|
2024-01-07 02:41:10 +08:00
|
|
|
|
public readonly uint TypeVal => (Value >> 28) & 0xF;
|
|
|
|
|
public readonly uint BaseShift => (Value & 0xF);
|
|
|
|
|
public readonly uint BaseSize => (0x200u << (int)BaseShift);
|
2023-11-14 23:16:59 +08:00
|
|
|
|
public readonly uint[] BaseSizes
|
2020-03-18 02:31:11 +08:00
|
|
|
|
{
|
|
|
|
|
get
|
|
|
|
|
{
|
|
|
|
|
var baseSize = BaseSize;
|
|
|
|
|
return new uint[]
|
|
|
|
|
{
|
|
|
|
|
baseSize << 8,
|
|
|
|
|
baseSize << 7,
|
|
|
|
|
baseSize << 6,
|
|
|
|
|
baseSize << 5,
|
|
|
|
|
baseSize << 4,
|
|
|
|
|
baseSize << 3,
|
|
|
|
|
baseSize << 2,
|
|
|
|
|
baseSize << 1,
|
|
|
|
|
baseSize << 0,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-11-14 23:16:59 +08:00
|
|
|
|
public readonly uint[] PageCounts
|
2020-03-18 02:31:11 +08:00
|
|
|
|
{
|
|
|
|
|
get
|
|
|
|
|
{
|
|
|
|
|
return new uint[]
|
|
|
|
|
{
|
|
|
|
|
((Value >> 4) & 0x1),
|
|
|
|
|
((Value >> 5) & 0x3),
|
|
|
|
|
((Value >> 7) & 0xF),
|
|
|
|
|
((Value >> 11) & 0x3F),
|
|
|
|
|
((Value >> 17) & 0x7F),
|
|
|
|
|
((Value >> 24) & 0x1),
|
|
|
|
|
((Value >> 25) & 0x1),
|
|
|
|
|
((Value >> 26) & 0x1),
|
|
|
|
|
((Value >> 27) & 0x1),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-11-14 23:16:59 +08:00
|
|
|
|
public readonly uint[] PageSizes
|
2020-03-18 02:31:11 +08:00
|
|
|
|
{
|
|
|
|
|
get
|
|
|
|
|
{
|
|
|
|
|
var counts = PageCounts;
|
|
|
|
|
var baseSizes = BaseSizes;
|
|
|
|
|
return new uint[]
|
|
|
|
|
{
|
|
|
|
|
baseSizes[0] * counts[0],
|
|
|
|
|
baseSizes[1] * counts[1],
|
|
|
|
|
baseSizes[2] * counts[2],
|
|
|
|
|
baseSizes[3] * counts[3],
|
|
|
|
|
baseSizes[4] * counts[4],
|
|
|
|
|
baseSizes[5] * counts[5],
|
|
|
|
|
baseSizes[6] * counts[6],
|
|
|
|
|
baseSizes[7] * counts[7],
|
|
|
|
|
baseSizes[8] * counts[8],
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-11-14 23:16:59 +08:00
|
|
|
|
public readonly uint Count
|
2020-03-18 02:31:11 +08:00
|
|
|
|
{
|
|
|
|
|
get
|
|
|
|
|
{
|
2024-01-07 02:41:10 +08:00
|
|
|
|
return Vector256.Sum(Vector256.Create<uint>(PageCounts.AsSpan())) + PageCounts[8];
|
|
|
|
|
//var c = PageCounts;
|
|
|
|
|
//return c[0] + c[1] + c[2] + c[3] + c[4] + c[5] + c[6] + c[7] + c[8];
|
2020-03-18 02:31:11 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
2023-11-14 23:16:59 +08:00
|
|
|
|
public readonly uint Size
|
2020-03-18 02:31:11 +08:00
|
|
|
|
{
|
|
|
|
|
get
|
|
|
|
|
{
|
|
|
|
|
var flags = Value;
|
|
|
|
|
var s0 = ((flags >> 27) & 0x1) << 0;
|
|
|
|
|
var s1 = ((flags >> 26) & 0x1) << 1;
|
|
|
|
|
var s2 = ((flags >> 25) & 0x1) << 2;
|
|
|
|
|
var s3 = ((flags >> 24) & 0x1) << 3;
|
|
|
|
|
var s4 = ((flags >> 17) & 0x7F) << 4;
|
|
|
|
|
var s5 = ((flags >> 11) & 0x3F) << 5;
|
|
|
|
|
var s6 = ((flags >> 7) & 0xF) << 6;
|
|
|
|
|
var s7 = ((flags >> 5) & 0x3) << 7;
|
|
|
|
|
var s8 = ((flags >> 4) & 0x1) << 8;
|
|
|
|
|
var ss = ((flags >> 0) & 0xF);
|
|
|
|
|
var baseSize = 0x200u << (int)ss;
|
|
|
|
|
return baseSize * (s0 + s1 + s2 + s3 + s4 + s5 + s6 + s7 + s8);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public RpfResourcePageFlags(uint v)
|
|
|
|
|
{
|
|
|
|
|
Value = v;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public RpfResourcePageFlags(uint[] pageCounts, uint baseShift)
|
|
|
|
|
{
|
|
|
|
|
var v = baseShift & 0xF;
|
|
|
|
|
v += (pageCounts[0] & 0x1) << 4;
|
|
|
|
|
v += (pageCounts[1] & 0x3) << 5;
|
|
|
|
|
v += (pageCounts[2] & 0xF) << 7;
|
|
|
|
|
v += (pageCounts[3] & 0x3F) << 11;
|
|
|
|
|
v += (pageCounts[4] & 0x7F) << 17;
|
|
|
|
|
v += (pageCounts[5] & 0x1) << 24;
|
|
|
|
|
v += (pageCounts[6] & 0x1) << 25;
|
|
|
|
|
v += (pageCounts[7] & 0x1) << 26;
|
|
|
|
|
v += (pageCounts[8] & 0x1) << 27;
|
|
|
|
|
Value = v;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public static implicit operator uint(RpfResourcePageFlags f)
|
|
|
|
|
{
|
|
|
|
|
return f.Value; //implicit conversion
|
|
|
|
|
}
|
|
|
|
|
public static implicit operator RpfResourcePageFlags(uint v)
|
|
|
|
|
{
|
|
|
|
|
return new RpfResourcePageFlags(v);
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-14 23:16:59 +08:00
|
|
|
|
public override readonly string ToString()
|
2020-03-18 02:31:11 +08:00
|
|
|
|
{
|
2024-01-07 02:41:10 +08:00
|
|
|
|
return $"Size: {Size}, Pages: {Count}";
|
2020-03-18 02:31:11 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-07 02:41:10 +08:00
|
|
|
|
[TypeConverter(typeof(ExpandableObjectConverter))]
|
|
|
|
|
public readonly struct RpfResourcePage(uint size, uint offset)
|
2020-03-18 02:31:11 +08:00
|
|
|
|
{
|
2024-01-07 02:41:10 +08:00
|
|
|
|
public uint Size { get; init; } = size;
|
|
|
|
|
public uint Offset { get; init; } = offset;
|
2020-03-18 02:31:11 +08:00
|
|
|
|
|
|
|
|
|
public override string ToString()
|
|
|
|
|
{
|
2024-01-07 02:41:10 +08:00
|
|
|
|
return $"{Size}: {Offset}";
|
2020-03-18 02:31:11 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
2017-09-21 18:33:05 +08:00
|
|
|
|
|
|
|
|
|
public interface PackedFile //interface for the different file types to use
|
|
|
|
|
{
|
|
|
|
|
void Load(byte[] data, RpfFileEntry entry);
|
|
|
|
|
}
|
|
|
|
|
|
2023-10-28 03:31:09 +08:00
|
|
|
|
public interface PackedFileStream : PackedFile
|
|
|
|
|
{
|
|
|
|
|
void Load(Stream stream, RpfFileEntry entry);
|
|
|
|
|
}
|
2017-09-21 18:33:05 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
}
|