diff --git a/LightlessSync/LightlessSync.csproj b/LightlessSync/LightlessSync.csproj index 1772e6b..b0b7b8e 100644 --- a/LightlessSync/LightlessSync.csproj +++ b/LightlessSync/LightlessSync.csproj @@ -3,7 +3,7 @@ - 2.0.2 + 2.0.3 https://github.com/Light-Public-Syncshells/LightlessClient diff --git a/LightlessSync/PlayerData/Pairs/PairHandlerAdapter.cs b/LightlessSync/PlayerData/Pairs/PairHandlerAdapter.cs index 82c4a94..b0f2710 100644 --- a/LightlessSync/PlayerData/Pairs/PairHandlerAdapter.cs +++ b/LightlessSync/PlayerData/Pairs/PairHandlerAdapter.cs @@ -1423,7 +1423,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa private Task _visibilityGraceTask; private async Task DownloadAndApplyCharacterAsync(Guid applicationBase, CharacterData charaData, Dictionary> 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); try @@ -1577,24 +1577,37 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa RecordFailure("Handler not available for application", "HandlerUnavailable"); return; } + _applicationCancellationTokenSource = _applicationCancellationTokenSource.CancelRecreate() ?? new CancellationTokenSource(); - var appToken = _applicationCancellationTokenSource?.Token; - while ((!_applicationTask?.IsCompleted ?? false) - && !downloadToken.IsCancellationRequested - && (!appToken?.IsCancellationRequested ?? false)) + if (_applicationTask != null && !_applicationTask.IsCompleted) { - Logger.LogDebug("[BASE-{appBase}] Waiting for current data application (Id: {id}) for player ({handler}) to finish", applicationBase, _applicationId, PlayerName); - await Task.Delay(250).ConfigureAwait(false); + Logger.LogDebug("[BASE-{appBase}] Cancelling current data application (Id: {id}) for player ({handler})", applicationBase, _applicationId, PlayerName); + + 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; RecordFailure("Application cancelled", "Cancellation"); return; } - _applicationCancellationTokenSource = _applicationCancellationTokenSource.CancelRecreate() ?? new CancellationTokenSource(); var token = _applicationCancellationTokenSource.Token; _applicationTask = ApplyCharacterDataAsync(applicationBase, handlerForApply, charaData, updatedData, updateModdedPaths, updateManip, moddedPaths, wantsModApply, pendingModReapply, token); diff --git a/LightlessSync/Services/ContextMenuService.cs b/LightlessSync/Services/ContextMenuService.cs index 3fe893c..024e17b 100644 --- a/LightlessSync/Services/ContextMenuService.cs +++ b/LightlessSync/Services/ContextMenuService.cs @@ -10,7 +10,6 @@ using LightlessSync.UI; using LightlessSync.UI.Services; using LightlessSync.Utils; using LightlessSync.WebAPI; -using Lumina.Excel.Sheets; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; @@ -172,9 +171,8 @@ internal class ContextMenuService : IHostedService _logger.LogTrace("Cannot send pair request to {TargetName}@{World} while in PvP or GPose.", target.TargetName, target.TargetHomeWorld.RowId); return; } - - var world = GetWorld(target.TargetHomeWorld.RowId); - if (!IsWorldValid(world)) + + if (!IsWorldValid(target.TargetHomeWorld.RowId)) { _logger.LogTrace("Target player {TargetName}@{World} is on an invalid world.", target.TargetName, target.TargetHomeWorld.RowId); return; @@ -226,9 +224,8 @@ internal class ContextMenuService : IHostedService { if (args.Target is not MenuTargetDefault target) return; - - var world = GetWorld(target.TargetHomeWorld.RowId); - if (!IsWorldValid(world)) + + if (!target.TargetHomeWorld.IsValid || !IsWorldValid(target.TargetHomeWorld.RowId)) return; try @@ -237,7 +234,7 @@ internal class ContextMenuService : IHostedService if (targetData == null || targetData.Address == nint.Zero) { - _logger.LogWarning("Target player {TargetName}@{World} not found in object table.", target.TargetName, world.Name); + _logger.LogWarning("Target player {TargetName}@{World} not found in object table.", target.TargetName, target.TargetHomeWorld.Value.Name); return; } @@ -252,7 +249,7 @@ internal class ContextMenuService : IHostedService } // Notify in chat when NotificationService is disabled - NotifyInChat($"Pair request sent to {target.TargetName}@{world.Name}.", NotificationType.Info); + NotifyInChat($"Pair request sent to {target.TargetName}@{target.TargetHomeWorld.Value.Name}.", NotificationType.Info); } catch (Exception ex) { @@ -312,37 +309,8 @@ internal class ContextMenuService : IHostedService p.HomeWorld.RowId == target.TargetHomeWorld.RowId); } - private World GetWorld(uint worldId) + private bool IsWorldValid(uint worldId) { - var sheet = _gameData.GetExcelSheet()!; - var luminaWorlds = sheet.Where(x => - { - var dc = x.DataCenter.ValueNullable; - var name = x.Name.ExtractText(); - var internalName = x.InternalName.ExtractText(); - - if (dc == null || dc.Value.Region == 0 || string.IsNullOrWhiteSpace(dc.Value.Name.ExtractText())) - return false; - - if (string.IsNullOrWhiteSpace(name) || string.IsNullOrWhiteSpace(internalName)) - return false; - - if (name.Contains('-', StringComparison.Ordinal) || name.Contains('_', StringComparison.Ordinal)) - return false; - - return x.DataCenter.Value.Region != 5 || x.RowId > 3001 && x.RowId != 1200 && IsChineseJapaneseKoreanString(name); - }); - - return luminaWorlds.FirstOrDefault(x => x.RowId == worldId); - } - - private static bool IsChineseJapaneseKoreanString(string text) => text.All(IsChineseJapaneseKoreanCharacter); - - private static bool IsChineseJapaneseKoreanCharacter(char c) => c >= 0x4E00 && c <= 0x9FFF; - - public static bool IsWorldValid(World world) - { - var name = world.Name.ToString(); - return !string.IsNullOrWhiteSpace(name) && char.IsUpper(name[0]); + return _dalamudUtil.WorldData.Value.ContainsKey((ushort)worldId); } } diff --git a/LightlessSync/Services/DalamudUtilService.cs b/LightlessSync/Services/DalamudUtilService.cs index 96c78f3..b278667 100644 --- a/LightlessSync/Services/DalamudUtilService.cs +++ b/LightlessSync/Services/DalamudUtilService.cs @@ -60,6 +60,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber private string _lastGlobalBlockReason = string.Empty; private ushort _lastZone = 0; private ushort _lastWorldId = 0; + private uint _lastMapId = 0; private bool _sentBetweenAreas = false; private Lazy _cid; @@ -90,7 +91,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber { return gameData.GetExcelSheet(clientLanguage)! .Where(w => !w.Name.IsEmpty && w.DataCenter.RowId != 0 && (w.IsPublic || char.IsUpper(w.Name.ToString()[0]) - || w is { RowId: > 1000, Region: 101 })) + || w is { RowId: > 1000, Region: 101 or 201 })) .ToDictionary(w => (ushort)w.RowId, w => w.Name.ToString()); }); JobData = new(() => @@ -689,7 +690,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber var outside = houseMan->OutdoorTerritory; var house = outside->HouseId; location.WardId = house.WardIndex + 1u; - location.HouseId = (uint)houseMan->GetCurrentPlot() + 1; + //location.HouseId = (uint)houseMan->GetCurrentPlot() + 1; location.DivisionId = houseMan->GetCurrentDivision(); } //_logger.LogWarning(LocationToString(location)); @@ -1139,6 +1140,18 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber Mediator.Publish(new ZoneSwitchEndMessage()); Mediator.Publish(new ResumeScanMessage(nameof(ConditionFlag.BetweenAreas))); } + + //Map + if (!_sentBetweenAreas) + { + var mapid = _clientState.MapId; + if (mapid != _lastMapId) + { + _lastMapId = mapid; + Mediator.Publish(new MapChangedMessage(mapid)); + } + } + var localPlayer = _objectTable.LocalPlayer; if (localPlayer != null) diff --git a/LightlessSync/Services/LocationShareService.cs b/LightlessSync/Services/LocationShareService.cs index 77b5c90..38b2834 100644 --- a/LightlessSync/Services/LocationShareService.cs +++ b/LightlessSync/Services/LocationShareService.cs @@ -17,8 +17,6 @@ namespace LightlessSync.Services private IMemoryCache _sharingStatus = new MemoryCache(new MemoryCacheOptions()); private CancellationTokenSource _resetToken = new CancellationTokenSource(); - - public LocationShareService(ILogger logger, LightlessMediator mediator, DalamudUtilService dalamudUtilService, ApiController apiController) : base(logger, mediator) { _dalamudUtilService = dalamudUtilService; @@ -37,7 +35,7 @@ namespace LightlessSync.Services _ = RequestAllLocation(); } ); Mediator.Subscribe(this, UpdateLocationList); - Mediator.Subscribe(this, + Mediator.Subscribe(this, msg => _ = _apiController.UpdateLocation(new LocationDto(new UserData(_apiController.UID, _apiController.DisplayName), _dalamudUtilService.GetMapData()))); } @@ -134,5 +132,6 @@ namespace LightlessSync.Services AddStatus(user, expireAt); } } + } } \ No newline at end of file diff --git a/LightlessSync/Services/Mediator/Messages.cs b/LightlessSync/Services/Mediator/Messages.cs index 00f8de7..efe3341 100644 --- a/LightlessSync/Services/Mediator/Messages.cs +++ b/LightlessSync/Services/Mediator/Messages.cs @@ -136,5 +136,6 @@ public record ChatChannelMessageAdded(string ChannelKey, ChatMessageEntry Messag public record GroupCollectionChangedMessage : MessageBase; public record OpenUserProfileMessage(UserData User) : MessageBase; public record LocationSharingMessage(UserData User, LocationInfo LocationInfo, DateTimeOffset ExpireAt) : MessageBase; +public record MapChangedMessage(uint MapId) : MessageBase; #pragma warning restore S2094 #pragma warning restore MA0048 // File name must match type name \ No newline at end of file diff --git a/LightlessSync/UI/Components/DrawUserPair.cs b/LightlessSync/UI/Components/DrawUserPair.cs index 021ad1a..c8725e2 100644 --- a/LightlessSync/UI/Components/DrawUserPair.cs +++ b/LightlessSync/UI/Components/DrawUserPair.cs @@ -642,9 +642,10 @@ public class DrawUserPair if (ImGui.IsItemHovered()) { ImGui.BeginTooltip(); - if (shareLocation) + + if (_pair.IsOnline) { - if (_pair.IsOnline) + if (shareLocation) { if (!string.IsNullOrEmpty(location)) { @@ -659,18 +660,18 @@ public class DrawUserPair } else { - ImGui.TextUnformatted("User not onlineㄟ( ▔, ▔ )ㄏ"); + ImGui.TextUnformatted("NOT Sharing location with you. o(TヘTo)"); } } else { - ImGui.TextUnformatted("NOT Sharing location with you.(⊙x⊙;)"); + ImGui.TextUnformatted("User not online. (´・ω・`)?"); } ImGui.Separator(); if (shareLocationToOther) { - ImGui.TextUnformatted("Sharing your location.ヾ(•ω•`)o"); + ImGui.TextUnformatted("Sharing your location. ヾ(•ω•`)o"); if (expireAt != DateTimeOffset.MaxValue) { ImGui.TextUnformatted("Expires at " + expireAt.ToLocalTime().ToString("g")); @@ -678,7 +679,7 @@ public class DrawUserPair } else { - ImGui.TextUnformatted("NOT sharing your location.(´。_。`)"); + ImGui.TextUnformatted("NOT sharing your location.  ̄へ ̄"); } ImGui.EndTooltip(); } diff --git a/LightlessSync/UI/DataAnalysisUi.cs b/LightlessSync/UI/DataAnalysisUi.cs index 32245d2..a3061a7 100644 --- a/LightlessSync/UI/DataAnalysisUi.cs +++ b/LightlessSync/UI/DataAnalysisUi.cs @@ -2183,7 +2183,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase bool toggleClicked = false; if (showToggle) { - var icon = isCollapsed ? FontAwesomeIcon.ChevronRight : FontAwesomeIcon.ChevronLeft; + var icon = !isCollapsed ? FontAwesomeIcon.ChevronRight : FontAwesomeIcon.ChevronLeft; Vector2 iconSize; using (_uiSharedService.IconFont.Push()) { diff --git a/LightlessSync/UI/SyncshellAdminUI.cs b/LightlessSync/UI/SyncshellAdminUI.cs index 0458c05..526b5ae 100644 --- a/LightlessSync/UI/SyncshellAdminUI.cs +++ b/LightlessSync/UI/SyncshellAdminUI.cs @@ -116,7 +116,7 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase var drawList = ImGui.GetWindowDrawList(); var purple = UIColors.Get("LightlessPurple"); - var gradLeft = purple.WithAlpha(0.0f); + var gradLeft = purple.WithAlpha(0.0f); var gradRight = purple.WithAlpha(0.85f); uint colTopLeft = ImGui.ColorConvertFloat4ToU32(gradLeft); @@ -162,7 +162,7 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase var subtitlePos = new Vector2( pMin.X + 12f * scale, - titlePos.Y + titleHeight - 2f * scale); + titlePos.Y + titleHeight - 2f * scale); ImGui.SetCursorScreenPos(subtitlePos); ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey); @@ -392,25 +392,27 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase } UiSharedService.AttachToolTip("When enabled, inactive non-pinned, non-moderator users will be pruned automatically on the server."); - ImGui.SameLine(); - ImGui.SetNextItemWidth(150); - - using (ImRaii.Disabled(!_autoPruneEnabled)) - { - _uiSharedService.DrawCombo( - "Day(s) of inactivity", - [1, 3, 7, 14, 30, 90], - days => $"{days} day(s)", - selected => - { - _autoPruneDays = selected; - SavePruneSettings(); - }, - _autoPruneDays); - } if (!_autoPruneEnabled) { + ImGui.BeginDisabled(); + } + ImGui.SameLine(); + ImGui.SetNextItemWidth(150); + _uiSharedService.DrawCombo( + "Day(s) of inactivity (gets checked hourly)", + [0, 1, 3, 7, 14, 30, 90], + (count) => count == 0 ? "2 hours(s)" : count + " day(s)", + selected => + { + _autoPruneDays = selected; + SavePruneSettings(); + }, + _autoPruneDays); + + if (!_autoPruneEnabled) + { + ImGui.EndDisabled(); UiSharedService.ColorTextWrapped( "Automatic prune is currently disabled. Enable it and choose an inactivity threshold to let the server clean up inactive users automatically.", ImGuiColors.DalamudGrey); @@ -593,7 +595,7 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase _uiSharedService.DrawCombo( "Day(s) of inactivity", [0, 1, 3, 7, 14, 30, 90], - (count) => count == 0 ? "15 minute(s)" : count + " day(s)", + (count) => count == 0 ? "2 hours(s)" : count + " day(s)", (selected) => { _pruneDays = selected; @@ -663,8 +665,8 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase var style = ImGui.GetStyle(); float fullW = ImGui.GetContentRegionAvail().X; - float colIdentity = fullW * 0.45f; - float colMeta = fullW * 0.35f; + float colIdentity = fullW * 0.45f; + float colMeta = fullW * 0.35f; float colActions = fullW - colIdentity - colMeta - style.ItemSpacing.X * 2.0f; // Header @@ -873,7 +875,7 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase var boolcolor = UiSharedService.GetBoolColor(pair.IsOnline); UiSharedService.ColorText(text, boolcolor); - + if (ImGui.IsItemClicked()) ImGui.SetClipboardText(pair.UserData.AliasOrUID); @@ -1093,6 +1095,7 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase ImGui.Dummy(new Vector2(0, 4 * ImGuiHelpers.GlobalScale)); } + private void SavePruneSettings() { if (_autoPruneDays <= 0) @@ -1100,8 +1103,7 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase _autoPruneEnabled = false; } - var enabled = _autoPruneEnabled && _autoPruneDays > 0; - var dto = new GroupPruneSettingsDto(Group: GroupFullInfo.Group, AutoPruneEnabled: enabled, AutoPruneDays: enabled ? _autoPruneDays : 0); + var dto = new GroupPruneSettingsDto(Group: GroupFullInfo.Group, AutoPruneEnabled: _autoPruneEnabled, AutoPruneDays: _autoPruneDays); try { diff --git a/LightlessSync/WebAPI/Files/FileDownloadManager.cs b/LightlessSync/WebAPI/Files/FileDownloadManager.cs index 2731619..8aa2b0b 100644 --- a/LightlessSync/WebAPI/Files/FileDownloadManager.cs +++ b/LightlessSync/WebAPI/Files/FileDownloadManager.cs @@ -502,6 +502,14 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase } } + private void RemoveStatus(string key) + { + lock (_downloadStatusLock) + { + _downloadStatus.Remove(key); + } + } + private async Task DecompressBlockFileAsync( string downloadStatusKey, string blockFilePath, @@ -595,6 +603,10 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase { Logger.LogError(ex, "{dlName}: Error during block file read", downloadLabel); } + finally + { + RemoveStatus(downloadStatusKey); + } } public async Task> InitiateDownloadList( @@ -866,6 +878,8 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase MarkTransferredFiles(directDownload.DirectDownloadUrl!, 1); Logger.LogDebug("Finished direct download of {hash}.", directDownload.Hash); + + RemoveStatus(directDownload.DirectDownloadUrl!); } catch (OperationCanceledException ex) {