Compare commits
164 Commits
2.0.0.69-D
...
1.42.0.70-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
af607e4380 | ||
|
|
29e6555480 | ||
|
|
9402731b2b | ||
|
|
4d0bf2d57e | ||
| 7f74f88302 | |||
|
|
934cdfbcf0 | ||
|
|
d2a68e6533 | ||
| 20008f904d | |||
|
|
54b50886c0 | ||
|
|
234fe5d360 | ||
|
|
05770d9a5b | ||
|
|
f43fb28257 | ||
|
|
465da1bdf2 | ||
|
|
0a592c87dd | ||
|
|
321a9c8b55 | ||
|
|
4aa09ce05e | ||
|
|
68dc8aef2f | ||
|
|
56143c5f3d | ||
|
|
fea633b6f6 | ||
| 91739536bf | |||
|
|
ae9df103f3 | ||
|
|
bade5ab6f5 | ||
| 5e22f3bff0 | |||
|
|
ed099f322d | ||
|
|
116e65b220 | ||
|
|
ee175efe41 | ||
|
|
6ca491ac30 | ||
| 4ffc2247b2 | |||
| 7b4e42c487 | |||
|
|
5e2afc8bfe | ||
| 6d57813ef2 | |||
|
|
8b75063b9d | ||
|
|
99b49762bb | ||
|
|
35e35591f5 | ||
|
|
e3c04e31e7 | ||
|
|
f7fb609c71 | ||
|
|
d766c2c42e | ||
| 1d212437f5 | |||
|
|
9d1d6783ce | ||
| f47df8fac2 | |||
| ecc1e7107f | |||
| 1cc8339307 | |||
|
|
6522b586d5 | ||
|
|
8b9e35283d | ||
|
|
755bae1294 | ||
|
|
a41f419076 | ||
|
|
dec6c4900b | ||
|
|
5dabd23d93 | ||
|
|
0dd520d926 | ||
|
|
4e4d19ad00 | ||
|
|
d5c11cd22f | ||
| 4444a88746 | |||
|
|
bdfcf254a8 | ||
|
|
eb11ff0b4c | ||
|
|
ee1fcb5661 | ||
|
|
44e91bef8f | ||
|
|
6891424b0d | ||
|
|
6395b1eb52 | ||
| 0671c46e5d | |||
|
|
09b78e1896 | ||
|
|
1b2db4c698 | ||
| 6cf0e3daed | |||
|
|
2e14fc2f8f | ||
|
|
675918624d | ||
|
|
25f0d41581 | ||
|
|
1cb326070b | ||
|
|
b444782b76 | ||
|
|
feec5e8ff3 | ||
|
|
cc1f381687 | ||
|
|
69b504c42f | ||
| 541d17132d | |||
| 1c36db97dc | |||
|
|
c335489cee | ||
|
|
6734021b89 | ||
|
|
46a8fc72cb | ||
|
|
962567fbfe | ||
|
|
0e076f6290 | ||
|
|
72cd5006db | ||
| 023ca2013e | |||
| a77261a096 | |||
| febc47442a | |||
|
|
481bc99dcd | ||
|
|
9d6a0a1257 | ||
| 8076d63ce2 | |||
| ba5c8b588e | |||
| 91393bf4a1 | |||
|
|
e0e2304253 | ||
|
|
a9181d2592 | ||
|
|
cab13874d8 | ||
|
|
04cd09cbb9 | ||
|
|
0b36c1bdc2 | ||
|
|
1e88fe0cf3 | ||
| 740b58afc4 | |||
|
|
9e12725f89 | ||
|
|
aa04ab05ab | ||
| 28967d6e17 | |||
| d995afcf48 | |||
| 5ab67c70d6 | |||
| 8cc83bce79 | |||
| 1cdc0a90f9 | |||
|
|
e350e8007a | ||
|
|
7a9ade95c3 | ||
|
|
01607c275a | ||
|
|
1e6109d1e6 | ||
|
|
961092ab87 | ||
|
|
36166f1399 | ||
| d057c638ab | |||
| 28d9110cb0 | |||
| ef592032b3 | |||
|
|
9c794137c1 | ||
|
|
4a256f7807 | ||
|
|
25756561b9 | ||
|
|
e8c546c128 | ||
| d4ba1cf437 | |||
| e0d1f98c70 | |||
|
|
1862689b1b | ||
| 325dc8947d | |||
|
|
95e7f2daa7 | ||
|
|
41a303dc91 | ||
|
|
25b03aea15 | ||
|
|
b6564156f0 | ||
|
|
f89ce900c7 | ||
| 299abc21ee | |||
|
|
c02a8ed2ee | ||
|
|
8692e877cf | ||
|
|
7de72471bb | ||
|
|
d7182e9d57 | ||
| 2b02de731a | |||
|
|
e9082ab8d0 | ||
| 2a06a11cbc | |||
|
|
557121a9b7 | ||
| b22140a8d4 | |||
| 4db468a480 | |||
| 8d8f8d20cd | |||
| 3722b79615 | |||
| cf97e7e800 | |||
|
|
1d672d2552 | ||
|
|
35636f27f6 | ||
|
|
1b686e45dc | ||
|
|
b6aa2bebb1 | ||
|
|
cfc9f60176 | ||
|
|
d4dca455ba | ||
| 76c2777f00 | |||
|
|
0af2a6134b | ||
|
|
6e3c60f627 | ||
|
|
5feb74c1c0 | ||
|
|
c1770528f3 | ||
|
|
bf139c128b | ||
|
|
b3cc41382f | ||
|
|
7c4d0fd5e9 | ||
|
|
c37e3badf1 | ||
| f4478f653a | |||
|
|
3f85852618 | ||
|
|
3e626c5e47 | ||
|
|
9a846a37d4 | ||
|
|
177534d78b | ||
|
|
de75b90703 | ||
|
|
c16891021c | ||
| d19d1c0a3a | |||
|
|
cabc4ec0fe | ||
|
|
8bccdc5ef1 | ||
|
|
ce5f8a43a2 | ||
|
|
437731749f | ||
|
|
55e78e088a |
Submodule LightlessAPI updated: 56566003e0...8e4432af45
@@ -6,7 +6,6 @@ using LightlessSync.Utils;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Immutable;
|
||||
using System.IO;
|
||||
|
||||
namespace LightlessSync.FileCache;
|
||||
|
||||
@@ -22,7 +21,6 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
||||
private CancellationTokenSource _scanCancellationTokenSource = 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"];
|
||||
private static readonly HashSet<string> AllowedFileExtensionSet = new(AllowedFileExtensions, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public CacheMonitor(ILogger<CacheMonitor> logger, IpcManager ipcManager, LightlessConfigService configService,
|
||||
FileCacheManager fileDbManager, LightlessMediator mediator, PerformanceCollectorService performanceCollector, DalamudUtilService dalamudUtil,
|
||||
@@ -165,7 +163,7 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
||||
{
|
||||
Logger.LogTrace("Lightless FSW: FileChanged: {change} => {path}", e.ChangeType, e.FullPath);
|
||||
|
||||
if (!HasAllowedExtension(e.FullPath)) return;
|
||||
if (!AllowedFileExtensions.Any(ext => e.FullPath.EndsWith(ext, StringComparison.OrdinalIgnoreCase))) return;
|
||||
|
||||
lock (_watcherChanges)
|
||||
{
|
||||
@@ -209,7 +207,7 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
||||
private void Fs_Changed(object sender, FileSystemEventArgs e)
|
||||
{
|
||||
if (Directory.Exists(e.FullPath)) return;
|
||||
if (!HasAllowedExtension(e.FullPath)) return;
|
||||
if (!AllowedFileExtensions.Any(ext => e.FullPath.EndsWith(ext, StringComparison.OrdinalIgnoreCase))) return;
|
||||
|
||||
if (e.ChangeType is not (WatcherChangeTypes.Changed or WatcherChangeTypes.Deleted or WatcherChangeTypes.Created))
|
||||
return;
|
||||
@@ -233,7 +231,7 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
||||
{
|
||||
foreach (var file in directoryFiles)
|
||||
{
|
||||
if (!HasAllowedExtension(file)) continue;
|
||||
if (!AllowedFileExtensions.Any(ext => file.EndsWith(ext, StringComparison.OrdinalIgnoreCase))) continue;
|
||||
var oldPath = file.Replace(e.FullPath, e.OldFullPath, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
_watcherChanges.Remove(oldPath);
|
||||
@@ -245,7 +243,7 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!HasAllowedExtension(e.FullPath)) return;
|
||||
if (!AllowedFileExtensions.Any(ext => e.FullPath.EndsWith(ext, StringComparison.OrdinalIgnoreCase))) return;
|
||||
|
||||
lock (_watcherChanges)
|
||||
{
|
||||
@@ -265,17 +263,6 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
||||
public FileSystemWatcher? PenumbraWatcher { 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()
|
||||
{
|
||||
_lightlessFswCts = _lightlessFswCts.CancelRecreate();
|
||||
@@ -619,7 +606,7 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
||||
[
|
||||
.. Directory.GetFiles(folder, "*.*", SearchOption.AllDirectories)
|
||||
.AsParallel()
|
||||
.Where(f => HasAllowedExtension(f)
|
||||
.Where(f => AllowedFileExtensions.Any(e => f.EndsWith(e, StringComparison.OrdinalIgnoreCase))
|
||||
&& !f.Contains(@"\bg\", StringComparison.OrdinalIgnoreCase)
|
||||
&& !f.Contains(@"\bgcommon\", StringComparison.OrdinalIgnoreCase)
|
||||
&& !f.Contains(@"\ui\", StringComparison.OrdinalIgnoreCase)),
|
||||
|
||||
@@ -59,7 +59,7 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
|
||||
_playerRelatedPointers.Remove(msg.GameObjectHandler);
|
||||
});
|
||||
|
||||
foreach (var descriptor in _actorObjectService.ObjectDescriptors)
|
||||
foreach (var descriptor in _actorObjectService.PlayerDescriptors)
|
||||
{
|
||||
HandleActorTracked(descriptor);
|
||||
}
|
||||
@@ -291,7 +291,7 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
|
||||
}
|
||||
|
||||
var activeDescriptors = new Dictionary<nint, ObjectKind>();
|
||||
foreach (var descriptor in _actorObjectService.ObjectDescriptors)
|
||||
foreach (var descriptor in _actorObjectService.PlayerDescriptors)
|
||||
{
|
||||
if (TryResolveObjectKind(descriptor, out var resolvedKind))
|
||||
{
|
||||
@@ -372,9 +372,6 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
|
||||
|
||||
private void HandleActorTracked(ActorObjectService.ActorDescriptor descriptor)
|
||||
{
|
||||
if (descriptor.IsInGpose)
|
||||
return;
|
||||
|
||||
if (!TryResolveObjectKind(descriptor, out var resolvedKind))
|
||||
return;
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ namespace LightlessSync.Interop.Ipc;
|
||||
|
||||
public sealed class IpcCallerBrio : IpcServiceBase
|
||||
{
|
||||
private static readonly IpcServiceDescriptor BrioDescriptor = new("Brio", "Brio", new Version(0, 0, 0, 0));
|
||||
private static readonly IpcServiceDescriptor BrioDescriptor = new("Brio", "Brio", new Version(3, 0, 0, 0));
|
||||
|
||||
private readonly ILogger<IpcCallerBrio> _logger;
|
||||
private readonly DalamudUtilService _dalamudUtilService;
|
||||
@@ -144,7 +144,7 @@ public sealed class IpcCallerBrio : IpcServiceBase
|
||||
try
|
||||
{
|
||||
var version = _apiVersion.Invoke();
|
||||
return version.Breaking == 3 && version.Feature >= 0
|
||||
return version.Item1 == 3 && version.Item2 >= 0
|
||||
? IpcConnectionState.Available
|
||||
: IpcConnectionState.VersionMismatch;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Dalamud.Plugin;
|
||||
using Dalamud.Game.ClientState.Objects.SubKinds;
|
||||
using Dalamud.Plugin;
|
||||
using Dalamud.Plugin.Ipc;
|
||||
using LightlessSync.Interop.Ipc.Framework;
|
||||
using LightlessSync.Services;
|
||||
@@ -12,7 +13,7 @@ public sealed class IpcCallerMoodles : IpcServiceBase
|
||||
private static readonly IpcServiceDescriptor MoodlesDescriptor = new("Moodles", "Moodles", new Version(0, 0, 0, 0));
|
||||
|
||||
private readonly ICallGateSubscriber<int> _moodlesApiVersion;
|
||||
private readonly ICallGateSubscriber<nint, object> _moodlesOnChange;
|
||||
private readonly ICallGateSubscriber<IPlayerCharacter, object> _moodlesOnChange;
|
||||
private readonly ICallGateSubscriber<nint, string> _moodlesGetStatus;
|
||||
private readonly ICallGateSubscriber<nint, string, object> _moodlesSetStatus;
|
||||
private readonly ICallGateSubscriber<nint, object> _moodlesRevertStatus;
|
||||
@@ -28,7 +29,7 @@ public sealed class IpcCallerMoodles : IpcServiceBase
|
||||
_lightlessMediator = lightlessMediator;
|
||||
|
||||
_moodlesApiVersion = pi.GetIpcSubscriber<int>("Moodles.Version");
|
||||
_moodlesOnChange = pi.GetIpcSubscriber<nint, object>("Moodles.StatusManagerModified");
|
||||
_moodlesOnChange = pi.GetIpcSubscriber<IPlayerCharacter, object>("Moodles.StatusManagerModified");
|
||||
_moodlesGetStatus = pi.GetIpcSubscriber<nint, string>("Moodles.GetStatusManagerByPtrV2");
|
||||
_moodlesSetStatus = pi.GetIpcSubscriber<nint, string, object>("Moodles.SetStatusManagerByPtrV2");
|
||||
_moodlesRevertStatus = pi.GetIpcSubscriber<nint, object>("Moodles.ClearStatusManagerByPtrV2");
|
||||
@@ -38,9 +39,9 @@ public sealed class IpcCallerMoodles : IpcServiceBase
|
||||
CheckAPI();
|
||||
}
|
||||
|
||||
private void OnMoodlesChange(nint address)
|
||||
private void OnMoodlesChange(IPlayerCharacter character)
|
||||
{
|
||||
_lightlessMediator.Publish(new MoodlesMessage(address));
|
||||
_lightlessMediator.Publish(new MoodlesMessage(character.Address));
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
@@ -106,7 +107,7 @@ public sealed class IpcCallerMoodles : IpcServiceBase
|
||||
|
||||
try
|
||||
{
|
||||
return _moodlesApiVersion.InvokeFunc() >= 4
|
||||
return _moodlesApiVersion.InvokeFunc() == 3
|
||||
? IpcConnectionState.Available
|
||||
: IpcConnectionState.VersionMismatch;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace LightlessSync.LightlessConfiguration.Configurations;
|
||||
|
||||
@@ -11,16 +10,7 @@ public sealed class ChatConfig : ILightlessConfiguration
|
||||
public bool ShowRulesOverlayOnOpen { get; set; } = true;
|
||||
public bool ShowMessageTimestamps { get; set; } = true;
|
||||
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 AutoOpenChatOnPluginLoad { get; set; } = false;
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -49,8 +49,7 @@ public class LightlessConfig : ILightlessConfiguration
|
||||
public int DownloadSpeedLimitInBytes { get; set; } = 0;
|
||||
public DownloadSpeeds DownloadSpeedType { get; set; } = DownloadSpeeds.MBps;
|
||||
public bool PreferNotesOverNamesForVisible { get; set; } = false;
|
||||
public VisiblePairSortMode VisiblePairSortMode { get; set; } = VisiblePairSortMode.Alphabetical;
|
||||
public OnlinePairSortMode OnlinePairSortMode { get; set; } = OnlinePairSortMode.Alphabetical;
|
||||
public VisiblePairSortMode VisiblePairSortMode { get; set; } = VisiblePairSortMode.Default;
|
||||
public float ProfileDelay { get; set; } = 1.5f;
|
||||
public bool ProfilePopoutRight { get; set; } = false;
|
||||
public bool ProfilesAllowNsfw { get; set; } = false;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<PropertyGroup>
|
||||
<Authors></Authors>
|
||||
<Company></Company>
|
||||
<Version>2.0.0.69</Version>
|
||||
<Version>1.42.0.70</Version>
|
||||
<Description></Description>
|
||||
<Copyright></Copyright>
|
||||
<PackageProjectUrl>https://github.com/Light-Public-Syncshells/LightlessClient</PackageProjectUrl>
|
||||
|
||||
@@ -119,7 +119,6 @@ public class PlayerDataFactory
|
||||
CharacterDataFragment fragment = objectKind == ObjectKind.Player ? new CharacterDataFragmentPlayer() : new();
|
||||
|
||||
_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
|
||||
await _dalamudUtil.WaitWhileCharacterIsDrawing(_logger, playerRelatedObject, Guid.NewGuid(), 30000, ct: ct).ConfigureAwait(false);
|
||||
@@ -133,6 +132,11 @@ public class PlayerDataFactory
|
||||
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
Dictionary<string, List<ushort>>? boneIndices =
|
||||
objectKind != ObjectKind.Player
|
||||
? null
|
||||
: await _dalamudUtil.RunOnFrameworkThread(() => _modelAnalyzer.GetSkeletonBoneIndices(playerRelatedObject)).ConfigureAwait(false);
|
||||
|
||||
DateTime start = DateTime.UtcNow;
|
||||
|
||||
// penumbra call, it's currently broken
|
||||
@@ -150,21 +154,11 @@ public class PlayerDataFactory
|
||||
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
if (logDebug)
|
||||
_logger.LogDebug("== Static Replacements ==");
|
||||
foreach (var replacement in fragment.FileReplacements.Where(i => i.HasFileReplacement).OrderBy(i => i.GamePaths.First(), StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
_logger.LogDebug("== Static Replacements ==");
|
||||
foreach (var replacement in fragment.FileReplacements.Where(i => i.HasFileReplacement).OrderBy(i => i.GamePaths.First(), StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
_logger.LogDebug("=> {repl}", replacement);
|
||||
ct.ThrowIfCancellationRequested();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var replacement in fragment.FileReplacements.Where(i => i.HasFileReplacement))
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
}
|
||||
_logger.LogDebug("=> {repl}", replacement);
|
||||
ct.ThrowIfCancellationRequested();
|
||||
}
|
||||
|
||||
await _transientResourceManager.WaitForRecording(ct).ConfigureAwait(false);
|
||||
@@ -196,21 +190,11 @@ public class PlayerDataFactory
|
||||
var transientPaths = ManageSemiTransientData(objectKind);
|
||||
var resolvedTransientPaths = await GetFileReplacementsFromPaths(transientPaths, new HashSet<string>(StringComparer.Ordinal)).ConfigureAwait(false);
|
||||
|
||||
if (logDebug)
|
||||
_logger.LogDebug("== Transient Replacements ==");
|
||||
foreach (var replacement in resolvedTransientPaths.Select(c => new FileReplacement([.. c.Value], c.Key)).OrderBy(f => f.ResolvedPath, StringComparer.Ordinal))
|
||||
{
|
||||
_logger.LogDebug("== Transient Replacements ==");
|
||||
foreach (var replacement in resolvedTransientPaths.Select(c => new FileReplacement([.. c.Value], c.Key)).OrderBy(f => f.ResolvedPath, StringComparer.Ordinal))
|
||||
{
|
||||
_logger.LogDebug("=> {repl}", replacement);
|
||||
fragment.FileReplacements.Add(replacement);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var replacement in resolvedTransientPaths.Select(c => new FileReplacement([.. c.Value], c.Key)))
|
||||
{
|
||||
fragment.FileReplacements.Add(replacement);
|
||||
}
|
||||
_logger.LogDebug("=> {repl}", replacement);
|
||||
fragment.FileReplacements.Add(replacement);
|
||||
}
|
||||
|
||||
// clean up all semi transient resources that don't have any file replacement (aka null resolve)
|
||||
@@ -268,26 +252,11 @@ public class PlayerDataFactory
|
||||
|
||||
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)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (hasPapFiles)
|
||||
{
|
||||
await VerifyPlayerAnimationBones(boneIndices, (fragment as CharacterDataFragmentPlayer)!, ct).ConfigureAwait(false);
|
||||
}
|
||||
await VerifyPlayerAnimationBones(boneIndices, (fragment as CharacterDataFragmentPlayer)!, ct).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException e)
|
||||
{
|
||||
@@ -309,16 +278,12 @@ public class PlayerDataFactory
|
||||
{
|
||||
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));
|
||||
}
|
||||
|
||||
var maxPlayerBoneIndex = boneIndices.SelectMany(kvp => kvp.Value).DefaultIfEmpty().Max();
|
||||
if (maxPlayerBoneIndex <= 0) return;
|
||||
if (boneIndices.All(u => u.Value.Count == 0)) return;
|
||||
|
||||
int noValidationFailed = 0;
|
||||
foreach (var file in fragment.FileReplacements.Where(f => !f.IsFileSwap && f.GamePaths.First().EndsWith("pap", StringComparison.OrdinalIgnoreCase)).ToList())
|
||||
@@ -338,13 +303,12 @@ public class PlayerDataFactory
|
||||
|
||||
_logger.LogDebug("Verifying bone indices for {path}, found {x} skeletons", file.ResolvedPath, skeletonIndices.Count);
|
||||
|
||||
foreach (var boneCount in skeletonIndices)
|
||||
foreach (var boneCount in skeletonIndices.Select(k => k).ToList())
|
||||
{
|
||||
var maxAnimationIndex = boneCount.Value.DefaultIfEmpty().Max();
|
||||
if (maxAnimationIndex > maxPlayerBoneIndex)
|
||||
if (boneCount.Value.Max() > boneIndices.SelectMany(b => b.Value).Max())
|
||||
{
|
||||
_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, maxAnimationIndex, maxPlayerBoneIndex);
|
||||
file.ResolvedPath, boneCount.Key, boneCount.Value.Max(), boneIndices.SelectMany(b => b.Value).Max());
|
||||
validationFailed = true;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -16,8 +16,6 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
||||
private readonly Func<IntPtr> _getAddress;
|
||||
private readonly bool _isOwnedObject;
|
||||
private readonly PerformanceCollectorService _performanceCollector;
|
||||
private readonly object _frameworkUpdateGate = new();
|
||||
private bool _frameworkUpdateSubscribed;
|
||||
private byte _classJob = 0;
|
||||
private Task? _delayedZoningTask;
|
||||
private bool _haltProcessing = false;
|
||||
@@ -49,10 +47,7 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
||||
});
|
||||
}
|
||||
|
||||
if (_isOwnedObject)
|
||||
{
|
||||
EnableFrameworkUpdates();
|
||||
}
|
||||
Mediator.Subscribe<FrameworkUpdateMessage>(this, (_) => FrameworkUpdate());
|
||||
|
||||
Mediator.Subscribe<ZoneSwitchEndMessage>(this, (_) => ZoneSwitchEnd());
|
||||
Mediator.Subscribe<ZoneSwitchStartMessage>(this, (_) => ZoneSwitchStart());
|
||||
@@ -114,7 +109,7 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
||||
{
|
||||
while (await _dalamudUtil.RunOnFrameworkThread(() =>
|
||||
{
|
||||
EnsureLatestObjectState();
|
||||
if (_haltProcessing) CheckAndUpdateObject();
|
||||
if (CurrentDrawCondition != DrawCondition.None) return true;
|
||||
var gameObj = _dalamudUtil.CreateGameObject(Address);
|
||||
if (gameObj is Dalamud.Game.ClientState.Objects.Types.ICharacter chara)
|
||||
@@ -153,11 +148,6 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
||||
_haltProcessing = false;
|
||||
}
|
||||
|
||||
public void Refresh()
|
||||
{
|
||||
_dalamudUtil.RunOnFrameworkThread(CheckAndUpdateObject).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public async Task<bool> IsBeingDrawnRunOnFrameworkAsync()
|
||||
{
|
||||
return await _dalamudUtil.RunOnFrameworkThread(IsBeingDrawn).ConfigureAwait(false);
|
||||
@@ -371,7 +361,7 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
||||
|
||||
private bool IsBeingDrawn()
|
||||
{
|
||||
EnsureLatestObjectState();
|
||||
if (_haltProcessing) CheckAndUpdateObject();
|
||||
|
||||
if (_dalamudUtil.IsAnythingDrawing)
|
||||
{
|
||||
@@ -383,28 +373,6 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
||||
return CurrentDrawCondition != DrawCondition.None;
|
||||
}
|
||||
|
||||
private void EnsureLatestObjectState()
|
||||
{
|
||||
if (_haltProcessing || !_frameworkUpdateSubscribed)
|
||||
{
|
||||
CheckAndUpdateObject();
|
||||
}
|
||||
}
|
||||
|
||||
private void EnableFrameworkUpdates()
|
||||
{
|
||||
lock (_frameworkUpdateGate)
|
||||
{
|
||||
if (_frameworkUpdateSubscribed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Mediator.Subscribe<FrameworkUpdateMessage>(this, _ => FrameworkUpdate());
|
||||
_frameworkUpdateSubscribed = true;
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe DrawCondition IsBeingDrawnUnsafe()
|
||||
{
|
||||
if (Address == IntPtr.Zero) return DrawCondition.ObjectZero;
|
||||
|
||||
@@ -1,43 +1,36 @@
|
||||
using LightlessSync.API.Data;
|
||||
using LightlessSync.API.Data;
|
||||
|
||||
namespace LightlessSync.PlayerData.Pairs;
|
||||
namespace LightlessSync.PlayerData.Pairs;
|
||||
|
||||
/// <summary>
|
||||
/// orchestrates the lifecycle of a paired character
|
||||
/// </summary>
|
||||
public interface IPairHandlerAdapter : IDisposable, IPairPerformanceSubject
|
||||
{
|
||||
new string Ident { get; }
|
||||
bool Initialized { get; }
|
||||
bool IsVisible { get; }
|
||||
bool ScheduledForDeletion { get; set; }
|
||||
CharacterData? LastReceivedCharacterData { get; }
|
||||
long LastAppliedDataBytes { get; }
|
||||
new string? PlayerName { get; }
|
||||
string PlayerNameHash { get; }
|
||||
uint PlayerCharacterId { get; }
|
||||
DateTime? LastDataReceivedAt { get; }
|
||||
DateTime? LastApplyAttemptAt { get; }
|
||||
DateTime? LastSuccessfulApplyAt { get; }
|
||||
string? LastFailureReason { get; }
|
||||
IReadOnlyList<string> LastBlockingConditions { get; }
|
||||
bool IsApplying { get; }
|
||||
bool IsDownloading { get; }
|
||||
int PendingDownloadCount { get; }
|
||||
int ForbiddenDownloadCount { get; }
|
||||
bool PendingModReapply { get; }
|
||||
bool ModApplyDeferred { get; }
|
||||
int MissingCriticalMods { get; }
|
||||
int MissingNonCriticalMods { get; }
|
||||
int MissingForbiddenMods { get; }
|
||||
DateTime? InvisibleSinceUtc { get; }
|
||||
DateTime? VisibilityEvictionDueAtUtc { get; }
|
||||
/// <summary>
|
||||
/// orchestrates the lifecycle of a paired character
|
||||
/// </summary>
|
||||
public interface IPairHandlerAdapter : IDisposable, IPairPerformanceSubject
|
||||
{
|
||||
new string Ident { get; }
|
||||
bool Initialized { get; }
|
||||
bool IsVisible { get; }
|
||||
bool ScheduledForDeletion { get; set; }
|
||||
CharacterData? LastReceivedCharacterData { get; }
|
||||
long LastAppliedDataBytes { get; }
|
||||
new string? PlayerName { get; }
|
||||
string PlayerNameHash { get; }
|
||||
uint PlayerCharacterId { get; }
|
||||
DateTime? LastDataReceivedAt { get; }
|
||||
DateTime? LastApplyAttemptAt { get; }
|
||||
DateTime? LastSuccessfulApplyAt { get; }
|
||||
string? LastFailureReason { get; }
|
||||
IReadOnlyList<string> LastBlockingConditions { get; }
|
||||
bool IsApplying { get; }
|
||||
bool IsDownloading { get; }
|
||||
int PendingDownloadCount { get; }
|
||||
int ForbiddenDownloadCount { get; }
|
||||
|
||||
void Initialize();
|
||||
void ApplyData(CharacterData data);
|
||||
void ApplyLastReceivedData(bool forced = false);
|
||||
bool FetchPerformanceMetricsFromCache();
|
||||
void LoadCachedCharacterData(CharacterData data);
|
||||
void SetUploading(bool uploading);
|
||||
void SetPaused(bool paused);
|
||||
}
|
||||
void ApplyData(CharacterData data);
|
||||
void ApplyLastReceivedData(bool forced = false);
|
||||
bool FetchPerformanceMetricsFromCache();
|
||||
void LoadCachedCharacterData(CharacterData data);
|
||||
void SetUploading(bool uploading);
|
||||
void SetPaused(bool paused);
|
||||
}
|
||||
|
||||
@@ -87,25 +87,22 @@ public class Pair
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.Target is not MenuTargetDefault target || target.TargetObjectId != handler.PlayerCharacterId)
|
||||
if (args.Target is not MenuTargetDefault target || target.TargetObjectId != handler.PlayerCharacterId || IsPaused)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!IsPaused)
|
||||
UiSharedService.AddContextMenuItem(args, name: "Open Profile", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () =>
|
||||
{
|
||||
UiSharedService.AddContextMenuItem(args, name: "Open Profile", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () =>
|
||||
{
|
||||
_mediator.Publish(new ProfileOpenStandaloneMessage(this));
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
_mediator.Publish(new ProfileOpenStandaloneMessage(this));
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
|
||||
UiSharedService.AddContextMenuItem(args, name: "Reapply last data", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () =>
|
||||
{
|
||||
ApplyLastReceivedData(forced: true);
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
}
|
||||
UiSharedService.AddContextMenuItem(args, name: "Reapply last data", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () =>
|
||||
{
|
||||
ApplyLastReceivedData(forced: true);
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
|
||||
UiSharedService.AddContextMenuItem(args, name: "Change Permissions", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () =>
|
||||
{
|
||||
@@ -113,24 +110,7 @@ public class Pair
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
|
||||
if (IsPaused)
|
||||
{
|
||||
UiSharedService.AddContextMenuItem(args, name: "Toggle Unpause State", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () =>
|
||||
{
|
||||
_ = _apiController.Value.UnpauseAsync(UserData);
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
UiSharedService.AddContextMenuItem(args, name: "Toggle Pause State", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () =>
|
||||
{
|
||||
_ = _apiController.Value.PauseAsync(UserData);
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
}
|
||||
|
||||
UiSharedService.AddContextMenuItem(args, name: "Cycle Pause State", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () =>
|
||||
UiSharedService.AddContextMenuItem(args, name: "Cycle pause state", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () =>
|
||||
{
|
||||
TriggerCyclePause();
|
||||
return Task.CompletedTask;
|
||||
@@ -214,13 +194,9 @@ public class Pair
|
||||
{
|
||||
var handler = TryGetHandler();
|
||||
if (handler is null)
|
||||
{
|
||||
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(
|
||||
true,
|
||||
@@ -230,19 +206,11 @@ public class Pair
|
||||
handler.LastDataReceivedAt,
|
||||
handler.LastApplyAttemptAt,
|
||||
handler.LastSuccessfulApplyAt,
|
||||
handler.InvisibleSinceUtc,
|
||||
handler.VisibilityEvictionDueAtUtc,
|
||||
remainingSeconds,
|
||||
handler.LastFailureReason,
|
||||
handler.LastBlockingConditions,
|
||||
handler.IsApplying,
|
||||
handler.IsDownloading,
|
||||
handler.PendingDownloadCount,
|
||||
handler.ForbiddenDownloadCount,
|
||||
handler.PendingModReapply,
|
||||
handler.ModApplyDeferred,
|
||||
handler.MissingCriticalMods,
|
||||
handler.MissingNonCriticalMods,
|
||||
handler.MissingForbiddenMods);
|
||||
handler.ForbiddenDownloadCount);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,20 +8,12 @@ public sealed record PairDebugInfo(
|
||||
DateTime? LastDataReceivedAt,
|
||||
DateTime? LastApplyAttemptAt,
|
||||
DateTime? LastSuccessfulApplyAt,
|
||||
DateTime? InvisibleSinceUtc,
|
||||
DateTime? VisibilityEvictionDueAtUtc,
|
||||
double? VisibilityEvictionRemainingSeconds,
|
||||
string? LastFailureReason,
|
||||
IReadOnlyList<string> BlockingConditions,
|
||||
bool IsApplying,
|
||||
bool IsDownloading,
|
||||
int PendingDownloadCount,
|
||||
int ForbiddenDownloadCount,
|
||||
bool PendingModReapply,
|
||||
bool ModApplyDeferred,
|
||||
int MissingCriticalMods,
|
||||
int MissingNonCriticalMods,
|
||||
int MissingForbiddenMods)
|
||||
int ForbiddenDownloadCount)
|
||||
{
|
||||
public static PairDebugInfo Empty { get; } = new(
|
||||
false,
|
||||
@@ -32,17 +24,9 @@ public sealed record PairDebugInfo(
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
Array.Empty<string>(),
|
||||
false,
|
||||
false,
|
||||
0,
|
||||
0,
|
||||
false,
|
||||
false,
|
||||
0,
|
||||
0,
|
||||
0);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ using LightlessSync.Interop.Ipc;
|
||||
using LightlessSync.PlayerData.Factories;
|
||||
using LightlessSync.PlayerData.Handlers;
|
||||
using LightlessSync.Services;
|
||||
using LightlessSync.Services.ActorTracking;
|
||||
using LightlessSync.Services.Events;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using LightlessSync.Services.PairProcessing;
|
||||
@@ -19,7 +18,6 @@ using LightlessSync.WebAPI.Files;
|
||||
using LightlessSync.WebAPI.Files.Models;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using DalamudObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind;
|
||||
using ObjectKind = LightlessSync.API.Data.Enum.ObjectKind;
|
||||
using FileReplacementDataComparer = LightlessSync.PlayerData.Data.FileReplacementDataComparer;
|
||||
|
||||
@@ -33,7 +31,6 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
private sealed record CombatData(Guid ApplicationId, CharacterData CharacterData, bool Forced);
|
||||
|
||||
private readonly DalamudUtilService _dalamudUtil;
|
||||
private readonly ActorObjectService _actorObjectService;
|
||||
private readonly FileDownloadManager _downloadManager;
|
||||
private readonly FileCacheManager _fileDbManager;
|
||||
private readonly GameObjectHandlerFactory _gameObjectHandlerFactory;
|
||||
@@ -59,15 +56,11 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
private bool _forceFullReapply;
|
||||
private Dictionary<(string GamePath, string? Hash), string>? _lastAppliedModdedPaths;
|
||||
private bool _needsCollectionRebuild;
|
||||
private bool _pendingModReapply;
|
||||
private bool _lastModApplyDeferred;
|
||||
private int _lastMissingCriticalMods;
|
||||
private int _lastMissingNonCriticalMods;
|
||||
private int _lastMissingForbiddenMods;
|
||||
private bool _isVisible;
|
||||
private Guid _penumbraCollection;
|
||||
private readonly object _collectionGate = new();
|
||||
private bool _redrawOnNextApplication = false;
|
||||
private bool _explicitRedrawQueued;
|
||||
private readonly object _initializationGate = new();
|
||||
private readonly object _pauseLock = new();
|
||||
private Task _pauseTransitionTask = Task.CompletedTask;
|
||||
@@ -77,29 +70,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
private DateTime? _lastSuccessfulApplyAt;
|
||||
private string? _lastFailureReason;
|
||||
private IReadOnlyList<string> _lastBlockingConditions = Array.Empty<string>();
|
||||
private readonly object _visibilityGraceGate = new();
|
||||
private CancellationTokenSource? _visibilityGraceCts;
|
||||
private static readonly TimeSpan VisibilityEvictionGrace = TimeSpan.FromMinutes(1);
|
||||
private static readonly HashSet<string> NonPriorityModExtensions = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
".tmb",
|
||||
".pap",
|
||||
".atex",
|
||||
".avfx",
|
||||
".scd"
|
||||
};
|
||||
private DateTime? _invisibleSinceUtc;
|
||||
private DateTime? _visibilityEvictionDueAtUtc;
|
||||
private DateTime _nextActorLookupUtc = DateTime.MinValue;
|
||||
private static readonly TimeSpan ActorLookupInterval = TimeSpan.FromSeconds(1);
|
||||
private static readonly SemaphoreSlim ActorInitializationLimiter = new(1, 1);
|
||||
private readonly object _actorInitializationGate = new();
|
||||
private ActorObjectService.ActorDescriptor? _pendingActorDescriptor;
|
||||
private bool _actorInitializationInProgress;
|
||||
private bool _frameworkUpdateSubscribed;
|
||||
|
||||
public DateTime? InvisibleSinceUtc => _invisibleSinceUtc;
|
||||
public DateTime? VisibilityEvictionDueAtUtc => _visibilityEvictionDueAtUtc;
|
||||
public string Ident { get; }
|
||||
public bool Initialized { get; private set; }
|
||||
public bool ScheduledForDeletion { get; set; }
|
||||
@@ -109,37 +80,24 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
get => _isVisible;
|
||||
private set
|
||||
{
|
||||
if (_isVisible == value) return;
|
||||
|
||||
_isVisible = value;
|
||||
|
||||
if (!_isVisible)
|
||||
if (_isVisible != value)
|
||||
{
|
||||
DisableSync();
|
||||
|
||||
_invisibleSinceUtc = DateTime.UtcNow;
|
||||
_visibilityEvictionDueAtUtc = _invisibleSinceUtc.Value.Add(VisibilityEvictionGrace);
|
||||
|
||||
StartVisibilityGraceTask();
|
||||
}
|
||||
else
|
||||
{
|
||||
CancelVisibilityGraceTask();
|
||||
|
||||
_invisibleSinceUtc = null;
|
||||
_visibilityEvictionDueAtUtc = null;
|
||||
|
||||
ScheduledForDeletion = false;
|
||||
|
||||
if (_charaHandler is not null && _charaHandler.Address != nint.Zero)
|
||||
_isVisible = value;
|
||||
if (!_isVisible)
|
||||
{
|
||||
DisableSync();
|
||||
ResetPenumbraCollection(reason: "VisibilityLost");
|
||||
}
|
||||
else if (_charaHandler is not null && _charaHandler.Address != nint.Zero)
|
||||
{
|
||||
_ = EnsurePenumbraCollection();
|
||||
}
|
||||
var user = GetPrimaryUserData();
|
||||
Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter),
|
||||
EventSeverity.Informational, "User Visibility Changed, now: " + (_isVisible ? "Is Visible" : "Is not Visible"))));
|
||||
Mediator.Publish(new RefreshUiMessage());
|
||||
Mediator.Publish(new VisibilityChange());
|
||||
}
|
||||
|
||||
var user = GetPrimaryUserData();
|
||||
Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter),
|
||||
EventSeverity.Informational, "User Visibility Changed, now: " + (_isVisible ? "Is Visible" : "Is not Visible"))));
|
||||
Mediator.Publish(new RefreshUiMessage());
|
||||
Mediator.Publish(new VisibilityChange());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,11 +106,6 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
public long LastAppliedApproximateVRAMBytes { get; set; } = -1;
|
||||
public long LastAppliedApproximateEffectiveVRAMBytes { get; set; } = -1;
|
||||
public CharacterData? LastReceivedCharacterData { get; private set; }
|
||||
public bool PendingModReapply => _pendingModReapply;
|
||||
public bool ModApplyDeferred => _lastModApplyDeferred;
|
||||
public int MissingCriticalMods => _lastMissingCriticalMods;
|
||||
public int MissingNonCriticalMods => _lastMissingNonCriticalMods;
|
||||
public int MissingForbiddenMods => _lastMissingForbiddenMods;
|
||||
public DateTime? LastDataReceivedAt => _lastDataReceivedAt;
|
||||
public DateTime? LastApplyAttemptAt => _lastApplyAttemptAt;
|
||||
public DateTime? LastSuccessfulApplyAt => _lastSuccessfulApplyAt;
|
||||
@@ -173,7 +126,6 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
FileDownloadManager transferManager,
|
||||
PluginWarningNotificationService pluginWarningNotificationManager,
|
||||
DalamudUtilService dalamudUtil,
|
||||
ActorObjectService actorObjectService,
|
||||
IHostApplicationLifetime lifetime,
|
||||
FileCacheManager fileDbManager,
|
||||
PlayerPerformanceService playerPerformanceService,
|
||||
@@ -190,7 +142,6 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
_downloadManager = transferManager;
|
||||
_pluginWarningNotificationManager = pluginWarningNotificationManager;
|
||||
_dalamudUtil = dalamudUtil;
|
||||
_actorObjectService = actorObjectService;
|
||||
_lifetime = lifetime;
|
||||
_fileDbManager = fileDbManager;
|
||||
_playerPerformanceService = playerPerformanceService;
|
||||
@@ -214,7 +165,6 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
return;
|
||||
}
|
||||
|
||||
ActorObjectService.ActorDescriptor? trackedDescriptor = null;
|
||||
lock (_initializationGate)
|
||||
{
|
||||
if (Initialized)
|
||||
@@ -228,12 +178,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
_forceApplyMods = true;
|
||||
}
|
||||
|
||||
var useFrameworkUpdate = !_actorObjectService.HooksActive;
|
||||
if (useFrameworkUpdate)
|
||||
{
|
||||
Mediator.Subscribe<FrameworkUpdateMessage>(this, _ => FrameworkUpdate());
|
||||
_frameworkUpdateSubscribed = true;
|
||||
}
|
||||
Mediator.Subscribe<FrameworkUpdateMessage>(this, _ => FrameworkUpdate());
|
||||
Mediator.Subscribe<ZoneSwitchStartMessage>(this, _ =>
|
||||
{
|
||||
_downloadCancellationTokenSource?.CancelDispose();
|
||||
@@ -269,49 +214,17 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
Mediator.Subscribe<CutsceneEndMessage>(this, _ => EnableSync());
|
||||
Mediator.Subscribe<GposeStartMessage>(this, _ => DisableSync());
|
||||
Mediator.Subscribe<GposeEndMessage>(this, _ => EnableSync());
|
||||
Mediator.Subscribe<ActorTrackedMessage>(this, msg => HandleActorTracked(msg.Descriptor));
|
||||
Mediator.Subscribe<ActorUntrackedMessage>(this, msg => HandleActorUntracked(msg.Descriptor));
|
||||
Mediator.Subscribe<DownloadFinishedMessage>(this, msg =>
|
||||
Mediator.Subscribe<DownloadFinishedMessage>(this, msg =>
|
||||
{
|
||||
if (_charaHandler is null || !ReferenceEquals(msg.DownloadId, _charaHandler))
|
||||
{
|
||||
if (_charaHandler is null || !ReferenceEquals(msg.DownloadId, _charaHandler))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_pendingModReapply && IsVisible)
|
||||
{
|
||||
if (LastReceivedCharacterData is not null)
|
||||
{
|
||||
Logger.LogDebug("Downloads finished for {handler}, reapplying pending mod data", GetLogIdentifier());
|
||||
ApplyLastReceivedData(forced: true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_cachedData is not null)
|
||||
{
|
||||
Logger.LogDebug("Downloads finished for {handler}, reapplying pending mod data from cache", GetLogIdentifier());
|
||||
ApplyCharacterData(Guid.NewGuid(), _cachedData, forceApplyCustomization: true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
TryApplyQueuedData();
|
||||
});
|
||||
|
||||
if (!useFrameworkUpdate
|
||||
&& _actorObjectService.TryGetActorByHash(Ident, out var descriptor)
|
||||
&& descriptor.Address != nint.Zero)
|
||||
{
|
||||
trackedDescriptor = descriptor;
|
||||
return;
|
||||
}
|
||||
TryApplyQueuedData();
|
||||
});
|
||||
|
||||
Initialized = true;
|
||||
}
|
||||
|
||||
if (trackedDescriptor.HasValue)
|
||||
{
|
||||
HandleActorTracked(trackedDescriptor.Value);
|
||||
}
|
||||
}
|
||||
|
||||
private IReadOnlyList<PairConnection> GetCurrentPairs()
|
||||
@@ -804,67 +717,6 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool IsForbiddenHash(string hash)
|
||||
=> _downloadManager.ForbiddenTransfers.Exists(f => string.Equals(f.Hash, hash, StringComparison.Ordinal));
|
||||
|
||||
private static bool IsNonPriorityModPath(string? gamePath)
|
||||
{
|
||||
if (string.IsNullOrEmpty(gamePath))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var extension = Path.GetExtension(gamePath);
|
||||
return !string.IsNullOrEmpty(extension) && NonPriorityModExtensions.Contains(extension);
|
||||
}
|
||||
|
||||
private static bool IsCriticalModReplacement(FileReplacementData replacement)
|
||||
{
|
||||
foreach (var gamePath in replacement.GamePaths)
|
||||
{
|
||||
if (!IsNonPriorityModPath(gamePath))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void CountMissingReplacements(IEnumerable<FileReplacementData> missing, out int critical, out int nonCritical, out int forbidden)
|
||||
{
|
||||
critical = 0;
|
||||
nonCritical = 0;
|
||||
forbidden = 0;
|
||||
|
||||
foreach (var replacement in missing)
|
||||
{
|
||||
if (IsForbiddenHash(replacement.Hash))
|
||||
{
|
||||
forbidden++;
|
||||
}
|
||||
|
||||
if (IsCriticalModReplacement(replacement))
|
||||
{
|
||||
critical++;
|
||||
}
|
||||
else
|
||||
{
|
||||
nonCritical++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void RemoveModApplyChanges(Dictionary<ObjectKind, HashSet<PlayerChanges>> updatedData)
|
||||
{
|
||||
foreach (var changes in updatedData.Values)
|
||||
{
|
||||
changes.Remove(PlayerChanges.ModFiles);
|
||||
changes.Remove(PlayerChanges.ModManip);
|
||||
changes.Remove(PlayerChanges.ForcedRedraw);
|
||||
}
|
||||
}
|
||||
|
||||
private bool CanApplyNow()
|
||||
{
|
||||
return !_dalamudUtil.IsInCombat
|
||||
@@ -888,16 +740,6 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
_lastBlockingConditions = Array.Empty<string>();
|
||||
}
|
||||
|
||||
private void DeferApplication(Guid applicationBase, CharacterData characterData, bool forceApplyCustomization, UserData user, string reason,
|
||||
string failureKey, LogLevel logLevel, string logMessage, params object?[] logArgs)
|
||||
{
|
||||
Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter), EventSeverity.Warning, reason)));
|
||||
Logger.Log(logLevel, logMessage, logArgs);
|
||||
RecordFailure(reason, failureKey);
|
||||
_dataReceivedInDowntime = new(applicationBase, characterData, forceApplyCustomization);
|
||||
SetUploading(false);
|
||||
}
|
||||
|
||||
public void ApplyCharacterData(Guid applicationBase, CharacterData characterData, bool forceApplyCustomization = false)
|
||||
{
|
||||
_lastApplyAttemptAt = DateTime.UtcNow;
|
||||
@@ -915,48 +757,72 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
if (_dalamudUtil.IsInCombat)
|
||||
{
|
||||
const string reason = "Cannot apply character data: you are in combat, deferring application";
|
||||
DeferApplication(applicationBase, characterData, forceApplyCustomization, user, reason, "Combat", LogLevel.Debug,
|
||||
"[BASE-{appBase}] Received data but player is in combat", applicationBase);
|
||||
Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter), EventSeverity.Warning,
|
||||
reason)));
|
||||
Logger.LogDebug("[BASE-{appBase}] Received data but player is in combat", applicationBase);
|
||||
RecordFailure(reason, "Combat");
|
||||
_dataReceivedInDowntime = new(applicationBase, characterData, forceApplyCustomization);
|
||||
SetUploading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_dalamudUtil.IsPerforming)
|
||||
{
|
||||
const string reason = "Cannot apply character data: you are performing music, deferring application";
|
||||
DeferApplication(applicationBase, characterData, forceApplyCustomization, user, reason, "Performance", LogLevel.Debug,
|
||||
"[BASE-{appBase}] Received data but player is performing", applicationBase);
|
||||
Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter), EventSeverity.Warning,
|
||||
reason)));
|
||||
Logger.LogDebug("[BASE-{appBase}] Received data but player is performing", applicationBase);
|
||||
RecordFailure(reason, "Performance");
|
||||
_dataReceivedInDowntime = new(applicationBase, characterData, forceApplyCustomization);
|
||||
SetUploading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_dalamudUtil.IsInInstance)
|
||||
{
|
||||
const string reason = "Cannot apply character data: you are in an instance, deferring application";
|
||||
DeferApplication(applicationBase, characterData, forceApplyCustomization, user, reason, "Instance", LogLevel.Debug,
|
||||
"[BASE-{appBase}] Received data but player is in instance", applicationBase);
|
||||
Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter), EventSeverity.Warning,
|
||||
reason)));
|
||||
Logger.LogDebug("[BASE-{appBase}] Received data but player is in instance", applicationBase);
|
||||
RecordFailure(reason, "Instance");
|
||||
_dataReceivedInDowntime = new(applicationBase, characterData, forceApplyCustomization);
|
||||
SetUploading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_dalamudUtil.IsInCutscene)
|
||||
{
|
||||
const string reason = "Cannot apply character data: you are in a cutscene, deferring application";
|
||||
DeferApplication(applicationBase, characterData, forceApplyCustomization, user, reason, "Cutscene", LogLevel.Debug,
|
||||
"[BASE-{appBase}] Received data but player is in a cutscene", applicationBase);
|
||||
Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter), EventSeverity.Warning,
|
||||
reason)));
|
||||
Logger.LogDebug("[BASE-{appBase}] Received data but player is in a cutscene", applicationBase);
|
||||
RecordFailure(reason, "Cutscene");
|
||||
_dataReceivedInDowntime = new(applicationBase, characterData, forceApplyCustomization);
|
||||
SetUploading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_dalamudUtil.IsInGpose)
|
||||
{
|
||||
const string reason = "Cannot apply character data: you are in GPose, deferring application";
|
||||
DeferApplication(applicationBase, characterData, forceApplyCustomization, user, reason, "GPose", LogLevel.Debug,
|
||||
"[BASE-{appBase}] Received data but player is in GPose", applicationBase);
|
||||
Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter), EventSeverity.Warning,
|
||||
reason)));
|
||||
Logger.LogDebug("[BASE-{appBase}] Received data but player is in GPose", applicationBase);
|
||||
RecordFailure(reason, "GPose");
|
||||
_dataReceivedInDowntime = new(applicationBase, characterData, forceApplyCustomization);
|
||||
SetUploading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_ipcManager.Penumbra.APIAvailable || !_ipcManager.Glamourer.APIAvailable)
|
||||
{
|
||||
const string reason = "Cannot apply character data: Penumbra or Glamourer is not available, deferring application";
|
||||
DeferApplication(applicationBase, characterData, forceApplyCustomization, user, reason, "PluginUnavailable", LogLevel.Information,
|
||||
"[BASE-{appbase}] Application of data for {player} while Penumbra/Glamourer unavailable, returning", applicationBase, GetLogIdentifier());
|
||||
Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter), EventSeverity.Warning,
|
||||
reason)));
|
||||
Logger.LogInformation("[BASE-{appbase}] Application of data for {player} while Penumbra/Glamourer unavailable, returning", applicationBase, GetLogIdentifier());
|
||||
RecordFailure(reason, "PluginUnavailable");
|
||||
_dataReceivedInDowntime = new(applicationBase, characterData, forceApplyCustomization);
|
||||
SetUploading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -999,10 +865,13 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
_forceApplyMods = false;
|
||||
}
|
||||
|
||||
_explicitRedrawQueued = false;
|
||||
|
||||
if (_redrawOnNextApplication && charaDataToUpdate.TryGetValue(ObjectKind.Player, out var player))
|
||||
{
|
||||
player.Add(PlayerChanges.ForcedRedraw);
|
||||
_redrawOnNextApplication = false;
|
||||
_explicitRedrawQueued = true;
|
||||
}
|
||||
|
||||
if (charaDataToUpdate.TryGetValue(ObjectKind.Player, out var playerChanges))
|
||||
@@ -1049,46 +918,6 @@ 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)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
@@ -1107,10 +936,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
_downloadCancellationTokenSource = null;
|
||||
_downloadManager.Dispose();
|
||||
_charaHandler?.Dispose();
|
||||
CancelVisibilityGraceTask();
|
||||
_charaHandler = null;
|
||||
_invisibleSinceUtc = null;
|
||||
_visibilityEvictionDueAtUtc = null;
|
||||
|
||||
if (!string.IsNullOrEmpty(name))
|
||||
{
|
||||
@@ -1196,14 +1022,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
|
||||
Logger.LogDebug("[{applicationId}] Applying Customization Data for {handler}", applicationId, handler);
|
||||
await _dalamudUtil.WaitWhileCharacterIsDrawing(Logger, handler, applicationId, 30000, token).ConfigureAwait(false);
|
||||
if (handler.Address != nint.Zero)
|
||||
{
|
||||
await _actorObjectService.WaitForFullyLoadedAsync(handler.Address, token).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
token.ThrowIfCancellationRequested();
|
||||
var tasks = new List<Task>();
|
||||
bool needsRedraw = false;
|
||||
foreach (var change in changes.Value.OrderBy(p => (int)p))
|
||||
{
|
||||
Logger.LogDebug("[{applicationId}] Processing {change} for {handler}", applicationId, change, handler);
|
||||
@@ -1212,39 +1031,45 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
case PlayerChanges.Customize:
|
||||
if (charaData.CustomizePlusData.TryGetValue(changes.Key, out var customizePlusData))
|
||||
{
|
||||
tasks.Add(ApplyCustomizeAsync(handler.Address, customizePlusData, changes.Key));
|
||||
_customizeIds[changes.Key] = await _ipcManager.CustomizePlus.SetBodyScaleAsync(handler.Address, customizePlusData).ConfigureAwait(false);
|
||||
}
|
||||
else if (_customizeIds.TryGetValue(changes.Key, out var customizeId))
|
||||
{
|
||||
tasks.Add(RevertCustomizeAsync(customizeId, changes.Key));
|
||||
await _ipcManager.CustomizePlus.RevertByIdAsync(customizeId).ConfigureAwait(false);
|
||||
_customizeIds.Remove(changes.Key);
|
||||
}
|
||||
break;
|
||||
|
||||
case PlayerChanges.Heels:
|
||||
tasks.Add(_ipcManager.Heels.SetOffsetForPlayerAsync(handler.Address, charaData.HeelsData));
|
||||
await _ipcManager.Heels.SetOffsetForPlayerAsync(handler.Address, charaData.HeelsData).ConfigureAwait(false);
|
||||
break;
|
||||
|
||||
case PlayerChanges.Honorific:
|
||||
tasks.Add(_ipcManager.Honorific.SetTitleAsync(handler.Address, charaData.HonorificData));
|
||||
await _ipcManager.Honorific.SetTitleAsync(handler.Address, charaData.HonorificData).ConfigureAwait(false);
|
||||
break;
|
||||
|
||||
case PlayerChanges.Glamourer:
|
||||
if (charaData.GlamourerData.TryGetValue(changes.Key, out var glamourerData))
|
||||
{
|
||||
tasks.Add(_ipcManager.Glamourer.ApplyAllAsync(Logger, handler, glamourerData, applicationId, token));
|
||||
await _ipcManager.Glamourer.ApplyAllAsync(Logger, handler, glamourerData, applicationId, token).ConfigureAwait(false);
|
||||
}
|
||||
break;
|
||||
|
||||
case PlayerChanges.Moodles:
|
||||
tasks.Add(_ipcManager.Moodles.SetStatusAsync(handler.Address, charaData.MoodlesData));
|
||||
await _ipcManager.Moodles.SetStatusAsync(handler.Address, charaData.MoodlesData).ConfigureAwait(false);
|
||||
break;
|
||||
|
||||
case PlayerChanges.PetNames:
|
||||
tasks.Add(_ipcManager.PetNames.SetPlayerData(handler.Address, charaData.PetNamesData));
|
||||
await _ipcManager.PetNames.SetPlayerData(handler.Address, charaData.PetNamesData).ConfigureAwait(false);
|
||||
break;
|
||||
|
||||
case PlayerChanges.ForcedRedraw:
|
||||
needsRedraw = true;
|
||||
if (!ShouldPerformForcedRedraw(changes.Key, changes.Value, charaData))
|
||||
{
|
||||
Logger.LogTrace("[{applicationId}] Skipping forced redraw for {handler}", applicationId, handler);
|
||||
break;
|
||||
}
|
||||
await _ipcManager.Penumbra.RedrawAsync(Logger, handler, applicationId, token).ConfigureAwait(false);
|
||||
break;
|
||||
|
||||
default:
|
||||
@@ -1252,16 +1077,6 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
}
|
||||
token.ThrowIfCancellationRequested();
|
||||
}
|
||||
|
||||
if (tasks.Count > 0)
|
||||
{
|
||||
await Task.WhenAll(tasks).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (needsRedraw)
|
||||
{
|
||||
await _ipcManager.Penumbra.RedrawAsync(Logger, handler, applicationId, token).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -1269,6 +1084,44 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
}
|
||||
}
|
||||
|
||||
private bool ShouldPerformForcedRedraw(ObjectKind objectKind, ICollection<PlayerChanges> changeSet, CharacterData newData)
|
||||
{
|
||||
if (objectKind != ObjectKind.Player)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var hasModFiles = changeSet.Contains(PlayerChanges.ModFiles);
|
||||
var hasManip = changeSet.Contains(PlayerChanges.ModManip);
|
||||
var modsChanged = hasModFiles && PlayerModFilesChanged(newData, _cachedData);
|
||||
var manipChanged = hasManip && !string.Equals(_cachedData?.ManipulationData, newData.ManipulationData, StringComparison.Ordinal);
|
||||
|
||||
if (modsChanged)
|
||||
{
|
||||
_explicitRedrawQueued = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (manipChanged)
|
||||
{
|
||||
_explicitRedrawQueued = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (_explicitRedrawQueued)
|
||||
{
|
||||
_explicitRedrawQueued = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
if ((hasModFiles || hasManip) && (_forceFullReapply || _needsCollectionRebuild))
|
||||
{
|
||||
_explicitRedrawQueued = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static Dictionary<ObjectKind, HashSet<PlayerChanges>> BuildFullChangeSet(CharacterData characterData)
|
||||
{
|
||||
@@ -1412,7 +1265,6 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
}
|
||||
|
||||
private Task? _pairDownloadTask;
|
||||
private Task _visibilityGraceTask;
|
||||
|
||||
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)
|
||||
@@ -1423,7 +1275,6 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
bool skipDownscaleForPair = ShouldSkipDownscale();
|
||||
var user = GetPrimaryUserData();
|
||||
Dictionary<(string GamePath, string? Hash), string> moddedPaths;
|
||||
List<FileReplacementData> missingReplacements = [];
|
||||
|
||||
if (updateModdedPaths)
|
||||
{
|
||||
@@ -1435,7 +1286,6 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
{
|
||||
int attempts = 0;
|
||||
List<FileReplacementData> toDownloadReplacements = TryCalculateModdedDictionary(applicationBase, charaData, out moddedPaths, downloadToken);
|
||||
missingReplacements = toDownloadReplacements;
|
||||
|
||||
while (toDownloadReplacements.Count > 0 && attempts++ <= 10 && !downloadToken.IsCancellationRequested)
|
||||
{
|
||||
@@ -1485,7 +1335,6 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
}
|
||||
|
||||
toDownloadReplacements = TryCalculateModdedDictionary(applicationBase, charaData, out moddedPaths, downloadToken);
|
||||
missingReplacements = toDownloadReplacements;
|
||||
|
||||
if (toDownloadReplacements.TrueForAll(c => _downloadManager.ForbiddenTransfers.Exists(f => string.Equals(f.Hash, c.Hash, StringComparison.Ordinal))))
|
||||
{
|
||||
@@ -1509,54 +1358,6 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
: [];
|
||||
}
|
||||
|
||||
var wantsModApply = updateModdedPaths || updateManip;
|
||||
var pendingModReapply = false;
|
||||
var deferModApply = false;
|
||||
|
||||
if (wantsModApply && missingReplacements.Count > 0)
|
||||
{
|
||||
CountMissingReplacements(missingReplacements, out var missingCritical, out var missingNonCritical, out var missingForbidden);
|
||||
_lastMissingCriticalMods = missingCritical;
|
||||
_lastMissingNonCriticalMods = missingNonCritical;
|
||||
_lastMissingForbiddenMods = missingForbidden;
|
||||
|
||||
var hasCriticalMissing = missingCritical > 0;
|
||||
var hasNonCriticalMissing = missingNonCritical > 0;
|
||||
var hasDownloadableMissing = missingReplacements.Any(replacement => !IsForbiddenHash(replacement.Hash));
|
||||
var hasDownloadableCriticalMissing = hasCriticalMissing
|
||||
&& missingReplacements.Any(replacement => !IsForbiddenHash(replacement.Hash) && IsCriticalModReplacement(replacement));
|
||||
|
||||
pendingModReapply = hasDownloadableMissing;
|
||||
_lastModApplyDeferred = false;
|
||||
|
||||
if (hasDownloadableCriticalMissing)
|
||||
{
|
||||
deferModApply = true;
|
||||
_lastModApplyDeferred = true;
|
||||
Logger.LogDebug("[BASE-{appBase}] Critical mod files missing for {handler}, deferring mod apply ({count} missing)",
|
||||
applicationBase, GetLogIdentifier(), missingReplacements.Count);
|
||||
}
|
||||
else if (hasNonCriticalMissing && hasDownloadableMissing)
|
||||
{
|
||||
Logger.LogDebug("[BASE-{appBase}] Non-critical mod files missing for {handler}, applying partial mods and reapplying after downloads ({count} missing)",
|
||||
applicationBase, GetLogIdentifier(), missingReplacements.Count);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_lastMissingCriticalMods = 0;
|
||||
_lastMissingNonCriticalMods = 0;
|
||||
_lastMissingForbiddenMods = 0;
|
||||
_lastModApplyDeferred = false;
|
||||
}
|
||||
|
||||
if (deferModApply)
|
||||
{
|
||||
updateModdedPaths = false;
|
||||
updateManip = false;
|
||||
RemoveModApplyChanges(updatedData);
|
||||
}
|
||||
|
||||
downloadToken.ThrowIfCancellationRequested();
|
||||
|
||||
var handlerForApply = _charaHandler;
|
||||
@@ -1589,7 +1390,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
_applicationCancellationTokenSource = _applicationCancellationTokenSource.CancelRecreate() ?? new CancellationTokenSource();
|
||||
var token = _applicationCancellationTokenSource.Token;
|
||||
|
||||
_applicationTask = ApplyCharacterDataAsync(applicationBase, handlerForApply, charaData, updatedData, updateModdedPaths, updateManip, moddedPaths, wantsModApply, pendingModReapply, token);
|
||||
_applicationTask = ApplyCharacterDataAsync(applicationBase, handlerForApply, charaData, updatedData, updateModdedPaths, updateManip, moddedPaths, token);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -1598,7 +1399,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
}
|
||||
|
||||
private async Task ApplyCharacterDataAsync(Guid applicationBase, GameObjectHandler handlerForApply, CharacterData charaData, Dictionary<ObjectKind, HashSet<PlayerChanges>> updatedData, bool updateModdedPaths, bool updateManip,
|
||||
Dictionary<(string GamePath, string? Hash), string> moddedPaths, bool wantsModApply, bool pendingModReapply, CancellationToken token)
|
||||
Dictionary<(string GamePath, string? Hash), string> moddedPaths, CancellationToken token)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -1607,10 +1408,6 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
|
||||
Logger.LogDebug("[{applicationId}] Waiting for initial draw for for {handler}", _applicationId, handlerForApply);
|
||||
await _dalamudUtil.WaitWhileCharacterIsDrawing(Logger, handlerForApply, _applicationId, 30000, token).ConfigureAwait(false);
|
||||
if (handlerForApply.Address != nint.Zero)
|
||||
{
|
||||
await _actorObjectService.WaitForFullyLoadedAsync(handlerForApply.Address, token).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
@@ -1677,11 +1474,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
|
||||
_cachedData = charaData;
|
||||
_pairStateCache.Store(Ident, charaData);
|
||||
if (wantsModApply)
|
||||
{
|
||||
_pendingModReapply = pendingModReapply;
|
||||
}
|
||||
_forceFullReapply = _pendingModReapply;
|
||||
_forceFullReapply = false;
|
||||
_needsCollectionRebuild = false;
|
||||
if (LastAppliedApproximateVRAMBytes < 0 || LastAppliedApproximateEffectiveVRAMBytes < 0)
|
||||
{
|
||||
@@ -1727,15 +1520,8 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
|
||||
private void FrameworkUpdate()
|
||||
{
|
||||
if (string.IsNullOrEmpty(PlayerName) && _charaHandler is null)
|
||||
if (string.IsNullOrEmpty(PlayerName))
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
if (now < _nextActorLookupUtc)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_nextActorLookupUtc = now + ActorLookupInterval;
|
||||
var pc = _dalamudUtil.FindPlayerByNameHash(Ident);
|
||||
if (pc == default((string, nint))) return;
|
||||
Logger.LogDebug("One-Time Initializing {handler}", GetLogIdentifier());
|
||||
@@ -1745,11 +1531,6 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
$"Initializing User For Character {pc.Name}")));
|
||||
}
|
||||
|
||||
TryHandleVisibilityUpdate();
|
||||
}
|
||||
|
||||
private void TryHandleVisibilityUpdate()
|
||||
{
|
||||
if (_charaHandler?.Address != nint.Zero && !IsVisible && !_pauseRequested)
|
||||
{
|
||||
Guid appData = Guid.NewGuid();
|
||||
@@ -1796,24 +1577,16 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
}
|
||||
else if (_charaHandler?.Address == nint.Zero && IsVisible)
|
||||
{
|
||||
HandleVisibilityLoss(logChange: true);
|
||||
IsVisible = false;
|
||||
_charaHandler.Invalidate();
|
||||
_downloadCancellationTokenSource?.CancelDispose();
|
||||
_downloadCancellationTokenSource = null;
|
||||
Logger.LogTrace("{handler} visibility changed, now: {visi}", GetLogIdentifier(), IsVisible);
|
||||
}
|
||||
|
||||
TryApplyQueuedData();
|
||||
}
|
||||
|
||||
private void HandleVisibilityLoss(bool logChange)
|
||||
{
|
||||
IsVisible = false;
|
||||
_charaHandler?.Invalidate();
|
||||
_downloadCancellationTokenSource?.CancelDispose();
|
||||
_downloadCancellationTokenSource = null;
|
||||
if (logChange)
|
||||
{
|
||||
Logger.LogTrace("{handler} visibility changed, now: {visi}", GetLogIdentifier(), IsVisible);
|
||||
}
|
||||
}
|
||||
|
||||
private void Initialize(string name)
|
||||
{
|
||||
PlayerName = name;
|
||||
@@ -2140,164 +1913,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
}
|
||||
|
||||
_dataReceivedInDowntime = null;
|
||||
_ = Task.Run(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
ApplyCharacterData(pending.ApplicationId, pending.CharacterData, pending.Forced);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Failed applying queued data for {handler}", GetLogIdentifier());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void HandleActorTracked(ActorObjectService.ActorDescriptor descriptor)
|
||||
{
|
||||
if (!TryResolveDescriptorHash(descriptor, out var hashedCid))
|
||||
return;
|
||||
|
||||
if (!string.Equals(hashedCid, Ident, StringComparison.Ordinal))
|
||||
return;
|
||||
|
||||
if (descriptor.Address == nint.Zero)
|
||||
return;
|
||||
|
||||
RefreshTrackedHandler(descriptor);
|
||||
QueueActorInitialization(descriptor);
|
||||
}
|
||||
|
||||
private void QueueActorInitialization(ActorObjectService.ActorDescriptor descriptor)
|
||||
{
|
||||
lock (_actorInitializationGate)
|
||||
{
|
||||
_pendingActorDescriptor = descriptor;
|
||||
if (_actorInitializationInProgress)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_actorInitializationInProgress = true;
|
||||
}
|
||||
|
||||
_ = Task.Run(InitializeFromTrackedAsync);
|
||||
}
|
||||
|
||||
private async Task InitializeFromTrackedAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
await ActorInitializationLimiter.WaitAsync().ConfigureAwait(false);
|
||||
while (true)
|
||||
{
|
||||
ActorObjectService.ActorDescriptor? descriptor;
|
||||
lock (_actorInitializationGate)
|
||||
{
|
||||
descriptor = _pendingActorDescriptor;
|
||||
_pendingActorDescriptor = null;
|
||||
}
|
||||
|
||||
if (!descriptor.HasValue)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (_frameworkUpdateSubscribed && _actorObjectService.HooksActive)
|
||||
{
|
||||
Mediator.Unsubscribe<FrameworkUpdateMessage>(this);
|
||||
_frameworkUpdateSubscribed = false;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(PlayerName) || _charaHandler is null)
|
||||
{
|
||||
Logger.LogDebug("Actor tracked for {handler}, initializing from hook", GetLogIdentifier());
|
||||
Initialize(descriptor.Value.Name);
|
||||
Mediator.Publish(new EventMessage(new Event(PlayerName, GetPrimaryUserData(), nameof(PairHandlerAdapter), EventSeverity.Informational,
|
||||
$"Initializing User For Character {descriptor.Value.Name}")));
|
||||
}
|
||||
|
||||
RefreshTrackedHandler(descriptor.Value);
|
||||
TryHandleVisibilityUpdate();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
ActorInitializationLimiter.Release();
|
||||
lock (_actorInitializationGate)
|
||||
{
|
||||
_actorInitializationInProgress = false;
|
||||
if (_pendingActorDescriptor.HasValue)
|
||||
{
|
||||
_actorInitializationInProgress = true;
|
||||
_ = Task.Run(InitializeFromTrackedAsync);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void RefreshTrackedHandler(ActorObjectService.ActorDescriptor descriptor)
|
||||
{
|
||||
if (_charaHandler is null)
|
||||
return;
|
||||
|
||||
if (descriptor.Address == nint.Zero)
|
||||
return;
|
||||
|
||||
if (_charaHandler.Address == descriptor.Address)
|
||||
return;
|
||||
|
||||
_charaHandler.Refresh();
|
||||
}
|
||||
|
||||
private void HandleActorUntracked(ActorObjectService.ActorDescriptor descriptor)
|
||||
{
|
||||
if (!TryResolveDescriptorHash(descriptor, out var hashedCid))
|
||||
{
|
||||
if (_charaHandler is null || _charaHandler.Address == nint.Zero)
|
||||
return;
|
||||
|
||||
if (descriptor.Address != _charaHandler.Address)
|
||||
return;
|
||||
}
|
||||
else if (!string.Equals(hashedCid, Ident, StringComparison.Ordinal))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_charaHandler is null || _charaHandler.Address == nint.Zero)
|
||||
return;
|
||||
|
||||
if (descriptor.Address != _charaHandler.Address)
|
||||
return;
|
||||
|
||||
HandleVisibilityLoss(logChange: false);
|
||||
}
|
||||
|
||||
private bool TryResolveDescriptorHash(ActorObjectService.ActorDescriptor descriptor, out string hashedCid)
|
||||
{
|
||||
hashedCid = descriptor.HashedContentId ?? string.Empty;
|
||||
if (!string.IsNullOrEmpty(hashedCid))
|
||||
return true;
|
||||
|
||||
if (descriptor.ObjectKind != DalamudObjectKind.Player || descriptor.Address == nint.Zero)
|
||||
return false;
|
||||
|
||||
hashedCid = DalamudUtilService.GetHashedCIDFromPlayerPointer(descriptor.Address);
|
||||
return !string.IsNullOrEmpty(hashedCid);
|
||||
}
|
||||
|
||||
private async Task ApplyCustomizeAsync(nint address, string customizeData, ObjectKind kind)
|
||||
{
|
||||
_customizeIds[kind] = await _ipcManager.CustomizePlus.SetBodyScaleAsync(address, customizeData).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task RevertCustomizeAsync(Guid? customizeId, ObjectKind kind)
|
||||
{
|
||||
if (!customizeId.HasValue)
|
||||
return;
|
||||
|
||||
await _ipcManager.CustomizePlus.RevertByIdAsync(customizeId.Value).ConfigureAwait(false);
|
||||
_customizeIds.Remove(kind);
|
||||
ApplyCharacterData(pending.ApplicationId,
|
||||
pending.CharacterData, pending.Forced);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ using LightlessSync.FileCache;
|
||||
using LightlessSync.Interop.Ipc;
|
||||
using LightlessSync.PlayerData.Factories;
|
||||
using LightlessSync.Services;
|
||||
using LightlessSync.Services.ActorTracking;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using LightlessSync.Services.PairProcessing;
|
||||
using LightlessSync.Services.ServerConfiguration;
|
||||
@@ -72,7 +71,6 @@ internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory
|
||||
{
|
||||
var downloadManager = _fileDownloadManagerFactory.Create();
|
||||
var dalamudUtilService = _serviceProvider.GetRequiredService<DalamudUtilService>();
|
||||
var actorObjectService = _serviceProvider.GetRequiredService<ActorObjectService>();
|
||||
return new PairHandlerAdapter(
|
||||
_loggerFactory.CreateLogger<PairHandlerAdapter>(),
|
||||
_mediator,
|
||||
@@ -83,7 +81,6 @@ internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory
|
||||
downloadManager,
|
||||
_pluginWarningNotificationManager,
|
||||
dalamudUtilService,
|
||||
actorObjectService,
|
||||
_lifetime,
|
||||
_fileCacheManager,
|
||||
_playerPerformanceService,
|
||||
|
||||
@@ -11,9 +11,7 @@ public sealed class PairHandlerRegistry : IDisposable
|
||||
{
|
||||
private readonly object _gate = new();
|
||||
private readonly object _pendingGate = new();
|
||||
private readonly object _visibilityGate = new();
|
||||
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 IPairHandlerAdapterFactory _handlerFactory;
|
||||
@@ -146,37 +144,6 @@ public sealed class PairHandlerRegistry : IDisposable
|
||||
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)
|
||||
{
|
||||
if (registration.CharacterIdent is null)
|
||||
@@ -333,7 +300,6 @@ public sealed class PairHandlerRegistry : IDisposable
|
||||
lock (_gate)
|
||||
{
|
||||
handlers = _entriesByHandler.Keys.ToList();
|
||||
CancelAllInvisibleEvictions();
|
||||
_entriesByIdent.Clear();
|
||||
_entriesByHandler.Clear();
|
||||
}
|
||||
@@ -366,7 +332,6 @@ public sealed class PairHandlerRegistry : IDisposable
|
||||
lock (_gate)
|
||||
{
|
||||
handlers = _entriesByHandler.Keys.ToList();
|
||||
CancelAllInvisibleEvictions();
|
||||
_entriesByIdent.Clear();
|
||||
_entriesByHandler.Clear();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using Dalamud.Game;
|
||||
using Dalamud.Game.ClientState.Objects;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.ImGuiFileDialog;
|
||||
using Dalamud.Interface.Windowing;
|
||||
using Dalamud.Plugin;
|
||||
@@ -106,7 +105,6 @@ public sealed class Plugin : IDalamudPlugin
|
||||
services.AddSingleton(new Dalamud.Localization("LightlessSync.Localization.", string.Empty, useEmbedded: true));
|
||||
services.AddSingleton(gameGui);
|
||||
services.AddSingleton(addonLifecycle);
|
||||
services.AddSingleton<IUiBuilder>(pluginInterface.UiBuilder);
|
||||
|
||||
// Core singletons
|
||||
services.AddSingleton<LightlessMediator>();
|
||||
@@ -201,7 +199,6 @@ public sealed class Plugin : IDalamudPlugin
|
||||
gameInteropProvider,
|
||||
objectTable,
|
||||
clientState,
|
||||
condition,
|
||||
sp.GetRequiredService<LightlessMediator>()));
|
||||
|
||||
services.AddSingleton(sp => new DalamudUtilService(
|
||||
@@ -268,7 +265,6 @@ public sealed class Plugin : IDalamudPlugin
|
||||
sp.GetRequiredService<ILogger<LightFinderPlateHandler>>(),
|
||||
addonLifecycle,
|
||||
gameGui,
|
||||
clientState,
|
||||
sp.GetRequiredService<LightlessConfigService>(),
|
||||
sp.GetRequiredService<LightlessMediator>(),
|
||||
objectTable,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.Collections.Concurrent;
|
||||
using Dalamud.Game.ClientState.Conditions;
|
||||
using Dalamud.Game.ClientState.Objects.SubKinds;
|
||||
using Dalamud.Hooking;
|
||||
using Dalamud.Plugin.Services;
|
||||
@@ -32,18 +31,13 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
||||
private readonly IFramework _framework;
|
||||
private readonly IGameInteropProvider _interop;
|
||||
private readonly IObjectTable _objectTable;
|
||||
private readonly IClientState _clientState;
|
||||
private readonly ICondition _condition;
|
||||
private readonly LightlessMediator _mediator;
|
||||
|
||||
private readonly ConcurrentDictionary<nint, ActorDescriptor> _activePlayers = new();
|
||||
private readonly ConcurrentDictionary<nint, ActorDescriptor> _gposePlayers = new();
|
||||
private readonly ConcurrentDictionary<string, ActorDescriptor> _actorsByHash = new(StringComparer.Ordinal);
|
||||
private readonly ConcurrentDictionary<string, ConcurrentDictionary<nint, ActorDescriptor>> _actorsByName = new(StringComparer.Ordinal);
|
||||
private readonly ConcurrentDictionary<nint, byte> _pendingHashResolutions = new();
|
||||
private readonly OwnedObjectTracker _ownedTracker = new();
|
||||
private ActorSnapshot _snapshot = ActorSnapshot.Empty;
|
||||
private GposeSnapshot _gposeSnapshot = GposeSnapshot.Empty;
|
||||
|
||||
private Hook<Character.Delegates.OnInitialize>? _onInitializeHook;
|
||||
private Hook<Character.Delegates.Terminate>? _onTerminateHook;
|
||||
@@ -61,29 +55,21 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
||||
IGameInteropProvider interop,
|
||||
IObjectTable objectTable,
|
||||
IClientState clientState,
|
||||
ICondition condition,
|
||||
LightlessMediator mediator)
|
||||
{
|
||||
_logger = logger;
|
||||
_framework = framework;
|
||||
_interop = interop;
|
||||
_objectTable = objectTable;
|
||||
_clientState = clientState;
|
||||
_condition = condition;
|
||||
_mediator = mediator;
|
||||
}
|
||||
|
||||
private bool IsZoning => _condition[ConditionFlag.BetweenAreas] || _condition[ConditionFlag.BetweenAreas51];
|
||||
|
||||
private ActorSnapshot Snapshot => Volatile.Read(ref _snapshot);
|
||||
private GposeSnapshot CurrentGposeSnapshot => Volatile.Read(ref _gposeSnapshot);
|
||||
|
||||
public IReadOnlyList<nint> PlayerAddresses => Snapshot.PlayerAddresses;
|
||||
|
||||
public IEnumerable<ActorDescriptor> ObjectDescriptors => _activePlayers.Values;
|
||||
public IReadOnlyList<ActorDescriptor> PlayerDescriptors => Snapshot.PlayerDescriptors;
|
||||
public IReadOnlyList<ActorDescriptor> OwnedDescriptors => Snapshot.OwnedDescriptors;
|
||||
public IReadOnlyList<ActorDescriptor> GposeDescriptors => CurrentGposeSnapshot.GposeDescriptors;
|
||||
public IEnumerable<ActorDescriptor> PlayerDescriptors => _activePlayers.Values;
|
||||
public IReadOnlyList<ActorDescriptor> PlayerCharacterDescriptors => Snapshot.PlayerDescriptors;
|
||||
|
||||
public bool TryGetActorByHash(string hash, out ActorDescriptor descriptor) => _actorsByHash.TryGetValue(hash, out descriptor);
|
||||
public bool TryGetValidatedActorByHash(string hash, out ActorDescriptor descriptor)
|
||||
@@ -127,7 +113,6 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
||||
return false;
|
||||
}
|
||||
public bool HooksActive => _hooksActive;
|
||||
public bool HasPendingHashResolutions => !_pendingHashResolutions.IsEmpty;
|
||||
public IReadOnlyList<nint> RenderedPlayerAddresses => Snapshot.OwnedObjects.RenderedPlayers;
|
||||
public IReadOnlyList<nint> RenderedCompanionAddresses => Snapshot.OwnedObjects.RenderedCompanions;
|
||||
public IReadOnlyList<nint> OwnedObjectAddresses => Snapshot.OwnedObjects.OwnedAddresses;
|
||||
@@ -222,7 +207,7 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var isLoaded = await _framework.RunOnFrameworkThread(() => IsObjectFullyLoaded(address)).ConfigureAwait(false);
|
||||
if (!IsZoning && isLoaded)
|
||||
if (isLoaded)
|
||||
return;
|
||||
|
||||
await Task.Delay(100, cancellationToken).ConfigureAwait(false);
|
||||
@@ -312,13 +297,10 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
||||
{
|
||||
DisposeHooks();
|
||||
_activePlayers.Clear();
|
||||
_gposePlayers.Clear();
|
||||
_actorsByHash.Clear();
|
||||
_actorsByName.Clear();
|
||||
_pendingHashResolutions.Clear();
|
||||
_ownedTracker.Reset();
|
||||
Volatile.Write(ref _snapshot, ActorSnapshot.Empty);
|
||||
Volatile.Write(ref _gposeSnapshot, GposeSnapshot.Empty);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
@@ -354,7 +336,7 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
||||
_onCompanionTerminateHook.Enable();
|
||||
|
||||
_hooksActive = true;
|
||||
_logger.LogTrace("ActorObjectService hooks enabled.");
|
||||
_logger.LogDebug("ActorObjectService hooks enabled.");
|
||||
}
|
||||
|
||||
private Task WarmupExistingActors()
|
||||
@@ -368,21 +350,36 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
||||
|
||||
private unsafe void OnCharacterInitialized(Character* chara)
|
||||
{
|
||||
ExecuteOriginal(() => _onInitializeHook!.Original(chara), "Error invoking original character initialize.");
|
||||
QueueTrack((GameObject*)chara);
|
||||
try
|
||||
{
|
||||
_onInitializeHook!.Original(chara);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error invoking original character initialize.");
|
||||
}
|
||||
|
||||
QueueFrameworkUpdate(() => TrackGameObject((GameObject*)chara));
|
||||
}
|
||||
|
||||
private unsafe void OnCharacterTerminated(Character* chara)
|
||||
{
|
||||
var address = (nint)chara;
|
||||
QueueUntrack(address);
|
||||
ExecuteOriginal(() => _onTerminateHook!.Original(chara), "Error invoking original character terminate.");
|
||||
QueueFrameworkUpdate(() => UntrackGameObject(address));
|
||||
try
|
||||
{
|
||||
_onTerminateHook!.Original(chara);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error invoking original character terminate.");
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe GameObject* OnCharacterDisposed(Character* chara, byte freeMemory)
|
||||
{
|
||||
var address = (nint)chara;
|
||||
QueueUntrack(address);
|
||||
QueueFrameworkUpdate(() => UntrackGameObject(address));
|
||||
try
|
||||
{
|
||||
return _onDestructorHook!.Original(chara, freeMemory);
|
||||
@@ -419,7 +416,7 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
||||
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogTrace("Actor tracked: {Name} addr={Address:X} idx={Index} owned={OwnedKind} local={Local} gpose={Gpose}",
|
||||
_logger.LogDebug("Actor tracked: {Name} addr={Address:X} idx={Index} owned={OwnedKind} local={Local} gpose={Gpose}",
|
||||
descriptor.Name,
|
||||
descriptor.Address,
|
||||
descriptor.ObjectIndex,
|
||||
@@ -537,7 +534,7 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
||||
RemoveDescriptor(descriptor);
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogTrace("Actor untracked: {Name} addr={Address:X} idx={Index} owned={OwnedKind}",
|
||||
_logger.LogDebug("Actor untracked: {Name} addr={Address:X} idx={Index} owned={OwnedKind}",
|
||||
descriptor.Name,
|
||||
descriptor.Address,
|
||||
descriptor.ObjectIndex,
|
||||
@@ -561,14 +558,10 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
||||
if (!seen.Add(address))
|
||||
continue;
|
||||
|
||||
var gameObject = (GameObject*)address;
|
||||
if (_activePlayers.TryGetValue(address, out var existing))
|
||||
{
|
||||
RefreshDescriptorIfNeeded(existing, gameObject);
|
||||
if (_activePlayers.ContainsKey(address))
|
||||
continue;
|
||||
}
|
||||
|
||||
TrackGameObject(gameObject);
|
||||
TrackGameObject((GameObject*)address);
|
||||
}
|
||||
|
||||
var stale = _activePlayers.Keys.Where(addr => !seen.Contains(addr)).ToList();
|
||||
@@ -581,50 +574,6 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
||||
{
|
||||
_nextRefreshAllowed = DateTime.UtcNow + SnapshotRefreshInterval;
|
||||
}
|
||||
|
||||
if (_clientState.IsGPosing)
|
||||
{
|
||||
RefreshGposeActorsInternal();
|
||||
}
|
||||
else if (!_gposePlayers.IsEmpty)
|
||||
{
|
||||
_gposePlayers.Clear();
|
||||
PublishGposeSnapshot();
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe void RefreshDescriptorIfNeeded(ActorDescriptor existing, GameObject* gameObject)
|
||||
{
|
||||
if (gameObject == null)
|
||||
return;
|
||||
|
||||
if (existing.ObjectKind != DalamudObjectKind.Player || !string.IsNullOrEmpty(existing.HashedContentId))
|
||||
return;
|
||||
|
||||
var objectKind = (DalamudObjectKind)gameObject->ObjectKind;
|
||||
if (!IsSupportedObjectKind(objectKind))
|
||||
return;
|
||||
|
||||
if (BuildDescriptor(gameObject, objectKind) is not { } updated)
|
||||
return;
|
||||
|
||||
if (string.IsNullOrEmpty(updated.HashedContentId))
|
||||
return;
|
||||
|
||||
ReplaceDescriptor(existing, updated);
|
||||
_mediator.Publish(new ActorTrackedMessage(updated));
|
||||
}
|
||||
|
||||
private void ReplaceDescriptor(ActorDescriptor existing, ActorDescriptor updated)
|
||||
{
|
||||
RemoveDescriptorFromIndexes(existing);
|
||||
_ownedTracker.OnDescriptorRemoved(existing);
|
||||
|
||||
_activePlayers[updated.Address] = updated;
|
||||
IndexDescriptor(updated);
|
||||
_ownedTracker.OnDescriptorAdded(updated);
|
||||
UpdatePendingHashResolutions(updated);
|
||||
PublishSnapshot();
|
||||
}
|
||||
|
||||
private void IndexDescriptor(ActorDescriptor descriptor)
|
||||
@@ -656,15 +605,30 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
||||
|
||||
private unsafe void OnCompanionInitialized(Companion* companion)
|
||||
{
|
||||
ExecuteOriginal(() => _onCompanionInitializeHook!.Original(companion), "Error invoking original companion initialize.");
|
||||
QueueTrack((GameObject*)companion);
|
||||
try
|
||||
{
|
||||
_onCompanionInitializeHook!.Original(companion);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error invoking original companion initialize.");
|
||||
}
|
||||
|
||||
QueueFrameworkUpdate(() => TrackGameObject((GameObject*)companion));
|
||||
}
|
||||
|
||||
private unsafe void OnCompanionTerminated(Companion* companion)
|
||||
{
|
||||
var address = (nint)companion;
|
||||
QueueUntrack(address);
|
||||
ExecuteOriginal(() => _onCompanionTerminateHook!.Original(companion), "Error invoking original companion terminate.");
|
||||
QueueFrameworkUpdate(() => UntrackGameObject(address));
|
||||
try
|
||||
{
|
||||
_onCompanionTerminateHook!.Original(companion);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error invoking original companion terminate.");
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveDescriptorFromIndexes(ActorDescriptor descriptor)
|
||||
@@ -691,7 +655,6 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
||||
_activePlayers[descriptor.Address] = descriptor;
|
||||
IndexDescriptor(descriptor);
|
||||
_ownedTracker.OnDescriptorAdded(descriptor);
|
||||
UpdatePendingHashResolutions(descriptor);
|
||||
PublishSnapshot();
|
||||
}
|
||||
|
||||
@@ -699,42 +662,21 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
||||
{
|
||||
RemoveDescriptorFromIndexes(descriptor);
|
||||
_ownedTracker.OnDescriptorRemoved(descriptor);
|
||||
_pendingHashResolutions.TryRemove(descriptor.Address, out _);
|
||||
PublishSnapshot();
|
||||
}
|
||||
|
||||
private void UpdatePendingHashResolutions(ActorDescriptor descriptor)
|
||||
{
|
||||
if (descriptor.ObjectKind != DalamudObjectKind.Player)
|
||||
{
|
||||
_pendingHashResolutions.TryRemove(descriptor.Address, out _);
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(descriptor.HashedContentId))
|
||||
{
|
||||
_pendingHashResolutions[descriptor.Address] = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
_pendingHashResolutions.TryRemove(descriptor.Address, out _);
|
||||
}
|
||||
|
||||
private void PublishSnapshot()
|
||||
{
|
||||
var playerDescriptors = _activePlayers.Values
|
||||
.Where(descriptor => descriptor.ObjectKind == DalamudObjectKind.Player)
|
||||
.ToArray();
|
||||
var ownedDescriptors = _activePlayers.Values
|
||||
.Where(descriptor => descriptor.OwnedKind is not null)
|
||||
.ToArray();
|
||||
var playerAddresses = new nint[playerDescriptors.Length];
|
||||
for (var i = 0; i < playerDescriptors.Length; i++)
|
||||
playerAddresses[i] = playerDescriptors[i].Address;
|
||||
|
||||
var ownedSnapshot = _ownedTracker.CreateSnapshot();
|
||||
var nextGeneration = Snapshot.Generation + 1;
|
||||
var snapshot = new ActorSnapshot(playerDescriptors, ownedDescriptors, playerAddresses, ownedSnapshot, nextGeneration);
|
||||
var snapshot = new ActorSnapshot(playerDescriptors, playerAddresses, ownedSnapshot, nextGeneration);
|
||||
Volatile.Write(ref _snapshot, snapshot);
|
||||
}
|
||||
|
||||
@@ -752,24 +694,6 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
||||
_ = _framework.RunOnFrameworkThread(action);
|
||||
}
|
||||
|
||||
private void ExecuteOriginal(Action action, string errorMessage)
|
||||
{
|
||||
try
|
||||
{
|
||||
action();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe void QueueTrack(GameObject* gameObject)
|
||||
=> QueueFrameworkUpdate(() => TrackGameObject(gameObject));
|
||||
|
||||
private void QueueUntrack(nint address)
|
||||
=> QueueFrameworkUpdate(() => UntrackGameObject(address));
|
||||
|
||||
private void DisposeHooks()
|
||||
{
|
||||
var hadHooks = _hooksActive
|
||||
@@ -801,7 +725,7 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
||||
|
||||
if (hadHooks)
|
||||
{
|
||||
_logger.LogTrace("ActorObjectService hooks disabled.");
|
||||
_logger.LogDebug("ActorObjectService hooks disabled.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -846,89 +770,6 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
||||
return results;
|
||||
}
|
||||
|
||||
private unsafe void RefreshGposeActorsInternal()
|
||||
{
|
||||
var addresses = EnumerateGposeCharacterAddresses();
|
||||
HashSet<nint> seen = new(addresses.Count);
|
||||
|
||||
foreach (var address in addresses)
|
||||
{
|
||||
if (address == nint.Zero)
|
||||
continue;
|
||||
|
||||
if (!seen.Add(address))
|
||||
continue;
|
||||
|
||||
if (_gposePlayers.ContainsKey(address))
|
||||
continue;
|
||||
|
||||
TrackGposeObject((GameObject*)address);
|
||||
}
|
||||
|
||||
var stale = _gposePlayers.Keys.Where(addr => !seen.Contains(addr)).ToList();
|
||||
foreach (var staleAddress in stale)
|
||||
{
|
||||
UntrackGposeObject(staleAddress);
|
||||
}
|
||||
|
||||
PublishGposeSnapshot();
|
||||
}
|
||||
|
||||
private unsafe void TrackGposeObject(GameObject* gameObject)
|
||||
{
|
||||
if (gameObject == null)
|
||||
return;
|
||||
|
||||
var objectKind = (DalamudObjectKind)gameObject->ObjectKind;
|
||||
if (objectKind != DalamudObjectKind.Player)
|
||||
return;
|
||||
|
||||
if (BuildDescriptor(gameObject, objectKind) is not { } descriptor)
|
||||
return;
|
||||
|
||||
if (!descriptor.IsInGpose)
|
||||
return;
|
||||
|
||||
_gposePlayers[descriptor.Address] = descriptor;
|
||||
}
|
||||
|
||||
private void UntrackGposeObject(nint address)
|
||||
{
|
||||
if (address == nint.Zero)
|
||||
return;
|
||||
|
||||
_gposePlayers.TryRemove(address, out _);
|
||||
}
|
||||
|
||||
private void PublishGposeSnapshot()
|
||||
{
|
||||
var gposeDescriptors = _gposePlayers.Values.ToArray();
|
||||
var gposeAddresses = new nint[gposeDescriptors.Length];
|
||||
for (var i = 0; i < gposeDescriptors.Length; i++)
|
||||
gposeAddresses[i] = gposeDescriptors[i].Address;
|
||||
|
||||
var nextGeneration = CurrentGposeSnapshot.Generation + 1;
|
||||
var snapshot = new GposeSnapshot(gposeDescriptors, gposeAddresses, nextGeneration);
|
||||
Volatile.Write(ref _gposeSnapshot, snapshot);
|
||||
}
|
||||
|
||||
private List<nint> EnumerateGposeCharacterAddresses()
|
||||
{
|
||||
var results = new List<nint>(16);
|
||||
foreach (var obj in _objectTable)
|
||||
{
|
||||
if (obj.ObjectKind != DalamudObjectKind.Player)
|
||||
continue;
|
||||
|
||||
if (obj.ObjectIndex < 200)
|
||||
continue;
|
||||
|
||||
results.Add(obj.Address);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private static unsafe bool IsObjectFullyLoaded(nint address)
|
||||
{
|
||||
if (address == nint.Zero)
|
||||
@@ -942,10 +783,13 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
||||
if (drawObject == null)
|
||||
return false;
|
||||
|
||||
if ((ulong)gameObject->RenderFlags == 2048)
|
||||
if ((gameObject->RenderFlags & VisibilityFlags.Nameplate) != VisibilityFlags.None)
|
||||
return false;
|
||||
|
||||
var characterBase = (CharacterBase*)drawObject;
|
||||
if (characterBase == null)
|
||||
return false;
|
||||
|
||||
if (characterBase->HasModelInSlotLoaded != 0)
|
||||
return false;
|
||||
|
||||
@@ -1081,27 +925,14 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
||||
|
||||
private sealed record ActorSnapshot(
|
||||
IReadOnlyList<ActorDescriptor> PlayerDescriptors,
|
||||
IReadOnlyList<ActorDescriptor> OwnedDescriptors,
|
||||
IReadOnlyList<nint> PlayerAddresses,
|
||||
OwnedObjectSnapshot OwnedObjects,
|
||||
int Generation)
|
||||
{
|
||||
public static ActorSnapshot Empty { get; } = new(
|
||||
Array.Empty<ActorDescriptor>(),
|
||||
Array.Empty<ActorDescriptor>(),
|
||||
Array.Empty<nint>(),
|
||||
OwnedObjectSnapshot.Empty,
|
||||
0);
|
||||
}
|
||||
|
||||
private sealed record GposeSnapshot(
|
||||
IReadOnlyList<ActorDescriptor> GposeDescriptors,
|
||||
IReadOnlyList<nint> GposeAddresses,
|
||||
int Generation)
|
||||
{
|
||||
public static GposeSnapshot Empty { get; } = new(
|
||||
Array.Empty<ActorDescriptor>(),
|
||||
Array.Empty<nint>(),
|
||||
0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using LightlessSync.API.Dto.Chat;
|
||||
using LightlessSync.API.Data.Extensions;
|
||||
using LightlessSync.Services.ActorTracking;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using LightlessSync.WebAPI;
|
||||
@@ -24,7 +23,6 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
||||
private readonly DalamudUtilService _dalamudUtilService;
|
||||
private readonly ActorObjectService _actorObjectService;
|
||||
private readonly PairUiService _pairUiService;
|
||||
private readonly ChatConfigService _chatConfigService;
|
||||
|
||||
private readonly Lock _sync = new();
|
||||
|
||||
@@ -37,8 +35,6 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
||||
private readonly Dictionary<string, bool> _lastPresenceStates = new(StringComparer.Ordinal);
|
||||
private readonly Dictionary<string, string> _selfTokens = new(StringComparer.Ordinal);
|
||||
private readonly List<PendingSelfMessage> _pendingSelfMessages = new();
|
||||
private List<ChatChannelSnapshot>? _cachedChannelSnapshots;
|
||||
private bool _channelsSnapshotDirty = true;
|
||||
|
||||
private bool _isLoggedIn;
|
||||
private bool _isConnected;
|
||||
@@ -61,7 +57,6 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
||||
_dalamudUtilService = dalamudUtilService;
|
||||
_actorObjectService = actorObjectService;
|
||||
_pairUiService = pairUiService;
|
||||
_chatConfigService = chatConfigService;
|
||||
|
||||
_isLoggedIn = _dalamudUtilService.IsLoggedIn;
|
||||
_isConnected = _apiController.IsConnected;
|
||||
@@ -72,11 +67,6 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
||||
{
|
||||
using (_sync.EnterScope())
|
||||
{
|
||||
if (!_channelsSnapshotDirty && _cachedChannelSnapshots is not null)
|
||||
{
|
||||
return _cachedChannelSnapshots;
|
||||
}
|
||||
|
||||
var snapshots = new List<ChatChannelSnapshot>(_channelOrder.Count);
|
||||
foreach (var key in _channelOrder)
|
||||
{
|
||||
@@ -106,8 +96,6 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
||||
state.Messages.ToList()));
|
||||
}
|
||||
|
||||
_cachedChannelSnapshots = snapshots;
|
||||
_channelsSnapshotDirty = false;
|
||||
return snapshots;
|
||||
}
|
||||
}
|
||||
@@ -145,44 +133,6 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
||||
state.UnreadCount = 0;
|
||||
_lastReadCounts[key] = state.Messages.Count;
|
||||
}
|
||||
|
||||
MarkChannelsSnapshotDirtyLocked();
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,7 +148,6 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
||||
if (!wasEnabled)
|
||||
{
|
||||
_chatEnabled = true;
|
||||
MarkChannelsSnapshotDirtyLocked();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -244,8 +193,6 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
||||
state.IsAvailable = false;
|
||||
state.StatusText = "Chat services disabled";
|
||||
}
|
||||
|
||||
MarkChannelsSnapshotDirtyLocked();
|
||||
}
|
||||
|
||||
UnregisterChatHandler();
|
||||
@@ -565,7 +512,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
||||
|
||||
if (!_isLoggedIn || !_apiController.IsConnected)
|
||||
{
|
||||
await LeaveCurrentZoneAsync(force, 0, 0).ConfigureAwait(false);
|
||||
await LeaveCurrentZoneAsync(force, 0).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -573,7 +520,6 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
||||
{
|
||||
var location = await _dalamudUtilService.GetMapDataAsync().ConfigureAwait(false);
|
||||
var territoryId = (ushort)location.TerritoryId;
|
||||
var worldId = (ushort)location.ServerId;
|
||||
|
||||
string? zoneKey;
|
||||
ZoneChannelDefinition? definition = null;
|
||||
@@ -590,14 +536,14 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
||||
|
||||
if (definition is null)
|
||||
{
|
||||
await LeaveCurrentZoneAsync(force, territoryId, worldId).ConfigureAwait(false);
|
||||
await LeaveCurrentZoneAsync(force, territoryId).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
var descriptor = await BuildZoneDescriptorAsync(definition.Value).ConfigureAwait(false);
|
||||
if (descriptor is null)
|
||||
{
|
||||
await LeaveCurrentZoneAsync(force, territoryId, worldId).ConfigureAwait(false);
|
||||
await LeaveCurrentZoneAsync(force, territoryId).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -640,7 +586,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LeaveCurrentZoneAsync(bool force, ushort territoryId, ushort worldId)
|
||||
private async Task LeaveCurrentZoneAsync(bool force, ushort territoryId)
|
||||
{
|
||||
ChatChannelDescriptor? descriptor = null;
|
||||
|
||||
@@ -656,27 +602,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
||||
state.StatusText = !_chatEnabled
|
||||
? "Chat services disabled"
|
||||
: (_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";
|
||||
}
|
||||
|
||||
if (worldId != 0)
|
||||
{
|
||||
state.Descriptor = new ChatChannelDescriptor
|
||||
{
|
||||
Type = ChatChannelType.Zone,
|
||||
WorldId = worldId,
|
||||
ZoneId = territoryId,
|
||||
CustomKey = string.Empty
|
||||
};
|
||||
}
|
||||
state.DisplayName = "Zone Chat";
|
||||
}
|
||||
|
||||
if (string.Equals(_activeChannelKey, ZoneChannelKey, StringComparison.Ordinal))
|
||||
@@ -732,7 +658,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
||||
_zoneDefinitions[key] = new ZoneChannelDefinition(key, info.DisplayName ?? key, descriptor, territories);
|
||||
}
|
||||
|
||||
var territoryData = _dalamudUtilService.TerritoryDataEnglish.Value;
|
||||
var territoryData = _dalamudUtilService.TerritoryData.Value;
|
||||
foreach (var kvp in territoryData)
|
||||
{
|
||||
foreach (var variant in EnumerateTerritoryKeys(kvp.Value))
|
||||
@@ -868,12 +794,6 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
||||
var infos = new List<GroupChatChannelInfoDto>(groups.Count);
|
||||
foreach (var group in groups)
|
||||
{
|
||||
// basically prune the channel if it's disabled
|
||||
if (group.GroupPermissions.IsDisableChat())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var descriptor = new ChatChannelDescriptor
|
||||
{
|
||||
Type = ChatChannelType.Group,
|
||||
@@ -1044,8 +964,6 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
||||
state.UnreadCount = Math.Min(Math.Max(unreadFromHistory, incrementalUnread), MaxUnreadCount);
|
||||
state.HasUnread = state.UnreadCount > 0;
|
||||
}
|
||||
|
||||
MarkChannelsSnapshotDirtyLocked();
|
||||
}
|
||||
|
||||
Mediator.Publish(new ChatChannelMessageAdded(key, message));
|
||||
@@ -1174,50 +1092,17 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
||||
{
|
||||
_channelOrder.Clear();
|
||||
|
||||
var configuredOrder = _chatConfigService.Current.ChannelOrder;
|
||||
if (configuredOrder.Count > 0)
|
||||
if (_channels.ContainsKey(ZoneChannelKey))
|
||||
{
|
||||
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);
|
||||
}
|
||||
_channelOrder.Add(ZoneChannelKey);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_channels.ContainsKey(ZoneChannelKey))
|
||||
{
|
||||
_channelOrder.Add(ZoneChannelKey);
|
||||
}
|
||||
|
||||
var groups = _channels.Values
|
||||
.Where(state => state.Type == ChatChannelType.Group)
|
||||
.OrderBy(state => state.DisplayName, StringComparer.OrdinalIgnoreCase)
|
||||
.Select(state => state.Key);
|
||||
var groups = _channels.Values
|
||||
.Where(state => state.Type == ChatChannelType.Group)
|
||||
.OrderBy(state => state.DisplayName, StringComparer.OrdinalIgnoreCase)
|
||||
.Select(state => state.Key);
|
||||
|
||||
_channelOrder.AddRange(groups);
|
||||
}
|
||||
_channelOrder.AddRange(groups);
|
||||
|
||||
if (_activeChannelKey is null && _channelOrder.Count > 0)
|
||||
{
|
||||
@@ -1227,25 +1112,9 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
||||
{
|
||||
_activeChannelKey = _channelOrder.Count > 0 ? _channelOrder[0] : null;
|
||||
}
|
||||
|
||||
MarkChannelsSnapshotDirtyLocked();
|
||||
}
|
||||
|
||||
private void MarkChannelsSnapshotDirty()
|
||||
{
|
||||
using (_sync.EnterScope())
|
||||
{
|
||||
_channelsSnapshotDirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void MarkChannelsSnapshotDirtyLocked() => _channelsSnapshotDirty = true;
|
||||
|
||||
private void PublishChannelListChanged()
|
||||
{
|
||||
MarkChannelsSnapshotDirty();
|
||||
Mediator.Publish(new ChatChannelsUpdated());
|
||||
}
|
||||
private void PublishChannelListChanged() => Mediator.Publish(new ChatChannelsUpdated());
|
||||
|
||||
private static IEnumerable<string> EnumerateTerritoryKeys(string? value)
|
||||
{
|
||||
|
||||
@@ -48,8 +48,7 @@ public sealed class CommandManagerService : IDisposable
|
||||
"\t /light gpose - Opens the Lightless Character Data Hub 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 finder - Opens the Lightfinder window" + Environment.NewLine +
|
||||
"\t /light chat - Opens the Lightless Chat window"
|
||||
"\t /light finder - Opens the Lightfinder window"
|
||||
});
|
||||
}
|
||||
|
||||
@@ -134,9 +133,5 @@ public sealed class CommandManagerService : IDisposable
|
||||
{
|
||||
_mediator.Publish(new UiToggleMessage(typeof(LightFinderUI)));
|
||||
}
|
||||
else if (string.Equals(splitArgs[0], "chat", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_mediator.Publish(new UiToggleMessage(typeof(ZoneChatUi)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -129,6 +129,7 @@ internal class ContextMenuService : IHostedService
|
||||
|
||||
var snapshot = _pairUiService.GetSnapshot();
|
||||
var pair = snapshot.PairsByUid.Values.FirstOrDefault(p =>
|
||||
p.IsVisible &&
|
||||
p.PlayerCharacterId != uint.MaxValue &&
|
||||
p.PlayerCharacterId == target.TargetObjectId);
|
||||
|
||||
|
||||
@@ -91,10 +91,43 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
return gameData.GetExcelSheet<ClassJob>(Dalamud.Game.ClientLanguage.English)!
|
||||
.ToDictionary(k => k.RowId, k => k.NameEnglish.ToString());
|
||||
});
|
||||
var clientLanguage = _clientState.ClientLanguage;
|
||||
TerritoryData = new(() => BuildTerritoryData(clientLanguage));
|
||||
TerritoryDataEnglish = new(() => BuildTerritoryData(Dalamud.Game.ClientLanguage.English));
|
||||
MapData = new(() => BuildMapData(clientLanguage));
|
||||
TerritoryData = new(() =>
|
||||
{
|
||||
return gameData.GetExcelSheet<TerritoryType>(Dalamud.Game.ClientLanguage.English)!
|
||||
.Where(w => w.RowId != 0)
|
||||
.ToDictionary(w => w.RowId, w =>
|
||||
{
|
||||
StringBuilder sb = new();
|
||||
sb.Append(w.PlaceNameRegion.Value.Name);
|
||||
if (w.PlaceName.ValueNullable != null)
|
||||
{
|
||||
sb.Append(" - ");
|
||||
sb.Append(w.PlaceName.Value.Name);
|
||||
}
|
||||
return sb.ToString();
|
||||
});
|
||||
});
|
||||
MapData = new(() =>
|
||||
{
|
||||
return gameData.GetExcelSheet<Map>(Dalamud.Game.ClientLanguage.English)!
|
||||
.Where(w => w.RowId != 0)
|
||||
.ToDictionary(w => w.RowId, w =>
|
||||
{
|
||||
StringBuilder sb = new();
|
||||
sb.Append(w.PlaceNameRegion.Value.Name);
|
||||
if (w.PlaceName.ValueNullable != null)
|
||||
{
|
||||
sb.Append(" - ");
|
||||
sb.Append(w.PlaceName.Value.Name);
|
||||
}
|
||||
if (w.PlaceNameSub.ValueNullable != null && !string.IsNullOrEmpty(w.PlaceNameSub.Value.Name.ToString()))
|
||||
{
|
||||
sb.Append(" - ");
|
||||
sb.Append(w.PlaceNameSub.Value.Name);
|
||||
}
|
||||
return (w, sb.ToString());
|
||||
});
|
||||
});
|
||||
mediator.Subscribe<TargetPairMessage>(this, (msg) =>
|
||||
{
|
||||
if (clientState.IsPvP) return;
|
||||
@@ -125,71 +158,6 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
private Lazy<ulong> RebuildCID() => new(GetCID);
|
||||
|
||||
public bool IsWine { get; init; }
|
||||
private Dictionary<uint, string> BuildTerritoryData(Dalamud.Game.ClientLanguage language)
|
||||
{
|
||||
var placeNames = _gameData.GetExcelSheet<PlaceName>(language)!;
|
||||
return _gameData.GetExcelSheet<TerritoryType>(language)!
|
||||
.Where(w => w.RowId != 0)
|
||||
.ToDictionary(w => w.RowId, w =>
|
||||
{
|
||||
var regionName = GetPlaceName(placeNames, w.PlaceNameRegion.RowId);
|
||||
var placeName = GetPlaceName(placeNames, w.PlaceName.RowId);
|
||||
return BuildPlaceName(regionName, placeName, string.Empty);
|
||||
});
|
||||
}
|
||||
|
||||
private Dictionary<uint, (Map Map, string MapName)> BuildMapData(Dalamud.Game.ClientLanguage language)
|
||||
{
|
||||
var placeNames = _gameData.GetExcelSheet<PlaceName>(language)!;
|
||||
return _gameData.GetExcelSheet<Map>(language)!
|
||||
.Where(w => w.RowId != 0)
|
||||
.ToDictionary(w => w.RowId, w =>
|
||||
{
|
||||
var regionName = GetPlaceName(placeNames, w.PlaceNameRegion.RowId);
|
||||
var placeName = GetPlaceName(placeNames, w.PlaceName.RowId);
|
||||
var subPlaceName = GetPlaceName(placeNames, w.PlaceNameSub.RowId);
|
||||
var displayName = BuildPlaceName(regionName, placeName, subPlaceName);
|
||||
return (w, displayName);
|
||||
});
|
||||
}
|
||||
private static string GetPlaceName(Lumina.Excel.ExcelSheet<PlaceName> placeNames, uint rowId)
|
||||
{
|
||||
if (rowId == 0)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return placeNames.GetRow(rowId).Name.ToString();
|
||||
}
|
||||
|
||||
private static string BuildPlaceName(string regionName, string placeName, string subPlaceName)
|
||||
{
|
||||
StringBuilder sb = new();
|
||||
if (!string.IsNullOrWhiteSpace(regionName))
|
||||
{
|
||||
sb.Append(regionName);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(placeName))
|
||||
{
|
||||
if (sb.Length > 0)
|
||||
{
|
||||
sb.Append(" - ");
|
||||
}
|
||||
sb.Append(placeName);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(subPlaceName))
|
||||
{
|
||||
if (sb.Length > 0)
|
||||
{
|
||||
sb.Append(" - ");
|
||||
}
|
||||
sb.Append(subPlaceName);
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
private bool ResolvePairAddress(Pair pair, out Pair resolvedPair, out nint address)
|
||||
{
|
||||
resolvedPair = _pairFactory.Value.Create(pair.UniqueIdent) ?? pair;
|
||||
@@ -271,43 +239,15 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
public bool IsInCombat { get; private set; } = false;
|
||||
public bool IsPerforming { get; private set; } = false;
|
||||
public bool IsInInstance { get; private set; } = false;
|
||||
public bool IsInDuty => _condition[ConditionFlag.BoundByDuty];
|
||||
public bool HasModifiedGameFiles => _gameData.HasModifiedGameDataFiles;
|
||||
public uint ClassJobId => _classJobId!.Value;
|
||||
public Lazy<Dictionary<uint, string>> JobData { get; private set; }
|
||||
public Lazy<Dictionary<ushort, string>> WorldData { get; private set; }
|
||||
public Lazy<Dictionary<uint, string>> TerritoryData { get; private set; }
|
||||
public Lazy<Dictionary<uint, string>> TerritoryDataEnglish { get; private set; }
|
||||
public Lazy<Dictionary<uint, (Map Map, string MapName)>> MapData { get; private set; }
|
||||
public bool IsLodEnabled { get; private set; }
|
||||
public LightlessMediator Mediator { get; }
|
||||
|
||||
public bool IsInFieldOperation
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!IsInDuty)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var territoryId = _clientState.TerritoryType;
|
||||
if (territoryId == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TerritoryDataEnglish.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)
|
||||
{
|
||||
EnsureIsOnFramework();
|
||||
@@ -360,8 +300,8 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
|
||||
public IEnumerable<ICharacter?> GetGposeCharactersFromObjectTable()
|
||||
{
|
||||
foreach (var actor in _objectTable
|
||||
.Where(a => a.ObjectIndex > 200 && a.ObjectKind == DalamudObjectKind.Player))
|
||||
foreach (var actor in _actorObjectService.PlayerDescriptors
|
||||
.Where(a => a.ObjectKind == DalamudObjectKind.Player && a.ObjectIndex > 200))
|
||||
{
|
||||
var character = _objectTable.CreateObjectReference(actor.Address) as ICharacter;
|
||||
if (character != null)
|
||||
@@ -388,8 +328,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
|
||||
var playerAddress = playerPointer.Value;
|
||||
var ownerEntityId = ((Character*)playerAddress)->EntityId;
|
||||
var candidateAddress = _objectTable.GetObjectAddress(((GameObject*)playerAddress)->ObjectIndex + 1);
|
||||
if (ownerEntityId == 0) return candidateAddress;
|
||||
if (ownerEntityId == 0) return IntPtr.Zero;
|
||||
|
||||
if (playerAddress == _actorObjectService.LocalPlayerAddress)
|
||||
{
|
||||
@@ -400,17 +339,6 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
}
|
||||
}
|
||||
|
||||
if (candidateAddress != nint.Zero)
|
||||
{
|
||||
var candidate = (GameObject*)candidateAddress;
|
||||
var candidateKind = (DalamudObjectKind)candidate->ObjectKind;
|
||||
if ((candidateKind == DalamudObjectKind.MountType || candidateKind == DalamudObjectKind.Companion)
|
||||
&& ResolveOwnerId(candidate) == ownerEntityId)
|
||||
{
|
||||
return candidateAddress;
|
||||
}
|
||||
}
|
||||
|
||||
var ownedObject = FindOwnedObject(ownerEntityId, playerAddress, static kind =>
|
||||
kind == DalamudObjectKind.MountType || kind == DalamudObjectKind.Companion);
|
||||
if (ownedObject != nint.Zero)
|
||||
@@ -418,7 +346,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
return ownedObject;
|
||||
}
|
||||
|
||||
return candidateAddress;
|
||||
return _objectTable.GetObjectAddress(((GameObject*)playerAddress)->ObjectIndex + 1);
|
||||
}
|
||||
|
||||
public async Task<IntPtr> GetMinionOrMountAsync(IntPtr? playerPointer = null)
|
||||
@@ -535,10 +463,6 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
{
|
||||
EnsureIsOnFramework();
|
||||
var playerChar = GetPlayerCharacter();
|
||||
|
||||
if (playerChar == null || playerChar.Address == IntPtr.Zero)
|
||||
return 0;
|
||||
|
||||
return ((BattleChara*)playerChar.Address)->Character.ContentId;
|
||||
}
|
||||
|
||||
@@ -829,7 +753,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
bool isDrawingChanged = false;
|
||||
if ((nint)drawObj != IntPtr.Zero)
|
||||
{
|
||||
isDrawing = gameObj->RenderFlags == (VisibilityFlags)0b100000000000;
|
||||
isDrawing = (gameObj->RenderFlags & VisibilityFlags.Nameplate) != VisibilityFlags.None;
|
||||
if (!isDrawing)
|
||||
{
|
||||
isDrawing = ((CharacterBase*)drawObj)->HasModelInSlotLoaded != 0;
|
||||
@@ -895,12 +819,9 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
_performanceCollector.LogPerformance(this, $"TrackedActorsToState",
|
||||
() =>
|
||||
{
|
||||
if (!_actorObjectService.HooksActive || !isNormalFrameworkUpdate || _actorObjectService.HasPendingHashResolutions)
|
||||
{
|
||||
_actorObjectService.RefreshTrackedActors();
|
||||
}
|
||||
_actorObjectService.RefreshTrackedActors();
|
||||
|
||||
var playerDescriptors = _actorObjectService.PlayerDescriptors;
|
||||
var playerDescriptors = _actorObjectService.PlayerCharacterDescriptors;
|
||||
for (var i = 0; i < playerDescriptors.Count; i++)
|
||||
{
|
||||
var actor = playerDescriptors[i];
|
||||
|
||||
@@ -4,7 +4,6 @@ using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
|
||||
using Dalamud.Game.ClientState.Objects.Enums;
|
||||
using Dalamud.Game.Text;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Plugin;
|
||||
using Dalamud.Plugin.Services;
|
||||
using FFXIVClientStructs.FFXIV.Client.System.Framework;
|
||||
@@ -17,27 +16,22 @@ using LightlessSync.UI;
|
||||
using LightlessSync.UI.Services;
|
||||
using LightlessSync.Utils;
|
||||
using LightlessSync.UtilsEnum.Enum;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Pictomancy;
|
||||
using System.Collections.Immutable;
|
||||
using System.Globalization;
|
||||
using System.Numerics;
|
||||
using System.Runtime.InteropServices;
|
||||
using Task = System.Threading.Tasks.Task;
|
||||
|
||||
namespace LightlessSync.Services.LightFinder;
|
||||
|
||||
/// <summary>
|
||||
/// The new lightfinder nameplate handler using ImGUI (pictomancy) for rendering the icon/labels.
|
||||
/// </summary>
|
||||
public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscriber
|
||||
{
|
||||
private readonly ILogger<LightFinderPlateHandler> _logger;
|
||||
private readonly IAddonLifecycle _addonLifecycle;
|
||||
private readonly IGameGui _gameGui;
|
||||
private readonly IObjectTable _objectTable;
|
||||
private readonly IClientState _clientState;
|
||||
private readonly LightlessConfigService _configService;
|
||||
private readonly PairUiService _pairUiService;
|
||||
private readonly LightlessMediator _mediator;
|
||||
@@ -48,33 +42,21 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
||||
private bool _needsLabelRefresh;
|
||||
private bool _drawSubscribed;
|
||||
private AddonNamePlate* _mpNameplateAddon;
|
||||
private readonly Lock _labelLock = new();
|
||||
private readonly object _labelLock = new();
|
||||
private readonly NameplateBuffers _buffers = new();
|
||||
private int _labelRenderCount;
|
||||
|
||||
private const string _defaultLabelText = "LightFinder";
|
||||
private const SeIconChar _defaultIcon = SeIconChar.Hyadelyn;
|
||||
private static readonly string _defaultIconGlyph = SeIconCharExtensions.ToIconString(_defaultIcon);
|
||||
private static readonly Vector2 _defaultPivot = new(0.5f, 1f);
|
||||
private uint _lastNamePlateDrawFrame;
|
||||
private const string DefaultLabelText = "LightFinder";
|
||||
private const SeIconChar DefaultIcon = SeIconChar.Hyadelyn;
|
||||
private static readonly string DefaultIconGlyph = SeIconCharExtensions.ToIconString(DefaultIcon);
|
||||
private static readonly Vector2 DefaultPivot = new(0.5f, 1f);
|
||||
|
||||
// / Overlay window flags
|
||||
private const ImGuiWindowFlags _overlayFlags =
|
||||
ImGuiWindowFlags.NoDecoration |
|
||||
ImGuiWindowFlags.NoBackground |
|
||||
ImGuiWindowFlags.NoMove |
|
||||
ImGuiWindowFlags.NoSavedSettings |
|
||||
ImGuiWindowFlags.NoNav |
|
||||
ImGuiWindowFlags.NoInputs;
|
||||
|
||||
private readonly List<RectF> _uiRects = new(128);
|
||||
private ImmutableHashSet<string> _activeBroadcastingCids = [];
|
||||
|
||||
public LightFinderPlateHandler(
|
||||
ILogger<LightFinderPlateHandler> logger,
|
||||
IAddonLifecycle addonLifecycle,
|
||||
IGameGui gameGui,
|
||||
IClientState clientState,
|
||||
LightlessConfigService configService,
|
||||
LightlessMediator mediator,
|
||||
IObjectTable objectTable,
|
||||
@@ -85,7 +67,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
||||
_logger = logger;
|
||||
_addonLifecycle = addonLifecycle;
|
||||
_gameGui = gameGui;
|
||||
_clientState = clientState;
|
||||
_configService = configService;
|
||||
_mediator = mediator;
|
||||
_objectTable = objectTable;
|
||||
@@ -120,9 +101,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
||||
_mpNameplateAddon = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enable nameplate handling.
|
||||
/// </summary>
|
||||
internal void EnableNameplate()
|
||||
{
|
||||
if (!_mEnabled)
|
||||
@@ -140,9 +118,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disable nameplate handling.
|
||||
/// </summary>
|
||||
internal void DisableNameplate()
|
||||
{
|
||||
if (_mEnabled)
|
||||
@@ -161,21 +136,8 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draw detour for nameplate addon.
|
||||
/// </summary>
|
||||
/// <param name="type"></param>
|
||||
/// <param name="args"></param>
|
||||
private void NameplateDrawDetour(AddonEvent type, AddonArgs args)
|
||||
{
|
||||
if (_clientState.IsGPosing)
|
||||
{
|
||||
ClearLabelBuffer();
|
||||
Array.Clear(_buffers.HasSmoothed, 0, _buffers.HasSmoothed.Length);
|
||||
_lastNamePlateDrawFrame = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.Addon.Address == nint.Zero)
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Warning))
|
||||
@@ -183,10 +145,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
||||
return;
|
||||
}
|
||||
|
||||
var fw = Framework.Instance();
|
||||
if (fw != null)
|
||||
_lastNamePlateDrawFrame = fw->FrameCounter;
|
||||
|
||||
var pNameplateAddon = (AddonNamePlate*)args.Addon.Address;
|
||||
|
||||
if (_mpNameplateAddon != pNameplateAddon)
|
||||
@@ -198,9 +156,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
||||
UpdateNameplateNodes();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the nameplate nodes with LightFinder objects.
|
||||
/// </summary>
|
||||
private void UpdateNameplateNodes()
|
||||
{
|
||||
var currentHandle = _gameGui.GetAddonByName("NamePlate");
|
||||
@@ -220,12 +175,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
||||
return;
|
||||
}
|
||||
|
||||
if (!IsNamePlateAddonVisible())
|
||||
{
|
||||
ClearLabelBuffer();
|
||||
return;
|
||||
}
|
||||
|
||||
var framework = Framework.Instance();
|
||||
if (framework == null)
|
||||
{
|
||||
@@ -258,7 +207,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
||||
}
|
||||
|
||||
var visibleUserIdsSnapshot = VisibleUserIds;
|
||||
var safeCount = Math.Min(ui3DModule->NamePlateObjectInfoCount, vec.Length);
|
||||
var safeCount = System.Math.Min(ui3DModule->NamePlateObjectInfoCount, vec.Length);
|
||||
var currentConfig = _configService.Current;
|
||||
var labelColor = UIColors.Get("Lightfinder");
|
||||
var edgeColor = UIColors.Get("LightfinderEdge");
|
||||
@@ -266,7 +215,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
||||
|
||||
for (int i = 0; i < safeCount; ++i)
|
||||
{
|
||||
|
||||
var objectInfoPtr = vec[i];
|
||||
if (objectInfoPtr == null)
|
||||
continue;
|
||||
@@ -302,6 +250,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
||||
var root = nameplateObject.RootComponentNode;
|
||||
var nameContainer = nameplateObject.NameContainer;
|
||||
var nameText = nameplateObject.NameText;
|
||||
var marker = nameplateObject.MarkerIcon;
|
||||
|
||||
if (root == null || root->Component == null || nameContainer == null || nameText == null)
|
||||
{
|
||||
@@ -312,14 +261,14 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
||||
|
||||
root->Component->UldManager.UpdateDrawNodeList();
|
||||
|
||||
bool isNameplateVisible =
|
||||
nameContainer->IsVisible() &&
|
||||
nameText->AtkResNode.IsVisible();
|
||||
bool isVisible =
|
||||
(marker != null && marker->AtkResNode.IsVisible()) ||
|
||||
(nameContainer->IsVisible() && nameText->AtkResNode.IsVisible()) ||
|
||||
currentConfig.LightfinderLabelShowHidden;
|
||||
|
||||
if (!currentConfig.LightfinderLabelShowHidden && !isNameplateVisible)
|
||||
if (!isVisible)
|
||||
continue;
|
||||
|
||||
// Prepare label content and scaling
|
||||
var scaleMultiplier = System.Math.Clamp(currentConfig.LightfinderLabelScale, 0.5f, 2.0f);
|
||||
var baseScale = currentConfig.LightfinderLabelUseIcon ? 1.0f : 0.5f;
|
||||
var effectiveScale = baseScale * scaleMultiplier;
|
||||
@@ -327,10 +276,10 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
||||
var targetFontSize = (int)System.Math.Round(baseFontSize * scaleMultiplier);
|
||||
var labelContent = currentConfig.LightfinderLabelUseIcon
|
||||
? NormalizeIconGlyph(currentConfig.LightfinderLabelIconGlyph)
|
||||
: _defaultLabelText;
|
||||
: DefaultLabelText;
|
||||
|
||||
if (!currentConfig.LightfinderLabelUseIcon && (string.IsNullOrWhiteSpace(labelContent) || string.Equals(labelContent, "-", StringComparison.Ordinal)))
|
||||
labelContent = _defaultLabelText;
|
||||
labelContent = DefaultLabelText;
|
||||
|
||||
var nodeWidth = (int)System.Math.Round(AtkNodeHelpers.DefaultTextNodeWidth * effectiveScale);
|
||||
var nodeHeight = (int)System.Math.Round(AtkNodeHelpers.DefaultTextNodeHeight * effectiveScale);
|
||||
@@ -373,7 +322,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
||||
() => GetScaledTextWidth(nameText),
|
||||
nodeWidth);
|
||||
|
||||
// Text offset caching
|
||||
var textOffset = (int)System.Math.Round(nameText->AtkResNode.X);
|
||||
var hasValidOffset = TryCacheTextOffset(nameplateIndex, rawTextWidth, textOffset);
|
||||
|
||||
@@ -384,93 +332,65 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
||||
continue;
|
||||
}
|
||||
|
||||
var res = nameContainer;
|
||||
|
||||
// X scale
|
||||
var worldScaleX = GetWorldScaleX(res);
|
||||
if (worldScaleX <= 0f) worldScaleX = 1f;
|
||||
|
||||
// Y scale
|
||||
var worldScaleY = GetWorldScaleY(res);
|
||||
if (worldScaleY <= 0f) worldScaleY = 1f;
|
||||
|
||||
positionY += currentConfig.LightfinderLabelOffsetY;
|
||||
var positionYScreen = positionY * worldScaleY;
|
||||
|
||||
float finalX;
|
||||
if (currentConfig.LightfinderAutoAlign)
|
||||
{
|
||||
// auto X positioning
|
||||
var measuredWidth = Math.Max(1, textWidth > 0 ? textWidth : nodeWidth);
|
||||
var measuredWidth = System.Math.Max(1, textWidth > 0 ? textWidth : nodeWidth);
|
||||
var measuredWidthF = (float)measuredWidth;
|
||||
var alignmentType = currentConfig.LabelAlignment;
|
||||
|
||||
// consider icon width
|
||||
var containerWidthLocal = res->Width > 0 ? res->Width : measuredWidthF;
|
||||
var containerWidthScreen = containerWidthLocal * worldScaleX;
|
||||
var containerScale = nameContainer->ScaleX;
|
||||
if (containerScale <= 0f)
|
||||
containerScale = 1f;
|
||||
var containerWidthRaw = (float)nameContainer->Width;
|
||||
if (containerWidthRaw <= 0f)
|
||||
containerWidthRaw = measuredWidthF;
|
||||
var containerWidth = containerWidthRaw * containerScale;
|
||||
if (containerWidth <= 0f)
|
||||
containerWidth = measuredWidthF;
|
||||
|
||||
// container bounds for positions
|
||||
var containerLeft = res->ScreenX;
|
||||
var containerRight = containerLeft + containerWidthScreen;
|
||||
var containerCenter = containerLeft + (containerWidthScreen * 0.5f);
|
||||
var containerLeft = nameContainer->ScreenX;
|
||||
var containerRight = containerLeft + containerWidth;
|
||||
var containerCenter = containerLeft + (containerWidth * 0.5f);
|
||||
|
||||
var iconMargin = currentConfig.LightfinderLabelUseIcon
|
||||
? MathF.Min(containerWidthScreen * 0.1f, 14f * worldScaleX)
|
||||
? System.Math.Min(containerWidth * 0.1f, 14f * containerScale)
|
||||
: 0f;
|
||||
|
||||
var offsetXScreen = currentConfig.LightfinderLabelOffsetX * worldScaleX;
|
||||
|
||||
// alignment based on config
|
||||
switch (currentConfig.LabelAlignment)
|
||||
switch (alignmentType)
|
||||
{
|
||||
case LabelAlignment.Left:
|
||||
finalX = containerLeft + iconMargin + offsetXScreen;
|
||||
finalX = containerLeft + iconMargin;
|
||||
alignment = AlignmentType.BottomLeft;
|
||||
break;
|
||||
case LabelAlignment.Right:
|
||||
finalX = containerRight - iconMargin + offsetXScreen;
|
||||
finalX = containerRight - iconMargin;
|
||||
alignment = AlignmentType.BottomRight;
|
||||
break;
|
||||
default:
|
||||
finalX = containerCenter + offsetXScreen;
|
||||
finalX = containerCenter;
|
||||
alignment = AlignmentType.Bottom;
|
||||
break;
|
||||
}
|
||||
|
||||
finalX += currentConfig.LightfinderLabelOffsetX;
|
||||
}
|
||||
else
|
||||
{
|
||||
// manual X positioning
|
||||
var cachedTextOffset = _buffers.TextOffsets[nameplateIndex];
|
||||
var hasCachedOffset = cachedTextOffset != int.MinValue;
|
||||
var baseOffsetXLocal = (!currentConfig.LightfinderLabelUseIcon && hasValidOffset && hasCachedOffset)
|
||||
? cachedTextOffset
|
||||
: 0;
|
||||
|
||||
finalX =
|
||||
res->ScreenX
|
||||
+ (baseOffsetXLocal * worldScaleX)
|
||||
+ (58f * worldScaleX)
|
||||
+ (currentConfig.LightfinderLabelOffsetX * worldScaleX);
|
||||
|
||||
var baseOffsetX = (!currentConfig.LightfinderLabelUseIcon && hasValidOffset && hasCachedOffset) ? cachedTextOffset : 0;
|
||||
finalX = nameContainer->ScreenX + baseOffsetX + 58 + currentConfig.LightfinderLabelOffsetX;
|
||||
alignment = AlignmentType.Bottom;
|
||||
}
|
||||
|
||||
alignment = (AlignmentType)Math.Clamp((int)alignment, 0, 8);
|
||||
positionY += currentConfig.LightfinderLabelOffsetY;
|
||||
alignment = (AlignmentType)System.Math.Clamp((int)alignment, 0, 8);
|
||||
|
||||
// final position before smoothing
|
||||
var finalPosition = new Vector2(finalX, res->ScreenY + positionYScreen);
|
||||
var dpiScale = ImGui.GetIO().DisplayFramebufferScale.X; // often same for Y
|
||||
var fw = Framework.Instance();
|
||||
float dt = fw->RealFrameDeltaTime;
|
||||
|
||||
//smoothing..
|
||||
finalPosition = SnapToPixels(finalPosition, dpiScale);
|
||||
finalPosition = SmoothPosition(nameplateIndex, finalPosition, dt);
|
||||
finalPosition = SnapToPixels(finalPosition, dpiScale);
|
||||
|
||||
// prepare label info
|
||||
var finalPosition = new Vector2(finalX, nameContainer->ScreenY + positionY);
|
||||
var pivot = (currentConfig.LightfinderAutoAlign || currentConfig.LightfinderLabelUseIcon)
|
||||
? AlignmentToPivot(alignment)
|
||||
: _defaultPivot;
|
||||
: DefaultPivot;
|
||||
var textColorPacked = PackColor(labelColor);
|
||||
var edgeColorPacked = PackColor(edgeColor);
|
||||
|
||||
@@ -498,42 +418,11 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// On each tick, process any needed updates for the UI Builder.
|
||||
/// </summary>
|
||||
private void OnUiBuilderDraw()
|
||||
{
|
||||
if (!_mEnabled)
|
||||
return;
|
||||
|
||||
var fw = Framework.Instance();
|
||||
if (fw == null)
|
||||
return;
|
||||
|
||||
// Frame skip check
|
||||
var frame = fw->FrameCounter;
|
||||
|
||||
if (_lastNamePlateDrawFrame == 0 || (frame - _lastNamePlateDrawFrame) > 1)
|
||||
{
|
||||
ClearLabelBuffer();
|
||||
Array.Clear(_buffers.HasSmoothed, 0, _buffers.HasSmoothed.Length);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
//Gpose Check
|
||||
if (_clientState.IsGPosing)
|
||||
{
|
||||
ClearLabelBuffer();
|
||||
Array.Clear(_buffers.HasSmoothed, 0, _buffers.HasSmoothed.Length);
|
||||
_lastNamePlateDrawFrame = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
// If nameplate addon is not visible, skip rendering
|
||||
if (!IsNamePlateAddonVisible())
|
||||
return;
|
||||
|
||||
int copyCount;
|
||||
lock (_labelLock)
|
||||
{
|
||||
@@ -544,84 +433,21 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
||||
Array.Copy(_buffers.LabelRender, _buffers.LabelCopy, copyCount);
|
||||
}
|
||||
|
||||
var uiModule = fw != null ? fw->GetUIModule() : null;
|
||||
|
||||
if (uiModule != null)
|
||||
{
|
||||
var rapture = uiModule->GetRaptureAtkModule();
|
||||
if (rapture != null)
|
||||
RefreshUiRects(&rapture->RaptureAtkUnitManager);
|
||||
else
|
||||
_uiRects.Clear();
|
||||
}
|
||||
else
|
||||
{
|
||||
_uiRects.Clear();
|
||||
}
|
||||
|
||||
// Needed for imgui overlay viewport for the multi window view.
|
||||
var vp = ImGui.GetMainViewport();
|
||||
var vpPos = vp.Pos;
|
||||
|
||||
ImGuiHelpers.ForceNextWindowMainViewport();
|
||||
|
||||
ImGui.SetNextWindowPos(vp.Pos);
|
||||
ImGui.SetNextWindowSize(vp.Size);
|
||||
|
||||
ImGui.PushStyleVar(ImGuiStyleVar.WindowRounding, 0);
|
||||
ImGui.PushStyleVar(ImGuiStyleVar.WindowBorderSize, 0);
|
||||
|
||||
ImGui.Begin("##LightFinderOverlay", _overlayFlags);
|
||||
|
||||
ImGui.PopStyleVar(2);
|
||||
|
||||
using var drawList = PictoService.Draw();
|
||||
if (drawList == null)
|
||||
{
|
||||
ImGui.End();
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < copyCount; ++i)
|
||||
{
|
||||
ref var info = ref _buffers.LabelCopy[i];
|
||||
|
||||
// final draw position with viewport offset
|
||||
var drawPos = info.ScreenPosition + vpPos;
|
||||
var font = default(ImFontPtr);
|
||||
if (info.UseIcon)
|
||||
{
|
||||
var ioFonts = ImGui.GetIO().Fonts;
|
||||
font = ioFonts.Fonts.Size > 1 ? new ImFontPtr(ioFonts.Fonts[1]) : ImGui.GetFont();
|
||||
}
|
||||
else
|
||||
{
|
||||
font = ImGui.GetFont();
|
||||
}
|
||||
|
||||
if (!font.IsNull)
|
||||
ImGui.PushFont(font);
|
||||
|
||||
// calculate size for occlusion checking
|
||||
var baseSize = ImGui.CalcTextSize(info.Text);
|
||||
var baseFontSize = ImGui.GetFontSize();
|
||||
|
||||
if (!font.IsNull)
|
||||
ImGui.PopFont();
|
||||
|
||||
// scale size based on font size
|
||||
var scale = baseFontSize > 0 ? (info.FontSize / baseFontSize) : 1f;
|
||||
var size = baseSize * scale;
|
||||
|
||||
// label rect for occlusion checking
|
||||
var topLeft = info.ScreenPosition - new Vector2(size.X * info.Pivot.X, size.Y * info.Pivot.Y);
|
||||
var labelRect = new RectF(topLeft.X, topLeft.Y, topLeft.X + size.X, topLeft.Y + size.Y);
|
||||
|
||||
// occlusion check
|
||||
if (IsOccludedByAnyUi(labelRect))
|
||||
continue;
|
||||
|
||||
drawList.AddScreenText(drawPos, info.Text, info.TextColor, info.FontSize, info.Pivot, info.EdgeColor, font);
|
||||
drawList.AddScreenText(info.ScreenPosition, info.Text, info.TextColor, info.FontSize, info.Pivot, info.EdgeColor, font);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -634,15 +460,15 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
||||
AlignmentType.Top => new Vector2(0.5f, 0f),
|
||||
AlignmentType.Left => new Vector2(0f, 0.5f),
|
||||
AlignmentType.Right => new Vector2(1f, 0.5f),
|
||||
_ => _defaultPivot
|
||||
_ => DefaultPivot
|
||||
};
|
||||
|
||||
private static uint PackColor(Vector4 color)
|
||||
{
|
||||
var r = (byte)Math.Clamp(color.X * 255f, 0f, 255f);
|
||||
var g = (byte)Math.Clamp(color.Y * 255f, 0f, 255f);
|
||||
var b = (byte)Math.Clamp(color.Z * 255f, 0f, 255f);
|
||||
var a = (byte)Math.Clamp(color.W * 255f, 0f, 255f);
|
||||
var r = (byte)System.Math.Clamp(color.X * 255f, 0f, 255f);
|
||||
var g = (byte)System.Math.Clamp(color.Y * 255f, 0f, 255f);
|
||||
var b = (byte)System.Math.Clamp(color.Z * 255f, 0f, 255f);
|
||||
var a = (byte)System.Math.Clamp(color.W * 255f, 0f, 255f);
|
||||
return (uint)((a << 24) | (b << 16) | (g << 8) | r);
|
||||
}
|
||||
|
||||
@@ -688,19 +514,10 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
||||
if (scale <= 0f)
|
||||
scale = 1f;
|
||||
|
||||
var computed = (int)Math.Round(rawWidth * scale);
|
||||
return Math.Max(1, computed);
|
||||
var computed = (int)System.Math.Round(rawWidth * scale);
|
||||
return System.Math.Max(1, computed);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves a cached value for the given index.
|
||||
/// </summary>
|
||||
/// <param name="cache"></param>
|
||||
/// <param name="index"></param>
|
||||
/// <param name="rawValue"></param>
|
||||
/// <param name="fallback"></param>
|
||||
/// <param name="fallbackWhenZero"></param>
|
||||
/// <returns></returns>
|
||||
private static int ResolveCache(
|
||||
int[] cache,
|
||||
int index,
|
||||
@@ -728,7 +545,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
||||
|
||||
private bool TryCacheTextOffset(int nameplateIndex, int measuredTextWidth, int textOffset)
|
||||
{
|
||||
if (Math.Abs(measuredTextWidth) > 0 || textOffset != 0)
|
||||
if (System.Math.Abs(measuredTextWidth) > 0 || textOffset != 0)
|
||||
{
|
||||
_buffers.TextOffsets[nameplateIndex] = textOffset;
|
||||
return true;
|
||||
@@ -737,193 +554,10 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Snapping a position to pixel grid based on DPI scale.
|
||||
/// </summary>
|
||||
/// <param name="p">Position</param>
|
||||
/// <param name="dpiScale">DPI Scale</param>
|
||||
/// <returns></returns>
|
||||
private static Vector2 SnapToPixels(Vector2 p, float dpiScale)
|
||||
{
|
||||
// snap to pixel grid
|
||||
var x = MathF.Round(p.X * dpiScale) / dpiScale;
|
||||
var y = MathF.Round(p.Y * dpiScale) / dpiScale;
|
||||
return new Vector2(x, y);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Smooths the position using exponential smoothing.
|
||||
/// </summary>
|
||||
/// <param name="idx">Nameplate Index</param>
|
||||
/// <param name="target">Final position</param>
|
||||
/// <param name="dt">Delta Time</param>
|
||||
/// <param name="responsiveness">How responssive the smooting should be</param>
|
||||
/// <returns></returns>
|
||||
private Vector2 SmoothPosition(int idx, Vector2 target, float dt, float responsiveness = 24f)
|
||||
{
|
||||
// exponential smoothing
|
||||
if (!_buffers.HasSmoothed[idx])
|
||||
{
|
||||
_buffers.HasSmoothed[idx] = true;
|
||||
_buffers.SmoothedPos[idx] = target;
|
||||
return target;
|
||||
}
|
||||
|
||||
// get current smoothed position
|
||||
var cur = _buffers.SmoothedPos[idx];
|
||||
|
||||
// compute smoothing factor
|
||||
var a = 1f - MathF.Exp(-responsiveness * dt);
|
||||
|
||||
// snap if close enough
|
||||
if (Vector2.DistanceSquared(cur, target) < 0.25f)
|
||||
return cur;
|
||||
|
||||
// lerp towards target
|
||||
cur = Vector2.Lerp(cur, target, a);
|
||||
_buffers.SmoothedPos[idx] = cur;
|
||||
return cur;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to get a valid screen rect for the given addon.
|
||||
/// </summary>
|
||||
/// <param name="addon">Addon UI</param>
|
||||
/// <param name="screen">Screen positioning/param>
|
||||
/// <param name="rect">RectF of Addon</param>
|
||||
/// <returns></returns>
|
||||
private static bool TryGetAddonRect(AtkUnitBase* addon, Vector2 screen, out RectF rect)
|
||||
{
|
||||
// Addon existence
|
||||
rect = default;
|
||||
if (addon == null)
|
||||
return false;
|
||||
|
||||
// Visibility check
|
||||
var root = addon->RootNode;
|
||||
if (root == null || !root->IsVisible())
|
||||
return false;
|
||||
|
||||
// Size check
|
||||
float w = root->Width;
|
||||
float h = root->Height;
|
||||
if (w <= 0 || h <= 0)
|
||||
return false;
|
||||
|
||||
// Local scale
|
||||
float sx = root->ScaleX; if (sx <= 0f) sx = 1f;
|
||||
float sy = root->ScaleY; if (sy <= 0f) sy = 1f;
|
||||
|
||||
// World/composed scale from Transform
|
||||
float wsx = GetWorldScaleX(root);
|
||||
float wsy = GetWorldScaleY(root);
|
||||
if (wsx <= 0f) wsx = 1f;
|
||||
if (wsy <= 0f) wsy = 1f;
|
||||
|
||||
// World scale may include parent scaling; use it if meaningfully different.
|
||||
float useX = MathF.Abs(wsx - sx) > 0.01f ? wsx : sx;
|
||||
float useY = MathF.Abs(wsy - sy) > 0.01f ? wsy : sy;
|
||||
|
||||
w *= useX;
|
||||
h *= useY;
|
||||
|
||||
if (w < 4f || h < 4f)
|
||||
return false;
|
||||
|
||||
// Screen coords
|
||||
float l = root->ScreenX;
|
||||
float t = root->ScreenY;
|
||||
float r = l + w;
|
||||
float b = t + h;
|
||||
|
||||
// Drop fullscreen-ish / insane rects
|
||||
if (w >= screen.X * 0.98f && h >= screen.Y * 0.98f)
|
||||
return false;
|
||||
|
||||
// Drop offscreen rects
|
||||
if (l < -screen.X || t < -screen.Y || r > screen.X * 2f || b > screen.Y * 2f)
|
||||
return false;
|
||||
|
||||
rect = new RectF(l, t, r, b);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes the cached UI rects for occlusion checking.
|
||||
/// </summary>
|
||||
/// <param name="unitMgr">Unit Manager</param>
|
||||
private void RefreshUiRects(RaptureAtkUnitManager* unitMgr)
|
||||
{
|
||||
_uiRects.Clear();
|
||||
if (unitMgr == null)
|
||||
return;
|
||||
|
||||
var screen = ImGui.GetIO().DisplaySize;
|
||||
|
||||
ref var list = ref unitMgr->AllLoadedUnitsList;
|
||||
var count = (int)list.Count;
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var addon = list.Entries[i].Value;
|
||||
if (addon == null)
|
||||
continue;
|
||||
|
||||
if (_mpNameplateAddon != null && addon == (AtkUnitBase*)_mpNameplateAddon)
|
||||
continue;
|
||||
|
||||
if (TryGetAddonRect(addon, screen, out var r))
|
||||
_uiRects.Add(r);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Is the given label rect occluded by any UI rects?
|
||||
/// </summary>
|
||||
/// <param name="labelRect">UI/Label Rect</param>
|
||||
/// <returns>Is occluded or not</returns>
|
||||
private bool IsOccludedByAnyUi(RectF labelRect)
|
||||
{
|
||||
for (int i = 0; i < _uiRects.Count; i++)
|
||||
{
|
||||
if (_uiRects[i].Intersects(labelRect))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the world scale X of the given node.
|
||||
/// </summary>
|
||||
/// <param name="n">Node</param>
|
||||
/// <returns>World Scale of node</returns>
|
||||
private static float GetWorldScaleX(AtkResNode* n)
|
||||
{
|
||||
var t = n->Transform;
|
||||
return MathF.Sqrt(t.M11 * t.M11 + t.M12 * t.M12);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the world scale Y of the given node.
|
||||
/// </summary>
|
||||
/// <param name="n">Node</param>
|
||||
/// <returns>World Scale of node</returns>
|
||||
private static float GetWorldScaleY(AtkResNode* n)
|
||||
{
|
||||
var t = n->Transform;
|
||||
return MathF.Sqrt(t.M21 * t.M21 + t.M22 * t.M22);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Normalize an icon glyph input into a valid string.
|
||||
/// </summary>
|
||||
/// <param name="rawInput">Raw glyph input</param>
|
||||
/// <returns>Normalized glyph input</returns>
|
||||
internal static string NormalizeIconGlyph(string? rawInput)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(rawInput))
|
||||
return _defaultIconGlyph;
|
||||
return DefaultIconGlyph;
|
||||
|
||||
var trimmed = rawInput.Trim();
|
||||
|
||||
@@ -941,36 +575,17 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
||||
if (enumerator.MoveNext())
|
||||
return enumerator.Current.ToString();
|
||||
|
||||
return _defaultIconGlyph;
|
||||
return DefaultIconGlyph;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Is the nameplate addon visible?
|
||||
/// </summary>
|
||||
/// <returns>Is it visible?</returns>
|
||||
private bool IsNamePlateAddonVisible()
|
||||
{
|
||||
if (_mpNameplateAddon == null)
|
||||
return false;
|
||||
|
||||
var root = _mpNameplateAddon->AtkUnitBase.RootNode;
|
||||
return root != null && root->IsVisible();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts raw icon glyph input into an icon editor string.
|
||||
/// </summary>
|
||||
/// <param name="rawInput">Raw icon glyph input</param>
|
||||
/// <returns>Icon editor string</returns>
|
||||
internal static string ToIconEditorString(string? rawInput)
|
||||
{
|
||||
var normalized = NormalizeIconGlyph(rawInput);
|
||||
var runeEnumerator = normalized.EnumerateRunes();
|
||||
return runeEnumerator.MoveNext()
|
||||
? runeEnumerator.Current.Value.ToString("X4", CultureInfo.InvariantCulture)
|
||||
: _defaultIconGlyph;
|
||||
: DefaultIconGlyph;
|
||||
}
|
||||
|
||||
private readonly struct NameplateLabelInfo
|
||||
{
|
||||
public NameplateLabelInfo(
|
||||
@@ -1000,9 +615,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
||||
public bool UseIcon { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Visible paired user IDs snapshot.
|
||||
/// </summary>
|
||||
private HashSet<ulong> VisibleUserIds
|
||||
=> [.. _pairUiService.GetSnapshot().PairsByUid.Values
|
||||
.Where(u => u.IsVisible && u.PlayerCharacterId != uint.MaxValue)
|
||||
@@ -1022,10 +634,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update the active broadcasting CIDs.
|
||||
/// </summary>
|
||||
/// <param name="cids">Inbound new CIDs</param>
|
||||
public void UpdateBroadcastingCids(IEnumerable<string> cids)
|
||||
{
|
||||
var newSet = cids.ToImmutableHashSet(StringComparer.Ordinal);
|
||||
@@ -1033,21 +641,15 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
||||
return;
|
||||
|
||||
_activeBroadcastingCids = newSet;
|
||||
if (_logger.IsEnabled(LogLevel.Trace))
|
||||
_logger.LogTrace("Active broadcast IDs: {Cids}", string.Join(',', _activeBroadcastingCids));
|
||||
if (_logger.IsEnabled(LogLevel.Information))
|
||||
_logger.LogInformation("Active broadcast CIDs: {Cids}", string.Join(',', _activeBroadcastingCids));
|
||||
FlagRefresh();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears all nameplate related caches.
|
||||
/// </summary>
|
||||
public void ClearNameplateCaches()
|
||||
{
|
||||
_buffers.Clear();
|
||||
ClearLabelBuffer();
|
||||
|
||||
Array.Clear(_buffers.HasSmoothed, 0, _buffers.HasSmoothed.Length);
|
||||
Array.Clear(_buffers.SmoothedPos, 0, _buffers.SmoothedPos.Length);
|
||||
}
|
||||
|
||||
private sealed class NameplateBuffers
|
||||
@@ -1066,10 +668,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
||||
public NameplateLabelInfo[] LabelRender { get; } = new NameplateLabelInfo[AddonNamePlate.NumNamePlateObjects];
|
||||
public NameplateLabelInfo[] LabelCopy { get; } = new NameplateLabelInfo[AddonNamePlate.NumNamePlateObjects];
|
||||
|
||||
public Vector2[] SmoothedPos = new Vector2[AddonNamePlate.NumNamePlateObjects];
|
||||
|
||||
public bool[] HasSmoothed = new bool[AddonNamePlate.NumNamePlateObjects];
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
System.Array.Clear(TextWidths, 0, TextWidths.Length);
|
||||
@@ -1079,38 +677,16 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts the LightFinder Plate Handler.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">Cancellation Token</param>
|
||||
/// <returns>Task Completed</returns>
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
Init();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops the LightFinder Plate Handler.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">Cancellation Token</param>
|
||||
/// <returns>Task Completed</returns>
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
Uninit();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rectangle with float coordinates for intersection testing.
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Auto)]
|
||||
private readonly struct RectF
|
||||
{
|
||||
public readonly float L, T, R, B;
|
||||
public RectF(float l, float t, float r, float b) { L = l; T = t; R = r; B = b; }
|
||||
|
||||
public bool Intersects(in RectF o) =>
|
||||
!(R <= o.L || o.R <= L || B <= o.T || o.B <= T);
|
||||
}
|
||||
}
|
||||
@@ -148,14 +148,10 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase
|
||||
private void UpdateSyncshellBroadcasts()
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
var nearbyCids = GetNearbyHashedCids(out _);
|
||||
var newSet = nearbyCids.Count == 0
|
||||
? new HashSet<string>(StringComparer.Ordinal)
|
||||
: _broadcastCache
|
||||
.Where(e => e.Value.IsBroadcasting && e.Value.ExpiryTime > now && !string.IsNullOrEmpty(e.Value.GID))
|
||||
.Where(e => nearbyCids.Contains(e.Key))
|
||||
.Select(e => e.Key)
|
||||
.ToHashSet(StringComparer.Ordinal);
|
||||
var newSet = _broadcastCache
|
||||
.Where(e => e.Value.IsBroadcasting && e.Value.ExpiryTime > now && !string.IsNullOrEmpty(e.Value.GID))
|
||||
.Select(e => e.Key)
|
||||
.ToHashSet(StringComparer.Ordinal);
|
||||
|
||||
if (!_syncshellCids.SetEquals(newSet))
|
||||
{
|
||||
@@ -167,17 +163,12 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase
|
||||
}
|
||||
}
|
||||
|
||||
public List<BroadcastStatusInfoDto> GetActiveSyncshellBroadcasts(bool excludeLocal = false)
|
||||
public List<BroadcastStatusInfoDto> GetActiveSyncshellBroadcasts()
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
var nearbyCids = GetNearbyHashedCids(out var localCid);
|
||||
if (nearbyCids.Count == 0)
|
||||
return [];
|
||||
|
||||
return [.. _broadcastCache
|
||||
.Where(e => e.Value.IsBroadcasting && e.Value.ExpiryTime > now && !string.IsNullOrEmpty(e.Value.GID))
|
||||
.Where(e => nearbyCids.Contains(e.Key))
|
||||
.Where(e => !excludeLocal || !string.Equals(e.Key, localCid, StringComparison.Ordinal))
|
||||
.Select(e => new BroadcastStatusInfoDto
|
||||
{
|
||||
HashedCID = e.Key,
|
||||
@@ -187,47 +178,6 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase
|
||||
})];
|
||||
}
|
||||
|
||||
public bool TryGetLocalHashedCid(out string hashedCid)
|
||||
{
|
||||
hashedCid = string.Empty;
|
||||
var descriptors = _actorTracker.PlayerDescriptors;
|
||||
if (descriptors.Count == 0)
|
||||
return false;
|
||||
|
||||
foreach (var descriptor in descriptors)
|
||||
{
|
||||
if (!descriptor.IsLocalPlayer || string.IsNullOrWhiteSpace(descriptor.HashedContentId))
|
||||
continue;
|
||||
|
||||
hashedCid = descriptor.HashedContentId;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private HashSet<string> GetNearbyHashedCids(out string? localCid)
|
||||
{
|
||||
localCid = null;
|
||||
var descriptors = _actorTracker.PlayerDescriptors;
|
||||
if (descriptors.Count == 0)
|
||||
return new HashSet<string>(StringComparer.Ordinal);
|
||||
|
||||
var set = new HashSet<string>(StringComparer.Ordinal);
|
||||
foreach (var descriptor in descriptors)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(descriptor.HashedContentId))
|
||||
continue;
|
||||
|
||||
if (descriptor.IsLocalPlayer)
|
||||
localCid = descriptor.HashedContentId;
|
||||
|
||||
set.Add(descriptor.HashedContentId);
|
||||
}
|
||||
|
||||
return set;
|
||||
}
|
||||
|
||||
private async Task ExpiredBroadcastCleanupLoop()
|
||||
{
|
||||
var token = _cleanupCts.Token;
|
||||
|
||||
@@ -126,11 +126,11 @@ public sealed class TextureMetadataHelper
|
||||
private const string TextureSegment = "/texture/";
|
||||
private const string MaterialSegment = "/material/";
|
||||
|
||||
private const uint NormalSamplerId = ShpkFile.NormalSamplerId;
|
||||
private const uint IndexSamplerId = ShpkFile.IndexSamplerId;
|
||||
private const uint SpecularSamplerId = ShpkFile.SpecularSamplerId;
|
||||
private const uint DiffuseSamplerId = ShpkFile.DiffuseSamplerId;
|
||||
private const uint MaskSamplerId = ShpkFile.MaskSamplerId;
|
||||
private const uint NormalSamplerId = 0x0C5EC1F1u;
|
||||
private const uint IndexSamplerId = 0x565F8FD8u;
|
||||
private const uint SpecularSamplerId = 0x2B99E025u;
|
||||
private const uint DiffuseSamplerId = 0x115306BEu;
|
||||
private const uint MaskSamplerId = 0x8A4E82B6u;
|
||||
|
||||
public TextureMetadataHelper(ILogger<TextureMetadataHelper> logger, IDataManager dataManager)
|
||||
{
|
||||
|
||||
@@ -843,16 +843,12 @@ public class CompactUi : WindowMediatorSubscriberBase
|
||||
|
||||
//Filter of not grouped/foldered and offline pairs
|
||||
var allOnlineNotTaggedPairs = SortEntries(allEntries.Where(FilterNotTaggedUsers));
|
||||
if (allOnlineNotTaggedPairs.Count > 0 && _configService.Current.ShowOfflineUsersSeparately) {
|
||||
var filteredOnlineEntries = SortOnlineEntries(filteredEntries.Where(e => FilterNotTaggedUsers(e) && FilterOnlineOrPausedSelf(e)));
|
||||
var onlineNotTaggedPairs = SortEntries(filteredEntries.Where(e => FilterNotTaggedUsers(e) && FilterOnlineOrPausedSelf(e)));
|
||||
|
||||
if (allOnlineNotTaggedPairs.Count > 0)
|
||||
{
|
||||
drawFolders.Add(_drawEntityFactory.CreateTagFolder(
|
||||
TagHandler.CustomOnlineTag,
|
||||
filteredOnlineEntries,
|
||||
allOnlineNotTaggedPairs));
|
||||
} else if (allOnlineNotTaggedPairs.Count > 0 && !_configService.Current.ShowOfflineUsersSeparately) {
|
||||
var onlineNotTaggedPairs = SortEntries(filteredEntries.Where(FilterNotTaggedUsers));
|
||||
drawFolders.Add(_drawEntityFactory.CreateTagFolder(
|
||||
TagHandler.CustomAllTag,
|
||||
_configService.Current.ShowOfflineUsersSeparately ? TagHandler.CustomOnlineTag : TagHandler.CustomAllTag,
|
||||
onlineNotTaggedPairs,
|
||||
allOnlineNotTaggedPairs));
|
||||
}
|
||||
@@ -889,7 +885,7 @@ public class CompactUi : WindowMediatorSubscriberBase
|
||||
}
|
||||
}
|
||||
|
||||
private static bool PassesFilter(PairUiEntry entry, string filter)
|
||||
private bool PassesFilter(PairUiEntry entry, string filter)
|
||||
{
|
||||
if (string.IsNullOrEmpty(filter)) return true;
|
||||
|
||||
@@ -950,17 +946,6 @@ 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)
|
||||
{
|
||||
return [.. entries
|
||||
|
||||
@@ -4,8 +4,8 @@ using Dalamud.Interface.Utility.Raii;
|
||||
using LightlessSync.UI.Handlers;
|
||||
using LightlessSync.UI.Models;
|
||||
using System.Collections.Immutable;
|
||||
using LightlessSync.UI;
|
||||
using LightlessSync.UI.Style;
|
||||
using OtterGui.Text;
|
||||
|
||||
namespace LightlessSync.UI.Components;
|
||||
|
||||
@@ -113,13 +113,9 @@ public abstract class DrawFolderBase : IDrawFolder
|
||||
using var indent = ImRaii.PushIndent(_uiSharedService.GetIconSize(FontAwesomeIcon.EllipsisV).X + ImGui.GetStyle().ItemSpacing.X, false);
|
||||
if (DrawPairs.Any())
|
||||
{
|
||||
using var clipper = ImUtf8.ListClipper(DrawPairs.Count, ImGui.GetFrameHeightWithSpacing());
|
||||
while (clipper.Step())
|
||||
foreach (var item in DrawPairs)
|
||||
{
|
||||
for (var i = clipper.DisplayStart; i < clipper.DisplayEnd; i++)
|
||||
{
|
||||
DrawPairs[i].DrawPairedClient();
|
||||
}
|
||||
item.DrawPairedClient();
|
||||
}
|
||||
}
|
||||
else
|
||||
|
||||
@@ -169,16 +169,11 @@ public class DrawFolderTag : DrawFolderBase
|
||||
|
||||
protected override float DrawRightSide(float currentRightSideX)
|
||||
{
|
||||
if (string.Equals(_id, TagHandler.CustomVisibleTag, StringComparison.Ordinal))
|
||||
if (_id == TagHandler.CustomVisibleTag)
|
||||
{
|
||||
return DrawVisibleFilter(currentRightSideX);
|
||||
}
|
||||
|
||||
if (string.Equals(_id, TagHandler.CustomOnlineTag, StringComparison.Ordinal))
|
||||
{
|
||||
return DrawOnlineFilter(currentRightSideX);
|
||||
}
|
||||
|
||||
if (!RenderPause)
|
||||
{
|
||||
return currentRightSideX;
|
||||
@@ -259,7 +254,7 @@ public class DrawFolderTag : DrawFolderBase
|
||||
foreach (VisiblePairSortMode mode in Enum.GetValues<VisiblePairSortMode>())
|
||||
{
|
||||
var selected = _configService.Current.VisiblePairSortMode == mode;
|
||||
if (ImGui.MenuItem(GetSortVisibleLabel(mode), string.Empty, selected))
|
||||
if (ImGui.MenuItem(GetSortLabel(mode), string.Empty, selected))
|
||||
{
|
||||
if (!selected)
|
||||
{
|
||||
@@ -278,49 +273,7 @@ public class DrawFolderTag : DrawFolderBase
|
||||
return buttonStart - spacingX;
|
||||
}
|
||||
|
||||
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
|
||||
private static string GetSortLabel(VisiblePairSortMode mode) => mode switch
|
||||
{
|
||||
VisiblePairSortMode.Alphabetical => "Alphabetical",
|
||||
VisiblePairSortMode.VramUsage => "VRAM usage (descending)",
|
||||
@@ -329,11 +282,4 @@ public class DrawFolderTag : DrawFolderBase
|
||||
VisiblePairSortMode.PreferredDirectPairs => "Preferred permissions & Direct pairs",
|
||||
_ => "Default",
|
||||
};
|
||||
|
||||
private static string GetSortOnlineLabel(OnlinePairSortMode mode) => mode switch
|
||||
{
|
||||
OnlinePairSortMode.Alphabetical => "Alphabetical",
|
||||
OnlinePairSortMode.PreferredDirectPairs => "Preferred permissions & Direct pairs",
|
||||
_ => "Default",
|
||||
};
|
||||
}
|
||||
@@ -133,26 +133,6 @@ public class DrawUserPair
|
||||
UiSharedService.AttachToolTip("This reapplies the last received character data to this character");
|
||||
}
|
||||
|
||||
var isPaused = _pair.UserPair!.OwnPermissions.IsPaused();
|
||||
if (!isPaused)
|
||||
{
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Pause, "Toggle Pause State", _menuWidth, true))
|
||||
{
|
||||
_ = _apiController.PauseAsync(_pair.UserData);
|
||||
ImGui.CloseCurrentPopup();
|
||||
}
|
||||
UiSharedService.AttachToolTip("Pauses syncing with this user.");
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Play, "Toggle Unpause State", _menuWidth, true))
|
||||
{
|
||||
_ = _apiController.UnpauseAsync(_pair.UserData);
|
||||
ImGui.CloseCurrentPopup();
|
||||
}
|
||||
UiSharedService.AttachToolTip("Resumes syncing with this user.");
|
||||
}
|
||||
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.PlayCircle, "Cycle pause state", _menuWidth, true))
|
||||
{
|
||||
_ = _apiController.CyclePauseAsync(_pair);
|
||||
|
||||
@@ -27,11 +27,8 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
|
||||
{
|
||||
private const float MinTextureFilterPaneWidth = 305f;
|
||||
private const float MaxTextureFilterPaneWidth = 405f;
|
||||
private const float MinTextureDetailPaneWidth = 480f;
|
||||
private const float MinTextureDetailPaneWidth = 580f;
|
||||
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 static readonly Vector4 SelectedTextureRowTextColor = new(0f, 0f, 0f, 1f);
|
||||
|
||||
@@ -83,7 +80,6 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
|
||||
private bool _modalOpen = false;
|
||||
private bool _showModal = false;
|
||||
private bool _textureRowsDirty = true;
|
||||
private bool _textureDetailCollapsed = false;
|
||||
private bool _conversionFailed;
|
||||
private bool _showAlreadyAddedTransients = false;
|
||||
private bool _acknowledgeReview = false;
|
||||
@@ -115,7 +111,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
|
||||
_hasUpdate = true;
|
||||
});
|
||||
WindowBuilder.For(this)
|
||||
.SetSizeConstraints(new Vector2(1240, 680), new Vector2(3840, 2160))
|
||||
.SetSizeConstraints(new Vector2(1650, 1000), new Vector2(3840, 2160))
|
||||
.Apply();
|
||||
|
||||
_conversionProgress.ProgressChanged += ConversionProgress_ProgressChanged;
|
||||
@@ -1209,52 +1205,35 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
|
||||
var availableSize = ImGui.GetContentRegionAvail();
|
||||
var windowPos = ImGui.GetWindowPos();
|
||||
var spacingX = ImGui.GetStyle().ItemSpacing.X;
|
||||
var filterSplitterWidth = TextureFilterSplitterWidth * scale;
|
||||
var detailSplitterWidth = (_textureDetailCollapsed ? TextureDetailSplitterCollapsedWidth : TextureDetailSplitterWidth) * scale;
|
||||
var totalSplitterWidth = filterSplitterWidth + detailSplitterWidth;
|
||||
var totalSpacing = 2 * spacingX;
|
||||
var splitterWidth = 6f * scale;
|
||||
const float minFilterWidth = MinTextureFilterPaneWidth;
|
||||
const float minDetailWidth = MinTextureDetailPaneWidth;
|
||||
const float minCenterWidth = 340f;
|
||||
|
||||
var detailMinForLayout = _textureDetailCollapsed ? 0f : minDetailWidth;
|
||||
var dynamicFilterMax = Math.Max(minFilterWidth, availableSize.X - detailMinForLayout - minCenterWidth - totalSplitterWidth - totalSpacing);
|
||||
var dynamicFilterMax = Math.Max(minFilterWidth, availableSize.X - minDetailWidth - minCenterWidth - 2 * (splitterWidth + spacingX));
|
||||
var filterMaxBound = Math.Min(MaxTextureFilterPaneWidth, dynamicFilterMax);
|
||||
var filterWidth = Math.Clamp(_textureFilterPaneWidth, minFilterWidth, filterMaxBound);
|
||||
|
||||
var dynamicDetailMax = Math.Max(detailMinForLayout, availableSize.X - filterWidth - minCenterWidth - totalSplitterWidth - totalSpacing);
|
||||
var detailMaxBound = _textureDetailCollapsed ? 0f : Math.Min(MaxTextureDetailPaneWidth, dynamicDetailMax);
|
||||
var detailWidth = _textureDetailCollapsed ? 0f : Math.Clamp(_textureDetailPaneWidth, minDetailWidth, detailMaxBound);
|
||||
var dynamicDetailMax = Math.Max(minDetailWidth, availableSize.X - filterWidth - minCenterWidth - 2 * (splitterWidth + spacingX));
|
||||
var detailMaxBound = Math.Min(MaxTextureDetailPaneWidth, dynamicDetailMax);
|
||||
var detailWidth = Math.Clamp(_textureDetailPaneWidth, minDetailWidth, detailMaxBound);
|
||||
|
||||
var centerWidth = availableSize.X - filterWidth - detailWidth - totalSplitterWidth - totalSpacing;
|
||||
var centerWidth = availableSize.X - filterWidth - detailWidth - 2 * (splitterWidth + spacingX);
|
||||
|
||||
if (centerWidth < minCenterWidth)
|
||||
{
|
||||
var deficit = minCenterWidth - centerWidth;
|
||||
if (!_textureDetailCollapsed)
|
||||
{
|
||||
detailWidth = Math.Clamp(detailWidth - deficit, minDetailWidth,
|
||||
Math.Min(MaxTextureDetailPaneWidth, Math.Max(minDetailWidth, availableSize.X - filterWidth - minCenterWidth - totalSplitterWidth - totalSpacing)));
|
||||
centerWidth = availableSize.X - filterWidth - detailWidth - totalSplitterWidth - totalSpacing;
|
||||
if (centerWidth < minCenterWidth)
|
||||
{
|
||||
deficit = minCenterWidth - centerWidth;
|
||||
filterWidth = Math.Clamp(filterWidth - deficit, minFilterWidth,
|
||||
Math.Min(MaxTextureFilterPaneWidth, Math.Max(minFilterWidth, availableSize.X - detailWidth - minCenterWidth - totalSplitterWidth - totalSpacing)));
|
||||
detailWidth = Math.Clamp(detailWidth, minDetailWidth,
|
||||
Math.Min(MaxTextureDetailPaneWidth, Math.Max(minDetailWidth, availableSize.X - filterWidth - minCenterWidth - totalSplitterWidth - totalSpacing)));
|
||||
centerWidth = availableSize.X - filterWidth - detailWidth - totalSplitterWidth - totalSpacing;
|
||||
if (centerWidth < minCenterWidth)
|
||||
{
|
||||
centerWidth = minCenterWidth;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
detailWidth = Math.Clamp(detailWidth - deficit, minDetailWidth,
|
||||
Math.Min(MaxTextureDetailPaneWidth, Math.Max(minDetailWidth, availableSize.X - filterWidth - minCenterWidth - 2 * (splitterWidth + spacingX))));
|
||||
centerWidth = availableSize.X - filterWidth - detailWidth - 2 * (splitterWidth + spacingX);
|
||||
if (centerWidth < minCenterWidth)
|
||||
{
|
||||
deficit = minCenterWidth - centerWidth;
|
||||
filterWidth = Math.Clamp(filterWidth - deficit, minFilterWidth,
|
||||
Math.Min(MaxTextureFilterPaneWidth, Math.Max(minFilterWidth, availableSize.X - minCenterWidth - totalSplitterWidth - totalSpacing)));
|
||||
centerWidth = availableSize.X - filterWidth - detailWidth - totalSplitterWidth - totalSpacing;
|
||||
Math.Min(MaxTextureFilterPaneWidth, Math.Max(minFilterWidth, availableSize.X - detailWidth - minCenterWidth - 2 * (splitterWidth + spacingX))));
|
||||
detailWidth = Math.Clamp(detailWidth, minDetailWidth,
|
||||
Math.Min(MaxTextureDetailPaneWidth, Math.Max(minDetailWidth, availableSize.X - filterWidth - minCenterWidth - 2 * (splitterWidth + spacingX))));
|
||||
centerWidth = availableSize.X - filterWidth - detailWidth - 2 * (splitterWidth + spacingX);
|
||||
if (centerWidth < minCenterWidth)
|
||||
{
|
||||
centerWidth = minCenterWidth;
|
||||
@@ -1263,10 +1242,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
|
||||
}
|
||||
|
||||
_textureFilterPaneWidth = filterWidth;
|
||||
if (!_textureDetailCollapsed)
|
||||
{
|
||||
_textureDetailPaneWidth = detailWidth;
|
||||
}
|
||||
_textureDetailPaneWidth = detailWidth;
|
||||
|
||||
ImGui.BeginGroup();
|
||||
using (var filters = ImRaii.Child("textureFilters", new Vector2(filterWidth, 0), true))
|
||||
@@ -1288,8 +1264,8 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
|
||||
var filterMax = ImGui.GetItemRectMax();
|
||||
var filterHeight = filterMax.Y - filterMin.Y;
|
||||
var filterTopLocal = filterMin - windowPos;
|
||||
var maxFilterResize = Math.Min(MaxTextureFilterPaneWidth, Math.Max(minFilterWidth, availableSize.X - minCenterWidth - detailMinForLayout - totalSplitterWidth - totalSpacing));
|
||||
DrawVerticalResizeHandle("##textureFilterSplitter", filterTopLocal.Y, filterHeight, ref _textureFilterPaneWidth, minFilterWidth, maxFilterResize, out _);
|
||||
var maxFilterResize = Math.Min(MaxTextureFilterPaneWidth, Math.Max(minFilterWidth, availableSize.X - minCenterWidth - minDetailWidth - 2 * (splitterWidth + spacingX)));
|
||||
DrawVerticalResizeHandle("##textureFilterSplitter", filterTopLocal.Y, filterHeight, ref _textureFilterPaneWidth, minFilterWidth, maxFilterResize);
|
||||
|
||||
TextureRow? selectedRow;
|
||||
ImGui.BeginGroup();
|
||||
@@ -1303,36 +1279,15 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
|
||||
var tableMax = ImGui.GetItemRectMax();
|
||||
var tableHeight = tableMax.Y - tableMin.Y;
|
||||
var tableTopLocal = tableMin - windowPos;
|
||||
var maxDetailResize = Math.Min(MaxTextureDetailPaneWidth, Math.Max(minDetailWidth, availableSize.X - _textureFilterPaneWidth - minCenterWidth - totalSplitterWidth - totalSpacing));
|
||||
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;
|
||||
}
|
||||
var maxDetailResize = Math.Min(MaxTextureDetailPaneWidth, Math.Max(minDetailWidth, availableSize.X - _textureFilterPaneWidth - minCenterWidth - 2 * (splitterWidth + spacingX)));
|
||||
DrawVerticalResizeHandle("##textureDetailSplitter", tableTopLocal.Y, tableHeight, ref _textureDetailPaneWidth, minDetailWidth, maxDetailResize, invert: true);
|
||||
|
||||
if (!_textureDetailCollapsed)
|
||||
ImGui.BeginGroup();
|
||||
using (var detailChild = ImRaii.Child("textureDetailPane", new Vector2(detailWidth, 0), true))
|
||||
{
|
||||
ImGui.BeginGroup();
|
||||
using (var detailChild = ImRaii.Child("textureDetailPane", new Vector2(detailWidth, 0), true))
|
||||
{
|
||||
DrawTextureDetail(selectedRow);
|
||||
}
|
||||
ImGui.EndGroup();
|
||||
DrawTextureDetail(selectedRow);
|
||||
}
|
||||
ImGui.EndGroup();
|
||||
}
|
||||
|
||||
private void DrawTextureFilters(
|
||||
@@ -1980,118 +1935,26 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
private void DrawVerticalResizeHandle(string id, float topY, float height, ref float leftWidth, float minWidth, float maxWidth, bool invert = false)
|
||||
{
|
||||
var scale = ImGuiHelpers.GlobalScale;
|
||||
var splitterWidth = (showToggle
|
||||
? (isCollapsed ? TextureDetailSplitterCollapsedWidth : TextureDetailSplitterWidth)
|
||||
: TextureFilterSplitterWidth) * scale;
|
||||
var splitterWidth = 8f * scale;
|
||||
ImGui.SameLine();
|
||||
var cursor = ImGui.GetCursorPos();
|
||||
var contentMin = ImGui.GetWindowContentRegionMin();
|
||||
var contentMax = ImGui.GetWindowContentRegionMax();
|
||||
var clampedTop = MathF.Max(topY, contentMin.Y);
|
||||
var clampedBottom = MathF.Min(topY + height, contentMax.Y);
|
||||
var clampedHeight = MathF.Max(0f, clampedBottom - clampedTop);
|
||||
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;
|
||||
}
|
||||
ImGui.SetCursorPos(new Vector2(cursor.X, topY));
|
||||
ImGui.PushStyleColor(ImGuiCol.Button, UIColors.Get("ButtonDefault"));
|
||||
ImGui.PushStyleColor(ImGuiCol.ButtonHovered, UIColors.Get("LightlessPurple"));
|
||||
ImGui.PushStyleColor(ImGuiCol.ButtonActive, UIColors.Get("LightlessPurpleActive"));
|
||||
ImGui.Button(id, new Vector2(splitterWidth, height));
|
||||
ImGui.PopStyleColor(3);
|
||||
|
||||
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)
|
||||
if (ImGui.IsItemActive())
|
||||
{
|
||||
var delta = ImGui.GetIO().MouseDelta.X / scale;
|
||||
leftWidth += invert ? -delta : delta;
|
||||
leftWidth = Math.Clamp(leftWidth, minWidth, maxWidth);
|
||||
}
|
||||
|
||||
drawList.PopClipRect();
|
||||
|
||||
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)
|
||||
@@ -2231,7 +2094,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
|
||||
}
|
||||
else
|
||||
{
|
||||
_uiSharedService.IconText(FontAwesomeIcon.Check, ImGuiColors.DalamudWhite);
|
||||
ImGui.TextDisabled("-");
|
||||
UiSharedService.AttachToolTip("Already stored in a compressed format; additional compression is disabled.");
|
||||
}
|
||||
|
||||
@@ -2312,10 +2175,6 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
|
||||
_textureSelections[key] = target;
|
||||
currentSelection = target;
|
||||
}
|
||||
if (TextureMetadataHelper.TryGetRecommendationInfo(target, out var targetInfo))
|
||||
{
|
||||
UiSharedService.AttachToolTip($"{targetInfo.Title}{UiSharedService.TooltipSeparator}{targetInfo.Description}");
|
||||
}
|
||||
if (targetSelected)
|
||||
{
|
||||
ImGui.SetItemDefaultFocus();
|
||||
|
||||
@@ -301,14 +301,6 @@ namespace LightlessSync.UI
|
||||
bool ShellFinderEnabled = _configService.Current.SyncshellFinderEnabled;
|
||||
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)
|
||||
ImGui.BeginDisabled();
|
||||
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
namespace LightlessSync.UI.Models;
|
||||
|
||||
public enum OnlinePairSortMode
|
||||
{
|
||||
Alphabetical = 0,
|
||||
PreferredDirectPairs = 1,
|
||||
}
|
||||
@@ -2,9 +2,10 @@ namespace LightlessSync.UI.Models;
|
||||
|
||||
public enum VisiblePairSortMode
|
||||
{
|
||||
Alphabetical = 0,
|
||||
VramUsage = 1,
|
||||
EffectiveVramUsage = 2,
|
||||
TriangleCount = 3,
|
||||
PreferredDirectPairs = 4,
|
||||
Default = 0,
|
||||
Alphabetical = 1,
|
||||
VramUsage = 2,
|
||||
EffectiveVramUsage = 3,
|
||||
TriangleCount = 4,
|
||||
PreferredDirectPairs = 5,
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -222,9 +222,7 @@ public class AnimatedHeader
|
||||
|
||||
if (ImGui.IsItemHovered() && !string.IsNullOrEmpty(button.Tooltip))
|
||||
{
|
||||
ImGui.PushFont(UiBuilder.DefaultFont);
|
||||
ImGui.SetTooltip(button.Tooltip);
|
||||
ImGui.PopFont();
|
||||
}
|
||||
|
||||
currentX -= buttonSize.X + spacing;
|
||||
|
||||
@@ -297,25 +297,6 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
|
||||
var ownerTab = ImRaii.TabItem("Owner Settings");
|
||||
if (ownerTab)
|
||||
{
|
||||
bool isChatDisabled = perm.IsDisableChat();
|
||||
ImGui.AlignTextToFramePadding();
|
||||
ImGui.TextUnformatted("Syncshell Chat");
|
||||
_uiSharedService.BooleanToColoredIcon(!isChatDisabled);
|
||||
ImGui.SameLine(230);
|
||||
using (ImRaii.PushColor(ImGuiCol.Text, isChatDisabled ? UIColors.Get("PairBlue") : UIColors.Get("DimRed")))
|
||||
{
|
||||
if (_uiSharedService.IconTextButton(
|
||||
isChatDisabled ? FontAwesomeIcon.Comment : FontAwesomeIcon.Ban,
|
||||
isChatDisabled ? "Enable syncshell chat" : "Disable syncshell chat"))
|
||||
{
|
||||
perm.SetDisableChat(!isChatDisabled);
|
||||
_ = _apiController.GroupChangeGroupPermissionState(new(GroupFullInfo.Group, perm));
|
||||
}
|
||||
}
|
||||
UiSharedService.AttachToolTip("Disables syncshell chat for all members.");
|
||||
|
||||
ImGuiHelpers.ScaledDummy(6f);
|
||||
|
||||
ImGui.AlignTextToFramePadding();
|
||||
ImGui.TextUnformatted("New Password");
|
||||
var availableWidth = ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X;
|
||||
|
||||
@@ -140,10 +140,19 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
|
||||
return;
|
||||
}
|
||||
|
||||
var broadcasts = _broadcastScannerService.GetActiveSyncshellBroadcasts().ToList() ?? [];
|
||||
_broadcastScannerService.TryGetLocalHashedCid(out var localHashedCid);
|
||||
string? myHashedCid = null;
|
||||
try
|
||||
{
|
||||
var cid = _dalamudUtilService.GetCID();
|
||||
myHashedCid = cid.ToString().GetHash256();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogDebug(ex, "Failed to get CID, not excluding own broadcast.");
|
||||
}
|
||||
var broadcasts = _broadcastScannerService.GetActiveSyncshellBroadcasts().Where(b => !string.Equals(b.HashedCID, myHashedCid, StringComparison.Ordinal)).ToList() ?? [];
|
||||
|
||||
var cardData = new List<(GroupJoinDto Shell, string BroadcasterName, bool IsSelfBroadcast)>();
|
||||
var cardData = new List<(GroupJoinDto Shell, string BroadcasterName)>();
|
||||
|
||||
foreach (var shell in _nearbySyncshells)
|
||||
{
|
||||
@@ -176,15 +185,9 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
|
||||
broadcasterName = !string.IsNullOrEmpty(worldName)
|
||||
? $"{name} ({worldName})"
|
||||
: name;
|
||||
|
||||
var isSelfBroadcast = !string.IsNullOrEmpty(localHashedCid)
|
||||
&& string.Equals(broadcast.HashedCID, localHashedCid, StringComparison.Ordinal);
|
||||
|
||||
cardData.Add((shell, broadcasterName, isSelfBroadcast));
|
||||
continue;
|
||||
}
|
||||
|
||||
cardData.Add((shell, broadcasterName, false));
|
||||
cardData.Add((shell, broadcasterName));
|
||||
}
|
||||
|
||||
if (cardData.Count == 0)
|
||||
@@ -207,7 +210,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
|
||||
DrawConfirmation();
|
||||
}
|
||||
|
||||
private void DrawSyncshellList(List<(GroupJoinDto Shell, string BroadcasterName, bool IsSelfBroadcast)> listData)
|
||||
private void DrawSyncshellList(List<(GroupJoinDto Shell, string BroadcasterName)> listData)
|
||||
{
|
||||
const int shellsPerPage = 3;
|
||||
var totalPages = (int)Math.Ceiling(listData.Count / (float)shellsPerPage);
|
||||
@@ -224,10 +227,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
|
||||
|
||||
for (int index = firstIndex; index < lastExclusive; index++)
|
||||
{
|
||||
var (shell, broadcasterName, isSelfBroadcast) = listData[index];
|
||||
var broadcasterLabel = string.IsNullOrEmpty(broadcasterName)
|
||||
? (isSelfBroadcast ? "You" : string.Empty)
|
||||
: (isSelfBroadcast ? $"{broadcasterName} (You)" : broadcasterName);
|
||||
var (shell, broadcasterName) = listData[index];
|
||||
|
||||
ImGui.PushID(shell.Group.GID);
|
||||
float rowHeight = 74f * ImGuiHelpers.GlobalScale;
|
||||
@@ -239,7 +239,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
|
||||
var style = ImGui.GetStyle();
|
||||
float startX = ImGui.GetCursorPosX();
|
||||
float regionW = ImGui.GetContentRegionAvail().X;
|
||||
float rightTxtW = ImGui.CalcTextSize(broadcasterLabel).X;
|
||||
float rightTxtW = ImGui.CalcTextSize(broadcasterName).X;
|
||||
|
||||
_uiSharedService.MediumText(displayName, UIColors.Get("LightlessPurple"));
|
||||
if (ImGui.IsItemHovered())
|
||||
@@ -252,7 +252,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
|
||||
float rightX = startX + regionW - rightTxtW - style.ItemSpacing.X;
|
||||
ImGui.SameLine();
|
||||
ImGui.SetCursorPosX(rightX);
|
||||
ImGui.TextUnformatted(broadcasterLabel);
|
||||
ImGui.TextUnformatted(broadcasterName);
|
||||
if (ImGui.IsItemHovered())
|
||||
ImGui.SetTooltip("Broadcaster of the syncshell.");
|
||||
|
||||
@@ -291,7 +291,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
|
||||
float joinX = rowStartLocal.X + (tagsWidth > 0 ? tagsWidth + style.ItemSpacing.X : 0f);
|
||||
|
||||
ImGui.SetCursorPos(new Vector2(joinX, btnBaselineY));
|
||||
DrawJoinButton(shell, isSelfBroadcast);
|
||||
DrawJoinButton(shell);
|
||||
|
||||
float btnHeight = ImGui.GetFrameHeightWithSpacing();
|
||||
float rowHeightUsed = MathF.Max(tagsHeight, btnHeight);
|
||||
@@ -311,7 +311,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
|
||||
DrawPagination(totalPages);
|
||||
}
|
||||
|
||||
private void DrawSyncshellGrid(List<(GroupJoinDto Shell, string BroadcasterName, bool IsSelfBroadcast)> cardData)
|
||||
private void DrawSyncshellGrid(List<(GroupJoinDto Shell, string BroadcasterName)> cardData)
|
||||
{
|
||||
const int shellsPerPage = 4;
|
||||
var totalPages = (int)Math.Ceiling(cardData.Count / (float)shellsPerPage);
|
||||
@@ -336,10 +336,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
|
||||
for (int index = firstIndex; index < lastExclusive; index++)
|
||||
{
|
||||
var localIndex = index - firstIndex;
|
||||
var (shell, broadcasterName, isSelfBroadcast) = cardData[index];
|
||||
var broadcasterLabel = string.IsNullOrEmpty(broadcasterName)
|
||||
? (isSelfBroadcast ? "You" : string.Empty)
|
||||
: (isSelfBroadcast ? $"{broadcasterName} (You)" : broadcasterName);
|
||||
var (shell, broadcasterName) = cardData[index];
|
||||
|
||||
if (localIndex % 2 != 0)
|
||||
ImGui.SameLine();
|
||||
@@ -353,9 +350,9 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
|
||||
? shell.Group.Alias
|
||||
: shell.Group.GID;
|
||||
|
||||
var style = ImGui.GetStyle();
|
||||
float startX = ImGui.GetCursorPosX();
|
||||
float availW = ImGui.GetContentRegionAvail().X;
|
||||
float availWidth = ImGui.GetContentRegionAvail().X;
|
||||
float rightTextW = ImGui.CalcTextSize(broadcasterName).X;
|
||||
|
||||
ImGui.BeginGroup();
|
||||
|
||||
@@ -367,45 +364,13 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
|
||||
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 = broadcasterLabel;
|
||||
|
||||
if (!string.IsNullOrEmpty(broadcasterLabel) && maxBroadcasterWidth > 0f)
|
||||
{
|
||||
float bcFullWidth = ImGui.CalcTextSize(broadcasterLabel).X;
|
||||
string toolTip;
|
||||
|
||||
if (bcFullWidth > maxBroadcasterWidth)
|
||||
{
|
||||
broadcasterToShow = TruncateTextToWidth(broadcasterLabel, maxBroadcasterWidth);
|
||||
toolTip = broadcasterLabel + 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();
|
||||
var curPos = ImGui.GetCursorPos();
|
||||
ImGui.SetCursorPos(new Vector2(broadX - regionMinScreen.X + startX, curPos.Y + 3f * ImGuiHelpers.GlobalScale));
|
||||
ImGui.TextUnformatted(broadcasterToShow);
|
||||
if (ImGui.IsItemHovered())
|
||||
ImGui.SetTooltip(toolTip);
|
||||
}
|
||||
ImGui.SameLine();
|
||||
float rightX = startX + availWidth - rightTextW;
|
||||
var pos = ImGui.GetCursorPos();
|
||||
ImGui.SetCursorPos(new Vector2(rightX, pos.Y + 3f * ImGuiHelpers.GlobalScale));
|
||||
ImGui.TextUnformatted(broadcasterName);
|
||||
if (ImGui.IsItemHovered())
|
||||
ImGui.SetTooltip("Broadcaster of the syncshell.");
|
||||
|
||||
ImGui.EndGroup();
|
||||
|
||||
@@ -446,7 +411,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
|
||||
if (remainingY > 0)
|
||||
ImGui.Dummy(new Vector2(0, remainingY));
|
||||
|
||||
DrawJoinButton(shell, isSelfBroadcast);
|
||||
DrawJoinButton(shell);
|
||||
|
||||
ImGui.EndChild();
|
||||
ImGui.EndGroup();
|
||||
@@ -492,7 +457,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawJoinButton(GroupJoinDto shell, bool isSelfBroadcast)
|
||||
private void DrawJoinButton(dynamic shell)
|
||||
{
|
||||
const string visibleLabel = "Join";
|
||||
var label = $"{visibleLabel}##{shell.Group.GID}";
|
||||
@@ -520,7 +485,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
|
||||
buttonSize = new Vector2(-1, 0);
|
||||
}
|
||||
|
||||
if (!isAlreadyMember && !isRecentlyJoined && !isSelfBroadcast)
|
||||
if (!isAlreadyMember && !isRecentlyJoined)
|
||||
{
|
||||
ImGui.PushStyleColor(ImGuiCol.Button, UIColors.Get("LightlessGreen"));
|
||||
ImGui.PushStyleColor(ImGuiCol.ButtonHovered, UIColors.Get("LightlessGreen").WithAlpha(0.85f));
|
||||
@@ -570,9 +535,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
|
||||
ImGui.Button(label, buttonSize);
|
||||
}
|
||||
|
||||
UiSharedService.AttachToolTip(isSelfBroadcast
|
||||
? "This is your own Syncshell."
|
||||
: "Already a member or owner of this Syncshell.");
|
||||
UiSharedService.AttachToolTip("Already a member or owner of this Syncshell.");
|
||||
}
|
||||
|
||||
ImGui.PopStyleColor(3);
|
||||
@@ -627,40 +590,6 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
|
||||
float widthUsed = cursorLocalX - baseLocal.X;
|
||||
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)
|
||||
{
|
||||
|
||||
@@ -800,9 +800,22 @@ public class TopTabMenu
|
||||
if (!_lightFinderService.IsBroadcasting)
|
||||
return "Syncshell Finder";
|
||||
|
||||
string? myHashedCid = null;
|
||||
try
|
||||
{
|
||||
var cid = _dalamudUtilService.GetCID();
|
||||
myHashedCid = cid.ToString().GetHash256();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Couldnt get own CID, log and return default table
|
||||
}
|
||||
|
||||
var nearbyCount = _lightFinderScannerService
|
||||
.GetActiveSyncshellBroadcasts(excludeLocal: true)
|
||||
.Where(b => !string.IsNullOrEmpty(b.GID))
|
||||
.GetActiveSyncshellBroadcasts()
|
||||
.Where(b =>
|
||||
!string.IsNullOrEmpty(b.GID) &&
|
||||
!string.Equals(b.HashedCID, myHashedCid, StringComparison.Ordinal))
|
||||
.Select(b => b.GID!)
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.Count();
|
||||
|
||||
@@ -947,16 +947,13 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
|
||||
}
|
||||
}
|
||||
|
||||
if (_discordOAuthCheck != null && _discordOAuthCheck.IsCompleted && _discordOAuthCheck.Result != null)
|
||||
if (_discordOAuthCheck != null && _discordOAuthCheck.IsCompleted)
|
||||
{
|
||||
if (_discordOAuthGetCode == null)
|
||||
if (IconTextButton(FontAwesomeIcon.ArrowRight, "Authenticate with Server"))
|
||||
{
|
||||
if (IconTextButton(FontAwesomeIcon.ArrowRight, "Authenticate with Server"))
|
||||
{
|
||||
_discordOAuthGetCode = _serverConfigurationManager.GetDiscordOAuthToken(_discordOAuthCheck.Result, selectedServer.ServerUri, _discordOAuthGetCts.Token);
|
||||
}
|
||||
_discordOAuthGetCode = _serverConfigurationManager.GetDiscordOAuthToken(_discordOAuthCheck.Result!, selectedServer.ServerUri, _discordOAuthGetCts.Token);
|
||||
}
|
||||
else if (!_discordOAuthGetCode.IsCompleted)
|
||||
else if (_discordOAuthGetCode != null && !_discordOAuthGetCode.IsCompleted)
|
||||
{
|
||||
TextWrapped("A browser window has been opened, follow it to authenticate. Click the button below if you accidentally closed the window and need to restart the authentication.");
|
||||
if (IconTextButton(FontAwesomeIcon.Ban, "Cancel Authentication"))
|
||||
@@ -965,7 +962,7 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
|
||||
_discordOAuthGetCode = null;
|
||||
}
|
||||
}
|
||||
else
|
||||
else if (_discordOAuthGetCode != null && _discordOAuthGetCode.IsCompleted)
|
||||
{
|
||||
TextWrapped("Discord OAuth is completed, status: ");
|
||||
ImGui.SameLine();
|
||||
|
||||
@@ -11,13 +11,9 @@ using LightlessSync.LightlessConfiguration;
|
||||
using LightlessSync.LightlessConfiguration.Models;
|
||||
using LightlessSync.Services;
|
||||
using LightlessSync.Services.Chat;
|
||||
using LightlessSync.Services.LightFinder;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using LightlessSync.Services.ServerConfiguration;
|
||||
using LightlessSync.UI.Services;
|
||||
using LightlessSync.UI.Style;
|
||||
using LightlessSync.Utils;
|
||||
using OtterGui.Text;
|
||||
using LightlessSync.WebAPI;
|
||||
using LightlessSync.WebAPI.SignalR.Utils;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -27,49 +23,35 @@ namespace LightlessSync.UI;
|
||||
public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
||||
{
|
||||
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 ReportPopupId = "Report Message##zone_chat_report_popup";
|
||||
private const string ChannelDragPayloadId = "zone_chat_channel_drag";
|
||||
private const float DefaultWindowOpacity = .97f;
|
||||
private const float DefaultUnfocusedWindowOpacity = 0.6f;
|
||||
private const float MinWindowOpacity = 0.05f;
|
||||
private const float MaxWindowOpacity = 1f;
|
||||
private const float MinChatFontScale = 0.75f;
|
||||
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 ReportContextMaxLength = 1000;
|
||||
private const int MaxChannelNoteTabLength = 25;
|
||||
|
||||
private readonly UiSharedService _uiSharedService;
|
||||
private readonly ZoneChatService _zoneChatService;
|
||||
private readonly PairUiService _pairUiService;
|
||||
private readonly LightFinderService _lightFinderService;
|
||||
private readonly LightlessProfileManager _profileManager;
|
||||
private readonly ApiController _apiController;
|
||||
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 ImGuiWindowFlags _unpinnedWindowFlags;
|
||||
private float _currentWindowOpacity = DefaultWindowOpacity;
|
||||
private float _baseWindowOpacity = DefaultWindowOpacity;
|
||||
private bool _isWindowPinned;
|
||||
private bool _showRulesOverlay;
|
||||
private bool _refocusChatInput;
|
||||
private string? _refocusChatInputKey;
|
||||
private bool _isWindowFocused = true;
|
||||
private int _titleBarStylePopCount;
|
||||
|
||||
private string? _selectedChannelKey;
|
||||
private bool _scrollToBottom = true;
|
||||
private float? _pendingChannelScroll;
|
||||
private float _channelScroll;
|
||||
private float _channelScrollMax;
|
||||
private readonly SeluneBrush _seluneBrush = new();
|
||||
private ChatChannelSnapshot? _reportTargetChannel;
|
||||
private ChatMessageEntry? _reportTargetMessage;
|
||||
private string _reportReason = string.Empty;
|
||||
@@ -79,11 +61,6 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
||||
private bool _reportSubmitting;
|
||||
private string? _reportError;
|
||||
private ChatReportResult? _reportSubmissionResult;
|
||||
private string? _dragChannelKey;
|
||||
private string? _dragHoverKey;
|
||||
private bool _HideStateActive;
|
||||
private bool _HideStateWasOpen;
|
||||
private bool _pushedStyle;
|
||||
|
||||
public ZoneChatUi(
|
||||
ILogger<ZoneChatUi> logger,
|
||||
@@ -91,12 +68,8 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
||||
UiSharedService uiSharedService,
|
||||
ZoneChatService zoneChatService,
|
||||
PairUiService pairUiService,
|
||||
LightFinderService lightFinderService,
|
||||
LightlessProfileManager profileManager,
|
||||
ChatConfigService chatConfigService,
|
||||
ServerConfigurationManager serverConfigurationManager,
|
||||
DalamudUtilService dalamudUtilService,
|
||||
IUiBuilder uiBuilder,
|
||||
ApiController apiController,
|
||||
PerformanceCollectorService performanceCollectorService)
|
||||
: base(logger, mediator, "Lightless Chat", performanceCollectorService)
|
||||
@@ -104,12 +77,8 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
||||
_uiSharedService = uiSharedService;
|
||||
_zoneChatService = zoneChatService;
|
||||
_pairUiService = pairUiService;
|
||||
_lightFinderService = lightFinderService;
|
||||
_profileManager = profileManager;
|
||||
_chatConfigService = chatConfigService;
|
||||
_serverConfigurationManager = serverConfigurationManager;
|
||||
_dalamudUtilService = dalamudUtilService;
|
||||
_uiBuilder = uiBuilder;
|
||||
_apiController = apiController;
|
||||
_isWindowPinned = _chatConfigService.Current.IsWindowPinned;
|
||||
_showRulesOverlay = _chatConfigService.Current.ShowRulesOverlayOnOpen;
|
||||
@@ -119,7 +88,6 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
||||
}
|
||||
_unpinnedWindowFlags = Flags;
|
||||
RefreshWindowFlags();
|
||||
ApplyUiVisibilitySettings();
|
||||
Size = new Vector2(450, 420) * ImGuiHelpers.GlobalScale;
|
||||
SizeCondition = ImGuiCond.FirstUseEver;
|
||||
WindowBuilder.For(this)
|
||||
@@ -130,112 +98,20 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
||||
|
||||
Mediator.Subscribe<ChatChannelMessageAdded>(this, OnChatChannelMessageAdded);
|
||||
Mediator.Subscribe<ChatChannelsUpdated>(this, _ => _scrollToBottom = true);
|
||||
Mediator.Subscribe<PriorityFrameworkUpdateMessage>(this, _ => UpdateHideState());
|
||||
Mediator.Subscribe<CutsceneFrameworkUpdateMessage>(this, _ => UpdateHideState());
|
||||
}
|
||||
|
||||
public override void PreDraw()
|
||||
{
|
||||
RefreshWindowFlags();
|
||||
base.PreDraw();
|
||||
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;
|
||||
}
|
||||
|
||||
_currentWindowOpacity = Math.Clamp(_chatConfigService.Current.ChatWindowOpacity, MinWindowOpacity, MaxWindowOpacity);
|
||||
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()
|
||||
{
|
||||
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];
|
||||
childBgColor.W *= _baseWindowOpacity;
|
||||
childBgColor.W *= _currentWindowOpacity;
|
||||
using var childBg = ImRaii.PushColor(ImGuiCol.ChildBg, childBgColor);
|
||||
DrawConnectionControls();
|
||||
|
||||
@@ -247,61 +123,39 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
||||
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey3);
|
||||
ImGui.TextWrapped("No chat channels available.");
|
||||
ImGui.PopStyleColor();
|
||||
return;
|
||||
}
|
||||
else
|
||||
|
||||
EnsureSelectedChannel(channels);
|
||||
CleanupDrafts(channels);
|
||||
|
||||
DrawChannelButtons(channels);
|
||||
|
||||
if (_selectedChannelKey is null)
|
||||
return;
|
||||
|
||||
var activeChannel = channels.FirstOrDefault(channel => string.Equals(channel.Key, _selectedChannelKey, StringComparison.Ordinal));
|
||||
if (activeChannel.Equals(default(ChatChannelSnapshot)))
|
||||
{
|
||||
EnsureSelectedChannel(channels);
|
||||
CleanupDrafts(channels);
|
||||
|
||||
DrawChannelButtons(channels);
|
||||
|
||||
if (_selectedChannelKey is null)
|
||||
{
|
||||
selune.DrawHighlightOnly(ImGui.GetIO().DeltaTime);
|
||||
return;
|
||||
}
|
||||
|
||||
var activeChannel = channels.FirstOrDefault(channel => string.Equals(channel.Key, _selectedChannelKey, StringComparison.Ordinal));
|
||||
if (activeChannel.Equals(default(ChatChannelSnapshot)))
|
||||
{
|
||||
activeChannel = channels[0];
|
||||
_selectedChannelKey = activeChannel.Key;
|
||||
}
|
||||
|
||||
_zoneChatService.SetActiveChannel(activeChannel.Key);
|
||||
|
||||
DrawHeader(activeChannel);
|
||||
ImGui.Separator();
|
||||
DrawMessageArea(activeChannel, _currentWindowOpacity);
|
||||
ImGui.Separator();
|
||||
DrawInput(activeChannel);
|
||||
activeChannel = channels[0];
|
||||
_selectedChannelKey = activeChannel.Key;
|
||||
}
|
||||
|
||||
_zoneChatService.SetActiveChannel(activeChannel.Key);
|
||||
|
||||
DrawHeader(activeChannel);
|
||||
ImGui.Separator();
|
||||
DrawMessageArea(activeChannel, _currentWindowOpacity);
|
||||
ImGui.Separator();
|
||||
DrawInput(activeChannel);
|
||||
|
||||
if (_showRulesOverlay)
|
||||
{
|
||||
DrawRulesOverlay();
|
||||
}
|
||||
|
||||
selune.DrawHighlightOnly(ImGui.GetIO().DeltaTime);
|
||||
}
|
||||
|
||||
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)
|
||||
private static void DrawHeader(ChatChannelSnapshot channel)
|
||||
{
|
||||
var prefix = channel.Type == ChatChannelType.Zone ? "Zone" : "Syncshell";
|
||||
Vector4 color;
|
||||
@@ -324,18 +178,11 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
||||
if (channel.Type == ChatChannelType.Zone && channel.Descriptor.WorldId != 0)
|
||||
{
|
||||
ImGui.SameLine();
|
||||
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}");
|
||||
}
|
||||
ImGui.TextUnformatted($"World #{channel.Descriptor.WorldId}");
|
||||
}
|
||||
|
||||
var showInlineStatus = string.Equals(channel.StatusText, ChatDisabledStatus, StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(channel.StatusText, ZoneUnavailableStatus, StringComparison.OrdinalIgnoreCase);
|
||||
if (showInlineStatus)
|
||||
var showInlineDisabled = string.Equals(channel.StatusText, ChatDisabledStatus, StringComparison.OrdinalIgnoreCase);
|
||||
if (showInlineDisabled)
|
||||
{
|
||||
ImGui.SameLine();
|
||||
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey3);
|
||||
@@ -395,57 +242,52 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
||||
}
|
||||
else
|
||||
{
|
||||
var itemHeight = ImGui.GetTextLineHeightWithSpacing();
|
||||
using var clipper = ImUtf8.ListClipper(channel.Messages.Count, itemHeight);
|
||||
while (clipper.Step())
|
||||
for (var i = 0; i < channel.Messages.Count; i++)
|
||||
{
|
||||
for (var i = clipper.DisplayStart; i < clipper.DisplayEnd; i++)
|
||||
var message = channel.Messages[i];
|
||||
ImGui.PushID(i);
|
||||
|
||||
if (message.IsSystem)
|
||||
{
|
||||
var message = channel.Messages[i];
|
||||
ImGui.PushID(i);
|
||||
|
||||
if (message.IsSystem)
|
||||
{
|
||||
DrawSystemEntry(message);
|
||||
ImGui.PopID();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (message.Payload is not { } payload)
|
||||
{
|
||||
ImGui.PopID();
|
||||
continue;
|
||||
}
|
||||
|
||||
var timestampText = string.Empty;
|
||||
if (showTimestamps)
|
||||
{
|
||||
timestampText = $"[{message.ReceivedAtUtc.ToLocalTime().ToString("HH:mm", CultureInfo.InvariantCulture)}] ";
|
||||
}
|
||||
var color = message.FromSelf ? UIColors.Get("LightlessBlue") : ImGuiColors.DalamudWhite;
|
||||
|
||||
ImGui.PushStyleColor(ImGuiCol.Text, color);
|
||||
ImGui.TextWrapped($"{timestampText}{message.DisplayName}: {payload.Message}");
|
||||
ImGui.PopStyleColor();
|
||||
|
||||
if (ImGui.BeginPopupContextItem($"chat_msg_ctx##{channel.Key}_{i}"))
|
||||
{
|
||||
var contextLocalTimestamp = payload.SentAtUtc.ToLocalTime();
|
||||
var contextTimestampText = contextLocalTimestamp.ToString("yyyy-MM-dd HH:mm:ss 'UTC'z", CultureInfo.InvariantCulture);
|
||||
ImGui.TextDisabled(contextTimestampText);
|
||||
ImGui.Separator();
|
||||
|
||||
var actionIndex = 0;
|
||||
foreach (var action in GetContextMenuActions(channel, message))
|
||||
{
|
||||
DrawContextMenuAction(action, actionIndex++);
|
||||
}
|
||||
|
||||
ImGui.EndPopup();
|
||||
}
|
||||
|
||||
DrawSystemEntry(message);
|
||||
ImGui.PopID();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (message.Payload is not { } payload)
|
||||
{
|
||||
ImGui.PopID();
|
||||
continue;
|
||||
}
|
||||
|
||||
var timestampText = string.Empty;
|
||||
if (showTimestamps)
|
||||
{
|
||||
timestampText = $"[{message.ReceivedAtUtc.ToLocalTime().ToString("HH:mm", CultureInfo.InvariantCulture)}] ";
|
||||
}
|
||||
var color = message.FromSelf ? UIColors.Get("LightlessBlue") : ImGuiColors.DalamudWhite;
|
||||
|
||||
ImGui.PushStyleColor(ImGuiCol.Text, color);
|
||||
ImGui.TextWrapped($"{timestampText}{message.DisplayName}: {payload.Message}");
|
||||
ImGui.PopStyleColor();
|
||||
|
||||
if (ImGui.BeginPopupContextItem($"chat_msg_ctx##{channel.Key}_{i}"))
|
||||
{
|
||||
var contextLocalTimestamp = payload.SentAtUtc.ToLocalTime();
|
||||
var contextTimestampText = contextLocalTimestamp.ToString("yyyy-MM-dd HH:mm:ss 'UTC'z", CultureInfo.InvariantCulture);
|
||||
ImGui.TextDisabled(contextTimestampText);
|
||||
ImGui.Separator();
|
||||
|
||||
var actionIndex = 0;
|
||||
foreach (var action in GetContextMenuActions(channel, message))
|
||||
{
|
||||
DrawContextMenuAction(action, actionIndex++);
|
||||
}
|
||||
|
||||
ImGui.EndPopup();
|
||||
}
|
||||
|
||||
ImGui.PopID();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -482,15 +324,6 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
||||
_refocusChatInputKey = null;
|
||||
}
|
||||
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()
|
||||
&& (ImGui.IsKeyPressed(ImGuiKey.Enter) || ImGui.IsKeyPressed(ImGuiKey.KeypadEnter));
|
||||
_draftMessages[channel.Key] = draft;
|
||||
@@ -647,7 +480,7 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
||||
|
||||
ImGui.Separator();
|
||||
_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."));
|
||||
_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."));
|
||||
|
||||
ImGui.Dummy(new Vector2(5));
|
||||
|
||||
@@ -826,21 +659,6 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
||||
ImGui.EndPopup();
|
||||
}
|
||||
|
||||
public override void PostDraw()
|
||||
{
|
||||
if (_pushedStyle)
|
||||
{
|
||||
ImGui.PopStyleVar(1);
|
||||
_pushedStyle = false;
|
||||
}
|
||||
if (_titleBarStylePopCount > 0)
|
||||
{
|
||||
ImGui.PopStyleColor(_titleBarStylePopCount);
|
||||
_titleBarStylePopCount = 0;
|
||||
}
|
||||
base.PostDraw();
|
||||
}
|
||||
|
||||
private void OpenReportPopup(ChatChannelSnapshot channel, ChatMessageEntry message)
|
||||
{
|
||||
if (message.Payload is not { } payload)
|
||||
@@ -1216,56 +1034,18 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
||||
var groupSize = ImGui.GetItemRectSize();
|
||||
var minBlockX = cursorStart.X + groupSize.X + style.ItemSpacing.X;
|
||||
var availableAfterGroup = contentRightX - (cursorStart.X + groupSize.X);
|
||||
var lightfinderButtonWidth = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.PersonCirclePlus).X;
|
||||
var settingsButtonWidth = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Cog).X;
|
||||
var pinIcon = _isWindowPinned ? FontAwesomeIcon.Lock : FontAwesomeIcon.Unlock;
|
||||
var pinButtonWidth = _uiSharedService.GetIconButtonSize(pinIcon).X;
|
||||
var blockWidth = lightfinderButtonWidth + style.ItemSpacing.X + rulesButtonWidth + style.ItemSpacing.X + settingsButtonWidth + style.ItemSpacing.X + pinButtonWidth;
|
||||
var blockWidth = rulesButtonWidth + style.ItemSpacing.X + settingsButtonWidth + style.ItemSpacing.X + pinButtonWidth;
|
||||
var desiredBlockX = availableAfterGroup > blockWidth + style.ItemSpacing.X
|
||||
? contentRightX - blockWidth
|
||||
: minBlockX;
|
||||
desiredBlockX = Math.Max(cursorStart.X, desiredBlockX);
|
||||
var lightfinderPos = new Vector2(desiredBlockX, 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 rulesPos = new Vector2(desiredBlockX, cursorStart.Y);
|
||||
var settingsPos = new Vector2(desiredBlockX + rulesButtonWidth + 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.SetCursorPos(rulesPos);
|
||||
if (ImGui.Button("Rules", new Vector2(rulesButtonWidth, 0f)))
|
||||
@@ -1407,71 +1187,6 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
||||
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 fontScaleChanged = ImGui.SliderFloat("Message font scale", ref fontScale, MinChatFontScale, MaxChatFontScale, "%.2fx");
|
||||
var resetFontScale = ImGui.IsItemClicked(ImGuiMouseButton.Right);
|
||||
@@ -1511,55 +1226,9 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
||||
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();
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
_ = Task.Run(async () =>
|
||||
@@ -1575,7 +1244,7 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
||||
});
|
||||
}
|
||||
|
||||
private unsafe void DrawChannelButtons(IReadOnlyList<ChatChannelSnapshot> channels)
|
||||
private void DrawChannelButtons(IReadOnlyList<ChatChannelSnapshot> channels)
|
||||
{
|
||||
var style = ImGui.GetStyle();
|
||||
var baseFramePadding = style.FramePadding;
|
||||
@@ -1636,8 +1305,6 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
||||
{
|
||||
if (child)
|
||||
{
|
||||
var dragActive = _dragChannelKey is not null && ImGui.IsMouseDragging(ImGuiMouseButton.Left);
|
||||
var hoveredTargetThisFrame = false;
|
||||
var first = true;
|
||||
foreach (var channel in channels)
|
||||
{
|
||||
@@ -1648,7 +1315,6 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
||||
var showBadge = !isSelected && channel.UnreadCount > 0;
|
||||
var isZoneChannel = channel.Type == ChatChannelType.Zone;
|
||||
(string Text, Vector2 TextSize, float Width, float Height)? badgeMetrics = null;
|
||||
var channelLabel = GetChannelTabLabel(channel);
|
||||
|
||||
var normal = isSelected ? UIColors.Get("LightlessPurpleDefault") : UIColors.Get("ButtonDefault");
|
||||
var hovered = isSelected
|
||||
@@ -1677,7 +1343,7 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
||||
badgeMetrics = (badgeText, badgeTextSize, badgeWidth, badgeHeight);
|
||||
}
|
||||
|
||||
var clicked = ImGui.Button($"{channelLabel}##chat_channel_{channel.Key}");
|
||||
var clicked = ImGui.Button($"{channel.DisplayName}##chat_channel_{channel.Key}");
|
||||
|
||||
if (showBadge)
|
||||
{
|
||||
@@ -1693,77 +1359,10 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
||||
_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 itemMin = ImGui.GetItemRectMin();
|
||||
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)
|
||||
{
|
||||
var borderColor = UIColors.Get("LightlessOrange");
|
||||
@@ -1791,11 +1390,6 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
||||
first = false;
|
||||
}
|
||||
|
||||
if (dragActive && !hoveredTargetThisFrame)
|
||||
{
|
||||
_dragHoverKey = null;
|
||||
}
|
||||
|
||||
if (_pendingChannelScroll.HasValue)
|
||||
{
|
||||
ImGui.SetScrollX(_pendingChannelScroll.Value);
|
||||
@@ -1836,123 +1430,9 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
||||
_channelScroll = currentScroll;
|
||||
_channelScrollMax = maxScroll;
|
||||
|
||||
if (_dragChannelKey is not null && !ImGui.IsMouseDown(ImGuiMouseButton.Left))
|
||||
{
|
||||
_dragChannelKey = null;
|
||||
_dragHoverKey = null;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
var system = entry.SystemMessage;
|
||||
|
||||
@@ -60,6 +60,16 @@ public static class VariousExtensions
|
||||
CharacterData? oldData, ILogger logger, IPairPerformanceSubject cachedPlayer, bool forceApplyCustomization, bool forceApplyMods)
|
||||
{
|
||||
oldData ??= new();
|
||||
static bool FileReplacementsEquivalent(ICollection<FileReplacementData> left, ICollection<FileReplacementData> right)
|
||||
{
|
||||
if (left.Count != right.Count)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var comparer = LightlessSync.PlayerData.Data.FileReplacementDataComparer.Instance;
|
||||
return !left.Except(right, comparer).Any() && !right.Except(left, comparer).Any();
|
||||
}
|
||||
|
||||
var charaDataToUpdate = new Dictionary<ObjectKind, HashSet<PlayerChanges>>();
|
||||
foreach (ObjectKind objectKind in Enum.GetValues<ObjectKind>())
|
||||
@@ -95,7 +105,7 @@ public static class VariousExtensions
|
||||
{
|
||||
var oldList = oldData.FileReplacements[objectKind];
|
||||
var newList = newData.FileReplacements[objectKind];
|
||||
var listsAreEqual = oldList.SequenceEqual(newList, PlayerData.Data.FileReplacementDataComparer.Instance);
|
||||
var listsAreEqual = FileReplacementsEquivalent(oldList, newList);
|
||||
if (!listsAreEqual || forceApplyMods)
|
||||
{
|
||||
logger.LogDebug("[BASE-{appBase}] Updating {object}/{kind} (FileReplacements not equal) => {change}", applicationBase, cachedPlayer, objectKind, PlayerChanges.ModFiles);
|
||||
@@ -118,9 +128,9 @@ public static class VariousExtensions
|
||||
.OrderBy(g => string.IsNullOrEmpty(g.Hash) ? g.FileSwapPath : g.Hash, StringComparer.OrdinalIgnoreCase).ToList();
|
||||
var newTail = newFileReplacements.Where(g => g.GamePaths.Any(p => p.Contains("/tail/", StringComparison.OrdinalIgnoreCase)))
|
||||
.OrderBy(g => string.IsNullOrEmpty(g.Hash) ? g.FileSwapPath : g.Hash, StringComparer.OrdinalIgnoreCase).ToList();
|
||||
var existingTransients = existingFileReplacements.Where(g => g.GamePaths.Any(g => !g.EndsWith("mdl") && !g.EndsWith("tex") && !g.EndsWith("mtrl")))
|
||||
var existingTransients = existingFileReplacements.Where(g => g.GamePaths.Any(g => !g.EndsWith("mdl", StringComparison.OrdinalIgnoreCase) && !g.EndsWith("tex", StringComparison.OrdinalIgnoreCase) && !g.EndsWith("mtrl", StringComparison.OrdinalIgnoreCase)))
|
||||
.OrderBy(g => string.IsNullOrEmpty(g.Hash) ? g.FileSwapPath : g.Hash, StringComparer.OrdinalIgnoreCase).ToList();
|
||||
var newTransients = newFileReplacements.Where(g => g.GamePaths.Any(g => !g.EndsWith("mdl") && !g.EndsWith("tex") && !g.EndsWith("mtrl")))
|
||||
var newTransients = newFileReplacements.Where(g => g.GamePaths.Any(g => !g.EndsWith("mdl", StringComparison.OrdinalIgnoreCase) && !g.EndsWith("tex", StringComparison.OrdinalIgnoreCase) && !g.EndsWith("mtrl", StringComparison.OrdinalIgnoreCase)))
|
||||
.OrderBy(g => string.IsNullOrEmpty(g.Hash) ? g.FileSwapPath : g.Hash, StringComparer.OrdinalIgnoreCase).ToList();
|
||||
|
||||
logger.LogTrace("[BASE-{appbase}] ExistingFace: {of}, NewFace: {fc}; ExistingHair: {eh}, NewHair: {nh}; ExistingTail: {et}, NewTail: {nt}; ExistingTransient: {etr}, NewTransient: {ntr}", applicationBase,
|
||||
@@ -167,7 +177,8 @@ public static class VariousExtensions
|
||||
if (objectKind != ObjectKind.Player) continue;
|
||||
|
||||
bool manipDataDifferent = !string.Equals(oldData.ManipulationData, newData.ManipulationData, StringComparison.Ordinal);
|
||||
if (manipDataDifferent || forceApplyMods)
|
||||
var hasManipulationData = !string.IsNullOrEmpty(newData.ManipulationData);
|
||||
if (manipDataDifferent || (forceApplyMods && hasManipulationData))
|
||||
{
|
||||
logger.LogDebug("[BASE-{appBase}] Updating {object}/{kind} (Diff manip data) => {change}", applicationBase, cachedPlayer, objectKind, PlayerChanges.ModManip);
|
||||
charaDataToUpdate[objectKind].Add(PlayerChanges.ModManip);
|
||||
|
||||
@@ -563,7 +563,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
||||
|
||||
if (directDownloads.Count > 0 || downloadBatches.Length > 0)
|
||||
{
|
||||
Logger.LogInformation("Downloading {direct} files directly, and {batchtotal} in {batches} batches.", directDownloads.Count, batchDownloads.Count, downloadBatches.Length);
|
||||
Logger.LogWarning("Downloading {direct} files directly, and {batchtotal} in {batches} batches.", directDownloads.Count, batchDownloads.Count, downloadBatches.Length);
|
||||
}
|
||||
|
||||
if (gameObjectHandler is not null)
|
||||
|
||||
@@ -418,7 +418,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
|
||||
|
||||
public Task CyclePauseAsync(PairUniqueIdentifier ident)
|
||||
{
|
||||
var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
||||
var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(8));
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
var token = timeoutCts.Token;
|
||||
@@ -430,19 +430,20 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
|
||||
return;
|
||||
}
|
||||
|
||||
var targetPermissions = entry.SelfPermissions;
|
||||
targetPermissions.SetPaused(paused: true);
|
||||
var originalPermissions = entry.SelfPermissions;
|
||||
var targetPermissions = originalPermissions;
|
||||
targetPermissions.SetPaused(!originalPermissions.IsPaused());
|
||||
|
||||
await UserSetPairPermissions(new UserPermissionsDto(entry.User, targetPermissions)).ConfigureAwait(false);
|
||||
|
||||
var pauseApplied = false;
|
||||
var applied = false;
|
||||
while (!token.IsCancellationRequested)
|
||||
{
|
||||
if (_pairCoordinator.Ledger.TryGetEntry(ident, out var updated) && updated is not null)
|
||||
{
|
||||
if (updated.SelfPermissions == targetPermissions)
|
||||
{
|
||||
pauseApplied = true;
|
||||
applied = true;
|
||||
entry = updated;
|
||||
break;
|
||||
}
|
||||
@@ -452,16 +453,13 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
|
||||
Logger.LogTrace("Waiting for permissions change for {uid}", ident.UserId);
|
||||
}
|
||||
|
||||
if (!pauseApplied)
|
||||
if (!applied)
|
||||
{
|
||||
Logger.LogWarning("CyclePauseAsync timed out waiting for pause acknowledgement for {uid}", ident.UserId);
|
||||
return;
|
||||
}
|
||||
|
||||
targetPermissions.SetPaused(paused: false);
|
||||
await UserSetPairPermissions(new UserPermissionsDto(entry.User, targetPermissions)).ConfigureAwait(false);
|
||||
|
||||
Logger.LogDebug("CyclePauseAsync completed pause cycle for {uid}", ident.UserId);
|
||||
Logger.LogDebug("CyclePauseAsync toggled paused for {uid} to {state}", ident.UserId, targetPermissions.IsPaused());
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
@@ -481,26 +479,16 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
|
||||
}
|
||||
|
||||
public async Task PauseAsync(UserData userData)
|
||||
{
|
||||
await SetPausedStateAsync(userData, paused: true).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task UnpauseAsync(UserData userData)
|
||||
{
|
||||
await SetPausedStateAsync(userData, paused: false).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task SetPausedStateAsync(UserData userData, bool paused)
|
||||
{
|
||||
var pairIdent = new PairUniqueIdentifier(userData.UID);
|
||||
if (!_pairCoordinator.Ledger.TryGetEntry(pairIdent, out var entry) || entry is null)
|
||||
{
|
||||
Logger.LogWarning("SetPausedStateAsync: pair {uid} not found in ledger", userData.UID);
|
||||
Logger.LogWarning("PauseAsync: pair {uid} not found in ledger", userData.UID);
|
||||
return;
|
||||
}
|
||||
|
||||
var permissions = entry.SelfPermissions;
|
||||
permissions.SetPaused(paused);
|
||||
permissions.SetPaused(paused: true);
|
||||
await UserSetPairPermissions(new UserPermissionsDto(userData, permissions)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
@@ -596,10 +584,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
|
||||
OnGroupSendInfo((dto) => _ = Client_GroupSendInfo(dto));
|
||||
OnGroupUpdateProfile((dto) => _ = Client_GroupSendProfile(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));
|
||||
OnGposeLobbyLeave((dto) => _ = Client_GposeLobbyLeave(dto));
|
||||
|
||||
Submodule Penumbra.Api updated: 52a3216a52...1750c41b53
Reference in New Issue
Block a user