Some checks failed
Tag and Release Lightless / tag-and-release (push) Failing after 32s
 Lightless 1.12.0 is HERE! In this major update, we are introducing something we've been working on and testing for the last couple of weeks. In this update we are introducing a new (**OPTIONAL**) feature called **LightFinder**! We took inspiration from FFXIV's very own Party Finder and decided to implement something that allows users to not only look for fellow Lightless users, but also put up their Syncshell for others looking to join a sync community! When you enable LightFinder, you will be visible to other LightFinder users for **3 hours** or when you want to disabled it. When the 3 hours are up, you can either leave it disabled or enable it again for another 3 hours. The tag shown above will show above your nameplate, and you will be able to receive pair requests in your UI from other users with LightFinder enabled without having to input their uid!  Are you at a Venue? In Limsa? Partying in the streets of Uldah? If you're looking for fellow Lightless users you can now enable LightFinder and you will be shown to others who also have LightFinder enabled! Looking for a Syncshell to join? Enable LightFinder to see what SyncShells are available to join! Want to advertise your Syncshell? Choose the syncshell you want to put up in LightFinder and enable LightFinder. **IMPORTANT: We want to stress the fact that, if you just want to sync with just your friends and no one else, this does not take that away. No one will know you use Lightless unless you choose to tell them, or use this **OPTIONAL** feature. Your privacy is still maintained if you don't want to use the feature.** # Major Changes ## LightFinder - **OPTIONAL FEATURE** - **DOES NOT AFFECT YOU IF YOU DON'T WANT TO USE IT**  * New **OPTIONAL** syncing feature where one can enable something like a Party Finder so that other Lightless users in the area can see you are looking for people to sync with. Enable it by clicking the compass button and then the `Enable LightFinder` button.  * [L] Send Pair Request has been added to player context menus. You should still be able to send a request without Lightfinder on BUT you will need to know the other player is using Lightless and have them send a pair request back.  * When in LightFinder mode, for X mins you will be visible to all Lightless Users WHO ALSO HAVE LIGHTFINDER ON and will receive notifications of people wanting to pair with you. If you are the person using LightFinder, you understand the risks of pairing with someone you don't know. If you are the person sending a request to someone with LightFinder on, you also understand the risks of pairing with someone you don't know. **AGAIN, THIS IS OPTIONAL.** * When in LightFinder mode, you can also put up a Syncshell you own on the Syncshell Finder so that others can easily find it and join. This has to be done prior to enabling LightFinder.  * Syncshell Finder allows you to join Syncshells that are indexed by LightFinder  # Minor Changes * Vanity addition to our supporters: On top of vanity ids you can find a fun addition under Your User Settings -> Edit Lightless Profile -> Vanity Settings to change the colour of your name in the lightless ui. This will be shown to all users.  * Pairing nameplate colour override can also now override FC tag (new option_ * Bunch of UI fixes, updates, and changes * kdb is now a whitelisted filetype that can upload Co-authored-by: CakeAndBanana <admin@cakeandbanana.nl> Co-authored-by: azyges <aaaaaa@aaa.aaa> Co-authored-by: choco <thijmenhogenkamp@gmail.com> Co-authored-by: choco <choco@noreply.git.lightless-sync.org> Co-authored-by: defnotken <itsdefnotken@gmail.com> Reviewed-on: #39
470 lines
18 KiB
C#
470 lines
18 KiB
C#
using LightlessSync.API.Data.Enum;
|
|
using LightlessSync.LightlessConfiguration;
|
|
using LightlessSync.LightlessConfiguration.Configurations;
|
|
using LightlessSync.PlayerData.Data;
|
|
using LightlessSync.PlayerData.Handlers;
|
|
using LightlessSync.Services;
|
|
using LightlessSync.Services.Mediator;
|
|
using LightlessSync.Utils;
|
|
using Microsoft.Extensions.Logging;
|
|
using System.Collections.Concurrent;
|
|
|
|
namespace LightlessSync.FileCache;
|
|
|
|
public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
|
|
{
|
|
private readonly object _cacheAdditionLock = new();
|
|
private readonly HashSet<string> _cachedHandledPaths = new(StringComparer.Ordinal);
|
|
private readonly TransientConfigService _configurationService;
|
|
private readonly DalamudUtilService _dalamudUtil;
|
|
private readonly string[] _handledFileTypes = ["tmb", "pap", "avfx", "atex", "sklb", "eid", "phyb", "scd", "skp", "shpk", "kdb"];
|
|
private readonly string[] _handledRecordingFileTypes = ["tex", "mdl", "mtrl"];
|
|
private readonly HashSet<GameObjectHandler> _playerRelatedPointers = [];
|
|
private ConcurrentDictionary<IntPtr, ObjectKind> _cachedFrameAddresses = [];
|
|
private ConcurrentDictionary<ObjectKind, HashSet<string>>? _semiTransientResources = null;
|
|
private uint _lastClassJobId = uint.MaxValue;
|
|
public bool IsTransientRecording { get; private set; } = false;
|
|
|
|
public TransientResourceManager(ILogger<TransientResourceManager> logger, TransientConfigService configurationService,
|
|
DalamudUtilService dalamudUtil, LightlessMediator mediator) : base(logger, mediator)
|
|
{
|
|
_configurationService = configurationService;
|
|
_dalamudUtil = dalamudUtil;
|
|
|
|
Mediator.Subscribe<PenumbraResourceLoadMessage>(this, Manager_PenumbraResourceLoadEvent);
|
|
Mediator.Subscribe<PenumbraModSettingChangedMessage>(this, (_) => Manager_PenumbraModSettingChanged());
|
|
Mediator.Subscribe<PriorityFrameworkUpdateMessage>(this, (_) => DalamudUtil_FrameworkUpdate());
|
|
Mediator.Subscribe<GameObjectHandlerCreatedMessage>(this, (msg) =>
|
|
{
|
|
if (!msg.OwnedObject) return;
|
|
_playerRelatedPointers.Add(msg.GameObjectHandler);
|
|
});
|
|
Mediator.Subscribe<GameObjectHandlerDestroyedMessage>(this, (msg) =>
|
|
{
|
|
if (!msg.OwnedObject) return;
|
|
_playerRelatedPointers.Remove(msg.GameObjectHandler);
|
|
});
|
|
}
|
|
|
|
private TransientConfig.TransientPlayerConfig PlayerConfig
|
|
{
|
|
get
|
|
{
|
|
if (!_configurationService.Current.TransientConfigs.TryGetValue(PlayerPersistentDataKey, out var transientConfig))
|
|
{
|
|
_configurationService.Current.TransientConfigs[PlayerPersistentDataKey] = transientConfig = new();
|
|
}
|
|
|
|
return transientConfig;
|
|
}
|
|
}
|
|
|
|
private string PlayerPersistentDataKey => _dalamudUtil.GetPlayerNameAsync().GetAwaiter().GetResult() + "_" + _dalamudUtil.GetHomeWorldIdAsync().GetAwaiter().GetResult();
|
|
private ConcurrentDictionary<ObjectKind, HashSet<string>> SemiTransientResources
|
|
{
|
|
get
|
|
{
|
|
if (_semiTransientResources == null)
|
|
{
|
|
_semiTransientResources = new();
|
|
PlayerConfig.JobSpecificCache.TryGetValue(_dalamudUtil.ClassJobId, out var jobSpecificData);
|
|
_semiTransientResources[ObjectKind.Player] = PlayerConfig.GlobalPersistentCache.Concat(jobSpecificData ?? []).ToHashSet(StringComparer.Ordinal);
|
|
PlayerConfig.JobSpecificPetCache.TryGetValue(_dalamudUtil.ClassJobId, out var petSpecificData);
|
|
_semiTransientResources[ObjectKind.Pet] = [.. petSpecificData ?? []];
|
|
}
|
|
|
|
return _semiTransientResources;
|
|
}
|
|
}
|
|
private ConcurrentDictionary<ObjectKind, HashSet<string>> TransientResources { get; } = new();
|
|
|
|
public void CleanUpSemiTransientResources(ObjectKind objectKind, List<FileReplacement>? fileReplacement = null)
|
|
{
|
|
if (!SemiTransientResources.TryGetValue(objectKind, out HashSet<string>? value))
|
|
return;
|
|
|
|
if (fileReplacement == null)
|
|
{
|
|
value.Clear();
|
|
return;
|
|
}
|
|
|
|
int removedPaths = 0;
|
|
foreach (var replacement in fileReplacement.Where(p => !p.HasFileReplacement).SelectMany(p => p.GamePaths).ToList())
|
|
{
|
|
removedPaths += PlayerConfig.RemovePath(replacement, objectKind);
|
|
value.Remove(replacement);
|
|
}
|
|
|
|
if (removedPaths > 0)
|
|
{
|
|
Logger.LogTrace("Removed {amount} of SemiTransient paths during CleanUp, Saving from {name}", removedPaths, nameof(CleanUpSemiTransientResources));
|
|
// force reload semi transient resources
|
|
_configurationService.Save();
|
|
}
|
|
}
|
|
|
|
public HashSet<string> GetSemiTransientResources(ObjectKind objectKind)
|
|
{
|
|
SemiTransientResources.TryGetValue(objectKind, out var result);
|
|
|
|
return result ?? new HashSet<string>(StringComparer.Ordinal);
|
|
}
|
|
|
|
public void PersistTransientResources(ObjectKind objectKind)
|
|
{
|
|
if (!SemiTransientResources.TryGetValue(objectKind, out HashSet<string>? semiTransientResources))
|
|
{
|
|
SemiTransientResources[objectKind] = semiTransientResources = new(StringComparer.Ordinal);
|
|
}
|
|
|
|
if (!TransientResources.TryGetValue(objectKind, out var resources))
|
|
{
|
|
return;
|
|
}
|
|
|
|
var transientResources = resources.ToList();
|
|
Logger.LogDebug("Persisting {count} transient resources", transientResources.Count);
|
|
List<string> newlyAddedGamePaths = resources.Except(semiTransientResources, StringComparer.Ordinal).ToList();
|
|
foreach (var gamePath in transientResources)
|
|
{
|
|
semiTransientResources.Add(gamePath);
|
|
}
|
|
|
|
bool saveConfig = false;
|
|
if (objectKind == ObjectKind.Player && newlyAddedGamePaths.Count != 0)
|
|
{
|
|
saveConfig = true;
|
|
foreach (var item in newlyAddedGamePaths.Where(f => !string.IsNullOrEmpty(f)))
|
|
{
|
|
PlayerConfig.AddOrElevate(_dalamudUtil.ClassJobId, item);
|
|
}
|
|
}
|
|
else if (objectKind == ObjectKind.Pet && newlyAddedGamePaths.Count != 0)
|
|
{
|
|
saveConfig = true;
|
|
|
|
if (!PlayerConfig.JobSpecificPetCache.TryGetValue(_dalamudUtil.ClassJobId, out var petPerma))
|
|
{
|
|
PlayerConfig.JobSpecificPetCache[_dalamudUtil.ClassJobId] = petPerma = [];
|
|
}
|
|
|
|
foreach (var item in newlyAddedGamePaths.Where(f => !string.IsNullOrEmpty(f)))
|
|
{
|
|
petPerma.Add(item);
|
|
}
|
|
}
|
|
|
|
if (saveConfig)
|
|
{
|
|
Logger.LogTrace("Saving transient.json from {method}", nameof(PersistTransientResources));
|
|
_configurationService.Save();
|
|
}
|
|
|
|
TransientResources[objectKind].Clear();
|
|
}
|
|
|
|
public void RemoveTransientResource(ObjectKind objectKind, string path)
|
|
{
|
|
if (SemiTransientResources.TryGetValue(objectKind, out var resources))
|
|
{
|
|
resources.RemoveWhere(f => string.Equals(path, f, StringComparison.Ordinal));
|
|
if (objectKind == ObjectKind.Player)
|
|
{
|
|
PlayerConfig.RemovePath(path, objectKind);
|
|
Logger.LogTrace("Saving transient.json from {method}", nameof(RemoveTransientResource));
|
|
_configurationService.Save();
|
|
}
|
|
}
|
|
}
|
|
|
|
internal bool AddTransientResource(ObjectKind objectKind, string item)
|
|
{
|
|
if (SemiTransientResources.TryGetValue(objectKind, out var semiTransient) && semiTransient != null && semiTransient.Contains(item))
|
|
return false;
|
|
|
|
if (!TransientResources.TryGetValue(objectKind, out HashSet<string>? transientResource))
|
|
{
|
|
transientResource = new HashSet<string>(StringComparer.Ordinal);
|
|
TransientResources[objectKind] = transientResource;
|
|
}
|
|
|
|
return transientResource.Add(item.ToLowerInvariant());
|
|
}
|
|
|
|
internal void ClearTransientPaths(ObjectKind objectKind, List<string> list)
|
|
{
|
|
// ignore all recording only datatypes
|
|
int recordingOnlyRemoved = list.RemoveAll(entry => _handledRecordingFileTypes.Any(ext => entry.EndsWith(ext, StringComparison.OrdinalIgnoreCase)));
|
|
if (recordingOnlyRemoved > 0)
|
|
{
|
|
Logger.LogTrace("Ignored {0} game paths when clearing transients", recordingOnlyRemoved);
|
|
}
|
|
|
|
if (TransientResources.TryGetValue(objectKind, out var set))
|
|
{
|
|
foreach (var file in set.Where(p => list.Contains(p, StringComparer.OrdinalIgnoreCase)))
|
|
{
|
|
Logger.LogTrace("Removing From Transient: {file}", file);
|
|
}
|
|
|
|
int removed = set.RemoveWhere(p => list.Contains(p, StringComparer.OrdinalIgnoreCase));
|
|
Logger.LogDebug("Removed {removed} previously existing transient paths", removed);
|
|
}
|
|
|
|
bool reloadSemiTransient = false;
|
|
if (objectKind == ObjectKind.Player && SemiTransientResources.TryGetValue(objectKind, out var semiset))
|
|
{
|
|
foreach (var file in semiset.Where(p => list.Contains(p, StringComparer.OrdinalIgnoreCase)))
|
|
{
|
|
Logger.LogTrace("Removing From SemiTransient: {file}", file);
|
|
PlayerConfig.RemovePath(file, objectKind);
|
|
}
|
|
|
|
int removed = semiset.RemoveWhere(p => list.Contains(p, StringComparer.OrdinalIgnoreCase));
|
|
Logger.LogDebug("Removed {removed} previously existing semi transient paths", removed);
|
|
if (removed > 0)
|
|
{
|
|
reloadSemiTransient = true;
|
|
Logger.LogTrace("Saving transient.json from {method}", nameof(ClearTransientPaths));
|
|
_configurationService.Save();
|
|
}
|
|
}
|
|
|
|
if (reloadSemiTransient)
|
|
_semiTransientResources = null;
|
|
}
|
|
|
|
protected override void Dispose(bool disposing)
|
|
{
|
|
base.Dispose(disposing);
|
|
|
|
TransientResources.Clear();
|
|
SemiTransientResources.Clear();
|
|
}
|
|
|
|
private void DalamudUtil_FrameworkUpdate()
|
|
{
|
|
_cachedFrameAddresses = new(_playerRelatedPointers.Where(k => k.Address != nint.Zero).ToDictionary(c => c.Address, c => c.ObjectKind));
|
|
lock (_cacheAdditionLock)
|
|
{
|
|
_cachedHandledPaths.Clear();
|
|
}
|
|
|
|
if (_lastClassJobId != _dalamudUtil.ClassJobId)
|
|
{
|
|
_lastClassJobId = _dalamudUtil.ClassJobId;
|
|
if (SemiTransientResources.TryGetValue(ObjectKind.Pet, out HashSet<string>? value))
|
|
{
|
|
value?.Clear();
|
|
}
|
|
|
|
// reload config for current new classjob
|
|
PlayerConfig.JobSpecificCache.TryGetValue(_dalamudUtil.ClassJobId, out var jobSpecificData);
|
|
SemiTransientResources[ObjectKind.Player] = PlayerConfig.GlobalPersistentCache.Concat(jobSpecificData ?? []).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
|
PlayerConfig.JobSpecificPetCache.TryGetValue(_dalamudUtil.ClassJobId, out var petSpecificData);
|
|
SemiTransientResources[ObjectKind.Pet] = [.. petSpecificData ?? []];
|
|
}
|
|
|
|
foreach (var kind in Enum.GetValues(typeof(ObjectKind)))
|
|
{
|
|
if (!_cachedFrameAddresses.Any(k => k.Value == (ObjectKind)kind) && TransientResources.Remove((ObjectKind)kind, out _))
|
|
{
|
|
Logger.LogDebug("Object not present anymore: {kind}", kind.ToString());
|
|
}
|
|
}
|
|
}
|
|
|
|
private void Manager_PenumbraModSettingChanged()
|
|
{
|
|
_ = Task.Run(() =>
|
|
{
|
|
Logger.LogDebug("Penumbra Mod Settings changed, verifying SemiTransientResources");
|
|
foreach (var item in _playerRelatedPointers)
|
|
{
|
|
Mediator.Publish(new TransientResourceChangedMessage(item.Address));
|
|
}
|
|
});
|
|
}
|
|
|
|
public void RebuildSemiTransientResources()
|
|
{
|
|
_semiTransientResources = null;
|
|
}
|
|
|
|
private void Manager_PenumbraResourceLoadEvent(PenumbraResourceLoadMessage msg)
|
|
{
|
|
var gamePath = msg.GamePath.ToLowerInvariant();
|
|
var gameObjectAddress = msg.GameObject;
|
|
var filePath = msg.FilePath;
|
|
|
|
// ignore files already processed this frame
|
|
if (_cachedHandledPaths.Contains(gamePath)) return;
|
|
|
|
lock (_cacheAdditionLock)
|
|
{
|
|
_cachedHandledPaths.Add(gamePath);
|
|
}
|
|
|
|
// replace individual mtrl stuff
|
|
if (filePath.StartsWith("|", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
filePath = filePath.Split("|")[2];
|
|
}
|
|
// replace filepath
|
|
filePath = filePath.ToLowerInvariant().Replace("\\", "/", StringComparison.OrdinalIgnoreCase);
|
|
|
|
// ignore files that are the same
|
|
var replacedGamePath = gamePath.ToLowerInvariant().Replace("\\", "/", StringComparison.OrdinalIgnoreCase);
|
|
if (string.Equals(filePath, replacedGamePath, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return;
|
|
}
|
|
|
|
// ignore files to not handle
|
|
var handledTypes = IsTransientRecording ? _handledRecordingFileTypes.Concat(_handledFileTypes) : _handledFileTypes;
|
|
if (!handledTypes.Any(type => gamePath.EndsWith(type, StringComparison.OrdinalIgnoreCase)))
|
|
{
|
|
lock (_cacheAdditionLock)
|
|
{
|
|
_cachedHandledPaths.Add(gamePath);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// ignore files not belonging to anything player related
|
|
if (!_cachedFrameAddresses.TryGetValue(gameObjectAddress, out var objectKind))
|
|
{
|
|
lock (_cacheAdditionLock)
|
|
{
|
|
_cachedHandledPaths.Add(gamePath);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// ^ all of the code above is just to sanitize the data
|
|
|
|
if (!TransientResources.TryGetValue(objectKind, out HashSet<string>? transientResources))
|
|
{
|
|
transientResources = new(StringComparer.OrdinalIgnoreCase);
|
|
TransientResources[objectKind] = transientResources;
|
|
}
|
|
|
|
var owner = _playerRelatedPointers.FirstOrDefault(f => f.Address == gameObjectAddress);
|
|
bool alreadyTransient = false;
|
|
|
|
bool transientContains = transientResources.Contains(replacedGamePath);
|
|
bool semiTransientContains = SemiTransientResources.SelectMany(k => k.Value).Any(f => string.Equals(f, gamePath, StringComparison.OrdinalIgnoreCase));
|
|
if (transientContains || semiTransientContains)
|
|
{
|
|
if (!IsTransientRecording)
|
|
Logger.LogTrace("Not adding {replacedPath} => {filePath}, Reason: Transient: {contains}, SemiTransient: {contains2}", replacedGamePath, filePath,
|
|
transientContains, semiTransientContains);
|
|
alreadyTransient = true;
|
|
}
|
|
else
|
|
{
|
|
if (!IsTransientRecording)
|
|
{
|
|
bool isAdded = transientResources.Add(replacedGamePath);
|
|
if (isAdded)
|
|
{
|
|
Logger.LogDebug("Adding {replacedGamePath} for {gameObject} ({filePath})", replacedGamePath, owner?.ToString() ?? gameObjectAddress.ToString("X"), filePath);
|
|
SendTransients(gameObjectAddress, objectKind);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (owner != null && IsTransientRecording)
|
|
{
|
|
_recordedTransients.Add(new TransientRecord(owner, replacedGamePath, filePath, alreadyTransient) { AddTransient = !alreadyTransient });
|
|
}
|
|
}
|
|
|
|
private void SendTransients(nint gameObject, ObjectKind objectKind)
|
|
{
|
|
_ = Task.Run(async () =>
|
|
{
|
|
_sendTransientCts?.Cancel();
|
|
_sendTransientCts?.Dispose();
|
|
_sendTransientCts = new();
|
|
var token = _sendTransientCts.Token;
|
|
await Task.Delay(TimeSpan.FromSeconds(5), token).ConfigureAwait(false);
|
|
foreach (var kvp in TransientResources)
|
|
{
|
|
if (TransientResources.TryGetValue(objectKind, out var values) && values.Any())
|
|
{
|
|
Logger.LogTrace("Sending Transients for {kind}", objectKind);
|
|
Mediator.Publish(new TransientResourceChangedMessage(gameObject));
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
public void StartRecording(CancellationToken token)
|
|
{
|
|
if (IsTransientRecording) return;
|
|
_recordedTransients.Clear();
|
|
IsTransientRecording = true;
|
|
RecordTimeRemaining.Value = TimeSpan.FromSeconds(150);
|
|
_ = Task.Run(async () =>
|
|
{
|
|
try
|
|
{
|
|
while (RecordTimeRemaining.Value > TimeSpan.Zero && !token.IsCancellationRequested)
|
|
{
|
|
await Task.Delay(TimeSpan.FromSeconds(1), token).ConfigureAwait(false);
|
|
RecordTimeRemaining.Value = RecordTimeRemaining.Value.Subtract(TimeSpan.FromSeconds(1));
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
IsTransientRecording = false;
|
|
}
|
|
});
|
|
}
|
|
|
|
public async Task WaitForRecording(CancellationToken token)
|
|
{
|
|
while (IsTransientRecording)
|
|
{
|
|
await Task.Delay(TimeSpan.FromSeconds(1), token).ConfigureAwait(false);
|
|
}
|
|
}
|
|
|
|
internal void SaveRecording()
|
|
{
|
|
HashSet<nint> addedTransients = [];
|
|
foreach (var item in _recordedTransients)
|
|
{
|
|
if (!item.AddTransient || item.AlreadyTransient) continue;
|
|
if (!TransientResources.TryGetValue(item.Owner.ObjectKind, out var transient))
|
|
{
|
|
TransientResources[item.Owner.ObjectKind] = transient = [];
|
|
}
|
|
|
|
Logger.LogTrace("Adding recorded: {gamePath} => {filePath}", item.GamePath, item.FilePath);
|
|
|
|
transient.Add(item.GamePath);
|
|
addedTransients.Add(item.Owner.Address);
|
|
}
|
|
|
|
_recordedTransients.Clear();
|
|
|
|
foreach (var item in addedTransients)
|
|
{
|
|
Mediator.Publish(new TransientResourceChangedMessage(item));
|
|
}
|
|
}
|
|
|
|
private readonly HashSet<TransientRecord> _recordedTransients = [];
|
|
public IReadOnlySet<TransientRecord> RecordedTransients => _recordedTransients;
|
|
|
|
public ValueProgress<TimeSpan> RecordTimeRemaining { get; } = new();
|
|
private CancellationTokenSource _sendTransientCts = new();
|
|
|
|
public record TransientRecord(GameObjectHandler Owner, string GamePath, string FilePath, bool AlreadyTransient)
|
|
{
|
|
public bool AddTransient { get; set; }
|
|
}
|
|
} |