Compare commits

...

8 Commits

Author SHA1 Message Date
defnotken
7d2a914c84 Queue File compacting to let workers download as priority, Offload decompression task 2026-01-01 20:57:37 -06:00
7e61954541 Location Sharing 2.0 (#125)
Need: Lightless-Sync/LightlessServer#49
Authored-by: Tsubasahane <wozaiha@gmail.com>
Reviewed-on: #125
Reviewed-by: cake <cake@noreply.git.lightless-sync.org>
Co-authored-by: Tsubasa <tsubasa@noreply.git.lightless-sync.org>
Co-committed-by: Tsubasa <tsubasa@noreply.git.lightless-sync.org>
2025-12-31 17:31:31 +00:00
bbb3375661 2.0.3 staaato 2025-12-31 02:44:31 +00:00
ed7932ab83 2.0.2
All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 2m3s
Reviewed-on: #106
2025-12-31 02:29:36 +00:00
4eaaaf694c Merge pull request 'Complete Decompression after try.' (#122) from decompression-bullshit into 2.0.2
Reviewed-on: #122
2025-12-30 15:25:55 +00:00
defnotken
c32c89d1a8 Complete Decompression after try. 2025-12-30 08:52:59 -06:00
a8b58d05d6 Merge pull request 'pair-adapter-debug' (#121) from pair-adapter-debug into 2.0.2
Reviewed-on: #121
2025-12-30 14:29:53 +00:00
cake
308c220735 Fixed auto prune options locked 2025-12-30 02:08:54 +01:00
15 changed files with 439 additions and 85 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>
@@ -37,6 +37,7 @@
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="10.0.1" /> <PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="10.0.1" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="10.0.1" /> <PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="10.0.1" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="10.0.1" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.1" /> <PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.1" />
<PackageReference Include="Glamourer.Api" Version="2.8.0" /> <PackageReference Include="Glamourer.Api" Version="2.8.0" />
<PackageReference Include="NReco.Logging.File" Version="1.3.1" /> <PackageReference Include="NReco.Logging.File" Version="1.3.1" />

View File

@@ -140,6 +140,7 @@ public sealed class Plugin : IDalamudPlugin
services.AddSingleton<IdDisplayHandler>(); services.AddSingleton<IdDisplayHandler>();
services.AddSingleton<PlayerPerformanceService>(); services.AddSingleton<PlayerPerformanceService>();
services.AddSingleton<PenumbraTempCollectionJanitor>(); services.AddSingleton<PenumbraTempCollectionJanitor>();
services.AddSingleton<LocationShareService>();
services.AddSingleton<TextureMetadataHelper>(sp => services.AddSingleton<TextureMetadataHelper>(sp =>
new TextureMetadataHelper(sp.GetRequiredService<ILogger<TextureMetadataHelper>>(), gameData)); new TextureMetadataHelper(sp.GetRequiredService<ILogger<TextureMetadataHelper>>(), gameData));

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;
@@ -173,8 +172,7 @@ internal class ContextMenuService : IHostedService
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;
@@ -227,8 +225,7 @@ 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

@@ -1,11 +1,13 @@
using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState.Conditions;
using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Game.ClientState.Objects.SubKinds;
using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Game.Text;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using Dalamud.Utility; using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Character;
using FFXIVClientStructs.FFXIV.Client.Game.Control; using FFXIVClientStructs.FFXIV.Client.Game.Control;
using FFXIVClientStructs.FFXIV.Client.Game.UI;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using FFXIVClientStructs.FFXIV.Client.UI.Agent; using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using LightlessSync.API.Dto.CharaData; using LightlessSync.API.Dto.CharaData;
@@ -26,6 +28,7 @@ using System.Text;
using BattleNpcSubKind = FFXIVClientStructs.FFXIV.Client.Game.Object.BattleNpcSubKind; using BattleNpcSubKind = FFXIVClientStructs.FFXIV.Client.Game.Object.BattleNpcSubKind;
using DalamudObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind; using DalamudObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind;
using GameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject; using GameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject;
using Map = Lumina.Excel.Sheets.Map;
using VisibilityFlags = FFXIVClientStructs.FFXIV.Client.Game.Object.VisibilityFlags; using VisibilityFlags = FFXIVClientStructs.FFXIV.Client.Game.Object.VisibilityFlags;
namespace LightlessSync.Services; namespace LightlessSync.Services;
@@ -57,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;
@@ -86,7 +90,8 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
WorldData = new(() => WorldData = new(() =>
{ {
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 or 201 }))
.ToDictionary(w => (ushort)w.RowId, w => w.Name.ToString()); .ToDictionary(w => (ushort)w.RowId, w => w.Name.ToString());
}); });
JobData = new(() => JobData = new(() =>
@@ -659,7 +664,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
var location = new LocationInfo(); var location = new LocationInfo();
location.ServerId = _playerState.CurrentWorld.RowId; location.ServerId = _playerState.CurrentWorld.RowId;
//location.InstanceId = UIState.Instance()->PublicInstance.InstanceId; //TODO:Need API update first location.InstanceId = UIState.Instance()->PublicInstance.InstanceId;
location.TerritoryId = _clientState.TerritoryType; location.TerritoryId = _clientState.TerritoryType;
location.MapId = _clientState.MapId; location.MapId = _clientState.MapId;
if (houseMan != null) if (houseMan != null)
@@ -685,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));
@@ -713,10 +718,10 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
str += $" - {MapData.Value[(ushort)location.MapId].MapName}"; str += $" - {MapData.Value[(ushort)location.MapId].MapName}";
} }
// if (location.InstanceId is not 0) if (location.InstanceId is not 0)
// { {
// str += ((SeIconChar)(57520 + location.InstanceId)).ToIconString(); str += ((SeIconChar)(57520 + location.InstanceId)).ToIconString();
// } }
if (location.WardId is not 0) if (location.WardId is not 0)
{ {
@@ -1136,6 +1141,18 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
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

@@ -0,0 +1,137 @@
using LightlessSync.API.Data;
using LightlessSync.API.Dto.CharaData;
using LightlessSync.API.Dto.User;
using LightlessSync.Services.Mediator;
using LightlessSync.WebAPI;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Primitives;
namespace LightlessSync.Services
{
public class LocationShareService : DisposableMediatorSubscriberBase
{
private readonly DalamudUtilService _dalamudUtilService;
private readonly ApiController _apiController;
private IMemoryCache _locations = new MemoryCache(new MemoryCacheOptions());
private IMemoryCache _sharingStatus = new MemoryCache(new MemoryCacheOptions());
private CancellationTokenSource _resetToken = new CancellationTokenSource();
public LocationShareService(ILogger<LocationShareService> logger, LightlessMediator mediator, DalamudUtilService dalamudUtilService, ApiController apiController) : base(logger, mediator)
{
_dalamudUtilService = dalamudUtilService;
_apiController = apiController;
Mediator.Subscribe<DisconnectedMessage>(this, (msg) =>
{
_resetToken.Cancel();
_resetToken.Dispose();
_resetToken = new CancellationTokenSource();
});
Mediator.Subscribe<ConnectedMessage>(this, (msg) =>
{
_ = _apiController.UpdateLocation(new LocationDto(new UserData(_apiController.UID, apiController.DisplayName), _dalamudUtilService.GetMapData()));
_ = RequestAllLocation();
} );
Mediator.Subscribe<LocationSharingMessage>(this, UpdateLocationList);
Mediator.Subscribe<MapChangedMessage>(this,
msg => _ = _apiController.UpdateLocation(new LocationDto(new UserData(_apiController.UID, _apiController.DisplayName), _dalamudUtilService.GetMapData())));
}
private void UpdateLocationList(LocationSharingMessage msg)
{
if (_locations.TryGetValue(msg.User.UID, out _) && msg.LocationInfo.ServerId is 0)
{
_locations.Remove(msg.User.UID);
return;
}
if ( msg.LocationInfo.ServerId is not 0 && msg.ExpireAt > DateTime.UtcNow)
{
AddLocationInfo(msg.User.UID, msg.LocationInfo, msg.ExpireAt);
}
}
private void AddLocationInfo(string uid, LocationInfo location, DateTimeOffset expireAt)
{
var options = new MemoryCacheEntryOptions()
.SetAbsoluteExpiration(expireAt)
.AddExpirationToken(new CancellationChangeToken(_resetToken.Token));
_locations.Set(uid, location, options);
}
private async Task RequestAllLocation()
{
try
{
var (data, status) = await _apiController.RequestAllLocationInfo().ConfigureAwait(false);
foreach (var dto in data)
{
AddLocationInfo(dto.LocationDto.User.UID, dto.LocationDto.Location, dto.ExpireAt);
}
foreach (var dto in status)
{
AddStatus(dto.User.UID, dto.ExpireAt);
}
}
catch (Exception e)
{
Logger.LogError(e,"RequestAllLocation error : ");
throw;
}
}
private void AddStatus(string uid, DateTimeOffset expireAt)
{
var options = new MemoryCacheEntryOptions()
.SetAbsoluteExpiration(expireAt)
.AddExpirationToken(new CancellationChangeToken(_resetToken.Token));
_sharingStatus.Set(uid, expireAt, options);
}
public string GetUserLocation(string uid)
{
try
{
if (_locations.TryGetValue<LocationInfo>(uid, out var location))
{
return _dalamudUtilService.LocationToString(location);
}
return String.Empty;
}
catch (Exception e)
{
Logger.LogError(e,"GetUserLocation error : ");
throw;
}
}
public DateTimeOffset GetSharingStatus(string uid)
{
try
{
if (_sharingStatus.TryGetValue<DateTimeOffset>(uid, out var expireAt))
{
return expireAt;
}
return DateTimeOffset.MinValue;
}
catch (Exception e)
{
Logger.LogError(e,"GetSharingStatus error : ");
throw;
}
}
public void UpdateSharingStatus(List<string> users, DateTimeOffset expireAt)
{
foreach (var user in users)
{
AddStatus(user, expireAt);
}
}
}
}

View File

@@ -135,5 +135,7 @@ public record ChatChannelsUpdated : MessageBase;
public record ChatChannelMessageAdded(string ChannelKey, ChatMessageEntry Message) : MessageBase; public record ChatChannelMessageAdded(string ChannelKey, ChatMessageEntry Message) : MessageBase;
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 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

@@ -37,6 +37,7 @@ public class DrawUserPair
private readonly UiSharedService _uiSharedService; private readonly UiSharedService _uiSharedService;
private readonly PlayerPerformanceConfigService _performanceConfigService; private readonly PlayerPerformanceConfigService _performanceConfigService;
private readonly LightlessConfigService _configService; private readonly LightlessConfigService _configService;
private readonly LocationShareService _locationShareService;
private readonly CharaDataManager _charaDataManager; private readonly CharaDataManager _charaDataManager;
private readonly PairLedger _pairLedger; private readonly PairLedger _pairLedger;
private float _menuWidth = -1; private float _menuWidth = -1;
@@ -57,6 +58,7 @@ public class DrawUserPair
UiSharedService uiSharedService, UiSharedService uiSharedService,
PlayerPerformanceConfigService performanceConfigService, PlayerPerformanceConfigService performanceConfigService,
LightlessConfigService configService, LightlessConfigService configService,
LocationShareService locationShareService,
CharaDataManager charaDataManager, CharaDataManager charaDataManager,
PairLedger pairLedger) PairLedger pairLedger)
{ {
@@ -74,6 +76,7 @@ public class DrawUserPair
_uiSharedService = uiSharedService; _uiSharedService = uiSharedService;
_performanceConfigService = performanceConfigService; _performanceConfigService = performanceConfigService;
_configService = configService; _configService = configService;
_locationShareService = locationShareService;
_charaDataManager = charaDataManager; _charaDataManager = charaDataManager;
_pairLedger = pairLedger; _pairLedger = pairLedger;
} }
@@ -216,6 +219,48 @@ public class DrawUserPair
_ = _apiController.UserSetPairPermissions(new UserPermissionsDto(_pair.UserData, permissions)); _ = _apiController.UserSetPairPermissions(new UserPermissionsDto(_pair.UserData, permissions));
} }
UiSharedService.AttachToolTip("Changes VFX sync permissions with this user." + (individual ? individualText : string.Empty)); UiSharedService.AttachToolTip("Changes VFX sync permissions with this user." + (individual ? individualText : string.Empty));
ImGui.SetCursorPosX(10f);
_uiSharedService.IconText(FontAwesomeIcon.Globe);
ImGui.SameLine();
if (ImGui.BeginMenu("Toggle Location sharing"))
{
if (ImGui.MenuItem("Share for 30 Mins"))
{
_ = ToggleLocationSharing([_pair.UserData.UID], DateTimeOffset.UtcNow.AddMinutes(30));
}
if (ImGui.MenuItem("Share for 1 Hour"))
{
_ = ToggleLocationSharing([_pair.UserData.UID], DateTimeOffset.UtcNow.AddHours(1));
}
if (ImGui.MenuItem("Share for 3 Hours"))
{
_ = ToggleLocationSharing([_pair.UserData.UID], DateTimeOffset.UtcNow.AddHours(3));
}
if (ImGui.MenuItem("Share until manually stop"))
{
_ = ToggleLocationSharing([_pair.UserData.UID], DateTimeOffset.MaxValue);
}
ImGui.Separator();
if (ImGui.MenuItem("Stop Sharing"))
{
_ = ToggleLocationSharing([_pair.UserData.UID], DateTimeOffset.MinValue);
}
ImGui.EndMenu();
}
}
private async Task ToggleLocationSharing(List<string> users, DateTimeOffset expireAt)
{
var updated = await _apiController.ToggleLocationSharing(new LocationSharingToggleDto(users, expireAt)).ConfigureAwait(false);
if (updated)
{
_locationShareService.UpdateSharingStatus(users, expireAt);
}
} }
private void DrawIndividualMenu() private void DrawIndividualMenu()
@@ -575,6 +620,71 @@ public class DrawUserPair
var individualIsSticky = _pair.UserPair!.OwnPermissions.IsSticky(); var individualIsSticky = _pair.UserPair!.OwnPermissions.IsSticky();
var individualIcon = individualIsSticky ? FontAwesomeIcon.ArrowCircleUp : FontAwesomeIcon.InfoCircle; var individualIcon = individualIsSticky ? FontAwesomeIcon.ArrowCircleUp : FontAwesomeIcon.InfoCircle;
var shareLocationIcon = FontAwesomeIcon.Globe;
var location = _locationShareService.GetUserLocation(_pair.UserPair!.User.UID);
var shareLocation = !string.IsNullOrEmpty(location);
var expireAt = _locationShareService.GetSharingStatus(_pair.UserPair!.User.UID);
var shareLocationToOther = expireAt > DateTimeOffset.UtcNow;
var shareColor = shareLocation switch
{
true when shareLocationToOther => UIColors.Get("LightlessGreen"),
true when !shareLocationToOther => UIColors.Get("LightlessBlue"),
_ => UIColors.Get("LightlessYellow"),
};
if (shareLocation || shareLocationToOther)
{
currentRightSide -= (_uiSharedService.GetIconSize(shareLocationIcon).X + spacingX);
ImGui.SameLine(currentRightSide);
using (ImRaii.PushColor(ImGuiCol.Text, shareColor, shareLocation || shareLocationToOther))
_uiSharedService.IconText(shareLocationIcon);
if (ImGui.IsItemHovered())
{
ImGui.BeginTooltip();
if (_pair.IsOnline)
{
if (shareLocation)
{
if (!string.IsNullOrEmpty(location))
{
_uiSharedService.IconText(FontAwesomeIcon.LocationArrow);
ImGui.SameLine();
ImGui.TextUnformatted(location);
}
else
{
ImGui.TextUnformatted("Location info not updated, reconnect or wait for update.");
}
}
else
{
ImGui.TextUnformatted("NOT Sharing location with you. o(TヘTo)");
}
}
else
{
ImGui.TextUnformatted("User not online. (´・ω・`)?");
}
ImGui.Separator();
if (shareLocationToOther)
{
ImGui.TextUnformatted("Sharing your location. ヾ(•ω•`)o");
if (expireAt != DateTimeOffset.MaxValue)
{
ImGui.TextUnformatted("Expires at " + expireAt.ToLocalTime().ToString("g"));
}
}
else
{
ImGui.TextUnformatted("NOT sharing your location.  ̄へ ̄");
}
ImGui.EndTooltip();
}
}
if (individualAnimDisabled || individualSoundsDisabled || individualVFXDisabled || individualIsSticky) if (individualAnimDisabled || individualSoundsDisabled || individualVFXDisabled || individualIsSticky)
{ {
currentRightSide -= (_uiSharedService.GetIconSize(individualIcon).X + spacingX); currentRightSide -= (_uiSharedService.GetIconSize(individualIcon).X + spacingX);

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

@@ -29,6 +29,7 @@ public class DrawEntityFactory
private readonly LightlessConfigService _configService; private readonly LightlessConfigService _configService;
private readonly UiSharedService _uiSharedService; private readonly UiSharedService _uiSharedService;
private readonly PlayerPerformanceConfigService _playerPerformanceConfigService; private readonly PlayerPerformanceConfigService _playerPerformanceConfigService;
private readonly LocationShareService _locationShareService;
private readonly CharaDataManager _charaDataManager; private readonly CharaDataManager _charaDataManager;
private readonly SelectTagForPairUi _selectTagForPairUi; private readonly SelectTagForPairUi _selectTagForPairUi;
private readonly RenamePairTagUi _renamePairTagUi; private readonly RenamePairTagUi _renamePairTagUi;
@@ -53,6 +54,7 @@ public class DrawEntityFactory
LightlessConfigService configService, LightlessConfigService configService,
UiSharedService uiSharedService, UiSharedService uiSharedService,
PlayerPerformanceConfigService playerPerformanceConfigService, PlayerPerformanceConfigService playerPerformanceConfigService,
LocationShareService locationShareService,
CharaDataManager charaDataManager, CharaDataManager charaDataManager,
SelectTagForSyncshellUi selectTagForSyncshellUi, SelectTagForSyncshellUi selectTagForSyncshellUi,
RenameSyncshellTagUi renameSyncshellTagUi, RenameSyncshellTagUi renameSyncshellTagUi,
@@ -72,6 +74,7 @@ public class DrawEntityFactory
_configService = configService; _configService = configService;
_uiSharedService = uiSharedService; _uiSharedService = uiSharedService;
_playerPerformanceConfigService = playerPerformanceConfigService; _playerPerformanceConfigService = playerPerformanceConfigService;
_locationShareService = locationShareService;
_charaDataManager = charaDataManager; _charaDataManager = charaDataManager;
_selectTagForSyncshellUi = selectTagForSyncshellUi; _selectTagForSyncshellUi = selectTagForSyncshellUi;
_renameSyncshellTagUi = renameSyncshellTagUi; _renameSyncshellTagUi = renameSyncshellTagUi;
@@ -162,6 +165,7 @@ public class DrawEntityFactory
_uiSharedService, _uiSharedService,
_playerPerformanceConfigService, _playerPerformanceConfigService,
_configService, _configService,
_locationShareService,
_charaDataManager, _charaDataManager,
_pairLedger); _pairLedger);
} }

View File

@@ -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.");
if (!_autoPruneEnabled)
{
ImGui.BeginDisabled();
}
ImGui.SameLine(); ImGui.SameLine();
ImGui.SetNextItemWidth(150); ImGui.SetNextItemWidth(150);
using (ImRaii.Disabled(!_autoPruneEnabled))
{
_uiSharedService.DrawCombo( _uiSharedService.DrawCombo(
"Day(s) of inactivity", "Day(s) of inactivity (gets checked hourly)",
[1, 3, 7, 14, 30, 90], [0, 1, 3, 7, 14, 30, 90],
days => $"{days} day(s)", (count) => count == 0 ? "2 hours(s)" : count + " day(s)",
selected => selected =>
{ {
_autoPruneDays = selected; _autoPruneDays = selected;
SavePruneSettings(); SavePruneSettings();
}, },
_autoPruneDays); _autoPruneDays);
}
if (!_autoPruneEnabled) 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;
@@ -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

@@ -31,6 +31,8 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
private readonly SemaphoreSlim _decompressGate = private readonly SemaphoreSlim _decompressGate =
new(Math.Max(1, Environment.ProcessorCount / 2), Math.Max(1, Environment.ProcessorCount / 2)); new(Math.Max(1, Environment.ProcessorCount / 2), Math.Max(1, Environment.ProcessorCount / 2));
private readonly ConcurrentQueue<string> _deferredCompressionQueue = new();
private volatile bool _disableDirectDownloads; private volatile bool _disableDirectDownloads;
private int _consecutiveDirectDownloadFailures; private int _consecutiveDirectDownloadFailures;
private bool _lastConfigDirectDownloadsState; private bool _lastConfigDirectDownloadsState;
@@ -502,6 +504,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,
@@ -548,7 +558,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
if (len == 0) if (len == 0)
{ {
await _fileCompactor.WriteAllBytesAsync(filePath, Array.Empty<byte>(), ct).ConfigureAwait(false); await File.WriteAllBytesAsync(filePath, Array.Empty<byte>(), ct).ConfigureAwait(false);
PersistFileToStorage(fileHash, filePath, repl.GamePath, skipDownscale); PersistFileToStorage(fileHash, filePath, repl.GamePath, skipDownscale);
continue; continue;
} }
@@ -558,6 +568,9 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
// limit concurrent decompressions // limit concurrent decompressions
await _decompressGate.WaitAsync(ct).ConfigureAwait(false); await _decompressGate.WaitAsync(ct).ConfigureAwait(false);
try try
{
// offload CPU-intensive decompression to threadpool to free up worker
await Task.Run(async () =>
{ {
var sw = System.Diagnostics.Stopwatch.StartNew(); var sw = System.Diagnostics.Stopwatch.StartNew();
@@ -567,9 +580,10 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
Logger.LogTrace("{dlName}: Unwrap {fileHash} took {ms}ms (compressed {c} bytes, decompressed {d} bytes)", Logger.LogTrace("{dlName}: Unwrap {fileHash} took {ms}ms (compressed {c} bytes, decompressed {d} bytes)",
downloadLabel, fileHash, sw.ElapsedMilliseconds, compressed.Length, decompressed?.Length ?? -1); downloadLabel, fileHash, sw.ElapsedMilliseconds, compressed.Length, decompressed?.Length ?? -1);
// write to file // write to file without compacting during download
await _fileCompactor.WriteAllBytesAsync(filePath, decompressed, ct).ConfigureAwait(false); await File.WriteAllBytesAsync(filePath, decompressed, ct).ConfigureAwait(false);
PersistFileToStorage(fileHash, filePath, repl.GamePath, skipDownscale); PersistFileToStorage(fileHash, filePath, repl.GamePath, skipDownscale);
}, ct).ConfigureAwait(false);
} }
finally finally
{ {
@@ -595,6 +609,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(
@@ -740,8 +758,16 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
if (gameObjectHandler is not null) if (gameObjectHandler is not null)
Mediator.Publish(new DownloadStartedMessage(gameObjectHandler, _downloadStatus)); Mediator.Publish(new DownloadStartedMessage(gameObjectHandler, _downloadStatus));
// work based on cpu count and slots
var coreCount = Environment.ProcessorCount;
var baseWorkers = Math.Min(slots, coreCount);
// only add buffer if decompression has capacity AND we have cores to spare
var availableDecompressSlots = _decompressGate.CurrentCount;
var extraWorkers = (availableDecompressSlots > 0 && coreCount >= 6) ? 2 : 0;
// allow some extra workers so downloads can continue while earlier items decompress. // allow some extra workers so downloads can continue while earlier items decompress.
var workerDop = Math.Clamp(slots * 2, 2, 16); var workerDop = Math.Clamp(baseWorkers + extraWorkers, 2, coreCount);
// batch downloads // batch downloads
Task batchTask = batchChunks.Length == 0 Task batchTask = batchChunks.Length == 0
@@ -757,6 +783,9 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
await Task.WhenAll(batchTask, directTask).ConfigureAwait(false); await Task.WhenAll(batchTask, directTask).ConfigureAwait(false);
// process deferred compressions after all downloads complete
await ProcessDeferredCompressionsAsync(ct).ConfigureAwait(false);
Logger.LogDebug("Download end: {id}", objectName); Logger.LogDebug("Download end: {id}", objectName);
ClearDownload(); ClearDownload();
} }
@@ -861,11 +890,13 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
byte[] compressedBytes = await File.ReadAllBytesAsync(tempFilename, ct).ConfigureAwait(false); byte[] compressedBytes = await File.ReadAllBytesAsync(tempFilename, ct).ConfigureAwait(false);
var decompressedBytes = LZ4Wrapper.Unwrap(compressedBytes); var decompressedBytes = LZ4Wrapper.Unwrap(compressedBytes);
await _fileCompactor.WriteAllBytesAsync(finalFilename, decompressedBytes, ct).ConfigureAwait(false); await File.WriteAllBytesAsync(finalFilename, decompressedBytes, ct).ConfigureAwait(false);
PersistFileToStorage(directDownload.Hash, finalFilename, repl.GamePath, skipDownscale); PersistFileToStorage(directDownload.Hash, finalFilename, repl.GamePath, skipDownscale);
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)
{ {
@@ -987,6 +1018,10 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
fi.LastAccessTime = DateTime.Today; fi.LastAccessTime = DateTime.Today;
fi.LastWriteTime = RandomDayInThePast().Invoke(); fi.LastWriteTime = RandomDayInThePast().Invoke();
// queue file for deferred compression instead of compressing immediately
if (_configService.Current.UseCompactor)
_deferredCompressionQueue.Enqueue(filePath);
try try
{ {
var entry = _fileDbManager.CreateCacheEntry(filePath); var entry = _fileDbManager.CreateCacheEntry(filePath);
@@ -1012,6 +1047,52 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
private static IProgress<long> CreateInlineProgress(Action<long> callback) => new InlineProgress(callback); private static IProgress<long> CreateInlineProgress(Action<long> callback) => new InlineProgress(callback);
private async Task ProcessDeferredCompressionsAsync(CancellationToken ct)
{
if (_deferredCompressionQueue.IsEmpty)
return;
var filesToCompress = new List<string>();
while (_deferredCompressionQueue.TryDequeue(out var filePath))
{
if (File.Exists(filePath))
filesToCompress.Add(filePath);
}
if (filesToCompress.Count == 0)
return;
Logger.LogDebug("Starting deferred compression of {count} files", filesToCompress.Count);
var compressionWorkers = Math.Clamp(Environment.ProcessorCount / 4, 2, 4);
await Parallel.ForEachAsync(filesToCompress,
new ParallelOptions
{
MaxDegreeOfParallelism = compressionWorkers,
CancellationToken = ct
},
async (filePath, token) =>
{
try
{
await Task.Yield();
if (_configService.Current.UseCompactor && File.Exists(filePath))
{
var bytes = await File.ReadAllBytesAsync(filePath, token).ConfigureAwait(false);
await _fileCompactor.WriteAllBytesAsync(filePath, bytes, token).ConfigureAwait(false);
Logger.LogTrace("Compressed file: {filePath}", filePath);
}
}
catch (Exception ex)
{
Logger.LogWarning(ex, "Failed to compress file: {filePath}", filePath);
}
}).ConfigureAwait(false);
Logger.LogDebug("Completed deferred compression of {count} files", filesToCompress.Count);
}
private sealed class InlineProgress : IProgress<long> private sealed class InlineProgress : IProgress<long>
{ {
private readonly Action<long> _callback; private readonly Action<long> _callback;

View File

@@ -200,5 +200,21 @@ public partial class ApiController
await UserPushData(new(visibleCharacters, character, censusDto)).ConfigureAwait(false); await UserPushData(new(visibleCharacters, character, censusDto)).ConfigureAwait(false);
} }
public async Task UpdateLocation(LocationDto locationDto, bool offline = false)
{
if (!IsConnected) return;
await _lightlessHub!.SendAsync(nameof(UpdateLocation), locationDto, offline).ConfigureAwait(false);
}
public async Task<(List<LocationWithTimeDto>, List<SharingStatusDto>)> RequestAllLocationInfo()
{
if (!IsConnected) return ([],[]);
return await _lightlessHub!.InvokeAsync<(List<LocationWithTimeDto>, List<SharingStatusDto>)>(nameof(RequestAllLocationInfo)).ConfigureAwait(false);
}
public async Task<bool> ToggleLocationSharing(LocationSharingToggleDto dto)
{
if (!IsConnected) return false;
return await _lightlessHub!.InvokeAsync<bool>(nameof(ToggleLocationSharing), dto).ConfigureAwait(false);
}
} }
#pragma warning restore MA0040 #pragma warning restore MA0040

View File

@@ -260,6 +260,13 @@ public partial class ApiController
return Task.CompletedTask; return Task.CompletedTask;
} }
public Task Client_SendLocationToClient(LocationDto locationDto, DateTimeOffset expireAt)
{
Logger.LogDebug($"{nameof(Client_SendLocationToClient)}: {locationDto.User} {expireAt}");
ExecuteSafely(() => Mediator.Publish(new LocationSharingMessage(locationDto.User, locationDto.Location, expireAt)));
return Task.CompletedTask;
}
public void OnDownloadReady(Action<Guid> act) public void OnDownloadReady(Action<Guid> act)
{ {
if (_initialized) return; if (_initialized) return;
@@ -441,6 +448,12 @@ public partial class ApiController
_lightlessHub!.On(nameof(Client_GposeLobbyPushWorldData), act); _lightlessHub!.On(nameof(Client_GposeLobbyPushWorldData), act);
} }
public void OnReceiveLocation(Action<LocationDto, DateTimeOffset> act)
{
if (_initialized) return;
_lightlessHub!.On(nameof(Client_SendLocationToClient), act);
}
private void ExecuteSafely(Action act) private void ExecuteSafely(Action act)
{ {
try try

View File

@@ -606,6 +606,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
OnGposeLobbyPushCharacterData((dto) => _ = Client_GposeLobbyPushCharacterData(dto)); OnGposeLobbyPushCharacterData((dto) => _ = Client_GposeLobbyPushCharacterData(dto));
OnGposeLobbyPushPoseData((dto, data) => _ = Client_GposeLobbyPushPoseData(dto, data)); OnGposeLobbyPushPoseData((dto, data) => _ = Client_GposeLobbyPushPoseData(dto, data));
OnGposeLobbyPushWorldData((dto, data) => _ = Client_GposeLobbyPushWorldData(dto, data)); OnGposeLobbyPushWorldData((dto, data) => _ = Client_GposeLobbyPushWorldData(dto, data));
OnReceiveLocation((dto, time) => _ = Client_SendLocationToClient(dto, time));
_healthCheckTokenSource?.Cancel(); _healthCheckTokenSource?.Cancel();
_healthCheckTokenSource?.Dispose(); _healthCheckTokenSource?.Dispose();
@@ -774,5 +775,6 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
ServerState = state; ServerState = state;
} }
} }
#pragma warning restore MA0040 #pragma warning restore MA0040