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
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@ public void DuplicateAddsAreRemovedOneAtATimeInReverseRegistrationOrder()

tracker.Add(handler, "first");
tracker.Add(handler, "second");
Assert.False(tracker.IsEmpty);

Assert.True(tracker.TryTakeLast(handler, out string? second));
Assert.Equal("second", second);
Assert.True(tracker.TryTakeLast(handler, out string? first));
Assert.Equal("first", first);
Assert.False(tracker.TryTakeLast(handler, out _));
Assert.True(tracker.IsEmpty);
}

[Fact]
Expand All @@ -30,6 +32,7 @@ public void TakeAllReturnsEveryRegistrationAndClearsTheTracker()
tracker.Add(firstHandler, "first");
tracker.Add(firstHandler, "second");
tracker.Add(secondHandler, "third");
Assert.False(tracker.IsEmpty);

var registrations = tracker.TakeAll();

Expand All @@ -42,5 +45,6 @@ public void TakeAllReturnsEveryRegistrationAndClearsTheTracker()
registration.ManagedHandler.Equals(secondHandler) && registration.NativeHandler == "third");
Assert.False(tracker.TryTakeLast(firstHandler, out _));
Assert.False(tracker.TryTakeLast(secondHandler, out _));
Assert.True(tracker.IsEmpty);
}
}
37 changes: 37 additions & 0 deletions S1API.Tests/Temperature/TemperatureApiCompileFixture.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using System;
using S1API.Temperature;
using UnityEngine;

namespace S1API.Tests.Temperature;

internal static class TemperatureApiCompileFixture
{
internal static void ConfigureAndQuery(GameObject gameObject)
{
TemperatureEmitter emitter = TemperatureEmitter.GetOrAddComponent(gameObject);
emitter.SetTemperature(20f);
emitter.SetRange(5f);
emitter.SetPosition(Vector3.zero);

Action changed = () => { };
emitter.OnChanged += changed;
emitter.OnChanged -= changed;

TemperatureEmitter? existing = TemperatureEmitter.FromGameObject(gameObject);
TemperatureEmitterInfo[] emitters = { emitter.ToInfo() };
float temperature = TemperatureAlgorithm.GetTemperatureAtPoint(
ambientTemperature: 20f,
originPoint: Vector3.zero,
point: Vector3.one,
emitters: emitters);

_ = existing;
_ = temperature;
_ = TemperatureUtility.TemperatureSystemEnabled;
_ = TemperatureUtility.ToFahrenheit(20f);
_ = TemperatureUtility.FormatCelsiusTemperature(20f, decimalPoints: 1);
_ = TemperatureUtility.FormatFahrenheitTemperature(68f, decimalPoints: 1);
_ = TemperatureUtility.FormatTemperatureWithAppropriateUnit(20f);
_ = TemperatureUtility.NormalizeTemperature(20f);
}
}
128 changes: 128 additions & 0 deletions S1API.Tests/Temperature/TemperatureContractTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
using System.Reflection;
using S1API.Internal.Temperature;
using S1API.Temperature;
using UnityEngine;

namespace S1API.Tests.Temperature;

public sealed class TemperatureContractTests
{
#if MONOMELON
[Fact]
public void SnapshotUsesLinearRangeAndClampsToNativeEmitterBounds()
{
var minimum = new TemperatureEmitterInfo(-10f, -5f, Vector3.zero);
var maximum = new TemperatureEmitterInfo(50f, 200f, Vector3.one);

Assert.Equal(TemperatureEmitter.MinTemperature, minimum.Temperature);
Assert.Equal(TemperatureEmitter.MinRange, minimum.Range);
Assert.Equal(TemperatureEmitter.MaxTemperature, maximum.Temperature);
Assert.Equal(TemperatureEmitter.MaxRange, maximum.Range);
Assert.Null(typeof(TemperatureEmitterInfo).GetProperty("SqrRange", BindingFlags.Instance | BindingFlags.Public));
}

[Fact]
public void SnapshotRejectsNonFiniteValues()
{
foreach (float value in new[] { float.NaN, float.NegativeInfinity, float.PositiveInfinity })
{
Assert.Throws<ArgumentOutOfRangeException>(() =>
new TemperatureEmitterInfo(value, TemperatureEmitter.DefaultRange, Vector3.zero));
Assert.Throws<ArgumentOutOfRangeException>(() =>
new TemperatureEmitterInfo(TemperatureEmitter.DefaultAmbientTemperature, value, Vector3.zero));
}

Assert.Throws<ArgumentOutOfRangeException>(() =>
new TemperatureEmitterInfo(
TemperatureEmitter.DefaultAmbientTemperature,
TemperatureEmitter.DefaultRange,
new Vector3(float.NaN, 0f, 0f)));
}

[Fact]
public void QueryRejectsNullSnapshotsBeforeCallingTheNativeRuntime()
{
Assert.Throws<ArgumentNullException>(() => TemperatureAlgorithm.GetTemperatureAtPoint(
TemperatureEmitter.DefaultAmbientTemperature,
Vector3.zero,
Vector3.zero,
null!));
}
#endif

[Fact]
public void ManagedScalarValidationMatchesNativeEmitterBounds()
{
Assert.Equal(
TemperatureValidation.MinTemperature,
TemperatureValidation.ClampTemperature(-10f, "temperature"));
Assert.Equal(
TemperatureValidation.MaxTemperature,
TemperatureValidation.ClampTemperature(50f, "temperature"));
Assert.Equal(
TemperatureValidation.MinRange,
TemperatureValidation.ClampRange(-5f, "range"));
Assert.Equal(
TemperatureValidation.MaxRange,
TemperatureValidation.ClampRange(200f, "range"));

foreach (float value in new[] { float.NaN, float.NegativeInfinity, float.PositiveInfinity })
{
Assert.Throws<ArgumentOutOfRangeException>(() =>
TemperatureValidation.ClampTemperature(value, "temperature"));
Assert.Throws<ArgumentOutOfRangeException>(() =>
TemperatureValidation.ClampRange(value, "range"));
}
}

[Fact]
public void PublicSurfaceExposesManagedTemperatureContractsOnly()
{
Assert.NotNull(typeof(TemperatureUtility).GetProperty(
nameof(TemperatureUtility.TemperatureSystemEnabled),
BindingFlags.Public | BindingFlags.Static));
Assert.NotNull(typeof(TemperatureEmitter).GetMethod(
nameof(TemperatureEmitter.ToInfo),
BindingFlags.Public | BindingFlags.Instance));

Type[] publicTypes =
{
typeof(TemperatureEmitter),
typeof(TemperatureEmitterInfo),
typeof(TemperatureAlgorithm),
typeof(TemperatureUtility)
};
foreach (Type type in publicTypes)
{
foreach (MemberInfo member in type.GetMembers(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static))
{
foreach (Type exposedType in GetExposedTypes(member))
{
Assert.DoesNotContain("ScheduleOne", exposedType.Namespace ?? string.Empty, StringComparison.Ordinal);
Assert.DoesNotContain("Il2Cpp", exposedType.FullName ?? string.Empty, StringComparison.Ordinal);
}
}
}
}

private static IEnumerable<Type> GetExposedTypes(MemberInfo member)
{
switch (member)
{
case MethodInfo method:
yield return method.ReturnType;
foreach (ParameterInfo parameter in method.GetParameters())
yield return parameter.ParameterType;
break;
case PropertyInfo property:
yield return property.PropertyType;
break;
case FieldInfo field:
yield return field.FieldType;
break;
case EventInfo eventInfo:
yield return eventInfo.EventHandlerType!;
break;
}
}
}
45 changes: 45 additions & 0 deletions S1API/Internal/Temperature/TemperatureValidation.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using System;
using UnityEngine;

namespace S1API.Internal.Temperature
{
internal static class TemperatureValidation
{
internal const float DefaultAmbientTemperature = 20f;
internal const float MinTemperature = 0f;
internal const float MaxTemperature = 40f;
internal const float DefaultRange = 5f;
internal const float MinRange = 0.1f;
internal const float MaxRange = 100f;

internal static float ClampTemperature(float temperature, string parameterName)
{
EnsureFinite(temperature, parameterName);
return Clamp(temperature, MinTemperature, MaxTemperature);
}

internal static float ClampRange(float range, string parameterName)
{
EnsureFinite(range, parameterName);
return Clamp(range, MinRange, MaxRange);
}

internal static void EnsureFinite(Vector3 position, string parameterName)
{
if (!IsFinite(position.x) || !IsFinite(position.y) || !IsFinite(position.z))
throw new ArgumentOutOfRangeException(parameterName, "Vector components must be finite.");
}

internal static void EnsureFinite(float value, string parameterName)
{
if (!IsFinite(value))
throw new ArgumentOutOfRangeException(parameterName, "Value must be finite.");
}

private static float Clamp(float value, float minimum, float maximum) =>
value < minimum ? minimum : value > maximum ? maximum : value;

private static bool IsFinite(float value) =>
!float.IsNaN(value) && !float.IsInfinity(value);
}
}
3 changes: 3 additions & 0 deletions S1API/Internal/Utils/ManagedEventRegistrationTracker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ internal sealed class ManagedEventRegistrationTracker<TNativeHandler>
{
private readonly Dictionary<Delegate, List<TNativeHandler>> _registrations = new Dictionary<Delegate, List<TNativeHandler>>();

internal bool IsEmpty =>
_registrations.Count == 0;

internal void Add(Delegate managedHandler, TNativeHandler nativeHandler)
{
if (!_registrations.TryGetValue(managedHandler, out var nativeHandlers))
Expand Down
77 changes: 77 additions & 0 deletions S1API/Temperature/TemperatureAlgorithm.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
#if IL2CPPMELON
using Il2CppInterop.Runtime.InteropTypes.Arrays;
using S1Temperature = Il2CppScheduleOne.Temperature;
#elif MONOMELON
using S1Temperature = ScheduleOne.Temperature;
#endif

using System;
using S1API.Internal.Temperature;
using UnityEngine;

namespace S1API.Temperature
{
/// <summary>
/// Queries temperatures with the game's native temperature algorithm.
/// </summary>
public static class TemperatureAlgorithm
{
/// <summary>
/// Calculates the temperature at a world position from an ambient temperature and emitter snapshots.
/// </summary>
/// <param name="ambientTemperature">The ambient temperature in degrees Celsius.</param>
/// <param name="originPoint">
/// The world origin forwarded to the native API for signature compatibility. The current native implementation
/// evaluates world-space emitter and query positions directly and does not otherwise use this value.
/// </param>
/// <param name="point">The world position to query.</param>
/// <param name="emitters">The emitter snapshots to include in the calculation.</param>
/// <returns>The temperature at <paramref name="point"/> in degrees Celsius.</returns>
/// <exception cref="ArgumentNullException"><paramref name="emitters"/> is <c>null</c>.</exception>
/// <exception cref="ArgumentOutOfRangeException">A scalar or vector input is not finite.</exception>
/// <remarks>
/// This method does not discover scene emitters or register the supplied snapshots with a grid.
/// </remarks>
public static float GetTemperatureAtPoint(
float ambientTemperature,
Vector3 originPoint,
Vector3 point,
TemperatureEmitterInfo[] emitters)
{
if (emitters == null)
throw new ArgumentNullException(nameof(emitters));

TemperatureValidation.EnsureFinite(ambientTemperature, nameof(ambientTemperature));
TemperatureValidation.EnsureFinite(originPoint, nameof(originPoint));
TemperatureValidation.EnsureFinite(point, nameof(point));
#if IL2CPPMELON
var nativeEmitters = new Il2CppStructArray<S1Temperature.TemperatureEmitterInfo>(emitters.Length);
#else
var nativeEmitters = new S1Temperature.TemperatureEmitterInfo[emitters.Length];
#endif
for (int i = 0; i < emitters.Length; i++)
{
TemperatureEmitterInfo emitter = emitters[i];
float temperature = TemperatureValidation.ClampTemperature(
emitter.Temperature,
$"{nameof(emitters)}[{i}].{nameof(TemperatureEmitterInfo.Temperature)}");
float range = TemperatureValidation.ClampRange(
emitter.Range,
$"{nameof(emitters)}[{i}].{nameof(TemperatureEmitterInfo.Range)}");
TemperatureValidation.EnsureFinite(
emitter.Position,
$"{nameof(emitters)}[{i}].{nameof(TemperatureEmitterInfo.Position)}");
nativeEmitters[i] = new S1Temperature.TemperatureEmitterInfo(
temperature,
range * range,
emitter.Position);
}

return S1Temperature.TemperatureAlgorithm.GetTemperatureAtPoint(
ambientTemperature,
originPoint,
point,
nativeEmitters);
}
}
}
Loading
Loading