From 6ddb94d59d1228b0eea7031a8d78c93a954e5874 Mon Sep 17 00:00:00 2001 From: "Diffuin[bot]" Date: Fri, 14 Aug 2026 00:21:29 +0000 Subject: [PATCH 1/2] chore(diffuin): address #241 --- .../Vehicles/VehicleColorCatalogCollection.cs | 7 + .../Vehicles/VehicleColorCatalogTests.cs | 171 +++++++++++++++++ S1API/Vehicles/VehicleColorCatalog.cs | 177 ++++++++++++++++++ S1API/Vehicles/VehicleColorMetadata.cs | 42 +++++ S1API/docs/modules-overview.md | 1 + 5 files changed, 398 insertions(+) create mode 100644 S1API.Tests/Vehicles/VehicleColorCatalogCollection.cs create mode 100644 S1API.Tests/Vehicles/VehicleColorCatalogTests.cs create mode 100644 S1API/Vehicles/VehicleColorCatalog.cs create mode 100644 S1API/Vehicles/VehicleColorMetadata.cs diff --git a/S1API.Tests/Vehicles/VehicleColorCatalogCollection.cs b/S1API.Tests/Vehicles/VehicleColorCatalogCollection.cs new file mode 100644 index 00000000..50e7e3fc --- /dev/null +++ b/S1API.Tests/Vehicles/VehicleColorCatalogCollection.cs @@ -0,0 +1,7 @@ +namespace S1API.Tests.Vehicles; + +[CollectionDefinition(Name, DisableParallelization = true)] +public sealed class VehicleColorCatalogCollection +{ + public const string Name = "Vehicle color catalog"; +} diff --git a/S1API.Tests/Vehicles/VehicleColorCatalogTests.cs b/S1API.Tests/Vehicles/VehicleColorCatalogTests.cs new file mode 100644 index 00000000..c6167289 --- /dev/null +++ b/S1API.Tests/Vehicles/VehicleColorCatalogTests.cs @@ -0,0 +1,171 @@ +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); + Color32 uiColor = new(12, 34, 56, 255); + 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); + Assert.Equal(uiColor, resolved.UIColor); + } + + [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 snapshot = VehicleColorCatalog.Colors; + _provider.Colors.Remove(red); + + Assert.Equal(new[] { red, black }, snapshot); + Assert.Equal(new[] { black }, VehicleColorCatalog.Colors); + + IList mutableView = + Assert.IsAssignableFrom>(snapshot); + Assert.True(mutableView.IsReadOnly); + Assert.Throws(() => 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 Colors { get; } = new(); + + internal int ColorLookupCount { get; private set; } + + public bool TryGetColors(out IReadOnlyList? 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; + } + } +} diff --git a/S1API/Vehicles/VehicleColorCatalog.cs b/S1API/Vehicles/VehicleColorCatalog.cs new file mode 100644 index 00000000..17e18353 --- /dev/null +++ b/S1API/Vehicles/VehicleColorCatalog.cs @@ -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 +{ + /// + /// Provides read-only access to the base game's vehicle color metadata. + /// + public static class VehicleColorCatalog + { + private static readonly IReadOnlyList EmptyColors = + new ReadOnlyCollection( + Array.Empty()); + private static IVehicleColorMetadataProvider _provider = + new NativeVehicleColorMetadataProvider(); + + /// + /// 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. + /// + public static IReadOnlyList Colors => + GetColors(); + + /// + /// Gets metadata for a vehicle color. + /// + /// The vehicle color to look up. + /// + /// The color metadata, or when the color is unavailable + /// or the base game's vehicle color library is not ready. + /// + public static VehicleColorMetadata? GetColor(VehicleColor color) + { + TryGetColor(color, out VehicleColorMetadata? metadata); + return metadata; + } + + /// + /// Tries to get metadata for a vehicle color. + /// + /// The vehicle color to look up. + /// The resolved metadata when available. + /// when metadata was found. + 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 GetColors() + { + if (!_provider.TryGetColors(out IReadOnlyList? metadata) + || metadata == null + || metadata.Count == 0) + { + return EmptyColors; + } + + return new ReadOnlyCollection( + new List(metadata)); + } + } + + internal interface IVehicleColorMetadataProvider + { + bool TryGetColors(out IReadOnlyList? metadata); + + bool TryGetColor( + VehicleColor color, + out VehicleColorMetadata? metadata); + } + + internal sealed class NativeVehicleColorMetadataProvider : + IVehicleColorMetadataProvider + { + public bool TryGetColors(out IReadOnlyList? metadata) + { + metadata = null; + + try + { + if (!S1DevUtilities.Singleton + .InstanceExists) + { + return false; + } + + S1Vehicles.Modification.VehicleColors vehicleColors = + S1DevUtilities.Singleton + .Instance; + if (vehicleColors.colorLibrary == null) + return false; + + var colors = new List( + 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(colors); + return true; + } + catch + { + return false; + } + } + + public bool TryGetColor( + VehicleColor color, + out VehicleColorMetadata? metadata) + { + metadata = null; + + if (!TryGetColors(out IReadOnlyList? 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; + } + } +} diff --git a/S1API/Vehicles/VehicleColorMetadata.cs b/S1API/Vehicles/VehicleColorMetadata.cs new file mode 100644 index 00000000..175afa54 --- /dev/null +++ b/S1API/Vehicles/VehicleColorMetadata.cs @@ -0,0 +1,42 @@ +using UnityEngine; + +namespace S1API.Vehicles +{ + /// + /// Describes a vehicle color using the base game's display metadata. + /// + public sealed class VehicleColorMetadata + { + internal VehicleColorMetadata( + VehicleColor color, + string? displayName, + Color materialColor, + Color32 uiColor) + { + Color = color; + DisplayName = displayName; + MaterialColor = materialColor; + UIColor = uiColor; + } + + /// + /// Gets the vehicle color represented by this metadata. + /// + public VehicleColor Color { get; } + + /// + /// Gets the display name configured for this color in the base game. + /// + public string? DisplayName { get; } + + /// + /// Gets the color applied to vehicle materials. + /// + public Color MaterialColor { get; } + + /// + /// Gets the color used for this color in the base game UI. + /// + public Color32 UIColor { get; } + } +} diff --git a/S1API/docs/modules-overview.md b/S1API/docs/modules-overview.md index 70374e33..508bc762 100644 --- a/S1API/docs/modules-overview.md +++ b/S1API/docs/modules-overview.md @@ -158,6 +158,7 @@ Vehicle spawning and management. - `LandVehicle` - Land vehicle wrapper - `VehicleRegistry` - Vehicle registration - `VehicleColor` - Color configuration +- `VehicleColorCatalog` - Native vehicle color metadata - `ParkingAlignment` - Parking helpers **Documentation**: In development From fb53d87e96b94881ce99dd6b5d415eec98f5854f Mon Sep 17 00:00:00 2001 From: ifBars Date: Thu, 13 Aug 2026 21:29:25 -0700 Subject: [PATCH 2/2] test(Vehicles): keep IL2CPP catalog tests host-safe --- S1API.Tests/Vehicles/VehicleColorCatalogTests.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/S1API.Tests/Vehicles/VehicleColorCatalogTests.cs b/S1API.Tests/Vehicles/VehicleColorCatalogTests.cs index c6167289..3e89db3c 100644 --- a/S1API.Tests/Vehicles/VehicleColorCatalogTests.cs +++ b/S1API.Tests/Vehicles/VehicleColorCatalogTests.cs @@ -22,7 +22,11 @@ public void Dispose() 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", @@ -38,7 +42,9 @@ public void LookupsExposeS1ApiOwnedMetadata() Assert.Same(metadata, resolved); Assert.Equal("Dark Blue", resolved!.DisplayName); AssertColor(materialColor, resolved.MaterialColor); +#if !IL2CPPMELON Assert.Equal(uiColor, resolved.UIColor); +#endif } [Fact]