Compare commits
168 Commits
2.0.1
...
1.42.0.71-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f4665a0909 | ||
|
|
cb4bcec5e9 | ||
|
|
e5fa477eee | ||
|
|
af607e4380 | ||
|
|
ac8270e4ad | ||
|
|
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
@@ -1,44 +1,11 @@
|
|||||||
tagline: "Lightless Sync v2.0.1"
|
tagline: "Lightless Sync v2.0.0"
|
||||||
subline: "LIGHTLESS IS EVOLVING!!"
|
subline: "LIGHTLESS IS EVOLVING!!"
|
||||||
changelog:
|
changelog:
|
||||||
- name: "v2.0.1"
|
|
||||||
tagline: "Some Fixes"
|
|
||||||
date: "December 23 2025"
|
|
||||||
# be sure to set this every new version
|
|
||||||
isCurrent: true
|
|
||||||
versions:
|
|
||||||
- number: "Chat"
|
|
||||||
icon: ""
|
|
||||||
items:
|
|
||||||
- "You can turn off the syncshell chat as Owner by going to the Syncshell Admin panel -> Owner -> Enable/Disable Chat."
|
|
||||||
- "Fixed an issue where you can't chat due to regions being in a different language."
|
|
||||||
- number: "LightFinder"
|
|
||||||
icon: ""
|
|
||||||
items:
|
|
||||||
- "The icon/Lightfinder Text will be hidden when Game UI is hidden and behind game elements/UI"
|
|
||||||
- "Able to select an icon for the selected list or a custom glyph if you know the code."
|
|
||||||
- "Smoothing and reducing jitter on the icon/Lightfinder Text."
|
|
||||||
- "Fixed so higher scaled UI options (100/150/200% UI scale) wouldn't break the element."
|
|
||||||
- "Detects if GPose is active, wouldn't render the elements"
|
|
||||||
- number: "Miscellaneous fixes"
|
|
||||||
icon: ""
|
|
||||||
items:
|
|
||||||
- "Fixed the null error given on GetCID when transferring between zones/housing."
|
|
||||||
- "Added push/pop on certain ImGUI elements to remove them after being used. "
|
|
||||||
- "Having all tabs open in the Main UI wouldn't lag out the game anymore."
|
|
||||||
- "Cycle pause has been adjusted to the old function. There is a separate button to pause normally, now called 'Toggle (Un)Pause State'."
|
|
||||||
- "Changes have been made to the character redraw to address the issues with the building character data constantly being redrawn and the redrawn behavior with Honorific titles."
|
|
||||||
- "GPose characters should appear again in the actor screen"
|
|
||||||
- "Lightspeed download console messages are no longer shown as warnings."
|
|
||||||
- number: "Server Updates"
|
|
||||||
icon: ""
|
|
||||||
items:
|
|
||||||
- "Changes have been made to the disabling of your profile. It should save again."
|
|
||||||
- "Ability added to toggle chats from syncshell to be disabled."
|
|
||||||
- "Files are continuously being deleted due to high volumes in storage, potentially causing MCDOs to have missing files. We have increased the limit of the storage in our configurations to see if that helps."
|
|
||||||
- name: "v2.0.0"
|
- name: "v2.0.0"
|
||||||
tagline: "Thank you for 4 months!"
|
tagline: "Thank you for 4 months!"
|
||||||
date: "December 2025"
|
date: "December 2025"
|
||||||
|
# be sure to set this every new version
|
||||||
|
isCurrent: true
|
||||||
versions:
|
versions:
|
||||||
- number: "Lightless Chat"
|
- number: "Lightless Chat"
|
||||||
icon: ""
|
icon: ""
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ using LightlessSync.Utils;
|
|||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
using System.IO;
|
|
||||||
|
|
||||||
namespace LightlessSync.FileCache;
|
namespace LightlessSync.FileCache;
|
||||||
|
|
||||||
@@ -22,7 +21,6 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
|||||||
private CancellationTokenSource _scanCancellationTokenSource = new();
|
private CancellationTokenSource _scanCancellationTokenSource = new();
|
||||||
private readonly CancellationTokenSource _periodicCalculationTokenSource = new();
|
private readonly CancellationTokenSource _periodicCalculationTokenSource = new();
|
||||||
public static readonly IImmutableList<string> AllowedFileExtensions = [".mdl", ".tex", ".mtrl", ".tmb", ".pap", ".avfx", ".atex", ".sklb", ".eid", ".phyb", ".pbd", ".scd", ".skp", ".shpk", ".kdb"];
|
public static readonly IImmutableList<string> AllowedFileExtensions = [".mdl", ".tex", ".mtrl", ".tmb", ".pap", ".avfx", ".atex", ".sklb", ".eid", ".phyb", ".pbd", ".scd", ".skp", ".shpk", ".kdb"];
|
||||||
private static readonly HashSet<string> AllowedFileExtensionSet = new(AllowedFileExtensions, StringComparer.OrdinalIgnoreCase);
|
|
||||||
|
|
||||||
public CacheMonitor(ILogger<CacheMonitor> logger, IpcManager ipcManager, LightlessConfigService configService,
|
public CacheMonitor(ILogger<CacheMonitor> logger, IpcManager ipcManager, LightlessConfigService configService,
|
||||||
FileCacheManager fileDbManager, LightlessMediator mediator, PerformanceCollectorService performanceCollector, DalamudUtilService dalamudUtil,
|
FileCacheManager fileDbManager, LightlessMediator mediator, PerformanceCollectorService performanceCollector, DalamudUtilService dalamudUtil,
|
||||||
@@ -165,7 +163,7 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
Logger.LogTrace("Lightless FSW: FileChanged: {change} => {path}", e.ChangeType, e.FullPath);
|
Logger.LogTrace("Lightless FSW: FileChanged: {change} => {path}", e.ChangeType, e.FullPath);
|
||||||
|
|
||||||
if (!HasAllowedExtension(e.FullPath)) return;
|
if (!AllowedFileExtensions.Any(ext => e.FullPath.EndsWith(ext, StringComparison.OrdinalIgnoreCase))) return;
|
||||||
|
|
||||||
lock (_watcherChanges)
|
lock (_watcherChanges)
|
||||||
{
|
{
|
||||||
@@ -209,7 +207,7 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
|||||||
private void Fs_Changed(object sender, FileSystemEventArgs e)
|
private void Fs_Changed(object sender, FileSystemEventArgs e)
|
||||||
{
|
{
|
||||||
if (Directory.Exists(e.FullPath)) return;
|
if (Directory.Exists(e.FullPath)) return;
|
||||||
if (!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))
|
if (e.ChangeType is not (WatcherChangeTypes.Changed or WatcherChangeTypes.Deleted or WatcherChangeTypes.Created))
|
||||||
return;
|
return;
|
||||||
@@ -233,7 +231,7 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
foreach (var file in directoryFiles)
|
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);
|
var oldPath = file.Replace(e.FullPath, e.OldFullPath, StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
_watcherChanges.Remove(oldPath);
|
_watcherChanges.Remove(oldPath);
|
||||||
@@ -245,7 +243,7 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (!HasAllowedExtension(e.FullPath)) return;
|
if (!AllowedFileExtensions.Any(ext => e.FullPath.EndsWith(ext, StringComparison.OrdinalIgnoreCase))) return;
|
||||||
|
|
||||||
lock (_watcherChanges)
|
lock (_watcherChanges)
|
||||||
{
|
{
|
||||||
@@ -265,17 +263,6 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
|||||||
public FileSystemWatcher? PenumbraWatcher { get; private set; }
|
public FileSystemWatcher? PenumbraWatcher { get; private set; }
|
||||||
public FileSystemWatcher? LightlessWatcher { get; private set; }
|
public FileSystemWatcher? LightlessWatcher { get; private set; }
|
||||||
|
|
||||||
private static bool HasAllowedExtension(string path)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(path))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var extension = Path.GetExtension(path);
|
|
||||||
return !string.IsNullOrEmpty(extension) && AllowedFileExtensionSet.Contains(extension);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task LightlessWatcherExecution()
|
private async Task LightlessWatcherExecution()
|
||||||
{
|
{
|
||||||
_lightlessFswCts = _lightlessFswCts.CancelRecreate();
|
_lightlessFswCts = _lightlessFswCts.CancelRecreate();
|
||||||
@@ -619,7 +606,7 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
|||||||
[
|
[
|
||||||
.. Directory.GetFiles(folder, "*.*", SearchOption.AllDirectories)
|
.. Directory.GetFiles(folder, "*.*", SearchOption.AllDirectories)
|
||||||
.AsParallel()
|
.AsParallel()
|
||||||
.Where(f => HasAllowedExtension(f)
|
.Where(f => AllowedFileExtensions.Any(e => f.EndsWith(e, StringComparison.OrdinalIgnoreCase))
|
||||||
&& !f.Contains(@"\bg\", StringComparison.OrdinalIgnoreCase)
|
&& !f.Contains(@"\bg\", StringComparison.OrdinalIgnoreCase)
|
||||||
&& !f.Contains(@"\bgcommon\", StringComparison.OrdinalIgnoreCase)
|
&& !f.Contains(@"\bgcommon\", StringComparison.OrdinalIgnoreCase)
|
||||||
&& !f.Contains(@"\ui\", StringComparison.OrdinalIgnoreCase)),
|
&& !f.Contains(@"\ui\", StringComparison.OrdinalIgnoreCase)),
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
|
|||||||
_playerRelatedPointers.Remove(msg.GameObjectHandler);
|
_playerRelatedPointers.Remove(msg.GameObjectHandler);
|
||||||
});
|
});
|
||||||
|
|
||||||
foreach (var descriptor in _actorObjectService.ObjectDescriptors)
|
foreach (var descriptor in _actorObjectService.PlayerDescriptors)
|
||||||
{
|
{
|
||||||
HandleActorTracked(descriptor);
|
HandleActorTracked(descriptor);
|
||||||
}
|
}
|
||||||
@@ -291,7 +291,7 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
var activeDescriptors = new Dictionary<nint, ObjectKind>();
|
var activeDescriptors = new Dictionary<nint, ObjectKind>();
|
||||||
foreach (var descriptor in _actorObjectService.ObjectDescriptors)
|
foreach (var descriptor in _actorObjectService.PlayerDescriptors)
|
||||||
{
|
{
|
||||||
if (TryResolveObjectKind(descriptor, out var resolvedKind))
|
if (TryResolveObjectKind(descriptor, out var resolvedKind))
|
||||||
{
|
{
|
||||||
@@ -372,9 +372,6 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
private void HandleActorTracked(ActorObjectService.ActorDescriptor descriptor)
|
private void HandleActorTracked(ActorObjectService.ActorDescriptor descriptor)
|
||||||
{
|
{
|
||||||
if (descriptor.IsInGpose)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (!TryResolveObjectKind(descriptor, out var resolvedKind))
|
if (!TryResolveObjectKind(descriptor, out var resolvedKind))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Dalamud.Plugin;
|
using Dalamud.Game.ClientState.Objects.SubKinds;
|
||||||
|
using Dalamud.Plugin;
|
||||||
using Dalamud.Plugin.Ipc;
|
using Dalamud.Plugin.Ipc;
|
||||||
using LightlessSync.Interop.Ipc.Framework;
|
using LightlessSync.Interop.Ipc.Framework;
|
||||||
using LightlessSync.Services;
|
using LightlessSync.Services;
|
||||||
@@ -12,7 +13,7 @@ public sealed class IpcCallerMoodles : IpcServiceBase
|
|||||||
private static readonly IpcServiceDescriptor MoodlesDescriptor = new("Moodles", "Moodles", new Version(0, 0, 0, 0));
|
private static readonly IpcServiceDescriptor MoodlesDescriptor = new("Moodles", "Moodles", new Version(0, 0, 0, 0));
|
||||||
|
|
||||||
private readonly ICallGateSubscriber<int> _moodlesApiVersion;
|
private readonly ICallGateSubscriber<int> _moodlesApiVersion;
|
||||||
private readonly ICallGateSubscriber<nint, object> _moodlesOnChange;
|
private readonly ICallGateSubscriber<IPlayerCharacter, object> _moodlesOnChange;
|
||||||
private readonly ICallGateSubscriber<nint, string> _moodlesGetStatus;
|
private readonly ICallGateSubscriber<nint, string> _moodlesGetStatus;
|
||||||
private readonly ICallGateSubscriber<nint, string, object> _moodlesSetStatus;
|
private readonly ICallGateSubscriber<nint, string, object> _moodlesSetStatus;
|
||||||
private readonly ICallGateSubscriber<nint, object> _moodlesRevertStatus;
|
private readonly ICallGateSubscriber<nint, object> _moodlesRevertStatus;
|
||||||
@@ -28,7 +29,7 @@ public sealed class IpcCallerMoodles : IpcServiceBase
|
|||||||
_lightlessMediator = lightlessMediator;
|
_lightlessMediator = lightlessMediator;
|
||||||
|
|
||||||
_moodlesApiVersion = pi.GetIpcSubscriber<int>("Moodles.Version");
|
_moodlesApiVersion = pi.GetIpcSubscriber<int>("Moodles.Version");
|
||||||
_moodlesOnChange = pi.GetIpcSubscriber<nint, object>("Moodles.StatusManagerModified");
|
_moodlesOnChange = pi.GetIpcSubscriber<IPlayerCharacter, object>("Moodles.StatusManagerModified");
|
||||||
_moodlesGetStatus = pi.GetIpcSubscriber<nint, string>("Moodles.GetStatusManagerByPtrV2");
|
_moodlesGetStatus = pi.GetIpcSubscriber<nint, string>("Moodles.GetStatusManagerByPtrV2");
|
||||||
_moodlesSetStatus = pi.GetIpcSubscriber<nint, string, object>("Moodles.SetStatusManagerByPtrV2");
|
_moodlesSetStatus = pi.GetIpcSubscriber<nint, string, object>("Moodles.SetStatusManagerByPtrV2");
|
||||||
_moodlesRevertStatus = pi.GetIpcSubscriber<nint, object>("Moodles.ClearStatusManagerByPtrV2");
|
_moodlesRevertStatus = pi.GetIpcSubscriber<nint, object>("Moodles.ClearStatusManagerByPtrV2");
|
||||||
@@ -38,9 +39,9 @@ public sealed class IpcCallerMoodles : IpcServiceBase
|
|||||||
CheckAPI();
|
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)
|
protected override void Dispose(bool disposing)
|
||||||
@@ -106,7 +107,7 @@ public sealed class IpcCallerMoodles : IpcServiceBase
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return _moodlesApiVersion.InvokeFunc() >= 4
|
return _moodlesApiVersion.InvokeFunc() == 3
|
||||||
? IpcConnectionState.Available
|
? IpcConnectionState.Available
|
||||||
: IpcConnectionState.VersionMismatch;
|
: IpcConnectionState.VersionMismatch;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace LightlessSync.LightlessConfiguration.Configurations;
|
namespace LightlessSync.LightlessConfiguration.Configurations;
|
||||||
|
|
||||||
@@ -11,16 +10,7 @@ public sealed class ChatConfig : ILightlessConfiguration
|
|||||||
public bool ShowRulesOverlayOnOpen { get; set; } = true;
|
public bool ShowRulesOverlayOnOpen { get; set; } = true;
|
||||||
public bool ShowMessageTimestamps { get; set; } = true;
|
public bool ShowMessageTimestamps { get; set; } = true;
|
||||||
public float ChatWindowOpacity { get; set; } = .97f;
|
public float ChatWindowOpacity { get; set; } = .97f;
|
||||||
public bool FadeWhenUnfocused { get; set; } = false;
|
|
||||||
public float UnfocusedWindowOpacity { get; set; } = 0.6f;
|
|
||||||
public bool IsWindowPinned { get; set; } = false;
|
public bool IsWindowPinned { get; set; } = false;
|
||||||
public bool AutoOpenChatOnPluginLoad { get; set; } = false;
|
public bool AutoOpenChatOnPluginLoad { get; set; } = false;
|
||||||
public float ChatFontScale { get; set; } = 1.0f;
|
public float ChatFontScale { get; set; } = 1.0f;
|
||||||
public bool HideInCombat { get; set; } = false;
|
|
||||||
public bool HideInDuty { get; set; } = false;
|
|
||||||
public bool ShowWhenUiHidden { get; set; } = true;
|
|
||||||
public bool ShowInCutscenes { get; set; } = true;
|
|
||||||
public bool ShowInGpose { get; set; } = true;
|
|
||||||
public List<string> ChannelOrder { get; set; } = new();
|
|
||||||
public Dictionary<string, bool> PreferNotesForChannels { get; set; } = new(StringComparer.Ordinal);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,8 +49,7 @@ public class LightlessConfig : ILightlessConfiguration
|
|||||||
public int DownloadSpeedLimitInBytes { get; set; } = 0;
|
public int DownloadSpeedLimitInBytes { get; set; } = 0;
|
||||||
public DownloadSpeeds DownloadSpeedType { get; set; } = DownloadSpeeds.MBps;
|
public DownloadSpeeds DownloadSpeedType { get; set; } = DownloadSpeeds.MBps;
|
||||||
public bool PreferNotesOverNamesForVisible { get; set; } = false;
|
public bool PreferNotesOverNamesForVisible { get; set; } = false;
|
||||||
public VisiblePairSortMode VisiblePairSortMode { get; set; } = VisiblePairSortMode.Alphabetical;
|
public VisiblePairSortMode VisiblePairSortMode { get; set; } = VisiblePairSortMode.Default;
|
||||||
public OnlinePairSortMode OnlinePairSortMode { get; set; } = OnlinePairSortMode.Alphabetical;
|
|
||||||
public float ProfileDelay { get; set; } = 1.5f;
|
public float ProfileDelay { get; set; } = 1.5f;
|
||||||
public bool ProfilePopoutRight { get; set; } = false;
|
public bool ProfilePopoutRight { get; set; } = false;
|
||||||
public bool ProfilesAllowNsfw { get; set; } = false;
|
public bool ProfilesAllowNsfw { get; set; } = false;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Authors></Authors>
|
<Authors></Authors>
|
||||||
<Company></Company>
|
<Company></Company>
|
||||||
<Version>2.0.1</Version>
|
<Version>1.42.0.71</Version>
|
||||||
<Description></Description>
|
<Description></Description>
|
||||||
<Copyright></Copyright>
|
<Copyright></Copyright>
|
||||||
<PackageProjectUrl>https://github.com/Light-Public-Syncshells/LightlessClient</PackageProjectUrl>
|
<PackageProjectUrl>https://github.com/Light-Public-Syncshells/LightlessClient</PackageProjectUrl>
|
||||||
|
|||||||
@@ -119,7 +119,6 @@ public class PlayerDataFactory
|
|||||||
CharacterDataFragment fragment = objectKind == ObjectKind.Player ? new CharacterDataFragmentPlayer() : new();
|
CharacterDataFragment fragment = objectKind == ObjectKind.Player ? new CharacterDataFragmentPlayer() : new();
|
||||||
|
|
||||||
_logger.LogDebug("Building character data for {obj}", playerRelatedObject);
|
_logger.LogDebug("Building character data for {obj}", playerRelatedObject);
|
||||||
var logDebug = _logger.IsEnabled(LogLevel.Debug);
|
|
||||||
|
|
||||||
// wait until chara is not drawing and present so nothing spontaneously explodes
|
// wait until chara is not drawing and present so nothing spontaneously explodes
|
||||||
await _dalamudUtil.WaitWhileCharacterIsDrawing(_logger, playerRelatedObject, Guid.NewGuid(), 30000, ct: ct).ConfigureAwait(false);
|
await _dalamudUtil.WaitWhileCharacterIsDrawing(_logger, playerRelatedObject, Guid.NewGuid(), 30000, ct: ct).ConfigureAwait(false);
|
||||||
@@ -133,6 +132,11 @@ public class PlayerDataFactory
|
|||||||
|
|
||||||
ct.ThrowIfCancellationRequested();
|
ct.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
Dictionary<string, List<ushort>>? boneIndices =
|
||||||
|
objectKind != ObjectKind.Player
|
||||||
|
? null
|
||||||
|
: await _dalamudUtil.RunOnFrameworkThread(() => _modelAnalyzer.GetSkeletonBoneIndices(playerRelatedObject)).ConfigureAwait(false);
|
||||||
|
|
||||||
DateTime start = DateTime.UtcNow;
|
DateTime start = DateTime.UtcNow;
|
||||||
|
|
||||||
// penumbra call, it's currently broken
|
// penumbra call, it's currently broken
|
||||||
@@ -150,22 +154,12 @@ public class PlayerDataFactory
|
|||||||
|
|
||||||
ct.ThrowIfCancellationRequested();
|
ct.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
if (logDebug)
|
|
||||||
{
|
|
||||||
_logger.LogDebug("== Static Replacements ==");
|
_logger.LogDebug("== Static Replacements ==");
|
||||||
foreach (var replacement in fragment.FileReplacements.Where(i => i.HasFileReplacement).OrderBy(i => i.GamePaths.First(), StringComparer.OrdinalIgnoreCase))
|
foreach (var replacement in fragment.FileReplacements.Where(i => i.HasFileReplacement).OrderBy(i => i.GamePaths.First(), StringComparer.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
_logger.LogDebug("=> {repl}", replacement);
|
_logger.LogDebug("=> {repl}", replacement);
|
||||||
ct.ThrowIfCancellationRequested();
|
ct.ThrowIfCancellationRequested();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
foreach (var replacement in fragment.FileReplacements.Where(i => i.HasFileReplacement))
|
|
||||||
{
|
|
||||||
ct.ThrowIfCancellationRequested();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await _transientResourceManager.WaitForRecording(ct).ConfigureAwait(false);
|
await _transientResourceManager.WaitForRecording(ct).ConfigureAwait(false);
|
||||||
|
|
||||||
@@ -196,22 +190,12 @@ public class PlayerDataFactory
|
|||||||
var transientPaths = ManageSemiTransientData(objectKind);
|
var transientPaths = ManageSemiTransientData(objectKind);
|
||||||
var resolvedTransientPaths = await GetFileReplacementsFromPaths(transientPaths, new HashSet<string>(StringComparer.Ordinal)).ConfigureAwait(false);
|
var resolvedTransientPaths = await GetFileReplacementsFromPaths(transientPaths, new HashSet<string>(StringComparer.Ordinal)).ConfigureAwait(false);
|
||||||
|
|
||||||
if (logDebug)
|
|
||||||
{
|
|
||||||
_logger.LogDebug("== Transient Replacements ==");
|
_logger.LogDebug("== Transient Replacements ==");
|
||||||
foreach (var replacement in resolvedTransientPaths.Select(c => new FileReplacement([.. c.Value], c.Key)).OrderBy(f => f.ResolvedPath, StringComparer.Ordinal))
|
foreach (var replacement in resolvedTransientPaths.Select(c => new FileReplacement([.. c.Value], c.Key)).OrderBy(f => f.ResolvedPath, StringComparer.Ordinal))
|
||||||
{
|
{
|
||||||
_logger.LogDebug("=> {repl}", replacement);
|
_logger.LogDebug("=> {repl}", replacement);
|
||||||
fragment.FileReplacements.Add(replacement);
|
fragment.FileReplacements.Add(replacement);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
foreach (var replacement in resolvedTransientPaths.Select(c => new FileReplacement([.. c.Value], c.Key)))
|
|
||||||
{
|
|
||||||
fragment.FileReplacements.Add(replacement);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// clean up all semi transient resources that don't have any file replacement (aka null resolve)
|
// clean up all semi transient resources that don't have any file replacement (aka null resolve)
|
||||||
_transientResourceManager.CleanUpSemiTransientResources(objectKind, [.. fragment.FileReplacements]);
|
_transientResourceManager.CleanUpSemiTransientResources(objectKind, [.. fragment.FileReplacements]);
|
||||||
@@ -268,27 +252,12 @@ public class PlayerDataFactory
|
|||||||
|
|
||||||
ct.ThrowIfCancellationRequested();
|
ct.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
Dictionary<string, List<ushort>>? boneIndices = null;
|
|
||||||
var hasPapFiles = false;
|
|
||||||
if (objectKind == ObjectKind.Player)
|
|
||||||
{
|
|
||||||
hasPapFiles = fragment.FileReplacements.Any(f =>
|
|
||||||
!f.IsFileSwap && f.GamePaths.First().EndsWith("pap", StringComparison.OrdinalIgnoreCase));
|
|
||||||
if (hasPapFiles)
|
|
||||||
{
|
|
||||||
boneIndices = await _dalamudUtil.RunOnFrameworkThread(() => _modelAnalyzer.GetSkeletonBoneIndices(playerRelatedObject)).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (objectKind == ObjectKind.Player)
|
if (objectKind == ObjectKind.Player)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
|
||||||
if (hasPapFiles)
|
|
||||||
{
|
{
|
||||||
await VerifyPlayerAnimationBones(boneIndices, (fragment as CharacterDataFragmentPlayer)!, ct).ConfigureAwait(false);
|
await VerifyPlayerAnimationBones(boneIndices, (fragment as CharacterDataFragmentPlayer)!, ct).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
catch (OperationCanceledException e)
|
catch (OperationCanceledException e)
|
||||||
{
|
{
|
||||||
_logger.LogDebug(e, "Cancelled during player animation verification");
|
_logger.LogDebug(e, "Cancelled during player animation verification");
|
||||||
@@ -309,16 +278,12 @@ public class PlayerDataFactory
|
|||||||
{
|
{
|
||||||
if (boneIndices == null) return;
|
if (boneIndices == null) return;
|
||||||
|
|
||||||
if (_logger.IsEnabled(LogLevel.Debug))
|
|
||||||
{
|
|
||||||
foreach (var kvp in boneIndices)
|
foreach (var kvp in boneIndices)
|
||||||
{
|
{
|
||||||
_logger.LogDebug("Found {skellyname} ({idx} bone indices) on player: {bones}", kvp.Key, kvp.Value.Any() ? kvp.Value.Max() : 0, string.Join(',', kvp.Value));
|
_logger.LogDebug("Found {skellyname} ({idx} bone indices) on player: {bones}", kvp.Key, kvp.Value.Any() ? kvp.Value.Max() : 0, string.Join(',', kvp.Value));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
var maxPlayerBoneIndex = boneIndices.SelectMany(kvp => kvp.Value).DefaultIfEmpty().Max();
|
if (boneIndices.All(u => u.Value.Count == 0)) return;
|
||||||
if (maxPlayerBoneIndex <= 0) return;
|
|
||||||
|
|
||||||
int noValidationFailed = 0;
|
int noValidationFailed = 0;
|
||||||
foreach (var file in fragment.FileReplacements.Where(f => !f.IsFileSwap && f.GamePaths.First().EndsWith("pap", StringComparison.OrdinalIgnoreCase)).ToList())
|
foreach (var file in fragment.FileReplacements.Where(f => !f.IsFileSwap && f.GamePaths.First().EndsWith("pap", StringComparison.OrdinalIgnoreCase)).ToList())
|
||||||
@@ -338,13 +303,12 @@ public class PlayerDataFactory
|
|||||||
|
|
||||||
_logger.LogDebug("Verifying bone indices for {path}, found {x} skeletons", file.ResolvedPath, skeletonIndices.Count);
|
_logger.LogDebug("Verifying bone indices for {path}, found {x} skeletons", file.ResolvedPath, skeletonIndices.Count);
|
||||||
|
|
||||||
foreach (var boneCount in skeletonIndices)
|
foreach (var boneCount in skeletonIndices.Select(k => k).ToList())
|
||||||
{
|
{
|
||||||
var maxAnimationIndex = boneCount.Value.DefaultIfEmpty().Max();
|
if (boneCount.Value.Max() > boneIndices.SelectMany(b => b.Value).Max())
|
||||||
if (maxAnimationIndex > maxPlayerBoneIndex)
|
|
||||||
{
|
{
|
||||||
_logger.LogWarning("Found more bone indices on the animation {path} skeleton {skl} (max indice {idx}) than on any player related skeleton (max indice {idx2})",
|
_logger.LogWarning("Found more bone indices on the animation {path} skeleton {skl} (max indice {idx}) than on any player related skeleton (max indice {idx2})",
|
||||||
file.ResolvedPath, boneCount.Key, maxAnimationIndex, maxPlayerBoneIndex);
|
file.ResolvedPath, boneCount.Key, boneCount.Value.Max(), boneIndices.SelectMany(b => b.Value).Max());
|
||||||
validationFailed = true;
|
validationFailed = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,8 +16,6 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
|||||||
private readonly Func<IntPtr> _getAddress;
|
private readonly Func<IntPtr> _getAddress;
|
||||||
private readonly bool _isOwnedObject;
|
private readonly bool _isOwnedObject;
|
||||||
private readonly PerformanceCollectorService _performanceCollector;
|
private readonly PerformanceCollectorService _performanceCollector;
|
||||||
private readonly object _frameworkUpdateGate = new();
|
|
||||||
private bool _frameworkUpdateSubscribed;
|
|
||||||
private byte _classJob = 0;
|
private byte _classJob = 0;
|
||||||
private Task? _delayedZoningTask;
|
private Task? _delayedZoningTask;
|
||||||
private bool _haltProcessing = false;
|
private bool _haltProcessing = false;
|
||||||
@@ -49,10 +47,7 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_isOwnedObject)
|
Mediator.Subscribe<FrameworkUpdateMessage>(this, (_) => FrameworkUpdate());
|
||||||
{
|
|
||||||
EnableFrameworkUpdates();
|
|
||||||
}
|
|
||||||
|
|
||||||
Mediator.Subscribe<ZoneSwitchEndMessage>(this, (_) => ZoneSwitchEnd());
|
Mediator.Subscribe<ZoneSwitchEndMessage>(this, (_) => ZoneSwitchEnd());
|
||||||
Mediator.Subscribe<ZoneSwitchStartMessage>(this, (_) => ZoneSwitchStart());
|
Mediator.Subscribe<ZoneSwitchStartMessage>(this, (_) => ZoneSwitchStart());
|
||||||
@@ -114,7 +109,7 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
|||||||
{
|
{
|
||||||
while (await _dalamudUtil.RunOnFrameworkThread(() =>
|
while (await _dalamudUtil.RunOnFrameworkThread(() =>
|
||||||
{
|
{
|
||||||
EnsureLatestObjectState();
|
if (_haltProcessing) CheckAndUpdateObject();
|
||||||
if (CurrentDrawCondition != DrawCondition.None) return true;
|
if (CurrentDrawCondition != DrawCondition.None) return true;
|
||||||
var gameObj = _dalamudUtil.CreateGameObject(Address);
|
var gameObj = _dalamudUtil.CreateGameObject(Address);
|
||||||
if (gameObj is Dalamud.Game.ClientState.Objects.Types.ICharacter chara)
|
if (gameObj is Dalamud.Game.ClientState.Objects.Types.ICharacter chara)
|
||||||
@@ -153,11 +148,6 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
|||||||
_haltProcessing = false;
|
_haltProcessing = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Refresh()
|
|
||||||
{
|
|
||||||
_dalamudUtil.RunOnFrameworkThread(CheckAndUpdateObject).GetAwaiter().GetResult();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> IsBeingDrawnRunOnFrameworkAsync()
|
public async Task<bool> IsBeingDrawnRunOnFrameworkAsync()
|
||||||
{
|
{
|
||||||
return await _dalamudUtil.RunOnFrameworkThread(IsBeingDrawn).ConfigureAwait(false);
|
return await _dalamudUtil.RunOnFrameworkThread(IsBeingDrawn).ConfigureAwait(false);
|
||||||
@@ -371,7 +361,7 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
|||||||
|
|
||||||
private bool IsBeingDrawn()
|
private bool IsBeingDrawn()
|
||||||
{
|
{
|
||||||
EnsureLatestObjectState();
|
if (_haltProcessing) CheckAndUpdateObject();
|
||||||
|
|
||||||
if (_dalamudUtil.IsAnythingDrawing)
|
if (_dalamudUtil.IsAnythingDrawing)
|
||||||
{
|
{
|
||||||
@@ -383,28 +373,6 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
|||||||
return CurrentDrawCondition != DrawCondition.None;
|
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()
|
private unsafe DrawCondition IsBeingDrawnUnsafe()
|
||||||
{
|
{
|
||||||
if (Address == IntPtr.Zero) return DrawCondition.ObjectZero;
|
if (Address == IntPtr.Zero) return DrawCondition.ObjectZero;
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
using LightlessSync.API.Data;
|
using LightlessSync.API.Data;
|
||||||
|
|
||||||
namespace LightlessSync.PlayerData.Pairs;
|
namespace LightlessSync.PlayerData.Pairs;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// orchestrates the lifecycle of a paired character
|
/// orchestrates the lifecycle of a paired character
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IPairHandlerAdapter : IDisposable, IPairPerformanceSubject
|
public interface IPairHandlerAdapter : IDisposable, IPairPerformanceSubject
|
||||||
{
|
{
|
||||||
new string Ident { get; }
|
new string Ident { get; }
|
||||||
bool Initialized { get; }
|
bool Initialized { get; }
|
||||||
bool IsVisible { get; }
|
bool IsVisible { get; }
|
||||||
@@ -25,13 +25,6 @@
|
|||||||
bool IsDownloading { get; }
|
bool IsDownloading { get; }
|
||||||
int PendingDownloadCount { get; }
|
int PendingDownloadCount { get; }
|
||||||
int ForbiddenDownloadCount { 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; }
|
|
||||||
|
|
||||||
void Initialize();
|
void Initialize();
|
||||||
void ApplyData(CharacterData data);
|
void ApplyData(CharacterData data);
|
||||||
@@ -40,4 +33,4 @@
|
|||||||
void LoadCachedCharacterData(CharacterData data);
|
void LoadCachedCharacterData(CharacterData data);
|
||||||
void SetUploading(bool uploading);
|
void SetUploading(bool uploading);
|
||||||
void SetPaused(bool paused);
|
void SetPaused(bool paused);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,13 +87,11 @@ public class Pair
|
|||||||
return;
|
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;
|
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));
|
_mediator.Publish(new ProfileOpenStandaloneMessage(this));
|
||||||
@@ -105,7 +103,6 @@ public class Pair
|
|||||||
ApplyLastReceivedData(forced: true);
|
ApplyLastReceivedData(forced: true);
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
UiSharedService.AddContextMenuItem(args, name: "Change Permissions", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () =>
|
UiSharedService.AddContextMenuItem(args, name: "Change Permissions", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () =>
|
||||||
{
|
{
|
||||||
@@ -113,24 +110,7 @@ public class Pair
|
|||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (IsPaused)
|
UiSharedService.AddContextMenuItem(args, name: "Cycle pause state", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () =>
|
||||||
{
|
|
||||||
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: () =>
|
|
||||||
{
|
{
|
||||||
TriggerCyclePause();
|
TriggerCyclePause();
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
@@ -214,13 +194,9 @@ public class Pair
|
|||||||
{
|
{
|
||||||
var handler = TryGetHandler();
|
var handler = TryGetHandler();
|
||||||
if (handler is null)
|
if (handler is null)
|
||||||
|
{
|
||||||
return PairDebugInfo.Empty;
|
return PairDebugInfo.Empty;
|
||||||
|
}
|
||||||
var now = DateTime.UtcNow;
|
|
||||||
var dueAt = handler.VisibilityEvictionDueAtUtc;
|
|
||||||
var remainingSeconds = dueAt.HasValue
|
|
||||||
? Math.Max(0, (dueAt.Value - now).TotalSeconds)
|
|
||||||
: (double?)null;
|
|
||||||
|
|
||||||
return new PairDebugInfo(
|
return new PairDebugInfo(
|
||||||
true,
|
true,
|
||||||
@@ -230,19 +206,11 @@ public class Pair
|
|||||||
handler.LastDataReceivedAt,
|
handler.LastDataReceivedAt,
|
||||||
handler.LastApplyAttemptAt,
|
handler.LastApplyAttemptAt,
|
||||||
handler.LastSuccessfulApplyAt,
|
handler.LastSuccessfulApplyAt,
|
||||||
handler.InvisibleSinceUtc,
|
|
||||||
handler.VisibilityEvictionDueAtUtc,
|
|
||||||
remainingSeconds,
|
|
||||||
handler.LastFailureReason,
|
handler.LastFailureReason,
|
||||||
handler.LastBlockingConditions,
|
handler.LastBlockingConditions,
|
||||||
handler.IsApplying,
|
handler.IsApplying,
|
||||||
handler.IsDownloading,
|
handler.IsDownloading,
|
||||||
handler.PendingDownloadCount,
|
handler.PendingDownloadCount,
|
||||||
handler.ForbiddenDownloadCount,
|
handler.ForbiddenDownloadCount);
|
||||||
handler.PendingModReapply,
|
|
||||||
handler.ModApplyDeferred,
|
|
||||||
handler.MissingCriticalMods,
|
|
||||||
handler.MissingNonCriticalMods,
|
|
||||||
handler.MissingForbiddenMods);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,20 +8,12 @@ public sealed record PairDebugInfo(
|
|||||||
DateTime? LastDataReceivedAt,
|
DateTime? LastDataReceivedAt,
|
||||||
DateTime? LastApplyAttemptAt,
|
DateTime? LastApplyAttemptAt,
|
||||||
DateTime? LastSuccessfulApplyAt,
|
DateTime? LastSuccessfulApplyAt,
|
||||||
DateTime? InvisibleSinceUtc,
|
|
||||||
DateTime? VisibilityEvictionDueAtUtc,
|
|
||||||
double? VisibilityEvictionRemainingSeconds,
|
|
||||||
string? LastFailureReason,
|
string? LastFailureReason,
|
||||||
IReadOnlyList<string> BlockingConditions,
|
IReadOnlyList<string> BlockingConditions,
|
||||||
bool IsApplying,
|
bool IsApplying,
|
||||||
bool IsDownloading,
|
bool IsDownloading,
|
||||||
int PendingDownloadCount,
|
int PendingDownloadCount,
|
||||||
int ForbiddenDownloadCount,
|
int ForbiddenDownloadCount)
|
||||||
bool PendingModReapply,
|
|
||||||
bool ModApplyDeferred,
|
|
||||||
int MissingCriticalMods,
|
|
||||||
int MissingNonCriticalMods,
|
|
||||||
int MissingForbiddenMods)
|
|
||||||
{
|
{
|
||||||
public static PairDebugInfo Empty { get; } = new(
|
public static PairDebugInfo Empty { get; } = new(
|
||||||
false,
|
false,
|
||||||
@@ -32,17 +24,9 @@ public sealed record PairDebugInfo(
|
|||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
Array.Empty<string>(),
|
Array.Empty<string>(),
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
0,
|
0,
|
||||||
0,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0);
|
0);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ using LightlessSync.Interop.Ipc;
|
|||||||
using LightlessSync.PlayerData.Factories;
|
using LightlessSync.PlayerData.Factories;
|
||||||
using LightlessSync.PlayerData.Handlers;
|
using LightlessSync.PlayerData.Handlers;
|
||||||
using LightlessSync.Services;
|
using LightlessSync.Services;
|
||||||
using LightlessSync.Services.ActorTracking;
|
|
||||||
using LightlessSync.Services.Events;
|
using LightlessSync.Services.Events;
|
||||||
using LightlessSync.Services.Mediator;
|
using LightlessSync.Services.Mediator;
|
||||||
using LightlessSync.Services.PairProcessing;
|
using LightlessSync.Services.PairProcessing;
|
||||||
@@ -19,7 +18,6 @@ using LightlessSync.WebAPI.Files;
|
|||||||
using LightlessSync.WebAPI.Files.Models;
|
using LightlessSync.WebAPI.Files.Models;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using DalamudObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind;
|
|
||||||
using ObjectKind = LightlessSync.API.Data.Enum.ObjectKind;
|
using ObjectKind = LightlessSync.API.Data.Enum.ObjectKind;
|
||||||
using FileReplacementDataComparer = LightlessSync.PlayerData.Data.FileReplacementDataComparer;
|
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 sealed record CombatData(Guid ApplicationId, CharacterData CharacterData, bool Forced);
|
||||||
|
|
||||||
private readonly DalamudUtilService _dalamudUtil;
|
private readonly DalamudUtilService _dalamudUtil;
|
||||||
private readonly ActorObjectService _actorObjectService;
|
|
||||||
private readonly FileDownloadManager _downloadManager;
|
private readonly FileDownloadManager _downloadManager;
|
||||||
private readonly FileCacheManager _fileDbManager;
|
private readonly FileCacheManager _fileDbManager;
|
||||||
private readonly GameObjectHandlerFactory _gameObjectHandlerFactory;
|
private readonly GameObjectHandlerFactory _gameObjectHandlerFactory;
|
||||||
@@ -59,15 +56,11 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
private bool _forceFullReapply;
|
private bool _forceFullReapply;
|
||||||
private Dictionary<(string GamePath, string? Hash), string>? _lastAppliedModdedPaths;
|
private Dictionary<(string GamePath, string? Hash), string>? _lastAppliedModdedPaths;
|
||||||
private bool _needsCollectionRebuild;
|
private bool _needsCollectionRebuild;
|
||||||
private bool _pendingModReapply;
|
|
||||||
private bool _lastModApplyDeferred;
|
|
||||||
private int _lastMissingCriticalMods;
|
|
||||||
private int _lastMissingNonCriticalMods;
|
|
||||||
private int _lastMissingForbiddenMods;
|
|
||||||
private bool _isVisible;
|
private bool _isVisible;
|
||||||
private Guid _penumbraCollection;
|
private Guid _penumbraCollection;
|
||||||
private readonly object _collectionGate = new();
|
private readonly object _collectionGate = new();
|
||||||
private bool _redrawOnNextApplication = false;
|
private bool _redrawOnNextApplication = false;
|
||||||
|
private bool _explicitRedrawQueued;
|
||||||
private readonly object _initializationGate = new();
|
private readonly object _initializationGate = new();
|
||||||
private readonly object _pauseLock = new();
|
private readonly object _pauseLock = new();
|
||||||
private Task _pauseTransitionTask = Task.CompletedTask;
|
private Task _pauseTransitionTask = Task.CompletedTask;
|
||||||
@@ -77,29 +70,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
private DateTime? _lastSuccessfulApplyAt;
|
private DateTime? _lastSuccessfulApplyAt;
|
||||||
private string? _lastFailureReason;
|
private string? _lastFailureReason;
|
||||||
private IReadOnlyList<string> _lastBlockingConditions = Array.Empty<string>();
|
private IReadOnlyList<string> _lastBlockingConditions = Array.Empty<string>();
|
||||||
private readonly object _visibilityGraceGate = new();
|
|
||||||
private CancellationTokenSource? _visibilityGraceCts;
|
|
||||||
private static readonly TimeSpan VisibilityEvictionGrace = TimeSpan.FromMinutes(1);
|
|
||||||
private 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 string Ident { get; }
|
||||||
public bool Initialized { get; private set; }
|
public bool Initialized { get; private set; }
|
||||||
public bool ScheduledForDeletion { get; set; }
|
public bool ScheduledForDeletion { get; set; }
|
||||||
@@ -109,32 +80,18 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
get => _isVisible;
|
get => _isVisible;
|
||||||
private set
|
private set
|
||||||
{
|
{
|
||||||
if (_isVisible == value) return;
|
if (_isVisible != value)
|
||||||
|
{
|
||||||
_isVisible = value;
|
_isVisible = value;
|
||||||
|
|
||||||
if (!_isVisible)
|
if (!_isVisible)
|
||||||
{
|
{
|
||||||
DisableSync();
|
DisableSync();
|
||||||
|
ResetPenumbraCollection(reason: "VisibilityLost");
|
||||||
_invisibleSinceUtc = DateTime.UtcNow;
|
|
||||||
_visibilityEvictionDueAtUtc = _invisibleSinceUtc.Value.Add(VisibilityEvictionGrace);
|
|
||||||
|
|
||||||
StartVisibilityGraceTask();
|
|
||||||
}
|
}
|
||||||
else
|
else if (_charaHandler is not null && _charaHandler.Address != nint.Zero)
|
||||||
{
|
{
|
||||||
CancelVisibilityGraceTask();
|
|
||||||
|
|
||||||
_invisibleSinceUtc = null;
|
|
||||||
_visibilityEvictionDueAtUtc = null;
|
|
||||||
|
|
||||||
ScheduledForDeletion = false;
|
|
||||||
|
|
||||||
if (_charaHandler is not null && _charaHandler.Address != nint.Zero)
|
|
||||||
_ = EnsurePenumbraCollection();
|
_ = EnsurePenumbraCollection();
|
||||||
}
|
}
|
||||||
|
|
||||||
var user = GetPrimaryUserData();
|
var user = GetPrimaryUserData();
|
||||||
Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter),
|
Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter),
|
||||||
EventSeverity.Informational, "User Visibility Changed, now: " + (_isVisible ? "Is Visible" : "Is not Visible"))));
|
EventSeverity.Informational, "User Visibility Changed, now: " + (_isVisible ? "Is Visible" : "Is not Visible"))));
|
||||||
@@ -142,17 +99,13 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
Mediator.Publish(new VisibilityChange());
|
Mediator.Publish(new VisibilityChange());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public long LastAppliedDataBytes { get; private set; }
|
public long LastAppliedDataBytes { get; private set; }
|
||||||
public long LastAppliedDataTris { get; set; } = -1;
|
public long LastAppliedDataTris { get; set; } = -1;
|
||||||
public long LastAppliedApproximateVRAMBytes { get; set; } = -1;
|
public long LastAppliedApproximateVRAMBytes { get; set; } = -1;
|
||||||
public long LastAppliedApproximateEffectiveVRAMBytes { get; set; } = -1;
|
public long LastAppliedApproximateEffectiveVRAMBytes { get; set; } = -1;
|
||||||
public CharacterData? LastReceivedCharacterData { get; private set; }
|
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? LastDataReceivedAt => _lastDataReceivedAt;
|
||||||
public DateTime? LastApplyAttemptAt => _lastApplyAttemptAt;
|
public DateTime? LastApplyAttemptAt => _lastApplyAttemptAt;
|
||||||
public DateTime? LastSuccessfulApplyAt => _lastSuccessfulApplyAt;
|
public DateTime? LastSuccessfulApplyAt => _lastSuccessfulApplyAt;
|
||||||
@@ -173,7 +126,6 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
FileDownloadManager transferManager,
|
FileDownloadManager transferManager,
|
||||||
PluginWarningNotificationService pluginWarningNotificationManager,
|
PluginWarningNotificationService pluginWarningNotificationManager,
|
||||||
DalamudUtilService dalamudUtil,
|
DalamudUtilService dalamudUtil,
|
||||||
ActorObjectService actorObjectService,
|
|
||||||
IHostApplicationLifetime lifetime,
|
IHostApplicationLifetime lifetime,
|
||||||
FileCacheManager fileDbManager,
|
FileCacheManager fileDbManager,
|
||||||
PlayerPerformanceService playerPerformanceService,
|
PlayerPerformanceService playerPerformanceService,
|
||||||
@@ -190,7 +142,6 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
_downloadManager = transferManager;
|
_downloadManager = transferManager;
|
||||||
_pluginWarningNotificationManager = pluginWarningNotificationManager;
|
_pluginWarningNotificationManager = pluginWarningNotificationManager;
|
||||||
_dalamudUtil = dalamudUtil;
|
_dalamudUtil = dalamudUtil;
|
||||||
_actorObjectService = actorObjectService;
|
|
||||||
_lifetime = lifetime;
|
_lifetime = lifetime;
|
||||||
_fileDbManager = fileDbManager;
|
_fileDbManager = fileDbManager;
|
||||||
_playerPerformanceService = playerPerformanceService;
|
_playerPerformanceService = playerPerformanceService;
|
||||||
@@ -214,7 +165,6 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ActorObjectService.ActorDescriptor? trackedDescriptor = null;
|
|
||||||
lock (_initializationGate)
|
lock (_initializationGate)
|
||||||
{
|
{
|
||||||
if (Initialized)
|
if (Initialized)
|
||||||
@@ -228,12 +178,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
_forceApplyMods = true;
|
_forceApplyMods = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
var useFrameworkUpdate = !_actorObjectService.HooksActive;
|
|
||||||
if (useFrameworkUpdate)
|
|
||||||
{
|
|
||||||
Mediator.Subscribe<FrameworkUpdateMessage>(this, _ => FrameworkUpdate());
|
Mediator.Subscribe<FrameworkUpdateMessage>(this, _ => FrameworkUpdate());
|
||||||
_frameworkUpdateSubscribed = true;
|
|
||||||
}
|
|
||||||
Mediator.Subscribe<ZoneSwitchStartMessage>(this, _ =>
|
Mediator.Subscribe<ZoneSwitchStartMessage>(this, _ =>
|
||||||
{
|
{
|
||||||
_downloadCancellationTokenSource?.CancelDispose();
|
_downloadCancellationTokenSource?.CancelDispose();
|
||||||
@@ -269,49 +214,17 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
Mediator.Subscribe<CutsceneEndMessage>(this, _ => EnableSync());
|
Mediator.Subscribe<CutsceneEndMessage>(this, _ => EnableSync());
|
||||||
Mediator.Subscribe<GposeStartMessage>(this, _ => DisableSync());
|
Mediator.Subscribe<GposeStartMessage>(this, _ => DisableSync());
|
||||||
Mediator.Subscribe<GposeEndMessage>(this, _ => EnableSync());
|
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;
|
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();
|
TryApplyQueuedData();
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!useFrameworkUpdate
|
|
||||||
&& _actorObjectService.TryGetActorByHash(Ident, out var descriptor)
|
|
||||||
&& descriptor.Address != nint.Zero)
|
|
||||||
{
|
|
||||||
trackedDescriptor = descriptor;
|
|
||||||
}
|
|
||||||
|
|
||||||
Initialized = true;
|
Initialized = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (trackedDescriptor.HasValue)
|
|
||||||
{
|
|
||||||
HandleActorTracked(trackedDescriptor.Value);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private IReadOnlyList<PairConnection> GetCurrentPairs()
|
private IReadOnlyList<PairConnection> GetCurrentPairs()
|
||||||
@@ -804,67 +717,6 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
return true;
|
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()
|
private bool CanApplyNow()
|
||||||
{
|
{
|
||||||
return !_dalamudUtil.IsInCombat
|
return !_dalamudUtil.IsInCombat
|
||||||
@@ -888,16 +740,6 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
_lastBlockingConditions = Array.Empty<string>();
|
_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)
|
public void ApplyCharacterData(Guid applicationBase, CharacterData characterData, bool forceApplyCustomization = false)
|
||||||
{
|
{
|
||||||
_lastApplyAttemptAt = DateTime.UtcNow;
|
_lastApplyAttemptAt = DateTime.UtcNow;
|
||||||
@@ -915,48 +757,72 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
if (_dalamudUtil.IsInCombat)
|
if (_dalamudUtil.IsInCombat)
|
||||||
{
|
{
|
||||||
const string reason = "Cannot apply character data: you are in combat, deferring application";
|
const string reason = "Cannot apply character data: you are in combat, deferring application";
|
||||||
DeferApplication(applicationBase, characterData, forceApplyCustomization, user, reason, "Combat", LogLevel.Debug,
|
Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter), EventSeverity.Warning,
|
||||||
"[BASE-{appBase}] Received data but player is in combat", applicationBase);
|
reason)));
|
||||||
|
Logger.LogDebug("[BASE-{appBase}] Received data but player is in combat", applicationBase);
|
||||||
|
RecordFailure(reason, "Combat");
|
||||||
|
_dataReceivedInDowntime = new(applicationBase, characterData, forceApplyCustomization);
|
||||||
|
SetUploading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_dalamudUtil.IsPerforming)
|
if (_dalamudUtil.IsPerforming)
|
||||||
{
|
{
|
||||||
const string reason = "Cannot apply character data: you are performing music, deferring application";
|
const string reason = "Cannot apply character data: you are performing music, deferring application";
|
||||||
DeferApplication(applicationBase, characterData, forceApplyCustomization, user, reason, "Performance", LogLevel.Debug,
|
Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter), EventSeverity.Warning,
|
||||||
"[BASE-{appBase}] Received data but player is performing", applicationBase);
|
reason)));
|
||||||
|
Logger.LogDebug("[BASE-{appBase}] Received data but player is performing", applicationBase);
|
||||||
|
RecordFailure(reason, "Performance");
|
||||||
|
_dataReceivedInDowntime = new(applicationBase, characterData, forceApplyCustomization);
|
||||||
|
SetUploading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_dalamudUtil.IsInInstance)
|
if (_dalamudUtil.IsInInstance)
|
||||||
{
|
{
|
||||||
const string reason = "Cannot apply character data: you are in an instance, deferring application";
|
const string reason = "Cannot apply character data: you are in an instance, deferring application";
|
||||||
DeferApplication(applicationBase, characterData, forceApplyCustomization, user, reason, "Instance", LogLevel.Debug,
|
Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter), EventSeverity.Warning,
|
||||||
"[BASE-{appBase}] Received data but player is in instance", applicationBase);
|
reason)));
|
||||||
|
Logger.LogDebug("[BASE-{appBase}] Received data but player is in instance", applicationBase);
|
||||||
|
RecordFailure(reason, "Instance");
|
||||||
|
_dataReceivedInDowntime = new(applicationBase, characterData, forceApplyCustomization);
|
||||||
|
SetUploading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_dalamudUtil.IsInCutscene)
|
if (_dalamudUtil.IsInCutscene)
|
||||||
{
|
{
|
||||||
const string reason = "Cannot apply character data: you are in a cutscene, deferring application";
|
const string reason = "Cannot apply character data: you are in a cutscene, deferring application";
|
||||||
DeferApplication(applicationBase, characterData, forceApplyCustomization, user, reason, "Cutscene", LogLevel.Debug,
|
Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter), EventSeverity.Warning,
|
||||||
"[BASE-{appBase}] Received data but player is in a cutscene", applicationBase);
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_dalamudUtil.IsInGpose)
|
if (_dalamudUtil.IsInGpose)
|
||||||
{
|
{
|
||||||
const string reason = "Cannot apply character data: you are in GPose, deferring application";
|
const string reason = "Cannot apply character data: you are in GPose, deferring application";
|
||||||
DeferApplication(applicationBase, characterData, forceApplyCustomization, user, reason, "GPose", LogLevel.Debug,
|
Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter), EventSeverity.Warning,
|
||||||
"[BASE-{appBase}] Received data but player is in GPose", applicationBase);
|
reason)));
|
||||||
|
Logger.LogDebug("[BASE-{appBase}] Received data but player is in GPose", applicationBase);
|
||||||
|
RecordFailure(reason, "GPose");
|
||||||
|
_dataReceivedInDowntime = new(applicationBase, characterData, forceApplyCustomization);
|
||||||
|
SetUploading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_ipcManager.Penumbra.APIAvailable || !_ipcManager.Glamourer.APIAvailable)
|
if (!_ipcManager.Penumbra.APIAvailable || !_ipcManager.Glamourer.APIAvailable)
|
||||||
{
|
{
|
||||||
const string reason = "Cannot apply character data: Penumbra or Glamourer is not available, deferring application";
|
const string reason = "Cannot apply character data: Penumbra or Glamourer is not available, deferring application";
|
||||||
DeferApplication(applicationBase, characterData, forceApplyCustomization, user, reason, "PluginUnavailable", LogLevel.Information,
|
Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter), EventSeverity.Warning,
|
||||||
"[BASE-{appbase}] Application of data for {player} while Penumbra/Glamourer unavailable, returning", applicationBase, GetLogIdentifier());
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -999,10 +865,13 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
_forceApplyMods = false;
|
_forceApplyMods = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_explicitRedrawQueued = false;
|
||||||
|
|
||||||
if (_redrawOnNextApplication && charaDataToUpdate.TryGetValue(ObjectKind.Player, out var player))
|
if (_redrawOnNextApplication && charaDataToUpdate.TryGetValue(ObjectKind.Player, out var player))
|
||||||
{
|
{
|
||||||
player.Add(PlayerChanges.ForcedRedraw);
|
player.Add(PlayerChanges.ForcedRedraw);
|
||||||
_redrawOnNextApplication = false;
|
_redrawOnNextApplication = false;
|
||||||
|
_explicitRedrawQueued = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (charaDataToUpdate.TryGetValue(ObjectKind.Player, out var playerChanges))
|
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)
|
protected override void Dispose(bool disposing)
|
||||||
{
|
{
|
||||||
base.Dispose(disposing);
|
base.Dispose(disposing);
|
||||||
@@ -1107,10 +936,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
_downloadCancellationTokenSource = null;
|
_downloadCancellationTokenSource = null;
|
||||||
_downloadManager.Dispose();
|
_downloadManager.Dispose();
|
||||||
_charaHandler?.Dispose();
|
_charaHandler?.Dispose();
|
||||||
CancelVisibilityGraceTask();
|
|
||||||
_charaHandler = null;
|
_charaHandler = null;
|
||||||
_invisibleSinceUtc = null;
|
|
||||||
_visibilityEvictionDueAtUtc = null;
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(name))
|
if (!string.IsNullOrEmpty(name))
|
||||||
{
|
{
|
||||||
@@ -1196,14 +1022,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
|
|
||||||
Logger.LogDebug("[{applicationId}] Applying Customization Data for {handler}", applicationId, handler);
|
Logger.LogDebug("[{applicationId}] Applying Customization Data for {handler}", applicationId, handler);
|
||||||
await _dalamudUtil.WaitWhileCharacterIsDrawing(Logger, handler, applicationId, 30000, token).ConfigureAwait(false);
|
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();
|
token.ThrowIfCancellationRequested();
|
||||||
var tasks = new List<Task>();
|
|
||||||
bool needsRedraw = false;
|
|
||||||
foreach (var change in changes.Value.OrderBy(p => (int)p))
|
foreach (var change in changes.Value.OrderBy(p => (int)p))
|
||||||
{
|
{
|
||||||
Logger.LogDebug("[{applicationId}] Processing {change} for {handler}", applicationId, change, handler);
|
Logger.LogDebug("[{applicationId}] Processing {change} for {handler}", applicationId, change, handler);
|
||||||
@@ -1212,39 +1031,45 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
case PlayerChanges.Customize:
|
case PlayerChanges.Customize:
|
||||||
if (charaData.CustomizePlusData.TryGetValue(changes.Key, out var customizePlusData))
|
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))
|
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;
|
break;
|
||||||
|
|
||||||
case PlayerChanges.Heels:
|
case PlayerChanges.Heels:
|
||||||
tasks.Add(_ipcManager.Heels.SetOffsetForPlayerAsync(handler.Address, charaData.HeelsData));
|
await _ipcManager.Heels.SetOffsetForPlayerAsync(handler.Address, charaData.HeelsData).ConfigureAwait(false);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case PlayerChanges.Honorific:
|
case PlayerChanges.Honorific:
|
||||||
tasks.Add(_ipcManager.Honorific.SetTitleAsync(handler.Address, charaData.HonorificData));
|
await _ipcManager.Honorific.SetTitleAsync(handler.Address, charaData.HonorificData).ConfigureAwait(false);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case PlayerChanges.Glamourer:
|
case PlayerChanges.Glamourer:
|
||||||
if (charaData.GlamourerData.TryGetValue(changes.Key, out var glamourerData))
|
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;
|
break;
|
||||||
|
|
||||||
case PlayerChanges.Moodles:
|
case PlayerChanges.Moodles:
|
||||||
tasks.Add(_ipcManager.Moodles.SetStatusAsync(handler.Address, charaData.MoodlesData));
|
await _ipcManager.Moodles.SetStatusAsync(handler.Address, charaData.MoodlesData).ConfigureAwait(false);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case PlayerChanges.PetNames:
|
case PlayerChanges.PetNames:
|
||||||
tasks.Add(_ipcManager.PetNames.SetPlayerData(handler.Address, charaData.PetNamesData));
|
await _ipcManager.PetNames.SetPlayerData(handler.Address, charaData.PetNamesData).ConfigureAwait(false);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case PlayerChanges.ForcedRedraw:
|
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;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@@ -1252,16 +1077,6 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
}
|
}
|
||||||
token.ThrowIfCancellationRequested();
|
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
|
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)
|
private static Dictionary<ObjectKind, HashSet<PlayerChanges>> BuildFullChangeSet(CharacterData characterData)
|
||||||
{
|
{
|
||||||
@@ -1412,7 +1265,6 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Task? _pairDownloadTask;
|
private Task? _pairDownloadTask;
|
||||||
private Task _visibilityGraceTask;
|
|
||||||
|
|
||||||
private async Task DownloadAndApplyCharacterAsync(Guid applicationBase, CharacterData charaData, Dictionary<ObjectKind, HashSet<PlayerChanges>> updatedData,
|
private async Task DownloadAndApplyCharacterAsync(Guid applicationBase, CharacterData charaData, Dictionary<ObjectKind, HashSet<PlayerChanges>> updatedData,
|
||||||
bool updateModdedPaths, bool updateManip, Dictionary<(string GamePath, string? Hash), string>? cachedModdedPaths, CancellationToken downloadToken)
|
bool updateModdedPaths, bool updateManip, Dictionary<(string GamePath, string? Hash), string>? cachedModdedPaths, CancellationToken downloadToken)
|
||||||
@@ -1423,7 +1275,6 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
bool skipDownscaleForPair = ShouldSkipDownscale();
|
bool skipDownscaleForPair = ShouldSkipDownscale();
|
||||||
var user = GetPrimaryUserData();
|
var user = GetPrimaryUserData();
|
||||||
Dictionary<(string GamePath, string? Hash), string> moddedPaths;
|
Dictionary<(string GamePath, string? Hash), string> moddedPaths;
|
||||||
List<FileReplacementData> missingReplacements = [];
|
|
||||||
|
|
||||||
if (updateModdedPaths)
|
if (updateModdedPaths)
|
||||||
{
|
{
|
||||||
@@ -1435,7 +1286,6 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
{
|
{
|
||||||
int attempts = 0;
|
int attempts = 0;
|
||||||
List<FileReplacementData> toDownloadReplacements = TryCalculateModdedDictionary(applicationBase, charaData, out moddedPaths, downloadToken);
|
List<FileReplacementData> toDownloadReplacements = TryCalculateModdedDictionary(applicationBase, charaData, out moddedPaths, downloadToken);
|
||||||
missingReplacements = toDownloadReplacements;
|
|
||||||
|
|
||||||
while (toDownloadReplacements.Count > 0 && attempts++ <= 10 && !downloadToken.IsCancellationRequested)
|
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);
|
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))))
|
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();
|
downloadToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
var handlerForApply = _charaHandler;
|
var handlerForApply = _charaHandler;
|
||||||
@@ -1589,7 +1390,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
_applicationCancellationTokenSource = _applicationCancellationTokenSource.CancelRecreate() ?? new CancellationTokenSource();
|
_applicationCancellationTokenSource = _applicationCancellationTokenSource.CancelRecreate() ?? new CancellationTokenSource();
|
||||||
var token = _applicationCancellationTokenSource.Token;
|
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
|
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,
|
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
|
try
|
||||||
{
|
{
|
||||||
@@ -1607,10 +1408,6 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
|
|
||||||
Logger.LogDebug("[{applicationId}] Waiting for initial draw for for {handler}", _applicationId, handlerForApply);
|
Logger.LogDebug("[{applicationId}] Waiting for initial draw for for {handler}", _applicationId, handlerForApply);
|
||||||
await _dalamudUtil.WaitWhileCharacterIsDrawing(Logger, handlerForApply, _applicationId, 30000, token).ConfigureAwait(false);
|
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();
|
token.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
@@ -1677,11 +1474,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
|
|
||||||
_cachedData = charaData;
|
_cachedData = charaData;
|
||||||
_pairStateCache.Store(Ident, charaData);
|
_pairStateCache.Store(Ident, charaData);
|
||||||
if (wantsModApply)
|
_forceFullReapply = false;
|
||||||
{
|
|
||||||
_pendingModReapply = pendingModReapply;
|
|
||||||
}
|
|
||||||
_forceFullReapply = _pendingModReapply;
|
|
||||||
_needsCollectionRebuild = false;
|
_needsCollectionRebuild = false;
|
||||||
if (LastAppliedApproximateVRAMBytes < 0 || LastAppliedApproximateEffectiveVRAMBytes < 0)
|
if (LastAppliedApproximateVRAMBytes < 0 || LastAppliedApproximateEffectiveVRAMBytes < 0)
|
||||||
{
|
{
|
||||||
@@ -1727,15 +1520,8 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
|
|
||||||
private void FrameworkUpdate()
|
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);
|
var pc = _dalamudUtil.FindPlayerByNameHash(Ident);
|
||||||
if (pc == default((string, nint))) return;
|
if (pc == default((string, nint))) return;
|
||||||
Logger.LogDebug("One-Time Initializing {handler}", GetLogIdentifier());
|
Logger.LogDebug("One-Time Initializing {handler}", GetLogIdentifier());
|
||||||
@@ -1745,11 +1531,6 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
$"Initializing User For Character {pc.Name}")));
|
$"Initializing User For Character {pc.Name}")));
|
||||||
}
|
}
|
||||||
|
|
||||||
TryHandleVisibilityUpdate();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void TryHandleVisibilityUpdate()
|
|
||||||
{
|
|
||||||
if (_charaHandler?.Address != nint.Zero && !IsVisible && !_pauseRequested)
|
if (_charaHandler?.Address != nint.Zero && !IsVisible && !_pauseRequested)
|
||||||
{
|
{
|
||||||
Guid appData = Guid.NewGuid();
|
Guid appData = Guid.NewGuid();
|
||||||
@@ -1796,24 +1577,16 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
}
|
}
|
||||||
else if (_charaHandler?.Address == nint.Zero && IsVisible)
|
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();
|
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)
|
private void Initialize(string name)
|
||||||
{
|
{
|
||||||
PlayerName = name;
|
PlayerName = name;
|
||||||
@@ -2140,164 +1913,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|||||||
}
|
}
|
||||||
|
|
||||||
_dataReceivedInDowntime = null;
|
_dataReceivedInDowntime = null;
|
||||||
_ = Task.Run(() =>
|
ApplyCharacterData(pending.ApplicationId,
|
||||||
{
|
pending.CharacterData, pending.Forced);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ using LightlessSync.FileCache;
|
|||||||
using LightlessSync.Interop.Ipc;
|
using LightlessSync.Interop.Ipc;
|
||||||
using LightlessSync.PlayerData.Factories;
|
using LightlessSync.PlayerData.Factories;
|
||||||
using LightlessSync.Services;
|
using LightlessSync.Services;
|
||||||
using LightlessSync.Services.ActorTracking;
|
|
||||||
using LightlessSync.Services.Mediator;
|
using LightlessSync.Services.Mediator;
|
||||||
using LightlessSync.Services.PairProcessing;
|
using LightlessSync.Services.PairProcessing;
|
||||||
using LightlessSync.Services.ServerConfiguration;
|
using LightlessSync.Services.ServerConfiguration;
|
||||||
@@ -72,7 +71,6 @@ internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory
|
|||||||
{
|
{
|
||||||
var downloadManager = _fileDownloadManagerFactory.Create();
|
var downloadManager = _fileDownloadManagerFactory.Create();
|
||||||
var dalamudUtilService = _serviceProvider.GetRequiredService<DalamudUtilService>();
|
var dalamudUtilService = _serviceProvider.GetRequiredService<DalamudUtilService>();
|
||||||
var actorObjectService = _serviceProvider.GetRequiredService<ActorObjectService>();
|
|
||||||
return new PairHandlerAdapter(
|
return new PairHandlerAdapter(
|
||||||
_loggerFactory.CreateLogger<PairHandlerAdapter>(),
|
_loggerFactory.CreateLogger<PairHandlerAdapter>(),
|
||||||
_mediator,
|
_mediator,
|
||||||
@@ -83,7 +81,6 @@ internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory
|
|||||||
downloadManager,
|
downloadManager,
|
||||||
_pluginWarningNotificationManager,
|
_pluginWarningNotificationManager,
|
||||||
dalamudUtilService,
|
dalamudUtilService,
|
||||||
actorObjectService,
|
|
||||||
_lifetime,
|
_lifetime,
|
||||||
_fileCacheManager,
|
_fileCacheManager,
|
||||||
_playerPerformanceService,
|
_playerPerformanceService,
|
||||||
|
|||||||
@@ -11,9 +11,7 @@ public sealed class PairHandlerRegistry : IDisposable
|
|||||||
{
|
{
|
||||||
private readonly object _gate = new();
|
private readonly object _gate = new();
|
||||||
private readonly object _pendingGate = new();
|
private readonly object _pendingGate = new();
|
||||||
private readonly object _visibilityGate = new();
|
|
||||||
private readonly Dictionary<string, PairHandlerEntry> _entriesByIdent = new(StringComparer.Ordinal);
|
private readonly Dictionary<string, PairHandlerEntry> _entriesByIdent = new(StringComparer.Ordinal);
|
||||||
private readonly Dictionary<string, CancellationTokenSource> _pendingInvisibleEvictions = new(StringComparer.Ordinal);
|
|
||||||
private readonly Dictionary<IPairHandlerAdapter, PairHandlerEntry> _entriesByHandler = new(ReferenceEqualityComparer.Instance);
|
private readonly Dictionary<IPairHandlerAdapter, PairHandlerEntry> _entriesByHandler = new(ReferenceEqualityComparer.Instance);
|
||||||
|
|
||||||
private readonly IPairHandlerAdapterFactory _handlerFactory;
|
private readonly IPairHandlerAdapterFactory _handlerFactory;
|
||||||
@@ -146,37 +144,6 @@ public sealed class PairHandlerRegistry : IDisposable
|
|||||||
return PairOperationResult<PairUniqueIdentifier>.Ok(registration.PairIdent);
|
return PairOperationResult<PairUniqueIdentifier>.Ok(registration.PairIdent);
|
||||||
}
|
}
|
||||||
|
|
||||||
private PairOperationResult CancelAllInvisibleEvictions()
|
|
||||||
{
|
|
||||||
List<CancellationTokenSource> snapshot;
|
|
||||||
lock (_visibilityGate)
|
|
||||||
{
|
|
||||||
snapshot = [.. _pendingInvisibleEvictions.Values];
|
|
||||||
_pendingInvisibleEvictions.Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
List<string>? errors = null;
|
|
||||||
|
|
||||||
foreach (var cts in snapshot)
|
|
||||||
{
|
|
||||||
try { cts.Cancel(); }
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
(errors ??= new List<string>()).Add($"Cancel: {ex.Message}");
|
|
||||||
}
|
|
||||||
|
|
||||||
try { cts.Dispose(); }
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
(errors ??= new List<string>()).Add($"Dispose: {ex.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors is null
|
|
||||||
? PairOperationResult.Ok()
|
|
||||||
: PairOperationResult.Fail($"CancelAllInvisibleEvictions had error(s): {string.Join(" | ", errors)}");
|
|
||||||
}
|
|
||||||
|
|
||||||
public PairOperationResult ApplyCharacterData(PairRegistration registration, OnlineUserCharaDataDto dto)
|
public PairOperationResult ApplyCharacterData(PairRegistration registration, OnlineUserCharaDataDto dto)
|
||||||
{
|
{
|
||||||
if (registration.CharacterIdent is null)
|
if (registration.CharacterIdent is null)
|
||||||
@@ -333,7 +300,6 @@ public sealed class PairHandlerRegistry : IDisposable
|
|||||||
lock (_gate)
|
lock (_gate)
|
||||||
{
|
{
|
||||||
handlers = _entriesByHandler.Keys.ToList();
|
handlers = _entriesByHandler.Keys.ToList();
|
||||||
CancelAllInvisibleEvictions();
|
|
||||||
_entriesByIdent.Clear();
|
_entriesByIdent.Clear();
|
||||||
_entriesByHandler.Clear();
|
_entriesByHandler.Clear();
|
||||||
}
|
}
|
||||||
@@ -366,7 +332,6 @@ public sealed class PairHandlerRegistry : IDisposable
|
|||||||
lock (_gate)
|
lock (_gate)
|
||||||
{
|
{
|
||||||
handlers = _entriesByHandler.Keys.ToList();
|
handlers = _entriesByHandler.Keys.ToList();
|
||||||
CancelAllInvisibleEvictions();
|
|
||||||
_entriesByIdent.Clear();
|
_entriesByIdent.Clear();
|
||||||
_entriesByHandler.Clear();
|
_entriesByHandler.Clear();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
using Dalamud.Game;
|
using Dalamud.Game;
|
||||||
using Dalamud.Game.ClientState.Objects;
|
using Dalamud.Game.ClientState.Objects;
|
||||||
using Dalamud.Interface;
|
|
||||||
using Dalamud.Interface.ImGuiFileDialog;
|
using Dalamud.Interface.ImGuiFileDialog;
|
||||||
using Dalamud.Interface.Windowing;
|
using Dalamud.Interface.Windowing;
|
||||||
using Dalamud.Plugin;
|
using Dalamud.Plugin;
|
||||||
@@ -106,7 +105,6 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
services.AddSingleton(new Dalamud.Localization("LightlessSync.Localization.", string.Empty, useEmbedded: true));
|
services.AddSingleton(new Dalamud.Localization("LightlessSync.Localization.", string.Empty, useEmbedded: true));
|
||||||
services.AddSingleton(gameGui);
|
services.AddSingleton(gameGui);
|
||||||
services.AddSingleton(addonLifecycle);
|
services.AddSingleton(addonLifecycle);
|
||||||
services.AddSingleton<IUiBuilder>(pluginInterface.UiBuilder);
|
|
||||||
|
|
||||||
// Core singletons
|
// Core singletons
|
||||||
services.AddSingleton<LightlessMediator>();
|
services.AddSingleton<LightlessMediator>();
|
||||||
@@ -201,7 +199,6 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
gameInteropProvider,
|
gameInteropProvider,
|
||||||
objectTable,
|
objectTable,
|
||||||
clientState,
|
clientState,
|
||||||
condition,
|
|
||||||
sp.GetRequiredService<LightlessMediator>()));
|
sp.GetRequiredService<LightlessMediator>()));
|
||||||
|
|
||||||
services.AddSingleton(sp => new DalamudUtilService(
|
services.AddSingleton(sp => new DalamudUtilService(
|
||||||
@@ -268,7 +265,6 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
sp.GetRequiredService<ILogger<LightFinderPlateHandler>>(),
|
sp.GetRequiredService<ILogger<LightFinderPlateHandler>>(),
|
||||||
addonLifecycle,
|
addonLifecycle,
|
||||||
gameGui,
|
gameGui,
|
||||||
clientState,
|
|
||||||
sp.GetRequiredService<LightlessConfigService>(),
|
sp.GetRequiredService<LightlessConfigService>(),
|
||||||
sp.GetRequiredService<LightlessMediator>(),
|
sp.GetRequiredService<LightlessMediator>(),
|
||||||
objectTable,
|
objectTable,
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using Dalamud.Game.ClientState.Conditions;
|
|
||||||
using Dalamud.Game.ClientState.Objects.SubKinds;
|
using Dalamud.Game.ClientState.Objects.SubKinds;
|
||||||
using Dalamud.Hooking;
|
using Dalamud.Hooking;
|
||||||
using Dalamud.Plugin.Services;
|
using Dalamud.Plugin.Services;
|
||||||
@@ -32,18 +31,13 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
|||||||
private readonly IFramework _framework;
|
private readonly IFramework _framework;
|
||||||
private readonly IGameInteropProvider _interop;
|
private readonly IGameInteropProvider _interop;
|
||||||
private readonly IObjectTable _objectTable;
|
private readonly IObjectTable _objectTable;
|
||||||
private readonly IClientState _clientState;
|
|
||||||
private readonly ICondition _condition;
|
|
||||||
private readonly LightlessMediator _mediator;
|
private readonly LightlessMediator _mediator;
|
||||||
|
|
||||||
private readonly ConcurrentDictionary<nint, ActorDescriptor> _activePlayers = new();
|
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, ActorDescriptor> _actorsByHash = new(StringComparer.Ordinal);
|
||||||
private readonly ConcurrentDictionary<string, ConcurrentDictionary<nint, ActorDescriptor>> _actorsByName = 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 readonly OwnedObjectTracker _ownedTracker = new();
|
||||||
private ActorSnapshot _snapshot = ActorSnapshot.Empty;
|
private ActorSnapshot _snapshot = ActorSnapshot.Empty;
|
||||||
private GposeSnapshot _gposeSnapshot = GposeSnapshot.Empty;
|
|
||||||
|
|
||||||
private Hook<Character.Delegates.OnInitialize>? _onInitializeHook;
|
private Hook<Character.Delegates.OnInitialize>? _onInitializeHook;
|
||||||
private Hook<Character.Delegates.Terminate>? _onTerminateHook;
|
private Hook<Character.Delegates.Terminate>? _onTerminateHook;
|
||||||
@@ -61,29 +55,21 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
|||||||
IGameInteropProvider interop,
|
IGameInteropProvider interop,
|
||||||
IObjectTable objectTable,
|
IObjectTable objectTable,
|
||||||
IClientState clientState,
|
IClientState clientState,
|
||||||
ICondition condition,
|
|
||||||
LightlessMediator mediator)
|
LightlessMediator mediator)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_framework = framework;
|
_framework = framework;
|
||||||
_interop = interop;
|
_interop = interop;
|
||||||
_objectTable = objectTable;
|
_objectTable = objectTable;
|
||||||
_clientState = clientState;
|
|
||||||
_condition = condition;
|
|
||||||
_mediator = mediator;
|
_mediator = mediator;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool IsZoning => _condition[ConditionFlag.BetweenAreas] || _condition[ConditionFlag.BetweenAreas51];
|
|
||||||
|
|
||||||
private ActorSnapshot Snapshot => Volatile.Read(ref _snapshot);
|
private ActorSnapshot Snapshot => Volatile.Read(ref _snapshot);
|
||||||
private GposeSnapshot CurrentGposeSnapshot => Volatile.Read(ref _gposeSnapshot);
|
|
||||||
|
|
||||||
public IReadOnlyList<nint> PlayerAddresses => Snapshot.PlayerAddresses;
|
public IReadOnlyList<nint> PlayerAddresses => Snapshot.PlayerAddresses;
|
||||||
|
|
||||||
public IEnumerable<ActorDescriptor> ObjectDescriptors => _activePlayers.Values;
|
public IEnumerable<ActorDescriptor> PlayerDescriptors => _activePlayers.Values;
|
||||||
public IReadOnlyList<ActorDescriptor> PlayerDescriptors => Snapshot.PlayerDescriptors;
|
public IReadOnlyList<ActorDescriptor> PlayerCharacterDescriptors => Snapshot.PlayerDescriptors;
|
||||||
public IReadOnlyList<ActorDescriptor> OwnedDescriptors => Snapshot.OwnedDescriptors;
|
|
||||||
public IReadOnlyList<ActorDescriptor> GposeDescriptors => CurrentGposeSnapshot.GposeDescriptors;
|
|
||||||
|
|
||||||
public bool TryGetActorByHash(string hash, out ActorDescriptor descriptor) => _actorsByHash.TryGetValue(hash, out descriptor);
|
public bool TryGetActorByHash(string hash, out ActorDescriptor descriptor) => _actorsByHash.TryGetValue(hash, out descriptor);
|
||||||
public bool TryGetValidatedActorByHash(string hash, out ActorDescriptor descriptor)
|
public bool TryGetValidatedActorByHash(string hash, out ActorDescriptor descriptor)
|
||||||
@@ -127,7 +113,6 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
public bool HooksActive => _hooksActive;
|
public bool HooksActive => _hooksActive;
|
||||||
public bool HasPendingHashResolutions => !_pendingHashResolutions.IsEmpty;
|
|
||||||
public IReadOnlyList<nint> RenderedPlayerAddresses => Snapshot.OwnedObjects.RenderedPlayers;
|
public IReadOnlyList<nint> RenderedPlayerAddresses => Snapshot.OwnedObjects.RenderedPlayers;
|
||||||
public IReadOnlyList<nint> RenderedCompanionAddresses => Snapshot.OwnedObjects.RenderedCompanions;
|
public IReadOnlyList<nint> RenderedCompanionAddresses => Snapshot.OwnedObjects.RenderedCompanions;
|
||||||
public IReadOnlyList<nint> OwnedObjectAddresses => Snapshot.OwnedObjects.OwnedAddresses;
|
public IReadOnlyList<nint> OwnedObjectAddresses => Snapshot.OwnedObjects.OwnedAddresses;
|
||||||
@@ -222,7 +207,7 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
|||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
var isLoaded = await _framework.RunOnFrameworkThread(() => IsObjectFullyLoaded(address)).ConfigureAwait(false);
|
var isLoaded = await _framework.RunOnFrameworkThread(() => IsObjectFullyLoaded(address)).ConfigureAwait(false);
|
||||||
if (!IsZoning && isLoaded)
|
if (isLoaded)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
await Task.Delay(100, cancellationToken).ConfigureAwait(false);
|
await Task.Delay(100, cancellationToken).ConfigureAwait(false);
|
||||||
@@ -312,13 +297,10 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
|||||||
{
|
{
|
||||||
DisposeHooks();
|
DisposeHooks();
|
||||||
_activePlayers.Clear();
|
_activePlayers.Clear();
|
||||||
_gposePlayers.Clear();
|
|
||||||
_actorsByHash.Clear();
|
_actorsByHash.Clear();
|
||||||
_actorsByName.Clear();
|
_actorsByName.Clear();
|
||||||
_pendingHashResolutions.Clear();
|
|
||||||
_ownedTracker.Reset();
|
_ownedTracker.Reset();
|
||||||
Volatile.Write(ref _snapshot, ActorSnapshot.Empty);
|
Volatile.Write(ref _snapshot, ActorSnapshot.Empty);
|
||||||
Volatile.Write(ref _gposeSnapshot, GposeSnapshot.Empty);
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -354,7 +336,7 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
|||||||
_onCompanionTerminateHook.Enable();
|
_onCompanionTerminateHook.Enable();
|
||||||
|
|
||||||
_hooksActive = true;
|
_hooksActive = true;
|
||||||
_logger.LogTrace("ActorObjectService hooks enabled.");
|
_logger.LogDebug("ActorObjectService hooks enabled.");
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task WarmupExistingActors()
|
private Task WarmupExistingActors()
|
||||||
@@ -368,21 +350,36 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
|||||||
|
|
||||||
private unsafe void OnCharacterInitialized(Character* chara)
|
private unsafe void OnCharacterInitialized(Character* chara)
|
||||||
{
|
{
|
||||||
ExecuteOriginal(() => _onInitializeHook!.Original(chara), "Error invoking original character initialize.");
|
try
|
||||||
QueueTrack((GameObject*)chara);
|
{
|
||||||
|
_onInitializeHook!.Original(chara);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error invoking original character initialize.");
|
||||||
|
}
|
||||||
|
|
||||||
|
QueueFrameworkUpdate(() => TrackGameObject((GameObject*)chara));
|
||||||
}
|
}
|
||||||
|
|
||||||
private unsafe void OnCharacterTerminated(Character* chara)
|
private unsafe void OnCharacterTerminated(Character* chara)
|
||||||
{
|
{
|
||||||
var address = (nint)chara;
|
var address = (nint)chara;
|
||||||
QueueUntrack(address);
|
QueueFrameworkUpdate(() => UntrackGameObject(address));
|
||||||
ExecuteOriginal(() => _onTerminateHook!.Original(chara), "Error invoking original character terminate.");
|
try
|
||||||
|
{
|
||||||
|
_onTerminateHook!.Original(chara);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error invoking original character terminate.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private unsafe GameObject* OnCharacterDisposed(Character* chara, byte freeMemory)
|
private unsafe GameObject* OnCharacterDisposed(Character* chara, byte freeMemory)
|
||||||
{
|
{
|
||||||
var address = (nint)chara;
|
var address = (nint)chara;
|
||||||
QueueUntrack(address);
|
QueueFrameworkUpdate(() => UntrackGameObject(address));
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return _onDestructorHook!.Original(chara, freeMemory);
|
return _onDestructorHook!.Original(chara, freeMemory);
|
||||||
@@ -419,7 +416,7 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
|||||||
|
|
||||||
if (_logger.IsEnabled(LogLevel.Debug))
|
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.Name,
|
||||||
descriptor.Address,
|
descriptor.Address,
|
||||||
descriptor.ObjectIndex,
|
descriptor.ObjectIndex,
|
||||||
@@ -537,7 +534,7 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
|||||||
RemoveDescriptor(descriptor);
|
RemoveDescriptor(descriptor);
|
||||||
if (_logger.IsEnabled(LogLevel.Debug))
|
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.Name,
|
||||||
descriptor.Address,
|
descriptor.Address,
|
||||||
descriptor.ObjectIndex,
|
descriptor.ObjectIndex,
|
||||||
@@ -561,14 +558,10 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
|||||||
if (!seen.Add(address))
|
if (!seen.Add(address))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
var gameObject = (GameObject*)address;
|
if (_activePlayers.ContainsKey(address))
|
||||||
if (_activePlayers.TryGetValue(address, out var existing))
|
|
||||||
{
|
|
||||||
RefreshDescriptorIfNeeded(existing, gameObject);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
|
||||||
|
|
||||||
TrackGameObject(gameObject);
|
TrackGameObject((GameObject*)address);
|
||||||
}
|
}
|
||||||
|
|
||||||
var stale = _activePlayers.Keys.Where(addr => !seen.Contains(addr)).ToList();
|
var stale = _activePlayers.Keys.Where(addr => !seen.Contains(addr)).ToList();
|
||||||
@@ -581,50 +574,6 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
|||||||
{
|
{
|
||||||
_nextRefreshAllowed = DateTime.UtcNow + SnapshotRefreshInterval;
|
_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)
|
private void IndexDescriptor(ActorDescriptor descriptor)
|
||||||
@@ -656,15 +605,30 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
|||||||
|
|
||||||
private unsafe void OnCompanionInitialized(Companion* companion)
|
private unsafe void OnCompanionInitialized(Companion* companion)
|
||||||
{
|
{
|
||||||
ExecuteOriginal(() => _onCompanionInitializeHook!.Original(companion), "Error invoking original companion initialize.");
|
try
|
||||||
QueueTrack((GameObject*)companion);
|
{
|
||||||
|
_onCompanionInitializeHook!.Original(companion);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error invoking original companion initialize.");
|
||||||
|
}
|
||||||
|
|
||||||
|
QueueFrameworkUpdate(() => TrackGameObject((GameObject*)companion));
|
||||||
}
|
}
|
||||||
|
|
||||||
private unsafe void OnCompanionTerminated(Companion* companion)
|
private unsafe void OnCompanionTerminated(Companion* companion)
|
||||||
{
|
{
|
||||||
var address = (nint)companion;
|
var address = (nint)companion;
|
||||||
QueueUntrack(address);
|
QueueFrameworkUpdate(() => UntrackGameObject(address));
|
||||||
ExecuteOriginal(() => _onCompanionTerminateHook!.Original(companion), "Error invoking original companion terminate.");
|
try
|
||||||
|
{
|
||||||
|
_onCompanionTerminateHook!.Original(companion);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error invoking original companion terminate.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RemoveDescriptorFromIndexes(ActorDescriptor descriptor)
|
private void RemoveDescriptorFromIndexes(ActorDescriptor descriptor)
|
||||||
@@ -691,7 +655,6 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
|||||||
_activePlayers[descriptor.Address] = descriptor;
|
_activePlayers[descriptor.Address] = descriptor;
|
||||||
IndexDescriptor(descriptor);
|
IndexDescriptor(descriptor);
|
||||||
_ownedTracker.OnDescriptorAdded(descriptor);
|
_ownedTracker.OnDescriptorAdded(descriptor);
|
||||||
UpdatePendingHashResolutions(descriptor);
|
|
||||||
PublishSnapshot();
|
PublishSnapshot();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -699,42 +662,21 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
|||||||
{
|
{
|
||||||
RemoveDescriptorFromIndexes(descriptor);
|
RemoveDescriptorFromIndexes(descriptor);
|
||||||
_ownedTracker.OnDescriptorRemoved(descriptor);
|
_ownedTracker.OnDescriptorRemoved(descriptor);
|
||||||
_pendingHashResolutions.TryRemove(descriptor.Address, out _);
|
|
||||||
PublishSnapshot();
|
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()
|
private void PublishSnapshot()
|
||||||
{
|
{
|
||||||
var playerDescriptors = _activePlayers.Values
|
var playerDescriptors = _activePlayers.Values
|
||||||
.Where(descriptor => descriptor.ObjectKind == DalamudObjectKind.Player)
|
.Where(descriptor => descriptor.ObjectKind == DalamudObjectKind.Player)
|
||||||
.ToArray();
|
.ToArray();
|
||||||
var ownedDescriptors = _activePlayers.Values
|
|
||||||
.Where(descriptor => descriptor.OwnedKind is not null)
|
|
||||||
.ToArray();
|
|
||||||
var playerAddresses = new nint[playerDescriptors.Length];
|
var playerAddresses = new nint[playerDescriptors.Length];
|
||||||
for (var i = 0; i < playerDescriptors.Length; i++)
|
for (var i = 0; i < playerDescriptors.Length; i++)
|
||||||
playerAddresses[i] = playerDescriptors[i].Address;
|
playerAddresses[i] = playerDescriptors[i].Address;
|
||||||
|
|
||||||
var ownedSnapshot = _ownedTracker.CreateSnapshot();
|
var ownedSnapshot = _ownedTracker.CreateSnapshot();
|
||||||
var nextGeneration = Snapshot.Generation + 1;
|
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);
|
Volatile.Write(ref _snapshot, snapshot);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -752,24 +694,6 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
|||||||
_ = _framework.RunOnFrameworkThread(action);
|
_ = _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()
|
private void DisposeHooks()
|
||||||
{
|
{
|
||||||
var hadHooks = _hooksActive
|
var hadHooks = _hooksActive
|
||||||
@@ -801,7 +725,7 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
|||||||
|
|
||||||
if (hadHooks)
|
if (hadHooks)
|
||||||
{
|
{
|
||||||
_logger.LogTrace("ActorObjectService hooks disabled.");
|
_logger.LogDebug("ActorObjectService hooks disabled.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -846,89 +770,6 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
|||||||
return results;
|
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)
|
private static unsafe bool IsObjectFullyLoaded(nint address)
|
||||||
{
|
{
|
||||||
if (address == nint.Zero)
|
if (address == nint.Zero)
|
||||||
@@ -942,10 +783,13 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
|||||||
if (drawObject == null)
|
if (drawObject == null)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if ((ulong)gameObject->RenderFlags == 2048)
|
if ((gameObject->RenderFlags & VisibilityFlags.Nameplate) != VisibilityFlags.None)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
var characterBase = (CharacterBase*)drawObject;
|
var characterBase = (CharacterBase*)drawObject;
|
||||||
|
if (characterBase == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
if (characterBase->HasModelInSlotLoaded != 0)
|
if (characterBase->HasModelInSlotLoaded != 0)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
@@ -1081,27 +925,14 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
|||||||
|
|
||||||
private sealed record ActorSnapshot(
|
private sealed record ActorSnapshot(
|
||||||
IReadOnlyList<ActorDescriptor> PlayerDescriptors,
|
IReadOnlyList<ActorDescriptor> PlayerDescriptors,
|
||||||
IReadOnlyList<ActorDescriptor> OwnedDescriptors,
|
|
||||||
IReadOnlyList<nint> PlayerAddresses,
|
IReadOnlyList<nint> PlayerAddresses,
|
||||||
OwnedObjectSnapshot OwnedObjects,
|
OwnedObjectSnapshot OwnedObjects,
|
||||||
int Generation)
|
int Generation)
|
||||||
{
|
{
|
||||||
public static ActorSnapshot Empty { get; } = new(
|
public static ActorSnapshot Empty { get; } = new(
|
||||||
Array.Empty<ActorDescriptor>(),
|
|
||||||
Array.Empty<ActorDescriptor>(),
|
Array.Empty<ActorDescriptor>(),
|
||||||
Array.Empty<nint>(),
|
Array.Empty<nint>(),
|
||||||
OwnedObjectSnapshot.Empty,
|
OwnedObjectSnapshot.Empty,
|
||||||
0);
|
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.Dto.Chat;
|
||||||
using LightlessSync.API.Data.Extensions;
|
|
||||||
using LightlessSync.Services.ActorTracking;
|
using LightlessSync.Services.ActorTracking;
|
||||||
using LightlessSync.Services.Mediator;
|
using LightlessSync.Services.Mediator;
|
||||||
using LightlessSync.WebAPI;
|
using LightlessSync.WebAPI;
|
||||||
@@ -24,7 +23,6 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
|||||||
private readonly DalamudUtilService _dalamudUtilService;
|
private readonly DalamudUtilService _dalamudUtilService;
|
||||||
private readonly ActorObjectService _actorObjectService;
|
private readonly ActorObjectService _actorObjectService;
|
||||||
private readonly PairUiService _pairUiService;
|
private readonly PairUiService _pairUiService;
|
||||||
private readonly ChatConfigService _chatConfigService;
|
|
||||||
|
|
||||||
private readonly Lock _sync = new();
|
private readonly Lock _sync = new();
|
||||||
|
|
||||||
@@ -37,8 +35,6 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
|||||||
private readonly Dictionary<string, bool> _lastPresenceStates = new(StringComparer.Ordinal);
|
private readonly Dictionary<string, bool> _lastPresenceStates = new(StringComparer.Ordinal);
|
||||||
private readonly Dictionary<string, string> _selfTokens = new(StringComparer.Ordinal);
|
private readonly Dictionary<string, string> _selfTokens = new(StringComparer.Ordinal);
|
||||||
private readonly List<PendingSelfMessage> _pendingSelfMessages = new();
|
private readonly List<PendingSelfMessage> _pendingSelfMessages = new();
|
||||||
private List<ChatChannelSnapshot>? _cachedChannelSnapshots;
|
|
||||||
private bool _channelsSnapshotDirty = true;
|
|
||||||
|
|
||||||
private bool _isLoggedIn;
|
private bool _isLoggedIn;
|
||||||
private bool _isConnected;
|
private bool _isConnected;
|
||||||
@@ -61,7 +57,6 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
|||||||
_dalamudUtilService = dalamudUtilService;
|
_dalamudUtilService = dalamudUtilService;
|
||||||
_actorObjectService = actorObjectService;
|
_actorObjectService = actorObjectService;
|
||||||
_pairUiService = pairUiService;
|
_pairUiService = pairUiService;
|
||||||
_chatConfigService = chatConfigService;
|
|
||||||
|
|
||||||
_isLoggedIn = _dalamudUtilService.IsLoggedIn;
|
_isLoggedIn = _dalamudUtilService.IsLoggedIn;
|
||||||
_isConnected = _apiController.IsConnected;
|
_isConnected = _apiController.IsConnected;
|
||||||
@@ -72,11 +67,6 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
|||||||
{
|
{
|
||||||
using (_sync.EnterScope())
|
using (_sync.EnterScope())
|
||||||
{
|
{
|
||||||
if (!_channelsSnapshotDirty && _cachedChannelSnapshots is not null)
|
|
||||||
{
|
|
||||||
return _cachedChannelSnapshots;
|
|
||||||
}
|
|
||||||
|
|
||||||
var snapshots = new List<ChatChannelSnapshot>(_channelOrder.Count);
|
var snapshots = new List<ChatChannelSnapshot>(_channelOrder.Count);
|
||||||
foreach (var key in _channelOrder)
|
foreach (var key in _channelOrder)
|
||||||
{
|
{
|
||||||
@@ -106,8 +96,6 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
|||||||
state.Messages.ToList()));
|
state.Messages.ToList()));
|
||||||
}
|
}
|
||||||
|
|
||||||
_cachedChannelSnapshots = snapshots;
|
|
||||||
_channelsSnapshotDirty = false;
|
|
||||||
return snapshots;
|
return snapshots;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -145,44 +133,6 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
|||||||
state.UnreadCount = 0;
|
state.UnreadCount = 0;
|
||||||
_lastReadCounts[key] = state.Messages.Count;
|
_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)
|
if (!wasEnabled)
|
||||||
{
|
{
|
||||||
_chatEnabled = true;
|
_chatEnabled = true;
|
||||||
MarkChannelsSnapshotDirtyLocked();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -244,8 +193,6 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
|||||||
state.IsAvailable = false;
|
state.IsAvailable = false;
|
||||||
state.StatusText = "Chat services disabled";
|
state.StatusText = "Chat services disabled";
|
||||||
}
|
}
|
||||||
|
|
||||||
MarkChannelsSnapshotDirtyLocked();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
UnregisterChatHandler();
|
UnregisterChatHandler();
|
||||||
@@ -565,7 +512,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
|||||||
|
|
||||||
if (!_isLoggedIn || !_apiController.IsConnected)
|
if (!_isLoggedIn || !_apiController.IsConnected)
|
||||||
{
|
{
|
||||||
await LeaveCurrentZoneAsync(force, 0, 0).ConfigureAwait(false);
|
await LeaveCurrentZoneAsync(force, 0).ConfigureAwait(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -573,7 +520,6 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
|||||||
{
|
{
|
||||||
var location = await _dalamudUtilService.GetMapDataAsync().ConfigureAwait(false);
|
var location = await _dalamudUtilService.GetMapDataAsync().ConfigureAwait(false);
|
||||||
var territoryId = (ushort)location.TerritoryId;
|
var territoryId = (ushort)location.TerritoryId;
|
||||||
var worldId = (ushort)location.ServerId;
|
|
||||||
|
|
||||||
string? zoneKey;
|
string? zoneKey;
|
||||||
ZoneChannelDefinition? definition = null;
|
ZoneChannelDefinition? definition = null;
|
||||||
@@ -590,14 +536,14 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
|||||||
|
|
||||||
if (definition is null)
|
if (definition is null)
|
||||||
{
|
{
|
||||||
await LeaveCurrentZoneAsync(force, territoryId, worldId).ConfigureAwait(false);
|
await LeaveCurrentZoneAsync(force, territoryId).ConfigureAwait(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var descriptor = await BuildZoneDescriptorAsync(definition.Value).ConfigureAwait(false);
|
var descriptor = await BuildZoneDescriptorAsync(definition.Value).ConfigureAwait(false);
|
||||||
if (descriptor is null)
|
if (descriptor is null)
|
||||||
{
|
{
|
||||||
await LeaveCurrentZoneAsync(force, territoryId, worldId).ConfigureAwait(false);
|
await LeaveCurrentZoneAsync(force, territoryId).ConfigureAwait(false);
|
||||||
return;
|
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;
|
ChatChannelDescriptor? descriptor = null;
|
||||||
|
|
||||||
@@ -656,29 +602,9 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
|||||||
state.StatusText = !_chatEnabled
|
state.StatusText = !_chatEnabled
|
||||||
? "Chat services disabled"
|
? "Chat services disabled"
|
||||||
: (_isConnected ? ZoneUnavailableMessage : "Disconnected from chat server");
|
: (_isConnected ? ZoneUnavailableMessage : "Disconnected from chat server");
|
||||||
if (territoryId != 0
|
|
||||||
&& _dalamudUtilService.TerritoryData.Value.TryGetValue(territoryId, out var territoryName)
|
|
||||||
&& !string.IsNullOrWhiteSpace(territoryName))
|
|
||||||
{
|
|
||||||
state.DisplayName = territoryName;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
state.DisplayName = "Zone Chat";
|
state.DisplayName = "Zone Chat";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (worldId != 0)
|
|
||||||
{
|
|
||||||
state.Descriptor = new ChatChannelDescriptor
|
|
||||||
{
|
|
||||||
Type = ChatChannelType.Zone,
|
|
||||||
WorldId = worldId,
|
|
||||||
ZoneId = territoryId,
|
|
||||||
CustomKey = string.Empty
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.Equals(_activeChannelKey, ZoneChannelKey, StringComparison.Ordinal))
|
if (string.Equals(_activeChannelKey, ZoneChannelKey, StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
_activeChannelKey = _channelOrder.FirstOrDefault(key => !string.Equals(key, ZoneChannelKey, StringComparison.Ordinal));
|
_activeChannelKey = _channelOrder.FirstOrDefault(key => !string.Equals(key, ZoneChannelKey, StringComparison.Ordinal));
|
||||||
@@ -732,7 +658,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
|||||||
_zoneDefinitions[key] = new ZoneChannelDefinition(key, info.DisplayName ?? key, descriptor, territories);
|
_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 kvp in territoryData)
|
||||||
{
|
{
|
||||||
foreach (var variant in EnumerateTerritoryKeys(kvp.Value))
|
foreach (var variant in EnumerateTerritoryKeys(kvp.Value))
|
||||||
@@ -868,12 +794,6 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
|||||||
var infos = new List<GroupChatChannelInfoDto>(groups.Count);
|
var infos = new List<GroupChatChannelInfoDto>(groups.Count);
|
||||||
foreach (var group in groups)
|
foreach (var group in groups)
|
||||||
{
|
{
|
||||||
// basically prune the channel if it's disabled
|
|
||||||
if (group.GroupPermissions.IsDisableChat())
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var descriptor = new ChatChannelDescriptor
|
var descriptor = new ChatChannelDescriptor
|
||||||
{
|
{
|
||||||
Type = ChatChannelType.Group,
|
Type = ChatChannelType.Group,
|
||||||
@@ -1044,8 +964,6 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
|||||||
state.UnreadCount = Math.Min(Math.Max(unreadFromHistory, incrementalUnread), MaxUnreadCount);
|
state.UnreadCount = Math.Min(Math.Max(unreadFromHistory, incrementalUnread), MaxUnreadCount);
|
||||||
state.HasUnread = state.UnreadCount > 0;
|
state.HasUnread = state.UnreadCount > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
MarkChannelsSnapshotDirtyLocked();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Mediator.Publish(new ChatChannelMessageAdded(key, message));
|
Mediator.Publish(new ChatChannelMessageAdded(key, message));
|
||||||
@@ -1174,38 +1092,6 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
|||||||
{
|
{
|
||||||
_channelOrder.Clear();
|
_channelOrder.Clear();
|
||||||
|
|
||||||
var configuredOrder = _chatConfigService.Current.ChannelOrder;
|
|
||||||
if (configuredOrder.Count > 0)
|
|
||||||
{
|
|
||||||
var seen = new HashSet<string>(StringComparer.Ordinal);
|
|
||||||
foreach (var key in configuredOrder)
|
|
||||||
{
|
|
||||||
if (_channels.ContainsKey(key) && seen.Add(key))
|
|
||||||
{
|
|
||||||
_channelOrder.Add(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var remaining = _channels.Values
|
|
||||||
.Where(state => !seen.Contains(state.Key))
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
if (remaining.Count > 0)
|
|
||||||
{
|
|
||||||
var zoneKeys = remaining
|
|
||||||
.Where(state => state.Type == ChatChannelType.Zone)
|
|
||||||
.Select(state => state.Key);
|
|
||||||
var groupKeys = remaining
|
|
||||||
.Where(state => state.Type == ChatChannelType.Group)
|
|
||||||
.OrderBy(state => state.DisplayName, StringComparer.OrdinalIgnoreCase)
|
|
||||||
.Select(state => state.Key);
|
|
||||||
|
|
||||||
_channelOrder.AddRange(zoneKeys);
|
|
||||||
_channelOrder.AddRange(groupKeys);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (_channels.ContainsKey(ZoneChannelKey))
|
if (_channels.ContainsKey(ZoneChannelKey))
|
||||||
{
|
{
|
||||||
_channelOrder.Add(ZoneChannelKey);
|
_channelOrder.Add(ZoneChannelKey);
|
||||||
@@ -1217,7 +1103,6 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
|||||||
.Select(state => state.Key);
|
.Select(state => state.Key);
|
||||||
|
|
||||||
_channelOrder.AddRange(groups);
|
_channelOrder.AddRange(groups);
|
||||||
}
|
|
||||||
|
|
||||||
if (_activeChannelKey is null && _channelOrder.Count > 0)
|
if (_activeChannelKey is null && _channelOrder.Count > 0)
|
||||||
{
|
{
|
||||||
@@ -1227,25 +1112,9 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
|||||||
{
|
{
|
||||||
_activeChannelKey = _channelOrder.Count > 0 ? _channelOrder[0] : null;
|
_activeChannelKey = _channelOrder.Count > 0 ? _channelOrder[0] : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
MarkChannelsSnapshotDirtyLocked();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void MarkChannelsSnapshotDirty()
|
private void PublishChannelListChanged() => Mediator.Publish(new ChatChannelsUpdated());
|
||||||
{
|
|
||||||
using (_sync.EnterScope())
|
|
||||||
{
|
|
||||||
_channelsSnapshotDirty = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void MarkChannelsSnapshotDirtyLocked() => _channelsSnapshotDirty = true;
|
|
||||||
|
|
||||||
private void PublishChannelListChanged()
|
|
||||||
{
|
|
||||||
MarkChannelsSnapshotDirty();
|
|
||||||
Mediator.Publish(new ChatChannelsUpdated());
|
|
||||||
}
|
|
||||||
|
|
||||||
private static IEnumerable<string> EnumerateTerritoryKeys(string? value)
|
private static IEnumerable<string> EnumerateTerritoryKeys(string? value)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ public sealed class CommandManagerService : IDisposable
|
|||||||
"\t /light analyze - Opens the Lightless Character Data Analysis window" + Environment.NewLine +
|
"\t /light analyze - Opens the Lightless Character Data Analysis window" + Environment.NewLine +
|
||||||
"\t /light settings - Opens the Lightless Settings window" + Environment.NewLine +
|
"\t /light settings - Opens the Lightless Settings window" + Environment.NewLine +
|
||||||
"\t /light finder - Opens the Lightfinder window" + Environment.NewLine +
|
"\t /light finder - Opens the Lightfinder window" + Environment.NewLine +
|
||||||
"\t /light chat - Opens the Lightless Chat window"
|
"\t /light finder - Opens the Lightless Chat window"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -129,6 +129,7 @@ internal class ContextMenuService : IHostedService
|
|||||||
|
|
||||||
var snapshot = _pairUiService.GetSnapshot();
|
var snapshot = _pairUiService.GetSnapshot();
|
||||||
var pair = snapshot.PairsByUid.Values.FirstOrDefault(p =>
|
var pair = snapshot.PairsByUid.Values.FirstOrDefault(p =>
|
||||||
|
p.IsVisible &&
|
||||||
p.PlayerCharacterId != uint.MaxValue &&
|
p.PlayerCharacterId != uint.MaxValue &&
|
||||||
p.PlayerCharacterId == target.TargetObjectId);
|
p.PlayerCharacterId == target.TargetObjectId);
|
||||||
|
|
||||||
|
|||||||
@@ -91,10 +91,43 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
return gameData.GetExcelSheet<ClassJob>(Dalamud.Game.ClientLanguage.English)!
|
return gameData.GetExcelSheet<ClassJob>(Dalamud.Game.ClientLanguage.English)!
|
||||||
.ToDictionary(k => k.RowId, k => k.NameEnglish.ToString());
|
.ToDictionary(k => k.RowId, k => k.NameEnglish.ToString());
|
||||||
});
|
});
|
||||||
var clientLanguage = _clientState.ClientLanguage;
|
TerritoryData = new(() =>
|
||||||
TerritoryData = new(() => BuildTerritoryData(clientLanguage));
|
{
|
||||||
TerritoryDataEnglish = new(() => BuildTerritoryData(Dalamud.Game.ClientLanguage.English));
|
return gameData.GetExcelSheet<TerritoryType>(Dalamud.Game.ClientLanguage.English)!
|
||||||
MapData = new(() => BuildMapData(clientLanguage));
|
.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) =>
|
mediator.Subscribe<TargetPairMessage>(this, (msg) =>
|
||||||
{
|
{
|
||||||
if (clientState.IsPvP) return;
|
if (clientState.IsPvP) return;
|
||||||
@@ -125,71 +158,6 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
private Lazy<ulong> RebuildCID() => new(GetCID);
|
private Lazy<ulong> RebuildCID() => new(GetCID);
|
||||||
|
|
||||||
public bool IsWine { get; init; }
|
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)
|
private bool ResolvePairAddress(Pair pair, out Pair resolvedPair, out nint address)
|
||||||
{
|
{
|
||||||
resolvedPair = _pairFactory.Value.Create(pair.UniqueIdent) ?? pair;
|
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 IsInCombat { get; private set; } = false;
|
||||||
public bool IsPerforming { get; private set; } = false;
|
public bool IsPerforming { get; private set; } = false;
|
||||||
public bool IsInInstance { get; private set; } = false;
|
public bool IsInInstance { get; private set; } = false;
|
||||||
public bool IsInDuty => _condition[ConditionFlag.BoundByDuty];
|
|
||||||
public bool HasModifiedGameFiles => _gameData.HasModifiedGameDataFiles;
|
public bool HasModifiedGameFiles => _gameData.HasModifiedGameDataFiles;
|
||||||
public uint ClassJobId => _classJobId!.Value;
|
public uint ClassJobId => _classJobId!.Value;
|
||||||
public Lazy<Dictionary<uint, string>> JobData { get; private set; }
|
public Lazy<Dictionary<uint, string>> JobData { get; private set; }
|
||||||
public Lazy<Dictionary<ushort, string>> WorldData { 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>> 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 Lazy<Dictionary<uint, (Map Map, string MapName)>> MapData { get; private set; }
|
||||||
public bool IsLodEnabled { get; private set; }
|
public bool IsLodEnabled { get; private set; }
|
||||||
public LightlessMediator Mediator { get; }
|
public LightlessMediator Mediator { get; }
|
||||||
|
|
||||||
public bool IsInFieldOperation
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (!IsInDuty)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var territoryId = _clientState.TerritoryType;
|
|
||||||
if (territoryId == 0)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!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)
|
public IGameObject? CreateGameObject(IntPtr reference)
|
||||||
{
|
{
|
||||||
EnsureIsOnFramework();
|
EnsureIsOnFramework();
|
||||||
@@ -360,8 +300,8 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
|
|
||||||
public IEnumerable<ICharacter?> GetGposeCharactersFromObjectTable()
|
public IEnumerable<ICharacter?> GetGposeCharactersFromObjectTable()
|
||||||
{
|
{
|
||||||
foreach (var actor in _objectTable
|
foreach (var actor in _actorObjectService.PlayerDescriptors
|
||||||
.Where(a => a.ObjectIndex > 200 && a.ObjectKind == DalamudObjectKind.Player))
|
.Where(a => a.ObjectKind == DalamudObjectKind.Player && a.ObjectIndex > 200))
|
||||||
{
|
{
|
||||||
var character = _objectTable.CreateObjectReference(actor.Address) as ICharacter;
|
var character = _objectTable.CreateObjectReference(actor.Address) as ICharacter;
|
||||||
if (character != null)
|
if (character != null)
|
||||||
@@ -388,8 +328,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
|
|
||||||
var playerAddress = playerPointer.Value;
|
var playerAddress = playerPointer.Value;
|
||||||
var ownerEntityId = ((Character*)playerAddress)->EntityId;
|
var ownerEntityId = ((Character*)playerAddress)->EntityId;
|
||||||
var candidateAddress = _objectTable.GetObjectAddress(((GameObject*)playerAddress)->ObjectIndex + 1);
|
if (ownerEntityId == 0) return IntPtr.Zero;
|
||||||
if (ownerEntityId == 0) return candidateAddress;
|
|
||||||
|
|
||||||
if (playerAddress == _actorObjectService.LocalPlayerAddress)
|
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 =>
|
var ownedObject = FindOwnedObject(ownerEntityId, playerAddress, static kind =>
|
||||||
kind == DalamudObjectKind.MountType || kind == DalamudObjectKind.Companion);
|
kind == DalamudObjectKind.MountType || kind == DalamudObjectKind.Companion);
|
||||||
if (ownedObject != nint.Zero)
|
if (ownedObject != nint.Zero)
|
||||||
@@ -418,7 +346,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
return ownedObject;
|
return ownedObject;
|
||||||
}
|
}
|
||||||
|
|
||||||
return candidateAddress;
|
return _objectTable.GetObjectAddress(((GameObject*)playerAddress)->ObjectIndex + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IntPtr> GetMinionOrMountAsync(IntPtr? playerPointer = null)
|
public async Task<IntPtr> GetMinionOrMountAsync(IntPtr? playerPointer = null)
|
||||||
@@ -535,10 +463,6 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
{
|
{
|
||||||
EnsureIsOnFramework();
|
EnsureIsOnFramework();
|
||||||
var playerChar = GetPlayerCharacter();
|
var playerChar = GetPlayerCharacter();
|
||||||
|
|
||||||
if (playerChar == null || playerChar.Address == IntPtr.Zero)
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
return ((BattleChara*)playerChar.Address)->Character.ContentId;
|
return ((BattleChara*)playerChar.Address)->Character.ContentId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -829,7 +753,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
bool isDrawingChanged = false;
|
bool isDrawingChanged = false;
|
||||||
if ((nint)drawObj != IntPtr.Zero)
|
if ((nint)drawObj != IntPtr.Zero)
|
||||||
{
|
{
|
||||||
isDrawing = gameObj->RenderFlags == (VisibilityFlags)0b100000000000;
|
isDrawing = (gameObj->RenderFlags & VisibilityFlags.Nameplate) != VisibilityFlags.None;
|
||||||
if (!isDrawing)
|
if (!isDrawing)
|
||||||
{
|
{
|
||||||
isDrawing = ((CharacterBase*)drawObj)->HasModelInSlotLoaded != 0;
|
isDrawing = ((CharacterBase*)drawObj)->HasModelInSlotLoaded != 0;
|
||||||
@@ -894,13 +818,10 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
IsAnythingDrawing = false;
|
IsAnythingDrawing = false;
|
||||||
_performanceCollector.LogPerformance(this, $"TrackedActorsToState",
|
_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++)
|
for (var i = 0; i < playerDescriptors.Count; i++)
|
||||||
{
|
{
|
||||||
var actor = playerDescriptors[i];
|
var actor = playerDescriptors[i];
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
|
|||||||
using Dalamud.Game.ClientState.Objects.Enums;
|
using Dalamud.Game.ClientState.Objects.Enums;
|
||||||
using Dalamud.Game.Text;
|
using Dalamud.Game.Text;
|
||||||
using Dalamud.Interface;
|
using Dalamud.Interface;
|
||||||
using Dalamud.Interface.Utility;
|
|
||||||
using Dalamud.Plugin;
|
using Dalamud.Plugin;
|
||||||
using Dalamud.Plugin.Services;
|
using Dalamud.Plugin.Services;
|
||||||
using FFXIVClientStructs.FFXIV.Client.System.Framework;
|
using FFXIVClientStructs.FFXIV.Client.System.Framework;
|
||||||
@@ -17,27 +16,22 @@ using LightlessSync.UI;
|
|||||||
using LightlessSync.UI.Services;
|
using LightlessSync.UI.Services;
|
||||||
using LightlessSync.Utils;
|
using LightlessSync.Utils;
|
||||||
using LightlessSync.UtilsEnum.Enum;
|
using LightlessSync.UtilsEnum.Enum;
|
||||||
using Microsoft.Extensions.Hosting;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
using Pictomancy;
|
using Pictomancy;
|
||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
using Task = System.Threading.Tasks.Task;
|
using Task = System.Threading.Tasks.Task;
|
||||||
|
|
||||||
namespace LightlessSync.Services.LightFinder;
|
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
|
public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscriber
|
||||||
{
|
{
|
||||||
private readonly ILogger<LightFinderPlateHandler> _logger;
|
private readonly ILogger<LightFinderPlateHandler> _logger;
|
||||||
private readonly IAddonLifecycle _addonLifecycle;
|
private readonly IAddonLifecycle _addonLifecycle;
|
||||||
private readonly IGameGui _gameGui;
|
private readonly IGameGui _gameGui;
|
||||||
private readonly IObjectTable _objectTable;
|
private readonly IObjectTable _objectTable;
|
||||||
private readonly IClientState _clientState;
|
|
||||||
private readonly LightlessConfigService _configService;
|
private readonly LightlessConfigService _configService;
|
||||||
private readonly PairUiService _pairUiService;
|
private readonly PairUiService _pairUiService;
|
||||||
private readonly LightlessMediator _mediator;
|
private readonly LightlessMediator _mediator;
|
||||||
@@ -48,33 +42,21 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
private bool _needsLabelRefresh;
|
private bool _needsLabelRefresh;
|
||||||
private bool _drawSubscribed;
|
private bool _drawSubscribed;
|
||||||
private AddonNamePlate* _mpNameplateAddon;
|
private AddonNamePlate* _mpNameplateAddon;
|
||||||
private readonly Lock _labelLock = new();
|
private readonly object _labelLock = new();
|
||||||
private readonly NameplateBuffers _buffers = new();
|
private readonly NameplateBuffers _buffers = new();
|
||||||
private int _labelRenderCount;
|
private int _labelRenderCount;
|
||||||
|
|
||||||
private const string _defaultLabelText = "LightFinder";
|
private const string DefaultLabelText = "LightFinder";
|
||||||
private const SeIconChar _defaultIcon = SeIconChar.Hyadelyn;
|
private const SeIconChar DefaultIcon = SeIconChar.Hyadelyn;
|
||||||
private static readonly string _defaultIconGlyph = SeIconCharExtensions.ToIconString(_defaultIcon);
|
private static readonly string DefaultIconGlyph = SeIconCharExtensions.ToIconString(DefaultIcon);
|
||||||
private static readonly Vector2 _defaultPivot = new(0.5f, 1f);
|
private static readonly Vector2 DefaultPivot = new(0.5f, 1f);
|
||||||
private uint _lastNamePlateDrawFrame;
|
|
||||||
|
|
||||||
// / 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 = [];
|
private ImmutableHashSet<string> _activeBroadcastingCids = [];
|
||||||
|
|
||||||
public LightFinderPlateHandler(
|
public LightFinderPlateHandler(
|
||||||
ILogger<LightFinderPlateHandler> logger,
|
ILogger<LightFinderPlateHandler> logger,
|
||||||
IAddonLifecycle addonLifecycle,
|
IAddonLifecycle addonLifecycle,
|
||||||
IGameGui gameGui,
|
IGameGui gameGui,
|
||||||
IClientState clientState,
|
|
||||||
LightlessConfigService configService,
|
LightlessConfigService configService,
|
||||||
LightlessMediator mediator,
|
LightlessMediator mediator,
|
||||||
IObjectTable objectTable,
|
IObjectTable objectTable,
|
||||||
@@ -85,7 +67,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
_logger = logger;
|
_logger = logger;
|
||||||
_addonLifecycle = addonLifecycle;
|
_addonLifecycle = addonLifecycle;
|
||||||
_gameGui = gameGui;
|
_gameGui = gameGui;
|
||||||
_clientState = clientState;
|
|
||||||
_configService = configService;
|
_configService = configService;
|
||||||
_mediator = mediator;
|
_mediator = mediator;
|
||||||
_objectTable = objectTable;
|
_objectTable = objectTable;
|
||||||
@@ -120,9 +101,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
_mpNameplateAddon = null;
|
_mpNameplateAddon = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Enable nameplate handling.
|
|
||||||
/// </summary>
|
|
||||||
internal void EnableNameplate()
|
internal void EnableNameplate()
|
||||||
{
|
{
|
||||||
if (!_mEnabled)
|
if (!_mEnabled)
|
||||||
@@ -140,9 +118,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Disable nameplate handling.
|
|
||||||
/// </summary>
|
|
||||||
internal void DisableNameplate()
|
internal void DisableNameplate()
|
||||||
{
|
{
|
||||||
if (_mEnabled)
|
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)
|
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 (args.Addon.Address == nint.Zero)
|
||||||
{
|
{
|
||||||
if (_logger.IsEnabled(LogLevel.Warning))
|
if (_logger.IsEnabled(LogLevel.Warning))
|
||||||
@@ -183,10 +145,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var fw = Framework.Instance();
|
|
||||||
if (fw != null)
|
|
||||||
_lastNamePlateDrawFrame = fw->FrameCounter;
|
|
||||||
|
|
||||||
var pNameplateAddon = (AddonNamePlate*)args.Addon.Address;
|
var pNameplateAddon = (AddonNamePlate*)args.Addon.Address;
|
||||||
|
|
||||||
if (_mpNameplateAddon != pNameplateAddon)
|
if (_mpNameplateAddon != pNameplateAddon)
|
||||||
@@ -198,9 +156,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
UpdateNameplateNodes();
|
UpdateNameplateNodes();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Updates the nameplate nodes with LightFinder objects.
|
|
||||||
/// </summary>
|
|
||||||
private void UpdateNameplateNodes()
|
private void UpdateNameplateNodes()
|
||||||
{
|
{
|
||||||
var currentHandle = _gameGui.GetAddonByName("NamePlate");
|
var currentHandle = _gameGui.GetAddonByName("NamePlate");
|
||||||
@@ -220,12 +175,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!IsNamePlateAddonVisible())
|
|
||||||
{
|
|
||||||
ClearLabelBuffer();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var framework = Framework.Instance();
|
var framework = Framework.Instance();
|
||||||
if (framework == null)
|
if (framework == null)
|
||||||
{
|
{
|
||||||
@@ -258,7 +207,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
}
|
}
|
||||||
|
|
||||||
var visibleUserIdsSnapshot = VisibleUserIds;
|
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 currentConfig = _configService.Current;
|
||||||
var labelColor = UIColors.Get("Lightfinder");
|
var labelColor = UIColors.Get("Lightfinder");
|
||||||
var edgeColor = UIColors.Get("LightfinderEdge");
|
var edgeColor = UIColors.Get("LightfinderEdge");
|
||||||
@@ -266,7 +215,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
|
|
||||||
for (int i = 0; i < safeCount; ++i)
|
for (int i = 0; i < safeCount; ++i)
|
||||||
{
|
{
|
||||||
|
|
||||||
var objectInfoPtr = vec[i];
|
var objectInfoPtr = vec[i];
|
||||||
if (objectInfoPtr == null)
|
if (objectInfoPtr == null)
|
||||||
continue;
|
continue;
|
||||||
@@ -302,6 +250,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
var root = nameplateObject.RootComponentNode;
|
var root = nameplateObject.RootComponentNode;
|
||||||
var nameContainer = nameplateObject.NameContainer;
|
var nameContainer = nameplateObject.NameContainer;
|
||||||
var nameText = nameplateObject.NameText;
|
var nameText = nameplateObject.NameText;
|
||||||
|
var marker = nameplateObject.MarkerIcon;
|
||||||
|
|
||||||
if (root == null || root->Component == null || nameContainer == null || nameText == null)
|
if (root == null || root->Component == null || nameContainer == null || nameText == null)
|
||||||
{
|
{
|
||||||
@@ -312,14 +261,14 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
|
|
||||||
root->Component->UldManager.UpdateDrawNodeList();
|
root->Component->UldManager.UpdateDrawNodeList();
|
||||||
|
|
||||||
bool isNameplateVisible =
|
bool isVisible =
|
||||||
nameContainer->IsVisible() &&
|
(marker != null && marker->AtkResNode.IsVisible()) ||
|
||||||
nameText->AtkResNode.IsVisible();
|
(nameContainer->IsVisible() && nameText->AtkResNode.IsVisible()) ||
|
||||||
|
currentConfig.LightfinderLabelShowHidden;
|
||||||
|
|
||||||
if (!currentConfig.LightfinderLabelShowHidden && !isNameplateVisible)
|
if (!isVisible)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
// Prepare label content and scaling
|
|
||||||
var scaleMultiplier = System.Math.Clamp(currentConfig.LightfinderLabelScale, 0.5f, 2.0f);
|
var scaleMultiplier = System.Math.Clamp(currentConfig.LightfinderLabelScale, 0.5f, 2.0f);
|
||||||
var baseScale = currentConfig.LightfinderLabelUseIcon ? 1.0f : 0.5f;
|
var baseScale = currentConfig.LightfinderLabelUseIcon ? 1.0f : 0.5f;
|
||||||
var effectiveScale = baseScale * scaleMultiplier;
|
var effectiveScale = baseScale * scaleMultiplier;
|
||||||
@@ -327,10 +276,10 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
var targetFontSize = (int)System.Math.Round(baseFontSize * scaleMultiplier);
|
var targetFontSize = (int)System.Math.Round(baseFontSize * scaleMultiplier);
|
||||||
var labelContent = currentConfig.LightfinderLabelUseIcon
|
var labelContent = currentConfig.LightfinderLabelUseIcon
|
||||||
? NormalizeIconGlyph(currentConfig.LightfinderLabelIconGlyph)
|
? NormalizeIconGlyph(currentConfig.LightfinderLabelIconGlyph)
|
||||||
: _defaultLabelText;
|
: DefaultLabelText;
|
||||||
|
|
||||||
if (!currentConfig.LightfinderLabelUseIcon && (string.IsNullOrWhiteSpace(labelContent) || string.Equals(labelContent, "-", StringComparison.Ordinal)))
|
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 nodeWidth = (int)System.Math.Round(AtkNodeHelpers.DefaultTextNodeWidth * effectiveScale);
|
||||||
var nodeHeight = (int)System.Math.Round(AtkNodeHelpers.DefaultTextNodeHeight * effectiveScale);
|
var nodeHeight = (int)System.Math.Round(AtkNodeHelpers.DefaultTextNodeHeight * effectiveScale);
|
||||||
@@ -373,7 +322,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
() => GetScaledTextWidth(nameText),
|
() => GetScaledTextWidth(nameText),
|
||||||
nodeWidth);
|
nodeWidth);
|
||||||
|
|
||||||
// Text offset caching
|
|
||||||
var textOffset = (int)System.Math.Round(nameText->AtkResNode.X);
|
var textOffset = (int)System.Math.Round(nameText->AtkResNode.X);
|
||||||
var hasValidOffset = TryCacheTextOffset(nameplateIndex, rawTextWidth, textOffset);
|
var hasValidOffset = TryCacheTextOffset(nameplateIndex, rawTextWidth, textOffset);
|
||||||
|
|
||||||
@@ -384,93 +332,65 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
continue;
|
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;
|
float finalX;
|
||||||
if (currentConfig.LightfinderAutoAlign)
|
if (currentConfig.LightfinderAutoAlign)
|
||||||
{
|
{
|
||||||
// auto X positioning
|
var measuredWidth = System.Math.Max(1, textWidth > 0 ? textWidth : nodeWidth);
|
||||||
var measuredWidth = Math.Max(1, textWidth > 0 ? textWidth : nodeWidth);
|
|
||||||
var measuredWidthF = (float)measuredWidth;
|
var measuredWidthF = (float)measuredWidth;
|
||||||
|
var alignmentType = currentConfig.LabelAlignment;
|
||||||
|
|
||||||
// consider icon width
|
var containerScale = nameContainer->ScaleX;
|
||||||
var containerWidthLocal = res->Width > 0 ? res->Width : measuredWidthF;
|
if (containerScale <= 0f)
|
||||||
var containerWidthScreen = containerWidthLocal * worldScaleX;
|
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 = nameContainer->ScreenX;
|
||||||
var containerLeft = res->ScreenX;
|
var containerRight = containerLeft + containerWidth;
|
||||||
var containerRight = containerLeft + containerWidthScreen;
|
var containerCenter = containerLeft + (containerWidth * 0.5f);
|
||||||
var containerCenter = containerLeft + (containerWidthScreen * 0.5f);
|
|
||||||
|
|
||||||
var iconMargin = currentConfig.LightfinderLabelUseIcon
|
var iconMargin = currentConfig.LightfinderLabelUseIcon
|
||||||
? MathF.Min(containerWidthScreen * 0.1f, 14f * worldScaleX)
|
? System.Math.Min(containerWidth * 0.1f, 14f * containerScale)
|
||||||
: 0f;
|
: 0f;
|
||||||
|
|
||||||
var offsetXScreen = currentConfig.LightfinderLabelOffsetX * worldScaleX;
|
switch (alignmentType)
|
||||||
|
|
||||||
// alignment based on config
|
|
||||||
switch (currentConfig.LabelAlignment)
|
|
||||||
{
|
{
|
||||||
case LabelAlignment.Left:
|
case LabelAlignment.Left:
|
||||||
finalX = containerLeft + iconMargin + offsetXScreen;
|
finalX = containerLeft + iconMargin;
|
||||||
alignment = AlignmentType.BottomLeft;
|
alignment = AlignmentType.BottomLeft;
|
||||||
break;
|
break;
|
||||||
case LabelAlignment.Right:
|
case LabelAlignment.Right:
|
||||||
finalX = containerRight - iconMargin + offsetXScreen;
|
finalX = containerRight - iconMargin;
|
||||||
alignment = AlignmentType.BottomRight;
|
alignment = AlignmentType.BottomRight;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
finalX = containerCenter + offsetXScreen;
|
finalX = containerCenter;
|
||||||
alignment = AlignmentType.Bottom;
|
alignment = AlignmentType.Bottom;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
finalX += currentConfig.LightfinderLabelOffsetX;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// manual X positioning
|
|
||||||
var cachedTextOffset = _buffers.TextOffsets[nameplateIndex];
|
var cachedTextOffset = _buffers.TextOffsets[nameplateIndex];
|
||||||
var hasCachedOffset = cachedTextOffset != int.MinValue;
|
var hasCachedOffset = cachedTextOffset != int.MinValue;
|
||||||
var baseOffsetXLocal = (!currentConfig.LightfinderLabelUseIcon && hasValidOffset && hasCachedOffset)
|
var baseOffsetX = (!currentConfig.LightfinderLabelUseIcon && hasValidOffset && hasCachedOffset) ? cachedTextOffset : 0;
|
||||||
? cachedTextOffset
|
finalX = nameContainer->ScreenX + baseOffsetX + 58 + currentConfig.LightfinderLabelOffsetX;
|
||||||
: 0;
|
|
||||||
|
|
||||||
finalX =
|
|
||||||
res->ScreenX
|
|
||||||
+ (baseOffsetXLocal * worldScaleX)
|
|
||||||
+ (58f * worldScaleX)
|
|
||||||
+ (currentConfig.LightfinderLabelOffsetX * worldScaleX);
|
|
||||||
|
|
||||||
alignment = AlignmentType.Bottom;
|
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, nameContainer->ScreenY + positionY);
|
||||||
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 pivot = (currentConfig.LightfinderAutoAlign || currentConfig.LightfinderLabelUseIcon)
|
var pivot = (currentConfig.LightfinderAutoAlign || currentConfig.LightfinderLabelUseIcon)
|
||||||
? AlignmentToPivot(alignment)
|
? AlignmentToPivot(alignment)
|
||||||
: _defaultPivot;
|
: DefaultPivot;
|
||||||
var textColorPacked = PackColor(labelColor);
|
var textColorPacked = PackColor(labelColor);
|
||||||
var edgeColorPacked = PackColor(edgeColor);
|
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()
|
private void OnUiBuilderDraw()
|
||||||
{
|
{
|
||||||
if (!_mEnabled)
|
if (!_mEnabled)
|
||||||
return;
|
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;
|
int copyCount;
|
||||||
lock (_labelLock)
|
lock (_labelLock)
|
||||||
{
|
{
|
||||||
@@ -544,84 +433,21 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
Array.Copy(_buffers.LabelRender, _buffers.LabelCopy, copyCount);
|
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();
|
using var drawList = PictoService.Draw();
|
||||||
if (drawList == null)
|
if (drawList == null)
|
||||||
{
|
|
||||||
ImGui.End();
|
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = 0; i < copyCount; ++i)
|
for (int i = 0; i < copyCount; ++i)
|
||||||
{
|
{
|
||||||
ref var info = ref _buffers.LabelCopy[i];
|
ref var info = ref _buffers.LabelCopy[i];
|
||||||
|
|
||||||
// final draw position with viewport offset
|
|
||||||
var drawPos = info.ScreenPosition + vpPos;
|
|
||||||
var font = default(ImFontPtr);
|
var font = default(ImFontPtr);
|
||||||
if (info.UseIcon)
|
if (info.UseIcon)
|
||||||
{
|
{
|
||||||
var ioFonts = ImGui.GetIO().Fonts;
|
var ioFonts = ImGui.GetIO().Fonts;
|
||||||
font = ioFonts.Fonts.Size > 1 ? new ImFontPtr(ioFonts.Fonts[1]) : ImGui.GetFont();
|
font = ioFonts.Fonts.Size > 1 ? new ImFontPtr(ioFonts.Fonts[1]) : ImGui.GetFont();
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
font = ImGui.GetFont();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!font.IsNull)
|
drawList.AddScreenText(info.ScreenPosition, info.Text, info.TextColor, info.FontSize, info.Pivot, info.EdgeColor, font);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -634,15 +460,15 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
AlignmentType.Top => new Vector2(0.5f, 0f),
|
AlignmentType.Top => new Vector2(0.5f, 0f),
|
||||||
AlignmentType.Left => new Vector2(0f, 0.5f),
|
AlignmentType.Left => new Vector2(0f, 0.5f),
|
||||||
AlignmentType.Right => new Vector2(1f, 0.5f),
|
AlignmentType.Right => new Vector2(1f, 0.5f),
|
||||||
_ => _defaultPivot
|
_ => DefaultPivot
|
||||||
};
|
};
|
||||||
|
|
||||||
private static uint PackColor(Vector4 color)
|
private static uint PackColor(Vector4 color)
|
||||||
{
|
{
|
||||||
var r = (byte)Math.Clamp(color.X * 255f, 0f, 255f);
|
var r = (byte)System.Math.Clamp(color.X * 255f, 0f, 255f);
|
||||||
var g = (byte)Math.Clamp(color.Y * 255f, 0f, 255f);
|
var g = (byte)System.Math.Clamp(color.Y * 255f, 0f, 255f);
|
||||||
var b = (byte)Math.Clamp(color.Z * 255f, 0f, 255f);
|
var b = (byte)System.Math.Clamp(color.Z * 255f, 0f, 255f);
|
||||||
var a = (byte)Math.Clamp(color.W * 255f, 0f, 255f);
|
var a = (byte)System.Math.Clamp(color.W * 255f, 0f, 255f);
|
||||||
return (uint)((a << 24) | (b << 16) | (g << 8) | r);
|
return (uint)((a << 24) | (b << 16) | (g << 8) | r);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -688,19 +514,10 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
if (scale <= 0f)
|
if (scale <= 0f)
|
||||||
scale = 1f;
|
scale = 1f;
|
||||||
|
|
||||||
var computed = (int)Math.Round(rawWidth * scale);
|
var computed = (int)System.Math.Round(rawWidth * scale);
|
||||||
return Math.Max(1, computed);
|
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(
|
private static int ResolveCache(
|
||||||
int[] cache,
|
int[] cache,
|
||||||
int index,
|
int index,
|
||||||
@@ -728,7 +545,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
|
|
||||||
private bool TryCacheTextOffset(int nameplateIndex, int measuredTextWidth, int textOffset)
|
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;
|
_buffers.TextOffsets[nameplateIndex] = textOffset;
|
||||||
return true;
|
return true;
|
||||||
@@ -737,193 +554,10 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
return false;
|
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)
|
internal static string NormalizeIconGlyph(string? rawInput)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(rawInput))
|
if (string.IsNullOrWhiteSpace(rawInput))
|
||||||
return _defaultIconGlyph;
|
return DefaultIconGlyph;
|
||||||
|
|
||||||
var trimmed = rawInput.Trim();
|
var trimmed = rawInput.Trim();
|
||||||
|
|
||||||
@@ -941,36 +575,17 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
if (enumerator.MoveNext())
|
if (enumerator.MoveNext())
|
||||||
return enumerator.Current.ToString();
|
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)
|
internal static string ToIconEditorString(string? rawInput)
|
||||||
{
|
{
|
||||||
var normalized = NormalizeIconGlyph(rawInput);
|
var normalized = NormalizeIconGlyph(rawInput);
|
||||||
var runeEnumerator = normalized.EnumerateRunes();
|
var runeEnumerator = normalized.EnumerateRunes();
|
||||||
return runeEnumerator.MoveNext()
|
return runeEnumerator.MoveNext()
|
||||||
? runeEnumerator.Current.Value.ToString("X4", CultureInfo.InvariantCulture)
|
? runeEnumerator.Current.Value.ToString("X4", CultureInfo.InvariantCulture)
|
||||||
: _defaultIconGlyph;
|
: DefaultIconGlyph;
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly struct NameplateLabelInfo
|
private readonly struct NameplateLabelInfo
|
||||||
{
|
{
|
||||||
public NameplateLabelInfo(
|
public NameplateLabelInfo(
|
||||||
@@ -1000,9 +615,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
public bool UseIcon { get; }
|
public bool UseIcon { get; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Visible paired user IDs snapshot.
|
|
||||||
/// </summary>
|
|
||||||
private HashSet<ulong> VisibleUserIds
|
private HashSet<ulong> VisibleUserIds
|
||||||
=> [.. _pairUiService.GetSnapshot().PairsByUid.Values
|
=> [.. _pairUiService.GetSnapshot().PairsByUid.Values
|
||||||
.Where(u => u.IsVisible && u.PlayerCharacterId != uint.MaxValue)
|
.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)
|
public void UpdateBroadcastingCids(IEnumerable<string> cids)
|
||||||
{
|
{
|
||||||
var newSet = cids.ToImmutableHashSet(StringComparer.Ordinal);
|
var newSet = cids.ToImmutableHashSet(StringComparer.Ordinal);
|
||||||
@@ -1033,21 +641,15 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
_activeBroadcastingCids = newSet;
|
_activeBroadcastingCids = newSet;
|
||||||
if (_logger.IsEnabled(LogLevel.Trace))
|
if (_logger.IsEnabled(LogLevel.Information))
|
||||||
_logger.LogTrace("Active broadcast IDs: {Cids}", string.Join(',', _activeBroadcastingCids));
|
_logger.LogInformation("Active broadcast CIDs: {Cids}", string.Join(',', _activeBroadcastingCids));
|
||||||
FlagRefresh();
|
FlagRefresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Clears all nameplate related caches.
|
|
||||||
/// </summary>
|
|
||||||
public void ClearNameplateCaches()
|
public void ClearNameplateCaches()
|
||||||
{
|
{
|
||||||
_buffers.Clear();
|
_buffers.Clear();
|
||||||
ClearLabelBuffer();
|
ClearLabelBuffer();
|
||||||
|
|
||||||
Array.Clear(_buffers.HasSmoothed, 0, _buffers.HasSmoothed.Length);
|
|
||||||
Array.Clear(_buffers.SmoothedPos, 0, _buffers.SmoothedPos.Length);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class NameplateBuffers
|
private sealed class NameplateBuffers
|
||||||
@@ -1066,10 +668,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
public NameplateLabelInfo[] LabelRender { get; } = new NameplateLabelInfo[AddonNamePlate.NumNamePlateObjects];
|
public NameplateLabelInfo[] LabelRender { get; } = new NameplateLabelInfo[AddonNamePlate.NumNamePlateObjects];
|
||||||
public NameplateLabelInfo[] LabelCopy { 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()
|
public void Clear()
|
||||||
{
|
{
|
||||||
System.Array.Clear(TextWidths, 0, TextWidths.Length);
|
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)
|
public Task StartAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
Init();
|
Init();
|
||||||
return Task.CompletedTask;
|
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)
|
public Task StopAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
Uninit();
|
Uninit();
|
||||||
return Task.CompletedTask;
|
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,12 +148,8 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase
|
|||||||
private void UpdateSyncshellBroadcasts()
|
private void UpdateSyncshellBroadcasts()
|
||||||
{
|
{
|
||||||
var now = DateTime.UtcNow;
|
var now = DateTime.UtcNow;
|
||||||
var nearbyCids = GetNearbyHashedCids(out _);
|
var newSet = _broadcastCache
|
||||||
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 => e.Value.IsBroadcasting && e.Value.ExpiryTime > now && !string.IsNullOrEmpty(e.Value.GID))
|
||||||
.Where(e => nearbyCids.Contains(e.Key))
|
|
||||||
.Select(e => e.Key)
|
.Select(e => e.Key)
|
||||||
.ToHashSet(StringComparer.Ordinal);
|
.ToHashSet(StringComparer.Ordinal);
|
||||||
|
|
||||||
@@ -167,17 +163,12 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<BroadcastStatusInfoDto> GetActiveSyncshellBroadcasts(bool excludeLocal = false)
|
public List<BroadcastStatusInfoDto> GetActiveSyncshellBroadcasts()
|
||||||
{
|
{
|
||||||
var now = DateTime.UtcNow;
|
var now = DateTime.UtcNow;
|
||||||
var nearbyCids = GetNearbyHashedCids(out var localCid);
|
|
||||||
if (nearbyCids.Count == 0)
|
|
||||||
return [];
|
|
||||||
|
|
||||||
return [.. _broadcastCache
|
return [.. _broadcastCache
|
||||||
.Where(e => e.Value.IsBroadcasting && e.Value.ExpiryTime > now && !string.IsNullOrEmpty(e.Value.GID))
|
.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
|
.Select(e => new BroadcastStatusInfoDto
|
||||||
{
|
{
|
||||||
HashedCID = e.Key,
|
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()
|
private async Task ExpiredBroadcastCleanupLoop()
|
||||||
{
|
{
|
||||||
var token = _cleanupCts.Token;
|
var token = _cleanupCts.Token;
|
||||||
|
|||||||
@@ -126,11 +126,11 @@ public sealed class TextureMetadataHelper
|
|||||||
private const string TextureSegment = "/texture/";
|
private const string TextureSegment = "/texture/";
|
||||||
private const string MaterialSegment = "/material/";
|
private const string MaterialSegment = "/material/";
|
||||||
|
|
||||||
private const uint NormalSamplerId = ShpkFile.NormalSamplerId;
|
private const uint NormalSamplerId = 0x0C5EC1F1u;
|
||||||
private const uint IndexSamplerId = ShpkFile.IndexSamplerId;
|
private const uint IndexSamplerId = 0x565F8FD8u;
|
||||||
private const uint SpecularSamplerId = ShpkFile.SpecularSamplerId;
|
private const uint SpecularSamplerId = 0x2B99E025u;
|
||||||
private const uint DiffuseSamplerId = ShpkFile.DiffuseSamplerId;
|
private const uint DiffuseSamplerId = 0x115306BEu;
|
||||||
private const uint MaskSamplerId = ShpkFile.MaskSamplerId;
|
private const uint MaskSamplerId = 0x8A4E82B6u;
|
||||||
|
|
||||||
public TextureMetadataHelper(ILogger<TextureMetadataHelper> logger, IDataManager dataManager)
|
public TextureMetadataHelper(ILogger<TextureMetadataHelper> logger, IDataManager dataManager)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -843,16 +843,12 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
|
|
||||||
//Filter of not grouped/foldered and offline pairs
|
//Filter of not grouped/foldered and offline pairs
|
||||||
var allOnlineNotTaggedPairs = SortEntries(allEntries.Where(FilterNotTaggedUsers));
|
var allOnlineNotTaggedPairs = SortEntries(allEntries.Where(FilterNotTaggedUsers));
|
||||||
if (allOnlineNotTaggedPairs.Count > 0 && _configService.Current.ShowOfflineUsersSeparately) {
|
var onlineNotTaggedPairs = SortEntries(filteredEntries.Where(e => FilterNotTaggedUsers(e) && FilterOnlineOrPausedSelf(e)));
|
||||||
var filteredOnlineEntries = SortOnlineEntries(filteredEntries.Where(e => FilterNotTaggedUsers(e) && FilterOnlineOrPausedSelf(e)));
|
|
||||||
|
if (allOnlineNotTaggedPairs.Count > 0)
|
||||||
|
{
|
||||||
drawFolders.Add(_drawEntityFactory.CreateTagFolder(
|
drawFolders.Add(_drawEntityFactory.CreateTagFolder(
|
||||||
TagHandler.CustomOnlineTag,
|
_configService.Current.ShowOfflineUsersSeparately ? TagHandler.CustomOnlineTag : TagHandler.CustomAllTag,
|
||||||
filteredOnlineEntries,
|
|
||||||
allOnlineNotTaggedPairs));
|
|
||||||
} else if (allOnlineNotTaggedPairs.Count > 0 && !_configService.Current.ShowOfflineUsersSeparately) {
|
|
||||||
var onlineNotTaggedPairs = SortEntries(filteredEntries.Where(FilterNotTaggedUsers));
|
|
||||||
drawFolders.Add(_drawEntityFactory.CreateTagFolder(
|
|
||||||
TagHandler.CustomAllTag,
|
|
||||||
onlineNotTaggedPairs,
|
onlineNotTaggedPairs,
|
||||||
allOnlineNotTaggedPairs));
|
allOnlineNotTaggedPairs));
|
||||||
}
|
}
|
||||||
@@ -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;
|
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)
|
private ImmutableList<PairUiEntry> SortVisibleByMetric(IEnumerable<PairUiEntry> entries, Func<PairUiEntry, long> selector)
|
||||||
{
|
{
|
||||||
return [.. entries
|
return [.. entries
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ using Dalamud.Interface.Utility.Raii;
|
|||||||
using LightlessSync.UI.Handlers;
|
using LightlessSync.UI.Handlers;
|
||||||
using LightlessSync.UI.Models;
|
using LightlessSync.UI.Models;
|
||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
|
using LightlessSync.UI;
|
||||||
using LightlessSync.UI.Style;
|
using LightlessSync.UI.Style;
|
||||||
using OtterGui.Text;
|
|
||||||
|
|
||||||
namespace LightlessSync.UI.Components;
|
namespace LightlessSync.UI.Components;
|
||||||
|
|
||||||
@@ -113,13 +113,9 @@ public abstract class DrawFolderBase : IDrawFolder
|
|||||||
using var indent = ImRaii.PushIndent(_uiSharedService.GetIconSize(FontAwesomeIcon.EllipsisV).X + ImGui.GetStyle().ItemSpacing.X, false);
|
using var indent = ImRaii.PushIndent(_uiSharedService.GetIconSize(FontAwesomeIcon.EllipsisV).X + ImGui.GetStyle().ItemSpacing.X, false);
|
||||||
if (DrawPairs.Any())
|
if (DrawPairs.Any())
|
||||||
{
|
{
|
||||||
using var clipper = ImUtf8.ListClipper(DrawPairs.Count, ImGui.GetFrameHeightWithSpacing());
|
foreach (var item in DrawPairs)
|
||||||
while (clipper.Step())
|
|
||||||
{
|
{
|
||||||
for (var i = clipper.DisplayStart; i < clipper.DisplayEnd; i++)
|
item.DrawPairedClient();
|
||||||
{
|
|
||||||
DrawPairs[i].DrawPairedClient();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -169,16 +169,11 @@ public class DrawFolderTag : DrawFolderBase
|
|||||||
|
|
||||||
protected override float DrawRightSide(float currentRightSideX)
|
protected override float DrawRightSide(float currentRightSideX)
|
||||||
{
|
{
|
||||||
if (string.Equals(_id, TagHandler.CustomVisibleTag, StringComparison.Ordinal))
|
if (_id == TagHandler.CustomVisibleTag)
|
||||||
{
|
{
|
||||||
return DrawVisibleFilter(currentRightSideX);
|
return DrawVisibleFilter(currentRightSideX);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.Equals(_id, TagHandler.CustomOnlineTag, StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
return DrawOnlineFilter(currentRightSideX);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!RenderPause)
|
if (!RenderPause)
|
||||||
{
|
{
|
||||||
return currentRightSideX;
|
return currentRightSideX;
|
||||||
@@ -259,7 +254,7 @@ public class DrawFolderTag : DrawFolderBase
|
|||||||
foreach (VisiblePairSortMode mode in Enum.GetValues<VisiblePairSortMode>())
|
foreach (VisiblePairSortMode mode in Enum.GetValues<VisiblePairSortMode>())
|
||||||
{
|
{
|
||||||
var selected = _configService.Current.VisiblePairSortMode == mode;
|
var selected = _configService.Current.VisiblePairSortMode == mode;
|
||||||
if (ImGui.MenuItem(GetSortVisibleLabel(mode), string.Empty, selected))
|
if (ImGui.MenuItem(GetSortLabel(mode), string.Empty, selected))
|
||||||
{
|
{
|
||||||
if (!selected)
|
if (!selected)
|
||||||
{
|
{
|
||||||
@@ -278,49 +273,7 @@ public class DrawFolderTag : DrawFolderBase
|
|||||||
return buttonStart - spacingX;
|
return buttonStart - spacingX;
|
||||||
}
|
}
|
||||||
|
|
||||||
private float DrawOnlineFilter(float currentRightSideX)
|
private static string GetSortLabel(VisiblePairSortMode mode) => mode switch
|
||||||
{
|
|
||||||
var buttonSize = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Filter);
|
|
||||||
var spacingX = ImGui.GetStyle().ItemSpacing.X;
|
|
||||||
var buttonStart = currentRightSideX - buttonSize.X;
|
|
||||||
|
|
||||||
ImGui.SameLine(buttonStart);
|
|
||||||
if (_uiSharedService.IconButton(FontAwesomeIcon.Filter))
|
|
||||||
{
|
|
||||||
SuppressNextRowToggle();
|
|
||||||
ImGui.OpenPopup($"online-filter-{_id}");
|
|
||||||
}
|
|
||||||
|
|
||||||
UiSharedService.AttachToolTip("Adjust how online pairs are ordered.");
|
|
||||||
|
|
||||||
if (ImGui.BeginPopup($"online-filter-{_id}"))
|
|
||||||
{
|
|
||||||
ImGui.TextUnformatted("Online Pair Ordering");
|
|
||||||
ImGui.Separator();
|
|
||||||
|
|
||||||
foreach (OnlinePairSortMode mode in Enum.GetValues<OnlinePairSortMode>())
|
|
||||||
{
|
|
||||||
var selected = _configService.Current.OnlinePairSortMode == mode;
|
|
||||||
if (ImGui.MenuItem(GetSortOnlineLabel(mode), string.Empty, selected))
|
|
||||||
{
|
|
||||||
if (!selected)
|
|
||||||
{
|
|
||||||
_configService.Current.OnlinePairSortMode = mode;
|
|
||||||
_configService.Save();
|
|
||||||
_mediator.Publish(new RefreshUiMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui.CloseCurrentPopup();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui.EndPopup();
|
|
||||||
}
|
|
||||||
|
|
||||||
return buttonStart - spacingX;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string GetSortVisibleLabel(VisiblePairSortMode mode) => mode switch
|
|
||||||
{
|
{
|
||||||
VisiblePairSortMode.Alphabetical => "Alphabetical",
|
VisiblePairSortMode.Alphabetical => "Alphabetical",
|
||||||
VisiblePairSortMode.VramUsage => "VRAM usage (descending)",
|
VisiblePairSortMode.VramUsage => "VRAM usage (descending)",
|
||||||
@@ -329,11 +282,4 @@ public class DrawFolderTag : DrawFolderBase
|
|||||||
VisiblePairSortMode.PreferredDirectPairs => "Preferred permissions & Direct pairs",
|
VisiblePairSortMode.PreferredDirectPairs => "Preferred permissions & Direct pairs",
|
||||||
_ => "Default",
|
_ => "Default",
|
||||||
};
|
};
|
||||||
|
|
||||||
private static string GetSortOnlineLabel(OnlinePairSortMode mode) => mode switch
|
|
||||||
{
|
|
||||||
OnlinePairSortMode.Alphabetical => "Alphabetical",
|
|
||||||
OnlinePairSortMode.PreferredDirectPairs => "Preferred permissions & Direct pairs",
|
|
||||||
_ => "Default",
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
@@ -133,26 +133,6 @@ public class DrawUserPair
|
|||||||
UiSharedService.AttachToolTip("This reapplies the last received character data to this character");
|
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))
|
if (_uiSharedService.IconTextButton(FontAwesomeIcon.PlayCircle, "Cycle pause state", _menuWidth, true))
|
||||||
{
|
{
|
||||||
_ = _apiController.CyclePauseAsync(_pair);
|
_ = _apiController.CyclePauseAsync(_pair);
|
||||||
|
|||||||
@@ -27,11 +27,8 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
private const float MinTextureFilterPaneWidth = 305f;
|
private const float MinTextureFilterPaneWidth = 305f;
|
||||||
private const float MaxTextureFilterPaneWidth = 405f;
|
private const float MaxTextureFilterPaneWidth = 405f;
|
||||||
private const float MinTextureDetailPaneWidth = 480f;
|
private const float MinTextureDetailPaneWidth = 580f;
|
||||||
private const float MaxTextureDetailPaneWidth = 720f;
|
private const float MaxTextureDetailPaneWidth = 720f;
|
||||||
private const float TextureFilterSplitterWidth = 8f;
|
|
||||||
private const float TextureDetailSplitterWidth = 12f;
|
|
||||||
private const float TextureDetailSplitterCollapsedWidth = 18f;
|
|
||||||
private const float SelectedFilePanelLogicalHeight = 90f;
|
private const float SelectedFilePanelLogicalHeight = 90f;
|
||||||
private static readonly Vector4 SelectedTextureRowTextColor = new(0f, 0f, 0f, 1f);
|
private static readonly Vector4 SelectedTextureRowTextColor = new(0f, 0f, 0f, 1f);
|
||||||
|
|
||||||
@@ -83,7 +80,6 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
|
|||||||
private bool _modalOpen = false;
|
private bool _modalOpen = false;
|
||||||
private bool _showModal = false;
|
private bool _showModal = false;
|
||||||
private bool _textureRowsDirty = true;
|
private bool _textureRowsDirty = true;
|
||||||
private bool _textureDetailCollapsed = false;
|
|
||||||
private bool _conversionFailed;
|
private bool _conversionFailed;
|
||||||
private bool _showAlreadyAddedTransients = false;
|
private bool _showAlreadyAddedTransients = false;
|
||||||
private bool _acknowledgeReview = false;
|
private bool _acknowledgeReview = false;
|
||||||
@@ -115,7 +111,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
|
|||||||
_hasUpdate = true;
|
_hasUpdate = true;
|
||||||
});
|
});
|
||||||
WindowBuilder.For(this)
|
WindowBuilder.For(this)
|
||||||
.SetSizeConstraints(new Vector2(1240, 680), new Vector2(3840, 2160))
|
.SetSizeConstraints(new Vector2(1650, 1000), new Vector2(3840, 2160))
|
||||||
.Apply();
|
.Apply();
|
||||||
|
|
||||||
_conversionProgress.ProgressChanged += ConversionProgress_ProgressChanged;
|
_conversionProgress.ProgressChanged += ConversionProgress_ProgressChanged;
|
||||||
@@ -1209,52 +1205,35 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
|
|||||||
var availableSize = ImGui.GetContentRegionAvail();
|
var availableSize = ImGui.GetContentRegionAvail();
|
||||||
var windowPos = ImGui.GetWindowPos();
|
var windowPos = ImGui.GetWindowPos();
|
||||||
var spacingX = ImGui.GetStyle().ItemSpacing.X;
|
var spacingX = ImGui.GetStyle().ItemSpacing.X;
|
||||||
var filterSplitterWidth = TextureFilterSplitterWidth * scale;
|
var splitterWidth = 6f * scale;
|
||||||
var detailSplitterWidth = (_textureDetailCollapsed ? TextureDetailSplitterCollapsedWidth : TextureDetailSplitterWidth) * scale;
|
|
||||||
var totalSplitterWidth = filterSplitterWidth + detailSplitterWidth;
|
|
||||||
var totalSpacing = 2 * spacingX;
|
|
||||||
const float minFilterWidth = MinTextureFilterPaneWidth;
|
const float minFilterWidth = MinTextureFilterPaneWidth;
|
||||||
const float minDetailWidth = MinTextureDetailPaneWidth;
|
const float minDetailWidth = MinTextureDetailPaneWidth;
|
||||||
const float minCenterWidth = 340f;
|
const float minCenterWidth = 340f;
|
||||||
|
|
||||||
var detailMinForLayout = _textureDetailCollapsed ? 0f : minDetailWidth;
|
var dynamicFilterMax = Math.Max(minFilterWidth, availableSize.X - minDetailWidth - minCenterWidth - 2 * (splitterWidth + spacingX));
|
||||||
var dynamicFilterMax = Math.Max(minFilterWidth, availableSize.X - detailMinForLayout - minCenterWidth - totalSplitterWidth - totalSpacing);
|
|
||||||
var filterMaxBound = Math.Min(MaxTextureFilterPaneWidth, dynamicFilterMax);
|
var filterMaxBound = Math.Min(MaxTextureFilterPaneWidth, dynamicFilterMax);
|
||||||
var filterWidth = Math.Clamp(_textureFilterPaneWidth, minFilterWidth, filterMaxBound);
|
var filterWidth = Math.Clamp(_textureFilterPaneWidth, minFilterWidth, filterMaxBound);
|
||||||
|
|
||||||
var dynamicDetailMax = Math.Max(detailMinForLayout, availableSize.X - filterWidth - minCenterWidth - totalSplitterWidth - totalSpacing);
|
var dynamicDetailMax = Math.Max(minDetailWidth, availableSize.X - filterWidth - minCenterWidth - 2 * (splitterWidth + spacingX));
|
||||||
var detailMaxBound = _textureDetailCollapsed ? 0f : Math.Min(MaxTextureDetailPaneWidth, dynamicDetailMax);
|
var detailMaxBound = Math.Min(MaxTextureDetailPaneWidth, dynamicDetailMax);
|
||||||
var detailWidth = _textureDetailCollapsed ? 0f : Math.Clamp(_textureDetailPaneWidth, minDetailWidth, detailMaxBound);
|
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)
|
if (centerWidth < minCenterWidth)
|
||||||
{
|
{
|
||||||
var deficit = minCenterWidth - centerWidth;
|
var deficit = minCenterWidth - centerWidth;
|
||||||
if (!_textureDetailCollapsed)
|
|
||||||
{
|
|
||||||
detailWidth = Math.Clamp(detailWidth - deficit, minDetailWidth,
|
detailWidth = Math.Clamp(detailWidth - deficit, minDetailWidth,
|
||||||
Math.Min(MaxTextureDetailPaneWidth, Math.Max(minDetailWidth, availableSize.X - filterWidth - minCenterWidth - totalSplitterWidth - totalSpacing)));
|
Math.Min(MaxTextureDetailPaneWidth, Math.Max(minDetailWidth, availableSize.X - filterWidth - minCenterWidth - 2 * (splitterWidth + spacingX))));
|
||||||
centerWidth = availableSize.X - filterWidth - detailWidth - totalSplitterWidth - totalSpacing;
|
centerWidth = availableSize.X - filterWidth - detailWidth - 2 * (splitterWidth + spacingX);
|
||||||
if (centerWidth < minCenterWidth)
|
if (centerWidth < minCenterWidth)
|
||||||
{
|
{
|
||||||
deficit = minCenterWidth - centerWidth;
|
deficit = minCenterWidth - centerWidth;
|
||||||
filterWidth = Math.Clamp(filterWidth - deficit, minFilterWidth,
|
filterWidth = Math.Clamp(filterWidth - deficit, minFilterWidth,
|
||||||
Math.Min(MaxTextureFilterPaneWidth, Math.Max(minFilterWidth, availableSize.X - detailWidth - minCenterWidth - totalSplitterWidth - totalSpacing)));
|
Math.Min(MaxTextureFilterPaneWidth, Math.Max(minFilterWidth, availableSize.X - detailWidth - minCenterWidth - 2 * (splitterWidth + spacingX))));
|
||||||
detailWidth = Math.Clamp(detailWidth, minDetailWidth,
|
detailWidth = Math.Clamp(detailWidth, minDetailWidth,
|
||||||
Math.Min(MaxTextureDetailPaneWidth, Math.Max(minDetailWidth, availableSize.X - filterWidth - minCenterWidth - totalSplitterWidth - totalSpacing)));
|
Math.Min(MaxTextureDetailPaneWidth, Math.Max(minDetailWidth, availableSize.X - filterWidth - minCenterWidth - 2 * (splitterWidth + spacingX))));
|
||||||
centerWidth = availableSize.X - filterWidth - detailWidth - totalSplitterWidth - totalSpacing;
|
centerWidth = availableSize.X - filterWidth - detailWidth - 2 * (splitterWidth + spacingX);
|
||||||
if (centerWidth < minCenterWidth)
|
|
||||||
{
|
|
||||||
centerWidth = minCenterWidth;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
filterWidth = Math.Clamp(filterWidth - deficit, minFilterWidth,
|
|
||||||
Math.Min(MaxTextureFilterPaneWidth, Math.Max(minFilterWidth, availableSize.X - minCenterWidth - totalSplitterWidth - totalSpacing)));
|
|
||||||
centerWidth = availableSize.X - filterWidth - detailWidth - totalSplitterWidth - totalSpacing;
|
|
||||||
if (centerWidth < minCenterWidth)
|
if (centerWidth < minCenterWidth)
|
||||||
{
|
{
|
||||||
centerWidth = minCenterWidth;
|
centerWidth = minCenterWidth;
|
||||||
@@ -1263,10 +1242,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
_textureFilterPaneWidth = filterWidth;
|
_textureFilterPaneWidth = filterWidth;
|
||||||
if (!_textureDetailCollapsed)
|
|
||||||
{
|
|
||||||
_textureDetailPaneWidth = detailWidth;
|
_textureDetailPaneWidth = detailWidth;
|
||||||
}
|
|
||||||
|
|
||||||
ImGui.BeginGroup();
|
ImGui.BeginGroup();
|
||||||
using (var filters = ImRaii.Child("textureFilters", new Vector2(filterWidth, 0), true))
|
using (var filters = ImRaii.Child("textureFilters", new Vector2(filterWidth, 0), true))
|
||||||
@@ -1288,8 +1264,8 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
|
|||||||
var filterMax = ImGui.GetItemRectMax();
|
var filterMax = ImGui.GetItemRectMax();
|
||||||
var filterHeight = filterMax.Y - filterMin.Y;
|
var filterHeight = filterMax.Y - filterMin.Y;
|
||||||
var filterTopLocal = filterMin - windowPos;
|
var filterTopLocal = filterMin - windowPos;
|
||||||
var maxFilterResize = Math.Min(MaxTextureFilterPaneWidth, Math.Max(minFilterWidth, availableSize.X - minCenterWidth - detailMinForLayout - totalSplitterWidth - totalSpacing));
|
var maxFilterResize = Math.Min(MaxTextureFilterPaneWidth, Math.Max(minFilterWidth, availableSize.X - minCenterWidth - minDetailWidth - 2 * (splitterWidth + spacingX)));
|
||||||
DrawVerticalResizeHandle("##textureFilterSplitter", filterTopLocal.Y, filterHeight, ref _textureFilterPaneWidth, minFilterWidth, maxFilterResize, out _);
|
DrawVerticalResizeHandle("##textureFilterSplitter", filterTopLocal.Y, filterHeight, ref _textureFilterPaneWidth, minFilterWidth, maxFilterResize);
|
||||||
|
|
||||||
TextureRow? selectedRow;
|
TextureRow? selectedRow;
|
||||||
ImGui.BeginGroup();
|
ImGui.BeginGroup();
|
||||||
@@ -1303,29 +1279,9 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
|
|||||||
var tableMax = ImGui.GetItemRectMax();
|
var tableMax = ImGui.GetItemRectMax();
|
||||||
var tableHeight = tableMax.Y - tableMin.Y;
|
var tableHeight = tableMax.Y - tableMin.Y;
|
||||||
var tableTopLocal = tableMin - windowPos;
|
var tableTopLocal = tableMin - windowPos;
|
||||||
var maxDetailResize = Math.Min(MaxTextureDetailPaneWidth, Math.Max(minDetailWidth, availableSize.X - _textureFilterPaneWidth - minCenterWidth - totalSplitterWidth - totalSpacing));
|
var maxDetailResize = Math.Min(MaxTextureDetailPaneWidth, Math.Max(minDetailWidth, availableSize.X - _textureFilterPaneWidth - minCenterWidth - 2 * (splitterWidth + spacingX)));
|
||||||
var detailToggle = DrawVerticalResizeHandle(
|
DrawVerticalResizeHandle("##textureDetailSplitter", tableTopLocal.Y, tableHeight, ref _textureDetailPaneWidth, minDetailWidth, maxDetailResize, invert: true);
|
||||||
"##textureDetailSplitter",
|
|
||||||
tableTopLocal.Y,
|
|
||||||
tableHeight,
|
|
||||||
ref _textureDetailPaneWidth,
|
|
||||||
minDetailWidth,
|
|
||||||
maxDetailResize,
|
|
||||||
out var detailDragging,
|
|
||||||
invert: true,
|
|
||||||
showToggle: true,
|
|
||||||
isCollapsed: _textureDetailCollapsed);
|
|
||||||
if (detailToggle)
|
|
||||||
{
|
|
||||||
_textureDetailCollapsed = !_textureDetailCollapsed;
|
|
||||||
}
|
|
||||||
if (_textureDetailCollapsed && detailDragging)
|
|
||||||
{
|
|
||||||
_textureDetailCollapsed = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!_textureDetailCollapsed)
|
|
||||||
{
|
|
||||||
ImGui.BeginGroup();
|
ImGui.BeginGroup();
|
||||||
using (var detailChild = ImRaii.Child("textureDetailPane", new Vector2(detailWidth, 0), true))
|
using (var detailChild = ImRaii.Child("textureDetailPane", new Vector2(detailWidth, 0), true))
|
||||||
{
|
{
|
||||||
@@ -1333,7 +1289,6 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
ImGui.EndGroup();
|
ImGui.EndGroup();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private void DrawTextureFilters(
|
private void DrawTextureFilters(
|
||||||
IReadOnlyList<TextureUsageCategory> categories,
|
IReadOnlyList<TextureUsageCategory> categories,
|
||||||
@@ -1980,118 +1935,26 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool DrawVerticalResizeHandle(
|
private void DrawVerticalResizeHandle(string id, float topY, float height, ref float leftWidth, float minWidth, float maxWidth, bool invert = false)
|
||||||
string id,
|
|
||||||
float topY,
|
|
||||||
float height,
|
|
||||||
ref float leftWidth,
|
|
||||||
float minWidth,
|
|
||||||
float maxWidth,
|
|
||||||
out bool isDragging,
|
|
||||||
bool invert = false,
|
|
||||||
bool showToggle = false,
|
|
||||||
bool isCollapsed = false)
|
|
||||||
{
|
{
|
||||||
var scale = ImGuiHelpers.GlobalScale;
|
var scale = ImGuiHelpers.GlobalScale;
|
||||||
var splitterWidth = (showToggle
|
var splitterWidth = 8f * scale;
|
||||||
? (isCollapsed ? TextureDetailSplitterCollapsedWidth : TextureDetailSplitterWidth)
|
|
||||||
: TextureFilterSplitterWidth) * scale;
|
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
var cursor = ImGui.GetCursorPos();
|
var cursor = ImGui.GetCursorPos();
|
||||||
var contentMin = ImGui.GetWindowContentRegionMin();
|
ImGui.SetCursorPos(new Vector2(cursor.X, topY));
|
||||||
var contentMax = ImGui.GetWindowContentRegionMax();
|
ImGui.PushStyleColor(ImGuiCol.Button, UIColors.Get("ButtonDefault"));
|
||||||
var clampedTop = MathF.Max(topY, contentMin.Y);
|
ImGui.PushStyleColor(ImGuiCol.ButtonHovered, UIColors.Get("LightlessPurple"));
|
||||||
var clampedBottom = MathF.Min(topY + height, contentMax.Y);
|
ImGui.PushStyleColor(ImGuiCol.ButtonActive, UIColors.Get("LightlessPurpleActive"));
|
||||||
var clampedHeight = MathF.Max(0f, clampedBottom - clampedTop);
|
ImGui.Button(id, new Vector2(splitterWidth, height));
|
||||||
var splitterRounding = ImGui.GetStyle().FrameRounding;
|
ImGui.PopStyleColor(3);
|
||||||
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.InvisibleButton(id, new Vector2(splitterWidth, clampedHeight));
|
if (ImGui.IsItemActive())
|
||||||
var drawList = ImGui.GetWindowDrawList();
|
|
||||||
var rectMin = ImGui.GetItemRectMin();
|
|
||||||
var rectMax = ImGui.GetItemRectMax();
|
|
||||||
var windowPos = ImGui.GetWindowPos();
|
|
||||||
var clipMin = windowPos + contentMin;
|
|
||||||
var clipMax = windowPos + contentMax;
|
|
||||||
drawList.PushClipRect(clipMin, clipMax, true);
|
|
||||||
var clipInset = 1f * scale;
|
|
||||||
var drawMin = new Vector2(
|
|
||||||
MathF.Max(rectMin.X, clipMin.X),
|
|
||||||
MathF.Max(rectMin.Y, clipMin.Y));
|
|
||||||
var drawMax = new Vector2(
|
|
||||||
MathF.Min(rectMax.X, clipMax.X - clipInset),
|
|
||||||
MathF.Min(rectMax.Y, clipMax.Y));
|
|
||||||
var hovered = ImGui.IsItemHovered();
|
|
||||||
isDragging = ImGui.IsItemActive();
|
|
||||||
var baseColor = UIColors.Get("ButtonDefault");
|
|
||||||
var hoverColor = UIColors.Get("LightlessPurple");
|
|
||||||
var activeColor = UIColors.Get("LightlessPurpleActive");
|
|
||||||
var handleColor = isDragging ? activeColor : hovered ? hoverColor : baseColor;
|
|
||||||
drawList.AddRectFilled(drawMin, drawMax, UiSharedService.Color(handleColor), splitterRounding);
|
|
||||||
drawList.AddRect(drawMin, drawMax, UiSharedService.Color(new Vector4(1f, 1f, 1f, 0.12f)), splitterRounding);
|
|
||||||
|
|
||||||
bool toggleHovered = false;
|
|
||||||
bool toggleClicked = false;
|
|
||||||
if (showToggle)
|
|
||||||
{
|
|
||||||
var icon = isCollapsed ? FontAwesomeIcon.ChevronRight : FontAwesomeIcon.ChevronLeft;
|
|
||||||
Vector2 iconSize;
|
|
||||||
using (_uiSharedService.IconFont.Push())
|
|
||||||
{
|
|
||||||
iconSize = ImGui.CalcTextSize(icon.ToIconString());
|
|
||||||
}
|
|
||||||
|
|
||||||
var toggleHeight = MathF.Min(clampedHeight, 64f * scale);
|
|
||||||
var toggleMin = new Vector2(
|
|
||||||
drawMin.X,
|
|
||||||
drawMin.Y + (drawMax.Y - drawMin.Y - toggleHeight) / 2f);
|
|
||||||
var toggleMax = new Vector2(
|
|
||||||
drawMax.X,
|
|
||||||
toggleMin.Y + toggleHeight);
|
|
||||||
var toggleColorBase = UIColors.Get("LightlessPurple");
|
|
||||||
toggleHovered = ImGui.IsMouseHoveringRect(toggleMin, toggleMax);
|
|
||||||
var toggleBg = toggleHovered
|
|
||||||
? new Vector4(toggleColorBase.X, toggleColorBase.Y, toggleColorBase.Z, 0.65f)
|
|
||||||
: new Vector4(toggleColorBase.X, toggleColorBase.Y, toggleColorBase.Z, 0.35f);
|
|
||||||
if (toggleHovered)
|
|
||||||
{
|
|
||||||
UiSharedService.AttachToolTip(isCollapsed ? "Show texture details." : "Hide texture details.");
|
|
||||||
}
|
|
||||||
|
|
||||||
drawList.AddRectFilled(toggleMin, toggleMax, UiSharedService.Color(toggleBg), splitterRounding);
|
|
||||||
drawList.AddRect(toggleMin, toggleMax, UiSharedService.Color(toggleColorBase), splitterRounding);
|
|
||||||
|
|
||||||
var iconPos = new Vector2(
|
|
||||||
drawMin.X + (drawMax.X - drawMin.X - iconSize.X) / 2f,
|
|
||||||
drawMin.Y + (drawMax.Y - drawMin.Y - iconSize.Y) / 2f);
|
|
||||||
using (_uiSharedService.IconFont.Push())
|
|
||||||
{
|
|
||||||
drawList.AddText(iconPos, ImGui.GetColorU32(ImGuiCol.Text), icon.ToIconString());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (toggleHovered && ImGui.IsMouseReleased(ImGuiMouseButton.Left) && !ImGui.IsMouseDragging(ImGuiMouseButton.Left))
|
|
||||||
{
|
|
||||||
toggleClicked = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isDragging && !toggleHovered)
|
|
||||||
{
|
{
|
||||||
var delta = ImGui.GetIO().MouseDelta.X / scale;
|
var delta = ImGui.GetIO().MouseDelta.X / scale;
|
||||||
leftWidth += invert ? -delta : delta;
|
leftWidth += invert ? -delta : delta;
|
||||||
leftWidth = Math.Clamp(leftWidth, minWidth, maxWidth);
|
leftWidth = Math.Clamp(leftWidth, minWidth, maxWidth);
|
||||||
}
|
}
|
||||||
|
|
||||||
drawList.PopClipRect();
|
|
||||||
|
|
||||||
ImGui.SetCursorPos(new Vector2(cursor.X + splitterWidth + ImGui.GetStyle().ItemSpacing.X, cursor.Y));
|
ImGui.SetCursorPos(new Vector2(cursor.X + splitterWidth + ImGui.GetStyle().ItemSpacing.X, cursor.Y));
|
||||||
return toggleClicked;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private (IDalamudTextureWrap? Texture, bool IsLoading, string? Error) GetTexturePreview(TextureRow row)
|
private (IDalamudTextureWrap? Texture, bool IsLoading, string? Error) GetTexturePreview(TextureRow row)
|
||||||
@@ -2231,7 +2094,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_uiSharedService.IconText(FontAwesomeIcon.Check, ImGuiColors.DalamudWhite);
|
ImGui.TextDisabled("-");
|
||||||
UiSharedService.AttachToolTip("Already stored in a compressed format; additional compression is disabled.");
|
UiSharedService.AttachToolTip("Already stored in a compressed format; additional compression is disabled.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2312,10 +2175,6 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
|
|||||||
_textureSelections[key] = target;
|
_textureSelections[key] = target;
|
||||||
currentSelection = target;
|
currentSelection = target;
|
||||||
}
|
}
|
||||||
if (TextureMetadataHelper.TryGetRecommendationInfo(target, out var targetInfo))
|
|
||||||
{
|
|
||||||
UiSharedService.AttachToolTip($"{targetInfo.Title}{UiSharedService.TooltipSeparator}{targetInfo.Description}");
|
|
||||||
}
|
|
||||||
if (targetSelected)
|
if (targetSelected)
|
||||||
{
|
{
|
||||||
ImGui.SetItemDefaultFocus();
|
ImGui.SetItemDefaultFocus();
|
||||||
|
|||||||
@@ -164,18 +164,7 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
|||||||
const float rounding = 6f;
|
const float rounding = 6f;
|
||||||
var shadowOffset = new Vector2(2, 2);
|
var shadowOffset = new Vector2(2, 2);
|
||||||
|
|
||||||
List<KeyValuePair<GameObjectHandler, Dictionary<string, FileDownloadStatus>>> transfers;
|
foreach (var transfer in _currentDownloads.ToList())
|
||||||
try
|
|
||||||
{
|
|
||||||
transfers = _currentDownloads.ToList();
|
|
||||||
}
|
|
||||||
catch (ArgumentException)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
foreach (var transfer in transfers)
|
|
||||||
{
|
{
|
||||||
var transferKey = transfer.Key;
|
var transferKey = transfer.Key;
|
||||||
var rawPos = _dalamudUtilService.WorldToScreen(transferKey.GetGameObject());
|
var rawPos = _dalamudUtilService.WorldToScreen(transferKey.GetGameObject());
|
||||||
|
|||||||
@@ -301,14 +301,6 @@ namespace LightlessSync.UI
|
|||||||
bool ShellFinderEnabled = _configService.Current.SyncshellFinderEnabled;
|
bool ShellFinderEnabled = _configService.Current.SyncshellFinderEnabled;
|
||||||
bool isBroadcasting = _broadcastService.IsBroadcasting;
|
bool isBroadcasting = _broadcastService.IsBroadcasting;
|
||||||
|
|
||||||
if (isBroadcasting)
|
|
||||||
{
|
|
||||||
var warningColor = UIColors.Get("LightlessYellow");
|
|
||||||
_uiSharedService.DrawNoteLine("! ", warningColor,
|
|
||||||
new SeStringUtils.RichTextEntry("Syncshell Finder can only be changed while Lightfinder is disabled.", warningColor));
|
|
||||||
ImGuiHelpers.ScaledDummy(0.2f);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isBroadcasting)
|
if (isBroadcasting)
|
||||||
ImGui.BeginDisabled();
|
ImGui.BeginDisabled();
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
public enum VisiblePairSortMode
|
||||||
{
|
{
|
||||||
Alphabetical = 0,
|
Default = 0,
|
||||||
VramUsage = 1,
|
Alphabetical = 1,
|
||||||
EffectiveVramUsage = 2,
|
VramUsage = 2,
|
||||||
TriangleCount = 3,
|
EffectiveVramUsage = 3,
|
||||||
PreferredDirectPairs = 4,
|
TriangleCount = 4,
|
||||||
|
PreferredDirectPairs = 5,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ using LightlessSync.PlayerData.Pairs;
|
|||||||
using LightlessSync.Services;
|
using LightlessSync.Services;
|
||||||
using LightlessSync.Services.ActorTracking;
|
using LightlessSync.Services.ActorTracking;
|
||||||
using LightlessSync.Services.Events;
|
using LightlessSync.Services.Events;
|
||||||
using LightlessSync.Services.LightFinder;
|
|
||||||
using LightlessSync.Services.Mediator;
|
using LightlessSync.Services.Mediator;
|
||||||
using LightlessSync.Services.PairProcessing;
|
using LightlessSync.Services.PairProcessing;
|
||||||
using LightlessSync.Services.ServerConfiguration;
|
using LightlessSync.Services.ServerConfiguration;
|
||||||
@@ -83,9 +82,6 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
private bool _pairDebugVisibleOnly = true;
|
private bool _pairDebugVisibleOnly = true;
|
||||||
private bool _pairDiagnosticsEnabled;
|
private bool _pairDiagnosticsEnabled;
|
||||||
private string? _selectedPairDebugUid = null;
|
private string? _selectedPairDebugUid = null;
|
||||||
private string _lightfinderIconInput = string.Empty;
|
|
||||||
private bool _lightfinderIconInputInitialized = false;
|
|
||||||
private int _lightfinderIconPresetIndex = -1;
|
|
||||||
private static readonly LightlessConfig DefaultConfig = new();
|
private static readonly LightlessConfig DefaultConfig = new();
|
||||||
private static readonly JsonSerializerOptions DebugJsonOptions = new() { WriteIndented = true };
|
private static readonly JsonSerializerOptions DebugJsonOptions = new() { WriteIndented = true };
|
||||||
private MainSettingsTab _selectedMainTab = MainSettingsTab.General;
|
private MainSettingsTab _selectedMainTab = MainSettingsTab.General;
|
||||||
@@ -126,15 +122,6 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
private const float GeneralTreeHighlightDuration = 1.5f;
|
private const float GeneralTreeHighlightDuration = 1.5f;
|
||||||
private readonly SeluneBrush _generalSeluneBrush = new();
|
private readonly SeluneBrush _generalSeluneBrush = new();
|
||||||
|
|
||||||
private static readonly (string Label, SeIconChar Icon)[] LightfinderIconPresets = new[]
|
|
||||||
{
|
|
||||||
("Link Marker", SeIconChar.LinkMarker), ("Hyadelyn", SeIconChar.Hyadelyn), ("Gil", SeIconChar.Gil),
|
|
||||||
("Quest Sync", SeIconChar.QuestSync), ("Glamoured", SeIconChar.Glamoured),
|
|
||||||
("Glamoured (Dyed)", SeIconChar.GlamouredDyed), ("Auto-Translate Open", SeIconChar.AutoTranslateOpen),
|
|
||||||
("Auto-Translate Close", SeIconChar.AutoTranslateClose), ("Boxed Star", SeIconChar.BoxedStar),
|
|
||||||
("Boxed Plus", SeIconChar.BoxedPlus)
|
|
||||||
};
|
|
||||||
|
|
||||||
private enum MainSettingsTab
|
private enum MainSettingsTab
|
||||||
{
|
{
|
||||||
General,
|
General,
|
||||||
@@ -1476,10 +1463,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
DrawPairPropertyRow("Has Handler", FormatBool(debugInfo.HasHandler));
|
DrawPairPropertyRow("Has Handler", FormatBool(debugInfo.HasHandler));
|
||||||
DrawPairPropertyRow("Handler Initialized", FormatBool(debugInfo.HandlerInitialized));
|
DrawPairPropertyRow("Handler Initialized", FormatBool(debugInfo.HandlerInitialized));
|
||||||
DrawPairPropertyRow("Handler Visible", FormatBool(debugInfo.HandlerVisible));
|
DrawPairPropertyRow("Handler Visible", FormatBool(debugInfo.HandlerVisible));
|
||||||
DrawPairPropertyRow("Last Time person rendered in", FormatTimestamp(debugInfo.InvisibleSinceUtc));
|
|
||||||
DrawPairPropertyRow("Handler Timer Temp Collection removal", FormatCountdown(debugInfo.VisibilityEvictionRemainingSeconds));
|
|
||||||
DrawPairPropertyRow("Handler Scheduled For Deletion", FormatBool(debugInfo.HandlerScheduledForDeletion));
|
DrawPairPropertyRow("Handler Scheduled For Deletion", FormatBool(debugInfo.HandlerScheduledForDeletion));
|
||||||
|
|
||||||
DrawPairPropertyRow("Note", pair.GetNote() ?? "(none)");
|
DrawPairPropertyRow("Note", pair.GetNote() ?? "(none)");
|
||||||
ImGui.EndTable();
|
ImGui.EndTable();
|
||||||
}
|
}
|
||||||
@@ -1546,11 +1530,6 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
DrawPairPropertyRow("Downloading", FormatBool(debugInfo.IsDownloading));
|
DrawPairPropertyRow("Downloading", FormatBool(debugInfo.IsDownloading));
|
||||||
DrawPairPropertyRow("Pending Downloads", debugInfo.PendingDownloadCount.ToString(CultureInfo.InvariantCulture));
|
DrawPairPropertyRow("Pending Downloads", debugInfo.PendingDownloadCount.ToString(CultureInfo.InvariantCulture));
|
||||||
DrawPairPropertyRow("Forbidden Downloads", debugInfo.ForbiddenDownloadCount.ToString(CultureInfo.InvariantCulture));
|
DrawPairPropertyRow("Forbidden Downloads", debugInfo.ForbiddenDownloadCount.ToString(CultureInfo.InvariantCulture));
|
||||||
DrawPairPropertyRow("Pending Mod Reapply", FormatBool(debugInfo.PendingModReapply));
|
|
||||||
DrawPairPropertyRow("Mod Apply Deferred", FormatBool(debugInfo.ModApplyDeferred));
|
|
||||||
DrawPairPropertyRow("Missing Critical Mods", debugInfo.MissingCriticalMods.ToString(CultureInfo.InvariantCulture));
|
|
||||||
DrawPairPropertyRow("Missing Non-Critical Mods", debugInfo.MissingNonCriticalMods.ToString(CultureInfo.InvariantCulture));
|
|
||||||
DrawPairPropertyRow("Missing Forbidden Mods", debugInfo.MissingForbiddenMods.ToString(CultureInfo.InvariantCulture));
|
|
||||||
ImGui.EndTable();
|
ImGui.EndTable();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1719,19 +1698,6 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
return value is null ? "n/a" : value.Value.ToLocalTime().ToString("G", CultureInfo.CurrentCulture);
|
return value is null ? "n/a" : value.Value.ToLocalTime().ToString("G", CultureInfo.CurrentCulture);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string? FormatCountdown(double? remainingSeconds)
|
|
||||||
{
|
|
||||||
if (!remainingSeconds.HasValue)
|
|
||||||
return "No";
|
|
||||||
|
|
||||||
var secs = Math.Max(0, remainingSeconds.Value);
|
|
||||||
var t = TimeSpan.FromSeconds(secs);
|
|
||||||
|
|
||||||
return t.TotalHours >= 1
|
|
||||||
? $"{(int)t.TotalHours:00}:{t.Minutes:00}:{t.Seconds:00}"
|
|
||||||
: $"{(int)t.TotalMinutes:00}:{t.Seconds:00}";
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string FormatBytes(long value) => value < 0 ? "n/a" : UiSharedService.ByteToString(value);
|
private static string FormatBytes(long value) => value < 0 ? "n/a" : UiSharedService.ByteToString(value);
|
||||||
|
|
||||||
private static string FormatCharacterId(uint id) => id == uint.MaxValue ? "n/a" : $"{id} (0x{id:X8})";
|
private static string FormatCharacterId(uint id) => id == uint.MaxValue ? "n/a" : $"{id} (0x{id:X8})";
|
||||||
@@ -1933,13 +1899,10 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
|
|
||||||
UiSharedService.TextWrapped(
|
UiSharedService.TextWrapped(
|
||||||
$"Storage validation is running: {_currentProgress.Item1}/{_currentProgress.Item2}");
|
$"Storage validation is running: {_currentProgress.Item1}/{_currentProgress.Item2}");
|
||||||
if (_currentProgress.Item3 != null)
|
|
||||||
{
|
|
||||||
UiSharedService.TextWrapped($"Current item: {_currentProgress.Item3.ResolvedFilepath}");
|
UiSharedService.TextWrapped($"Current item: {_currentProgress.Item3.ResolvedFilepath}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
UiSharedService.ColoredSeparator(UIColors.Get("LightlessYellow"), 1.5f);
|
UiSharedService.ColoredSeparator(UIColors.Get("LightlessYellow"), 1.5f);
|
||||||
ImGui.TreePop();
|
ImGui.TreePop();
|
||||||
@@ -2516,15 +2479,11 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
_configService.Current.LightfinderLabelUseIcon = useIcon;
|
_configService.Current.LightfinderLabelUseIcon = useIcon;
|
||||||
_configService.Save();
|
_configService.Save();
|
||||||
|
_nameplateService.RequestRedraw();
|
||||||
|
|
||||||
if (useIcon)
|
if (useIcon)
|
||||||
{
|
{
|
||||||
RefreshLightfinderIconState();
|
// redo
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_lightfinderIconInputInitialized = false;
|
|
||||||
_lightfinderIconPresetIndex = -1;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2532,89 +2491,14 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
|
|
||||||
if (useIcon)
|
if (useIcon)
|
||||||
{
|
{
|
||||||
if (!_lightfinderIconInputInitialized)
|
//redo
|
||||||
{
|
|
||||||
RefreshLightfinderIconState();
|
|
||||||
}
|
|
||||||
|
|
||||||
var currentPresetLabel = _lightfinderIconPresetIndex >= 0
|
|
||||||
? $"{GetLightfinderPresetGlyph(_lightfinderIconPresetIndex)} {LightfinderIconPresets[_lightfinderIconPresetIndex].Label}"
|
|
||||||
: "Custom";
|
|
||||||
|
|
||||||
if (ImGui.BeginCombo("Preset Icon", currentPresetLabel))
|
|
||||||
{
|
|
||||||
for (int i = 0; i < LightfinderIconPresets.Length; i++)
|
|
||||||
{
|
|
||||||
var optionGlyph = GetLightfinderPresetGlyph(i);
|
|
||||||
var preview = $"{optionGlyph} {LightfinderIconPresets[i].Label}";
|
|
||||||
var selected = i == _lightfinderIconPresetIndex;
|
|
||||||
if (ImGui.Selectable(preview, selected))
|
|
||||||
{
|
|
||||||
_lightfinderIconInput = LightFinderPlateHandler.ToIconEditorString(optionGlyph);
|
|
||||||
_lightfinderIconPresetIndex = i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ImGui.Selectable("Custom", _lightfinderIconPresetIndex == -1))
|
|
||||||
{
|
|
||||||
_lightfinderIconPresetIndex = -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui.EndCombo();
|
|
||||||
}
|
|
||||||
|
|
||||||
var editorBuffer = _lightfinderIconInput;
|
|
||||||
if (ImGui.InputText("Icon Glyph", ref editorBuffer, 16))
|
|
||||||
{
|
|
||||||
_lightfinderIconInput = editorBuffer;
|
|
||||||
_lightfinderIconPresetIndex = -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ImGui.Button("Apply Icon"))
|
|
||||||
{
|
|
||||||
var normalized = LightFinderPlateHandler.NormalizeIconGlyph(_lightfinderIconInput);
|
|
||||||
ApplyLightfinderIcon(normalized, _lightfinderIconPresetIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui.SameLine();
|
|
||||||
if (ImGui.Button("Reset Icon"))
|
|
||||||
{
|
|
||||||
var defaultGlyph = LightFinderPlateHandler.NormalizeIconGlyph(null);
|
|
||||||
var defaultIndex = -1;
|
|
||||||
for (int i = 0; i < LightfinderIconPresets.Length; i++)
|
|
||||||
{
|
|
||||||
if (string.Equals(GetLightfinderPresetGlyph(i), defaultGlyph, StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
defaultIndex = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (defaultIndex < 0)
|
|
||||||
{
|
|
||||||
defaultIndex = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
ApplyLightfinderIcon(GetLightfinderPresetGlyph(defaultIndex), defaultIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
var previewGlyph = LightFinderPlateHandler.NormalizeIconGlyph(_lightfinderIconInput);
|
|
||||||
ImGui.SameLine();
|
|
||||||
ImGui.AlignTextToFramePadding();
|
|
||||||
ImGui.Text($"Preview: {previewGlyph}");
|
|
||||||
_uiShared.DrawHelpText(
|
|
||||||
"Enter a hex code (e.g. E0BB), pick a preset, or paste an icon character directly.");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_lightfinderIconInputInitialized = false;
|
|
||||||
_lightfinderIconPresetIndex = -1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f);
|
UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f);
|
||||||
ImGui.TreePop();
|
ImGui.TreePop();
|
||||||
lightfinderTree.MarkContentEnd();
|
lightfinderTree.MarkContentEnd();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ImGui.Separator();
|
ImGui.Separator();
|
||||||
|
|
||||||
@@ -3006,7 +2890,6 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
|
|
||||||
generalSelune.DrawHighlightOnly(ImGui.GetIO().DeltaTime);
|
generalSelune.DrawHighlightOnly(ImGui.GetIO().DeltaTime);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private void DrawGeneralNavigation()
|
private void DrawGeneralNavigation()
|
||||||
{
|
{
|
||||||
@@ -4017,39 +3900,6 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string GetLightfinderPresetGlyph(int index)
|
|
||||||
{
|
|
||||||
return LightFinderPlateHandler.NormalizeIconGlyph(
|
|
||||||
SeIconCharExtensions.ToIconString(LightfinderIconPresets[index].Icon));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void RefreshLightfinderIconState()
|
|
||||||
{
|
|
||||||
var normalized = LightFinderPlateHandler.NormalizeIconGlyph(_configService.Current.LightfinderLabelIconGlyph);
|
|
||||||
_lightfinderIconInput = LightFinderPlateHandler.ToIconEditorString(normalized);
|
|
||||||
_lightfinderIconInputInitialized = true;
|
|
||||||
|
|
||||||
_lightfinderIconPresetIndex = -1;
|
|
||||||
for (int i = 0; i < LightfinderIconPresets.Length; i++)
|
|
||||||
{
|
|
||||||
if (string.Equals(GetLightfinderPresetGlyph(i), normalized, StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
_lightfinderIconPresetIndex = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ApplyLightfinderIcon(string normalizedGlyph, int presetIndex)
|
|
||||||
{
|
|
||||||
_configService.Current.LightfinderLabelIconGlyph = normalizedGlyph;
|
|
||||||
_configService.Save();
|
|
||||||
_lightfinderIconInput = LightFinderPlateHandler.ToIconEditorString(normalizedGlyph);
|
|
||||||
_lightfinderIconPresetIndex = presetIndex;
|
|
||||||
_lightfinderIconInputInitialized = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void DrawServerServiceConfiguration(ServerStorage selectedServer, ref bool useOauth)
|
private void DrawServerServiceConfiguration(ServerStorage selectedServer, ref bool useOauth)
|
||||||
{
|
{
|
||||||
var serverName = selectedServer.ServerName;
|
var serverName = selectedServer.ServerName;
|
||||||
|
|||||||
@@ -222,9 +222,7 @@ public class AnimatedHeader
|
|||||||
|
|
||||||
if (ImGui.IsItemHovered() && !string.IsNullOrEmpty(button.Tooltip))
|
if (ImGui.IsItemHovered() && !string.IsNullOrEmpty(button.Tooltip))
|
||||||
{
|
{
|
||||||
ImGui.PushFont(UiBuilder.DefaultFont);
|
|
||||||
ImGui.SetTooltip(button.Tooltip);
|
ImGui.SetTooltip(button.Tooltip);
|
||||||
ImGui.PopFont();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
currentX -= buttonSize.X + spacing;
|
currentX -= buttonSize.X + spacing;
|
||||||
|
|||||||
@@ -297,25 +297,6 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
|
|||||||
var ownerTab = ImRaii.TabItem("Owner Settings");
|
var ownerTab = ImRaii.TabItem("Owner Settings");
|
||||||
if (ownerTab)
|
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.AlignTextToFramePadding();
|
||||||
ImGui.TextUnformatted("New Password");
|
ImGui.TextUnformatted("New Password");
|
||||||
var availableWidth = ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X;
|
var availableWidth = ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X;
|
||||||
|
|||||||
@@ -140,10 +140,19 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var broadcasts = _broadcastScannerService.GetActiveSyncshellBroadcasts().ToList() ?? [];
|
string? myHashedCid = null;
|
||||||
_broadcastScannerService.TryGetLocalHashedCid(out var localHashedCid);
|
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)
|
foreach (var shell in _nearbySyncshells)
|
||||||
{
|
{
|
||||||
@@ -176,15 +185,9 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
|
|||||||
broadcasterName = !string.IsNullOrEmpty(worldName)
|
broadcasterName = !string.IsNullOrEmpty(worldName)
|
||||||
? $"{name} ({worldName})"
|
? $"{name} ({worldName})"
|
||||||
: name;
|
: 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)
|
if (cardData.Count == 0)
|
||||||
@@ -207,7 +210,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
|
|||||||
DrawConfirmation();
|
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;
|
const int shellsPerPage = 3;
|
||||||
var totalPages = (int)Math.Ceiling(listData.Count / (float)shellsPerPage);
|
var totalPages = (int)Math.Ceiling(listData.Count / (float)shellsPerPage);
|
||||||
@@ -224,10 +227,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
|
|||||||
|
|
||||||
for (int index = firstIndex; index < lastExclusive; index++)
|
for (int index = firstIndex; index < lastExclusive; index++)
|
||||||
{
|
{
|
||||||
var (shell, broadcasterName, isSelfBroadcast) = listData[index];
|
var (shell, broadcasterName) = listData[index];
|
||||||
var broadcasterLabel = string.IsNullOrEmpty(broadcasterName)
|
|
||||||
? (isSelfBroadcast ? "You" : string.Empty)
|
|
||||||
: (isSelfBroadcast ? $"{broadcasterName} (You)" : broadcasterName);
|
|
||||||
|
|
||||||
ImGui.PushID(shell.Group.GID);
|
ImGui.PushID(shell.Group.GID);
|
||||||
float rowHeight = 74f * ImGuiHelpers.GlobalScale;
|
float rowHeight = 74f * ImGuiHelpers.GlobalScale;
|
||||||
@@ -239,7 +239,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
|
|||||||
var style = ImGui.GetStyle();
|
var style = ImGui.GetStyle();
|
||||||
float startX = ImGui.GetCursorPosX();
|
float startX = ImGui.GetCursorPosX();
|
||||||
float regionW = ImGui.GetContentRegionAvail().X;
|
float regionW = ImGui.GetContentRegionAvail().X;
|
||||||
float rightTxtW = ImGui.CalcTextSize(broadcasterLabel).X;
|
float rightTxtW = ImGui.CalcTextSize(broadcasterName).X;
|
||||||
|
|
||||||
_uiSharedService.MediumText(displayName, UIColors.Get("LightlessPurple"));
|
_uiSharedService.MediumText(displayName, UIColors.Get("LightlessPurple"));
|
||||||
if (ImGui.IsItemHovered())
|
if (ImGui.IsItemHovered())
|
||||||
@@ -252,7 +252,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
|
|||||||
float rightX = startX + regionW - rightTxtW - style.ItemSpacing.X;
|
float rightX = startX + regionW - rightTxtW - style.ItemSpacing.X;
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
ImGui.SetCursorPosX(rightX);
|
ImGui.SetCursorPosX(rightX);
|
||||||
ImGui.TextUnformatted(broadcasterLabel);
|
ImGui.TextUnformatted(broadcasterName);
|
||||||
if (ImGui.IsItemHovered())
|
if (ImGui.IsItemHovered())
|
||||||
ImGui.SetTooltip("Broadcaster of the syncshell.");
|
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);
|
float joinX = rowStartLocal.X + (tagsWidth > 0 ? tagsWidth + style.ItemSpacing.X : 0f);
|
||||||
|
|
||||||
ImGui.SetCursorPos(new Vector2(joinX, btnBaselineY));
|
ImGui.SetCursorPos(new Vector2(joinX, btnBaselineY));
|
||||||
DrawJoinButton(shell, isSelfBroadcast);
|
DrawJoinButton(shell);
|
||||||
|
|
||||||
float btnHeight = ImGui.GetFrameHeightWithSpacing();
|
float btnHeight = ImGui.GetFrameHeightWithSpacing();
|
||||||
float rowHeightUsed = MathF.Max(tagsHeight, btnHeight);
|
float rowHeightUsed = MathF.Max(tagsHeight, btnHeight);
|
||||||
@@ -311,7 +311,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
|
|||||||
DrawPagination(totalPages);
|
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;
|
const int shellsPerPage = 4;
|
||||||
var totalPages = (int)Math.Ceiling(cardData.Count / (float)shellsPerPage);
|
var totalPages = (int)Math.Ceiling(cardData.Count / (float)shellsPerPage);
|
||||||
@@ -336,10 +336,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
|
|||||||
for (int index = firstIndex; index < lastExclusive; index++)
|
for (int index = firstIndex; index < lastExclusive; index++)
|
||||||
{
|
{
|
||||||
var localIndex = index - firstIndex;
|
var localIndex = index - firstIndex;
|
||||||
var (shell, broadcasterName, isSelfBroadcast) = cardData[index];
|
var (shell, broadcasterName) = cardData[index];
|
||||||
var broadcasterLabel = string.IsNullOrEmpty(broadcasterName)
|
|
||||||
? (isSelfBroadcast ? "You" : string.Empty)
|
|
||||||
: (isSelfBroadcast ? $"{broadcasterName} (You)" : broadcasterName);
|
|
||||||
|
|
||||||
if (localIndex % 2 != 0)
|
if (localIndex % 2 != 0)
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
@@ -353,9 +350,9 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
|
|||||||
? shell.Group.Alias
|
? shell.Group.Alias
|
||||||
: shell.Group.GID;
|
: shell.Group.GID;
|
||||||
|
|
||||||
var style = ImGui.GetStyle();
|
|
||||||
float startX = ImGui.GetCursorPosX();
|
float startX = ImGui.GetCursorPosX();
|
||||||
float availW = ImGui.GetContentRegionAvail().X;
|
float availWidth = ImGui.GetContentRegionAvail().X;
|
||||||
|
float rightTextW = ImGui.CalcTextSize(broadcasterName).X;
|
||||||
|
|
||||||
ImGui.BeginGroup();
|
ImGui.BeginGroup();
|
||||||
|
|
||||||
@@ -367,45 +364,13 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
|
|||||||
Mediator.Publish(new GroupProfileOpenStandaloneMessage(shell.Group));
|
Mediator.Publish(new GroupProfileOpenStandaloneMessage(shell.Group));
|
||||||
}
|
}
|
||||||
|
|
||||||
float nameRightX = ImGui.GetItemRectMax().X;
|
|
||||||
|
|
||||||
var regionMinScreen = ImGui.GetCursorScreenPos();
|
|
||||||
float regionRightX = regionMinScreen.X + availW;
|
|
||||||
|
|
||||||
float minBroadcasterX = nameRightX + style.ItemSpacing.X;
|
|
||||||
|
|
||||||
float maxBroadcasterWidth = regionRightX - minBroadcasterX;
|
|
||||||
|
|
||||||
string broadcasterToShow = 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();
|
ImGui.SameLine();
|
||||||
var curPos = ImGui.GetCursorPos();
|
float rightX = startX + availWidth - rightTextW;
|
||||||
ImGui.SetCursorPos(new Vector2(broadX - regionMinScreen.X + startX, curPos.Y + 3f * ImGuiHelpers.GlobalScale));
|
var pos = ImGui.GetCursorPos();
|
||||||
ImGui.TextUnformatted(broadcasterToShow);
|
ImGui.SetCursorPos(new Vector2(rightX, pos.Y + 3f * ImGuiHelpers.GlobalScale));
|
||||||
|
ImGui.TextUnformatted(broadcasterName);
|
||||||
if (ImGui.IsItemHovered())
|
if (ImGui.IsItemHovered())
|
||||||
ImGui.SetTooltip(toolTip);
|
ImGui.SetTooltip("Broadcaster of the syncshell.");
|
||||||
}
|
|
||||||
|
|
||||||
ImGui.EndGroup();
|
ImGui.EndGroup();
|
||||||
|
|
||||||
@@ -446,7 +411,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
|
|||||||
if (remainingY > 0)
|
if (remainingY > 0)
|
||||||
ImGui.Dummy(new Vector2(0, remainingY));
|
ImGui.Dummy(new Vector2(0, remainingY));
|
||||||
|
|
||||||
DrawJoinButton(shell, isSelfBroadcast);
|
DrawJoinButton(shell);
|
||||||
|
|
||||||
ImGui.EndChild();
|
ImGui.EndChild();
|
||||||
ImGui.EndGroup();
|
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";
|
const string visibleLabel = "Join";
|
||||||
var label = $"{visibleLabel}##{shell.Group.GID}";
|
var label = $"{visibleLabel}##{shell.Group.GID}";
|
||||||
@@ -520,7 +485,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
|
|||||||
buttonSize = new Vector2(-1, 0);
|
buttonSize = new Vector2(-1, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isAlreadyMember && !isRecentlyJoined && !isSelfBroadcast)
|
if (!isAlreadyMember && !isRecentlyJoined)
|
||||||
{
|
{
|
||||||
ImGui.PushStyleColor(ImGuiCol.Button, UIColors.Get("LightlessGreen"));
|
ImGui.PushStyleColor(ImGuiCol.Button, UIColors.Get("LightlessGreen"));
|
||||||
ImGui.PushStyleColor(ImGuiCol.ButtonHovered, UIColors.Get("LightlessGreen").WithAlpha(0.85f));
|
ImGui.PushStyleColor(ImGuiCol.ButtonHovered, UIColors.Get("LightlessGreen").WithAlpha(0.85f));
|
||||||
@@ -570,9 +535,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
|
|||||||
ImGui.Button(label, buttonSize);
|
ImGui.Button(label, buttonSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
UiSharedService.AttachToolTip(isSelfBroadcast
|
UiSharedService.AttachToolTip("Already a member or owner of this Syncshell.");
|
||||||
? "This is your own Syncshell."
|
|
||||||
: "Already a member or owner of this Syncshell.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui.PopStyleColor(3);
|
ImGui.PopStyleColor(3);
|
||||||
@@ -627,40 +590,6 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
|
|||||||
float widthUsed = cursorLocalX - baseLocal.X;
|
float widthUsed = cursorLocalX - baseLocal.X;
|
||||||
return (widthUsed, rowHeight);
|
return (widthUsed, rowHeight);
|
||||||
}
|
}
|
||||||
private static string TruncateTextToWidth(string text, float maxWidth)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(text))
|
|
||||||
return text;
|
|
||||||
|
|
||||||
const string ellipsis = "...";
|
|
||||||
float ellipsisWidth = ImGui.CalcTextSize(ellipsis).X;
|
|
||||||
|
|
||||||
if (maxWidth <= ellipsisWidth)
|
|
||||||
return ellipsis;
|
|
||||||
|
|
||||||
int low = 0;
|
|
||||||
int high = text.Length;
|
|
||||||
string best = ellipsis;
|
|
||||||
|
|
||||||
while (low <= high)
|
|
||||||
{
|
|
||||||
int mid = (low + high) / 2;
|
|
||||||
string candidate = string.Concat(text.AsSpan(0, mid), ellipsis);
|
|
||||||
float width = ImGui.CalcTextSize(candidate).X;
|
|
||||||
|
|
||||||
if (width <= maxWidth)
|
|
||||||
{
|
|
||||||
best = candidate;
|
|
||||||
low = mid + 1;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
high = mid - 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return best;
|
|
||||||
}
|
|
||||||
|
|
||||||
private IDalamudTextureWrap? GetIconWrap(uint iconId)
|
private IDalamudTextureWrap? GetIconWrap(uint iconId)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -800,9 +800,22 @@ public class TopTabMenu
|
|||||||
if (!_lightFinderService.IsBroadcasting)
|
if (!_lightFinderService.IsBroadcasting)
|
||||||
return "Syncshell Finder";
|
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
|
var nearbyCount = _lightFinderScannerService
|
||||||
.GetActiveSyncshellBroadcasts(excludeLocal: true)
|
.GetActiveSyncshellBroadcasts()
|
||||||
.Where(b => !string.IsNullOrEmpty(b.GID))
|
.Where(b =>
|
||||||
|
!string.IsNullOrEmpty(b.GID) &&
|
||||||
|
!string.Equals(b.HashedCID, myHashedCid, StringComparison.Ordinal))
|
||||||
.Select(b => b.GID!)
|
.Select(b => b.GID!)
|
||||||
.Distinct(StringComparer.Ordinal)
|
.Distinct(StringComparer.Ordinal)
|
||||||
.Count();
|
.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 != null && !_discordOAuthGetCode.IsCompleted)
|
||||||
else if (!_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.");
|
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"))
|
if (IconTextButton(FontAwesomeIcon.Ban, "Cancel Authentication"))
|
||||||
@@ -965,7 +962,7 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
|
|||||||
_discordOAuthGetCode = null;
|
_discordOAuthGetCode = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else if (_discordOAuthGetCode != null && _discordOAuthGetCode.IsCompleted)
|
||||||
{
|
{
|
||||||
TextWrapped("Discord OAuth is completed, status: ");
|
TextWrapped("Discord OAuth is completed, status: ");
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
|
|||||||
@@ -11,13 +11,9 @@ using LightlessSync.LightlessConfiguration;
|
|||||||
using LightlessSync.LightlessConfiguration.Models;
|
using LightlessSync.LightlessConfiguration.Models;
|
||||||
using LightlessSync.Services;
|
using LightlessSync.Services;
|
||||||
using LightlessSync.Services.Chat;
|
using LightlessSync.Services.Chat;
|
||||||
using LightlessSync.Services.LightFinder;
|
|
||||||
using LightlessSync.Services.Mediator;
|
using LightlessSync.Services.Mediator;
|
||||||
using LightlessSync.Services.ServerConfiguration;
|
|
||||||
using LightlessSync.UI.Services;
|
using LightlessSync.UI.Services;
|
||||||
using LightlessSync.UI.Style;
|
|
||||||
using LightlessSync.Utils;
|
using LightlessSync.Utils;
|
||||||
using OtterGui.Text;
|
|
||||||
using LightlessSync.WebAPI;
|
using LightlessSync.WebAPI;
|
||||||
using LightlessSync.WebAPI.SignalR.Utils;
|
using LightlessSync.WebAPI.SignalR.Utils;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@@ -27,49 +23,35 @@ namespace LightlessSync.UI;
|
|||||||
public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
||||||
{
|
{
|
||||||
private const string ChatDisabledStatus = "Chat services disabled";
|
private const string ChatDisabledStatus = "Chat services disabled";
|
||||||
private const string ZoneUnavailableStatus = "Zone chat is only available in major cities.";
|
|
||||||
private const string SettingsPopupId = "zone_chat_settings_popup";
|
private const string SettingsPopupId = "zone_chat_settings_popup";
|
||||||
private const string ReportPopupId = "Report Message##zone_chat_report_popup";
|
private const string ReportPopupId = "Report Message##zone_chat_report_popup";
|
||||||
private const string ChannelDragPayloadId = "zone_chat_channel_drag";
|
|
||||||
private const float DefaultWindowOpacity = .97f;
|
private const float DefaultWindowOpacity = .97f;
|
||||||
private const float DefaultUnfocusedWindowOpacity = 0.6f;
|
|
||||||
private const float MinWindowOpacity = 0.05f;
|
private const float MinWindowOpacity = 0.05f;
|
||||||
private const float MaxWindowOpacity = 1f;
|
private const float MaxWindowOpacity = 1f;
|
||||||
private const float MinChatFontScale = 0.75f;
|
private const float MinChatFontScale = 0.75f;
|
||||||
private const float MaxChatFontScale = 1.5f;
|
private const float MaxChatFontScale = 1.5f;
|
||||||
private const float UnfocusedFadeOutSpeed = 0.22f;
|
|
||||||
private const float FocusFadeInSpeed = 2.0f;
|
|
||||||
private const int ReportReasonMaxLength = 500;
|
private const int ReportReasonMaxLength = 500;
|
||||||
private const int ReportContextMaxLength = 1000;
|
private const int ReportContextMaxLength = 1000;
|
||||||
private const int MaxChannelNoteTabLength = 25;
|
|
||||||
|
|
||||||
private readonly UiSharedService _uiSharedService;
|
private readonly UiSharedService _uiSharedService;
|
||||||
private readonly ZoneChatService _zoneChatService;
|
private readonly ZoneChatService _zoneChatService;
|
||||||
private readonly PairUiService _pairUiService;
|
private readonly PairUiService _pairUiService;
|
||||||
private readonly LightFinderService _lightFinderService;
|
|
||||||
private readonly LightlessProfileManager _profileManager;
|
private readonly LightlessProfileManager _profileManager;
|
||||||
private readonly ApiController _apiController;
|
private readonly ApiController _apiController;
|
||||||
private readonly ChatConfigService _chatConfigService;
|
private readonly ChatConfigService _chatConfigService;
|
||||||
private readonly ServerConfigurationManager _serverConfigurationManager;
|
|
||||||
private readonly DalamudUtilService _dalamudUtilService;
|
|
||||||
private readonly IUiBuilder _uiBuilder;
|
|
||||||
private readonly Dictionary<string, string> _draftMessages = new(StringComparer.Ordinal);
|
private readonly Dictionary<string, string> _draftMessages = new(StringComparer.Ordinal);
|
||||||
private readonly ImGuiWindowFlags _unpinnedWindowFlags;
|
private readonly ImGuiWindowFlags _unpinnedWindowFlags;
|
||||||
private float _currentWindowOpacity = DefaultWindowOpacity;
|
private float _currentWindowOpacity = DefaultWindowOpacity;
|
||||||
private float _baseWindowOpacity = DefaultWindowOpacity;
|
|
||||||
private bool _isWindowPinned;
|
private bool _isWindowPinned;
|
||||||
private bool _showRulesOverlay;
|
private bool _showRulesOverlay;
|
||||||
private bool _refocusChatInput;
|
private bool _refocusChatInput;
|
||||||
private string? _refocusChatInputKey;
|
private string? _refocusChatInputKey;
|
||||||
private bool _isWindowFocused = true;
|
|
||||||
private int _titleBarStylePopCount;
|
|
||||||
|
|
||||||
private string? _selectedChannelKey;
|
private string? _selectedChannelKey;
|
||||||
private bool _scrollToBottom = true;
|
private bool _scrollToBottom = true;
|
||||||
private float? _pendingChannelScroll;
|
private float? _pendingChannelScroll;
|
||||||
private float _channelScroll;
|
private float _channelScroll;
|
||||||
private float _channelScrollMax;
|
private float _channelScrollMax;
|
||||||
private readonly SeluneBrush _seluneBrush = new();
|
|
||||||
private ChatChannelSnapshot? _reportTargetChannel;
|
private ChatChannelSnapshot? _reportTargetChannel;
|
||||||
private ChatMessageEntry? _reportTargetMessage;
|
private ChatMessageEntry? _reportTargetMessage;
|
||||||
private string _reportReason = string.Empty;
|
private string _reportReason = string.Empty;
|
||||||
@@ -79,11 +61,6 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
|||||||
private bool _reportSubmitting;
|
private bool _reportSubmitting;
|
||||||
private string? _reportError;
|
private string? _reportError;
|
||||||
private ChatReportResult? _reportSubmissionResult;
|
private ChatReportResult? _reportSubmissionResult;
|
||||||
private string? _dragChannelKey;
|
|
||||||
private string? _dragHoverKey;
|
|
||||||
private bool _HideStateActive;
|
|
||||||
private bool _HideStateWasOpen;
|
|
||||||
private bool _pushedStyle;
|
|
||||||
|
|
||||||
public ZoneChatUi(
|
public ZoneChatUi(
|
||||||
ILogger<ZoneChatUi> logger,
|
ILogger<ZoneChatUi> logger,
|
||||||
@@ -91,12 +68,8 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
|||||||
UiSharedService uiSharedService,
|
UiSharedService uiSharedService,
|
||||||
ZoneChatService zoneChatService,
|
ZoneChatService zoneChatService,
|
||||||
PairUiService pairUiService,
|
PairUiService pairUiService,
|
||||||
LightFinderService lightFinderService,
|
|
||||||
LightlessProfileManager profileManager,
|
LightlessProfileManager profileManager,
|
||||||
ChatConfigService chatConfigService,
|
ChatConfigService chatConfigService,
|
||||||
ServerConfigurationManager serverConfigurationManager,
|
|
||||||
DalamudUtilService dalamudUtilService,
|
|
||||||
IUiBuilder uiBuilder,
|
|
||||||
ApiController apiController,
|
ApiController apiController,
|
||||||
PerformanceCollectorService performanceCollectorService)
|
PerformanceCollectorService performanceCollectorService)
|
||||||
: base(logger, mediator, "Lightless Chat", performanceCollectorService)
|
: base(logger, mediator, "Lightless Chat", performanceCollectorService)
|
||||||
@@ -104,12 +77,8 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
|||||||
_uiSharedService = uiSharedService;
|
_uiSharedService = uiSharedService;
|
||||||
_zoneChatService = zoneChatService;
|
_zoneChatService = zoneChatService;
|
||||||
_pairUiService = pairUiService;
|
_pairUiService = pairUiService;
|
||||||
_lightFinderService = lightFinderService;
|
|
||||||
_profileManager = profileManager;
|
_profileManager = profileManager;
|
||||||
_chatConfigService = chatConfigService;
|
_chatConfigService = chatConfigService;
|
||||||
_serverConfigurationManager = serverConfigurationManager;
|
|
||||||
_dalamudUtilService = dalamudUtilService;
|
|
||||||
_uiBuilder = uiBuilder;
|
|
||||||
_apiController = apiController;
|
_apiController = apiController;
|
||||||
_isWindowPinned = _chatConfigService.Current.IsWindowPinned;
|
_isWindowPinned = _chatConfigService.Current.IsWindowPinned;
|
||||||
_showRulesOverlay = _chatConfigService.Current.ShowRulesOverlayOnOpen;
|
_showRulesOverlay = _chatConfigService.Current.ShowRulesOverlayOnOpen;
|
||||||
@@ -119,7 +88,6 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
_unpinnedWindowFlags = Flags;
|
_unpinnedWindowFlags = Flags;
|
||||||
RefreshWindowFlags();
|
RefreshWindowFlags();
|
||||||
ApplyUiVisibilitySettings();
|
|
||||||
Size = new Vector2(450, 420) * ImGuiHelpers.GlobalScale;
|
Size = new Vector2(450, 420) * ImGuiHelpers.GlobalScale;
|
||||||
SizeCondition = ImGuiCond.FirstUseEver;
|
SizeCondition = ImGuiCond.FirstUseEver;
|
||||||
WindowBuilder.For(this)
|
WindowBuilder.For(this)
|
||||||
@@ -130,112 +98,20 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
|||||||
|
|
||||||
Mediator.Subscribe<ChatChannelMessageAdded>(this, OnChatChannelMessageAdded);
|
Mediator.Subscribe<ChatChannelMessageAdded>(this, OnChatChannelMessageAdded);
|
||||||
Mediator.Subscribe<ChatChannelsUpdated>(this, _ => _scrollToBottom = true);
|
Mediator.Subscribe<ChatChannelsUpdated>(this, _ => _scrollToBottom = true);
|
||||||
Mediator.Subscribe<PriorityFrameworkUpdateMessage>(this, _ => UpdateHideState());
|
|
||||||
Mediator.Subscribe<CutsceneFrameworkUpdateMessage>(this, _ => UpdateHideState());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void PreDraw()
|
public override void PreDraw()
|
||||||
{
|
{
|
||||||
RefreshWindowFlags();
|
RefreshWindowFlags();
|
||||||
base.PreDraw();
|
base.PreDraw();
|
||||||
var config = _chatConfigService.Current;
|
_currentWindowOpacity = Math.Clamp(_chatConfigService.Current.ChatWindowOpacity, MinWindowOpacity, MaxWindowOpacity);
|
||||||
var baseOpacity = Math.Clamp(config.ChatWindowOpacity, MinWindowOpacity, MaxWindowOpacity);
|
|
||||||
_baseWindowOpacity = baseOpacity;
|
|
||||||
ImGui.PushStyleVar(ImGuiStyleVar.WindowBorderSize, 0);
|
|
||||||
_pushedStyle = true;
|
|
||||||
|
|
||||||
if (config.FadeWhenUnfocused)
|
|
||||||
{
|
|
||||||
var unfocusedOpacity = Math.Clamp(config.UnfocusedWindowOpacity, MinWindowOpacity, MaxWindowOpacity);
|
|
||||||
var targetOpacity = _isWindowFocused ? baseOpacity : Math.Min(baseOpacity, unfocusedOpacity);
|
|
||||||
var delta = ImGui.GetIO().DeltaTime;
|
|
||||||
var speed = _isWindowFocused ? FocusFadeInSpeed : UnfocusedFadeOutSpeed;
|
|
||||||
_currentWindowOpacity = MoveTowards(_currentWindowOpacity, targetOpacity, speed * delta);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_currentWindowOpacity = baseOpacity;
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui.SetNextWindowBgAlpha(_currentWindowOpacity);
|
ImGui.SetNextWindowBgAlpha(_currentWindowOpacity);
|
||||||
PushTitleBarFadeColors(_currentWindowOpacity);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateHideState()
|
|
||||||
{
|
|
||||||
ApplyUiVisibilitySettings();
|
|
||||||
var shouldHide = ShouldHide();
|
|
||||||
if (shouldHide)
|
|
||||||
{
|
|
||||||
_HideStateWasOpen |= IsOpen;
|
|
||||||
if (IsOpen)
|
|
||||||
{
|
|
||||||
IsOpen = false;
|
|
||||||
}
|
|
||||||
_HideStateActive = true;
|
|
||||||
}
|
|
||||||
else if (_HideStateActive)
|
|
||||||
{
|
|
||||||
if (_HideStateWasOpen)
|
|
||||||
{
|
|
||||||
IsOpen = true;
|
|
||||||
}
|
|
||||||
_HideStateActive = false;
|
|
||||||
_HideStateWasOpen = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ApplyUiVisibilitySettings()
|
|
||||||
{
|
|
||||||
var config = _chatConfigService.Current;
|
|
||||||
_uiBuilder.DisableAutomaticUiHide = config.ShowWhenUiHidden;
|
|
||||||
_uiBuilder.DisableCutsceneUiHide = config.ShowInCutscenes;
|
|
||||||
_uiBuilder.DisableGposeUiHide = config.ShowInGpose;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool ShouldHide()
|
|
||||||
{
|
|
||||||
var config = _chatConfigService.Current;
|
|
||||||
|
|
||||||
if (config.HideInCombat && _dalamudUtilService.IsInCombat)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config.HideInDuty && _dalamudUtilService.IsInDuty && !_dalamudUtilService.IsInFieldOperation)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void DrawInternal()
|
protected override void DrawInternal()
|
||||||
{
|
{
|
||||||
var config = _chatConfigService.Current;
|
|
||||||
var isFocused = ImGui.IsWindowFocused(ImGuiFocusedFlags.RootAndChildWindows);
|
|
||||||
var isHovered = ImGui.IsWindowHovered(ImGuiHoveredFlags.RootAndChildWindows);
|
|
||||||
if (config.FadeWhenUnfocused && isHovered && !isFocused)
|
|
||||||
{
|
|
||||||
ImGui.SetWindowFocus();
|
|
||||||
}
|
|
||||||
|
|
||||||
_isWindowFocused = config.FadeWhenUnfocused ? (isFocused || isHovered) : isFocused;
|
|
||||||
|
|
||||||
var contentAlpha = 1f;
|
|
||||||
if (config.FadeWhenUnfocused)
|
|
||||||
{
|
|
||||||
var baseOpacity = MathF.Max(_baseWindowOpacity, 0.001f);
|
|
||||||
contentAlpha = Math.Clamp(_currentWindowOpacity / baseOpacity, 0f, 1f);
|
|
||||||
}
|
|
||||||
|
|
||||||
using var alpha = ImRaii.PushStyle(ImGuiStyleVar.Alpha, contentAlpha);
|
|
||||||
var drawList = ImGui.GetWindowDrawList();
|
|
||||||
var windowPos = ImGui.GetWindowPos();
|
|
||||||
var windowSize = ImGui.GetWindowSize();
|
|
||||||
using var selune = Selune.Begin(_seluneBrush, drawList, windowPos, windowSize);
|
|
||||||
var childBgColor = ImGui.GetStyle().Colors[(int)ImGuiCol.ChildBg];
|
var childBgColor = ImGui.GetStyle().Colors[(int)ImGuiCol.ChildBg];
|
||||||
childBgColor.W *= _baseWindowOpacity;
|
childBgColor.W *= _currentWindowOpacity;
|
||||||
using var childBg = ImRaii.PushColor(ImGuiCol.ChildBg, childBgColor);
|
using var childBg = ImRaii.PushColor(ImGuiCol.ChildBg, childBgColor);
|
||||||
DrawConnectionControls();
|
DrawConnectionControls();
|
||||||
|
|
||||||
@@ -247,19 +123,16 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
|||||||
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey3);
|
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey3);
|
||||||
ImGui.TextWrapped("No chat channels available.");
|
ImGui.TextWrapped("No chat channels available.");
|
||||||
ImGui.PopStyleColor();
|
ImGui.PopStyleColor();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
EnsureSelectedChannel(channels);
|
EnsureSelectedChannel(channels);
|
||||||
CleanupDrafts(channels);
|
CleanupDrafts(channels);
|
||||||
|
|
||||||
DrawChannelButtons(channels);
|
DrawChannelButtons(channels);
|
||||||
|
|
||||||
if (_selectedChannelKey is null)
|
if (_selectedChannelKey is null)
|
||||||
{
|
|
||||||
selune.DrawHighlightOnly(ImGui.GetIO().DeltaTime);
|
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
var activeChannel = channels.FirstOrDefault(channel => string.Equals(channel.Key, _selectedChannelKey, StringComparison.Ordinal));
|
var activeChannel = channels.FirstOrDefault(channel => string.Equals(channel.Key, _selectedChannelKey, StringComparison.Ordinal));
|
||||||
if (activeChannel.Equals(default(ChatChannelSnapshot)))
|
if (activeChannel.Equals(default(ChatChannelSnapshot)))
|
||||||
@@ -275,33 +148,14 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
|||||||
DrawMessageArea(activeChannel, _currentWindowOpacity);
|
DrawMessageArea(activeChannel, _currentWindowOpacity);
|
||||||
ImGui.Separator();
|
ImGui.Separator();
|
||||||
DrawInput(activeChannel);
|
DrawInput(activeChannel);
|
||||||
}
|
|
||||||
|
|
||||||
if (_showRulesOverlay)
|
if (_showRulesOverlay)
|
||||||
{
|
{
|
||||||
DrawRulesOverlay();
|
DrawRulesOverlay();
|
||||||
}
|
}
|
||||||
|
|
||||||
selune.DrawHighlightOnly(ImGui.GetIO().DeltaTime);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void PushTitleBarFadeColors(float opacity)
|
private static void DrawHeader(ChatChannelSnapshot channel)
|
||||||
{
|
|
||||||
_titleBarStylePopCount = 0;
|
|
||||||
var alpha = Math.Clamp(opacity, 0f, 1f);
|
|
||||||
var colors = ImGui.GetStyle().Colors;
|
|
||||||
|
|
||||||
var titleBg = colors[(int)ImGuiCol.TitleBg];
|
|
||||||
var titleBgActive = colors[(int)ImGuiCol.TitleBgActive];
|
|
||||||
var titleBgCollapsed = colors[(int)ImGuiCol.TitleBgCollapsed];
|
|
||||||
|
|
||||||
ImGui.PushStyleColor(ImGuiCol.TitleBg, new Vector4(titleBg.X, titleBg.Y, titleBg.Z, titleBg.W * alpha));
|
|
||||||
ImGui.PushStyleColor(ImGuiCol.TitleBgActive, new Vector4(titleBgActive.X, titleBgActive.Y, titleBgActive.Z, titleBgActive.W * alpha));
|
|
||||||
ImGui.PushStyleColor(ImGuiCol.TitleBgCollapsed, new Vector4(titleBgCollapsed.X, titleBgCollapsed.Y, titleBgCollapsed.Z, titleBgCollapsed.W * alpha));
|
|
||||||
_titleBarStylePopCount = 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DrawHeader(ChatChannelSnapshot channel)
|
|
||||||
{
|
{
|
||||||
var prefix = channel.Type == ChatChannelType.Zone ? "Zone" : "Syncshell";
|
var prefix = channel.Type == ChatChannelType.Zone ? "Zone" : "Syncshell";
|
||||||
Vector4 color;
|
Vector4 color;
|
||||||
@@ -324,18 +178,11 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
|||||||
if (channel.Type == ChatChannelType.Zone && channel.Descriptor.WorldId != 0)
|
if (channel.Type == ChatChannelType.Zone && channel.Descriptor.WorldId != 0)
|
||||||
{
|
{
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
var worldId = channel.Descriptor.WorldId;
|
ImGui.TextUnformatted($"World #{channel.Descriptor.WorldId}");
|
||||||
var worldName = _dalamudUtilService.WorldData.Value.TryGetValue(worldId, out var name) ? name : $"World #{worldId}";
|
|
||||||
ImGui.TextUnformatted(worldName);
|
|
||||||
if (ImGui.IsItemHovered())
|
|
||||||
{
|
|
||||||
ImGui.SetTooltip($"World ID: {worldId}");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var showInlineStatus = string.Equals(channel.StatusText, ChatDisabledStatus, StringComparison.OrdinalIgnoreCase)
|
var showInlineDisabled = string.Equals(channel.StatusText, ChatDisabledStatus, StringComparison.OrdinalIgnoreCase);
|
||||||
|| string.Equals(channel.StatusText, ZoneUnavailableStatus, StringComparison.OrdinalIgnoreCase);
|
if (showInlineDisabled)
|
||||||
if (showInlineStatus)
|
|
||||||
{
|
{
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey3);
|
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey3);
|
||||||
@@ -395,11 +242,7 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var itemHeight = ImGui.GetTextLineHeightWithSpacing();
|
for (var i = 0; i < channel.Messages.Count; i++)
|
||||||
using var clipper = ImUtf8.ListClipper(channel.Messages.Count, itemHeight);
|
|
||||||
while (clipper.Step())
|
|
||||||
{
|
|
||||||
for (var i = clipper.DisplayStart; i < clipper.DisplayEnd; i++)
|
|
||||||
{
|
{
|
||||||
var message = channel.Messages[i];
|
var message = channel.Messages[i];
|
||||||
ImGui.PushID(i);
|
ImGui.PushID(i);
|
||||||
@@ -447,7 +290,6 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
|||||||
ImGui.PopID();
|
ImGui.PopID();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (_scrollToBottom)
|
if (_scrollToBottom)
|
||||||
{
|
{
|
||||||
@@ -482,15 +324,6 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
|||||||
_refocusChatInputKey = null;
|
_refocusChatInputKey = null;
|
||||||
}
|
}
|
||||||
ImGui.InputText(inputId, ref draft, MaxMessageLength);
|
ImGui.InputText(inputId, ref draft, MaxMessageLength);
|
||||||
if (ImGui.IsItemActive() || ImGui.IsItemFocused())
|
|
||||||
{
|
|
||||||
var drawList = ImGui.GetWindowDrawList();
|
|
||||||
var itemMin = ImGui.GetItemRectMin();
|
|
||||||
var itemMax = ImGui.GetItemRectMax();
|
|
||||||
var highlight = UIColors.Get("LightlessPurple").WithAlpha(0.35f);
|
|
||||||
var highlightU32 = ImGui.ColorConvertFloat4ToU32(highlight);
|
|
||||||
drawList.AddRect(itemMin, itemMax, highlightU32, style.FrameRounding, ImDrawFlags.None, Math.Max(1f, ImGuiHelpers.GlobalScale));
|
|
||||||
}
|
|
||||||
var enterPressed = ImGui.IsItemFocused()
|
var enterPressed = ImGui.IsItemFocused()
|
||||||
&& (ImGui.IsKeyPressed(ImGuiKey.Enter) || ImGui.IsKeyPressed(ImGuiKey.KeypadEnter));
|
&& (ImGui.IsKeyPressed(ImGuiKey.Enter) || ImGui.IsKeyPressed(ImGuiKey.KeypadEnter));
|
||||||
_draftMessages[channel.Key] = draft;
|
_draftMessages[channel.Key] = draft;
|
||||||
@@ -647,7 +480,7 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
|||||||
|
|
||||||
ImGui.Separator();
|
ImGui.Separator();
|
||||||
_uiSharedService.MediumText("Syncshell Chat Rules", UIColors.Get("LightlessYellow"));
|
_uiSharedService.MediumText("Syncshell Chat Rules", UIColors.Get("LightlessYellow"));
|
||||||
_uiSharedService.DrawNoteLine("! ", UIColors.Get("LightlessYellow"), new SeStringUtils.RichTextEntry("Syncshell chats are self-moderated (their own set rules) by it's owner and appointed moderators."));
|
_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));
|
ImGui.Dummy(new Vector2(5));
|
||||||
|
|
||||||
@@ -826,21 +659,6 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
|||||||
ImGui.EndPopup();
|
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)
|
private void OpenReportPopup(ChatChannelSnapshot channel, ChatMessageEntry message)
|
||||||
{
|
{
|
||||||
if (message.Payload is not { } payload)
|
if (message.Payload is not { } payload)
|
||||||
@@ -1216,56 +1034,18 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
|||||||
var groupSize = ImGui.GetItemRectSize();
|
var groupSize = ImGui.GetItemRectSize();
|
||||||
var minBlockX = cursorStart.X + groupSize.X + style.ItemSpacing.X;
|
var minBlockX = cursorStart.X + groupSize.X + style.ItemSpacing.X;
|
||||||
var availableAfterGroup = contentRightX - (cursorStart.X + groupSize.X);
|
var availableAfterGroup = contentRightX - (cursorStart.X + groupSize.X);
|
||||||
var lightfinderButtonWidth = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.PersonCirclePlus).X;
|
|
||||||
var settingsButtonWidth = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Cog).X;
|
var settingsButtonWidth = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Cog).X;
|
||||||
var pinIcon = _isWindowPinned ? FontAwesomeIcon.Lock : FontAwesomeIcon.Unlock;
|
var pinIcon = _isWindowPinned ? FontAwesomeIcon.Lock : FontAwesomeIcon.Unlock;
|
||||||
var pinButtonWidth = _uiSharedService.GetIconButtonSize(pinIcon).X;
|
var pinButtonWidth = _uiSharedService.GetIconButtonSize(pinIcon).X;
|
||||||
var blockWidth = 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
|
var desiredBlockX = availableAfterGroup > blockWidth + style.ItemSpacing.X
|
||||||
? contentRightX - blockWidth
|
? contentRightX - blockWidth
|
||||||
: minBlockX;
|
: minBlockX;
|
||||||
desiredBlockX = Math.Max(cursorStart.X, desiredBlockX);
|
desiredBlockX = Math.Max(cursorStart.X, desiredBlockX);
|
||||||
var lightfinderPos = new Vector2(desiredBlockX, cursorStart.Y);
|
var rulesPos = new Vector2(desiredBlockX, cursorStart.Y);
|
||||||
var rulesPos = new Vector2(lightfinderPos.X + lightfinderButtonWidth + style.ItemSpacing.X, cursorStart.Y);
|
var settingsPos = new Vector2(desiredBlockX + rulesButtonWidth + style.ItemSpacing.X, cursorStart.Y);
|
||||||
var settingsPos = new Vector2(rulesPos.X + rulesButtonWidth + style.ItemSpacing.X, cursorStart.Y);
|
|
||||||
var pinPos = new Vector2(settingsPos.X + settingsButtonWidth + style.ItemSpacing.X, cursorStart.Y);
|
var pinPos = new Vector2(settingsPos.X + settingsButtonWidth + style.ItemSpacing.X, cursorStart.Y);
|
||||||
|
|
||||||
ImGui.SameLine();
|
|
||||||
ImGui.SetCursorPos(lightfinderPos);
|
|
||||||
var lightfinderEnabled = _lightFinderService.IsBroadcasting;
|
|
||||||
var lightfinderColor = lightfinderEnabled ? UIColors.Get("LightlessGreen") : ImGuiColors.DalamudGrey3;
|
|
||||||
var lightfinderButtonSize = new Vector2(lightfinderButtonWidth, ImGui.GetFrameHeight());
|
|
||||||
ImGui.InvisibleButton("zone_chat_lightfinder_button", lightfinderButtonSize);
|
|
||||||
var lightfinderMin = ImGui.GetItemRectMin();
|
|
||||||
var lightfinderMax = ImGui.GetItemRectMax();
|
|
||||||
var iconSize = _uiSharedService.GetIconSize(FontAwesomeIcon.PersonCirclePlus);
|
|
||||||
var iconPos = new Vector2(
|
|
||||||
lightfinderMin.X + (lightfinderButtonSize.X - iconSize.X) * 0.5f,
|
|
||||||
lightfinderMin.Y + (lightfinderButtonSize.Y - iconSize.Y) * 0.5f);
|
|
||||||
using (_uiSharedService.IconFont.Push())
|
|
||||||
{
|
|
||||||
ImGui.GetWindowDrawList().AddText(iconPos, ImGui.GetColorU32(lightfinderColor), FontAwesomeIcon.PersonCirclePlus.ToIconString());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ImGui.IsItemClicked())
|
|
||||||
{
|
|
||||||
Mediator.Publish(new UiToggleMessage(typeof(LightFinderUI)));
|
|
||||||
}
|
|
||||||
if (ImGui.IsItemHovered())
|
|
||||||
{
|
|
||||||
var padding = new Vector2(8f * ImGuiHelpers.GlobalScale);
|
|
||||||
Selune.RegisterHighlight(
|
|
||||||
lightfinderMin - padding,
|
|
||||||
lightfinderMax + padding,
|
|
||||||
SeluneHighlightMode.Point,
|
|
||||||
exactSize: true,
|
|
||||||
clipToElement: true,
|
|
||||||
clipPadding: padding,
|
|
||||||
highlightColorOverride: lightfinderColor,
|
|
||||||
highlightAlphaOverride: 0.2f);
|
|
||||||
ImGui.SetTooltip("If Lightfinder is enabled, you will be able to see the character names of other Lightfinder users in the same zone when they send a message.");
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
ImGui.SetCursorPos(rulesPos);
|
ImGui.SetCursorPos(rulesPos);
|
||||||
if (ImGui.Button("Rules", new Vector2(rulesButtonWidth, 0f)))
|
if (ImGui.Button("Rules", new Vector2(rulesButtonWidth, 0f)))
|
||||||
@@ -1407,71 +1187,6 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
|||||||
ImGui.SetTooltip("Toggles the timestamp prefix on messages.");
|
ImGui.SetTooltip("Toggles the timestamp prefix on messages.");
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui.Separator();
|
|
||||||
ImGui.TextUnformatted("Chat Visibility");
|
|
||||||
|
|
||||||
var autoHideCombat = chatConfig.HideInCombat;
|
|
||||||
if (ImGui.Checkbox("Hide in combat", ref autoHideCombat))
|
|
||||||
{
|
|
||||||
chatConfig.HideInCombat = autoHideCombat;
|
|
||||||
_chatConfigService.Save();
|
|
||||||
UpdateHideState();
|
|
||||||
}
|
|
||||||
if (ImGui.IsItemHovered())
|
|
||||||
{
|
|
||||||
ImGui.SetTooltip("Temporarily hides the chat window while in combat.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var autoHideDuty = chatConfig.HideInDuty;
|
|
||||||
if (ImGui.Checkbox("Hide in duty (Not in field operations)", ref autoHideDuty))
|
|
||||||
{
|
|
||||||
chatConfig.HideInDuty = autoHideDuty;
|
|
||||||
_chatConfigService.Save();
|
|
||||||
UpdateHideState();
|
|
||||||
}
|
|
||||||
if (ImGui.IsItemHovered())
|
|
||||||
{
|
|
||||||
ImGui.SetTooltip("Hides the chat window inside duties.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var showWhenUiHidden = chatConfig.ShowWhenUiHidden;
|
|
||||||
if (ImGui.Checkbox("Show when game UI is hidden", ref showWhenUiHidden))
|
|
||||||
{
|
|
||||||
chatConfig.ShowWhenUiHidden = showWhenUiHidden;
|
|
||||||
_chatConfigService.Save();
|
|
||||||
UpdateHideState();
|
|
||||||
}
|
|
||||||
if (ImGui.IsItemHovered())
|
|
||||||
{
|
|
||||||
ImGui.SetTooltip("Allow the chat window to remain visible when the game UI is hidden.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var showInCutscenes = chatConfig.ShowInCutscenes;
|
|
||||||
if (ImGui.Checkbox("Show in cutscenes", ref showInCutscenes))
|
|
||||||
{
|
|
||||||
chatConfig.ShowInCutscenes = showInCutscenes;
|
|
||||||
_chatConfigService.Save();
|
|
||||||
UpdateHideState();
|
|
||||||
}
|
|
||||||
if (ImGui.IsItemHovered())
|
|
||||||
{
|
|
||||||
ImGui.SetTooltip("Allow the chat window to remain visible during cutscenes.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var showInGpose = chatConfig.ShowInGpose;
|
|
||||||
if (ImGui.Checkbox("Show in group pose", ref showInGpose))
|
|
||||||
{
|
|
||||||
chatConfig.ShowInGpose = showInGpose;
|
|
||||||
_chatConfigService.Save();
|
|
||||||
UpdateHideState();
|
|
||||||
}
|
|
||||||
if (ImGui.IsItemHovered())
|
|
||||||
{
|
|
||||||
ImGui.SetTooltip("Allow the chat window to remain visible in /gpose.");
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui.Separator();
|
|
||||||
|
|
||||||
var fontScale = Math.Clamp(chatConfig.ChatFontScale, MinChatFontScale, MaxChatFontScale);
|
var fontScale = Math.Clamp(chatConfig.ChatFontScale, MinChatFontScale, MaxChatFontScale);
|
||||||
var fontScaleChanged = ImGui.SliderFloat("Message font scale", ref fontScale, MinChatFontScale, MaxChatFontScale, "%.2fx");
|
var fontScaleChanged = ImGui.SliderFloat("Message font scale", ref fontScale, MinChatFontScale, MaxChatFontScale, "%.2fx");
|
||||||
var resetFontScale = ImGui.IsItemClicked(ImGuiMouseButton.Right);
|
var resetFontScale = ImGui.IsItemClicked(ImGuiMouseButton.Right);
|
||||||
@@ -1511,55 +1226,9 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
|||||||
ImGui.SetTooltip("Adjust chat window transparency.\nRight-click to reset to default.");
|
ImGui.SetTooltip("Adjust chat window transparency.\nRight-click to reset to default.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var fadeUnfocused = chatConfig.FadeWhenUnfocused;
|
|
||||||
if (ImGui.Checkbox("Fade window when unfocused", ref fadeUnfocused))
|
|
||||||
{
|
|
||||||
chatConfig.FadeWhenUnfocused = fadeUnfocused;
|
|
||||||
_chatConfigService.Save();
|
|
||||||
}
|
|
||||||
if (ImGui.IsItemHovered())
|
|
||||||
{
|
|
||||||
ImGui.SetTooltip("When enabled, the chat window fades after it loses focus.\nHovering the window restores focus.");
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui.BeginDisabled(!fadeUnfocused);
|
|
||||||
var unfocusedOpacity = Math.Clamp(chatConfig.UnfocusedWindowOpacity, MinWindowOpacity, MaxWindowOpacity);
|
|
||||||
var unfocusedChanged = ImGui.SliderFloat("Unfocused transparency", ref unfocusedOpacity, MinWindowOpacity, MaxWindowOpacity, "%.2f");
|
|
||||||
var resetUnfocused = ImGui.IsItemClicked(ImGuiMouseButton.Right);
|
|
||||||
if (resetUnfocused)
|
|
||||||
{
|
|
||||||
unfocusedOpacity = DefaultUnfocusedWindowOpacity;
|
|
||||||
unfocusedChanged = true;
|
|
||||||
}
|
|
||||||
if (unfocusedChanged)
|
|
||||||
{
|
|
||||||
chatConfig.UnfocusedWindowOpacity = unfocusedOpacity;
|
|
||||||
_chatConfigService.Save();
|
|
||||||
}
|
|
||||||
if (ImGui.IsItemHovered())
|
|
||||||
{
|
|
||||||
ImGui.SetTooltip("Target transparency while the chat window is unfocused.\nRight-click to reset to default.");
|
|
||||||
}
|
|
||||||
ImGui.EndDisabled();
|
|
||||||
|
|
||||||
ImGui.EndPopup();
|
ImGui.EndPopup();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static float MoveTowards(float current, float target, float maxDelta)
|
|
||||||
{
|
|
||||||
if (current < target)
|
|
||||||
{
|
|
||||||
return MathF.Min(current + maxDelta, target);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (current > target)
|
|
||||||
{
|
|
||||||
return MathF.Max(current - maxDelta, target);
|
|
||||||
}
|
|
||||||
|
|
||||||
return target;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ToggleChatConnection(bool currentlyEnabled)
|
private void ToggleChatConnection(bool currentlyEnabled)
|
||||||
{
|
{
|
||||||
_ = Task.Run(async () =>
|
_ = Task.Run(async () =>
|
||||||
@@ -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 style = ImGui.GetStyle();
|
||||||
var baseFramePadding = style.FramePadding;
|
var baseFramePadding = style.FramePadding;
|
||||||
@@ -1636,8 +1305,6 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
if (child)
|
if (child)
|
||||||
{
|
{
|
||||||
var dragActive = _dragChannelKey is not null && ImGui.IsMouseDragging(ImGuiMouseButton.Left);
|
|
||||||
var hoveredTargetThisFrame = false;
|
|
||||||
var first = true;
|
var first = true;
|
||||||
foreach (var channel in channels)
|
foreach (var channel in channels)
|
||||||
{
|
{
|
||||||
@@ -1648,7 +1315,6 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
|||||||
var showBadge = !isSelected && channel.UnreadCount > 0;
|
var showBadge = !isSelected && channel.UnreadCount > 0;
|
||||||
var isZoneChannel = channel.Type == ChatChannelType.Zone;
|
var isZoneChannel = channel.Type == ChatChannelType.Zone;
|
||||||
(string Text, Vector2 TextSize, float Width, float Height)? badgeMetrics = null;
|
(string Text, Vector2 TextSize, float Width, float Height)? badgeMetrics = null;
|
||||||
var channelLabel = GetChannelTabLabel(channel);
|
|
||||||
|
|
||||||
var normal = isSelected ? UIColors.Get("LightlessPurpleDefault") : UIColors.Get("ButtonDefault");
|
var normal = isSelected ? UIColors.Get("LightlessPurpleDefault") : UIColors.Get("ButtonDefault");
|
||||||
var hovered = isSelected
|
var hovered = isSelected
|
||||||
@@ -1677,7 +1343,7 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
|||||||
badgeMetrics = (badgeText, badgeTextSize, badgeWidth, badgeHeight);
|
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)
|
if (showBadge)
|
||||||
{
|
{
|
||||||
@@ -1693,77 +1359,10 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
|||||||
_scrollToBottom = true;
|
_scrollToBottom = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ShouldShowChannelTabContextMenu(channel)
|
|
||||||
&& ImGui.BeginPopupContextItem($"chat_channel_ctx##{channel.Key}"))
|
|
||||||
{
|
|
||||||
DrawChannelTabContextMenu(channel);
|
|
||||||
ImGui.EndPopup();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ImGui.BeginDragDropSource(ImGuiDragDropFlags.None))
|
|
||||||
{
|
|
||||||
if (!string.Equals(_dragChannelKey, channel.Key, StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
_dragHoverKey = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
_dragChannelKey = channel.Key;
|
|
||||||
ImGui.SetDragDropPayload(ChannelDragPayloadId, null, 0);
|
|
||||||
ImGui.TextUnformatted(channelLabel);
|
|
||||||
ImGui.EndDragDropSource();
|
|
||||||
}
|
|
||||||
|
|
||||||
var isDragTarget = false;
|
|
||||||
|
|
||||||
if (ImGui.BeginDragDropTarget())
|
|
||||||
{
|
|
||||||
var acceptFlags = ImGuiDragDropFlags.AcceptBeforeDelivery | ImGuiDragDropFlags.AcceptNoDrawDefaultRect;
|
|
||||||
var payload = ImGui.AcceptDragDropPayload(ChannelDragPayloadId, acceptFlags);
|
|
||||||
if (!payload.IsNull && _dragChannelKey is { } draggedKey
|
|
||||||
&& !string.Equals(draggedKey, channel.Key, StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
isDragTarget = true;
|
|
||||||
if (!string.Equals(_dragHoverKey, channel.Key, StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
_dragHoverKey = channel.Key;
|
|
||||||
_zoneChatService.MoveChannel(draggedKey, channel.Key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui.EndDragDropTarget();
|
|
||||||
}
|
|
||||||
|
|
||||||
var isHoveredDuringDrag = dragActive
|
|
||||||
&& ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenBlockedByActiveItem | ImGuiHoveredFlags.AllowWhenOverlapped);
|
|
||||||
|
|
||||||
if (!isDragTarget && isHoveredDuringDrag
|
|
||||||
&& !string.Equals(_dragChannelKey, channel.Key, StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
isDragTarget = true;
|
|
||||||
if (!string.Equals(_dragHoverKey, channel.Key, StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
_dragHoverKey = channel.Key;
|
|
||||||
_zoneChatService.MoveChannel(_dragChannelKey!, channel.Key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var drawList = ImGui.GetWindowDrawList();
|
var drawList = ImGui.GetWindowDrawList();
|
||||||
var itemMin = ImGui.GetItemRectMin();
|
var itemMin = ImGui.GetItemRectMin();
|
||||||
var itemMax = ImGui.GetItemRectMax();
|
var itemMax = ImGui.GetItemRectMax();
|
||||||
|
|
||||||
if (isHoveredDuringDrag)
|
|
||||||
{
|
|
||||||
var highlight = UIColors.Get("LightlessPurple").WithAlpha(0.35f);
|
|
||||||
var highlightU32 = ImGui.ColorConvertFloat4ToU32(highlight);
|
|
||||||
drawList.AddRectFilled(itemMin, itemMax, highlightU32, style.FrameRounding);
|
|
||||||
drawList.AddRect(itemMin, itemMax, highlightU32, style.FrameRounding, ImDrawFlags.None, Math.Max(1f, ImGuiHelpers.GlobalScale));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isDragTarget)
|
|
||||||
{
|
|
||||||
hoveredTargetThisFrame = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isZoneChannel)
|
if (isZoneChannel)
|
||||||
{
|
{
|
||||||
var borderColor = UIColors.Get("LightlessOrange");
|
var borderColor = UIColors.Get("LightlessOrange");
|
||||||
@@ -1791,11 +1390,6 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
|||||||
first = false;
|
first = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dragActive && !hoveredTargetThisFrame)
|
|
||||||
{
|
|
||||||
_dragHoverKey = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_pendingChannelScroll.HasValue)
|
if (_pendingChannelScroll.HasValue)
|
||||||
{
|
{
|
||||||
ImGui.SetScrollX(_pendingChannelScroll.Value);
|
ImGui.SetScrollX(_pendingChannelScroll.Value);
|
||||||
@@ -1836,123 +1430,9 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
|||||||
_channelScroll = currentScroll;
|
_channelScroll = currentScroll;
|
||||||
_channelScrollMax = maxScroll;
|
_channelScrollMax = maxScroll;
|
||||||
|
|
||||||
if (_dragChannelKey is not null && !ImGui.IsMouseDown(ImGuiMouseButton.Left))
|
|
||||||
{
|
|
||||||
_dragChannelKey = null;
|
|
||||||
_dragHoverKey = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui.SetCursorPosY(ImGui.GetCursorPosY() - style.ItemSpacing.Y * 0.3f);
|
ImGui.SetCursorPosY(ImGui.GetCursorPosY() - style.ItemSpacing.Y * 0.3f);
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetChannelTabLabel(ChatChannelSnapshot channel)
|
|
||||||
{
|
|
||||||
if (channel.Type != ChatChannelType.Group)
|
|
||||||
{
|
|
||||||
return channel.DisplayName;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!_chatConfigService.Current.PreferNotesForChannels.TryGetValue(channel.Key, out var preferNote) || !preferNote)
|
|
||||||
{
|
|
||||||
return channel.DisplayName;
|
|
||||||
}
|
|
||||||
|
|
||||||
var note = GetChannelNote(channel);
|
|
||||||
if (string.IsNullOrWhiteSpace(note))
|
|
||||||
{
|
|
||||||
return channel.DisplayName;
|
|
||||||
}
|
|
||||||
|
|
||||||
return TruncateChannelNoteForTab(note);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string TruncateChannelNoteForTab(string note)
|
|
||||||
{
|
|
||||||
if (note.Length <= MaxChannelNoteTabLength)
|
|
||||||
{
|
|
||||||
return note;
|
|
||||||
}
|
|
||||||
|
|
||||||
var ellipsis = "...";
|
|
||||||
var maxPrefix = Math.Max(0, MaxChannelNoteTabLength - ellipsis.Length);
|
|
||||||
return note[..maxPrefix] + ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool ShouldShowChannelTabContextMenu(ChatChannelSnapshot channel)
|
|
||||||
{
|
|
||||||
if (channel.Type != ChatChannelType.Group)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_chatConfigService.Current.PreferNotesForChannels.TryGetValue(channel.Key, out var preferNote) && preferNote)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
var note = GetChannelNote(channel);
|
|
||||||
return !string.IsNullOrWhiteSpace(note);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DrawChannelTabContextMenu(ChatChannelSnapshot channel)
|
|
||||||
{
|
|
||||||
var preferNote = _chatConfigService.Current.PreferNotesForChannels.TryGetValue(channel.Key, out var value) && value;
|
|
||||||
var note = GetChannelNote(channel);
|
|
||||||
var hasNote = !string.IsNullOrWhiteSpace(note);
|
|
||||||
if (preferNote || hasNote)
|
|
||||||
{
|
|
||||||
var label = preferNote ? "Prefer Name Instead" : "Prefer Note Instead";
|
|
||||||
if (ImGui.MenuItem(label))
|
|
||||||
{
|
|
||||||
SetPreferNoteForChannel(channel.Key, !preferNote);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (preferNote)
|
|
||||||
{
|
|
||||||
ImGui.Separator();
|
|
||||||
ImGui.TextDisabled("Name:");
|
|
||||||
ImGui.TextWrapped(channel.DisplayName);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasNote)
|
|
||||||
{
|
|
||||||
ImGui.Separator();
|
|
||||||
ImGui.TextDisabled("Note:");
|
|
||||||
ImGui.TextWrapped(note);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private string? GetChannelNote(ChatChannelSnapshot channel)
|
|
||||||
{
|
|
||||||
if (channel.Type != ChatChannelType.Group)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var gid = channel.Descriptor.CustomKey;
|
|
||||||
if (string.IsNullOrWhiteSpace(gid))
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return _serverConfigurationManager.GetNoteForGid(gid);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SetPreferNoteForChannel(string channelKey, bool preferNote)
|
|
||||||
{
|
|
||||||
if (preferNote)
|
|
||||||
{
|
|
||||||
_chatConfigService.Current.PreferNotesForChannels[channelKey] = true;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_chatConfigService.Current.PreferNotesForChannels.Remove(channelKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
_chatConfigService.Save();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DrawSystemEntry(ChatMessageEntry entry)
|
private void DrawSystemEntry(ChatMessageEntry entry)
|
||||||
{
|
{
|
||||||
var system = entry.SystemMessage;
|
var system = entry.SystemMessage;
|
||||||
|
|||||||
@@ -60,6 +60,16 @@ public static class VariousExtensions
|
|||||||
CharacterData? oldData, ILogger logger, IPairPerformanceSubject cachedPlayer, bool forceApplyCustomization, bool forceApplyMods)
|
CharacterData? oldData, ILogger logger, IPairPerformanceSubject cachedPlayer, bool forceApplyCustomization, bool forceApplyMods)
|
||||||
{
|
{
|
||||||
oldData ??= new();
|
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>>();
|
var charaDataToUpdate = new Dictionary<ObjectKind, HashSet<PlayerChanges>>();
|
||||||
foreach (ObjectKind objectKind in Enum.GetValues<ObjectKind>())
|
foreach (ObjectKind objectKind in Enum.GetValues<ObjectKind>())
|
||||||
@@ -95,7 +105,7 @@ public static class VariousExtensions
|
|||||||
{
|
{
|
||||||
var oldList = oldData.FileReplacements[objectKind];
|
var oldList = oldData.FileReplacements[objectKind];
|
||||||
var newList = newData.FileReplacements[objectKind];
|
var newList = newData.FileReplacements[objectKind];
|
||||||
var listsAreEqual = oldList.SequenceEqual(newList, PlayerData.Data.FileReplacementDataComparer.Instance);
|
var listsAreEqual = FileReplacementsEquivalent(oldList, newList);
|
||||||
if (!listsAreEqual || forceApplyMods)
|
if (!listsAreEqual || forceApplyMods)
|
||||||
{
|
{
|
||||||
logger.LogDebug("[BASE-{appBase}] Updating {object}/{kind} (FileReplacements not equal) => {change}", applicationBase, cachedPlayer, objectKind, PlayerChanges.ModFiles);
|
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();
|
.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)))
|
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();
|
.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();
|
.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();
|
.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,
|
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;
|
if (objectKind != ObjectKind.Player) continue;
|
||||||
|
|
||||||
bool manipDataDifferent = !string.Equals(oldData.ManipulationData, newData.ManipulationData, StringComparison.Ordinal);
|
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);
|
logger.LogDebug("[BASE-{appBase}] Updating {object}/{kind} (Diff manip data) => {change}", applicationBase, cachedPlayer, objectKind, PlayerChanges.ModManip);
|
||||||
charaDataToUpdate[objectKind].Add(PlayerChanges.ModManip);
|
charaDataToUpdate[objectKind].Add(PlayerChanges.ModManip);
|
||||||
|
|||||||
@@ -563,7 +563,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
if (directDownloads.Count > 0 || downloadBatches.Length > 0)
|
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)
|
if (gameObjectHandler is not null)
|
||||||
|
|||||||
@@ -418,7 +418,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
|
|||||||
|
|
||||||
public Task CyclePauseAsync(PairUniqueIdentifier ident)
|
public Task CyclePauseAsync(PairUniqueIdentifier ident)
|
||||||
{
|
{
|
||||||
var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(8));
|
||||||
_ = Task.Run(async () =>
|
_ = Task.Run(async () =>
|
||||||
{
|
{
|
||||||
var token = timeoutCts.Token;
|
var token = timeoutCts.Token;
|
||||||
@@ -430,19 +430,20 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var targetPermissions = entry.SelfPermissions;
|
var originalPermissions = entry.SelfPermissions;
|
||||||
targetPermissions.SetPaused(paused: true);
|
var targetPermissions = originalPermissions;
|
||||||
|
targetPermissions.SetPaused(!originalPermissions.IsPaused());
|
||||||
|
|
||||||
await UserSetPairPermissions(new UserPermissionsDto(entry.User, targetPermissions)).ConfigureAwait(false);
|
await UserSetPairPermissions(new UserPermissionsDto(entry.User, targetPermissions)).ConfigureAwait(false);
|
||||||
|
|
||||||
var pauseApplied = false;
|
var applied = false;
|
||||||
while (!token.IsCancellationRequested)
|
while (!token.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
if (_pairCoordinator.Ledger.TryGetEntry(ident, out var updated) && updated is not null)
|
if (_pairCoordinator.Ledger.TryGetEntry(ident, out var updated) && updated is not null)
|
||||||
{
|
{
|
||||||
if (updated.SelfPermissions == targetPermissions)
|
if (updated.SelfPermissions == targetPermissions)
|
||||||
{
|
{
|
||||||
pauseApplied = true;
|
applied = true;
|
||||||
entry = updated;
|
entry = updated;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -452,16 +453,13 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
|
|||||||
Logger.LogTrace("Waiting for permissions change for {uid}", ident.UserId);
|
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);
|
Logger.LogWarning("CyclePauseAsync timed out waiting for pause acknowledgement for {uid}", ident.UserId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
targetPermissions.SetPaused(paused: false);
|
Logger.LogDebug("CyclePauseAsync toggled paused for {uid} to {state}", ident.UserId, targetPermissions.IsPaused());
|
||||||
await UserSetPairPermissions(new UserPermissionsDto(entry.User, targetPermissions)).ConfigureAwait(false);
|
|
||||||
|
|
||||||
Logger.LogDebug("CyclePauseAsync completed pause cycle for {uid}", ident.UserId);
|
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
@@ -481,26 +479,16 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async Task PauseAsync(UserData userData)
|
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);
|
var pairIdent = new PairUniqueIdentifier(userData.UID);
|
||||||
if (!_pairCoordinator.Ledger.TryGetEntry(pairIdent, out var entry) || entry is null)
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var permissions = entry.SelfPermissions;
|
var permissions = entry.SelfPermissions;
|
||||||
permissions.SetPaused(paused);
|
permissions.SetPaused(paused: true);
|
||||||
await UserSetPairPermissions(new UserPermissionsDto(userData, permissions)).ConfigureAwait(false);
|
await UserSetPairPermissions(new UserPermissionsDto(userData, permissions)).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -596,10 +584,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
|
|||||||
OnGroupSendInfo((dto) => _ = Client_GroupSendInfo(dto));
|
OnGroupSendInfo((dto) => _ = Client_GroupSendInfo(dto));
|
||||||
OnGroupUpdateProfile((dto) => _ = Client_GroupSendProfile(dto));
|
OnGroupUpdateProfile((dto) => _ = Client_GroupSendProfile(dto));
|
||||||
OnGroupChangeUserPairPermissions((dto) => _ = Client_GroupChangeUserPairPermissions(dto));
|
OnGroupChangeUserPairPermissions((dto) => _ = Client_GroupChangeUserPairPermissions(dto));
|
||||||
if (!_initialized)
|
|
||||||
{
|
|
||||||
_lightlessHub.On(nameof(Client_ChatReceive), (Func<ChatMessageDto, Task>)Client_ChatReceive);
|
_lightlessHub.On(nameof(Client_ChatReceive), (Func<ChatMessageDto, Task>)Client_ChatReceive);
|
||||||
}
|
|
||||||
|
|
||||||
OnGposeLobbyJoin((dto) => _ = Client_GposeLobbyJoin(dto));
|
OnGposeLobbyJoin((dto) => _ = Client_GposeLobbyJoin(dto));
|
||||||
OnGposeLobbyLeave((dto) => _ = Client_GposeLobbyLeave(dto));
|
OnGposeLobbyLeave((dto) => _ = Client_GposeLobbyLeave(dto));
|
||||||
|
|||||||
Submodule Penumbra.Api updated: 52a3216a52...1750c41b53
Reference in New Issue
Block a user