Skip to content
Open
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
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@

All notable changes to Chromatics are documented here.

## 4.3.33

- Fixed lighting stopping for the rest of the session when the Screen Capture base layer was in use and you returned to the title or character select screen.
- Yeelight, LIFX, Nanoleaf, and Alienware lights that are switched off or off the network at startup now log as device notices instead of errors.

## 4.3.32

- Improved the accuracy and speed of game-data reads on FFXIV patch 7.55, including inventory, chat log, job gauges, and player stats.

## 4.3.31

- Added support for FFXIV patch 7.55.
Expand Down
2 changes: 1 addition & 1 deletion Chromatics.Tests/Chromatics.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Sharlayan" Version="9.1.3" />
<PackageReference Include="Sharlayan" Version="9.2.0-prerelease.64" />
</ItemGroup>

</Project>
21 changes: 21 additions & 0 deletions Chromatics.Tests/Core/ScreenCaptureProcessorLifetimeTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using Chromatics.Layers;

namespace Chromatics.Tests.Core;

public class ScreenCaptureProcessorLifetimeTests
{
[Fact]
public void Instance_IsRebuiltAfterDispose()
{
// DisposeAll runs every time the player returns to the title screen and
// nulls the processor's surface. Handing the disposed object back out
// left the Screen Capture base layer attaching to a dead surface on
// every later tick, which killed lighting for the rest of the session.
var first = ScreenCaptureProcessor.Instance;
first.Dispose();

var second = ScreenCaptureProcessor.Instance;

Assert.NotSame(first, second);
}
}
53 changes: 53 additions & 0 deletions Chromatics.Tests/Helpers/NetworkFailureHelperTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
using System;
using System.IO;
using System.Net.Http;
using System.Net.Sockets;
using Chromatics.Helpers;

namespace Chromatics.Tests.Helpers;

public class NetworkFailureHelperTests
{
[Fact]
public void ConnectionRefused_IsUnreachable()
{
var ex = new SocketException((int)SocketError.ConnectionRefused);

Assert.True(NetworkFailureHelper.IsUnreachable(ex));
}

[Fact]
public void SocketFailureWrappedInIoException_IsUnreachable()
{
var ex = new IOException("write failed", new SocketException((int)SocketError.HostUnreachable));

Assert.True(NetworkFailureHelper.IsUnreachable(ex));
}

[Fact]
public void HttpAndTimeoutFailures_AreUnreachable()
{
Assert.True(NetworkFailureHelper.IsUnreachable(new HttpRequestException("no route")));
Assert.True(NetworkFailureHelper.IsUnreachable(new TimeoutException()));
Assert.True(NetworkFailureHelper.IsUnreachable(new TaskCanceledException()));
}

[Fact]
public void SocketFailureInsideAggregate_IsUnreachable()
{
var ex = new AggregateException(
new InvalidOperationException("unrelated"),
new SocketException((int)SocketError.TimedOut));

Assert.True(NetworkFailureHelper.IsUnreachable(ex));
}

[Fact]
public void ApplicationBugs_AreNotUnreachable()
{
Assert.False(NetworkFailureHelper.IsUnreachable(new NullReferenceException()));
Assert.False(NetworkFailureHelper.IsUnreachable(new InvalidOperationException()));
Assert.False(NetworkFailureHelper.IsUnreachable(
new InvalidOperationException("outer", new ArgumentOutOfRangeException())));
}
}
2 changes: 1 addition & 1 deletion Chromatics.Tests/Helpers/UpdateNotesTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ public void EmptyOrWhitespaceNotes_ReturnEmpty()
{
Assert.Equal(string.Empty, UpdateService.TrimNotesToOwnSection("", "4.3.26"));
Assert.Equal(string.Empty, UpdateService.TrimNotesToOwnSection(" ", "4.3.26"));
Assert.Equal(string.Empty, UpdateService.TrimNotesToOwnSection(null, "4.3.26"));
Assert.Equal(string.Empty, UpdateService.TrimNotesToOwnSection(null!, "4.3.26"));
}

[Fact]
Expand Down
4 changes: 2 additions & 2 deletions Chromatics/Chromatics.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<TargetFramework>net10.0-windows10.0.19041.0</TargetFramework>
<SupportedOSPlatformVersion>10.0.17763.0</SupportedOSPlatformVersion>
<StartupObject>Chromatics.Program</StartupObject>
<Version>4.3.31.0</Version>
<Version>4.3.33.0</Version>
<Authors>Danielle Thompson</Authors>
<!-- ApplicationManifest is conditional: local Debug + Release builds embed
app.manifest (no fusion-identity <msix> element) so VS debug runs and
Expand Down Expand Up @@ -135,7 +135,7 @@
<PackageReference Include="RGB.NET.Devices.Wooting" Version="3.2.0" />
<PackageReference Include="RGB.NET.HID" Version="3.2.0" />
<PackageReference Include="RGB.NET.Presets" Version="3.2.0" />
<PackageReference Include="Sharlayan" Version="9.1.3" />
<PackageReference Include="Sharlayan" Version="9.2.0-prerelease.64" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,10 @@ protected override IEnumerable<IRGBDevice> LoadDevices()
}
catch (Exception ex)
{
Logger.WriteConsole(LoggerTypes.Error,
$"[Alienware] failed to set up {def.Product}: {ex.Message}");
bool unreachable = Chromatics.Helpers.NetworkFailureHelper.IsUnreachable(ex);
Logger.WriteConsole(unreachable ? LoggerTypes.Devices : LoggerTypes.Error,
$"[Alienware] failed to set up {def.Product}: {ex.Message}",
forwardToSentry: !unreachable);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,10 @@ private async Task<IEnumerable<IRGBDevice>> LoadDevicesAsync()
}
catch (Exception ex)
{
Logger.WriteConsole(LoggerTypes.Error, $"[LIFX] failed to set up {def.Label}: {ex.Message}");
bool unreachable = Chromatics.Helpers.NetworkFailureHelper.IsUnreachable(ex);
Logger.WriteConsole(unreachable ? LoggerTypes.Devices : LoggerTypes.Error,
$"[LIFX] failed to set up {def.Label}: {ex.Message}",
forwardToSentry: !unreachable);
}
}

Expand Down Expand Up @@ -162,7 +165,10 @@ private async Task<Dictionary<string, IPEndPoint>> ResolveEndpointsAsync(IList<L
}
catch (Exception ex)
{
Logger.WriteConsole(LoggerTypes.Error, $"[LIFX] discovery sweep failed: {ex.Message}");
bool unreachable = Chromatics.Helpers.NetworkFailureHelper.IsUnreachable(ex);
Logger.WriteConsole(unreachable ? LoggerTypes.Devices : LoggerTypes.Error,
$"[LIFX] discovery sweep failed: {ex.Message}",
forwardToSentry: !unreachable);
}

return result;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,10 @@ private async Task<IEnumerable<IRGBDevice>> LoadDevicesAsync()
}
catch (Exception ex)
{
Logger.WriteConsole(LoggerTypes.Error, $"[Nanoleaf] failed to set up {def.Label}: {ex.Message}");
bool unreachable = Chromatics.Helpers.NetworkFailureHelper.IsUnreachable(ex);
Logger.WriteConsole(unreachable ? LoggerTypes.Devices : LoggerTypes.Error,
$"[Nanoleaf] failed to set up {def.Label}: {ex.Message}",
forwardToSentry: !unreachable);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,8 +129,10 @@ private async Task<IEnumerable<IRGBDevice>> LoadDevicesAsync()
}
catch (Exception ex)
{
Logger.WriteConsole(LoggerTypes.Error,
$"[Yeelight] failed to set up {def.Label}: {ex.Message}");
bool unreachable = Chromatics.Helpers.NetworkFailureHelper.IsUnreachable(ex);
Logger.WriteConsole(unreachable ? LoggerTypes.Devices : LoggerTypes.Error,
$"[Yeelight] failed to set up {def.Label}: {ex.Message}",
forwardToSentry: !unreachable);
}
}

Expand All @@ -154,7 +156,10 @@ private async Task<Dictionary<string, IPEndPoint>> ResolveEndpointsAsync(IList<Y
}
catch (Exception ex)
{
Logger.WriteConsole(LoggerTypes.Error, $"[Yeelight] discovery sweep failed: {ex.Message}");
bool unreachable = Chromatics.Helpers.NetworkFailureHelper.IsUnreachable(ex);
Logger.WriteConsole(unreachable ? LoggerTypes.Devices : LoggerTypes.Error,
$"[Yeelight] discovery sweep failed: {ex.Message}",
forwardToSentry: !unreachable);
}
return result;
}
Expand Down
42 changes: 42 additions & 0 deletions Chromatics/Helpers/NetworkFailureHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using System;
using System.IO;
using System.Net.Http;
using System.Net.Sockets;

namespace Chromatics.Helpers
{
public static class NetworkFailureHelper
{
/// <summary>
/// True when an exception describes a network device that could not be
/// reached, rather than a fault in Chromatics. Callers log these for the
/// user but keep them out of Sentry, where a bulb switched off at the
/// wall would otherwise read as an application error.
/// </summary>
public static bool IsUnreachable(Exception ex)
{
for (var current = ex; current != null; current = current.InnerException)
{
switch (current)
{
case SocketException:
case TimeoutException:
case HttpRequestException:
case IOException:
case OperationCanceledException:
return true;
}

if (current is AggregateException aggregate)
{
foreach (var inner in aggregate.InnerExceptions)
{
if (IsUnreachable(inner)) return true;
}
}
}

return false;
}
}
}
28 changes: 24 additions & 4 deletions Chromatics/Layers/BaseLayers/ScreenCapture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,34 @@ namespace Chromatics.Layers
{
public class ScreenCaptureProcessor : LayerProcessor
{
private static readonly ScreenCaptureProcessor _instance = new();
private static ScreenCaptureProcessor _instance;
private static ScreenCaptureExtension _screenCapture;

private ScreenCaptureProcessor() { }

public static ScreenCaptureProcessor Instance => _instance;
// Rebuilt on demand rather than held in a readonly field: DisposeAll
// runs every time the player returns to the title screen and nulls
// the cached surface, so the processor has to be replaceable or every
// later tick attaches its group to a dead surface.
public static ScreenCaptureProcessor Instance
{
get
{
if (_instance == null)
{
_instance = new ScreenCaptureProcessor();
}
return _instance;
}
}

public override void Process(IMappingLayer layer)
{
// Read once: Dispose runs from the UI thread at app exit and nulls
// the field mid-tick, so a second read here would attach to null.
var activeSurface = surface;
if (activeSurface == null) return;

if (RGBController.IsBaseLayerEffectRunning()) return;

var _layergroups = RGBController.GetLiveLayerGroups();
Expand All @@ -38,7 +57,7 @@ public override void Process(IMappingLayer layer)
}
else
{
layergroup = new ListLedGroup(surface, ledArray) { ZIndex = layer.zindex };
layergroup = new ListLedGroup(activeSurface, ledArray) { ZIndex = layer.zindex };
_layergroups[layer.layerID] = new[] { layergroup };
layergroup.Detach();
}
Expand All @@ -58,7 +77,7 @@ public override void Process(IMappingLayer layer)
: new SolidColorBrush(ColorHelper.ColorToRGBColor(System.Drawing.Color.Black));
}

layergroup.Attach(surface);
layergroup.Attach(activeSurface);
_init = true;
layer.requestUpdate = false;
}
Expand Down Expand Up @@ -109,6 +128,7 @@ protected override void Dispose(bool disposing)
_screenCapture = null;
}
base.Dispose(disposing);
_instance = null;
}
}
}
Loading