Compare commits

...

9 Commits

Author SHA1 Message Date
9ea0571e82 Lower Time out 2025-12-30 14:29:38 +00:00
defnotken
27d4da4615 thought a variable was unused. 2025-12-29 08:47:51 -06:00
defnotken
6b49c92ef9 Add a timeout to prevent deadlock of application data 2025-12-29 08:41:32 -06:00
cake
6d20995dbf Added decompression gate to decompress files 2025-12-29 02:50:49 +01:00
cake
cf495dc826 Merge branch '2.0.2' of https://git.lightless-sync.org/Lightless-Sync/LightlessClient into 2.0.2 2025-12-28 16:28:37 +01:00
cake
08050614da Own profiles are shown as online now. 2025-12-28 16:28:27 +01:00
94f520d0e7 Add Serious Warning about nameplates (#118)
Co-authored-by: defnotken <itsdefnotken@gmail.com>
Reviewed-on: #118
Co-authored-by: defnotken <defnotken@noreply.git.lightless-sync.org>
Co-committed-by: defnotken <defnotken@noreply.git.lightless-sync.org>
2025-12-28 15:13:40 +00:00
474fd5ef11 Merge pull request 'Fix Async hiccup in chat.' (#117) from quick-chat-fix into 2.0.2
Reviewed-on: #117
Reviewed-by: cake <cake@noreply.git.lightless-sync.org>
2025-12-28 03:58:35 +00:00
defnotken
759066731e Fix Async hiccup in chat. 2025-12-27 21:57:01 -06:00
6 changed files with 148 additions and 49 deletions

View File

@@ -1423,7 +1423,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
private Task _visibilityGraceTask; 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)
{ {
var concurrencyLease = await _pairProcessingLimiter.AcquireAsync(downloadToken).ConfigureAwait(false); var concurrencyLease = await _pairProcessingLimiter.AcquireAsync(downloadToken).ConfigureAwait(false);
try try
@@ -1577,24 +1577,37 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
RecordFailure("Handler not available for application", "HandlerUnavailable"); RecordFailure("Handler not available for application", "HandlerUnavailable");
return; return;
} }
_applicationCancellationTokenSource = _applicationCancellationTokenSource.CancelRecreate() ?? new CancellationTokenSource();
var appToken = _applicationCancellationTokenSource?.Token; if (_applicationTask != null && !_applicationTask.IsCompleted)
while ((!_applicationTask?.IsCompleted ?? false)
&& !downloadToken.IsCancellationRequested
&& (!appToken?.IsCancellationRequested ?? false))
{ {
Logger.LogDebug("[BASE-{appBase}] Waiting for current data application (Id: {id}) for player ({handler}) to finish", applicationBase, _applicationId, PlayerName); Logger.LogDebug("[BASE-{appBase}] Cancelling current data application (Id: {id}) for player ({handler})", applicationBase, _applicationId, PlayerName);
await Task.Delay(250).ConfigureAwait(false);
var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
var combinedCts = CancellationTokenSource.CreateLinkedTokenSource(downloadToken, timeoutCts.Token);
try
{
await _applicationTask.WaitAsync(combinedCts.Token).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
Logger.LogWarning("[BASE-{appBase}] Timeout waiting for application task {id} to complete, proceeding anyway", applicationBase, _applicationId);
}
finally
{
timeoutCts.Dispose();
combinedCts.Dispose();
}
} }
if (downloadToken.IsCancellationRequested || (appToken?.IsCancellationRequested ?? false)) if (downloadToken.IsCancellationRequested)
{ {
_forceFullReapply = true; _forceFullReapply = true;
RecordFailure("Application cancelled", "Cancellation"); RecordFailure("Application cancelled", "Cancellation");
return; return;
} }
_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, wantsModApply, pendingModReapply, token);

View File

@@ -105,6 +105,7 @@ public class UiFactory
groupData: groupData, groupData: groupData,
isLightfinderContext: isLightfinderContext, isLightfinderContext: isLightfinderContext,
lightfinderCid: lightfinderCid, lightfinderCid: lightfinderCid,
performanceCollector: _performanceCollectorService); performanceCollector: _performanceCollectorService,
_apiController);
} }
} }

View File

@@ -84,6 +84,8 @@ public class SettingsUi : WindowMediatorSubscriberBase
private bool _pairDiagnosticsEnabled; private bool _pairDiagnosticsEnabled;
private string? _selectedPairDebugUid = null; private string? _selectedPairDebugUid = null;
private string _lightfinderIconInput = string.Empty; private string _lightfinderIconInput = string.Empty;
private bool _showLightfinderRendererWarning = false;
private LightfinderLabelRenderer _pendingLightfinderRenderer = LightfinderLabelRenderer.Pictomancy;
private bool _lightfinderIconInputInitialized = false; private bool _lightfinderIconInputInitialized = false;
private int _lightfinderIconPresetIndex = -1; private int _lightfinderIconPresetIndex = -1;
private static readonly LightlessConfig DefaultConfig = new(); private static readonly LightlessConfig DefaultConfig = new();
@@ -2372,7 +2374,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
var labelRenderer = _configService.Current.LightfinderLabelRenderer; var labelRenderer = _configService.Current.LightfinderLabelRenderer;
var labelRendererLabel = labelRenderer switch var labelRendererLabel = labelRenderer switch
{ {
LightfinderLabelRenderer.SignatureHook => "Native nameplate (sig hook)", LightfinderLabelRenderer.SignatureHook => "Native Nameplate Rendering",
_ => "ImGui Overlay", _ => "ImGui Overlay",
}; };
@@ -2382,18 +2384,25 @@ public class SettingsUi : WindowMediatorSubscriberBase
{ {
var optionLabel = option switch var optionLabel = option switch
{ {
LightfinderLabelRenderer.SignatureHook => "Native Nameplate (sig hook)", LightfinderLabelRenderer.SignatureHook => "Native Nameplate Rendering",
_ => "ImGui Overlay", _ => "ImGui Overlay",
}; };
var selected = option == labelRenderer; var selected = option == labelRenderer;
if (ImGui.Selectable(optionLabel, selected)) if (ImGui.Selectable(optionLabel, selected))
{ {
_configService.Current.LightfinderLabelRenderer = option; if (option == LightfinderLabelRenderer.SignatureHook)
_configService.Save(); {
_nameplateService.RequestRedraw(); _pendingLightfinderRenderer = option;
_showLightfinderRendererWarning = true;
}
else
{
_configService.Current.LightfinderLabelRenderer = option;
_configService.Save();
_nameplateService.RequestRedraw();
}
} }
if (selected) if (selected)
ImGui.SetItemDefaultFocus(); ImGui.SetItemDefaultFocus();
} }
@@ -2401,6 +2410,34 @@ public class SettingsUi : WindowMediatorSubscriberBase
ImGui.EndCombo(); ImGui.EndCombo();
} }
if (_showLightfinderRendererWarning)
{
ImGui.SetNextWindowSize(new Vector2(450f, 0f), ImGuiCond.Appearing);
ImGui.OpenPopup("Nameplate Warning");
}
if (ImGui.BeginPopupModal("Nameplate Warning", ref _showLightfinderRendererWarning, ImGuiWindowFlags.AlwaysAutoResize))
{
ImGui.TextColored(UIColors.Get("DimRed"), "USE AT YOUR RISK!");
ImGui.Spacing();
ImGui.TextWrapped("Writing on to the native Nameplates is known to be unstable and MAY cause crashes. DO NOT REPORT THOSE CRASHES TO DALAMUD. We will also not be supporting Nameplate crashes. You have been warned.");
ImGui.Spacing();
ImGui.TextWrapped("By accepting this warning, you understand that you are using this feature at risk of crashing.");
ImGui.Spacing();
var buttonWidth = ImGui.GetContentRegionAvail().X;
if (ImGui.Button("I Understand", new Vector2(buttonWidth, 0)))
{
_configService.Current.LightfinderLabelRenderer = _pendingLightfinderRenderer;
_configService.Save();
_nameplateService.RequestRedraw();
_showLightfinderRendererWarning = false;
ImGui.CloseCurrentPopup();
}
ImGui.EndPopup();
}
_uiShared.DrawHelpText("Choose how Lightfinder labels render: the default ImGui overlay or native nameplate nodes via signature hook."); _uiShared.DrawHelpText("Choose how Lightfinder labels render: the default ImGui overlay or native nameplate nodes via signature hook.");
UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f); UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f);

View File

@@ -11,6 +11,7 @@ using LightlessSync.Services.ServerConfiguration;
using LightlessSync.UI.Services; using LightlessSync.UI.Services;
using LightlessSync.UI.Tags; using LightlessSync.UI.Tags;
using LightlessSync.Utils; using LightlessSync.Utils;
using LightlessSync.WebAPI;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System.Numerics; using System.Numerics;
@@ -22,6 +23,7 @@ public class StandaloneProfileUi : WindowMediatorSubscriberBase
private readonly PairUiService _pairUiService; private readonly PairUiService _pairUiService;
private readonly ServerConfigurationManager _serverManager; private readonly ServerConfigurationManager _serverManager;
private readonly ProfileTagService _profileTagService; private readonly ProfileTagService _profileTagService;
private readonly ApiController _apiController;
private readonly UiSharedService _uiSharedService; private readonly UiSharedService _uiSharedService;
private readonly UserData? _userData; private readonly UserData? _userData;
private readonly GroupData? _groupData; private readonly GroupData? _groupData;
@@ -60,7 +62,8 @@ public class StandaloneProfileUi : WindowMediatorSubscriberBase
GroupData? groupData, GroupData? groupData,
bool isLightfinderContext, bool isLightfinderContext,
string? lightfinderCid, string? lightfinderCid,
PerformanceCollectorService performanceCollector) PerformanceCollectorService performanceCollector,
ApiController apiController)
: base(logger, mediator, BuildWindowTitle( : base(logger, mediator, BuildWindowTitle(
userData, userData,
groupData, groupData,
@@ -94,6 +97,7 @@ public class StandaloneProfileUi : WindowMediatorSubscriberBase
.Apply(); .Apply();
IsOpen = true; IsOpen = true;
_apiController = apiController;
} }
public Pair? Pair { get; } public Pair? Pair { get; }
@@ -248,19 +252,33 @@ public class StandaloneProfileUi : WindowMediatorSubscriberBase
ResetBannerTexture(); ResetBannerTexture();
_lastBannerPicture = bannerBytes; _lastBannerPicture = bannerBytes;
} }
string? noteText = null; string? noteText = null;
string statusLabel = _isLightfinderContext ? "Exploring" : "Offline";
var isSelfProfile = !_isLightfinderContext
&& _userData is not null
&& !string.IsNullOrEmpty(_apiController.UID)
&& string.Equals(_userData.UID, _apiController.UID, StringComparison.Ordinal);
string statusLabel = _isLightfinderContext
? "Exploring"
: isSelfProfile ? "Online" : "Offline";
string? visiblePlayerName = null; string? visiblePlayerName = null;
bool directPair = false; bool directPair = false;
bool youPaused = false; bool youPaused = false;
bool theyPaused = false; bool theyPaused = false;
List<string> syncshellLines = []; List<string> syncshellLines = [];
if (!_isLightfinderContext)
{
noteText = _serverManager.GetNoteForUid(_userData!.UID);
}
if (!_isLightfinderContext && Pair != null) if (!_isLightfinderContext && Pair != null)
{ {
var snapshot = _pairUiService.GetSnapshot(); var snapshot = _pairUiService.GetSnapshot();
noteText = _serverManager.GetNoteForUid(Pair.UserData.UID); noteText = _serverManager.GetNoteForUid(Pair.UserData.UID);
statusLabel = Pair.IsVisible ? "Visible" : (Pair.IsOnline ? "Online" : "Offline"); statusLabel = Pair.IsVisible ? "Visible" : (Pair.IsOnline ? "Online" : "Offline");
visiblePlayerName = Pair.IsVisible ? Pair.PlayerName : null; visiblePlayerName = Pair.IsVisible ? Pair.PlayerName : null;
@@ -282,11 +300,15 @@ public class StandaloneProfileUi : WindowMediatorSubscriberBase
var groupLabel = snapshot.GroupsByGid.TryGetValue(gid, out var groupInfo) var groupLabel = snapshot.GroupsByGid.TryGetValue(gid, out var groupInfo)
? groupInfo.GroupAliasOrGID ? groupInfo.GroupAliasOrGID
: gid; : gid;
var groupNote = _serverManager.GetNoteForGid(gid); var groupNote = _serverManager.GetNoteForGid(gid);
syncshellLines.Add(string.IsNullOrEmpty(groupNote) ? groupLabel : $"{groupNote} ({groupLabel})"); syncshellLines.Add(string.IsNullOrEmpty(groupNote) ? groupLabel : $"{groupNote} ({groupLabel})");
} }
} }
} }
if (isSelfProfile)
statusLabel = "Online";
} }
var presenceTokens = new List<PresenceToken> var presenceTokens = new List<PresenceToken>

View File

@@ -1000,23 +1000,26 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
if (sanitized is not null) if (sanitized is not null)
{ {
TrackPendingDraftClear(channel.Key, sanitized); TrackPendingDraftClear(channel.Key, sanitized);
draft = string.Empty;
_draftMessages[channel.Key] = draft;
_scrollToBottom = true;
if (TrySendDraft(channel, sanitized)) _ = Task.Run(async () =>
{ {
_scrollToBottom = true; try
if (_draftMessages.TryGetValue(channel.Key, out var current) &&
string.Equals(current, draftAtSend, StringComparison.Ordinal))
{ {
draft = string.Empty; var succeeded = await _zoneChatService.SendMessageAsync(channel.Descriptor, sanitized).ConfigureAwait(false);
_draftMessages[channel.Key] = draft; if (!succeeded)
{
RemovePendingDraftClear(channel.Key, sanitized);
}
} }
catch (Exception ex)
} {
else _logger.LogWarning(ex, "Failed to send chat message");
{ RemovePendingDraftClear(channel.Key, sanitized);
RemovePendingDraftClear(channel.Key, sanitized); }
} });
} }
} }
} }

View File

@@ -28,6 +28,8 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
private readonly TextureMetadataHelper _textureMetadataHelper; private readonly TextureMetadataHelper _textureMetadataHelper;
private readonly ConcurrentDictionary<ThrottledStream, byte> _activeDownloadStreams; private readonly ConcurrentDictionary<ThrottledStream, byte> _activeDownloadStreams;
private readonly SemaphoreSlim _decompressGate =
new(Math.Max(1, Environment.ProcessorCount / 2), Math.Max(1, Environment.ProcessorCount / 2));
private volatile bool _disableDirectDownloads; private volatile bool _disableDirectDownloads;
private int _consecutiveDirectDownloadFailures; private int _consecutiveDirectDownloadFailures;
@@ -522,32 +524,57 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
try try
{ {
// sanity check length
if (fileLengthBytes < 0 || fileLengthBytes > int.MaxValue) if (fileLengthBytes < 0 || fileLengthBytes > int.MaxValue)
throw new InvalidDataException($"Invalid block entry length: {fileLengthBytes}"); throw new InvalidDataException($"Invalid block entry length: {fileLengthBytes}");
// safe cast after check
var len = checked((int)fileLengthBytes);
if (!replacementLookup.TryGetValue(fileHash, out var repl)) if (!replacementLookup.TryGetValue(fileHash, out var repl))
{ {
Logger.LogWarning("{dlName}: No replacement mapping for {fileHash}", downloadLabel, fileHash); Logger.LogWarning("{dlName}: No replacement mapping for {fileHash}", downloadLabel, fileHash);
// still need to skip bytes: fileBlockStream.Seek(len, SeekOrigin.Current);
var skip = checked((int)fileLengthBytes);
fileBlockStream.Position += skip;
continue; continue;
} }
// decompress
var filePath = _fileDbManager.GetCacheFilePath(fileHash, repl.Extension); var filePath = _fileDbManager.GetCacheFilePath(fileHash, repl.Extension);
Logger.LogTrace("{dlName}: Decompressing {file}:{len} => {dest}", downloadLabel, fileHash, fileLengthBytes, filePath);
Logger.LogDebug("{dlName}: Decompressing {file}:{len} => {dest}", downloadLabel, fileHash, fileLengthBytes, filePath); // read compressed data
var len = checked((int)fileLengthBytes);
var compressed = new byte[len]; var compressed = new byte[len];
await ReadExactlyAsync(fileBlockStream, compressed.AsMemory(0, len), ct).ConfigureAwait(false); await ReadExactlyAsync(fileBlockStream, compressed.AsMemory(0, len), ct).ConfigureAwait(false);
MungeBuffer(compressed); if (len == 0)
var decompressed = LZ4Wrapper.Unwrap(compressed); {
await _fileCompactor.WriteAllBytesAsync(filePath, Array.Empty<byte>(), ct).ConfigureAwait(false);
PersistFileToStorage(fileHash, filePath, repl.GamePath, skipDownscale);
continue;
}
await _fileCompactor.WriteAllBytesAsync(filePath, decompressed, ct).ConfigureAwait(false); MungeBuffer(compressed);
PersistFileToStorage(fileHash, filePath, repl.GamePath, skipDownscale);
// limit concurrent decompressions
await _decompressGate.WaitAsync(ct).ConfigureAwait(false);
try
{
var sw = System.Diagnostics.Stopwatch.StartNew();
// decompress
var decompressed = LZ4Wrapper.Unwrap(compressed);
Logger.LogTrace("{dlName}: Unwrap {fileHash} took {ms}ms (compressed {c} bytes, decompressed {d} bytes)",
downloadLabel, fileHash, sw.ElapsedMilliseconds, compressed.Length, decompressed?.Length ?? -1);
// write to file
await _fileCompactor.WriteAllBytesAsync(filePath, decompressed, ct).ConfigureAwait(false);
PersistFileToStorage(fileHash, filePath, repl.GamePath, skipDownscale);
}
finally
{
_decompressGate.Release();
}
} }
catch (EndOfStreamException) catch (EndOfStreamException)
{ {
@@ -605,20 +632,16 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
.. await FilesGetSizes(hashes, ct).ConfigureAwait(false), .. await FilesGetSizes(hashes, ct).ConfigureAwait(false),
]; ];
Logger.LogDebug("Files with size 0 or less: {files}",
string.Join(", ", downloadFileInfoFromService.Where(f => f.Size <= 0).Select(f => f.Hash)));
foreach (var dto in downloadFileInfoFromService.Where(c => c.IsForbidden)) foreach (var dto in downloadFileInfoFromService.Where(c => c.IsForbidden))
{ {
if (!_orchestrator.ForbiddenTransfers.Exists(f => string.Equals(f.Hash, dto.Hash, StringComparison.Ordinal))) if (!_orchestrator.ForbiddenTransfers.Exists(f => string.Equals(f.Hash, dto.Hash, StringComparison.Ordinal)))
_orchestrator.ForbiddenTransfers.Add(new DownloadFileTransfer(dto)); _orchestrator.ForbiddenTransfers.Add(new DownloadFileTransfer(dto));
} }
CurrentDownloads = downloadFileInfoFromService CurrentDownloads = [.. downloadFileInfoFromService
.Distinct() .Distinct()
.Select(d => new DownloadFileTransfer(d)) .Select(d => new DownloadFileTransfer(d))
.Where(d => d.CanBeTransferred) .Where(d => d.CanBeTransferred)];
.ToList();
return CurrentDownloads; return CurrentDownloads;
} }