Skip to content

Commit

Permalink
io: Refactor Biff attributes.
Browse files Browse the repository at this point in the history
  • Loading branch information
freezy committed Dec 23, 2019
1 parent a8fcdfd commit 1f27b25
Show file tree
Hide file tree
Showing 17 changed files with 553 additions and 311 deletions.
4 changes: 4 additions & 0 deletions .editorconfig
@@ -0,0 +1,4 @@
[*]
end_of_line = crlf
insert_final_newline = true
indent_style = tab
129 changes: 129 additions & 0 deletions VisualPinball.Engine.Test/IO/BiffAttributeTest.cs
@@ -0,0 +1,129 @@
using System;
using System.Linq;
using System.Reflection;
using VisualPinball.Engine.IO;
using VisualPinball.Engine.Math;
using Xunit;

namespace VisualPinball.Engine.Test.IO
{
public class BiffAttributeTest
{
[Fact]
public void ShouldNotUseCountAndIndex()
{
GetAttributes(typeof(BiffAttribute), (memberType, member, biffDataType, attr) =>
{
if (attr.Count > -1 && attr.Index > -1) {
throw new Exception($"Must use either Count or Index but not both at {biffDataType.FullName}.{member.Name} ({attr.Name}).");
}
});
}

[Fact]
public void ShouldBeAppliedToStrings()
{
GetAttributes(typeof(BiffStringAttribute), (memberType, member, biffDataType, attr) =>
{
if (memberType != typeof(string) && memberType != typeof(string[])) {
throw new Exception($"BiffString of {biffDataType.FullName}.{member.Name} ({attr.Name}) must be either string or string[], but is {memberType.Name}.");
}
});
}

[Fact]
public void ShouldBeAppliedToIntegers()
{
GetAttributes(typeof(BiffIntAttribute), (memberType, member, biffDataType, attr) =>
{
if (memberType != typeof(int) && memberType != typeof(int[])) {
throw new Exception($"BiffInt of {biffDataType.FullName}.{member.Name} ({attr.Name}) must be either int or int[], but is {memberType.Name}.");
}
});
}

[Fact]
public void ShouldBeAppliedToFloats()
{
GetAttributes(typeof(BiffFloatAttribute), (memberType, member, biffDataType, attr) =>
{
if (memberType != typeof(float) && memberType != typeof(float[])) {
throw new Exception($"BiffFloat of {biffDataType.FullName}.{member.Name} ({attr.Name}) must be either float or float[], but is {memberType.Name}.");
}
});
}

[Fact]
public void ShouldBeAppliedToBooleans()
{
GetAttributes(typeof(BiffBoolAttribute), (memberType, member, biffDataType, attr) =>
{
if (memberType != typeof(bool) && memberType != typeof(bool[])) {
throw new Exception($"BiffBool of {biffDataType.FullName}.{member.Name} ({attr.Name}) must be either bool or bool[], but is {memberType.Name}.");
}
});
}

[Fact]
public void ShouldBeAppliedToColors()
{
GetAttributes(typeof(BiffColorAttribute), (memberType, member, biffDataType, attr) =>
{
if (memberType != typeof(Color) && memberType != typeof(Color[])) {
throw new Exception($"BiffColor of {biffDataType.FullName}.{member.Name} ({attr.Name}) must be either Color or Color[], but is {memberType.Name}.");
}
});
}

[Fact]
public void ShouldBeAppliedToVertices()
{
GetAttributes(typeof(BiffVertexAttribute), (memberType, member, biffDataType, attr) =>
{
if (memberType != typeof(Vertex2D) && memberType != typeof(Vertex3D)) {
throw new Exception($"BiffColor of {biffDataType.FullName}.{member.Name} ({attr.Name}) must be either Vertex2D or Vertex3D, but is {memberType.Name}.");
}
});
}

private static void GetAttributes(Type attributeType, Action<Type, MemberInfo, Type, BiffAttribute> assert)
{
var biffDataTypes = AppDomain.CurrentDomain
.GetAssemblies()
.First(a => a.GetName().Name == "VisualPinball.Engine")
.GetTypes()
.Where(t => t.IsSubclassOf(typeof(BiffData)))
.ToArray();

foreach (var biffDataType in biffDataTypes) {
var members = biffDataType.GetMembers()
.Where(member => member.MemberType == MemberTypes.Field || member.MemberType == MemberTypes.Property);

foreach (var member in members) {
var attrs = Attribute
.GetCustomAttributes(member, attributeType)
.Select(a => a as BiffAttribute)
.Where(a => a != null);

foreach (var attr in attrs) {
Type memberType = null;
switch (member) {
case FieldInfo field:
memberType = field.FieldType;
break;
case PropertyInfo property:
memberType = property.PropertyType;
break;
}

if (memberType == null) {
throw new Exception("Member type is null, that shouldn't happen because we filter by fields and properties.");
}

assert(memberType, member, biffDataType, attr);
}
}
}
}
}
}
27 changes: 14 additions & 13 deletions VisualPinball.Engine.Test/VPT/Table/TableDataTests.cs
Expand Up @@ -12,11 +12,8 @@ public void ShouldLoadCorrectData()
var table = Engine.VPT.Table.Table.Load(@"..\..\Fixtures\VPX\TableData.vpx");
var data = table.Data;

Assert.Equal(0.01435f, data._3DmaxSeparation);
Assert.Equal(0.002f, data._3DOffset);
Assert.Equal(0.53f, data._3DZPD);
Assert.Equal(0.60606f, data.AngleTiltMax);
Assert.Equal(0.2033f, data.AngletiltMin);
Assert.Equal(0.2033f, data.AngleTiltMin);
Assert.Equal(1.23f, data.AoScale);
Assert.Equal(true, data.BallDecalMode);
Assert.Equal("test_pattern", data.BallImage);
Expand Down Expand Up @@ -48,15 +45,15 @@ public void ShouldLoadCorrectData()
Assert.Equal(1.3211f, data.BgScaleZ[BackglassIndex.Desktop]);
Assert.Equal(1.41f, data.BgScaleZ[BackglassIndex.Fullscreen]);
Assert.Equal(12f, data.BgScaleZ[BackglassIndex.FullSingleScreen]);
Assert.Equal(12.33f, data.BgXlateX[BackglassIndex.Desktop]);
Assert.Equal(0.1f, data.BgXlateX[BackglassIndex.Fullscreen]);
Assert.Equal(0.2f, data.BgXlateX[BackglassIndex.FullSingleScreen]);
Assert.Equal(100.43f, data.BgXlateY[BackglassIndex.Desktop]);
Assert.Equal(90.1f, data.BgXlateY[BackglassIndex.Fullscreen]);
Assert.Equal(0.2332f, data.BgXlateY[BackglassIndex.FullSingleScreen]);
Assert.Equal(-50.092f, data.BgXlateZ[BackglassIndex.Desktop]);
Assert.Equal(400.1f, data.BgXlateZ[BackglassIndex.Fullscreen]);
Assert.Equal(-50.223f, data.BgXlateZ[BackglassIndex.FullSingleScreen]);
Assert.Equal(12.33f, data.BgOffsetX[BackglassIndex.Desktop]);
Assert.Equal(0.1f, data.BgOffsetX[BackglassIndex.Fullscreen]);
Assert.Equal(0.2f, data.BgOffsetX[BackglassIndex.FullSingleScreen]);
Assert.Equal(100.43f, data.BgOffsetY[BackglassIndex.Desktop]);
Assert.Equal(90.1f, data.BgOffsetY[BackglassIndex.Fullscreen]);
Assert.Equal(0.2332f, data.BgOffsetY[BackglassIndex.FullSingleScreen]);
Assert.Equal(-50.092f, data.BgOffsetZ[BackglassIndex.Desktop]);
Assert.Equal(400.1f, data.BgOffsetZ[BackglassIndex.Fullscreen]);
Assert.Equal(-50.223f, data.BgOffsetZ[BackglassIndex.FullSingleScreen]);
Assert.Equal(1.5055f, data.BloomStrength);
Assert.Equal(2224, data.Bottom);
Assert.Equal("Option Explicit\r\n", data.Code);
Expand Down Expand Up @@ -119,12 +116,16 @@ public void ShouldLoadCorrectData()
Assert.Equal("", data.ScreenShot);
Assert.Equal(true, data.ShowGrid);
Assert.Equal(0.4123f, data.SsrScale);
Assert.Equal(0.01435f, data.StereoMaxSeparation);
Assert.Equal(0.002f, data.StereoOffset);
Assert.Equal(0.53f, data.StereoZeroParallaxDisplacement);
Assert.Equal(6649, data.TableAdaptiveVSync);
Assert.Equal(2.009231f, data.TableHeight);
Assert.Equal(0.24f, data.TableMusicVolume);
Assert.Equal(0.23f, data.TableSoundVolume);
Assert.Equal(0f, data.Top); // set in debug mode
Assert.Equal(0, data.UseAA);
Assert.Equal(0, data.UseAO);
Assert.Equal(1, data.UseFXAA);
Assert.Equal(7, data.UserDetailLevel);
Assert.Equal(0, data.UseReflectionForBalls);
Expand Down
1 change: 1 addition & 0 deletions VisualPinball.Engine.Test/VisualPinball.Engine.Test.csproj
Expand Up @@ -51,6 +51,7 @@
</Reference>
</ItemGroup>
<ItemGroup>
<Compile Include="IO\BiffAttributeTest.cs" />
<Compile Include="Math\ColorTest.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="VPT\Table\TableDataTests.cs" />
Expand Down
126 changes: 6 additions & 120 deletions VisualPinball.Engine/IO/BiffAttribute.cs
@@ -1,9 +1,6 @@
using System;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using VisualPinball.Engine.Math;
using VisualPinball.Engine.VPT;

namespace VisualPinball.Engine.IO
Expand All @@ -21,7 +18,7 @@ namespace VisualPinball.Engine.IO
/// <see href="https://en.wikipedia.org/wiki/COM_Structured_Storage">COM Structured Storage</see>
/// <see href="https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-xls/cd03cb5f-ca02-4934-a391-bb674cb8aa06">BIFF Format</see>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = true)]
public class BiffAttribute : Attribute
public abstract class BiffAttribute : Attribute
{
/// <summary>
/// Name of the BIFF record, usually four characters
Expand All @@ -34,29 +31,16 @@ public class BiffAttribute : Attribute
/// </summary>
public bool IsStreaming;

/// <summary>
/// Wide strings have a zero byte between each character.
/// </summary>
public bool IsWideString;

public int QuantizedUnsignedBits = -1;
public bool AsPercent = false;

/// <summary>
/// For arrays, this defines how many values should be read
/// </summary>
public int Count = 1;
public int Count = -1;

/// <summary>
/// For arrays, this defines that only one value should be read
/// and stored at the given position.
/// </summary>
public int Index;

/// <summary>
/// For colors, this defines how the integer is encoded.
/// </summary>
public ColorFormat ColorFormat = ColorFormat.Bgr;
public int Index = -1;

/// <summary>
/// If put on a field, this is the info from C#'s reflection API.
Expand All @@ -67,9 +51,9 @@ public class BiffAttribute : Attribute
/// </summary>
public PropertyInfo Property { get; set; }

private Type Type => Field != null ? Field.FieldType : Property.PropertyType;
protected Type Type => Field != null ? Field.FieldType : Property.PropertyType;

public BiffAttribute(string name)
protected BiffAttribute(string name)
{
Name = name;
}
Expand All @@ -90,105 +74,7 @@ public BiffAttribute(string name)
/// <param name="reader">Binary data from the VPX file</param>
/// <param name="len">Length of the BIFF record</param>
/// <typeparam name="T">Type of the item data we're currently parsing</typeparam>
public virtual void Parse<T>(T obj, BinaryReader reader, int len) where T : ItemData
{
if (Type == typeof(float)) {
SetValue(obj, ReadFloat(reader));

} else if (Type == typeof(int)) {
SetValue(obj, reader.ReadInt32());

} else if (Type == typeof(bool)) {
SetValue(obj, reader.ReadInt32() > 0);

} else if (Type == typeof(float[])) {
if (GetValue(obj) is float[] arr) {
arr[Index] = ReadFloat(reader);
} else {
Console.Error.WriteLine($"[BiffAttribute.Parse] Expected float[] for {Name}, but got {GetValue(obj).GetType()}.");
}

} else if (Type == typeof(uint[])) {
if (!(GetValue(obj) is uint[] arr)) {
Console.Error.WriteLine($"[BiffAttribute.Parse] Expected uint[] for {Name}, but got {GetValue(obj).GetType()}.");
return;
}
if (Count > 1) {
for (var i = 0; i < Count; i++) {
arr[i] = reader.ReadUInt32();
}
} else {
arr[Index] = reader.ReadUInt32();
}

} else if (Type == typeof(string[])) {
if (GetValue(obj) is string[] arr) {
arr[Index] = ReadString(reader, len);
} else {
Console.Error.WriteLine($"[BiffAttribute.Parse] Expected string[] for {Name}, but got {GetValue(obj).GetType()}.");
}

} else if (Type == typeof(string)) {
SetValue(obj, ReadString(reader, len));

} else if (Type == typeof(Vertex3D)) {
SetValue(obj, new Vertex3D(reader));

} else if (Type == typeof(Vertex2D)) {
SetValue(obj, new Vertex2D(reader));

} else if (Type == typeof(Color)) {
SetValue(obj, new Color(reader.ReadInt32(), ColorFormat));

} else if (Type == typeof(Color[])) {
if (GetValue(obj) is Color[] arr) {
if (Count > 1) {
for (var i = 0; i < Count; i++) {
arr[i] = new Color(reader.ReadInt32(), ColorFormat);
}
} else {
arr[Index] = new Color(reader.ReadInt32(), ColorFormat);
}
} else {
Console.Error.WriteLine($"[BiffAttribute.Parse] Expected Color[] for {Name}, but got {GetValue(obj).GetType()}.");
}

} else {
Console.Error.WriteLine("[BiffAttribute.Parse] Unknown type \"{0}\" for tag {1}", Type, Name);
reader.BaseStream.Seek(len, SeekOrigin.Current);
}
}

private string ReadString(BinaryReader reader, int len)
{
byte[] bytes;
if (IsWideString) {
var wideLen = reader.ReadInt32();
bytes = reader.ReadBytes(wideLen).Where((x, i) => i % 2 == 0).ToArray();
} else {
bytes = IsStreaming ? reader.ReadBytes(len) : reader.ReadBytes(len).Skip(4).ToArray();
}
return Encoding.ASCII.GetString(bytes);
}

private float ReadFloat(BinaryReader reader)
{
var f = QuantizedUnsignedBits > 0
? DequantizeUnsigned(QuantizedUnsignedBits, reader.ReadInt32())
: reader.ReadSingle();

if (AsPercent) {
return f * 100f;
}

return f;
}

private float DequantizeUnsigned(int bits, int i)
{
var N = (1 << bits) - 1;
return System.Math.Min(i / (float) N, 1.0f);
}
public abstract void Parse<T>(T obj, BinaryReader reader, int len) where T : ItemData;

/// <summary>
/// Sets the value to either field or property, depending on which
Expand Down
27 changes: 27 additions & 0 deletions VisualPinball.Engine/IO/BiffBoolAttribute.cs
@@ -0,0 +1,27 @@
using System;
using System.IO;

namespace VisualPinball.Engine.IO
{
public class BiffBoolAttribute : BiffAttribute
{
public BiffBoolAttribute(string name) : base(name) { }

public override void Parse<T>(T obj, BinaryReader reader, int len)
{
if (Type == typeof(bool)) {
SetValue(obj, reader.ReadInt32() > 0);

} else if (Type == typeof(bool[])) {
var arr = GetValue(obj) as bool[];
if (Count > 1) {
for (var i = 0; i < Count; i++) {
arr[i] = reader.ReadInt32() > 0;
}
} else {
arr[Index] = reader.ReadInt32() > 0;
}
}
}
}
}

0 comments on commit 1f27b25

Please sign in to comment.