Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions S1API.Tests/Vehicles/VehicleColorCatalogCollection.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace S1API.Tests.Vehicles;

[CollectionDefinition(Name, DisableParallelization = true)]
public sealed class VehicleColorCatalogCollection
{
public const string Name = "Vehicle color catalog";
}
177 changes: 177 additions & 0 deletions S1API.Tests/Vehicles/VehicleColorCatalogTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
using S1API.Vehicles;
using UnityEngine;

namespace S1API.Tests.Vehicles;

[Collection(VehicleColorCatalogCollection.Name)]
public sealed class VehicleColorCatalogTests : IDisposable
{
private readonly FakeVehicleColorMetadataProvider _provider = new();

public VehicleColorCatalogTests()
{
VehicleColorCatalog.ResetForTesting(_provider);
}

public void Dispose()
{
VehicleColorCatalog.RestoreProviderForTesting();
}

[Fact]
public void LookupsExposeS1ApiOwnedMetadata()
{
Color materialColor = CreateColor(0.2f, 0.4f, 0.6f);
#if IL2CPPMELON
Color32 uiColor = default;
#else
Color32 uiColor = new(12, 34, 56, 255);
#endif
var metadata = new VehicleColorMetadata(
VehicleColor.DarkBlue,
"Dark Blue",
materialColor,
uiColor);
_provider.Colors.Add(metadata);

Assert.Same(metadata, VehicleColorCatalog.GetColor(VehicleColor.DarkBlue));
Assert.True(
VehicleColorCatalog.TryGetColor(
VehicleColor.DarkBlue,
out VehicleColorMetadata? resolved));
Assert.Same(metadata, resolved);
Assert.Equal("Dark Blue", resolved!.DisplayName);
AssertColor(materialColor, resolved.MaterialColor);
#if !IL2CPPMELON
Assert.Equal(uiColor, resolved.UIColor);
#endif
}

[Fact]
public void ColorsReturnsOrderedReadOnlySnapshots()
{
var red = new VehicleColorMetadata(
VehicleColor.Red,
"Red",
default,
default);
var black = new VehicleColorMetadata(
VehicleColor.Black,
"Black",
default,
default);
_provider.Colors.Add(red);
_provider.Colors.Add(black);

IReadOnlyList<VehicleColorMetadata> snapshot = VehicleColorCatalog.Colors;
_provider.Colors.Remove(red);

Assert.Equal(new[] { red, black }, snapshot);
Assert.Equal(new[] { black }, VehicleColorCatalog.Colors);

IList<VehicleColorMetadata> mutableView =
Assert.IsAssignableFrom<IList<VehicleColorMetadata>>(snapshot);
Assert.True(mutableView.IsReadOnly);
Assert.Throws<NotSupportedException>(() => mutableView.Add(red));
}

[Fact]
public void InvalidValuesDoNotReachNativeProvider()
{
Assert.False(
VehicleColorCatalog.TryGetColor(
(VehicleColor)(-1),
out VehicleColorMetadata? negativeMetadata));
Assert.False(
VehicleColorCatalog.TryGetColor(
(VehicleColor)999,
out VehicleColorMetadata? largeMetadata));

Assert.Null(negativeMetadata);
Assert.Null(largeMetadata);
Assert.Equal(0, _provider.ColorLookupCount);
}

[Fact]
public void UnavailableRuntimeReturnsEmptyAndNullResults()
{
Assert.Empty(VehicleColorCatalog.Colors);
Assert.Null(VehicleColorCatalog.GetColor(VehicleColor.Black));
}

[Fact]
public void MissingNativeColorsReturnFalse()
{
_provider.Colors.Add(new VehicleColorMetadata(
VehicleColor.Black,
"Black",
default,
default));

Assert.False(
VehicleColorCatalog.TryGetColor(
VehicleColor.Custom,
out VehicleColorMetadata? customMetadata));
Assert.Null(customMetadata);
}

[Fact]
public void MetadataPropertiesAreReadOnly()
{
Assert.All(
typeof(VehicleColorMetadata).GetProperties(),
property => Assert.False(property.CanWrite));
}

private static Color CreateColor(float red, float green, float blue)
{
Color color = default;
color.r = red;
color.g = green;
color.b = blue;
color.a = 1f;
return color;
}

private static void AssertColor(Color expected, Color actual)
{
Assert.Equal(expected.r, actual.r);
Assert.Equal(expected.g, actual.g);
Assert.Equal(expected.b, actual.b);
Assert.Equal(expected.a, actual.a);
}

private sealed class FakeVehicleColorMetadataProvider :
IVehicleColorMetadataProvider
{
internal List<VehicleColorMetadata> Colors { get; } = new();

internal int ColorLookupCount { get; private set; }

public bool TryGetColors(out IReadOnlyList<VehicleColorMetadata>? metadata)
{
metadata = Colors.Count == 0 ? null : Colors;
return metadata != null;
}

public bool TryGetColor(
VehicleColor color,
out VehicleColorMetadata? metadata)
{
ColorLookupCount++;

for (int i = 0; i < Colors.Count; i++)
{
VehicleColorMetadata entry = Colors[i];
if (entry.Color == color)
{
metadata = entry;
return true;
}
}

metadata = null;
return false;
}
}
}
177 changes: 177 additions & 0 deletions S1API/Vehicles/VehicleColorCatalog.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using UnityEngine;
#if MONOMELON
using S1DevUtilities = ScheduleOne.DevUtilities;
using S1Vehicles = ScheduleOne.Vehicles;
#elif IL2CPPMELON
using S1DevUtilities = Il2CppScheduleOne.DevUtilities;
using S1Vehicles = Il2CppScheduleOne.Vehicles;
#endif

namespace S1API.Vehicles
{
/// <summary>
/// Provides read-only access to the base game's vehicle color metadata.
/// </summary>
public static class VehicleColorCatalog
{
private static readonly IReadOnlyList<VehicleColorMetadata> EmptyColors =
new ReadOnlyCollection<VehicleColorMetadata>(
Array.Empty<VehicleColorMetadata>());
private static IVehicleColorMetadataProvider _provider =
new NativeVehicleColorMetadataProvider();

/// <summary>
/// Gets a read-only snapshot of the available vehicle color metadata in native library order.
/// Returns an empty list before the base game's vehicle color library is ready.
/// </summary>
public static IReadOnlyList<VehicleColorMetadata> Colors =>
GetColors();

/// <summary>
/// Gets metadata for a vehicle color.
/// </summary>
/// <param name="color">The vehicle color to look up.</param>
/// <returns>
/// The color metadata, or <see langword="null"/> when the color is unavailable
/// or the base game's vehicle color library is not ready.
/// </returns>
public static VehicleColorMetadata? GetColor(VehicleColor color)
{
TryGetColor(color, out VehicleColorMetadata? metadata);
return metadata;
}

/// <summary>
/// Tries to get metadata for a vehicle color.
/// </summary>
/// <param name="color">The vehicle color to look up.</param>
/// <param name="metadata">The resolved metadata when available.</param>
/// <returns><see langword="true"/> when metadata was found.</returns>
public static bool TryGetColor(
VehicleColor color,
out VehicleColorMetadata? metadata)
{
if (!Enum.IsDefined(typeof(VehicleColor), color))
{
metadata = null;
return false;
}

return _provider.TryGetColor(color, out metadata);
}

internal static void ResetForTesting(IVehicleColorMetadataProvider provider)
{
_provider = provider ?? throw new ArgumentNullException(nameof(provider));
}

internal static void RestoreProviderForTesting()
{
_provider = new NativeVehicleColorMetadataProvider();
}

private static IReadOnlyList<VehicleColorMetadata> GetColors()
{
if (!_provider.TryGetColors(out IReadOnlyList<VehicleColorMetadata>? metadata)
|| metadata == null
|| metadata.Count == 0)
{
return EmptyColors;
}

return new ReadOnlyCollection<VehicleColorMetadata>(
new List<VehicleColorMetadata>(metadata));
}
}

internal interface IVehicleColorMetadataProvider
{
bool TryGetColors(out IReadOnlyList<VehicleColorMetadata>? metadata);

bool TryGetColor(
VehicleColor color,
out VehicleColorMetadata? metadata);
}

internal sealed class NativeVehicleColorMetadataProvider :
IVehicleColorMetadataProvider
{
public bool TryGetColors(out IReadOnlyList<VehicleColorMetadata>? metadata)
{
metadata = null;

try
{
if (!S1DevUtilities.Singleton<S1Vehicles.Modification.VehicleColors>
.InstanceExists)
{
return false;
}

S1Vehicles.Modification.VehicleColors vehicleColors =
S1DevUtilities.Singleton<S1Vehicles.Modification.VehicleColors>
.Instance;
if (vehicleColors.colorLibrary == null)
return false;

var colors = new List<VehicleColorMetadata>(
vehicleColors.colorLibrary.Count);
for (int i = 0; i < vehicleColors.colorLibrary.Count; i++)
{
S1Vehicles.Modification.VehicleColors.VehicleColorData? nativeColor =
vehicleColors.colorLibrary[i];
if (nativeColor == null)
continue;

VehicleColor color = (VehicleColor)nativeColor.color;
if (!Enum.IsDefined(typeof(VehicleColor), color))
continue;

colors.Add(new VehicleColorMetadata(
color,
nativeColor.colorName,
nativeColor.MaterialColor,
nativeColor.UIColor));
}

if (colors.Count == 0)
return false;

metadata = new ReadOnlyCollection<VehicleColorMetadata>(colors);
return true;
}
catch
{
return false;
}
}

public bool TryGetColor(
VehicleColor color,
out VehicleColorMetadata? metadata)
{
metadata = null;

if (!TryGetColors(out IReadOnlyList<VehicleColorMetadata>? colors)
|| colors == null)
{
return false;
}

for (int i = 0; i < colors.Count; i++)
{
VehicleColorMetadata entry = colors[i];
if (entry.Color == color)
{
metadata = entry;
return true;
}
}

return false;
}
}
}
Loading
Loading