Compare commits

...

20 Commits

Author SHA1 Message Date
choco
812fbaec78 Merge remote-tracking branch 'origin/2.0.1' into animatedheader-fixes
# Conflicts:
#	LightlessSync/LightlessSync.csproj
#	LightlessSync/UI/Style/AnimatedHeader.cs
#	LightlessSync/UI/ZoneChatUi.cs
2025-12-21 20:54:33 +01:00
choco
e11135bb23 i love imgui 2025-12-21 20:49:13 +01:00
defnotken
be068ed6d1 Imgui Assertion fix 2025-12-21 12:50:51 -06:00
defnotken
3c3c8fd90b ver bump 2025-12-21 12:34:21 -06:00
835a0a637d 2.0.0 (#92)
All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 2m27s
2.0.0 Changes:

- Reworked shell finder UI with compact or list view with profile tags showing with the listing, allowing moderators to broadcast the syncshell as well to have it be used more.
- Reworked user list in syncshell admin screen to have filter visible and moved away from table to its own thing, allowing to copy uid/note/alias when clicking on the name.
- Reworked download bars and download box to make it look more modern, removed the jitter around, so it shouldn't vibrate around much.
- Chat has been added to the top menu, working in Zone or in Syncshells to be used there.
- Paired system has been revamped to make pausing and unpausing faster, and loading people should be faster as well.
- Moved to the internal object table to have faster load times for users; people should load in faster
- Compactor is running on a multi-threaded level instead of single-threaded; this should increase the speed of compacting files
- Nameplate Service has been reworked so it wouldn't use the nameplate handler anymore.
- Files can be resized when downloading to reduce load on users if they aren't compressed. (can be toggled to resize all).
- Penumbra Collections are now only made when people are visible, reducing the load on boot-up when having many syncshells in your list.
- Lightfinder plates have been moved away from using Nameplates, but will use an overlay.
- Main UI has been changed a bit with a gradient, and on hover will glow up now.
- Reworked Profile UI for Syncshell and Users to be more user-facing with more customizable items.
- Reworked Settings UI to look more modern.
- Performance should be better due to new systems that would dispose of the collections and better caching of items.

Co-authored-by: defnotken <itsdefnotken@gmail.com>
Co-authored-by: azyges <aaaaaa@aaa.aaa>
Co-authored-by: choco <choco@patat.nl>
Co-authored-by: cake <admin@cakeandbanana.nl>
Co-authored-by: Minmoose <KennethBohr@outlook.com>
Reviewed-on: #92
2025-12-21 17:19:34 +00:00
ad0254a812 meow moodles ipc meow 2025-12-21 20:37:23 +09:00
cake
1c4c73327f Added online filter like visible, seperated them for now. need to refactor. 2025-12-21 02:50:03 +01:00
cake
7b74fa7c4e Attempt to have a minute grace whenever collection get removed. 2025-12-21 01:55:26 +01:00
cake
2a670b3e64 Merge branch '2.0.0' of https://git.lightless-sync.org/Lightless-Sync/LightlessClient into 2.0.0 2025-12-21 01:20:43 +01:00
cake
f225989a00 Trunculate the broadcaster name in the grid view as it was overlapping into the Shell name 2025-12-21 01:20:34 +01:00
f2b17120fa implement focus fade for chat 2025-12-21 09:00:34 +09:00
79539e3db8 moderators will love this one 2025-12-21 07:55:41 +09:00
bcf6aea89d Merge branch '2.0.0' of https://git.lightless-sync.org/Lightless-Sync/LightlessClient into 2.0.0 2025-12-21 07:27:17 +09:00
779ff06981 goodbye lag 2025-12-21 07:26:37 +09:00
defnotken
54530cb16d fixing a typo for help message 2025-12-20 15:23:36 -06:00
03105e0755 fix log level 2025-12-21 04:28:36 +09:00
b99f68a891 collapsible texture details 2025-12-21 02:23:18 +09:00
7c7a98f770 This looks better 2025-12-21 01:19:17 +09:00
ab369d008e can drag chat tabs around as much as u want
syncshell tabs can use notes instead by rightclicking and prefering it
added some visibility settings (hide in combat, etc)
and cleaned up some of the ui
2025-12-21 01:17:00 +09:00
Minmoose
e5fa477eee Fix Brio IPC 2025-12-19 19:06:34 -06:00
32 changed files with 1333 additions and 206 deletions

View File

@@ -6,6 +6,7 @@ using LightlessSync.Utils;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.IO;
namespace LightlessSync.FileCache; namespace LightlessSync.FileCache;
@@ -21,6 +22,7 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
private CancellationTokenSource _scanCancellationTokenSource = new(); private CancellationTokenSource _scanCancellationTokenSource = new();
private readonly CancellationTokenSource _periodicCalculationTokenSource = new(); private readonly CancellationTokenSource _periodicCalculationTokenSource = new();
public static readonly IImmutableList<string> AllowedFileExtensions = [".mdl", ".tex", ".mtrl", ".tmb", ".pap", ".avfx", ".atex", ".sklb", ".eid", ".phyb", ".pbd", ".scd", ".skp", ".shpk", ".kdb"]; public static readonly IImmutableList<string> AllowedFileExtensions = [".mdl", ".tex", ".mtrl", ".tmb", ".pap", ".avfx", ".atex", ".sklb", ".eid", ".phyb", ".pbd", ".scd", ".skp", ".shpk", ".kdb"];
private static readonly HashSet<string> AllowedFileExtensionSet = new(AllowedFileExtensions, StringComparer.OrdinalIgnoreCase);
public CacheMonitor(ILogger<CacheMonitor> logger, IpcManager ipcManager, LightlessConfigService configService, public CacheMonitor(ILogger<CacheMonitor> logger, IpcManager ipcManager, LightlessConfigService configService,
FileCacheManager fileDbManager, LightlessMediator mediator, PerformanceCollectorService performanceCollector, DalamudUtilService dalamudUtil, FileCacheManager fileDbManager, LightlessMediator mediator, PerformanceCollectorService performanceCollector, DalamudUtilService dalamudUtil,
@@ -163,7 +165,7 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
{ {
Logger.LogTrace("Lightless FSW: FileChanged: {change} => {path}", e.ChangeType, e.FullPath); Logger.LogTrace("Lightless FSW: FileChanged: {change} => {path}", e.ChangeType, e.FullPath);
if (!AllowedFileExtensions.Any(ext => e.FullPath.EndsWith(ext, StringComparison.OrdinalIgnoreCase))) return; if (!HasAllowedExtension(e.FullPath)) return;
lock (_watcherChanges) lock (_watcherChanges)
{ {
@@ -207,7 +209,7 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
private void Fs_Changed(object sender, FileSystemEventArgs e) private void Fs_Changed(object sender, FileSystemEventArgs e)
{ {
if (Directory.Exists(e.FullPath)) return; if (Directory.Exists(e.FullPath)) return;
if (!AllowedFileExtensions.Any(ext => e.FullPath.EndsWith(ext, StringComparison.OrdinalIgnoreCase))) return; if (!HasAllowedExtension(e.FullPath)) return;
if (e.ChangeType is not (WatcherChangeTypes.Changed or WatcherChangeTypes.Deleted or WatcherChangeTypes.Created)) if (e.ChangeType is not (WatcherChangeTypes.Changed or WatcherChangeTypes.Deleted or WatcherChangeTypes.Created))
return; return;
@@ -231,7 +233,7 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
{ {
foreach (var file in directoryFiles) foreach (var file in directoryFiles)
{ {
if (!AllowedFileExtensions.Any(ext => file.EndsWith(ext, StringComparison.OrdinalIgnoreCase))) continue; if (!HasAllowedExtension(file)) continue;
var oldPath = file.Replace(e.FullPath, e.OldFullPath, StringComparison.OrdinalIgnoreCase); var oldPath = file.Replace(e.FullPath, e.OldFullPath, StringComparison.OrdinalIgnoreCase);
_watcherChanges.Remove(oldPath); _watcherChanges.Remove(oldPath);
@@ -243,7 +245,7 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
} }
else else
{ {
if (!AllowedFileExtensions.Any(ext => e.FullPath.EndsWith(ext, StringComparison.OrdinalIgnoreCase))) return; if (!HasAllowedExtension(e.FullPath)) return;
lock (_watcherChanges) lock (_watcherChanges)
{ {
@@ -263,6 +265,17 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
public FileSystemWatcher? PenumbraWatcher { get; private set; } public FileSystemWatcher? PenumbraWatcher { get; private set; }
public FileSystemWatcher? LightlessWatcher { get; private set; } public FileSystemWatcher? LightlessWatcher { get; private set; }
private static bool HasAllowedExtension(string path)
{
if (string.IsNullOrEmpty(path))
{
return false;
}
var extension = Path.GetExtension(path);
return !string.IsNullOrEmpty(extension) && AllowedFileExtensionSet.Contains(extension);
}
private async Task LightlessWatcherExecution() private async Task LightlessWatcherExecution()
{ {
_lightlessFswCts = _lightlessFswCts.CancelRecreate(); _lightlessFswCts = _lightlessFswCts.CancelRecreate();
@@ -606,7 +619,7 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
[ [
.. Directory.GetFiles(folder, "*.*", SearchOption.AllDirectories) .. Directory.GetFiles(folder, "*.*", SearchOption.AllDirectories)
.AsParallel() .AsParallel()
.Where(f => AllowedFileExtensions.Any(e => f.EndsWith(e, StringComparison.OrdinalIgnoreCase)) .Where(f => HasAllowedExtension(f)
&& !f.Contains(@"\bg\", StringComparison.OrdinalIgnoreCase) && !f.Contains(@"\bg\", StringComparison.OrdinalIgnoreCase)
&& !f.Contains(@"\bgcommon\", StringComparison.OrdinalIgnoreCase) && !f.Contains(@"\bgcommon\", StringComparison.OrdinalIgnoreCase)
&& !f.Contains(@"\ui\", StringComparison.OrdinalIgnoreCase)), && !f.Contains(@"\ui\", StringComparison.OrdinalIgnoreCase)),

View File

@@ -372,6 +372,9 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
private void HandleActorTracked(ActorObjectService.ActorDescriptor descriptor) private void HandleActorTracked(ActorObjectService.ActorDescriptor descriptor)
{ {
if (descriptor.IsInGpose)
return;
if (!TryResolveObjectKind(descriptor, out var resolvedKind)) if (!TryResolveObjectKind(descriptor, out var resolvedKind))
return; return;

View File

@@ -13,7 +13,7 @@ namespace LightlessSync.Interop.Ipc;
public sealed class IpcCallerBrio : IpcServiceBase public sealed class IpcCallerBrio : IpcServiceBase
{ {
private static readonly IpcServiceDescriptor BrioDescriptor = new("Brio", "Brio", new Version(3, 0, 0, 0)); private static readonly IpcServiceDescriptor BrioDescriptor = new("Brio", "Brio", new Version(0, 0, 0, 0));
private readonly ILogger<IpcCallerBrio> _logger; private readonly ILogger<IpcCallerBrio> _logger;
private readonly DalamudUtilService _dalamudUtilService; private readonly DalamudUtilService _dalamudUtilService;
@@ -144,7 +144,7 @@ public sealed class IpcCallerBrio : IpcServiceBase
try try
{ {
var version = _apiVersion.Invoke(); var version = _apiVersion.Invoke();
return version.Item1 == 3 && version.Item2 >= 0 return version.Breaking == 3 && version.Feature >= 0
? IpcConnectionState.Available ? IpcConnectionState.Available
: IpcConnectionState.VersionMismatch; : IpcConnectionState.VersionMismatch;
} }

View File

@@ -1,5 +1,4 @@
using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Plugin;
using Dalamud.Plugin;
using Dalamud.Plugin.Ipc; using Dalamud.Plugin.Ipc;
using LightlessSync.Interop.Ipc.Framework; using LightlessSync.Interop.Ipc.Framework;
using LightlessSync.Services; using LightlessSync.Services;
@@ -13,7 +12,7 @@ public sealed class IpcCallerMoodles : IpcServiceBase
private static readonly IpcServiceDescriptor MoodlesDescriptor = new("Moodles", "Moodles", new Version(0, 0, 0, 0)); private static readonly IpcServiceDescriptor MoodlesDescriptor = new("Moodles", "Moodles", new Version(0, 0, 0, 0));
private readonly ICallGateSubscriber<int> _moodlesApiVersion; private readonly ICallGateSubscriber<int> _moodlesApiVersion;
private readonly ICallGateSubscriber<IPlayerCharacter, object> _moodlesOnChange; private readonly ICallGateSubscriber<nint, object> _moodlesOnChange;
private readonly ICallGateSubscriber<nint, string> _moodlesGetStatus; private readonly ICallGateSubscriber<nint, string> _moodlesGetStatus;
private readonly ICallGateSubscriber<nint, string, object> _moodlesSetStatus; private readonly ICallGateSubscriber<nint, string, object> _moodlesSetStatus;
private readonly ICallGateSubscriber<nint, object> _moodlesRevertStatus; private readonly ICallGateSubscriber<nint, object> _moodlesRevertStatus;
@@ -29,7 +28,7 @@ public sealed class IpcCallerMoodles : IpcServiceBase
_lightlessMediator = lightlessMediator; _lightlessMediator = lightlessMediator;
_moodlesApiVersion = pi.GetIpcSubscriber<int>("Moodles.Version"); _moodlesApiVersion = pi.GetIpcSubscriber<int>("Moodles.Version");
_moodlesOnChange = pi.GetIpcSubscriber<IPlayerCharacter, object>("Moodles.StatusManagerModified"); _moodlesOnChange = pi.GetIpcSubscriber<nint, object>("Moodles.StatusManagerModified");
_moodlesGetStatus = pi.GetIpcSubscriber<nint, string>("Moodles.GetStatusManagerByPtrV2"); _moodlesGetStatus = pi.GetIpcSubscriber<nint, string>("Moodles.GetStatusManagerByPtrV2");
_moodlesSetStatus = pi.GetIpcSubscriber<nint, string, object>("Moodles.SetStatusManagerByPtrV2"); _moodlesSetStatus = pi.GetIpcSubscriber<nint, string, object>("Moodles.SetStatusManagerByPtrV2");
_moodlesRevertStatus = pi.GetIpcSubscriber<nint, object>("Moodles.ClearStatusManagerByPtrV2"); _moodlesRevertStatus = pi.GetIpcSubscriber<nint, object>("Moodles.ClearStatusManagerByPtrV2");
@@ -39,9 +38,9 @@ public sealed class IpcCallerMoodles : IpcServiceBase
CheckAPI(); CheckAPI();
} }
private void OnMoodlesChange(IPlayerCharacter character) private void OnMoodlesChange(nint address)
{ {
_lightlessMediator.Publish(new MoodlesMessage(character.Address)); _lightlessMediator.Publish(new MoodlesMessage(address));
} }
protected override void Dispose(bool disposing) protected override void Dispose(bool disposing)
@@ -107,7 +106,7 @@ public sealed class IpcCallerMoodles : IpcServiceBase
try try
{ {
return _moodlesApiVersion.InvokeFunc() == 3 return _moodlesApiVersion.InvokeFunc() >= 4
? IpcConnectionState.Available ? IpcConnectionState.Available
: IpcConnectionState.VersionMismatch; : IpcConnectionState.VersionMismatch;
} }

View File

@@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
namespace LightlessSync.LightlessConfiguration.Configurations; namespace LightlessSync.LightlessConfiguration.Configurations;
@@ -10,7 +11,16 @@ public sealed class ChatConfig : ILightlessConfiguration
public bool ShowRulesOverlayOnOpen { get; set; } = true; public bool ShowRulesOverlayOnOpen { get; set; } = true;
public bool ShowMessageTimestamps { get; set; } = true; public bool ShowMessageTimestamps { get; set; } = true;
public float ChatWindowOpacity { get; set; } = .97f; public float ChatWindowOpacity { get; set; } = .97f;
public bool FadeWhenUnfocused { get; set; } = false;
public float UnfocusedWindowOpacity { get; set; } = 0.6f;
public bool IsWindowPinned { get; set; } = false; public bool IsWindowPinned { get; set; } = false;
public bool AutoOpenChatOnPluginLoad { get; set; } = false; public bool AutoOpenChatOnPluginLoad { get; set; } = false;
public float ChatFontScale { get; set; } = 1.0f; public float ChatFontScale { get; set; } = 1.0f;
public bool HideInCombat { get; set; } = false;
public bool HideInDuty { get; set; } = false;
public bool ShowWhenUiHidden { get; set; } = true;
public bool ShowInCutscenes { get; set; } = true;
public bool ShowInGpose { get; set; } = true;
public List<string> ChannelOrder { get; set; } = new();
public Dictionary<string, bool> PreferNotesForChannels { get; set; } = new(StringComparer.Ordinal);
} }

View File

@@ -49,7 +49,8 @@ public class LightlessConfig : ILightlessConfiguration
public int DownloadSpeedLimitInBytes { get; set; } = 0; public int DownloadSpeedLimitInBytes { get; set; } = 0;
public DownloadSpeeds DownloadSpeedType { get; set; } = DownloadSpeeds.MBps; public DownloadSpeeds DownloadSpeedType { get; set; } = DownloadSpeeds.MBps;
public bool PreferNotesOverNamesForVisible { get; set; } = false; public bool PreferNotesOverNamesForVisible { get; set; } = false;
public VisiblePairSortMode VisiblePairSortMode { get; set; } = VisiblePairSortMode.Default; public VisiblePairSortMode VisiblePairSortMode { get; set; } = VisiblePairSortMode.Alphabetical;
public OnlinePairSortMode OnlinePairSortMode { get; set; } = OnlinePairSortMode.Alphabetical;
public float ProfileDelay { get; set; } = 1.5f; public float ProfileDelay { get; set; } = 1.5f;
public bool ProfilePopoutRight { get; set; } = false; public bool ProfilePopoutRight { get; set; } = false;
public bool ProfilesAllowNsfw { get; set; } = false; public bool ProfilesAllowNsfw { get; set; } = false;

View File

@@ -3,7 +3,7 @@
<PropertyGroup> <PropertyGroup>
<Authors></Authors> <Authors></Authors>
<Company></Company> <Company></Company>
<Version>2.0.0</Version> <Version>2.0.1</Version>
<Description></Description> <Description></Description>
<Copyright></Copyright> <Copyright></Copyright>
<PackageProjectUrl>https://github.com/Light-Public-Syncshells/LightlessClient</PackageProjectUrl> <PackageProjectUrl>https://github.com/Light-Public-Syncshells/LightlessClient</PackageProjectUrl>

View File

@@ -119,6 +119,7 @@ public class PlayerDataFactory
CharacterDataFragment fragment = objectKind == ObjectKind.Player ? new CharacterDataFragmentPlayer() : new(); CharacterDataFragment fragment = objectKind == ObjectKind.Player ? new CharacterDataFragmentPlayer() : new();
_logger.LogDebug("Building character data for {obj}", playerRelatedObject); _logger.LogDebug("Building character data for {obj}", playerRelatedObject);
var logDebug = _logger.IsEnabled(LogLevel.Debug);
// wait until chara is not drawing and present so nothing spontaneously explodes // wait until chara is not drawing and present so nothing spontaneously explodes
await _dalamudUtil.WaitWhileCharacterIsDrawing(_logger, playerRelatedObject, Guid.NewGuid(), 30000, ct: ct).ConfigureAwait(false); await _dalamudUtil.WaitWhileCharacterIsDrawing(_logger, playerRelatedObject, Guid.NewGuid(), 30000, ct: ct).ConfigureAwait(false);
@@ -132,11 +133,6 @@ public class PlayerDataFactory
ct.ThrowIfCancellationRequested(); ct.ThrowIfCancellationRequested();
Dictionary<string, List<ushort>>? boneIndices =
objectKind != ObjectKind.Player
? null
: await _dalamudUtil.RunOnFrameworkThread(() => _modelAnalyzer.GetSkeletonBoneIndices(playerRelatedObject)).ConfigureAwait(false);
DateTime start = DateTime.UtcNow; DateTime start = DateTime.UtcNow;
// penumbra call, it's currently broken // penumbra call, it's currently broken
@@ -154,12 +150,22 @@ public class PlayerDataFactory
ct.ThrowIfCancellationRequested(); ct.ThrowIfCancellationRequested();
if (logDebug)
{
_logger.LogDebug("== Static Replacements =="); _logger.LogDebug("== Static Replacements ==");
foreach (var replacement in fragment.FileReplacements.Where(i => i.HasFileReplacement).OrderBy(i => i.GamePaths.First(), StringComparer.OrdinalIgnoreCase)) foreach (var replacement in fragment.FileReplacements.Where(i => i.HasFileReplacement).OrderBy(i => i.GamePaths.First(), StringComparer.OrdinalIgnoreCase))
{ {
_logger.LogDebug("=> {repl}", replacement); _logger.LogDebug("=> {repl}", replacement);
ct.ThrowIfCancellationRequested(); ct.ThrowIfCancellationRequested();
} }
}
else
{
foreach (var replacement in fragment.FileReplacements.Where(i => i.HasFileReplacement))
{
ct.ThrowIfCancellationRequested();
}
}
await _transientResourceManager.WaitForRecording(ct).ConfigureAwait(false); await _transientResourceManager.WaitForRecording(ct).ConfigureAwait(false);
@@ -190,12 +196,22 @@ public class PlayerDataFactory
var transientPaths = ManageSemiTransientData(objectKind); var transientPaths = ManageSemiTransientData(objectKind);
var resolvedTransientPaths = await GetFileReplacementsFromPaths(transientPaths, new HashSet<string>(StringComparer.Ordinal)).ConfigureAwait(false); var resolvedTransientPaths = await GetFileReplacementsFromPaths(transientPaths, new HashSet<string>(StringComparer.Ordinal)).ConfigureAwait(false);
if (logDebug)
{
_logger.LogDebug("== Transient Replacements =="); _logger.LogDebug("== Transient Replacements ==");
foreach (var replacement in resolvedTransientPaths.Select(c => new FileReplacement([.. c.Value], c.Key)).OrderBy(f => f.ResolvedPath, StringComparer.Ordinal)) foreach (var replacement in resolvedTransientPaths.Select(c => new FileReplacement([.. c.Value], c.Key)).OrderBy(f => f.ResolvedPath, StringComparer.Ordinal))
{ {
_logger.LogDebug("=> {repl}", replacement); _logger.LogDebug("=> {repl}", replacement);
fragment.FileReplacements.Add(replacement); fragment.FileReplacements.Add(replacement);
} }
}
else
{
foreach (var replacement in resolvedTransientPaths.Select(c => new FileReplacement([.. c.Value], c.Key)))
{
fragment.FileReplacements.Add(replacement);
}
}
// clean up all semi transient resources that don't have any file replacement (aka null resolve) // clean up all semi transient resources that don't have any file replacement (aka null resolve)
_transientResourceManager.CleanUpSemiTransientResources(objectKind, [.. fragment.FileReplacements]); _transientResourceManager.CleanUpSemiTransientResources(objectKind, [.. fragment.FileReplacements]);
@@ -252,12 +268,27 @@ public class PlayerDataFactory
ct.ThrowIfCancellationRequested(); ct.ThrowIfCancellationRequested();
Dictionary<string, List<ushort>>? boneIndices = null;
var hasPapFiles = false;
if (objectKind == ObjectKind.Player)
{
hasPapFiles = fragment.FileReplacements.Any(f =>
!f.IsFileSwap && f.GamePaths.First().EndsWith("pap", StringComparison.OrdinalIgnoreCase));
if (hasPapFiles)
{
boneIndices = await _dalamudUtil.RunOnFrameworkThread(() => _modelAnalyzer.GetSkeletonBoneIndices(playerRelatedObject)).ConfigureAwait(false);
}
}
if (objectKind == ObjectKind.Player) if (objectKind == ObjectKind.Player)
{ {
try try
{
if (hasPapFiles)
{ {
await VerifyPlayerAnimationBones(boneIndices, (fragment as CharacterDataFragmentPlayer)!, ct).ConfigureAwait(false); await VerifyPlayerAnimationBones(boneIndices, (fragment as CharacterDataFragmentPlayer)!, ct).ConfigureAwait(false);
} }
}
catch (OperationCanceledException e) catch (OperationCanceledException e)
{ {
_logger.LogDebug(e, "Cancelled during player animation verification"); _logger.LogDebug(e, "Cancelled during player animation verification");
@@ -278,12 +309,16 @@ public class PlayerDataFactory
{ {
if (boneIndices == null) return; if (boneIndices == null) return;
if (_logger.IsEnabled(LogLevel.Debug))
{
foreach (var kvp in boneIndices) foreach (var kvp in boneIndices)
{ {
_logger.LogDebug("Found {skellyname} ({idx} bone indices) on player: {bones}", kvp.Key, kvp.Value.Any() ? kvp.Value.Max() : 0, string.Join(',', kvp.Value)); _logger.LogDebug("Found {skellyname} ({idx} bone indices) on player: {bones}", kvp.Key, kvp.Value.Any() ? kvp.Value.Max() : 0, string.Join(',', kvp.Value));
} }
}
if (boneIndices.All(u => u.Value.Count == 0)) return; var maxPlayerBoneIndex = boneIndices.SelectMany(kvp => kvp.Value).DefaultIfEmpty().Max();
if (maxPlayerBoneIndex <= 0) return;
int noValidationFailed = 0; int noValidationFailed = 0;
foreach (var file in fragment.FileReplacements.Where(f => !f.IsFileSwap && f.GamePaths.First().EndsWith("pap", StringComparison.OrdinalIgnoreCase)).ToList()) foreach (var file in fragment.FileReplacements.Where(f => !f.IsFileSwap && f.GamePaths.First().EndsWith("pap", StringComparison.OrdinalIgnoreCase)).ToList())
@@ -303,12 +338,13 @@ public class PlayerDataFactory
_logger.LogDebug("Verifying bone indices for {path}, found {x} skeletons", file.ResolvedPath, skeletonIndices.Count); _logger.LogDebug("Verifying bone indices for {path}, found {x} skeletons", file.ResolvedPath, skeletonIndices.Count);
foreach (var boneCount in skeletonIndices.Select(k => k).ToList()) foreach (var boneCount in skeletonIndices)
{ {
if (boneCount.Value.Max() > boneIndices.SelectMany(b => b.Value).Max()) var maxAnimationIndex = boneCount.Value.DefaultIfEmpty().Max();
if (maxAnimationIndex > maxPlayerBoneIndex)
{ {
_logger.LogWarning("Found more bone indices on the animation {path} skeleton {skl} (max indice {idx}) than on any player related skeleton (max indice {idx2})", _logger.LogWarning("Found more bone indices on the animation {path} skeleton {skl} (max indice {idx}) than on any player related skeleton (max indice {idx2})",
file.ResolvedPath, boneCount.Key, boneCount.Value.Max(), boneIndices.SelectMany(b => b.Value).Max()); file.ResolvedPath, boneCount.Key, maxAnimationIndex, maxPlayerBoneIndex);
validationFailed = true; validationFailed = true;
break; break;
} }

View File

@@ -1,12 +1,12 @@
using LightlessSync.API.Data; using LightlessSync.API.Data;
namespace LightlessSync.PlayerData.Pairs; namespace LightlessSync.PlayerData.Pairs;
/// <summary> /// <summary>
/// orchestrates the lifecycle of a paired character /// orchestrates the lifecycle of a paired character
/// </summary> /// </summary>
public interface IPairHandlerAdapter : IDisposable, IPairPerformanceSubject public interface IPairHandlerAdapter : IDisposable, IPairPerformanceSubject
{ {
new string Ident { get; } new string Ident { get; }
bool Initialized { get; } bool Initialized { get; }
bool IsVisible { get; } bool IsVisible { get; }
@@ -25,6 +25,8 @@ public interface IPairHandlerAdapter : IDisposable, IPairPerformanceSubject
bool IsDownloading { get; } bool IsDownloading { get; }
int PendingDownloadCount { get; } int PendingDownloadCount { get; }
int ForbiddenDownloadCount { get; } int ForbiddenDownloadCount { get; }
DateTime? InvisibleSinceUtc { get; }
DateTime? VisibilityEvictionDueAtUtc { get; }
void Initialize(); void Initialize();
void ApplyData(CharacterData data); void ApplyData(CharacterData data);
@@ -33,4 +35,4 @@ public interface IPairHandlerAdapter : IDisposable, IPairPerformanceSubject
void LoadCachedCharacterData(CharacterData data); void LoadCachedCharacterData(CharacterData data);
void SetUploading(bool uploading); void SetUploading(bool uploading);
void SetPaused(bool paused); void SetPaused(bool paused);
} }

View File

@@ -194,9 +194,13 @@ public class Pair
{ {
var handler = TryGetHandler(); var handler = TryGetHandler();
if (handler is null) if (handler is null)
{
return PairDebugInfo.Empty; return PairDebugInfo.Empty;
}
var now = DateTime.UtcNow;
var dueAt = handler.VisibilityEvictionDueAtUtc;
var remainingSeconds = dueAt.HasValue
? Math.Max(0, (dueAt.Value - now).TotalSeconds)
: (double?)null;
return new PairDebugInfo( return new PairDebugInfo(
true, true,
@@ -206,6 +210,9 @@ public class Pair
handler.LastDataReceivedAt, handler.LastDataReceivedAt,
handler.LastApplyAttemptAt, handler.LastApplyAttemptAt,
handler.LastSuccessfulApplyAt, handler.LastSuccessfulApplyAt,
handler.InvisibleSinceUtc,
handler.VisibilityEvictionDueAtUtc,
remainingSeconds,
handler.LastFailureReason, handler.LastFailureReason,
handler.LastBlockingConditions, handler.LastBlockingConditions,
handler.IsApplying, handler.IsApplying,

View File

@@ -8,6 +8,9 @@ public sealed record PairDebugInfo(
DateTime? LastDataReceivedAt, DateTime? LastDataReceivedAt,
DateTime? LastApplyAttemptAt, DateTime? LastApplyAttemptAt,
DateTime? LastSuccessfulApplyAt, DateTime? LastSuccessfulApplyAt,
DateTime? InvisibleSinceUtc,
DateTime? VisibilityEvictionDueAtUtc,
double? VisibilityEvictionRemainingSeconds,
string? LastFailureReason, string? LastFailureReason,
IReadOnlyList<string> BlockingConditions, IReadOnlyList<string> BlockingConditions,
bool IsApplying, bool IsApplying,
@@ -24,6 +27,9 @@ public sealed record PairDebugInfo(
null, null,
null, null,
null, null,
null,
null,
null,
Array.Empty<string>(), Array.Empty<string>(),
false, false,
false, false,

View File

@@ -70,7 +70,14 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
private DateTime? _lastSuccessfulApplyAt; private DateTime? _lastSuccessfulApplyAt;
private string? _lastFailureReason; private string? _lastFailureReason;
private IReadOnlyList<string> _lastBlockingConditions = Array.Empty<string>(); private IReadOnlyList<string> _lastBlockingConditions = Array.Empty<string>();
private readonly object _visibilityGraceGate = new();
private CancellationTokenSource? _visibilityGraceCts;
private static readonly TimeSpan VisibilityEvictionGrace = TimeSpan.FromMinutes(1);
private DateTime? _invisibleSinceUtc;
private DateTime? _visibilityEvictionDueAtUtc;
public DateTime? InvisibleSinceUtc => _invisibleSinceUtc;
public DateTime? VisibilityEvictionDueAtUtc => _visibilityEvictionDueAtUtc;
public string Ident { get; } public string Ident { get; }
public bool Initialized { get; private set; } public bool Initialized { get; private set; }
public bool ScheduledForDeletion { get; set; } public bool ScheduledForDeletion { get; set; }
@@ -80,18 +87,32 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
get => _isVisible; get => _isVisible;
private set private set
{ {
if (_isVisible != value) if (_isVisible == value) return;
{
_isVisible = value; _isVisible = value;
if (!_isVisible) if (!_isVisible)
{ {
DisableSync(); DisableSync();
ResetPenumbraCollection(reason: "VisibilityLost");
_invisibleSinceUtc = DateTime.UtcNow;
_visibilityEvictionDueAtUtc = _invisibleSinceUtc.Value.Add(VisibilityEvictionGrace);
StartVisibilityGraceTask();
} }
else if (_charaHandler is not null && _charaHandler.Address != nint.Zero) else
{ {
CancelVisibilityGraceTask();
_invisibleSinceUtc = null;
_visibilityEvictionDueAtUtc = null;
ScheduledForDeletion = false;
if (_charaHandler is not null && _charaHandler.Address != nint.Zero)
_ = EnsurePenumbraCollection(); _ = EnsurePenumbraCollection();
} }
var user = GetPrimaryUserData(); var user = GetPrimaryUserData();
Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter), Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter),
EventSeverity.Informational, "User Visibility Changed, now: " + (_isVisible ? "Is Visible" : "Is not Visible")))); EventSeverity.Informational, "User Visibility Changed, now: " + (_isVisible ? "Is Visible" : "Is not Visible"))));
@@ -99,7 +120,6 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
Mediator.Publish(new VisibilityChange()); Mediator.Publish(new VisibilityChange());
} }
} }
}
public long LastAppliedDataBytes { get; private set; } public long LastAppliedDataBytes { get; private set; }
public long LastAppliedDataTris { get; set; } = -1; public long LastAppliedDataTris { get; set; } = -1;
@@ -918,6 +938,46 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
} }
} }
private void CancelVisibilityGraceTask()
{
lock (_visibilityGraceGate)
{
_visibilityGraceCts?.CancelDispose();
_visibilityGraceCts = null;
}
}
private void StartVisibilityGraceTask()
{
CancellationToken token;
lock (_visibilityGraceGate)
{
_visibilityGraceCts = _visibilityGraceCts?.CancelRecreate() ?? new CancellationTokenSource();
token = _visibilityGraceCts.Token;
}
_visibilityGraceTask = Task.Run(async () =>
{
try
{
await Task.Delay(VisibilityEvictionGrace, token).ConfigureAwait(false);
token.ThrowIfCancellationRequested();
if (IsVisible) return;
ScheduledForDeletion = true;
ResetPenumbraCollection(reason: "VisibilityLostTimeout");
}
catch (OperationCanceledException)
{
// operation cancelled, do nothing
}
catch (Exception ex)
{
Logger.LogDebug(ex, "Visibility grace task failed for {handler}", GetLogIdentifier());
}
}, CancellationToken.None);
}
protected override void Dispose(bool disposing) protected override void Dispose(bool disposing)
{ {
base.Dispose(disposing); base.Dispose(disposing);
@@ -936,7 +996,10 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
_downloadCancellationTokenSource = null; _downloadCancellationTokenSource = null;
_downloadManager.Dispose(); _downloadManager.Dispose();
_charaHandler?.Dispose(); _charaHandler?.Dispose();
CancelVisibilityGraceTask();
_charaHandler = null; _charaHandler = null;
_invisibleSinceUtc = null;
_visibilityEvictionDueAtUtc = null;
if (!string.IsNullOrEmpty(name)) if (!string.IsNullOrEmpty(name))
{ {
@@ -1265,6 +1328,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
} }
private Task? _pairDownloadTask; private Task? _pairDownloadTask;
private Task _visibilityGraceTask;
private async Task DownloadAndApplyCharacterAsync(Guid applicationBase, CharacterData charaData, Dictionary<ObjectKind, HashSet<PlayerChanges>> updatedData, private async Task DownloadAndApplyCharacterAsync(Guid applicationBase, CharacterData charaData, Dictionary<ObjectKind, HashSet<PlayerChanges>> updatedData,
bool updateModdedPaths, bool updateManip, Dictionary<(string GamePath, string? Hash), string>? cachedModdedPaths, CancellationToken downloadToken) bool updateModdedPaths, bool updateManip, Dictionary<(string GamePath, string? Hash), string>? cachedModdedPaths, CancellationToken downloadToken)

View File

@@ -11,7 +11,9 @@ public sealed class PairHandlerRegistry : IDisposable
{ {
private readonly object _gate = new(); private readonly object _gate = new();
private readonly object _pendingGate = new(); private readonly object _pendingGate = new();
private readonly object _visibilityGate = new();
private readonly Dictionary<string, PairHandlerEntry> _entriesByIdent = new(StringComparer.Ordinal); private readonly Dictionary<string, PairHandlerEntry> _entriesByIdent = new(StringComparer.Ordinal);
private readonly Dictionary<string, CancellationTokenSource> _pendingInvisibleEvictions = new(StringComparer.Ordinal);
private readonly Dictionary<IPairHandlerAdapter, PairHandlerEntry> _entriesByHandler = new(ReferenceEqualityComparer.Instance); private readonly Dictionary<IPairHandlerAdapter, PairHandlerEntry> _entriesByHandler = new(ReferenceEqualityComparer.Instance);
private readonly IPairHandlerAdapterFactory _handlerFactory; private readonly IPairHandlerAdapterFactory _handlerFactory;
@@ -144,6 +146,37 @@ public sealed class PairHandlerRegistry : IDisposable
return PairOperationResult<PairUniqueIdentifier>.Ok(registration.PairIdent); return PairOperationResult<PairUniqueIdentifier>.Ok(registration.PairIdent);
} }
private PairOperationResult CancelAllInvisibleEvictions()
{
List<CancellationTokenSource> snapshot;
lock (_visibilityGate)
{
snapshot = [.. _pendingInvisibleEvictions.Values];
_pendingInvisibleEvictions.Clear();
}
List<string>? errors = null;
foreach (var cts in snapshot)
{
try { cts.Cancel(); }
catch (Exception ex)
{
(errors ??= new List<string>()).Add($"Cancel: {ex.Message}");
}
try { cts.Dispose(); }
catch (Exception ex)
{
(errors ??= new List<string>()).Add($"Dispose: {ex.Message}");
}
}
return errors is null
? PairOperationResult.Ok()
: PairOperationResult.Fail($"CancelAllInvisibleEvictions had error(s): {string.Join(" | ", errors)}");
}
public PairOperationResult ApplyCharacterData(PairRegistration registration, OnlineUserCharaDataDto dto) public PairOperationResult ApplyCharacterData(PairRegistration registration, OnlineUserCharaDataDto dto)
{ {
if (registration.CharacterIdent is null) if (registration.CharacterIdent is null)
@@ -300,6 +333,7 @@ public sealed class PairHandlerRegistry : IDisposable
lock (_gate) lock (_gate)
{ {
handlers = _entriesByHandler.Keys.ToList(); handlers = _entriesByHandler.Keys.ToList();
CancelAllInvisibleEvictions();
_entriesByIdent.Clear(); _entriesByIdent.Clear();
_entriesByHandler.Clear(); _entriesByHandler.Clear();
} }
@@ -332,6 +366,7 @@ public sealed class PairHandlerRegistry : IDisposable
lock (_gate) lock (_gate)
{ {
handlers = _entriesByHandler.Keys.ToList(); handlers = _entriesByHandler.Keys.ToList();
CancelAllInvisibleEvictions();
_entriesByIdent.Clear(); _entriesByIdent.Clear();
_entriesByHandler.Clear(); _entriesByHandler.Clear();
} }

View File

@@ -1,5 +1,6 @@
using Dalamud.Game; using Dalamud.Game;
using Dalamud.Game.ClientState.Objects; using Dalamud.Game.ClientState.Objects;
using Dalamud.Interface;
using Dalamud.Interface.ImGuiFileDialog; using Dalamud.Interface.ImGuiFileDialog;
using Dalamud.Interface.Windowing; using Dalamud.Interface.Windowing;
using Dalamud.Plugin; using Dalamud.Plugin;
@@ -105,6 +106,7 @@ public sealed class Plugin : IDalamudPlugin
services.AddSingleton(new Dalamud.Localization("LightlessSync.Localization.", string.Empty, useEmbedded: true)); services.AddSingleton(new Dalamud.Localization("LightlessSync.Localization.", string.Empty, useEmbedded: true));
services.AddSingleton(gameGui); services.AddSingleton(gameGui);
services.AddSingleton(addonLifecycle); services.AddSingleton(addonLifecycle);
services.AddSingleton<IUiBuilder>(pluginInterface.UiBuilder);
// Core singletons // Core singletons
services.AddSingleton<LightlessMediator>(); services.AddSingleton<LightlessMediator>();

View File

@@ -23,6 +23,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
private readonly DalamudUtilService _dalamudUtilService; private readonly DalamudUtilService _dalamudUtilService;
private readonly ActorObjectService _actorObjectService; private readonly ActorObjectService _actorObjectService;
private readonly PairUiService _pairUiService; private readonly PairUiService _pairUiService;
private readonly ChatConfigService _chatConfigService;
private readonly Lock _sync = new(); private readonly Lock _sync = new();
@@ -57,6 +58,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
_dalamudUtilService = dalamudUtilService; _dalamudUtilService = dalamudUtilService;
_actorObjectService = actorObjectService; _actorObjectService = actorObjectService;
_pairUiService = pairUiService; _pairUiService = pairUiService;
_chatConfigService = chatConfigService;
_isLoggedIn = _dalamudUtilService.IsLoggedIn; _isLoggedIn = _dalamudUtilService.IsLoggedIn;
_isConnected = _apiController.IsConnected; _isConnected = _apiController.IsConnected;
@@ -136,6 +138,42 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
} }
} }
public void MoveChannel(string draggedKey, string targetKey)
{
if (string.IsNullOrWhiteSpace(draggedKey) || string.IsNullOrWhiteSpace(targetKey))
{
return;
}
bool updated = false;
using (_sync.EnterScope())
{
if (!_channels.ContainsKey(draggedKey) || !_channels.ContainsKey(targetKey))
{
return;
}
var fromIndex = _channelOrder.IndexOf(draggedKey);
var toIndex = _channelOrder.IndexOf(targetKey);
if (fromIndex < 0 || toIndex < 0 || fromIndex == toIndex)
{
return;
}
_channelOrder.RemoveAt(fromIndex);
var insertIndex = Math.Clamp(toIndex, 0, _channelOrder.Count);
_channelOrder.Insert(insertIndex, draggedKey);
_chatConfigService.Current.ChannelOrder = new List<string>(_channelOrder);
_chatConfigService.Save();
updated = true;
}
if (updated)
{
PublishChannelListChanged();
}
}
public Task SetChatEnabledAsync(bool enabled) public Task SetChatEnabledAsync(bool enabled)
=> enabled ? EnableChatAsync() : DisableChatAsync(); => enabled ? EnableChatAsync() : DisableChatAsync();
@@ -512,7 +550,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
if (!_isLoggedIn || !_apiController.IsConnected) if (!_isLoggedIn || !_apiController.IsConnected)
{ {
await LeaveCurrentZoneAsync(force, 0).ConfigureAwait(false); await LeaveCurrentZoneAsync(force, 0, 0).ConfigureAwait(false);
return; return;
} }
@@ -520,6 +558,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
{ {
var location = await _dalamudUtilService.GetMapDataAsync().ConfigureAwait(false); var location = await _dalamudUtilService.GetMapDataAsync().ConfigureAwait(false);
var territoryId = (ushort)location.TerritoryId; var territoryId = (ushort)location.TerritoryId;
var worldId = (ushort)location.ServerId;
string? zoneKey; string? zoneKey;
ZoneChannelDefinition? definition = null; ZoneChannelDefinition? definition = null;
@@ -536,14 +575,14 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
if (definition is null) if (definition is null)
{ {
await LeaveCurrentZoneAsync(force, territoryId).ConfigureAwait(false); await LeaveCurrentZoneAsync(force, territoryId, worldId).ConfigureAwait(false);
return; return;
} }
var descriptor = await BuildZoneDescriptorAsync(definition.Value).ConfigureAwait(false); var descriptor = await BuildZoneDescriptorAsync(definition.Value).ConfigureAwait(false);
if (descriptor is null) if (descriptor is null)
{ {
await LeaveCurrentZoneAsync(force, territoryId).ConfigureAwait(false); await LeaveCurrentZoneAsync(force, territoryId, worldId).ConfigureAwait(false);
return; return;
} }
@@ -586,7 +625,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
} }
} }
private async Task LeaveCurrentZoneAsync(bool force, ushort territoryId) private async Task LeaveCurrentZoneAsync(bool force, ushort territoryId, ushort worldId)
{ {
ChatChannelDescriptor? descriptor = null; ChatChannelDescriptor? descriptor = null;
@@ -602,9 +641,29 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
state.StatusText = !_chatEnabled state.StatusText = !_chatEnabled
? "Chat services disabled" ? "Chat services disabled"
: (_isConnected ? ZoneUnavailableMessage : "Disconnected from chat server"); : (_isConnected ? ZoneUnavailableMessage : "Disconnected from chat server");
if (territoryId != 0
&& _dalamudUtilService.TerritoryData.Value.TryGetValue(territoryId, out var territoryName)
&& !string.IsNullOrWhiteSpace(territoryName))
{
state.DisplayName = territoryName;
}
else
{
state.DisplayName = "Zone Chat"; state.DisplayName = "Zone Chat";
} }
if (worldId != 0)
{
state.Descriptor = new ChatChannelDescriptor
{
Type = ChatChannelType.Zone,
WorldId = worldId,
ZoneId = territoryId,
CustomKey = string.Empty
};
}
}
if (string.Equals(_activeChannelKey, ZoneChannelKey, StringComparison.Ordinal)) if (string.Equals(_activeChannelKey, ZoneChannelKey, StringComparison.Ordinal))
{ {
_activeChannelKey = _channelOrder.FirstOrDefault(key => !string.Equals(key, ZoneChannelKey, StringComparison.Ordinal)); _activeChannelKey = _channelOrder.FirstOrDefault(key => !string.Equals(key, ZoneChannelKey, StringComparison.Ordinal));
@@ -1092,6 +1151,38 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
{ {
_channelOrder.Clear(); _channelOrder.Clear();
var configuredOrder = _chatConfigService.Current.ChannelOrder;
if (configuredOrder.Count > 0)
{
var seen = new HashSet<string>(StringComparer.Ordinal);
foreach (var key in configuredOrder)
{
if (_channels.ContainsKey(key) && seen.Add(key))
{
_channelOrder.Add(key);
}
}
var remaining = _channels.Values
.Where(state => !seen.Contains(state.Key))
.ToList();
if (remaining.Count > 0)
{
var zoneKeys = remaining
.Where(state => state.Type == ChatChannelType.Zone)
.Select(state => state.Key);
var groupKeys = remaining
.Where(state => state.Type == ChatChannelType.Group)
.OrderBy(state => state.DisplayName, StringComparer.OrdinalIgnoreCase)
.Select(state => state.Key);
_channelOrder.AddRange(zoneKeys);
_channelOrder.AddRange(groupKeys);
}
}
else
{
if (_channels.ContainsKey(ZoneChannelKey)) if (_channels.ContainsKey(ZoneChannelKey))
{ {
_channelOrder.Add(ZoneChannelKey); _channelOrder.Add(ZoneChannelKey);
@@ -1103,6 +1194,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
.Select(state => state.Key); .Select(state => state.Key);
_channelOrder.AddRange(groups); _channelOrder.AddRange(groups);
}
if (_activeChannelKey is null && _channelOrder.Count > 0) if (_activeChannelKey is null && _channelOrder.Count > 0)
{ {

View File

@@ -49,7 +49,7 @@ public sealed class CommandManagerService : IDisposable
"\t /light analyze - Opens the Lightless Character Data Analysis window" + Environment.NewLine + "\t /light analyze - Opens the Lightless Character Data Analysis window" + Environment.NewLine +
"\t /light settings - Opens the Lightless Settings window" + Environment.NewLine + "\t /light settings - Opens the Lightless Settings window" + Environment.NewLine +
"\t /light finder - Opens the Lightfinder window" + Environment.NewLine + "\t /light finder - Opens the Lightfinder window" + Environment.NewLine +
"\t /light finder - Opens the Lightless Chat window" "\t /light chat - Opens the Lightless Chat window"
}); });
} }

View File

@@ -239,6 +239,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
public bool IsInCombat { get; private set; } = false; public bool IsInCombat { get; private set; } = false;
public bool IsPerforming { get; private set; } = false; public bool IsPerforming { get; private set; } = false;
public bool IsInInstance { get; private set; } = false; public bool IsInInstance { get; private set; } = false;
public bool IsInDuty => _condition[ConditionFlag.BoundByDuty];
public bool HasModifiedGameFiles => _gameData.HasModifiedGameDataFiles; public bool HasModifiedGameFiles => _gameData.HasModifiedGameDataFiles;
public uint ClassJobId => _classJobId!.Value; public uint ClassJobId => _classJobId!.Value;
public Lazy<Dictionary<uint, string>> JobData { get; private set; } public Lazy<Dictionary<uint, string>> JobData { get; private set; }
@@ -248,6 +249,32 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
public bool IsLodEnabled { get; private set; } public bool IsLodEnabled { get; private set; }
public LightlessMediator Mediator { get; } public LightlessMediator Mediator { get; }
public bool IsInFieldOperation
{
get
{
if (!IsInDuty)
{
return false;
}
var territoryId = _clientState.TerritoryType;
if (territoryId == 0)
{
return false;
}
if (!TerritoryData.Value.TryGetValue(territoryId, out var name) || string.IsNullOrWhiteSpace(name))
{
return false;
}
return name.Contains("Eureka", StringComparison.OrdinalIgnoreCase)
|| name.Contains("Bozja", StringComparison.OrdinalIgnoreCase)
|| name.Contains("Zadnor", StringComparison.OrdinalIgnoreCase);
}
}
public IGameObject? CreateGameObject(IntPtr reference) public IGameObject? CreateGameObject(IntPtr reference)
{ {
EnsureIsOnFramework(); EnsureIsOnFramework();

View File

@@ -641,8 +641,8 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
return; return;
_activeBroadcastingCids = newSet; _activeBroadcastingCids = newSet;
if (_logger.IsEnabled(LogLevel.Information)) if (_logger.IsEnabled(LogLevel.Trace))
_logger.LogInformation("Active broadcast CIDs: {Cids}", string.Join(',', _activeBroadcastingCids)); _logger.LogTrace("Active broadcast IDs: {Cids}", string.Join(',', _activeBroadcastingCids));
FlagRefresh(); FlagRefresh();
} }

View File

@@ -126,11 +126,11 @@ public sealed class TextureMetadataHelper
private const string TextureSegment = "/texture/"; private const string TextureSegment = "/texture/";
private const string MaterialSegment = "/material/"; private const string MaterialSegment = "/material/";
private const uint NormalSamplerId = 0x0C5EC1F1u; private const uint NormalSamplerId = ShpkFile.NormalSamplerId;
private const uint IndexSamplerId = 0x565F8FD8u; private const uint IndexSamplerId = ShpkFile.IndexSamplerId;
private const uint SpecularSamplerId = 0x2B99E025u; private const uint SpecularSamplerId = ShpkFile.SpecularSamplerId;
private const uint DiffuseSamplerId = 0x115306BEu; private const uint DiffuseSamplerId = ShpkFile.DiffuseSamplerId;
private const uint MaskSamplerId = 0x8A4E82B6u; private const uint MaskSamplerId = ShpkFile.MaskSamplerId;
public TextureMetadataHelper(ILogger<TextureMetadataHelper> logger, IDataManager dataManager) public TextureMetadataHelper(ILogger<TextureMetadataHelper> logger, IDataManager dataManager)
{ {

View File

@@ -843,12 +843,16 @@ public class CompactUi : WindowMediatorSubscriberBase
//Filter of not grouped/foldered and offline pairs //Filter of not grouped/foldered and offline pairs
var allOnlineNotTaggedPairs = SortEntries(allEntries.Where(FilterNotTaggedUsers)); var allOnlineNotTaggedPairs = SortEntries(allEntries.Where(FilterNotTaggedUsers));
var onlineNotTaggedPairs = SortEntries(filteredEntries.Where(e => FilterNotTaggedUsers(e) && FilterOnlineOrPausedSelf(e))); if (allOnlineNotTaggedPairs.Count > 0 && _configService.Current.ShowOfflineUsersSeparately) {
var filteredOnlineEntries = SortOnlineEntries(filteredEntries.Where(e => FilterNotTaggedUsers(e) && FilterOnlineOrPausedSelf(e)));
if (allOnlineNotTaggedPairs.Count > 0)
{
drawFolders.Add(_drawEntityFactory.CreateTagFolder( drawFolders.Add(_drawEntityFactory.CreateTagFolder(
_configService.Current.ShowOfflineUsersSeparately ? TagHandler.CustomOnlineTag : TagHandler.CustomAllTag, TagHandler.CustomOnlineTag,
filteredOnlineEntries,
allOnlineNotTaggedPairs));
} else if (allOnlineNotTaggedPairs.Count > 0 && !_configService.Current.ShowOfflineUsersSeparately) {
var onlineNotTaggedPairs = SortEntries(filteredEntries.Where(FilterNotTaggedUsers));
drawFolders.Add(_drawEntityFactory.CreateTagFolder(
TagHandler.CustomAllTag,
onlineNotTaggedPairs, onlineNotTaggedPairs,
allOnlineNotTaggedPairs)); allOnlineNotTaggedPairs));
} }
@@ -885,7 +889,7 @@ public class CompactUi : WindowMediatorSubscriberBase
} }
} }
private bool PassesFilter(PairUiEntry entry, string filter) private static bool PassesFilter(PairUiEntry entry, string filter)
{ {
if (string.IsNullOrEmpty(filter)) return true; if (string.IsNullOrEmpty(filter)) return true;
@@ -946,6 +950,17 @@ public class CompactUi : WindowMediatorSubscriberBase
}; };
} }
private ImmutableList<PairUiEntry> SortOnlineEntries(IEnumerable<PairUiEntry> entries)
{
var entryList = entries.ToList();
return _configService.Current.OnlinePairSortMode switch
{
OnlinePairSortMode.Alphabetical => [.. entryList.OrderBy(e => AlphabeticalSortKey(e), StringComparer.OrdinalIgnoreCase)],
OnlinePairSortMode.PreferredDirectPairs => SortVisibleByPreferred(entryList),
_ => SortEntries(entryList),
};
}
private ImmutableList<PairUiEntry> SortVisibleByMetric(IEnumerable<PairUiEntry> entries, Func<PairUiEntry, long> selector) private ImmutableList<PairUiEntry> SortVisibleByMetric(IEnumerable<PairUiEntry> entries, Func<PairUiEntry, long> selector)
{ {
return [.. entries return [.. entries

View File

@@ -4,8 +4,8 @@ using Dalamud.Interface.Utility.Raii;
using LightlessSync.UI.Handlers; using LightlessSync.UI.Handlers;
using LightlessSync.UI.Models; using LightlessSync.UI.Models;
using System.Collections.Immutable; using System.Collections.Immutable;
using LightlessSync.UI;
using LightlessSync.UI.Style; using LightlessSync.UI.Style;
using OtterGui.Text;
namespace LightlessSync.UI.Components; namespace LightlessSync.UI.Components;
@@ -113,9 +113,13 @@ public abstract class DrawFolderBase : IDrawFolder
using var indent = ImRaii.PushIndent(_uiSharedService.GetIconSize(FontAwesomeIcon.EllipsisV).X + ImGui.GetStyle().ItemSpacing.X, false); using var indent = ImRaii.PushIndent(_uiSharedService.GetIconSize(FontAwesomeIcon.EllipsisV).X + ImGui.GetStyle().ItemSpacing.X, false);
if (DrawPairs.Any()) if (DrawPairs.Any())
{ {
foreach (var item in DrawPairs) using var clipper = ImUtf8.ListClipper(DrawPairs.Count, ImGui.GetFrameHeightWithSpacing());
while (clipper.Step())
{ {
item.DrawPairedClient(); for (var i = clipper.DisplayStart; i < clipper.DisplayEnd; i++)
{
DrawPairs[i].DrawPairedClient();
}
} }
} }
else else

View File

@@ -169,11 +169,16 @@ public class DrawFolderTag : DrawFolderBase
protected override float DrawRightSide(float currentRightSideX) protected override float DrawRightSide(float currentRightSideX)
{ {
if (_id == TagHandler.CustomVisibleTag) if (string.Equals(_id, TagHandler.CustomVisibleTag, StringComparison.Ordinal))
{ {
return DrawVisibleFilter(currentRightSideX); return DrawVisibleFilter(currentRightSideX);
} }
if (string.Equals(_id, TagHandler.CustomOnlineTag, StringComparison.Ordinal))
{
return DrawOnlineFilter(currentRightSideX);
}
if (!RenderPause) if (!RenderPause)
{ {
return currentRightSideX; return currentRightSideX;
@@ -254,7 +259,7 @@ public class DrawFolderTag : DrawFolderBase
foreach (VisiblePairSortMode mode in Enum.GetValues<VisiblePairSortMode>()) foreach (VisiblePairSortMode mode in Enum.GetValues<VisiblePairSortMode>())
{ {
var selected = _configService.Current.VisiblePairSortMode == mode; var selected = _configService.Current.VisiblePairSortMode == mode;
if (ImGui.MenuItem(GetSortLabel(mode), string.Empty, selected)) if (ImGui.MenuItem(GetSortVisibleLabel(mode), string.Empty, selected))
{ {
if (!selected) if (!selected)
{ {
@@ -273,7 +278,49 @@ public class DrawFolderTag : DrawFolderBase
return buttonStart - spacingX; return buttonStart - spacingX;
} }
private static string GetSortLabel(VisiblePairSortMode mode) => mode switch private float DrawOnlineFilter(float currentRightSideX)
{
var buttonSize = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Filter);
var spacingX = ImGui.GetStyle().ItemSpacing.X;
var buttonStart = currentRightSideX - buttonSize.X;
ImGui.SameLine(buttonStart);
if (_uiSharedService.IconButton(FontAwesomeIcon.Filter))
{
SuppressNextRowToggle();
ImGui.OpenPopup($"online-filter-{_id}");
}
UiSharedService.AttachToolTip("Adjust how online pairs are ordered.");
if (ImGui.BeginPopup($"online-filter-{_id}"))
{
ImGui.TextUnformatted("Online Pair Ordering");
ImGui.Separator();
foreach (OnlinePairSortMode mode in Enum.GetValues<OnlinePairSortMode>())
{
var selected = _configService.Current.OnlinePairSortMode == mode;
if (ImGui.MenuItem(GetSortOnlineLabel(mode), string.Empty, selected))
{
if (!selected)
{
_configService.Current.OnlinePairSortMode = mode;
_configService.Save();
_mediator.Publish(new RefreshUiMessage());
}
ImGui.CloseCurrentPopup();
}
}
ImGui.EndPopup();
}
return buttonStart - spacingX;
}
private static string GetSortVisibleLabel(VisiblePairSortMode mode) => mode switch
{ {
VisiblePairSortMode.Alphabetical => "Alphabetical", VisiblePairSortMode.Alphabetical => "Alphabetical",
VisiblePairSortMode.VramUsage => "VRAM usage (descending)", VisiblePairSortMode.VramUsage => "VRAM usage (descending)",
@@ -282,4 +329,11 @@ public class DrawFolderTag : DrawFolderBase
VisiblePairSortMode.PreferredDirectPairs => "Preferred permissions & Direct pairs", VisiblePairSortMode.PreferredDirectPairs => "Preferred permissions & Direct pairs",
_ => "Default", _ => "Default",
}; };
private static string GetSortOnlineLabel(OnlinePairSortMode mode) => mode switch
{
OnlinePairSortMode.Alphabetical => "Alphabetical",
OnlinePairSortMode.PreferredDirectPairs => "Preferred permissions & Direct pairs",
_ => "Default",
};
} }

View File

@@ -27,8 +27,11 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
{ {
private const float MinTextureFilterPaneWidth = 305f; private const float MinTextureFilterPaneWidth = 305f;
private const float MaxTextureFilterPaneWidth = 405f; private const float MaxTextureFilterPaneWidth = 405f;
private const float MinTextureDetailPaneWidth = 580f; private const float MinTextureDetailPaneWidth = 480f;
private const float MaxTextureDetailPaneWidth = 720f; private const float MaxTextureDetailPaneWidth = 720f;
private const float TextureFilterSplitterWidth = 8f;
private const float TextureDetailSplitterWidth = 12f;
private const float TextureDetailSplitterCollapsedWidth = 18f;
private const float SelectedFilePanelLogicalHeight = 90f; private const float SelectedFilePanelLogicalHeight = 90f;
private static readonly Vector4 SelectedTextureRowTextColor = new(0f, 0f, 0f, 1f); private static readonly Vector4 SelectedTextureRowTextColor = new(0f, 0f, 0f, 1f);
@@ -80,6 +83,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
private bool _modalOpen = false; private bool _modalOpen = false;
private bool _showModal = false; private bool _showModal = false;
private bool _textureRowsDirty = true; private bool _textureRowsDirty = true;
private bool _textureDetailCollapsed = false;
private bool _conversionFailed; private bool _conversionFailed;
private bool _showAlreadyAddedTransients = false; private bool _showAlreadyAddedTransients = false;
private bool _acknowledgeReview = false; private bool _acknowledgeReview = false;
@@ -111,7 +115,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
_hasUpdate = true; _hasUpdate = true;
}); });
WindowBuilder.For(this) WindowBuilder.For(this)
.SetSizeConstraints(new Vector2(1650, 1000), new Vector2(3840, 2160)) .SetSizeConstraints(new Vector2(1240, 680), new Vector2(3840, 2160))
.Apply(); .Apply();
_conversionProgress.ProgressChanged += ConversionProgress_ProgressChanged; _conversionProgress.ProgressChanged += ConversionProgress_ProgressChanged;
@@ -1205,35 +1209,52 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
var availableSize = ImGui.GetContentRegionAvail(); var availableSize = ImGui.GetContentRegionAvail();
var windowPos = ImGui.GetWindowPos(); var windowPos = ImGui.GetWindowPos();
var spacingX = ImGui.GetStyle().ItemSpacing.X; var spacingX = ImGui.GetStyle().ItemSpacing.X;
var splitterWidth = 6f * scale; var filterSplitterWidth = TextureFilterSplitterWidth * scale;
var detailSplitterWidth = (_textureDetailCollapsed ? TextureDetailSplitterCollapsedWidth : TextureDetailSplitterWidth) * scale;
var totalSplitterWidth = filterSplitterWidth + detailSplitterWidth;
var totalSpacing = 2 * spacingX;
const float minFilterWidth = MinTextureFilterPaneWidth; const float minFilterWidth = MinTextureFilterPaneWidth;
const float minDetailWidth = MinTextureDetailPaneWidth; const float minDetailWidth = MinTextureDetailPaneWidth;
const float minCenterWidth = 340f; const float minCenterWidth = 340f;
var dynamicFilterMax = Math.Max(minFilterWidth, availableSize.X - minDetailWidth - minCenterWidth - 2 * (splitterWidth + spacingX)); var detailMinForLayout = _textureDetailCollapsed ? 0f : minDetailWidth;
var dynamicFilterMax = Math.Max(minFilterWidth, availableSize.X - detailMinForLayout - minCenterWidth - totalSplitterWidth - totalSpacing);
var filterMaxBound = Math.Min(MaxTextureFilterPaneWidth, dynamicFilterMax); var filterMaxBound = Math.Min(MaxTextureFilterPaneWidth, dynamicFilterMax);
var filterWidth = Math.Clamp(_textureFilterPaneWidth, minFilterWidth, filterMaxBound); var filterWidth = Math.Clamp(_textureFilterPaneWidth, minFilterWidth, filterMaxBound);
var dynamicDetailMax = Math.Max(minDetailWidth, availableSize.X - filterWidth - minCenterWidth - 2 * (splitterWidth + spacingX)); var dynamicDetailMax = Math.Max(detailMinForLayout, availableSize.X - filterWidth - minCenterWidth - totalSplitterWidth - totalSpacing);
var detailMaxBound = Math.Min(MaxTextureDetailPaneWidth, dynamicDetailMax); var detailMaxBound = _textureDetailCollapsed ? 0f : Math.Min(MaxTextureDetailPaneWidth, dynamicDetailMax);
var detailWidth = Math.Clamp(_textureDetailPaneWidth, minDetailWidth, detailMaxBound); var detailWidth = _textureDetailCollapsed ? 0f : Math.Clamp(_textureDetailPaneWidth, minDetailWidth, detailMaxBound);
var centerWidth = availableSize.X - filterWidth - detailWidth - 2 * (splitterWidth + spacingX); var centerWidth = availableSize.X - filterWidth - detailWidth - totalSplitterWidth - totalSpacing;
if (centerWidth < minCenterWidth) if (centerWidth < minCenterWidth)
{ {
var deficit = minCenterWidth - centerWidth; var deficit = minCenterWidth - centerWidth;
if (!_textureDetailCollapsed)
{
detailWidth = Math.Clamp(detailWidth - deficit, minDetailWidth, detailWidth = Math.Clamp(detailWidth - deficit, minDetailWidth,
Math.Min(MaxTextureDetailPaneWidth, Math.Max(minDetailWidth, availableSize.X - filterWidth - minCenterWidth - 2 * (splitterWidth + spacingX)))); Math.Min(MaxTextureDetailPaneWidth, Math.Max(minDetailWidth, availableSize.X - filterWidth - minCenterWidth - totalSplitterWidth - totalSpacing)));
centerWidth = availableSize.X - filterWidth - detailWidth - 2 * (splitterWidth + spacingX); centerWidth = availableSize.X - filterWidth - detailWidth - totalSplitterWidth - totalSpacing;
if (centerWidth < minCenterWidth) if (centerWidth < minCenterWidth)
{ {
deficit = minCenterWidth - centerWidth; deficit = minCenterWidth - centerWidth;
filterWidth = Math.Clamp(filterWidth - deficit, minFilterWidth, filterWidth = Math.Clamp(filterWidth - deficit, minFilterWidth,
Math.Min(MaxTextureFilterPaneWidth, Math.Max(minFilterWidth, availableSize.X - detailWidth - minCenterWidth - 2 * (splitterWidth + spacingX)))); Math.Min(MaxTextureFilterPaneWidth, Math.Max(minFilterWidth, availableSize.X - detailWidth - minCenterWidth - totalSplitterWidth - totalSpacing)));
detailWidth = Math.Clamp(detailWidth, minDetailWidth, detailWidth = Math.Clamp(detailWidth, minDetailWidth,
Math.Min(MaxTextureDetailPaneWidth, Math.Max(minDetailWidth, availableSize.X - filterWidth - minCenterWidth - 2 * (splitterWidth + spacingX)))); Math.Min(MaxTextureDetailPaneWidth, Math.Max(minDetailWidth, availableSize.X - filterWidth - minCenterWidth - totalSplitterWidth - totalSpacing)));
centerWidth = availableSize.X - filterWidth - detailWidth - 2 * (splitterWidth + spacingX); centerWidth = availableSize.X - filterWidth - detailWidth - totalSplitterWidth - totalSpacing;
if (centerWidth < minCenterWidth)
{
centerWidth = minCenterWidth;
}
}
}
else
{
filterWidth = Math.Clamp(filterWidth - deficit, minFilterWidth,
Math.Min(MaxTextureFilterPaneWidth, Math.Max(minFilterWidth, availableSize.X - minCenterWidth - totalSplitterWidth - totalSpacing)));
centerWidth = availableSize.X - filterWidth - detailWidth - totalSplitterWidth - totalSpacing;
if (centerWidth < minCenterWidth) if (centerWidth < minCenterWidth)
{ {
centerWidth = minCenterWidth; centerWidth = minCenterWidth;
@@ -1242,7 +1263,10 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
} }
_textureFilterPaneWidth = filterWidth; _textureFilterPaneWidth = filterWidth;
if (!_textureDetailCollapsed)
{
_textureDetailPaneWidth = detailWidth; _textureDetailPaneWidth = detailWidth;
}
ImGui.BeginGroup(); ImGui.BeginGroup();
using (var filters = ImRaii.Child("textureFilters", new Vector2(filterWidth, 0), true)) using (var filters = ImRaii.Child("textureFilters", new Vector2(filterWidth, 0), true))
@@ -1264,8 +1288,8 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
var filterMax = ImGui.GetItemRectMax(); var filterMax = ImGui.GetItemRectMax();
var filterHeight = filterMax.Y - filterMin.Y; var filterHeight = filterMax.Y - filterMin.Y;
var filterTopLocal = filterMin - windowPos; var filterTopLocal = filterMin - windowPos;
var maxFilterResize = Math.Min(MaxTextureFilterPaneWidth, Math.Max(minFilterWidth, availableSize.X - minCenterWidth - minDetailWidth - 2 * (splitterWidth + spacingX))); var maxFilterResize = Math.Min(MaxTextureFilterPaneWidth, Math.Max(minFilterWidth, availableSize.X - minCenterWidth - detailMinForLayout - totalSplitterWidth - totalSpacing));
DrawVerticalResizeHandle("##textureFilterSplitter", filterTopLocal.Y, filterHeight, ref _textureFilterPaneWidth, minFilterWidth, maxFilterResize); DrawVerticalResizeHandle("##textureFilterSplitter", filterTopLocal.Y, filterHeight, ref _textureFilterPaneWidth, minFilterWidth, maxFilterResize, out _);
TextureRow? selectedRow; TextureRow? selectedRow;
ImGui.BeginGroup(); ImGui.BeginGroup();
@@ -1279,9 +1303,29 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
var tableMax = ImGui.GetItemRectMax(); var tableMax = ImGui.GetItemRectMax();
var tableHeight = tableMax.Y - tableMin.Y; var tableHeight = tableMax.Y - tableMin.Y;
var tableTopLocal = tableMin - windowPos; var tableTopLocal = tableMin - windowPos;
var maxDetailResize = Math.Min(MaxTextureDetailPaneWidth, Math.Max(minDetailWidth, availableSize.X - _textureFilterPaneWidth - minCenterWidth - 2 * (splitterWidth + spacingX))); var maxDetailResize = Math.Min(MaxTextureDetailPaneWidth, Math.Max(minDetailWidth, availableSize.X - _textureFilterPaneWidth - minCenterWidth - totalSplitterWidth - totalSpacing));
DrawVerticalResizeHandle("##textureDetailSplitter", tableTopLocal.Y, tableHeight, ref _textureDetailPaneWidth, minDetailWidth, maxDetailResize, invert: true); var detailToggle = DrawVerticalResizeHandle(
"##textureDetailSplitter",
tableTopLocal.Y,
tableHeight,
ref _textureDetailPaneWidth,
minDetailWidth,
maxDetailResize,
out var detailDragging,
invert: true,
showToggle: true,
isCollapsed: _textureDetailCollapsed);
if (detailToggle)
{
_textureDetailCollapsed = !_textureDetailCollapsed;
}
if (_textureDetailCollapsed && detailDragging)
{
_textureDetailCollapsed = false;
}
if (!_textureDetailCollapsed)
{
ImGui.BeginGroup(); ImGui.BeginGroup();
using (var detailChild = ImRaii.Child("textureDetailPane", new Vector2(detailWidth, 0), true)) using (var detailChild = ImRaii.Child("textureDetailPane", new Vector2(detailWidth, 0), true))
{ {
@@ -1289,6 +1333,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
} }
ImGui.EndGroup(); ImGui.EndGroup();
} }
}
private void DrawTextureFilters( private void DrawTextureFilters(
IReadOnlyList<TextureUsageCategory> categories, IReadOnlyList<TextureUsageCategory> categories,
@@ -1935,26 +1980,118 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
} }
} }
private void DrawVerticalResizeHandle(string id, float topY, float height, ref float leftWidth, float minWidth, float maxWidth, bool invert = false) private bool DrawVerticalResizeHandle(
string id,
float topY,
float height,
ref float leftWidth,
float minWidth,
float maxWidth,
out bool isDragging,
bool invert = false,
bool showToggle = false,
bool isCollapsed = false)
{ {
var scale = ImGuiHelpers.GlobalScale; var scale = ImGuiHelpers.GlobalScale;
var splitterWidth = 8f * scale; var splitterWidth = (showToggle
? (isCollapsed ? TextureDetailSplitterCollapsedWidth : TextureDetailSplitterWidth)
: TextureFilterSplitterWidth) * scale;
ImGui.SameLine(); ImGui.SameLine();
var cursor = ImGui.GetCursorPos(); var cursor = ImGui.GetCursorPos();
ImGui.SetCursorPos(new Vector2(cursor.X, topY)); var contentMin = ImGui.GetWindowContentRegionMin();
ImGui.PushStyleColor(ImGuiCol.Button, UIColors.Get("ButtonDefault")); var contentMax = ImGui.GetWindowContentRegionMax();
ImGui.PushStyleColor(ImGuiCol.ButtonHovered, UIColors.Get("LightlessPurple")); var clampedTop = MathF.Max(topY, contentMin.Y);
ImGui.PushStyleColor(ImGuiCol.ButtonActive, UIColors.Get("LightlessPurpleActive")); var clampedBottom = MathF.Min(topY + height, contentMax.Y);
ImGui.Button(id, new Vector2(splitterWidth, height)); var clampedHeight = MathF.Max(0f, clampedBottom - clampedTop);
ImGui.PopStyleColor(3); var splitterRounding = ImGui.GetStyle().FrameRounding;
ImGui.SetCursorPos(new Vector2(cursor.X, clampedTop));
if (clampedHeight <= 0f)
{
isDragging = false;
ImGui.SetCursorPos(new Vector2(cursor.X + splitterWidth + ImGui.GetStyle().ItemSpacing.X, cursor.Y));
return false;
}
if (ImGui.IsItemActive()) ImGui.InvisibleButton(id, new Vector2(splitterWidth, clampedHeight));
var drawList = ImGui.GetWindowDrawList();
var rectMin = ImGui.GetItemRectMin();
var rectMax = ImGui.GetItemRectMax();
var windowPos = ImGui.GetWindowPos();
var clipMin = windowPos + contentMin;
var clipMax = windowPos + contentMax;
drawList.PushClipRect(clipMin, clipMax, true);
var clipInset = 1f * scale;
var drawMin = new Vector2(
MathF.Max(rectMin.X, clipMin.X),
MathF.Max(rectMin.Y, clipMin.Y));
var drawMax = new Vector2(
MathF.Min(rectMax.X, clipMax.X - clipInset),
MathF.Min(rectMax.Y, clipMax.Y));
var hovered = ImGui.IsItemHovered();
isDragging = ImGui.IsItemActive();
var baseColor = UIColors.Get("ButtonDefault");
var hoverColor = UIColors.Get("LightlessPurple");
var activeColor = UIColors.Get("LightlessPurpleActive");
var handleColor = isDragging ? activeColor : hovered ? hoverColor : baseColor;
drawList.AddRectFilled(drawMin, drawMax, UiSharedService.Color(handleColor), splitterRounding);
drawList.AddRect(drawMin, drawMax, UiSharedService.Color(new Vector4(1f, 1f, 1f, 0.12f)), splitterRounding);
bool toggleHovered = false;
bool toggleClicked = false;
if (showToggle)
{
var icon = isCollapsed ? FontAwesomeIcon.ChevronRight : FontAwesomeIcon.ChevronLeft;
Vector2 iconSize;
using (_uiSharedService.IconFont.Push())
{
iconSize = ImGui.CalcTextSize(icon.ToIconString());
}
var toggleHeight = MathF.Min(clampedHeight, 64f * scale);
var toggleMin = new Vector2(
drawMin.X,
drawMin.Y + (drawMax.Y - drawMin.Y - toggleHeight) / 2f);
var toggleMax = new Vector2(
drawMax.X,
toggleMin.Y + toggleHeight);
var toggleColorBase = UIColors.Get("LightlessPurple");
toggleHovered = ImGui.IsMouseHoveringRect(toggleMin, toggleMax);
var toggleBg = toggleHovered
? new Vector4(toggleColorBase.X, toggleColorBase.Y, toggleColorBase.Z, 0.65f)
: new Vector4(toggleColorBase.X, toggleColorBase.Y, toggleColorBase.Z, 0.35f);
if (toggleHovered)
{
UiSharedService.AttachToolTip(isCollapsed ? "Show texture details." : "Hide texture details.");
}
drawList.AddRectFilled(toggleMin, toggleMax, UiSharedService.Color(toggleBg), splitterRounding);
drawList.AddRect(toggleMin, toggleMax, UiSharedService.Color(toggleColorBase), splitterRounding);
var iconPos = new Vector2(
drawMin.X + (drawMax.X - drawMin.X - iconSize.X) / 2f,
drawMin.Y + (drawMax.Y - drawMin.Y - iconSize.Y) / 2f);
using (_uiSharedService.IconFont.Push())
{
drawList.AddText(iconPos, ImGui.GetColorU32(ImGuiCol.Text), icon.ToIconString());
}
if (toggleHovered && ImGui.IsMouseReleased(ImGuiMouseButton.Left) && !ImGui.IsMouseDragging(ImGuiMouseButton.Left))
{
toggleClicked = true;
}
}
if (isDragging && !toggleHovered)
{ {
var delta = ImGui.GetIO().MouseDelta.X / scale; var delta = ImGui.GetIO().MouseDelta.X / scale;
leftWidth += invert ? -delta : delta; leftWidth += invert ? -delta : delta;
leftWidth = Math.Clamp(leftWidth, minWidth, maxWidth); leftWidth = Math.Clamp(leftWidth, minWidth, maxWidth);
} }
drawList.PopClipRect();
ImGui.SetCursorPos(new Vector2(cursor.X + splitterWidth + ImGui.GetStyle().ItemSpacing.X, cursor.Y)); ImGui.SetCursorPos(new Vector2(cursor.X + splitterWidth + ImGui.GetStyle().ItemSpacing.X, cursor.Y));
return toggleClicked;
} }
private (IDalamudTextureWrap? Texture, bool IsLoading, string? Error) GetTexturePreview(TextureRow row) private (IDalamudTextureWrap? Texture, bool IsLoading, string? Error) GetTexturePreview(TextureRow row)
@@ -2094,7 +2231,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
} }
else else
{ {
ImGui.TextDisabled("-"); _uiSharedService.IconText(FontAwesomeIcon.Check, ImGuiColors.DalamudWhite);
UiSharedService.AttachToolTip("Already stored in a compressed format; additional compression is disabled."); UiSharedService.AttachToolTip("Already stored in a compressed format; additional compression is disabled.");
} }
@@ -2175,6 +2312,10 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
_textureSelections[key] = target; _textureSelections[key] = target;
currentSelection = target; currentSelection = target;
} }
if (TextureMetadataHelper.TryGetRecommendationInfo(target, out var targetInfo))
{
UiSharedService.AttachToolTip($"{targetInfo.Title}{UiSharedService.TooltipSeparator}{targetInfo.Description}");
}
if (targetSelected) if (targetSelected)
{ {
ImGui.SetItemDefaultFocus(); ImGui.SetItemDefaultFocus();

View File

@@ -301,6 +301,14 @@ namespace LightlessSync.UI
bool ShellFinderEnabled = _configService.Current.SyncshellFinderEnabled; bool ShellFinderEnabled = _configService.Current.SyncshellFinderEnabled;
bool isBroadcasting = _broadcastService.IsBroadcasting; bool isBroadcasting = _broadcastService.IsBroadcasting;
if (isBroadcasting)
{
var warningColor = UIColors.Get("LightlessYellow");
_uiSharedService.DrawNoteLine("! ", warningColor,
new SeStringUtils.RichTextEntry("Syncshell Finder can only be changed while Lightfinder is disabled.", warningColor));
ImGuiHelpers.ScaledDummy(0.2f);
}
if (isBroadcasting) if (isBroadcasting)
ImGui.BeginDisabled(); ImGui.BeginDisabled();

View File

@@ -0,0 +1,7 @@
namespace LightlessSync.UI.Models;
public enum OnlinePairSortMode
{
Alphabetical = 0,
PreferredDirectPairs = 1,
}

View File

@@ -2,10 +2,9 @@ namespace LightlessSync.UI.Models;
public enum VisiblePairSortMode public enum VisiblePairSortMode
{ {
Default = 0, Alphabetical = 0,
Alphabetical = 1, VramUsage = 1,
VramUsage = 2, EffectiveVramUsage = 2,
EffectiveVramUsage = 3, TriangleCount = 3,
TriangleCount = 4, PreferredDirectPairs = 4,
PreferredDirectPairs = 5,
} }

View File

@@ -1463,7 +1463,10 @@ public class SettingsUi : WindowMediatorSubscriberBase
DrawPairPropertyRow("Has Handler", FormatBool(debugInfo.HasHandler)); DrawPairPropertyRow("Has Handler", FormatBool(debugInfo.HasHandler));
DrawPairPropertyRow("Handler Initialized", FormatBool(debugInfo.HandlerInitialized)); DrawPairPropertyRow("Handler Initialized", FormatBool(debugInfo.HandlerInitialized));
DrawPairPropertyRow("Handler Visible", FormatBool(debugInfo.HandlerVisible)); DrawPairPropertyRow("Handler Visible", FormatBool(debugInfo.HandlerVisible));
DrawPairPropertyRow("Last Time person rendered in", FormatTimestamp(debugInfo.InvisibleSinceUtc));
DrawPairPropertyRow("Handler Timer Temp Collection removal", FormatCountdown(debugInfo.VisibilityEvictionRemainingSeconds));
DrawPairPropertyRow("Handler Scheduled For Deletion", FormatBool(debugInfo.HandlerScheduledForDeletion)); DrawPairPropertyRow("Handler Scheduled For Deletion", FormatBool(debugInfo.HandlerScheduledForDeletion));
DrawPairPropertyRow("Note", pair.GetNote() ?? "(none)"); DrawPairPropertyRow("Note", pair.GetNote() ?? "(none)");
ImGui.EndTable(); ImGui.EndTable();
} }
@@ -1698,6 +1701,19 @@ public class SettingsUi : WindowMediatorSubscriberBase
return value is null ? "n/a" : value.Value.ToLocalTime().ToString("G", CultureInfo.CurrentCulture); return value is null ? "n/a" : value.Value.ToLocalTime().ToString("G", CultureInfo.CurrentCulture);
} }
private static string? FormatCountdown(double? remainingSeconds)
{
if (!remainingSeconds.HasValue)
return "No";
var secs = Math.Max(0, remainingSeconds.Value);
var t = TimeSpan.FromSeconds(secs);
return t.TotalHours >= 1
? $"{(int)t.TotalHours:00}:{t.Minutes:00}:{t.Seconds:00}"
: $"{(int)t.TotalMinutes:00}:{t.Seconds:00}";
}
private static string FormatBytes(long value) => value < 0 ? "n/a" : UiSharedService.ByteToString(value); private static string FormatBytes(long value) => value < 0 ? "n/a" : UiSharedService.ByteToString(value);
private static string FormatCharacterId(uint id) => id == uint.MaxValue ? "n/a" : $"{id} (0x{id:X8})"; private static string FormatCharacterId(uint id) => id == uint.MaxValue ? "n/a" : $"{id} (0x{id:X8})";

View File

@@ -222,7 +222,9 @@ public class AnimatedHeader
if (ImGui.IsItemHovered() && !string.IsNullOrEmpty(button.Tooltip)) if (ImGui.IsItemHovered() && !string.IsNullOrEmpty(button.Tooltip))
{ {
ImGui.PushFont(UiBuilder.DefaultFont);
ImGui.SetTooltip(button.Tooltip); ImGui.SetTooltip(button.Tooltip);
ImGui.PopFont();
} }
currentX -= buttonSize.X + spacing; currentX -= buttonSize.X + spacing;

View File

@@ -350,9 +350,9 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
? shell.Group.Alias ? shell.Group.Alias
: shell.Group.GID; : shell.Group.GID;
var style = ImGui.GetStyle();
float startX = ImGui.GetCursorPosX(); float startX = ImGui.GetCursorPosX();
float availWidth = ImGui.GetContentRegionAvail().X; float availW = ImGui.GetContentRegionAvail().X;
float rightTextW = ImGui.CalcTextSize(broadcasterName).X;
ImGui.BeginGroup(); ImGui.BeginGroup();
@@ -364,13 +364,45 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
Mediator.Publish(new GroupProfileOpenStandaloneMessage(shell.Group)); Mediator.Publish(new GroupProfileOpenStandaloneMessage(shell.Group));
} }
float nameRightX = ImGui.GetItemRectMax().X;
var regionMinScreen = ImGui.GetCursorScreenPos();
float regionRightX = regionMinScreen.X + availW;
float minBroadcasterX = nameRightX + style.ItemSpacing.X;
float maxBroadcasterWidth = regionRightX - minBroadcasterX;
string broadcasterToShow = broadcasterName;
if (!string.IsNullOrEmpty(broadcasterName) && maxBroadcasterWidth > 0f)
{
float bcFullWidth = ImGui.CalcTextSize(broadcasterName).X;
string toolTip;
if (bcFullWidth > maxBroadcasterWidth)
{
broadcasterToShow = TruncateTextToWidth(broadcasterName, maxBroadcasterWidth);
toolTip = broadcasterName + Environment.NewLine + Environment.NewLine + "Broadcaster of the syncshell.";
}
else
{
toolTip = "Broadcaster of the syncshell.";
}
float bcWidth = ImGui.CalcTextSize(broadcasterToShow).X;
float broadX = regionRightX - bcWidth;
broadX = MathF.Max(broadX, minBroadcasterX);
ImGui.SameLine(); ImGui.SameLine();
float rightX = startX + availWidth - rightTextW; var curPos = ImGui.GetCursorPos();
var pos = ImGui.GetCursorPos(); ImGui.SetCursorPos(new Vector2(broadX - regionMinScreen.X + startX, curPos.Y + 3f * ImGuiHelpers.GlobalScale));
ImGui.SetCursorPos(new Vector2(rightX, pos.Y + 3f * ImGuiHelpers.GlobalScale)); ImGui.TextUnformatted(broadcasterToShow);
ImGui.TextUnformatted(broadcasterName);
if (ImGui.IsItemHovered()) if (ImGui.IsItemHovered())
ImGui.SetTooltip("Broadcaster of the syncshell."); ImGui.SetTooltip(toolTip);
}
ImGui.EndGroup(); ImGui.EndGroup();
@@ -590,6 +622,40 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
float widthUsed = cursorLocalX - baseLocal.X; float widthUsed = cursorLocalX - baseLocal.X;
return (widthUsed, rowHeight); return (widthUsed, rowHeight);
} }
private static string TruncateTextToWidth(string text, float maxWidth)
{
if (string.IsNullOrEmpty(text))
return text;
const string ellipsis = "...";
float ellipsisWidth = ImGui.CalcTextSize(ellipsis).X;
if (maxWidth <= ellipsisWidth)
return ellipsis;
int low = 0;
int high = text.Length;
string best = ellipsis;
while (low <= high)
{
int mid = (low + high) / 2;
string candidate = string.Concat(text.AsSpan(0, mid), ellipsis);
float width = ImGui.CalcTextSize(candidate).X;
if (width <= maxWidth)
{
best = candidate;
low = mid + 1;
}
else
{
high = mid - 1;
}
}
return best;
}
private IDalamudTextureWrap? GetIconWrap(uint iconId) private IDalamudTextureWrap? GetIconWrap(uint iconId)
{ {

View File

@@ -11,8 +11,11 @@ using LightlessSync.LightlessConfiguration;
using LightlessSync.LightlessConfiguration.Models; using LightlessSync.LightlessConfiguration.Models;
using LightlessSync.Services; using LightlessSync.Services;
using LightlessSync.Services.Chat; using LightlessSync.Services.Chat;
using LightlessSync.Services.LightFinder;
using LightlessSync.Services.Mediator; using LightlessSync.Services.Mediator;
using LightlessSync.Services.ServerConfiguration;
using LightlessSync.UI.Services; using LightlessSync.UI.Services;
using LightlessSync.UI.Style;
using LightlessSync.Utils; using LightlessSync.Utils;
using LightlessSync.WebAPI; using LightlessSync.WebAPI;
using LightlessSync.WebAPI.SignalR.Utils; using LightlessSync.WebAPI.SignalR.Utils;
@@ -23,35 +26,49 @@ namespace LightlessSync.UI;
public sealed class ZoneChatUi : WindowMediatorSubscriberBase public sealed class ZoneChatUi : WindowMediatorSubscriberBase
{ {
private const string ChatDisabledStatus = "Chat services disabled"; private const string ChatDisabledStatus = "Chat services disabled";
private const string ZoneUnavailableStatus = "Zone chat is only available in major cities.";
private const string SettingsPopupId = "zone_chat_settings_popup"; private const string SettingsPopupId = "zone_chat_settings_popup";
private const string ReportPopupId = "Report Message##zone_chat_report_popup"; private const string ReportPopupId = "Report Message##zone_chat_report_popup";
private const string ChannelDragPayloadId = "zone_chat_channel_drag";
private const float DefaultWindowOpacity = .97f; private const float DefaultWindowOpacity = .97f;
private const float DefaultUnfocusedWindowOpacity = 0.6f;
private const float MinWindowOpacity = 0.05f; private const float MinWindowOpacity = 0.05f;
private const float MaxWindowOpacity = 1f; private const float MaxWindowOpacity = 1f;
private const float MinChatFontScale = 0.75f; private const float MinChatFontScale = 0.75f;
private const float MaxChatFontScale = 1.5f; private const float MaxChatFontScale = 1.5f;
private const float UnfocusedFadeOutSpeed = 0.22f;
private const float FocusFadeInSpeed = 2.0f;
private const int ReportReasonMaxLength = 500; private const int ReportReasonMaxLength = 500;
private const int ReportContextMaxLength = 1000; private const int ReportContextMaxLength = 1000;
private const int MaxChannelNoteTabLength = 25;
private readonly UiSharedService _uiSharedService; private readonly UiSharedService _uiSharedService;
private readonly ZoneChatService _zoneChatService; private readonly ZoneChatService _zoneChatService;
private readonly PairUiService _pairUiService; private readonly PairUiService _pairUiService;
private readonly LightFinderService _lightFinderService;
private readonly LightlessProfileManager _profileManager; private readonly LightlessProfileManager _profileManager;
private readonly ApiController _apiController; private readonly ApiController _apiController;
private readonly ChatConfigService _chatConfigService; private readonly ChatConfigService _chatConfigService;
private readonly ServerConfigurationManager _serverConfigurationManager;
private readonly DalamudUtilService _dalamudUtilService;
private readonly IUiBuilder _uiBuilder;
private readonly Dictionary<string, string> _draftMessages = new(StringComparer.Ordinal); private readonly Dictionary<string, string> _draftMessages = new(StringComparer.Ordinal);
private readonly ImGuiWindowFlags _unpinnedWindowFlags; private readonly ImGuiWindowFlags _unpinnedWindowFlags;
private float _currentWindowOpacity = DefaultWindowOpacity; private float _currentWindowOpacity = DefaultWindowOpacity;
private float _baseWindowOpacity = DefaultWindowOpacity;
private bool _isWindowPinned; private bool _isWindowPinned;
private bool _showRulesOverlay; private bool _showRulesOverlay;
private bool _refocusChatInput; private bool _refocusChatInput;
private string? _refocusChatInputKey; private string? _refocusChatInputKey;
private bool _isWindowFocused = true;
private int _titleBarStylePopCount;
private string? _selectedChannelKey; private string? _selectedChannelKey;
private bool _scrollToBottom = true; private bool _scrollToBottom = true;
private float? _pendingChannelScroll; private float? _pendingChannelScroll;
private float _channelScroll; private float _channelScroll;
private float _channelScrollMax; private float _channelScrollMax;
private readonly SeluneBrush _seluneBrush = new();
private ChatChannelSnapshot? _reportTargetChannel; private ChatChannelSnapshot? _reportTargetChannel;
private ChatMessageEntry? _reportTargetMessage; private ChatMessageEntry? _reportTargetMessage;
private string _reportReason = string.Empty; private string _reportReason = string.Empty;
@@ -61,6 +78,11 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
private bool _reportSubmitting; private bool _reportSubmitting;
private string? _reportError; private string? _reportError;
private ChatReportResult? _reportSubmissionResult; private ChatReportResult? _reportSubmissionResult;
private string? _dragChannelKey;
private string? _dragHoverKey;
private bool _HideStateActive;
private bool _HideStateWasOpen;
private bool _pushedStyle;
public ZoneChatUi( public ZoneChatUi(
ILogger<ZoneChatUi> logger, ILogger<ZoneChatUi> logger,
@@ -68,8 +90,12 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
UiSharedService uiSharedService, UiSharedService uiSharedService,
ZoneChatService zoneChatService, ZoneChatService zoneChatService,
PairUiService pairUiService, PairUiService pairUiService,
LightFinderService lightFinderService,
LightlessProfileManager profileManager, LightlessProfileManager profileManager,
ChatConfigService chatConfigService, ChatConfigService chatConfigService,
ServerConfigurationManager serverConfigurationManager,
DalamudUtilService dalamudUtilService,
IUiBuilder uiBuilder,
ApiController apiController, ApiController apiController,
PerformanceCollectorService performanceCollectorService) PerformanceCollectorService performanceCollectorService)
: base(logger, mediator, "Lightless Chat", performanceCollectorService) : base(logger, mediator, "Lightless Chat", performanceCollectorService)
@@ -77,8 +103,12 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
_uiSharedService = uiSharedService; _uiSharedService = uiSharedService;
_zoneChatService = zoneChatService; _zoneChatService = zoneChatService;
_pairUiService = pairUiService; _pairUiService = pairUiService;
_lightFinderService = lightFinderService;
_profileManager = profileManager; _profileManager = profileManager;
_chatConfigService = chatConfigService; _chatConfigService = chatConfigService;
_serverConfigurationManager = serverConfigurationManager;
_dalamudUtilService = dalamudUtilService;
_uiBuilder = uiBuilder;
_apiController = apiController; _apiController = apiController;
_isWindowPinned = _chatConfigService.Current.IsWindowPinned; _isWindowPinned = _chatConfigService.Current.IsWindowPinned;
_showRulesOverlay = _chatConfigService.Current.ShowRulesOverlayOnOpen; _showRulesOverlay = _chatConfigService.Current.ShowRulesOverlayOnOpen;
@@ -88,6 +118,7 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
} }
_unpinnedWindowFlags = Flags; _unpinnedWindowFlags = Flags;
RefreshWindowFlags(); RefreshWindowFlags();
ApplyUiVisibilitySettings();
Size = new Vector2(450, 420) * ImGuiHelpers.GlobalScale; Size = new Vector2(450, 420) * ImGuiHelpers.GlobalScale;
SizeCondition = ImGuiCond.FirstUseEver; SizeCondition = ImGuiCond.FirstUseEver;
WindowBuilder.For(this) WindowBuilder.For(this)
@@ -98,20 +129,118 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
Mediator.Subscribe<ChatChannelMessageAdded>(this, OnChatChannelMessageAdded); Mediator.Subscribe<ChatChannelMessageAdded>(this, OnChatChannelMessageAdded);
Mediator.Subscribe<ChatChannelsUpdated>(this, _ => _scrollToBottom = true); Mediator.Subscribe<ChatChannelsUpdated>(this, _ => _scrollToBottom = true);
Mediator.Subscribe<PriorityFrameworkUpdateMessage>(this, _ => UpdateHideState());
Mediator.Subscribe<CutsceneFrameworkUpdateMessage>(this, _ => UpdateHideState());
} }
public override void PreDraw() public override void PreDraw()
{ {
RefreshWindowFlags(); RefreshWindowFlags();
base.PreDraw(); base.PreDraw();
_currentWindowOpacity = Math.Clamp(_chatConfigService.Current.ChatWindowOpacity, MinWindowOpacity, MaxWindowOpacity); var config = _chatConfigService.Current;
var baseOpacity = Math.Clamp(config.ChatWindowOpacity, MinWindowOpacity, MaxWindowOpacity);
_baseWindowOpacity = baseOpacity;
ImGui.PushStyleVar(ImGuiStyleVar.WindowBorderSize, 0);
_pushedStyle = true;
if (config.FadeWhenUnfocused)
{
var unfocusedOpacity = Math.Clamp(config.UnfocusedWindowOpacity, MinWindowOpacity, MaxWindowOpacity);
var targetOpacity = _isWindowFocused ? baseOpacity : Math.Min(baseOpacity, unfocusedOpacity);
var delta = ImGui.GetIO().DeltaTime;
var speed = _isWindowFocused ? FocusFadeInSpeed : UnfocusedFadeOutSpeed;
_currentWindowOpacity = MoveTowards(_currentWindowOpacity, targetOpacity, speed * delta);
}
else
{
_currentWindowOpacity = baseOpacity;
}
ImGui.SetNextWindowBgAlpha(_currentWindowOpacity); ImGui.SetNextWindowBgAlpha(_currentWindowOpacity);
PushTitleBarFadeColors(_currentWindowOpacity);
}
private void UpdateHideState()
{
ApplyUiVisibilitySettings();
var shouldHide = ShouldHide();
if (shouldHide)
{
_HideStateWasOpen |= IsOpen;
if (IsOpen)
{
IsOpen = false;
}
_HideStateActive = true;
}
else if (_HideStateActive)
{
if (_HideStateWasOpen)
{
IsOpen = true;
}
_HideStateActive = false;
_HideStateWasOpen = false;
}
}
private void ApplyUiVisibilitySettings()
{
var config = _chatConfigService.Current;
_uiBuilder.DisableAutomaticUiHide = config.ShowWhenUiHidden;
_uiBuilder.DisableCutsceneUiHide = config.ShowInCutscenes;
_uiBuilder.DisableGposeUiHide = config.ShowInGpose;
}
private bool ShouldHide()
{
var config = _chatConfigService.Current;
if (config.HideInCombat && _dalamudUtilService.IsInCombat)
{
return true;
}
if (config.HideInDuty && _dalamudUtilService.IsInDuty && !_dalamudUtilService.IsInFieldOperation)
{
return true;
}
return false;
} }
protected override void DrawInternal() protected override void DrawInternal()
{ {
if (_titleBarStylePopCount > 0)
{
ImGui.PopStyleColor(_titleBarStylePopCount);
_titleBarStylePopCount = 0;
}
var config = _chatConfigService.Current;
var isFocused = ImGui.IsWindowFocused(ImGuiFocusedFlags.RootAndChildWindows);
var isHovered = ImGui.IsWindowHovered(ImGuiHoveredFlags.RootAndChildWindows);
if (config.FadeWhenUnfocused && isHovered && !isFocused)
{
ImGui.SetWindowFocus();
}
_isWindowFocused = config.FadeWhenUnfocused ? (isFocused || isHovered) : isFocused;
var contentAlpha = 1f;
if (config.FadeWhenUnfocused)
{
var baseOpacity = MathF.Max(_baseWindowOpacity, 0.001f);
contentAlpha = Math.Clamp(_currentWindowOpacity / baseOpacity, 0f, 1f);
}
using var alpha = ImRaii.PushStyle(ImGuiStyleVar.Alpha, contentAlpha);
var drawList = ImGui.GetWindowDrawList();
var windowPos = ImGui.GetWindowPos();
var windowSize = ImGui.GetWindowSize();
using var selune = Selune.Begin(_seluneBrush, drawList, windowPos, windowSize);
var childBgColor = ImGui.GetStyle().Colors[(int)ImGuiCol.ChildBg]; var childBgColor = ImGui.GetStyle().Colors[(int)ImGuiCol.ChildBg];
childBgColor.W *= _currentWindowOpacity; childBgColor.W *= _baseWindowOpacity;
using var childBg = ImRaii.PushColor(ImGuiCol.ChildBg, childBgColor); using var childBg = ImRaii.PushColor(ImGuiCol.ChildBg, childBgColor);
DrawConnectionControls(); DrawConnectionControls();
@@ -123,16 +252,19 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey3); ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey3);
ImGui.TextWrapped("No chat channels available."); ImGui.TextWrapped("No chat channels available.");
ImGui.PopStyleColor(); ImGui.PopStyleColor();
return;
} }
else
{
EnsureSelectedChannel(channels); EnsureSelectedChannel(channels);
CleanupDrafts(channels); CleanupDrafts(channels);
DrawChannelButtons(channels); DrawChannelButtons(channels);
if (_selectedChannelKey is null) if (_selectedChannelKey is null)
{
selune.DrawHighlightOnly(ImGui.GetIO().DeltaTime);
return; return;
}
var activeChannel = channels.FirstOrDefault(channel => string.Equals(channel.Key, _selectedChannelKey, StringComparison.Ordinal)); var activeChannel = channels.FirstOrDefault(channel => string.Equals(channel.Key, _selectedChannelKey, StringComparison.Ordinal));
if (activeChannel.Equals(default(ChatChannelSnapshot))) if (activeChannel.Equals(default(ChatChannelSnapshot)))
@@ -148,14 +280,33 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
DrawMessageArea(activeChannel, _currentWindowOpacity); DrawMessageArea(activeChannel, _currentWindowOpacity);
ImGui.Separator(); ImGui.Separator();
DrawInput(activeChannel); DrawInput(activeChannel);
}
if (_showRulesOverlay) if (_showRulesOverlay)
{ {
DrawRulesOverlay(); DrawRulesOverlay();
} }
selune.DrawHighlightOnly(ImGui.GetIO().DeltaTime);
} }
private static void DrawHeader(ChatChannelSnapshot channel) private void PushTitleBarFadeColors(float opacity)
{
_titleBarStylePopCount = 0;
var alpha = Math.Clamp(opacity, 0f, 1f);
var colors = ImGui.GetStyle().Colors;
var titleBg = colors[(int)ImGuiCol.TitleBg];
var titleBgActive = colors[(int)ImGuiCol.TitleBgActive];
var titleBgCollapsed = colors[(int)ImGuiCol.TitleBgCollapsed];
ImGui.PushStyleColor(ImGuiCol.TitleBg, new Vector4(titleBg.X, titleBg.Y, titleBg.Z, titleBg.W * alpha));
ImGui.PushStyleColor(ImGuiCol.TitleBgActive, new Vector4(titleBgActive.X, titleBgActive.Y, titleBgActive.Z, titleBgActive.W * alpha));
ImGui.PushStyleColor(ImGuiCol.TitleBgCollapsed, new Vector4(titleBgCollapsed.X, titleBgCollapsed.Y, titleBgCollapsed.Z, titleBgCollapsed.W * alpha));
_titleBarStylePopCount = 3;
}
private void DrawHeader(ChatChannelSnapshot channel)
{ {
var prefix = channel.Type == ChatChannelType.Zone ? "Zone" : "Syncshell"; var prefix = channel.Type == ChatChannelType.Zone ? "Zone" : "Syncshell";
Vector4 color; Vector4 color;
@@ -178,11 +329,18 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
if (channel.Type == ChatChannelType.Zone && channel.Descriptor.WorldId != 0) if (channel.Type == ChatChannelType.Zone && channel.Descriptor.WorldId != 0)
{ {
ImGui.SameLine(); ImGui.SameLine();
ImGui.TextUnformatted($"World #{channel.Descriptor.WorldId}"); var worldId = channel.Descriptor.WorldId;
var worldName = _dalamudUtilService.WorldData.Value.TryGetValue(worldId, out var name) ? name : $"World #{worldId}";
ImGui.TextUnformatted(worldName);
if (ImGui.IsItemHovered())
{
ImGui.SetTooltip($"World ID: {worldId}");
}
} }
var showInlineDisabled = string.Equals(channel.StatusText, ChatDisabledStatus, StringComparison.OrdinalIgnoreCase); var showInlineStatus = string.Equals(channel.StatusText, ChatDisabledStatus, StringComparison.OrdinalIgnoreCase)
if (showInlineDisabled) || string.Equals(channel.StatusText, ZoneUnavailableStatus, StringComparison.OrdinalIgnoreCase);
if (showInlineStatus)
{ {
ImGui.SameLine(); ImGui.SameLine();
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey3); ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey3);
@@ -324,6 +482,15 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
_refocusChatInputKey = null; _refocusChatInputKey = null;
} }
ImGui.InputText(inputId, ref draft, MaxMessageLength); ImGui.InputText(inputId, ref draft, MaxMessageLength);
if (ImGui.IsItemActive() || ImGui.IsItemFocused())
{
var drawList = ImGui.GetWindowDrawList();
var itemMin = ImGui.GetItemRectMin();
var itemMax = ImGui.GetItemRectMax();
var highlight = UIColors.Get("LightlessPurple").WithAlpha(0.35f);
var highlightU32 = ImGui.ColorConvertFloat4ToU32(highlight);
drawList.AddRect(itemMin, itemMax, highlightU32, style.FrameRounding, ImDrawFlags.None, Math.Max(1f, ImGuiHelpers.GlobalScale));
}
var enterPressed = ImGui.IsItemFocused() var enterPressed = ImGui.IsItemFocused()
&& (ImGui.IsKeyPressed(ImGuiKey.Enter) || ImGui.IsKeyPressed(ImGuiKey.KeypadEnter)); && (ImGui.IsKeyPressed(ImGuiKey.Enter) || ImGui.IsKeyPressed(ImGuiKey.KeypadEnter));
_draftMessages[channel.Key] = draft; _draftMessages[channel.Key] = draft;
@@ -480,7 +647,7 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
ImGui.Separator(); ImGui.Separator();
_uiSharedService.MediumText("Syncshell Chat Rules", UIColors.Get("LightlessYellow")); _uiSharedService.MediumText("Syncshell Chat Rules", UIColors.Get("LightlessYellow"));
_uiSharedService.DrawNoteLine("! ", UIColors.Get("LightlessYellow"), new SeStringUtils.RichTextEntry("Syncshell chats are self-moderated (their own set rules) by it's owner and appointed moderators. If they fail to enforce chat rules within their syncshell, the owner (and its moderators) may face punishment.")); _uiSharedService.DrawNoteLine("! ", UIColors.Get("LightlessYellow"), new SeStringUtils.RichTextEntry("Syncshell chats are self-moderated (their own set rules) by it's owner and appointed moderators."));
ImGui.Dummy(new Vector2(5)); ImGui.Dummy(new Vector2(5));
@@ -659,6 +826,16 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
ImGui.EndPopup(); ImGui.EndPopup();
} }
public override void PostDraw()
{
if (_pushedStyle)
{
ImGui.PopStyleVar(1);
_pushedStyle = false;
}
base.PostDraw();
}
private void OpenReportPopup(ChatChannelSnapshot channel, ChatMessageEntry message) private void OpenReportPopup(ChatChannelSnapshot channel, ChatMessageEntry message)
{ {
if (message.Payload is not { } payload) if (message.Payload is not { } payload)
@@ -1034,18 +1211,56 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
var groupSize = ImGui.GetItemRectSize(); var groupSize = ImGui.GetItemRectSize();
var minBlockX = cursorStart.X + groupSize.X + style.ItemSpacing.X; var minBlockX = cursorStart.X + groupSize.X + style.ItemSpacing.X;
var availableAfterGroup = contentRightX - (cursorStart.X + groupSize.X); var availableAfterGroup = contentRightX - (cursorStart.X + groupSize.X);
var lightfinderButtonWidth = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.PersonCirclePlus).X;
var settingsButtonWidth = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Cog).X; var settingsButtonWidth = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Cog).X;
var pinIcon = _isWindowPinned ? FontAwesomeIcon.Lock : FontAwesomeIcon.Unlock; var pinIcon = _isWindowPinned ? FontAwesomeIcon.Lock : FontAwesomeIcon.Unlock;
var pinButtonWidth = _uiSharedService.GetIconButtonSize(pinIcon).X; var pinButtonWidth = _uiSharedService.GetIconButtonSize(pinIcon).X;
var blockWidth = rulesButtonWidth + style.ItemSpacing.X + settingsButtonWidth + style.ItemSpacing.X + pinButtonWidth; var blockWidth = lightfinderButtonWidth + style.ItemSpacing.X + rulesButtonWidth + style.ItemSpacing.X + settingsButtonWidth + style.ItemSpacing.X + pinButtonWidth;
var desiredBlockX = availableAfterGroup > blockWidth + style.ItemSpacing.X var desiredBlockX = availableAfterGroup > blockWidth + style.ItemSpacing.X
? contentRightX - blockWidth ? contentRightX - blockWidth
: minBlockX; : minBlockX;
desiredBlockX = Math.Max(cursorStart.X, desiredBlockX); desiredBlockX = Math.Max(cursorStart.X, desiredBlockX);
var rulesPos = new Vector2(desiredBlockX, cursorStart.Y); var lightfinderPos = new Vector2(desiredBlockX, cursorStart.Y);
var settingsPos = new Vector2(desiredBlockX + rulesButtonWidth + style.ItemSpacing.X, cursorStart.Y); var rulesPos = new Vector2(lightfinderPos.X + lightfinderButtonWidth + style.ItemSpacing.X, cursorStart.Y);
var settingsPos = new Vector2(rulesPos.X + rulesButtonWidth + style.ItemSpacing.X, cursorStart.Y);
var pinPos = new Vector2(settingsPos.X + settingsButtonWidth + style.ItemSpacing.X, cursorStart.Y); var pinPos = new Vector2(settingsPos.X + settingsButtonWidth + style.ItemSpacing.X, cursorStart.Y);
ImGui.SameLine();
ImGui.SetCursorPos(lightfinderPos);
var lightfinderEnabled = _lightFinderService.IsBroadcasting;
var lightfinderColor = lightfinderEnabled ? UIColors.Get("LightlessGreen") : ImGuiColors.DalamudGrey3;
var lightfinderButtonSize = new Vector2(lightfinderButtonWidth, ImGui.GetFrameHeight());
ImGui.InvisibleButton("zone_chat_lightfinder_button", lightfinderButtonSize);
var lightfinderMin = ImGui.GetItemRectMin();
var lightfinderMax = ImGui.GetItemRectMax();
var iconSize = _uiSharedService.GetIconSize(FontAwesomeIcon.PersonCirclePlus);
var iconPos = new Vector2(
lightfinderMin.X + (lightfinderButtonSize.X - iconSize.X) * 0.5f,
lightfinderMin.Y + (lightfinderButtonSize.Y - iconSize.Y) * 0.5f);
using (_uiSharedService.IconFont.Push())
{
ImGui.GetWindowDrawList().AddText(iconPos, ImGui.GetColorU32(lightfinderColor), FontAwesomeIcon.PersonCirclePlus.ToIconString());
}
if (ImGui.IsItemClicked())
{
Mediator.Publish(new UiToggleMessage(typeof(LightFinderUI)));
}
if (ImGui.IsItemHovered())
{
var padding = new Vector2(8f * ImGuiHelpers.GlobalScale);
Selune.RegisterHighlight(
lightfinderMin - padding,
lightfinderMax + padding,
SeluneHighlightMode.Point,
exactSize: true,
clipToElement: true,
clipPadding: padding,
highlightColorOverride: lightfinderColor,
highlightAlphaOverride: 0.2f);
ImGui.SetTooltip("If Lightfinder is enabled, you will be able to see the character names of other Lightfinder users in the same zone when they send a message.");
}
ImGui.SameLine(); ImGui.SameLine();
ImGui.SetCursorPos(rulesPos); ImGui.SetCursorPos(rulesPos);
if (ImGui.Button("Rules", new Vector2(rulesButtonWidth, 0f))) if (ImGui.Button("Rules", new Vector2(rulesButtonWidth, 0f)))
@@ -1187,6 +1402,71 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
ImGui.SetTooltip("Toggles the timestamp prefix on messages."); ImGui.SetTooltip("Toggles the timestamp prefix on messages.");
} }
ImGui.Separator();
ImGui.TextUnformatted("Chat Visibility");
var autoHideCombat = chatConfig.HideInCombat;
if (ImGui.Checkbox("Hide in combat", ref autoHideCombat))
{
chatConfig.HideInCombat = autoHideCombat;
_chatConfigService.Save();
UpdateHideState();
}
if (ImGui.IsItemHovered())
{
ImGui.SetTooltip("Temporarily hides the chat window while in combat.");
}
var autoHideDuty = chatConfig.HideInDuty;
if (ImGui.Checkbox("Hide in duty (Not in field operations)", ref autoHideDuty))
{
chatConfig.HideInDuty = autoHideDuty;
_chatConfigService.Save();
UpdateHideState();
}
if (ImGui.IsItemHovered())
{
ImGui.SetTooltip("Hides the chat window inside duties.");
}
var showWhenUiHidden = chatConfig.ShowWhenUiHidden;
if (ImGui.Checkbox("Show when game UI is hidden", ref showWhenUiHidden))
{
chatConfig.ShowWhenUiHidden = showWhenUiHidden;
_chatConfigService.Save();
UpdateHideState();
}
if (ImGui.IsItemHovered())
{
ImGui.SetTooltip("Allow the chat window to remain visible when the game UI is hidden.");
}
var showInCutscenes = chatConfig.ShowInCutscenes;
if (ImGui.Checkbox("Show in cutscenes", ref showInCutscenes))
{
chatConfig.ShowInCutscenes = showInCutscenes;
_chatConfigService.Save();
UpdateHideState();
}
if (ImGui.IsItemHovered())
{
ImGui.SetTooltip("Allow the chat window to remain visible during cutscenes.");
}
var showInGpose = chatConfig.ShowInGpose;
if (ImGui.Checkbox("Show in group pose", ref showInGpose))
{
chatConfig.ShowInGpose = showInGpose;
_chatConfigService.Save();
UpdateHideState();
}
if (ImGui.IsItemHovered())
{
ImGui.SetTooltip("Allow the chat window to remain visible in /gpose.");
}
ImGui.Separator();
var fontScale = Math.Clamp(chatConfig.ChatFontScale, MinChatFontScale, MaxChatFontScale); var fontScale = Math.Clamp(chatConfig.ChatFontScale, MinChatFontScale, MaxChatFontScale);
var fontScaleChanged = ImGui.SliderFloat("Message font scale", ref fontScale, MinChatFontScale, MaxChatFontScale, "%.2fx"); var fontScaleChanged = ImGui.SliderFloat("Message font scale", ref fontScale, MinChatFontScale, MaxChatFontScale, "%.2fx");
var resetFontScale = ImGui.IsItemClicked(ImGuiMouseButton.Right); var resetFontScale = ImGui.IsItemClicked(ImGuiMouseButton.Right);
@@ -1226,9 +1506,55 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
ImGui.SetTooltip("Adjust chat window transparency.\nRight-click to reset to default."); ImGui.SetTooltip("Adjust chat window transparency.\nRight-click to reset to default.");
} }
var fadeUnfocused = chatConfig.FadeWhenUnfocused;
if (ImGui.Checkbox("Fade window when unfocused", ref fadeUnfocused))
{
chatConfig.FadeWhenUnfocused = fadeUnfocused;
_chatConfigService.Save();
}
if (ImGui.IsItemHovered())
{
ImGui.SetTooltip("When enabled, the chat window fades after it loses focus.\nHovering the window restores focus.");
}
ImGui.BeginDisabled(!fadeUnfocused);
var unfocusedOpacity = Math.Clamp(chatConfig.UnfocusedWindowOpacity, MinWindowOpacity, MaxWindowOpacity);
var unfocusedChanged = ImGui.SliderFloat("Unfocused transparency", ref unfocusedOpacity, MinWindowOpacity, MaxWindowOpacity, "%.2f");
var resetUnfocused = ImGui.IsItemClicked(ImGuiMouseButton.Right);
if (resetUnfocused)
{
unfocusedOpacity = DefaultUnfocusedWindowOpacity;
unfocusedChanged = true;
}
if (unfocusedChanged)
{
chatConfig.UnfocusedWindowOpacity = unfocusedOpacity;
_chatConfigService.Save();
}
if (ImGui.IsItemHovered())
{
ImGui.SetTooltip("Target transparency while the chat window is unfocused.\nRight-click to reset to default.");
}
ImGui.EndDisabled();
ImGui.EndPopup(); ImGui.EndPopup();
} }
private static float MoveTowards(float current, float target, float maxDelta)
{
if (current < target)
{
return MathF.Min(current + maxDelta, target);
}
if (current > target)
{
return MathF.Max(current - maxDelta, target);
}
return target;
}
private void ToggleChatConnection(bool currentlyEnabled) private void ToggleChatConnection(bool currentlyEnabled)
{ {
_ = Task.Run(async () => _ = Task.Run(async () =>
@@ -1244,7 +1570,7 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
}); });
} }
private void DrawChannelButtons(IReadOnlyList<ChatChannelSnapshot> channels) private unsafe void DrawChannelButtons(IReadOnlyList<ChatChannelSnapshot> channels)
{ {
var style = ImGui.GetStyle(); var style = ImGui.GetStyle();
var baseFramePadding = style.FramePadding; var baseFramePadding = style.FramePadding;
@@ -1305,6 +1631,8 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
{ {
if (child) if (child)
{ {
var dragActive = _dragChannelKey is not null && ImGui.IsMouseDragging(ImGuiMouseButton.Left);
var hoveredTargetThisFrame = false;
var first = true; var first = true;
foreach (var channel in channels) foreach (var channel in channels)
{ {
@@ -1315,6 +1643,7 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
var showBadge = !isSelected && channel.UnreadCount > 0; var showBadge = !isSelected && channel.UnreadCount > 0;
var isZoneChannel = channel.Type == ChatChannelType.Zone; var isZoneChannel = channel.Type == ChatChannelType.Zone;
(string Text, Vector2 TextSize, float Width, float Height)? badgeMetrics = null; (string Text, Vector2 TextSize, float Width, float Height)? badgeMetrics = null;
var channelLabel = GetChannelTabLabel(channel);
var normal = isSelected ? UIColors.Get("LightlessPurpleDefault") : UIColors.Get("ButtonDefault"); var normal = isSelected ? UIColors.Get("LightlessPurpleDefault") : UIColors.Get("ButtonDefault");
var hovered = isSelected var hovered = isSelected
@@ -1343,7 +1672,7 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
badgeMetrics = (badgeText, badgeTextSize, badgeWidth, badgeHeight); badgeMetrics = (badgeText, badgeTextSize, badgeWidth, badgeHeight);
} }
var clicked = ImGui.Button($"{channel.DisplayName}##chat_channel_{channel.Key}"); var clicked = ImGui.Button($"{channelLabel}##chat_channel_{channel.Key}");
if (showBadge) if (showBadge)
{ {
@@ -1359,10 +1688,77 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
_scrollToBottom = true; _scrollToBottom = true;
} }
if (ShouldShowChannelTabContextMenu(channel)
&& ImGui.BeginPopupContextItem($"chat_channel_ctx##{channel.Key}"))
{
DrawChannelTabContextMenu(channel);
ImGui.EndPopup();
}
if (ImGui.BeginDragDropSource(ImGuiDragDropFlags.None))
{
if (!string.Equals(_dragChannelKey, channel.Key, StringComparison.Ordinal))
{
_dragHoverKey = null;
}
_dragChannelKey = channel.Key;
ImGui.SetDragDropPayload(ChannelDragPayloadId, null, 0);
ImGui.TextUnformatted(channelLabel);
ImGui.EndDragDropSource();
}
var isDragTarget = false;
if (ImGui.BeginDragDropTarget())
{
var acceptFlags = ImGuiDragDropFlags.AcceptBeforeDelivery | ImGuiDragDropFlags.AcceptNoDrawDefaultRect;
var payload = ImGui.AcceptDragDropPayload(ChannelDragPayloadId, acceptFlags);
if (!payload.IsNull && _dragChannelKey is { } draggedKey
&& !string.Equals(draggedKey, channel.Key, StringComparison.Ordinal))
{
isDragTarget = true;
if (!string.Equals(_dragHoverKey, channel.Key, StringComparison.Ordinal))
{
_dragHoverKey = channel.Key;
_zoneChatService.MoveChannel(draggedKey, channel.Key);
}
}
ImGui.EndDragDropTarget();
}
var isHoveredDuringDrag = dragActive
&& ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenBlockedByActiveItem | ImGuiHoveredFlags.AllowWhenOverlapped);
if (!isDragTarget && isHoveredDuringDrag
&& !string.Equals(_dragChannelKey, channel.Key, StringComparison.Ordinal))
{
isDragTarget = true;
if (!string.Equals(_dragHoverKey, channel.Key, StringComparison.Ordinal))
{
_dragHoverKey = channel.Key;
_zoneChatService.MoveChannel(_dragChannelKey!, channel.Key);
}
}
var drawList = ImGui.GetWindowDrawList(); var drawList = ImGui.GetWindowDrawList();
var itemMin = ImGui.GetItemRectMin(); var itemMin = ImGui.GetItemRectMin();
var itemMax = ImGui.GetItemRectMax(); var itemMax = ImGui.GetItemRectMax();
if (isHoveredDuringDrag)
{
var highlight = UIColors.Get("LightlessPurple").WithAlpha(0.35f);
var highlightU32 = ImGui.ColorConvertFloat4ToU32(highlight);
drawList.AddRectFilled(itemMin, itemMax, highlightU32, style.FrameRounding);
drawList.AddRect(itemMin, itemMax, highlightU32, style.FrameRounding, ImDrawFlags.None, Math.Max(1f, ImGuiHelpers.GlobalScale));
}
if (isDragTarget)
{
hoveredTargetThisFrame = true;
}
if (isZoneChannel) if (isZoneChannel)
{ {
var borderColor = UIColors.Get("LightlessOrange"); var borderColor = UIColors.Get("LightlessOrange");
@@ -1390,6 +1786,11 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
first = false; first = false;
} }
if (dragActive && !hoveredTargetThisFrame)
{
_dragHoverKey = null;
}
if (_pendingChannelScroll.HasValue) if (_pendingChannelScroll.HasValue)
{ {
ImGui.SetScrollX(_pendingChannelScroll.Value); ImGui.SetScrollX(_pendingChannelScroll.Value);
@@ -1430,9 +1831,123 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
_channelScroll = currentScroll; _channelScroll = currentScroll;
_channelScrollMax = maxScroll; _channelScrollMax = maxScroll;
if (_dragChannelKey is not null && !ImGui.IsMouseDown(ImGuiMouseButton.Left))
{
_dragChannelKey = null;
_dragHoverKey = null;
}
ImGui.SetCursorPosY(ImGui.GetCursorPosY() - style.ItemSpacing.Y * 0.3f); ImGui.SetCursorPosY(ImGui.GetCursorPosY() - style.ItemSpacing.Y * 0.3f);
} }
private string GetChannelTabLabel(ChatChannelSnapshot channel)
{
if (channel.Type != ChatChannelType.Group)
{
return channel.DisplayName;
}
if (!_chatConfigService.Current.PreferNotesForChannels.TryGetValue(channel.Key, out var preferNote) || !preferNote)
{
return channel.DisplayName;
}
var note = GetChannelNote(channel);
if (string.IsNullOrWhiteSpace(note))
{
return channel.DisplayName;
}
return TruncateChannelNoteForTab(note);
}
private static string TruncateChannelNoteForTab(string note)
{
if (note.Length <= MaxChannelNoteTabLength)
{
return note;
}
var ellipsis = "...";
var maxPrefix = Math.Max(0, MaxChannelNoteTabLength - ellipsis.Length);
return note[..maxPrefix] + ellipsis;
}
private bool ShouldShowChannelTabContextMenu(ChatChannelSnapshot channel)
{
if (channel.Type != ChatChannelType.Group)
{
return false;
}
if (_chatConfigService.Current.PreferNotesForChannels.TryGetValue(channel.Key, out var preferNote) && preferNote)
{
return true;
}
var note = GetChannelNote(channel);
return !string.IsNullOrWhiteSpace(note);
}
private void DrawChannelTabContextMenu(ChatChannelSnapshot channel)
{
var preferNote = _chatConfigService.Current.PreferNotesForChannels.TryGetValue(channel.Key, out var value) && value;
var note = GetChannelNote(channel);
var hasNote = !string.IsNullOrWhiteSpace(note);
if (preferNote || hasNote)
{
var label = preferNote ? "Prefer Name Instead" : "Prefer Note Instead";
if (ImGui.MenuItem(label))
{
SetPreferNoteForChannel(channel.Key, !preferNote);
}
}
if (preferNote)
{
ImGui.Separator();
ImGui.TextDisabled("Name:");
ImGui.TextWrapped(channel.DisplayName);
}
if (hasNote)
{
ImGui.Separator();
ImGui.TextDisabled("Note:");
ImGui.TextWrapped(note);
}
}
private string? GetChannelNote(ChatChannelSnapshot channel)
{
if (channel.Type != ChatChannelType.Group)
{
return null;
}
var gid = channel.Descriptor.CustomKey;
if (string.IsNullOrWhiteSpace(gid))
{
return null;
}
return _serverConfigurationManager.GetNoteForGid(gid);
}
private void SetPreferNoteForChannel(string channelKey, bool preferNote)
{
if (preferNote)
{
_chatConfigService.Current.PreferNotesForChannels[channelKey] = true;
}
else
{
_chatConfigService.Current.PreferNotesForChannels.Remove(channelKey);
}
_chatConfigService.Save();
}
private void DrawSystemEntry(ChatMessageEntry entry) private void DrawSystemEntry(ChatMessageEntry entry)
{ {
var system = entry.SystemMessage; var system = entry.SystemMessage;

View File

@@ -584,7 +584,10 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
OnGroupSendInfo((dto) => _ = Client_GroupSendInfo(dto)); OnGroupSendInfo((dto) => _ = Client_GroupSendInfo(dto));
OnGroupUpdateProfile((dto) => _ = Client_GroupSendProfile(dto)); OnGroupUpdateProfile((dto) => _ = Client_GroupSendProfile(dto));
OnGroupChangeUserPairPermissions((dto) => _ = Client_GroupChangeUserPairPermissions(dto)); OnGroupChangeUserPairPermissions((dto) => _ = Client_GroupChangeUserPairPermissions(dto));
if (!_initialized)
{
_lightlessHub.On(nameof(Client_ChatReceive), (Func<ChatMessageDto, Task>)Client_ChatReceive); _lightlessHub.On(nameof(Client_ChatReceive), (Func<ChatMessageDto, Task>)Client_ChatReceive);
}
OnGposeLobbyJoin((dto) => _ = Client_GposeLobbyJoin(dto)); OnGposeLobbyJoin((dto) => _ = Client_GposeLobbyJoin(dto));
OnGposeLobbyLeave((dto) => _ = Client_GposeLobbyLeave(dto)); OnGposeLobbyLeave((dto) => _ = Client_GposeLobbyLeave(dto));