All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 2m9s
# Patchnotes 2.1.0 The changes in this update are more than just "patches". With a new UI, a new feature, and a bunch of bug fixes, improvements and a new member on the dev team, we thought this was more of a minor update. We would like to introduce @tsubasahane of MareCN to the team! We’re happy to work with them to bring Lightless and its features to the CN client as well as having another talented dev bring features and ideas to us. Speaking of which: # Location Sharing (Big shout out to @tsubasahane for bringing this feature) - Are you TIRED of scrambling to find the address of the venue you're in to share with your friends? We are introducing Location Sharing! An optional feature where you can share your location with direct pairs temporarily [30 minutes, 1 hour, 3 hours] minutes or until you turn it off for them. That's up to you! [#125](<#125>) [#49](<Lightless-Sync/LightlessServer#49>) - To share your location with a pair, click the three dots beside the pair and choose a duration to share with them. [#125](<#125>) [#49](<Lightless-Sync/LightlessServer#49>) - To view the location of someone who's shared with you, simply hover over the globe icon! [#125](<#125>) [#49](<Lightless-Sync/LightlessServer#49>) [1] # Model Optimization (Mesh Decimating) - This new option can automatically “simplify” incoming character meshes to help performance by reducing triangle counts. You choose how strong the reduction is (default/recommended is 80%). [#131](<#131>) - Decimation only kicks in when a mesh is above a certain triangle threshold, and only for the items that qualify for it and you selected for. [#131](<#131>) - Hair meshes is always excluded, since simplifying hair meshes is very prone to breaking. - You can find everything under Settings → Performance → Model Optimization. [#131](<#131>) + ** IF YOU HAVE USED DECIMATION IN TESTING, PLEASE CLEAR YOUR CACHE ❗ ** [2] # Animation (PAP) Validation (Safer animations) - Lightless now checks your currently animations to see if they work with your local skeleton/bone mod. If an animation matches, it’s included in what gets sent to other players. If it doesn’t, Lightless will skip it and write a warning to your log showing how many were skipped due to skeleton changes. Its defaulted to Unsafe (off). turn it on if you experience crashes from others users. [#131](<#131>) - Lightless also does the same kind of check for incoming animation files, to make sure they match the body/skeleton they were sent with. [#131](<#131>) - Because these checks can sometimes be a little picky, you can adjust how strict they are in Settings -> General -> Animation & Bones to reduce false positives. [#131](<#131>) # UI Changes (Thanks to @kyuwu for UI Changes) - The top part of the main screen has gotten a makeover. You can adjust the colors of the gradiant in the Color settings of Lightless. [#127](<#127>) [3] - Settings have gotten some changes as well to make this change more universal, and will use the same color settings. [#127](<#127>) - The particle effects of the gradient are toggleable in 'Settings -> UI -> Behavior' [#127](<#127>) - Instead of showing download/upload on bottom of Main UI, it will show VRAM usage and triangles with their optimization options next to it [#138](<#138>) # LightFinder / ShellFinder - UI Changes that follow our new design follow the color codes for the Gradient top as the main screen does. [#127](<#127>) [4] Co-authored-by: defnotken <itsdefnotken@gmail.com> Co-authored-by: azyges <aaaaaa@aaa.aaa> Co-authored-by: cake <admin@cakeandbanana.nl> Co-authored-by: Tsubasa <tsubasa@noreply.git.lightless-sync.org> Co-authored-by: choco <choco@patat.nl> Co-authored-by: celine <aaa@aaa.aaa> Co-authored-by: celine <celine@noreply.git.lightless-sync.org> Co-authored-by: Tsubasahane <wozaiha@gmail.com> Co-authored-by: cake <cake@noreply.git.lightless-sync.org> Reviewed-on: #123
570 lines
19 KiB
C#
570 lines
19 KiB
C#
using Dalamud.Game.Gui.Dtr;
|
|
using Dalamud.Game.Text.SeStringHandling;
|
|
using Dalamud.Game.Text.SeStringHandling.Payloads;
|
|
using Dalamud.Plugin.Services;
|
|
using LightlessSync.LightlessConfiguration;
|
|
using LightlessSync.LightlessConfiguration.Configurations;
|
|
using LightlessSync.Services;
|
|
using LightlessSync.Services.Mediator;
|
|
using LightlessSync.Services.ServerConfiguration;
|
|
using LightlessSync.Utils;
|
|
using LightlessSync.WebAPI;
|
|
using LightlessSync.WebAPI.SignalR.Utils;
|
|
using Microsoft.Extensions.Hosting;
|
|
using Microsoft.Extensions.Logging;
|
|
using System.Runtime.InteropServices;
|
|
using System.Text;
|
|
using LightlessSync.UI.Services;
|
|
using static LightlessSync.Services.PairRequestService;
|
|
using LightlessSync.Services.LightFinder;
|
|
|
|
namespace LightlessSync.UI;
|
|
|
|
public sealed class DtrEntry : IDisposable, IHostedService
|
|
{
|
|
private static readonly TimeSpan _localHashedCidCacheDuration = TimeSpan.FromMinutes(2);
|
|
private static readonly TimeSpan _localHashedCidErrorCooldown = TimeSpan.FromMinutes(1);
|
|
|
|
private readonly ApiController _apiController;
|
|
private readonly ServerConfigurationManager _serverManager;
|
|
private readonly CancellationTokenSource _cancellationTokenSource = new();
|
|
private readonly ConfigurationServiceBase<LightlessConfig> _configService;
|
|
private readonly IDtrBar _dtrBar;
|
|
private readonly Lazy<IDtrBarEntry> _statusEntry;
|
|
private readonly Lazy<IDtrBarEntry> _lightfinderEntry;
|
|
private readonly ILogger<DtrEntry> _logger;
|
|
private readonly LightFinderService _broadcastService;
|
|
private readonly LightFinderScannerService _broadcastScannerService;
|
|
private readonly LightlessMediator _lightlessMediator;
|
|
private readonly PairUiService _pairUiService;
|
|
private readonly PairRequestService _pairRequestService;
|
|
private readonly DalamudUtilService _dalamudUtilService;
|
|
private Task? _runTask;
|
|
private string? _statusText;
|
|
private string? _statusTooltip;
|
|
private Colors _statusColors;
|
|
private string? _lightfinderText;
|
|
private string? _lightfinderTooltip;
|
|
private Colors _lightfinderColors;
|
|
private readonly object _localHashedCidLock = new();
|
|
private string? _localHashedCid;
|
|
private DateTime _localHashedCidFetchedAt = DateTime.MinValue;
|
|
private DateTime _localHashedCidNextErrorLog = DateTime.MinValue;
|
|
private DateTime _pairRequestNextErrorLog = DateTime.MinValue;
|
|
private int _localHashedCidRefreshActive;
|
|
|
|
public DtrEntry(
|
|
ILogger<DtrEntry> logger,
|
|
IDtrBar dtrBar,
|
|
ConfigurationServiceBase<LightlessConfig> configService,
|
|
LightlessMediator lightlessMediator,
|
|
PairUiService pairUiService,
|
|
PairRequestService pairRequestService,
|
|
ApiController apiController,
|
|
ServerConfigurationManager serverManager,
|
|
LightFinderService broadcastService,
|
|
LightFinderScannerService broadcastScannerService,
|
|
DalamudUtilService dalamudUtilService)
|
|
{
|
|
_logger = logger;
|
|
_dtrBar = dtrBar;
|
|
_statusEntry = new(CreateStatusEntry);
|
|
_lightfinderEntry = new(CreateLightfinderEntry);
|
|
_configService = configService;
|
|
_lightlessMediator = lightlessMediator;
|
|
_pairUiService = pairUiService;
|
|
_pairRequestService = pairRequestService;
|
|
_apiController = apiController;
|
|
_serverManager = serverManager;
|
|
_broadcastService = broadcastService;
|
|
_broadcastScannerService = broadcastScannerService;
|
|
_dalamudUtilService = dalamudUtilService;
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
if (_statusEntry.IsValueCreated)
|
|
{
|
|
_logger.LogDebug("Disposing DtrEntry");
|
|
Clear();
|
|
_statusEntry.Value.Remove();
|
|
}
|
|
if (_lightfinderEntry.IsValueCreated)
|
|
_lightfinderEntry.Value.Remove();
|
|
}
|
|
|
|
public Task StartAsync(CancellationToken cancellationToken)
|
|
{
|
|
_logger.LogInformation("Starting DtrEntry");
|
|
_runTask = Task.Run(RunAsync, _cancellationTokenSource.Token);
|
|
_logger.LogInformation("Started DtrEntry");
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public async Task StopAsync(CancellationToken cancellationToken)
|
|
{
|
|
_cancellationTokenSource.Cancel();
|
|
|
|
if (_dalamudUtilService.IsOnFrameworkThread)
|
|
{
|
|
_logger.LogDebug("Skipping Lightfinder DTR wait on framework thread during shutdown.");
|
|
_cancellationTokenSource.Dispose();
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
if (_runTask != null)
|
|
await _runTask.ConfigureAwait(false);
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
_logger.LogInformation("Lightfinder operation was canceled.");
|
|
}
|
|
finally
|
|
{
|
|
_cancellationTokenSource.Dispose();
|
|
}
|
|
}
|
|
|
|
private void Clear()
|
|
{
|
|
HideStatusEntry();
|
|
HideLightfinderEntry();
|
|
}
|
|
|
|
private void HideStatusEntry()
|
|
{
|
|
if (_statusEntry.IsValueCreated && _statusEntry.Value.Shown)
|
|
{
|
|
_logger.LogInformation("Hiding status entry");
|
|
_statusEntry.Value.Shown = false;
|
|
}
|
|
|
|
_statusText = null;
|
|
_statusTooltip = null;
|
|
_statusColors = default;
|
|
}
|
|
|
|
private void HideLightfinderEntry()
|
|
{
|
|
if (_lightfinderEntry.IsValueCreated && _lightfinderEntry.Value.Shown)
|
|
{
|
|
_logger.LogInformation("Hiding Lightfinder entry");
|
|
_lightfinderEntry.Value.Shown = false;
|
|
}
|
|
|
|
_lightfinderText = null;
|
|
_lightfinderTooltip = null;
|
|
_lightfinderColors = default;
|
|
}
|
|
|
|
private IDtrBarEntry CreateStatusEntry()
|
|
{
|
|
_logger.LogTrace("Creating status DtrBar entry");
|
|
var entry = _dtrBar.Get("Lightless Sync");
|
|
entry.OnClick = interactionEvent => OnStatusEntryClick(interactionEvent);
|
|
|
|
return entry;
|
|
}
|
|
|
|
private IDtrBarEntry CreateLightfinderEntry()
|
|
{
|
|
_logger.LogTrace("Creating Lightfinder DtrBar entry");
|
|
var entry = _dtrBar.Get("Lightfinder");
|
|
entry.OnClick = interactionEvent => OnLightfinderEntryClick(interactionEvent);
|
|
return entry;
|
|
}
|
|
|
|
private void OnStatusEntryClick(DtrInteractionEvent interactionEvent)
|
|
{
|
|
if (interactionEvent.ClickType.Equals(MouseClickType.Left))
|
|
{
|
|
if (interactionEvent.ModifierKeys.HasFlag(ClickModifierKeys.Shift))
|
|
{
|
|
_lightlessMediator.Publish(new UiToggleMessage(typeof(SettingsUi)));
|
|
}
|
|
else
|
|
{
|
|
_lightlessMediator.Publish(new UiToggleMessage(typeof(CompactUi)));
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (interactionEvent.ClickType.Equals(MouseClickType.Right))
|
|
{
|
|
bool isConnectingOrConnected = _apiController.ServerState is ServerState.Connected or ServerState.Connecting or ServerState.Reconnecting;
|
|
|
|
if (_apiController.ServerState is not (ServerState.Reconnecting or ServerState.Disconnecting))
|
|
{
|
|
if (isConnectingOrConnected && !_serverManager.CurrentServer.FullPause)
|
|
{
|
|
_serverManager.CurrentServer.FullPause = true;
|
|
_serverManager.Save();
|
|
}
|
|
else if (!isConnectingOrConnected && _serverManager.CurrentServer.FullPause)
|
|
{
|
|
_serverManager.CurrentServer.FullPause = false;
|
|
_serverManager.Save();
|
|
}
|
|
|
|
_ = _apiController.CreateConnectionsAsync();
|
|
}
|
|
}
|
|
}
|
|
|
|
private void OnLightfinderEntryClick(DtrInteractionEvent interactionEvent)
|
|
{
|
|
if (!_configService.Current.ShowLightfinderInDtr)
|
|
return;
|
|
|
|
if (interactionEvent.ClickType.Equals(MouseClickType.Left))
|
|
{
|
|
_broadcastService.ToggleBroadcast();
|
|
}
|
|
}
|
|
|
|
private async Task RunAsync()
|
|
{
|
|
while (!_cancellationTokenSource.IsCancellationRequested)
|
|
{
|
|
await Task.Delay(1000, _cancellationTokenSource.Token).ConfigureAwait(false);
|
|
|
|
Update();
|
|
}
|
|
}
|
|
|
|
private void Update()
|
|
{
|
|
var config = _configService.Current;
|
|
|
|
if (!config.HasValidSetup())
|
|
{
|
|
HideStatusEntry();
|
|
HideLightfinderEntry();
|
|
return;
|
|
}
|
|
|
|
if (config.EnableDtrEntry)
|
|
UpdateStatusEntry(config);
|
|
else
|
|
HideStatusEntry();
|
|
|
|
if (config.ShowLightfinderInDtr)
|
|
UpdateLightfinderEntry(config);
|
|
else
|
|
HideLightfinderEntry();
|
|
}
|
|
|
|
private void UpdateStatusEntry(LightlessConfig config)
|
|
{
|
|
string text;
|
|
string tooltip;
|
|
Colors colors;
|
|
|
|
if (_apiController.IsConnected)
|
|
{
|
|
var snapshot = _pairUiService.GetSnapshot();
|
|
var visiblePairsQuery = snapshot.PairsByUid.Values.Where(x => x.IsVisible && !x.IsPaused);
|
|
var pairCount = visiblePairsQuery.Count();
|
|
text = $"\uE044 {pairCount}";
|
|
if (pairCount > 0)
|
|
{
|
|
var preferNote = config.PreferNoteInDtrTooltip;
|
|
var showUid = config.ShowUidInDtrTooltip;
|
|
|
|
IEnumerable<string> visiblePairs = showUid
|
|
? visiblePairsQuery.Select(x => string.Format("{0} ({1})", preferNote ? x.GetNote() ?? x.PlayerName : x.PlayerName, x.UserData.AliasOrUID))
|
|
: visiblePairsQuery.Select(x => string.Format("{0}", preferNote ? x.GetNote() ?? x.PlayerName : x.PlayerName));
|
|
|
|
tooltip = $"Lightless Sync: Connected{Environment.NewLine}----------{Environment.NewLine}{string.Join(Environment.NewLine, visiblePairs)}";
|
|
colors = config.DtrColorsPairsInRange;
|
|
}
|
|
else
|
|
{
|
|
tooltip = "Lightless Sync: Connected";
|
|
colors = config.DtrColorsDefault;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
text = "\uE044 \uE04C";
|
|
tooltip = "Lightless Sync: Not Connected";
|
|
colors = config.DtrColorsNotConnected;
|
|
}
|
|
|
|
if (!config.UseColorsInDtr)
|
|
colors = default;
|
|
|
|
var statusEntry = _statusEntry.Value;
|
|
if (!statusEntry.Shown)
|
|
{
|
|
_logger.LogInformation("Showing status entry");
|
|
statusEntry.Shown = true;
|
|
}
|
|
|
|
bool statusNeedsUpdate =
|
|
!string.Equals(text, _statusText, StringComparison.Ordinal) ||
|
|
!string.Equals(tooltip, _statusTooltip, StringComparison.Ordinal) ||
|
|
colors != _statusColors;
|
|
|
|
if (statusNeedsUpdate)
|
|
{
|
|
statusEntry.Text = BuildColoredSeString(text, colors);
|
|
statusEntry.Tooltip = tooltip;
|
|
_statusText = text;
|
|
_statusTooltip = tooltip;
|
|
_statusColors = colors;
|
|
}
|
|
}
|
|
|
|
private void UpdateLightfinderEntry(LightlessConfig config)
|
|
{
|
|
var lightfinderEntry = _lightfinderEntry.Value;
|
|
if (!lightfinderEntry.Shown)
|
|
{
|
|
_logger.LogInformation("Showing Lightfinder entry");
|
|
lightfinderEntry.Shown = true;
|
|
}
|
|
|
|
var indicator = BuildLightfinderIndicator();
|
|
var lightfinderText = indicator.Text ?? string.Empty;
|
|
var lightfinderColors = config.UseLightfinderColorsInDtr ? indicator.Colors : default;
|
|
var lightfinderTooltip = BuildLightfinderTooltip(indicator.Tooltip);
|
|
|
|
bool lightfinderNeedsUpdate =
|
|
!string.Equals(lightfinderText, _lightfinderText, StringComparison.Ordinal) ||
|
|
!string.Equals(lightfinderTooltip, _lightfinderTooltip, StringComparison.Ordinal) ||
|
|
lightfinderColors != _lightfinderColors;
|
|
|
|
if (lightfinderNeedsUpdate)
|
|
{
|
|
lightfinderEntry.Text = BuildColoredSeString(lightfinderText, lightfinderColors);
|
|
lightfinderEntry.Tooltip = lightfinderTooltip;
|
|
_lightfinderText = lightfinderText;
|
|
_lightfinderTooltip = lightfinderTooltip;
|
|
_lightfinderColors = lightfinderColors;
|
|
}
|
|
}
|
|
|
|
private string? GetLocalHashedCid()
|
|
{
|
|
var now = DateTime.UtcNow;
|
|
lock (_localHashedCidLock)
|
|
{
|
|
if (_localHashedCid is not null && now - _localHashedCidFetchedAt < _localHashedCidCacheDuration)
|
|
{
|
|
return _localHashedCid;
|
|
}
|
|
}
|
|
|
|
QueueLocalHashedCidRefresh();
|
|
|
|
lock (_localHashedCidLock)
|
|
{
|
|
return _localHashedCid;
|
|
}
|
|
}
|
|
|
|
private void QueueLocalHashedCidRefresh()
|
|
{
|
|
if (Interlocked.Exchange(ref _localHashedCidRefreshActive, 1) != 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_ = Task.Run(async () =>
|
|
{
|
|
try
|
|
{
|
|
var cid = _dalamudUtilService.GetCID();
|
|
var hashedCid = cid.ToString().GetHash256();
|
|
lock (_localHashedCidLock)
|
|
{
|
|
_localHashedCid = hashedCid;
|
|
_localHashedCidFetchedAt = DateTime.UtcNow;
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
var now = DateTime.UtcNow;
|
|
lock (_localHashedCidLock)
|
|
{
|
|
if (now >= _localHashedCidNextErrorLog)
|
|
{
|
|
_logger.LogDebug(ex, "Failed to refresh local hashed CID for Lightfinder DTR entry.");
|
|
_localHashedCidNextErrorLog = now + _localHashedCidErrorCooldown;
|
|
}
|
|
|
|
_localHashedCid = null;
|
|
_localHashedCidFetchedAt = now;
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
Interlocked.Exchange(ref _localHashedCidRefreshActive, 0);
|
|
}
|
|
});
|
|
}
|
|
|
|
private List<string> GetNearbyBroadcasts()
|
|
{
|
|
try
|
|
{
|
|
var localHashedCid = GetLocalHashedCid();
|
|
return [.. _broadcastScannerService
|
|
.GetActiveBroadcasts(string.IsNullOrEmpty(localHashedCid) ? null : localHashedCid)
|
|
.Select(b => _dalamudUtilService.FindPlayerByNameHash(b.Key).Name)];
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
var now = DateTime.UtcNow;
|
|
|
|
if (now >= _pairRequestNextErrorLog)
|
|
{
|
|
_logger.LogDebug(ex, "Failed to retrieve nearby broadcasts for Lightfinder DTR entry.");
|
|
_pairRequestNextErrorLog = now + _localHashedCidErrorCooldown;
|
|
}
|
|
|
|
return [];
|
|
}
|
|
}
|
|
|
|
private IReadOnlyList<PairRequestDisplay> GetPendingPairRequest()
|
|
{
|
|
try
|
|
{
|
|
return _pairRequestService.GetActiveRequests();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
var now = DateTime.UtcNow;
|
|
|
|
if (now >= _pairRequestNextErrorLog)
|
|
{
|
|
_logger.LogDebug(ex, "Failed to retrieve pair request count for Lightfinder DTR entry.");
|
|
_pairRequestNextErrorLog = now + _localHashedCidErrorCooldown;
|
|
}
|
|
|
|
return [];
|
|
}
|
|
}
|
|
|
|
private (string Text, Colors Colors, string Tooltip) BuildLightfinderIndicator()
|
|
{
|
|
var config = _configService.Current;
|
|
const string icon = "\uE048";
|
|
if (!_broadcastService.IsLightFinderAvailable)
|
|
{
|
|
return ($"{icon} --", SwapColorChannels(config.DtrColorsLightfinderUnavailable), "Lightfinder - Unavailable on this server.");
|
|
}
|
|
|
|
if (_broadcastService.IsBroadcasting)
|
|
{
|
|
switch (config.LightfinderDtrDisplayMode)
|
|
{
|
|
case LightfinderDtrDisplayMode.PendingPairRequests:
|
|
{
|
|
return FormatTooltip("Pending pair requests", GetPendingPairRequest().Select(x => x.DisplayName), icon, SwapColorChannels(config.DtrColorsLightfinderEnabled));
|
|
}
|
|
default:
|
|
{
|
|
return FormatTooltip("Nearby Lightfinder users", GetNearbyBroadcasts(), icon, SwapColorChannels(config.DtrColorsLightfinderEnabled));
|
|
}
|
|
}
|
|
}
|
|
|
|
var tooltip = new StringBuilder("Lightfinder - Disabled");
|
|
var colors = SwapColorChannels(config.DtrColorsLightfinderDisabled);
|
|
if (_broadcastService.RemainingCooldown is { } cooldown && cooldown > TimeSpan.Zero)
|
|
{
|
|
tooltip.AppendLine();
|
|
tooltip.Append("Cooldown: ").Append(Math.Ceiling(cooldown.TotalSeconds)).Append("s");
|
|
colors = SwapColorChannels(config.DtrColorsLightfinderCooldown);
|
|
}
|
|
|
|
return ($"{icon} OFF", colors, tooltip.ToString());
|
|
}
|
|
|
|
private static (string, Colors, string) FormatTooltip(string title, IEnumerable<string> names, string icon, Colors color)
|
|
{
|
|
var list = names.Where(x => !string.IsNullOrEmpty(x)).ToList();
|
|
var tooltip = new StringBuilder()
|
|
.Append($"Lightfinder - Enabled{Environment.NewLine}")
|
|
.Append($"{title}: {list.Count}{Environment.NewLine}")
|
|
.AppendJoin(Environment.NewLine, list)
|
|
.ToString();
|
|
|
|
return ($"{icon} {list.Count}", color, tooltip);
|
|
}
|
|
|
|
private static string BuildLightfinderTooltip(string baseTooltip)
|
|
{
|
|
var builder = new StringBuilder();
|
|
if (!string.IsNullOrWhiteSpace(baseTooltip))
|
|
builder.Append(baseTooltip.TrimEnd());
|
|
else
|
|
builder.Append("Lightfinder status unavailable.");
|
|
|
|
return builder.ToString().TrimEnd();
|
|
}
|
|
|
|
private static void AppendColoredSegment(SeStringBuilder builder, string? text, Colors colors)
|
|
{
|
|
if (string.IsNullOrEmpty(text))
|
|
return;
|
|
|
|
if (colors.Foreground != default)
|
|
builder.Add(BuildColorStartPayload(_colorTypeForeground, colors.Foreground));
|
|
if (colors.Glow != default)
|
|
builder.Add(BuildColorStartPayload(_colorTypeGlow, colors.Glow));
|
|
|
|
builder.AddText(text);
|
|
|
|
if (colors.Glow != default)
|
|
builder.Add(BuildColorEndPayload(_colorTypeGlow));
|
|
if (colors.Foreground != default)
|
|
builder.Add(BuildColorEndPayload(_colorTypeForeground));
|
|
}
|
|
|
|
#region Colored SeString
|
|
private const byte _colorTypeForeground = 0x13;
|
|
private const byte _colorTypeGlow = 0x14;
|
|
|
|
internal static Colors SwapColorChannels(Colors colors)
|
|
=> new(SwapColorComponent(colors.Foreground), SwapColorComponent(colors.Glow));
|
|
|
|
private static uint SwapColorComponent(uint color)
|
|
{
|
|
if (color == 0)
|
|
return 0;
|
|
|
|
return ((color & 0xFFu) << 16) | (color & 0xFF00u) | ((color >> 16) & 0xFFu);
|
|
}
|
|
|
|
private static SeString BuildColoredSeString(string text, Colors colors)
|
|
{
|
|
var ssb = new SeStringBuilder();
|
|
AppendColoredSegment(ssb, text, colors);
|
|
return ssb.Build();
|
|
}
|
|
|
|
private static RawPayload BuildColorStartPayload(byte colorType, uint color)
|
|
=> new(unchecked([
|
|
0x02,
|
|
colorType,
|
|
0x05,
|
|
0xF6,
|
|
byte.Max((byte)color, (byte)0x01),
|
|
byte.Max((byte)(color >> 8), (byte)0x01),
|
|
byte.Max((byte)(color >> 16), (byte)0x01),
|
|
0x03
|
|
]));
|
|
|
|
private static RawPayload BuildColorEndPayload(byte colorType)
|
|
=> new([0x02, colorType, 0x02, 0xEC, 0x03]);
|
|
|
|
[StructLayout(LayoutKind.Sequential)]
|
|
public readonly record struct Colors(uint Foreground = default, uint Glow = default);
|
|
#endregion
|
|
} |