Overview
Since more and more maps and mutators are turning up that are only available via the Steam Workop it is becomming more and more frustrating trying to work out what files they require so that the complete set can be successfuly transfered onto a Dedicated Server.This guide is the result of me having to reverse engineer the internal structure of the TempArchiveXX files that show up in root of the KillingFloor directory when you subscribe to a Workshop item, and writting a utility that will extract the individual files from them.To all those authors who who publish a link to an external download of you work for use with a dedicated server – a big thank you!To everyone else – you drove me to this 🙂
Basic structure of the file
A header containing the number of files
# Bytes Type Meaning 4 int32 Number of files in this archive
Then one block per file:
# Bytes Type Meaning (variable) FString Relative path of File (1-5) FCompactIndex Byte length of File (count) byte File contents
An FString is a null terminated ANSI string stored as follows:
# Bytes Type Meaning (1-5) FCompactIndex Byte length of the string (including null terminator byte) (count) byte String characters each byte appears to be an ANSI code point.
An FCompactIndex is an int32 that is stored in a bit stuffed format.
I found a reference of how to decode an FCompactIndex here:
Packages (snipers paradise)[www.snipersparadise.net]
and here:
Package File Format (beyondunreal wiki)[wiki.beyondunreal.com]
Byte 0 1 2 3 4 Range Bit 76543210 76543210 76543210 76543210 76543210 6 bit s0xxxxxx 13 bit s1xxxxxx 0xxxxxxx 20 bit s1xxxxxx 1xxxxxxx 0xxxxxxx 27 bit s1xxxxxx 1xxxxxxx 1xxxxxxx 0xxxxxxx 32 bit s1xxxxxx 1xxxxxxx 1xxxxxxx 1xxxxxxx 000xxxxx bits 5 – 0 12 – 6 19 – 13 26 – 20 31 – 27
Sample C# source code for extraction program
In Visual Studio create a Solution for a Windows Console Application.
Add the following two classes to the Project:
Program.cs
using System; using System.IO; using System.Text; namespace TempArchiveExtractor { class Program { static void Main(String[] args) { if (0 == args.Length || String.IsNullOrWhiteSpace(args[0])) { Console.Out.WriteLine(“Usage: TempArchiveExtractor [TempArchiveFile]”); return; } FileInfo archiveFile = new FileInfo(args[0]); if (!archiveFile.Exists) { Console.Out.WriteLine(“No such file”); return; } FileStream archiveStream = null; try { archiveStream = archiveFile.OpenRead(); TempArchiveReader reader = new TempArchiveReader(archiveStream); Int32 fileCount = reader.ReadInt32(); Console.Out.WriteLine(“Archive contains ” + fileCount + ” files”); for (int i=0; i< fileCount; i++) { String filePath = reader.ReadFString(); Int32 fileLen = reader.ReadFCompactIndex(); FileInfo fi = new FileInfo(filePath); String msg = String.Format(“{0} {1} Size: 0x{2:X} {2:N0} [Y/N] : ” , fi.Exists ? “Overwrite” : “Extract ” , filePath, fileLen); Console.Out.Write(msg); String resp =
Console.In.ReadLine(); if (“Y”.Equals(resp.ToUpper())) { DirectoryInfo di = fi.Directory; if (!di.Exists) di.Create(); using (FileStream fs = fi.OpenWrite()) { reader.ReadIntoStream(fileLen, fs); } } else { reader.Skip(fileLen); } } } catch (Exception ex) { Console.Out.WriteLine(“Error reading archive”); Console.Out.WriteLine(ex); } finally { if (null != archiveStream) archiveStream.Close(); } } } }
TempArchiveReader.cs
using System; using System.IO; using System.Text; namespace TempArchiveExtractor { public class TempArchiveReader { static Encoding ANSIEncoding = Encoding.GetEncoding(1252); public TempArchiveReader(FileStream archiveStream) { _archiveStream = archiveStream; } FileStream _archiveStream; public Int32 ReadInt32() { Int32 result = _archiveStream.ReadByte(); result += _archiveStream.ReadByte() << 8; result += _archiveStream.ReadByte() << 16; result += _archiveStream.ReadByte() << 32; return result; } public Int32 ReadFCompactIndex() { Int32 result = 0; int B0 = _archiveStream.ReadByte(); //String msg = String.Format(“FCompactIndex: 0x{0:X}”, B0); if ((B0 & 0x40) != 0) { int B1 = _archiveStream.ReadByte(); //msg += String.Format(“{0:X}”, B1); if ((B1 & 0x80) != 0) { int B2 = _archiveStream.ReadByte(); //msg += String.Format(“{0:X}”, B2); if ((B2 & 0x80) != 0) { int B3 = _archiveStream.ReadByte(); //msg += String.Format(“{0:X}”, B3); if ((B3 & 0x80) != 0) { int B4 = _archiveStream.ReadByte(); //msg += String.Format(“{0:X}”, B4); result = B4; } result = (result << 7) + (B3 & 0x7F); } result = (result << 7) + (B2 & 0x7F); } result = (result << 7) + (B1 & 0x7F); } result = (result << 6) + (B0 & 0x3F); if ((B0 & 0x80) != 0) result *= -1; //msg += String.Format(” => 0x{0:X} {0}”, result); //Console.Out.WriteLine(msg); return result; } public String ReadFString() { Int32 strLen = ReadFCompactIndex(); Byte[] strBytes = new Byte[strLen]; _archiveStream.Read(strBytes, 0, strLen); //String msg = String.Format(“FString: length 0x{0:X} {0} : {1:X}..{2:X}” // , strLen, strBytes[0], strBytes[strLen-1]); //Console.Out.WriteLine(msg); String str = ANSIEncoding.GetString(strBytes, 0, strLen-1); return str; } public void Skip(Int32 count) { _archiveStream.Seek(count, SeekOrigin.Current); } const int BLOCKSIZE = 0x10000; public void ReadIntoStream(Int32 count, Stream outStream) { Byte[] buffer = new Byte[BLOCKSIZE]; while (count > BLOCKSIZE) { _archiveStream.Read(buffer, 0, BLOCKSIZE); outStream.Write(buffer, 0, BLOCKSIZE); count -= BLOCKSIZE; } _archiveStream.Read(buffer, 0, count); outStream.Write(buffer, 0, count); } } }