Files
LibBSP/LibBSP/Source/Structs/BSP/Texture.cs

852 lines
22 KiB
C#

#if UNITY_3_4 || UNITY_3_5 || UNITY_4_0 || UNITY_4_0_1 || UNITY_4_2 || UNITY_4_3 || UNITY_4_5 || UNITY_4_6 || UNITY_5 || UNITY_5_3_OR_NEWER
#define UNITY
#endif
using System;
using System.Text;
namespace LibBSP {
#if UNITY
using Vector2 = UnityEngine.Vector2;
using Vector3 = UnityEngine.Vector3;
#elif GODOT
using Vector2 = Godot.Vector2;
using Vector3 = Godot.Vector3;
#else
using Vector2 = System.Numerics.Vector2;
using Vector3 = System.Numerics.Vector3;
#endif
/// <summary>
/// An all-encompassing class to handle the texture information of any given BSP format.
/// </summary>
/// <remarks>
/// The way texture information is stored varies wildly between versions. As a general
/// rule, this class only handles the lump containing the string of a texture's name,
/// and data from within the lump associated with it.
/// For example, Nightfire's texture lump only contains 64-byte null-padded strings, but
/// Quake 2's has texture scaling included.
/// </remarks>
public struct Texture : ILumpObject {
/// <summary>
/// Number of MipMap levels used by Quake/GoldSrc engines.
/// </summary>
public const int NumMipmaps = 4;
/// <summary>
/// Index of the full mipmap.
/// </summary>
public const int FullMipmap = 0;
/// <summary>
/// Index of the half mipmap.
/// </summary>
public const int HalfMipmap = 1;
/// <summary>
/// Index of the quarter mipmap.
/// </summary>
public const int QuarterMipmap = 2;
/// <summary>
/// Index of the eighth mipmap.
/// </summary>
public const int EighthMipmap = 3;
/// <summary>
/// The <see cref="ILump"/> this <see cref="ILumpObject"/> came from.
/// </summary>
public ILump Parent { get; private set; }
/// <summary>
/// Array of <c>byte</c>s used as the data source for this <see cref="ILumpObject"/>.
/// </summary>
public byte[] Data { get; private set; }
private byte[][] _mipmaps;
/// <summary>
/// Mipmap image data for this <see cref="Texture"/>.
/// </summary>
public byte[][] Mipmaps {
get {
if (_mipmaps == null && MipmapFullOffset >= 0) {
_mipmaps = new byte[NumMipmaps][];
}
return _mipmaps;
}
}
/// <summary>
/// The <see cref="LibBSP.MapType"/> to use to interpret <see cref="Data"/>.
/// </summary>
public MapType MapType {
get {
if (Parent == null || Parent.Bsp == null) {
return MapType.Undefined;
}
return Parent.Bsp.version;
}
}
/// <summary>
/// The version number of the <see cref="ILump"/> this <see cref="ILumpObject"/> came from.
/// </summary>
public int LumpVersion {
get {
if (Parent == null) {
return 0;
}
return Parent.LumpInfo.version;
}
}
/// <summary>
/// Gets or sets the name of this <see cref="Texture"/>.
/// </summary>
public string Name {
get {
switch (MapType) {
case MapType.Quake:
case MapType.GoldSrc: {
return Data.ToNullTerminatedString(0, 16);
}
case MapType.STEF2:
case MapType.STEF2Demo:
case MapType.Raven:
case MapType.Quake3:
case MapType.CoD:
case MapType.CoD2:
case MapType.CoD4:
case MapType.FAKK:
case MapType.MOHAA:
case MapType.Nightfire: {
return Data.ToNullTerminatedString(0, 64);
}
case MapType.Quake2:
case MapType.SoF:
case MapType.Daikatana: {
return Data.ToNullTerminatedString(40, 32);
}
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.L4D2:
case MapType.TacticalInterventionEncrypted:
case MapType.Vindictus:
case MapType.DMoMaM:
case MapType.Titanfall: {
return Data.ToRawString();
}
case MapType.SiN: {
return Data.ToNullTerminatedString(36, 64);
}
default: {
return null;
}
}
}
set {
byte[] bytes = Encoding.ASCII.GetBytes(value);
switch (MapType) {
case MapType.Quake:
case MapType.GoldSrc: {
for (int i = 0; i < 16; ++i) {
Data[i] = 0;
}
Array.Copy(bytes, 0, Data, 0, Math.Min(bytes.Length, 15));
break;
}
case MapType.STEF2:
case MapType.STEF2Demo:
case MapType.Raven:
case MapType.Quake3:
case MapType.CoD:
case MapType.CoD2:
case MapType.CoD4:
case MapType.FAKK:
case MapType.MOHAA:
case MapType.Nightfire: {
for (int i = 0; i < 64; ++i) {
Data[i] = 0;
}
Array.Copy(bytes, 0, Data, 0, Math.Min(bytes.Length, 63));
break;
}
case MapType.Quake2:
case MapType.SoF:
case MapType.Daikatana: {
for (int i = 0; i < 32; ++i) {
Data[i + 40] = 0;
}
Array.Copy(bytes, 0, Data, 40, Math.Min(bytes.Length, 31));
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.L4D2:
case MapType.TacticalInterventionEncrypted:
case MapType.Vindictus:
case MapType.DMoMaM:
case MapType.Titanfall: {
Data = bytes;
break;
}
case MapType.SiN: {
for (int i = 0; i < 64; ++i) {
Data[i + 36] = 0;
}
Array.Copy(bytes, 0, Data, 36, Math.Min(bytes.Length, 63));
break;
}
}
}
}
/// <summary>
/// Gets or sets the name of the mask used on this <see cref="Texture"/>.
/// </summary>
public string Mask {
get {
switch (MapType) {
case MapType.MOHAA: {
return Data.ToNullTerminatedString(76, 64);
}
default: {
return null;
}
}
}
set {
switch (MapType) {
case MapType.MOHAA: {
for (int i = 0; i < 64; ++i) {
Data[i + 76] = 0;
}
byte[] strBytes = Encoding.ASCII.GetBytes(value);
Array.Copy(strBytes, 0, Data, 76, Math.Min(strBytes.Length, 63));
break;
}
}
}
}
/// <summary>
/// Gets or sets the flags on this <see cref="Texture"/>.
/// </summary>
public int Flags {
get {
switch (MapType) {
case MapType.Quake2:
case MapType.SoF:
case MapType.Daikatana:
case MapType.SiN: {
return BitConverter.ToInt32(Data, 32);
}
case MapType.MOHAA:
case MapType.STEF2:
case MapType.STEF2Demo:
case MapType.Raven:
case MapType.Quake3:
case MapType.CoD:
case MapType.CoD2:
case MapType.CoD4:
case MapType.FAKK: {
return BitConverter.ToInt32(Data, 64);
}
default: {
return -1;
}
}
}
set {
byte[] bytes = BitConverter.GetBytes(value);
switch (MapType) {
case MapType.Quake2:
case MapType.SoF:
case MapType.Daikatana:
case MapType.SiN: {
bytes.CopyTo(Data, 32);
break;
}
case MapType.MOHAA:
case MapType.STEF2:
case MapType.STEF2Demo:
case MapType.Raven:
case MapType.Quake3:
case MapType.CoD:
case MapType.CoD2:
case MapType.CoD4:
case MapType.FAKK: {
bytes.CopyTo(Data, 64);
break;
}
}
}
}
/// <summary>
/// Gets or sets the contents flags used by this <see cref="Texture"/>.
/// </summary>
public int Contents {
get {
switch (MapType) {
case MapType.STEF2:
case MapType.STEF2Demo:
case MapType.Raven:
case MapType.Quake3:
case MapType.CoD:
case MapType.CoD2:
case MapType.CoD4:
case MapType.FAKK:
case MapType.MOHAA: {
return BitConverter.ToInt32(Data, 68);
}
default: {
return -1;
}
}
}
set {
byte[] bytes = BitConverter.GetBytes(value);
switch (MapType) {
case MapType.STEF2:
case MapType.STEF2Demo:
case MapType.Raven:
case MapType.Quake3:
case MapType.CoD:
case MapType.CoD2:
case MapType.CoD4:
case MapType.FAKK:
case MapType.MOHAA: {
bytes.CopyTo(Data, 68);
break;
}
}
}
}
/// <summary>
/// Gets or sets the <see cref="LibBSP.TextureInfo"/> in this <see cref="Texture"/>.
/// </summary>
public TextureInfo TextureInfo {
get {
switch (MapType) {
case MapType.Quake2:
case MapType.SoF:
case MapType.Daikatana:
case MapType.SiN: {
return new TextureInfo(new Vector3(BitConverter.ToSingle(Data, 0), BitConverter.ToSingle(Data, 4), BitConverter.ToSingle(Data, 8)),
new Vector3(BitConverter.ToSingle(Data, 16), BitConverter.ToSingle(Data, 20), BitConverter.ToSingle(Data, 24)),
new Vector2(BitConverter.ToSingle(Data, 12), BitConverter.ToSingle(Data, 28)),
new Vector2(1, 1),
-1, -1, 0);
}
default: {
return new TextureInfo();
}
}
}
set {
switch (MapType) {
case MapType.Quake2:
case MapType.SoF:
case MapType.Daikatana:
case MapType.SiN: {
byte[] bytes = value.UAxis.GetBytes();
bytes.CopyTo(Data, 0);
bytes = value.VAxis.GetBytes();
bytes.CopyTo(Data, 16);
bytes = BitConverter.GetBytes(value.Translation.X());
bytes.CopyTo(Data, 12);
bytes = BitConverter.GetBytes(value.Translation.Y());
bytes.CopyTo(Data, 28);
break;
}
}
}
}
/// <summary>
/// Gets or sets the dimensions of this <see cref="Texture"/>.
/// </summary>
public Vector2 Dimensions {
get {
switch (MapType) {
case MapType.Quake:
case MapType.GoldSrc: {
return new Vector2(BitConverter.ToUInt32(Data, 16), BitConverter.ToUInt32(Data, 20));
}
default: {
return new Vector2(0, 0);
}
}
}
set {
switch (MapType) {
case MapType.Quake:
case MapType.GoldSrc: {
BitConverter.GetBytes((int)value.X()).CopyTo(Data, 16);
BitConverter.GetBytes((int)value.Y()).CopyTo(Data, 20);
break;
}
}
}
}
/// <summary>
/// Gets the offset to the full mipmap of this <see cref="Texture"/>.
/// </summary>
public int MipmapFullOffset {
get {
switch (MapType) {
case MapType.Quake:
case MapType.GoldSrc: {
return BitConverter.ToInt32(Data, 24);
}
default: {
return -1;
}
}
}
set {
byte[] bytes = BitConverter.GetBytes(value);
switch (MapType) {
case MapType.Quake:
case MapType.GoldSrc: {
bytes.CopyTo(Data, 24);
break;
}
}
}
}
/// <summary>
/// Gets the offset to the half mipmap of this <see cref="Texture"/>.
/// </summary>
public int MipmapHalfOffset {
get {
switch (MapType) {
case MapType.Quake:
case MapType.GoldSrc: {
return BitConverter.ToInt32(Data, 28);
}
default: {
return -1;
}
}
}
set {
byte[] bytes = BitConverter.GetBytes(value);
switch (MapType) {
case MapType.Quake:
case MapType.GoldSrc: {
bytes.CopyTo(Data, 28);
break;
}
}
}
}
/// <summary>
/// Gets the offset to the quarter mipmap of this <see cref="Texture"/>.
/// </summary>
public int MipmapQuarterOffset {
get {
switch (MapType) {
case MapType.Quake:
case MapType.GoldSrc: {
return BitConverter.ToInt32(Data, 32);
}
default: {
return -1;
}
}
}
set {
byte[] bytes = BitConverter.GetBytes(value);
switch (MapType) {
case MapType.Quake:
case MapType.GoldSrc: {
bytes.CopyTo(Data, 32);
break;
}
}
}
}
/// <summary>
/// Gets the offset to the eighth mipmap of this <see cref="Texture"/>.
/// </summary>
public int MipmapEighthOffset {
get {
switch (MapType) {
case MapType.Quake:
case MapType.GoldSrc: {
return BitConverter.ToInt32(Data, 36);
}
default: {
return -1;
}
}
}
set {
byte[] bytes = BitConverter.GetBytes(value);
switch (MapType) {
case MapType.Quake:
case MapType.GoldSrc: {
bytes.CopyTo(Data, 36);
break;
}
}
}
}
/// <summary>
/// Gets the miscellaneous value for this <see cref="Texture"/>.
/// </summary>
public int Value {
get {
switch (MapType) {
case MapType.Quake2:
case MapType.SoF:
case MapType.Daikatana: {
return BitConverter.ToInt32(Data, 36);
}
default: {
return 0;
}
}
}
set {
byte[] bytes = BitConverter.GetBytes(value);
switch (MapType) {
case MapType.Quake2:
case MapType.SoF:
case MapType.Daikatana: {
bytes.CopyTo(Data, 36);
break;
}
}
}
}
/// <summary>
/// Gets or sets the next frame's <see cref="Texture"/> if this one is animated.
/// </summary>
public int Next {
get {
switch (MapType) {
case MapType.Quake2:
case MapType.SoF:
case MapType.Daikatana: {
return BitConverter.ToInt32(Data, 72);
}
case MapType.SiN: {
return BitConverter.ToInt32(Data, 100);
}
default: {
return 0;
}
}
}
set {
byte[] bytes = BitConverter.GetBytes(value);
switch (MapType) {
case MapType.Quake2:
case MapType.SoF:
case MapType.Daikatana: {
bytes.CopyTo(Data, 72);
break;
}
case MapType.SiN: {
bytes.CopyTo(Data, 100);
break;
}
}
}
}
/// <summary>
/// Gets or sets the number of curve subdivisions for this <see cref="Texture"/>.
/// </summary>
public int Subdivisions {
get {
switch (MapType) {
case MapType.STEF2:
case MapType.STEF2Demo:
case MapType.MOHAA:
case MapType.FAKK: {
return BitConverter.ToInt32(Data, 72);
}
default: {
return 16;
}
}
}
set {
byte[] bytes = BitConverter.GetBytes(value);
switch (MapType) {
case MapType.STEF2:
case MapType.STEF2Demo:
case MapType.MOHAA:
case MapType.FAKK: {
bytes.CopyTo(Data, 72);
break;
}
}
}
}
/// <summary>
/// Creates a new <see cref="Texture"/> object from a <c>byte</c> array.
/// </summary>
/// <param name="data"><c>byte</c> array to parse.</param>
/// <param name="parent">The <see cref="ILump"/> this <see cref="Texture"/> came from.</param>
/// <exception cref="ArgumentNullException"><paramref name="data"/> was <c>null</c>.</exception>
public Texture(byte[] data, ILump parent) : this(data, parent, null) { }
/// <summary>
/// Creates a new <see cref="Texture"/> object from a <c>byte</c> array.
/// </summary>
/// <param name="data"><c>byte</c> array to parse.</param>
/// <param name="parent">The <see cref="ILump"/> this <see cref="Texture"/> came from.</param>
/// <param name="mipmaps">Data for the mipmap levels.</param>
/// <exception cref="ArgumentNullException"><paramref name="data"/> was <c>null</c>.</exception>
public Texture(byte[] data, ILump parent, byte[][] mipmaps) {
if (data == null) {
throw new ArgumentNullException();
}
Data = data;
Parent = parent;
_mipmaps = mipmaps;
}
/// <summary>
/// Creates a new <see cref="Texture"/> by copying the fields in <paramref name="source"/>, using
/// <paramref name="parent"/> to get <see cref="LibBSP.MapType"/> and <see cref="LumpInfo.version"/>
/// to use when creating the new <see cref="Texture"/>.
/// If the <paramref name="parent"/>'s <see cref="BSP"/>'s <see cref="LibBSP.MapType"/> is different from
/// the one from <paramref name="source"/>, it does not matter, because fields are copied by name.
/// </summary>
/// <param name="source">The <see cref="Texture"/> to copy.</param>
/// <param name="parent">
/// The <see cref="ILump"/> to use as the <see cref="Parent"/> of the new <see cref="Texture"/>.
/// Use <c>null</c> to use the <paramref name="source"/>'s <see cref="Parent"/> instead.
/// </param>
public Texture(Texture source, ILump parent) {
Parent = parent;
if (parent != null && parent.Bsp != null) {
if (source.Parent != null && source.Parent.Bsp != null && source.Parent.Bsp.version == parent.Bsp.version && source.LumpVersion == parent.LumpInfo.version) {
Data = new byte[source.Data.Length];
Array.Copy(source.Data, Data, source.Data.Length);
_mipmaps = source.Mipmaps;
return;
} else {
Data = new byte[GetStructLength(parent.Bsp.version, parent.LumpInfo.version)];
}
} else {
if (source.Parent != null && source.Parent.Bsp != null) {
Data = new byte[GetStructLength(source.Parent.Bsp.version, source.Parent.LumpInfo.version)];
} else {
Data = new byte[GetStructLength(MapType.Undefined, 0)];
}
}
_mipmaps = source.Mipmaps;
Name = source.Name;
Mask = source.Mask;
Flags = source.Flags;
Contents = source.Contents;
TextureInfo = source.TextureInfo;
Dimensions = source.Dimensions;
MipmapFullOffset = source.MipmapFullOffset;
MipmapHalfOffset = source.MipmapHalfOffset;
MipmapQuarterOffset = source.MipmapQuarterOffset;
MipmapEighthOffset = source.MipmapEighthOffset;
Value = source.Value;
Next = source.Next;
Subdivisions = source.Subdivisions;
}
/// <summary>
/// Factory method to parse a <c>byte</c> array into a <see cref="Textures"/> object.
/// </summary>
/// <param name="data">The data to parse.</param>
/// <param name="bsp">The <see cref="BSP"/> this lump came from.</param>
/// <param name="lumpInfo">The <see cref="LumpInfo"/> associated with this lump.</param>
/// <returns>A <see cref="Textures"/> object.</returns>
/// <exception cref="ArgumentNullException"><paramref name="data"/> parameter was <c>null</c>.</exception>
public static Textures LumpFactory(byte[] data, BSP bsp, LumpInfo lumpInfo) {
if (data == null) {
throw new ArgumentNullException();
}
return new Textures(data, GetStructLength(bsp.version, lumpInfo.version), bsp, lumpInfo);
}
/// <summary>
/// Depending on format, this is a variable length structure. Return -1. The <see cref="Textures"/> class will handle object creation.
/// </summary>
/// <param name="mapType">The <see cref="LibBSP.MapType"/> of the BSP.</param>
/// <param name="lumpVersion">The version number for the lump.</param>
/// <returns>-1</returns>
public static int GetStructLength(MapType type, int version) {
return -1;
}
/// <summary>
/// Gets the index for this lump in the BSP file for a specific map format.
/// </summary>
/// <param name="type">The map type.</param>
/// <returns>Index for this lump, or -1 if the format doesn't have this lump.</returns>
public static int GetIndexForLump(MapType type) {
switch (type) {
case MapType.CoD:
case MapType.CoD2:
case MapType.CoD4:
case MapType.FAKK:
case MapType.MOHAA:
case MapType.STEF2:
case MapType.STEF2Demo: {
return 0;
}
case MapType.Raven:
case MapType.Quake3: {
return 1;
}
case MapType.Quake:
case MapType.GoldSrc:
case MapType.Nightfire: {
return 2;
}
case MapType.Quake2:
case MapType.SiN:
case MapType.Daikatana:
case MapType.SoF: {
return 5;
}
case MapType.Vindictus:
case MapType.TacticalInterventionEncrypted:
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.L4D2:
case MapType.DMoMaM:
case MapType.Titanfall: {
return 43;
}
default: {
return -1;
}
}
}
/// <summary>
/// Gets the index for the materials lump in the BSP file for a specific map format.
/// </summary>
/// <param name="type">The map type.</param>
/// <returns>Index for this lump, or -1 if the format doesn't have this lump.</returns>
public static int GetIndexForMaterialLump(MapType type) {
switch (type) {
case MapType.Nightfire: {
return 3;
}
default: {
return -1;
}
}
}
/// <summary>
/// Returns a texture <see cref="name"/> with back slashes converted to forward slashes,
/// Source cubemap names fixed, etc.
/// </summary>
/// <param name="name">The name of the texture to process.</param>
/// <param name="mapType">The <see cref="MapType"/> of the BSP this texture name is from.</param>
/// <returns>A sanitized version of the passed <paramref name="name"/>.</returns>
public static string SanitizeName(string name, MapType mapType) {
string sanitized = name.Replace('\\', '/');
switch (mapType) {
case MapType.Vindictus:
case MapType.TacticalInterventionEncrypted:
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.L4D2:
case MapType.DMoMaM:
case MapType.Titanfall: {
if (sanitized.Length >= 5 && sanitized.Substring(0, 5).Equals("maps/", StringComparison.InvariantCultureIgnoreCase)) {
sanitized = sanitized.Substring(5);
for (int i = 0; i < sanitized.Length; ++i) {
if (sanitized[i] == '/') {
sanitized = sanitized.Substring(i + 1);
break;
}
}
}
// Parse cubemap textures
// TODO: Use regex? .{1,}(_-?[0-9]{1,}){3}$
int numUnderscores = 0;
bool validnumber = false;
for (int i = sanitized.Length - 1; i > 0; --i) {
if (sanitized[i] <= '9' && sanitized[i] >= '0') {
// Current is a number, this may be a cubemap reference
validnumber = true;
} else {
if (sanitized[i] == '-') {
// Current is a minus sign (-).
if (!validnumber) {
break; // Make sure there's a number to add the minus sign to. If not, kill the loop.
}
} else {
if (sanitized[i] == '_') {
// Current is an underscore (_)
if (validnumber) {
// Make sure there is a number in the current string
++numUnderscores; // before moving on to the next one.
if (numUnderscores == 3) {
// If we've got all our numbers
sanitized = sanitized.Substring(0, i); // Cut the texture string
}
validnumber = false;
} else {
// No number after the underscore
break;
}
} else {
// Not an acceptable character
break;
}
}
}
}
break;
}
}
return sanitized;
}
}
}