Merge remote-tracking branch 'origin/2.0.3' into i18n

This commit is contained in:
Tsubasahane
2026-01-03 10:29:25 +08:00
10 changed files with 97 additions and 86 deletions

View File

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

View File

@@ -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

@@ -10,7 +10,6 @@ using LightlessSync.UI;
using LightlessSync.UI.Services; using LightlessSync.UI.Services;
using LightlessSync.Utils; using LightlessSync.Utils;
using LightlessSync.WebAPI; using LightlessSync.WebAPI;
using Lumina.Excel.Sheets;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging; 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); _logger.LogTrace("Cannot send pair request to {TargetName}@{World} while in PvP or GPose.", target.TargetName, target.TargetHomeWorld.RowId);
return; return;
} }
var world = GetWorld(target.TargetHomeWorld.RowId); if (!IsWorldValid(target.TargetHomeWorld.RowId))
if (!IsWorldValid(world))
{ {
_logger.LogTrace("Target player {TargetName}@{World} is on an invalid world.", target.TargetName, target.TargetHomeWorld.RowId); _logger.LogTrace("Target player {TargetName}@{World} is on an invalid world.", target.TargetName, target.TargetHomeWorld.RowId);
return; return;
@@ -226,9 +224,8 @@ internal class ContextMenuService : IHostedService
{ {
if (args.Target is not MenuTargetDefault target) if (args.Target is not MenuTargetDefault target)
return; return;
var world = GetWorld(target.TargetHomeWorld.RowId); if (!target.TargetHomeWorld.IsValid || !IsWorldValid(target.TargetHomeWorld.RowId))
if (!IsWorldValid(world))
return; return;
try try
@@ -237,7 +234,7 @@ internal class ContextMenuService : IHostedService
if (targetData == null || targetData.Address == nint.Zero) 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; return;
} }
@@ -252,7 +249,7 @@ internal class ContextMenuService : IHostedService
} }
// Notify in chat when NotificationService is disabled // 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) catch (Exception ex)
{ {
@@ -312,37 +309,8 @@ internal class ContextMenuService : IHostedService
p.HomeWorld.RowId == target.TargetHomeWorld.RowId); p.HomeWorld.RowId == target.TargetHomeWorld.RowId);
} }
private World GetWorld(uint worldId) private bool IsWorldValid(uint worldId)
{ {
var sheet = _gameData.GetExcelSheet<World>()!; return _dalamudUtil.WorldData.Value.ContainsKey((ushort)worldId);
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]);
} }
} }

View File

@@ -60,6 +60,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
private string _lastGlobalBlockReason = string.Empty; private string _lastGlobalBlockReason = string.Empty;
private ushort _lastZone = 0; private ushort _lastZone = 0;
private ushort _lastWorldId = 0; private ushort _lastWorldId = 0;
private uint _lastMapId = 0;
private bool _sentBetweenAreas = false; private bool _sentBetweenAreas = false;
private Lazy<ulong> _cid; private Lazy<ulong> _cid;
@@ -90,7 +91,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
{ {
return gameData.GetExcelSheet<Lumina.Excel.Sheets.World>(clientLanguage)! return gameData.GetExcelSheet<Lumina.Excel.Sheets.World>(clientLanguage)!
.Where(w => !w.Name.IsEmpty && w.DataCenter.RowId != 0 && (w.IsPublic || char.IsUpper(w.Name.ToString()[0]) .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()); .ToDictionary(w => (ushort)w.RowId, w => w.Name.ToString());
}); });
JobData = new(() => JobData = new(() =>
@@ -689,7 +690,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
var outside = houseMan->OutdoorTerritory; var outside = houseMan->OutdoorTerritory;
var house = outside->HouseId; var house = outside->HouseId;
location.WardId = house.WardIndex + 1u; location.WardId = house.WardIndex + 1u;
location.HouseId = (uint)houseMan->GetCurrentPlot() + 1; //location.HouseId = (uint)houseMan->GetCurrentPlot() + 1;
location.DivisionId = houseMan->GetCurrentDivision(); location.DivisionId = houseMan->GetCurrentDivision();
} }
//_logger.LogWarning(LocationToString(location)); //_logger.LogWarning(LocationToString(location));
@@ -1139,6 +1140,18 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
Mediator.Publish(new ZoneSwitchEndMessage()); Mediator.Publish(new ZoneSwitchEndMessage());
Mediator.Publish(new ResumeScanMessage(nameof(ConditionFlag.BetweenAreas))); 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; var localPlayer = _objectTable.LocalPlayer;
if (localPlayer != null) if (localPlayer != null)

View File

@@ -17,8 +17,6 @@ namespace LightlessSync.Services
private IMemoryCache _sharingStatus = new MemoryCache(new MemoryCacheOptions()); private IMemoryCache _sharingStatus = new MemoryCache(new MemoryCacheOptions());
private CancellationTokenSource _resetToken = new CancellationTokenSource(); private CancellationTokenSource _resetToken = new CancellationTokenSource();
public LocationShareService(ILogger<LocationShareService> logger, LightlessMediator mediator, DalamudUtilService dalamudUtilService, ApiController apiController) : base(logger, mediator) public LocationShareService(ILogger<LocationShareService> logger, LightlessMediator mediator, DalamudUtilService dalamudUtilService, ApiController apiController) : base(logger, mediator)
{ {
_dalamudUtilService = dalamudUtilService; _dalamudUtilService = dalamudUtilService;
@@ -37,7 +35,7 @@ namespace LightlessSync.Services
_ = RequestAllLocation(); _ = RequestAllLocation();
} ); } );
Mediator.Subscribe<LocationSharingMessage>(this, UpdateLocationList); Mediator.Subscribe<LocationSharingMessage>(this, UpdateLocationList);
Mediator.Subscribe<ZoneSwitchEndMessage>(this, Mediator.Subscribe<MapChangedMessage>(this,
msg => _ = _apiController.UpdateLocation(new LocationDto(new UserData(_apiController.UID, _apiController.DisplayName), _dalamudUtilService.GetMapData()))); msg => _ = _apiController.UpdateLocation(new LocationDto(new UserData(_apiController.UID, _apiController.DisplayName), _dalamudUtilService.GetMapData())));
} }
@@ -134,5 +132,6 @@ namespace LightlessSync.Services
AddStatus(user, expireAt); AddStatus(user, expireAt);
} }
} }
} }
} }

View File

@@ -136,5 +136,6 @@ public record ChatChannelMessageAdded(string ChannelKey, ChatMessageEntry Messag
public record GroupCollectionChangedMessage : MessageBase; public record GroupCollectionChangedMessage : MessageBase;
public record OpenUserProfileMessage(UserData User) : MessageBase; public record OpenUserProfileMessage(UserData User) : MessageBase;
public record LocationSharingMessage(UserData User, LocationInfo LocationInfo, DateTimeOffset ExpireAt) : MessageBase; public record LocationSharingMessage(UserData User, LocationInfo LocationInfo, DateTimeOffset ExpireAt) : MessageBase;
public record MapChangedMessage(uint MapId) : MessageBase;
#pragma warning restore S2094 #pragma warning restore S2094
#pragma warning restore MA0048 // File name must match type name #pragma warning restore MA0048 // File name must match type name

View File

@@ -642,9 +642,10 @@ public class DrawUserPair
if (ImGui.IsItemHovered()) if (ImGui.IsItemHovered())
{ {
ImGui.BeginTooltip(); ImGui.BeginTooltip();
if (shareLocation)
if (_pair.IsOnline)
{ {
if (_pair.IsOnline) if (shareLocation)
{ {
if (!string.IsNullOrEmpty(location)) if (!string.IsNullOrEmpty(location))
{ {
@@ -659,18 +660,18 @@ public class DrawUserPair
} }
else else
{ {
ImGui.TextUnformatted("User not onlineㄟ( ▔, ▔ )ㄏ"); ImGui.TextUnformatted("NOT Sharing location with you. o(TヘTo)");
} }
} }
else else
{ {
ImGui.TextUnformatted("NOT Sharing location with you.(⊙x⊙;)"); ImGui.TextUnformatted("User not online. (´・ω・`)?");
} }
ImGui.Separator(); ImGui.Separator();
if (shareLocationToOther) if (shareLocationToOther)
{ {
ImGui.TextUnformatted("Sharing your location.ヾ(•ω•`)o"); ImGui.TextUnformatted("Sharing your location. ヾ(•ω•`)o");
if (expireAt != DateTimeOffset.MaxValue) if (expireAt != DateTimeOffset.MaxValue)
{ {
ImGui.TextUnformatted("Expires at " + expireAt.ToLocalTime().ToString("g")); ImGui.TextUnformatted("Expires at " + expireAt.ToLocalTime().ToString("g"));
@@ -678,7 +679,7 @@ public class DrawUserPair
} }
else else
{ {
ImGui.TextUnformatted("NOT sharing your location.(´。_。`)"); ImGui.TextUnformatted("NOT sharing your location.  ̄へ ̄");
} }
ImGui.EndTooltip(); ImGui.EndTooltip();
} }

View File

@@ -2183,7 +2183,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
bool toggleClicked = false; bool toggleClicked = false;
if (showToggle) if (showToggle)
{ {
var icon = isCollapsed ? FontAwesomeIcon.ChevronRight : FontAwesomeIcon.ChevronLeft; var icon = !isCollapsed ? FontAwesomeIcon.ChevronRight : FontAwesomeIcon.ChevronLeft;
Vector2 iconSize; Vector2 iconSize;
using (_uiSharedService.IconFont.Push()) using (_uiSharedService.IconFont.Push())
{ {

View File

@@ -116,7 +116,7 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
var drawList = ImGui.GetWindowDrawList(); var drawList = ImGui.GetWindowDrawList();
var purple = UIColors.Get("LightlessPurple"); var purple = UIColors.Get("LightlessPurple");
var gradLeft = purple.WithAlpha(0.0f); var gradLeft = purple.WithAlpha(0.0f);
var gradRight = purple.WithAlpha(0.85f); var gradRight = purple.WithAlpha(0.85f);
uint colTopLeft = ImGui.ColorConvertFloat4ToU32(gradLeft); uint colTopLeft = ImGui.ColorConvertFloat4ToU32(gradLeft);
@@ -162,7 +162,7 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
var subtitlePos = new Vector2( var subtitlePos = new Vector2(
pMin.X + 12f * scale, pMin.X + 12f * scale,
titlePos.Y + titleHeight - 2f * scale); titlePos.Y + titleHeight - 2f * scale);
ImGui.SetCursorScreenPos(subtitlePos); ImGui.SetCursorScreenPos(subtitlePos);
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey); 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."); 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) 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( UiSharedService.ColorTextWrapped(
"Automatic prune is currently disabled. Enable it and choose an inactivity threshold to let the server clean up inactive users automatically.", "Automatic prune is currently disabled. Enable it and choose an inactivity threshold to let the server clean up inactive users automatically.",
ImGuiColors.DalamudGrey); ImGuiColors.DalamudGrey);
@@ -593,7 +595,7 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
_uiSharedService.DrawCombo( _uiSharedService.DrawCombo(
"Day(s) of inactivity", "Day(s) of inactivity",
[0, 1, 3, 7, 14, 30, 90], [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) => (selected) =>
{ {
_pruneDays = selected; _pruneDays = selected;
@@ -663,8 +665,8 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
var style = ImGui.GetStyle(); var style = ImGui.GetStyle();
float fullW = ImGui.GetContentRegionAvail().X; float fullW = ImGui.GetContentRegionAvail().X;
float colIdentity = fullW * 0.45f; float colIdentity = fullW * 0.45f;
float colMeta = fullW * 0.35f; float colMeta = fullW * 0.35f;
float colActions = fullW - colIdentity - colMeta - style.ItemSpacing.X * 2.0f; float colActions = fullW - colIdentity - colMeta - style.ItemSpacing.X * 2.0f;
// Header // Header
@@ -873,7 +875,7 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
var boolcolor = UiSharedService.GetBoolColor(pair.IsOnline); var boolcolor = UiSharedService.GetBoolColor(pair.IsOnline);
UiSharedService.ColorText(text, boolcolor); UiSharedService.ColorText(text, boolcolor);
if (ImGui.IsItemClicked()) if (ImGui.IsItemClicked())
ImGui.SetClipboardText(pair.UserData.AliasOrUID); ImGui.SetClipboardText(pair.UserData.AliasOrUID);
@@ -1093,6 +1095,7 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
ImGui.Dummy(new Vector2(0, 4 * ImGuiHelpers.GlobalScale)); ImGui.Dummy(new Vector2(0, 4 * ImGuiHelpers.GlobalScale));
} }
private void SavePruneSettings() private void SavePruneSettings()
{ {
if (_autoPruneDays <= 0) if (_autoPruneDays <= 0)
@@ -1100,8 +1103,7 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
_autoPruneEnabled = false; _autoPruneEnabled = false;
} }
var enabled = _autoPruneEnabled && _autoPruneDays > 0; var dto = new GroupPruneSettingsDto(Group: GroupFullInfo.Group, AutoPruneEnabled: _autoPruneEnabled, AutoPruneDays: _autoPruneDays);
var dto = new GroupPruneSettingsDto(Group: GroupFullInfo.Group, AutoPruneEnabled: enabled, AutoPruneDays: enabled ? _autoPruneDays : 0);
try try
{ {

View File

@@ -502,6 +502,14 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
} }
} }
private void RemoveStatus(string key)
{
lock (_downloadStatusLock)
{
_downloadStatus.Remove(key);
}
}
private async Task DecompressBlockFileAsync( private async Task DecompressBlockFileAsync(
string downloadStatusKey, string downloadStatusKey,
string blockFilePath, string blockFilePath,
@@ -595,6 +603,10 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
{ {
Logger.LogError(ex, "{dlName}: Error during block file read", downloadLabel); Logger.LogError(ex, "{dlName}: Error during block file read", downloadLabel);
} }
finally
{
RemoveStatus(downloadStatusKey);
}
} }
public async Task<List<DownloadFileTransfer>> InitiateDownloadList( public async Task<List<DownloadFileTransfer>> InitiateDownloadList(
@@ -866,6 +878,8 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
MarkTransferredFiles(directDownload.DirectDownloadUrl!, 1); MarkTransferredFiles(directDownload.DirectDownloadUrl!, 1);
Logger.LogDebug("Finished direct download of {hash}.", directDownload.Hash); Logger.LogDebug("Finished direct download of {hash}.", directDownload.Hash);
RemoveStatus(directDownload.DirectDownloadUrl!);
} }
catch (OperationCanceledException ex) catch (OperationCanceledException ex)
{ {