Compare commits

...

20 Commits

Author SHA1 Message Date
defnotken
de9c9955ef add more functionality for future features. 2026-01-04 00:54:40 -06:00
defnotken
df33a0f0a2 Move buttons to debug 2026-01-01 17:27:12 -06:00
c439d1c822 Merge branch '2.0.3' into lifestream-ipc 2026-01-01 23:21:46 +00: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
defnotken
fb58d8657d Lifestream IPC witrh Debug Example 2025-12-30 23:43:22 -06: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
9ea0571e82 Lower Time out 2025-12-30 14:29:38 +00:00
cake
308c220735 Fixed auto prune options locked 2025-12-30 02:08:54 +01: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
26 changed files with 725 additions and 128 deletions

View File

@@ -0,0 +1,11 @@
namespace Lifestream.Enums;
public enum ResidentialAetheryteKind
{
None = -1,
Uldah = 9,
Gridania = 2,
Limsa = 8,
Foundation = 70,
Kugane = 111,
}

View File

@@ -0,0 +1 @@
global using AddressBookEntryTuple = (string Name, int World, int City, int Ward, int PropertyType, int Plot, int Apartment, bool ApartmentSubdivision, bool AliasEnabled, string Alias);

View File

@@ -0,0 +1,129 @@
using Dalamud.Plugin;
using Dalamud.Plugin.Ipc;
using Lifestream.Enums;
using LightlessSync.Interop.Ipc.Framework;
using LightlessSync.Services.Mediator;
using Microsoft.Extensions.Logging;
namespace LightlessSync.Interop.Ipc;
public sealed class IpcCallerLifestream : IpcServiceBase
{
private static readonly IpcServiceDescriptor LifestreamDescriptor = new("Lifestream", "Lifestream", new Version(0, 0, 0, 0));
private readonly ICallGateSubscriber<string, object> _executeLifestreamCommand;
private readonly ICallGateSubscriber<AddressBookEntryTuple, bool> _isHere;
private readonly ICallGateSubscriber<AddressBookEntryTuple, object> _goToHousingAddress;
private readonly ICallGateSubscriber<bool> _isBusy;
private readonly ICallGateSubscriber<object> _abort;
private readonly ICallGateSubscriber<string, bool> _changeWorld;
private readonly ICallGateSubscriber<uint, bool> _changeWorldById;
private readonly ICallGateSubscriber<string, bool> _aetheryteTeleport;
private readonly ICallGateSubscriber<uint, bool> _aetheryteTeleportById;
private readonly ICallGateSubscriber<bool> _canChangeInstance;
private readonly ICallGateSubscriber<int> _getCurrentInstance;
private readonly ICallGateSubscriber<int> _getNumberOfInstances;
private readonly ICallGateSubscriber<int, object> _changeInstance;
private readonly ICallGateSubscriber<(ResidentialAetheryteKind, int, int)> _getCurrentPlotInfo;
public IpcCallerLifestream(IDalamudPluginInterface pi, LightlessMediator lightlessMediator, ILogger<IpcCallerLifestream> logger)
: base(logger, lightlessMediator, pi, LifestreamDescriptor)
{
_executeLifestreamCommand = pi.GetIpcSubscriber<string, object>("Lifestream.ExecuteCommand");
_isHere = pi.GetIpcSubscriber<AddressBookEntryTuple, bool>("Lifestream.IsHere");
_goToHousingAddress = pi.GetIpcSubscriber<AddressBookEntryTuple, object>("Lifestream.GoToHousingAddress");
_isBusy = pi.GetIpcSubscriber<bool>("Lifestream.IsBusy");
_abort = pi.GetIpcSubscriber<object>("Lifestream.Abort");
_changeWorld = pi.GetIpcSubscriber<string, bool>("Lifestream.ChangeWorld");
_changeWorldById = pi.GetIpcSubscriber<uint, bool>("Lifestream.ChangeWorldById");
_aetheryteTeleport = pi.GetIpcSubscriber<string, bool>("Lifestream.AetheryteTeleport");
_aetheryteTeleportById = pi.GetIpcSubscriber<uint, bool>("Lifestream.AetheryteTeleportById");
_canChangeInstance = pi.GetIpcSubscriber<bool>("Lifestream.CanChangeInstance");
_getCurrentInstance = pi.GetIpcSubscriber<int>("Lifestream.GetCurrentInstance");
_getNumberOfInstances = pi.GetIpcSubscriber<int>("Lifestream.GetNumberOfInstances");
_changeInstance = pi.GetIpcSubscriber<int, object>("Lifestream.ChangeInstance");
_getCurrentPlotInfo = pi.GetIpcSubscriber<(ResidentialAetheryteKind, int, int)>("Lifestream.GetCurrentPlotInfo");
CheckAPI();
}
public void ExecuteLifestreamCommand(string command)
{
if (!APIAvailable) return;
_executeLifestreamCommand.InvokeAction(command);
}
public bool IsHere(AddressBookEntryTuple entry)
{
if (!APIAvailable) return false;
return _isHere.InvokeFunc(entry);
}
public void GoToHousingAddress(AddressBookEntryTuple entry)
{
if (!APIAvailable) return;
_goToHousingAddress.InvokeAction(entry);
}
public bool IsBusy()
{
if (!APIAvailable) return false;
return _isBusy.InvokeFunc();
}
public void Abort()
{
if (!APIAvailable) return;
_abort.InvokeAction();
}
public bool ChangeWorld(string worldName)
{
if (!APIAvailable) return false;
return _changeWorld.InvokeFunc(worldName);
}
public bool AetheryteTeleport(string aetheryteName)
{
if (!APIAvailable) return false;
return _aetheryteTeleport.InvokeFunc(aetheryteName);
}
public bool ChangeWorldById(uint worldId)
{
if (!APIAvailable) return false;
return _changeWorldById.InvokeFunc(worldId);
}
public bool AetheryteTeleportById(uint aetheryteId)
{
if (!APIAvailable) return false;
return _aetheryteTeleportById.InvokeFunc(aetheryteId);
}
public bool CanChangeInstance()
{
if (!APIAvailable) return false;
return _canChangeInstance.InvokeFunc();
}
public int GetCurrentInstance()
{
if (!APIAvailable) return -1;
return _getCurrentInstance.InvokeFunc();
}
public int GetNumberOfInstances()
{
if (!APIAvailable) return -1;
return _getNumberOfInstances.InvokeFunc();
}
public void ChangeInstance(int instanceNumber)
{
if (!APIAvailable) return;
_changeInstance.InvokeAction(instanceNumber);
}
public (ResidentialAetheryteKind, int, int)? GetCurrentPlotInfo()
{
if (!APIAvailable) return (ResidentialAetheryteKind.None, -1, -1);
return _getCurrentPlotInfo.InvokeFunc();
}
}

View File

@@ -7,7 +7,8 @@ public sealed partial class IpcManager : DisposableMediatorSubscriberBase
{
public IpcManager(ILogger<IpcManager> logger, LightlessMediator mediator,
IpcCallerPenumbra penumbraIpc, IpcCallerGlamourer glamourerIpc, IpcCallerCustomize customizeIpc, IpcCallerHeels heelsIpc,
IpcCallerHonorific honorificIpc, IpcCallerMoodles moodlesIpc, IpcCallerPetNames ipcCallerPetNames, IpcCallerBrio ipcCallerBrio) : base(logger, mediator)
IpcCallerHonorific honorificIpc, IpcCallerMoodles moodlesIpc, IpcCallerPetNames ipcCallerPetNames, IpcCallerBrio ipcCallerBrio,
IpcCallerLifestream ipcCallerLifestream) : base(logger, mediator)
{
CustomizePlus = customizeIpc;
Heels = heelsIpc;
@@ -17,6 +18,7 @@ public sealed partial class IpcManager : DisposableMediatorSubscriberBase
Moodles = moodlesIpc;
PetNames = ipcCallerPetNames;
Brio = ipcCallerBrio;
Lifestream = ipcCallerLifestream;
if (Initialized)
{
@@ -44,8 +46,8 @@ public sealed partial class IpcManager : DisposableMediatorSubscriberBase
public IpcCallerPenumbra Penumbra { get; }
public IpcCallerMoodles Moodles { get; }
public IpcCallerPetNames PetNames { get; }
public IpcCallerBrio Brio { get; }
public IpcCallerLifestream Lifestream { get; }
private void PeriodicApiStateCheck()
{
@@ -58,5 +60,6 @@ public sealed partial class IpcManager : DisposableMediatorSubscriberBase
Moodles.CheckAPI();
PetNames.CheckAPI();
Brio.CheckAPI();
Lifestream.CheckAPI();
}
}

View File

@@ -3,7 +3,7 @@
<PropertyGroup>
<Authors></Authors>
<Company></Company>
<Version>2.0.2</Version>
<Version>2.0.3</Version>
<Description></Description>
<Copyright></Copyright>
<PackageProjectUrl>https://github.com/Light-Public-Syncshells/LightlessClient</PackageProjectUrl>
@@ -37,6 +37,7 @@
</PackageReference>
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" 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="Glamourer.Api" Version="2.8.0" />
<PackageReference Include="NReco.Logging.File" Version="1.3.1" />

View File

@@ -1423,7 +1423,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
private Task _visibilityGraceTask;
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);
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);

View File

@@ -140,6 +140,7 @@ public sealed class Plugin : IDalamudPlugin
services.AddSingleton<IdDisplayHandler>();
services.AddSingleton<PlayerPerformanceService>();
services.AddSingleton<PenumbraTempCollectionJanitor>();
services.AddSingleton<LocationShareService>();
services.AddSingleton<TextureMetadataHelper>(sp =>
new TextureMetadataHelper(sp.GetRequiredService<ILogger<TextureMetadataHelper>>(), gameData));
@@ -372,6 +373,11 @@ public sealed class Plugin : IDalamudPlugin
sp.GetRequiredService<DalamudUtilService>(),
sp.GetRequiredService<LightlessMediator>()));
services.AddSingleton(sp => new IpcCallerLifestream(
pluginInterface,
sp.GetRequiredService<LightlessMediator>(),
sp.GetRequiredService<ILogger<IpcCallerLifestream>>()));
services.AddSingleton(sp => new IpcManager(
sp.GetRequiredService<ILogger<IpcManager>>(),
sp.GetRequiredService<LightlessMediator>(),
@@ -382,7 +388,9 @@ public sealed class Plugin : IDalamudPlugin
sp.GetRequiredService<IpcCallerHonorific>(),
sp.GetRequiredService<IpcCallerMoodles>(),
sp.GetRequiredService<IpcCallerPetNames>(),
sp.GetRequiredService<IpcCallerBrio>()));
sp.GetRequiredService<IpcCallerBrio>(),
sp.GetRequiredService<IpcCallerLifestream>()
));
// Notifications / HTTP
services.AddSingleton(sp => new NotificationService(

View File

@@ -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<World>()!;
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);
}
}

View File

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

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

View File

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

View File

@@ -37,6 +37,7 @@ public class DrawUserPair
private readonly UiSharedService _uiSharedService;
private readonly PlayerPerformanceConfigService _performanceConfigService;
private readonly LightlessConfigService _configService;
private readonly LocationShareService _locationShareService;
private readonly CharaDataManager _charaDataManager;
private readonly PairLedger _pairLedger;
private float _menuWidth = -1;
@@ -57,6 +58,7 @@ public class DrawUserPair
UiSharedService uiSharedService,
PlayerPerformanceConfigService performanceConfigService,
LightlessConfigService configService,
LocationShareService locationShareService,
CharaDataManager charaDataManager,
PairLedger pairLedger)
{
@@ -74,6 +76,7 @@ public class DrawUserPair
_uiSharedService = uiSharedService;
_performanceConfigService = performanceConfigService;
_configService = configService;
_locationShareService = locationShareService;
_charaDataManager = charaDataManager;
_pairLedger = pairLedger;
}
@@ -216,6 +219,48 @@ public class DrawUserPair
_ = _apiController.UserSetPairPermissions(new UserPermissionsDto(_pair.UserData, permissions));
}
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()
@@ -574,6 +619,71 @@ public class DrawUserPair
var individualVFXDisabled = (_pair.UserPair?.OwnPermissions.IsDisableVFX() ?? false) || (_pair.UserPair?.OtherPermissions.IsDisableVFX() ?? false);
var individualIsSticky = _pair.UserPair!.OwnPermissions.IsSticky();
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)
{

View File

@@ -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())
{

View File

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

View File

@@ -5,6 +5,7 @@ using Dalamud.Interface.Colors;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Utility;
using Lifestream.Enums;
using LightlessSync.API.Data;
using LightlessSync.API.Data.Comparer;
using LightlessSync.API.Data.Enum;
@@ -84,6 +85,8 @@ public class SettingsUi : WindowMediatorSubscriberBase
private bool _pairDiagnosticsEnabled;
private string? _selectedPairDebugUid = null;
private string _lightfinderIconInput = string.Empty;
private bool _showLightfinderRendererWarning = false;
private LightfinderLabelRenderer _pendingLightfinderRenderer = LightfinderLabelRenderer.Pictomancy;
private bool _lightfinderIconInputInitialized = false;
private int _lightfinderIconPresetIndex = -1;
private static readonly LightlessConfig DefaultConfig = new();
@@ -1241,7 +1244,6 @@ public class SettingsUi : WindowMediatorSubscriberBase
ImGui.TreePop();
}
#endif
if (_uiShared.IconTextButton(FontAwesomeIcon.Copy, "[DEBUG] Copy Last created Character Data to clipboard"))
{
if (LastCreatedCharacterData != null)
@@ -1257,6 +1259,39 @@ public class SettingsUi : WindowMediatorSubscriberBase
UiSharedService.AttachToolTip("Use this when reporting mods being rejected from the server.");
if (_uiShared.IconTextButton(FontAwesomeIcon.Home, "Teleport to Limsa [LIFESTREAM TEST]") && _ipcManager.Lifestream.APIAvailable)
{
_ipcManager.Lifestream.ExecuteLifestreamCommand("limsa");
}
if (_uiShared.IconTextButton(FontAwesomeIcon.Home, "Teleport to JoyHouse [LIFESTREAM TEST]") && _ipcManager.Lifestream.APIAvailable)
{
var twintania = _dalamudUtilService.WorldData.Value
.FirstOrDefault(kvp => kvp.Value.Equals("Twintania", StringComparison.OrdinalIgnoreCase));
int ward = 29;
int plot = 7;
AddressBookEntryTuple addressEntry = (
Name: "",
World: (int)twintania.Key,
City: (int)ResidentialAetheryteKind.Kugane,
Ward: ward,
PropertyType: 0,
Plot: plot,
Apartment: 1,
ApartmentSubdivision: false,
AliasEnabled: false,
Alias: ""
);
_logger.LogInformation("going to: {address}", addressEntry);
_ipcManager.Lifestream.GoToHousingAddress(addressEntry);
}
#endif
_uiShared.DrawCombo("Log Level", Enum.GetValues<LogLevel>(), (l) => l.ToString(), (l) =>
{
_configService.Current.LogLevel = l;
@@ -2372,7 +2407,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
var labelRenderer = _configService.Current.LightfinderLabelRenderer;
var labelRendererLabel = labelRenderer switch
{
LightfinderLabelRenderer.SignatureHook => "Native nameplate (sig hook)",
LightfinderLabelRenderer.SignatureHook => "Native Nameplate Rendering",
_ => "ImGui Overlay",
};
@@ -2382,18 +2417,25 @@ public class SettingsUi : WindowMediatorSubscriberBase
{
var optionLabel = option switch
{
LightfinderLabelRenderer.SignatureHook => "Native Nameplate (sig hook)",
LightfinderLabelRenderer.SignatureHook => "Native Nameplate Rendering",
_ => "ImGui Overlay",
};
var selected = option == labelRenderer;
if (ImGui.Selectable(optionLabel, selected))
{
_configService.Current.LightfinderLabelRenderer = option;
_configService.Save();
_nameplateService.RequestRedraw();
if (option == LightfinderLabelRenderer.SignatureHook)
{
_pendingLightfinderRenderer = option;
_showLightfinderRendererWarning = true;
}
else
{
_configService.Current.LightfinderLabelRenderer = option;
_configService.Save();
_nameplateService.RequestRedraw();
}
}
if (selected)
ImGui.SetItemDefaultFocus();
}
@@ -2401,6 +2443,34 @@ public class SettingsUi : WindowMediatorSubscriberBase
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.");
UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f);

View File

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

View File

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

View File

@@ -79,6 +79,7 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
private readonly Dictionary<string, DateTime> _oauthTokenExpiry = [];
private bool _penumbraExists = false;
private bool _petNamesExists = false;
private bool _lifestreamExists = false;
private int _serverSelectionIndex = -1;
public UiSharedService(ILogger<UiSharedService> logger, IpcManager ipcManager, ApiController apiController,
CacheMonitor cacheMonitor, FileDialogManager fileDialogManager,
@@ -112,6 +113,7 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
_moodlesExists = _ipcManager.Moodles.APIAvailable;
_petNamesExists = _ipcManager.PetNames.APIAvailable;
_brioExists = _ipcManager.Brio.APIAvailable;
_lifestreamExists = _ipcManager.Lifestream.APIAvailable;
});
UidFont = _pluginInterface.UiBuilder.FontAtlas.NewDelegateFontHandle(e =>
@@ -1105,6 +1107,10 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
ColorText("Brio", GetBoolColor(_brioExists));
AttachToolTip(BuildPluginTooltip("Brio", _brioExists, _ipcManager.Brio.State));
ImGui.SameLine();
ColorText("Lifestream", GetBoolColor(_lifestreamExists));
AttachToolTip(BuildPluginTooltip("Lifestream", _lifestreamExists, _ipcManager.Lifestream.State));
if (!_penumbraExists || !_glamourerExists)
{
ImGui.TextColored(ImGuiColors.DalamudRed, "You need to install both Penumbra and Glamourer and keep them up to date to use Lightless Sync.");

View File

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

View File

@@ -28,6 +28,8 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
private readonly TextureMetadataHelper _textureMetadataHelper;
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 int _consecutiveDirectDownloadFailures;
@@ -500,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,
@@ -522,32 +532,57 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
try
{
// sanity check length
if (fileLengthBytes < 0 || fileLengthBytes > int.MaxValue)
throw new InvalidDataException($"Invalid block entry length: {fileLengthBytes}");
// safe cast after check
var len = checked((int)fileLengthBytes);
if (!replacementLookup.TryGetValue(fileHash, out var repl))
{
Logger.LogWarning("{dlName}: No replacement mapping for {fileHash}", downloadLabel, fileHash);
// still need to skip bytes:
var skip = checked((int)fileLengthBytes);
fileBlockStream.Position += skip;
fileBlockStream.Seek(len, SeekOrigin.Current);
continue;
}
// decompress
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);
var len = checked((int)fileLengthBytes);
// read compressed data
var compressed = new byte[len];
await ReadExactlyAsync(fileBlockStream, compressed.AsMemory(0, len), ct).ConfigureAwait(false);
MungeBuffer(compressed);
var decompressed = LZ4Wrapper.Unwrap(compressed);
if (len == 0)
{
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);
PersistFileToStorage(fileHash, filePath, repl.GamePath, skipDownscale);
MungeBuffer(compressed);
// 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)
{
@@ -568,6 +603,10 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
{
Logger.LogError(ex, "{dlName}: Error during block file read", downloadLabel);
}
finally
{
RemoveStatus(downloadStatusKey);
}
}
public async Task<List<DownloadFileTransfer>> InitiateDownloadList(
@@ -605,20 +644,16 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
.. 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))
{
if (!_orchestrator.ForbiddenTransfers.Exists(f => string.Equals(f.Hash, dto.Hash, StringComparison.Ordinal)))
_orchestrator.ForbiddenTransfers.Add(new DownloadFileTransfer(dto));
}
CurrentDownloads = downloadFileInfoFromService
CurrentDownloads = [.. downloadFileInfoFromService
.Distinct()
.Select(d => new DownloadFileTransfer(d))
.Where(d => d.CanBeTransferred)
.ToList();
.Where(d => d.CanBeTransferred)];
return CurrentDownloads;
}
@@ -843,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)
{

View File

@@ -200,5 +200,21 @@ public partial class ApiController
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

View File

@@ -259,6 +259,13 @@ public partial class ApiController
ExecuteSafely(() => Mediator.Publish(new GPoseLobbyReceiveWorldData(userData, worldData)));
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)
{
@@ -441,6 +448,12 @@ public partial class ApiController
_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)
{
try

View File

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

View File

@@ -76,6 +76,19 @@
"Microsoft.AspNetCore.SignalR.Common": "10.0.1"
}
},
"Microsoft.Extensions.Caching.Memory": {
"type": "Direct",
"requested": "[10.0.1, )",
"resolved": "10.0.1",
"contentHash": "NxqSP0Ky4dZ5ybszdZCqs1X2C70s+dXflqhYBUh/vhcQVTIooNCXIYnLVbafoAFGZMs51d9+rHxveXs0ZC3SQQ==",
"dependencies": {
"Microsoft.Extensions.Caching.Abstractions": "10.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.1",
"Microsoft.Extensions.Logging.Abstractions": "10.0.1",
"Microsoft.Extensions.Options": "10.0.1",
"Microsoft.Extensions.Primitives": "10.0.1"
}
},
"Microsoft.Extensions.Hosting": {
"type": "Direct",
"requested": "[10.0.1, )",
@@ -233,6 +246,14 @@
"Microsoft.AspNetCore.SignalR.Common": "10.0.1"
}
},
"Microsoft.Extensions.Caching.Abstractions": {
"type": "Transitive",
"resolved": "10.0.1",
"contentHash": "Vb1vVAQDxHpXVdL9fpOX2BzeV7bbhzG4pAcIKRauRl0/VfkE8mq0f+fYC+gWICh3dlzTZInJ/cTeBS2MgU/XvQ==",
"dependencies": {
"Microsoft.Extensions.Primitives": "10.0.1"
}
},
"Microsoft.Extensions.Configuration": {
"type": "Transitive",
"resolved": "10.0.1",
@@ -618,7 +639,7 @@
"FlatSharp.Compiler": "[7.9.0, )",
"FlatSharp.Runtime": "[7.9.0, )",
"OtterGui": "[1.0.0, )",
"Penumbra.Api": "[5.13.0, )",
"Penumbra.Api": "[5.13.1, )",
"Penumbra.String": "[1.0.7, )"
}
},