Merge pull request 'update-decimation-filters' (#140) from update-decimation-filters into 2.0.3
Reviewed-on: #140
This commit was merged in pull request #140.
This commit is contained in:
@@ -1,11 +1,17 @@
|
|||||||
using LightlessSync.WebAPI;
|
using System.Globalization;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Nodes;
|
||||||
|
using LightlessSync.LightlessConfiguration.Configurations;
|
||||||
|
using LightlessSync.LightlessConfiguration.Models;
|
||||||
|
using LightlessSync.WebAPI;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace LightlessSync.LightlessConfiguration;
|
namespace LightlessSync.LightlessConfiguration;
|
||||||
|
|
||||||
public class ConfigurationMigrator(ILogger<ConfigurationMigrator> logger, TransientConfigService transientConfigService,
|
public class ConfigurationMigrator(ILogger<ConfigurationMigrator> logger, TransientConfigService transientConfigService,
|
||||||
ServerConfigService serverConfigService) : IHostedService
|
ServerConfigService serverConfigService, TempCollectionConfigService tempCollectionConfigService,
|
||||||
|
LightlessConfigService lightlessConfigService) : IHostedService
|
||||||
{
|
{
|
||||||
private readonly ILogger<ConfigurationMigrator> _logger = logger;
|
private readonly ILogger<ConfigurationMigrator> _logger = logger;
|
||||||
|
|
||||||
@@ -51,6 +57,8 @@ public class ConfigurationMigrator(ILogger<ConfigurationMigrator> logger, Transi
|
|||||||
serverConfigService.Current.Version = 2;
|
serverConfigService.Current.Version = 2;
|
||||||
serverConfigService.Save();
|
serverConfigService.Save();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MigrateTempCollectionConfig(tempCollectionConfigService, lightlessConfigService);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task StartAsync(CancellationToken cancellationToken)
|
public Task StartAsync(CancellationToken cancellationToken)
|
||||||
@@ -63,4 +71,273 @@ public class ConfigurationMigrator(ILogger<ConfigurationMigrator> logger, Transi
|
|||||||
{
|
{
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void MigrateTempCollectionConfig(TempCollectionConfigService tempCollectionConfigService, LightlessConfigService lightlessConfigService)
|
||||||
|
{
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
TempCollectionConfig tempConfig = tempCollectionConfigService.Current;
|
||||||
|
var tempChanged = false;
|
||||||
|
var tempNeedsSave = false;
|
||||||
|
|
||||||
|
if (TryReadTempCollectionData(lightlessConfigService.ConfigurationPath, out var root, out var ids, out var entries))
|
||||||
|
{
|
||||||
|
tempChanged |= MergeTempCollectionData(tempConfig, ids, entries, now);
|
||||||
|
var removed = root.Remove("OrphanableTempCollections");
|
||||||
|
removed |= root.Remove("OrphanableTempCollectionEntries");
|
||||||
|
if (removed)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string updatedJson = root.ToJsonString(new JsonSerializerOptions
|
||||||
|
{
|
||||||
|
WriteIndented = true
|
||||||
|
});
|
||||||
|
File.WriteAllText(lightlessConfigService.ConfigurationPath, updatedJson);
|
||||||
|
lightlessConfigService.UpdateLastWriteTime();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Failed to rewrite {config} after temp collection migration", lightlessConfigService.ConfigurationPath);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ids.Count > 0 || entries.Count > 0)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Migrated {ids} temp collection ids and {entries} entries to {configName}",
|
||||||
|
ids.Count, entries.Count, tempCollectionConfigService.ConfigurationName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (TryReadTempCollectionData(tempCollectionConfigService.ConfigurationPath, out var tempRoot, out var tempIds, out var tempEntries))
|
||||||
|
{
|
||||||
|
tempChanged |= MergeTempCollectionData(tempConfig, tempIds, tempEntries, now);
|
||||||
|
if (tempRoot.Remove("OrphanableTempCollections"))
|
||||||
|
{
|
||||||
|
tempNeedsSave = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tempChanged || tempNeedsSave)
|
||||||
|
{
|
||||||
|
tempCollectionConfigService.Save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryReadTempCollectionData(string configPath, out JsonObject root, out HashSet<Guid> ids, out List<OrphanableTempCollectionEntry> entries)
|
||||||
|
{
|
||||||
|
root = new JsonObject();
|
||||||
|
ids = [];
|
||||||
|
entries = [];
|
||||||
|
|
||||||
|
if (!File.Exists(configPath))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
root = JsonNode.Parse(File.ReadAllText(configPath)) as JsonObject ?? new JsonObject();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Failed to read temp collection data from {config}", configPath);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
root.TryGetPropertyValue("OrphanableTempCollections", out JsonNode? idsNode);
|
||||||
|
root.TryGetPropertyValue("OrphanableTempCollectionEntries", out JsonNode? entriesNode);
|
||||||
|
|
||||||
|
if (idsNode == null && entriesNode == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ids = ParseGuidSet(idsNode);
|
||||||
|
entries = ParseEntries(entriesNode);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static HashSet<Guid> ParseGuidSet(JsonNode? node)
|
||||||
|
{
|
||||||
|
HashSet<Guid> ids = [];
|
||||||
|
if (node is not JsonArray array)
|
||||||
|
{
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (JsonNode? item in array)
|
||||||
|
{
|
||||||
|
Guid id = ParseGuid(item);
|
||||||
|
if (id != Guid.Empty)
|
||||||
|
{
|
||||||
|
ids.Add(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<OrphanableTempCollectionEntry> ParseEntries(JsonNode? node)
|
||||||
|
{
|
||||||
|
List<OrphanableTempCollectionEntry> entries = [];
|
||||||
|
if (node is not JsonArray array)
|
||||||
|
{
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (JsonNode? item in array)
|
||||||
|
{
|
||||||
|
if (item is not JsonObject obj)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Guid id = ParseGuid(obj["Id"]);
|
||||||
|
if (id == Guid.Empty)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
DateTime registeredAtUtc = DateTime.MinValue;
|
||||||
|
if (TryParseDateTime(obj["RegisteredAtUtc"], out DateTime parsed))
|
||||||
|
{
|
||||||
|
registeredAtUtc = parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
entries.Add(new OrphanableTempCollectionEntry
|
||||||
|
{
|
||||||
|
Id = id,
|
||||||
|
RegisteredAtUtc = registeredAtUtc
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Guid ParseGuid(JsonNode? node)
|
||||||
|
{
|
||||||
|
if (node is JsonValue value)
|
||||||
|
{
|
||||||
|
if (value.TryGetValue<string>(out string? stringValue) && Guid.TryParse(stringValue, out Guid parsed))
|
||||||
|
{
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Guid.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryParseDateTime(JsonNode? node, out DateTime value)
|
||||||
|
{
|
||||||
|
value = DateTime.MinValue;
|
||||||
|
if (node is not JsonValue val)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (val.TryGetValue<DateTime>(out DateTime direct))
|
||||||
|
{
|
||||||
|
value = direct;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (val.TryGetValue<string>(out string? stringValue)
|
||||||
|
&& DateTime.TryParse(stringValue, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out DateTime parsed))
|
||||||
|
{
|
||||||
|
value = parsed;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool MergeTempCollectionData(TempCollectionConfig config, HashSet<Guid> ids, List<OrphanableTempCollectionEntry> entries, DateTime now)
|
||||||
|
{
|
||||||
|
bool changed = false;
|
||||||
|
Dictionary<Guid, OrphanableTempCollectionEntry> entryLookup = new();
|
||||||
|
for (var i = config.OrphanableTempCollectionEntries.Count - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
var entry = config.OrphanableTempCollectionEntries[i];
|
||||||
|
if (entry.Id == Guid.Empty)
|
||||||
|
{
|
||||||
|
config.OrphanableTempCollectionEntries.RemoveAt(i);
|
||||||
|
changed = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entryLookup.TryGetValue(entry.Id, out var existing))
|
||||||
|
{
|
||||||
|
if (entry.RegisteredAtUtc != DateTime.MinValue
|
||||||
|
&& (existing.RegisteredAtUtc == DateTime.MinValue || entry.RegisteredAtUtc < existing.RegisteredAtUtc))
|
||||||
|
{
|
||||||
|
existing.RegisteredAtUtc = entry.RegisteredAtUtc;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
config.OrphanableTempCollectionEntries.RemoveAt(i);
|
||||||
|
changed = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
entryLookup[entry.Id] = entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (OrphanableTempCollectionEntry entry in entries)
|
||||||
|
{
|
||||||
|
if (entry.Id == Guid.Empty)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!entryLookup.TryGetValue(entry.Id, out OrphanableTempCollectionEntry? existing))
|
||||||
|
{
|
||||||
|
var added = new OrphanableTempCollectionEntry
|
||||||
|
{
|
||||||
|
Id = entry.Id,
|
||||||
|
RegisteredAtUtc = entry.RegisteredAtUtc
|
||||||
|
};
|
||||||
|
config.OrphanableTempCollectionEntries.Add(added);
|
||||||
|
entryLookup[entry.Id] = added;
|
||||||
|
changed = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.RegisteredAtUtc != DateTime.MinValue
|
||||||
|
&& (existing.RegisteredAtUtc == DateTime.MinValue || entry.RegisteredAtUtc < existing.RegisteredAtUtc))
|
||||||
|
{
|
||||||
|
existing.RegisteredAtUtc = entry.RegisteredAtUtc;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (Guid id in ids)
|
||||||
|
{
|
||||||
|
if (id == Guid.Empty)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!entryLookup.TryGetValue(id, out OrphanableTempCollectionEntry? existing))
|
||||||
|
{
|
||||||
|
var added = new OrphanableTempCollectionEntry
|
||||||
|
{
|
||||||
|
Id = id,
|
||||||
|
RegisteredAtUtc = now
|
||||||
|
};
|
||||||
|
config.OrphanableTempCollectionEntries.Add(added);
|
||||||
|
entryLookup[id] = added;
|
||||||
|
changed = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existing.RegisteredAtUtc == DateTime.MinValue)
|
||||||
|
{
|
||||||
|
existing.RegisteredAtUtc = now;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,37 +72,41 @@ public class ConfigurationSaveService : IHostedService
|
|||||||
{
|
{
|
||||||
_logger.LogTrace("Saving {configName}", config.ConfigurationName);
|
_logger.LogTrace("Saving {configName}", config.ConfigurationName);
|
||||||
var configDir = config.ConfigurationPath.Replace(config.ConfigurationName, string.Empty);
|
var configDir = config.ConfigurationPath.Replace(config.ConfigurationName, string.Empty);
|
||||||
|
var isTempCollections = string.Equals(config.ConfigurationName, TempCollectionConfigService.ConfigName, StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
try
|
if (!isTempCollections)
|
||||||
{
|
{
|
||||||
var configBackupFolder = Path.Join(configDir, BackupFolder);
|
try
|
||||||
if (!Directory.Exists(configBackupFolder))
|
|
||||||
Directory.CreateDirectory(configBackupFolder);
|
|
||||||
|
|
||||||
var configNameSplit = config.ConfigurationName.Split(".");
|
|
||||||
var existingConfigs = Directory.EnumerateFiles(
|
|
||||||
configBackupFolder,
|
|
||||||
configNameSplit[0] + "*")
|
|
||||||
.Select(c => new FileInfo(c))
|
|
||||||
.OrderByDescending(c => c.LastWriteTime).ToList();
|
|
||||||
if (existingConfigs.Skip(10).Any())
|
|
||||||
{
|
{
|
||||||
foreach (var oldBak in existingConfigs.Skip(10).ToList())
|
var configBackupFolder = Path.Join(configDir, BackupFolder);
|
||||||
{
|
if (!Directory.Exists(configBackupFolder))
|
||||||
oldBak.Delete();
|
Directory.CreateDirectory(configBackupFolder);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
string backupPath = Path.Combine(configBackupFolder, configNameSplit[0] + "." + DateTime.Now.ToString("yyyyMMddHHmmss") + "." + configNameSplit[1]);
|
var configNameSplit = config.ConfigurationName.Split(".");
|
||||||
_logger.LogTrace("Backing up current config to {backupPath}", backupPath);
|
var existingConfigs = Directory.EnumerateFiles(
|
||||||
File.Copy(config.ConfigurationPath, backupPath, overwrite: true);
|
configBackupFolder,
|
||||||
FileInfo fi = new(backupPath);
|
configNameSplit[0] + "*")
|
||||||
fi.LastWriteTimeUtc = DateTime.UtcNow;
|
.Select(c => new FileInfo(c))
|
||||||
}
|
.OrderByDescending(c => c.LastWriteTime).ToList();
|
||||||
catch (Exception ex)
|
if (existingConfigs.Skip(10).Any())
|
||||||
{
|
{
|
||||||
// ignore if file cannot be backupped
|
foreach (var oldBak in existingConfigs.Skip(10).ToList())
|
||||||
_logger.LogWarning(ex, "Could not create backup for {config}", config.ConfigurationPath);
|
{
|
||||||
|
oldBak.Delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
string backupPath = Path.Combine(configBackupFolder, configNameSplit[0] + "." + DateTime.Now.ToString("yyyyMMddHHmmss") + "." + configNameSplit[1]);
|
||||||
|
_logger.LogTrace("Backing up current config to {backupPath}", backupPath);
|
||||||
|
File.Copy(config.ConfigurationPath, backupPath, overwrite: true);
|
||||||
|
FileInfo fi = new(backupPath);
|
||||||
|
fi.LastWriteTimeUtc = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// ignore if file cannot be backupped
|
||||||
|
_logger.LogWarning(ex, "Could not create backup for {config}", config.ConfigurationPath);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var temp = config.ConfigurationPath + ".tmp";
|
var temp = config.ConfigurationPath + ".tmp";
|
||||||
@@ -110,7 +114,7 @@ public class ConfigurationSaveService : IHostedService
|
|||||||
{
|
{
|
||||||
await File.WriteAllTextAsync(temp, JsonSerializer.Serialize(config.Current, typeof(T), new JsonSerializerOptions()
|
await File.WriteAllTextAsync(temp, JsonSerializer.Serialize(config.Current, typeof(T), new JsonSerializerOptions()
|
||||||
{
|
{
|
||||||
WriteIndented = true
|
WriteIndented = !isTempCollections
|
||||||
})).ConfigureAwait(false);
|
})).ConfigureAwait(false);
|
||||||
File.Move(temp, config.ConfigurationPath, true);
|
File.Move(temp, config.ConfigurationPath, true);
|
||||||
config.UpdateLastWriteTime();
|
config.UpdateLastWriteTime();
|
||||||
|
|||||||
@@ -160,8 +160,6 @@ public class LightlessConfig : ILightlessConfiguration
|
|||||||
public string? SelectedFinderSyncshell { get; set; } = null;
|
public string? SelectedFinderSyncshell { get; set; } = null;
|
||||||
public string LastSeenVersion { get; set; } = string.Empty;
|
public string LastSeenVersion { get; set; } = string.Empty;
|
||||||
public bool EnableParticleEffects { get; set; } = true;
|
public bool EnableParticleEffects { get; set; } = true;
|
||||||
public HashSet<Guid> OrphanableTempCollections { get; set; } = [];
|
|
||||||
public List<OrphanableTempCollectionEntry> OrphanableTempCollectionEntries { get; set; } = [];
|
|
||||||
public AnimationValidationMode AnimationValidationMode { get; set; } = AnimationValidationMode.Unsafe;
|
public AnimationValidationMode AnimationValidationMode { get; set; } = AnimationValidationMode.Unsafe;
|
||||||
public bool AnimationAllowOneBasedShift { get; set; } = false;
|
public bool AnimationAllowOneBasedShift { get; set; } = false;
|
||||||
public bool AnimationAllowNeighborIndexTolerance { get; set; } = false;
|
public bool AnimationAllowNeighborIndexTolerance { get; set; } = false;
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
using LightlessSync.LightlessConfiguration.Models;
|
||||||
|
|
||||||
|
namespace LightlessSync.LightlessConfiguration.Configurations;
|
||||||
|
|
||||||
|
[Serializable]
|
||||||
|
public sealed class TempCollectionConfig : ILightlessConfiguration
|
||||||
|
{
|
||||||
|
public int Version { get; set; } = 1;
|
||||||
|
public List<OrphanableTempCollectionEntry> OrphanableTempCollectionEntries { get; set; } = [];
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
using LightlessSync.LightlessConfiguration.Configurations;
|
||||||
|
|
||||||
|
namespace LightlessSync.LightlessConfiguration;
|
||||||
|
|
||||||
|
public sealed class TempCollectionConfigService : ConfigurationServiceBase<TempCollectionConfig>
|
||||||
|
{
|
||||||
|
public const string ConfigName = "tempcollections.json";
|
||||||
|
|
||||||
|
public TempCollectionConfigService(string configDir) : base(configDir) { }
|
||||||
|
|
||||||
|
public override string ConfigurationName => ConfigName;
|
||||||
|
}
|
||||||
@@ -429,6 +429,7 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
LightlessSync.UI.Style.MainStyle.Init(cfg, theme);
|
LightlessSync.UI.Style.MainStyle.Init(cfg, theme);
|
||||||
return cfg;
|
return cfg;
|
||||||
});
|
});
|
||||||
|
services.AddSingleton(sp => new TempCollectionConfigService(configDir));
|
||||||
services.AddSingleton(sp => new ServerConfigService(configDir));
|
services.AddSingleton(sp => new ServerConfigService(configDir));
|
||||||
services.AddSingleton(sp => new NotesConfigService(configDir));
|
services.AddSingleton(sp => new NotesConfigService(configDir));
|
||||||
services.AddSingleton(sp => new PairTagConfigService(configDir));
|
services.AddSingleton(sp => new PairTagConfigService(configDir));
|
||||||
@@ -442,6 +443,7 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<LightlessConfigService>());
|
services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<LightlessConfigService>());
|
||||||
services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<UiThemeConfigService>());
|
services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<UiThemeConfigService>());
|
||||||
services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<ChatConfigService>());
|
services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<ChatConfigService>());
|
||||||
|
services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<TempCollectionConfigService>());
|
||||||
services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<ServerConfigService>());
|
services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<ServerConfigService>());
|
||||||
services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<NotesConfigService>());
|
services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<NotesConfigService>());
|
||||||
services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<PairTagConfigService>());
|
services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<PairTagConfigService>());
|
||||||
|
|||||||
@@ -1343,22 +1343,11 @@ internal static class MdlDecimator
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return IsBodyMaterial(mdl.Materials[mesh.MaterialIndex]);
|
return ModelDecimationFilters.IsBodyMaterial(mdl.Materials[mesh.MaterialIndex]);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool IsBodyMaterial(string materialPath)
|
private static bool IsBodyMaterial(string materialPath)
|
||||||
{
|
=> ModelDecimationFilters.IsBodyMaterial(materialPath);
|
||||||
if (string.IsNullOrWhiteSpace(materialPath))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var normalized = materialPath.Replace('\\', '/').ToLowerInvariant();
|
|
||||||
var nameStart = normalized.LastIndexOf('/');
|
|
||||||
var fileName = nameStart >= 0 ? normalized[(nameStart + 1)..] : normalized;
|
|
||||||
return fileName.Contains("_bibo", StringComparison.Ordinal)
|
|
||||||
|| fileName.EndsWith("_a.mtrl", StringComparison.Ordinal);
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class BodyCollisionData
|
private sealed class BodyCollisionData
|
||||||
{
|
{
|
||||||
|
|||||||
132
LightlessSync/Services/ModelDecimation/ModelDecimationFilters.cs
Normal file
132
LightlessSync/Services/ModelDecimation/ModelDecimationFilters.cs
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
namespace LightlessSync.Services.ModelDecimation;
|
||||||
|
|
||||||
|
internal static class ModelDecimationFilters
|
||||||
|
{
|
||||||
|
// MODELS ONLY HERE, NOT MATERIALS
|
||||||
|
internal static readonly string[] HairPaths =
|
||||||
|
[
|
||||||
|
"/hair/",
|
||||||
|
"hir.mdl",
|
||||||
|
];
|
||||||
|
|
||||||
|
internal static readonly string[] ClothingPaths =
|
||||||
|
[
|
||||||
|
"chara/equipment/",
|
||||||
|
"/equipment/",
|
||||||
|
|
||||||
|
"met.mdl",
|
||||||
|
"top.mdl",
|
||||||
|
"glv.mdl",
|
||||||
|
"dwn.mdl",
|
||||||
|
"sho.mdl",
|
||||||
|
];
|
||||||
|
|
||||||
|
internal static readonly string[] AccessoryPaths =
|
||||||
|
[
|
||||||
|
"/accessory/",
|
||||||
|
"chara/accessory/",
|
||||||
|
|
||||||
|
"ear.mdl",
|
||||||
|
"nek.mdl",
|
||||||
|
"wrs.mdl",
|
||||||
|
"ril.mdl",
|
||||||
|
"rir.mdl",
|
||||||
|
];
|
||||||
|
|
||||||
|
internal static readonly string[] BodyPaths =
|
||||||
|
[
|
||||||
|
"/body/",
|
||||||
|
"chara/equipment/e0000/model/",
|
||||||
|
"chara/equipment/e9903/model/",
|
||||||
|
"chara/equipment/e9903/model/",
|
||||||
|
"chara/equipment/e0279/model/",
|
||||||
|
];
|
||||||
|
|
||||||
|
internal static readonly string[] FaceHeadPaths =
|
||||||
|
[
|
||||||
|
"/face/",
|
||||||
|
"/obj/face/",
|
||||||
|
"/head/",
|
||||||
|
"fac.mdl",
|
||||||
|
];
|
||||||
|
|
||||||
|
internal static readonly string[] TailOrEarPaths =
|
||||||
|
[
|
||||||
|
"/tail/",
|
||||||
|
"/obj/tail/",
|
||||||
|
"/zear/",
|
||||||
|
"/obj/zear/",
|
||||||
|
|
||||||
|
"til.mdl",
|
||||||
|
"zer.mdl",
|
||||||
|
];
|
||||||
|
|
||||||
|
// BODY MATERIALS ONLY, NOT MESHES
|
||||||
|
internal static readonly string[] BodyMaterials =
|
||||||
|
[
|
||||||
|
"b0001_bibo.mtrl",
|
||||||
|
"b0101_bibo.mtrl",
|
||||||
|
|
||||||
|
"b0001_a.mtrl",
|
||||||
|
"b0001_b.mtrl",
|
||||||
|
|
||||||
|
"b0101_a.mtrl",
|
||||||
|
"b0101_b.mtrl",
|
||||||
|
];
|
||||||
|
|
||||||
|
internal static string NormalizePath(string path)
|
||||||
|
=> path.Replace('\\', '/').ToLowerInvariant();
|
||||||
|
|
||||||
|
internal static bool IsHairPath(string normalizedPath)
|
||||||
|
=> ContainsAny(normalizedPath, HairPaths);
|
||||||
|
|
||||||
|
internal static bool IsClothingPath(string normalizedPath)
|
||||||
|
=> ContainsAny(normalizedPath, ClothingPaths);
|
||||||
|
|
||||||
|
internal static bool IsAccessoryPath(string normalizedPath)
|
||||||
|
=> ContainsAny(normalizedPath, AccessoryPaths);
|
||||||
|
|
||||||
|
|
||||||
|
internal static bool IsBodyPath(string normalizedPath)
|
||||||
|
=> ContainsAny(normalizedPath, BodyPaths);
|
||||||
|
|
||||||
|
internal static bool IsFaceHeadPath(string normalizedPath)
|
||||||
|
=> ContainsAny(normalizedPath, FaceHeadPaths);
|
||||||
|
|
||||||
|
internal static bool IsTailOrEarPath(string normalizedPath)
|
||||||
|
=> ContainsAny(normalizedPath, TailOrEarPaths);
|
||||||
|
|
||||||
|
internal static bool ContainsAny(string normalizedPath, IReadOnlyList<string> markers)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < markers.Count; i++)
|
||||||
|
{
|
||||||
|
if (normalizedPath.Contains(markers[i], StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static bool IsBodyMaterial(string materialPath)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(materialPath))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var normalized = NormalizePath(materialPath);
|
||||||
|
var nameStart = normalized.LastIndexOf('/');
|
||||||
|
var fileName = nameStart >= 0 ? normalized[(nameStart + 1)..] : normalized;
|
||||||
|
foreach (var marker in BodyMaterials)
|
||||||
|
{
|
||||||
|
if (fileName.Contains(marker, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -348,46 +348,40 @@ public sealed class ModelDecimationService
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
var normalized = NormalizeGamePath(gamePath);
|
var normalized = ModelDecimationFilters.NormalizePath(gamePath);
|
||||||
if (normalized.Contains("/hair/", StringComparison.Ordinal))
|
if (ModelDecimationFilters.IsHairPath(normalized))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (normalized.Contains("/chara/equipment/", StringComparison.Ordinal))
|
if (ModelDecimationFilters.IsClothingPath(normalized))
|
||||||
{
|
{
|
||||||
return _performanceConfigService.Current.ModelDecimationAllowClothing;
|
return _performanceConfigService.Current.ModelDecimationAllowClothing;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (normalized.Contains("/chara/accessory/", StringComparison.Ordinal))
|
if (ModelDecimationFilters.IsAccessoryPath(normalized))
|
||||||
{
|
{
|
||||||
return _performanceConfigService.Current.ModelDecimationAllowAccessories;
|
return _performanceConfigService.Current.ModelDecimationAllowAccessories;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (normalized.Contains("/chara/human/", StringComparison.Ordinal))
|
if (ModelDecimationFilters.IsBodyPath(normalized))
|
||||||
{
|
{
|
||||||
if (normalized.Contains("/body/", StringComparison.Ordinal))
|
return _performanceConfigService.Current.ModelDecimationAllowBody;
|
||||||
{
|
}
|
||||||
return _performanceConfigService.Current.ModelDecimationAllowBody;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (normalized.Contains("/face/", StringComparison.Ordinal) || normalized.Contains("/head/", StringComparison.Ordinal))
|
if (ModelDecimationFilters.IsFaceHeadPath(normalized))
|
||||||
{
|
{
|
||||||
return _performanceConfigService.Current.ModelDecimationAllowFaceHead;
|
return _performanceConfigService.Current.ModelDecimationAllowFaceHead;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (normalized.Contains("/tail/", StringComparison.Ordinal))
|
if (ModelDecimationFilters.IsTailOrEarPath(normalized))
|
||||||
{
|
{
|
||||||
return _performanceConfigService.Current.ModelDecimationAllowTail;
|
return _performanceConfigService.Current.ModelDecimationAllowTail;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string NormalizeGamePath(string path)
|
|
||||||
=> path.Replace('\\', '/').ToLowerInvariant();
|
|
||||||
|
|
||||||
private bool TryGetDecimationSettings(out ModelDecimationSettings settings)
|
private bool TryGetDecimationSettings(out ModelDecimationSettings settings)
|
||||||
{
|
{
|
||||||
settings = new ModelDecimationSettings(
|
settings = new ModelDecimationSettings(
|
||||||
|
|||||||
@@ -10,15 +10,18 @@ namespace LightlessSync.Services;
|
|||||||
public sealed class PenumbraTempCollectionJanitor : DisposableMediatorSubscriberBase
|
public sealed class PenumbraTempCollectionJanitor : DisposableMediatorSubscriberBase
|
||||||
{
|
{
|
||||||
private readonly IpcManager _ipc;
|
private readonly IpcManager _ipc;
|
||||||
private readonly LightlessConfigService _config;
|
private readonly TempCollectionConfigService _config;
|
||||||
|
private readonly CancellationTokenSource _cleanupCts = new();
|
||||||
private int _ran;
|
private int _ran;
|
||||||
|
private const int CleanupBatchSize = 50;
|
||||||
|
private static readonly TimeSpan CleanupBatchDelay = TimeSpan.FromMilliseconds(50);
|
||||||
private static readonly TimeSpan OrphanCleanupDelay = TimeSpan.FromDays(1);
|
private static readonly TimeSpan OrphanCleanupDelay = TimeSpan.FromDays(1);
|
||||||
|
|
||||||
public PenumbraTempCollectionJanitor(
|
public PenumbraTempCollectionJanitor(
|
||||||
ILogger<PenumbraTempCollectionJanitor> logger,
|
ILogger<PenumbraTempCollectionJanitor> logger,
|
||||||
LightlessMediator mediator,
|
LightlessMediator mediator,
|
||||||
IpcManager ipc,
|
IpcManager ipc,
|
||||||
LightlessConfigService config) : base(logger, mediator)
|
TempCollectionConfigService config) : base(logger, mediator)
|
||||||
{
|
{
|
||||||
_ipc = ipc;
|
_ipc = ipc;
|
||||||
_config = config;
|
_config = config;
|
||||||
@@ -31,10 +34,6 @@ public sealed class PenumbraTempCollectionJanitor : DisposableMediatorSubscriber
|
|||||||
if (id == Guid.Empty) return;
|
if (id == Guid.Empty) return;
|
||||||
var changed = false;
|
var changed = false;
|
||||||
var config = _config.Current;
|
var config = _config.Current;
|
||||||
if (config.OrphanableTempCollections.Add(id))
|
|
||||||
{
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
var now = DateTime.UtcNow;
|
var now = DateTime.UtcNow;
|
||||||
var existing = config.OrphanableTempCollectionEntries.FirstOrDefault(entry => entry.Id == id);
|
var existing = config.OrphanableTempCollectionEntries.FirstOrDefault(entry => entry.Id == id);
|
||||||
@@ -63,8 +62,7 @@ public sealed class PenumbraTempCollectionJanitor : DisposableMediatorSubscriber
|
|||||||
{
|
{
|
||||||
if (id == Guid.Empty) return;
|
if (id == Guid.Empty) return;
|
||||||
var config = _config.Current;
|
var config = _config.Current;
|
||||||
var changed = config.OrphanableTempCollections.Remove(id);
|
var changed = RemoveEntry(config.OrphanableTempCollectionEntries, id) > 0;
|
||||||
changed |= RemoveEntry(config.OrphanableTempCollectionEntries, id) > 0;
|
|
||||||
if (changed)
|
if (changed)
|
||||||
{
|
{
|
||||||
_config.Save();
|
_config.Save();
|
||||||
@@ -79,14 +77,31 @@ public sealed class PenumbraTempCollectionJanitor : DisposableMediatorSubscriber
|
|||||||
if (!_ipc.Penumbra.APIAvailable)
|
if (!_ipc.Penumbra.APIAvailable)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
_ = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await CleanupOrphansOnBootAsync(_cleanupCts.Token).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError(ex, "Error cleaning orphaned temp collections");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CleanupOrphansOnBootAsync(CancellationToken token)
|
||||||
|
{
|
||||||
var config = _config.Current;
|
var config = _config.Current;
|
||||||
var ids = config.OrphanableTempCollections;
|
|
||||||
var entries = config.OrphanableTempCollectionEntries;
|
var entries = config.OrphanableTempCollectionEntries;
|
||||||
if (ids.Count == 0 && entries.Count == 0)
|
if (entries.Count == 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var now = DateTime.UtcNow;
|
var now = DateTime.UtcNow;
|
||||||
var changed = EnsureEntries(ids, entries, now);
|
var changed = EnsureEntryTimes(entries, now);
|
||||||
var cutoff = now - OrphanCleanupDelay;
|
var cutoff = now - OrphanCleanupDelay;
|
||||||
var expired = entries
|
var expired = entries
|
||||||
.Where(entry => entry.Id != Guid.Empty && entry.RegisteredAtUtc != DateTime.MinValue && entry.RegisteredAtUtc <= cutoff)
|
.Where(entry => entry.Id != Guid.Empty && entry.RegisteredAtUtc != DateTime.MinValue && entry.RegisteredAtUtc <= cutoff)
|
||||||
@@ -105,25 +120,47 @@ public sealed class PenumbraTempCollectionJanitor : DisposableMediatorSubscriber
|
|||||||
var appId = Guid.NewGuid();
|
var appId = Guid.NewGuid();
|
||||||
Logger.LogInformation("Cleaning up {count} orphaned Lightless temp collections older than {delay}", expired.Count, OrphanCleanupDelay);
|
Logger.LogInformation("Cleaning up {count} orphaned Lightless temp collections older than {delay}", expired.Count, OrphanCleanupDelay);
|
||||||
|
|
||||||
|
List<Guid> removedIds = [];
|
||||||
foreach (var id in expired)
|
foreach (var id in expired)
|
||||||
{
|
{
|
||||||
|
if (token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_ipc.Penumbra.RemoveTemporaryCollectionAsync(Logger, appId, id)
|
await _ipc.Penumbra.RemoveTemporaryCollectionAsync(Logger, appId, id).ConfigureAwait(false);
|
||||||
.GetAwaiter().GetResult();
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Logger.LogDebug(ex, "Failed removing orphaned temp collection {id}", id);
|
Logger.LogDebug(ex, "Failed removing orphaned temp collection {id}", id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
removedIds.Add(id);
|
||||||
|
if (removedIds.Count % CleanupBatchSize == 0)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await Task.Delay(CleanupBatchDelay, token).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var id in expired)
|
if (removedIds.Count == 0)
|
||||||
{
|
{
|
||||||
ids.Remove(id);
|
if (changed)
|
||||||
|
{
|
||||||
|
_config.Save();
|
||||||
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var id in expired)
|
foreach (var id in removedIds)
|
||||||
{
|
{
|
||||||
RemoveEntry(entries, id);
|
RemoveEntry(entries, id);
|
||||||
}
|
}
|
||||||
@@ -131,6 +168,17 @@ public sealed class PenumbraTempCollectionJanitor : DisposableMediatorSubscriber
|
|||||||
_config.Save();
|
_config.Save();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
if (disposing)
|
||||||
|
{
|
||||||
|
_cleanupCts.Cancel();
|
||||||
|
_cleanupCts.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
base.Dispose(disposing);
|
||||||
|
}
|
||||||
|
|
||||||
private static int RemoveEntry(List<OrphanableTempCollectionEntry> entries, Guid id)
|
private static int RemoveEntry(List<OrphanableTempCollectionEntry> entries, Guid id)
|
||||||
{
|
{
|
||||||
var removed = 0;
|
var removed = 0;
|
||||||
@@ -148,29 +196,9 @@ public sealed class PenumbraTempCollectionJanitor : DisposableMediatorSubscriber
|
|||||||
return removed;
|
return removed;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool EnsureEntries(HashSet<Guid> ids, List<OrphanableTempCollectionEntry> entries, DateTime now)
|
private static bool EnsureEntryTimes(List<OrphanableTempCollectionEntry> entries, DateTime now)
|
||||||
{
|
{
|
||||||
var changed = false;
|
var changed = false;
|
||||||
foreach (var id in ids)
|
|
||||||
{
|
|
||||||
if (id == Guid.Empty)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (entries.Any(entry => entry.Id == id))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
entries.Add(new OrphanableTempCollectionEntry
|
|
||||||
{
|
|
||||||
Id = id,
|
|
||||||
RegisteredAtUtc = now
|
|
||||||
});
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var entry in entries)
|
foreach (var entry in entries)
|
||||||
{
|
{
|
||||||
if (entry.Id == Guid.Empty || entry.RegisteredAtUtc != DateTime.MinValue)
|
if (entry.Id == Guid.Empty || entry.RegisteredAtUtc != DateTime.MinValue)
|
||||||
|
|||||||
Reference in New Issue
Block a user