mirror of
https://github.com/celisej567/LibBSP.git
synced 2026-09-11 20:09:36 +03:00
424 lines
13 KiB
C#
424 lines
13 KiB
C#
using System;
|
|
using System.IO;
|
|
using System.Collections.Generic;
|
|
|
|
namespace LibBSP {
|
|
/// <summary>
|
|
/// Handles reading of a BSP file on-demand.
|
|
/// </summary>
|
|
public class BSPReader {
|
|
private FileStream stream;
|
|
private BinaryReader binaryReader;
|
|
|
|
private bool _bigEndian = false;
|
|
|
|
// Decryption key for Tactical Intervention; should be 32 bytes
|
|
private byte[] key = new byte[0];
|
|
|
|
public bool bigEndian { get { return _bigEndian; } }
|
|
|
|
/// <summary>
|
|
/// Creates a new instance of a <c>BSPReader</c> class to read the specified file.
|
|
/// </summary>
|
|
/// <param name="file"></param>
|
|
public BSPReader(FileInfo file) {
|
|
if (!File.Exists(file.FullName)) {
|
|
throw new FileNotFoundException("Unable to open BSP file; file " + file.FullName + " not found.");
|
|
} else {
|
|
this.stream = new FileStream(file.FullName, FileMode.Open, FileAccess.Read);
|
|
this.binaryReader = new BinaryReader(this.stream);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reads this lump in the map.
|
|
/// </summary>
|
|
/// <param name="index">The index of the lump to get</param>
|
|
/// <param name="version">The version of BSP this is</param>
|
|
/// <returns>Array of bytes read from the BSP file</returns>
|
|
public byte[] ReadLumpNum(int index, MapType version) {
|
|
switch (version) {
|
|
case MapType.Quake:
|
|
case MapType.Nightfire: {
|
|
return ReadLumpFromOffsetLengthPairAtOffset(4 + (8 * index), version);
|
|
}
|
|
case MapType.Quake2:
|
|
case MapType.Daikatana:
|
|
case MapType.SiN:
|
|
case MapType.SoF:
|
|
case MapType.Quake3:
|
|
case MapType.Raven:
|
|
case MapType.CoD: {
|
|
return ReadLumpFromOffsetLengthPairAtOffset(8 + (8 * index), version);
|
|
}
|
|
case MapType.CoD2: {
|
|
stream.Seek(8 + (8 * index), SeekOrigin.Begin);
|
|
int temp = binaryReader.ReadInt32();
|
|
return ReadLump(binaryReader.ReadInt32(), temp, version);
|
|
}
|
|
case MapType.STEF2:
|
|
case MapType.STEF2Demo:
|
|
case MapType.MOHAA:
|
|
case MapType.FAKK: {
|
|
return ReadLumpFromOffsetLengthPairAtOffset(12 + (8 * index), version);
|
|
}
|
|
case MapType.CoD4: {
|
|
stream.Seek(8, SeekOrigin.Begin);
|
|
int numlumps = binaryReader.ReadInt32();
|
|
int offset = (numlumps * 8) + 12;
|
|
for (int i = 0; i < numlumps; i++) {
|
|
int id = binaryReader.ReadInt32();
|
|
int length = binaryReader.ReadInt32();
|
|
if (id == index) {
|
|
return ReadLump(offset, length, version);
|
|
} else {
|
|
offset += length;
|
|
while (offset % 4 != 0) { offset++; }
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case MapType.Source17:
|
|
case MapType.Source18:
|
|
case MapType.Source19:
|
|
case MapType.Source20:
|
|
case MapType.Source21:
|
|
case MapType.Source22:
|
|
case MapType.Source23:
|
|
case MapType.Source27:
|
|
case MapType.TacticalInterventionEncrypted:
|
|
case MapType.Vindictus:
|
|
case MapType.DMoMaM: {
|
|
return ReadLumpFromOffsetLengthPairAtOffset(8 + (16 * index), version);
|
|
}
|
|
/*
|
|
case MapType.Doom:
|
|
case MapType.Hexen: {
|
|
int[] ol = getLumpInfo(index);
|
|
return readLump(ol[0], ol[1]);
|
|
}*/
|
|
}
|
|
return new byte[0];
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the lump referenced by the offset/length pair at the specified <paramref name="offset"/>,
|
|
/// read as two Int32.
|
|
/// </summary>
|
|
/// <param name="offset">The byte offset for the offset/length pair</param>
|
|
/// <param name="version">The version of BSP this is</param>
|
|
/// <returns>Array of bytes read from the BSP file</returns>
|
|
private byte[] ReadLumpFromOffsetLengthPairAtOffset(int offset, MapType version) {
|
|
stream.Seek(offset, SeekOrigin.Begin);
|
|
byte[] input = binaryReader.ReadBytes(12);
|
|
if (version == MapType.TacticalInterventionEncrypted) {
|
|
input = XorWithKeyStartingAtIndex(input, offset);
|
|
}
|
|
int lumpOffset;
|
|
int lumpLength;
|
|
if (version == MapType.L4D2) {
|
|
lumpOffset = BitConverter.ToInt32(input, 4);
|
|
lumpLength = BitConverter.ToInt32(input, 8);
|
|
} else {
|
|
lumpOffset = BitConverter.ToInt32(input, 0);
|
|
lumpLength = BitConverter.ToInt32(input, 4);
|
|
}
|
|
/*if (bigEndian) {
|
|
byte[] bytes = BitConverter.GetBytes(lumpLength);
|
|
Array.Reverse(bytes);
|
|
lumpLength = BitConverter.ToInt32(bytes, 0);
|
|
bytes = BitConverter.GetBytes(lumpOffset);
|
|
Array.Reverse(bytes);
|
|
lumpOffset = BitConverter.ToInt32(bytes, 0);
|
|
}*/
|
|
return ReadLump(lumpOffset, lumpLength, version);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reads the lump <paramref name="length"/> bytes long at <paramref name="offset"/> in the file
|
|
/// </summary>
|
|
/// <param name="offset">Offset to start reading from</param>
|
|
/// <param name="length">Length of the lump to read</param>
|
|
/// <param name="version">The version of BSP this is</param>
|
|
/// <returns>Array of bytes read from the BSP file</returns>
|
|
public byte[] ReadLump(int offset, int length, MapType version) {
|
|
stream.Seek(offset, SeekOrigin.Begin);
|
|
byte[] input = binaryReader.ReadBytes(length);
|
|
if (version == MapType.TacticalInterventionEncrypted) {
|
|
input = XorWithKeyStartingAtIndex(input, offset);
|
|
}
|
|
return input;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Xors the <paramref name="data"/> <c>byte</c> array with the localls stored key <c>byte</c> array, starting at a certain <paramref name="index"/> in the key.
|
|
/// </summary>
|
|
/// <param name="data">The byte array to Xor</param>
|
|
/// <param name="index">The index in the key byte array to start reading from</param>
|
|
/// <returns>The input <c>byte</c> array Xored with the key <c>byte</c> array</returns>
|
|
/// <exception cref="ArgumentNullException">The passed <paramref name="data"/> parameter was null</exception>
|
|
private byte[] XorWithKeyStartingAtIndex(byte[] data, int index = 0) {
|
|
if (data == null) {
|
|
throw new ArgumentNullException();
|
|
}
|
|
if (key.Length == 0 || data.Length == 0) {
|
|
return data;
|
|
}
|
|
byte[] output = new byte[data.Length];
|
|
for (int i = 0; i < data.Length; i++) {
|
|
output[i] = (byte)(data[i] ^ key[(i + index) % key.Length]);
|
|
}
|
|
return output;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tries to get the <c>MapType</c> member most closely represented by the referenced file. If the file is
|
|
/// found to be big-endian, this will set <c>bigEndian</c> to <c>true</c>.
|
|
/// </summary>
|
|
/// <returns>The <c>MapType</c> of this BSP, <c>MapType.Undefined</c> if it could not be determined</returns>
|
|
public MapType GetVersion() {
|
|
MapType ret = GetVersion(false);
|
|
if (ret == MapType.Undefined) {
|
|
ret = GetVersion(true);
|
|
if (ret != MapType.Undefined) {
|
|
_bigEndian = true;
|
|
}
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tries to get the <c>MapType</c> member most closely represented by the referenced file.
|
|
/// </summary>
|
|
/// <param name="bigEndian">Set to <c>true</c> to attempt reading the data in big-endian byte order</param>
|
|
/// <returns>The <c>MapType</c> of this BSP, <c>MapType.Undefined</c> if it could not be determined</returns>
|
|
private MapType GetVersion(bool bigEndian) {
|
|
MapType current = MapType.Undefined;
|
|
stream.Seek(0, SeekOrigin.Begin);
|
|
int data = binaryReader.ReadInt32();
|
|
if (bigEndian) {
|
|
byte[] bytes = BitConverter.GetBytes(data);
|
|
Array.Reverse(bytes);
|
|
data = BitConverter.ToInt32(bytes, 0);
|
|
}
|
|
if (data == 1347633737) {
|
|
// 1347633737 reads in ASCII as "IBSP"
|
|
// Versions: CoD, CoD2, CoD4, Quake 2, Daikatana, Quake 3 (RtCW), Soldier of Fortune
|
|
data = binaryReader.ReadInt32();
|
|
if (bigEndian) {
|
|
byte[] bytes = BitConverter.GetBytes(data);
|
|
Array.Reverse(bytes);
|
|
data = BitConverter.ToInt32(bytes, 0);
|
|
}
|
|
switch (data) {
|
|
case 4: {
|
|
current = MapType.CoD2;
|
|
break;
|
|
}
|
|
case 22: {
|
|
current = MapType.CoD4;
|
|
break;
|
|
}
|
|
case 38: {
|
|
current = MapType.Quake2;
|
|
break;
|
|
}
|
|
case 41: {
|
|
current = MapType.Daikatana;
|
|
break;
|
|
}
|
|
case 46: {
|
|
current = MapType.Quake3;
|
|
// This version number is both Quake 3 and Soldier of Fortune. Find out the length of the
|
|
// header, based on offsets.
|
|
for (int i = 0; i < 17; i++) {
|
|
stream.Seek((i + 1) * 8, SeekOrigin.Begin);
|
|
int temp = binaryReader.ReadInt32();
|
|
if (bigEndian) {
|
|
byte[] bytes = BitConverter.GetBytes(temp);
|
|
Array.Reverse(bytes);
|
|
temp = BitConverter.ToInt32(bytes, 0);
|
|
}
|
|
if (temp == 184) {
|
|
current = MapType.SoF;
|
|
break;
|
|
} else {
|
|
if (temp == 144) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case 47: {
|
|
current = MapType.Quake3;
|
|
break;
|
|
}
|
|
case 59: {
|
|
current = MapType.CoD;
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
if (data == 892416050) {
|
|
// 892416050 reads in ASCII as "2015," the game studio which developed MoHAA
|
|
current = MapType.MOHAA;
|
|
} else {
|
|
if (data == 1095516485) {
|
|
// 1095516485 reads in ASCII as "EALA," the ones who developed MoHAA Spearhead and Breakthrough
|
|
current = MapType.MOHAA;
|
|
} else {
|
|
if (data == 1347633750) {
|
|
// 1347633750 reads in ASCII as "VBSP." Indicates Source engine.
|
|
// Some source games handle this as 2 shorts.
|
|
// TODO: Big endian?
|
|
// Formats: Source 17-23 and 27, DMoMaM, Vindictus
|
|
data = (int)binaryReader.ReadUInt16();
|
|
switch (data) {
|
|
case 17: {
|
|
current = MapType.Source17;
|
|
break;
|
|
}
|
|
case 18: {
|
|
current = MapType.Source18;
|
|
break;
|
|
}
|
|
case 19: {
|
|
current = MapType.Source19;
|
|
break;
|
|
}
|
|
case 20: {
|
|
int version2 = (int)binaryReader.ReadUInt16();
|
|
if (version2 == 4) {
|
|
// TODO: This doesn't necessarily mean the whole map should be read as DMoMaM.
|
|
current = MapType.DMoMaM;
|
|
} else {
|
|
// TODO: Vindictus? Before I was determining these by looking at the Game Lump data, is there a better way?
|
|
current = MapType.Source20;
|
|
}
|
|
break;
|
|
}
|
|
case 21: {
|
|
current = MapType.Source21;
|
|
// Hack to determine if this is a L4D2 map. Read what would normally be the offset of
|
|
// a lump. If it is less than the header length it's probably not an offset, indicating L4D2.
|
|
stream.Seek(8, SeekOrigin.Begin);
|
|
int test = binaryReader.ReadInt32();
|
|
if (bigEndian) {
|
|
byte[] bytes = BitConverter.GetBytes(test);
|
|
Array.Reverse(bytes);
|
|
test = BitConverter.ToInt32(bytes, 0);
|
|
}
|
|
if (test < 1032) {
|
|
current = MapType.L4D2;
|
|
}
|
|
break;
|
|
}
|
|
case 22: {
|
|
current = MapType.Source22;
|
|
break;
|
|
}
|
|
case 23: {
|
|
current = MapType.Source23;
|
|
break;
|
|
}
|
|
case 27: {
|
|
current = MapType.Source27;
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
if (data == 1347633746) {
|
|
// Reads in ASCII as "RBSP". Raven software's modification of Q3BSP, or Ritual's modification of Q2.
|
|
// Formats: Raven, SiN
|
|
current = MapType.Raven;
|
|
for (int i = 0; i < 17; i++) {
|
|
// Find out where the first lump starts, based on offsets.
|
|
stream.Seek((i + 1) * 8, SeekOrigin.Begin);
|
|
int temp = binaryReader.ReadInt32();
|
|
if (bigEndian) {
|
|
byte[] bytes = BitConverter.GetBytes(temp);
|
|
Array.Reverse(bytes);
|
|
temp = BitConverter.ToInt32(bytes, 0);
|
|
}
|
|
if (temp == 168) {
|
|
current = MapType.SiN;
|
|
break;
|
|
} else {
|
|
if (temp == 152) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
if (data == 556942917) {
|
|
// "EF2!"
|
|
current = MapType.STEF2;
|
|
} else {
|
|
if (data == 1263223110) {
|
|
// "FAKK"
|
|
// Formats: STEF2 demo, Heavy Metal FAKK2 (American McGee's Alice)
|
|
data = binaryReader.ReadInt32();
|
|
if (bigEndian) {
|
|
byte[] bytes = BitConverter.GetBytes(data);
|
|
Array.Reverse(bytes);
|
|
data = BitConverter.ToInt32(bytes, 0);
|
|
}
|
|
switch (data) {
|
|
case 19: {
|
|
current = MapType.STEF2Demo;
|
|
break;
|
|
}
|
|
case 12:
|
|
case 42: {// American McGee's Alice
|
|
current = MapType.FAKK;
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
switch (data) {
|
|
// Various numbers not representing a string
|
|
// Formats: HL1, Quake, Nightfire, or perhaps Tactical Intervention's encrypted format
|
|
case 29:
|
|
case 30: {
|
|
current = MapType.Quake;
|
|
break;
|
|
}
|
|
case 42: {
|
|
current = MapType.Nightfire;
|
|
break;
|
|
}
|
|
default: {
|
|
// Hack to get Tactical Intervention's encryption key. At offset 384, there are two unused lumps whose
|
|
// values in the header are always 0s. Grab these 32 bytes (256 bits) and see if they match an expected value.
|
|
stream.Seek(384, SeekOrigin.Begin);
|
|
key = binaryReader.ReadBytes(32);
|
|
stream.Seek(0, SeekOrigin.Begin);
|
|
data = BitConverter.ToInt32(XorWithKeyStartingAtIndex(binaryReader.ReadBytes(4)), 0);
|
|
if (data == 1347633750) {
|
|
current = MapType.TacticalInterventionEncrypted;
|
|
} else {
|
|
current = MapType.Undefined;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return current;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Disposes of the <see cref="FileStream"/> and releases the handle to the File.
|
|
/// </summary>
|
|
public void Close() {
|
|
stream.Dispose();
|
|
}
|
|
}
|
|
}
|