Compare commits
30 Commits
slight-twe
...
2.0.2.80-D
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e8f598e695 | ||
|
|
861a337029 | ||
| 06f89955d3 | |||
|
|
367af2c3d0 | ||
|
|
19a238c808 | ||
|
|
c7a2b679f2 | ||
|
|
bec69074a5 | ||
| 7d86b41cee | |||
| 0185e6b534 | |||
|
|
90bf84f8eb | ||
| f27db300ec | |||
| 828be6eb5b | |||
| d039d2fd90 | |||
| e75a371475 | |||
|
|
ac711d9a43 | ||
|
|
b875e0c3a1 | ||
|
|
46e76bbfe6 | ||
|
|
3654365f2a | ||
|
|
9b256dd185 | ||
|
|
223ade39cb | ||
|
|
5aca9e70b2 | ||
|
|
92772cf334 | ||
|
|
0395e81a9f | ||
|
|
7734a7bf7e | ||
|
|
db2d19bb1e | ||
|
|
ab305a249c | ||
|
|
9d104a9dd8 | ||
|
|
bcd3bd5ca2 | ||
|
|
c1829a9837 | ||
|
|
cca23f6e05 |
@@ -0,0 +1,51 @@
|
||||
namespace Lifestream.Enums;
|
||||
|
||||
public enum TerritoryTypeIdHousing
|
||||
{
|
||||
None = -1,
|
||||
|
||||
// Mist (Limsa Lominsa)
|
||||
Mist = 339,
|
||||
MistSmall = 282,
|
||||
MistMedium = 283,
|
||||
MistLarge = 284,
|
||||
MistFCRoom = 384,
|
||||
MistFCWorkshop = 423,
|
||||
MistApartment = 608,
|
||||
|
||||
// Lavender Beds (Gridania)
|
||||
Lavender = 340,
|
||||
LavenderSmall = 342,
|
||||
LavenderMedium = 343,
|
||||
LavenderLarge = 344,
|
||||
LavenderFCRoom = 385,
|
||||
LavenderFCWorkshop = 425,
|
||||
LavenderApartment = 609,
|
||||
|
||||
// Goblet (Ul'dah)
|
||||
Goblet = 341,
|
||||
GobletSmall = 345,
|
||||
GobletMedium = 346,
|
||||
GobletLarge = 347,
|
||||
GobletFCRoom = 386,
|
||||
GobletFCWorkshop = 424,
|
||||
GobletApartment = 610,
|
||||
|
||||
// Shirogane (Kugane)
|
||||
Shirogane = 641,
|
||||
ShiroganeSmall = 649,
|
||||
ShiroganeMedium = 650,
|
||||
ShiroganeLarge = 651,
|
||||
ShiroganeFCRoom = 652,
|
||||
ShiroganeFCWorkshop = 653,
|
||||
ShiroganeApartment = 655,
|
||||
|
||||
// Empyreum (Ishgard)
|
||||
Empyream = 979,
|
||||
EmpyreamSmall = 980,
|
||||
EmpyreamMedium = 981,
|
||||
EmpyreamLarge = 982,
|
||||
EmpyreamFCRoom = 983,
|
||||
EmpyreamFCWorkshop = 984,
|
||||
EmpyreamApartment = 999,
|
||||
}
|
||||
@@ -92,7 +92,7 @@ public sealed class PenumbraTexture : PenumbraBase
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
logger.LogDebug("Converting texture {Input} -> {Output} ({Target})", job.InputFile, job.OutputFile, job.TargetType);
|
||||
logger.LogInformation("Converting texture {Input} -> {Output} ({Target})", job.InputFile, job.OutputFile, job.TargetType);
|
||||
var convertTask = _convertTextureFile.Invoke(job.InputFile, job.OutputFile, job.TargetType, job.IncludeMipMaps);
|
||||
await convertTask.ConfigureAwait(false);
|
||||
|
||||
|
||||
@@ -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.Logging;
|
||||
|
||||
namespace LightlessSync.LightlessConfiguration;
|
||||
|
||||
public class ConfigurationMigrator(ILogger<ConfigurationMigrator> logger, TransientConfigService transientConfigService,
|
||||
ServerConfigService serverConfigService) : IHostedService
|
||||
ServerConfigService serverConfigService, TempCollectionConfigService tempCollectionConfigService,
|
||||
LightlessConfigService lightlessConfigService) : IHostedService
|
||||
{
|
||||
private readonly ILogger<ConfigurationMigrator> _logger = logger;
|
||||
|
||||
@@ -51,6 +57,8 @@ public class ConfigurationMigrator(ILogger<ConfigurationMigrator> logger, Transi
|
||||
serverConfigService.Current.Version = 2;
|
||||
serverConfigService.Save();
|
||||
}
|
||||
|
||||
MigrateTempCollectionConfig(tempCollectionConfigService, lightlessConfigService);
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
@@ -63,4 +71,273 @@ public class ConfigurationMigrator(ILogger<ConfigurationMigrator> logger, Transi
|
||||
{
|
||||
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);
|
||||
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);
|
||||
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())
|
||||
try
|
||||
{
|
||||
foreach (var oldBak in existingConfigs.Skip(10).ToList())
|
||||
{
|
||||
oldBak.Delete();
|
||||
}
|
||||
}
|
||||
var configBackupFolder = Path.Join(configDir, BackupFolder);
|
||||
if (!Directory.Exists(configBackupFolder))
|
||||
Directory.CreateDirectory(configBackupFolder);
|
||||
|
||||
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 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())
|
||||
{
|
||||
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";
|
||||
@@ -110,7 +114,7 @@ public class ConfigurationSaveService : IHostedService
|
||||
{
|
||||
await File.WriteAllTextAsync(temp, JsonSerializer.Serialize(config.Current, typeof(T), new JsonSerializerOptions()
|
||||
{
|
||||
WriteIndented = true
|
||||
WriteIndented = !isTempCollections
|
||||
})).ConfigureAwait(false);
|
||||
File.Move(temp, config.ConfigurationPath, true);
|
||||
config.UpdateLastWriteTime();
|
||||
|
||||
@@ -160,8 +160,6 @@ public class LightlessConfig : ILightlessConfiguration
|
||||
public string? SelectedFinderSyncshell { get; set; } = null;
|
||||
public string LastSeenVersion { get; set; } = string.Empty;
|
||||
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 bool AnimationAllowOneBasedShift { 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;
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
<PropertyGroup>
|
||||
<Authors></Authors>
|
||||
<Company></Company>
|
||||
<Version>2.0.3</Version>
|
||||
<Version>2.0.2.80</Version>
|
||||
<Description></Description>
|
||||
<Copyright></Copyright>
|
||||
<PackageProjectUrl>https://github.com/Light-Public-Syncshells/LightlessClient</PackageProjectUrl>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using Dalamud.Utility;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
||||
using LightlessSync.API.Data.Enum;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.Object;
|
||||
using LightlessSync.FileCache;
|
||||
using LightlessSync.Interop.Ipc;
|
||||
using LightlessSync.LightlessConfiguration;
|
||||
@@ -14,6 +13,7 @@ using Microsoft.Extensions.Logging;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
using ObjectKind = LightlessSync.API.Data.Enum.ObjectKind;
|
||||
|
||||
namespace LightlessSync.PlayerData.Factories;
|
||||
|
||||
@@ -119,39 +119,48 @@ public class PlayerDataFactory
|
||||
return null;
|
||||
}
|
||||
|
||||
private static readonly int _drawObjectOffset =
|
||||
(int)Marshal.OffsetOf<GameObject>(nameof(GameObject.DrawObject));
|
||||
|
||||
private async Task<bool> CheckForNullDrawObject(IntPtr playerPointer)
|
||||
=> await _dalamudUtil.RunOnFrameworkThread(() => CheckForNullDrawObjectUnsafe(playerPointer)).ConfigureAwait(false);
|
||||
=> await _dalamudUtil.RunOnFrameworkThread(() =>
|
||||
{
|
||||
nint basePtr = playerPointer;
|
||||
|
||||
private unsafe static bool CheckForNullDrawObjectUnsafe(IntPtr playerPointer)
|
||||
if (!LooksLikeUserPtr(basePtr))
|
||||
return true;
|
||||
|
||||
nint drawObjAddr = basePtr + _drawObjectOffset;
|
||||
|
||||
if (!TryReadIntPtr(drawObjAddr, out var drawObj))
|
||||
return true;
|
||||
|
||||
return drawObj == 0;
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
private static bool LooksLikeUserPtr(nint p)
|
||||
{
|
||||
if (playerPointer == IntPtr.Zero)
|
||||
return true;
|
||||
if (p == 0) return false;
|
||||
|
||||
if (!IsPointerValid(playerPointer))
|
||||
return true;
|
||||
ulong u = (ulong)p;
|
||||
|
||||
var character = (Character*)playerPointer;
|
||||
if (character == null)
|
||||
return true;
|
||||
if (u < 0x0000_0001_0000UL) return false;
|
||||
if (u > 0x0000_7FFF_FFFF_FFFFUL) return false;
|
||||
if ((u & 0x7UL) != 0) return false;
|
||||
|
||||
var gameObject = &character->GameObject;
|
||||
if (gameObject == null)
|
||||
return true;
|
||||
|
||||
if (!IsPointerValid((IntPtr)gameObject))
|
||||
return true;
|
||||
|
||||
return gameObject->DrawObject == null;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool IsPointerValid(IntPtr ptr)
|
||||
private static bool TryReadIntPtr(nint addr, out nint value)
|
||||
{
|
||||
if (ptr == IntPtr.Zero)
|
||||
value = 0;
|
||||
|
||||
if (!VirtualReadable(addr))
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
_ = Marshal.ReadByte(ptr);
|
||||
value = Marshal.ReadIntPtr(addr);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
@@ -160,6 +169,37 @@ public class PlayerDataFactory
|
||||
}
|
||||
}
|
||||
|
||||
private static bool VirtualReadable(nint addr)
|
||||
{
|
||||
if (VirtualQuery(addr, out var mbi, (nuint)Marshal.SizeOf<MEMORY_BASIC_INFORMATION>()) == 0)
|
||||
return false;
|
||||
|
||||
const uint MEM_COMMIT = 0x1000;
|
||||
const uint PAGE_NOACCESS = 0x01;
|
||||
const uint PAGE_GUARD = 0x100;
|
||||
|
||||
if (mbi.State != MEM_COMMIT) return false;
|
||||
if ((mbi.Protect & PAGE_GUARD) != 0) return false;
|
||||
if (mbi.Protect == PAGE_NOACCESS) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern nuint VirtualQuery(nint lpAddress, out MEMORY_BASIC_INFORMATION lpBuffer, nuint dwLength);
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct MEMORY_BASIC_INFORMATION
|
||||
{
|
||||
public nint BaseAddress;
|
||||
public nint AllocationBase;
|
||||
public uint AllocationProtect;
|
||||
public nuint RegionSize;
|
||||
public uint State;
|
||||
public uint Protect;
|
||||
public uint Type;
|
||||
}
|
||||
|
||||
private static bool IsCacheFresh(CacheEntry entry)
|
||||
=> (DateTime.UtcNow - entry.CreatedUtc) <= _characterCacheTtl;
|
||||
|
||||
|
||||
@@ -125,8 +125,6 @@ public sealed class Plugin : IDalamudPlugin
|
||||
services.AddSingleton<FileTransferOrchestrator>();
|
||||
services.AddSingleton<LightlessPlugin>();
|
||||
services.AddSingleton<LightlessProfileManager>();
|
||||
services.AddSingleton<TextureProcessingQueue>();
|
||||
services.AddSingleton<ModelProcessingQueue>();
|
||||
services.AddSingleton<TextureCompressionService>();
|
||||
services.AddSingleton<TextureDownscaleService>();
|
||||
services.AddSingleton<ModelDecimationService>();
|
||||
@@ -431,6 +429,7 @@ public sealed class Plugin : IDalamudPlugin
|
||||
LightlessSync.UI.Style.MainStyle.Init(cfg, theme);
|
||||
return cfg;
|
||||
});
|
||||
services.AddSingleton(sp => new TempCollectionConfigService(configDir));
|
||||
services.AddSingleton(sp => new ServerConfigService(configDir));
|
||||
services.AddSingleton(sp => new NotesConfigService(configDir));
|
||||
services.AddSingleton(sp => new PairTagConfigService(configDir));
|
||||
@@ -444,6 +443,7 @@ public sealed class Plugin : IDalamudPlugin
|
||||
services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<LightlessConfigService>());
|
||||
services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<UiThemeConfigService>());
|
||||
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<NotesConfigService>());
|
||||
services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<PairTagConfigService>());
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace LightlessSync.Services;
|
||||
|
||||
public sealed class AssetProcessingQueue : IDisposable
|
||||
{
|
||||
private readonly BlockingCollection<WorkItem> _queue = new();
|
||||
private readonly Thread _worker;
|
||||
private readonly ILogger _logger;
|
||||
private bool _disposed;
|
||||
|
||||
public AssetProcessingQueue(ILogger logger, string name)
|
||||
{
|
||||
_logger = logger;
|
||||
_worker = new Thread(Run)
|
||||
{
|
||||
IsBackground = true,
|
||||
Name = string.IsNullOrWhiteSpace(name) ? "LightlessSync.AssetProcessing" : name
|
||||
};
|
||||
_worker.Start();
|
||||
}
|
||||
|
||||
public Task Enqueue(Func<CancellationToken, Task> work, CancellationToken token = default)
|
||||
{
|
||||
if (work is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(work));
|
||||
}
|
||||
|
||||
var completion = new TaskCompletionSource<object?>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
if (token.IsCancellationRequested)
|
||||
{
|
||||
completion.TrySetCanceled(token);
|
||||
return completion.Task;
|
||||
}
|
||||
|
||||
if (_queue.IsAddingCompleted || _disposed)
|
||||
{
|
||||
completion.TrySetException(new ObjectDisposedException(nameof(AssetProcessingQueue)));
|
||||
return completion.Task;
|
||||
}
|
||||
|
||||
_queue.Add(new WorkItem(work, token, completion));
|
||||
return completion.Task;
|
||||
}
|
||||
|
||||
private void Run()
|
||||
{
|
||||
foreach (var item in _queue.GetConsumingEnumerable())
|
||||
{
|
||||
if (item.Token.IsCancellationRequested)
|
||||
{
|
||||
item.Completion.TrySetCanceled(item.Token);
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
item.Work(item.Token).GetAwaiter().GetResult();
|
||||
item.Completion.TrySetResult(null);
|
||||
}
|
||||
catch (OperationCanceledException ex)
|
||||
{
|
||||
var token = ex.CancellationToken.IsCancellationRequested ? ex.CancellationToken : item.Token;
|
||||
item.Completion.TrySetCanceled(token);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Asset processing job failed.");
|
||||
item.Completion.TrySetException(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
_queue.CompleteAdding();
|
||||
_worker.Join(TimeSpan.FromSeconds(2));
|
||||
_queue.Dispose();
|
||||
}
|
||||
|
||||
private readonly record struct WorkItem(
|
||||
Func<CancellationToken, Task> Work,
|
||||
CancellationToken Token,
|
||||
TaskCompletionSource<object?> Completion);
|
||||
}
|
||||
@@ -701,7 +701,23 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
str += $" Room #{location.RoomId}";
|
||||
}
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
public string LocationToLifestream(LocationInfo location)
|
||||
{
|
||||
if (location.ServerId is 0 || location.TerritoryId is 0 || ContentFinderData.Value.ContainsKey(location.TerritoryId)) return String.Empty;
|
||||
var str = WorldData.Value[(ushort)location.ServerId];
|
||||
if (location.HouseId is 0 && location.MapId is not 0)
|
||||
{
|
||||
var mapName = MapData.Value[(ushort)location.MapId].MapName;
|
||||
var parts = mapName.Split(" - ", StringSplitOptions.RemoveEmptyEntries);
|
||||
var locationName = parts.Length > 0 ? parts[^1] : mapName;
|
||||
str += $", tp {locationName}";
|
||||
string message = $"LocationToLifestream: {str}";
|
||||
_logger.LogInformation(message);
|
||||
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Lifestream.Enums;
|
||||
using LightlessSync.API.Data;
|
||||
using LightlessSync.API.Dto.CharaData;
|
||||
using LightlessSync.API.Dto.User;
|
||||
@@ -108,6 +109,144 @@ namespace LightlessSync.Services
|
||||
}
|
||||
}
|
||||
|
||||
public LocationInfo? GetLocationForLifestreamByUid(string uid)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_locations.TryGetValue<LocationInfo>(uid, out var location))
|
||||
{
|
||||
return location;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.LogError(e,"GetLocationInfoByUid error : ");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public AddressBookEntryTuple? GetAddressBookEntryByLocation(LocationInfo location)
|
||||
{
|
||||
if (location.ServerId is 0 || location.TerritoryId is 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var territoryHousing = (TerritoryTypeIdHousing)location.TerritoryId;
|
||||
|
||||
if (territoryHousing == TerritoryTypeIdHousing.None || !Enum.IsDefined(typeof(TerritoryTypeIdHousing), territoryHousing))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var city = GetResidentialAetheryteKind(territoryHousing);
|
||||
|
||||
if (city == ResidentialAetheryteKind.None)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (location.HouseId is not 0 and not 100)
|
||||
{
|
||||
AddressBookEntryTuple addressEntry = (
|
||||
Name: "",
|
||||
World: (int)location.ServerId,
|
||||
City: (int)city,
|
||||
Ward: (int)location.WardId,
|
||||
PropertyType: 0,
|
||||
Plot: (int)location.HouseId,
|
||||
Apartment: 0,
|
||||
ApartmentSubdivision: location.DivisionId == 2,
|
||||
AliasEnabled: false,
|
||||
Alias: ""
|
||||
);
|
||||
return addressEntry;
|
||||
}
|
||||
else if (location.HouseId is 100)
|
||||
{
|
||||
AddressBookEntryTuple addressEntry = (
|
||||
Name: "",
|
||||
World: (int)location.ServerId,
|
||||
City: (int)city,
|
||||
Ward: (int)location.WardId,
|
||||
PropertyType: 1,
|
||||
Plot: 0,
|
||||
Apartment: (int)location.RoomId,
|
||||
ApartmentSubdivision: location.DivisionId == 2,
|
||||
AliasEnabled: false,
|
||||
Alias: ""
|
||||
);
|
||||
return addressEntry;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private ResidentialAetheryteKind GetResidentialAetheryteKind(TerritoryTypeIdHousing territoryHousing)
|
||||
{
|
||||
return territoryHousing switch
|
||||
{
|
||||
TerritoryTypeIdHousing.Shirogane or
|
||||
TerritoryTypeIdHousing.ShiroganeApartment or
|
||||
TerritoryTypeIdHousing.ShiroganeSmall or
|
||||
TerritoryTypeIdHousing.ShiroganeMedium or
|
||||
TerritoryTypeIdHousing.ShiroganeLarge or
|
||||
TerritoryTypeIdHousing.ShiroganeFCRoom or
|
||||
TerritoryTypeIdHousing.ShiroganeFCWorkshop
|
||||
=> ResidentialAetheryteKind.Kugane,
|
||||
|
||||
TerritoryTypeIdHousing.Lavender or
|
||||
TerritoryTypeIdHousing.LavenderSmall or
|
||||
TerritoryTypeIdHousing.LavenderMedium or
|
||||
TerritoryTypeIdHousing.LavenderLarge or
|
||||
TerritoryTypeIdHousing.LavenderApartment or
|
||||
TerritoryTypeIdHousing.LavenderFCRoom or
|
||||
TerritoryTypeIdHousing.LavenderFCWorkshop
|
||||
=> ResidentialAetheryteKind.Gridania,
|
||||
|
||||
TerritoryTypeIdHousing.Mist or
|
||||
TerritoryTypeIdHousing.MistSmall or
|
||||
TerritoryTypeIdHousing.MistMedium or
|
||||
TerritoryTypeIdHousing.MistLarge or
|
||||
TerritoryTypeIdHousing.MistApartment or
|
||||
TerritoryTypeIdHousing.MistFCRoom or
|
||||
TerritoryTypeIdHousing.MistFCWorkshop
|
||||
=> ResidentialAetheryteKind.Limsa,
|
||||
|
||||
TerritoryTypeIdHousing.Goblet or
|
||||
TerritoryTypeIdHousing.GobletSmall or
|
||||
TerritoryTypeIdHousing.GobletMedium or
|
||||
TerritoryTypeIdHousing.GobletLarge or
|
||||
TerritoryTypeIdHousing.GobletApartment or
|
||||
TerritoryTypeIdHousing.GobletFCRoom or
|
||||
TerritoryTypeIdHousing.GobletFCWorkshop
|
||||
=> ResidentialAetheryteKind.Uldah,
|
||||
|
||||
TerritoryTypeIdHousing.Empyream or
|
||||
TerritoryTypeIdHousing.EmpyreamSmall or
|
||||
TerritoryTypeIdHousing.EmpyreamMedium or
|
||||
TerritoryTypeIdHousing.EmpyreamLarge or
|
||||
TerritoryTypeIdHousing.EmpyreamApartment or
|
||||
TerritoryTypeIdHousing.EmpyreamFCRoom or
|
||||
TerritoryTypeIdHousing.EmpyreamFCWorkshop
|
||||
=> ResidentialAetheryteKind.Foundation,
|
||||
|
||||
_ => ResidentialAetheryteKind.None
|
||||
};
|
||||
}
|
||||
|
||||
public string? GetMapAddressByLocation(LocationInfo location)
|
||||
{
|
||||
string? liString = null;
|
||||
var territoryHousing = (TerritoryTypeIdHousing)location.TerritoryId;
|
||||
if (GetResidentialAetheryteKind(territoryHousing) == ResidentialAetheryteKind.None)
|
||||
{
|
||||
liString = _dalamudUtilService.LocationToLifestream(location);
|
||||
}
|
||||
return liString;
|
||||
}
|
||||
|
||||
public DateTimeOffset GetSharingStatus(string uid)
|
||||
{
|
||||
try
|
||||
|
||||
@@ -1343,22 +1343,11 @@ internal static class MdlDecimator
|
||||
return false;
|
||||
}
|
||||
|
||||
return IsBodyMaterial(mdl.Materials[mesh.MaterialIndex]);
|
||||
return ModelDecimationFilters.IsBodyMaterial(mdl.Materials[mesh.MaterialIndex]);
|
||||
}
|
||||
|
||||
private static bool IsBodyMaterial(string 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);
|
||||
}
|
||||
=> ModelDecimationFilters.IsBodyMaterial(materialPath);
|
||||
|
||||
private sealed class BodyCollisionData
|
||||
{
|
||||
@@ -2115,16 +2104,6 @@ internal static class MdlDecimator
|
||||
}
|
||||
}
|
||||
|
||||
if (boneWeights != null
|
||||
&& blendWeightEncoding == BlendWeightEncoding.Default
|
||||
&& format.BlendWeightsElement is { } blendWeightsElement
|
||||
&& (MdlFile.VertexType)blendWeightsElement.Type == MdlFile.VertexType.UShort4
|
||||
&& ShouldTreatWeightsAsByteNormalized(boneWeights))
|
||||
{
|
||||
RescaleUShortAsByteWeights(boneWeights);
|
||||
blendWeightEncoding = BlendWeightEncoding.UShortAsByte;
|
||||
}
|
||||
|
||||
decoded = new DecodedMeshData(positions, normals, tangents, tangents2, colors, boneWeights, uvChannels, positionWs, normalWs, blendWeightEncoding);
|
||||
return true;
|
||||
}
|
||||
@@ -3434,44 +3413,6 @@ internal static class MdlDecimator
|
||||
return ToUShortNormalized(normalized);
|
||||
}
|
||||
|
||||
private static bool ShouldTreatWeightsAsByteNormalized(BoneWeight[] weights)
|
||||
{
|
||||
const float maxByteUnorm = byte.MaxValue / (float)ushort.MaxValue;
|
||||
var maxWeight = 0f;
|
||||
for (var i = 0; i < weights.Length; i++)
|
||||
{
|
||||
var weight = weights[i];
|
||||
maxWeight = Math.Max(maxWeight, weight.weight0);
|
||||
maxWeight = Math.Max(maxWeight, weight.weight1);
|
||||
maxWeight = Math.Max(maxWeight, weight.weight2);
|
||||
maxWeight = Math.Max(maxWeight, weight.weight3);
|
||||
if (maxWeight > maxByteUnorm)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return maxWeight > 0f;
|
||||
}
|
||||
|
||||
private static void RescaleUShortAsByteWeights(BoneWeight[] weights)
|
||||
{
|
||||
var scale = ushort.MaxValue / (float)byte.MaxValue;
|
||||
for (var i = 0; i < weights.Length; i++)
|
||||
{
|
||||
var weight = weights[i];
|
||||
weights[i] = new BoneWeight(
|
||||
weight.index0,
|
||||
weight.index1,
|
||||
weight.index2,
|
||||
weight.index3,
|
||||
weight.weight0 * scale,
|
||||
weight.weight1 * scale,
|
||||
weight.weight2 * scale,
|
||||
weight.weight3 * scale);
|
||||
}
|
||||
}
|
||||
|
||||
private static void NormalizeWeights(float[] weights)
|
||||
{
|
||||
var sum = weights.Sum();
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
using LightlessSync.FileCache;
|
||||
using LightlessSync.LightlessConfiguration;
|
||||
using LightlessSync.LightlessConfiguration.Configurations;
|
||||
using LightlessSync.Services;
|
||||
using LightlessSync.Utils;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Collections.Concurrent;
|
||||
@@ -20,7 +19,6 @@ public sealed class ModelDecimationService
|
||||
private readonly FileCacheManager _fileCacheManager;
|
||||
private readonly PlayerPerformanceConfigService _performanceConfigService;
|
||||
private readonly XivDataStorageService _xivDataStorageService;
|
||||
private readonly ModelProcessingQueue _processingQueue;
|
||||
private readonly SemaphoreSlim _decimationSemaphore = new(MaxConcurrentJobs);
|
||||
|
||||
private readonly TaskRegistry<string> _decimationDeduplicator = new();
|
||||
@@ -32,15 +30,13 @@ public sealed class ModelDecimationService
|
||||
LightlessConfigService configService,
|
||||
FileCacheManager fileCacheManager,
|
||||
PlayerPerformanceConfigService performanceConfigService,
|
||||
XivDataStorageService xivDataStorageService,
|
||||
ModelProcessingQueue processingQueue)
|
||||
XivDataStorageService xivDataStorageService)
|
||||
{
|
||||
_logger = logger;
|
||||
_configService = configService;
|
||||
_fileCacheManager = fileCacheManager;
|
||||
_performanceConfigService = performanceConfigService;
|
||||
_xivDataStorageService = xivDataStorageService;
|
||||
_processingQueue = processingQueue;
|
||||
}
|
||||
|
||||
public void ScheduleDecimation(string hash, string filePath, string? gamePath = null)
|
||||
@@ -57,9 +53,9 @@ public sealed class ModelDecimationService
|
||||
|
||||
_logger.LogDebug("Queued model decimation for {Hash}", hash);
|
||||
|
||||
_decimationDeduplicator.GetOrStart(hash, () => _processingQueue.Enqueue(async token =>
|
||||
_decimationDeduplicator.GetOrStart(hash, async () =>
|
||||
{
|
||||
await _decimationSemaphore.WaitAsync(token).ConfigureAwait(false);
|
||||
await _decimationSemaphore.WaitAsync().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
await DecimateInternalAsync(hash, filePath).ConfigureAwait(false);
|
||||
@@ -73,7 +69,7 @@ public sealed class ModelDecimationService
|
||||
{
|
||||
_decimationSemaphore.Release();
|
||||
}
|
||||
}, CancellationToken.None));
|
||||
});
|
||||
}
|
||||
|
||||
public void ScheduleBatchDecimation(string hash, string filePath, ModelDecimationSettings settings)
|
||||
@@ -93,9 +89,9 @@ public sealed class ModelDecimationService
|
||||
|
||||
_logger.LogInformation("Queued batch model decimation for {Hash}", hash);
|
||||
|
||||
_decimationDeduplicator.GetOrStart(hash, () => _processingQueue.Enqueue(async token =>
|
||||
_decimationDeduplicator.GetOrStart(hash, async () =>
|
||||
{
|
||||
await _decimationSemaphore.WaitAsync(token).ConfigureAwait(false);
|
||||
await _decimationSemaphore.WaitAsync().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
await DecimateInternalAsync(hash, filePath, settings, allowExisting: false, destinationOverride: filePath, registerDecimatedPath: false).ConfigureAwait(false);
|
||||
@@ -109,7 +105,7 @@ public sealed class ModelDecimationService
|
||||
{
|
||||
_decimationSemaphore.Release();
|
||||
}
|
||||
}, CancellationToken.None));
|
||||
});
|
||||
}
|
||||
|
||||
public bool ShouldScheduleDecimation(string hash, string filePath, string? gamePath = null)
|
||||
@@ -352,46 +348,40 @@ public sealed class ModelDecimationService
|
||||
return true;
|
||||
}
|
||||
|
||||
var normalized = NormalizeGamePath(gamePath);
|
||||
if (normalized.Contains("/hair/", StringComparison.Ordinal))
|
||||
var normalized = ModelDecimationFilters.NormalizePath(gamePath);
|
||||
if (ModelDecimationFilters.IsHairPath(normalized))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (normalized.Contains("/chara/equipment/", StringComparison.Ordinal))
|
||||
if (ModelDecimationFilters.IsClothingPath(normalized))
|
||||
{
|
||||
return _performanceConfigService.Current.ModelDecimationAllowClothing;
|
||||
}
|
||||
|
||||
if (normalized.Contains("/chara/accessory/", StringComparison.Ordinal))
|
||||
if (ModelDecimationFilters.IsAccessoryPath(normalized))
|
||||
{
|
||||
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))
|
||||
{
|
||||
return _performanceConfigService.Current.ModelDecimationAllowFaceHead;
|
||||
}
|
||||
if (ModelDecimationFilters.IsFaceHeadPath(normalized))
|
||||
{
|
||||
return _performanceConfigService.Current.ModelDecimationAllowFaceHead;
|
||||
}
|
||||
|
||||
if (normalized.Contains("/tail/", StringComparison.Ordinal))
|
||||
{
|
||||
return _performanceConfigService.Current.ModelDecimationAllowTail;
|
||||
}
|
||||
if (ModelDecimationFilters.IsTailOrEarPath(normalized))
|
||||
{
|
||||
return _performanceConfigService.Current.ModelDecimationAllowTail;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static string NormalizeGamePath(string path)
|
||||
=> path.Replace('\\', '/').ToLowerInvariant();
|
||||
|
||||
private bool TryGetDecimationSettings(out ModelDecimationSettings settings)
|
||||
{
|
||||
settings = new ModelDecimationSettings(
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace LightlessSync.Services;
|
||||
|
||||
public sealed class ModelProcessingQueue : IDisposable
|
||||
{
|
||||
private readonly AssetProcessingQueue _queue;
|
||||
|
||||
public ModelProcessingQueue(ILogger<ModelProcessingQueue> logger)
|
||||
{
|
||||
_queue = new AssetProcessingQueue(logger, "LightlessSync.ModelProcessing");
|
||||
}
|
||||
|
||||
public Task Enqueue(Func<CancellationToken, Task> work, CancellationToken token = default)
|
||||
=> _queue.Enqueue(work, token);
|
||||
|
||||
public void Dispose()
|
||||
=> _queue.Dispose();
|
||||
}
|
||||
@@ -10,15 +10,18 @@ namespace LightlessSync.Services;
|
||||
public sealed class PenumbraTempCollectionJanitor : DisposableMediatorSubscriberBase
|
||||
{
|
||||
private readonly IpcManager _ipc;
|
||||
private readonly LightlessConfigService _config;
|
||||
private readonly TempCollectionConfigService _config;
|
||||
private readonly CancellationTokenSource _cleanupCts = new();
|
||||
private int _ran;
|
||||
private const int CleanupBatchSize = 50;
|
||||
private static readonly TimeSpan CleanupBatchDelay = TimeSpan.FromMilliseconds(50);
|
||||
private static readonly TimeSpan OrphanCleanupDelay = TimeSpan.FromDays(1);
|
||||
|
||||
public PenumbraTempCollectionJanitor(
|
||||
ILogger<PenumbraTempCollectionJanitor> logger,
|
||||
LightlessMediator mediator,
|
||||
IpcManager ipc,
|
||||
LightlessConfigService config) : base(logger, mediator)
|
||||
TempCollectionConfigService config) : base(logger, mediator)
|
||||
{
|
||||
_ipc = ipc;
|
||||
_config = config;
|
||||
@@ -31,10 +34,6 @@ public sealed class PenumbraTempCollectionJanitor : DisposableMediatorSubscriber
|
||||
if (id == Guid.Empty) return;
|
||||
var changed = false;
|
||||
var config = _config.Current;
|
||||
if (config.OrphanableTempCollections.Add(id))
|
||||
{
|
||||
changed = true;
|
||||
}
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
var existing = config.OrphanableTempCollectionEntries.FirstOrDefault(entry => entry.Id == id);
|
||||
@@ -63,8 +62,7 @@ public sealed class PenumbraTempCollectionJanitor : DisposableMediatorSubscriber
|
||||
{
|
||||
if (id == Guid.Empty) return;
|
||||
var config = _config.Current;
|
||||
var changed = config.OrphanableTempCollections.Remove(id);
|
||||
changed |= RemoveEntry(config.OrphanableTempCollectionEntries, id) > 0;
|
||||
var changed = RemoveEntry(config.OrphanableTempCollectionEntries, id) > 0;
|
||||
if (changed)
|
||||
{
|
||||
_config.Save();
|
||||
@@ -79,14 +77,31 @@ public sealed class PenumbraTempCollectionJanitor : DisposableMediatorSubscriber
|
||||
if (!_ipc.Penumbra.APIAvailable)
|
||||
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 ids = config.OrphanableTempCollections;
|
||||
var entries = config.OrphanableTempCollectionEntries;
|
||||
if (ids.Count == 0 && entries.Count == 0)
|
||||
if (entries.Count == 0)
|
||||
return;
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
var changed = EnsureEntries(ids, entries, now);
|
||||
var changed = EnsureEntryTimes(entries, now);
|
||||
var cutoff = now - OrphanCleanupDelay;
|
||||
var expired = entries
|
||||
.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();
|
||||
Logger.LogInformation("Cleaning up {count} orphaned Lightless temp collections older than {delay}", expired.Count, OrphanCleanupDelay);
|
||||
|
||||
List<Guid> removedIds = [];
|
||||
foreach (var id in expired)
|
||||
{
|
||||
if (token.IsCancellationRequested)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_ipc.Penumbra.RemoveTemporaryCollectionAsync(Logger, appId, id)
|
||||
.GetAwaiter().GetResult();
|
||||
await _ipc.Penumbra.RemoveTemporaryCollectionAsync(Logger, appId, id).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
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);
|
||||
}
|
||||
@@ -131,6 +168,17 @@ public sealed class PenumbraTempCollectionJanitor : DisposableMediatorSubscriber
|
||||
_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)
|
||||
{
|
||||
var removed = 0;
|
||||
@@ -148,29 +196,9 @@ public sealed class PenumbraTempCollectionJanitor : DisposableMediatorSubscriber
|
||||
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;
|
||||
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)
|
||||
{
|
||||
if (entry.Id == Guid.Empty || entry.RegisteredAtUtc != DateTime.MinValue)
|
||||
|
||||
@@ -8,7 +8,6 @@ using System.Threading;
|
||||
using OtterTex;
|
||||
using OtterImage = OtterTex.Image;
|
||||
using LightlessSync.LightlessConfiguration;
|
||||
using LightlessSync.Services;
|
||||
using LightlessSync.Utils;
|
||||
using LightlessSync.FileCache;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -34,7 +33,6 @@ public sealed class TextureDownscaleService
|
||||
private readonly PlayerPerformanceConfigService _playerPerformanceConfigService;
|
||||
private readonly FileCacheManager _fileCacheManager;
|
||||
private readonly TextureCompressionService _textureCompressionService;
|
||||
private readonly TextureProcessingQueue _processingQueue;
|
||||
|
||||
private readonly TaskRegistry<string> _downscaleDeduplicator = new();
|
||||
private readonly ConcurrentDictionary<string, string> _downscaledPaths = new(StringComparer.OrdinalIgnoreCase);
|
||||
@@ -75,15 +73,13 @@ public sealed class TextureDownscaleService
|
||||
LightlessConfigService configService,
|
||||
PlayerPerformanceConfigService playerPerformanceConfigService,
|
||||
FileCacheManager fileCacheManager,
|
||||
TextureCompressionService textureCompressionService,
|
||||
TextureProcessingQueue processingQueue)
|
||||
TextureCompressionService textureCompressionService)
|
||||
{
|
||||
_logger = logger;
|
||||
_configService = configService;
|
||||
_playerPerformanceConfigService = playerPerformanceConfigService;
|
||||
_fileCacheManager = fileCacheManager;
|
||||
_textureCompressionService = textureCompressionService;
|
||||
_processingQueue = processingQueue;
|
||||
}
|
||||
|
||||
public void ScheduleDownscale(string hash, string filePath, TextureMapKind mapKind)
|
||||
@@ -94,7 +90,7 @@ public sealed class TextureDownscaleService
|
||||
if (!filePath.EndsWith(".tex", StringComparison.OrdinalIgnoreCase)) return;
|
||||
if (_downscaleDeduplicator.TryGetExisting(hash, out _)) return;
|
||||
|
||||
_downscaleDeduplicator.GetOrStart(hash, () => _processingQueue.Enqueue(async token =>
|
||||
_downscaleDeduplicator.GetOrStart(hash, async () =>
|
||||
{
|
||||
TextureMapKind mapKind;
|
||||
try
|
||||
@@ -108,7 +104,7 @@ public sealed class TextureDownscaleService
|
||||
}
|
||||
|
||||
await DownscaleInternalAsync(hash, filePath, mapKind).ConfigureAwait(false);
|
||||
}, CancellationToken.None));
|
||||
});
|
||||
}
|
||||
|
||||
public bool ShouldScheduleDownscale(string filePath)
|
||||
@@ -386,12 +382,6 @@ public sealed class TextureDownscaleService
|
||||
{
|
||||
var isCompressed = sourceFormat.IsCompressed();
|
||||
var targetFormat = isCompressed ? sourceFormat : DXGIFormat.B8G8R8A8UNorm;
|
||||
_logger.LogDebug(
|
||||
"Downscale convert target {TargetFormat} (source {SourceFormat}, compressed {IsCompressed}, penumbraFallback {PenumbraFallback})",
|
||||
targetFormat,
|
||||
sourceFormat,
|
||||
isCompressed,
|
||||
attemptPenumbraFallback);
|
||||
try
|
||||
{
|
||||
result = source.Convert(targetFormat);
|
||||
@@ -443,7 +433,6 @@ public sealed class TextureDownscaleService
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogDebug("Downscale Penumbra re-encode target {Target} for {Hash}.", target, hash);
|
||||
using var uncompressed = resizedScratch.Convert(DXGIFormat.B8G8R8A8UNorm);
|
||||
TexFileHelper.Save(destination, uncompressed);
|
||||
}
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace LightlessSync.Services;
|
||||
|
||||
public sealed class TextureProcessingQueue : IDisposable
|
||||
{
|
||||
private readonly AssetProcessingQueue _queue;
|
||||
|
||||
public TextureProcessingQueue(ILogger<TextureProcessingQueue> logger)
|
||||
{
|
||||
_queue = new AssetProcessingQueue(logger, "LightlessSync.TextureProcessing");
|
||||
}
|
||||
|
||||
public Task Enqueue(Func<CancellationToken, Task> work, CancellationToken token = default)
|
||||
=> _queue.Enqueue(work, token);
|
||||
|
||||
public void Dispose()
|
||||
=> _queue.Dispose();
|
||||
}
|
||||
@@ -4,8 +4,10 @@ using Dalamud.Interface.Utility;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
using LightlessSync.API.Data.Enum;
|
||||
using LightlessSync.API.Data.Extensions;
|
||||
using LightlessSync.API.Dto.CharaData;
|
||||
using LightlessSync.API.Dto.Group;
|
||||
using LightlessSync.API.Dto.User;
|
||||
using LightlessSync.Interop.Ipc;
|
||||
using LightlessSync.LightlessConfiguration;
|
||||
using LightlessSync.PlayerData.Pairs;
|
||||
using LightlessSync.Services;
|
||||
@@ -40,6 +42,7 @@ public class DrawUserPair
|
||||
private readonly LocationShareService _locationShareService;
|
||||
private readonly CharaDataManager _charaDataManager;
|
||||
private readonly PairLedger _pairLedger;
|
||||
private readonly IpcCallerLifestream _lifestreamIpc;
|
||||
private float _menuWidth = -1;
|
||||
private bool _wasHovered = false;
|
||||
private TooltipSnapshot _tooltipSnapshot = TooltipSnapshot.Empty;
|
||||
@@ -60,7 +63,8 @@ public class DrawUserPair
|
||||
LightlessConfigService configService,
|
||||
LocationShareService locationShareService,
|
||||
CharaDataManager charaDataManager,
|
||||
PairLedger pairLedger)
|
||||
PairLedger pairLedger,
|
||||
IpcCallerLifestream lifestreamIpc)
|
||||
{
|
||||
_id = id;
|
||||
_uiEntry = uiEntry;
|
||||
@@ -79,6 +83,7 @@ public class DrawUserPair
|
||||
_locationShareService = locationShareService;
|
||||
_charaDataManager = charaDataManager;
|
||||
_pairLedger = pairLedger;
|
||||
_lifestreamIpc = lifestreamIpc;
|
||||
}
|
||||
|
||||
public PairDisplayEntry DisplayEntry => _displayEntry;
|
||||
@@ -656,6 +661,13 @@ public class DrawUserPair
|
||||
using (ImRaii.PushColor(ImGuiCol.Text, shareColor, shareLocation || shareLocationToOther))
|
||||
_uiSharedService.IconText(shareLocationIcon);
|
||||
|
||||
var popupId = $"LocationPopup_{_pair.UserData.UID}";
|
||||
|
||||
if (ImGui.IsItemClicked(ImGuiMouseButton.Left) && shareLocation && !string.IsNullOrEmpty(location))
|
||||
{
|
||||
ImGui.OpenPopup(popupId);
|
||||
}
|
||||
|
||||
if (ImGui.IsItemHovered())
|
||||
{
|
||||
ImGui.BeginTooltip();
|
||||
@@ -669,6 +681,8 @@ public class DrawUserPair
|
||||
_uiSharedService.IconText(FontAwesomeIcon.LocationArrow);
|
||||
ImGui.SameLine();
|
||||
ImGui.TextUnformatted(location);
|
||||
ImGui.Separator();
|
||||
ImGui.TextUnformatted("Click to teleport to this location");
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -700,6 +714,62 @@ public class DrawUserPair
|
||||
}
|
||||
ImGui.EndTooltip();
|
||||
}
|
||||
|
||||
if (ImGui.BeginPopup(popupId))
|
||||
{
|
||||
|
||||
var locationInfo = _locationShareService.GetLocationForLifestreamByUid(_pair.UserData.UID);
|
||||
if (locationInfo != null)
|
||||
{
|
||||
var locationLi = locationInfo.Value;
|
||||
var housingAddress = _locationShareService.GetAddressBookEntryByLocation(locationLi);
|
||||
var mapAddress = _locationShareService.GetMapAddressByLocation(locationLi);
|
||||
ImGui.TextUnformatted("Teleport to user?");
|
||||
ImGui.Separator();
|
||||
if (!_lifestreamIpc.APIAvailable)
|
||||
{
|
||||
ImGui.TextUnformatted("Lifestream IPC is not available. Please ensure Lifestream is enabled");
|
||||
}
|
||||
else if (housingAddress != null || mapAddress != null)
|
||||
{
|
||||
ImGui.TextUnformatted($"Go to {location}?");
|
||||
ImGui.TextUnformatted($"NOTE: Teleporting to maps with multiple aetherytes or instances may not be accurate currently. (ie. Thavnair, Yanxia)");
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui.TextUnformatted("Lifestream cannot teleport here. If you are in a residential area, please make sure you're inside a plot.");
|
||||
}
|
||||
ImGui.Separator();
|
||||
if (_lifestreamIpc.APIAvailable && (housingAddress != null || mapAddress != null))
|
||||
{
|
||||
if (locationLi.HouseId is not 0 && housingAddress != null)
|
||||
{
|
||||
if (ImGui.Button("Navigate"))
|
||||
{
|
||||
_lifestreamIpc.GoToHousingAddress(housingAddress.Value);
|
||||
|
||||
ImGui.CloseCurrentPopup();
|
||||
}
|
||||
}
|
||||
else if (mapAddress != null && locationLi.HouseId is 0)
|
||||
{
|
||||
if (ImGui.Button("Navigate"))
|
||||
{
|
||||
_lifestreamIpc.ExecuteLifestreamCommand(mapAddress);
|
||||
|
||||
ImGui.CloseCurrentPopup();
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
}
|
||||
if (ImGui.Button("Close"))
|
||||
{
|
||||
ImGui.CloseCurrentPopup();
|
||||
}
|
||||
ImGui.EndPopup();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (individualAnimDisabled || individualSoundsDisabled || individualVFXDisabled || individualIsSticky)
|
||||
|
||||
@@ -56,7 +56,6 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
|
||||
private readonly ModelDecimationService _modelDecimationService;
|
||||
private readonly TextureCompressionService _textureCompressionService;
|
||||
private readonly TextureMetadataHelper _textureMetadataHelper;
|
||||
private readonly TextureProcessingQueue _processingQueue;
|
||||
|
||||
private readonly List<TextureRow> _textureRows = new();
|
||||
private readonly Dictionary<string, TextureCompressionTarget> _textureSelections = new(StringComparer.OrdinalIgnoreCase);
|
||||
@@ -138,8 +137,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
|
||||
LightlessConfigService configService,
|
||||
PlayerPerformanceConfigService playerPerformanceConfig, TransientResourceManager transientResourceManager,
|
||||
TransientConfigService transientConfigService, ModelDecimationService modelDecimationService,
|
||||
TextureCompressionService textureCompressionService, TextureMetadataHelper textureMetadataHelper,
|
||||
TextureProcessingQueue processingQueue)
|
||||
TextureCompressionService textureCompressionService, TextureMetadataHelper textureMetadataHelper)
|
||||
: base(logger, mediator, "Lightless Character Data Analysis", performanceCollectorService)
|
||||
{
|
||||
_characterAnalyzer = characterAnalyzer;
|
||||
@@ -152,7 +150,6 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
|
||||
_modelDecimationService = modelDecimationService;
|
||||
_textureCompressionService = textureCompressionService;
|
||||
_textureMetadataHelper = textureMetadataHelper;
|
||||
_processingQueue = processingQueue;
|
||||
Mediator.Subscribe<CharacterDataAnalyzedMessage>(this, (_) =>
|
||||
{
|
||||
_hasUpdate = true;
|
||||
@@ -3719,10 +3716,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
|
||||
_conversionCurrentFileProgress = 0;
|
||||
_conversionFailed = false;
|
||||
|
||||
var conversionToken = _conversionCancellationTokenSource.Token;
|
||||
_conversionTask = _processingQueue.Enqueue(
|
||||
queueToken => RunTextureConversionAsync(requests, queueToken),
|
||||
conversionToken);
|
||||
_conversionTask = RunTextureConversionAsync(requests, _conversionCancellationTokenSource.Token);
|
||||
_showModal = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ using LightlessSync.UI.Handlers;
|
||||
using LightlessSync.UI.Models;
|
||||
using LightlessSync.WebAPI;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using LightlessSync.Interop.Ipc;
|
||||
|
||||
namespace LightlessSync.UI;
|
||||
|
||||
@@ -40,6 +41,7 @@ public class DrawEntityFactory
|
||||
private readonly IdDisplayHandler _uidDisplayHandler;
|
||||
private readonly PairLedger _pairLedger;
|
||||
private readonly PairFactory _pairFactory;
|
||||
private readonly IpcCallerLifestream _lifestreamIpc;
|
||||
|
||||
public DrawEntityFactory(
|
||||
ILogger<DrawEntityFactory> logger,
|
||||
@@ -60,7 +62,8 @@ public class DrawEntityFactory
|
||||
RenameSyncshellTagUi renameSyncshellTagUi,
|
||||
SelectSyncshellForTagUi selectSyncshellForTagUi,
|
||||
PairLedger pairLedger,
|
||||
PairFactory pairFactory)
|
||||
PairFactory pairFactory,
|
||||
IpcCallerLifestream lifestreamIpc)
|
||||
{
|
||||
_logger = logger;
|
||||
_apiController = apiController;
|
||||
@@ -81,6 +84,7 @@ public class DrawEntityFactory
|
||||
_selectSyncshellForTagUi = selectSyncshellForTagUi;
|
||||
_pairLedger = pairLedger;
|
||||
_pairFactory = pairFactory;
|
||||
_lifestreamIpc = lifestreamIpc;
|
||||
}
|
||||
|
||||
public DrawFolderGroup CreateGroupFolder(
|
||||
@@ -167,7 +171,8 @@ public class DrawEntityFactory
|
||||
_configService,
|
||||
_locationShareService,
|
||||
_charaDataManager,
|
||||
_pairLedger);
|
||||
_pairLedger,
|
||||
_lifestreamIpc);
|
||||
}
|
||||
|
||||
public IReadOnlyList<PairUiEntry> GetAllEntries()
|
||||
|
||||
Reference in New Issue
Block a user