Compare commits
61 Commits
collection
...
2.0.2.83-D
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
84a3293f6b | ||
|
|
28db3d6fd2 | ||
| d83ca98008 | |||
| e6df37bcca | |||
| 60d144b881 | |||
|
|
995e11371a | ||
| 5089dbd6c8 | |||
|
|
abc324bf4f | ||
|
|
eee0e072bd | ||
|
|
d8335eb04f | ||
|
|
994335c6b0 | ||
|
|
172288c755 | ||
|
|
1c17be53d0 | ||
| 68b4863f52 | |||
|
|
22fe9901a4 | ||
|
|
cff866dcc2 | ||
|
|
e8f598e695 | ||
|
|
861a337029 | ||
| 06f89955d3 | |||
|
|
367af2c3d0 | ||
|
|
19a238c808 | ||
|
|
c7a2b679f2 | ||
|
|
bec69074a5 | ||
| 7d86b41cee | |||
| 0185e6b534 | |||
|
|
90bf84f8eb | ||
| f27db300ec | |||
| 828be6eb5b | |||
| d039d2fd90 | |||
| e75a371475 | |||
|
|
ac711d9a43 | ||
|
|
b875e0c3a1 | ||
|
|
d6437998ac | ||
|
|
4fa9876c1c | ||
|
|
46e76bbfe6 | ||
| 9dd8e19fb7 | |||
| 5167465d28 | |||
| e8c7539770 | |||
| 54d6a0a1a4 | |||
| b57d54d69c | |||
| 8be0811b4a | |||
| 7c281926a5 | |||
| 6c7e4e6303 | |||
| e2d663cae9 | |||
| 96123d00a2 | |||
|
|
3654365f2a | ||
|
|
9b256dd185 | ||
| 59ed03a825 | |||
| ae76efedf8 | |||
| 0e24da75d5 | |||
|
|
223ade39cb | ||
|
|
5aca9e70b2 | ||
|
|
92772cf334 | ||
|
|
0395e81a9f | ||
|
|
7734a7bf7e | ||
|
|
db2d19bb1e | ||
|
|
ab305a249c | ||
|
|
9d104a9dd8 | ||
|
|
bcd3bd5ca2 | ||
|
|
c1829a9837 | ||
|
|
cca23f6e05 |
18
LightlessCompactor/FileCache/CompactorInterfaces.cs
Normal file
18
LightlessCompactor/FileCache/CompactorInterfaces.cs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
namespace LightlessSync.FileCache;
|
||||||
|
|
||||||
|
public interface ICompactorContext
|
||||||
|
{
|
||||||
|
bool UseCompactor { get; }
|
||||||
|
string CacheFolder { get; }
|
||||||
|
bool IsWine { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface ICompactionExecutor
|
||||||
|
{
|
||||||
|
bool TryCompact(string filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class NoopCompactionExecutor : ICompactionExecutor
|
||||||
|
{
|
||||||
|
public bool TryCompact(string filePath) => false;
|
||||||
|
}
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
using LightlessSync.LightlessConfiguration;
|
using LightlessSync.Services.Compactor;
|
||||||
using LightlessSync.Services;
|
|
||||||
using LightlessSync.Services.Compactor;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Win32.SafeHandles;
|
using Microsoft.Win32.SafeHandles;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
@@ -20,8 +18,8 @@ public sealed partial class FileCompactor : IDisposable
|
|||||||
|
|
||||||
private readonly ConcurrentDictionary<string, byte> _pendingCompactions;
|
private readonly ConcurrentDictionary<string, byte> _pendingCompactions;
|
||||||
private readonly ILogger<FileCompactor> _logger;
|
private readonly ILogger<FileCompactor> _logger;
|
||||||
private readonly LightlessConfigService _lightlessConfigService;
|
private readonly ICompactorContext _context;
|
||||||
private readonly DalamudUtilService _dalamudUtilService;
|
private readonly ICompactionExecutor _compactionExecutor;
|
||||||
|
|
||||||
private readonly Channel<string> _compactionQueue;
|
private readonly Channel<string> _compactionQueue;
|
||||||
private readonly CancellationTokenSource _compactionCts = new();
|
private readonly CancellationTokenSource _compactionCts = new();
|
||||||
@@ -59,12 +57,12 @@ public sealed partial class FileCompactor : IDisposable
|
|||||||
XPRESS16K = 3
|
XPRESS16K = 3
|
||||||
}
|
}
|
||||||
|
|
||||||
public FileCompactor(ILogger<FileCompactor> logger, LightlessConfigService lightlessConfigService, DalamudUtilService dalamudUtilService)
|
public FileCompactor(ILogger<FileCompactor> logger, ICompactorContext context, ICompactionExecutor compactionExecutor)
|
||||||
{
|
{
|
||||||
_pendingCompactions = new(StringComparer.OrdinalIgnoreCase);
|
_pendingCompactions = new(StringComparer.OrdinalIgnoreCase);
|
||||||
_logger = logger;
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||||
_lightlessConfigService = lightlessConfigService;
|
_context = context ?? throw new ArgumentNullException(nameof(context));
|
||||||
_dalamudUtilService = dalamudUtilService;
|
_compactionExecutor = compactionExecutor ?? throw new ArgumentNullException(nameof(compactionExecutor));
|
||||||
_isWindows = OperatingSystem.IsWindows();
|
_isWindows = OperatingSystem.IsWindows();
|
||||||
|
|
||||||
_compactionQueue = Channel.CreateUnbounded<string>(new UnboundedChannelOptions
|
_compactionQueue = Channel.CreateUnbounded<string>(new UnboundedChannelOptions
|
||||||
@@ -94,7 +92,7 @@ public sealed partial class FileCompactor : IDisposable
|
|||||||
|
|
||||||
//Uses an batching service for the filefrag command on Linux
|
//Uses an batching service for the filefrag command on Linux
|
||||||
_fragBatch = new BatchFilefragService(
|
_fragBatch = new BatchFilefragService(
|
||||||
useShell: _dalamudUtilService.IsWine,
|
useShell: _context.IsWine,
|
||||||
log: _logger,
|
log: _logger,
|
||||||
batchSize: 64,
|
batchSize: 64,
|
||||||
flushMs: 25,
|
flushMs: 25,
|
||||||
@@ -118,7 +116,7 @@ public sealed partial class FileCompactor : IDisposable
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var folder = _lightlessConfigService.Current.CacheFolder;
|
var folder = _context.CacheFolder;
|
||||||
if (string.IsNullOrWhiteSpace(folder) || !Directory.Exists(folder))
|
if (string.IsNullOrWhiteSpace(folder) || !Directory.Exists(folder))
|
||||||
{
|
{
|
||||||
if (_logger.IsEnabled(LogLevel.Warning))
|
if (_logger.IsEnabled(LogLevel.Warning))
|
||||||
@@ -127,7 +125,7 @@ public sealed partial class FileCompactor : IDisposable
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var files = Directory.EnumerateFiles(folder).ToArray();
|
var files = Directory.EnumerateFiles(folder, "*", SearchOption.AllDirectories).ToArray();
|
||||||
var total = files.Length;
|
var total = files.Length;
|
||||||
Progress = $"0/{total}";
|
Progress = $"0/{total}";
|
||||||
if (total == 0) return;
|
if (total == 0) return;
|
||||||
@@ -155,7 +153,7 @@ public sealed partial class FileCompactor : IDisposable
|
|||||||
{
|
{
|
||||||
if (compress)
|
if (compress)
|
||||||
{
|
{
|
||||||
if (_lightlessConfigService.Current.UseCompactor)
|
if (_context.UseCompactor)
|
||||||
CompactFile(file, workerId);
|
CompactFile(file, workerId);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -213,7 +211,7 @@ public sealed partial class FileCompactor : IDisposable
|
|||||||
/// <param name="bytes">Bytes that have to be written</param>
|
/// <param name="bytes">Bytes that have to be written</param>
|
||||||
/// <param name="token">Cancellation Token for interupts</param>
|
/// <param name="token">Cancellation Token for interupts</param>
|
||||||
/// <returns>Writing Task</returns>
|
/// <returns>Writing Task</returns>
|
||||||
public async Task WriteAllBytesAsync(string filePath, byte[] bytes, CancellationToken token, bool enqueueCompaction = true)
|
public async Task WriteAllBytesAsync(string filePath, byte[] bytes, CancellationToken token)
|
||||||
{
|
{
|
||||||
var dir = Path.GetDirectoryName(filePath);
|
var dir = Path.GetDirectoryName(filePath);
|
||||||
if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
|
if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
|
||||||
@@ -221,25 +219,52 @@ public sealed partial class FileCompactor : IDisposable
|
|||||||
|
|
||||||
await File.WriteAllBytesAsync(filePath, bytes, token).ConfigureAwait(false);
|
await File.WriteAllBytesAsync(filePath, bytes, token).ConfigureAwait(false);
|
||||||
|
|
||||||
if (enqueueCompaction && _lightlessConfigService.Current.UseCompactor)
|
if (_context.UseCompactor)
|
||||||
EnqueueCompaction(filePath);
|
EnqueueCompaction(filePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RequestCompaction(string filePath)
|
/// <summary>
|
||||||
|
/// Notify the compactor that a file was written directly (streamed) so it can enqueue compaction.
|
||||||
|
/// </summary>
|
||||||
|
public void NotifyFileWritten(string filePath)
|
||||||
{
|
{
|
||||||
if (_lightlessConfigService.Current.UseCompactor)
|
|
||||||
EnqueueCompaction(filePath);
|
EnqueueCompaction(filePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool TryCompactFile(string filePath)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(filePath))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (!_context.UseCompactor || !File.Exists(filePath))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
CompactFile(filePath, workerId: -1);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (IOException ioEx)
|
||||||
|
{
|
||||||
|
_logger.LogDebug(ioEx, "File being read/written, skipping file: {file}", filePath);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Error compacting file: {file}", filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the File size for an BTRFS or NTFS file system for the given FileInfo
|
/// Gets the File size for an BTRFS or NTFS file system for the given FileInfo
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="path">Amount of blocks used in the disk</param>
|
/// <param name="path">Amount of blocks used in the disk</param>
|
||||||
public long GetFileSizeOnDisk(FileInfo fileInfo)
|
public long GetFileSizeOnDisk(FileInfo fileInfo)
|
||||||
{
|
{
|
||||||
var fsType = GetFilesystemType(fileInfo.FullName, _dalamudUtilService.IsWine);
|
var fsType = GetFilesystemType(fileInfo.FullName, _context.IsWine);
|
||||||
|
|
||||||
if (fsType == FilesystemType.NTFS && !_dalamudUtilService.IsWine)
|
if (fsType == FilesystemType.NTFS && !_context.IsWine)
|
||||||
{
|
{
|
||||||
(bool flowControl, long value) = GetFileSizeNTFS(fileInfo);
|
(bool flowControl, long value) = GetFileSizeNTFS(fileInfo);
|
||||||
if (!flowControl)
|
if (!flowControl)
|
||||||
@@ -296,7 +321,7 @@ public sealed partial class FileCompactor : IDisposable
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var blockSize = GetBlockSizeForPath(fileInfo.FullName, _logger, _dalamudUtilService.IsWine);
|
var blockSize = GetBlockSizeForPath(fileInfo.FullName, _logger, _context.IsWine);
|
||||||
if (blockSize <= 0)
|
if (blockSize <= 0)
|
||||||
throw new InvalidOperationException($"Invalid block size {blockSize} for {fileInfo.FullName}");
|
throw new InvalidOperationException($"Invalid block size {blockSize} for {fileInfo.FullName}");
|
||||||
|
|
||||||
@@ -336,7 +361,7 @@ public sealed partial class FileCompactor : IDisposable
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var fsType = GetFilesystemType(filePath, _dalamudUtilService.IsWine);
|
var fsType = GetFilesystemType(filePath, _context.IsWine);
|
||||||
var oldSize = fi.Length;
|
var oldSize = fi.Length;
|
||||||
int blockSize = (int)(GetFileSizeOnDisk(fi) / 512);
|
int blockSize = (int)(GetFileSizeOnDisk(fi) / 512);
|
||||||
|
|
||||||
@@ -352,7 +377,7 @@ public sealed partial class FileCompactor : IDisposable
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fsType == FilesystemType.NTFS && !_dalamudUtilService.IsWine)
|
if (fsType == FilesystemType.NTFS && !_context.IsWine)
|
||||||
{
|
{
|
||||||
if (!IsWOFCompactedFile(filePath))
|
if (!IsWOFCompactedFile(filePath))
|
||||||
{
|
{
|
||||||
@@ -408,9 +433,9 @@ public sealed partial class FileCompactor : IDisposable
|
|||||||
private void DecompressFile(string filePath, int workerId)
|
private void DecompressFile(string filePath, int workerId)
|
||||||
{
|
{
|
||||||
_logger.LogDebug("[W{worker}] Decompress request: {file}", workerId, filePath);
|
_logger.LogDebug("[W{worker}] Decompress request: {file}", workerId, filePath);
|
||||||
var fsType = GetFilesystemType(filePath, _dalamudUtilService.IsWine);
|
var fsType = GetFilesystemType(filePath, _context.IsWine);
|
||||||
|
|
||||||
if (fsType == FilesystemType.NTFS && !_dalamudUtilService.IsWine)
|
if (fsType == FilesystemType.NTFS && !_context.IsWine)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -454,7 +479,7 @@ public sealed partial class FileCompactor : IDisposable
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
bool isWine = _dalamudUtilService?.IsWine ?? false;
|
bool isWine = _context.IsWine;
|
||||||
string linuxPath = isWine ? ToLinuxPathIfWine(path, isWine) : path;
|
string linuxPath = isWine ? ToLinuxPathIfWine(path, isWine) : path;
|
||||||
|
|
||||||
var opts = GetMountOptionsForPath(linuxPath);
|
var opts = GetMountOptionsForPath(linuxPath);
|
||||||
@@ -967,7 +992,7 @@ public sealed partial class FileCompactor : IDisposable
|
|||||||
if (finished != bothTasks)
|
if (finished != bothTasks)
|
||||||
return KillProcess(proc, outTask, errTask, token);
|
return KillProcess(proc, outTask, errTask, token);
|
||||||
|
|
||||||
bool isWine = _dalamudUtilService?.IsWine ?? false;
|
bool isWine = _context.IsWine;
|
||||||
if (!isWine)
|
if (!isWine)
|
||||||
{
|
{
|
||||||
try { proc.WaitForExit(); } catch { /* ignore quirks */ }
|
try { proc.WaitForExit(); } catch { /* ignore quirks */ }
|
||||||
@@ -1011,7 +1036,7 @@ public sealed partial class FileCompactor : IDisposable
|
|||||||
if (string.IsNullOrWhiteSpace(filePath))
|
if (string.IsNullOrWhiteSpace(filePath))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (!_lightlessConfigService.Current.UseCompactor)
|
if (!_context.UseCompactor)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (!File.Exists(filePath))
|
if (!File.Exists(filePath))
|
||||||
@@ -1023,7 +1048,7 @@ public sealed partial class FileCompactor : IDisposable
|
|||||||
bool enqueued = false;
|
bool enqueued = false;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
bool isWine = _dalamudUtilService?.IsWine ?? false;
|
bool isWine = _context.IsWine;
|
||||||
var fsType = GetFilesystemType(filePath, isWine);
|
var fsType = GetFilesystemType(filePath, isWine);
|
||||||
|
|
||||||
// If under Wine, we should skip NTFS because its not Windows but might return NTFS.
|
// If under Wine, we should skip NTFS because its not Windows but might return NTFS.
|
||||||
@@ -1076,9 +1101,12 @@ public sealed partial class FileCompactor : IDisposable
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (_lightlessConfigService.Current.UseCompactor && File.Exists(filePath))
|
if (_context.UseCompactor && File.Exists(filePath))
|
||||||
|
{
|
||||||
|
if (!_compactionExecutor.TryCompact(filePath))
|
||||||
CompactFile(filePath, workerId);
|
CompactFile(filePath, workerId);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
_globalGate.Release();
|
_globalGate.Release();
|
||||||
15
LightlessCompactor/LightlessCompactor.csproj
Normal file
15
LightlessCompactor/LightlessCompactor.csproj
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<LangVersion>latest</LangVersion>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.1" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
19
LightlessCompactorWorker/LightlessCompactorWorker.csproj
Normal file
19
LightlessCompactorWorker/LightlessCompactorWorker.csproj
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>WinExe</OutputType>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<LangVersion>latest</LangVersion>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\LightlessCompactor\LightlessCompactor.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="10.0.1" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
270
LightlessCompactorWorker/Program.cs
Normal file
270
LightlessCompactorWorker/Program.cs
Normal file
@@ -0,0 +1,270 @@
|
|||||||
|
using LightlessSync.FileCache;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO.Pipes;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
internal sealed class WorkerCompactorContext : ICompactorContext
|
||||||
|
{
|
||||||
|
public WorkerCompactorContext(string cacheFolder, bool isWine)
|
||||||
|
{
|
||||||
|
CacheFolder = cacheFolder;
|
||||||
|
IsWine = isWine;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool UseCompactor => true;
|
||||||
|
public string CacheFolder { get; }
|
||||||
|
public bool IsWine { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class WorkerOptions
|
||||||
|
{
|
||||||
|
public string? FilePath { get; init; }
|
||||||
|
public bool IsWine { get; init; }
|
||||||
|
public string CacheFolder { get; init; } = string.Empty;
|
||||||
|
public LogLevel LogLevel { get; init; } = LogLevel.Information;
|
||||||
|
public string PipeName { get; init; } = "LightlessCompactor";
|
||||||
|
public int? ParentProcessId { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static class Program
|
||||||
|
{
|
||||||
|
public static async Task<int> Main(string[] args)
|
||||||
|
{
|
||||||
|
var options = ParseOptions(args, out var error);
|
||||||
|
if (options is null)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(error ?? "Invalid arguments.");
|
||||||
|
Console.Error.WriteLine("Usage: LightlessCompactorWorker --file <path> [--wine] [--cache-folder <path>] [--verbose]");
|
||||||
|
Console.Error.WriteLine(" or: LightlessCompactorWorker --pipe <name> [--wine] [--parent <pid>] [--verbose]");
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
TrySetLowPriority();
|
||||||
|
|
||||||
|
using var loggerFactory = LoggerFactory.Create(builder =>
|
||||||
|
{
|
||||||
|
builder.SetMinimumLevel(options.LogLevel);
|
||||||
|
builder.AddSimpleConsole(o =>
|
||||||
|
{
|
||||||
|
o.SingleLine = true;
|
||||||
|
o.TimestampFormat = "HH:mm:ss.fff ";
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
var logger = loggerFactory.CreateLogger<FileCompactor>();
|
||||||
|
var context = new WorkerCompactorContext(options.CacheFolder, options.IsWine);
|
||||||
|
|
||||||
|
using var compactor = new FileCompactor(logger, context, new NoopCompactionExecutor());
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(options.FilePath))
|
||||||
|
{
|
||||||
|
var success = compactor.TryCompactFile(options.FilePath!);
|
||||||
|
return success ? 0 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
var serverLogger = loggerFactory.CreateLogger("CompactorWorker");
|
||||||
|
return await RunServerAsync(compactor, options, serverLogger).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<int> RunServerAsync(FileCompactor compactor, WorkerOptions options, ILogger serverLogger)
|
||||||
|
{
|
||||||
|
using var cts = new CancellationTokenSource();
|
||||||
|
var token = cts.Token;
|
||||||
|
|
||||||
|
if (options.ParentProcessId.HasValue)
|
||||||
|
{
|
||||||
|
_ = Task.Run(() => MonitorParent(options.ParentProcessId.Value, cts));
|
||||||
|
}
|
||||||
|
|
||||||
|
serverLogger.LogInformation("Compactor worker listening on pipe {pipe}", options.PipeName);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
while (!token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
var server = new NamedPipeServerStream(
|
||||||
|
options.PipeName,
|
||||||
|
PipeDirection.InOut,
|
||||||
|
NamedPipeServerStream.MaxAllowedServerInstances,
|
||||||
|
PipeTransmissionMode.Byte,
|
||||||
|
PipeOptions.Asynchronous);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await server.WaitForConnectionAsync(token).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
server.Dispose();
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = Task.Run(() => HandleClientAsync(server, compactor, cts));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
// shutdown requested
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
serverLogger.LogWarning(ex, "Compactor worker terminated unexpectedly.");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task HandleClientAsync(NamedPipeServerStream pipe, FileCompactor compactor, CancellationTokenSource shutdownCts)
|
||||||
|
{
|
||||||
|
await using var _ = pipe;
|
||||||
|
using var reader = new StreamReader(pipe);
|
||||||
|
using var writer = new StreamWriter(pipe) { AutoFlush = true };
|
||||||
|
|
||||||
|
var line = await reader.ReadLineAsync().ConfigureAwait(false);
|
||||||
|
if (string.IsNullOrWhiteSpace(line))
|
||||||
|
return;
|
||||||
|
|
||||||
|
CompactorRequest? request = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
request = JsonSerializer.Deserialize<CompactorRequest>(line);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
CompactorResponse response;
|
||||||
|
if (request is null)
|
||||||
|
{
|
||||||
|
response = new CompactorResponse { Success = false, Error = "Invalid request." };
|
||||||
|
}
|
||||||
|
else if (string.Equals(request.Type, "shutdown", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
shutdownCts.Cancel();
|
||||||
|
response = new CompactorResponse { Success = true };
|
||||||
|
}
|
||||||
|
else if (string.Equals(request.Type, "compact", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
var success = compactor.TryCompactFile(request.Path ?? string.Empty);
|
||||||
|
response = new CompactorResponse { Success = success };
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
response = new CompactorResponse { Success = false, Error = "Unknown request type." };
|
||||||
|
}
|
||||||
|
|
||||||
|
await writer.WriteLineAsync(JsonSerializer.Serialize(response)).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void MonitorParent(int parentPid, CancellationTokenSource shutdownCts)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var parent = Process.GetProcessById(parentPid);
|
||||||
|
parent.WaitForExit();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// parent missing
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
shutdownCts.Cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static WorkerOptions? ParseOptions(string[] args, out string? error)
|
||||||
|
{
|
||||||
|
string? filePath = null;
|
||||||
|
bool isWine = false;
|
||||||
|
string cacheFolder = string.Empty;
|
||||||
|
var logLevel = LogLevel.Information;
|
||||||
|
string pipeName = "LightlessCompactor";
|
||||||
|
int? parentPid = null;
|
||||||
|
|
||||||
|
for (int i = 0; i < args.Length; i++)
|
||||||
|
{
|
||||||
|
var arg = args[i];
|
||||||
|
switch (arg)
|
||||||
|
{
|
||||||
|
case "--file":
|
||||||
|
if (i + 1 >= args.Length)
|
||||||
|
{
|
||||||
|
error = "Missing value for --file.";
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
filePath = args[++i];
|
||||||
|
break;
|
||||||
|
case "--cache-folder":
|
||||||
|
if (i + 1 >= args.Length)
|
||||||
|
{
|
||||||
|
error = "Missing value for --cache-folder.";
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
cacheFolder = args[++i];
|
||||||
|
break;
|
||||||
|
case "--pipe":
|
||||||
|
if (i + 1 >= args.Length)
|
||||||
|
{
|
||||||
|
error = "Missing value for --pipe.";
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
pipeName = args[++i];
|
||||||
|
break;
|
||||||
|
case "--parent":
|
||||||
|
if (i + 1 >= args.Length || !int.TryParse(args[++i], out var pid))
|
||||||
|
{
|
||||||
|
error = "Invalid value for --parent.";
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
parentPid = pid;
|
||||||
|
break;
|
||||||
|
case "--wine":
|
||||||
|
isWine = true;
|
||||||
|
break;
|
||||||
|
case "--verbose":
|
||||||
|
logLevel = LogLevel.Trace;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
error = null;
|
||||||
|
return new WorkerOptions
|
||||||
|
{
|
||||||
|
FilePath = filePath,
|
||||||
|
IsWine = isWine,
|
||||||
|
CacheFolder = cacheFolder,
|
||||||
|
LogLevel = logLevel,
|
||||||
|
PipeName = pipeName,
|
||||||
|
ParentProcessId = parentPid
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void TrySetLowPriority()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (OperatingSystem.IsWindows())
|
||||||
|
Process.GetCurrentProcess().PriorityClass = ProcessPriorityClass.BelowNormal;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class CompactorRequest
|
||||||
|
{
|
||||||
|
public string Type { get; init; } = "compact";
|
||||||
|
public string? Path { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class CompactorResponse
|
||||||
|
{
|
||||||
|
public bool Success { get; init; }
|
||||||
|
public string? Error { get; init; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,6 +22,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OtterGui", "OtterGui\OtterG
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Pictomancy", "ffxiv_pictomancy\Pictomancy\Pictomancy.csproj", "{825F17D8-2704-24F6-DF8B-2542AC92C765}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Pictomancy", "ffxiv_pictomancy\Pictomancy\Pictomancy.csproj", "{825F17D8-2704-24F6-DF8B-2542AC92C765}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LightlessCompactor", "LightlessCompactor\LightlessCompactor.csproj", "{01F31917-9F1E-426D-BDAE-17268CBF9523}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LightlessCompactorWorker", "LightlessCompactorWorker\LightlessCompactorWorker.csproj", "{72BE3664-CD0E-4DA4-B040-91338A2798E0}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@@ -116,6 +120,30 @@ Global
|
|||||||
{825F17D8-2704-24F6-DF8B-2542AC92C765}.Release|x64.Build.0 = Release|x64
|
{825F17D8-2704-24F6-DF8B-2542AC92C765}.Release|x64.Build.0 = Release|x64
|
||||||
{825F17D8-2704-24F6-DF8B-2542AC92C765}.Release|x86.ActiveCfg = Release|x64
|
{825F17D8-2704-24F6-DF8B-2542AC92C765}.Release|x86.ActiveCfg = Release|x64
|
||||||
{825F17D8-2704-24F6-DF8B-2542AC92C765}.Release|x86.Build.0 = Release|x64
|
{825F17D8-2704-24F6-DF8B-2542AC92C765}.Release|x86.Build.0 = Release|x64
|
||||||
|
{01F31917-9F1E-426D-BDAE-17268CBF9523}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{01F31917-9F1E-426D-BDAE-17268CBF9523}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{01F31917-9F1E-426D-BDAE-17268CBF9523}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{01F31917-9F1E-426D-BDAE-17268CBF9523}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{01F31917-9F1E-426D-BDAE-17268CBF9523}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{01F31917-9F1E-426D-BDAE-17268CBF9523}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{01F31917-9F1E-426D-BDAE-17268CBF9523}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{01F31917-9F1E-426D-BDAE-17268CBF9523}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{01F31917-9F1E-426D-BDAE-17268CBF9523}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{01F31917-9F1E-426D-BDAE-17268CBF9523}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{01F31917-9F1E-426D-BDAE-17268CBF9523}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{01F31917-9F1E-426D-BDAE-17268CBF9523}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{72BE3664-CD0E-4DA4-B040-91338A2798E0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{72BE3664-CD0E-4DA4-B040-91338A2798E0}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{72BE3664-CD0E-4DA4-B040-91338A2798E0}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{72BE3664-CD0E-4DA4-B040-91338A2798E0}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{72BE3664-CD0E-4DA4-B040-91338A2798E0}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{72BE3664-CD0E-4DA4-B040-91338A2798E0}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{72BE3664-CD0E-4DA4-B040-91338A2798E0}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{72BE3664-CD0E-4DA4-B040-91338A2798E0}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{72BE3664-CD0E-4DA4-B040-91338A2798E0}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{72BE3664-CD0E-4DA4-B040-91338A2798E0}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{72BE3664-CD0E-4DA4-B040-91338A2798E0}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{72BE3664-CD0E-4DA4-B040-91338A2798E0}.Release|x86.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
|||||||
private long _currentFileProgress = 0;
|
private long _currentFileProgress = 0;
|
||||||
private CancellationTokenSource _scanCancellationTokenSource = new();
|
private CancellationTokenSource _scanCancellationTokenSource = new();
|
||||||
private readonly CancellationTokenSource _periodicCalculationTokenSource = new();
|
private readonly CancellationTokenSource _periodicCalculationTokenSource = new();
|
||||||
private readonly SemaphoreSlim _dbGate = new(1, 1);
|
|
||||||
public static readonly IImmutableList<string> AllowedFileExtensions = [".mdl", ".tex", ".mtrl", ".tmb", ".pap", ".avfx", ".atex", ".sklb", ".eid", ".phyb", ".pbd", ".scd", ".skp", ".shpk", ".kdb"];
|
public static readonly IImmutableList<string> AllowedFileExtensions = [".mdl", ".tex", ".mtrl", ".tmb", ".pap", ".avfx", ".atex", ".sklb", ".eid", ".phyb", ".pbd", ".scd", ".skp", ".shpk", ".kdb"];
|
||||||
private static readonly HashSet<string> AllowedFileExtensionSet = new(AllowedFileExtensions, StringComparer.OrdinalIgnoreCase);
|
private static readonly HashSet<string> AllowedFileExtensionSet = new(AllowedFileExtensions, StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
@@ -69,9 +68,6 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
Logger.LogInformation("Starting Periodic Storage Directory Calculation Task");
|
Logger.LogInformation("Starting Periodic Storage Directory Calculation Task");
|
||||||
var token = _periodicCalculationTokenSource.Token;
|
var token = _periodicCalculationTokenSource.Token;
|
||||||
while (IsHalted() && !token.IsCancellationRequested)
|
|
||||||
await Task.Delay(TimeSpan.FromSeconds(5), token).ConfigureAwait(false);
|
|
||||||
|
|
||||||
while (!token.IsCancellationRequested)
|
while (!token.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -95,9 +91,6 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
|||||||
public long CurrentFileProgress => _currentFileProgress;
|
public long CurrentFileProgress => _currentFileProgress;
|
||||||
public long FileCacheSize { get; set; }
|
public long FileCacheSize { get; set; }
|
||||||
public long FileCacheDriveFree { get; set; }
|
public long FileCacheDriveFree { get; set; }
|
||||||
|
|
||||||
private int _haltCount;
|
|
||||||
private bool IsHalted() => Volatile.Read(ref _haltCount) > 0;
|
|
||||||
public ConcurrentDictionary<string, int> HaltScanLocks { get; set; } = new(StringComparer.Ordinal);
|
public ConcurrentDictionary<string, int> HaltScanLocks { get; set; } = new(StringComparer.Ordinal);
|
||||||
public bool IsScanRunning => CurrentFileProgress > 0 || TotalFiles > 0;
|
public bool IsScanRunning => CurrentFileProgress > 0 || TotalFiles > 0;
|
||||||
public long TotalFiles { get; private set; }
|
public long TotalFiles { get; private set; }
|
||||||
@@ -105,36 +98,14 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
public void HaltScan(string source)
|
public void HaltScan(string source)
|
||||||
{
|
{
|
||||||
HaltScanLocks.AddOrUpdate(source, 1, (_, v) => v + 1);
|
if (!HaltScanLocks.ContainsKey(source)) HaltScanLocks[source] = 0;
|
||||||
Interlocked.Increment(ref _haltCount);
|
HaltScanLocks[source]++;
|
||||||
}
|
}
|
||||||
|
|
||||||
record WatcherChange(WatcherChangeTypes ChangeType, string? OldPath = null);
|
record WatcherChange(WatcherChangeTypes ChangeType, string? OldPath = null);
|
||||||
private readonly record struct CacheEvictionCandidate(string FullPath, long Size, DateTime LastAccessTime);
|
private readonly record struct CacheEvictionCandidate(string FullPath, long Size, DateTime LastAccessTime);
|
||||||
private readonly object _penumbraGate = new();
|
private readonly Dictionary<string, WatcherChange> _watcherChanges = new(StringComparer.OrdinalIgnoreCase);
|
||||||
private Dictionary<string, WatcherChange> _watcherChanges = new(StringComparer.OrdinalIgnoreCase);
|
private readonly Dictionary<string, WatcherChange> _lightlessChanges = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
private readonly object _lightlessGate = new();
|
|
||||||
private Dictionary<string, WatcherChange> _lightlessChanges = new(StringComparer.OrdinalIgnoreCase);
|
|
||||||
private Dictionary<string, WatcherChange> DrainPenumbraChanges()
|
|
||||||
{
|
|
||||||
lock (_penumbraGate)
|
|
||||||
{
|
|
||||||
var snapshot = _watcherChanges;
|
|
||||||
_watcherChanges = new(StringComparer.OrdinalIgnoreCase);
|
|
||||||
return snapshot;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Dictionary<string, WatcherChange> DrainLightlessChanges()
|
|
||||||
{
|
|
||||||
lock (_lightlessGate)
|
|
||||||
{
|
|
||||||
var snapshot = _lightlessChanges;
|
|
||||||
_lightlessChanges = new(StringComparer.OrdinalIgnoreCase);
|
|
||||||
return snapshot;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void StopMonitoring()
|
public void StopMonitoring()
|
||||||
{
|
{
|
||||||
@@ -197,7 +168,7 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
if (!HasAllowedExtension(e.FullPath)) return;
|
if (!HasAllowedExtension(e.FullPath)) return;
|
||||||
|
|
||||||
lock (_lightlessChanges)
|
lock (_watcherChanges)
|
||||||
{
|
{
|
||||||
_lightlessChanges[e.FullPath] = new(e.ChangeType);
|
_lightlessChanges[e.FullPath] = new(e.ChangeType);
|
||||||
}
|
}
|
||||||
@@ -310,56 +281,65 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
_lightlessFswCts = _lightlessFswCts.CancelRecreate();
|
_lightlessFswCts = _lightlessFswCts.CancelRecreate();
|
||||||
var token = _lightlessFswCts.Token;
|
var token = _lightlessFswCts.Token;
|
||||||
|
var delay = TimeSpan.FromSeconds(5);
|
||||||
|
Dictionary<string, WatcherChange> changes;
|
||||||
|
lock (_lightlessChanges)
|
||||||
|
changes = _lightlessChanges.ToDictionary(t => t.Key, t => t.Value, StringComparer.Ordinal);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await Task.Delay(TimeSpan.FromSeconds(10), token).ConfigureAwait(false);
|
do
|
||||||
while (IsHalted() && !token.IsCancellationRequested)
|
|
||||||
await Task.Delay(250, token).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
catch (TaskCanceledException) { return; }
|
|
||||||
|
|
||||||
var changes = DrainLightlessChanges();
|
|
||||||
if (changes.Count > 0)
|
|
||||||
_ = HandleChangesAsync(changes, token);
|
|
||||||
}
|
|
||||||
private async Task HandleChangesAsync(Dictionary<string, WatcherChange> changes, CancellationToken token)
|
|
||||||
{
|
{
|
||||||
await _dbGate.WaitAsync(token).ConfigureAwait(false);
|
await Task.Delay(delay, token).ConfigureAwait(false);
|
||||||
try
|
} while (HaltScanLocks.Any(f => f.Value > 0));
|
||||||
|
}
|
||||||
|
catch (TaskCanceledException)
|
||||||
{
|
{
|
||||||
var deleted = changes.Where(c => c.Value.ChangeType == WatcherChangeTypes.Deleted).Select(c => c.Key);
|
return;
|
||||||
var renamed = changes.Where(c => c.Value.ChangeType == WatcherChangeTypes.Renamed);
|
}
|
||||||
var remaining = changes.Where(c => c.Value.ChangeType != WatcherChangeTypes.Deleted).Select(c => c.Key);
|
|
||||||
|
|
||||||
foreach (var entry in deleted)
|
lock (_lightlessChanges)
|
||||||
|
{
|
||||||
|
foreach (var key in changes.Keys)
|
||||||
|
{
|
||||||
|
_lightlessChanges.Remove(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HandleChanges(changes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleChanges(Dictionary<string, WatcherChange> changes)
|
||||||
|
{
|
||||||
|
lock (_fileDbManager)
|
||||||
|
{
|
||||||
|
var deletedEntries = changes.Where(c => c.Value.ChangeType == WatcherChangeTypes.Deleted).Select(c => c.Key);
|
||||||
|
var renamedEntries = changes.Where(c => c.Value.ChangeType == WatcherChangeTypes.Renamed);
|
||||||
|
var remainingEntries = changes.Where(c => c.Value.ChangeType != WatcherChangeTypes.Deleted).Select(c => c.Key);
|
||||||
|
|
||||||
|
foreach (var entry in deletedEntries)
|
||||||
{
|
{
|
||||||
Logger.LogDebug("FSW Change: Deletion - {val}", entry);
|
Logger.LogDebug("FSW Change: Deletion - {val}", entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var entry in renamed)
|
foreach (var entry in renamedEntries)
|
||||||
{
|
{
|
||||||
Logger.LogDebug("FSW Change: Renamed - {oldVal} => {val}", entry.Value.OldPath, entry.Key);
|
Logger.LogDebug("FSW Change: Renamed - {oldVal} => {val}", entry.Value.OldPath, entry.Key);
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var entry in remaining)
|
foreach (var entry in remainingEntries)
|
||||||
{
|
{
|
||||||
Logger.LogDebug("FSW Change: Creation or Change - {val}", entry);
|
Logger.LogDebug("FSW Change: Creation or Change - {val}", entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
var allChanges = deleted
|
var allChanges = deletedEntries
|
||||||
.Concat(renamed.Select(c => c.Value.OldPath!))
|
.Concat(renamedEntries.Select(c => c.Value.OldPath!))
|
||||||
.Concat(renamed.Select(c => c.Key))
|
.Concat(renamedEntries.Select(c => c.Key))
|
||||||
.Concat(remaining)
|
.Concat(remainingEntries)
|
||||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
|
||||||
.ToArray();
|
.ToArray();
|
||||||
|
|
||||||
_ = _fileDbManager.GetFileCachesByPaths(allChanges);
|
_ = _fileDbManager.GetFileCachesByPaths(allChanges);
|
||||||
await _fileDbManager.WriteOutFullCsvAsync(token).ConfigureAwait(false);
|
|
||||||
}
|
_fileDbManager.WriteOutFullCsv();
|
||||||
finally
|
|
||||||
{
|
|
||||||
_dbGate.Release();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -367,97 +347,77 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
_penumbraFswCts = _penumbraFswCts.CancelRecreate();
|
_penumbraFswCts = _penumbraFswCts.CancelRecreate();
|
||||||
var token = _penumbraFswCts.Token;
|
var token = _penumbraFswCts.Token;
|
||||||
|
Dictionary<string, WatcherChange> changes;
|
||||||
|
lock (_watcherChanges)
|
||||||
|
changes = _watcherChanges.ToDictionary(t => t.Key, t => t.Value, StringComparer.Ordinal);
|
||||||
|
var delay = TimeSpan.FromSeconds(10);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await Task.Delay(TimeSpan.FromSeconds(10), token).ConfigureAwait(false);
|
do
|
||||||
while (IsHalted() && !token.IsCancellationRequested)
|
{
|
||||||
await Task.Delay(250, token).ConfigureAwait(false);
|
await Task.Delay(delay, token).ConfigureAwait(false);
|
||||||
|
} while (HaltScanLocks.Any(f => f.Value > 0));
|
||||||
|
}
|
||||||
|
catch (TaskCanceledException)
|
||||||
|
{
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
catch (TaskCanceledException) { return; }
|
|
||||||
|
|
||||||
var changes = DrainPenumbraChanges();
|
lock (_watcherChanges)
|
||||||
if (changes.Count > 0)
|
{
|
||||||
_ = HandleChangesAsync(changes, token);
|
foreach (var key in changes.Keys)
|
||||||
|
{
|
||||||
|
_watcherChanges.Remove(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HandleChanges(changes);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void InvokeScan()
|
public void InvokeScan()
|
||||||
{
|
{
|
||||||
TotalFiles = 0;
|
TotalFiles = 0;
|
||||||
TotalFilesStorage = 0;
|
_currentFileProgress = 0;
|
||||||
Interlocked.Exchange(ref _currentFileProgress, 0);
|
|
||||||
|
|
||||||
_scanCancellationTokenSource = _scanCancellationTokenSource?.CancelRecreate() ?? new CancellationTokenSource();
|
_scanCancellationTokenSource = _scanCancellationTokenSource?.CancelRecreate() ?? new CancellationTokenSource();
|
||||||
var token = _scanCancellationTokenSource.Token;
|
var token = _scanCancellationTokenSource.Token;
|
||||||
|
|
||||||
_ = Task.Run(async () =>
|
_ = Task.Run(async () =>
|
||||||
{
|
|
||||||
TaskCompletionSource scanTcs = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
Logger.LogDebug("Starting Full File Scan");
|
Logger.LogDebug("Starting Full File Scan");
|
||||||
|
TotalFiles = 0;
|
||||||
while (IsHalted() && !token.IsCancellationRequested)
|
_currentFileProgress = 0;
|
||||||
|
while (_dalamudUtil.IsOnFrameworkThread)
|
||||||
{
|
{
|
||||||
Logger.LogDebug("Scan is halted, waiting...");
|
Logger.LogWarning("Scanner is on framework, waiting for leaving thread before continuing");
|
||||||
await Task.Delay(250, token).ConfigureAwait(false);
|
await Task.Delay(250, token).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
var scanThread = new Thread(() =>
|
Thread scanThread = new(() =>
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
token.ThrowIfCancellationRequested();
|
_performanceCollector.LogPerformance(this, $"FullFileScan", () => FullFileScan(token));
|
||||||
|
|
||||||
_performanceCollector.LogPerformance(this, $"FullFileScan",
|
|
||||||
() => FullFileScan(token));
|
|
||||||
|
|
||||||
scanTcs.TrySetResult();
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException)
|
|
||||||
{
|
|
||||||
scanTcs.TrySetCanceled(token);
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Logger.LogError(ex, "Error during Full File Scan");
|
Logger.LogError(ex, "Error during Full File Scan");
|
||||||
scanTcs.TrySetException(ex);
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
{
|
{
|
||||||
Priority = ThreadPriority.Lowest,
|
Priority = ThreadPriority.Lowest,
|
||||||
IsBackground = true,
|
IsBackground = true
|
||||||
Name = "LightlessSync.FullFileScan"
|
|
||||||
};
|
};
|
||||||
|
|
||||||
scanThread.Start();
|
scanThread.Start();
|
||||||
|
while (scanThread.IsAlive)
|
||||||
using var _ = token.Register(() => scanTcs.TrySetCanceled(token));
|
|
||||||
|
|
||||||
await scanTcs.Task.ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
catch (TaskCanceledException)
|
|
||||||
{
|
{
|
||||||
Logger.LogInformation("Full File Scan was canceled.");
|
await Task.Delay(250, token).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Logger.LogError(ex, "Unexpected error in InvokeScan task");
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
TotalFiles = 0;
|
TotalFiles = 0;
|
||||||
TotalFilesStorage = 0;
|
_currentFileProgress = 0;
|
||||||
Interlocked.Exchange(ref _currentFileProgress, 0);
|
|
||||||
}
|
|
||||||
}, token);
|
}, token);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RecalculateFileCacheSize(CancellationToken token)
|
public void RecalculateFileCacheSize(CancellationToken token)
|
||||||
{
|
{
|
||||||
if (IsHalted()) return;
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(_configService.Current.CacheFolder) ||
|
if (string.IsNullOrEmpty(_configService.Current.CacheFolder) ||
|
||||||
!Directory.Exists(_configService.Current.CacheFolder))
|
!Directory.Exists(_configService.Current.CacheFolder))
|
||||||
{
|
{
|
||||||
@@ -634,20 +594,10 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
public void ResumeScan(string source)
|
public void ResumeScan(string source)
|
||||||
{
|
{
|
||||||
int delta = 0;
|
if (!HaltScanLocks.ContainsKey(source)) HaltScanLocks[source] = 0;
|
||||||
|
|
||||||
HaltScanLocks.AddOrUpdate(source,
|
HaltScanLocks[source]--;
|
||||||
addValueFactory: _ => 0,
|
if (HaltScanLocks[source] < 0) HaltScanLocks[source] = 0;
|
||||||
updateValueFactory: (_, v) =>
|
|
||||||
{
|
|
||||||
ArgumentException.ThrowIfNullOrEmpty(_);
|
|
||||||
if (v <= 0) return 0;
|
|
||||||
delta = 1;
|
|
||||||
return v - 1;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (delta == 1)
|
|
||||||
Interlocked.Decrement(ref _haltCount);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Dispose(bool disposing)
|
protected override void Dispose(bool disposing)
|
||||||
@@ -671,81 +621,97 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
|||||||
private void FullFileScan(CancellationToken ct)
|
private void FullFileScan(CancellationToken ct)
|
||||||
{
|
{
|
||||||
TotalFiles = 1;
|
TotalFiles = 1;
|
||||||
_currentFileProgress = 0;
|
|
||||||
|
|
||||||
var penumbraDir = _ipcManager.Penumbra.ModDirectory;
|
var penumbraDir = _ipcManager.Penumbra.ModDirectory;
|
||||||
var cacheFolder = _configService.Current.CacheFolder;
|
bool penDirExists = true;
|
||||||
|
bool cacheDirExists = true;
|
||||||
if (string.IsNullOrEmpty(penumbraDir) || !Directory.Exists(penumbraDir))
|
if (string.IsNullOrEmpty(penumbraDir) || !Directory.Exists(penumbraDir))
|
||||||
{
|
{
|
||||||
|
penDirExists = false;
|
||||||
Logger.LogWarning("Penumbra directory is not set or does not exist.");
|
Logger.LogWarning("Penumbra directory is not set or does not exist.");
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
if (string.IsNullOrEmpty(_configService.Current.CacheFolder) || !Directory.Exists(_configService.Current.CacheFolder))
|
||||||
if (string.IsNullOrEmpty(cacheFolder) || !Directory.Exists(cacheFolder))
|
|
||||||
{
|
{
|
||||||
|
cacheDirExists = false;
|
||||||
Logger.LogWarning("Lightless Cache directory is not set or does not exist.");
|
Logger.LogWarning("Lightless Cache directory is not set or does not exist.");
|
||||||
|
}
|
||||||
|
if (!penDirExists || !cacheDirExists)
|
||||||
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var prevPriority = Thread.CurrentThread.Priority;
|
var previousThreadPriority = Thread.CurrentThread.Priority;
|
||||||
Thread.CurrentThread.Priority = ThreadPriority.Lowest;
|
Thread.CurrentThread.Priority = ThreadPriority.Lowest;
|
||||||
|
Logger.LogDebug("Getting files from {penumbra} and {storage}", penumbraDir, _configService.Current.CacheFolder);
|
||||||
|
|
||||||
|
Dictionary<string, string[]> penumbraFiles = new(StringComparer.Ordinal);
|
||||||
|
foreach (var folder in Directory.EnumerateDirectories(penumbraDir!))
|
||||||
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Logger.LogDebug("Getting files from {penumbra} and {storage}", penumbraDir, cacheFolder);
|
penumbraFiles[folder] =
|
||||||
|
[
|
||||||
var onDiskPaths = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
.. Directory.GetFiles(folder, "*.*", SearchOption.AllDirectories)
|
||||||
|
.AsParallel()
|
||||||
static bool IsExcludedPenumbraPath(string path)
|
.Where(f => HasAllowedExtension(f)
|
||||||
=> path.Contains(@"\bg\", StringComparison.OrdinalIgnoreCase)
|
&& !f.Contains(@"\bg\", StringComparison.OrdinalIgnoreCase)
|
||||||
|| path.Contains(@"\bgcommon\", StringComparison.OrdinalIgnoreCase)
|
&& !f.Contains(@"\bgcommon\", StringComparison.OrdinalIgnoreCase)
|
||||||
|| path.Contains(@"\ui\", StringComparison.OrdinalIgnoreCase);
|
&& !f.Contains(@"\ui\", StringComparison.OrdinalIgnoreCase)),
|
||||||
|
];
|
||||||
foreach (var folder in Directory.EnumerateDirectories(penumbraDir))
|
|
||||||
{
|
|
||||||
ct.ThrowIfCancellationRequested();
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
foreach (var file in Directory.EnumerateFiles(folder, "*.*", SearchOption.AllDirectories))
|
|
||||||
{
|
|
||||||
ct.ThrowIfCancellationRequested();
|
|
||||||
|
|
||||||
if (!HasAllowedExtension(file)) continue;
|
|
||||||
if (IsExcludedPenumbraPath(file)) continue;
|
|
||||||
|
|
||||||
onDiskPaths.Add(file);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Logger.LogWarning(ex, "Could not enumerate path {path}", folder);
|
Logger.LogWarning(ex, "Could not enumerate path {path}", folder);
|
||||||
}
|
}
|
||||||
|
Thread.Sleep(50);
|
||||||
|
if (ct.IsCancellationRequested) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var file in Directory.EnumerateFiles(cacheFolder, "*.*", SearchOption.TopDirectoryOnly))
|
var allCacheFiles = Directory.GetFiles(_configService.Current.CacheFolder, "*.*", SearchOption.TopDirectoryOnly)
|
||||||
|
.AsParallel()
|
||||||
|
.Where(f =>
|
||||||
{
|
{
|
||||||
ct.ThrowIfCancellationRequested();
|
var val = f.Split('\\')[^1];
|
||||||
|
return val.Length == 40 || (val.Split('.').FirstOrDefault()?.Length ?? 0) == 40;
|
||||||
|
});
|
||||||
|
|
||||||
var name = Path.GetFileName(file);
|
if (ct.IsCancellationRequested) return;
|
||||||
var stem = Path.GetFileNameWithoutExtension(file);
|
|
||||||
|
|
||||||
if (name.Length == 40 || stem.Length == 40)
|
var allScannedFiles = (penumbraFiles.SelectMany(k => k.Value))
|
||||||
onDiskPaths.Add(file);
|
.Concat(allCacheFiles)
|
||||||
}
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToDictionary(t => t.ToLowerInvariant(), t => false, StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
TotalFiles = allScannedFiles.Count;
|
||||||
|
Thread.CurrentThread.Priority = previousThreadPriority;
|
||||||
|
|
||||||
|
Thread.Sleep(TimeSpan.FromSeconds(2));
|
||||||
|
|
||||||
|
if (ct.IsCancellationRequested) return;
|
||||||
|
|
||||||
|
// scan files from database
|
||||||
var threadCount = Math.Clamp((int)(Environment.ProcessorCount / 2.0f), 2, 8);
|
var threadCount = Math.Clamp((int)(Environment.ProcessorCount / 2.0f), 2, 8);
|
||||||
|
|
||||||
var fileCacheList = _fileDbManager.GetAllFileCaches();
|
List<FileCacheEntity> entitiesToRemove = [];
|
||||||
var fileCaches = new ConcurrentQueue<FileCacheEntity>(fileCacheList);
|
List<FileCacheEntity> entitiesToUpdate = [];
|
||||||
|
Lock sync = new();
|
||||||
|
Thread[] workerThreads = new Thread[threadCount];
|
||||||
|
|
||||||
|
ConcurrentQueue<FileCacheEntity> fileCaches = new(_fileDbManager.GetAllFileCaches());
|
||||||
|
|
||||||
TotalFilesStorage = fileCaches.Count;
|
TotalFilesStorage = fileCaches.Count;
|
||||||
TotalFiles = onDiskPaths.Count + TotalFilesStorage;
|
|
||||||
|
|
||||||
var validOrPresentInDb = new ConcurrentDictionary<string, byte>(StringComparer.OrdinalIgnoreCase);
|
for (int i = 0; i < threadCount; i++)
|
||||||
var entitiesToUpdate = new ConcurrentBag<FileCacheEntity>();
|
{
|
||||||
var entitiesToRemove = new ConcurrentBag<FileCacheEntity>();
|
Logger.LogTrace("Creating Thread {i}", i);
|
||||||
|
workerThreads[i] = new((tcounter) =>
|
||||||
|
{
|
||||||
|
var threadNr = (int)tcounter!;
|
||||||
|
Logger.LogTrace("Spawning Worker Thread {i}", threadNr);
|
||||||
|
while (!ct.IsCancellationRequested && fileCaches.TryDequeue(out var workload))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (ct.IsCancellationRequested) return;
|
||||||
|
|
||||||
if (!_ipcManager.Penumbra.APIAvailable)
|
if (!_ipcManager.Penumbra.APIAvailable)
|
||||||
{
|
{
|
||||||
@@ -753,52 +719,34 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Thread[] workerThreads = new Thread[threadCount];
|
var validatedCacheResult = _fileDbManager.ValidateFileCacheEntity(workload);
|
||||||
for (int i = 0; i < threadCount; i++)
|
if (validatedCacheResult.State != FileState.RequireDeletion)
|
||||||
{
|
{
|
||||||
workerThreads[i] = new Thread(tcounter =>
|
lock (sync) { allScannedFiles[validatedCacheResult.FileCache.ResolvedFilepath] = true; }
|
||||||
{
|
|
||||||
var threadNr = (int)tcounter!;
|
|
||||||
Logger.LogTrace("Spawning Worker Thread {i}", threadNr);
|
|
||||||
|
|
||||||
while (!ct.IsCancellationRequested && fileCaches.TryDequeue(out var workload))
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (ct.IsCancellationRequested) break;
|
|
||||||
|
|
||||||
if (!_ipcManager.Penumbra.APIAvailable)
|
|
||||||
break;
|
|
||||||
|
|
||||||
var validated = _fileDbManager.ValidateFileCacheEntity(workload);
|
|
||||||
|
|
||||||
if (validated.State != FileState.RequireDeletion)
|
|
||||||
{
|
|
||||||
validOrPresentInDb.TryAdd(validated.FileCache.ResolvedFilepath, 0);
|
|
||||||
}
|
}
|
||||||
|
if (validatedCacheResult.State == FileState.RequireUpdate)
|
||||||
if (validated.State == FileState.RequireUpdate)
|
|
||||||
{
|
{
|
||||||
Logger.LogTrace("To update: {path}", validated.FileCache.ResolvedFilepath);
|
Logger.LogTrace("To update: {path}", validatedCacheResult.FileCache.ResolvedFilepath);
|
||||||
entitiesToUpdate.Add(validated.FileCache);
|
lock (sync) { entitiesToUpdate.Add(validatedCacheResult.FileCache); }
|
||||||
}
|
}
|
||||||
else if (validated.State == FileState.RequireDeletion)
|
else if (validatedCacheResult.State == FileState.RequireDeletion)
|
||||||
{
|
{
|
||||||
Logger.LogTrace("To delete: {path}", validated.FileCache.ResolvedFilepath);
|
Logger.LogTrace("To delete: {path}", validatedCacheResult.FileCache.ResolvedFilepath);
|
||||||
entitiesToRemove.Add(validated.FileCache);
|
lock (sync) { entitiesToRemove.Add(validatedCacheResult.FileCache); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
if (workload != null)
|
if (workload != null)
|
||||||
|
{
|
||||||
Logger.LogWarning(ex, "Failed validating {path}", workload.ResolvedFilepath);
|
Logger.LogWarning(ex, "Failed validating {path}", workload.ResolvedFilepath);
|
||||||
|
}
|
||||||
else
|
else
|
||||||
|
{
|
||||||
Logger.LogWarning(ex, "Failed validating unknown workload");
|
Logger.LogWarning(ex, "Failed validating unknown workload");
|
||||||
}
|
}
|
||||||
finally
|
|
||||||
{
|
|
||||||
Interlocked.Increment(ref _currentFileProgress);
|
|
||||||
}
|
}
|
||||||
|
Interlocked.Increment(ref _currentFileProgress);
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.LogTrace("Ending Worker Thread {i}", threadNr);
|
Logger.LogTrace("Ending Worker Thread {i}", threadNr);
|
||||||
@@ -807,17 +755,39 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
|||||||
Priority = ThreadPriority.Lowest,
|
Priority = ThreadPriority.Lowest,
|
||||||
IsBackground = true
|
IsBackground = true
|
||||||
};
|
};
|
||||||
|
|
||||||
workerThreads[i].Start(i);
|
workerThreads[i].Start(i);
|
||||||
}
|
}
|
||||||
|
|
||||||
while (!ct.IsCancellationRequested && workerThreads.Any(t => t.IsAlive))
|
while (!ct.IsCancellationRequested && workerThreads.Any(u => u.IsAlive))
|
||||||
{
|
{
|
||||||
ct.WaitHandle.WaitOne(250);
|
Thread.Sleep(1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ct.IsCancellationRequested) return;
|
if (ct.IsCancellationRequested) return;
|
||||||
|
|
||||||
|
Logger.LogTrace("Threads exited");
|
||||||
|
|
||||||
|
if (!_ipcManager.Penumbra.APIAvailable)
|
||||||
|
{
|
||||||
|
Logger.LogWarning("Penumbra not available");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entitiesToUpdate.Count != 0 || entitiesToRemove.Count != 0)
|
||||||
|
{
|
||||||
|
foreach (var entity in entitiesToUpdate)
|
||||||
|
{
|
||||||
|
_fileDbManager.UpdateHashedFile(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var entity in entitiesToRemove)
|
||||||
|
{
|
||||||
|
_fileDbManager.RemoveHashedFile(entity.Hash, entity.PrefixedFilePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
_fileDbManager.WriteOutFullCsv();
|
||||||
|
}
|
||||||
|
|
||||||
Logger.LogTrace("Scanner validated existing db files");
|
Logger.LogTrace("Scanner validated existing db files");
|
||||||
|
|
||||||
if (!_ipcManager.Penumbra.APIAvailable)
|
if (!_ipcManager.Penumbra.APIAvailable)
|
||||||
@@ -826,40 +796,26 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var didMutateDb = false;
|
|
||||||
|
|
||||||
foreach (var entity in entitiesToUpdate)
|
|
||||||
{
|
|
||||||
didMutateDb = true;
|
|
||||||
_fileDbManager.UpdateHashedFile(entity);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var entity in entitiesToRemove)
|
|
||||||
{
|
|
||||||
didMutateDb = true;
|
|
||||||
_fileDbManager.RemoveHashedFile(entity.Hash, entity.PrefixedFilePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (didMutateDb)
|
|
||||||
_fileDbManager.WriteOutFullCsv();
|
|
||||||
|
|
||||||
if (ct.IsCancellationRequested) return;
|
if (ct.IsCancellationRequested) return;
|
||||||
|
|
||||||
var newFiles = onDiskPaths.Where(p => !validOrPresentInDb.ContainsKey(p)).ToList();
|
var newFiles = allScannedFiles.Where(c => !c.Value).Select(c => c.Key).ToList();
|
||||||
|
foreach (var cachePath in newFiles)
|
||||||
foreach (var path in newFiles)
|
|
||||||
{
|
{
|
||||||
if (ct.IsCancellationRequested) break;
|
if (ct.IsCancellationRequested) break;
|
||||||
ProcessOne(path);
|
ProcessOne(cachePath);
|
||||||
Interlocked.Increment(ref _currentFileProgress);
|
Interlocked.Increment(ref _currentFileProgress);
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.LogTrace("Scanner added {count} new files to db", newFiles.Count);
|
Logger.LogTrace("Scanner added {count} new files to db", newFiles.Count);
|
||||||
|
|
||||||
void ProcessOne(string? filePath)
|
void ProcessOne(string? cachePath)
|
||||||
{
|
{
|
||||||
if (filePath == null)
|
if (_fileDbManager == null || _ipcManager?.Penumbra == null || cachePath == null)
|
||||||
|
{
|
||||||
|
Logger.LogTrace("Potential null in db: {isDbNull} penumbra: {isPenumbraNull} cachepath: {isPathNull}",
|
||||||
|
_fileDbManager == null, _ipcManager?.Penumbra == null, cachePath == null);
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!_ipcManager.Penumbra.APIAvailable)
|
if (!_ipcManager.Penumbra.APIAvailable)
|
||||||
{
|
{
|
||||||
@@ -869,45 +825,31 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var entry = _fileDbManager.CreateFileEntry(filePath);
|
var entry = _fileDbManager.CreateFileEntry(cachePath);
|
||||||
if (entry == null)
|
if (entry == null) _ = _fileDbManager.CreateCacheEntry(cachePath);
|
||||||
_ = _fileDbManager.CreateCacheEntry(filePath);
|
|
||||||
}
|
}
|
||||||
catch (IOException ioex)
|
catch (IOException ioex)
|
||||||
{
|
{
|
||||||
Logger.LogDebug(ioex, "File busy or locked: {file}", filePath);
|
Logger.LogDebug(ioex, "File busy or locked: {file}", cachePath);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Logger.LogWarning(ex, "Failed adding {file}", filePath);
|
Logger.LogWarning(ex, "Failed adding {file}", cachePath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.LogDebug("Scan complete");
|
Logger.LogDebug("Scan complete");
|
||||||
|
|
||||||
TotalFiles = 0;
|
TotalFiles = 0;
|
||||||
_currentFileProgress = 0;
|
_currentFileProgress = 0;
|
||||||
|
entitiesToRemove.Clear();
|
||||||
|
allScannedFiles.Clear();
|
||||||
|
|
||||||
if (!_configService.Current.InitialScanComplete)
|
if (!_configService.Current.InitialScanComplete)
|
||||||
{
|
{
|
||||||
_configService.Current.InitialScanComplete = true;
|
_configService.Current.InitialScanComplete = true;
|
||||||
_configService.Save();
|
_configService.Save();
|
||||||
|
StartLightlessWatcher(_configService.Current.CacheFolder);
|
||||||
StartLightlessWatcher(cacheFolder);
|
|
||||||
StartPenumbraWatcher(penumbraDir);
|
StartPenumbraWatcher(penumbraDir);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
|
||||||
{
|
|
||||||
// normal cancellation
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Logger.LogError(ex, "Error during Full File Scan");
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
Thread.CurrentThread.Priority = prevPriority;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
241
LightlessSync/FileCache/ExternalCompactionExecutor.cs
Normal file
241
LightlessSync/FileCache/ExternalCompactionExecutor.cs
Normal file
@@ -0,0 +1,241 @@
|
|||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO.Pipes;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace LightlessSync.FileCache;
|
||||||
|
|
||||||
|
internal sealed class ExternalCompactionExecutor : ICompactionExecutor, IDisposable
|
||||||
|
{
|
||||||
|
private readonly ILogger<ExternalCompactionExecutor> _logger;
|
||||||
|
private readonly ICompactorContext _context;
|
||||||
|
private readonly TimeSpan _timeout = TimeSpan.FromMinutes(5);
|
||||||
|
private readonly string _pipeName;
|
||||||
|
private Process? _workerProcess;
|
||||||
|
private bool _disposed;
|
||||||
|
private readonly object _sync = new();
|
||||||
|
|
||||||
|
public ExternalCompactionExecutor(ILogger<ExternalCompactionExecutor> logger, ICompactorContext context)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_context = context;
|
||||||
|
_pipeName = $"LightlessCompactor-{Environment.ProcessId}";
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryCompact(string filePath)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(filePath) || !File.Exists(filePath))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (!EnsureWorkerRunning())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var request = new CompactorRequest
|
||||||
|
{
|
||||||
|
Type = "compact",
|
||||||
|
Path = filePath
|
||||||
|
};
|
||||||
|
|
||||||
|
return SendRequest(request, out var response) && response?.Success == true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "External compactor failed for {file}", filePath);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_disposed)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_disposed = true;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
SendRequest(new CompactorRequest { Type = "shutdown" }, out _);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_sync)
|
||||||
|
{
|
||||||
|
if (_workerProcess is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
TryKill(_workerProcess);
|
||||||
|
_workerProcess.Dispose();
|
||||||
|
_workerProcess = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool EnsureWorkerRunning()
|
||||||
|
{
|
||||||
|
lock (_sync)
|
||||||
|
{
|
||||||
|
if (_workerProcess is { HasExited: false })
|
||||||
|
return true;
|
||||||
|
|
||||||
|
_workerProcess?.Dispose();
|
||||||
|
_workerProcess = null;
|
||||||
|
|
||||||
|
var workerPath = ResolveWorkerPath();
|
||||||
|
if (string.IsNullOrEmpty(workerPath))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var args = BuildArguments();
|
||||||
|
var startInfo = new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = workerPath,
|
||||||
|
Arguments = args,
|
||||||
|
CreateNoWindow = true,
|
||||||
|
UseShellExecute = false,
|
||||||
|
RedirectStandardOutput = true,
|
||||||
|
RedirectStandardError = true
|
||||||
|
};
|
||||||
|
|
||||||
|
var process = new Process { StartInfo = startInfo };
|
||||||
|
if (!process.Start())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
TrySetLowPriority(process);
|
||||||
|
_ = DrainAsync(process.StandardOutput, "stdout");
|
||||||
|
_ = DrainAsync(process.StandardError, "stderr");
|
||||||
|
|
||||||
|
_workerProcess = process;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool SendRequest(CompactorRequest request, out CompactorResponse? response)
|
||||||
|
{
|
||||||
|
response = null;
|
||||||
|
using var pipe = new NamedPipeClientStream(".", _pipeName, PipeDirection.InOut, PipeOptions.Asynchronous);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
pipe.Connect((int)_timeout.TotalMilliseconds);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogDebug(ex, "Compactor pipe connection failed.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
using var writer = new StreamWriter(pipe) { AutoFlush = true };
|
||||||
|
using var reader = new StreamReader(pipe);
|
||||||
|
|
||||||
|
var payload = JsonSerializer.Serialize(request);
|
||||||
|
writer.WriteLine(payload);
|
||||||
|
|
||||||
|
var readTask = reader.ReadLineAsync();
|
||||||
|
if (!readTask.Wait(_timeout))
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Compactor pipe timed out waiting for response.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var line = readTask.Result;
|
||||||
|
if (string.IsNullOrWhiteSpace(line))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
response = JsonSerializer.Deserialize<CompactorResponse>(line);
|
||||||
|
return response is not null;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogDebug(ex, "Failed to parse compactor response.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string? ResolveWorkerPath()
|
||||||
|
{
|
||||||
|
var baseDir = AppContext.BaseDirectory;
|
||||||
|
var exeName = OperatingSystem.IsWindows() || _context.IsWine
|
||||||
|
? "LightlessCompactorWorker.exe"
|
||||||
|
: "LightlessCompactorWorker";
|
||||||
|
var path = Path.Combine(baseDir, exeName);
|
||||||
|
return File.Exists(path) ? path : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string BuildArguments()
|
||||||
|
{
|
||||||
|
var args = new List<string> { "--pipe", Quote(_pipeName), "--parent", Environment.ProcessId.ToString() };
|
||||||
|
if (_context.IsWine)
|
||||||
|
args.Add("--wine");
|
||||||
|
return string.Join(' ', args);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Quote(string value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(value))
|
||||||
|
return "\"\"";
|
||||||
|
|
||||||
|
if (!value.Contains('"', StringComparison.Ordinal))
|
||||||
|
return "\"" + value + "\"";
|
||||||
|
|
||||||
|
return "\"" + value.Replace("\"", "\\\"", StringComparison.Ordinal) + "\"";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void TrySetLowPriority(Process process)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (OperatingSystem.IsWindows())
|
||||||
|
process.PriorityClass = ProcessPriorityClass.BelowNormal;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task DrainAsync(StreamReader reader, string label)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string? line;
|
||||||
|
while ((line = await reader.ReadLineAsync().ConfigureAwait(false)) != null)
|
||||||
|
{
|
||||||
|
if (_logger.IsEnabled(LogLevel.Trace))
|
||||||
|
_logger.LogTrace("Compactor {label}: {line}", label, line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void TryKill(Process process)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
process.Kill(entireProcessTree: true);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class CompactorRequest
|
||||||
|
{
|
||||||
|
public string Type { get; init; } = "compact";
|
||||||
|
public string? Path { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class CompactorResponse
|
||||||
|
{
|
||||||
|
public bool Success { get; init; }
|
||||||
|
public string? Error { get; init; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -115,6 +115,35 @@ public sealed class FileCacheManager : IHostedService
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool TryGetHashFromFileName(FileInfo fileInfo, out string hash)
|
||||||
|
{
|
||||||
|
hash = Path.GetFileNameWithoutExtension(fileInfo.Name);
|
||||||
|
if (string.IsNullOrWhiteSpace(hash))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hash.Length is not (40 or 64))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < hash.Length; i++)
|
||||||
|
{
|
||||||
|
var c = hash[i];
|
||||||
|
var isHex = (c >= '0' && c <= '9')
|
||||||
|
|| (c >= 'a' && c <= 'f')
|
||||||
|
|| (c >= 'A' && c <= 'F');
|
||||||
|
if (!isHex)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hash = hash.ToUpperInvariant();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private static string BuildVersionHeader() => $"{FileCacheVersionHeaderPrefix}{FileCacheVersion}";
|
private static string BuildVersionHeader() => $"{FileCacheVersionHeaderPrefix}{FileCacheVersion}";
|
||||||
|
|
||||||
private static bool TryParseVersionHeader(string? line, out int version)
|
private static bool TryParseVersionHeader(string? line, out int version)
|
||||||
@@ -288,6 +317,11 @@ public sealed class FileCacheManager : IHostedService
|
|||||||
_logger.LogTrace("Creating cache entry for {path}", path);
|
_logger.LogTrace("Creating cache entry for {path}", path);
|
||||||
var cacheFolder = _configService.Current.CacheFolder;
|
var cacheFolder = _configService.Current.CacheFolder;
|
||||||
if (string.IsNullOrEmpty(cacheFolder)) return null;
|
if (string.IsNullOrEmpty(cacheFolder)) return null;
|
||||||
|
if (TryGetHashFromFileName(fi, out var hash))
|
||||||
|
{
|
||||||
|
return CreateCacheEntryWithKnownHash(fi.FullName, hash);
|
||||||
|
}
|
||||||
|
|
||||||
return CreateFileEntity(cacheFolder, CachePrefix, fi);
|
return CreateFileEntity(cacheFolder, CachePrefix, fi);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
20
LightlessSync/FileCache/PluginCompactorContext.cs
Normal file
20
LightlessSync/FileCache/PluginCompactorContext.cs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
using LightlessSync.LightlessConfiguration;
|
||||||
|
using LightlessSync.Services;
|
||||||
|
|
||||||
|
namespace LightlessSync.FileCache;
|
||||||
|
|
||||||
|
internal sealed class PluginCompactorContext : ICompactorContext
|
||||||
|
{
|
||||||
|
private readonly LightlessConfigService _configService;
|
||||||
|
private readonly DalamudUtilService _dalamudUtilService;
|
||||||
|
|
||||||
|
public PluginCompactorContext(LightlessConfigService configService, DalamudUtilService dalamudUtilService)
|
||||||
|
{
|
||||||
|
_configService = configService;
|
||||||
|
_dalamudUtilService = dalamudUtilService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool UseCompactor => _configService.Current.UseCompactor;
|
||||||
|
public string CacheFolder => _configService.Current.CacheFolder;
|
||||||
|
public bool IsWine => _dalamudUtilService.IsWine;
|
||||||
|
}
|
||||||
@@ -25,7 +25,6 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
|
|||||||
private readonly object _ownedHandlerLock = new();
|
private readonly object _ownedHandlerLock = new();
|
||||||
private readonly string[] _handledFileTypes = ["tmb", "pap", "avfx", "atex", "sklb", "eid", "phyb", "scd", "skp", "shpk", "kdb"];
|
private readonly string[] _handledFileTypes = ["tmb", "pap", "avfx", "atex", "sklb", "eid", "phyb", "scd", "skp", "shpk", "kdb"];
|
||||||
private readonly string[] _handledRecordingFileTypes = ["tex", "mdl", "mtrl"];
|
private readonly string[] _handledRecordingFileTypes = ["tex", "mdl", "mtrl"];
|
||||||
private readonly string[] _handledFileTypesWithRecording;
|
|
||||||
private readonly HashSet<GameObjectHandler> _playerRelatedPointers = [];
|
private readonly HashSet<GameObjectHandler> _playerRelatedPointers = [];
|
||||||
private readonly object _playerRelatedLock = new();
|
private readonly object _playerRelatedLock = new();
|
||||||
private readonly ConcurrentDictionary<nint, GameObjectHandler> _playerRelatedByAddress = new();
|
private readonly ConcurrentDictionary<nint, GameObjectHandler> _playerRelatedByAddress = new();
|
||||||
@@ -42,8 +41,6 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
|
|||||||
_dalamudUtil = dalamudUtil;
|
_dalamudUtil = dalamudUtil;
|
||||||
_actorObjectService = actorObjectService;
|
_actorObjectService = actorObjectService;
|
||||||
_gameObjectHandlerFactory = gameObjectHandlerFactory;
|
_gameObjectHandlerFactory = gameObjectHandlerFactory;
|
||||||
_handledFileTypesWithRecording = _handledRecordingFileTypes.Concat(_handledFileTypes).ToArray();
|
|
||||||
|
|
||||||
Mediator.Subscribe<PenumbraResourceLoadMessage>(this, Manager_PenumbraResourceLoadEvent);
|
Mediator.Subscribe<PenumbraResourceLoadMessage>(this, Manager_PenumbraResourceLoadEvent);
|
||||||
Mediator.Subscribe<ActorTrackedMessage>(this, msg => HandleActorTracked(msg.Descriptor));
|
Mediator.Subscribe<ActorTrackedMessage>(this, msg => HandleActorTracked(msg.Descriptor));
|
||||||
Mediator.Subscribe<ActorUntrackedMessage>(this, msg => HandleActorUntracked(msg.Descriptor));
|
Mediator.Subscribe<ActorUntrackedMessage>(this, msg => HandleActorUntracked(msg.Descriptor));
|
||||||
@@ -321,7 +318,7 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
foreach (var handler in _playerRelatedPointers)
|
foreach (var handler in _playerRelatedPointers)
|
||||||
{
|
{
|
||||||
var address = handler.Address;
|
var address = (nint)handler.Address;
|
||||||
if (address != nint.Zero)
|
if (address != nint.Zero)
|
||||||
{
|
{
|
||||||
tempMap[address] = handler;
|
tempMap[address] = handler;
|
||||||
@@ -523,46 +520,51 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
private void Manager_PenumbraResourceLoadEvent(PenumbraResourceLoadMessage msg)
|
private void Manager_PenumbraResourceLoadEvent(PenumbraResourceLoadMessage msg)
|
||||||
{
|
{
|
||||||
|
var gamePath = msg.GamePath.ToLowerInvariant();
|
||||||
var gameObjectAddress = msg.GameObject;
|
var gameObjectAddress = msg.GameObject;
|
||||||
if (!_cachedFrameAddresses.TryGetValue(gameObjectAddress, out var objectKind))
|
var filePath = msg.FilePath;
|
||||||
{
|
|
||||||
if (_actorObjectService.TryGetOwnedKind(gameObjectAddress, out var ownedKind))
|
|
||||||
{
|
|
||||||
objectKind = ownedKind;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var gamePath = NormalizeGamePath(msg.GamePath);
|
|
||||||
if (string.IsNullOrEmpty(gamePath))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ignore files already processed this frame
|
// ignore files already processed this frame
|
||||||
|
if (_cachedHandledPaths.Contains(gamePath)) return;
|
||||||
|
|
||||||
lock (_cacheAdditionLock)
|
lock (_cacheAdditionLock)
|
||||||
{
|
{
|
||||||
if (!_cachedHandledPaths.Add(gamePath))
|
_cachedHandledPaths.Add(gamePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// replace individual mtrl stuff
|
||||||
|
if (filePath.StartsWith("|", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
filePath = filePath.Split("|")[2];
|
||||||
|
}
|
||||||
|
// replace filepath
|
||||||
|
filePath = filePath.ToLowerInvariant().Replace("\\", "/", StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
// ignore files that are the same
|
||||||
|
var replacedGamePath = gamePath.ToLowerInvariant().Replace("\\", "/", StringComparison.OrdinalIgnoreCase);
|
||||||
|
if (string.Equals(filePath, replacedGamePath, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// ignore files to not handle
|
// ignore files to not handle
|
||||||
var handledTypes = IsTransientRecording ? _handledFileTypesWithRecording : _handledFileTypes;
|
var handledTypes = IsTransientRecording ? _handledRecordingFileTypes.Concat(_handledFileTypes) : _handledFileTypes;
|
||||||
if (!HasHandledFileType(gamePath, handledTypes))
|
if (!handledTypes.Any(type => gamePath.EndsWith(type, StringComparison.OrdinalIgnoreCase)))
|
||||||
{
|
{
|
||||||
|
lock (_cacheAdditionLock)
|
||||||
|
{
|
||||||
|
_cachedHandledPaths.Add(gamePath);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var filePath = NormalizeFilePath(msg.FilePath);
|
// ignore files not belonging to anything player related
|
||||||
|
if (!_cachedFrameAddresses.TryGetValue(gameObjectAddress, out var objectKind))
|
||||||
// ignore files that are the same
|
|
||||||
if (string.Equals(filePath, gamePath, StringComparison.Ordinal))
|
|
||||||
{
|
{
|
||||||
|
lock (_cacheAdditionLock)
|
||||||
|
{
|
||||||
|
_cachedHandledPaths.Add(gamePath);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -577,12 +579,13 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
|
|||||||
_playerRelatedByAddress.TryGetValue(gameObjectAddress, out var owner);
|
_playerRelatedByAddress.TryGetValue(gameObjectAddress, out var owner);
|
||||||
bool alreadyTransient = false;
|
bool alreadyTransient = false;
|
||||||
|
|
||||||
bool transientContains = transientResources.Contains(gamePath);
|
bool transientContains = transientResources.Contains(replacedGamePath);
|
||||||
bool semiTransientContains = SemiTransientResources.Values.Any(value => value.Contains(gamePath));
|
bool semiTransientContains = SemiTransientResources.SelectMany(k => k.Value)
|
||||||
|
.Any(f => string.Equals(f, gamePath, StringComparison.OrdinalIgnoreCase));
|
||||||
if (transientContains || semiTransientContains)
|
if (transientContains || semiTransientContains)
|
||||||
{
|
{
|
||||||
if (!IsTransientRecording)
|
if (!IsTransientRecording)
|
||||||
Logger.LogTrace("Not adding {replacedPath} => {filePath}, Reason: Transient: {contains}, SemiTransient: {contains2}", gamePath, filePath,
|
Logger.LogTrace("Not adding {replacedPath} => {filePath}, Reason: Transient: {contains}, SemiTransient: {contains2}", replacedGamePath, filePath,
|
||||||
transientContains, semiTransientContains);
|
transientContains, semiTransientContains);
|
||||||
alreadyTransient = true;
|
alreadyTransient = true;
|
||||||
}
|
}
|
||||||
@@ -590,10 +593,10 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
if (!IsTransientRecording)
|
if (!IsTransientRecording)
|
||||||
{
|
{
|
||||||
bool isAdded = transientResources.Add(gamePath);
|
bool isAdded = transientResources.Add(replacedGamePath);
|
||||||
if (isAdded)
|
if (isAdded)
|
||||||
{
|
{
|
||||||
Logger.LogDebug("Adding {replacedGamePath} for {gameObject} ({filePath})", gamePath, owner?.ToString() ?? gameObjectAddress.ToString("X"), filePath);
|
Logger.LogDebug("Adding {replacedGamePath} for {gameObject} ({filePath})", replacedGamePath, owner?.ToString() ?? gameObjectAddress.ToString("X"), filePath);
|
||||||
SendTransients(gameObjectAddress, objectKind);
|
SendTransients(gameObjectAddress, objectKind);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -601,7 +604,7 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
if (owner != null && IsTransientRecording)
|
if (owner != null && IsTransientRecording)
|
||||||
{
|
{
|
||||||
_recordedTransients.Add(new TransientRecord(owner, gamePath, filePath, alreadyTransient) { AddTransient = !alreadyTransient });
|
_recordedTransients.Add(new TransientRecord(owner, replacedGamePath, filePath, alreadyTransient) { AddTransient = !alreadyTransient });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
}
|
||||||
@@ -4,7 +4,6 @@ using LightlessSync.Interop.Ipc.Penumbra;
|
|||||||
using LightlessSync.LightlessConfiguration.Models;
|
using LightlessSync.LightlessConfiguration.Models;
|
||||||
using LightlessSync.PlayerData.Handlers;
|
using LightlessSync.PlayerData.Handlers;
|
||||||
using LightlessSync.Services;
|
using LightlessSync.Services;
|
||||||
using LightlessSync.Services.ActorTracking;
|
|
||||||
using LightlessSync.Services.Mediator;
|
using LightlessSync.Services.Mediator;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Penumbra.Api.Enums;
|
using Penumbra.Api.Enums;
|
||||||
@@ -36,8 +35,7 @@ public sealed class IpcCallerPenumbra : IpcServiceBase
|
|||||||
IDalamudPluginInterface pluginInterface,
|
IDalamudPluginInterface pluginInterface,
|
||||||
DalamudUtilService dalamudUtil,
|
DalamudUtilService dalamudUtil,
|
||||||
LightlessMediator mediator,
|
LightlessMediator mediator,
|
||||||
RedrawManager redrawManager,
|
RedrawManager redrawManager) : base(logger, mediator, pluginInterface, PenumbraDescriptor)
|
||||||
ActorObjectService actorObjectService) : base(logger, mediator, pluginInterface, PenumbraDescriptor)
|
|
||||||
{
|
{
|
||||||
_penumbraEnabled = new GetEnabledState(pluginInterface);
|
_penumbraEnabled = new GetEnabledState(pluginInterface);
|
||||||
_penumbraGetModDirectory = new GetModDirectory(pluginInterface);
|
_penumbraGetModDirectory = new GetModDirectory(pluginInterface);
|
||||||
@@ -46,7 +44,7 @@ public sealed class IpcCallerPenumbra : IpcServiceBase
|
|||||||
_penumbraModSettingChanged = ModSettingChanged.Subscriber(pluginInterface, HandlePenumbraModSettingChanged);
|
_penumbraModSettingChanged = ModSettingChanged.Subscriber(pluginInterface, HandlePenumbraModSettingChanged);
|
||||||
|
|
||||||
_collections = RegisterInterop(new PenumbraCollections(logger, pluginInterface, dalamudUtil, mediator));
|
_collections = RegisterInterop(new PenumbraCollections(logger, pluginInterface, dalamudUtil, mediator));
|
||||||
_resources = RegisterInterop(new PenumbraResource(logger, pluginInterface, dalamudUtil, mediator, actorObjectService));
|
_resources = RegisterInterop(new PenumbraResource(logger, pluginInterface, dalamudUtil, mediator));
|
||||||
_redraw = RegisterInterop(new PenumbraRedraw(logger, pluginInterface, dalamudUtil, mediator, redrawManager));
|
_redraw = RegisterInterop(new PenumbraRedraw(logger, pluginInterface, dalamudUtil, mediator, redrawManager));
|
||||||
_textures = RegisterInterop(new PenumbraTexture(logger, pluginInterface, dalamudUtil, mediator, _redraw));
|
_textures = RegisterInterop(new PenumbraTexture(logger, pluginInterface, dalamudUtil, mediator, _redraw));
|
||||||
|
|
||||||
@@ -81,10 +79,7 @@ public sealed class IpcCallerPenumbra : IpcServiceBase
|
|||||||
=> _collections.RemoveTemporaryCollectionAsync(logger, applicationId, collectionId);
|
=> _collections.RemoveTemporaryCollectionAsync(logger, applicationId, collectionId);
|
||||||
|
|
||||||
public Task SetTemporaryModsAsync(ILogger logger, Guid applicationId, Guid collectionId, Dictionary<string, string> modPaths)
|
public Task SetTemporaryModsAsync(ILogger logger, Guid applicationId, Guid collectionId, Dictionary<string, string> modPaths)
|
||||||
=> _collections.SetTemporaryModsAsync(logger, applicationId, collectionId, modPaths, "Player");
|
=> _collections.SetTemporaryModsAsync(logger, applicationId, collectionId, modPaths);
|
||||||
|
|
||||||
public Task SetTemporaryModsAsync(ILogger logger, Guid applicationId, Guid collectionId, Dictionary<string, string> modPaths, string scope)
|
|
||||||
=> _collections.SetTemporaryModsAsync(logger, applicationId, collectionId, modPaths, scope);
|
|
||||||
|
|
||||||
public Task SetManipulationDataAsync(ILogger logger, Guid applicationId, Guid collectionId, string manipulationData)
|
public Task SetManipulationDataAsync(ILogger logger, Guid applicationId, Guid collectionId, string manipulationData)
|
||||||
=> _collections.SetManipulationDataAsync(logger, applicationId, collectionId, manipulationData);
|
=> _collections.SetManipulationDataAsync(logger, applicationId, collectionId, manipulationData);
|
||||||
@@ -107,8 +102,11 @@ public sealed class IpcCallerPenumbra : IpcServiceBase
|
|||||||
public Task RedrawAsync(ILogger logger, GameObjectHandler handler, Guid applicationId, CancellationToken token)
|
public Task RedrawAsync(ILogger logger, GameObjectHandler handler, Guid applicationId, CancellationToken token)
|
||||||
=> _redraw.RedrawAsync(logger, handler, applicationId, token);
|
=> _redraw.RedrawAsync(logger, handler, applicationId, token);
|
||||||
|
|
||||||
public Task ConvertTextureFiles(ILogger logger, IReadOnlyList<TextureConversionJob> jobs, IProgress<TextureConversionProgress>? progress, CancellationToken token)
|
public void RequestImmediateRedraw(int objectIndex, RedrawType redrawType)
|
||||||
=> _textures.ConvertTextureFilesAsync(logger, jobs, progress, token);
|
=> _redraw.RequestImmediateRedraw(objectIndex, redrawType);
|
||||||
|
|
||||||
|
public Task ConvertTextureFiles(ILogger logger, IReadOnlyList<TextureConversionJob> jobs, IProgress<TextureConversionProgress>? progress, CancellationToken token, bool requestRedraw = true)
|
||||||
|
=> _textures.ConvertTextureFilesAsync(logger, jobs, progress, token, requestRedraw);
|
||||||
|
|
||||||
public Task ConvertTextureFileDirectAsync(TextureConversionJob job, CancellationToken token)
|
public Task ConvertTextureFileDirectAsync(TextureConversionJob job, CancellationToken token)
|
||||||
=> _textures.ConvertTextureFileDirectAsync(job, token);
|
=> _textures.ConvertTextureFileDirectAsync(job, token);
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
using System.Collections.Concurrent;
|
|
||||||
using Dalamud.Plugin;
|
using Dalamud.Plugin;
|
||||||
using LightlessSync.Interop.Ipc.Framework;
|
using LightlessSync.Interop.Ipc.Framework;
|
||||||
using LightlessSync.Services;
|
using LightlessSync.Services;
|
||||||
using LightlessSync.Services.Mediator;
|
using LightlessSync.Services.Mediator;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Penumbra.Api.Enums;
|
|
||||||
using Penumbra.Api.IpcSubscribers;
|
using Penumbra.Api.IpcSubscribers;
|
||||||
|
|
||||||
namespace LightlessSync.Interop.Ipc.Penumbra;
|
namespace LightlessSync.Interop.Ipc.Penumbra;
|
||||||
@@ -16,10 +14,6 @@ public sealed class PenumbraCollections : PenumbraBase
|
|||||||
private readonly DeleteTemporaryCollection _removeTemporaryCollection;
|
private readonly DeleteTemporaryCollection _removeTemporaryCollection;
|
||||||
private readonly AddTemporaryMod _addTemporaryMod;
|
private readonly AddTemporaryMod _addTemporaryMod;
|
||||||
private readonly RemoveTemporaryMod _removeTemporaryMod;
|
private readonly RemoveTemporaryMod _removeTemporaryMod;
|
||||||
private readonly GetCollections _getCollections;
|
|
||||||
private readonly ConcurrentDictionary<Guid, string> _activeTemporaryCollections = new();
|
|
||||||
|
|
||||||
private int _cleanupScheduled;
|
|
||||||
|
|
||||||
public PenumbraCollections(
|
public PenumbraCollections(
|
||||||
ILogger logger,
|
ILogger logger,
|
||||||
@@ -32,7 +26,6 @@ public sealed class PenumbraCollections : PenumbraBase
|
|||||||
_removeTemporaryCollection = new DeleteTemporaryCollection(pluginInterface);
|
_removeTemporaryCollection = new DeleteTemporaryCollection(pluginInterface);
|
||||||
_addTemporaryMod = new AddTemporaryMod(pluginInterface);
|
_addTemporaryMod = new AddTemporaryMod(pluginInterface);
|
||||||
_removeTemporaryMod = new RemoveTemporaryMod(pluginInterface);
|
_removeTemporaryMod = new RemoveTemporaryMod(pluginInterface);
|
||||||
_getCollections = new GetCollections(pluginInterface);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override string Name => "Penumbra.Collections";
|
public override string Name => "Penumbra.Collections";
|
||||||
@@ -62,16 +55,11 @@ public sealed class PenumbraCollections : PenumbraBase
|
|||||||
var (collectionId, collectionName) = await DalamudUtil.RunOnFrameworkThread(() =>
|
var (collectionId, collectionName) = await DalamudUtil.RunOnFrameworkThread(() =>
|
||||||
{
|
{
|
||||||
var name = $"Lightless_{uid}";
|
var name = $"Lightless_{uid}";
|
||||||
_createNamedTemporaryCollection.Invoke(name, name, out var tempCollectionId);
|
var createResult = _createNamedTemporaryCollection.Invoke(name, name, out var tempCollectionId);
|
||||||
logger.LogTrace("Creating Temp Collection {CollectionName}, GUID: {CollectionId}", name, tempCollectionId);
|
logger.LogTrace("Creating Temp Collection {CollectionName}, GUID: {CollectionId}, Result: {Result}", name, tempCollectionId, createResult);
|
||||||
return (tempCollectionId, name);
|
return (tempCollectionId, name);
|
||||||
}).ConfigureAwait(false);
|
}).ConfigureAwait(false);
|
||||||
|
|
||||||
if (collectionId != Guid.Empty)
|
|
||||||
{
|
|
||||||
_activeTemporaryCollections[collectionId] = collectionName;
|
|
||||||
}
|
|
||||||
|
|
||||||
return collectionId;
|
return collectionId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,46 +77,27 @@ public sealed class PenumbraCollections : PenumbraBase
|
|||||||
logger.LogTrace("[{ApplicationId}] RemoveTemporaryCollection: {Result}", applicationId, result);
|
logger.LogTrace("[{ApplicationId}] RemoveTemporaryCollection: {Result}", applicationId, result);
|
||||||
}).ConfigureAwait(false);
|
}).ConfigureAwait(false);
|
||||||
|
|
||||||
_activeTemporaryCollections.TryRemove(collectionId, out _);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SetTemporaryModsAsync(
|
public async Task SetTemporaryModsAsync(ILogger logger, Guid applicationId, Guid collectionId, Dictionary<string, string> modPaths)
|
||||||
ILogger logger,
|
|
||||||
Guid applicationId,
|
|
||||||
Guid collectionId,
|
|
||||||
Dictionary<string, string> modPaths,
|
|
||||||
string scope)
|
|
||||||
{
|
{
|
||||||
if (!IsAvailable || collectionId == Guid.Empty)
|
if (!IsAvailable || collectionId == Guid.Empty)
|
||||||
return;
|
|
||||||
|
|
||||||
var modName = $"LightlessChara_Files_{applicationId:N}_{scope}";
|
|
||||||
|
|
||||||
var normalized = new Dictionary<string, string>(modPaths.Count, StringComparer.OrdinalIgnoreCase);
|
|
||||||
foreach (var kvp in modPaths)
|
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(kvp.Key) || string.IsNullOrWhiteSpace(kvp.Value))
|
return;
|
||||||
continue;
|
|
||||||
|
|
||||||
var gamePath = kvp.Key.Replace('\\', '/').ToLowerInvariant();
|
|
||||||
normalized[gamePath] = kvp.Value;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await DalamudUtil.RunOnFrameworkThread(() =>
|
await DalamudUtil.RunOnFrameworkThread(() =>
|
||||||
{
|
{
|
||||||
foreach (var mod in normalized)
|
foreach (var mod in modPaths)
|
||||||
logger.LogTrace("[{ApplicationId}] {ModName}: {From} => {To}", applicationId, modName, mod.Key, mod.Value);
|
{
|
||||||
|
logger.LogTrace("[{ApplicationId}] Change: {From} => {To}", applicationId, mod.Key, mod.Value);
|
||||||
|
}
|
||||||
|
|
||||||
var removeResult = _removeTemporaryMod.Invoke(modName, collectionId, 0);
|
var removeResult = _removeTemporaryMod.Invoke("LightlessChara_Files", collectionId, 0);
|
||||||
logger.LogTrace("[{ApplicationId}] Removing temp mod {ModName} for {CollectionId}, Success: {Result}",
|
logger.LogTrace("[{ApplicationId}] Removing temp files mod for {CollectionId}, Success: {Result}", applicationId, collectionId, removeResult);
|
||||||
applicationId, modName, collectionId, removeResult);
|
|
||||||
|
|
||||||
if (normalized.Count == 0)
|
var addResult = _addTemporaryMod.Invoke("LightlessChara_Files", collectionId, modPaths, string.Empty, 0);
|
||||||
return;
|
logger.LogTrace("[{ApplicationId}] Setting temp files mod for {CollectionId}, Success: {Result}", applicationId, collectionId, addResult);
|
||||||
|
|
||||||
var addResult = _addTemporaryMod.Invoke(modName, collectionId, normalized, string.Empty, 0);
|
|
||||||
logger.LogTrace("[{ApplicationId}] Setting temp mod {ModName} for {CollectionId}, Success: {Result}",
|
|
||||||
applicationId, modName, collectionId, addResult);
|
|
||||||
}).ConfigureAwait(false);
|
}).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,67 +118,5 @@ public sealed class PenumbraCollections : PenumbraBase
|
|||||||
|
|
||||||
protected override void HandleStateChange(IpcConnectionState previous, IpcConnectionState current)
|
protected override void HandleStateChange(IpcConnectionState previous, IpcConnectionState current)
|
||||||
{
|
{
|
||||||
if (current == IpcConnectionState.Available)
|
|
||||||
{
|
|
||||||
ScheduleCleanup();
|
|
||||||
}
|
}
|
||||||
else if (previous == IpcConnectionState.Available && current != IpcConnectionState.Available)
|
|
||||||
{
|
|
||||||
Interlocked.Exchange(ref _cleanupScheduled, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ScheduleCleanup()
|
|
||||||
{
|
|
||||||
if (Interlocked.Exchange(ref _cleanupScheduled, 1) != 0)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = Task.Run(CleanupTemporaryCollectionsAsync);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task CleanupTemporaryCollectionsAsync()
|
|
||||||
{
|
|
||||||
if (!IsAvailable)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var collections = await DalamudUtil.RunOnFrameworkThread(() => _getCollections.Invoke()).ConfigureAwait(false);
|
|
||||||
foreach (var (collectionId, name) in collections)
|
|
||||||
{
|
|
||||||
if (!IsLightlessCollectionName(name) || _activeTemporaryCollections.ContainsKey(collectionId))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.LogDebug("Cleaning up stale temporary collection {CollectionName} ({CollectionId})", name, collectionId);
|
|
||||||
var deleteResult = await DalamudUtil.RunOnFrameworkThread(() =>
|
|
||||||
{
|
|
||||||
var result = _removeTemporaryCollection.Invoke(collectionId);
|
|
||||||
Logger.LogTrace("Cleanup RemoveTemporaryCollection result for {CollectionName} ({CollectionId}): {Result}", name, collectionId, result);
|
|
||||||
return result;
|
|
||||||
}).ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (deleteResult == PenumbraApiEc.Success)
|
|
||||||
{
|
|
||||||
_activeTemporaryCollections.TryRemove(collectionId, out _);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Logger.LogDebug("Skipped removing temporary collection {CollectionName} ({CollectionId}). Result: {Result}", name, collectionId, deleteResult);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Logger.LogWarning(ex, "Failed to clean up Penumbra temporary collections");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool IsLightlessCollectionName(string? name)
|
|
||||||
=> !string.IsNullOrEmpty(name) && name.StartsWith("Lightless_", StringComparison.Ordinal);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ using Dalamud.Plugin;
|
|||||||
using LightlessSync.Interop.Ipc.Framework;
|
using LightlessSync.Interop.Ipc.Framework;
|
||||||
using LightlessSync.PlayerData.Handlers;
|
using LightlessSync.PlayerData.Handlers;
|
||||||
using LightlessSync.Services;
|
using LightlessSync.Services;
|
||||||
using LightlessSync.Services.ActorTracking;
|
|
||||||
using LightlessSync.Services.Mediator;
|
using LightlessSync.Services.Mediator;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Globalization;
|
||||||
using Penumbra.Api.Helpers;
|
using Penumbra.Api.Helpers;
|
||||||
using Penumbra.Api.IpcSubscribers;
|
using Penumbra.Api.IpcSubscribers;
|
||||||
|
|
||||||
@@ -12,7 +13,6 @@ namespace LightlessSync.Interop.Ipc.Penumbra;
|
|||||||
|
|
||||||
public sealed class PenumbraResource : PenumbraBase
|
public sealed class PenumbraResource : PenumbraBase
|
||||||
{
|
{
|
||||||
private readonly ActorObjectService _actorObjectService;
|
|
||||||
private readonly GetGameObjectResourcePaths _gameObjectResourcePaths;
|
private readonly GetGameObjectResourcePaths _gameObjectResourcePaths;
|
||||||
private readonly ResolveGameObjectPath _resolveGameObjectPath;
|
private readonly ResolveGameObjectPath _resolveGameObjectPath;
|
||||||
private readonly ReverseResolveGameObjectPath _reverseResolveGameObjectPath;
|
private readonly ReverseResolveGameObjectPath _reverseResolveGameObjectPath;
|
||||||
@@ -24,10 +24,8 @@ public sealed class PenumbraResource : PenumbraBase
|
|||||||
ILogger logger,
|
ILogger logger,
|
||||||
IDalamudPluginInterface pluginInterface,
|
IDalamudPluginInterface pluginInterface,
|
||||||
DalamudUtilService dalamudUtil,
|
DalamudUtilService dalamudUtil,
|
||||||
LightlessMediator mediator,
|
LightlessMediator mediator) : base(logger, pluginInterface, dalamudUtil, mediator)
|
||||||
ActorObjectService actorObjectService) : base(logger, pluginInterface, dalamudUtil, mediator)
|
|
||||||
{
|
{
|
||||||
_actorObjectService = actorObjectService;
|
|
||||||
_gameObjectResourcePaths = new GetGameObjectResourcePaths(pluginInterface);
|
_gameObjectResourcePaths = new GetGameObjectResourcePaths(pluginInterface);
|
||||||
_resolveGameObjectPath = new ResolveGameObjectPath(pluginInterface);
|
_resolveGameObjectPath = new ResolveGameObjectPath(pluginInterface);
|
||||||
_reverseResolveGameObjectPath = new ReverseResolveGameObjectPath(pluginInterface);
|
_reverseResolveGameObjectPath = new ReverseResolveGameObjectPath(pluginInterface);
|
||||||
@@ -45,17 +43,33 @@ public sealed class PenumbraResource : PenumbraBase
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return await DalamudUtil.RunOnFrameworkThread(() =>
|
var requestId = Guid.NewGuid();
|
||||||
|
var totalTimer = Stopwatch.StartNew();
|
||||||
|
logger.LogTrace("[{requestId}] Requesting Penumbra.GetGameObjectResourcePaths for {handler}", requestId, handler);
|
||||||
|
|
||||||
|
var result = await DalamudUtil.RunOnFrameworkThread(() =>
|
||||||
{
|
{
|
||||||
logger.LogTrace("Calling On IPC: Penumbra.GetGameObjectResourcePaths");
|
|
||||||
var idx = handler.GetGameObject()?.ObjectIndex;
|
var idx = handler.GetGameObject()?.ObjectIndex;
|
||||||
if (idx == null)
|
if (idx == null)
|
||||||
{
|
{
|
||||||
|
logger.LogTrace("[{requestId}] GetGameObjectResourcePaths aborted (missing object index) for {handler}", requestId, handler);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return _gameObjectResourcePaths.Invoke(idx.Value)[0];
|
logger.LogTrace("[{requestId}] Invoking Penumbra.GetGameObjectResourcePaths for index {index}", requestId, idx.Value);
|
||||||
|
var invokeTimer = Stopwatch.StartNew();
|
||||||
|
var data = _gameObjectResourcePaths.Invoke(idx.Value)[0];
|
||||||
|
invokeTimer.Stop();
|
||||||
|
logger.LogTrace("[{requestId}] Penumbra.GetGameObjectResourcePaths returned {count} entries in {elapsedMs}ms",
|
||||||
|
requestId, data?.Count ?? 0, invokeTimer.ElapsedMilliseconds);
|
||||||
|
return data;
|
||||||
}).ConfigureAwait(false);
|
}).ConfigureAwait(false);
|
||||||
|
|
||||||
|
totalTimer.Stop();
|
||||||
|
logger.LogTrace("[{requestId}] Penumbra.GetGameObjectResourcePaths finished in {elapsedMs}ms (null: {isNull})",
|
||||||
|
requestId, totalTimer.ElapsedMilliseconds, result is null);
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string GetMetaManipulations()
|
public string GetMetaManipulations()
|
||||||
@@ -79,23 +93,11 @@ public sealed class PenumbraResource : PenumbraBase
|
|||||||
|
|
||||||
private void HandleResourceLoaded(nint ptr, string gamePath, string resolvedPath)
|
private void HandleResourceLoaded(nint ptr, string gamePath, string resolvedPath)
|
||||||
{
|
{
|
||||||
if (ptr == nint.Zero)
|
if (ptr != nint.Zero && string.Compare(gamePath, resolvedPath, ignoreCase: true, CultureInfo.InvariantCulture) != 0)
|
||||||
{
|
{
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!_actorObjectService.TryGetOwnedKind(ptr, out _))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.Compare(gamePath, resolvedPath, StringComparison.OrdinalIgnoreCase) == 0)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Mediator.Publish(new PenumbraResourceLoadMessage(ptr, gamePath, resolvedPath));
|
Mediator.Publish(new PenumbraResourceLoadMessage(ptr, gamePath, resolvedPath));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected override void HandleStateChange(IpcConnectionState previous, IpcConnectionState current)
|
protected override void HandleStateChange(IpcConnectionState previous, IpcConnectionState current)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ public sealed class PenumbraTexture : PenumbraBase
|
|||||||
|
|
||||||
public override string Name => "Penumbra.Textures";
|
public override string Name => "Penumbra.Textures";
|
||||||
|
|
||||||
public async Task ConvertTextureFilesAsync(ILogger logger, IReadOnlyList<TextureConversionJob> jobs, IProgress<TextureConversionProgress>? progress, CancellationToken token)
|
public async Task ConvertTextureFilesAsync(ILogger logger, IReadOnlyList<TextureConversionJob> jobs, IProgress<TextureConversionProgress>? progress, CancellationToken token, bool requestRedraw)
|
||||||
{
|
{
|
||||||
if (!IsAvailable || jobs.Count == 0)
|
if (!IsAvailable || jobs.Count == 0)
|
||||||
{
|
{
|
||||||
@@ -57,7 +57,7 @@ public sealed class PenumbraTexture : PenumbraBase
|
|||||||
Mediator.Publish(new ResumeScanMessage(nameof(ConvertTextureFilesAsync)));
|
Mediator.Publish(new ResumeScanMessage(nameof(ConvertTextureFilesAsync)));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (completedJobs > 0 && !token.IsCancellationRequested)
|
if (requestRedraw && completedJobs > 0 && !token.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
await DalamudUtil.RunOnFrameworkThread(async () =>
|
await DalamudUtil.RunOnFrameworkThread(async () =>
|
||||||
{
|
{
|
||||||
@@ -92,7 +92,7 @@ public sealed class PenumbraTexture : PenumbraBase
|
|||||||
{
|
{
|
||||||
token.ThrowIfCancellationRequested();
|
token.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
logger.LogInformation("Converting texture {Input} -> {Output} ({Target})", job.InputFile, job.OutputFile, job.TargetType);
|
logger.LogDebug("Converting texture {Input} -> {Output} ({Target})", job.InputFile, job.OutputFile, job.TargetType);
|
||||||
var convertTask = _convertTextureFile.Invoke(job.InputFile, job.OutputFile, job.TargetType, job.IncludeMipMaps);
|
var convertTask = _convertTextureFile.Invoke(job.InputFile, job.OutputFile, job.TargetType, job.IncludeMipMaps);
|
||||||
await convertTask.ConfigureAwait(false);
|
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.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,7 +72,10 @@ 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);
|
||||||
|
|
||||||
|
if (!isTempCollections)
|
||||||
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var configBackupFolder = Path.Join(configDir, BackupFolder);
|
var configBackupFolder = Path.Join(configDir, BackupFolder);
|
||||||
@@ -104,13 +107,14 @@ public class ConfigurationSaveService : IHostedService
|
|||||||
// ignore if file cannot be backupped
|
// ignore if file cannot be backupped
|
||||||
_logger.LogWarning(ex, "Could not create backup for {config}", config.ConfigurationPath);
|
_logger.LogWarning(ex, "Could not create backup for {config}", config.ConfigurationPath);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var temp = config.ConfigurationPath + ".tmp";
|
var temp = config.ConfigurationPath + ".tmp";
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
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();
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ public sealed class ChatConfig : ILightlessConfiguration
|
|||||||
public bool ShowMessageTimestamps { get; set; } = true;
|
public bool ShowMessageTimestamps { get; set; } = true;
|
||||||
public bool ShowNotesInSyncshellChat { get; set; } = true;
|
public bool ShowNotesInSyncshellChat { get; set; } = true;
|
||||||
public bool EnableAnimatedEmotes { get; set; } = true;
|
public bool EnableAnimatedEmotes { get; set; } = true;
|
||||||
|
public float EmoteScale { get; set; } = 1.5f;
|
||||||
|
public bool EnableMentionNotifications { get; set; } = true;
|
||||||
|
public bool AutoOpenChatOnNewMessage { get; set; } = false;
|
||||||
public float ChatWindowOpacity { get; set; } = .97f;
|
public float ChatWindowOpacity { get; set; } = .97f;
|
||||||
public bool FadeWhenUnfocused { get; set; } = false;
|
public bool FadeWhenUnfocused { get; set; } = false;
|
||||||
public float UnfocusedWindowOpacity { get; set; } = 0.6f;
|
public float UnfocusedWindowOpacity { get; set; } = 0.6f;
|
||||||
@@ -23,6 +26,9 @@ public sealed class ChatConfig : ILightlessConfiguration
|
|||||||
public bool ShowWhenUiHidden { get; set; } = true;
|
public bool ShowWhenUiHidden { get; set; } = true;
|
||||||
public bool ShowInCutscenes { get; set; } = true;
|
public bool ShowInCutscenes { get; set; } = true;
|
||||||
public bool ShowInGpose { get; set; } = true;
|
public bool ShowInGpose { get; set; } = true;
|
||||||
|
public bool PersistSyncshellHistory { get; set; } = false;
|
||||||
public List<string> ChannelOrder { get; set; } = new();
|
public List<string> ChannelOrder { get; set; } = new();
|
||||||
|
public Dictionary<string, bool> HiddenChannels { get; set; } = new(StringComparer.Ordinal);
|
||||||
|
public Dictionary<string, string> SyncshellChannelHistory { get; set; } = new(StringComparer.Ordinal);
|
||||||
public Dictionary<string, bool> PreferNotesForChannels { get; set; } = new(StringComparer.Ordinal);
|
public Dictionary<string, bool> PreferNotesForChannels { get; set; } = new(StringComparer.Ordinal);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ public class LightlessConfig : ILightlessConfiguration
|
|||||||
public DtrEntry.Colors DtrColorsLightfinderUnavailable { get; set; } = new(Foreground: 0x000000u, Glow: 0x000000u);
|
public DtrEntry.Colors DtrColorsLightfinderUnavailable { get; set; } = new(Foreground: 0x000000u, Glow: 0x000000u);
|
||||||
public LightfinderDtrDisplayMode LightfinderDtrDisplayMode { get; set; } = LightfinderDtrDisplayMode.PendingPairRequests;
|
public LightfinderDtrDisplayMode LightfinderDtrDisplayMode { get; set; } = LightfinderDtrDisplayMode.PendingPairRequests;
|
||||||
public bool UseLightlessRedesign { get; set; } = true;
|
public bool UseLightlessRedesign { get; set; } = true;
|
||||||
|
public bool ShowUiWhenUiHidden { get; set; } = true;
|
||||||
|
public bool ShowUiInGpose { get; set; } = true;
|
||||||
public bool EnableRightClickMenus { get; set; } = true;
|
public bool EnableRightClickMenus { get; set; } = true;
|
||||||
public NotificationLocation ErrorNotification { get; set; } = NotificationLocation.Both;
|
public NotificationLocation ErrorNotification { get; set; } = NotificationLocation.Both;
|
||||||
public string ExportFolder { get; set; } = string.Empty;
|
public string ExportFolder { get; set; } = string.Empty;
|
||||||
@@ -160,6 +162,5 @@ public class LightlessConfig : ILightlessConfiguration
|
|||||||
public bool EnableParticleEffects { get; set; } = true;
|
public bool EnableParticleEffects { get; set; } = true;
|
||||||
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,156 @@
|
|||||||
|
namespace LightlessSync.LightlessConfiguration.Configurations;
|
||||||
|
|
||||||
|
public static class ModelDecimationDefaults
|
||||||
|
{
|
||||||
|
public const bool EnableAutoDecimation = false;
|
||||||
|
public const int TriangleThreshold = 15_000;
|
||||||
|
public const double TargetRatio = 0.8;
|
||||||
|
public const bool NormalizeTangents = true;
|
||||||
|
public const bool AvoidBodyIntersection = true;
|
||||||
|
|
||||||
|
/// <summary>Default triangle threshold for batch decimation (0 = no threshold).</summary>
|
||||||
|
public const int BatchTriangleThreshold = 0;
|
||||||
|
|
||||||
|
/// <summary>Default target triangle ratio for batch decimation.</summary>
|
||||||
|
public const double BatchTargetRatio = 0.8;
|
||||||
|
|
||||||
|
/// <summary>Default tangent normalization toggle for batch decimation.</summary>
|
||||||
|
public const bool BatchNormalizeTangents = true;
|
||||||
|
|
||||||
|
/// <summary>Default body collision guard toggle for batch decimation.</summary>
|
||||||
|
public const bool BatchAvoidBodyIntersection = true;
|
||||||
|
|
||||||
|
/// <summary>Default display for the batch decimation warning overlay.</summary>
|
||||||
|
public const bool ShowBatchDecimationWarning = true;
|
||||||
|
|
||||||
|
public const bool KeepOriginalModelFiles = true;
|
||||||
|
public const bool SkipPreferredPairs = true;
|
||||||
|
public const bool AllowBody = false;
|
||||||
|
public const bool AllowFaceHead = false;
|
||||||
|
public const bool AllowTail = false;
|
||||||
|
public const bool AllowClothing = true;
|
||||||
|
public const bool AllowAccessories = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class ModelDecimationAdvancedSettings
|
||||||
|
{
|
||||||
|
/// <summary>Minimum triangles per connected component before skipping decimation.</summary>
|
||||||
|
public const int DefaultMinComponentTriangles = 6;
|
||||||
|
|
||||||
|
/// <summary>Average-edge multiplier used to cap collapses.</summary>
|
||||||
|
public const float DefaultMaxCollapseEdgeLengthFactor = 1.25f;
|
||||||
|
|
||||||
|
/// <summary>Maximum normal deviation (degrees) allowed for a collapse.</summary>
|
||||||
|
public const float DefaultNormalSimilarityThresholdDegrees = 60f;
|
||||||
|
|
||||||
|
/// <summary>Minimum bone-weight overlap required to allow a collapse.</summary>
|
||||||
|
public const float DefaultBoneWeightSimilarityThreshold = 0.85f;
|
||||||
|
|
||||||
|
/// <summary>UV similarity threshold to protect seams.</summary>
|
||||||
|
public const float DefaultUvSimilarityThreshold = 0.02f;
|
||||||
|
|
||||||
|
/// <summary>UV seam cosine threshold for blocking seam collapses.</summary>
|
||||||
|
public const float DefaultUvSeamAngleCos = 0.99f;
|
||||||
|
|
||||||
|
/// <summary>Whether to block UV seam vertices from collapsing.</summary>
|
||||||
|
public const bool DefaultBlockUvSeamVertices = true;
|
||||||
|
|
||||||
|
/// <summary>Whether to allow collapses on boundary edges.</summary>
|
||||||
|
public const bool DefaultAllowBoundaryCollapses = false;
|
||||||
|
|
||||||
|
/// <summary>Body collision distance factor for the primary pass.</summary>
|
||||||
|
public const float DefaultBodyCollisionDistanceFactor = 0.75f;
|
||||||
|
|
||||||
|
/// <summary>Body collision distance factor for the relaxed fallback pass.</summary>
|
||||||
|
public const float DefaultBodyCollisionNoOpDistanceFactor = 0.25f;
|
||||||
|
|
||||||
|
/// <summary>Relax multiplier applied when the mesh is close to the body.</summary>
|
||||||
|
public const float DefaultBodyCollisionAdaptiveRelaxFactor = 1.0f;
|
||||||
|
|
||||||
|
/// <summary>Ratio of near-body vertices required to trigger relaxation.</summary>
|
||||||
|
public const float DefaultBodyCollisionAdaptiveNearRatio = 0.4f;
|
||||||
|
|
||||||
|
/// <summary>UV threshold for relaxed body-collision mode.</summary>
|
||||||
|
public const float DefaultBodyCollisionAdaptiveUvThreshold = 0.08f;
|
||||||
|
|
||||||
|
/// <summary>UV seam cosine threshold for relaxed body-collision mode.</summary>
|
||||||
|
public const float DefaultBodyCollisionNoOpUvSeamAngleCos = 0.98f;
|
||||||
|
|
||||||
|
/// <summary>Expansion factor for protected vertices near the body.</summary>
|
||||||
|
public const float DefaultBodyCollisionProtectionFactor = 1.5f;
|
||||||
|
|
||||||
|
/// <summary>Minimum ratio used when decimating the body proxy.</summary>
|
||||||
|
public const float DefaultBodyProxyTargetRatioMin = 0.85f;
|
||||||
|
|
||||||
|
/// <summary>Inflation applied to body collision distances.</summary>
|
||||||
|
public const float DefaultBodyCollisionProxyInflate = 0.0005f;
|
||||||
|
|
||||||
|
/// <summary>Body collision penetration factor used during collapse checks.</summary>
|
||||||
|
public const float DefaultBodyCollisionPenetrationFactor = 0.75f;
|
||||||
|
|
||||||
|
/// <summary>Minimum body collision distance threshold.</summary>
|
||||||
|
public const float DefaultMinBodyCollisionDistance = 0.0001f;
|
||||||
|
|
||||||
|
/// <summary>Minimum cell size for body collision spatial hashing.</summary>
|
||||||
|
public const float DefaultMinBodyCollisionCellSize = 0.0001f;
|
||||||
|
|
||||||
|
/// <summary>Minimum triangles per connected component before skipping decimation.</summary>
|
||||||
|
public int MinComponentTriangles { get; set; } = DefaultMinComponentTriangles;
|
||||||
|
|
||||||
|
/// <summary>Average-edge multiplier used to cap collapses.</summary>
|
||||||
|
public float MaxCollapseEdgeLengthFactor { get; set; } = DefaultMaxCollapseEdgeLengthFactor;
|
||||||
|
|
||||||
|
/// <summary>Maximum normal deviation (degrees) allowed for a collapse.</summary>
|
||||||
|
public float NormalSimilarityThresholdDegrees { get; set; } = DefaultNormalSimilarityThresholdDegrees;
|
||||||
|
|
||||||
|
/// <summary>Minimum bone-weight overlap required to allow a collapse.</summary>
|
||||||
|
public float BoneWeightSimilarityThreshold { get; set; } = DefaultBoneWeightSimilarityThreshold;
|
||||||
|
|
||||||
|
/// <summary>UV similarity threshold to protect seams.</summary>
|
||||||
|
public float UvSimilarityThreshold { get; set; } = DefaultUvSimilarityThreshold;
|
||||||
|
|
||||||
|
/// <summary>UV seam cosine threshold for blocking seam collapses.</summary>
|
||||||
|
public float UvSeamAngleCos { get; set; } = DefaultUvSeamAngleCos;
|
||||||
|
|
||||||
|
/// <summary>Whether to block UV seam vertices from collapsing.</summary>
|
||||||
|
public bool BlockUvSeamVertices { get; set; } = DefaultBlockUvSeamVertices;
|
||||||
|
|
||||||
|
/// <summary>Whether to allow collapses on boundary edges.</summary>
|
||||||
|
public bool AllowBoundaryCollapses { get; set; } = DefaultAllowBoundaryCollapses;
|
||||||
|
|
||||||
|
/// <summary>Body collision distance factor for the primary pass.</summary>
|
||||||
|
public float BodyCollisionDistanceFactor { get; set; } = DefaultBodyCollisionDistanceFactor;
|
||||||
|
|
||||||
|
/// <summary>Body collision distance factor for the relaxed fallback pass.</summary>
|
||||||
|
public float BodyCollisionNoOpDistanceFactor { get; set; } = DefaultBodyCollisionNoOpDistanceFactor;
|
||||||
|
|
||||||
|
/// <summary>Relax multiplier applied when the mesh is close to the body.</summary>
|
||||||
|
public float BodyCollisionAdaptiveRelaxFactor { get; set; } = DefaultBodyCollisionAdaptiveRelaxFactor;
|
||||||
|
|
||||||
|
/// <summary>Ratio of near-body vertices required to trigger relaxation.</summary>
|
||||||
|
public float BodyCollisionAdaptiveNearRatio { get; set; } = DefaultBodyCollisionAdaptiveNearRatio;
|
||||||
|
|
||||||
|
/// <summary>UV threshold for relaxed body-collision mode.</summary>
|
||||||
|
public float BodyCollisionAdaptiveUvThreshold { get; set; } = DefaultBodyCollisionAdaptiveUvThreshold;
|
||||||
|
|
||||||
|
/// <summary>UV seam cosine threshold for relaxed body-collision mode.</summary>
|
||||||
|
public float BodyCollisionNoOpUvSeamAngleCos { get; set; } = DefaultBodyCollisionNoOpUvSeamAngleCos;
|
||||||
|
|
||||||
|
/// <summary>Expansion factor for protected vertices near the body.</summary>
|
||||||
|
public float BodyCollisionProtectionFactor { get; set; } = DefaultBodyCollisionProtectionFactor;
|
||||||
|
|
||||||
|
/// <summary>Minimum ratio used when decimating the body proxy.</summary>
|
||||||
|
public float BodyProxyTargetRatioMin { get; set; } = DefaultBodyProxyTargetRatioMin;
|
||||||
|
|
||||||
|
/// <summary>Inflation applied to body collision distances.</summary>
|
||||||
|
public float BodyCollisionProxyInflate { get; set; } = DefaultBodyCollisionProxyInflate;
|
||||||
|
|
||||||
|
/// <summary>Body collision penetration factor used during collapse checks.</summary>
|
||||||
|
public float BodyCollisionPenetrationFactor { get; set; } = DefaultBodyCollisionPenetrationFactor;
|
||||||
|
|
||||||
|
/// <summary>Minimum body collision distance threshold.</summary>
|
||||||
|
public float MinBodyCollisionDistance { get; set; } = DefaultMinBodyCollisionDistance;
|
||||||
|
|
||||||
|
/// <summary>Minimum cell size for body collision spatial hashing.</summary>
|
||||||
|
public float MinBodyCollisionCellSize { get; set; } = DefaultMinBodyCollisionCellSize;
|
||||||
|
}
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
namespace LightlessSync.LightlessConfiguration.Configurations;
|
|
||||||
|
|
||||||
public class PenumbraJanitorConfig : ILightlessConfiguration
|
|
||||||
{
|
|
||||||
public int Version { get; set; } = 0;
|
|
||||||
|
|
||||||
public HashSet<Guid> OrphanableTempCollections { get; set; } = [];
|
|
||||||
}
|
|
||||||
@@ -21,16 +21,26 @@ public class PlayerPerformanceConfig : ILightlessConfiguration
|
|||||||
public bool EnableIndexTextureDownscale { get; set; } = false;
|
public bool EnableIndexTextureDownscale { get; set; } = false;
|
||||||
public int TextureDownscaleMaxDimension { get; set; } = 2048;
|
public int TextureDownscaleMaxDimension { get; set; } = 2048;
|
||||||
public bool OnlyDownscaleUncompressedTextures { get; set; } = true;
|
public bool OnlyDownscaleUncompressedTextures { get; set; } = true;
|
||||||
|
public bool EnableUncompressedTextureCompression { get; set; } = false;
|
||||||
|
public bool SkipUncompressedTextureCompressionMipMaps { get; set; } = false;
|
||||||
public bool KeepOriginalTextureFiles { get; set; } = false;
|
public bool KeepOriginalTextureFiles { get; set; } = false;
|
||||||
public bool SkipTextureDownscaleForPreferredPairs { get; set; } = true;
|
public bool SkipTextureDownscaleForPreferredPairs { get; set; } = true;
|
||||||
public bool EnableModelDecimation { get; set; } = false;
|
public bool EnableModelDecimation { get; set; } = ModelDecimationDefaults.EnableAutoDecimation;
|
||||||
public int ModelDecimationTriangleThreshold { get; set; } = 20_000;
|
public int ModelDecimationTriangleThreshold { get; set; } = ModelDecimationDefaults.TriangleThreshold;
|
||||||
public double ModelDecimationTargetRatio { get; set; } = 0.8;
|
public double ModelDecimationTargetRatio { get; set; } = ModelDecimationDefaults.TargetRatio;
|
||||||
public bool KeepOriginalModelFiles { get; set; } = true;
|
public bool ModelDecimationNormalizeTangents { get; set; } = ModelDecimationDefaults.NormalizeTangents;
|
||||||
public bool SkipModelDecimationForPreferredPairs { get; set; } = true;
|
public bool ModelDecimationAvoidBodyIntersection { get; set; } = ModelDecimationDefaults.AvoidBodyIntersection;
|
||||||
public bool ModelDecimationAllowBody { get; set; } = false;
|
public ModelDecimationAdvancedSettings ModelDecimationAdvanced { get; set; } = new();
|
||||||
public bool ModelDecimationAllowFaceHead { get; set; } = false;
|
public int BatchModelDecimationTriangleThreshold { get; set; } = ModelDecimationDefaults.BatchTriangleThreshold;
|
||||||
public bool ModelDecimationAllowTail { get; set; } = false;
|
public double BatchModelDecimationTargetRatio { get; set; } = ModelDecimationDefaults.BatchTargetRatio;
|
||||||
public bool ModelDecimationAllowClothing { get; set; } = true;
|
public bool BatchModelDecimationNormalizeTangents { get; set; } = ModelDecimationDefaults.BatchNormalizeTangents;
|
||||||
public bool ModelDecimationAllowAccessories { get; set; } = true;
|
public bool BatchModelDecimationAvoidBodyIntersection { get; set; } = ModelDecimationDefaults.BatchAvoidBodyIntersection;
|
||||||
|
public bool ShowBatchModelDecimationWarning { get; set; } = ModelDecimationDefaults.ShowBatchDecimationWarning;
|
||||||
|
public bool KeepOriginalModelFiles { get; set; } = ModelDecimationDefaults.KeepOriginalModelFiles;
|
||||||
|
public bool SkipModelDecimationForPreferredPairs { get; set; } = ModelDecimationDefaults.SkipPreferredPairs;
|
||||||
|
public bool ModelDecimationAllowBody { get; set; } = ModelDecimationDefaults.AllowBody;
|
||||||
|
public bool ModelDecimationAllowFaceHead { get; set; } = ModelDecimationDefaults.AllowFaceHead;
|
||||||
|
public bool ModelDecimationAllowTail { get; set; } = ModelDecimationDefaults.AllowTail;
|
||||||
|
public bool ModelDecimationAllowClothing { get; set; } = ModelDecimationDefaults.AllowClothing;
|
||||||
|
public bool ModelDecimationAllowAccessories { get; set; } = ModelDecimationDefaults.AllowAccessories;
|
||||||
}
|
}
|
||||||
@@ -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,7 @@
|
|||||||
|
namespace LightlessSync.LightlessConfiguration.Models;
|
||||||
|
|
||||||
|
public sealed class OrphanableTempCollectionEntry
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public DateTime RegisteredAtUtc { get; set; } = DateTime.MinValue;
|
||||||
|
}
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
using LightlessSync.LightlessConfiguration.Configurations;
|
|
||||||
|
|
||||||
namespace LightlessSync.LightlessConfiguration;
|
|
||||||
|
|
||||||
public class PenumbraJanitorConfigService : ConfigurationServiceBase<PenumbraJanitorConfig>
|
|
||||||
{
|
|
||||||
public const string ConfigName = "penumbra-collections.json";
|
|
||||||
|
|
||||||
public PenumbraJanitorConfigService(string configDir) : base(configDir)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public override string ConfigurationName => ConfigName;
|
|
||||||
}
|
|
||||||
@@ -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>
|
<PropertyGroup>
|
||||||
<Authors></Authors>
|
<Authors></Authors>
|
||||||
<Company></Company>
|
<Company></Company>
|
||||||
<Version>2.0.3</Version>
|
<Version>2.0.2.83</Version>
|
||||||
<Description></Description>
|
<Description></Description>
|
||||||
<Copyright></Copyright>
|
<Copyright></Copyright>
|
||||||
<PackageProjectUrl>https://github.com/Light-Public-Syncshells/LightlessClient</PackageProjectUrl>
|
<PackageProjectUrl>https://github.com/Light-Public-Syncshells/LightlessClient</PackageProjectUrl>
|
||||||
@@ -85,6 +85,8 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\LightlessAPI\LightlessSyncAPI\LightlessSync.API.csproj" />
|
<ProjectReference Include="..\LightlessAPI\LightlessSyncAPI\LightlessSync.API.csproj" />
|
||||||
|
<ProjectReference Include="..\LightlessCompactor\LightlessCompactor.csproj" />
|
||||||
|
<ProjectReference Include="..\LightlessCompactorWorker\LightlessCompactorWorker.csproj" ReferenceOutputAssembly="false" />
|
||||||
<ProjectReference Include="..\Penumbra.Api\Penumbra.Api.csproj" />
|
<ProjectReference Include="..\Penumbra.Api\Penumbra.Api.csproj" />
|
||||||
<ProjectReference Include="..\Penumbra.GameData\Penumbra.GameData.csproj" />
|
<ProjectReference Include="..\Penumbra.GameData\Penumbra.GameData.csproj" />
|
||||||
<ProjectReference Include="..\Penumbra.String\Penumbra.String.csproj" />
|
<ProjectReference Include="..\Penumbra.String\Penumbra.String.csproj" />
|
||||||
@@ -109,4 +111,12 @@
|
|||||||
<PackageReference Update="DalamudPackager" Version="14.0.1" />
|
<PackageReference Update="DalamudPackager" Version="14.0.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<CompactorWorkerFiles Include="..\LightlessCompactorWorker\bin\$(Configuration)\net10.0\*.*" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<Target Name="CopyCompactorWorker" AfterTargets="Build">
|
||||||
|
<Copy SourceFiles="@(CompactorWorkerFiles)" DestinationFolder="$(OutputPath)" SkipUnchangedFiles="true" />
|
||||||
|
</Target>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ public class FileDownloadManagerFactory
|
|||||||
private readonly TextureDownscaleService _textureDownscaleService;
|
private readonly TextureDownscaleService _textureDownscaleService;
|
||||||
private readonly ModelDecimationService _modelDecimationService;
|
private readonly ModelDecimationService _modelDecimationService;
|
||||||
private readonly TextureMetadataHelper _textureMetadataHelper;
|
private readonly TextureMetadataHelper _textureMetadataHelper;
|
||||||
|
private readonly FileDownloadDeduplicator _downloadDeduplicator;
|
||||||
|
|
||||||
public FileDownloadManagerFactory(
|
public FileDownloadManagerFactory(
|
||||||
ILoggerFactory loggerFactory,
|
ILoggerFactory loggerFactory,
|
||||||
@@ -29,7 +30,8 @@ public class FileDownloadManagerFactory
|
|||||||
LightlessConfigService configService,
|
LightlessConfigService configService,
|
||||||
TextureDownscaleService textureDownscaleService,
|
TextureDownscaleService textureDownscaleService,
|
||||||
ModelDecimationService modelDecimationService,
|
ModelDecimationService modelDecimationService,
|
||||||
TextureMetadataHelper textureMetadataHelper)
|
TextureMetadataHelper textureMetadataHelper,
|
||||||
|
FileDownloadDeduplicator downloadDeduplicator)
|
||||||
{
|
{
|
||||||
_loggerFactory = loggerFactory;
|
_loggerFactory = loggerFactory;
|
||||||
_lightlessMediator = lightlessMediator;
|
_lightlessMediator = lightlessMediator;
|
||||||
@@ -40,6 +42,7 @@ public class FileDownloadManagerFactory
|
|||||||
_textureDownscaleService = textureDownscaleService;
|
_textureDownscaleService = textureDownscaleService;
|
||||||
_modelDecimationService = modelDecimationService;
|
_modelDecimationService = modelDecimationService;
|
||||||
_textureMetadataHelper = textureMetadataHelper;
|
_textureMetadataHelper = textureMetadataHelper;
|
||||||
|
_downloadDeduplicator = downloadDeduplicator;
|
||||||
}
|
}
|
||||||
|
|
||||||
public FileDownloadManager Create()
|
public FileDownloadManager Create()
|
||||||
@@ -53,6 +56,7 @@ public class FileDownloadManagerFactory
|
|||||||
_configService,
|
_configService,
|
||||||
_textureDownscaleService,
|
_textureDownscaleService,
|
||||||
_modelDecimationService,
|
_modelDecimationService,
|
||||||
_textureMetadataHelper);
|
_textureMetadataHelper,
|
||||||
|
_downloadDeduplicator);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using Dalamud.Plugin.Services;
|
using LightlessSync.API.Data.Enum;
|
||||||
using LightlessSync.API.Data.Enum;
|
|
||||||
using LightlessSync.PlayerData.Handlers;
|
using LightlessSync.PlayerData.Handlers;
|
||||||
using LightlessSync.Services;
|
using LightlessSync.Services;
|
||||||
using LightlessSync.Services.Mediator;
|
using LightlessSync.Services.Mediator;
|
||||||
@@ -12,7 +11,6 @@ public class GameObjectHandlerFactory
|
|||||||
{
|
{
|
||||||
private readonly IServiceProvider _serviceProvider;
|
private readonly IServiceProvider _serviceProvider;
|
||||||
private readonly ILoggerFactory _loggerFactory;
|
private readonly ILoggerFactory _loggerFactory;
|
||||||
private readonly IObjectTable _objectTable;
|
|
||||||
private readonly LightlessMediator _lightlessMediator;
|
private readonly LightlessMediator _lightlessMediator;
|
||||||
private readonly PerformanceCollectorService _performanceCollectorService;
|
private readonly PerformanceCollectorService _performanceCollectorService;
|
||||||
|
|
||||||
@@ -20,14 +18,12 @@ public class GameObjectHandlerFactory
|
|||||||
ILoggerFactory loggerFactory,
|
ILoggerFactory loggerFactory,
|
||||||
PerformanceCollectorService performanceCollectorService,
|
PerformanceCollectorService performanceCollectorService,
|
||||||
LightlessMediator lightlessMediator,
|
LightlessMediator lightlessMediator,
|
||||||
IServiceProvider serviceProvider,
|
IServiceProvider serviceProvider)
|
||||||
IObjectTable objectTable)
|
|
||||||
{
|
{
|
||||||
_loggerFactory = loggerFactory;
|
_loggerFactory = loggerFactory;
|
||||||
_performanceCollectorService = performanceCollectorService;
|
_performanceCollectorService = performanceCollectorService;
|
||||||
_lightlessMediator = lightlessMediator;
|
_lightlessMediator = lightlessMediator;
|
||||||
_serviceProvider = serviceProvider;
|
_serviceProvider = serviceProvider;
|
||||||
_objectTable = objectTable;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<GameObjectHandler> Create(ObjectKind objectKind, Func<nint> getAddressFunc, bool isWatched = false)
|
public async Task<GameObjectHandler> Create(ObjectKind objectKind, Func<nint> getAddressFunc, bool isWatched = false)
|
||||||
@@ -40,7 +36,6 @@ public class GameObjectHandlerFactory
|
|||||||
dalamudUtilService,
|
dalamudUtilService,
|
||||||
objectKind,
|
objectKind,
|
||||||
getAddressFunc,
|
getAddressFunc,
|
||||||
_objectTable,
|
|
||||||
isWatched)).ConfigureAwait(false);
|
isWatched)).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
using LightlessSync.API.Data.Enum;
|
using LightlessSync.API.Data.Enum;
|
||||||
using LightlessSync.API.Dto.User;
|
using LightlessSync.API.Dto.User;
|
||||||
using LightlessSync.PlayerData.Pairs;
|
using LightlessSync.PlayerData.Pairs;
|
||||||
using LightlessSync.Services.Mediator;
|
using LightlessSync.Services.Mediator;
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using Dalamud.Utility;
|
using Dalamud.Utility;
|
||||||
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
|
||||||
using FFXIVClientStructs.FFXIV.Client.Game.Object;
|
using FFXIVClientStructs.FFXIV.Client.Game.Object;
|
||||||
using LightlessSync.FileCache;
|
using LightlessSync.FileCache;
|
||||||
using LightlessSync.Interop.Ipc;
|
using LightlessSync.Interop.Ipc;
|
||||||
@@ -35,7 +34,7 @@ public class PlayerDataFactory
|
|||||||
private const int _maxTransientResolvedEntries = 1000;
|
private const int _maxTransientResolvedEntries = 1000;
|
||||||
|
|
||||||
// Character build caches
|
// Character build caches
|
||||||
private readonly ConcurrentDictionary<nint, Task<CharacterDataFragment>> _characterBuildInflight = new();
|
private readonly TaskRegistry<nint> _characterBuildInflight = new();
|
||||||
private readonly ConcurrentDictionary<nint, CacheEntry> _characterBuildCache = new();
|
private readonly ConcurrentDictionary<nint, CacheEntry> _characterBuildCache = new();
|
||||||
|
|
||||||
// Time out thresholds
|
// Time out thresholds
|
||||||
@@ -120,29 +119,30 @@ public class PlayerDataFactory
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static readonly int _characterGameObjectOffset =
|
private static readonly int _drawObjectOffset =
|
||||||
(int)Marshal.OffsetOf<Character>(nameof(Character.GameObject));
|
|
||||||
|
|
||||||
private static readonly int _gameObjectDrawObjectOffset =
|
|
||||||
(int)Marshal.OffsetOf<GameObject>(nameof(GameObject.DrawObject));
|
(int)Marshal.OffsetOf<GameObject>(nameof(GameObject.DrawObject));
|
||||||
|
|
||||||
private async Task<bool> CheckForNullDrawObject(IntPtr playerPointer)
|
private async Task<bool> CheckForNullDrawObject(IntPtr playerPointer)
|
||||||
=> await _dalamudUtil.RunOnFrameworkThread(() => CheckForNullDrawObjectSafe(playerPointer))
|
=> await _dalamudUtil.RunOnFrameworkThread(() =>
|
||||||
.ConfigureAwait(false);
|
|
||||||
|
|
||||||
private static bool CheckForNullDrawObjectSafe(nint playerPointer)
|
|
||||||
{
|
{
|
||||||
if (playerPointer == nint.Zero)
|
nint basePtr = playerPointer;
|
||||||
|
|
||||||
|
if (!PtrGuard.LooksLikePtr(basePtr))
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
var drawObjPtrAddress = playerPointer + _characterGameObjectOffset + _gameObjectDrawObjectOffset;
|
nint drawObjAddr = basePtr + _drawObjectOffset;
|
||||||
|
|
||||||
// Read the DrawObject pointer from memory
|
if (!PtrGuard.IsReadable(drawObjAddr, (nuint)IntPtr.Size))
|
||||||
if (!MemoryProcessProbe.TryReadIntPtr(drawObjPtrAddress, out var drawObj))
|
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
return drawObj == nint.Zero;
|
if (!PtrGuard.TryReadIntPtr(drawObjAddr, out var drawObj))
|
||||||
}
|
return true;
|
||||||
|
|
||||||
|
if (drawObj != 0 && !PtrGuard.LooksLikePtr(drawObj))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
return drawObj == 0;
|
||||||
|
}).ConfigureAwait(false);
|
||||||
|
|
||||||
private static bool IsCacheFresh(CacheEntry entry)
|
private static bool IsCacheFresh(CacheEntry entry)
|
||||||
=> (DateTime.UtcNow - entry.CreatedUtc) <= _characterCacheTtl;
|
=> (DateTime.UtcNow - entry.CreatedUtc) <= _characterCacheTtl;
|
||||||
@@ -154,10 +154,10 @@ public class PlayerDataFactory
|
|||||||
{
|
{
|
||||||
var key = obj.Address;
|
var key = obj.Address;
|
||||||
|
|
||||||
if (_characterBuildCache.TryGetValue(key, out var cached) && IsCacheFresh(cached) && !_characterBuildInflight.ContainsKey(key))
|
if (_characterBuildCache.TryGetValue(key, out CacheEntry cached) && IsCacheFresh(cached) && !_characterBuildInflight.TryGetExisting(key, out _))
|
||||||
return cached.Fragment;
|
return cached.Fragment;
|
||||||
|
|
||||||
var buildTask = _characterBuildInflight.GetOrAdd(key, valueFactory: k => BuildAndCacheAsync(obj, k));
|
Task<CharacterDataFragment> buildTask = _characterBuildInflight.GetOrStart(key, () => BuildAndCacheAsync(obj, key));
|
||||||
|
|
||||||
if (_characterBuildCache.TryGetValue(key, out cached))
|
if (_characterBuildCache.TryGetValue(key, out cached))
|
||||||
{
|
{
|
||||||
@@ -172,22 +172,15 @@ public class PlayerDataFactory
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async Task<CharacterDataFragment> BuildAndCacheAsync(GameObjectHandler obj, nint key)
|
private async Task<CharacterDataFragment> BuildAndCacheAsync(GameObjectHandler obj, nint key)
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
using var cts = new CancellationTokenSource(_hardBuildTimeout);
|
using var cts = new CancellationTokenSource(_hardBuildTimeout);
|
||||||
var fragment = await CreateCharacterDataInternal(obj, cts.Token).ConfigureAwait(false);
|
CharacterDataFragment fragment = await CreateCharacterDataInternal(obj, cts.Token).ConfigureAwait(false);
|
||||||
|
|
||||||
_characterBuildCache[key] = new CacheEntry(fragment, DateTime.UtcNow);
|
_characterBuildCache[key] = new CacheEntry(fragment, DateTime.UtcNow);
|
||||||
PruneCharacterCacheIfNeeded();
|
PruneCharacterCacheIfNeeded();
|
||||||
|
|
||||||
return fragment;
|
return fragment;
|
||||||
}
|
}
|
||||||
finally
|
|
||||||
{
|
|
||||||
_characterBuildInflight.TryRemove(key, out _);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void PruneCharacterCacheIfNeeded()
|
private void PruneCharacterCacheIfNeeded()
|
||||||
{
|
{
|
||||||
@@ -241,7 +234,28 @@ public class PlayerDataFactory
|
|||||||
getMoodlesData = _ipcManager.Moodles.GetStatusAsync(playerRelatedObject.Address);
|
getMoodlesData = _ipcManager.Moodles.GetStatusAsync(playerRelatedObject.Address);
|
||||||
}
|
}
|
||||||
|
|
||||||
var resolvedPaths = await _ipcManager.Penumbra.GetCharacterData(_logger, playerRelatedObject).ConfigureAwait(false) ?? throw new InvalidOperationException("Penumbra returned null data; couldn't proceed with character");
|
Guid penumbraRequestId = Guid.Empty;
|
||||||
|
Stopwatch? penumbraSw = null;
|
||||||
|
if (logDebug)
|
||||||
|
{
|
||||||
|
penumbraRequestId = Guid.NewGuid();
|
||||||
|
penumbraSw = Stopwatch.StartNew();
|
||||||
|
_logger.LogDebug("Penumbra GetCharacterData start {id} for {obj}", penumbraRequestId, playerRelatedObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
var resolvedPaths = await _ipcManager.Penumbra.GetCharacterData(_logger, playerRelatedObject).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (logDebug)
|
||||||
|
{
|
||||||
|
penumbraSw!.Stop();
|
||||||
|
_logger.LogDebug("Penumbra GetCharacterData done {id} in {elapsedMs}ms (count={count})",
|
||||||
|
penumbraRequestId,
|
||||||
|
penumbraSw.ElapsedMilliseconds,
|
||||||
|
resolvedPaths?.Count ?? -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resolvedPaths == null)
|
||||||
|
throw new InvalidOperationException("Penumbra returned null data; couldn't proceed with character");
|
||||||
ct.ThrowIfCancellationRequested();
|
ct.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
var staticBuildTask = Task.Run(() => BuildStaticReplacements(resolvedPaths), ct);
|
var staticBuildTask = Task.Run(() => BuildStaticReplacements(resolvedPaths), ct);
|
||||||
@@ -460,7 +474,7 @@ public class PlayerDataFactory
|
|||||||
if (transientPaths.Count == 0)
|
if (transientPaths.Count == 0)
|
||||||
return (new Dictionary<string, string[]>(StringComparer.Ordinal), clearedReplacements);
|
return (new Dictionary<string, string[]>(StringComparer.Ordinal), clearedReplacements);
|
||||||
|
|
||||||
var resolved = await GetFileReplacementsFromPaths(obj, transientPaths, new HashSet<string>(StringComparer.Ordinal))
|
var resolved = await GetFileReplacementsFromPaths(transientPaths, new HashSet<string>(StringComparer.Ordinal))
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
if (_maxTransientResolvedEntries > 0 && resolved.Count > _maxTransientResolvedEntries)
|
if (_maxTransientResolvedEntries > 0 && resolved.Count > _maxTransientResolvedEntries)
|
||||||
@@ -662,7 +676,6 @@ public class PlayerDataFactory
|
|||||||
|
|
||||||
|
|
||||||
private async Task<IReadOnlyDictionary<string, string[]>> GetFileReplacementsFromPaths(
|
private async Task<IReadOnlyDictionary<string, string[]>> GetFileReplacementsFromPaths(
|
||||||
GameObjectHandler handler,
|
|
||||||
HashSet<string> forwardResolve,
|
HashSet<string> forwardResolve,
|
||||||
HashSet<string> reverseResolve)
|
HashSet<string> reverseResolve)
|
||||||
{
|
{
|
||||||
@@ -677,59 +690,6 @@ public class PlayerDataFactory
|
|||||||
var reversePathsLower = reversePaths.Length == 0 ? [] : reversePaths.Select(p => p.ToLowerInvariant()).ToArray();
|
var reversePathsLower = reversePaths.Length == 0 ? [] : reversePaths.Select(p => p.ToLowerInvariant()).ToArray();
|
||||||
|
|
||||||
Dictionary<string, List<string>> resolvedPaths = new(forwardPaths.Length + reversePaths.Length, StringComparer.Ordinal);
|
Dictionary<string, List<string>> resolvedPaths = new(forwardPaths.Length + reversePaths.Length, StringComparer.Ordinal);
|
||||||
if (handler.ObjectKind != ObjectKind.Player)
|
|
||||||
{
|
|
||||||
var (objectIndex, forwardResolved, reverseResolved) = await _dalamudUtil.RunOnFrameworkThread(() =>
|
|
||||||
{
|
|
||||||
var idx = handler.GetGameObject()?.ObjectIndex;
|
|
||||||
if (!idx.HasValue)
|
|
||||||
return ((int?)null, Array.Empty<string>(), Array.Empty<string[]>());
|
|
||||||
|
|
||||||
var resolvedForward = new string[forwardPaths.Length];
|
|
||||||
for (int i = 0; i < forwardPaths.Length; i++)
|
|
||||||
resolvedForward[i] = _ipcManager.Penumbra.ResolveGameObjectPath(forwardPaths[i], idx.Value);
|
|
||||||
|
|
||||||
var resolvedReverse = new string[reversePaths.Length][];
|
|
||||||
for (int i = 0; i < reversePaths.Length; i++)
|
|
||||||
resolvedReverse[i] = _ipcManager.Penumbra.ReverseResolveGameObjectPath(reversePaths[i], idx.Value);
|
|
||||||
|
|
||||||
return (idx, resolvedForward, resolvedReverse);
|
|
||||||
}).ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (objectIndex.HasValue)
|
|
||||||
{
|
|
||||||
for (int i = 0; i < forwardPaths.Length; i++)
|
|
||||||
{
|
|
||||||
var filePath = forwardResolved[i]?.ToLowerInvariant();
|
|
||||||
if (string.IsNullOrEmpty(filePath))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (resolvedPaths.TryGetValue(filePath, out var list))
|
|
||||||
list.Add(forwardPaths[i].ToLowerInvariant());
|
|
||||||
else
|
|
||||||
{
|
|
||||||
resolvedPaths[filePath] = [forwardPathsLower[i]];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = 0; i < reversePaths.Length; i++)
|
|
||||||
{
|
|
||||||
var filePath = reversePathsLower[i];
|
|
||||||
var reverseResolvedLower = new string[reverseResolved[i].Length];
|
|
||||||
for (var j = 0; j < reverseResolvedLower.Length; j++)
|
|
||||||
{
|
|
||||||
reverseResolvedLower[j] = reverseResolved[i][j].ToLowerInvariant();
|
|
||||||
}
|
|
||||||
if (resolvedPaths.TryGetValue(filePath, out var list))
|
|
||||||
list.AddRange(reverseResolved[i].Select(c => c.ToLowerInvariant()));
|
|
||||||
else
|
|
||||||
resolvedPaths[filePath] = [.. reverseResolved[i].Select(c => c.ToLowerInvariant()).ToList()];
|
|
||||||
}
|
|
||||||
|
|
||||||
return resolvedPaths.ToDictionary(k => k.Key, k => k.Value.ToArray(), StringComparer.OrdinalIgnoreCase).AsReadOnly();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var (forward, reverse) = await _ipcManager.Penumbra.ResolvePathsAsync(forwardPaths, reversePaths).ConfigureAwait(false);
|
var (forward, reverse) = await _ipcManager.Penumbra.ResolvePathsAsync(forwardPaths, reversePaths).ConfigureAwait(false);
|
||||||
|
|
||||||
for (int i = 0; i < forwardPaths.Length; i++)
|
for (int i = 0; i < forwardPaths.Length; i++)
|
||||||
|
|||||||
@@ -1,68 +1,46 @@
|
|||||||
using Dalamud.Game.ClientState.Objects.Enums;
|
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
||||||
using Dalamud.Game.ClientState.Objects.Types;
|
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
||||||
using Dalamud.Plugin.Services;
|
|
||||||
using LightlessSync.Services;
|
using LightlessSync.Services;
|
||||||
using LightlessSync.Services.Mediator;
|
using LightlessSync.Services.Mediator;
|
||||||
|
using LightlessSync.Utils;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using static FFXIVClientStructs.FFXIV.Client.Game.Character.DrawDataContainer;
|
||||||
using ObjectKind = LightlessSync.API.Data.Enum.ObjectKind;
|
using ObjectKind = LightlessSync.API.Data.Enum.ObjectKind;
|
||||||
|
using VisibilityFlags = FFXIVClientStructs.FFXIV.Client.Game.Object.VisibilityFlags;
|
||||||
|
|
||||||
namespace LightlessSync.PlayerData.Handlers;
|
namespace LightlessSync.PlayerData.Handlers;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Game object handler for managing game object state and updates
|
|
||||||
/// </summary>
|
|
||||||
public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighPriorityMediatorSubscriber
|
public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighPriorityMediatorSubscriber
|
||||||
{
|
{
|
||||||
private readonly DalamudUtilService _dalamudUtil;
|
private readonly DalamudUtilService _dalamudUtil;
|
||||||
private readonly IObjectTable _objectTable;
|
|
||||||
private readonly Func<IntPtr> _getAddress;
|
private readonly Func<IntPtr> _getAddress;
|
||||||
private readonly bool _isOwnedObject;
|
private readonly bool _isOwnedObject;
|
||||||
private readonly PerformanceCollectorService _performanceCollector;
|
private readonly PerformanceCollectorService _performanceCollector;
|
||||||
private readonly Lock _frameworkUpdateGate = new();
|
private readonly object _frameworkUpdateGate = new();
|
||||||
private bool _frameworkUpdateSubscribed;
|
private bool _frameworkUpdateSubscribed;
|
||||||
private byte _classJob = 0;
|
private byte _classJob = 0;
|
||||||
private Task? _delayedZoningTask;
|
private Task? _delayedZoningTask;
|
||||||
private bool _haltProcessing = false;
|
private bool _haltProcessing = false;
|
||||||
private CancellationTokenSource _zoningCts = new();
|
private CancellationTokenSource _zoningCts = new();
|
||||||
|
|
||||||
/// <summary>
|
public GameObjectHandler(ILogger<GameObjectHandler> logger, PerformanceCollectorService performanceCollector,
|
||||||
/// Constructor for GameObjectHandler
|
LightlessMediator mediator, DalamudUtilService dalamudUtil, ObjectKind objectKind, Func<IntPtr> getAddress, bool ownedObject = true) : base(logger, mediator)
|
||||||
/// </summary>
|
|
||||||
/// <param name="logger">Logger</param>
|
|
||||||
/// <param name="performanceCollector">Performance Collector</param>
|
|
||||||
/// <param name="mediator">Lightless Mediator</param>
|
|
||||||
/// <param name="dalamudUtil">Dalamud Utilties Service</param>
|
|
||||||
/// <param name="objectKind">Object kind of Object</param>
|
|
||||||
/// <param name="getAddress">Get Adress</param>
|
|
||||||
/// <param name="objectTable">Object table of Dalamud</param>
|
|
||||||
/// <param name="ownedObject">Object is owned by user</param>
|
|
||||||
public GameObjectHandler(
|
|
||||||
ILogger<GameObjectHandler> logger,
|
|
||||||
PerformanceCollectorService performanceCollector,
|
|
||||||
LightlessMediator mediator,
|
|
||||||
DalamudUtilService dalamudUtil,
|
|
||||||
ObjectKind objectKind,
|
|
||||||
Func<IntPtr> getAddress,
|
|
||||||
IObjectTable objectTable,
|
|
||||||
bool ownedObject = true) : base(logger, mediator)
|
|
||||||
{
|
{
|
||||||
_performanceCollector = performanceCollector;
|
_performanceCollector = performanceCollector;
|
||||||
ObjectKind = objectKind;
|
ObjectKind = objectKind;
|
||||||
_dalamudUtil = dalamudUtil;
|
_dalamudUtil = dalamudUtil;
|
||||||
_objectTable = objectTable;
|
|
||||||
|
|
||||||
_getAddress = () =>
|
_getAddress = () =>
|
||||||
{
|
{
|
||||||
_dalamudUtil.EnsureIsOnFramework();
|
_dalamudUtil.EnsureIsOnFramework();
|
||||||
return getAddress.Invoke();
|
return getAddress.Invoke();
|
||||||
};
|
};
|
||||||
|
|
||||||
_isOwnedObject = ownedObject;
|
_isOwnedObject = ownedObject;
|
||||||
Name = string.Empty;
|
Name = string.Empty;
|
||||||
|
|
||||||
if (ownedObject)
|
if (ownedObject)
|
||||||
{
|
{
|
||||||
Mediator.Subscribe<TransientResourceChangedMessage>(this, msg =>
|
Mediator.Subscribe<TransientResourceChangedMessage>(this, (msg) =>
|
||||||
{
|
{
|
||||||
if (_delayedZoningTask?.IsCompleted ?? true)
|
if (_delayedZoningTask?.IsCompleted ?? true)
|
||||||
{
|
{
|
||||||
@@ -72,36 +50,43 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_isOwnedObject)
|
||||||
|
{
|
||||||
EnableFrameworkUpdates();
|
EnableFrameworkUpdates();
|
||||||
|
}
|
||||||
|
|
||||||
Mediator.Subscribe<ZoneSwitchEndMessage>(this, _ => ZoneSwitchEnd());
|
Mediator.Subscribe<ZoneSwitchEndMessage>(this, (_) => ZoneSwitchEnd());
|
||||||
Mediator.Subscribe<ZoneSwitchStartMessage>(this, _ => ZoneSwitchStart());
|
Mediator.Subscribe<ZoneSwitchStartMessage>(this, (_) => ZoneSwitchStart());
|
||||||
|
|
||||||
Mediator.Subscribe<CutsceneStartMessage>(this, _ => _haltProcessing = true);
|
Mediator.Subscribe<CutsceneStartMessage>(this, (_) =>
|
||||||
Mediator.Subscribe<CutsceneEndMessage>(this, _ =>
|
{
|
||||||
|
_haltProcessing = true;
|
||||||
|
});
|
||||||
|
Mediator.Subscribe<CutsceneEndMessage>(this, (_) =>
|
||||||
{
|
{
|
||||||
_haltProcessing = false;
|
_haltProcessing = false;
|
||||||
ZoneSwitchEnd();
|
ZoneSwitchEnd();
|
||||||
});
|
});
|
||||||
|
Mediator.Subscribe<PenumbraStartRedrawMessage>(this, (msg) =>
|
||||||
Mediator.Subscribe<PenumbraStartRedrawMessage>(this, msg =>
|
|
||||||
{
|
{
|
||||||
if (msg.Address == Address) _haltProcessing = true;
|
if (msg.Address == Address)
|
||||||
|
{
|
||||||
|
_haltProcessing = true;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
Mediator.Subscribe<PenumbraEndRedrawMessage>(this, msg =>
|
Mediator.Subscribe<PenumbraEndRedrawMessage>(this, (msg) =>
|
||||||
{
|
{
|
||||||
if (msg.Address == Address) _haltProcessing = false;
|
if (msg.Address == Address)
|
||||||
|
{
|
||||||
|
_haltProcessing = false;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Mediator.Publish(new GameObjectHandlerCreatedMessage(this, _isOwnedObject));
|
Mediator.Publish(new GameObjectHandlerCreatedMessage(this, _isOwnedObject));
|
||||||
|
|
||||||
_dalamudUtil.EnsureIsOnFramework();
|
_dalamudUtil.RunOnFrameworkThread(CheckAndUpdateObject).GetAwaiter().GetResult();
|
||||||
CheckAndUpdateObject(allowPublish: true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Draw Condition Enum
|
|
||||||
/// </summary>
|
|
||||||
public enum DrawCondition
|
public enum DrawCondition
|
||||||
{
|
{
|
||||||
None,
|
None,
|
||||||
@@ -112,7 +97,6 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
|||||||
ModelFilesInSlotLoaded
|
ModelFilesInSlotLoaded
|
||||||
}
|
}
|
||||||
|
|
||||||
// Properties
|
|
||||||
public IntPtr Address { get; private set; }
|
public IntPtr Address { get; private set; }
|
||||||
public DrawCondition CurrentDrawCondition { get; set; } = DrawCondition.None;
|
public DrawCondition CurrentDrawCondition { get; set; } = DrawCondition.None;
|
||||||
public byte Gender { get; private set; }
|
public byte Gender { get; private set; }
|
||||||
@@ -123,21 +107,18 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
|||||||
public byte TribeId { get; private set; }
|
public byte TribeId { get; private set; }
|
||||||
private byte[] CustomizeData { get; set; } = new byte[26];
|
private byte[] CustomizeData { get; set; } = new byte[26];
|
||||||
private IntPtr DrawObjectAddress { get; set; }
|
private IntPtr DrawObjectAddress { get; set; }
|
||||||
|
private byte[] EquipSlotData { get; set; } = new byte[40];
|
||||||
|
private ushort[] MainHandData { get; set; } = new ushort[3];
|
||||||
|
private ushort[] OffHandData { get; set; } = new ushort[3];
|
||||||
|
|
||||||
/// <summary>
|
public async Task ActOnFrameworkAfterEnsureNoDrawAsync(Action<Dalamud.Game.ClientState.Objects.Types.ICharacter> act, CancellationToken token)
|
||||||
/// Act on framework thread after ensuring no draw condition
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="act">Action of Character</param>
|
|
||||||
/// <param name="token">Cancellation Token</param>
|
|
||||||
/// <returns>Task Completion</returns>
|
|
||||||
public async Task ActOnFrameworkAfterEnsureNoDrawAsync(Action<ICharacter> act, CancellationToken token)
|
|
||||||
{
|
{
|
||||||
while (await _dalamudUtil.RunOnFrameworkThread(() =>
|
while (await _dalamudUtil.RunOnFrameworkThread(() =>
|
||||||
{
|
{
|
||||||
EnsureLatestObjectState();
|
EnsureLatestObjectState();
|
||||||
if (CurrentDrawCondition != DrawCondition.None) return true;
|
if (CurrentDrawCondition != DrawCondition.None) return true;
|
||||||
var gameObj = _dalamudUtil.CreateGameObject(Address);
|
var gameObj = _dalamudUtil.CreateGameObject(Address);
|
||||||
if (gameObj is ICharacter chara)
|
if (gameObj is Dalamud.Game.ClientState.Objects.Types.ICharacter chara)
|
||||||
{
|
{
|
||||||
act.Invoke(chara);
|
act.Invoke(chara);
|
||||||
}
|
}
|
||||||
@@ -148,11 +129,6 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Compare Name And Throw if not equal
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="name">Name that will be compared to Object Handler.</param>
|
|
||||||
/// <exception cref="InvalidOperationException">Not equal if thrown</exception>
|
|
||||||
public void CompareNameAndThrow(string name)
|
public void CompareNameAndThrow(string name)
|
||||||
{
|
{
|
||||||
if (!string.Equals(Name, name, StringComparison.OrdinalIgnoreCase))
|
if (!string.Equals(Name, name, StringComparison.OrdinalIgnoreCase))
|
||||||
@@ -165,18 +141,11 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
public Dalamud.Game.ClientState.Objects.Types.IGameObject? GetGameObject()
|
||||||
/// Gets the game object from the address
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>Gane object</returns>
|
|
||||||
public IGameObject? GetGameObject()
|
|
||||||
{
|
{
|
||||||
return _dalamudUtil.CreateGameObject(Address);
|
return _dalamudUtil.CreateGameObject(Address);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Invalidate the object handler
|
|
||||||
/// </summary>
|
|
||||||
public void Invalidate()
|
public void Invalidate()
|
||||||
{
|
{
|
||||||
Address = IntPtr.Zero;
|
Address = IntPtr.Zero;
|
||||||
@@ -185,203 +154,182 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
|||||||
_haltProcessing = false;
|
_haltProcessing = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Refresh the object handler state
|
|
||||||
/// </summary>
|
|
||||||
public void Refresh()
|
public void Refresh()
|
||||||
{
|
{
|
||||||
_dalamudUtil.RunOnFrameworkThread(CheckAndUpdateObject).GetAwaiter().GetResult();
|
_dalamudUtil.RunOnFrameworkThread(CheckAndUpdateObject).GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Is Being Drawn Run On Framework Asyncronously
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>Object is being run in framework</returns>
|
|
||||||
public async Task<bool> IsBeingDrawnRunOnFrameworkAsync()
|
public async Task<bool> IsBeingDrawnRunOnFrameworkAsync()
|
||||||
{
|
{
|
||||||
return await _dalamudUtil.RunOnFrameworkThread(IsBeingDrawn).ConfigureAwait(false);
|
return await _dalamudUtil.RunOnFrameworkThread(IsBeingDrawn).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Override ToString method for GameObjectHandler
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>String</returns>
|
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
var owned = _isOwnedObject ? "Self" : "Other";
|
var owned = _isOwnedObject ? "Self" : "Other";
|
||||||
return $"{owned}/{ObjectKind}:{Name} ({Address:X},{DrawObjectAddress:X})";
|
return $"{owned}/{ObjectKind}:{Name} ({Address:X},{DrawObjectAddress:X})";
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Try Get Object By Address from Object Table
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="address">Object address</param>
|
|
||||||
/// <returns>Game Object of adress</returns>
|
|
||||||
private IGameObject? TryGetObjectByAddress(nint address)
|
|
||||||
{
|
|
||||||
if (address == nint.Zero) return null;
|
|
||||||
|
|
||||||
// Search object table
|
|
||||||
foreach (var obj in _objectTable)
|
|
||||||
{
|
|
||||||
if (obj is null) continue;
|
|
||||||
if (obj.Address == address)
|
|
||||||
return obj;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Checks and updates the object state
|
|
||||||
/// </summary>
|
|
||||||
private void CheckAndUpdateObject() => CheckAndUpdateObject(allowPublish: true);
|
private void CheckAndUpdateObject() => CheckAndUpdateObject(allowPublish: true);
|
||||||
|
|
||||||
/// <summary>
|
private unsafe void CheckAndUpdateObject(bool allowPublish)
|
||||||
/// Checks and updates the object state with option to allow publish
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="allowPublish">Allows to publish the object</param>
|
|
||||||
private void CheckAndUpdateObject(bool allowPublish)
|
|
||||||
{
|
{
|
||||||
var prevAddr = Address;
|
var prevAddr = Address;
|
||||||
var prevDrawObj = DrawObjectAddress;
|
var prevDrawObj = DrawObjectAddress;
|
||||||
string? nameString = null;
|
string? nameString = null;
|
||||||
|
|
||||||
Address = _getAddress();
|
var nextAddr = _getAddress();
|
||||||
|
|
||||||
IGameObject? obj = null;
|
if (nextAddr != IntPtr.Zero && !PtrGuard.LooksLikePtr(nextAddr))
|
||||||
ICharacter? chara = null;
|
|
||||||
|
|
||||||
if (Address != nint.Zero)
|
|
||||||
{
|
{
|
||||||
// Try get object
|
nextAddr = IntPtr.Zero;
|
||||||
obj = TryGetObjectByAddress(Address);
|
}
|
||||||
|
|
||||||
if (obj is not null)
|
if (nextAddr != IntPtr.Zero &&
|
||||||
|
!PtrGuard.IsReadable(nextAddr, (nuint)sizeof(FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject)))
|
||||||
{
|
{
|
||||||
EntityId = obj.EntityId;
|
nextAddr = IntPtr.Zero;
|
||||||
|
}
|
||||||
|
|
||||||
DrawObjectAddress = Address;
|
Address = nextAddr;
|
||||||
|
|
||||||
|
if (Address != IntPtr.Zero)
|
||||||
|
{
|
||||||
|
var gameObject = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)Address;
|
||||||
|
|
||||||
|
var draw = (nint)gameObject->DrawObject;
|
||||||
|
|
||||||
|
if (!PtrGuard.LooksLikePtr(draw) || !PtrGuard.IsReadable(draw, (nuint)sizeof(DrawObject)))
|
||||||
|
draw = 0;
|
||||||
|
|
||||||
|
DrawObjectAddress = draw;
|
||||||
|
EntityId = gameObject->EntityId;
|
||||||
|
|
||||||
|
if (PtrGuard.IsReadable(Address, (nuint)sizeof(Character)))
|
||||||
|
{
|
||||||
|
var chara = (Character*)Address;
|
||||||
|
nameString = chara->GameObject.NameString;
|
||||||
|
|
||||||
// Name update
|
|
||||||
nameString = obj.Name.TextValue ?? string.Empty;
|
|
||||||
if (!string.IsNullOrEmpty(nameString) && !string.Equals(nameString, Name, StringComparison.Ordinal))
|
if (!string.IsNullOrEmpty(nameString) && !string.Equals(nameString, Name, StringComparison.Ordinal))
|
||||||
Name = nameString;
|
Name = nameString;
|
||||||
|
|
||||||
chara = obj as ICharacter;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
DrawObjectAddress = nint.Zero;
|
|
||||||
EntityId = uint.MaxValue;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
DrawObjectAddress = nint.Zero;
|
DrawObjectAddress = IntPtr.Zero;
|
||||||
EntityId = uint.MaxValue;
|
EntityId = uint.MaxValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update draw condition
|
CurrentDrawCondition = (Address != IntPtr.Zero && DrawObjectAddress != IntPtr.Zero)
|
||||||
CurrentDrawCondition = IsBeingDrawnSafe(obj, chara);
|
? IsBeingDrawnUnsafe()
|
||||||
|
: DrawCondition.DrawObjectZero;
|
||||||
|
|
||||||
if (_haltProcessing || !allowPublish) return;
|
if (_haltProcessing || !allowPublish) return;
|
||||||
|
|
||||||
// Determine differences
|
|
||||||
bool drawObjDiff = DrawObjectAddress != prevDrawObj;
|
bool drawObjDiff = DrawObjectAddress != prevDrawObj;
|
||||||
bool addrDiff = Address != prevAddr;
|
bool addrDiff = Address != prevAddr;
|
||||||
|
|
||||||
// Name change check
|
if (Address != IntPtr.Zero && DrawObjectAddress != IntPtr.Zero
|
||||||
bool nameChange = false;
|
&& PtrGuard.IsReadable(Address, (nuint)sizeof(Character))
|
||||||
if (nameString is not null)
|
&& PtrGuard.IsReadable(DrawObjectAddress, (nuint)sizeof(DrawObject)))
|
||||||
{
|
{
|
||||||
nameChange = !string.Equals(nameString, Name, StringComparison.Ordinal);
|
var chara = (Character*)Address;
|
||||||
if (nameChange) Name = nameString;
|
var drawObj = (DrawObject*)DrawObjectAddress;
|
||||||
}
|
|
||||||
|
|
||||||
// Customize data change check
|
var objType = drawObj->Object.GetObjectType();
|
||||||
bool customizeDiff = false;
|
var isHuman = objType == ObjectType.CharacterBase
|
||||||
if (chara is not null)
|
&& ((CharacterBase*)drawObj)->GetModelType() == CharacterBase.ModelType.Human;
|
||||||
|
|
||||||
|
nameString ??= chara->GameObject.NameString;
|
||||||
|
var nameChange = !string.Equals(nameString, Name, StringComparison.Ordinal);
|
||||||
|
if (nameChange) Name = nameString;
|
||||||
|
|
||||||
|
bool equipDiff = false;
|
||||||
|
|
||||||
|
if (isHuman)
|
||||||
{
|
{
|
||||||
// Class job change check
|
if (PtrGuard.IsReadable(DrawObjectAddress, (nuint)sizeof(Human)))
|
||||||
var classJob = chara.ClassJob.RowId;
|
{
|
||||||
|
var classJob = chara->CharacterData.ClassJob;
|
||||||
if (classJob != _classJob)
|
if (classJob != _classJob)
|
||||||
{
|
{
|
||||||
Logger.LogTrace("[{this}] classjob changed from {old} to {new}", this, _classJob, classJob);
|
Logger.LogTrace("[{this}] classjob changed from {old} to {new}", this, _classJob, classJob);
|
||||||
_classJob = (byte)classJob;
|
_classJob = classJob;
|
||||||
Mediator.Publish(new ClassJobChangedMessage(this));
|
Mediator.Publish(new ClassJobChangedMessage(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Customize data comparison
|
equipDiff = CompareAndUpdateEquipByteData((byte*)&((Human*)drawObj)->Head);
|
||||||
customizeDiff = CompareAndUpdateCustomizeData(chara.Customize);
|
|
||||||
|
|
||||||
// Census update publish
|
ref var mh = ref chara->DrawData.Weapon(WeaponSlot.MainHand);
|
||||||
if (_isOwnedObject && ObjectKind == ObjectKind.Player && chara.Customize.Length > (int)CustomizeIndex.Tribe)
|
ref var oh = ref chara->DrawData.Weapon(WeaponSlot.OffHand);
|
||||||
|
|
||||||
|
equipDiff |= CompareAndUpdateMainHand((Weapon*)mh.DrawObject);
|
||||||
|
equipDiff |= CompareAndUpdateOffHand((Weapon*)oh.DrawObject);
|
||||||
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
var gender = chara.Customize[(int)CustomizeIndex.Gender];
|
isHuman = false;
|
||||||
var raceId = chara.Customize[(int)CustomizeIndex.Race];
|
}
|
||||||
var tribeId = chara.Customize[(int)CustomizeIndex.Tribe];
|
}
|
||||||
|
|
||||||
if (gender != Gender || raceId != RaceId || tribeId != TribeId)
|
if (!isHuman)
|
||||||
|
{
|
||||||
|
equipDiff = CompareAndUpdateEquipByteData((byte*)Unsafe.AsPointer(ref chara->DrawData.EquipmentModelIds[0]));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (equipDiff && !_isOwnedObject)
|
||||||
|
{
|
||||||
|
Logger.LogTrace("[{this}] Changed", this);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool customizeDiff = false;
|
||||||
|
|
||||||
|
if (isHuman && PtrGuard.IsReadable(DrawObjectAddress, (nuint)sizeof(Human)))
|
||||||
|
{
|
||||||
|
var human = (Human*)drawObj;
|
||||||
|
|
||||||
|
var gender = human->Customize.Sex;
|
||||||
|
var raceId = human->Customize.Race;
|
||||||
|
var tribeId = human->Customize.Tribe;
|
||||||
|
|
||||||
|
if (_isOwnedObject && ObjectKind == ObjectKind.Player
|
||||||
|
&& (gender != Gender || raceId != RaceId || tribeId != TribeId))
|
||||||
{
|
{
|
||||||
Mediator.Publish(new CensusUpdateMessage(gender, raceId, tribeId));
|
Mediator.Publish(new CensusUpdateMessage(gender, raceId, tribeId));
|
||||||
Gender = gender;
|
Gender = gender;
|
||||||
RaceId = raceId;
|
RaceId = raceId;
|
||||||
TribeId = tribeId;
|
TribeId = tribeId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
customizeDiff = CompareAndUpdateCustomizeData(human->Customize.Data);
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
customizeDiff = CompareAndUpdateCustomizeData(chara->DrawData.CustomizeData.Data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ((addrDiff || drawObjDiff || equipDiff || customizeDiff || nameChange) && _isOwnedObject)
|
||||||
if ((addrDiff || drawObjDiff || customizeDiff || nameChange) && _isOwnedObject)
|
|
||||||
{
|
{
|
||||||
Logger.LogDebug("[{this}] Changed, Sending CreateCacheObjectMessage", this);
|
Logger.LogDebug("[{this}] Changed, Sending CreateCacheObjectMessage", this);
|
||||||
Mediator.Publish(new CreateCacheForObjectMessage(this));
|
Mediator.Publish(new CreateCacheForObjectMessage(this));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
else if (addrDiff || drawObjDiff)
|
else if (addrDiff || drawObjDiff)
|
||||||
{
|
{
|
||||||
if (Address == nint.Zero)
|
|
||||||
CurrentDrawCondition = DrawCondition.ObjectZero;
|
|
||||||
else if (DrawObjectAddress == nint.Zero)
|
|
||||||
CurrentDrawCondition = DrawCondition.DrawObjectZero;
|
CurrentDrawCondition = DrawCondition.DrawObjectZero;
|
||||||
|
|
||||||
Logger.LogTrace("[{this}] Changed", this);
|
Logger.LogTrace("[{this}] Changed", this);
|
||||||
|
|
||||||
if (_isOwnedObject && ObjectKind != ObjectKind.Player)
|
if (_isOwnedObject && ObjectKind != ObjectKind.Player)
|
||||||
Mediator.Publish(new ClearCacheForObjectMessage(this));
|
Mediator.Publish(new ClearCacheForObjectMessage(this));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Is object being drawn safe check
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="obj">Object thats being checked</param>
|
|
||||||
/// <param name="chara">Character of the object</param>
|
|
||||||
/// <returns>Draw Condition of character</returns>
|
|
||||||
private DrawCondition IsBeingDrawnSafe(IGameObject? obj, ICharacter? chara)
|
|
||||||
{
|
|
||||||
// Object zero check
|
|
||||||
if (Address == nint.Zero) return DrawCondition.ObjectZero;
|
|
||||||
if (obj is null) return DrawCondition.DrawObjectZero;
|
|
||||||
|
|
||||||
// Draw Object check
|
private unsafe bool CompareAndUpdateCustomizeData(Span<byte> customizeData)
|
||||||
if (chara is not null && (chara.Customize is null || chara.Customize.Length == 0))
|
|
||||||
return DrawCondition.DrawObjectZero;
|
|
||||||
|
|
||||||
return DrawCondition.None;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Compare and update customize data of character
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="customizeData">Customize+ data of object</param>
|
|
||||||
/// <returns>Successfully applied or not</returns>
|
|
||||||
private bool CompareAndUpdateCustomizeData(ReadOnlySpan<byte> customizeData)
|
|
||||||
{
|
{
|
||||||
bool hasChanges = false;
|
bool hasChanges = false;
|
||||||
|
|
||||||
// Resize if needed
|
for (int i = 0; i < customizeData.Length; i++)
|
||||||
var len = Math.Min(customizeData.Length, CustomizeData.Length);
|
|
||||||
for (int i = 0; i < len; i++)
|
|
||||||
{
|
{
|
||||||
var data = customizeData[i];
|
var data = customizeData[i];
|
||||||
if (CustomizeData[i] != data)
|
if (CustomizeData[i] != data)
|
||||||
@@ -394,9 +342,54 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
|||||||
return hasChanges;
|
return hasChanges;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
private unsafe bool CompareAndUpdateEquipByteData(byte* equipSlotData)
|
||||||
/// Framework update method
|
{
|
||||||
/// </summary>
|
bool hasChanges = false;
|
||||||
|
for (int i = 0; i < EquipSlotData.Length; i++)
|
||||||
|
{
|
||||||
|
var data = equipSlotData[i];
|
||||||
|
if (EquipSlotData[i] != data)
|
||||||
|
{
|
||||||
|
EquipSlotData[i] = data;
|
||||||
|
hasChanges = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return hasChanges;
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe bool CompareAndUpdateMainHand(Weapon* weapon)
|
||||||
|
{
|
||||||
|
var p = (nint)weapon;
|
||||||
|
if (!PtrGuard.LooksLikePtr(p) || !PtrGuard.IsReadable(p, (nuint)sizeof(Weapon)))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
bool hasChanges = false;
|
||||||
|
hasChanges |= weapon->ModelSetId != MainHandData[0];
|
||||||
|
MainHandData[0] = weapon->ModelSetId;
|
||||||
|
hasChanges |= weapon->Variant != MainHandData[1];
|
||||||
|
MainHandData[1] = weapon->Variant;
|
||||||
|
hasChanges |= weapon->SecondaryId != MainHandData[2];
|
||||||
|
MainHandData[2] = weapon->SecondaryId;
|
||||||
|
return hasChanges;
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe bool CompareAndUpdateOffHand(Weapon* weapon)
|
||||||
|
{
|
||||||
|
var p = (nint)weapon;
|
||||||
|
if (!PtrGuard.LooksLikePtr(p) || !PtrGuard.IsReadable(p, (nuint)sizeof(Weapon)))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
bool hasChanges = false;
|
||||||
|
hasChanges |= weapon->ModelSetId != OffHandData[0];
|
||||||
|
OffHandData[0] = weapon->ModelSetId;
|
||||||
|
hasChanges |= weapon->Variant != OffHandData[1];
|
||||||
|
OffHandData[1] = weapon->Variant;
|
||||||
|
hasChanges |= weapon->SecondaryId != OffHandData[2];
|
||||||
|
OffHandData[2] = weapon->SecondaryId;
|
||||||
|
return hasChanges;
|
||||||
|
}
|
||||||
|
|
||||||
private void FrameworkUpdate()
|
private void FrameworkUpdate()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -410,10 +403,6 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Is object being drawn check
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>Is being drawn</returns>
|
|
||||||
private bool IsBeingDrawn()
|
private bool IsBeingDrawn()
|
||||||
{
|
{
|
||||||
EnsureLatestObjectState();
|
EnsureLatestObjectState();
|
||||||
@@ -428,9 +417,6 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
|||||||
return CurrentDrawCondition != DrawCondition.None;
|
return CurrentDrawCondition != DrawCondition.None;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Ensures the latest object state
|
|
||||||
/// </summary>
|
|
||||||
private void EnsureLatestObjectState()
|
private void EnsureLatestObjectState()
|
||||||
{
|
{
|
||||||
if (_haltProcessing || !_frameworkUpdateSubscribed)
|
if (_haltProcessing || !_frameworkUpdateSubscribed)
|
||||||
@@ -439,9 +425,6 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Enables framework updates for the object handler
|
|
||||||
/// </summary>
|
|
||||||
private void EnableFrameworkUpdates()
|
private void EnableFrameworkUpdates()
|
||||||
{
|
{
|
||||||
lock (_frameworkUpdateGate)
|
lock (_frameworkUpdateGate)
|
||||||
@@ -456,9 +439,24 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
private unsafe DrawCondition IsBeingDrawnUnsafe()
|
||||||
/// Zone switch end handling
|
{
|
||||||
/// </summary>
|
if (Address == IntPtr.Zero) return DrawCondition.ObjectZero;
|
||||||
|
if (DrawObjectAddress == IntPtr.Zero) return DrawCondition.DrawObjectZero;
|
||||||
|
var visibilityFlags = ((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)Address)->RenderFlags;
|
||||||
|
if (visibilityFlags != VisibilityFlags.None) return DrawCondition.RenderFlags;
|
||||||
|
|
||||||
|
if (ObjectKind == ObjectKind.Player)
|
||||||
|
{
|
||||||
|
var modelInSlotLoaded = (((CharacterBase*)DrawObjectAddress)->HasModelInSlotLoaded != 0);
|
||||||
|
if (modelInSlotLoaded) return DrawCondition.ModelInSlotLoaded;
|
||||||
|
var modelFilesInSlotLoaded = (((CharacterBase*)DrawObjectAddress)->HasModelFilesInSlotLoaded != 0);
|
||||||
|
if (modelFilesInSlotLoaded) return DrawCondition.ModelFilesInSlotLoaded;
|
||||||
|
}
|
||||||
|
|
||||||
|
return DrawCondition.None;
|
||||||
|
}
|
||||||
|
|
||||||
private void ZoneSwitchEnd()
|
private void ZoneSwitchEnd()
|
||||||
{
|
{
|
||||||
if (!_isOwnedObject) return;
|
if (!_isOwnedObject) return;
|
||||||
@@ -469,7 +467,7 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
|||||||
}
|
}
|
||||||
catch (ObjectDisposedException)
|
catch (ObjectDisposedException)
|
||||||
{
|
{
|
||||||
// ignore canelled after disposed
|
// ignore
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -477,9 +475,6 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Zone switch start handling
|
|
||||||
/// </summary>
|
|
||||||
private void ZoneSwitchStart()
|
private void ZoneSwitchStart()
|
||||||
{
|
{
|
||||||
if (!_isOwnedObject) return;
|
if (!_isOwnedObject) return;
|
||||||
|
|||||||
@@ -1,461 +0,0 @@
|
|||||||
using Dalamud.Game.ClientState.Objects.Enums;
|
|
||||||
using Dalamud.Game.ClientState.Objects.Types;
|
|
||||||
using Dalamud.Plugin.Services;
|
|
||||||
using LightlessSync.API.Data;
|
|
||||||
using LightlessSync.Interop.Ipc;
|
|
||||||
using LightlessSync.PlayerData.Factories;
|
|
||||||
using LightlessSync.PlayerData.Pairs;
|
|
||||||
using LightlessSync.Services;
|
|
||||||
using LightlessSync.Services.ActorTracking;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using DalamudObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind;
|
|
||||||
using ObjectKind = LightlessSync.API.Data.Enum.ObjectKind;
|
|
||||||
|
|
||||||
namespace LightlessSync.PlayerData.Handlers;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Owned object handler for applying changes to owned objects.
|
|
||||||
/// </summary>
|
|
||||||
internal sealed class OwnedObjectHandler
|
|
||||||
{
|
|
||||||
// Debug information for owned object resolution
|
|
||||||
internal readonly record struct OwnedResolveDebug(
|
|
||||||
DateTime? ResolvedAtUtc,
|
|
||||||
nint Address,
|
|
||||||
ushort? ObjectIndex,
|
|
||||||
string Stage,
|
|
||||||
string? FailureReason)
|
|
||||||
{
|
|
||||||
public string? AddressHex => Address == nint.Zero ? null : $"0x{Address:X}";
|
|
||||||
public static OwnedResolveDebug Empty => new(null, nint.Zero, null, string.Empty, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private OwnedResolveDebug _minionResolveDebug = OwnedResolveDebug.Empty;
|
|
||||||
public OwnedResolveDebug MinionResolveDebug => _minionResolveDebug;
|
|
||||||
|
|
||||||
// Dependencies
|
|
||||||
private readonly ILogger _logger;
|
|
||||||
private readonly DalamudUtilService _dalamudUtil;
|
|
||||||
private readonly GameObjectHandlerFactory _handlerFactory;
|
|
||||||
private readonly IpcManager _ipc;
|
|
||||||
private readonly ActorObjectService _actorObjectService;
|
|
||||||
private readonly IObjectTable _objectTable;
|
|
||||||
|
|
||||||
// Timeouts for fully loaded checks
|
|
||||||
private const int _fullyLoadedTimeoutMsPlayer = 30000;
|
|
||||||
private const int _fullyLoadedTimeoutMsOther = 5000;
|
|
||||||
|
|
||||||
public OwnedObjectHandler(
|
|
||||||
ILogger logger,
|
|
||||||
DalamudUtilService dalamudUtil,
|
|
||||||
GameObjectHandlerFactory handlerFactory,
|
|
||||||
IpcManager ipc,
|
|
||||||
ActorObjectService actorObjectService,
|
|
||||||
IObjectTable objectTable)
|
|
||||||
{
|
|
||||||
_logger = logger;
|
|
||||||
_dalamudUtil = dalamudUtil;
|
|
||||||
_handlerFactory = handlerFactory;
|
|
||||||
_ipc = ipc;
|
|
||||||
_actorObjectService = actorObjectService;
|
|
||||||
_objectTable = objectTable;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Applies the specified changes to the owned object of the given kind.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="applicationId">Application ID of the Character Object</param>
|
|
||||||
/// <param name="kind">Object Kind of the given object</param>
|
|
||||||
/// <param name="changes">Changes of the object</param>
|
|
||||||
/// <param name="data">Data of the object</param>
|
|
||||||
/// <param name="playerHandler">Owner of the object</param>
|
|
||||||
/// <param name="penumbraCollection">Collection if needed</param>
|
|
||||||
/// <param name="customizeIds">Customizing identications for the object</param>
|
|
||||||
/// <param name="token">Cancellation Token</param>
|
|
||||||
/// <returns>Successfully applied or not</returns>
|
|
||||||
public async Task<bool> ApplyAsync(
|
|
||||||
Guid applicationId,
|
|
||||||
ObjectKind kind,
|
|
||||||
HashSet<PlayerChanges> changes,
|
|
||||||
CharacterData data,
|
|
||||||
GameObjectHandler playerHandler,
|
|
||||||
Guid penumbraCollection,
|
|
||||||
Dictionary<ObjectKind, Guid?> customizeIds,
|
|
||||||
CancellationToken token)
|
|
||||||
{
|
|
||||||
// Validate player handler
|
|
||||||
if (playerHandler.Address == nint.Zero)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// Create handler for owned object
|
|
||||||
var handler = await CreateHandlerAsync(kind, playerHandler, token).ConfigureAwait(false);
|
|
||||||
if (handler is null || handler.Address == nint.Zero)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
token.ThrowIfCancellationRequested();
|
|
||||||
|
|
||||||
// Determine if we have file replacements for this kind
|
|
||||||
bool hasFileReplacements =
|
|
||||||
kind != ObjectKind.Player
|
|
||||||
&& data.FileReplacements.TryGetValue(kind, out var repls)
|
|
||||||
&& repls is { Count: > 0 };
|
|
||||||
|
|
||||||
// Determine if we should assign a Penumbra collection
|
|
||||||
bool shouldAssignCollection =
|
|
||||||
kind != ObjectKind.Player
|
|
||||||
&& hasFileReplacements
|
|
||||||
&& penumbraCollection != Guid.Empty
|
|
||||||
&& _ipc.Penumbra.APIAvailable;
|
|
||||||
|
|
||||||
// Determine if only IPC-only changes are being made for player
|
|
||||||
bool isPlayerIpcOnly =
|
|
||||||
kind == ObjectKind.Player
|
|
||||||
&& changes.Count > 0
|
|
||||||
&& changes.All(c => c is PlayerChanges.Honorific
|
|
||||||
or PlayerChanges.Moodles
|
|
||||||
or PlayerChanges.PetNames
|
|
||||||
or PlayerChanges.Heels);
|
|
||||||
|
|
||||||
// Wait for drawing to complete
|
|
||||||
await handler.IsBeingDrawnRunOnFrameworkAsync().ConfigureAwait(false);
|
|
||||||
|
|
||||||
// Determine timeouts
|
|
||||||
var drawTimeoutMs = handler.ObjectKind == ObjectKind.Player ? 30000 : 5000;
|
|
||||||
var fullyLoadedTimeoutMs = handler.ObjectKind == ObjectKind.Player ? _fullyLoadedTimeoutMsPlayer : _fullyLoadedTimeoutMsOther;
|
|
||||||
|
|
||||||
// Wait for drawing to complete
|
|
||||||
await _dalamudUtil
|
|
||||||
.WaitWhileCharacterIsDrawing(_logger, handler, applicationId, drawTimeoutMs, token)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (handler.Address != nint.Zero)
|
|
||||||
{
|
|
||||||
// Wait for fully loaded
|
|
||||||
var loaded = await _actorObjectService
|
|
||||||
.WaitForFullyLoadedAsync(handler.Address, token, fullyLoadedTimeoutMs)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (!loaded)
|
|
||||||
{
|
|
||||||
_logger.LogTrace("[{appId}] {kind}: not fully loaded in time, skipping for now", applicationId, kind);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
token.ThrowIfCancellationRequested();
|
|
||||||
|
|
||||||
// Assign Penumbra collection if needed
|
|
||||||
if (shouldAssignCollection)
|
|
||||||
{
|
|
||||||
// Get object index
|
|
||||||
var objIndex = await _dalamudUtil
|
|
||||||
.RunOnFrameworkThread(() => handler.GetGameObject()?.ObjectIndex)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (!objIndex.HasValue)
|
|
||||||
{
|
|
||||||
_logger.LogTrace("[{appId}] {kind}: ObjectIndex not available yet, cannot assign collection", applicationId, kind);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assign collection
|
|
||||||
await _ipc.Penumbra
|
|
||||||
.AssignTemporaryCollectionAsync(_logger, penumbraCollection, objIndex.Value)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
var tasks = new List<Task>();
|
|
||||||
|
|
||||||
// Apply each change
|
|
||||||
foreach (var change in changes.OrderBy(c => (int)c))
|
|
||||||
{
|
|
||||||
token.ThrowIfCancellationRequested();
|
|
||||||
|
|
||||||
// Handle each change type
|
|
||||||
switch (change)
|
|
||||||
{
|
|
||||||
case PlayerChanges.Customize:
|
|
||||||
if (data.CustomizePlusData.TryGetValue(kind, out var customizeData) && !string.IsNullOrEmpty(customizeData))
|
|
||||||
tasks.Add(ApplyCustomizeAsync(handler.Address, customizeData, kind, customizeIds));
|
|
||||||
else if (customizeIds.TryGetValue(kind, out var existingId))
|
|
||||||
tasks.Add(RevertCustomizeAsync(existingId, kind, customizeIds));
|
|
||||||
break;
|
|
||||||
|
|
||||||
case PlayerChanges.Glamourer:
|
|
||||||
if (data.GlamourerData.TryGetValue(kind, out var glamourerData) && !string.IsNullOrEmpty(glamourerData))
|
|
||||||
tasks.Add(_ipc.Glamourer.ApplyAllAsync(_logger, handler, glamourerData, applicationId, token));
|
|
||||||
break;
|
|
||||||
|
|
||||||
case PlayerChanges.Heels:
|
|
||||||
if (kind == ObjectKind.Player && !string.IsNullOrEmpty(data.HeelsData))
|
|
||||||
tasks.Add(_ipc.Heels.SetOffsetForPlayerAsync(handler.Address, data.HeelsData));
|
|
||||||
break;
|
|
||||||
|
|
||||||
case PlayerChanges.Honorific:
|
|
||||||
if (kind == ObjectKind.Player && !string.IsNullOrEmpty(data.HonorificData))
|
|
||||||
tasks.Add(_ipc.Honorific.SetTitleAsync(handler.Address, data.HonorificData));
|
|
||||||
break;
|
|
||||||
|
|
||||||
case PlayerChanges.Moodles:
|
|
||||||
if (kind == ObjectKind.Player && !string.IsNullOrEmpty(data.MoodlesData))
|
|
||||||
tasks.Add(_ipc.Moodles.SetStatusAsync(handler.Address, data.MoodlesData));
|
|
||||||
break;
|
|
||||||
|
|
||||||
case PlayerChanges.PetNames:
|
|
||||||
if (kind == ObjectKind.Player && !string.IsNullOrEmpty(data.PetNamesData))
|
|
||||||
tasks.Add(_ipc.PetNames.SetPlayerData(handler.Address, data.PetNamesData));
|
|
||||||
break;
|
|
||||||
|
|
||||||
case PlayerChanges.ModFiles:
|
|
||||||
case PlayerChanges.ModManip:
|
|
||||||
case PlayerChanges.ForcedRedraw:
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Await all tasks for change applications
|
|
||||||
if (tasks.Count > 0)
|
|
||||||
await Task.WhenAll(tasks).ConfigureAwait(false);
|
|
||||||
|
|
||||||
token.ThrowIfCancellationRequested();
|
|
||||||
|
|
||||||
// Determine if redraw is needed
|
|
||||||
bool needsRedraw =
|
|
||||||
_ipc.Penumbra.APIAvailable
|
|
||||||
&& (
|
|
||||||
shouldAssignCollection
|
|
||||||
|| changes.Contains(PlayerChanges.ForcedRedraw)
|
|
||||||
|| changes.Contains(PlayerChanges.ModFiles)
|
|
||||||
|| changes.Contains(PlayerChanges.ModManip)
|
|
||||||
|| changes.Contains(PlayerChanges.Glamourer)
|
|
||||||
|| changes.Contains(PlayerChanges.Customize)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Skip redraw for player if only IPC-only changes were made
|
|
||||||
if (isPlayerIpcOnly)
|
|
||||||
needsRedraw = false;
|
|
||||||
|
|
||||||
// Perform redraw if needed
|
|
||||||
if (needsRedraw && _ipc.Penumbra.APIAvailable)
|
|
||||||
{
|
|
||||||
_logger.LogWarning(
|
|
||||||
"[{appId}] {kind}: Redrawing ownedTarget={isOwned} (needsRedraw={needsRedraw})",
|
|
||||||
applicationId, kind, kind != ObjectKind.Player, needsRedraw);
|
|
||||||
|
|
||||||
await _ipc.Penumbra
|
|
||||||
.RedrawAsync(_logger, handler, applicationId, token)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
if (!ReferenceEquals(handler, playerHandler))
|
|
||||||
handler.Dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Creates a GameObjectHandler for the owned object of the specified kind.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="kind">Object kind of the handler</param>
|
|
||||||
/// <param name="playerHandler">Owner of the given object</param>
|
|
||||||
/// <param name="token">Cancellation Token</param>
|
|
||||||
/// <returns>Handler for the GameObject with the handler</returns>
|
|
||||||
private async Task<GameObjectHandler?> CreateHandlerAsync(ObjectKind kind, GameObjectHandler playerHandler, CancellationToken token)
|
|
||||||
{
|
|
||||||
token.ThrowIfCancellationRequested();
|
|
||||||
|
|
||||||
// Debug info setter
|
|
||||||
void SetMinionDebug(string stage, string? failure, nint addr = default, ushort? objIndex = null)
|
|
||||||
{
|
|
||||||
if (kind != ObjectKind.MinionOrMount)
|
|
||||||
return;
|
|
||||||
|
|
||||||
_minionResolveDebug = new OwnedResolveDebug(
|
|
||||||
DateTime.UtcNow,
|
|
||||||
addr,
|
|
||||||
objIndex,
|
|
||||||
stage,
|
|
||||||
failure);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Direct return for player
|
|
||||||
if (kind == ObjectKind.Player)
|
|
||||||
return playerHandler;
|
|
||||||
|
|
||||||
// First, try direct retrieval via Dalamud API
|
|
||||||
var playerPtr = playerHandler.Address;
|
|
||||||
if (playerPtr == nint.Zero)
|
|
||||||
{
|
|
||||||
SetMinionDebug("player_ptr_zero", "playerHandler.Address == 0");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try direct retrieval
|
|
||||||
nint ownedPtr = kind switch
|
|
||||||
{
|
|
||||||
ObjectKind.Companion => await _dalamudUtil.GetCompanionAsync(playerPtr).ConfigureAwait(false),
|
|
||||||
ObjectKind.MinionOrMount => await _dalamudUtil.GetMinionOrMountAsync(playerPtr).ConfigureAwait(false),
|
|
||||||
ObjectKind.Pet => await _dalamudUtil.GetPetAsync(playerPtr).ConfigureAwait(false),
|
|
||||||
_ => nint.Zero
|
|
||||||
};
|
|
||||||
|
|
||||||
// If that fails, scan the object table for owned objects
|
|
||||||
var stage = ownedPtr != nint.Zero ? "direct" : "direct_miss";
|
|
||||||
|
|
||||||
// Owner ID based scan
|
|
||||||
if (ownedPtr == nint.Zero)
|
|
||||||
{
|
|
||||||
token.ThrowIfCancellationRequested();
|
|
||||||
|
|
||||||
// Get owner entity ID
|
|
||||||
var ownerEntityId = playerHandler.EntityId;
|
|
||||||
if (ownerEntityId == 0 || ownerEntityId == uint.MaxValue)
|
|
||||||
{
|
|
||||||
// Read unsafe
|
|
||||||
ownerEntityId = await _dalamudUtil
|
|
||||||
.RunOnFrameworkThread(() => ReadEntityIdSafe(playerHandler))
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ownerEntityId != 0 && ownerEntityId != uint.MaxValue)
|
|
||||||
{
|
|
||||||
// Scan for owned object
|
|
||||||
ownedPtr = await _dalamudUtil
|
|
||||||
.RunOnFrameworkThread(() => FindOwnedByOwnerIdSafe(kind, ownerEntityId))
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
|
|
||||||
stage = ownedPtr != nint.Zero ? "owner_scan" : "owner_scan_miss";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
stage = "owner_id_unavailable";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ownedPtr == nint.Zero)
|
|
||||||
{
|
|
||||||
SetMinionDebug(stage, "ownedPtr == 0");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
token.ThrowIfCancellationRequested();
|
|
||||||
|
|
||||||
// Create handler
|
|
||||||
var handler = await _handlerFactory.Create(kind, () => ownedPtr, isWatched: false).ConfigureAwait(false);
|
|
||||||
if (handler is null || handler.Address == nint.Zero)
|
|
||||||
{
|
|
||||||
SetMinionDebug(stage, "handlerFactory returned null/zero", ownedPtr);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get object index for debug
|
|
||||||
ushort? objIndex = await _dalamudUtil.RunOnFrameworkThread(() => handler.GetGameObject()?.ObjectIndex)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
|
|
||||||
SetMinionDebug(stage, failure: null, handler.Address, objIndex);
|
|
||||||
return handler;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Entity ID reader with safety checks.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="playerHandler">Handler of the Object</param>
|
|
||||||
/// <returns>Entity Id</returns>
|
|
||||||
private static uint ReadEntityIdSafe(GameObjectHandler playerHandler) => playerHandler.GetGameObject()?.EntityId ?? 0;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Finds an owned object by scanning the object table for the specified owner entity ID.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="kind">Object kind to find of owned object</param>
|
|
||||||
/// <param name="ownerEntityId">Owner Id</param>
|
|
||||||
/// <returns>Object Id</returns>
|
|
||||||
private nint FindOwnedByOwnerIdSafe(ObjectKind kind, uint ownerEntityId)
|
|
||||||
{
|
|
||||||
// Validate owner ID
|
|
||||||
if (ownerEntityId == 0 || ownerEntityId == uint.MaxValue)
|
|
||||||
return nint.Zero;
|
|
||||||
|
|
||||||
// Scan object table
|
|
||||||
foreach (var obj in _objectTable)
|
|
||||||
{
|
|
||||||
// Validate object
|
|
||||||
if (obj is null || obj.Address == nint.Zero)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
// Check owner ID match
|
|
||||||
if (obj.OwnerId != ownerEntityId)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
// Check kind match
|
|
||||||
if (!IsOwnedKindMatch(obj, kind))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
return obj.Address;
|
|
||||||
}
|
|
||||||
|
|
||||||
return nint.Zero;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Determines if the given object matches the specified owned kind.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="obj">Game Object</param>
|
|
||||||
/// <param name="kind">Object Kind</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
private static bool IsOwnedKindMatch(IGameObject obj, ObjectKind kind) => kind switch
|
|
||||||
{
|
|
||||||
// Match minion or mount
|
|
||||||
ObjectKind.MinionOrMount =>
|
|
||||||
obj.ObjectKind is DalamudObjectKind.MountType
|
|
||||||
or DalamudObjectKind.Companion,
|
|
||||||
|
|
||||||
// Match pet
|
|
||||||
ObjectKind.Pet =>
|
|
||||||
obj.ObjectKind == DalamudObjectKind.BattleNpc
|
|
||||||
&& obj is IBattleNpc bnPet
|
|
||||||
&& bnPet.BattleNpcKind == BattleNpcSubKind.Pet,
|
|
||||||
|
|
||||||
// Match companion
|
|
||||||
ObjectKind.Companion =>
|
|
||||||
obj.ObjectKind == DalamudObjectKind.BattleNpc
|
|
||||||
&& obj is IBattleNpc bnBuddy
|
|
||||||
&& bnBuddy.BattleNpcKind == BattleNpcSubKind.Chocobo,
|
|
||||||
|
|
||||||
_ => false
|
|
||||||
};
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Applies Customize Plus data to the specified object.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="address">Object Address</param>
|
|
||||||
/// <param name="customizeData">Data of the Customize+ that has to be applied</param>
|
|
||||||
/// <param name="kind">Object Kind</param>
|
|
||||||
/// <param name="customizeIds">Customize+ Ids</param>
|
|
||||||
/// <returns>Task</returns>
|
|
||||||
private async Task ApplyCustomizeAsync(nint address, string customizeData, ObjectKind kind, Dictionary<ObjectKind, Guid?> customizeIds)
|
|
||||||
{
|
|
||||||
customizeIds[kind] = await _ipc.CustomizePlus.SetBodyScaleAsync(address, customizeData).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Reverts Customize Plus changes for the specified object.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="customizeId">Customize+ Id</param>
|
|
||||||
/// <param name="kind">Object Id</param>
|
|
||||||
/// <param name="customizeIds">List of Customize+ ids</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
private async Task RevertCustomizeAsync(Guid? customizeId, ObjectKind kind, Dictionary<ObjectKind, Guid?> customizeIds)
|
|
||||||
{
|
|
||||||
if (!customizeId.HasValue)
|
|
||||||
return;
|
|
||||||
|
|
||||||
await _ipc.CustomizePlus.RevertByIdAsync(customizeId.Value).ConfigureAwait(false);
|
|
||||||
customizeIds.Remove(kind);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
using LightlessSync.API.Data;
|
using LightlessSync.API.Data;
|
||||||
using LightlessSync.API.Data.Enum;
|
|
||||||
|
|
||||||
namespace LightlessSync.PlayerData.Pairs;
|
namespace LightlessSync.PlayerData.Pairs;
|
||||||
|
|
||||||
@@ -17,51 +16,25 @@ public interface IPairHandlerAdapter : IDisposable, IPairPerformanceSubject
|
|||||||
new string? PlayerName { get; }
|
new string? PlayerName { get; }
|
||||||
string PlayerNameHash { get; }
|
string PlayerNameHash { get; }
|
||||||
uint PlayerCharacterId { get; }
|
uint PlayerCharacterId { get; }
|
||||||
|
|
||||||
DateTime? LastDataReceivedAt { get; }
|
DateTime? LastDataReceivedAt { get; }
|
||||||
DateTime? LastApplyAttemptAt { get; }
|
DateTime? LastApplyAttemptAt { get; }
|
||||||
DateTime? LastSuccessfulApplyAt { get; }
|
DateTime? LastSuccessfulApplyAt { get; }
|
||||||
|
|
||||||
string? LastFailureReason { get; }
|
string? LastFailureReason { get; }
|
||||||
IReadOnlyList<string> LastBlockingConditions { get; }
|
IReadOnlyList<string> LastBlockingConditions { get; }
|
||||||
|
|
||||||
bool IsApplying { get; }
|
bool IsApplying { get; }
|
||||||
bool IsDownloading { get; }
|
bool IsDownloading { get; }
|
||||||
int PendingDownloadCount { get; }
|
int PendingDownloadCount { get; }
|
||||||
int ForbiddenDownloadCount { get; }
|
int ForbiddenDownloadCount { get; }
|
||||||
|
|
||||||
bool PendingModReapply { get; }
|
bool PendingModReapply { get; }
|
||||||
bool ModApplyDeferred { get; }
|
bool ModApplyDeferred { get; }
|
||||||
int MissingCriticalMods { get; }
|
int MissingCriticalMods { get; }
|
||||||
int MissingNonCriticalMods { get; }
|
int MissingNonCriticalMods { get; }
|
||||||
int MissingForbiddenMods { get; }
|
int MissingForbiddenMods { get; }
|
||||||
|
|
||||||
DateTime? InvisibleSinceUtc { get; }
|
|
||||||
DateTime? VisibilityEvictionDueAtUtc { get; }
|
|
||||||
|
|
||||||
string? MinionAddressHex { get; }
|
|
||||||
|
|
||||||
ushort? MinionObjectIndex { get; }
|
|
||||||
|
|
||||||
DateTime? MinionResolvedAtUtc { get; }
|
|
||||||
string? MinionResolveStage { get; }
|
|
||||||
string? MinionResolveFailureReason { get; }
|
|
||||||
|
|
||||||
bool MinionPendingRetry { get; }
|
|
||||||
IReadOnlyList<string> MinionPendingRetryChanges { get; }
|
|
||||||
bool MinionHasAppearanceData { get; }
|
|
||||||
|
|
||||||
Guid OwnedPenumbraCollectionId { get; }
|
|
||||||
bool NeedsCollectionRebuildDebug { get; }
|
|
||||||
|
|
||||||
uint MinionOrMountCharacterId { get; }
|
|
||||||
uint PetCharacterId { get; }
|
|
||||||
uint CompanionCharacterId { get; }
|
|
||||||
|
|
||||||
void Initialize();
|
void Initialize();
|
||||||
void ApplyData(CharacterData data);
|
void ApplyData(CharacterData data);
|
||||||
void ApplyLastReceivedData(bool forced = false);
|
void ApplyLastReceivedData(bool forced = false);
|
||||||
void HardReapplyLastData();
|
Task EnsurePerformanceMetricsAsync(CancellationToken cancellationToken);
|
||||||
bool FetchPerformanceMetricsFromCache();
|
bool FetchPerformanceMetricsFromCache();
|
||||||
void LoadCachedCharacterData(CharacterData data);
|
void LoadCachedCharacterData(CharacterData data);
|
||||||
void SetUploading(bool uploading);
|
void SetUploading(bool uploading);
|
||||||
|
|||||||
@@ -82,69 +82,33 @@ public class Pair
|
|||||||
|
|
||||||
public void AddContextMenu(IMenuOpenedArgs args)
|
public void AddContextMenu(IMenuOpenedArgs args)
|
||||||
{
|
{
|
||||||
|
|
||||||
var handler = TryGetHandler();
|
var handler = TryGetHandler();
|
||||||
if (handler is null)
|
if (handler is null)
|
||||||
return;
|
|
||||||
|
|
||||||
if (args.Target is not MenuTargetDefault target)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var obj = target.TargetObject;
|
|
||||||
if (obj is null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var eid = obj.EntityId;
|
|
||||||
|
|
||||||
var isPlayerTarget = eid != 0 && eid != uint.MaxValue && eid == handler.PlayerCharacterId;
|
|
||||||
|
|
||||||
if (!(isPlayerTarget))
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (isPlayerTarget)
|
|
||||||
{
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.Target is not MenuTargetDefault target || target.TargetObjectId != handler.PlayerCharacterId)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!IsPaused)
|
if (!IsPaused)
|
||||||
{
|
{
|
||||||
UiSharedService.AddContextMenuItem(
|
UiSharedService.AddContextMenuItem(args, name: "Open Profile", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () =>
|
||||||
args,
|
|
||||||
name: "Open Profile",
|
|
||||||
prefixChar: 'L',
|
|
||||||
colorMenuItem: _lightlessPrefixColor,
|
|
||||||
onClick: () =>
|
|
||||||
{
|
{
|
||||||
_mediator.Publish(new ProfileOpenStandaloneMessage(this));
|
_mediator.Publish(new ProfileOpenStandaloneMessage(this));
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
});
|
});
|
||||||
|
|
||||||
UiSharedService.AddContextMenuItem(
|
UiSharedService.AddContextMenuItem(args, name: "Reapply last data", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () =>
|
||||||
args,
|
|
||||||
name: "(Soft) - Reapply last data",
|
|
||||||
prefixChar: 'L',
|
|
||||||
colorMenuItem: _lightlessPrefixColor,
|
|
||||||
onClick: () =>
|
|
||||||
{
|
{
|
||||||
ApplyLastReceivedData(forced: true);
|
ApplyLastReceivedData(forced: true);
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
});
|
});
|
||||||
|
|
||||||
UiSharedService.AddContextMenuItem(
|
|
||||||
args,
|
|
||||||
name: "(Hard) - Reapply last data",
|
|
||||||
prefixChar: 'L',
|
|
||||||
colorMenuItem: _lightlessPrefixColor,
|
|
||||||
onClick: () =>
|
|
||||||
{
|
|
||||||
HardApplyLastReceivedData();
|
|
||||||
return Task.CompletedTask;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
UiSharedService.AddContextMenuItem(
|
UiSharedService.AddContextMenuItem(args, name: "Change Permissions", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () =>
|
||||||
args,
|
|
||||||
name: "Change Permissions",
|
|
||||||
prefixChar: 'L',
|
|
||||||
colorMenuItem: _lightlessPrefixColor,
|
|
||||||
onClick: () =>
|
|
||||||
{
|
{
|
||||||
_mediator.Publish(new OpenPermissionWindow(this));
|
_mediator.Publish(new OpenPermissionWindow(this));
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
@@ -152,12 +116,7 @@ public class Pair
|
|||||||
|
|
||||||
if (IsPaused)
|
if (IsPaused)
|
||||||
{
|
{
|
||||||
UiSharedService.AddContextMenuItem(
|
UiSharedService.AddContextMenuItem(args, name: "Toggle Unpause State", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () =>
|
||||||
args,
|
|
||||||
name: "Toggle Unpause State",
|
|
||||||
prefixChar: 'L',
|
|
||||||
colorMenuItem: _lightlessPrefixColor,
|
|
||||||
onClick: () =>
|
|
||||||
{
|
{
|
||||||
_ = _apiController.Value.UnpauseAsync(UserData);
|
_ = _apiController.Value.UnpauseAsync(UserData);
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
@@ -165,31 +124,18 @@ public class Pair
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
UiSharedService.AddContextMenuItem(
|
UiSharedService.AddContextMenuItem(args, name: "Toggle Pause State", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () =>
|
||||||
args,
|
|
||||||
name: "Toggle Pause State",
|
|
||||||
prefixChar: 'L',
|
|
||||||
colorMenuItem: _lightlessPrefixColor,
|
|
||||||
onClick: () =>
|
|
||||||
{
|
{
|
||||||
_ = _apiController.Value.PauseAsync(UserData);
|
_ = _apiController.Value.PauseAsync(UserData);
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
UiSharedService.AddContextMenuItem(
|
UiSharedService.AddContextMenuItem(args, name: "Cycle Pause State", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () =>
|
||||||
args,
|
|
||||||
name: "Cycle Pause State",
|
|
||||||
prefixChar: 'L',
|
|
||||||
colorMenuItem: _lightlessPrefixColor,
|
|
||||||
onClick: () =>
|
|
||||||
{
|
{
|
||||||
TriggerCyclePause();
|
TriggerCyclePause();
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
});
|
});
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ApplyData(OnlineUserCharaDataDto data)
|
public void ApplyData(OnlineUserCharaDataDto data)
|
||||||
@@ -214,18 +160,6 @@ public class Pair
|
|||||||
handler.ApplyLastReceivedData(forced);
|
handler.ApplyLastReceivedData(forced);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void HardApplyLastReceivedData()
|
|
||||||
{
|
|
||||||
var handler = TryGetHandler();
|
|
||||||
if (handler is null)
|
|
||||||
{
|
|
||||||
_logger.LogTrace("ApplyLastReceivedData skipped for {Uid}: handler missing.", UserData.UID);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
handler.HardReapplyLastData();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void CreateCachedPlayer(OnlineUserIdentDto? dto = null)
|
public void CreateCachedPlayer(OnlineUserIdentDto? dto = null)
|
||||||
{
|
{
|
||||||
var handler = TryGetHandler();
|
var handler = TryGetHandler();
|
||||||
@@ -283,12 +217,6 @@ public class Pair
|
|||||||
if (handler is null)
|
if (handler is null)
|
||||||
return PairDebugInfo.Empty;
|
return PairDebugInfo.Empty;
|
||||||
|
|
||||||
var now = DateTime.UtcNow;
|
|
||||||
var dueAt = handler.VisibilityEvictionDueAtUtc;
|
|
||||||
var remainingSeconds = dueAt.HasValue
|
|
||||||
? Math.Max(0, (dueAt.Value - now).TotalSeconds)
|
|
||||||
: (double?)null;
|
|
||||||
|
|
||||||
return new PairDebugInfo(
|
return new PairDebugInfo(
|
||||||
true,
|
true,
|
||||||
handler.Initialized,
|
handler.Initialized,
|
||||||
@@ -297,9 +225,6 @@ public class Pair
|
|||||||
handler.LastDataReceivedAt,
|
handler.LastDataReceivedAt,
|
||||||
handler.LastApplyAttemptAt,
|
handler.LastApplyAttemptAt,
|
||||||
handler.LastSuccessfulApplyAt,
|
handler.LastSuccessfulApplyAt,
|
||||||
handler.InvisibleSinceUtc,
|
|
||||||
handler.VisibilityEvictionDueAtUtc,
|
|
||||||
remainingSeconds,
|
|
||||||
handler.LastFailureReason,
|
handler.LastFailureReason,
|
||||||
handler.LastBlockingConditions,
|
handler.LastBlockingConditions,
|
||||||
handler.IsApplying,
|
handler.IsApplying,
|
||||||
@@ -310,17 +235,6 @@ public class Pair
|
|||||||
handler.ModApplyDeferred,
|
handler.ModApplyDeferred,
|
||||||
handler.MissingCriticalMods,
|
handler.MissingCriticalMods,
|
||||||
handler.MissingNonCriticalMods,
|
handler.MissingNonCriticalMods,
|
||||||
handler.MissingForbiddenMods,
|
handler.MissingForbiddenMods);
|
||||||
|
|
||||||
handler.MinionAddressHex,
|
|
||||||
handler.MinionObjectIndex,
|
|
||||||
handler.MinionResolvedAtUtc,
|
|
||||||
handler.MinionResolveStage,
|
|
||||||
handler.MinionResolveFailureReason,
|
|
||||||
handler.MinionPendingRetry,
|
|
||||||
handler.MinionPendingRetryChanges,
|
|
||||||
handler.MinionHasAppearanceData,
|
|
||||||
handler.OwnedPenumbraCollectionId,
|
|
||||||
handler.NeedsCollectionRebuildDebug);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ public sealed partial class PairCoordinator
|
|||||||
_pendingCharacterData.TryRemove(user.UID, out _);
|
_pendingCharacterData.TryRemove(user.UID, out _);
|
||||||
if (registrationResult.Value.CharacterIdent is not null)
|
if (registrationResult.Value.CharacterIdent is not null)
|
||||||
{
|
{
|
||||||
_ = _handlerRegistry.DeregisterOfflinePair(registrationResult.Value);
|
_ = _handlerRegistry.DeregisterOfflinePair(registrationResult.Value, forceDisposal: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
_mediator.Publish(new ClearProfileUserDataMessage(user));
|
_mediator.Publish(new ClearProfileUserDataMessage(user));
|
||||||
|
|||||||
@@ -8,9 +8,6 @@ public sealed record PairDebugInfo(
|
|||||||
DateTime? LastDataReceivedAt,
|
DateTime? LastDataReceivedAt,
|
||||||
DateTime? LastApplyAttemptAt,
|
DateTime? LastApplyAttemptAt,
|
||||||
DateTime? LastSuccessfulApplyAt,
|
DateTime? LastSuccessfulApplyAt,
|
||||||
DateTime? InvisibleSinceUtc,
|
|
||||||
DateTime? VisibilityEvictionDueAtUtc,
|
|
||||||
double? VisibilityEvictionRemainingSeconds,
|
|
||||||
string? LastFailureReason,
|
string? LastFailureReason,
|
||||||
IReadOnlyList<string> BlockingConditions,
|
IReadOnlyList<string> BlockingConditions,
|
||||||
bool IsApplying,
|
bool IsApplying,
|
||||||
@@ -21,50 +18,25 @@ public sealed record PairDebugInfo(
|
|||||||
bool ModApplyDeferred,
|
bool ModApplyDeferred,
|
||||||
int MissingCriticalMods,
|
int MissingCriticalMods,
|
||||||
int MissingNonCriticalMods,
|
int MissingNonCriticalMods,
|
||||||
int MissingForbiddenMods,
|
int MissingForbiddenMods)
|
||||||
|
|
||||||
string? MinionAddressHex,
|
|
||||||
ushort? MinionObjectIndex,
|
|
||||||
DateTime? MinionResolvedAtUtc,
|
|
||||||
string? MinionResolveStage,
|
|
||||||
string? MinionResolveFailureReason,
|
|
||||||
bool MinionPendingRetry,
|
|
||||||
IReadOnlyList<string> MinionPendingRetryChanges,
|
|
||||||
bool MinionHasAppearanceData,
|
|
||||||
Guid OwnedPenumbraCollectionId,
|
|
||||||
bool NeedsCollectionRebuild)
|
|
||||||
{
|
{
|
||||||
public static PairDebugInfo Empty { get; } = new(
|
public static PairDebugInfo Empty { get; } = new(
|
||||||
HasHandler: false,
|
false,
|
||||||
HandlerInitialized: false,
|
false,
|
||||||
HandlerVisible: false,
|
false,
|
||||||
HandlerScheduledForDeletion: false,
|
false,
|
||||||
LastDataReceivedAt: null,
|
null,
|
||||||
LastApplyAttemptAt: null,
|
null,
|
||||||
LastSuccessfulApplyAt: null,
|
null,
|
||||||
InvisibleSinceUtc: null,
|
null,
|
||||||
VisibilityEvictionDueAtUtc: null,
|
Array.Empty<string>(),
|
||||||
VisibilityEvictionRemainingSeconds: null,
|
false,
|
||||||
LastFailureReason: null,
|
false,
|
||||||
BlockingConditions: [],
|
0,
|
||||||
IsApplying: false,
|
0,
|
||||||
IsDownloading: false,
|
false,
|
||||||
PendingDownloadCount: 0,
|
false,
|
||||||
ForbiddenDownloadCount: 0,
|
0,
|
||||||
PendingModReapply: false,
|
0,
|
||||||
ModApplyDeferred: false,
|
0);
|
||||||
MissingCriticalMods: 0,
|
|
||||||
MissingNonCriticalMods: 0,
|
|
||||||
MissingForbiddenMods: 0,
|
|
||||||
|
|
||||||
MinionAddressHex: null,
|
|
||||||
MinionObjectIndex: null,
|
|
||||||
MinionResolvedAtUtc: null,
|
|
||||||
MinionResolveStage: null,
|
|
||||||
MinionResolveFailureReason: null,
|
|
||||||
MinionPendingRetry: false,
|
|
||||||
MinionPendingRetryChanges: [],
|
|
||||||
MinionHasAppearanceData: false,
|
|
||||||
OwnedPenumbraCollectionId: Guid.Empty,
|
|
||||||
NeedsCollectionRebuild: false);
|
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -40,7 +40,6 @@ internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory
|
|||||||
private readonly LightlessConfigService _configService;
|
private readonly LightlessConfigService _configService;
|
||||||
private readonly XivDataAnalyzer _modelAnalyzer;
|
private readonly XivDataAnalyzer _modelAnalyzer;
|
||||||
private readonly IFramework _framework;
|
private readonly IFramework _framework;
|
||||||
private readonly IObjectTable _objectTable;
|
|
||||||
|
|
||||||
public PairHandlerAdapterFactory(
|
public PairHandlerAdapterFactory(
|
||||||
ILoggerFactory loggerFactory,
|
ILoggerFactory loggerFactory,
|
||||||
@@ -64,8 +63,7 @@ internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory
|
|||||||
PairPerformanceMetricsCache pairPerformanceMetricsCache,
|
PairPerformanceMetricsCache pairPerformanceMetricsCache,
|
||||||
PenumbraTempCollectionJanitor tempCollectionJanitor,
|
PenumbraTempCollectionJanitor tempCollectionJanitor,
|
||||||
XivDataAnalyzer modelAnalyzer,
|
XivDataAnalyzer modelAnalyzer,
|
||||||
LightlessConfigService configService,
|
LightlessConfigService configService)
|
||||||
IObjectTable objectTable)
|
|
||||||
{
|
{
|
||||||
_loggerFactory = loggerFactory;
|
_loggerFactory = loggerFactory;
|
||||||
_mediator = mediator;
|
_mediator = mediator;
|
||||||
@@ -89,7 +87,6 @@ internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory
|
|||||||
_tempCollectionJanitor = tempCollectionJanitor;
|
_tempCollectionJanitor = tempCollectionJanitor;
|
||||||
_modelAnalyzer = modelAnalyzer;
|
_modelAnalyzer = modelAnalyzer;
|
||||||
_configService = configService;
|
_configService = configService;
|
||||||
_objectTable = objectTable;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public IPairHandlerAdapter Create(string ident)
|
public IPairHandlerAdapter Create(string ident)
|
||||||
@@ -108,7 +105,6 @@ internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory
|
|||||||
_pluginWarningNotificationManager,
|
_pluginWarningNotificationManager,
|
||||||
dalamudUtilService,
|
dalamudUtilService,
|
||||||
_framework,
|
_framework,
|
||||||
_objectTable,
|
|
||||||
actorObjectService,
|
actorObjectService,
|
||||||
_lifetime,
|
_lifetime,
|
||||||
_fileCacheManager,
|
_fileCacheManager,
|
||||||
|
|||||||
@@ -136,6 +136,7 @@ public sealed class PairHandlerRegistry : IDisposable
|
|||||||
if (TryFinalizeHandlerRemoval(handler))
|
if (TryFinalizeHandlerRemoval(handler))
|
||||||
{
|
{
|
||||||
handler.Dispose();
|
handler.Dispose();
|
||||||
|
_pairStateCache.Clear(registration.CharacterIdent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (shouldScheduleRemoval && handler is not null)
|
else if (shouldScheduleRemoval && handler is not null)
|
||||||
@@ -356,6 +357,7 @@ public sealed class PairHandlerRegistry : IDisposable
|
|||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
_pairPerformanceMetricsCache.Clear(handler.Ident);
|
_pairPerformanceMetricsCache.Clear(handler.Ident);
|
||||||
|
_pairStateCache.Clear(handler.Ident);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -377,6 +379,7 @@ public sealed class PairHandlerRegistry : IDisposable
|
|||||||
{
|
{
|
||||||
handler.Dispose();
|
handler.Dispose();
|
||||||
_pairPerformanceMetricsCache.Clear(handler.Ident);
|
_pairPerformanceMetricsCache.Clear(handler.Ident);
|
||||||
|
_pairStateCache.Clear(handler.Ident);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -401,6 +404,7 @@ public sealed class PairHandlerRegistry : IDisposable
|
|||||||
if (TryFinalizeHandlerRemoval(handler))
|
if (TryFinalizeHandlerRemoval(handler))
|
||||||
{
|
{
|
||||||
handler.Dispose();
|
handler.Dispose();
|
||||||
|
_pairStateCache.Clear(handler.Ident);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -271,7 +271,20 @@ public sealed class PairLedger : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
handler.ApplyLastReceivedData(forced: true);
|
_ = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await handler.EnsurePerformanceMetricsAsync(CancellationToken.None).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
if (_logger.IsEnabled(LogLevel.Debug))
|
||||||
|
{
|
||||||
|
_logger.LogDebug(ex, "Failed to ensure performance metrics for {Ident}", handler.Ident);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -160,8 +160,9 @@ public sealed class PairManager
|
|||||||
return PairOperationResult<PairRegistration>.Fail($"Pair {user.UID} not found.");
|
return PairOperationResult<PairRegistration>.Fail($"Pair {user.UID} not found.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var ident = connection.Ident;
|
||||||
connection.SetOffline();
|
connection.SetOffline();
|
||||||
return PairOperationResult<PairRegistration>.Ok(new PairRegistration(new PairUniqueIdentifier(user.UID), connection.Ident));
|
return PairOperationResult<PairRegistration>.Ok(new PairRegistration(new PairUniqueIdentifier(user.UID), ident));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -530,6 +531,7 @@ public sealed class PairManager
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var ident = connection.Ident;
|
||||||
if (connection.IsOnline)
|
if (connection.IsOnline)
|
||||||
{
|
{
|
||||||
connection.SetOffline();
|
connection.SetOffline();
|
||||||
@@ -542,7 +544,7 @@ public sealed class PairManager
|
|||||||
shell.Users.Remove(userId);
|
shell.Users.Remove(userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new PairRegistration(new PairUniqueIdentifier(userId), connection.Ident);
|
return new PairRegistration(new PairUniqueIdentifier(userId), ident);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static PairConnection CreateFromFullData(UserFullPairDto dto)
|
public static PairConnection CreateFromFullData(UserFullPairDto dto)
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ public sealed class PairConnection
|
|||||||
public void SetOffline()
|
public void SetOffline()
|
||||||
{
|
{
|
||||||
IsOnline = false;
|
IsOnline = false;
|
||||||
|
Ident = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void UpdatePermissions(UserPermissions own, UserPermissions other)
|
public void UpdatePermissions(UserPermissions own, UserPermissions other)
|
||||||
|
|||||||
@@ -110,7 +110,6 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
services.AddSingleton(gameGui);
|
services.AddSingleton(gameGui);
|
||||||
services.AddSingleton(gameInteropProvider);
|
services.AddSingleton(gameInteropProvider);
|
||||||
services.AddSingleton(addonLifecycle);
|
services.AddSingleton(addonLifecycle);
|
||||||
services.AddSingleton(objectTable);
|
|
||||||
services.AddSingleton<IUiBuilder>(pluginInterface.UiBuilder);
|
services.AddSingleton<IUiBuilder>(pluginInterface.UiBuilder);
|
||||||
|
|
||||||
// Core singletons
|
// Core singletons
|
||||||
@@ -126,16 +125,21 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
services.AddSingleton<FileTransferOrchestrator>();
|
services.AddSingleton<FileTransferOrchestrator>();
|
||||||
services.AddSingleton<LightlessPlugin>();
|
services.AddSingleton<LightlessPlugin>();
|
||||||
services.AddSingleton<LightlessProfileManager>();
|
services.AddSingleton<LightlessProfileManager>();
|
||||||
|
services.AddSingleton<TextureProcessingQueue>();
|
||||||
|
services.AddSingleton<ModelProcessingQueue>();
|
||||||
services.AddSingleton<TextureCompressionService>();
|
services.AddSingleton<TextureCompressionService>();
|
||||||
services.AddSingleton<TextureDownscaleService>();
|
services.AddSingleton<TextureDownscaleService>();
|
||||||
services.AddSingleton<ModelDecimationService>();
|
services.AddSingleton<ModelDecimationService>();
|
||||||
services.AddSingleton<GameObjectHandlerFactory>();
|
services.AddSingleton<GameObjectHandlerFactory>();
|
||||||
|
services.AddSingleton<FileDownloadDeduplicator>();
|
||||||
services.AddSingleton<FileDownloadManagerFactory>();
|
services.AddSingleton<FileDownloadManagerFactory>();
|
||||||
services.AddSingleton<PairProcessingLimiter>();
|
services.AddSingleton<PairProcessingLimiter>();
|
||||||
services.AddSingleton<XivDataAnalyzer>();
|
services.AddSingleton<XivDataAnalyzer>();
|
||||||
services.AddSingleton<CharacterAnalyzer>();
|
services.AddSingleton<CharacterAnalyzer>();
|
||||||
services.AddSingleton<TokenProvider>();
|
services.AddSingleton<TokenProvider>();
|
||||||
services.AddSingleton<PluginWarningNotificationService>();
|
services.AddSingleton<PluginWarningNotificationService>();
|
||||||
|
services.AddSingleton<ICompactorContext, PluginCompactorContext>();
|
||||||
|
services.AddSingleton<ICompactionExecutor, ExternalCompactionExecutor>();
|
||||||
services.AddSingleton<FileCompactor>();
|
services.AddSingleton<FileCompactor>();
|
||||||
services.AddSingleton<TagHandler>();
|
services.AddSingleton<TagHandler>();
|
||||||
services.AddSingleton<PairRequestService>();
|
services.AddSingleton<PairRequestService>();
|
||||||
@@ -332,8 +336,7 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
pluginInterface,
|
pluginInterface,
|
||||||
sp.GetRequiredService<DalamudUtilService>(),
|
sp.GetRequiredService<DalamudUtilService>(),
|
||||||
sp.GetRequiredService<LightlessMediator>(),
|
sp.GetRequiredService<LightlessMediator>(),
|
||||||
sp.GetRequiredService<RedrawManager>(),
|
sp.GetRequiredService<RedrawManager>()));
|
||||||
sp.GetRequiredService<ActorObjectService>()));
|
|
||||||
|
|
||||||
services.AddSingleton(sp => new IpcCallerGlamourer(
|
services.AddSingleton(sp => new IpcCallerGlamourer(
|
||||||
sp.GetRequiredService<ILogger<IpcCallerGlamourer>>(),
|
sp.GetRequiredService<ILogger<IpcCallerGlamourer>>(),
|
||||||
@@ -428,8 +431,8 @@ 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 PenumbraJanitorConfigService(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));
|
||||||
services.AddSingleton(sp => new SyncshellTagConfigService(configDir));
|
services.AddSingleton(sp => new SyncshellTagConfigService(configDir));
|
||||||
@@ -442,8 +445,8 @@ 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<PenumbraJanitorConfigService>());
|
|
||||||
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>());
|
||||||
services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<SyncshellTagConfigService>());
|
services.AddSingleton<IConfigService<ILightlessConfiguration>>(sp => sp.GetRequiredService<SyncshellTagConfigService>());
|
||||||
@@ -519,6 +522,7 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
sp.GetRequiredService<ILogger<UiService>>(),
|
sp.GetRequiredService<ILogger<UiService>>(),
|
||||||
pluginInterface.UiBuilder,
|
pluginInterface.UiBuilder,
|
||||||
sp.GetRequiredService<LightlessConfigService>(),
|
sp.GetRequiredService<LightlessConfigService>(),
|
||||||
|
sp.GetRequiredService<DalamudUtilService>(),
|
||||||
sp.GetRequiredService<WindowSystem>(),
|
sp.GetRequiredService<WindowSystem>(),
|
||||||
sp.GetServices<WindowMediatorSubscriberBase>(),
|
sp.GetServices<WindowMediatorSubscriberBase>(),
|
||||||
sp.GetRequiredService<UiFactory>(),
|
sp.GetRequiredService<UiFactory>(),
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ using BattleNpcSubKind = FFXIVClientStructs.FFXIV.Client.Game.Object.BattleNpcSu
|
|||||||
using IPlayerCharacter = Dalamud.Game.ClientState.Objects.SubKinds.IPlayerCharacter;
|
using IPlayerCharacter = Dalamud.Game.ClientState.Objects.SubKinds.IPlayerCharacter;
|
||||||
using DalamudObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind;
|
using DalamudObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind;
|
||||||
using LightlessObjectKind = LightlessSync.API.Data.Enum.ObjectKind;
|
using LightlessObjectKind = LightlessSync.API.Data.Enum.ObjectKind;
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
|
|
||||||
namespace LightlessSync.Services.ActorTracking;
|
namespace LightlessSync.Services.ActorTracking;
|
||||||
|
|
||||||
@@ -58,8 +57,6 @@ public sealed class ActorObjectService : IHostedService, IDisposable, IMediatorS
|
|||||||
private bool _hooksActive;
|
private bool _hooksActive;
|
||||||
private static readonly TimeSpan SnapshotRefreshInterval = TimeSpan.FromSeconds(1);
|
private static readonly TimeSpan SnapshotRefreshInterval = TimeSpan.FromSeconds(1);
|
||||||
private DateTime _nextRefreshAllowed = DateTime.MinValue;
|
private DateTime _nextRefreshAllowed = DateTime.MinValue;
|
||||||
private int _warmStartQueued;
|
|
||||||
private int _warmStartRan;
|
|
||||||
|
|
||||||
public ActorObjectService(
|
public ActorObjectService(
|
||||||
ILogger<ActorObjectService> logger,
|
ILogger<ActorObjectService> logger,
|
||||||
@@ -77,6 +74,7 @@ public sealed class ActorObjectService : IHostedService, IDisposable, IMediatorS
|
|||||||
_clientState = clientState;
|
_clientState = clientState;
|
||||||
_condition = condition;
|
_condition = condition;
|
||||||
_mediator = mediator;
|
_mediator = mediator;
|
||||||
|
|
||||||
_mediator.Subscribe<GameObjectHandlerCreatedMessage>(this, (msg) =>
|
_mediator.Subscribe<GameObjectHandlerCreatedMessage>(this, (msg) =>
|
||||||
{
|
{
|
||||||
if (!msg.OwnedObject) return;
|
if (!msg.OwnedObject) return;
|
||||||
@@ -95,9 +93,11 @@ public sealed class ActorObjectService : IHostedService, IDisposable, IMediatorS
|
|||||||
}
|
}
|
||||||
RefreshTrackedActors(force: true);
|
RefreshTrackedActors(force: true);
|
||||||
});
|
});
|
||||||
|
_mediator.Subscribe<DalamudLogoutMessage>(this, _ => ClearTrackingState());
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool IsZoning => _condition[ConditionFlag.BetweenAreas] || _condition[ConditionFlag.BetweenAreas51];
|
private bool IsZoning => _condition[ConditionFlag.BetweenAreas] || _condition[ConditionFlag.BetweenAreas51];
|
||||||
|
|
||||||
private ActorSnapshot Snapshot => Volatile.Read(ref _snapshot);
|
private ActorSnapshot Snapshot => Volatile.Read(ref _snapshot);
|
||||||
private GposeSnapshot CurrentGposeSnapshot => Volatile.Read(ref _gposeSnapshot);
|
private GposeSnapshot CurrentGposeSnapshot => Volatile.Read(ref _gposeSnapshot);
|
||||||
|
|
||||||
@@ -342,21 +342,9 @@ public sealed class ActorObjectService : IHostedService, IDisposable, IMediatorS
|
|||||||
|
|
||||||
public Task StopAsync(CancellationToken cancellationToken)
|
public Task StopAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
_warmStartRan = 0;
|
|
||||||
|
|
||||||
DisposeHooks();
|
DisposeHooks();
|
||||||
_activePlayers.Clear();
|
ClearTrackingState();
|
||||||
_gposePlayers.Clear();
|
|
||||||
_actorsByHash.Clear();
|
|
||||||
_actorsByName.Clear();
|
|
||||||
_pendingHashResolutions.Clear();
|
|
||||||
_mediator.UnsubscribeAll(this);
|
_mediator.UnsubscribeAll(this);
|
||||||
lock (_playerRelatedHandlerLock)
|
|
||||||
{
|
|
||||||
_playerRelatedHandlers.Clear();
|
|
||||||
}
|
|
||||||
Volatile.Write(ref _snapshot, ActorSnapshot.Empty);
|
|
||||||
Volatile.Write(ref _gposeSnapshot, GposeSnapshot.Empty);
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -508,10 +496,7 @@ public sealed class ActorObjectService : IHostedService, IDisposable, IMediatorS
|
|||||||
return new ActorDescriptor(name, hashedCid, address, objectIndex, isLocal, isInGpose, objectKind, ownedKind, ownerEntityId);
|
return new ActorDescriptor(name, hashedCid, address, objectIndex, isLocal, isInGpose, objectKind, ownedKind, ownerEntityId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private unsafe (LightlessObjectKind? OwnedKind, uint OwnerEntityId) DetermineOwnedKind(
|
private unsafe (LightlessObjectKind? OwnedKind, uint OwnerEntityId) DetermineOwnedKind(GameObject* gameObject, DalamudObjectKind objectKind, bool isLocalPlayer)
|
||||||
GameObject* gameObject,
|
|
||||||
DalamudObjectKind objectKind,
|
|
||||||
bool isLocalPlayer)
|
|
||||||
{
|
{
|
||||||
if (gameObject == null)
|
if (gameObject == null)
|
||||||
return (null, 0);
|
return (null, 0);
|
||||||
@@ -523,7 +508,6 @@ public sealed class ActorObjectService : IHostedService, IDisposable, IMediatorS
|
|||||||
}
|
}
|
||||||
|
|
||||||
var ownerId = ResolveOwnerId(gameObject);
|
var ownerId = ResolveOwnerId(gameObject);
|
||||||
|
|
||||||
var localPlayerAddress = _objectTable.LocalPlayer?.Address ?? nint.Zero;
|
var localPlayerAddress = _objectTable.LocalPlayer?.Address ?? nint.Zero;
|
||||||
if (localPlayerAddress == nint.Zero)
|
if (localPlayerAddress == nint.Zero)
|
||||||
return (null, ownerId);
|
return (null, ownerId);
|
||||||
@@ -535,7 +519,9 @@ public sealed class ActorObjectService : IHostedService, IDisposable, IMediatorS
|
|||||||
if (objectKind is DalamudObjectKind.MountType or DalamudObjectKind.Companion)
|
if (objectKind is DalamudObjectKind.MountType or DalamudObjectKind.Companion)
|
||||||
{
|
{
|
||||||
var expectedMinionOrMount = GetMinionOrMountAddress(localPlayerAddress, localEntityId);
|
var expectedMinionOrMount = GetMinionOrMountAddress(localPlayerAddress, localEntityId);
|
||||||
if (expectedMinionOrMount != nint.Zero && (nint)gameObject == expectedMinionOrMount)
|
if (expectedMinionOrMount != nint.Zero
|
||||||
|
&& (nint)gameObject == expectedMinionOrMount
|
||||||
|
&& IsPlayerRelatedOwnedAddress(expectedMinionOrMount, LightlessObjectKind.MinionOrMount))
|
||||||
{
|
{
|
||||||
var resolvedOwner = ownerId != 0 ? ownerId : localEntityId;
|
var resolvedOwner = ownerId != 0 ? ownerId : localEntityId;
|
||||||
return (LightlessObjectKind.MinionOrMount, resolvedOwner);
|
return (LightlessObjectKind.MinionOrMount, resolvedOwner);
|
||||||
@@ -545,16 +531,20 @@ public sealed class ActorObjectService : IHostedService, IDisposable, IMediatorS
|
|||||||
if (objectKind != DalamudObjectKind.BattleNpc)
|
if (objectKind != DalamudObjectKind.BattleNpc)
|
||||||
return (null, ownerId);
|
return (null, ownerId);
|
||||||
|
|
||||||
if (ownerId != 0 && ownerId != localEntityId)
|
if (ownerId != localEntityId)
|
||||||
return (null, ownerId);
|
return (null, ownerId);
|
||||||
|
|
||||||
var expectedPet = GetPetAddress(localPlayerAddress, localEntityId);
|
var expectedPet = GetPetAddress(localPlayerAddress, localEntityId);
|
||||||
if (expectedPet != nint.Zero && (nint)gameObject == expectedPet)
|
if (expectedPet != nint.Zero
|
||||||
return (LightlessObjectKind.Pet, ownerId != 0 ? ownerId : localEntityId);
|
&& (nint)gameObject == expectedPet
|
||||||
|
&& IsPlayerRelatedOwnedAddress(expectedPet, LightlessObjectKind.Pet))
|
||||||
|
return (LightlessObjectKind.Pet, ownerId);
|
||||||
|
|
||||||
var expectedCompanion = GetCompanionAddress(localPlayerAddress, localEntityId);
|
var expectedCompanion = GetCompanionAddress(localPlayerAddress, localEntityId);
|
||||||
if (expectedCompanion != nint.Zero && (nint)gameObject == expectedCompanion)
|
if (expectedCompanion != nint.Zero
|
||||||
return (LightlessObjectKind.Companion, ownerId != 0 ? ownerId : localEntityId);
|
&& (nint)gameObject == expectedCompanion
|
||||||
|
&& IsPlayerRelatedOwnedAddress(expectedCompanion, LightlessObjectKind.Companion))
|
||||||
|
return (LightlessObjectKind.Companion, ownerId);
|
||||||
|
|
||||||
return (null, ownerId);
|
return (null, ownerId);
|
||||||
}
|
}
|
||||||
@@ -581,124 +571,19 @@ public sealed class ActorObjectService : IHostedService, IDisposable, IMediatorS
|
|||||||
if (localPlayerAddress == nint.Zero)
|
if (localPlayerAddress == nint.Zero)
|
||||||
return nint.Zero;
|
return nint.Zero;
|
||||||
|
|
||||||
var playerObject = (GameObject*)localPlayerAddress;
|
|
||||||
|
|
||||||
var candidateAddress = _objectTable.GetObjectAddress(playerObject->ObjectIndex + 1);
|
|
||||||
if (candidateAddress != nint.Zero)
|
|
||||||
{
|
|
||||||
var candidate = (GameObject*)candidateAddress;
|
|
||||||
var candidateKind = (DalamudObjectKind)candidate->ObjectKind;
|
|
||||||
|
|
||||||
if (candidateKind is DalamudObjectKind.MountType or DalamudObjectKind.Companion)
|
|
||||||
{
|
|
||||||
var resolvedOwner = ResolveOwnerId(candidate);
|
|
||||||
|
|
||||||
if (resolvedOwner == ownerEntityId || resolvedOwner == 0)
|
|
||||||
return candidateAddress;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ownerEntityId == 0)
|
if (ownerEntityId == 0)
|
||||||
return nint.Zero;
|
return nint.Zero;
|
||||||
|
|
||||||
foreach (var obj in _objectTable)
|
var playerObject = (GameObject*)localPlayerAddress;
|
||||||
{
|
var candidateAddress = _objectTable.GetObjectAddress(playerObject->ObjectIndex + 1);
|
||||||
if (obj is null || obj.Address == nint.Zero || obj.Address == localPlayerAddress)
|
if (candidateAddress == nint.Zero)
|
||||||
continue;
|
|
||||||
|
|
||||||
if (obj.ObjectKind is not (DalamudObjectKind.MountType or DalamudObjectKind.Companion))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
var candidate = (GameObject*)obj.Address;
|
|
||||||
if (ResolveOwnerId(candidate) == ownerEntityId)
|
|
||||||
return obj.Address;
|
|
||||||
}
|
|
||||||
|
|
||||||
return nint.Zero;
|
return nint.Zero;
|
||||||
}
|
|
||||||
|
|
||||||
public unsafe bool TryFindOwnedObject(uint ownerEntityId, LightlessObjectKind kind, out nint address)
|
var candidate = (GameObject*)candidateAddress;
|
||||||
{
|
var candidateKind = (DalamudObjectKind)candidate->ObjectKind;
|
||||||
address = nint.Zero;
|
return candidateKind is DalamudObjectKind.MountType or DalamudObjectKind.Companion
|
||||||
if (ownerEntityId == 0) return false;
|
? candidateAddress
|
||||||
|
: nint.Zero;
|
||||||
foreach (var addr in EnumerateActiveCharacterAddresses())
|
|
||||||
{
|
|
||||||
if (addr == nint.Zero) continue;
|
|
||||||
|
|
||||||
var go = (GameObject*)addr;
|
|
||||||
var ok = (DalamudObjectKind)go->ObjectKind;
|
|
||||||
|
|
||||||
switch (kind)
|
|
||||||
{
|
|
||||||
case LightlessObjectKind.MinionOrMount:
|
|
||||||
if (ok is DalamudObjectKind.MountType or DalamudObjectKind.Companion
|
|
||||||
&& ResolveOwnerId(go) == ownerEntityId)
|
|
||||||
{
|
|
||||||
address = addr;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case LightlessObjectKind.Pet:
|
|
||||||
if (ok == DalamudObjectKind.BattleNpc
|
|
||||||
&& go->BattleNpcSubKind == BattleNpcSubKind.Pet
|
|
||||||
&& ResolveOwnerId(go) == ownerEntityId)
|
|
||||||
{
|
|
||||||
address = addr;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case LightlessObjectKind.Companion:
|
|
||||||
if (ok == DalamudObjectKind.BattleNpc
|
|
||||||
&& go->BattleNpcSubKind == BattleNpcSubKind.Buddy
|
|
||||||
&& ResolveOwnerId(go) == ownerEntityId)
|
|
||||||
{
|
|
||||||
address = addr;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public unsafe IReadOnlyList<nint> GetMinionOrMountCandidates(uint ownerEntityId, ushort preferredPlayerIndex)
|
|
||||||
{
|
|
||||||
var results = new List<(nint Ptr, int Score)>(4);
|
|
||||||
|
|
||||||
var manager = GameObjectManager.Instance();
|
|
||||||
if (manager == null || ownerEntityId == 0)
|
|
||||||
return Array.Empty<nint>();
|
|
||||||
|
|
||||||
const int objectLimit = 200;
|
|
||||||
for (var i = 0; i < objectLimit; i++)
|
|
||||||
{
|
|
||||||
var obj = manager->Objects.IndexSorted[i].Value;
|
|
||||||
if (obj == null)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
var kind = (DalamudObjectKind)obj->ObjectKind;
|
|
||||||
if (kind is not (DalamudObjectKind.MountType or DalamudObjectKind.Companion))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
var owner = ResolveOwnerId(obj);
|
|
||||||
if (owner != ownerEntityId)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
var idx = obj->ObjectIndex;
|
|
||||||
var score = Math.Abs(idx - (preferredPlayerIndex + 1));
|
|
||||||
if (obj->DrawObject == null) score += 50;
|
|
||||||
|
|
||||||
results.Add(((nint)obj, score));
|
|
||||||
}
|
|
||||||
|
|
||||||
return results
|
|
||||||
.OrderBy(r => r.Score)
|
|
||||||
.Select(r => r.Ptr)
|
|
||||||
.ToArray();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private unsafe nint GetPetAddress(nint localPlayerAddress, uint ownerEntityId)
|
private unsafe nint GetPetAddress(nint localPlayerAddress, uint ownerEntityId)
|
||||||
@@ -718,22 +603,6 @@ public sealed class ActorObjectService : IHostedService, IDisposable, IMediatorS
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var obj in _objectTable)
|
|
||||||
{
|
|
||||||
if (obj is null || obj.Address == nint.Zero || obj.Address == localPlayerAddress)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (obj.ObjectKind != DalamudObjectKind.BattleNpc)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
var candidate = (GameObject*)obj.Address;
|
|
||||||
if (candidate->BattleNpcSubKind != BattleNpcSubKind.Pet)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (ResolveOwnerId(candidate) == ownerEntityId)
|
|
||||||
return obj.Address;
|
|
||||||
}
|
|
||||||
|
|
||||||
return nint.Zero;
|
return nint.Zero;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -753,23 +622,6 @@ public sealed class ActorObjectService : IHostedService, IDisposable, IMediatorS
|
|||||||
return candidate;
|
return candidate;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var obj in _objectTable)
|
|
||||||
{
|
|
||||||
if (obj is null || obj.Address == nint.Zero || obj.Address == localPlayerAddress)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (obj.ObjectKind != DalamudObjectKind.BattleNpc)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
var candidate = (GameObject*)obj.Address;
|
|
||||||
if (candidate->BattleNpcSubKind != BattleNpcSubKind.Buddy)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (ResolveOwnerId(candidate) == ownerEntityId)
|
|
||||||
return obj.Address;
|
|
||||||
}
|
|
||||||
|
|
||||||
return nint.Zero;
|
return nint.Zero;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1166,6 +1018,22 @@ public sealed class ActorObjectService : IHostedService, IDisposable, IMediatorS
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ClearTrackingState()
|
||||||
|
{
|
||||||
|
_activePlayers.Clear();
|
||||||
|
_gposePlayers.Clear();
|
||||||
|
_actorsByHash.Clear();
|
||||||
|
_actorsByName.Clear();
|
||||||
|
_pendingHashResolutions.Clear();
|
||||||
|
lock (_playerRelatedHandlerLock)
|
||||||
|
{
|
||||||
|
_playerRelatedHandlers.Clear();
|
||||||
|
}
|
||||||
|
Volatile.Write(ref _snapshot, ActorSnapshot.Empty);
|
||||||
|
Volatile.Write(ref _gposeSnapshot, GposeSnapshot.Empty);
|
||||||
|
_nextRefreshAllowed = DateTime.MinValue;
|
||||||
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
DisposeHooks();
|
DisposeHooks();
|
||||||
@@ -1305,19 +1173,21 @@ public sealed class ActorObjectService : IHostedService, IDisposable, IMediatorS
|
|||||||
|
|
||||||
private static unsafe bool IsObjectFullyLoaded(nint address)
|
private static unsafe bool IsObjectFullyLoaded(nint address)
|
||||||
{
|
{
|
||||||
if (address == nint.Zero) return false;
|
if (address == nint.Zero)
|
||||||
|
return false;
|
||||||
|
|
||||||
var gameObject = (GameObject*)address;
|
var gameObject = (GameObject*)address;
|
||||||
if (gameObject == null) return false;
|
if (gameObject == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
var drawObject = gameObject->DrawObject;
|
var drawObject = gameObject->DrawObject;
|
||||||
if (drawObject == null) return false;
|
if (drawObject == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
if ((ulong)gameObject->RenderFlags == 2048)
|
if ((ulong)gameObject->RenderFlags == 2048)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
var characterBase = (CharacterBase*)drawObject;
|
var characterBase = (CharacterBase*)drawObject;
|
||||||
|
|
||||||
if (characterBase->HasModelInSlotLoaded != 0)
|
if (characterBase->HasModelInSlotLoaded != 0)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
@@ -1327,7 +1197,6 @@ public sealed class ActorObjectService : IHostedService, IDisposable, IMediatorS
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
[StructLayout(LayoutKind.Auto)]
|
|
||||||
private readonly record struct LoadState(bool IsValid, bool IsLoaded)
|
private readonly record struct LoadState(bool IsValid, bool IsLoaded)
|
||||||
{
|
{
|
||||||
public static LoadState Invalid => new(false, false);
|
public static LoadState Invalid => new(false, false);
|
||||||
|
|||||||
93
LightlessSync/Services/AssetProcessingQueue.cs
Normal file
93
LightlessSync/Services/AssetProcessingQueue.cs
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
@@ -106,7 +106,7 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable
|
|||||||
_baseAnalysisCts.Dispose();
|
_baseAnalysisCts.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task UpdateFileEntriesAsync(IEnumerable<string> filePaths, CancellationToken token)
|
public async Task UpdateFileEntriesAsync(IEnumerable<string> filePaths, CancellationToken token, bool force = false)
|
||||||
{
|
{
|
||||||
var normalized = new HashSet<string>(
|
var normalized = new HashSet<string>(
|
||||||
filePaths.Where(path => !string.IsNullOrWhiteSpace(path)),
|
filePaths.Where(path => !string.IsNullOrWhiteSpace(path)),
|
||||||
@@ -115,6 +115,8 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable
|
|||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var updated = false;
|
||||||
foreach (var objectEntries in LastAnalysis.Values)
|
foreach (var objectEntries in LastAnalysis.Values)
|
||||||
{
|
{
|
||||||
foreach (var entry in objectEntries.Values)
|
foreach (var entry in objectEntries.Values)
|
||||||
@@ -124,9 +126,26 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
token.ThrowIfCancellationRequested();
|
token.ThrowIfCancellationRequested();
|
||||||
await entry.ComputeSizes(_fileCacheManager, token).ConfigureAwait(false);
|
await entry.ComputeSizes(_fileCacheManager, token, force).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (string.Equals(entry.FileType, "mdl", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
var sourcePath = entry.FilePaths.FirstOrDefault(path => !string.IsNullOrWhiteSpace(path));
|
||||||
|
if (!string.IsNullOrWhiteSpace(sourcePath))
|
||||||
|
{
|
||||||
|
entry.UpdateTriangles(_xivDataAnalyzer.RefreshTrianglesForPath(entry.Hash, sourcePath));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updated)
|
||||||
|
{
|
||||||
|
RecalculateSummary();
|
||||||
|
Mediator.Publish(new CharacterDataAnalyzedMessage());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task BaseAnalysis(CharacterData charaData, CancellationToken token)
|
private async Task BaseAnalysis(CharacterData charaData, CancellationToken token)
|
||||||
@@ -311,6 +330,10 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable
|
|||||||
var original = new FileInfo(path).Length;
|
var original = new FileInfo(path).Length;
|
||||||
|
|
||||||
var compressedLen = await fileCacheManager.GetCompressedSizeAsync(Hash, token).ConfigureAwait(false);
|
var compressedLen = await fileCacheManager.GetCompressedSizeAsync(Hash, token).ConfigureAwait(false);
|
||||||
|
if (compressedLen <= 0 && !string.Equals(FileType, "tex", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
compressedLen = original;
|
||||||
|
}
|
||||||
|
|
||||||
fileCacheManager.SetSizeInfo(Hash, original, compressedLen);
|
fileCacheManager.SetSizeInfo(Hash, original, compressedLen);
|
||||||
FileCacheManager.ApplySizesToEntries(CacheEntries, original, compressedLen);
|
FileCacheManager.ApplySizesToEntries(CacheEntries, original, compressedLen);
|
||||||
@@ -326,6 +349,7 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable
|
|||||||
private Lazy<string>? _format;
|
private Lazy<string>? _format;
|
||||||
|
|
||||||
public void RefreshFormat() => _format = CreateFormatValue();
|
public void RefreshFormat() => _format = CreateFormatValue();
|
||||||
|
public void UpdateTriangles(long triangles) => Triangles = triangles;
|
||||||
|
|
||||||
private Lazy<string> CreateFormatValue()
|
private Lazy<string> CreateFormatValue()
|
||||||
=> new(() =>
|
=> new(() =>
|
||||||
|
|||||||
@@ -8,18 +8,26 @@ using Microsoft.Extensions.Hosting;
|
|||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using LightlessSync.UI.Services;
|
using LightlessSync.UI.Services;
|
||||||
using LightlessSync.LightlessConfiguration;
|
using LightlessSync.LightlessConfiguration;
|
||||||
|
using LightlessSync.LightlessConfiguration.Models;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace LightlessSync.Services.Chat;
|
namespace LightlessSync.Services.Chat;
|
||||||
|
|
||||||
public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedService
|
public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedService
|
||||||
{
|
{
|
||||||
private const int MaxMessageHistory = 150;
|
private const int MaxMessageHistory = 200;
|
||||||
internal const int MaxOutgoingLength = 200;
|
internal const int MaxOutgoingLength = 200;
|
||||||
private const int MaxUnreadCount = 999;
|
private const int MaxUnreadCount = 999;
|
||||||
private const string ZoneUnavailableMessage = "Zone chat is only available in major cities.";
|
private const string ZoneUnavailableMessage = "Zone chat is only available in major cities.";
|
||||||
private const string ZoneChannelKey = "zone";
|
private const string ZoneChannelKey = "zone";
|
||||||
private const int MaxReportReasonLength = 100;
|
private const int MaxReportReasonLength = 100;
|
||||||
private const int MaxReportContextLength = 1000;
|
private const int MaxReportContextLength = 1000;
|
||||||
|
private static readonly JsonSerializerOptions PersistedHistorySerializerOptions = new()
|
||||||
|
{
|
||||||
|
PropertyNameCaseInsensitive = true,
|
||||||
|
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
||||||
|
};
|
||||||
|
|
||||||
private readonly ApiController _apiController;
|
private readonly ApiController _apiController;
|
||||||
private readonly DalamudUtilService _dalamudUtilService;
|
private readonly DalamudUtilService _dalamudUtilService;
|
||||||
@@ -376,6 +384,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
|||||||
|
|
||||||
public Task StartAsync(CancellationToken cancellationToken)
|
public Task StartAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
LoadPersistedSyncshellHistory();
|
||||||
Mediator.Subscribe<DalamudLoginMessage>(this, _ => HandleLogin());
|
Mediator.Subscribe<DalamudLoginMessage>(this, _ => HandleLogin());
|
||||||
Mediator.Subscribe<DalamudLogoutMessage>(this, _ => HandleLogout());
|
Mediator.Subscribe<DalamudLogoutMessage>(this, _ => HandleLogout());
|
||||||
Mediator.Subscribe<ZoneSwitchEndMessage>(this, _ => ScheduleZonePresenceUpdate());
|
Mediator.Subscribe<ZoneSwitchEndMessage>(this, _ => ScheduleZonePresenceUpdate());
|
||||||
@@ -1000,11 +1009,22 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
|||||||
|
|
||||||
private void OnChatMessageReceived(ChatMessageDto dto)
|
private void OnChatMessageReceived(ChatMessageDto dto)
|
||||||
{
|
{
|
||||||
var descriptor = dto.Channel.WithNormalizedCustomKey();
|
ChatChannelDescriptor descriptor = dto.Channel.WithNormalizedCustomKey();
|
||||||
var key = descriptor.Type == ChatChannelType.Zone ? ZoneChannelKey : BuildChannelKey(descriptor);
|
string key = descriptor.Type == ChatChannelType.Zone ? ZoneChannelKey : BuildChannelKey(descriptor);
|
||||||
var fromSelf = IsMessageFromSelf(dto, key);
|
bool fromSelf = IsMessageFromSelf(dto, key);
|
||||||
var message = BuildMessage(dto, fromSelf);
|
ChatMessageEntry message = BuildMessage(dto, fromSelf);
|
||||||
|
bool mentionNotificationsEnabled = _chatConfigService.Current.EnableMentionNotifications;
|
||||||
|
bool notifyMention = mentionNotificationsEnabled
|
||||||
|
&& !fromSelf
|
||||||
|
&& descriptor.Type == ChatChannelType.Group
|
||||||
|
&& TryGetSelfMentionToken(dto.Message, out _);
|
||||||
|
|
||||||
|
string? mentionChannelName = null;
|
||||||
|
string? mentionSenderName = null;
|
||||||
bool publishChannelList = false;
|
bool publishChannelList = false;
|
||||||
|
bool shouldPersistHistory = _chatConfigService.Current.PersistSyncshellHistory;
|
||||||
|
List<PersistedChatMessage>? persistedMessages = null;
|
||||||
|
string? persistedChannelKey = null;
|
||||||
|
|
||||||
using (_sync.EnterScope())
|
using (_sync.EnterScope())
|
||||||
{
|
{
|
||||||
@@ -1042,6 +1062,12 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
|||||||
state.Messages.RemoveAt(0);
|
state.Messages.RemoveAt(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (notifyMention)
|
||||||
|
{
|
||||||
|
mentionChannelName = state.DisplayName;
|
||||||
|
mentionSenderName = message.DisplayName;
|
||||||
|
}
|
||||||
|
|
||||||
if (string.Equals(_activeChannelKey, key, StringComparison.Ordinal))
|
if (string.Equals(_activeChannelKey, key, StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
state.HasUnread = false;
|
state.HasUnread = false;
|
||||||
@@ -1058,10 +1084,29 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
|||||||
}
|
}
|
||||||
|
|
||||||
MarkChannelsSnapshotDirtyLocked();
|
MarkChannelsSnapshotDirtyLocked();
|
||||||
|
|
||||||
|
if (shouldPersistHistory && state.Type == ChatChannelType.Group)
|
||||||
|
{
|
||||||
|
persistedChannelKey = state.Key;
|
||||||
|
persistedMessages = BuildPersistedHistoryLocked(state);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Mediator.Publish(new ChatChannelMessageAdded(key, message));
|
Mediator.Publish(new ChatChannelMessageAdded(key, message));
|
||||||
|
|
||||||
|
if (persistedMessages is not null && persistedChannelKey is not null)
|
||||||
|
{
|
||||||
|
PersistSyncshellHistory(persistedChannelKey, persistedMessages);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notifyMention)
|
||||||
|
{
|
||||||
|
string channelName = mentionChannelName ?? "Syncshell";
|
||||||
|
string senderName = mentionSenderName ?? "Someone";
|
||||||
|
string notificationText = $"You were mentioned by {senderName} in {channelName}.";
|
||||||
|
Mediator.Publish(new NotificationMessage("Syncshell mention", notificationText, NotificationType.Info));
|
||||||
|
}
|
||||||
|
|
||||||
if (publishChannelList)
|
if (publishChannelList)
|
||||||
{
|
{
|
||||||
using (_sync.EnterScope())
|
using (_sync.EnterScope())
|
||||||
@@ -1108,6 +1153,113 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool TryGetSelfMentionToken(string message, out string matchedToken)
|
||||||
|
{
|
||||||
|
matchedToken = string.Empty;
|
||||||
|
if (string.IsNullOrWhiteSpace(message))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
HashSet<string> tokens = BuildSelfMentionTokens();
|
||||||
|
if (tokens.Count == 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return TryFindMentionToken(message, tokens, out matchedToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private HashSet<string> BuildSelfMentionTokens()
|
||||||
|
{
|
||||||
|
HashSet<string> tokens = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
string uid = _apiController.UID;
|
||||||
|
if (IsValidMentionToken(uid))
|
||||||
|
{
|
||||||
|
tokens.Add(uid);
|
||||||
|
}
|
||||||
|
|
||||||
|
string displayName = _apiController.DisplayName;
|
||||||
|
if (IsValidMentionToken(displayName))
|
||||||
|
{
|
||||||
|
tokens.Add(displayName);
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsValidMentionToken(string value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < value.Length; i++)
|
||||||
|
{
|
||||||
|
if (!IsMentionChar(value[i]))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryFindMentionToken(string message, IReadOnlyCollection<string> tokens, out string matchedToken)
|
||||||
|
{
|
||||||
|
matchedToken = string.Empty;
|
||||||
|
if (tokens.Count == 0 || string.IsNullOrEmpty(message))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int index = 0;
|
||||||
|
while (index < message.Length)
|
||||||
|
{
|
||||||
|
if (message[index] != '@')
|
||||||
|
{
|
||||||
|
index++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index > 0 && IsMentionChar(message[index - 1]))
|
||||||
|
{
|
||||||
|
index++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
int start = index + 1;
|
||||||
|
int end = start;
|
||||||
|
while (end < message.Length && IsMentionChar(message[end]))
|
||||||
|
{
|
||||||
|
end++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (end == start)
|
||||||
|
{
|
||||||
|
index++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
string token = message.Substring(start, end - start);
|
||||||
|
if (tokens.Contains(token))
|
||||||
|
{
|
||||||
|
matchedToken = token;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
index = end;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsMentionChar(char value)
|
||||||
|
{
|
||||||
|
return char.IsLetterOrDigit(value) || value == '_' || value == '-' || value == '\'';
|
||||||
|
}
|
||||||
|
|
||||||
private ChatMessageEntry BuildMessage(ChatMessageDto dto, bool fromSelf)
|
private ChatMessageEntry BuildMessage(ChatMessageDto dto, bool fromSelf)
|
||||||
{
|
{
|
||||||
var displayName = ResolveDisplayName(dto, fromSelf);
|
var displayName = ResolveDisplayName(dto, fromSelf);
|
||||||
@@ -1364,6 +1516,313 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void LoadPersistedSyncshellHistory()
|
||||||
|
{
|
||||||
|
if (!_chatConfigService.Current.PersistSyncshellHistory)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Dictionary<string, string> persisted = _chatConfigService.Current.SyncshellChannelHistory;
|
||||||
|
if (persisted.Count == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<string> invalidKeys = new();
|
||||||
|
foreach (KeyValuePair<string, string> entry in persisted)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(entry.Key) || string.IsNullOrWhiteSpace(entry.Value))
|
||||||
|
{
|
||||||
|
invalidKeys.Add(entry.Key);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryDecodePersistedHistory(entry.Value, out List<PersistedChatMessage> persistedMessages))
|
||||||
|
{
|
||||||
|
invalidKeys.Add(entry.Key);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (persistedMessages.Count == 0)
|
||||||
|
{
|
||||||
|
invalidKeys.Add(entry.Key);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (persistedMessages.Count > MaxMessageHistory)
|
||||||
|
{
|
||||||
|
int startIndex = Math.Max(0, persistedMessages.Count - MaxMessageHistory);
|
||||||
|
persistedMessages = persistedMessages.GetRange(startIndex, persistedMessages.Count - startIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ChatMessageEntry> restoredMessages = new(persistedMessages.Count);
|
||||||
|
foreach (PersistedChatMessage persistedMessage in persistedMessages)
|
||||||
|
{
|
||||||
|
if (!TryBuildRestoredMessage(entry.Key, persistedMessage, out ChatMessageEntry restoredMessage))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
restoredMessages.Add(restoredMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (restoredMessages.Count == 0)
|
||||||
|
{
|
||||||
|
invalidKeys.Add(entry.Key);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
using (_sync.EnterScope())
|
||||||
|
{
|
||||||
|
_messageHistoryCache[entry.Key] = restoredMessages;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (invalidKeys.Count > 0)
|
||||||
|
{
|
||||||
|
foreach (string key in invalidKeys)
|
||||||
|
{
|
||||||
|
persisted.Remove(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
_chatConfigService.Save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<PersistedChatMessage> BuildPersistedHistoryLocked(ChatChannelState state)
|
||||||
|
{
|
||||||
|
int startIndex = Math.Max(0, state.Messages.Count - MaxMessageHistory);
|
||||||
|
List<PersistedChatMessage> persistedMessages = new(state.Messages.Count - startIndex);
|
||||||
|
for (int i = startIndex; i < state.Messages.Count; i++)
|
||||||
|
{
|
||||||
|
ChatMessageEntry entry = state.Messages[i];
|
||||||
|
if (entry.Payload is not { } payload)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
persistedMessages.Add(new PersistedChatMessage(
|
||||||
|
payload.Message,
|
||||||
|
entry.DisplayName,
|
||||||
|
entry.FromSelf,
|
||||||
|
entry.ReceivedAtUtc,
|
||||||
|
payload.SentAtUtc));
|
||||||
|
}
|
||||||
|
|
||||||
|
return persistedMessages;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PersistSyncshellHistory(string channelKey, List<PersistedChatMessage> persistedMessages)
|
||||||
|
{
|
||||||
|
if (!_chatConfigService.Current.PersistSyncshellHistory)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Dictionary<string, string> persisted = _chatConfigService.Current.SyncshellChannelHistory;
|
||||||
|
if (persistedMessages.Count == 0)
|
||||||
|
{
|
||||||
|
if (persisted.Remove(channelKey))
|
||||||
|
{
|
||||||
|
_chatConfigService.Save();
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string? base64 = EncodePersistedMessages(persistedMessages);
|
||||||
|
if (string.IsNullOrWhiteSpace(base64))
|
||||||
|
{
|
||||||
|
if (persisted.Remove(channelKey))
|
||||||
|
{
|
||||||
|
_chatConfigService.Save();
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
persisted[channelKey] = base64;
|
||||||
|
_chatConfigService.Save();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? EncodePersistedMessages(List<PersistedChatMessage> persistedMessages)
|
||||||
|
{
|
||||||
|
if (persistedMessages.Count == 0)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] jsonBytes = JsonSerializer.SerializeToUtf8Bytes(persistedMessages, PersistedHistorySerializerOptions);
|
||||||
|
return Convert.ToBase64String(jsonBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryDecodePersistedHistory(string base64, out List<PersistedChatMessage> persistedMessages)
|
||||||
|
{
|
||||||
|
persistedMessages = new List<PersistedChatMessage>();
|
||||||
|
if (string.IsNullOrWhiteSpace(base64))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
byte[] jsonBytes = Convert.FromBase64String(base64);
|
||||||
|
List<PersistedChatMessage>? decoded = JsonSerializer.Deserialize<List<PersistedChatMessage>>(jsonBytes, PersistedHistorySerializerOptions);
|
||||||
|
if (decoded is null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
persistedMessages = decoded;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryBuildRestoredMessage(string channelKey, PersistedChatMessage persistedMessage, out ChatMessageEntry restoredMessage)
|
||||||
|
{
|
||||||
|
restoredMessage = default;
|
||||||
|
string messageText = persistedMessage.Message;
|
||||||
|
DateTime sentAtUtc = persistedMessage.SentAtUtc;
|
||||||
|
if (string.IsNullOrWhiteSpace(messageText) && persistedMessage.LegacyPayload is { } legacy)
|
||||||
|
{
|
||||||
|
messageText = legacy.Message;
|
||||||
|
sentAtUtc = legacy.SentAtUtc;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(messageText))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ChatChannelDescriptor descriptor = BuildDescriptorFromChannelKey(channelKey);
|
||||||
|
ChatSenderDescriptor sender = new ChatSenderDescriptor(
|
||||||
|
ChatSenderKind.Anonymous,
|
||||||
|
string.Empty,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
false);
|
||||||
|
|
||||||
|
ChatMessageDto payload = new ChatMessageDto(descriptor, sender, messageText, sentAtUtc, string.Empty);
|
||||||
|
restoredMessage = new ChatMessageEntry(payload, persistedMessage.DisplayName, persistedMessage.FromSelf, persistedMessage.ReceivedAtUtc);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ChatChannelDescriptor BuildDescriptorFromChannelKey(string channelKey)
|
||||||
|
{
|
||||||
|
if (string.Equals(channelKey, ZoneChannelKey, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return new ChatChannelDescriptor { Type = ChatChannelType.Zone };
|
||||||
|
}
|
||||||
|
|
||||||
|
int separatorIndex = channelKey.IndexOf(':', StringComparison.Ordinal);
|
||||||
|
if (separatorIndex <= 0 || separatorIndex >= channelKey.Length - 1)
|
||||||
|
{
|
||||||
|
return new ChatChannelDescriptor { Type = ChatChannelType.Group };
|
||||||
|
}
|
||||||
|
|
||||||
|
string typeValue = channelKey[..separatorIndex];
|
||||||
|
if (!int.TryParse(typeValue, out int parsedType))
|
||||||
|
{
|
||||||
|
return new ChatChannelDescriptor { Type = ChatChannelType.Group };
|
||||||
|
}
|
||||||
|
|
||||||
|
string customKey = channelKey[(separatorIndex + 1)..];
|
||||||
|
ChatChannelType channelType = parsedType switch
|
||||||
|
{
|
||||||
|
(int)ChatChannelType.Zone => ChatChannelType.Zone,
|
||||||
|
(int)ChatChannelType.Group => ChatChannelType.Group,
|
||||||
|
_ => ChatChannelType.Group
|
||||||
|
};
|
||||||
|
|
||||||
|
return new ChatChannelDescriptor
|
||||||
|
{
|
||||||
|
Type = channelType,
|
||||||
|
CustomKey = customKey
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ClearPersistedSyncshellHistory(bool clearLoadedMessages)
|
||||||
|
{
|
||||||
|
bool shouldPublish = false;
|
||||||
|
bool saveConfig = false;
|
||||||
|
|
||||||
|
using (_sync.EnterScope())
|
||||||
|
{
|
||||||
|
Dictionary<string, List<ChatMessageEntry>> cache = _messageHistoryCache;
|
||||||
|
if (cache.Count > 0)
|
||||||
|
{
|
||||||
|
List<string> keysToRemove = new();
|
||||||
|
foreach (string key in cache.Keys)
|
||||||
|
{
|
||||||
|
if (!string.Equals(key, ZoneChannelKey, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
keysToRemove.Add(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (string key in keysToRemove)
|
||||||
|
{
|
||||||
|
cache.Remove(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keysToRemove.Count > 0)
|
||||||
|
{
|
||||||
|
shouldPublish = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clearLoadedMessages)
|
||||||
|
{
|
||||||
|
foreach (ChatChannelState state in _channels.Values)
|
||||||
|
{
|
||||||
|
if (state.Type != ChatChannelType.Group)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.Messages.Count == 0 && state.UnreadCount == 0 && !state.HasUnread)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.Messages.Clear();
|
||||||
|
state.HasUnread = false;
|
||||||
|
state.UnreadCount = 0;
|
||||||
|
_lastReadCounts[state.Key] = 0;
|
||||||
|
shouldPublish = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Dictionary<string, string> persisted = _chatConfigService.Current.SyncshellChannelHistory;
|
||||||
|
if (persisted.Count > 0)
|
||||||
|
{
|
||||||
|
persisted.Clear();
|
||||||
|
saveConfig = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldPublish)
|
||||||
|
{
|
||||||
|
MarkChannelsSnapshotDirtyLocked();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (saveConfig)
|
||||||
|
{
|
||||||
|
_chatConfigService.Save();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldPublish)
|
||||||
|
{
|
||||||
|
PublishChannelListChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private sealed class ChatChannelState
|
private sealed class ChatChannelState
|
||||||
{
|
{
|
||||||
public ChatChannelState(string key, ChatChannelType type, string displayName, ChatChannelDescriptor descriptor)
|
public ChatChannelState(string key, ChatChannelType type, string displayName, ChatChannelDescriptor descriptor)
|
||||||
@@ -1400,4 +1859,12 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
|||||||
bool IsOwner);
|
bool IsOwner);
|
||||||
|
|
||||||
private readonly record struct PendingSelfMessage(string ChannelKey, string Message);
|
private readonly record struct PendingSelfMessage(string ChannelKey, string Message);
|
||||||
|
|
||||||
|
public sealed record PersistedChatMessage(
|
||||||
|
string Message = "",
|
||||||
|
string DisplayName = "",
|
||||||
|
bool FromSelf = false,
|
||||||
|
DateTime ReceivedAtUtc = default,
|
||||||
|
DateTime SentAtUtc = default,
|
||||||
|
[property: JsonPropertyName("Payload")] ChatMessageDto? LegacyPayload = null);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,10 +22,8 @@ using LightlessSync.Utils;
|
|||||||
using Lumina.Excel.Sheets;
|
using Lumina.Excel.Sheets;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using BattleNpcSubKind = FFXIVClientStructs.FFXIV.Client.Game.Object.BattleNpcSubKind;
|
using BattleNpcSubKind = FFXIVClientStructs.FFXIV.Client.Game.Object.BattleNpcSubKind;
|
||||||
using DalamudObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind;
|
using DalamudObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind;
|
||||||
@@ -229,6 +227,28 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
_ = RunOnFrameworkThread(ReleaseFocusUnsafe);
|
_ = RunOnFrameworkThread(ReleaseFocusUnsafe);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void TargetPlayerByAddress(nint address)
|
||||||
|
{
|
||||||
|
if (address == nint.Zero) return;
|
||||||
|
if (_clientState.IsPvP) return;
|
||||||
|
|
||||||
|
_ = RunOnFrameworkThread(() =>
|
||||||
|
{
|
||||||
|
var gameObject = CreateGameObject(address);
|
||||||
|
if (gameObject is null) return;
|
||||||
|
|
||||||
|
var useFocusTarget = _configService.Current.UseFocusTarget;
|
||||||
|
if (useFocusTarget)
|
||||||
|
{
|
||||||
|
_targetManager.FocusTarget = gameObject;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_targetManager.Target = gameObject;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private void FocusPairUnsafe(nint address, PairUniqueIdentifier pairIdent)
|
private void FocusPairUnsafe(nint address, PairUniqueIdentifier pairIdent)
|
||||||
{
|
{
|
||||||
var target = CreateGameObject(address);
|
var target = CreateGameObject(address);
|
||||||
@@ -404,38 +424,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
if (playerPointer == IntPtr.Zero) return IntPtr.Zero;
|
if (playerPointer == IntPtr.Zero) return IntPtr.Zero;
|
||||||
|
|
||||||
var playerAddress = playerPointer.Value;
|
var playerAddress = playerPointer.Value;
|
||||||
var ownerEntityId = ((Character*)playerAddress)->EntityId;
|
return _objectTable.GetObjectAddress(((GameObject*)playerAddress)->ObjectIndex + 1);
|
||||||
var candidateAddress = _objectTable.GetObjectAddress(((GameObject*)playerAddress)->ObjectIndex + 1);
|
|
||||||
if (ownerEntityId == 0) return candidateAddress;
|
|
||||||
|
|
||||||
if (playerAddress == _actorObjectService.LocalPlayerAddress)
|
|
||||||
{
|
|
||||||
var localOwned = _actorObjectService.LocalMinionOrMountAddress;
|
|
||||||
if (localOwned != nint.Zero)
|
|
||||||
{
|
|
||||||
return localOwned;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (candidateAddress != nint.Zero)
|
|
||||||
{
|
|
||||||
var candidate = (GameObject*)candidateAddress;
|
|
||||||
var candidateKind = (DalamudObjectKind)candidate->ObjectKind;
|
|
||||||
if ((candidateKind == DalamudObjectKind.MountType || candidateKind == DalamudObjectKind.Companion)
|
|
||||||
&& ResolveOwnerId(candidate) == ownerEntityId)
|
|
||||||
{
|
|
||||||
return candidateAddress;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var ownedObject = FindOwnedObject(ownerEntityId, playerAddress, static kind =>
|
|
||||||
kind == DalamudObjectKind.MountType || kind == DalamudObjectKind.Companion);
|
|
||||||
if (ownedObject != nint.Zero)
|
|
||||||
{
|
|
||||||
return ownedObject;
|
|
||||||
}
|
|
||||||
|
|
||||||
return candidateAddress;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IntPtr> GetMinionOrMountAsync(IntPtr? playerPointer = null)
|
public async Task<IntPtr> GetMinionOrMountAsync(IntPtr? playerPointer = null)
|
||||||
@@ -465,7 +454,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return FindOwnedPet(ownerEntityId, ownerAddress);
|
return IntPtr.Zero;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IntPtr> GetPetAsync(IntPtr? playerPointer = null)
|
public async Task<IntPtr> GetPetAsync(IntPtr? playerPointer = null)
|
||||||
@@ -473,69 +462,6 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
return await RunOnFrameworkThread(() => GetPetPtr(playerPointer)).ConfigureAwait(false);
|
return await RunOnFrameworkThread(() => GetPetPtr(playerPointer)).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private unsafe nint FindOwnedObject(uint ownerEntityId, nint ownerAddress, Func<DalamudObjectKind, bool> matchesKind)
|
|
||||||
{
|
|
||||||
if (ownerEntityId == 0)
|
|
||||||
{
|
|
||||||
return nint.Zero;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var obj in _objectTable)
|
|
||||||
{
|
|
||||||
if (obj is null || obj.Address == nint.Zero || obj.Address == ownerAddress)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!matchesKind(obj.ObjectKind))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var candidate = (GameObject*)obj.Address;
|
|
||||||
if (ResolveOwnerId(candidate) == ownerEntityId)
|
|
||||||
{
|
|
||||||
return obj.Address;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nint.Zero;
|
|
||||||
}
|
|
||||||
|
|
||||||
private unsafe nint FindOwnedPet(uint ownerEntityId, nint ownerAddress)
|
|
||||||
{
|
|
||||||
if (ownerEntityId == 0)
|
|
||||||
{
|
|
||||||
return nint.Zero;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var obj in _objectTable)
|
|
||||||
{
|
|
||||||
if (obj is null || obj.Address == nint.Zero || obj.Address == ownerAddress)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (obj.ObjectKind != DalamudObjectKind.BattleNpc)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var candidate = (GameObject*)obj.Address;
|
|
||||||
if (candidate->BattleNpcSubKind != BattleNpcSubKind.Pet)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ResolveOwnerId(candidate) == ownerEntityId)
|
|
||||||
{
|
|
||||||
return obj.Address;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nint.Zero;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static unsafe bool IsPetMatch(GameObject* candidate, uint ownerEntityId)
|
private static unsafe bool IsPetMatch(GameObject* candidate, uint ownerEntityId)
|
||||||
{
|
{
|
||||||
if (candidate == null)
|
if (candidate == null)
|
||||||
@@ -634,6 +560,37 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool TryGetHashedCIDFromAddress(nint address, out string hashedCid)
|
||||||
|
{
|
||||||
|
hashedCid = string.Empty;
|
||||||
|
if (address == nint.Zero)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (_framework.IsInFrameworkUpdateThread)
|
||||||
|
{
|
||||||
|
return TryGetHashedCIDFromAddressInternal(address, out hashedCid);
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = _framework.RunOnFrameworkThread(() =>
|
||||||
|
{
|
||||||
|
var success = TryGetHashedCIDFromAddressInternal(address, out var resolved);
|
||||||
|
return (success, resolved);
|
||||||
|
}).GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
hashedCid = result.resolved;
|
||||||
|
return result.success;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryGetHashedCIDFromAddressInternal(nint address, out string hashedCid)
|
||||||
|
{
|
||||||
|
hashedCid = string.Empty;
|
||||||
|
var player = _objectTable.CreateObjectReference(address) as IPlayerCharacter;
|
||||||
|
if (player == null || player.Address != address)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return TryGetHashedCID(player, out hashedCid);
|
||||||
|
}
|
||||||
|
|
||||||
public unsafe static string GetHashedCIDFromPlayerPointer(nint ptr)
|
public unsafe static string GetHashedCIDFromPlayerPointer(nint ptr)
|
||||||
{
|
{
|
||||||
return ((BattleChara*)ptr)->Character.ContentId.ToString().GetHash256();
|
return ((BattleChara*)ptr)->Character.ContentId.ToString().GetHash256();
|
||||||
@@ -744,7 +701,23 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
str += $" Room #{location.RoomId}";
|
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;
|
return str;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -836,7 +809,6 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
_framework.Update -= FrameworkOnUpdate;
|
_framework.Update -= FrameworkOnUpdate;
|
||||||
_clientState.Login -= OnClientLogin;
|
_clientState.Login -= OnClientLogin;
|
||||||
_clientState.Logout -= OnClientLogout;
|
_clientState.Logout -= OnClientLogout;
|
||||||
|
|
||||||
if (_FocusPairIdent.HasValue)
|
if (_FocusPairIdent.HasValue)
|
||||||
{
|
{
|
||||||
if (_framework.IsInFrameworkUpdateThread)
|
if (_framework.IsInFrameworkUpdateThread)
|
||||||
@@ -855,10 +827,12 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
{
|
{
|
||||||
if (IsLoggedIn)
|
if (IsLoggedIn)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
_ = RunOnFrameworkThread(() =>
|
_ = RunOnFrameworkThread(() =>
|
||||||
{
|
{
|
||||||
if (IsLoggedIn)
|
if (IsLoggedIn)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var localPlayer = _objectTable.LocalPlayer;
|
var localPlayer = _objectTable.LocalPlayer;
|
||||||
IsLoggedIn = true;
|
IsLoggedIn = true;
|
||||||
_lastZone = _clientState.TerritoryType;
|
_lastZone = _clientState.TerritoryType;
|
||||||
@@ -867,6 +841,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
_lastWorldId = (ushort)localPlayer.CurrentWorld.RowId;
|
_lastWorldId = (ushort)localPlayer.CurrentWorld.RowId;
|
||||||
_classJobId = localPlayer.ClassJob.RowId;
|
_classJobId = localPlayer.ClassJob.RowId;
|
||||||
}
|
}
|
||||||
|
|
||||||
_cid = RebuildCID();
|
_cid = RebuildCID();
|
||||||
Mediator.Publish(new DalamudLoginMessage());
|
Mediator.Publish(new DalamudLoginMessage());
|
||||||
});
|
});
|
||||||
@@ -880,49 +855,40 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
{
|
{
|
||||||
if (!IsLoggedIn)
|
if (!IsLoggedIn)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
IsLoggedIn = false;
|
IsLoggedIn = false;
|
||||||
_lastWorldId = 0;
|
_lastWorldId = 0;
|
||||||
Mediator.Publish(new DalamudLogoutMessage());
|
Mediator.Publish(new DalamudLogoutMessage());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task WaitWhileCharacterIsDrawing(
|
public async Task WaitWhileCharacterIsDrawing(ILogger logger, GameObjectHandler handler, Guid redrawId, int timeOut = 5000, CancellationToken? ct = null)
|
||||||
ILogger logger,
|
|
||||||
GameObjectHandler handler,
|
|
||||||
Guid redrawId,
|
|
||||||
int timeOut = 5000,
|
|
||||||
CancellationToken? ct = null)
|
|
||||||
{
|
{
|
||||||
if (!_clientState.IsLoggedIn) return;
|
if (!_clientState.IsLoggedIn) return;
|
||||||
|
|
||||||
var token = ct ?? CancellationToken.None;
|
if (ct == null)
|
||||||
|
ct = CancellationToken.None;
|
||||||
|
|
||||||
const int tick = 250;
|
const int tick = 250;
|
||||||
const int initialSettle = 50;
|
int curWaitTime = 0;
|
||||||
|
|
||||||
var sw = Stopwatch.StartNew();
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
logger.LogTrace("[{redrawId}] Starting wait for {handler} to draw", redrawId, handler);
|
logger.LogTrace("[{redrawId}] Starting wait for {handler} to draw", redrawId, handler);
|
||||||
|
await Task.Delay(tick, ct.Value).ConfigureAwait(true);
|
||||||
|
curWaitTime += tick;
|
||||||
|
|
||||||
await Task.Delay(initialSettle, token).ConfigureAwait(false);
|
while ((!ct.Value.IsCancellationRequested)
|
||||||
|
&& curWaitTime < timeOut
|
||||||
while (!token.IsCancellationRequested
|
&& await handler.IsBeingDrawnRunOnFrameworkAsync().ConfigureAwait(false)) // 0b100000000000 is "still rendering" or something
|
||||||
&& sw.ElapsedMilliseconds < timeOut
|
|
||||||
&& await handler.IsBeingDrawnRunOnFrameworkAsync().ConfigureAwait(false))
|
|
||||||
{
|
{
|
||||||
logger.LogTrace("[{redrawId}] Waiting for {handler} to finish drawing", redrawId, handler);
|
logger.LogTrace("[{redrawId}] Waiting for {handler} to finish drawing", redrawId, handler);
|
||||||
await Task.Delay(tick, token).ConfigureAwait(false);
|
curWaitTime += tick;
|
||||||
|
await Task.Delay(tick, ct.Value).ConfigureAwait(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.LogTrace("[{redrawId}] Finished drawing after {ms}ms", redrawId, sw.ElapsedMilliseconds);
|
logger.LogTrace("[{redrawId}] Finished drawing after {curWaitTime}ms", redrawId, curWaitTime);
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (AccessViolationException ex)
|
||||||
{
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
{
|
||||||
logger.LogWarning(ex, "Error accessing {handler}, object does not exist anymore?", handler);
|
logger.LogWarning(ex, "Error accessing {handler}, object does not exist anymore?", handler);
|
||||||
}
|
}
|
||||||
@@ -972,92 +938,21 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
return WorldData.Value.TryGetValue(worldId, out var worldName) ? worldName : null;
|
return WorldData.Value.TryGetValue(worldId, out var worldName) ? worldName : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void TargetPlayerByAddress(nint address)
|
|
||||||
{
|
|
||||||
if (address == nint.Zero) return;
|
|
||||||
if (_clientState.IsPvP) return;
|
|
||||||
|
|
||||||
_ = RunOnFrameworkThread(() =>
|
|
||||||
{
|
|
||||||
var gameObject = CreateGameObject(address);
|
|
||||||
if (gameObject is null) return;
|
|
||||||
|
|
||||||
var useFocusTarget = _configService.Current.UseFocusTarget;
|
|
||||||
if (useFocusTarget)
|
|
||||||
{
|
|
||||||
_targetManager.FocusTarget = gameObject;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_targetManager.Target = gameObject;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
[DllImport("kernel32.dll", SetLastError = true)]
|
|
||||||
private static extern bool IsBadReadPtr(IntPtr ptr, UIntPtr size);
|
|
||||||
|
|
||||||
private static bool IsValidPointer(nint ptr, int size = 8)
|
|
||||||
{
|
|
||||||
if (ptr == nint.Zero)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (!Util.IsWine())
|
|
||||||
{
|
|
||||||
return !IsBadReadPtr(ptr, (UIntPtr)size);
|
|
||||||
}
|
|
||||||
return ptr != nint.Zero && (ptr % IntPtr.Size) == 0;
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private unsafe void CheckCharacterForDrawing(nint address, string characterName)
|
private unsafe void CheckCharacterForDrawing(nint address, string characterName)
|
||||||
{
|
{
|
||||||
if (address == nint.Zero)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (!IsValidPointer(address))
|
|
||||||
{
|
|
||||||
_logger.LogDebug("Invalid pointer for character {name} at {addr}", characterName, address.ToString("X"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var gameObj = (GameObject*)address;
|
var gameObj = (GameObject*)address;
|
||||||
|
|
||||||
if (gameObj == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (!_objectTable.Any(o => o?.Address == address))
|
|
||||||
{
|
|
||||||
_logger.LogDebug("Character {name} at {addr} no longer in object table", characterName, address.ToString("X"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (gameObj->ObjectKind == 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var drawObj = gameObj->DrawObject;
|
var drawObj = gameObj->DrawObject;
|
||||||
bool isDrawing = false;
|
bool isDrawing = false;
|
||||||
bool isDrawingChanged = false;
|
bool isDrawingChanged = false;
|
||||||
|
if ((nint)drawObj != IntPtr.Zero)
|
||||||
if ((nint)drawObj != IntPtr.Zero && IsValidPointer((nint)drawObj))
|
|
||||||
{
|
{
|
||||||
isDrawing = gameObj->RenderFlags == (VisibilityFlags)0b100000000000;
|
isDrawing = gameObj->RenderFlags == (VisibilityFlags)0b100000000000;
|
||||||
|
|
||||||
if (!isDrawing)
|
if (!isDrawing)
|
||||||
{
|
{
|
||||||
var charBase = (CharacterBase*)drawObj;
|
isDrawing = ((CharacterBase*)drawObj)->HasModelInSlotLoaded != 0;
|
||||||
if (charBase != null && IsValidPointer((nint)charBase))
|
|
||||||
{
|
|
||||||
isDrawing = charBase->HasModelInSlotLoaded != 0;
|
|
||||||
if (!isDrawing)
|
if (!isDrawing)
|
||||||
{
|
{
|
||||||
isDrawing = charBase->HasModelFilesInSlotLoaded != 0;
|
isDrawing = ((CharacterBase*)drawObj)->HasModelFilesInSlotLoaded != 0;
|
||||||
if (isDrawing && !string.Equals(_lastGlobalBlockPlayer, characterName, StringComparison.Ordinal)
|
if (isDrawing && !string.Equals(_lastGlobalBlockPlayer, characterName, StringComparison.Ordinal)
|
||||||
&& !string.Equals(_lastGlobalBlockReason, "HasModelFilesInSlotLoaded", StringComparison.Ordinal))
|
&& !string.Equals(_lastGlobalBlockReason, "HasModelFilesInSlotLoaded", StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
@@ -1077,7 +972,6 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (!string.Equals(_lastGlobalBlockPlayer, characterName, StringComparison.Ordinal)
|
if (!string.Equals(_lastGlobalBlockPlayer, characterName, StringComparison.Ordinal)
|
||||||
@@ -1105,21 +999,39 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
|
|
||||||
private unsafe void FrameworkOnUpdateInternal()
|
private unsafe void FrameworkOnUpdateInternal()
|
||||||
{
|
{
|
||||||
if (!_clientState.IsLoggedIn || _objectTable.LocalPlayer == null)
|
var localPlayer = _objectTable.LocalPlayer;
|
||||||
{
|
if ((localPlayer?.IsDead ?? false) && _condition[ConditionFlag.BoundByDuty])
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((_objectTable.LocalPlayer?.IsDead ?? false) && _condition[ConditionFlag.BoundByDuty])
|
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isNormalFrameworkUpdate = DateTime.UtcNow < _delayedFrameworkUpdateCheck.AddSeconds(1);
|
bool isNormalFrameworkUpdate = DateTime.UtcNow < _delayedFrameworkUpdateCheck.AddSeconds(1);
|
||||||
|
var clientLoggedIn = _clientState.IsLoggedIn;
|
||||||
|
|
||||||
_performanceCollector.LogPerformance(this, $"FrameworkOnUpdateInternal+{(isNormalFrameworkUpdate ? "Regular" : "Delayed")}", () =>
|
_performanceCollector.LogPerformance(this, $"FrameworkOnUpdateInternal+{(isNormalFrameworkUpdate ? "Regular" : "Delayed")}", () =>
|
||||||
{
|
{
|
||||||
IsAnythingDrawing = false;
|
IsAnythingDrawing = false;
|
||||||
|
|
||||||
|
if (!isNormalFrameworkUpdate)
|
||||||
|
{
|
||||||
|
if (_gameConfig != null
|
||||||
|
&& _gameConfig.TryGet(Dalamud.Game.Config.SystemConfigOption.LodType_DX11, out bool lodEnabled))
|
||||||
|
{
|
||||||
|
IsLodEnabled = lodEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsInCombat || IsPerforming || IsInInstance)
|
||||||
|
Mediator.Publish(new FrameworkUpdateMessage());
|
||||||
|
|
||||||
|
Mediator.Publish(new DelayedFrameworkUpdateMessage());
|
||||||
|
|
||||||
|
_delayedFrameworkUpdateCheck = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!clientLoggedIn)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
_performanceCollector.LogPerformance(this, $"TrackedActorsToState",
|
_performanceCollector.LogPerformance(this, $"TrackedActorsToState",
|
||||||
() =>
|
() =>
|
||||||
{
|
{
|
||||||
@@ -1128,23 +1040,24 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
_actorObjectService.RefreshTrackedActors();
|
_actorObjectService.RefreshTrackedActors();
|
||||||
}
|
}
|
||||||
|
|
||||||
var playerDescriptors = _actorObjectService.PlayerDescriptors;
|
if (_clientState.IsLoggedIn && localPlayer != null)
|
||||||
var descriptorCount = playerDescriptors.Count;
|
{
|
||||||
|
var playerDescriptors = _actorObjectService.PlayerDescriptors;
|
||||||
for (var i = 0; i < descriptorCount; i++)
|
for (var i = 0; i < playerDescriptors.Count; i++)
|
||||||
{
|
{
|
||||||
if (i >= playerDescriptors.Count)
|
|
||||||
break;
|
|
||||||
|
|
||||||
var actor = playerDescriptors[i];
|
var actor = playerDescriptors[i];
|
||||||
|
|
||||||
var playerAddress = actor.Address;
|
var playerAddress = actor.Address;
|
||||||
if (playerAddress == nint.Zero || !IsValidPointer(playerAddress))
|
if (playerAddress == nint.Zero)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (actor.ObjectIndex >= 200)
|
if (actor.ObjectIndex >= 200)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
var obj = _objectTable[actor.ObjectIndex];
|
||||||
|
if (obj is not IPlayerCharacter player || player.Address != playerAddress)
|
||||||
|
continue;
|
||||||
|
|
||||||
if (_blockedCharacterHandler.IsCharacterBlocked(playerAddress, actor.ObjectIndex, out bool firstTime) && firstTime)
|
if (_blockedCharacterHandler.IsCharacterBlocked(playerAddress, actor.ObjectIndex, out bool firstTime) && firstTime)
|
||||||
{
|
{
|
||||||
_logger.LogTrace("Skipping character {addr}, blocked/muted", playerAddress.ToString("X"));
|
_logger.LogTrace("Skipping character {addr}, blocked/muted", playerAddress.ToString("X"));
|
||||||
@@ -1153,16 +1066,21 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
|
|
||||||
if (!IsAnythingDrawing)
|
if (!IsAnythingDrawing)
|
||||||
{
|
{
|
||||||
if (!_objectTable.Any(o => o?.Address == playerAddress))
|
var charaName = player.Name.TextValue;
|
||||||
|
if (string.IsNullOrEmpty(charaName))
|
||||||
{
|
{
|
||||||
continue;
|
charaName = actor.Name;
|
||||||
}
|
}
|
||||||
|
|
||||||
CheckCharacterForDrawing(playerAddress, actor.Name);
|
CheckCharacterForDrawing(playerAddress, charaName);
|
||||||
|
|
||||||
if (IsAnythingDrawing)
|
if (IsAnythingDrawing)
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1287,7 +1205,6 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
var localPlayer = _objectTable.LocalPlayer;
|
|
||||||
if (localPlayer != null)
|
if (localPlayer != null)
|
||||||
{
|
{
|
||||||
_classJobId = localPlayer.ClassJob.RowId;
|
_classJobId = localPlayer.ClassJob.RowId;
|
||||||
@@ -1309,22 +1226,6 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
Mediator.Publish(new FrameworkUpdateMessage());
|
Mediator.Publish(new FrameworkUpdateMessage());
|
||||||
|
|
||||||
Mediator.Publish(new PriorityFrameworkUpdateMessage());
|
Mediator.Publish(new PriorityFrameworkUpdateMessage());
|
||||||
|
|
||||||
if (isNormalFrameworkUpdate)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (_gameConfig != null
|
|
||||||
&& _gameConfig.TryGet(Dalamud.Game.Config.SystemConfigOption.LodType_DX11, out bool lodEnabled))
|
|
||||||
{
|
|
||||||
IsLodEnabled = lodEnabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (IsInCombat || IsPerforming || IsInInstance)
|
|
||||||
Mediator.Publish(new FrameworkUpdateMessage());
|
|
||||||
|
|
||||||
Mediator.Publish(new DelayedFrameworkUpdateMessage());
|
|
||||||
|
|
||||||
_delayedFrameworkUpdateCheck = DateTime.UtcNow;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -68,7 +68,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
|
|||||||
ImGuiWindowFlags.NoMove |
|
ImGuiWindowFlags.NoMove |
|
||||||
ImGuiWindowFlags.NoSavedSettings |
|
ImGuiWindowFlags.NoSavedSettings |
|
||||||
ImGuiWindowFlags.NoNav |
|
ImGuiWindowFlags.NoNav |
|
||||||
ImGuiWindowFlags.NoFocusOnAppearing |
|
|
||||||
ImGuiWindowFlags.NoInputs;
|
ImGuiWindowFlags.NoInputs;
|
||||||
|
|
||||||
private readonly List<RectF> _uiRects = new(128);
|
private readonly List<RectF> _uiRects = new(128);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using Lifestream.Enums;
|
||||||
using LightlessSync.API.Data;
|
using LightlessSync.API.Data;
|
||||||
using LightlessSync.API.Dto.CharaData;
|
using LightlessSync.API.Dto.CharaData;
|
||||||
using LightlessSync.API.Dto.User;
|
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)
|
public DateTimeOffset GetSharingStatus(string uid)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|||||||
@@ -21,6 +21,12 @@ public record SwitchToIntroUiMessage : MessageBase;
|
|||||||
public record SwitchToMainUiMessage : MessageBase;
|
public record SwitchToMainUiMessage : MessageBase;
|
||||||
public record OpenSettingsUiMessage : MessageBase;
|
public record OpenSettingsUiMessage : MessageBase;
|
||||||
public record OpenLightfinderSettingsMessage : MessageBase;
|
public record OpenLightfinderSettingsMessage : MessageBase;
|
||||||
|
public enum PerformanceSettingsSection
|
||||||
|
{
|
||||||
|
TextureOptimization,
|
||||||
|
ModelOptimization,
|
||||||
|
}
|
||||||
|
public record OpenPerformanceSettingsMessage(PerformanceSettingsSection Section) : MessageBase;
|
||||||
public record DalamudLoginMessage : MessageBase;
|
public record DalamudLoginMessage : MessageBase;
|
||||||
public record DalamudLogoutMessage : MessageBase;
|
public record DalamudLogoutMessage : MessageBase;
|
||||||
public record ActorTrackedMessage(ActorObjectService.ActorDescriptor Descriptor) : SameThreadMessage;
|
public record ActorTrackedMessage(ActorObjectService.ActorDescriptor Descriptor) : SameThreadMessage;
|
||||||
@@ -138,6 +144,5 @@ public record GroupCollectionChangedMessage : MessageBase;
|
|||||||
public record OpenUserProfileMessage(UserData User) : MessageBase;
|
public record OpenUserProfileMessage(UserData User) : MessageBase;
|
||||||
public record LocationSharingMessage(UserData User, LocationInfo LocationInfo, DateTimeOffset ExpireAt) : MessageBase;
|
public record LocationSharingMessage(UserData User, LocationInfo LocationInfo, DateTimeOffset ExpireAt) : MessageBase;
|
||||||
public record MapChangedMessage(uint MapId) : MessageBase;
|
public record MapChangedMessage(uint MapId) : MessageBase;
|
||||||
public record PenumbraTempCollectionsCleanedMessage : MessageBase;
|
|
||||||
#pragma warning restore S2094
|
#pragma warning restore S2094
|
||||||
#pragma warning restore MA0048 // File name must match type name
|
#pragma warning restore MA0048 // File name must match type name
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
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,5 +1,8 @@
|
|||||||
using LightlessSync.FileCache;
|
using LightlessSync.FileCache;
|
||||||
using LightlessSync.LightlessConfiguration;
|
using LightlessSync.LightlessConfiguration;
|
||||||
|
using LightlessSync.LightlessConfiguration.Configurations;
|
||||||
|
using LightlessSync.Services;
|
||||||
|
using LightlessSync.Utils;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
@@ -17,9 +20,10 @@ public sealed class ModelDecimationService
|
|||||||
private readonly FileCacheManager _fileCacheManager;
|
private readonly FileCacheManager _fileCacheManager;
|
||||||
private readonly PlayerPerformanceConfigService _performanceConfigService;
|
private readonly PlayerPerformanceConfigService _performanceConfigService;
|
||||||
private readonly XivDataStorageService _xivDataStorageService;
|
private readonly XivDataStorageService _xivDataStorageService;
|
||||||
|
private readonly ModelProcessingQueue _processingQueue;
|
||||||
private readonly SemaphoreSlim _decimationSemaphore = new(MaxConcurrentJobs);
|
private readonly SemaphoreSlim _decimationSemaphore = new(MaxConcurrentJobs);
|
||||||
|
|
||||||
private readonly ConcurrentDictionary<string, Task> _activeJobs = new(StringComparer.OrdinalIgnoreCase);
|
private readonly TaskRegistry<string> _decimationDeduplicator = new();
|
||||||
private readonly ConcurrentDictionary<string, string> _decimatedPaths = new(StringComparer.OrdinalIgnoreCase);
|
private readonly ConcurrentDictionary<string, string> _decimatedPaths = new(StringComparer.OrdinalIgnoreCase);
|
||||||
private readonly ConcurrentDictionary<string, byte> _failedHashes = new(StringComparer.OrdinalIgnoreCase);
|
private readonly ConcurrentDictionary<string, byte> _failedHashes = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
@@ -28,13 +32,15 @@ public sealed class ModelDecimationService
|
|||||||
LightlessConfigService configService,
|
LightlessConfigService configService,
|
||||||
FileCacheManager fileCacheManager,
|
FileCacheManager fileCacheManager,
|
||||||
PlayerPerformanceConfigService performanceConfigService,
|
PlayerPerformanceConfigService performanceConfigService,
|
||||||
XivDataStorageService xivDataStorageService)
|
XivDataStorageService xivDataStorageService,
|
||||||
|
ModelProcessingQueue processingQueue)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_configService = configService;
|
_configService = configService;
|
||||||
_fileCacheManager = fileCacheManager;
|
_fileCacheManager = fileCacheManager;
|
||||||
_performanceConfigService = performanceConfigService;
|
_performanceConfigService = performanceConfigService;
|
||||||
_xivDataStorageService = xivDataStorageService;
|
_xivDataStorageService = xivDataStorageService;
|
||||||
|
_processingQueue = processingQueue;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ScheduleDecimation(string hash, string filePath, string? gamePath = null)
|
public void ScheduleDecimation(string hash, string filePath, string? gamePath = null)
|
||||||
@@ -44,16 +50,16 @@ public sealed class ModelDecimationService
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_decimatedPaths.ContainsKey(hash) || _failedHashes.ContainsKey(hash) || _activeJobs.ContainsKey(hash))
|
if (_decimatedPaths.ContainsKey(hash) || _failedHashes.ContainsKey(hash) || _decimationDeduplicator.TryGetExisting(hash, out _))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogInformation("Queued model decimation for {Hash}", hash);
|
_logger.LogDebug("Queued model decimation for {Hash}", hash);
|
||||||
|
|
||||||
_activeJobs[hash] = Task.Run(async () =>
|
_decimationDeduplicator.GetOrStart(hash, () => _processingQueue.Enqueue(async token =>
|
||||||
{
|
{
|
||||||
await _decimationSemaphore.WaitAsync().ConfigureAwait(false);
|
await _decimationSemaphore.WaitAsync(token).ConfigureAwait(false);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await DecimateInternalAsync(hash, filePath).ConfigureAwait(false);
|
await DecimateInternalAsync(hash, filePath).ConfigureAwait(false);
|
||||||
@@ -66,16 +72,54 @@ public sealed class ModelDecimationService
|
|||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
_decimationSemaphore.Release();
|
_decimationSemaphore.Release();
|
||||||
_activeJobs.TryRemove(hash, out _);
|
|
||||||
}
|
}
|
||||||
}, CancellationToken.None);
|
}, CancellationToken.None));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ScheduleBatchDecimation(string hash, string filePath, ModelDecimationSettings settings)
|
||||||
|
{
|
||||||
|
if (!ShouldScheduleBatchDecimation(hash, filePath, settings))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_decimationDeduplicator.TryGetExisting(hash, out _))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_failedHashes.TryRemove(hash, out _);
|
||||||
|
_decimatedPaths.TryRemove(hash, out _);
|
||||||
|
|
||||||
|
_logger.LogInformation("Queued batch model decimation for {Hash}", hash);
|
||||||
|
|
||||||
|
_decimationDeduplicator.GetOrStart(hash, () => _processingQueue.Enqueue(async token =>
|
||||||
|
{
|
||||||
|
await _decimationSemaphore.WaitAsync(token).ConfigureAwait(false);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await DecimateInternalAsync(hash, filePath, settings, allowExisting: false, destinationOverride: filePath, registerDecimatedPath: false).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_failedHashes[hash] = 1;
|
||||||
|
_logger.LogWarning(ex, "Batch model decimation failed for {Hash}", hash);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_decimationSemaphore.Release();
|
||||||
|
}
|
||||||
|
}, CancellationToken.None));
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool ShouldScheduleDecimation(string hash, string filePath, string? gamePath = null)
|
public bool ShouldScheduleDecimation(string hash, string filePath, string? gamePath = null)
|
||||||
=> IsDecimationEnabled()
|
{
|
||||||
|
var threshold = Math.Max(0, _performanceConfigService.Current.ModelDecimationTriangleThreshold);
|
||||||
|
return IsDecimationEnabled()
|
||||||
&& filePath.EndsWith(".mdl", StringComparison.OrdinalIgnoreCase)
|
&& filePath.EndsWith(".mdl", StringComparison.OrdinalIgnoreCase)
|
||||||
&& IsDecimationAllowed(gamePath)
|
&& IsDecimationAllowed(gamePath)
|
||||||
&& !ShouldSkipByTriangleCache(hash);
|
&& !ShouldSkipByTriangleCache(hash, threshold);
|
||||||
|
}
|
||||||
|
|
||||||
public string GetPreferredPath(string hash, string originalPath)
|
public string GetPreferredPath(string hash, string originalPath)
|
||||||
{
|
{
|
||||||
@@ -116,7 +160,7 @@ public sealed class ModelDecimationService
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_activeJobs.TryGetValue(hash, out var job))
|
if (_decimationDeduplicator.TryGetExisting(hash, out var job))
|
||||||
{
|
{
|
||||||
pending.Add(job);
|
pending.Add(job);
|
||||||
}
|
}
|
||||||
@@ -131,6 +175,23 @@ public sealed class ModelDecimationService
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Task DecimateInternalAsync(string hash, string sourcePath)
|
private Task DecimateInternalAsync(string hash, string sourcePath)
|
||||||
|
{
|
||||||
|
if (!TryGetDecimationSettings(out var settings))
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Model decimation disabled or invalid settings for {Hash}", hash);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
return DecimateInternalAsync(hash, sourcePath, settings, allowExisting: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task DecimateInternalAsync(
|
||||||
|
string hash,
|
||||||
|
string sourcePath,
|
||||||
|
ModelDecimationSettings settings,
|
||||||
|
bool allowExisting,
|
||||||
|
string? destinationOverride = null,
|
||||||
|
bool registerDecimatedPath = true)
|
||||||
{
|
{
|
||||||
if (!File.Exists(sourcePath))
|
if (!File.Exists(sourcePath))
|
||||||
{
|
{
|
||||||
@@ -139,30 +200,48 @@ public sealed class ModelDecimationService
|
|||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryGetDecimationSettings(out var triangleThreshold, out var targetRatio))
|
if (!TryNormalizeSettings(settings, out var normalized))
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Model decimation disabled or invalid settings for {Hash}", hash);
|
_logger.LogDebug("Model decimation skipped for {Hash}; invalid settings.", hash);
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogInformation("Starting model decimation for {Hash} (threshold {Threshold}, ratio {Ratio:0.##})", hash, triangleThreshold, targetRatio);
|
_logger.LogDebug(
|
||||||
|
"Starting model decimation for {Hash} (threshold {Threshold}, ratio {Ratio:0.##}, normalize tangents {NormalizeTangents}, avoid body intersection {AvoidBodyIntersection})",
|
||||||
|
hash,
|
||||||
|
normalized.TriangleThreshold,
|
||||||
|
normalized.TargetRatio,
|
||||||
|
normalized.NormalizeTangents,
|
||||||
|
normalized.AvoidBodyIntersection);
|
||||||
|
|
||||||
var destination = Path.Combine(GetDecimatedDirectory(), $"{hash}.mdl");
|
var destination = destinationOverride ?? Path.Combine(GetDecimatedDirectory(), $"{hash}.mdl");
|
||||||
if (File.Exists(destination))
|
var inPlace = string.Equals(destination, sourcePath, StringComparison.OrdinalIgnoreCase);
|
||||||
|
if (!inPlace && File.Exists(destination))
|
||||||
|
{
|
||||||
|
if (allowExisting)
|
||||||
|
{
|
||||||
|
if (registerDecimatedPath)
|
||||||
{
|
{
|
||||||
RegisterDecimatedModel(hash, sourcePath, destination);
|
RegisterDecimatedModel(hash, sourcePath, destination);
|
||||||
|
}
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!MdlDecimator.TryDecimate(sourcePath, destination, triangleThreshold, targetRatio, _logger))
|
TryDelete(destination);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!MdlDecimator.TryDecimate(sourcePath, destination, normalized, _logger))
|
||||||
{
|
{
|
||||||
_failedHashes[hash] = 1;
|
_failedHashes[hash] = 1;
|
||||||
_logger.LogInformation("Model decimation skipped for {Hash}", hash);
|
_logger.LogDebug("Model decimation skipped for {Hash}", hash);
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (registerDecimatedPath)
|
||||||
|
{
|
||||||
RegisterDecimatedModel(hash, sourcePath, destination);
|
RegisterDecimatedModel(hash, sourcePath, destination);
|
||||||
_logger.LogInformation("Decimated model {Hash} -> {Path}", hash, destination);
|
}
|
||||||
|
_logger.LogDebug("Decimated model {Hash} -> {Path}", hash, destination);
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -250,7 +329,7 @@ public sealed class ModelDecimationService
|
|||||||
private bool IsDecimationEnabled()
|
private bool IsDecimationEnabled()
|
||||||
=> _performanceConfigService.Current.EnableModelDecimation;
|
=> _performanceConfigService.Current.EnableModelDecimation;
|
||||||
|
|
||||||
private bool ShouldSkipByTriangleCache(string hash)
|
private bool ShouldSkipByTriangleCache(string hash, int triangleThreshold)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(hash))
|
if (string.IsNullOrEmpty(hash))
|
||||||
{
|
{
|
||||||
@@ -262,7 +341,7 @@ public sealed class ModelDecimationService
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var threshold = Math.Max(0, _performanceConfigService.Current.ModelDecimationTriangleThreshold);
|
var threshold = Math.Max(0, triangleThreshold);
|
||||||
return threshold > 0 && cachedTris < threshold;
|
return threshold > 0 && cachedTris < threshold;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -273,50 +352,48 @@ 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)
|
private bool TryGetDecimationSettings(out ModelDecimationSettings settings)
|
||||||
=> path.Replace('\\', '/').ToLowerInvariant();
|
|
||||||
|
|
||||||
private bool TryGetDecimationSettings(out int triangleThreshold, out double targetRatio)
|
|
||||||
{
|
{
|
||||||
triangleThreshold = 15_000;
|
settings = new ModelDecimationSettings(
|
||||||
targetRatio = 0.8;
|
ModelDecimationDefaults.TriangleThreshold,
|
||||||
|
ModelDecimationDefaults.TargetRatio,
|
||||||
|
ModelDecimationDefaults.NormalizeTangents,
|
||||||
|
ModelDecimationDefaults.AvoidBodyIntersection,
|
||||||
|
new ModelDecimationAdvancedSettings());
|
||||||
|
|
||||||
var config = _performanceConfigService.Current;
|
var config = _performanceConfigService.Current;
|
||||||
if (!config.EnableModelDecimation)
|
if (!config.EnableModelDecimation)
|
||||||
@@ -324,14 +401,86 @@ public sealed class ModelDecimationService
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
triangleThreshold = Math.Max(0, config.ModelDecimationTriangleThreshold);
|
var advanced = NormalizeAdvancedSettings(config.ModelDecimationAdvanced);
|
||||||
targetRatio = config.ModelDecimationTargetRatio;
|
settings = new ModelDecimationSettings(
|
||||||
if (double.IsNaN(targetRatio) || double.IsInfinity(targetRatio))
|
Math.Max(0, config.ModelDecimationTriangleThreshold),
|
||||||
|
config.ModelDecimationTargetRatio,
|
||||||
|
config.ModelDecimationNormalizeTangents,
|
||||||
|
config.ModelDecimationAvoidBodyIntersection,
|
||||||
|
advanced);
|
||||||
|
|
||||||
|
return TryNormalizeSettings(settings, out settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryNormalizeSettings(ModelDecimationSettings settings, out ModelDecimationSettings normalized)
|
||||||
|
{
|
||||||
|
var ratio = settings.TargetRatio;
|
||||||
|
if (double.IsNaN(ratio) || double.IsInfinity(ratio))
|
||||||
|
{
|
||||||
|
normalized = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ratio = Math.Clamp(ratio, MinTargetRatio, MaxTargetRatio);
|
||||||
|
var advanced = NormalizeAdvancedSettings(settings.Advanced);
|
||||||
|
normalized = new ModelDecimationSettings(
|
||||||
|
Math.Max(0, settings.TriangleThreshold),
|
||||||
|
ratio,
|
||||||
|
settings.NormalizeTangents,
|
||||||
|
settings.AvoidBodyIntersection,
|
||||||
|
advanced);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ModelDecimationAdvancedSettings NormalizeAdvancedSettings(ModelDecimationAdvancedSettings? settings)
|
||||||
|
{
|
||||||
|
var source = settings ?? new ModelDecimationAdvancedSettings();
|
||||||
|
return new ModelDecimationAdvancedSettings
|
||||||
|
{
|
||||||
|
MinComponentTriangles = Math.Clamp(source.MinComponentTriangles, 0, 1000),
|
||||||
|
MaxCollapseEdgeLengthFactor = ClampFloat(source.MaxCollapseEdgeLengthFactor, 0.1f, 10f, ModelDecimationAdvancedSettings.DefaultMaxCollapseEdgeLengthFactor),
|
||||||
|
NormalSimilarityThresholdDegrees = ClampFloat(source.NormalSimilarityThresholdDegrees, 0f, 180f, ModelDecimationAdvancedSettings.DefaultNormalSimilarityThresholdDegrees),
|
||||||
|
BoneWeightSimilarityThreshold = ClampFloat(source.BoneWeightSimilarityThreshold, 0f, 1f, ModelDecimationAdvancedSettings.DefaultBoneWeightSimilarityThreshold),
|
||||||
|
UvSimilarityThreshold = ClampFloat(source.UvSimilarityThreshold, 0f, 1f, ModelDecimationAdvancedSettings.DefaultUvSimilarityThreshold),
|
||||||
|
UvSeamAngleCos = ClampFloat(source.UvSeamAngleCos, -1f, 1f, ModelDecimationAdvancedSettings.DefaultUvSeamAngleCos),
|
||||||
|
BlockUvSeamVertices = source.BlockUvSeamVertices,
|
||||||
|
AllowBoundaryCollapses = source.AllowBoundaryCollapses,
|
||||||
|
BodyCollisionDistanceFactor = ClampFloat(source.BodyCollisionDistanceFactor, 0f, 10f, ModelDecimationAdvancedSettings.DefaultBodyCollisionDistanceFactor),
|
||||||
|
BodyCollisionNoOpDistanceFactor = ClampFloat(source.BodyCollisionNoOpDistanceFactor, 0f, 10f, ModelDecimationAdvancedSettings.DefaultBodyCollisionNoOpDistanceFactor),
|
||||||
|
BodyCollisionAdaptiveRelaxFactor = ClampFloat(source.BodyCollisionAdaptiveRelaxFactor, 0f, 10f, ModelDecimationAdvancedSettings.DefaultBodyCollisionAdaptiveRelaxFactor),
|
||||||
|
BodyCollisionAdaptiveNearRatio = ClampFloat(source.BodyCollisionAdaptiveNearRatio, 0f, 1f, ModelDecimationAdvancedSettings.DefaultBodyCollisionAdaptiveNearRatio),
|
||||||
|
BodyCollisionAdaptiveUvThreshold = ClampFloat(source.BodyCollisionAdaptiveUvThreshold, 0f, 1f, ModelDecimationAdvancedSettings.DefaultBodyCollisionAdaptiveUvThreshold),
|
||||||
|
BodyCollisionNoOpUvSeamAngleCos = ClampFloat(source.BodyCollisionNoOpUvSeamAngleCos, -1f, 1f, ModelDecimationAdvancedSettings.DefaultBodyCollisionNoOpUvSeamAngleCos),
|
||||||
|
BodyCollisionProtectionFactor = ClampFloat(source.BodyCollisionProtectionFactor, 0f, 10f, ModelDecimationAdvancedSettings.DefaultBodyCollisionProtectionFactor),
|
||||||
|
BodyProxyTargetRatioMin = ClampFloat(source.BodyProxyTargetRatioMin, 0f, 1f, ModelDecimationAdvancedSettings.DefaultBodyProxyTargetRatioMin),
|
||||||
|
BodyCollisionProxyInflate = ClampFloat(source.BodyCollisionProxyInflate, 0f, 0.1f, ModelDecimationAdvancedSettings.DefaultBodyCollisionProxyInflate),
|
||||||
|
BodyCollisionPenetrationFactor = ClampFloat(source.BodyCollisionPenetrationFactor, 0f, 1f, ModelDecimationAdvancedSettings.DefaultBodyCollisionPenetrationFactor),
|
||||||
|
MinBodyCollisionDistance = ClampFloat(source.MinBodyCollisionDistance, 1e-6f, 1f, ModelDecimationAdvancedSettings.DefaultMinBodyCollisionDistance),
|
||||||
|
MinBodyCollisionCellSize = ClampFloat(source.MinBodyCollisionCellSize, 1e-6f, 1f, ModelDecimationAdvancedSettings.DefaultMinBodyCollisionCellSize),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static float ClampFloat(float value, float min, float max, float fallback)
|
||||||
|
{
|
||||||
|
if (float.IsNaN(value) || float.IsInfinity(value))
|
||||||
|
{
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.Clamp(value, min, max);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool ShouldScheduleBatchDecimation(string hash, string filePath, ModelDecimationSettings settings)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(filePath) || !filePath.EndsWith(".mdl", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
targetRatio = Math.Clamp(targetRatio, MinTargetRatio, MaxTargetRatio);
|
if (!TryNormalizeSettings(settings, out _))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
using LightlessSync.LightlessConfiguration.Configurations;
|
||||||
|
|
||||||
|
namespace LightlessSync.Services.ModelDecimation;
|
||||||
|
|
||||||
|
public readonly record struct ModelDecimationSettings(
|
||||||
|
int TriangleThreshold,
|
||||||
|
double TargetRatio,
|
||||||
|
bool NormalizeTangents,
|
||||||
|
bool AvoidBodyIntersection,
|
||||||
|
ModelDecimationAdvancedSettings Advanced);
|
||||||
19
LightlessSync/Services/ModelProcessingQueue.cs
Normal file
19
LightlessSync/Services/ModelProcessingQueue.cs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
using LightlessSync.Interop.Ipc;
|
using System.Linq;
|
||||||
|
using LightlessSync.Interop.Ipc;
|
||||||
|
using LightlessSync.LightlessConfiguration.Models;
|
||||||
using LightlessSync.LightlessConfiguration;
|
using LightlessSync.LightlessConfiguration;
|
||||||
using LightlessSync.Services.Mediator;
|
using LightlessSync.Services.Mediator;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@@ -8,14 +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 PenumbraJanitorConfigService _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);
|
||||||
|
|
||||||
public PenumbraTempCollectionJanitor(
|
public PenumbraTempCollectionJanitor(
|
||||||
ILogger<PenumbraTempCollectionJanitor> logger,
|
ILogger<PenumbraTempCollectionJanitor> logger,
|
||||||
LightlessMediator mediator,
|
LightlessMediator mediator,
|
||||||
IpcManager ipc,
|
IpcManager ipc,
|
||||||
PenumbraJanitorConfigService config) : base(logger, mediator)
|
TempCollectionConfigService config) : base(logger, mediator)
|
||||||
{
|
{
|
||||||
_ipc = ipc;
|
_ipc = ipc;
|
||||||
_config = config;
|
_config = config;
|
||||||
@@ -26,16 +32,42 @@ public sealed class PenumbraTempCollectionJanitor : DisposableMediatorSubscriber
|
|||||||
public void Register(Guid id)
|
public void Register(Guid id)
|
||||||
{
|
{
|
||||||
if (id == Guid.Empty) return;
|
if (id == Guid.Empty) return;
|
||||||
if (_config.Current.OrphanableTempCollections.Add(id))
|
var changed = false;
|
||||||
|
var config = _config.Current;
|
||||||
|
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
var existing = config.OrphanableTempCollectionEntries.FirstOrDefault(entry => entry.Id == id);
|
||||||
|
if (existing is null)
|
||||||
|
{
|
||||||
|
config.OrphanableTempCollectionEntries.Add(new OrphanableTempCollectionEntry
|
||||||
|
{
|
||||||
|
Id = id,
|
||||||
|
RegisteredAtUtc = now
|
||||||
|
});
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
else if (existing.RegisteredAtUtc == DateTime.MinValue)
|
||||||
|
{
|
||||||
|
existing.RegisteredAtUtc = now;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changed)
|
||||||
|
{
|
||||||
_config.Save();
|
_config.Save();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void Unregister(Guid id)
|
public void Unregister(Guid id)
|
||||||
{
|
{
|
||||||
if (id == Guid.Empty) return;
|
if (id == Guid.Empty) return;
|
||||||
if (_config.Current.OrphanableTempCollections.Remove(id))
|
var config = _config.Current;
|
||||||
|
var changed = RemoveEntry(config.OrphanableTempCollectionEntries, id) > 0;
|
||||||
|
if (changed)
|
||||||
|
{
|
||||||
_config.Save();
|
_config.Save();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void CleanupOrphansOnBoot()
|
private void CleanupOrphansOnBoot()
|
||||||
{
|
{
|
||||||
@@ -45,30 +77,139 @@ public sealed class PenumbraTempCollectionJanitor : DisposableMediatorSubscriber
|
|||||||
if (!_ipc.Penumbra.APIAvailable)
|
if (!_ipc.Penumbra.APIAvailable)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var ids = _config.Current.OrphanableTempCollections.ToArray();
|
_ = Task.Run(async () =>
|
||||||
if (ids.Length == 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var appId = Guid.NewGuid();
|
|
||||||
Logger.LogInformation("Cleaning up {count} orphaned Lightless temp collections found in configuration", ids.Length);
|
|
||||||
|
|
||||||
foreach (var id in ids)
|
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_ipc.Penumbra.RemoveTemporaryCollectionAsync(Logger, appId, id)
|
await CleanupOrphansOnBootAsync(_cleanupCts.Token).ConfigureAwait(false);
|
||||||
.GetAwaiter().GetResult();
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError(ex, "Error cleaning orphaned temp collections");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CleanupOrphansOnBootAsync(CancellationToken token)
|
||||||
|
{
|
||||||
|
var config = _config.Current;
|
||||||
|
var entries = config.OrphanableTempCollectionEntries;
|
||||||
|
if (entries.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
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)
|
||||||
|
.Select(entry => entry.Id)
|
||||||
|
.Distinct()
|
||||||
|
.ToList();
|
||||||
|
if (expired.Count == 0)
|
||||||
|
{
|
||||||
|
if (changed)
|
||||||
|
{
|
||||||
|
_config.Save();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
{
|
||||||
|
await _ipc.Penumbra.RemoveTemporaryCollectionAsync(Logger, appId, id).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_config.Current.OrphanableTempCollections.Clear();
|
if (removedIds.Count == 0)
|
||||||
|
{
|
||||||
|
if (changed)
|
||||||
|
{
|
||||||
_config.Save();
|
_config.Save();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Notify cleanup complete
|
foreach (var id in removedIds)
|
||||||
Mediator.Publish(new PenumbraTempCollectionsCleanedMessage());
|
{
|
||||||
|
RemoveEntry(entries, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
_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;
|
||||||
|
for (var i = entries.Count - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
if (entries[i].Id != id)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
entries.RemoveAt(i);
|
||||||
|
removed++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return removed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool EnsureEntryTimes(List<OrphanableTempCollectionEntry> entries, DateTime now)
|
||||||
|
{
|
||||||
|
var changed = false;
|
||||||
|
foreach (var entry in entries)
|
||||||
|
{
|
||||||
|
if (entry.Id == Guid.Empty || entry.RegisteredAtUtc != DateTime.MinValue)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
entry.RegisteredAtUtc = now;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return changed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -131,7 +131,10 @@ public sealed class PerformanceCollectorService : IHostedService
|
|||||||
DrawSeparator(sb, longestCounterName);
|
DrawSeparator(sb, longestCounterName);
|
||||||
}
|
}
|
||||||
|
|
||||||
var pastEntries = limitBySeconds > 0 ? entry.Value.Where(e => e.Item1.AddMinutes(limitBySeconds / 60.0d) >= TimeOnly.FromDateTime(DateTime.Now)).ToList() : [.. entry.Value];
|
var snapshot = entry.Value.Snapshot();
|
||||||
|
var pastEntries = limitBySeconds > 0
|
||||||
|
? snapshot.Where(e => e.Item1.AddMinutes(limitBySeconds / 60.0d) >= TimeOnly.FromDateTime(DateTime.Now)).ToList()
|
||||||
|
: snapshot;
|
||||||
|
|
||||||
if (pastEntries.Any())
|
if (pastEntries.Any())
|
||||||
{
|
{
|
||||||
@@ -189,7 +192,11 @@ public sealed class PerformanceCollectorService : IHostedService
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var last = entries.Value.ToList()[^1];
|
if (!entries.Value.TryGetLast(out var last))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (last.Item1.AddMinutes(10) < TimeOnly.FromDateTime(DateTime.Now) && !PerformanceCounters.TryRemove(entries.Key, out _))
|
if (last.Item1.AddMinutes(10) < TimeOnly.FromDateTime(DateTime.Now) && !PerformanceCounters.TryRemove(entries.Key, out _))
|
||||||
{
|
{
|
||||||
_logger.LogDebug("Could not remove performance counter {counter}", entries.Key);
|
_logger.LogDebug("Could not remove performance counter {counter}", entries.Key);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using LightlessSync.Interop.Ipc;
|
|||||||
using LightlessSync.FileCache;
|
using LightlessSync.FileCache;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Penumbra.Api.Enums;
|
using Penumbra.Api.Enums;
|
||||||
|
using System.Globalization;
|
||||||
|
|
||||||
namespace LightlessSync.Services.TextureCompression;
|
namespace LightlessSync.Services.TextureCompression;
|
||||||
|
|
||||||
@@ -27,7 +28,9 @@ public sealed class TextureCompressionService
|
|||||||
public async Task ConvertTexturesAsync(
|
public async Task ConvertTexturesAsync(
|
||||||
IReadOnlyList<TextureCompressionRequest> requests,
|
IReadOnlyList<TextureCompressionRequest> requests,
|
||||||
IProgress<TextureConversionProgress>? progress,
|
IProgress<TextureConversionProgress>? progress,
|
||||||
CancellationToken token)
|
CancellationToken token,
|
||||||
|
bool requestRedraw = true,
|
||||||
|
bool includeMipMaps = true)
|
||||||
{
|
{
|
||||||
if (requests.Count == 0)
|
if (requests.Count == 0)
|
||||||
{
|
{
|
||||||
@@ -48,7 +51,7 @@ public sealed class TextureCompressionService
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
await RunPenumbraConversionAsync(request, textureType, total, completed, progress, token).ConfigureAwait(false);
|
await RunPenumbraConversionAsync(request, textureType, total, completed, progress, token, requestRedraw, includeMipMaps).ConfigureAwait(false);
|
||||||
|
|
||||||
completed++;
|
completed++;
|
||||||
}
|
}
|
||||||
@@ -65,14 +68,16 @@ public sealed class TextureCompressionService
|
|||||||
int total,
|
int total,
|
||||||
int completedBefore,
|
int completedBefore,
|
||||||
IProgress<TextureConversionProgress>? progress,
|
IProgress<TextureConversionProgress>? progress,
|
||||||
CancellationToken token)
|
CancellationToken token,
|
||||||
|
bool requestRedraw,
|
||||||
|
bool includeMipMaps)
|
||||||
{
|
{
|
||||||
var primaryPath = request.PrimaryFilePath;
|
var primaryPath = request.PrimaryFilePath;
|
||||||
var displayJob = new TextureConversionJob(
|
var displayJob = new TextureConversionJob(
|
||||||
primaryPath,
|
primaryPath,
|
||||||
primaryPath,
|
primaryPath,
|
||||||
targetType,
|
targetType,
|
||||||
IncludeMipMaps: true,
|
IncludeMipMaps: includeMipMaps,
|
||||||
request.DuplicateFilePaths);
|
request.DuplicateFilePaths);
|
||||||
|
|
||||||
var backupPath = CreateBackupCopy(primaryPath);
|
var backupPath = CreateBackupCopy(primaryPath);
|
||||||
@@ -83,7 +88,7 @@ public sealed class TextureCompressionService
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
WaitForAccess(primaryPath);
|
WaitForAccess(primaryPath);
|
||||||
await _ipcManager.Penumbra.ConvertTextureFiles(_logger, new[] { conversionJob }, null, token).ConfigureAwait(false);
|
await _ipcManager.Penumbra.ConvertTextureFiles(_logger, new[] { conversionJob }, null, token, requestRedraw).ConfigureAwait(false);
|
||||||
|
|
||||||
if (!IsValidConversionResult(displayJob.OutputFile))
|
if (!IsValidConversionResult(displayJob.OutputFile))
|
||||||
{
|
{
|
||||||
@@ -128,20 +133,47 @@ public sealed class TextureCompressionService
|
|||||||
var cacheEntries = _fileCacheManager.GetFileCachesByPaths(paths.ToArray());
|
var cacheEntries = _fileCacheManager.GetFileCachesByPaths(paths.ToArray());
|
||||||
foreach (var path in paths)
|
foreach (var path in paths)
|
||||||
{
|
{
|
||||||
|
var hasExpectedHash = TryGetExpectedHashFromPath(path, out var expectedHash);
|
||||||
if (!cacheEntries.TryGetValue(path, out var entry) || entry is null)
|
if (!cacheEntries.TryGetValue(path, out var entry) || entry is null)
|
||||||
{
|
{
|
||||||
entry = _fileCacheManager.CreateFileEntry(path);
|
if (hasExpectedHash)
|
||||||
|
{
|
||||||
|
entry = _fileCacheManager.CreateCacheEntryWithKnownHash(path, expectedHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
entry ??= _fileCacheManager.CreateFileEntry(path);
|
||||||
if (entry is null)
|
if (entry is null)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("Unable to locate cache entry for {Path}; skipping hash refresh", path);
|
_logger.LogWarning("Unable to locate cache entry for {Path}; skipping hash refresh", path);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else if (hasExpectedHash && entry.IsCacheEntry && !string.Equals(entry.Hash, expectedHash, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Fixing cache hash mismatch for {Path}: {Current} -> {Expected}", path, entry.Hash, expectedHash);
|
||||||
|
_fileCacheManager.RemoveHashedFile(entry.Hash, entry.PrefixedFilePath, removeDerivedFiles: false);
|
||||||
|
var corrected = _fileCacheManager.CreateCacheEntryWithKnownHash(path, expectedHash);
|
||||||
|
if (corrected is not null)
|
||||||
|
{
|
||||||
|
entry = corrected;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
|
{
|
||||||
|
if (entry.IsCacheEntry)
|
||||||
|
{
|
||||||
|
var info = new FileInfo(path);
|
||||||
|
entry.Size = info.Length;
|
||||||
|
entry.CompressedSize = null;
|
||||||
|
entry.LastModifiedDateTicks = info.LastWriteTimeUtc.Ticks.ToString(CultureInfo.InvariantCulture);
|
||||||
|
_fileCacheManager.UpdateHashedFile(entry, computeProperties: false);
|
||||||
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
_fileCacheManager.UpdateHashedFile(entry);
|
_fileCacheManager.UpdateHashedFile(entry);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogWarning(ex, "Failed to refresh file cache entry for {Path}", path);
|
_logger.LogWarning(ex, "Failed to refresh file cache entry for {Path}", path);
|
||||||
@@ -149,6 +181,35 @@ public sealed class TextureCompressionService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool TryGetExpectedHashFromPath(string path, out string hash)
|
||||||
|
{
|
||||||
|
hash = Path.GetFileNameWithoutExtension(path);
|
||||||
|
if (string.IsNullOrWhiteSpace(hash))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hash.Length is not (40 or 64))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < hash.Length; i++)
|
||||||
|
{
|
||||||
|
var c = hash[i];
|
||||||
|
var isHex = (c >= '0' && c <= '9')
|
||||||
|
|| (c >= 'a' && c <= 'f')
|
||||||
|
|| (c >= 'A' && c <= 'F');
|
||||||
|
if (!isHex)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hash = hash.ToUpperInvariant();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private static readonly string WorkingDirectory =
|
private static readonly string WorkingDirectory =
|
||||||
Path.Combine(Path.GetTempPath(), "LightlessSync.TextureCompression");
|
Path.Combine(Path.GetTempPath(), "LightlessSync.TextureCompression");
|
||||||
|
|
||||||
|
|||||||
@@ -4,9 +4,12 @@ using System.Buffers.Binary;
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Threading;
|
||||||
using OtterTex;
|
using OtterTex;
|
||||||
using OtterImage = OtterTex.Image;
|
using OtterImage = OtterTex.Image;
|
||||||
using LightlessSync.LightlessConfiguration;
|
using LightlessSync.LightlessConfiguration;
|
||||||
|
using LightlessSync.Services;
|
||||||
|
using LightlessSync.Utils;
|
||||||
using LightlessSync.FileCache;
|
using LightlessSync.FileCache;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Lumina.Data.Files;
|
using Lumina.Data.Files;
|
||||||
@@ -30,10 +33,13 @@ public sealed class TextureDownscaleService
|
|||||||
private readonly LightlessConfigService _configService;
|
private readonly LightlessConfigService _configService;
|
||||||
private readonly PlayerPerformanceConfigService _playerPerformanceConfigService;
|
private readonly PlayerPerformanceConfigService _playerPerformanceConfigService;
|
||||||
private readonly FileCacheManager _fileCacheManager;
|
private readonly FileCacheManager _fileCacheManager;
|
||||||
|
private readonly TextureCompressionService _textureCompressionService;
|
||||||
|
private readonly TextureProcessingQueue _processingQueue;
|
||||||
|
|
||||||
private readonly ConcurrentDictionary<string, Task> _activeJobs = new(StringComparer.OrdinalIgnoreCase);
|
private readonly TaskRegistry<string> _downscaleDeduplicator = new();
|
||||||
private readonly ConcurrentDictionary<string, string> _downscaledPaths = new(StringComparer.OrdinalIgnoreCase);
|
private readonly ConcurrentDictionary<string, string> _downscaledPaths = new(StringComparer.OrdinalIgnoreCase);
|
||||||
private readonly SemaphoreSlim _downscaleSemaphore = new(4);
|
private readonly SemaphoreSlim _downscaleSemaphore = new(4);
|
||||||
|
private readonly SemaphoreSlim _compressionSemaphore = new(1);
|
||||||
private static readonly IReadOnlyDictionary<int, TextureCompressionTarget> BlockCompressedFormatMap =
|
private static readonly IReadOnlyDictionary<int, TextureCompressionTarget> BlockCompressedFormatMap =
|
||||||
new Dictionary<int, TextureCompressionTarget>
|
new Dictionary<int, TextureCompressionTarget>
|
||||||
{
|
{
|
||||||
@@ -68,12 +74,16 @@ public sealed class TextureDownscaleService
|
|||||||
ILogger<TextureDownscaleService> logger,
|
ILogger<TextureDownscaleService> logger,
|
||||||
LightlessConfigService configService,
|
LightlessConfigService configService,
|
||||||
PlayerPerformanceConfigService playerPerformanceConfigService,
|
PlayerPerformanceConfigService playerPerformanceConfigService,
|
||||||
FileCacheManager fileCacheManager)
|
FileCacheManager fileCacheManager,
|
||||||
|
TextureCompressionService textureCompressionService,
|
||||||
|
TextureProcessingQueue processingQueue)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_configService = configService;
|
_configService = configService;
|
||||||
_playerPerformanceConfigService = playerPerformanceConfigService;
|
_playerPerformanceConfigService = playerPerformanceConfigService;
|
||||||
_fileCacheManager = fileCacheManager;
|
_fileCacheManager = fileCacheManager;
|
||||||
|
_textureCompressionService = textureCompressionService;
|
||||||
|
_processingQueue = processingQueue;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ScheduleDownscale(string hash, string filePath, TextureMapKind mapKind)
|
public void ScheduleDownscale(string hash, string filePath, TextureMapKind mapKind)
|
||||||
@@ -82,9 +92,9 @@ public sealed class TextureDownscaleService
|
|||||||
public void ScheduleDownscale(string hash, string filePath, Func<TextureMapKind> mapKindFactory)
|
public void ScheduleDownscale(string hash, string filePath, Func<TextureMapKind> mapKindFactory)
|
||||||
{
|
{
|
||||||
if (!filePath.EndsWith(".tex", StringComparison.OrdinalIgnoreCase)) return;
|
if (!filePath.EndsWith(".tex", StringComparison.OrdinalIgnoreCase)) return;
|
||||||
if (_activeJobs.ContainsKey(hash)) return;
|
if (_downscaleDeduplicator.TryGetExisting(hash, out _)) return;
|
||||||
|
|
||||||
_activeJobs[hash] = Task.Run(async () =>
|
_downscaleDeduplicator.GetOrStart(hash, () => _processingQueue.Enqueue(async token =>
|
||||||
{
|
{
|
||||||
TextureMapKind mapKind;
|
TextureMapKind mapKind;
|
||||||
try
|
try
|
||||||
@@ -98,7 +108,7 @@ public sealed class TextureDownscaleService
|
|||||||
}
|
}
|
||||||
|
|
||||||
await DownscaleInternalAsync(hash, filePath, mapKind).ConfigureAwait(false);
|
await DownscaleInternalAsync(hash, filePath, mapKind).ConfigureAwait(false);
|
||||||
}, CancellationToken.None);
|
}, CancellationToken.None));
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool ShouldScheduleDownscale(string filePath)
|
public bool ShouldScheduleDownscale(string filePath)
|
||||||
@@ -107,7 +117,9 @@ public sealed class TextureDownscaleService
|
|||||||
return false;
|
return false;
|
||||||
|
|
||||||
var performanceConfig = _playerPerformanceConfigService.Current;
|
var performanceConfig = _playerPerformanceConfigService.Current;
|
||||||
return performanceConfig.EnableNonIndexTextureMipTrim || performanceConfig.EnableIndexTextureDownscale;
|
return performanceConfig.EnableNonIndexTextureMipTrim
|
||||||
|
|| performanceConfig.EnableIndexTextureDownscale
|
||||||
|
|| performanceConfig.EnableUncompressedTextureCompression;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string GetPreferredPath(string hash, string originalPath)
|
public string GetPreferredPath(string hash, string originalPath)
|
||||||
@@ -144,7 +156,7 @@ public sealed class TextureDownscaleService
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_activeJobs.TryGetValue(hash, out var job))
|
if (_downscaleDeduplicator.TryGetExisting(hash, out var job))
|
||||||
{
|
{
|
||||||
pending.Add(job);
|
pending.Add(job);
|
||||||
}
|
}
|
||||||
@@ -182,10 +194,18 @@ public sealed class TextureDownscaleService
|
|||||||
targetMaxDimension = ResolveTargetMaxDimension();
|
targetMaxDimension = ResolveTargetMaxDimension();
|
||||||
onlyDownscaleUncompressed = performanceConfig.OnlyDownscaleUncompressedTextures;
|
onlyDownscaleUncompressed = performanceConfig.OnlyDownscaleUncompressedTextures;
|
||||||
|
|
||||||
|
if (onlyDownscaleUncompressed && !headerInfo.HasValue)
|
||||||
|
{
|
||||||
|
_downscaledPaths[hash] = sourcePath;
|
||||||
|
_logger.LogTrace("Skipping downscale for texture {Hash}; format unknown and only-uncompressed enabled.", hash);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
destination = Path.Combine(GetDownscaledDirectory(), $"{hash}.tex");
|
destination = Path.Combine(GetDownscaledDirectory(), $"{hash}.tex");
|
||||||
if (File.Exists(destination))
|
if (File.Exists(destination))
|
||||||
{
|
{
|
||||||
RegisterDownscaledTexture(hash, sourcePath, destination);
|
RegisterDownscaledTexture(hash, sourcePath, destination);
|
||||||
|
await TryAutoCompressAsync(hash, destination, mapKind, null).ConfigureAwait(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,6 +216,7 @@ public sealed class TextureDownscaleService
|
|||||||
if (performanceConfig.EnableNonIndexTextureMipTrim
|
if (performanceConfig.EnableNonIndexTextureMipTrim
|
||||||
&& await TryDropTopMipAsync(hash, sourcePath, destination, targetMaxDimension, onlyDownscaleUncompressed, headerInfo).ConfigureAwait(false))
|
&& await TryDropTopMipAsync(hash, sourcePath, destination, targetMaxDimension, onlyDownscaleUncompressed, headerInfo).ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
|
await TryAutoCompressAsync(hash, destination, mapKind, null).ConfigureAwait(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,6 +227,7 @@ public sealed class TextureDownscaleService
|
|||||||
|
|
||||||
_downscaledPaths[hash] = sourcePath;
|
_downscaledPaths[hash] = sourcePath;
|
||||||
_logger.LogTrace("Skipping downscale for non-index texture {Hash}; no mip reduction required.", hash);
|
_logger.LogTrace("Skipping downscale for non-index texture {Hash}; no mip reduction required.", hash);
|
||||||
|
await TryAutoCompressAsync(hash, sourcePath, mapKind, headerInfo).ConfigureAwait(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,6 +235,7 @@ public sealed class TextureDownscaleService
|
|||||||
{
|
{
|
||||||
_downscaledPaths[hash] = sourcePath;
|
_downscaledPaths[hash] = sourcePath;
|
||||||
_logger.LogTrace("Skipping downscale for index texture {Hash}; feature disabled.", hash);
|
_logger.LogTrace("Skipping downscale for index texture {Hash}; feature disabled.", hash);
|
||||||
|
await TryAutoCompressAsync(hash, sourcePath, mapKind, headerInfo).ConfigureAwait(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,6 +245,7 @@ public sealed class TextureDownscaleService
|
|||||||
{
|
{
|
||||||
_downscaledPaths[hash] = sourcePath;
|
_downscaledPaths[hash] = sourcePath;
|
||||||
_logger.LogTrace("Skipping downscale for index texture {Hash}; header dimensions {Width}x{Height} within target.", hash, headerValue.Width, headerValue.Height);
|
_logger.LogTrace("Skipping downscale for index texture {Hash}; header dimensions {Width}x{Height} within target.", hash, headerValue.Width, headerValue.Height);
|
||||||
|
await TryAutoCompressAsync(hash, sourcePath, mapKind, headerInfo).ConfigureAwait(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,10 +253,12 @@ public sealed class TextureDownscaleService
|
|||||||
{
|
{
|
||||||
_downscaledPaths[hash] = sourcePath;
|
_downscaledPaths[hash] = sourcePath;
|
||||||
_logger.LogTrace("Skipping downscale for index texture {Hash}; block compressed format {Format}.", hash, headerInfo.Value.Format);
|
_logger.LogTrace("Skipping downscale for index texture {Hash}; block compressed format {Format}.", hash, headerInfo.Value.Format);
|
||||||
|
await TryAutoCompressAsync(hash, sourcePath, mapKind, headerInfo).ConfigureAwait(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
using var sourceScratch = TexFileHelper.Load(sourcePath);
|
using var sourceScratch = TexFileHelper.Load(sourcePath);
|
||||||
|
var sourceFormat = sourceScratch.Meta.Format;
|
||||||
using var rgbaScratch = sourceScratch.GetRGBA(out var rgbaInfo).ThrowIfError(rgbaInfo);
|
using var rgbaScratch = sourceScratch.GetRGBA(out var rgbaInfo).ThrowIfError(rgbaInfo);
|
||||||
|
|
||||||
var bytesPerPixel = rgbaInfo.Meta.Format.BitsPerPixel() / 8;
|
var bytesPerPixel = rgbaInfo.Meta.Format.BitsPerPixel() / 8;
|
||||||
@@ -248,17 +274,40 @@ public sealed class TextureDownscaleService
|
|||||||
{
|
{
|
||||||
_downscaledPaths[hash] = sourcePath;
|
_downscaledPaths[hash] = sourcePath;
|
||||||
_logger.LogTrace("Skipping downscale for index texture {Hash}; already within bounds.", hash);
|
_logger.LogTrace("Skipping downscale for index texture {Hash}; already within bounds.", hash);
|
||||||
|
await TryAutoCompressAsync(hash, sourcePath, mapKind, headerInfo).ConfigureAwait(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
using var resized = IndexDownscaler.Downscale(originalImage, targetSize.width, targetSize.height, BlockMultiple);
|
using var resized = IndexDownscaler.Downscale(originalImage, targetSize.width, targetSize.height, BlockMultiple);
|
||||||
|
|
||||||
|
var canReencodeWithPenumbra = TryResolveCompressionTarget(headerInfo, sourceFormat, out var compressionTarget);
|
||||||
using var resizedScratch = CreateScratchImage(resized, targetSize.width, targetSize.height);
|
using var resizedScratch = CreateScratchImage(resized, targetSize.width, targetSize.height);
|
||||||
using var finalScratch = resizedScratch.Convert(DXGIFormat.B8G8R8A8UNorm);
|
if (!TryConvertForSave(resizedScratch, sourceFormat, out var finalScratch, canReencodeWithPenumbra))
|
||||||
|
{
|
||||||
|
if (canReencodeWithPenumbra
|
||||||
|
&& await TryReencodeWithPenumbraAsync(hash, sourcePath, destination, resizedScratch, compressionTarget).ConfigureAwait(false))
|
||||||
|
{
|
||||||
|
await TryAutoCompressAsync(hash, destination, mapKind, null).ConfigureAwait(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_downscaledPaths[hash] = sourcePath;
|
||||||
|
_logger.LogTrace(
|
||||||
|
"Skipping downscale for index texture {Hash}; failed to re-encode to {Format}.",
|
||||||
|
hash,
|
||||||
|
sourceFormat);
|
||||||
|
await TryAutoCompressAsync(hash, sourcePath, mapKind, headerInfo).ConfigureAwait(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
using (finalScratch)
|
||||||
|
{
|
||||||
TexFileHelper.Save(destination, finalScratch);
|
TexFileHelper.Save(destination, finalScratch);
|
||||||
RegisterDownscaledTexture(hash, sourcePath, destination);
|
RegisterDownscaledTexture(hash, sourcePath, destination);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await TryAutoCompressAsync(hash, destination, mapKind, null).ConfigureAwait(false);
|
||||||
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
TryDelete(destination);
|
TryDelete(destination);
|
||||||
@@ -277,7 +326,6 @@ public sealed class TextureDownscaleService
|
|||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
_downscaleSemaphore.Release();
|
_downscaleSemaphore.Release();
|
||||||
_activeJobs.TryRemove(hash, out _);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -330,6 +378,164 @@ public sealed class TextureDownscaleService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool TryConvertForSave(
|
||||||
|
ScratchImage source,
|
||||||
|
DXGIFormat sourceFormat,
|
||||||
|
out ScratchImage result,
|
||||||
|
bool attemptPenumbraFallback)
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
var compressedFallback = attemptPenumbraFallback
|
||||||
|
? " Attempting Penumbra re-encode."
|
||||||
|
: " Skipping downscale.";
|
||||||
|
_logger.LogWarning(
|
||||||
|
ex,
|
||||||
|
"Failed to convert downscaled texture to {Format}.{Fallback}",
|
||||||
|
targetFormat,
|
||||||
|
isCompressed ? compressedFallback : " Falling back to B8G8R8A8.");
|
||||||
|
if (isCompressed)
|
||||||
|
{
|
||||||
|
result = default!;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
result = source.Convert(DXGIFormat.B8G8R8A8UNorm);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryResolveCompressionTarget(TexHeaderInfo? headerInfo, DXGIFormat sourceFormat, out TextureCompressionTarget target)
|
||||||
|
{
|
||||||
|
if (headerInfo is { } info && TryGetCompressionTarget(info.Format, out target))
|
||||||
|
{
|
||||||
|
return _textureCompressionService.IsTargetSelectable(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sourceFormat.IsCompressed() && BlockCompressedFormatMap.TryGetValue((int)sourceFormat, out target))
|
||||||
|
{
|
||||||
|
return _textureCompressionService.IsTargetSelectable(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
target = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<bool> TryReencodeWithPenumbraAsync(
|
||||||
|
string hash,
|
||||||
|
string sourcePath,
|
||||||
|
string destination,
|
||||||
|
ScratchImage resizedScratch,
|
||||||
|
TextureCompressionTarget target)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Downscale Penumbra re-encode target {Target} for {Hash}.", target, hash);
|
||||||
|
using var uncompressed = resizedScratch.Convert(DXGIFormat.B8G8R8A8UNorm);
|
||||||
|
TexFileHelper.Save(destination, uncompressed);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Failed to save uncompressed downscaled texture for {Hash}. Skipping downscale.", hash);
|
||||||
|
TryDelete(destination);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _compressionSemaphore.WaitAsync().ConfigureAwait(false);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var request = new TextureCompressionRequest(destination, Array.Empty<string>(), target);
|
||||||
|
await _textureCompressionService
|
||||||
|
.ConvertTexturesAsync(new[] { request }, null, CancellationToken.None, requestRedraw: false)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Failed to re-encode downscaled texture {Hash} to {Target}. Skipping downscale.", hash, target);
|
||||||
|
TryDelete(destination);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_compressionSemaphore.Release();
|
||||||
|
}
|
||||||
|
|
||||||
|
RegisterDownscaledTexture(hash, sourcePath, destination);
|
||||||
|
_logger.LogDebug("Downscaled texture {Hash} -> {Path} (re-encoded via Penumbra).", hash, destination);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task TryAutoCompressAsync(string hash, string texturePath, TextureMapKind mapKind, TexHeaderInfo? headerInfo)
|
||||||
|
{
|
||||||
|
var performanceConfig = _playerPerformanceConfigService.Current;
|
||||||
|
if (!performanceConfig.EnableUncompressedTextureCompression)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(texturePath) || !File.Exists(texturePath))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var info = headerInfo ?? (TryReadTexHeader(texturePath, out var header) ? header : (TexHeaderInfo?)null);
|
||||||
|
if (!info.HasValue)
|
||||||
|
{
|
||||||
|
_logger.LogTrace("Skipping auto-compress for texture {Hash}; unable to read header.", hash);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsBlockCompressedFormat(info.Value.Format))
|
||||||
|
{
|
||||||
|
_logger.LogTrace("Skipping auto-compress for texture {Hash}; already block-compressed.", hash);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var suggestion = TextureMetadataHelper.GetSuggestedTarget(info.Value.Format.ToString(), mapKind, texturePath);
|
||||||
|
if (suggestion is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var target = _textureCompressionService.NormalizeTarget(suggestion.Value.Target);
|
||||||
|
if (!_textureCompressionService.IsTargetSelectable(target))
|
||||||
|
{
|
||||||
|
_logger.LogTrace("Skipping auto-compress for texture {Hash}; target {Target} not supported.", hash, target);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _compressionSemaphore.WaitAsync().ConfigureAwait(false);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var includeMipMaps = !performanceConfig.SkipUncompressedTextureCompressionMipMaps;
|
||||||
|
var request = new TextureCompressionRequest(texturePath, Array.Empty<string>(), target);
|
||||||
|
await _textureCompressionService
|
||||||
|
.ConvertTexturesAsync(new[] { request }, null, CancellationToken.None, requestRedraw: false, includeMipMaps: includeMipMaps)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Auto-compress failed for texture {Hash} ({Path})", hash, texturePath);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_compressionSemaphore.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static bool IsIndexMap(TextureMapKind kind)
|
private static bool IsIndexMap(TextureMapKind kind)
|
||||||
=> kind is TextureMapKind.Mask
|
=> kind is TextureMapKind.Mask
|
||||||
or TextureMapKind.Index;
|
or TextureMapKind.Index;
|
||||||
|
|||||||
19
LightlessSync/Services/TextureProcessingQueue.cs
Normal file
19
LightlessSync/Services/TextureProcessingQueue.cs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
@@ -13,16 +13,20 @@ namespace LightlessSync.Services;
|
|||||||
public sealed class UiService : DisposableMediatorSubscriberBase
|
public sealed class UiService : DisposableMediatorSubscriberBase
|
||||||
{
|
{
|
||||||
private readonly List<WindowMediatorSubscriberBase> _createdWindows = [];
|
private readonly List<WindowMediatorSubscriberBase> _createdWindows = [];
|
||||||
|
private readonly List<WindowMediatorSubscriberBase> _registeredWindows = [];
|
||||||
|
private readonly HashSet<WindowMediatorSubscriberBase> _uiHiddenWindows = [];
|
||||||
private readonly IUiBuilder _uiBuilder;
|
private readonly IUiBuilder _uiBuilder;
|
||||||
private readonly FileDialogManager _fileDialogManager;
|
private readonly FileDialogManager _fileDialogManager;
|
||||||
private readonly ILogger<UiService> _logger;
|
private readonly ILogger<UiService> _logger;
|
||||||
private readonly LightlessConfigService _lightlessConfigService;
|
private readonly LightlessConfigService _lightlessConfigService;
|
||||||
|
private readonly DalamudUtilService _dalamudUtilService;
|
||||||
private readonly WindowSystem _windowSystem;
|
private readonly WindowSystem _windowSystem;
|
||||||
private readonly UiFactory _uiFactory;
|
private readonly UiFactory _uiFactory;
|
||||||
private readonly PairFactory _pairFactory;
|
private readonly PairFactory _pairFactory;
|
||||||
|
private bool _uiHideActive;
|
||||||
|
|
||||||
public UiService(ILogger<UiService> logger, IUiBuilder uiBuilder,
|
public UiService(ILogger<UiService> logger, IUiBuilder uiBuilder,
|
||||||
LightlessConfigService lightlessConfigService, WindowSystem windowSystem,
|
LightlessConfigService lightlessConfigService, DalamudUtilService dalamudUtilService, WindowSystem windowSystem,
|
||||||
IEnumerable<WindowMediatorSubscriberBase> windows,
|
IEnumerable<WindowMediatorSubscriberBase> windows,
|
||||||
UiFactory uiFactory, FileDialogManager fileDialogManager,
|
UiFactory uiFactory, FileDialogManager fileDialogManager,
|
||||||
LightlessMediator lightlessMediator, PairFactory pairFactory) : base(logger, lightlessMediator)
|
LightlessMediator lightlessMediator, PairFactory pairFactory) : base(logger, lightlessMediator)
|
||||||
@@ -31,6 +35,7 @@ public sealed class UiService : DisposableMediatorSubscriberBase
|
|||||||
_logger.LogTrace("Creating {type}", GetType().Name);
|
_logger.LogTrace("Creating {type}", GetType().Name);
|
||||||
_uiBuilder = uiBuilder;
|
_uiBuilder = uiBuilder;
|
||||||
_lightlessConfigService = lightlessConfigService;
|
_lightlessConfigService = lightlessConfigService;
|
||||||
|
_dalamudUtilService = dalamudUtilService;
|
||||||
_windowSystem = windowSystem;
|
_windowSystem = windowSystem;
|
||||||
_uiFactory = uiFactory;
|
_uiFactory = uiFactory;
|
||||||
_pairFactory = pairFactory;
|
_pairFactory = pairFactory;
|
||||||
@@ -43,6 +48,7 @@ public sealed class UiService : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
foreach (var window in windows)
|
foreach (var window in windows)
|
||||||
{
|
{
|
||||||
|
_registeredWindows.Add(window);
|
||||||
_windowSystem.AddWindow(window);
|
_windowSystem.AddWindow(window);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,6 +182,8 @@ public sealed class UiService : DisposableMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
_windowSystem.RemoveWindow(msg.Window);
|
_windowSystem.RemoveWindow(msg.Window);
|
||||||
_createdWindows.Remove(msg.Window);
|
_createdWindows.Remove(msg.Window);
|
||||||
|
_registeredWindows.Remove(msg.Window);
|
||||||
|
_uiHiddenWindows.Remove(msg.Window);
|
||||||
msg.Window.Dispose();
|
msg.Window.Dispose();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -219,7 +227,10 @@ public sealed class UiService : DisposableMediatorSubscriberBase
|
|||||||
MainStyle.PushStyle();
|
MainStyle.PushStyle();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
var hideOtherUi = ShouldHideOtherUi();
|
||||||
|
UpdateUiHideState(hideOtherUi);
|
||||||
_windowSystem.Draw();
|
_windowSystem.Draw();
|
||||||
|
if (!hideOtherUi)
|
||||||
_fileDialogManager.Draw();
|
_fileDialogManager.Draw();
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
@@ -227,4 +238,61 @@ public sealed class UiService : DisposableMediatorSubscriberBase
|
|||||||
MainStyle.PopStyle();
|
MainStyle.PopStyle();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool ShouldHideOtherUi()
|
||||||
|
{
|
||||||
|
var config = _lightlessConfigService.Current;
|
||||||
|
if (!config.ShowUiWhenUiHidden && _dalamudUtilService.IsGameUiHidden)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (!config.ShowUiInGpose && _dalamudUtilService.IsInGpose)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateUiHideState(bool hideOtherUi)
|
||||||
|
{
|
||||||
|
if (!hideOtherUi)
|
||||||
|
{
|
||||||
|
if (_uiHideActive)
|
||||||
|
{
|
||||||
|
foreach (var window in _uiHiddenWindows)
|
||||||
|
{
|
||||||
|
window.IsOpen = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_uiHiddenWindows.Clear();
|
||||||
|
_uiHideActive = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_uiHideActive = true;
|
||||||
|
foreach (var window in EnumerateManagedWindows())
|
||||||
|
{
|
||||||
|
if (window is ZoneChatUi)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (!window.IsOpen)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
_uiHiddenWindows.Add(window);
|
||||||
|
window.IsOpen = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerable<WindowMediatorSubscriberBase> EnumerateManagedWindows()
|
||||||
|
{
|
||||||
|
foreach (var window in _registeredWindows)
|
||||||
|
{
|
||||||
|
yield return window;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var window in _createdWindows)
|
||||||
|
{
|
||||||
|
yield return window;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -480,6 +480,20 @@ public sealed partial class XivDataAnalyzer
|
|||||||
return CalculateTrianglesFromPath(hash, path.ResolvedFilepath, _configService.Current.TriangleDictionary, _failedCalculatedTris);
|
return CalculateTrianglesFromPath(hash, path.ResolvedFilepath, _configService.Current.TriangleDictionary, _failedCalculatedTris);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public long RefreshTrianglesForPath(string hash, string filePath)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(filePath)
|
||||||
|
|| !filePath.EndsWith(".mdl", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| !File.Exists(filePath))
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
_failedCalculatedTris.RemoveAll(entry => entry.Equals(hash, StringComparison.Ordinal));
|
||||||
|
_configService.Current.TriangleDictionary.TryRemove(hash, out _);
|
||||||
|
return CalculateTrianglesFromPath(hash, filePath, _configService.Current.TriangleDictionary, _failedCalculatedTris);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<long> GetEffectiveTrianglesByHash(string hash, string filePath)
|
public async Task<long> GetEffectiveTrianglesByHash(string hash, string filePath)
|
||||||
{
|
{
|
||||||
if (_configService.Current.EffectiveTriangleDictionary.TryGetValue(hash, out var cachedTris) && cachedTris > 0)
|
if (_configService.Current.EffectiveTriangleDictionary.TryGetValue(hash, out var cachedTris) && cachedTris > 0)
|
||||||
|
|||||||
@@ -1,169 +0,0 @@
|
|||||||
#region License
|
|
||||||
/*
|
|
||||||
MIT License
|
|
||||||
|
|
||||||
Copyright(c) 2017-2018 Mattias Edlund
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
*/
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace MeshDecimator.Algorithms
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// A decimation algorithm.
|
|
||||||
/// </summary>
|
|
||||||
public abstract class DecimationAlgorithm
|
|
||||||
{
|
|
||||||
#region Delegates
|
|
||||||
/// <summary>
|
|
||||||
/// A callback for decimation status reports.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="iteration">The current iteration, starting at zero.</param>
|
|
||||||
/// <param name="originalTris">The original count of triangles.</param>
|
|
||||||
/// <param name="currentTris">The current count of triangles.</param>
|
|
||||||
/// <param name="targetTris">The target count of triangles.</param>
|
|
||||||
public delegate void StatusReportCallback(int iteration, int originalTris, int currentTris, int targetTris);
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Fields
|
|
||||||
private bool preserveBorders = false;
|
|
||||||
private int maxVertexCount = 0;
|
|
||||||
private bool verbose = false;
|
|
||||||
|
|
||||||
private StatusReportCallback statusReportInvoker = null;
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Properties
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets if borders should be kept.
|
|
||||||
/// Default value: false
|
|
||||||
/// </summary>
|
|
||||||
[Obsolete("Use the 'DecimationAlgorithm.PreserveBorders' property instead.", false)]
|
|
||||||
public bool KeepBorders
|
|
||||||
{
|
|
||||||
get { return preserveBorders; }
|
|
||||||
set { preserveBorders = value; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets if borders should be preserved.
|
|
||||||
/// Default value: false
|
|
||||||
/// </summary>
|
|
||||||
public bool PreserveBorders
|
|
||||||
{
|
|
||||||
get { return preserveBorders; }
|
|
||||||
set { preserveBorders = value; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets if linked vertices should be kept.
|
|
||||||
/// Default value: false
|
|
||||||
/// </summary>
|
|
||||||
[Obsolete("This feature has been removed, for more details why please read the readme.", true)]
|
|
||||||
public bool KeepLinkedVertices
|
|
||||||
{
|
|
||||||
get { return false; }
|
|
||||||
set { }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the maximum vertex count. Set to zero for no limitation.
|
|
||||||
/// Default value: 0 (no limitation)
|
|
||||||
/// </summary>
|
|
||||||
public int MaxVertexCount
|
|
||||||
{
|
|
||||||
get { return maxVertexCount; }
|
|
||||||
set { maxVertexCount = Math.MathHelper.Max(value, 0); }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets if verbose information should be printed in the console.
|
|
||||||
/// Default value: false
|
|
||||||
/// </summary>
|
|
||||||
public bool Verbose
|
|
||||||
{
|
|
||||||
get { return verbose; }
|
|
||||||
set { verbose = value; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the logger used for diagnostics.
|
|
||||||
/// </summary>
|
|
||||||
public ILogger? Logger { get; set; }
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Events
|
|
||||||
/// <summary>
|
|
||||||
/// An event for status reports for this algorithm.
|
|
||||||
/// </summary>
|
|
||||||
public event StatusReportCallback StatusReport
|
|
||||||
{
|
|
||||||
add { statusReportInvoker += value; }
|
|
||||||
remove { statusReportInvoker -= value; }
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Protected Methods
|
|
||||||
/// <summary>
|
|
||||||
/// Reports the current status of the decimation.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="iteration">The current iteration, starting at zero.</param>
|
|
||||||
/// <param name="originalTris">The original count of triangles.</param>
|
|
||||||
/// <param name="currentTris">The current count of triangles.</param>
|
|
||||||
/// <param name="targetTris">The target count of triangles.</param>
|
|
||||||
protected void ReportStatus(int iteration, int originalTris, int currentTris, int targetTris)
|
|
||||||
{
|
|
||||||
var statusReportInvoker = this.statusReportInvoker;
|
|
||||||
if (statusReportInvoker != null)
|
|
||||||
{
|
|
||||||
statusReportInvoker.Invoke(iteration, originalTris, currentTris, targetTris);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Public Methods
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes the algorithm with the original mesh.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="mesh">The mesh.</param>
|
|
||||||
public abstract void Initialize(Mesh mesh);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Decimates the mesh.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="targetTrisCount">The target triangle count.</param>
|
|
||||||
public abstract void DecimateMesh(int targetTrisCount);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Decimates the mesh without losing any quality.
|
|
||||||
/// </summary>
|
|
||||||
public abstract void DecimateMeshLossless();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns the resulting mesh.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>The resulting mesh.</returns>
|
|
||||||
public abstract Mesh ToMesh();
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
249
LightlessSync/ThirdParty/MeshDecimator/BoneWeight.cs
vendored
249
LightlessSync/ThirdParty/MeshDecimator/BoneWeight.cs
vendored
@@ -1,249 +0,0 @@
|
|||||||
#region License
|
|
||||||
/*
|
|
||||||
MIT License
|
|
||||||
|
|
||||||
Copyright(c) 2017-2018 Mattias Edlund
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
*/
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using MeshDecimator.Math;
|
|
||||||
|
|
||||||
namespace MeshDecimator
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// A bone weight.
|
|
||||||
/// </summary>
|
|
||||||
public struct BoneWeight : IEquatable<BoneWeight>
|
|
||||||
{
|
|
||||||
#region Fields
|
|
||||||
/// <summary>
|
|
||||||
/// The first bone index.
|
|
||||||
/// </summary>
|
|
||||||
public int boneIndex0;
|
|
||||||
/// <summary>
|
|
||||||
/// The second bone index.
|
|
||||||
/// </summary>
|
|
||||||
public int boneIndex1;
|
|
||||||
/// <summary>
|
|
||||||
/// The third bone index.
|
|
||||||
/// </summary>
|
|
||||||
public int boneIndex2;
|
|
||||||
/// <summary>
|
|
||||||
/// The fourth bone index.
|
|
||||||
/// </summary>
|
|
||||||
public int boneIndex3;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The first bone weight.
|
|
||||||
/// </summary>
|
|
||||||
public float boneWeight0;
|
|
||||||
/// <summary>
|
|
||||||
/// The second bone weight.
|
|
||||||
/// </summary>
|
|
||||||
public float boneWeight1;
|
|
||||||
/// <summary>
|
|
||||||
/// The third bone weight.
|
|
||||||
/// </summary>
|
|
||||||
public float boneWeight2;
|
|
||||||
/// <summary>
|
|
||||||
/// The fourth bone weight.
|
|
||||||
/// </summary>
|
|
||||||
public float boneWeight3;
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Constructor
|
|
||||||
/// <summary>
|
|
||||||
/// Creates a new bone weight.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="boneIndex0">The first bone index.</param>
|
|
||||||
/// <param name="boneIndex1">The second bone index.</param>
|
|
||||||
/// <param name="boneIndex2">The third bone index.</param>
|
|
||||||
/// <param name="boneIndex3">The fourth bone index.</param>
|
|
||||||
/// <param name="boneWeight0">The first bone weight.</param>
|
|
||||||
/// <param name="boneWeight1">The second bone weight.</param>
|
|
||||||
/// <param name="boneWeight2">The third bone weight.</param>
|
|
||||||
/// <param name="boneWeight3">The fourth bone weight.</param>
|
|
||||||
public BoneWeight(int boneIndex0, int boneIndex1, int boneIndex2, int boneIndex3, float boneWeight0, float boneWeight1, float boneWeight2, float boneWeight3)
|
|
||||||
{
|
|
||||||
this.boneIndex0 = boneIndex0;
|
|
||||||
this.boneIndex1 = boneIndex1;
|
|
||||||
this.boneIndex2 = boneIndex2;
|
|
||||||
this.boneIndex3 = boneIndex3;
|
|
||||||
|
|
||||||
this.boneWeight0 = boneWeight0;
|
|
||||||
this.boneWeight1 = boneWeight1;
|
|
||||||
this.boneWeight2 = boneWeight2;
|
|
||||||
this.boneWeight3 = boneWeight3;
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Operators
|
|
||||||
/// <summary>
|
|
||||||
/// Returns if two bone weights equals eachother.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="lhs">The left hand side bone weight.</param>
|
|
||||||
/// <param name="rhs">The right hand side bone weight.</param>
|
|
||||||
/// <returns>If equals.</returns>
|
|
||||||
public static bool operator ==(BoneWeight lhs, BoneWeight rhs)
|
|
||||||
{
|
|
||||||
return (lhs.boneIndex0 == rhs.boneIndex0 && lhs.boneIndex1 == rhs.boneIndex1 && lhs.boneIndex2 == rhs.boneIndex2 && lhs.boneIndex3 == rhs.boneIndex3 &&
|
|
||||||
new Vector4(lhs.boneWeight0, lhs.boneWeight1, lhs.boneWeight2, lhs.boneWeight3) == new Vector4(rhs.boneWeight0, rhs.boneWeight1, rhs.boneWeight2, rhs.boneWeight3));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns if two bone weights don't equal eachother.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="lhs">The left hand side bone weight.</param>
|
|
||||||
/// <param name="rhs">The right hand side bone weight.</param>
|
|
||||||
/// <returns>If not equals.</returns>
|
|
||||||
public static bool operator !=(BoneWeight lhs, BoneWeight rhs)
|
|
||||||
{
|
|
||||||
return !(lhs == rhs);
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Private Methods
|
|
||||||
private void MergeBoneWeight(int boneIndex, float weight)
|
|
||||||
{
|
|
||||||
if (boneIndex == boneIndex0)
|
|
||||||
{
|
|
||||||
boneWeight0 = (boneWeight0 + weight) * 0.5f;
|
|
||||||
}
|
|
||||||
else if (boneIndex == boneIndex1)
|
|
||||||
{
|
|
||||||
boneWeight1 = (boneWeight1 + weight) * 0.5f;
|
|
||||||
}
|
|
||||||
else if (boneIndex == boneIndex2)
|
|
||||||
{
|
|
||||||
boneWeight2 = (boneWeight2 + weight) * 0.5f;
|
|
||||||
}
|
|
||||||
else if (boneIndex == boneIndex3)
|
|
||||||
{
|
|
||||||
boneWeight3 = (boneWeight3 + weight) * 0.5f;
|
|
||||||
}
|
|
||||||
else if(boneWeight0 == 0f)
|
|
||||||
{
|
|
||||||
boneIndex0 = boneIndex;
|
|
||||||
boneWeight0 = weight;
|
|
||||||
}
|
|
||||||
else if (boneWeight1 == 0f)
|
|
||||||
{
|
|
||||||
boneIndex1 = boneIndex;
|
|
||||||
boneWeight1 = weight;
|
|
||||||
}
|
|
||||||
else if (boneWeight2 == 0f)
|
|
||||||
{
|
|
||||||
boneIndex2 = boneIndex;
|
|
||||||
boneWeight2 = weight;
|
|
||||||
}
|
|
||||||
else if (boneWeight3 == 0f)
|
|
||||||
{
|
|
||||||
boneIndex3 = boneIndex;
|
|
||||||
boneWeight3 = weight;
|
|
||||||
}
|
|
||||||
Normalize();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Normalize()
|
|
||||||
{
|
|
||||||
float mag = (float)System.Math.Sqrt(boneWeight0 * boneWeight0 + boneWeight1 * boneWeight1 + boneWeight2 * boneWeight2 + boneWeight3 * boneWeight3);
|
|
||||||
if (mag > float.Epsilon)
|
|
||||||
{
|
|
||||||
boneWeight0 /= mag;
|
|
||||||
boneWeight1 /= mag;
|
|
||||||
boneWeight2 /= mag;
|
|
||||||
boneWeight3 /= mag;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
boneWeight0 = boneWeight1 = boneWeight2 = boneWeight3 = 0f;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Public Methods
|
|
||||||
#region Object
|
|
||||||
/// <summary>
|
|
||||||
/// Returns a hash code for this vector.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>The hash code.</returns>
|
|
||||||
public override int GetHashCode()
|
|
||||||
{
|
|
||||||
return boneIndex0.GetHashCode() ^ boneIndex1.GetHashCode() << 2 ^ boneIndex2.GetHashCode() >> 2 ^ boneIndex3.GetHashCode() >>
|
|
||||||
1 ^ boneWeight0.GetHashCode() << 5 ^ boneWeight1.GetHashCode() << 4 ^ boneWeight2.GetHashCode() >> 4 ^ boneWeight3.GetHashCode() >> 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns if this bone weight is equal to another object.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="obj">The other object to compare to.</param>
|
|
||||||
/// <returns>If equals.</returns>
|
|
||||||
public override bool Equals(object obj)
|
|
||||||
{
|
|
||||||
if (!(obj is BoneWeight))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
BoneWeight other = (BoneWeight)obj;
|
|
||||||
return (boneIndex0 == other.boneIndex0 && boneIndex1 == other.boneIndex1 && boneIndex2 == other.boneIndex2 && boneIndex3 == other.boneIndex3 &&
|
|
||||||
boneWeight0 == other.boneWeight0 && boneWeight1 == other.boneWeight1 && boneWeight2 == other.boneWeight2 && boneWeight3 == other.boneWeight3);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns if this bone weight is equal to another one.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="other">The other bone weight to compare to.</param>
|
|
||||||
/// <returns>If equals.</returns>
|
|
||||||
public bool Equals(BoneWeight other)
|
|
||||||
{
|
|
||||||
return (boneIndex0 == other.boneIndex0 && boneIndex1 == other.boneIndex1 && boneIndex2 == other.boneIndex2 && boneIndex3 == other.boneIndex3 &&
|
|
||||||
boneWeight0 == other.boneWeight0 && boneWeight1 == other.boneWeight1 && boneWeight2 == other.boneWeight2 && boneWeight3 == other.boneWeight3);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns a nicely formatted string for this bone weight.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>The string.</returns>
|
|
||||||
public override string ToString()
|
|
||||||
{
|
|
||||||
return string.Format("({0}:{4:F1}, {1}:{5:F1}, {2}:{6:F1}, {3}:{7:F1})",
|
|
||||||
boneIndex0, boneIndex1, boneIndex2, boneIndex3, boneWeight0, boneWeight1, boneWeight2, boneWeight3);
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Static
|
|
||||||
/// <summary>
|
|
||||||
/// Merges two bone weights and stores the merged result in the first parameter.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="a">The first bone weight, also stores result.</param>
|
|
||||||
/// <param name="b">The second bone weight.</param>
|
|
||||||
public static void Merge(ref BoneWeight a, ref BoneWeight b)
|
|
||||||
{
|
|
||||||
if (b.boneWeight0 > 0f) a.MergeBoneWeight(b.boneIndex0, b.boneWeight0);
|
|
||||||
if (b.boneWeight1 > 0f) a.MergeBoneWeight(b.boneIndex1, b.boneWeight1);
|
|
||||||
if (b.boneWeight2 > 0f) a.MergeBoneWeight(b.boneIndex2, b.boneWeight2);
|
|
||||||
if (b.boneWeight3 > 0f) a.MergeBoneWeight(b.boneIndex3, b.boneWeight3);
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,179 +0,0 @@
|
|||||||
#region License
|
|
||||||
/*
|
|
||||||
MIT License
|
|
||||||
|
|
||||||
Copyright(c) 2017-2018 Mattias Edlund
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
*/
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
using System;
|
|
||||||
|
|
||||||
namespace MeshDecimator.Collections
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// A resizable array.
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="T">The item type.</typeparam>
|
|
||||||
internal sealed class ResizableArray<T>
|
|
||||||
{
|
|
||||||
#region Fields
|
|
||||||
private T[] items = null;
|
|
||||||
private int length = 0;
|
|
||||||
|
|
||||||
private static T[] emptyArr = new T[0];
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Properties
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the length of this array.
|
|
||||||
/// </summary>
|
|
||||||
public int Length
|
|
||||||
{
|
|
||||||
get { return length; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the internal data buffer for this array.
|
|
||||||
/// </summary>
|
|
||||||
public T[] Data
|
|
||||||
{
|
|
||||||
get { return items; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the element value at a specific index.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="index">The element index.</param>
|
|
||||||
/// <returns>The element value.</returns>
|
|
||||||
public T this[int index]
|
|
||||||
{
|
|
||||||
get { return items[index]; }
|
|
||||||
set { items[index] = value; }
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Constructor
|
|
||||||
/// <summary>
|
|
||||||
/// Creates a new resizable array.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="capacity">The initial array capacity.</param>
|
|
||||||
public ResizableArray(int capacity)
|
|
||||||
: this(capacity, 0)
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Creates a new resizable array.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="capacity">The initial array capacity.</param>
|
|
||||||
/// <param name="length">The initial length of the array.</param>
|
|
||||||
public ResizableArray(int capacity, int length)
|
|
||||||
{
|
|
||||||
if (capacity < 0)
|
|
||||||
throw new ArgumentOutOfRangeException("capacity");
|
|
||||||
else if (length < 0 || length > capacity)
|
|
||||||
throw new ArgumentOutOfRangeException("length");
|
|
||||||
|
|
||||||
if (capacity > 0)
|
|
||||||
items = new T[capacity];
|
|
||||||
else
|
|
||||||
items = emptyArr;
|
|
||||||
|
|
||||||
this.length = length;
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Private Methods
|
|
||||||
private void IncreaseCapacity(int capacity)
|
|
||||||
{
|
|
||||||
T[] newItems = new T[capacity];
|
|
||||||
Array.Copy(items, 0, newItems, 0, System.Math.Min(length, capacity));
|
|
||||||
items = newItems;
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Public Methods
|
|
||||||
/// <summary>
|
|
||||||
/// Clears this array.
|
|
||||||
/// </summary>
|
|
||||||
public void Clear()
|
|
||||||
{
|
|
||||||
Array.Clear(items, 0, length);
|
|
||||||
length = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Resizes this array.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="length">The new length.</param>
|
|
||||||
/// <param name="trimExess">If exess memory should be trimmed.</param>
|
|
||||||
public void Resize(int length, bool trimExess = false)
|
|
||||||
{
|
|
||||||
if (length < 0)
|
|
||||||
throw new ArgumentOutOfRangeException("capacity");
|
|
||||||
|
|
||||||
if (length > items.Length)
|
|
||||||
{
|
|
||||||
IncreaseCapacity(length);
|
|
||||||
}
|
|
||||||
else if (length < this.length)
|
|
||||||
{
|
|
||||||
//Array.Clear(items, capacity, length - capacity);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.length = length;
|
|
||||||
|
|
||||||
if (trimExess)
|
|
||||||
{
|
|
||||||
TrimExcess();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Trims any excess memory for this array.
|
|
||||||
/// </summary>
|
|
||||||
public void TrimExcess()
|
|
||||||
{
|
|
||||||
if (items.Length == length) // Nothing to do
|
|
||||||
return;
|
|
||||||
|
|
||||||
T[] newItems = new T[length];
|
|
||||||
Array.Copy(items, 0, newItems, 0, length);
|
|
||||||
items = newItems;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Adds a new item to the end of this array.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="item">The new item.</param>
|
|
||||||
public void Add(T item)
|
|
||||||
{
|
|
||||||
if (length >= items.Length)
|
|
||||||
{
|
|
||||||
IncreaseCapacity(items.Length << 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
items[length++] = item;
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
using System;
|
|
||||||
|
|
||||||
namespace MeshDecimator.Collections
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// A collection of UV channels.
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="TVec">The UV vector type.</typeparam>
|
|
||||||
internal sealed class UVChannels<TVec>
|
|
||||||
{
|
|
||||||
#region Fields
|
|
||||||
private ResizableArray<TVec>[] channels = null;
|
|
||||||
private TVec[][] channelsData = null;
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Properties
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the channel collection data.
|
|
||||||
/// </summary>
|
|
||||||
public TVec[][] Data
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
for (int i = 0; i < Mesh.UVChannelCount; i++)
|
|
||||||
{
|
|
||||||
if (channels[i] != null)
|
|
||||||
{
|
|
||||||
channelsData[i] = channels[i].Data;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
channelsData[i] = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return channelsData;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets a specific channel by index.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="index">The channel index.</param>
|
|
||||||
public ResizableArray<TVec> this[int index]
|
|
||||||
{
|
|
||||||
get { return channels[index]; }
|
|
||||||
set { channels[index] = value; }
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Constructor
|
|
||||||
/// <summary>
|
|
||||||
/// Creates a new collection of UV channels.
|
|
||||||
/// </summary>
|
|
||||||
public UVChannels()
|
|
||||||
{
|
|
||||||
channels = new ResizableArray<TVec>[Mesh.UVChannelCount];
|
|
||||||
channelsData = new TVec[Mesh.UVChannelCount][];
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Public Methods
|
|
||||||
/// <summary>
|
|
||||||
/// Resizes all channels at once.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="capacity">The new capacity.</param>
|
|
||||||
/// <param name="trimExess">If exess memory should be trimmed.</param>
|
|
||||||
public void Resize(int capacity, bool trimExess = false)
|
|
||||||
{
|
|
||||||
for (int i = 0; i < Mesh.UVChannelCount; i++)
|
|
||||||
{
|
|
||||||
if (channels[i] != null)
|
|
||||||
{
|
|
||||||
channels[i].Resize(capacity, trimExess);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2017-2018 Mattias Edlund
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
@@ -1,286 +0,0 @@
|
|||||||
#region License
|
|
||||||
/*
|
|
||||||
MIT License
|
|
||||||
|
|
||||||
Copyright(c) 2017-2018 Mattias Edlund
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
*/
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
using System;
|
|
||||||
|
|
||||||
namespace MeshDecimator.Math
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Math helpers.
|
|
||||||
/// </summary>
|
|
||||||
public static class MathHelper
|
|
||||||
{
|
|
||||||
#region Consts
|
|
||||||
/// <summary>
|
|
||||||
/// The Pi constant.
|
|
||||||
/// </summary>
|
|
||||||
public const float PI = 3.14159274f;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The Pi constant.
|
|
||||||
/// </summary>
|
|
||||||
public const double PId = 3.1415926535897932384626433832795;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Degrees to radian constant.
|
|
||||||
/// </summary>
|
|
||||||
public const float Deg2Rad = PI / 180f;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Degrees to radian constant.
|
|
||||||
/// </summary>
|
|
||||||
public const double Deg2Radd = PId / 180.0;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Radians to degrees constant.
|
|
||||||
/// </summary>
|
|
||||||
public const float Rad2Deg = 180f / PI;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Radians to degrees constant.
|
|
||||||
/// </summary>
|
|
||||||
public const double Rad2Degd = 180.0 / PId;
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Min
|
|
||||||
/// <summary>
|
|
||||||
/// Returns the minimum of two values.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="val1">The first value.</param>
|
|
||||||
/// <param name="val2">The second value.</param>
|
|
||||||
/// <returns>The minimum value.</returns>
|
|
||||||
public static int Min(int val1, int val2)
|
|
||||||
{
|
|
||||||
return (val1 < val2 ? val1 : val2);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns the minimum of three values.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="val1">The first value.</param>
|
|
||||||
/// <param name="val2">The second value.</param>
|
|
||||||
/// <param name="val3">The third value.</param>
|
|
||||||
/// <returns>The minimum value.</returns>
|
|
||||||
public static int Min(int val1, int val2, int val3)
|
|
||||||
{
|
|
||||||
return (val1 < val2 ? (val1 < val3 ? val1 : val3) : (val2 < val3 ? val2 : val3));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns the minimum of two values.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="val1">The first value.</param>
|
|
||||||
/// <param name="val2">The second value.</param>
|
|
||||||
/// <returns>The minimum value.</returns>
|
|
||||||
public static float Min(float val1, float val2)
|
|
||||||
{
|
|
||||||
return (val1 < val2 ? val1 : val2);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns the minimum of three values.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="val1">The first value.</param>
|
|
||||||
/// <param name="val2">The second value.</param>
|
|
||||||
/// <param name="val3">The third value.</param>
|
|
||||||
/// <returns>The minimum value.</returns>
|
|
||||||
public static float Min(float val1, float val2, float val3)
|
|
||||||
{
|
|
||||||
return (val1 < val2 ? (val1 < val3 ? val1 : val3) : (val2 < val3 ? val2 : val3));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns the minimum of two values.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="val1">The first value.</param>
|
|
||||||
/// <param name="val2">The second value.</param>
|
|
||||||
/// <returns>The minimum value.</returns>
|
|
||||||
public static double Min(double val1, double val2)
|
|
||||||
{
|
|
||||||
return (val1 < val2 ? val1 : val2);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns the minimum of three values.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="val1">The first value.</param>
|
|
||||||
/// <param name="val2">The second value.</param>
|
|
||||||
/// <param name="val3">The third value.</param>
|
|
||||||
/// <returns>The minimum value.</returns>
|
|
||||||
public static double Min(double val1, double val2, double val3)
|
|
||||||
{
|
|
||||||
return (val1 < val2 ? (val1 < val3 ? val1 : val3) : (val2 < val3 ? val2 : val3));
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Max
|
|
||||||
/// <summary>
|
|
||||||
/// Returns the maximum of two values.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="val1">The first value.</param>
|
|
||||||
/// <param name="val2">The second value.</param>
|
|
||||||
/// <returns>The maximum value.</returns>
|
|
||||||
public static int Max(int val1, int val2)
|
|
||||||
{
|
|
||||||
return (val1 > val2 ? val1 : val2);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns the maximum of three values.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="val1">The first value.</param>
|
|
||||||
/// <param name="val2">The second value.</param>
|
|
||||||
/// <param name="val3">The third value.</param>
|
|
||||||
/// <returns>The maximum value.</returns>
|
|
||||||
public static int Max(int val1, int val2, int val3)
|
|
||||||
{
|
|
||||||
return (val1 > val2 ? (val1 > val3 ? val1 : val3) : (val2 > val3 ? val2 : val3));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns the maximum of two values.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="val1">The first value.</param>
|
|
||||||
/// <param name="val2">The second value.</param>
|
|
||||||
/// <returns>The maximum value.</returns>
|
|
||||||
public static float Max(float val1, float val2)
|
|
||||||
{
|
|
||||||
return (val1 > val2 ? val1 : val2);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns the maximum of three values.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="val1">The first value.</param>
|
|
||||||
/// <param name="val2">The second value.</param>
|
|
||||||
/// <param name="val3">The third value.</param>
|
|
||||||
/// <returns>The maximum value.</returns>
|
|
||||||
public static float Max(float val1, float val2, float val3)
|
|
||||||
{
|
|
||||||
return (val1 > val2 ? (val1 > val3 ? val1 : val3) : (val2 > val3 ? val2 : val3));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns the maximum of two values.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="val1">The first value.</param>
|
|
||||||
/// <param name="val2">The second value.</param>
|
|
||||||
/// <returns>The maximum value.</returns>
|
|
||||||
public static double Max(double val1, double val2)
|
|
||||||
{
|
|
||||||
return (val1 > val2 ? val1 : val2);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns the maximum of three values.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="val1">The first value.</param>
|
|
||||||
/// <param name="val2">The second value.</param>
|
|
||||||
/// <param name="val3">The third value.</param>
|
|
||||||
/// <returns>The maximum value.</returns>
|
|
||||||
public static double Max(double val1, double val2, double val3)
|
|
||||||
{
|
|
||||||
return (val1 > val2 ? (val1 > val3 ? val1 : val3) : (val2 > val3 ? val2 : val3));
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Clamping
|
|
||||||
/// <summary>
|
|
||||||
/// Clamps a value between a minimum and a maximum value.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="value">The value to clamp.</param>
|
|
||||||
/// <param name="min">The minimum value.</param>
|
|
||||||
/// <param name="max">The maximum value.</param>
|
|
||||||
/// <returns>The clamped value.</returns>
|
|
||||||
public static float Clamp(float value, float min, float max)
|
|
||||||
{
|
|
||||||
return (value >= min ? (value <= max ? value : max) : min);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Clamps a value between a minimum and a maximum value.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="value">The value to clamp.</param>
|
|
||||||
/// <param name="min">The minimum value.</param>
|
|
||||||
/// <param name="max">The maximum value.</param>
|
|
||||||
/// <returns>The clamped value.</returns>
|
|
||||||
public static double Clamp(double value, double min, double max)
|
|
||||||
{
|
|
||||||
return (value >= min ? (value <= max ? value : max) : min);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Clamps the value between 0 and 1.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="value">The value to clamp.</param>
|
|
||||||
/// <returns>The clamped value.</returns>
|
|
||||||
public static float Clamp01(float value)
|
|
||||||
{
|
|
||||||
return (value > 0f ? (value < 1f ? value : 1f) : 0f);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Clamps the value between 0 and 1.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="value">The value to clamp.</param>
|
|
||||||
/// <returns>The clamped value.</returns>
|
|
||||||
public static double Clamp01(double value)
|
|
||||||
{
|
|
||||||
return (value > 0.0 ? (value < 1.0 ? value : 1.0) : 0.0);
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Triangle Area
|
|
||||||
/// <summary>
|
|
||||||
/// Calculates the area of a triangle.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="p0">The first point.</param>
|
|
||||||
/// <param name="p1">The second point.</param>
|
|
||||||
/// <param name="p2">The third point.</param>
|
|
||||||
/// <returns>The triangle area.</returns>
|
|
||||||
public static float TriangleArea(ref Vector3 p0, ref Vector3 p1, ref Vector3 p2)
|
|
||||||
{
|
|
||||||
var dx = p1 - p0;
|
|
||||||
var dy = p2 - p0;
|
|
||||||
return dx.Magnitude * ((float)System.Math.Sin(Vector3.Angle(ref dx, ref dy) * Deg2Rad) * dy.Magnitude) * 0.5f;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Calculates the area of a triangle.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="p0">The first point.</param>
|
|
||||||
/// <param name="p1">The second point.</param>
|
|
||||||
/// <param name="p2">The third point.</param>
|
|
||||||
/// <returns>The triangle area.</returns>
|
|
||||||
public static double TriangleArea(ref Vector3d p0, ref Vector3d p1, ref Vector3d p2)
|
|
||||||
{
|
|
||||||
var dx = p1 - p0;
|
|
||||||
var dy = p2 - p0;
|
|
||||||
return dx.Magnitude * (System.Math.Sin(Vector3d.Angle(ref dx, ref dy) * Deg2Radd) * dy.Magnitude) * 0.5f;
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,303 +0,0 @@
|
|||||||
#region License
|
|
||||||
/*
|
|
||||||
MIT License
|
|
||||||
|
|
||||||
Copyright(c) 2017-2018 Mattias Edlund
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
*/
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
using System;
|
|
||||||
|
|
||||||
namespace MeshDecimator.Math
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// A symmetric matrix.
|
|
||||||
/// </summary>
|
|
||||||
public struct SymmetricMatrix
|
|
||||||
{
|
|
||||||
#region Fields
|
|
||||||
/// <summary>
|
|
||||||
/// The m11 component.
|
|
||||||
/// </summary>
|
|
||||||
public double m0;
|
|
||||||
/// <summary>
|
|
||||||
/// The m12 component.
|
|
||||||
/// </summary>
|
|
||||||
public double m1;
|
|
||||||
/// <summary>
|
|
||||||
/// The m13 component.
|
|
||||||
/// </summary>
|
|
||||||
public double m2;
|
|
||||||
/// <summary>
|
|
||||||
/// The m14 component.
|
|
||||||
/// </summary>
|
|
||||||
public double m3;
|
|
||||||
/// <summary>
|
|
||||||
/// The m22 component.
|
|
||||||
/// </summary>
|
|
||||||
public double m4;
|
|
||||||
/// <summary>
|
|
||||||
/// The m23 component.
|
|
||||||
/// </summary>
|
|
||||||
public double m5;
|
|
||||||
/// <summary>
|
|
||||||
/// The m24 component.
|
|
||||||
/// </summary>
|
|
||||||
public double m6;
|
|
||||||
/// <summary>
|
|
||||||
/// The m33 component.
|
|
||||||
/// </summary>
|
|
||||||
public double m7;
|
|
||||||
/// <summary>
|
|
||||||
/// The m34 component.
|
|
||||||
/// </summary>
|
|
||||||
public double m8;
|
|
||||||
/// <summary>
|
|
||||||
/// The m44 component.
|
|
||||||
/// </summary>
|
|
||||||
public double m9;
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Properties
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the component value with a specific index.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="index">The component index.</param>
|
|
||||||
/// <returns>The value.</returns>
|
|
||||||
public double this[int index]
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
switch (index)
|
|
||||||
{
|
|
||||||
case 0:
|
|
||||||
return m0;
|
|
||||||
case 1:
|
|
||||||
return m1;
|
|
||||||
case 2:
|
|
||||||
return m2;
|
|
||||||
case 3:
|
|
||||||
return m3;
|
|
||||||
case 4:
|
|
||||||
return m4;
|
|
||||||
case 5:
|
|
||||||
return m5;
|
|
||||||
case 6:
|
|
||||||
return m6;
|
|
||||||
case 7:
|
|
||||||
return m7;
|
|
||||||
case 8:
|
|
||||||
return m8;
|
|
||||||
case 9:
|
|
||||||
return m9;
|
|
||||||
default:
|
|
||||||
throw new IndexOutOfRangeException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Constructor
|
|
||||||
/// <summary>
|
|
||||||
/// Creates a symmetric matrix with a value in each component.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="c">The component value.</param>
|
|
||||||
public SymmetricMatrix(double c)
|
|
||||||
{
|
|
||||||
this.m0 = c;
|
|
||||||
this.m1 = c;
|
|
||||||
this.m2 = c;
|
|
||||||
this.m3 = c;
|
|
||||||
this.m4 = c;
|
|
||||||
this.m5 = c;
|
|
||||||
this.m6 = c;
|
|
||||||
this.m7 = c;
|
|
||||||
this.m8 = c;
|
|
||||||
this.m9 = c;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Creates a symmetric matrix.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="m0">The m11 component.</param>
|
|
||||||
/// <param name="m1">The m12 component.</param>
|
|
||||||
/// <param name="m2">The m13 component.</param>
|
|
||||||
/// <param name="m3">The m14 component.</param>
|
|
||||||
/// <param name="m4">The m22 component.</param>
|
|
||||||
/// <param name="m5">The m23 component.</param>
|
|
||||||
/// <param name="m6">The m24 component.</param>
|
|
||||||
/// <param name="m7">The m33 component.</param>
|
|
||||||
/// <param name="m8">The m34 component.</param>
|
|
||||||
/// <param name="m9">The m44 component.</param>
|
|
||||||
public SymmetricMatrix(double m0, double m1, double m2, double m3,
|
|
||||||
double m4, double m5, double m6, double m7, double m8, double m9)
|
|
||||||
{
|
|
||||||
this.m0 = m0;
|
|
||||||
this.m1 = m1;
|
|
||||||
this.m2 = m2;
|
|
||||||
this.m3 = m3;
|
|
||||||
this.m4 = m4;
|
|
||||||
this.m5 = m5;
|
|
||||||
this.m6 = m6;
|
|
||||||
this.m7 = m7;
|
|
||||||
this.m8 = m8;
|
|
||||||
this.m9 = m9;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Creates a symmetric matrix from a plane.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="a">The plane x-component.</param>
|
|
||||||
/// <param name="b">The plane y-component</param>
|
|
||||||
/// <param name="c">The plane z-component</param>
|
|
||||||
/// <param name="d">The plane w-component</param>
|
|
||||||
public SymmetricMatrix(double a, double b, double c, double d)
|
|
||||||
{
|
|
||||||
this.m0 = a * a;
|
|
||||||
this.m1 = a * b;
|
|
||||||
this.m2 = a * c;
|
|
||||||
this.m3 = a * d;
|
|
||||||
|
|
||||||
this.m4 = b * b;
|
|
||||||
this.m5 = b * c;
|
|
||||||
this.m6 = b * d;
|
|
||||||
|
|
||||||
this.m7 = c * c;
|
|
||||||
this.m8 = c * d;
|
|
||||||
|
|
||||||
this.m9 = d * d;
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Operators
|
|
||||||
/// <summary>
|
|
||||||
/// Adds two matrixes together.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="a">The left hand side.</param>
|
|
||||||
/// <param name="b">The right hand side.</param>
|
|
||||||
/// <returns>The resulting matrix.</returns>
|
|
||||||
public static SymmetricMatrix operator +(SymmetricMatrix a, SymmetricMatrix b)
|
|
||||||
{
|
|
||||||
return new SymmetricMatrix(
|
|
||||||
a.m0 + b.m0, a.m1 + b.m1, a.m2 + b.m2, a.m3 + b.m3,
|
|
||||||
a.m4 + b.m4, a.m5 + b.m5, a.m6 + b.m6,
|
|
||||||
a.m7 + b.m7, a.m8 + b.m8,
|
|
||||||
a.m9 + b.m9
|
|
||||||
);
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Internal Methods
|
|
||||||
/// <summary>
|
|
||||||
/// Determinant(0, 1, 2, 1, 4, 5, 2, 5, 7)
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
internal double Determinant1()
|
|
||||||
{
|
|
||||||
double det =
|
|
||||||
m0 * m4 * m7 +
|
|
||||||
m2 * m1 * m5 +
|
|
||||||
m1 * m5 * m2 -
|
|
||||||
m2 * m4 * m2 -
|
|
||||||
m0 * m5 * m5 -
|
|
||||||
m1 * m1 * m7;
|
|
||||||
return det;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Determinant(1, 2, 3, 4, 5, 6, 5, 7, 8)
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
internal double Determinant2()
|
|
||||||
{
|
|
||||||
double det =
|
|
||||||
m1 * m5 * m8 +
|
|
||||||
m3 * m4 * m7 +
|
|
||||||
m2 * m6 * m5 -
|
|
||||||
m3 * m5 * m5 -
|
|
||||||
m1 * m6 * m7 -
|
|
||||||
m2 * m4 * m8;
|
|
||||||
return det;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Determinant(0, 2, 3, 1, 5, 6, 2, 7, 8)
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
internal double Determinant3()
|
|
||||||
{
|
|
||||||
double det =
|
|
||||||
m0 * m5 * m8 +
|
|
||||||
m3 * m1 * m7 +
|
|
||||||
m2 * m6 * m2 -
|
|
||||||
m3 * m5 * m2 -
|
|
||||||
m0 * m6 * m7 -
|
|
||||||
m2 * m1 * m8;
|
|
||||||
return det;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Determinant(0, 1, 3, 1, 4, 6, 2, 5, 8)
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
internal double Determinant4()
|
|
||||||
{
|
|
||||||
double det =
|
|
||||||
m0 * m4 * m8 +
|
|
||||||
m3 * m1 * m5 +
|
|
||||||
m1 * m6 * m2 -
|
|
||||||
m3 * m4 * m2 -
|
|
||||||
m0 * m6 * m5 -
|
|
||||||
m1 * m1 * m8;
|
|
||||||
return det;
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Public Methods
|
|
||||||
/// <summary>
|
|
||||||
/// Computes the determinant of this matrix.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="a11">The a11 index.</param>
|
|
||||||
/// <param name="a12">The a12 index.</param>
|
|
||||||
/// <param name="a13">The a13 index.</param>
|
|
||||||
/// <param name="a21">The a21 index.</param>
|
|
||||||
/// <param name="a22">The a22 index.</param>
|
|
||||||
/// <param name="a23">The a23 index.</param>
|
|
||||||
/// <param name="a31">The a31 index.</param>
|
|
||||||
/// <param name="a32">The a32 index.</param>
|
|
||||||
/// <param name="a33">The a33 index.</param>
|
|
||||||
/// <returns>The determinant value.</returns>
|
|
||||||
public double Determinant(int a11, int a12, int a13,
|
|
||||||
int a21, int a22, int a23,
|
|
||||||
int a31, int a32, int a33)
|
|
||||||
{
|
|
||||||
double det =
|
|
||||||
this[a11] * this[a22] * this[a33] +
|
|
||||||
this[a13] * this[a21] * this[a32] +
|
|
||||||
this[a12] * this[a23] * this[a31] -
|
|
||||||
this[a13] * this[a22] * this[a31] -
|
|
||||||
this[a11] * this[a23] * this[a32] -
|
|
||||||
this[a12] * this[a21] * this[a33];
|
|
||||||
return det;
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,425 +0,0 @@
|
|||||||
#region License
|
|
||||||
/*
|
|
||||||
MIT License
|
|
||||||
|
|
||||||
Copyright(c) 2017-2018 Mattias Edlund
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
*/
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Globalization;
|
|
||||||
|
|
||||||
namespace MeshDecimator.Math
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// A single precision 2D vector.
|
|
||||||
/// </summary>
|
|
||||||
public struct Vector2 : IEquatable<Vector2>
|
|
||||||
{
|
|
||||||
#region Static Read-Only
|
|
||||||
/// <summary>
|
|
||||||
/// The zero vector.
|
|
||||||
/// </summary>
|
|
||||||
public static readonly Vector2 zero = new Vector2(0, 0);
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Consts
|
|
||||||
/// <summary>
|
|
||||||
/// The vector epsilon.
|
|
||||||
/// </summary>
|
|
||||||
public const float Epsilon = 9.99999944E-11f;
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Fields
|
|
||||||
/// <summary>
|
|
||||||
/// The x component.
|
|
||||||
/// </summary>
|
|
||||||
public float x;
|
|
||||||
/// <summary>
|
|
||||||
/// The y component.
|
|
||||||
/// </summary>
|
|
||||||
public float y;
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Properties
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the magnitude of this vector.
|
|
||||||
/// </summary>
|
|
||||||
public float Magnitude
|
|
||||||
{
|
|
||||||
get { return (float)System.Math.Sqrt(x * x + y * y); }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the squared magnitude of this vector.
|
|
||||||
/// </summary>
|
|
||||||
public float MagnitudeSqr
|
|
||||||
{
|
|
||||||
get { return (x * x + y * y); }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets a normalized vector from this vector.
|
|
||||||
/// </summary>
|
|
||||||
public Vector2 Normalized
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
Vector2 result;
|
|
||||||
Normalize(ref this, out result);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets a specific component by index in this vector.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="index">The component index.</param>
|
|
||||||
public float this[int index]
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
switch (index)
|
|
||||||
{
|
|
||||||
case 0:
|
|
||||||
return x;
|
|
||||||
case 1:
|
|
||||||
return y;
|
|
||||||
default:
|
|
||||||
throw new IndexOutOfRangeException("Invalid Vector2 index!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
set
|
|
||||||
{
|
|
||||||
switch (index)
|
|
||||||
{
|
|
||||||
case 0:
|
|
||||||
x = value;
|
|
||||||
break;
|
|
||||||
case 1:
|
|
||||||
y = value;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new IndexOutOfRangeException("Invalid Vector2 index!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Constructor
|
|
||||||
/// <summary>
|
|
||||||
/// Creates a new vector with one value for all components.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="value">The value.</param>
|
|
||||||
public Vector2(float value)
|
|
||||||
{
|
|
||||||
this.x = value;
|
|
||||||
this.y = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Creates a new vector.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="x">The x value.</param>
|
|
||||||
/// <param name="y">The y value.</param>
|
|
||||||
public Vector2(float x, float y)
|
|
||||||
{
|
|
||||||
this.x = x;
|
|
||||||
this.y = y;
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Operators
|
|
||||||
/// <summary>
|
|
||||||
/// Adds two vectors.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="a">The first vector.</param>
|
|
||||||
/// <param name="b">The second vector.</param>
|
|
||||||
/// <returns>The resulting vector.</returns>
|
|
||||||
public static Vector2 operator +(Vector2 a, Vector2 b)
|
|
||||||
{
|
|
||||||
return new Vector2(a.x + b.x, a.y + b.y);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Subtracts two vectors.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="a">The first vector.</param>
|
|
||||||
/// <param name="b">The second vector.</param>
|
|
||||||
/// <returns>The resulting vector.</returns>
|
|
||||||
public static Vector2 operator -(Vector2 a, Vector2 b)
|
|
||||||
{
|
|
||||||
return new Vector2(a.x - b.x, a.y - b.y);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Scales the vector uniformly.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="a">The vector.</param>
|
|
||||||
/// <param name="d">The scaling value.</param>
|
|
||||||
/// <returns>The resulting vector.</returns>
|
|
||||||
public static Vector2 operator *(Vector2 a, float d)
|
|
||||||
{
|
|
||||||
return new Vector2(a.x * d, a.y * d);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Scales the vector uniformly.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="d">The scaling value.</param>
|
|
||||||
/// <param name="a">The vector.</param>
|
|
||||||
/// <returns>The resulting vector.</returns>
|
|
||||||
public static Vector2 operator *(float d, Vector2 a)
|
|
||||||
{
|
|
||||||
return new Vector2(a.x * d, a.y * d);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Divides the vector with a float.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="a">The vector.</param>
|
|
||||||
/// <param name="d">The dividing float value.</param>
|
|
||||||
/// <returns>The resulting vector.</returns>
|
|
||||||
public static Vector2 operator /(Vector2 a, float d)
|
|
||||||
{
|
|
||||||
return new Vector2(a.x / d, a.y / d);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Subtracts the vector from a zero vector.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="a">The vector.</param>
|
|
||||||
/// <returns>The resulting vector.</returns>
|
|
||||||
public static Vector2 operator -(Vector2 a)
|
|
||||||
{
|
|
||||||
return new Vector2(-a.x, -a.y);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns if two vectors equals eachother.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="lhs">The left hand side vector.</param>
|
|
||||||
/// <param name="rhs">The right hand side vector.</param>
|
|
||||||
/// <returns>If equals.</returns>
|
|
||||||
public static bool operator ==(Vector2 lhs, Vector2 rhs)
|
|
||||||
{
|
|
||||||
return (lhs - rhs).MagnitudeSqr < Epsilon;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns if two vectors don't equal eachother.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="lhs">The left hand side vector.</param>
|
|
||||||
/// <param name="rhs">The right hand side vector.</param>
|
|
||||||
/// <returns>If not equals.</returns>
|
|
||||||
public static bool operator !=(Vector2 lhs, Vector2 rhs)
|
|
||||||
{
|
|
||||||
return (lhs - rhs).MagnitudeSqr >= Epsilon;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Explicitly converts from a double-precision vector into a single-precision vector.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="v">The double-precision vector.</param>
|
|
||||||
public static explicit operator Vector2(Vector2d v)
|
|
||||||
{
|
|
||||||
return new Vector2((float)v.x, (float)v.y);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Implicitly converts from an integer vector into a single-precision vector.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="v">The integer vector.</param>
|
|
||||||
public static implicit operator Vector2(Vector2i v)
|
|
||||||
{
|
|
||||||
return new Vector2(v.x, v.y);
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Public Methods
|
|
||||||
#region Instance
|
|
||||||
/// <summary>
|
|
||||||
/// Set x and y components of an existing vector.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="x">The x value.</param>
|
|
||||||
/// <param name="y">The y value.</param>
|
|
||||||
public void Set(float x, float y)
|
|
||||||
{
|
|
||||||
this.x = x;
|
|
||||||
this.y = y;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Multiplies with another vector component-wise.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="scale">The vector to multiply with.</param>
|
|
||||||
public void Scale(ref Vector2 scale)
|
|
||||||
{
|
|
||||||
x *= scale.x;
|
|
||||||
y *= scale.y;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Normalizes this vector.
|
|
||||||
/// </summary>
|
|
||||||
public void Normalize()
|
|
||||||
{
|
|
||||||
float mag = this.Magnitude;
|
|
||||||
if (mag > Epsilon)
|
|
||||||
{
|
|
||||||
x /= mag;
|
|
||||||
y /= mag;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
x = y = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Clamps this vector between a specific range.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="min">The minimum component value.</param>
|
|
||||||
/// <param name="max">The maximum component value.</param>
|
|
||||||
public void Clamp(float min, float max)
|
|
||||||
{
|
|
||||||
if (x < min) x = min;
|
|
||||||
else if (x > max) x = max;
|
|
||||||
|
|
||||||
if (y < min) y = min;
|
|
||||||
else if (y > max) y = max;
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Object
|
|
||||||
/// <summary>
|
|
||||||
/// Returns a hash code for this vector.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>The hash code.</returns>
|
|
||||||
public override int GetHashCode()
|
|
||||||
{
|
|
||||||
return x.GetHashCode() ^ y.GetHashCode() << 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns if this vector is equal to another one.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="other">The other vector to compare to.</param>
|
|
||||||
/// <returns>If equals.</returns>
|
|
||||||
public override bool Equals(object other)
|
|
||||||
{
|
|
||||||
if (!(other is Vector2))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
Vector2 vector = (Vector2)other;
|
|
||||||
return (x == vector.x && y == vector.y);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns if this vector is equal to another one.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="other">The other vector to compare to.</param>
|
|
||||||
/// <returns>If equals.</returns>
|
|
||||||
public bool Equals(Vector2 other)
|
|
||||||
{
|
|
||||||
return (x == other.x && y == other.y);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns a nicely formatted string for this vector.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>The string.</returns>
|
|
||||||
public override string ToString()
|
|
||||||
{
|
|
||||||
return string.Format("({0}, {1})",
|
|
||||||
x.ToString("F1", CultureInfo.InvariantCulture),
|
|
||||||
y.ToString("F1", CultureInfo.InvariantCulture));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns a nicely formatted string for this vector.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="format">The float format.</param>
|
|
||||||
/// <returns>The string.</returns>
|
|
||||||
public string ToString(string format)
|
|
||||||
{
|
|
||||||
return string.Format("({0}, {1})",
|
|
||||||
x.ToString(format, CultureInfo.InvariantCulture),
|
|
||||||
y.ToString(format, CultureInfo.InvariantCulture));
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Static
|
|
||||||
/// <summary>
|
|
||||||
/// Dot Product of two vectors.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="lhs">The left hand side vector.</param>
|
|
||||||
/// <param name="rhs">The right hand side vector.</param>
|
|
||||||
public static float Dot(ref Vector2 lhs, ref Vector2 rhs)
|
|
||||||
{
|
|
||||||
return lhs.x * rhs.x + lhs.y * rhs.y;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Performs a linear interpolation between two vectors.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="a">The vector to interpolate from.</param>
|
|
||||||
/// <param name="b">The vector to interpolate to.</param>
|
|
||||||
/// <param name="t">The time fraction.</param>
|
|
||||||
/// <param name="result">The resulting vector.</param>
|
|
||||||
public static void Lerp(ref Vector2 a, ref Vector2 b, float t, out Vector2 result)
|
|
||||||
{
|
|
||||||
result = new Vector2(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Multiplies two vectors component-wise.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="a">The first vector.</param>
|
|
||||||
/// <param name="b">The second vector.</param>
|
|
||||||
/// <param name="result">The resulting vector.</param>
|
|
||||||
public static void Scale(ref Vector2 a, ref Vector2 b, out Vector2 result)
|
|
||||||
{
|
|
||||||
result = new Vector2(a.x * b.x, a.y * b.y);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Normalizes a vector.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="value">The vector to normalize.</param>
|
|
||||||
/// <param name="result">The resulting normalized vector.</param>
|
|
||||||
public static void Normalize(ref Vector2 value, out Vector2 result)
|
|
||||||
{
|
|
||||||
float mag = value.Magnitude;
|
|
||||||
if (mag > Epsilon)
|
|
||||||
{
|
|
||||||
result = new Vector2(value.x / mag, value.y / mag);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
result = Vector2.zero;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,425 +0,0 @@
|
|||||||
#region License
|
|
||||||
/*
|
|
||||||
MIT License
|
|
||||||
|
|
||||||
Copyright(c) 2017-2018 Mattias Edlund
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
*/
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Globalization;
|
|
||||||
|
|
||||||
namespace MeshDecimator.Math
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// A double precision 2D vector.
|
|
||||||
/// </summary>
|
|
||||||
public struct Vector2d : IEquatable<Vector2d>
|
|
||||||
{
|
|
||||||
#region Static Read-Only
|
|
||||||
/// <summary>
|
|
||||||
/// The zero vector.
|
|
||||||
/// </summary>
|
|
||||||
public static readonly Vector2d zero = new Vector2d(0, 0);
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Consts
|
|
||||||
/// <summary>
|
|
||||||
/// The vector epsilon.
|
|
||||||
/// </summary>
|
|
||||||
public const double Epsilon = double.Epsilon;
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Fields
|
|
||||||
/// <summary>
|
|
||||||
/// The x component.
|
|
||||||
/// </summary>
|
|
||||||
public double x;
|
|
||||||
/// <summary>
|
|
||||||
/// The y component.
|
|
||||||
/// </summary>
|
|
||||||
public double y;
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Properties
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the magnitude of this vector.
|
|
||||||
/// </summary>
|
|
||||||
public double Magnitude
|
|
||||||
{
|
|
||||||
get { return System.Math.Sqrt(x * x + y * y); }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the squared magnitude of this vector.
|
|
||||||
/// </summary>
|
|
||||||
public double MagnitudeSqr
|
|
||||||
{
|
|
||||||
get { return (x * x + y * y); }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets a normalized vector from this vector.
|
|
||||||
/// </summary>
|
|
||||||
public Vector2d Normalized
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
Vector2d result;
|
|
||||||
Normalize(ref this, out result);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets a specific component by index in this vector.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="index">The component index.</param>
|
|
||||||
public double this[int index]
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
switch (index)
|
|
||||||
{
|
|
||||||
case 0:
|
|
||||||
return x;
|
|
||||||
case 1:
|
|
||||||
return y;
|
|
||||||
default:
|
|
||||||
throw new IndexOutOfRangeException("Invalid Vector2d index!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
set
|
|
||||||
{
|
|
||||||
switch (index)
|
|
||||||
{
|
|
||||||
case 0:
|
|
||||||
x = value;
|
|
||||||
break;
|
|
||||||
case 1:
|
|
||||||
y = value;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new IndexOutOfRangeException("Invalid Vector2d index!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Constructor
|
|
||||||
/// <summary>
|
|
||||||
/// Creates a new vector with one value for all components.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="value">The value.</param>
|
|
||||||
public Vector2d(double value)
|
|
||||||
{
|
|
||||||
this.x = value;
|
|
||||||
this.y = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Creates a new vector.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="x">The x value.</param>
|
|
||||||
/// <param name="y">The y value.</param>
|
|
||||||
public Vector2d(double x, double y)
|
|
||||||
{
|
|
||||||
this.x = x;
|
|
||||||
this.y = y;
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Operators
|
|
||||||
/// <summary>
|
|
||||||
/// Adds two vectors.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="a">The first vector.</param>
|
|
||||||
/// <param name="b">The second vector.</param>
|
|
||||||
/// <returns>The resulting vector.</returns>
|
|
||||||
public static Vector2d operator +(Vector2d a, Vector2d b)
|
|
||||||
{
|
|
||||||
return new Vector2d(a.x + b.x, a.y + b.y);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Subtracts two vectors.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="a">The first vector.</param>
|
|
||||||
/// <param name="b">The second vector.</param>
|
|
||||||
/// <returns>The resulting vector.</returns>
|
|
||||||
public static Vector2d operator -(Vector2d a, Vector2d b)
|
|
||||||
{
|
|
||||||
return new Vector2d(a.x - b.x, a.y - b.y);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Scales the vector uniformly.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="a">The vector.</param>
|
|
||||||
/// <param name="d">The scaling value.</param>
|
|
||||||
/// <returns>The resulting vector.</returns>
|
|
||||||
public static Vector2d operator *(Vector2d a, double d)
|
|
||||||
{
|
|
||||||
return new Vector2d(a.x * d, a.y * d);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Scales the vector uniformly.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="d">The scaling value.</param>
|
|
||||||
/// <param name="a">The vector.</param>
|
|
||||||
/// <returns>The resulting vector.</returns>
|
|
||||||
public static Vector2d operator *(double d, Vector2d a)
|
|
||||||
{
|
|
||||||
return new Vector2d(a.x * d, a.y * d);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Divides the vector with a float.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="a">The vector.</param>
|
|
||||||
/// <param name="d">The dividing float value.</param>
|
|
||||||
/// <returns>The resulting vector.</returns>
|
|
||||||
public static Vector2d operator /(Vector2d a, double d)
|
|
||||||
{
|
|
||||||
return new Vector2d(a.x / d, a.y / d);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Subtracts the vector from a zero vector.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="a">The vector.</param>
|
|
||||||
/// <returns>The resulting vector.</returns>
|
|
||||||
public static Vector2d operator -(Vector2d a)
|
|
||||||
{
|
|
||||||
return new Vector2d(-a.x, -a.y);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns if two vectors equals eachother.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="lhs">The left hand side vector.</param>
|
|
||||||
/// <param name="rhs">The right hand side vector.</param>
|
|
||||||
/// <returns>If equals.</returns>
|
|
||||||
public static bool operator ==(Vector2d lhs, Vector2d rhs)
|
|
||||||
{
|
|
||||||
return (lhs - rhs).MagnitudeSqr < Epsilon;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns if two vectors don't equal eachother.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="lhs">The left hand side vector.</param>
|
|
||||||
/// <param name="rhs">The right hand side vector.</param>
|
|
||||||
/// <returns>If not equals.</returns>
|
|
||||||
public static bool operator !=(Vector2d lhs, Vector2d rhs)
|
|
||||||
{
|
|
||||||
return (lhs - rhs).MagnitudeSqr >= Epsilon;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Implicitly converts from a single-precision vector into a double-precision vector.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="v">The single-precision vector.</param>
|
|
||||||
public static implicit operator Vector2d(Vector2 v)
|
|
||||||
{
|
|
||||||
return new Vector2d(v.x, v.y);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Implicitly converts from an integer vector into a double-precision vector.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="v">The integer vector.</param>
|
|
||||||
public static implicit operator Vector2d(Vector2i v)
|
|
||||||
{
|
|
||||||
return new Vector2d(v.x, v.y);
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Public Methods
|
|
||||||
#region Instance
|
|
||||||
/// <summary>
|
|
||||||
/// Set x and y components of an existing vector.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="x">The x value.</param>
|
|
||||||
/// <param name="y">The y value.</param>
|
|
||||||
public void Set(double x, double y)
|
|
||||||
{
|
|
||||||
this.x = x;
|
|
||||||
this.y = y;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Multiplies with another vector component-wise.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="scale">The vector to multiply with.</param>
|
|
||||||
public void Scale(ref Vector2d scale)
|
|
||||||
{
|
|
||||||
x *= scale.x;
|
|
||||||
y *= scale.y;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Normalizes this vector.
|
|
||||||
/// </summary>
|
|
||||||
public void Normalize()
|
|
||||||
{
|
|
||||||
double mag = this.Magnitude;
|
|
||||||
if (mag > Epsilon)
|
|
||||||
{
|
|
||||||
x /= mag;
|
|
||||||
y /= mag;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
x = y = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Clamps this vector between a specific range.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="min">The minimum component value.</param>
|
|
||||||
/// <param name="max">The maximum component value.</param>
|
|
||||||
public void Clamp(double min, double max)
|
|
||||||
{
|
|
||||||
if (x < min) x = min;
|
|
||||||
else if (x > max) x = max;
|
|
||||||
|
|
||||||
if (y < min) y = min;
|
|
||||||
else if (y > max) y = max;
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Object
|
|
||||||
/// <summary>
|
|
||||||
/// Returns a hash code for this vector.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>The hash code.</returns>
|
|
||||||
public override int GetHashCode()
|
|
||||||
{
|
|
||||||
return x.GetHashCode() ^ y.GetHashCode() << 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns if this vector is equal to another one.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="other">The other vector to compare to.</param>
|
|
||||||
/// <returns>If equals.</returns>
|
|
||||||
public override bool Equals(object other)
|
|
||||||
{
|
|
||||||
if (!(other is Vector2d))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
Vector2d vector = (Vector2d)other;
|
|
||||||
return (x == vector.x && y == vector.y);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns if this vector is equal to another one.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="other">The other vector to compare to.</param>
|
|
||||||
/// <returns>If equals.</returns>
|
|
||||||
public bool Equals(Vector2d other)
|
|
||||||
{
|
|
||||||
return (x == other.x && y == other.y);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns a nicely formatted string for this vector.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>The string.</returns>
|
|
||||||
public override string ToString()
|
|
||||||
{
|
|
||||||
return string.Format("({0}, {1})",
|
|
||||||
x.ToString("F1", CultureInfo.InvariantCulture),
|
|
||||||
y.ToString("F1", CultureInfo.InvariantCulture));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns a nicely formatted string for this vector.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="format">The float format.</param>
|
|
||||||
/// <returns>The string.</returns>
|
|
||||||
public string ToString(string format)
|
|
||||||
{
|
|
||||||
return string.Format("({0}, {1})",
|
|
||||||
x.ToString(format, CultureInfo.InvariantCulture),
|
|
||||||
y.ToString(format, CultureInfo.InvariantCulture));
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Static
|
|
||||||
/// <summary>
|
|
||||||
/// Dot Product of two vectors.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="lhs">The left hand side vector.</param>
|
|
||||||
/// <param name="rhs">The right hand side vector.</param>
|
|
||||||
public static double Dot(ref Vector2d lhs, ref Vector2d rhs)
|
|
||||||
{
|
|
||||||
return lhs.x * rhs.x + lhs.y * rhs.y;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Performs a linear interpolation between two vectors.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="a">The vector to interpolate from.</param>
|
|
||||||
/// <param name="b">The vector to interpolate to.</param>
|
|
||||||
/// <param name="t">The time fraction.</param>
|
|
||||||
/// <param name="result">The resulting vector.</param>
|
|
||||||
public static void Lerp(ref Vector2d a, ref Vector2d b, double t, out Vector2d result)
|
|
||||||
{
|
|
||||||
result = new Vector2d(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Multiplies two vectors component-wise.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="a">The first vector.</param>
|
|
||||||
/// <param name="b">The second vector.</param>
|
|
||||||
/// <param name="result">The resulting vector.</param>
|
|
||||||
public static void Scale(ref Vector2d a, ref Vector2d b, out Vector2d result)
|
|
||||||
{
|
|
||||||
result = new Vector2d(a.x * b.x, a.y * b.y);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Normalizes a vector.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="value">The vector to normalize.</param>
|
|
||||||
/// <param name="result">The resulting normalized vector.</param>
|
|
||||||
public static void Normalize(ref Vector2d value, out Vector2d result)
|
|
||||||
{
|
|
||||||
double mag = value.Magnitude;
|
|
||||||
if (mag > Epsilon)
|
|
||||||
{
|
|
||||||
result = new Vector2d(value.x / mag, value.y / mag);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
result = Vector2d.zero;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,348 +0,0 @@
|
|||||||
#region License
|
|
||||||
/*
|
|
||||||
MIT License
|
|
||||||
|
|
||||||
Copyright(c) 2017-2018 Mattias Edlund
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
*/
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Globalization;
|
|
||||||
|
|
||||||
namespace MeshDecimator.Math
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// A 2D integer vector.
|
|
||||||
/// </summary>
|
|
||||||
public struct Vector2i : IEquatable<Vector2i>
|
|
||||||
{
|
|
||||||
#region Static Read-Only
|
|
||||||
/// <summary>
|
|
||||||
/// The zero vector.
|
|
||||||
/// </summary>
|
|
||||||
public static readonly Vector2i zero = new Vector2i(0, 0);
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Fields
|
|
||||||
/// <summary>
|
|
||||||
/// The x component.
|
|
||||||
/// </summary>
|
|
||||||
public int x;
|
|
||||||
/// <summary>
|
|
||||||
/// The y component.
|
|
||||||
/// </summary>
|
|
||||||
public int y;
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Properties
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the magnitude of this vector.
|
|
||||||
/// </summary>
|
|
||||||
public int Magnitude
|
|
||||||
{
|
|
||||||
get { return (int)System.Math.Sqrt(x * x + y * y); }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the squared magnitude of this vector.
|
|
||||||
/// </summary>
|
|
||||||
public int MagnitudeSqr
|
|
||||||
{
|
|
||||||
get { return (x * x + y * y); }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets a specific component by index in this vector.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="index">The component index.</param>
|
|
||||||
public int this[int index]
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
switch (index)
|
|
||||||
{
|
|
||||||
case 0:
|
|
||||||
return x;
|
|
||||||
case 1:
|
|
||||||
return y;
|
|
||||||
default:
|
|
||||||
throw new IndexOutOfRangeException("Invalid Vector2i index!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
set
|
|
||||||
{
|
|
||||||
switch (index)
|
|
||||||
{
|
|
||||||
case 0:
|
|
||||||
x = value;
|
|
||||||
break;
|
|
||||||
case 1:
|
|
||||||
y = value;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new IndexOutOfRangeException("Invalid Vector2i index!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Constructor
|
|
||||||
/// <summary>
|
|
||||||
/// Creates a new vector with one value for all components.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="value">The value.</param>
|
|
||||||
public Vector2i(int value)
|
|
||||||
{
|
|
||||||
this.x = value;
|
|
||||||
this.y = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Creates a new vector.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="x">The x value.</param>
|
|
||||||
/// <param name="y">The y value.</param>
|
|
||||||
public Vector2i(int x, int y)
|
|
||||||
{
|
|
||||||
this.x = x;
|
|
||||||
this.y = y;
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Operators
|
|
||||||
/// <summary>
|
|
||||||
/// Adds two vectors.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="a">The first vector.</param>
|
|
||||||
/// <param name="b">The second vector.</param>
|
|
||||||
/// <returns>The resulting vector.</returns>
|
|
||||||
public static Vector2i operator +(Vector2i a, Vector2i b)
|
|
||||||
{
|
|
||||||
return new Vector2i(a.x + b.x, a.y + b.y);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Subtracts two vectors.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="a">The first vector.</param>
|
|
||||||
/// <param name="b">The second vector.</param>
|
|
||||||
/// <returns>The resulting vector.</returns>
|
|
||||||
public static Vector2i operator -(Vector2i a, Vector2i b)
|
|
||||||
{
|
|
||||||
return new Vector2i(a.x - b.x, a.y - b.y);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Scales the vector uniformly.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="a">The vector.</param>
|
|
||||||
/// <param name="d">The scaling value.</param>
|
|
||||||
/// <returns>The resulting vector.</returns>
|
|
||||||
public static Vector2i operator *(Vector2i a, int d)
|
|
||||||
{
|
|
||||||
return new Vector2i(a.x * d, a.y * d);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Scales the vector uniformly.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="d">The scaling value.</param>
|
|
||||||
/// <param name="a">The vector.</param>
|
|
||||||
/// <returns>The resulting vector.</returns>
|
|
||||||
public static Vector2i operator *(int d, Vector2i a)
|
|
||||||
{
|
|
||||||
return new Vector2i(a.x * d, a.y * d);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Divides the vector with a float.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="a">The vector.</param>
|
|
||||||
/// <param name="d">The dividing float value.</param>
|
|
||||||
/// <returns>The resulting vector.</returns>
|
|
||||||
public static Vector2i operator /(Vector2i a, int d)
|
|
||||||
{
|
|
||||||
return new Vector2i(a.x / d, a.y / d);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Subtracts the vector from a zero vector.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="a">The vector.</param>
|
|
||||||
/// <returns>The resulting vector.</returns>
|
|
||||||
public static Vector2i operator -(Vector2i a)
|
|
||||||
{
|
|
||||||
return new Vector2i(-a.x, -a.y);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns if two vectors equals eachother.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="lhs">The left hand side vector.</param>
|
|
||||||
/// <param name="rhs">The right hand side vector.</param>
|
|
||||||
/// <returns>If equals.</returns>
|
|
||||||
public static bool operator ==(Vector2i lhs, Vector2i rhs)
|
|
||||||
{
|
|
||||||
return (lhs.x == rhs.x && lhs.y == rhs.y);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns if two vectors don't equal eachother.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="lhs">The left hand side vector.</param>
|
|
||||||
/// <param name="rhs">The right hand side vector.</param>
|
|
||||||
/// <returns>If not equals.</returns>
|
|
||||||
public static bool operator !=(Vector2i lhs, Vector2i rhs)
|
|
||||||
{
|
|
||||||
return (lhs.x != rhs.x || lhs.y != rhs.y);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Explicitly converts from a single-precision vector into an integer vector.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="v">The single-precision vector.</param>
|
|
||||||
public static explicit operator Vector2i(Vector2 v)
|
|
||||||
{
|
|
||||||
return new Vector2i((int)v.x, (int)v.y);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Explicitly converts from a double-precision vector into an integer vector.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="v">The double-precision vector.</param>
|
|
||||||
public static explicit operator Vector2i(Vector2d v)
|
|
||||||
{
|
|
||||||
return new Vector2i((int)v.x, (int)v.y);
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Public Methods
|
|
||||||
#region Instance
|
|
||||||
/// <summary>
|
|
||||||
/// Set x and y components of an existing vector.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="x">The x value.</param>
|
|
||||||
/// <param name="y">The y value.</param>
|
|
||||||
public void Set(int x, int y)
|
|
||||||
{
|
|
||||||
this.x = x;
|
|
||||||
this.y = y;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Multiplies with another vector component-wise.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="scale">The vector to multiply with.</param>
|
|
||||||
public void Scale(ref Vector2i scale)
|
|
||||||
{
|
|
||||||
x *= scale.x;
|
|
||||||
y *= scale.y;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Clamps this vector between a specific range.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="min">The minimum component value.</param>
|
|
||||||
/// <param name="max">The maximum component value.</param>
|
|
||||||
public void Clamp(int min, int max)
|
|
||||||
{
|
|
||||||
if (x < min) x = min;
|
|
||||||
else if (x > max) x = max;
|
|
||||||
|
|
||||||
if (y < min) y = min;
|
|
||||||
else if (y > max) y = max;
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Object
|
|
||||||
/// <summary>
|
|
||||||
/// Returns a hash code for this vector.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>The hash code.</returns>
|
|
||||||
public override int GetHashCode()
|
|
||||||
{
|
|
||||||
return x.GetHashCode() ^ y.GetHashCode() << 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns if this vector is equal to another one.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="other">The other vector to compare to.</param>
|
|
||||||
/// <returns>If equals.</returns>
|
|
||||||
public override bool Equals(object other)
|
|
||||||
{
|
|
||||||
if (!(other is Vector2i))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
Vector2i vector = (Vector2i)other;
|
|
||||||
return (x == vector.x && y == vector.y);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns if this vector is equal to another one.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="other">The other vector to compare to.</param>
|
|
||||||
/// <returns>If equals.</returns>
|
|
||||||
public bool Equals(Vector2i other)
|
|
||||||
{
|
|
||||||
return (x == other.x && y == other.y);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns a nicely formatted string for this vector.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>The string.</returns>
|
|
||||||
public override string ToString()
|
|
||||||
{
|
|
||||||
return string.Format("({0}, {1})",
|
|
||||||
x.ToString(CultureInfo.InvariantCulture),
|
|
||||||
y.ToString(CultureInfo.InvariantCulture));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns a nicely formatted string for this vector.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="format">The integer format.</param>
|
|
||||||
/// <returns>The string.</returns>
|
|
||||||
public string ToString(string format)
|
|
||||||
{
|
|
||||||
return string.Format("({0}, {1})",
|
|
||||||
x.ToString(format, CultureInfo.InvariantCulture),
|
|
||||||
y.ToString(format, CultureInfo.InvariantCulture));
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Static
|
|
||||||
/// <summary>
|
|
||||||
/// Multiplies two vectors component-wise.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="a">The first vector.</param>
|
|
||||||
/// <param name="b">The second vector.</param>
|
|
||||||
/// <param name="result">The resulting vector.</param>
|
|
||||||
public static void Scale(ref Vector2i a, ref Vector2i b, out Vector2i result)
|
|
||||||
{
|
|
||||||
result = new Vector2i(a.x * b.x, a.y * b.y);
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,494 +0,0 @@
|
|||||||
#region License
|
|
||||||
/*
|
|
||||||
MIT License
|
|
||||||
|
|
||||||
Copyright(c) 2017-2018 Mattias Edlund
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
*/
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Globalization;
|
|
||||||
|
|
||||||
namespace MeshDecimator.Math
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// A single precision 3D vector.
|
|
||||||
/// </summary>
|
|
||||||
public struct Vector3 : IEquatable<Vector3>
|
|
||||||
{
|
|
||||||
#region Static Read-Only
|
|
||||||
/// <summary>
|
|
||||||
/// The zero vector.
|
|
||||||
/// </summary>
|
|
||||||
public static readonly Vector3 zero = new Vector3(0, 0, 0);
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Consts
|
|
||||||
/// <summary>
|
|
||||||
/// The vector epsilon.
|
|
||||||
/// </summary>
|
|
||||||
public const float Epsilon = 9.99999944E-11f;
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Fields
|
|
||||||
/// <summary>
|
|
||||||
/// The x component.
|
|
||||||
/// </summary>
|
|
||||||
public float x;
|
|
||||||
/// <summary>
|
|
||||||
/// The y component.
|
|
||||||
/// </summary>
|
|
||||||
public float y;
|
|
||||||
/// <summary>
|
|
||||||
/// The z component.
|
|
||||||
/// </summary>
|
|
||||||
public float z;
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Properties
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the magnitude of this vector.
|
|
||||||
/// </summary>
|
|
||||||
public float Magnitude
|
|
||||||
{
|
|
||||||
get { return (float)System.Math.Sqrt(x * x + y * y + z * z); }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the squared magnitude of this vector.
|
|
||||||
/// </summary>
|
|
||||||
public float MagnitudeSqr
|
|
||||||
{
|
|
||||||
get { return (x * x + y * y + z * z); }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets a normalized vector from this vector.
|
|
||||||
/// </summary>
|
|
||||||
public Vector3 Normalized
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
Vector3 result;
|
|
||||||
Normalize(ref this, out result);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets a specific component by index in this vector.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="index">The component index.</param>
|
|
||||||
public float this[int index]
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
switch (index)
|
|
||||||
{
|
|
||||||
case 0:
|
|
||||||
return x;
|
|
||||||
case 1:
|
|
||||||
return y;
|
|
||||||
case 2:
|
|
||||||
return z;
|
|
||||||
default:
|
|
||||||
throw new IndexOutOfRangeException("Invalid Vector3 index!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
set
|
|
||||||
{
|
|
||||||
switch (index)
|
|
||||||
{
|
|
||||||
case 0:
|
|
||||||
x = value;
|
|
||||||
break;
|
|
||||||
case 1:
|
|
||||||
y = value;
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
z = value;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new IndexOutOfRangeException("Invalid Vector3 index!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Constructor
|
|
||||||
/// <summary>
|
|
||||||
/// Creates a new vector with one value for all components.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="value">The value.</param>
|
|
||||||
public Vector3(float value)
|
|
||||||
{
|
|
||||||
this.x = value;
|
|
||||||
this.y = value;
|
|
||||||
this.z = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Creates a new vector.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="x">The x value.</param>
|
|
||||||
/// <param name="y">The y value.</param>
|
|
||||||
/// <param name="z">The z value.</param>
|
|
||||||
public Vector3(float x, float y, float z)
|
|
||||||
{
|
|
||||||
this.x = x;
|
|
||||||
this.y = y;
|
|
||||||
this.z = z;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Creates a new vector from a double precision vector.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="vector">The double precision vector.</param>
|
|
||||||
public Vector3(Vector3d vector)
|
|
||||||
{
|
|
||||||
this.x = (float)vector.x;
|
|
||||||
this.y = (float)vector.y;
|
|
||||||
this.z = (float)vector.z;
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Operators
|
|
||||||
/// <summary>
|
|
||||||
/// Adds two vectors.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="a">The first vector.</param>
|
|
||||||
/// <param name="b">The second vector.</param>
|
|
||||||
/// <returns>The resulting vector.</returns>
|
|
||||||
public static Vector3 operator +(Vector3 a, Vector3 b)
|
|
||||||
{
|
|
||||||
return new Vector3(a.x + b.x, a.y + b.y, a.z + b.z);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Subtracts two vectors.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="a">The first vector.</param>
|
|
||||||
/// <param name="b">The second vector.</param>
|
|
||||||
/// <returns>The resulting vector.</returns>
|
|
||||||
public static Vector3 operator -(Vector3 a, Vector3 b)
|
|
||||||
{
|
|
||||||
return new Vector3(a.x - b.x, a.y - b.y, a.z - b.z);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Scales the vector uniformly.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="a">The vector.</param>
|
|
||||||
/// <param name="d">The scaling value.</param>
|
|
||||||
/// <returns>The resulting vector.</returns>
|
|
||||||
public static Vector3 operator *(Vector3 a, float d)
|
|
||||||
{
|
|
||||||
return new Vector3(a.x * d, a.y * d, a.z * d);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Scales the vector uniformly.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="d">The scaling value.</param>
|
|
||||||
/// <param name="a">The vector.</param>
|
|
||||||
/// <returns>The resulting vector.</returns>
|
|
||||||
public static Vector3 operator *(float d, Vector3 a)
|
|
||||||
{
|
|
||||||
return new Vector3(a.x * d, a.y * d, a.z * d);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Divides the vector with a float.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="a">The vector.</param>
|
|
||||||
/// <param name="d">The dividing float value.</param>
|
|
||||||
/// <returns>The resulting vector.</returns>
|
|
||||||
public static Vector3 operator /(Vector3 a, float d)
|
|
||||||
{
|
|
||||||
return new Vector3(a.x / d, a.y / d, a.z / d);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Subtracts the vector from a zero vector.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="a">The vector.</param>
|
|
||||||
/// <returns>The resulting vector.</returns>
|
|
||||||
public static Vector3 operator -(Vector3 a)
|
|
||||||
{
|
|
||||||
return new Vector3(-a.x, -a.y, -a.z);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns if two vectors equals eachother.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="lhs">The left hand side vector.</param>
|
|
||||||
/// <param name="rhs">The right hand side vector.</param>
|
|
||||||
/// <returns>If equals.</returns>
|
|
||||||
public static bool operator ==(Vector3 lhs, Vector3 rhs)
|
|
||||||
{
|
|
||||||
return (lhs - rhs).MagnitudeSqr < Epsilon;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns if two vectors don't equal eachother.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="lhs">The left hand side vector.</param>
|
|
||||||
/// <param name="rhs">The right hand side vector.</param>
|
|
||||||
/// <returns>If not equals.</returns>
|
|
||||||
public static bool operator !=(Vector3 lhs, Vector3 rhs)
|
|
||||||
{
|
|
||||||
return (lhs - rhs).MagnitudeSqr >= Epsilon;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Explicitly converts from a double-precision vector into a single-precision vector.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="v">The double-precision vector.</param>
|
|
||||||
public static explicit operator Vector3(Vector3d v)
|
|
||||||
{
|
|
||||||
return new Vector3((float)v.x, (float)v.y, (float)v.z);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Implicitly converts from an integer vector into a single-precision vector.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="v">The integer vector.</param>
|
|
||||||
public static implicit operator Vector3(Vector3i v)
|
|
||||||
{
|
|
||||||
return new Vector3(v.x, v.y, v.z);
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Public Methods
|
|
||||||
#region Instance
|
|
||||||
/// <summary>
|
|
||||||
/// Set x, y and z components of an existing vector.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="x">The x value.</param>
|
|
||||||
/// <param name="y">The y value.</param>
|
|
||||||
/// <param name="z">The z value.</param>
|
|
||||||
public void Set(float x, float y, float z)
|
|
||||||
{
|
|
||||||
this.x = x;
|
|
||||||
this.y = y;
|
|
||||||
this.z = z;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Multiplies with another vector component-wise.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="scale">The vector to multiply with.</param>
|
|
||||||
public void Scale(ref Vector3 scale)
|
|
||||||
{
|
|
||||||
x *= scale.x;
|
|
||||||
y *= scale.y;
|
|
||||||
z *= scale.z;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Normalizes this vector.
|
|
||||||
/// </summary>
|
|
||||||
public void Normalize()
|
|
||||||
{
|
|
||||||
float mag = this.Magnitude;
|
|
||||||
if (mag > Epsilon)
|
|
||||||
{
|
|
||||||
x /= mag;
|
|
||||||
y /= mag;
|
|
||||||
z /= mag;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
x = y = z = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Clamps this vector between a specific range.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="min">The minimum component value.</param>
|
|
||||||
/// <param name="max">The maximum component value.</param>
|
|
||||||
public void Clamp(float min, float max)
|
|
||||||
{
|
|
||||||
if (x < min) x = min;
|
|
||||||
else if (x > max) x = max;
|
|
||||||
|
|
||||||
if (y < min) y = min;
|
|
||||||
else if (y > max) y = max;
|
|
||||||
|
|
||||||
if (z < min) z = min;
|
|
||||||
else if (z > max) z = max;
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Object
|
|
||||||
/// <summary>
|
|
||||||
/// Returns a hash code for this vector.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>The hash code.</returns>
|
|
||||||
public override int GetHashCode()
|
|
||||||
{
|
|
||||||
return x.GetHashCode() ^ y.GetHashCode() << 2 ^ z.GetHashCode() >> 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns if this vector is equal to another one.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="other">The other vector to compare to.</param>
|
|
||||||
/// <returns>If equals.</returns>
|
|
||||||
public override bool Equals(object other)
|
|
||||||
{
|
|
||||||
if (!(other is Vector3))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
Vector3 vector = (Vector3)other;
|
|
||||||
return (x == vector.x && y == vector.y && z == vector.z);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns if this vector is equal to another one.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="other">The other vector to compare to.</param>
|
|
||||||
/// <returns>If equals.</returns>
|
|
||||||
public bool Equals(Vector3 other)
|
|
||||||
{
|
|
||||||
return (x == other.x && y == other.y && z == other.z);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns a nicely formatted string for this vector.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>The string.</returns>
|
|
||||||
public override string ToString()
|
|
||||||
{
|
|
||||||
return string.Format("({0}, {1}, {2})",
|
|
||||||
x.ToString("F1", CultureInfo.InvariantCulture),
|
|
||||||
y.ToString("F1", CultureInfo.InvariantCulture),
|
|
||||||
z.ToString("F1", CultureInfo.InvariantCulture));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns a nicely formatted string for this vector.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="format">The float format.</param>
|
|
||||||
/// <returns>The string.</returns>
|
|
||||||
public string ToString(string format)
|
|
||||||
{
|
|
||||||
return string.Format("({0}, {1}, {2})",
|
|
||||||
x.ToString(format, CultureInfo.InvariantCulture),
|
|
||||||
y.ToString(format, CultureInfo.InvariantCulture),
|
|
||||||
z.ToString(format, CultureInfo.InvariantCulture));
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Static
|
|
||||||
/// <summary>
|
|
||||||
/// Dot Product of two vectors.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="lhs">The left hand side vector.</param>
|
|
||||||
/// <param name="rhs">The right hand side vector.</param>
|
|
||||||
public static float Dot(ref Vector3 lhs, ref Vector3 rhs)
|
|
||||||
{
|
|
||||||
return lhs.x * rhs.x + lhs.y * rhs.y + lhs.z * rhs.z;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Cross Product of two vectors.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="lhs">The left hand side vector.</param>
|
|
||||||
/// <param name="rhs">The right hand side vector.</param>
|
|
||||||
/// <param name="result">The resulting vector.</param>
|
|
||||||
public static void Cross(ref Vector3 lhs, ref Vector3 rhs, out Vector3 result)
|
|
||||||
{
|
|
||||||
result = new Vector3(lhs.y * rhs.z - lhs.z * rhs.y, lhs.z * rhs.x - lhs.x * rhs.z, lhs.x * rhs.y - lhs.y * rhs.x);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Calculates the angle between two vectors.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="from">The from vector.</param>
|
|
||||||
/// <param name="to">The to vector.</param>
|
|
||||||
/// <returns>The angle.</returns>
|
|
||||||
public static float Angle(ref Vector3 from, ref Vector3 to)
|
|
||||||
{
|
|
||||||
Vector3 fromNormalized = from.Normalized;
|
|
||||||
Vector3 toNormalized = to.Normalized;
|
|
||||||
return (float)System.Math.Acos(MathHelper.Clamp(Vector3.Dot(ref fromNormalized, ref toNormalized), -1f, 1f)) * MathHelper.Rad2Deg;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Performs a linear interpolation between two vectors.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="a">The vector to interpolate from.</param>
|
|
||||||
/// <param name="b">The vector to interpolate to.</param>
|
|
||||||
/// <param name="t">The time fraction.</param>
|
|
||||||
/// <param name="result">The resulting vector.</param>
|
|
||||||
public static void Lerp(ref Vector3 a, ref Vector3 b, float t, out Vector3 result)
|
|
||||||
{
|
|
||||||
result = new Vector3(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t, a.z + (b.z - a.z) * t);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Multiplies two vectors component-wise.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="a">The first vector.</param>
|
|
||||||
/// <param name="b">The second vector.</param>
|
|
||||||
/// <param name="result">The resulting vector.</param>
|
|
||||||
public static void Scale(ref Vector3 a, ref Vector3 b, out Vector3 result)
|
|
||||||
{
|
|
||||||
result = new Vector3(a.x * b.x, a.y * b.y, a.z * b.z);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Normalizes a vector.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="value">The vector to normalize.</param>
|
|
||||||
/// <param name="result">The resulting normalized vector.</param>
|
|
||||||
public static void Normalize(ref Vector3 value, out Vector3 result)
|
|
||||||
{
|
|
||||||
float mag = value.Magnitude;
|
|
||||||
if (mag > Epsilon)
|
|
||||||
{
|
|
||||||
result = new Vector3(value.x / mag, value.y / mag, value.z / mag);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
result = Vector3.zero;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Normalizes both vectors and makes them orthogonal to each other.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="normal">The normal vector.</param>
|
|
||||||
/// <param name="tangent">The tangent.</param>
|
|
||||||
public static void OrthoNormalize(ref Vector3 normal, ref Vector3 tangent)
|
|
||||||
{
|
|
||||||
normal.Normalize();
|
|
||||||
Vector3 proj = normal * Vector3.Dot(ref tangent, ref normal);
|
|
||||||
tangent -= proj;
|
|
||||||
tangent.Normalize();
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,481 +0,0 @@
|
|||||||
#region License
|
|
||||||
/*
|
|
||||||
MIT License
|
|
||||||
|
|
||||||
Copyright(c) 2017-2018 Mattias Edlund
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
*/
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Globalization;
|
|
||||||
|
|
||||||
namespace MeshDecimator.Math
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// A double precision 3D vector.
|
|
||||||
/// </summary>
|
|
||||||
public struct Vector3d : IEquatable<Vector3d>
|
|
||||||
{
|
|
||||||
#region Static Read-Only
|
|
||||||
/// <summary>
|
|
||||||
/// The zero vector.
|
|
||||||
/// </summary>
|
|
||||||
public static readonly Vector3d zero = new Vector3d(0, 0, 0);
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Consts
|
|
||||||
/// <summary>
|
|
||||||
/// The vector epsilon.
|
|
||||||
/// </summary>
|
|
||||||
public const double Epsilon = double.Epsilon;
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Fields
|
|
||||||
/// <summary>
|
|
||||||
/// The x component.
|
|
||||||
/// </summary>
|
|
||||||
public double x;
|
|
||||||
/// <summary>
|
|
||||||
/// The y component.
|
|
||||||
/// </summary>
|
|
||||||
public double y;
|
|
||||||
/// <summary>
|
|
||||||
/// The z component.
|
|
||||||
/// </summary>
|
|
||||||
public double z;
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Properties
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the magnitude of this vector.
|
|
||||||
/// </summary>
|
|
||||||
public double Magnitude
|
|
||||||
{
|
|
||||||
get { return System.Math.Sqrt(x * x + y * y + z * z); }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the squared magnitude of this vector.
|
|
||||||
/// </summary>
|
|
||||||
public double MagnitudeSqr
|
|
||||||
{
|
|
||||||
get { return (x * x + y * y + z * z); }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets a normalized vector from this vector.
|
|
||||||
/// </summary>
|
|
||||||
public Vector3d Normalized
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
Vector3d result;
|
|
||||||
Normalize(ref this, out result);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets a specific component by index in this vector.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="index">The component index.</param>
|
|
||||||
public double this[int index]
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
switch (index)
|
|
||||||
{
|
|
||||||
case 0:
|
|
||||||
return x;
|
|
||||||
case 1:
|
|
||||||
return y;
|
|
||||||
case 2:
|
|
||||||
return z;
|
|
||||||
default:
|
|
||||||
throw new IndexOutOfRangeException("Invalid Vector3d index!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
set
|
|
||||||
{
|
|
||||||
switch (index)
|
|
||||||
{
|
|
||||||
case 0:
|
|
||||||
x = value;
|
|
||||||
break;
|
|
||||||
case 1:
|
|
||||||
y = value;
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
z = value;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new IndexOutOfRangeException("Invalid Vector3d index!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Constructor
|
|
||||||
/// <summary>
|
|
||||||
/// Creates a new vector with one value for all components.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="value">The value.</param>
|
|
||||||
public Vector3d(double value)
|
|
||||||
{
|
|
||||||
this.x = value;
|
|
||||||
this.y = value;
|
|
||||||
this.z = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Creates a new vector.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="x">The x value.</param>
|
|
||||||
/// <param name="y">The y value.</param>
|
|
||||||
/// <param name="z">The z value.</param>
|
|
||||||
public Vector3d(double x, double y, double z)
|
|
||||||
{
|
|
||||||
this.x = x;
|
|
||||||
this.y = y;
|
|
||||||
this.z = z;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Creates a new vector from a single precision vector.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="vector">The single precision vector.</param>
|
|
||||||
public Vector3d(Vector3 vector)
|
|
||||||
{
|
|
||||||
this.x = vector.x;
|
|
||||||
this.y = vector.y;
|
|
||||||
this.z = vector.z;
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Operators
|
|
||||||
/// <summary>
|
|
||||||
/// Adds two vectors.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="a">The first vector.</param>
|
|
||||||
/// <param name="b">The second vector.</param>
|
|
||||||
/// <returns>The resulting vector.</returns>
|
|
||||||
public static Vector3d operator +(Vector3d a, Vector3d b)
|
|
||||||
{
|
|
||||||
return new Vector3d(a.x + b.x, a.y + b.y, a.z + b.z);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Subtracts two vectors.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="a">The first vector.</param>
|
|
||||||
/// <param name="b">The second vector.</param>
|
|
||||||
/// <returns>The resulting vector.</returns>
|
|
||||||
public static Vector3d operator -(Vector3d a, Vector3d b)
|
|
||||||
{
|
|
||||||
return new Vector3d(a.x - b.x, a.y - b.y, a.z - b.z);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Scales the vector uniformly.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="a">The vector.</param>
|
|
||||||
/// <param name="d">The scaling value.</param>
|
|
||||||
/// <returns>The resulting vector.</returns>
|
|
||||||
public static Vector3d operator *(Vector3d a, double d)
|
|
||||||
{
|
|
||||||
return new Vector3d(a.x * d, a.y * d, a.z * d);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Scales the vector uniformly.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="d">The scaling value.</param>
|
|
||||||
/// <param name="a">The vector.</param>
|
|
||||||
/// <returns>The resulting vector.</returns>
|
|
||||||
public static Vector3d operator *(double d, Vector3d a)
|
|
||||||
{
|
|
||||||
return new Vector3d(a.x * d, a.y * d, a.z * d);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Divides the vector with a float.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="a">The vector.</param>
|
|
||||||
/// <param name="d">The dividing float value.</param>
|
|
||||||
/// <returns>The resulting vector.</returns>
|
|
||||||
public static Vector3d operator /(Vector3d a, double d)
|
|
||||||
{
|
|
||||||
return new Vector3d(a.x / d, a.y / d, a.z / d);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Subtracts the vector from a zero vector.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="a">The vector.</param>
|
|
||||||
/// <returns>The resulting vector.</returns>
|
|
||||||
public static Vector3d operator -(Vector3d a)
|
|
||||||
{
|
|
||||||
return new Vector3d(-a.x, -a.y, -a.z);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns if two vectors equals eachother.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="lhs">The left hand side vector.</param>
|
|
||||||
/// <param name="rhs">The right hand side vector.</param>
|
|
||||||
/// <returns>If equals.</returns>
|
|
||||||
public static bool operator ==(Vector3d lhs, Vector3d rhs)
|
|
||||||
{
|
|
||||||
return (lhs - rhs).MagnitudeSqr < Epsilon;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns if two vectors don't equal eachother.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="lhs">The left hand side vector.</param>
|
|
||||||
/// <param name="rhs">The right hand side vector.</param>
|
|
||||||
/// <returns>If not equals.</returns>
|
|
||||||
public static bool operator !=(Vector3d lhs, Vector3d rhs)
|
|
||||||
{
|
|
||||||
return (lhs - rhs).MagnitudeSqr >= Epsilon;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Implicitly converts from a single-precision vector into a double-precision vector.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="v">The single-precision vector.</param>
|
|
||||||
public static implicit operator Vector3d(Vector3 v)
|
|
||||||
{
|
|
||||||
return new Vector3d(v.x, v.y, v.z);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Implicitly converts from an integer vector into a double-precision vector.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="v">The integer vector.</param>
|
|
||||||
public static implicit operator Vector3d(Vector3i v)
|
|
||||||
{
|
|
||||||
return new Vector3d(v.x, v.y, v.z);
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Public Methods
|
|
||||||
#region Instance
|
|
||||||
/// <summary>
|
|
||||||
/// Set x, y and z components of an existing vector.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="x">The x value.</param>
|
|
||||||
/// <param name="y">The y value.</param>
|
|
||||||
/// <param name="z">The z value.</param>
|
|
||||||
public void Set(double x, double y, double z)
|
|
||||||
{
|
|
||||||
this.x = x;
|
|
||||||
this.y = y;
|
|
||||||
this.z = z;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Multiplies with another vector component-wise.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="scale">The vector to multiply with.</param>
|
|
||||||
public void Scale(ref Vector3d scale)
|
|
||||||
{
|
|
||||||
x *= scale.x;
|
|
||||||
y *= scale.y;
|
|
||||||
z *= scale.z;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Normalizes this vector.
|
|
||||||
/// </summary>
|
|
||||||
public void Normalize()
|
|
||||||
{
|
|
||||||
double mag = this.Magnitude;
|
|
||||||
if (mag > Epsilon)
|
|
||||||
{
|
|
||||||
x /= mag;
|
|
||||||
y /= mag;
|
|
||||||
z /= mag;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
x = y = z = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Clamps this vector between a specific range.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="min">The minimum component value.</param>
|
|
||||||
/// <param name="max">The maximum component value.</param>
|
|
||||||
public void Clamp(double min, double max)
|
|
||||||
{
|
|
||||||
if (x < min) x = min;
|
|
||||||
else if (x > max) x = max;
|
|
||||||
|
|
||||||
if (y < min) y = min;
|
|
||||||
else if (y > max) y = max;
|
|
||||||
|
|
||||||
if (z < min) z = min;
|
|
||||||
else if (z > max) z = max;
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Object
|
|
||||||
/// <summary>
|
|
||||||
/// Returns a hash code for this vector.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>The hash code.</returns>
|
|
||||||
public override int GetHashCode()
|
|
||||||
{
|
|
||||||
return x.GetHashCode() ^ y.GetHashCode() << 2 ^ z.GetHashCode() >> 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns if this vector is equal to another one.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="other">The other vector to compare to.</param>
|
|
||||||
/// <returns>If equals.</returns>
|
|
||||||
public override bool Equals(object other)
|
|
||||||
{
|
|
||||||
if (!(other is Vector3d))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
Vector3d vector = (Vector3d)other;
|
|
||||||
return (x == vector.x && y == vector.y && z == vector.z);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns if this vector is equal to another one.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="other">The other vector to compare to.</param>
|
|
||||||
/// <returns>If equals.</returns>
|
|
||||||
public bool Equals(Vector3d other)
|
|
||||||
{
|
|
||||||
return (x == other.x && y == other.y && z == other.z);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns a nicely formatted string for this vector.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>The string.</returns>
|
|
||||||
public override string ToString()
|
|
||||||
{
|
|
||||||
return string.Format("({0}, {1}, {2})",
|
|
||||||
x.ToString("F1", CultureInfo.InvariantCulture),
|
|
||||||
y.ToString("F1", CultureInfo.InvariantCulture),
|
|
||||||
z.ToString("F1", CultureInfo.InvariantCulture));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns a nicely formatted string for this vector.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="format">The float format.</param>
|
|
||||||
/// <returns>The string.</returns>
|
|
||||||
public string ToString(string format)
|
|
||||||
{
|
|
||||||
return string.Format("({0}, {1}, {2})",
|
|
||||||
x.ToString(format, CultureInfo.InvariantCulture),
|
|
||||||
y.ToString(format, CultureInfo.InvariantCulture),
|
|
||||||
z.ToString(format, CultureInfo.InvariantCulture));
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Static
|
|
||||||
/// <summary>
|
|
||||||
/// Dot Product of two vectors.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="lhs">The left hand side vector.</param>
|
|
||||||
/// <param name="rhs">The right hand side vector.</param>
|
|
||||||
public static double Dot(ref Vector3d lhs, ref Vector3d rhs)
|
|
||||||
{
|
|
||||||
return lhs.x * rhs.x + lhs.y * rhs.y + lhs.z * rhs.z;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Cross Product of two vectors.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="lhs">The left hand side vector.</param>
|
|
||||||
/// <param name="rhs">The right hand side vector.</param>
|
|
||||||
/// <param name="result">The resulting vector.</param>
|
|
||||||
public static void Cross(ref Vector3d lhs, ref Vector3d rhs, out Vector3d result)
|
|
||||||
{
|
|
||||||
result = new Vector3d(lhs.y * rhs.z - lhs.z * rhs.y, lhs.z * rhs.x - lhs.x * rhs.z, lhs.x * rhs.y - lhs.y * rhs.x);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Calculates the angle between two vectors.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="from">The from vector.</param>
|
|
||||||
/// <param name="to">The to vector.</param>
|
|
||||||
/// <returns>The angle.</returns>
|
|
||||||
public static double Angle(ref Vector3d from, ref Vector3d to)
|
|
||||||
{
|
|
||||||
Vector3d fromNormalized = from.Normalized;
|
|
||||||
Vector3d toNormalized = to.Normalized;
|
|
||||||
return System.Math.Acos(MathHelper.Clamp(Vector3d.Dot(ref fromNormalized, ref toNormalized), -1.0, 1.0)) * MathHelper.Rad2Degd;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Performs a linear interpolation between two vectors.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="a">The vector to interpolate from.</param>
|
|
||||||
/// <param name="b">The vector to interpolate to.</param>
|
|
||||||
/// <param name="t">The time fraction.</param>
|
|
||||||
/// <param name="result">The resulting vector.</param>
|
|
||||||
public static void Lerp(ref Vector3d a, ref Vector3d b, double t, out Vector3d result)
|
|
||||||
{
|
|
||||||
result = new Vector3d(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t, a.z + (b.z - a.z) * t);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Multiplies two vectors component-wise.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="a">The first vector.</param>
|
|
||||||
/// <param name="b">The second vector.</param>
|
|
||||||
/// <param name="result">The resulting vector.</param>
|
|
||||||
public static void Scale(ref Vector3d a, ref Vector3d b, out Vector3d result)
|
|
||||||
{
|
|
||||||
result = new Vector3d(a.x * b.x, a.y * b.y, a.z * b.z);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Normalizes a vector.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="value">The vector to normalize.</param>
|
|
||||||
/// <param name="result">The resulting normalized vector.</param>
|
|
||||||
public static void Normalize(ref Vector3d value, out Vector3d result)
|
|
||||||
{
|
|
||||||
double mag = value.Magnitude;
|
|
||||||
if (mag > Epsilon)
|
|
||||||
{
|
|
||||||
result = new Vector3d(value.x / mag, value.y / mag, value.z / mag);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
result = Vector3d.zero;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,368 +0,0 @@
|
|||||||
#region License
|
|
||||||
/*
|
|
||||||
MIT License
|
|
||||||
|
|
||||||
Copyright(c) 2017-2018 Mattias Edlund
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
*/
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Globalization;
|
|
||||||
|
|
||||||
namespace MeshDecimator.Math
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// A 3D integer vector.
|
|
||||||
/// </summary>
|
|
||||||
public struct Vector3i : IEquatable<Vector3i>
|
|
||||||
{
|
|
||||||
#region Static Read-Only
|
|
||||||
/// <summary>
|
|
||||||
/// The zero vector.
|
|
||||||
/// </summary>
|
|
||||||
public static readonly Vector3i zero = new Vector3i(0, 0, 0);
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Fields
|
|
||||||
/// <summary>
|
|
||||||
/// The x component.
|
|
||||||
/// </summary>
|
|
||||||
public int x;
|
|
||||||
/// <summary>
|
|
||||||
/// The y component.
|
|
||||||
/// </summary>
|
|
||||||
public int y;
|
|
||||||
/// <summary>
|
|
||||||
/// The z component.
|
|
||||||
/// </summary>
|
|
||||||
public int z;
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Properties
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the magnitude of this vector.
|
|
||||||
/// </summary>
|
|
||||||
public int Magnitude
|
|
||||||
{
|
|
||||||
get { return (int)System.Math.Sqrt(x * x + y * y + z * z); }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the squared magnitude of this vector.
|
|
||||||
/// </summary>
|
|
||||||
public int MagnitudeSqr
|
|
||||||
{
|
|
||||||
get { return (x * x + y * y + z * z); }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets a specific component by index in this vector.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="index">The component index.</param>
|
|
||||||
public int this[int index]
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
switch (index)
|
|
||||||
{
|
|
||||||
case 0:
|
|
||||||
return x;
|
|
||||||
case 1:
|
|
||||||
return y;
|
|
||||||
case 2:
|
|
||||||
return z;
|
|
||||||
default:
|
|
||||||
throw new IndexOutOfRangeException("Invalid Vector3i index!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
set
|
|
||||||
{
|
|
||||||
switch (index)
|
|
||||||
{
|
|
||||||
case 0:
|
|
||||||
x = value;
|
|
||||||
break;
|
|
||||||
case 1:
|
|
||||||
y = value;
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
z = value;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new IndexOutOfRangeException("Invalid Vector3i index!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Constructor
|
|
||||||
/// <summary>
|
|
||||||
/// Creates a new vector with one value for all components.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="value">The value.</param>
|
|
||||||
public Vector3i(int value)
|
|
||||||
{
|
|
||||||
this.x = value;
|
|
||||||
this.y = value;
|
|
||||||
this.z = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Creates a new vector.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="x">The x value.</param>
|
|
||||||
/// <param name="y">The y value.</param>
|
|
||||||
/// <param name="z">The z value.</param>
|
|
||||||
public Vector3i(int x, int y, int z)
|
|
||||||
{
|
|
||||||
this.x = x;
|
|
||||||
this.y = y;
|
|
||||||
this.z = z;
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Operators
|
|
||||||
/// <summary>
|
|
||||||
/// Adds two vectors.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="a">The first vector.</param>
|
|
||||||
/// <param name="b">The second vector.</param>
|
|
||||||
/// <returns>The resulting vector.</returns>
|
|
||||||
public static Vector3i operator +(Vector3i a, Vector3i b)
|
|
||||||
{
|
|
||||||
return new Vector3i(a.x + b.x, a.y + b.y, a.z + b.z);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Subtracts two vectors.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="a">The first vector.</param>
|
|
||||||
/// <param name="b">The second vector.</param>
|
|
||||||
/// <returns>The resulting vector.</returns>
|
|
||||||
public static Vector3i operator -(Vector3i a, Vector3i b)
|
|
||||||
{
|
|
||||||
return new Vector3i(a.x - b.x, a.y - b.y, a.z - b.z);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Scales the vector uniformly.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="a">The vector.</param>
|
|
||||||
/// <param name="d">The scaling value.</param>
|
|
||||||
/// <returns>The resulting vector.</returns>
|
|
||||||
public static Vector3i operator *(Vector3i a, int d)
|
|
||||||
{
|
|
||||||
return new Vector3i(a.x * d, a.y * d, a.z * d);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Scales the vector uniformly.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="d">The scaling value.</param>
|
|
||||||
/// <param name="a">The vector.</param>
|
|
||||||
/// <returns>The resulting vector.</returns>
|
|
||||||
public static Vector3i operator *(int d, Vector3i a)
|
|
||||||
{
|
|
||||||
return new Vector3i(a.x * d, a.y * d, a.z * d);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Divides the vector with a float.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="a">The vector.</param>
|
|
||||||
/// <param name="d">The dividing float value.</param>
|
|
||||||
/// <returns>The resulting vector.</returns>
|
|
||||||
public static Vector3i operator /(Vector3i a, int d)
|
|
||||||
{
|
|
||||||
return new Vector3i(a.x / d, a.y / d, a.z / d);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Subtracts the vector from a zero vector.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="a">The vector.</param>
|
|
||||||
/// <returns>The resulting vector.</returns>
|
|
||||||
public static Vector3i operator -(Vector3i a)
|
|
||||||
{
|
|
||||||
return new Vector3i(-a.x, -a.y, -a.z);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns if two vectors equals eachother.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="lhs">The left hand side vector.</param>
|
|
||||||
/// <param name="rhs">The right hand side vector.</param>
|
|
||||||
/// <returns>If equals.</returns>
|
|
||||||
public static bool operator ==(Vector3i lhs, Vector3i rhs)
|
|
||||||
{
|
|
||||||
return (lhs.x == rhs.x && lhs.y == rhs.y && lhs.z == rhs.z);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns if two vectors don't equal eachother.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="lhs">The left hand side vector.</param>
|
|
||||||
/// <param name="rhs">The right hand side vector.</param>
|
|
||||||
/// <returns>If not equals.</returns>
|
|
||||||
public static bool operator !=(Vector3i lhs, Vector3i rhs)
|
|
||||||
{
|
|
||||||
return (lhs.x != rhs.x || lhs.y != rhs.y || lhs.z != rhs.z);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Explicitly converts from a single-precision vector into an integer vector.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="v">The single-precision vector.</param>
|
|
||||||
public static implicit operator Vector3i(Vector3 v)
|
|
||||||
{
|
|
||||||
return new Vector3i((int)v.x, (int)v.y, (int)v.z);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Explicitly converts from a double-precision vector into an integer vector.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="v">The double-precision vector.</param>
|
|
||||||
public static explicit operator Vector3i(Vector3d v)
|
|
||||||
{
|
|
||||||
return new Vector3i((int)v.x, (int)v.y, (int)v.z);
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Public Methods
|
|
||||||
#region Instance
|
|
||||||
/// <summary>
|
|
||||||
/// Set x, y and z components of an existing vector.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="x">The x value.</param>
|
|
||||||
/// <param name="y">The y value.</param>
|
|
||||||
/// <param name="z">The z value.</param>
|
|
||||||
public void Set(int x, int y, int z)
|
|
||||||
{
|
|
||||||
this.x = x;
|
|
||||||
this.y = y;
|
|
||||||
this.z = z;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Multiplies with another vector component-wise.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="scale">The vector to multiply with.</param>
|
|
||||||
public void Scale(ref Vector3i scale)
|
|
||||||
{
|
|
||||||
x *= scale.x;
|
|
||||||
y *= scale.y;
|
|
||||||
z *= scale.z;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Clamps this vector between a specific range.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="min">The minimum component value.</param>
|
|
||||||
/// <param name="max">The maximum component value.</param>
|
|
||||||
public void Clamp(int min, int max)
|
|
||||||
{
|
|
||||||
if (x < min) x = min;
|
|
||||||
else if (x > max) x = max;
|
|
||||||
|
|
||||||
if (y < min) y = min;
|
|
||||||
else if (y > max) y = max;
|
|
||||||
|
|
||||||
if (z < min) z = min;
|
|
||||||
else if (z > max) z = max;
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Object
|
|
||||||
/// <summary>
|
|
||||||
/// Returns a hash code for this vector.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>The hash code.</returns>
|
|
||||||
public override int GetHashCode()
|
|
||||||
{
|
|
||||||
return x.GetHashCode() ^ y.GetHashCode() << 2 ^ z.GetHashCode() >> 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns if this vector is equal to another one.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="other">The other vector to compare to.</param>
|
|
||||||
/// <returns>If equals.</returns>
|
|
||||||
public override bool Equals(object other)
|
|
||||||
{
|
|
||||||
if (!(other is Vector3i))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
Vector3i vector = (Vector3i)other;
|
|
||||||
return (x == vector.x && y == vector.y && z == vector.z);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns if this vector is equal to another one.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="other">The other vector to compare to.</param>
|
|
||||||
/// <returns>If equals.</returns>
|
|
||||||
public bool Equals(Vector3i other)
|
|
||||||
{
|
|
||||||
return (x == other.x && y == other.y && z == other.z);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns a nicely formatted string for this vector.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>The string.</returns>
|
|
||||||
public override string ToString()
|
|
||||||
{
|
|
||||||
return string.Format("({0}, {1}, {2})",
|
|
||||||
x.ToString(CultureInfo.InvariantCulture),
|
|
||||||
y.ToString(CultureInfo.InvariantCulture),
|
|
||||||
z.ToString(CultureInfo.InvariantCulture));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns a nicely formatted string for this vector.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="format">The integer format.</param>
|
|
||||||
/// <returns>The string.</returns>
|
|
||||||
public string ToString(string format)
|
|
||||||
{
|
|
||||||
return string.Format("({0}, {1}, {2})",
|
|
||||||
x.ToString(format, CultureInfo.InvariantCulture),
|
|
||||||
y.ToString(format, CultureInfo.InvariantCulture),
|
|
||||||
z.ToString(format, CultureInfo.InvariantCulture));
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Static
|
|
||||||
/// <summary>
|
|
||||||
/// Multiplies two vectors component-wise.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="a">The first vector.</param>
|
|
||||||
/// <param name="b">The second vector.</param>
|
|
||||||
/// <param name="result">The resulting vector.</param>
|
|
||||||
public static void Scale(ref Vector3i a, ref Vector3i b, out Vector3i result)
|
|
||||||
{
|
|
||||||
result = new Vector3i(a.x * b.x, a.y * b.y, a.z * b.z);
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,467 +0,0 @@
|
|||||||
#region License
|
|
||||||
/*
|
|
||||||
MIT License
|
|
||||||
|
|
||||||
Copyright(c) 2017-2018 Mattias Edlund
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
*/
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Globalization;
|
|
||||||
|
|
||||||
namespace MeshDecimator.Math
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// A single precision 4D vector.
|
|
||||||
/// </summary>
|
|
||||||
public struct Vector4 : IEquatable<Vector4>
|
|
||||||
{
|
|
||||||
#region Static Read-Only
|
|
||||||
/// <summary>
|
|
||||||
/// The zero vector.
|
|
||||||
/// </summary>
|
|
||||||
public static readonly Vector4 zero = new Vector4(0, 0, 0, 0);
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Consts
|
|
||||||
/// <summary>
|
|
||||||
/// The vector epsilon.
|
|
||||||
/// </summary>
|
|
||||||
public const float Epsilon = 9.99999944E-11f;
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Fields
|
|
||||||
/// <summary>
|
|
||||||
/// The x component.
|
|
||||||
/// </summary>
|
|
||||||
public float x;
|
|
||||||
/// <summary>
|
|
||||||
/// The y component.
|
|
||||||
/// </summary>
|
|
||||||
public float y;
|
|
||||||
/// <summary>
|
|
||||||
/// The z component.
|
|
||||||
/// </summary>
|
|
||||||
public float z;
|
|
||||||
/// <summary>
|
|
||||||
/// The w component.
|
|
||||||
/// </summary>
|
|
||||||
public float w;
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Properties
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the magnitude of this vector.
|
|
||||||
/// </summary>
|
|
||||||
public float Magnitude
|
|
||||||
{
|
|
||||||
get { return (float)System.Math.Sqrt(x * x + y * y + z * z + w * w); }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the squared magnitude of this vector.
|
|
||||||
/// </summary>
|
|
||||||
public float MagnitudeSqr
|
|
||||||
{
|
|
||||||
get { return (x * x + y * y + z * z + w * w); }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets a normalized vector from this vector.
|
|
||||||
/// </summary>
|
|
||||||
public Vector4 Normalized
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
Vector4 result;
|
|
||||||
Normalize(ref this, out result);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets a specific component by index in this vector.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="index">The component index.</param>
|
|
||||||
public float this[int index]
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
switch (index)
|
|
||||||
{
|
|
||||||
case 0:
|
|
||||||
return x;
|
|
||||||
case 1:
|
|
||||||
return y;
|
|
||||||
case 2:
|
|
||||||
return z;
|
|
||||||
case 3:
|
|
||||||
return w;
|
|
||||||
default:
|
|
||||||
throw new IndexOutOfRangeException("Invalid Vector4 index!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
set
|
|
||||||
{
|
|
||||||
switch (index)
|
|
||||||
{
|
|
||||||
case 0:
|
|
||||||
x = value;
|
|
||||||
break;
|
|
||||||
case 1:
|
|
||||||
y = value;
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
z = value;
|
|
||||||
break;
|
|
||||||
case 3:
|
|
||||||
w = value;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new IndexOutOfRangeException("Invalid Vector4 index!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Constructor
|
|
||||||
/// <summary>
|
|
||||||
/// Creates a new vector with one value for all components.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="value">The value.</param>
|
|
||||||
public Vector4(float value)
|
|
||||||
{
|
|
||||||
this.x = value;
|
|
||||||
this.y = value;
|
|
||||||
this.z = value;
|
|
||||||
this.w = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Creates a new vector.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="x">The x value.</param>
|
|
||||||
/// <param name="y">The y value.</param>
|
|
||||||
/// <param name="z">The z value.</param>
|
|
||||||
/// <param name="w">The w value.</param>
|
|
||||||
public Vector4(float x, float y, float z, float w)
|
|
||||||
{
|
|
||||||
this.x = x;
|
|
||||||
this.y = y;
|
|
||||||
this.z = z;
|
|
||||||
this.w = w;
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Operators
|
|
||||||
/// <summary>
|
|
||||||
/// Adds two vectors.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="a">The first vector.</param>
|
|
||||||
/// <param name="b">The second vector.</param>
|
|
||||||
/// <returns>The resulting vector.</returns>
|
|
||||||
public static Vector4 operator +(Vector4 a, Vector4 b)
|
|
||||||
{
|
|
||||||
return new Vector4(a.x + b.x, a.y + b.y, a.z + b.z, a.w + b.w);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Subtracts two vectors.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="a">The first vector.</param>
|
|
||||||
/// <param name="b">The second vector.</param>
|
|
||||||
/// <returns>The resulting vector.</returns>
|
|
||||||
public static Vector4 operator -(Vector4 a, Vector4 b)
|
|
||||||
{
|
|
||||||
return new Vector4(a.x - b.x, a.y - b.y, a.z - b.z, a.w - b.w);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Scales the vector uniformly.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="a">The vector.</param>
|
|
||||||
/// <param name="d">The scaling value.</param>
|
|
||||||
/// <returns>The resulting vector.</returns>
|
|
||||||
public static Vector4 operator *(Vector4 a, float d)
|
|
||||||
{
|
|
||||||
return new Vector4(a.x * d, a.y * d, a.z * d, a.w * d);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Scales the vector uniformly.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="d">The scaling value.</param>
|
|
||||||
/// <param name="a">The vector.</param>
|
|
||||||
/// <returns>The resulting vector.</returns>
|
|
||||||
public static Vector4 operator *(float d, Vector4 a)
|
|
||||||
{
|
|
||||||
return new Vector4(a.x * d, a.y * d, a.z * d, a.w * d);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Divides the vector with a float.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="a">The vector.</param>
|
|
||||||
/// <param name="d">The dividing float value.</param>
|
|
||||||
/// <returns>The resulting vector.</returns>
|
|
||||||
public static Vector4 operator /(Vector4 a, float d)
|
|
||||||
{
|
|
||||||
return new Vector4(a.x / d, a.y / d, a.z / d, a.w / d);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Subtracts the vector from a zero vector.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="a">The vector.</param>
|
|
||||||
/// <returns>The resulting vector.</returns>
|
|
||||||
public static Vector4 operator -(Vector4 a)
|
|
||||||
{
|
|
||||||
return new Vector4(-a.x, -a.y, -a.z, -a.w);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns if two vectors equals eachother.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="lhs">The left hand side vector.</param>
|
|
||||||
/// <param name="rhs">The right hand side vector.</param>
|
|
||||||
/// <returns>If equals.</returns>
|
|
||||||
public static bool operator ==(Vector4 lhs, Vector4 rhs)
|
|
||||||
{
|
|
||||||
return (lhs - rhs).MagnitudeSqr < Epsilon;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns if two vectors don't equal eachother.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="lhs">The left hand side vector.</param>
|
|
||||||
/// <param name="rhs">The right hand side vector.</param>
|
|
||||||
/// <returns>If not equals.</returns>
|
|
||||||
public static bool operator !=(Vector4 lhs, Vector4 rhs)
|
|
||||||
{
|
|
||||||
return (lhs - rhs).MagnitudeSqr >= Epsilon;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Explicitly converts from a double-precision vector into a single-precision vector.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="v">The double-precision vector.</param>
|
|
||||||
public static explicit operator Vector4(Vector4d v)
|
|
||||||
{
|
|
||||||
return new Vector4((float)v.x, (float)v.y, (float)v.z, (float)v.w);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Implicitly converts from an integer vector into a single-precision vector.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="v">The integer vector.</param>
|
|
||||||
public static implicit operator Vector4(Vector4i v)
|
|
||||||
{
|
|
||||||
return new Vector4(v.x, v.y, v.z, v.w);
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Public Methods
|
|
||||||
#region Instance
|
|
||||||
/// <summary>
|
|
||||||
/// Set x, y and z components of an existing vector.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="x">The x value.</param>
|
|
||||||
/// <param name="y">The y value.</param>
|
|
||||||
/// <param name="z">The z value.</param>
|
|
||||||
/// <param name="w">The w value.</param>
|
|
||||||
public void Set(float x, float y, float z, float w)
|
|
||||||
{
|
|
||||||
this.x = x;
|
|
||||||
this.y = y;
|
|
||||||
this.z = z;
|
|
||||||
this.w = w;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Multiplies with another vector component-wise.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="scale">The vector to multiply with.</param>
|
|
||||||
public void Scale(ref Vector4 scale)
|
|
||||||
{
|
|
||||||
x *= scale.x;
|
|
||||||
y *= scale.y;
|
|
||||||
z *= scale.z;
|
|
||||||
w *= scale.w;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Normalizes this vector.
|
|
||||||
/// </summary>
|
|
||||||
public void Normalize()
|
|
||||||
{
|
|
||||||
float mag = this.Magnitude;
|
|
||||||
if (mag > Epsilon)
|
|
||||||
{
|
|
||||||
x /= mag;
|
|
||||||
y /= mag;
|
|
||||||
z /= mag;
|
|
||||||
w /= mag;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
x = y = z = w = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Clamps this vector between a specific range.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="min">The minimum component value.</param>
|
|
||||||
/// <param name="max">The maximum component value.</param>
|
|
||||||
public void Clamp(float min, float max)
|
|
||||||
{
|
|
||||||
if (x < min) x = min;
|
|
||||||
else if (x > max) x = max;
|
|
||||||
|
|
||||||
if (y < min) y = min;
|
|
||||||
else if (y > max) y = max;
|
|
||||||
|
|
||||||
if (z < min) z = min;
|
|
||||||
else if (z > max) z = max;
|
|
||||||
|
|
||||||
if (w < min) w = min;
|
|
||||||
else if (w > max) w = max;
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Object
|
|
||||||
/// <summary>
|
|
||||||
/// Returns a hash code for this vector.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>The hash code.</returns>
|
|
||||||
public override int GetHashCode()
|
|
||||||
{
|
|
||||||
return x.GetHashCode() ^ y.GetHashCode() << 2 ^ z.GetHashCode() >> 2 ^ w.GetHashCode() >> 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns if this vector is equal to another one.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="other">The other vector to compare to.</param>
|
|
||||||
/// <returns>If equals.</returns>
|
|
||||||
public override bool Equals(object other)
|
|
||||||
{
|
|
||||||
if (!(other is Vector4))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
Vector4 vector = (Vector4)other;
|
|
||||||
return (x == vector.x && y == vector.y && z == vector.z && w == vector.w);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns if this vector is equal to another one.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="other">The other vector to compare to.</param>
|
|
||||||
/// <returns>If equals.</returns>
|
|
||||||
public bool Equals(Vector4 other)
|
|
||||||
{
|
|
||||||
return (x == other.x && y == other.y && z == other.z && w == other.w);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns a nicely formatted string for this vector.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>The string.</returns>
|
|
||||||
public override string ToString()
|
|
||||||
{
|
|
||||||
return string.Format("({0}, {1}, {2}, {3})",
|
|
||||||
x.ToString("F1", CultureInfo.InvariantCulture),
|
|
||||||
y.ToString("F1", CultureInfo.InvariantCulture),
|
|
||||||
z.ToString("F1", CultureInfo.InvariantCulture),
|
|
||||||
w.ToString("F1", CultureInfo.InvariantCulture));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns a nicely formatted string for this vector.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="format">The float format.</param>
|
|
||||||
/// <returns>The string.</returns>
|
|
||||||
public string ToString(string format)
|
|
||||||
{
|
|
||||||
return string.Format("({0}, {1}, {2}, {3})",
|
|
||||||
x.ToString(format, CultureInfo.InvariantCulture),
|
|
||||||
y.ToString(format, CultureInfo.InvariantCulture),
|
|
||||||
z.ToString(format, CultureInfo.InvariantCulture),
|
|
||||||
w.ToString(format, CultureInfo.InvariantCulture));
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Static
|
|
||||||
/// <summary>
|
|
||||||
/// Dot Product of two vectors.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="lhs">The left hand side vector.</param>
|
|
||||||
/// <param name="rhs">The right hand side vector.</param>
|
|
||||||
public static float Dot(ref Vector4 lhs, ref Vector4 rhs)
|
|
||||||
{
|
|
||||||
return lhs.x * rhs.x + lhs.y * rhs.y + lhs.z * rhs.z + lhs.w * rhs.w;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Performs a linear interpolation between two vectors.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="a">The vector to interpolate from.</param>
|
|
||||||
/// <param name="b">The vector to interpolate to.</param>
|
|
||||||
/// <param name="t">The time fraction.</param>
|
|
||||||
/// <param name="result">The resulting vector.</param>
|
|
||||||
public static void Lerp(ref Vector4 a, ref Vector4 b, float t, out Vector4 result)
|
|
||||||
{
|
|
||||||
result = new Vector4(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t, a.z + (b.z - a.z) * t, a.w + (b.w - a.w) * t);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Multiplies two vectors component-wise.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="a">The first vector.</param>
|
|
||||||
/// <param name="b">The second vector.</param>
|
|
||||||
/// <param name="result">The resulting vector.</param>
|
|
||||||
public static void Scale(ref Vector4 a, ref Vector4 b, out Vector4 result)
|
|
||||||
{
|
|
||||||
result = new Vector4(a.x * b.x, a.y * b.y, a.z * b.z, a.w * b.w);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Normalizes a vector.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="value">The vector to normalize.</param>
|
|
||||||
/// <param name="result">The resulting normalized vector.</param>
|
|
||||||
public static void Normalize(ref Vector4 value, out Vector4 result)
|
|
||||||
{
|
|
||||||
float mag = value.Magnitude;
|
|
||||||
if (mag > Epsilon)
|
|
||||||
{
|
|
||||||
result = new Vector4(value.x / mag, value.y / mag, value.z / mag, value.w / mag);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
result = Vector4.zero;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,467 +0,0 @@
|
|||||||
#region License
|
|
||||||
/*
|
|
||||||
MIT License
|
|
||||||
|
|
||||||
Copyright(c) 2017-2018 Mattias Edlund
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
*/
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Globalization;
|
|
||||||
|
|
||||||
namespace MeshDecimator.Math
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// A double precision 4D vector.
|
|
||||||
/// </summary>
|
|
||||||
public struct Vector4d : IEquatable<Vector4d>
|
|
||||||
{
|
|
||||||
#region Static Read-Only
|
|
||||||
/// <summary>
|
|
||||||
/// The zero vector.
|
|
||||||
/// </summary>
|
|
||||||
public static readonly Vector4d zero = new Vector4d(0, 0, 0, 0);
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Consts
|
|
||||||
/// <summary>
|
|
||||||
/// The vector epsilon.
|
|
||||||
/// </summary>
|
|
||||||
public const double Epsilon = double.Epsilon;
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Fields
|
|
||||||
/// <summary>
|
|
||||||
/// The x component.
|
|
||||||
/// </summary>
|
|
||||||
public double x;
|
|
||||||
/// <summary>
|
|
||||||
/// The y component.
|
|
||||||
/// </summary>
|
|
||||||
public double y;
|
|
||||||
/// <summary>
|
|
||||||
/// The z component.
|
|
||||||
/// </summary>
|
|
||||||
public double z;
|
|
||||||
/// <summary>
|
|
||||||
/// The w component.
|
|
||||||
/// </summary>
|
|
||||||
public double w;
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Properties
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the magnitude of this vector.
|
|
||||||
/// </summary>
|
|
||||||
public double Magnitude
|
|
||||||
{
|
|
||||||
get { return System.Math.Sqrt(x * x + y * y + z * z + w * w); }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the squared magnitude of this vector.
|
|
||||||
/// </summary>
|
|
||||||
public double MagnitudeSqr
|
|
||||||
{
|
|
||||||
get { return (x * x + y * y + z * z + w * w); }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets a normalized vector from this vector.
|
|
||||||
/// </summary>
|
|
||||||
public Vector4d Normalized
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
Vector4d result;
|
|
||||||
Normalize(ref this, out result);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets a specific component by index in this vector.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="index">The component index.</param>
|
|
||||||
public double this[int index]
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
switch (index)
|
|
||||||
{
|
|
||||||
case 0:
|
|
||||||
return x;
|
|
||||||
case 1:
|
|
||||||
return y;
|
|
||||||
case 2:
|
|
||||||
return z;
|
|
||||||
case 3:
|
|
||||||
return w;
|
|
||||||
default:
|
|
||||||
throw new IndexOutOfRangeException("Invalid Vector4d index!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
set
|
|
||||||
{
|
|
||||||
switch (index)
|
|
||||||
{
|
|
||||||
case 0:
|
|
||||||
x = value;
|
|
||||||
break;
|
|
||||||
case 1:
|
|
||||||
y = value;
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
z = value;
|
|
||||||
break;
|
|
||||||
case 3:
|
|
||||||
w = value;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new IndexOutOfRangeException("Invalid Vector4d index!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Constructor
|
|
||||||
/// <summary>
|
|
||||||
/// Creates a new vector with one value for all components.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="value">The value.</param>
|
|
||||||
public Vector4d(double value)
|
|
||||||
{
|
|
||||||
this.x = value;
|
|
||||||
this.y = value;
|
|
||||||
this.z = value;
|
|
||||||
this.w = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Creates a new vector.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="x">The x value.</param>
|
|
||||||
/// <param name="y">The y value.</param>
|
|
||||||
/// <param name="z">The z value.</param>
|
|
||||||
/// <param name="w">The w value.</param>
|
|
||||||
public Vector4d(double x, double y, double z, double w)
|
|
||||||
{
|
|
||||||
this.x = x;
|
|
||||||
this.y = y;
|
|
||||||
this.z = z;
|
|
||||||
this.w = w;
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Operators
|
|
||||||
/// <summary>
|
|
||||||
/// Adds two vectors.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="a">The first vector.</param>
|
|
||||||
/// <param name="b">The second vector.</param>
|
|
||||||
/// <returns>The resulting vector.</returns>
|
|
||||||
public static Vector4d operator +(Vector4d a, Vector4d b)
|
|
||||||
{
|
|
||||||
return new Vector4d(a.x + b.x, a.y + b.y, a.z + b.z, a.w + b.w);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Subtracts two vectors.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="a">The first vector.</param>
|
|
||||||
/// <param name="b">The second vector.</param>
|
|
||||||
/// <returns>The resulting vector.</returns>
|
|
||||||
public static Vector4d operator -(Vector4d a, Vector4d b)
|
|
||||||
{
|
|
||||||
return new Vector4d(a.x - b.x, a.y - b.y, a.z - b.z, a.w - b.w);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Scales the vector uniformly.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="a">The vector.</param>
|
|
||||||
/// <param name="d">The scaling value.</param>
|
|
||||||
/// <returns>The resulting vector.</returns>
|
|
||||||
public static Vector4d operator *(Vector4d a, double d)
|
|
||||||
{
|
|
||||||
return new Vector4d(a.x * d, a.y * d, a.z * d, a.w * d);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Scales the vector uniformly.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="d">The scaling value.</param>
|
|
||||||
/// <param name="a">The vector.</param>
|
|
||||||
/// <returns>The resulting vector.</returns>
|
|
||||||
public static Vector4d operator *(double d, Vector4d a)
|
|
||||||
{
|
|
||||||
return new Vector4d(a.x * d, a.y * d, a.z * d, a.w * d);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Divides the vector with a float.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="a">The vector.</param>
|
|
||||||
/// <param name="d">The dividing float value.</param>
|
|
||||||
/// <returns>The resulting vector.</returns>
|
|
||||||
public static Vector4d operator /(Vector4d a, double d)
|
|
||||||
{
|
|
||||||
return new Vector4d(a.x / d, a.y / d, a.z / d, a.w / d);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Subtracts the vector from a zero vector.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="a">The vector.</param>
|
|
||||||
/// <returns>The resulting vector.</returns>
|
|
||||||
public static Vector4d operator -(Vector4d a)
|
|
||||||
{
|
|
||||||
return new Vector4d(-a.x, -a.y, -a.z, -a.w);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns if two vectors equals eachother.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="lhs">The left hand side vector.</param>
|
|
||||||
/// <param name="rhs">The right hand side vector.</param>
|
|
||||||
/// <returns>If equals.</returns>
|
|
||||||
public static bool operator ==(Vector4d lhs, Vector4d rhs)
|
|
||||||
{
|
|
||||||
return (lhs - rhs).MagnitudeSqr < Epsilon;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns if two vectors don't equal eachother.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="lhs">The left hand side vector.</param>
|
|
||||||
/// <param name="rhs">The right hand side vector.</param>
|
|
||||||
/// <returns>If not equals.</returns>
|
|
||||||
public static bool operator !=(Vector4d lhs, Vector4d rhs)
|
|
||||||
{
|
|
||||||
return (lhs - rhs).MagnitudeSqr >= Epsilon;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Implicitly converts from a single-precision vector into a double-precision vector.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="v">The single-precision vector.</param>
|
|
||||||
public static implicit operator Vector4d(Vector4 v)
|
|
||||||
{
|
|
||||||
return new Vector4d(v.x, v.y, v.z, v.w);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Implicitly converts from an integer vector into a double-precision vector.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="v">The integer vector.</param>
|
|
||||||
public static implicit operator Vector4d(Vector4i v)
|
|
||||||
{
|
|
||||||
return new Vector4d(v.x, v.y, v.z, v.w);
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Public Methods
|
|
||||||
#region Instance
|
|
||||||
/// <summary>
|
|
||||||
/// Set x, y and z components of an existing vector.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="x">The x value.</param>
|
|
||||||
/// <param name="y">The y value.</param>
|
|
||||||
/// <param name="z">The z value.</param>
|
|
||||||
/// <param name="w">The w value.</param>
|
|
||||||
public void Set(double x, double y, double z, double w)
|
|
||||||
{
|
|
||||||
this.x = x;
|
|
||||||
this.y = y;
|
|
||||||
this.z = z;
|
|
||||||
this.w = w;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Multiplies with another vector component-wise.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="scale">The vector to multiply with.</param>
|
|
||||||
public void Scale(ref Vector4d scale)
|
|
||||||
{
|
|
||||||
x *= scale.x;
|
|
||||||
y *= scale.y;
|
|
||||||
z *= scale.z;
|
|
||||||
w *= scale.w;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Normalizes this vector.
|
|
||||||
/// </summary>
|
|
||||||
public void Normalize()
|
|
||||||
{
|
|
||||||
double mag = this.Magnitude;
|
|
||||||
if (mag > Epsilon)
|
|
||||||
{
|
|
||||||
x /= mag;
|
|
||||||
y /= mag;
|
|
||||||
z /= mag;
|
|
||||||
w /= mag;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
x = y = z = w = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Clamps this vector between a specific range.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="min">The minimum component value.</param>
|
|
||||||
/// <param name="max">The maximum component value.</param>
|
|
||||||
public void Clamp(double min, double max)
|
|
||||||
{
|
|
||||||
if (x < min) x = min;
|
|
||||||
else if (x > max) x = max;
|
|
||||||
|
|
||||||
if (y < min) y = min;
|
|
||||||
else if (y > max) y = max;
|
|
||||||
|
|
||||||
if (z < min) z = min;
|
|
||||||
else if (z > max) z = max;
|
|
||||||
|
|
||||||
if (w < min) w = min;
|
|
||||||
else if (w > max) w = max;
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Object
|
|
||||||
/// <summary>
|
|
||||||
/// Returns a hash code for this vector.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>The hash code.</returns>
|
|
||||||
public override int GetHashCode()
|
|
||||||
{
|
|
||||||
return x.GetHashCode() ^ y.GetHashCode() << 2 ^ z.GetHashCode() >> 2 ^ w.GetHashCode() >> 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns if this vector is equal to another one.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="other">The other vector to compare to.</param>
|
|
||||||
/// <returns>If equals.</returns>
|
|
||||||
public override bool Equals(object other)
|
|
||||||
{
|
|
||||||
if (!(other is Vector4d))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
Vector4d vector = (Vector4d)other;
|
|
||||||
return (x == vector.x && y == vector.y && z == vector.z && w == vector.w);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns if this vector is equal to another one.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="other">The other vector to compare to.</param>
|
|
||||||
/// <returns>If equals.</returns>
|
|
||||||
public bool Equals(Vector4d other)
|
|
||||||
{
|
|
||||||
return (x == other.x && y == other.y && z == other.z && w == other.w);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns a nicely formatted string for this vector.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>The string.</returns>
|
|
||||||
public override string ToString()
|
|
||||||
{
|
|
||||||
return string.Format("({0}, {1}, {2}, {3})",
|
|
||||||
x.ToString("F1", CultureInfo.InvariantCulture),
|
|
||||||
y.ToString("F1", CultureInfo.InvariantCulture),
|
|
||||||
z.ToString("F1", CultureInfo.InvariantCulture),
|
|
||||||
w.ToString("F1", CultureInfo.InvariantCulture));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns a nicely formatted string for this vector.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="format">The float format.</param>
|
|
||||||
/// <returns>The string.</returns>
|
|
||||||
public string ToString(string format)
|
|
||||||
{
|
|
||||||
return string.Format("({0}, {1}, {2}, {3})",
|
|
||||||
x.ToString(format, CultureInfo.InvariantCulture),
|
|
||||||
y.ToString(format, CultureInfo.InvariantCulture),
|
|
||||||
z.ToString(format, CultureInfo.InvariantCulture),
|
|
||||||
w.ToString(format, CultureInfo.InvariantCulture));
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Static
|
|
||||||
/// <summary>
|
|
||||||
/// Dot Product of two vectors.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="lhs">The left hand side vector.</param>
|
|
||||||
/// <param name="rhs">The right hand side vector.</param>
|
|
||||||
public static double Dot(ref Vector4d lhs, ref Vector4d rhs)
|
|
||||||
{
|
|
||||||
return lhs.x * rhs.x + lhs.y * rhs.y + lhs.z * rhs.z + lhs.w * rhs.w;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Performs a linear interpolation between two vectors.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="a">The vector to interpolate from.</param>
|
|
||||||
/// <param name="b">The vector to interpolate to.</param>
|
|
||||||
/// <param name="t">The time fraction.</param>
|
|
||||||
/// <param name="result">The resulting vector.</param>
|
|
||||||
public static void Lerp(ref Vector4d a, ref Vector4d b, double t, out Vector4d result)
|
|
||||||
{
|
|
||||||
result = new Vector4d(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t, a.z + (b.z - a.z) * t, a.w + (b.w - a.w) * t);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Multiplies two vectors component-wise.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="a">The first vector.</param>
|
|
||||||
/// <param name="b">The second vector.</param>
|
|
||||||
/// <param name="result">The resulting vector.</param>
|
|
||||||
public static void Scale(ref Vector4d a, ref Vector4d b, out Vector4d result)
|
|
||||||
{
|
|
||||||
result = new Vector4d(a.x * b.x, a.y * b.y, a.z * b.z, a.w * b.w);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Normalizes a vector.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="value">The vector to normalize.</param>
|
|
||||||
/// <param name="result">The resulting normalized vector.</param>
|
|
||||||
public static void Normalize(ref Vector4d value, out Vector4d result)
|
|
||||||
{
|
|
||||||
double mag = value.Magnitude;
|
|
||||||
if (mag > Epsilon)
|
|
||||||
{
|
|
||||||
result = new Vector4d(value.x / mag, value.y / mag, value.z / mag, value.w / mag);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
result = Vector4d.zero;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,388 +0,0 @@
|
|||||||
#region License
|
|
||||||
/*
|
|
||||||
MIT License
|
|
||||||
|
|
||||||
Copyright(c) 2017-2018 Mattias Edlund
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
*/
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Globalization;
|
|
||||||
|
|
||||||
namespace MeshDecimator.Math
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// A 4D integer vector.
|
|
||||||
/// </summary>
|
|
||||||
public struct Vector4i : IEquatable<Vector4i>
|
|
||||||
{
|
|
||||||
#region Static Read-Only
|
|
||||||
/// <summary>
|
|
||||||
/// The zero vector.
|
|
||||||
/// </summary>
|
|
||||||
public static readonly Vector4i zero = new Vector4i(0, 0, 0, 0);
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Fields
|
|
||||||
/// <summary>
|
|
||||||
/// The x component.
|
|
||||||
/// </summary>
|
|
||||||
public int x;
|
|
||||||
/// <summary>
|
|
||||||
/// The y component.
|
|
||||||
/// </summary>
|
|
||||||
public int y;
|
|
||||||
/// <summary>
|
|
||||||
/// The z component.
|
|
||||||
/// </summary>
|
|
||||||
public int z;
|
|
||||||
/// <summary>
|
|
||||||
/// The w component.
|
|
||||||
/// </summary>
|
|
||||||
public int w;
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Properties
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the magnitude of this vector.
|
|
||||||
/// </summary>
|
|
||||||
public int Magnitude
|
|
||||||
{
|
|
||||||
get { return (int)System.Math.Sqrt(x * x + y * y + z * z + w * w); }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the squared magnitude of this vector.
|
|
||||||
/// </summary>
|
|
||||||
public int MagnitudeSqr
|
|
||||||
{
|
|
||||||
get { return (x * x + y * y + z * z + w * w); }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets a specific component by index in this vector.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="index">The component index.</param>
|
|
||||||
public int this[int index]
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
switch (index)
|
|
||||||
{
|
|
||||||
case 0:
|
|
||||||
return x;
|
|
||||||
case 1:
|
|
||||||
return y;
|
|
||||||
case 2:
|
|
||||||
return z;
|
|
||||||
case 3:
|
|
||||||
return w;
|
|
||||||
default:
|
|
||||||
throw new IndexOutOfRangeException("Invalid Vector4i index!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
set
|
|
||||||
{
|
|
||||||
switch (index)
|
|
||||||
{
|
|
||||||
case 0:
|
|
||||||
x = value;
|
|
||||||
break;
|
|
||||||
case 1:
|
|
||||||
y = value;
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
z = value;
|
|
||||||
break;
|
|
||||||
case 3:
|
|
||||||
w = value;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new IndexOutOfRangeException("Invalid Vector4i index!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Constructor
|
|
||||||
/// <summary>
|
|
||||||
/// Creates a new vector with one value for all components.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="value">The value.</param>
|
|
||||||
public Vector4i(int value)
|
|
||||||
{
|
|
||||||
this.x = value;
|
|
||||||
this.y = value;
|
|
||||||
this.z = value;
|
|
||||||
this.w = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Creates a new vector.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="x">The x value.</param>
|
|
||||||
/// <param name="y">The y value.</param>
|
|
||||||
/// <param name="z">The z value.</param>
|
|
||||||
/// <param name="w">The w value.</param>
|
|
||||||
public Vector4i(int x, int y, int z, int w)
|
|
||||||
{
|
|
||||||
this.x = x;
|
|
||||||
this.y = y;
|
|
||||||
this.z = z;
|
|
||||||
this.w = w;
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Operators
|
|
||||||
/// <summary>
|
|
||||||
/// Adds two vectors.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="a">The first vector.</param>
|
|
||||||
/// <param name="b">The second vector.</param>
|
|
||||||
/// <returns>The resulting vector.</returns>
|
|
||||||
public static Vector4i operator +(Vector4i a, Vector4i b)
|
|
||||||
{
|
|
||||||
return new Vector4i(a.x + b.x, a.y + b.y, a.z + b.z, a.w + b.w);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Subtracts two vectors.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="a">The first vector.</param>
|
|
||||||
/// <param name="b">The second vector.</param>
|
|
||||||
/// <returns>The resulting vector.</returns>
|
|
||||||
public static Vector4i operator -(Vector4i a, Vector4i b)
|
|
||||||
{
|
|
||||||
return new Vector4i(a.x - b.x, a.y - b.y, a.z - b.z, a.w - b.w);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Scales the vector uniformly.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="a">The vector.</param>
|
|
||||||
/// <param name="d">The scaling value.</param>
|
|
||||||
/// <returns>The resulting vector.</returns>
|
|
||||||
public static Vector4i operator *(Vector4i a, int d)
|
|
||||||
{
|
|
||||||
return new Vector4i(a.x * d, a.y * d, a.z * d, a.w * d);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Scales the vector uniformly.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="d">The scaling value.</param>
|
|
||||||
/// <param name="a">The vector.</param>
|
|
||||||
/// <returns>The resulting vector.</returns>
|
|
||||||
public static Vector4i operator *(int d, Vector4i a)
|
|
||||||
{
|
|
||||||
return new Vector4i(a.x * d, a.y * d, a.z * d, a.w * d);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Divides the vector with a float.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="a">The vector.</param>
|
|
||||||
/// <param name="d">The dividing float value.</param>
|
|
||||||
/// <returns>The resulting vector.</returns>
|
|
||||||
public static Vector4i operator /(Vector4i a, int d)
|
|
||||||
{
|
|
||||||
return new Vector4i(a.x / d, a.y / d, a.z / d, a.w / d);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Subtracts the vector from a zero vector.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="a">The vector.</param>
|
|
||||||
/// <returns>The resulting vector.</returns>
|
|
||||||
public static Vector4i operator -(Vector4i a)
|
|
||||||
{
|
|
||||||
return new Vector4i(-a.x, -a.y, -a.z, -a.w);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns if two vectors equals eachother.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="lhs">The left hand side vector.</param>
|
|
||||||
/// <param name="rhs">The right hand side vector.</param>
|
|
||||||
/// <returns>If equals.</returns>
|
|
||||||
public static bool operator ==(Vector4i lhs, Vector4i rhs)
|
|
||||||
{
|
|
||||||
return (lhs.x == rhs.x && lhs.y == rhs.y && lhs.z == rhs.z && lhs.w == rhs.w);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns if two vectors don't equal eachother.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="lhs">The left hand side vector.</param>
|
|
||||||
/// <param name="rhs">The right hand side vector.</param>
|
|
||||||
/// <returns>If not equals.</returns>
|
|
||||||
public static bool operator !=(Vector4i lhs, Vector4i rhs)
|
|
||||||
{
|
|
||||||
return (lhs.x != rhs.x || lhs.y != rhs.y || lhs.z != rhs.z || lhs.w != rhs.w);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Explicitly converts from a single-precision vector into an integer vector.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="v">The single-precision vector.</param>
|
|
||||||
public static explicit operator Vector4i(Vector4 v)
|
|
||||||
{
|
|
||||||
return new Vector4i((int)v.x, (int)v.y, (int)v.z, (int)v.w);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Explicitly converts from a double-precision vector into an integer vector.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="v">The double-precision vector.</param>
|
|
||||||
public static explicit operator Vector4i(Vector4d v)
|
|
||||||
{
|
|
||||||
return new Vector4i((int)v.x, (int)v.y, (int)v.z, (int)v.w);
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Public Methods
|
|
||||||
#region Instance
|
|
||||||
/// <summary>
|
|
||||||
/// Set x, y and z components of an existing vector.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="x">The x value.</param>
|
|
||||||
/// <param name="y">The y value.</param>
|
|
||||||
/// <param name="z">The z value.</param>
|
|
||||||
/// <param name="w">The w value.</param>
|
|
||||||
public void Set(int x, int y, int z, int w)
|
|
||||||
{
|
|
||||||
this.x = x;
|
|
||||||
this.y = y;
|
|
||||||
this.z = z;
|
|
||||||
this.w = w;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Multiplies with another vector component-wise.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="scale">The vector to multiply with.</param>
|
|
||||||
public void Scale(ref Vector4i scale)
|
|
||||||
{
|
|
||||||
x *= scale.x;
|
|
||||||
y *= scale.y;
|
|
||||||
z *= scale.z;
|
|
||||||
w *= scale.w;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Clamps this vector between a specific range.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="min">The minimum component value.</param>
|
|
||||||
/// <param name="max">The maximum component value.</param>
|
|
||||||
public void Clamp(int min, int max)
|
|
||||||
{
|
|
||||||
if (x < min) x = min;
|
|
||||||
else if (x > max) x = max;
|
|
||||||
|
|
||||||
if (y < min) y = min;
|
|
||||||
else if (y > max) y = max;
|
|
||||||
|
|
||||||
if (z < min) z = min;
|
|
||||||
else if (z > max) z = max;
|
|
||||||
|
|
||||||
if (w < min) w = min;
|
|
||||||
else if (w > max) w = max;
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Object
|
|
||||||
/// <summary>
|
|
||||||
/// Returns a hash code for this vector.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>The hash code.</returns>
|
|
||||||
public override int GetHashCode()
|
|
||||||
{
|
|
||||||
return x.GetHashCode() ^ y.GetHashCode() << 2 ^ z.GetHashCode() >> 2 ^ w.GetHashCode() >> 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns if this vector is equal to another one.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="other">The other vector to compare to.</param>
|
|
||||||
/// <returns>If equals.</returns>
|
|
||||||
public override bool Equals(object other)
|
|
||||||
{
|
|
||||||
if (!(other is Vector4i))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
Vector4i vector = (Vector4i)other;
|
|
||||||
return (x == vector.x && y == vector.y && z == vector.z && w == vector.w);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns if this vector is equal to another one.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="other">The other vector to compare to.</param>
|
|
||||||
/// <returns>If equals.</returns>
|
|
||||||
public bool Equals(Vector4i other)
|
|
||||||
{
|
|
||||||
return (x == other.x && y == other.y && z == other.z && w == other.w);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns a nicely formatted string for this vector.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>The string.</returns>
|
|
||||||
public override string ToString()
|
|
||||||
{
|
|
||||||
return string.Format("({0}, {1}, {2}, {3})",
|
|
||||||
x.ToString(CultureInfo.InvariantCulture),
|
|
||||||
y.ToString(CultureInfo.InvariantCulture),
|
|
||||||
z.ToString(CultureInfo.InvariantCulture),
|
|
||||||
w.ToString(CultureInfo.InvariantCulture));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns a nicely formatted string for this vector.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="format">The integer format.</param>
|
|
||||||
/// <returns>The string.</returns>
|
|
||||||
public string ToString(string format)
|
|
||||||
{
|
|
||||||
return string.Format("({0}, {1}, {2}, {3})",
|
|
||||||
x.ToString(format, CultureInfo.InvariantCulture),
|
|
||||||
y.ToString(format, CultureInfo.InvariantCulture),
|
|
||||||
z.ToString(format, CultureInfo.InvariantCulture),
|
|
||||||
w.ToString(format, CultureInfo.InvariantCulture));
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Static
|
|
||||||
/// <summary>
|
|
||||||
/// Multiplies two vectors component-wise.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="a">The first vector.</param>
|
|
||||||
/// <param name="b">The second vector.</param>
|
|
||||||
/// <param name="result">The resulting vector.</param>
|
|
||||||
public static void Scale(ref Vector4i a, ref Vector4i b, out Vector4i result)
|
|
||||||
{
|
|
||||||
result = new Vector4i(a.x * b.x, a.y * b.y, a.z * b.z, a.w * b.w);
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
}
|
|
||||||
955
LightlessSync/ThirdParty/MeshDecimator/Mesh.cs
vendored
955
LightlessSync/ThirdParty/MeshDecimator/Mesh.cs
vendored
@@ -1,955 +0,0 @@
|
|||||||
#region License
|
|
||||||
/*
|
|
||||||
MIT License
|
|
||||||
|
|
||||||
Copyright(c) 2017-2018 Mattias Edlund
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
*/
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using MeshDecimator.Math;
|
|
||||||
|
|
||||||
namespace MeshDecimator
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// A mesh.
|
|
||||||
/// </summary>
|
|
||||||
public sealed class Mesh
|
|
||||||
{
|
|
||||||
#region Consts
|
|
||||||
/// <summary>
|
|
||||||
/// The count of supported UV channels.
|
|
||||||
/// </summary>
|
|
||||||
public const int UVChannelCount = 4;
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Fields
|
|
||||||
private Vector3d[] vertices = null;
|
|
||||||
private int[][] indices = null;
|
|
||||||
private Vector3[] normals = null;
|
|
||||||
private Vector4[] tangents = null;
|
|
||||||
private Vector2[][] uvs2D = null;
|
|
||||||
private Vector3[][] uvs3D = null;
|
|
||||||
private Vector4[][] uvs4D = null;
|
|
||||||
private Vector4[] colors = null;
|
|
||||||
private BoneWeight[] boneWeights = null;
|
|
||||||
|
|
||||||
private static readonly int[] emptyIndices = new int[0];
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Properties
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the count of vertices of this mesh.
|
|
||||||
/// </summary>
|
|
||||||
public int VertexCount
|
|
||||||
{
|
|
||||||
get { return vertices.Length; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the count of submeshes in this mesh.
|
|
||||||
/// </summary>
|
|
||||||
public int SubMeshCount
|
|
||||||
{
|
|
||||||
get { return indices.Length; }
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if (value <= 0)
|
|
||||||
throw new ArgumentOutOfRangeException("value");
|
|
||||||
|
|
||||||
int[][] newIndices = new int[value][];
|
|
||||||
Array.Copy(indices, 0, newIndices, 0, MathHelper.Min(indices.Length, newIndices.Length));
|
|
||||||
indices = newIndices;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the total count of triangles in this mesh.
|
|
||||||
/// </summary>
|
|
||||||
public int TriangleCount
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
int triangleCount = 0;
|
|
||||||
for (int i = 0; i < indices.Length; i++)
|
|
||||||
{
|
|
||||||
if (indices[i] != null)
|
|
||||||
{
|
|
||||||
triangleCount += indices[i].Length / 3;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return triangleCount;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the vertices for this mesh. Note that this resets all other vertex attributes.
|
|
||||||
/// </summary>
|
|
||||||
public Vector3d[] Vertices
|
|
||||||
{
|
|
||||||
get { return vertices; }
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if (value == null)
|
|
||||||
throw new ArgumentNullException("value");
|
|
||||||
|
|
||||||
vertices = value;
|
|
||||||
ClearVertexAttributes();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the combined indices for this mesh. Once set, the sub-mesh count gets set to 1.
|
|
||||||
/// </summary>
|
|
||||||
public int[] Indices
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (indices.Length == 1)
|
|
||||||
{
|
|
||||||
return indices[0] ?? emptyIndices;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
List<int> indexList = new List<int>(TriangleCount * 3);
|
|
||||||
for (int i = 0; i < indices.Length; i++)
|
|
||||||
{
|
|
||||||
if (indices[i] != null)
|
|
||||||
{
|
|
||||||
indexList.AddRange(indices[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return indexList.ToArray();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if (value == null)
|
|
||||||
throw new ArgumentNullException("value");
|
|
||||||
else if ((value.Length % 3) != 0)
|
|
||||||
throw new ArgumentException("The index count must be multiple by 3.", "value");
|
|
||||||
|
|
||||||
SubMeshCount = 1;
|
|
||||||
SetIndices(0, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the normals for this mesh.
|
|
||||||
/// </summary>
|
|
||||||
public Vector3[] Normals
|
|
||||||
{
|
|
||||||
get { return normals; }
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if (value != null && value.Length != vertices.Length)
|
|
||||||
throw new ArgumentException(string.Format("The vertex normals must be as many as the vertices. Assigned: {0} Require: {1}", value.Length, vertices.Length));
|
|
||||||
|
|
||||||
normals = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the tangents for this mesh.
|
|
||||||
/// </summary>
|
|
||||||
public Vector4[] Tangents
|
|
||||||
{
|
|
||||||
get { return tangents; }
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if (value != null && value.Length != vertices.Length)
|
|
||||||
throw new ArgumentException(string.Format("The vertex tangents must be as many as the vertices. Assigned: {0} Require: {1}", value.Length, vertices.Length));
|
|
||||||
|
|
||||||
tangents = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the first UV set for this mesh.
|
|
||||||
/// </summary>
|
|
||||||
public Vector2[] UV1
|
|
||||||
{
|
|
||||||
get { return GetUVs2D(0); }
|
|
||||||
set { SetUVs(0, value); }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the second UV set for this mesh.
|
|
||||||
/// </summary>
|
|
||||||
public Vector2[] UV2
|
|
||||||
{
|
|
||||||
get { return GetUVs2D(1); }
|
|
||||||
set { SetUVs(1, value); }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the third UV set for this mesh.
|
|
||||||
/// </summary>
|
|
||||||
public Vector2[] UV3
|
|
||||||
{
|
|
||||||
get { return GetUVs2D(2); }
|
|
||||||
set { SetUVs(2, value); }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the fourth UV set for this mesh.
|
|
||||||
/// </summary>
|
|
||||||
public Vector2[] UV4
|
|
||||||
{
|
|
||||||
get { return GetUVs2D(3); }
|
|
||||||
set { SetUVs(3, value); }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the vertex colors for this mesh.
|
|
||||||
/// </summary>
|
|
||||||
public Vector4[] Colors
|
|
||||||
{
|
|
||||||
get { return colors; }
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if (value != null && value.Length != vertices.Length)
|
|
||||||
throw new ArgumentException(string.Format("The vertex colors must be as many as the vertices. Assigned: {0} Require: {1}", value.Length, vertices.Length));
|
|
||||||
|
|
||||||
colors = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the vertex bone weights for this mesh.
|
|
||||||
/// </summary>
|
|
||||||
public BoneWeight[] BoneWeights
|
|
||||||
{
|
|
||||||
get { return boneWeights; }
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if (value != null && value.Length != vertices.Length)
|
|
||||||
throw new ArgumentException(string.Format("The vertex bone weights must be as many as the vertices. Assigned: {0} Require: {1}", value.Length, vertices.Length));
|
|
||||||
|
|
||||||
boneWeights = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Constructor
|
|
||||||
/// <summary>
|
|
||||||
/// Creates a new mesh.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="vertices">The mesh vertices.</param>
|
|
||||||
/// <param name="indices">The mesh indices.</param>
|
|
||||||
public Mesh(Vector3d[] vertices, int[] indices)
|
|
||||||
{
|
|
||||||
if (vertices == null)
|
|
||||||
throw new ArgumentNullException("vertices");
|
|
||||||
else if (indices == null)
|
|
||||||
throw new ArgumentNullException("indices");
|
|
||||||
else if ((indices.Length % 3) != 0)
|
|
||||||
throw new ArgumentException("The index count must be multiple by 3.", "indices");
|
|
||||||
|
|
||||||
this.vertices = vertices;
|
|
||||||
this.indices = new int[1][];
|
|
||||||
this.indices[0] = indices;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Creates a new mesh.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="vertices">The mesh vertices.</param>
|
|
||||||
/// <param name="indices">The mesh indices.</param>
|
|
||||||
public Mesh(Vector3d[] vertices, int[][] indices)
|
|
||||||
{
|
|
||||||
if (vertices == null)
|
|
||||||
throw new ArgumentNullException("vertices");
|
|
||||||
else if (indices == null)
|
|
||||||
throw new ArgumentNullException("indices");
|
|
||||||
|
|
||||||
for (int i = 0; i < indices.Length; i++)
|
|
||||||
{
|
|
||||||
if (indices[i] != null && (indices[i].Length % 3) != 0)
|
|
||||||
throw new ArgumentException(string.Format("The index count must be multiple by 3 at sub-mesh index {0}.", i), "indices");
|
|
||||||
}
|
|
||||||
|
|
||||||
this.vertices = vertices;
|
|
||||||
this.indices = indices;
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Private Methods
|
|
||||||
private void ClearVertexAttributes()
|
|
||||||
{
|
|
||||||
normals = null;
|
|
||||||
tangents = null;
|
|
||||||
uvs2D = null;
|
|
||||||
uvs3D = null;
|
|
||||||
uvs4D = null;
|
|
||||||
colors = null;
|
|
||||||
boneWeights = null;
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Public Methods
|
|
||||||
#region Recalculate Normals
|
|
||||||
/// <summary>
|
|
||||||
/// Recalculates the normals for this mesh smoothly.
|
|
||||||
/// </summary>
|
|
||||||
public void RecalculateNormals()
|
|
||||||
{
|
|
||||||
int vertexCount = vertices.Length;
|
|
||||||
Vector3[] normals = new Vector3[vertexCount];
|
|
||||||
|
|
||||||
int subMeshCount = this.indices.Length;
|
|
||||||
for (int subMeshIndex = 0; subMeshIndex < subMeshCount; subMeshIndex++)
|
|
||||||
{
|
|
||||||
int[] indices = this.indices[subMeshIndex];
|
|
||||||
if (indices == null)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
int indexCount = indices.Length;
|
|
||||||
for (int i = 0; i < indexCount; i += 3)
|
|
||||||
{
|
|
||||||
int i0 = indices[i];
|
|
||||||
int i1 = indices[i + 1];
|
|
||||||
int i2 = indices[i + 2];
|
|
||||||
|
|
||||||
var v0 = (Vector3)vertices[i0];
|
|
||||||
var v1 = (Vector3)vertices[i1];
|
|
||||||
var v2 = (Vector3)vertices[i2];
|
|
||||||
|
|
||||||
var nx = v1 - v0;
|
|
||||||
var ny = v2 - v0;
|
|
||||||
Vector3 normal;
|
|
||||||
Vector3.Cross(ref nx, ref ny, out normal);
|
|
||||||
normal.Normalize();
|
|
||||||
|
|
||||||
normals[i0] += normal;
|
|
||||||
normals[i1] += normal;
|
|
||||||
normals[i2] += normal;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = 0; i < vertexCount; i++)
|
|
||||||
{
|
|
||||||
normals[i].Normalize();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.normals = normals;
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Recalculate Tangents
|
|
||||||
/// <summary>
|
|
||||||
/// Recalculates the tangents for this mesh.
|
|
||||||
/// </summary>
|
|
||||||
public void RecalculateTangents()
|
|
||||||
{
|
|
||||||
// Make sure we have the normals first
|
|
||||||
if (normals == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
// Also make sure that we have the first UV set
|
|
||||||
bool uvIs2D = (uvs2D != null && uvs2D[0] != null);
|
|
||||||
bool uvIs3D = (uvs3D != null && uvs3D[0] != null);
|
|
||||||
bool uvIs4D = (uvs4D != null && uvs4D[0] != null);
|
|
||||||
if (!uvIs2D && !uvIs3D && !uvIs4D)
|
|
||||||
return;
|
|
||||||
|
|
||||||
int vertexCount = vertices.Length;
|
|
||||||
|
|
||||||
var tangents = new Vector4[vertexCount];
|
|
||||||
var tan1 = new Vector3[vertexCount];
|
|
||||||
var tan2 = new Vector3[vertexCount];
|
|
||||||
|
|
||||||
Vector2[] uv2D = (uvIs2D ? uvs2D[0] : null);
|
|
||||||
Vector3[] uv3D = (uvIs3D ? uvs3D[0] : null);
|
|
||||||
Vector4[] uv4D = (uvIs4D ? uvs4D[0] : null);
|
|
||||||
|
|
||||||
int subMeshCount = this.indices.Length;
|
|
||||||
for (int subMeshIndex = 0; subMeshIndex < subMeshCount; subMeshIndex++)
|
|
||||||
{
|
|
||||||
int[] indices = this.indices[subMeshIndex];
|
|
||||||
if (indices == null)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
int indexCount = indices.Length;
|
|
||||||
for (int i = 0; i < indexCount; i += 3)
|
|
||||||
{
|
|
||||||
int i0 = indices[i];
|
|
||||||
int i1 = indices[i + 1];
|
|
||||||
int i2 = indices[i + 2];
|
|
||||||
|
|
||||||
var v0 = vertices[i0];
|
|
||||||
var v1 = vertices[i1];
|
|
||||||
var v2 = vertices[i2];
|
|
||||||
|
|
||||||
float s1, s2, t1, t2;
|
|
||||||
if (uvIs2D)
|
|
||||||
{
|
|
||||||
var w0 = uv2D[i0];
|
|
||||||
var w1 = uv2D[i1];
|
|
||||||
var w2 = uv2D[i2];
|
|
||||||
s1 = w1.x - w0.x;
|
|
||||||
s2 = w2.x - w0.x;
|
|
||||||
t1 = w1.y - w0.y;
|
|
||||||
t2 = w2.y - w0.y;
|
|
||||||
}
|
|
||||||
else if (uvIs3D)
|
|
||||||
{
|
|
||||||
var w0 = uv3D[i0];
|
|
||||||
var w1 = uv3D[i1];
|
|
||||||
var w2 = uv3D[i2];
|
|
||||||
s1 = w1.x - w0.x;
|
|
||||||
s2 = w2.x - w0.x;
|
|
||||||
t1 = w1.y - w0.y;
|
|
||||||
t2 = w2.y - w0.y;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var w0 = uv4D[i0];
|
|
||||||
var w1 = uv4D[i1];
|
|
||||||
var w2 = uv4D[i2];
|
|
||||||
s1 = w1.x - w0.x;
|
|
||||||
s2 = w2.x - w0.x;
|
|
||||||
t1 = w1.y - w0.y;
|
|
||||||
t2 = w2.y - w0.y;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
float x1 = (float)(v1.x - v0.x);
|
|
||||||
float x2 = (float)(v2.x - v0.x);
|
|
||||||
float y1 = (float)(v1.y - v0.y);
|
|
||||||
float y2 = (float)(v2.y - v0.y);
|
|
||||||
float z1 = (float)(v1.z - v0.z);
|
|
||||||
float z2 = (float)(v2.z - v0.z);
|
|
||||||
float r = 1f / (s1 * t2 - s2 * t1);
|
|
||||||
|
|
||||||
var sdir = new Vector3((t2 * x1 - t1 * x2) * r, (t2 * y1 - t1 * y2) * r, (t2 * z1 - t1 * z2) * r);
|
|
||||||
var tdir = new Vector3((s1 * x2 - s2 * x1) * r, (s1 * y2 - s2 * y1) * r, (s1 * z2 - s2 * z1) * r);
|
|
||||||
|
|
||||||
tan1[i0] += sdir;
|
|
||||||
tan1[i1] += sdir;
|
|
||||||
tan1[i2] += sdir;
|
|
||||||
tan2[i0] += tdir;
|
|
||||||
tan2[i1] += tdir;
|
|
||||||
tan2[i2] += tdir;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = 0; i < vertexCount; i++)
|
|
||||||
{
|
|
||||||
var n = normals[i];
|
|
||||||
var t = tan1[i];
|
|
||||||
|
|
||||||
var tmp = (t - n * Vector3.Dot(ref n, ref t));
|
|
||||||
tmp.Normalize();
|
|
||||||
|
|
||||||
Vector3 c;
|
|
||||||
Vector3.Cross(ref n, ref t, out c);
|
|
||||||
float dot = Vector3.Dot(ref c, ref tan2[i]);
|
|
||||||
float w = (dot < 0f ? -1f : 1f);
|
|
||||||
tangents[i] = new Vector4(tmp.x, tmp.y, tmp.z, w);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.tangents = tangents;
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Triangles
|
|
||||||
/// <summary>
|
|
||||||
/// Returns the count of triangles for a specific sub-mesh in this mesh.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="subMeshIndex">The sub-mesh index.</param>
|
|
||||||
/// <returns>The triangle count.</returns>
|
|
||||||
public int GetTriangleCount(int subMeshIndex)
|
|
||||||
{
|
|
||||||
if (subMeshIndex < 0 || subMeshIndex >= indices.Length)
|
|
||||||
throw new IndexOutOfRangeException();
|
|
||||||
|
|
||||||
return indices[subMeshIndex].Length / 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns the triangle indices of a specific sub-mesh in this mesh.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="subMeshIndex">The sub-mesh index.</param>
|
|
||||||
/// <returns>The triangle indices.</returns>
|
|
||||||
public int[] GetIndices(int subMeshIndex)
|
|
||||||
{
|
|
||||||
if (subMeshIndex < 0 || subMeshIndex >= indices.Length)
|
|
||||||
throw new IndexOutOfRangeException();
|
|
||||||
|
|
||||||
return indices[subMeshIndex] ?? emptyIndices;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns the triangle indices for all sub-meshes in this mesh.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>The sub-mesh triangle indices.</returns>
|
|
||||||
public int[][] GetSubMeshIndices()
|
|
||||||
{
|
|
||||||
var subMeshIndices = new int[indices.Length][];
|
|
||||||
for (int subMeshIndex = 0; subMeshIndex < indices.Length; subMeshIndex++)
|
|
||||||
{
|
|
||||||
subMeshIndices[subMeshIndex] = indices[subMeshIndex] ?? emptyIndices;
|
|
||||||
}
|
|
||||||
return subMeshIndices;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Sets the triangle indices of a specific sub-mesh in this mesh.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="subMeshIndex">The sub-mesh index.</param>
|
|
||||||
/// <param name="indices">The triangle indices.</param>
|
|
||||||
public void SetIndices(int subMeshIndex, int[] indices)
|
|
||||||
{
|
|
||||||
if (subMeshIndex < 0 || subMeshIndex >= this.indices.Length)
|
|
||||||
throw new IndexOutOfRangeException();
|
|
||||||
else if (indices == null)
|
|
||||||
throw new ArgumentNullException("indices");
|
|
||||||
else if ((indices.Length % 3) != 0)
|
|
||||||
throw new ArgumentException("The index count must be multiple by 3.", "indices");
|
|
||||||
|
|
||||||
this.indices[subMeshIndex] = indices;
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region UV Sets
|
|
||||||
#region Getting
|
|
||||||
/// <summary>
|
|
||||||
/// Returns the UV dimension for a specific channel.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="channel"></param>
|
|
||||||
/// <returns>The UV dimension count.</returns>
|
|
||||||
public int GetUVDimension(int channel)
|
|
||||||
{
|
|
||||||
if (channel < 0 || channel >= UVChannelCount)
|
|
||||||
throw new ArgumentOutOfRangeException("channel");
|
|
||||||
|
|
||||||
if (uvs2D != null && uvs2D[channel] != null)
|
|
||||||
{
|
|
||||||
return 2;
|
|
||||||
}
|
|
||||||
else if (uvs3D != null && uvs3D[channel] != null)
|
|
||||||
{
|
|
||||||
return 3;
|
|
||||||
}
|
|
||||||
else if (uvs4D != null && uvs4D[channel] != null)
|
|
||||||
{
|
|
||||||
return 4;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns the UVs (2D) from a specific channel.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="channel">The channel index.</param>
|
|
||||||
/// <returns>The UVs.</returns>
|
|
||||||
public Vector2[] GetUVs2D(int channel)
|
|
||||||
{
|
|
||||||
if (channel < 0 || channel >= UVChannelCount)
|
|
||||||
throw new ArgumentOutOfRangeException("channel");
|
|
||||||
|
|
||||||
if (uvs2D != null && uvs2D[channel] != null)
|
|
||||||
{
|
|
||||||
return uvs2D[channel];
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns the UVs (3D) from a specific channel.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="channel">The channel index.</param>
|
|
||||||
/// <returns>The UVs.</returns>
|
|
||||||
public Vector3[] GetUVs3D(int channel)
|
|
||||||
{
|
|
||||||
if (channel < 0 || channel >= UVChannelCount)
|
|
||||||
throw new ArgumentOutOfRangeException("channel");
|
|
||||||
|
|
||||||
if (uvs3D != null && uvs3D[channel] != null)
|
|
||||||
{
|
|
||||||
return uvs3D[channel];
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns the UVs (4D) from a specific channel.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="channel">The channel index.</param>
|
|
||||||
/// <returns>The UVs.</returns>
|
|
||||||
public Vector4[] GetUVs4D(int channel)
|
|
||||||
{
|
|
||||||
if (channel < 0 || channel >= UVChannelCount)
|
|
||||||
throw new ArgumentOutOfRangeException("channel");
|
|
||||||
|
|
||||||
if (uvs4D != null && uvs4D[channel] != null)
|
|
||||||
{
|
|
||||||
return uvs4D[channel];
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns the UVs (2D) from a specific channel.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="channel">The channel index.</param>
|
|
||||||
/// <param name="uvs">The UVs.</param>
|
|
||||||
public void GetUVs(int channel, List<Vector2> uvs)
|
|
||||||
{
|
|
||||||
if (channel < 0 || channel >= UVChannelCount)
|
|
||||||
throw new ArgumentOutOfRangeException("channel");
|
|
||||||
else if (uvs == null)
|
|
||||||
throw new ArgumentNullException("uvs");
|
|
||||||
|
|
||||||
uvs.Clear();
|
|
||||||
if (uvs2D != null && uvs2D[channel] != null)
|
|
||||||
{
|
|
||||||
var uvData = uvs2D[channel];
|
|
||||||
if (uvData != null)
|
|
||||||
{
|
|
||||||
uvs.AddRange(uvData);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns the UVs (3D) from a specific channel.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="channel">The channel index.</param>
|
|
||||||
/// <param name="uvs">The UVs.</param>
|
|
||||||
public void GetUVs(int channel, List<Vector3> uvs)
|
|
||||||
{
|
|
||||||
if (channel < 0 || channel >= UVChannelCount)
|
|
||||||
throw new ArgumentOutOfRangeException("channel");
|
|
||||||
else if (uvs == null)
|
|
||||||
throw new ArgumentNullException("uvs");
|
|
||||||
|
|
||||||
uvs.Clear();
|
|
||||||
if (uvs3D != null && uvs3D[channel] != null)
|
|
||||||
{
|
|
||||||
var uvData = uvs3D[channel];
|
|
||||||
if (uvData != null)
|
|
||||||
{
|
|
||||||
uvs.AddRange(uvData);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns the UVs (4D) from a specific channel.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="channel">The channel index.</param>
|
|
||||||
/// <param name="uvs">The UVs.</param>
|
|
||||||
public void GetUVs(int channel, List<Vector4> uvs)
|
|
||||||
{
|
|
||||||
if (channel < 0 || channel >= UVChannelCount)
|
|
||||||
throw new ArgumentOutOfRangeException("channel");
|
|
||||||
else if (uvs == null)
|
|
||||||
throw new ArgumentNullException("uvs");
|
|
||||||
|
|
||||||
uvs.Clear();
|
|
||||||
if (uvs4D != null && uvs4D[channel] != null)
|
|
||||||
{
|
|
||||||
var uvData = uvs4D[channel];
|
|
||||||
if (uvData != null)
|
|
||||||
{
|
|
||||||
uvs.AddRange(uvData);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Setting
|
|
||||||
/// <summary>
|
|
||||||
/// Sets the UVs (2D) for a specific channel.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="channel">The channel index.</param>
|
|
||||||
/// <param name="uvs">The UVs.</param>
|
|
||||||
public void SetUVs(int channel, Vector2[] uvs)
|
|
||||||
{
|
|
||||||
if (channel < 0 || channel >= UVChannelCount)
|
|
||||||
throw new ArgumentOutOfRangeException("channel");
|
|
||||||
|
|
||||||
if (uvs != null && uvs.Length > 0)
|
|
||||||
{
|
|
||||||
if (uvs.Length != vertices.Length)
|
|
||||||
throw new ArgumentException(string.Format("The vertex UVs must be as many as the vertices. Assigned: {0} Require: {1}", uvs.Length, vertices.Length));
|
|
||||||
|
|
||||||
if (uvs2D == null)
|
|
||||||
uvs2D = new Vector2[UVChannelCount][];
|
|
||||||
|
|
||||||
int uvCount = uvs.Length;
|
|
||||||
var uvSet = new Vector2[uvCount];
|
|
||||||
uvs2D[channel] = uvSet;
|
|
||||||
uvs.CopyTo(uvSet, 0);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (uvs2D != null)
|
|
||||||
{
|
|
||||||
uvs2D[channel] = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (uvs3D != null)
|
|
||||||
{
|
|
||||||
uvs3D[channel] = null;
|
|
||||||
}
|
|
||||||
if (uvs4D != null)
|
|
||||||
{
|
|
||||||
uvs4D[channel] = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Sets the UVs (3D) for a specific channel.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="channel">The channel index.</param>
|
|
||||||
/// <param name="uvs">The UVs.</param>
|
|
||||||
public void SetUVs(int channel, Vector3[] uvs)
|
|
||||||
{
|
|
||||||
if (channel < 0 || channel >= UVChannelCount)
|
|
||||||
throw new ArgumentOutOfRangeException("channel");
|
|
||||||
|
|
||||||
if (uvs != null && uvs.Length > 0)
|
|
||||||
{
|
|
||||||
int uvCount = uvs.Length;
|
|
||||||
if (uvCount != vertices.Length)
|
|
||||||
throw new ArgumentException(string.Format("The vertex UVs must be as many as the vertices. Assigned: {0} Require: {1}", uvCount, vertices.Length), "uvs");
|
|
||||||
|
|
||||||
if (uvs3D == null)
|
|
||||||
uvs3D = new Vector3[UVChannelCount][];
|
|
||||||
|
|
||||||
var uvSet = new Vector3[uvCount];
|
|
||||||
uvs3D[channel] = uvSet;
|
|
||||||
uvs.CopyTo(uvSet, 0);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (uvs3D != null)
|
|
||||||
{
|
|
||||||
uvs3D[channel] = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (uvs2D != null)
|
|
||||||
{
|
|
||||||
uvs2D[channel] = null;
|
|
||||||
}
|
|
||||||
if (uvs4D != null)
|
|
||||||
{
|
|
||||||
uvs4D[channel] = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Sets the UVs (4D) for a specific channel.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="channel">The channel index.</param>
|
|
||||||
/// <param name="uvs">The UVs.</param>
|
|
||||||
public void SetUVs(int channel, Vector4[] uvs)
|
|
||||||
{
|
|
||||||
if (channel < 0 || channel >= UVChannelCount)
|
|
||||||
throw new ArgumentOutOfRangeException("channel");
|
|
||||||
|
|
||||||
if (uvs != null && uvs.Length > 0)
|
|
||||||
{
|
|
||||||
int uvCount = uvs.Length;
|
|
||||||
if (uvCount != vertices.Length)
|
|
||||||
throw new ArgumentException(string.Format("The vertex UVs must be as many as the vertices. Assigned: {0} Require: {1}", uvCount, vertices.Length), "uvs");
|
|
||||||
|
|
||||||
if (uvs4D == null)
|
|
||||||
uvs4D = new Vector4[UVChannelCount][];
|
|
||||||
|
|
||||||
var uvSet = new Vector4[uvCount];
|
|
||||||
uvs4D[channel] = uvSet;
|
|
||||||
uvs.CopyTo(uvSet, 0);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (uvs4D != null)
|
|
||||||
{
|
|
||||||
uvs4D[channel] = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (uvs2D != null)
|
|
||||||
{
|
|
||||||
uvs2D[channel] = null;
|
|
||||||
}
|
|
||||||
if (uvs3D != null)
|
|
||||||
{
|
|
||||||
uvs3D[channel] = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Sets the UVs (2D) for a specific channel.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="channel">The channel index.</param>
|
|
||||||
/// <param name="uvs">The UVs.</param>
|
|
||||||
public void SetUVs(int channel, List<Vector2> uvs)
|
|
||||||
{
|
|
||||||
if (channel < 0 || channel >= UVChannelCount)
|
|
||||||
throw new ArgumentOutOfRangeException("channel");
|
|
||||||
|
|
||||||
if (uvs != null && uvs.Count > 0)
|
|
||||||
{
|
|
||||||
int uvCount = uvs.Count;
|
|
||||||
if (uvCount != vertices.Length)
|
|
||||||
throw new ArgumentException(string.Format("The vertex UVs must be as many as the vertices. Assigned: {0} Require: {1}", uvCount, vertices.Length), "uvs");
|
|
||||||
|
|
||||||
if (uvs2D == null)
|
|
||||||
uvs2D = new Vector2[UVChannelCount][];
|
|
||||||
|
|
||||||
var uvSet = new Vector2[uvCount];
|
|
||||||
uvs2D[channel] = uvSet;
|
|
||||||
uvs.CopyTo(uvSet, 0);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (uvs2D != null)
|
|
||||||
{
|
|
||||||
uvs2D[channel] = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (uvs3D != null)
|
|
||||||
{
|
|
||||||
uvs3D[channel] = null;
|
|
||||||
}
|
|
||||||
if (uvs4D != null)
|
|
||||||
{
|
|
||||||
uvs4D[channel] = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Sets the UVs (3D) for a specific channel.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="channel">The channel index.</param>
|
|
||||||
/// <param name="uvs">The UVs.</param>
|
|
||||||
public void SetUVs(int channel, List<Vector3> uvs)
|
|
||||||
{
|
|
||||||
if (channel < 0 || channel >= UVChannelCount)
|
|
||||||
throw new ArgumentOutOfRangeException("channel");
|
|
||||||
|
|
||||||
if (uvs != null && uvs.Count > 0)
|
|
||||||
{
|
|
||||||
int uvCount = uvs.Count;
|
|
||||||
if (uvCount != vertices.Length)
|
|
||||||
throw new ArgumentException(string.Format("The vertex UVs must be as many as the vertices. Assigned: {0} Require: {1}", uvCount, vertices.Length), "uvs");
|
|
||||||
|
|
||||||
if (uvs3D == null)
|
|
||||||
uvs3D = new Vector3[UVChannelCount][];
|
|
||||||
|
|
||||||
var uvSet = new Vector3[uvCount];
|
|
||||||
uvs3D[channel] = uvSet;
|
|
||||||
uvs.CopyTo(uvSet, 0);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (uvs3D != null)
|
|
||||||
{
|
|
||||||
uvs3D[channel] = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (uvs2D != null)
|
|
||||||
{
|
|
||||||
uvs2D[channel] = null;
|
|
||||||
}
|
|
||||||
if (uvs4D != null)
|
|
||||||
{
|
|
||||||
uvs4D[channel] = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Sets the UVs (4D) for a specific channel.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="channel">The channel index.</param>
|
|
||||||
/// <param name="uvs">The UVs.</param>
|
|
||||||
public void SetUVs(int channel, List<Vector4> uvs)
|
|
||||||
{
|
|
||||||
if (channel < 0 || channel >= UVChannelCount)
|
|
||||||
throw new ArgumentOutOfRangeException("channel");
|
|
||||||
|
|
||||||
if (uvs != null && uvs.Count > 0)
|
|
||||||
{
|
|
||||||
int uvCount = uvs.Count;
|
|
||||||
if (uvCount != vertices.Length)
|
|
||||||
throw new ArgumentException(string.Format("The vertex UVs must be as many as the vertices. Assigned: {0} Require: {1}", uvCount, vertices.Length), "uvs");
|
|
||||||
|
|
||||||
if (uvs4D == null)
|
|
||||||
uvs4D = new Vector4[UVChannelCount][];
|
|
||||||
|
|
||||||
var uvSet = new Vector4[uvCount];
|
|
||||||
uvs4D[channel] = uvSet;
|
|
||||||
uvs.CopyTo(uvSet, 0);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (uvs4D != null)
|
|
||||||
{
|
|
||||||
uvs4D[channel] = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (uvs2D != null)
|
|
||||||
{
|
|
||||||
uvs2D[channel] = null;
|
|
||||||
}
|
|
||||||
if (uvs3D != null)
|
|
||||||
{
|
|
||||||
uvs3D[channel] = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region To String
|
|
||||||
/// <summary>
|
|
||||||
/// Returns the text-representation of this mesh.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>The text-representation.</returns>
|
|
||||||
public override string ToString()
|
|
||||||
{
|
|
||||||
return string.Format("Vertices: {0}", vertices.Length);
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,180 +0,0 @@
|
|||||||
#region License
|
|
||||||
/*
|
|
||||||
MIT License
|
|
||||||
|
|
||||||
Copyright(c) 2017-2018 Mattias Edlund
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
*/
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using MeshDecimator.Algorithms;
|
|
||||||
|
|
||||||
namespace MeshDecimator
|
|
||||||
{
|
|
||||||
#region Algorithm
|
|
||||||
/// <summary>
|
|
||||||
/// The decimation algorithms.
|
|
||||||
/// </summary>
|
|
||||||
public enum Algorithm
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The default algorithm.
|
|
||||||
/// </summary>
|
|
||||||
Default,
|
|
||||||
/// <summary>
|
|
||||||
/// The fast quadric mesh simplification algorithm.
|
|
||||||
/// </summary>
|
|
||||||
FastQuadricMesh
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The mesh decimation API.
|
|
||||||
/// </summary>
|
|
||||||
public static class MeshDecimation
|
|
||||||
{
|
|
||||||
#region Public Methods
|
|
||||||
#region Create Algorithm
|
|
||||||
/// <summary>
|
|
||||||
/// Creates a specific decimation algorithm.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="algorithm">The desired algorithm.</param>
|
|
||||||
/// <returns>The decimation algorithm.</returns>
|
|
||||||
public static DecimationAlgorithm CreateAlgorithm(Algorithm algorithm)
|
|
||||||
{
|
|
||||||
DecimationAlgorithm alg = null;
|
|
||||||
|
|
||||||
switch (algorithm)
|
|
||||||
{
|
|
||||||
case Algorithm.Default:
|
|
||||||
case Algorithm.FastQuadricMesh:
|
|
||||||
alg = new FastQuadricMeshSimplification();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new ArgumentException("The specified algorithm is not supported.", "algorithm");
|
|
||||||
}
|
|
||||||
|
|
||||||
return alg;
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Decimate Mesh
|
|
||||||
/// <summary>
|
|
||||||
/// Decimates a mesh.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="mesh">The mesh to decimate.</param>
|
|
||||||
/// <param name="targetTriangleCount">The target triangle count.</param>
|
|
||||||
/// <returns>The decimated mesh.</returns>
|
|
||||||
public static Mesh DecimateMesh(Mesh mesh, int targetTriangleCount)
|
|
||||||
{
|
|
||||||
return DecimateMesh(Algorithm.Default, mesh, targetTriangleCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Decimates a mesh.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="algorithm">The desired algorithm.</param>
|
|
||||||
/// <param name="mesh">The mesh to decimate.</param>
|
|
||||||
/// <param name="targetTriangleCount">The target triangle count.</param>
|
|
||||||
/// <returns>The decimated mesh.</returns>
|
|
||||||
public static Mesh DecimateMesh(Algorithm algorithm, Mesh mesh, int targetTriangleCount)
|
|
||||||
{
|
|
||||||
if (mesh == null)
|
|
||||||
throw new ArgumentNullException("mesh");
|
|
||||||
|
|
||||||
var decimationAlgorithm = CreateAlgorithm(algorithm);
|
|
||||||
return DecimateMesh(decimationAlgorithm, mesh, targetTriangleCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Decimates a mesh.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="algorithm">The decimation algorithm.</param>
|
|
||||||
/// <param name="mesh">The mesh to decimate.</param>
|
|
||||||
/// <param name="targetTriangleCount">The target triangle count.</param>
|
|
||||||
/// <returns>The decimated mesh.</returns>
|
|
||||||
public static Mesh DecimateMesh(DecimationAlgorithm algorithm, Mesh mesh, int targetTriangleCount)
|
|
||||||
{
|
|
||||||
if (algorithm == null)
|
|
||||||
throw new ArgumentNullException("algorithm");
|
|
||||||
else if (mesh == null)
|
|
||||||
throw new ArgumentNullException("mesh");
|
|
||||||
|
|
||||||
int currentTriangleCount = mesh.TriangleCount;
|
|
||||||
if (targetTriangleCount > currentTriangleCount)
|
|
||||||
targetTriangleCount = currentTriangleCount;
|
|
||||||
else if (targetTriangleCount < 0)
|
|
||||||
targetTriangleCount = 0;
|
|
||||||
|
|
||||||
algorithm.Initialize(mesh);
|
|
||||||
algorithm.DecimateMesh(targetTriangleCount);
|
|
||||||
return algorithm.ToMesh();
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Decimate Mesh Lossless
|
|
||||||
/// <summary>
|
|
||||||
/// Decimates a mesh without losing any quality.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="mesh">The mesh to decimate.</param>
|
|
||||||
/// <returns>The decimated mesh.</returns>
|
|
||||||
public static Mesh DecimateMeshLossless(Mesh mesh)
|
|
||||||
{
|
|
||||||
return DecimateMeshLossless(Algorithm.Default, mesh);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Decimates a mesh without losing any quality.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="algorithm">The desired algorithm.</param>
|
|
||||||
/// <param name="mesh">The mesh to decimate.</param>
|
|
||||||
/// <returns>The decimated mesh.</returns>
|
|
||||||
public static Mesh DecimateMeshLossless(Algorithm algorithm, Mesh mesh)
|
|
||||||
{
|
|
||||||
if (mesh == null)
|
|
||||||
throw new ArgumentNullException("mesh");
|
|
||||||
|
|
||||||
var decimationAlgorithm = CreateAlgorithm(algorithm);
|
|
||||||
return DecimateMeshLossless(decimationAlgorithm, mesh);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Decimates a mesh without losing any quality.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="algorithm">The decimation algorithm.</param>
|
|
||||||
/// <param name="mesh">The mesh to decimate.</param>
|
|
||||||
/// <returns>The decimated mesh.</returns>
|
|
||||||
public static Mesh DecimateMeshLossless(DecimationAlgorithm algorithm, Mesh mesh)
|
|
||||||
{
|
|
||||||
if (algorithm == null)
|
|
||||||
throw new ArgumentNullException("algorithm");
|
|
||||||
else if (mesh == null)
|
|
||||||
throw new ArgumentNullException("mesh");
|
|
||||||
|
|
||||||
int currentTriangleCount = mesh.TriangleCount;
|
|
||||||
algorithm.Initialize(mesh);
|
|
||||||
algorithm.DecimateMeshLossless();
|
|
||||||
return algorithm.ToMesh();
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
}
|
|
||||||
1325
LightlessSync/ThirdParty/Nanomesh/Algo/Decimation/Decimate.cs
vendored
Normal file
1325
LightlessSync/ThirdParty/Nanomesh/Algo/Decimation/Decimate.cs
vendored
Normal file
File diff suppressed because it is too large
Load Diff
88
LightlessSync/ThirdParty/Nanomesh/Algo/Decimation/EdgeCollapse.cs
vendored
Normal file
88
LightlessSync/ThirdParty/Nanomesh/Algo/Decimation/EdgeCollapse.cs
vendored
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Nanomesh
|
||||||
|
{
|
||||||
|
public partial class DecimateModifier
|
||||||
|
{
|
||||||
|
public class EdgeCollapse : IComparable<EdgeCollapse>, IEquatable<EdgeCollapse>
|
||||||
|
{
|
||||||
|
public int posA;
|
||||||
|
public int posB;
|
||||||
|
public Vector3 result;
|
||||||
|
public double error;
|
||||||
|
|
||||||
|
private double _weight = -1;
|
||||||
|
|
||||||
|
public ref double Weight => ref _weight;
|
||||||
|
|
||||||
|
public void SetWeight(double weight)
|
||||||
|
{
|
||||||
|
_weight = weight;
|
||||||
|
}
|
||||||
|
|
||||||
|
public EdgeCollapse(int posA, int posB)
|
||||||
|
{
|
||||||
|
this.posA = posA;
|
||||||
|
this.posB = posB;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int GetHashCode()
|
||||||
|
{
|
||||||
|
unchecked
|
||||||
|
{
|
||||||
|
return posA + posB;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool Equals(object obj)
|
||||||
|
{
|
||||||
|
return Equals((EdgeCollapse)obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Equals(EdgeCollapse pc)
|
||||||
|
{
|
||||||
|
if (ReferenceEquals(pc, null))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (ReferenceEquals(this, pc))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return (posA == pc.posA && posB == pc.posB) || (posA == pc.posB && posB == pc.posA);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int CompareTo(EdgeCollapse other)
|
||||||
|
{
|
||||||
|
return error > other.error ? 1 : error < other.error ? -1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool operator >(EdgeCollapse x, EdgeCollapse y)
|
||||||
|
{
|
||||||
|
return x.error > y.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool operator >=(EdgeCollapse x, EdgeCollapse y)
|
||||||
|
{
|
||||||
|
return x.error >= y.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool operator <(EdgeCollapse x, EdgeCollapse y)
|
||||||
|
{
|
||||||
|
return x.error < y.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool operator <=(EdgeCollapse x, EdgeCollapse y)
|
||||||
|
{
|
||||||
|
return x.error <= y.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return $"<A:{posA} B:{posB} error:{error} topology:{_weight}>";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
15
LightlessSync/ThirdParty/Nanomesh/Algo/Decimation/EdgeComparer.cs
vendored
Normal file
15
LightlessSync/ThirdParty/Nanomesh/Algo/Decimation/EdgeComparer.cs
vendored
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Nanomesh
|
||||||
|
{
|
||||||
|
public partial class DecimateModifier
|
||||||
|
{
|
||||||
|
private class EdgeComparer : IComparer<EdgeCollapse>
|
||||||
|
{
|
||||||
|
public int Compare(EdgeCollapse x, EdgeCollapse y)
|
||||||
|
{
|
||||||
|
return x.CompareTo(y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
72
LightlessSync/ThirdParty/Nanomesh/Algo/Decimation/SceneDecimator.cs
vendored
Normal file
72
LightlessSync/ThirdParty/Nanomesh/Algo/Decimation/SceneDecimator.cs
vendored
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace Nanomesh
|
||||||
|
{
|
||||||
|
public class SceneDecimator
|
||||||
|
{
|
||||||
|
private class ModifierAndOccurrences
|
||||||
|
{
|
||||||
|
public int occurrences = 1;
|
||||||
|
public DecimateModifier modifier = new DecimateModifier();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Dictionary<ConnectedMesh, ModifierAndOccurrences> _modifiers;
|
||||||
|
|
||||||
|
public void Initialize(IEnumerable<ConnectedMesh> meshes)
|
||||||
|
{
|
||||||
|
_modifiers = new Dictionary<ConnectedMesh, ModifierAndOccurrences>();
|
||||||
|
|
||||||
|
foreach (ConnectedMesh mesh in meshes)
|
||||||
|
{
|
||||||
|
ModifierAndOccurrences modifier;
|
||||||
|
if (_modifiers.ContainsKey(mesh))
|
||||||
|
{
|
||||||
|
modifier = _modifiers[mesh];
|
||||||
|
modifier.occurrences++;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_modifiers.Add(mesh, modifier = new ModifierAndOccurrences());
|
||||||
|
//System.Console.WriteLine($"Faces:{mesh.FaceCount}");
|
||||||
|
modifier.modifier.Initialize(mesh);
|
||||||
|
}
|
||||||
|
|
||||||
|
_faceCount += mesh.FaceCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
_initalFaceCount = _faceCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int _faceCount;
|
||||||
|
private int _initalFaceCount;
|
||||||
|
|
||||||
|
public void DecimateToRatio(float targetTriangleRatio)
|
||||||
|
{
|
||||||
|
targetTriangleRatio = MathF.Clamp(targetTriangleRatio, 0f, 1f);
|
||||||
|
DecimateToPolycount((int)MathF.Round(targetTriangleRatio * _initalFaceCount));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DecimatePolycount(int polycount)
|
||||||
|
{
|
||||||
|
DecimateToPolycount((int)MathF.Round(_initalFaceCount - polycount));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DecimateToPolycount(int targetTriangleCount)
|
||||||
|
{
|
||||||
|
//System.Console.WriteLine($"Faces:{_faceCount} Target:{targetTriangleCount}");
|
||||||
|
while (_faceCount > targetTriangleCount)
|
||||||
|
{
|
||||||
|
KeyValuePair<ConnectedMesh, ModifierAndOccurrences> pair = _modifiers.OrderBy(x => x.Value.modifier.GetMinimumError()).First();
|
||||||
|
|
||||||
|
int facesBefore = pair.Key.FaceCount;
|
||||||
|
pair.Value.modifier.Iterate();
|
||||||
|
|
||||||
|
if (facesBefore == pair.Key.FaceCount)
|
||||||
|
break; // Exit !
|
||||||
|
|
||||||
|
_faceCount -= (facesBefore - pair.Key.FaceCount) * pair.Value.occurrences;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
76
LightlessSync/ThirdParty/Nanomesh/Algo/NormalsCreator.cs
vendored
Normal file
76
LightlessSync/ThirdParty/Nanomesh/Algo/NormalsCreator.cs
vendored
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace Nanomesh
|
||||||
|
{
|
||||||
|
public class NormalsModifier
|
||||||
|
{
|
||||||
|
public struct PosAndAttribute : IEquatable<PosAndAttribute>
|
||||||
|
{
|
||||||
|
public int position;
|
||||||
|
public Attribute attribute;
|
||||||
|
|
||||||
|
public override int GetHashCode()
|
||||||
|
{
|
||||||
|
return position.GetHashCode() ^ (attribute.GetHashCode() << 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Equals(PosAndAttribute other)
|
||||||
|
{
|
||||||
|
return position == other.position && attribute.Equals(other.attribute);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Run(ConnectedMesh mesh, float smoothingAngle)
|
||||||
|
{
|
||||||
|
float cosineThreshold = MathF.Cos(smoothingAngle * MathF.PI / 180f);
|
||||||
|
|
||||||
|
int[] positionToNode = mesh.GetPositionToNode();
|
||||||
|
|
||||||
|
Dictionary<PosAndAttribute, int> attributeToIndex = new Dictionary<PosAndAttribute, int>();
|
||||||
|
|
||||||
|
for (int p = 0; p < positionToNode.Length; p++)
|
||||||
|
{
|
||||||
|
int nodeIndex = positionToNode[p];
|
||||||
|
if (nodeIndex < 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Debug.Assert(!mesh.nodes[nodeIndex].IsRemoved);
|
||||||
|
|
||||||
|
int sibling1 = nodeIndex;
|
||||||
|
do
|
||||||
|
{
|
||||||
|
Vector3F sum = Vector3F.Zero;
|
||||||
|
|
||||||
|
Vector3F normal1 = mesh.GetFaceNormal(sibling1);
|
||||||
|
|
||||||
|
int sibling2 = nodeIndex;
|
||||||
|
do
|
||||||
|
{
|
||||||
|
Vector3F normal2 = mesh.GetFaceNormal(sibling2);
|
||||||
|
|
||||||
|
float dot = Vector3F.Dot(normal1, normal2);
|
||||||
|
|
||||||
|
if (dot >= cosineThreshold)
|
||||||
|
{
|
||||||
|
// Area and angle weighting (it gives better results)
|
||||||
|
sum += mesh.GetFaceArea(sibling2) * mesh.GetAngleRadians(sibling2) * normal2;
|
||||||
|
}
|
||||||
|
|
||||||
|
} while ((sibling2 = mesh.nodes[sibling2].sibling) != nodeIndex);
|
||||||
|
|
||||||
|
sum = sum.Normalized;
|
||||||
|
|
||||||
|
|
||||||
|
} while ((sibling1 = mesh.nodes[sibling1].sibling) != nodeIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assign new attributes
|
||||||
|
|
||||||
|
// TODO : Fix
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
17
LightlessSync/ThirdParty/Nanomesh/Algo/NormalsFixer.cs
vendored
Normal file
17
LightlessSync/ThirdParty/Nanomesh/Algo/NormalsFixer.cs
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
namespace Nanomesh
|
||||||
|
{
|
||||||
|
public class NormalsFixer
|
||||||
|
{
|
||||||
|
public void Start(ConnectedMesh mesh)
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
for (int i = 0; i < mesh.attributes.Length; i++)
|
||||||
|
{
|
||||||
|
Attribute attribute = mesh.attributes[i];
|
||||||
|
attribute.normal = attribute.normal.Normalized;
|
||||||
|
mesh.attributes[i] = attribute;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
27
LightlessSync/ThirdParty/Nanomesh/Algo/Triangulate.cs
vendored
Normal file
27
LightlessSync/ThirdParty/Nanomesh/Algo/Triangulate.cs
vendored
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Nanomesh
|
||||||
|
{
|
||||||
|
public class TriangulateModifier
|
||||||
|
{
|
||||||
|
public void Run(ConnectedMesh mesh)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < mesh.nodes.Length; i++)
|
||||||
|
{
|
||||||
|
int edgeCount = 0;
|
||||||
|
int relative = i;
|
||||||
|
while ((relative = mesh.nodes[relative].relative) != i) // Circulate around face
|
||||||
|
{
|
||||||
|
edgeCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (edgeCount > 2)
|
||||||
|
{
|
||||||
|
throw new Exception("Mesh has polygons of dimension 4 or greater");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Todo : Implement
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
144
LightlessSync/ThirdParty/Nanomesh/Base/BoneWeight.cs
vendored
Normal file
144
LightlessSync/ThirdParty/Nanomesh/Base/BoneWeight.cs
vendored
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace Nanomesh
|
||||||
|
{
|
||||||
|
public readonly struct BoneWeight : IEquatable<BoneWeight>, IInterpolable<BoneWeight>
|
||||||
|
{
|
||||||
|
public readonly int index0;
|
||||||
|
public readonly int index1;
|
||||||
|
public readonly int index2;
|
||||||
|
public readonly int index3;
|
||||||
|
public readonly float weight0;
|
||||||
|
public readonly float weight1;
|
||||||
|
public readonly float weight2;
|
||||||
|
public readonly float weight3;
|
||||||
|
|
||||||
|
public int GetIndex(int i)
|
||||||
|
{
|
||||||
|
switch (i)
|
||||||
|
{
|
||||||
|
case 0: return index0;
|
||||||
|
case 1: return index1;
|
||||||
|
case 2: return index2;
|
||||||
|
case 3: return index3;
|
||||||
|
default: return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public float GetWeight(int i)
|
||||||
|
{
|
||||||
|
switch (i)
|
||||||
|
{
|
||||||
|
case 0: return weight0;
|
||||||
|
case 1: return weight1;
|
||||||
|
case 2: return weight2;
|
||||||
|
case 3: return weight3;
|
||||||
|
default: return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public BoneWeight(int index0, int index1, int index2, int index3, float weight0, float weight1, float weight2, float weight3)
|
||||||
|
{
|
||||||
|
this.index0 = index0;
|
||||||
|
this.index1 = index1;
|
||||||
|
this.index2 = index2;
|
||||||
|
this.index3 = index3;
|
||||||
|
this.weight0 = weight0;
|
||||||
|
this.weight1 = weight1;
|
||||||
|
this.weight2 = weight2;
|
||||||
|
this.weight3 = weight3;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Equals(BoneWeight other)
|
||||||
|
{
|
||||||
|
return index0 == other.index0
|
||||||
|
&& index1 == other.index1
|
||||||
|
&& index2 == other.index2
|
||||||
|
&& index3 == other.index3
|
||||||
|
&& weight0 == other.weight0
|
||||||
|
&& weight1 == other.weight1
|
||||||
|
&& weight2 == other.weight2
|
||||||
|
&& weight3 == other.weight3;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int GetHashCode()
|
||||||
|
{
|
||||||
|
unchecked
|
||||||
|
{
|
||||||
|
int hash = 17;
|
||||||
|
hash = hash * 31 + index0;
|
||||||
|
hash = hash * 31 + index1;
|
||||||
|
hash = hash * 31 + index2;
|
||||||
|
hash = hash * 31 + index3;
|
||||||
|
hash = hash * 31 + weight0.GetHashCode();
|
||||||
|
hash = hash * 31 + weight1.GetHashCode();
|
||||||
|
hash = hash * 31 + weight2.GetHashCode();
|
||||||
|
hash = hash * 31 + weight3.GetHashCode();
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public unsafe BoneWeight Interpolate(BoneWeight other, double ratio)
|
||||||
|
{
|
||||||
|
BoneWeight boneWeightA = this;
|
||||||
|
BoneWeight boneWeightB = other;
|
||||||
|
|
||||||
|
Dictionary<int, float> newBoneWeight = new Dictionary<int, float>();
|
||||||
|
|
||||||
|
// Map weights and indices
|
||||||
|
for (int i = 0; i < 4; i++)
|
||||||
|
{
|
||||||
|
newBoneWeight.TryAdd(boneWeightA.GetIndex(i), 0);
|
||||||
|
newBoneWeight.TryAdd(boneWeightB.GetIndex(i), 0);
|
||||||
|
newBoneWeight[boneWeightA.GetIndex(i)] += (float)((1 - ratio) * boneWeightA.GetWeight(i));
|
||||||
|
newBoneWeight[boneWeightB.GetIndex(i)] += (float)(ratio * boneWeightB.GetWeight(i));
|
||||||
|
}
|
||||||
|
|
||||||
|
int* newIndices = stackalloc int[4];
|
||||||
|
float* newWeights = stackalloc float[4];
|
||||||
|
|
||||||
|
// Order from biggest to smallest weight, and drop bones above 4th
|
||||||
|
float totalWeight = 0;
|
||||||
|
int k = 0;
|
||||||
|
foreach (KeyValuePair<int, float> boneWeightN in newBoneWeight.OrderByDescending(x => x.Value))
|
||||||
|
{
|
||||||
|
newIndices[k] = boneWeightN.Key;
|
||||||
|
newWeights[k] = boneWeightN.Value;
|
||||||
|
totalWeight += boneWeightN.Value;
|
||||||
|
if (k == 3)
|
||||||
|
break;
|
||||||
|
k++;
|
||||||
|
}
|
||||||
|
|
||||||
|
var sumA = boneWeightA.weight0 + boneWeightA.weight1 + boneWeightA.weight2 + boneWeightA.weight3;
|
||||||
|
var sumB = boneWeightB.weight0 + boneWeightB.weight1 + boneWeightB.weight2 + boneWeightB.weight3;
|
||||||
|
var targetSum = (float)((1d - ratio) * sumA + ratio * sumB);
|
||||||
|
|
||||||
|
// Normalize and re-scale to preserve original weight sum.
|
||||||
|
if (totalWeight > 0f)
|
||||||
|
{
|
||||||
|
var scale = targetSum / totalWeight;
|
||||||
|
for (int j = 0; j < 4; j++)
|
||||||
|
{
|
||||||
|
newWeights[j] *= scale;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new BoneWeight(
|
||||||
|
newIndices[0], newIndices[1], newIndices[2], newIndices[3],
|
||||||
|
newWeights[0], newWeights[1], newWeights[2], newWeights[3]);
|
||||||
|
|
||||||
|
//return new BoneWeight(
|
||||||
|
// ratio < 0.5f ? index0 : other.index0,
|
||||||
|
// ratio < 0.5f ? index1 : other.index1,
|
||||||
|
// ratio < 0.5f ? index2 : other.index2,
|
||||||
|
// ratio < 0.5f ? index3 : other.index3,
|
||||||
|
// (float)(ratio * weight0 + (1 - ratio) * other.weight0),
|
||||||
|
// (float)(ratio * weight1 + (1 - ratio) * other.weight1),
|
||||||
|
// (float)(ratio * weight2 + (1 - ratio) * other.weight2),
|
||||||
|
// (float)(ratio * weight3 + (1 - ratio) * other.weight3));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
110
LightlessSync/ThirdParty/Nanomesh/Base/Color32.cs
vendored
Normal file
110
LightlessSync/ThirdParty/Nanomesh/Base/Color32.cs
vendored
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
using System;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace Nanomesh
|
||||||
|
{
|
||||||
|
[StructLayout(LayoutKind.Explicit)]
|
||||||
|
public readonly struct Color32 : IEquatable<Color32>, IInterpolable<Color32>
|
||||||
|
{
|
||||||
|
[FieldOffset(0)]
|
||||||
|
internal readonly int rgba;
|
||||||
|
|
||||||
|
[FieldOffset(0)]
|
||||||
|
public readonly byte r;
|
||||||
|
|
||||||
|
[FieldOffset(1)]
|
||||||
|
public readonly byte g;
|
||||||
|
|
||||||
|
[FieldOffset(2)]
|
||||||
|
public readonly byte b;
|
||||||
|
|
||||||
|
[FieldOffset(3)]
|
||||||
|
public readonly byte a;
|
||||||
|
|
||||||
|
public Color32(byte r, byte g, byte b, byte a)
|
||||||
|
{
|
||||||
|
rgba = 0;
|
||||||
|
this.r = r;
|
||||||
|
this.g = g;
|
||||||
|
this.b = b;
|
||||||
|
this.a = a;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Color32(float r, float g, float b, float a)
|
||||||
|
{
|
||||||
|
rgba = 0;
|
||||||
|
this.r = (byte)MathF.Round(r);
|
||||||
|
this.g = (byte)MathF.Round(g);
|
||||||
|
this.b = (byte)MathF.Round(b);
|
||||||
|
this.a = (byte)MathF.Round(a);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Color32(double r, double g, double b, double a)
|
||||||
|
{
|
||||||
|
rgba = 0;
|
||||||
|
this.r = (byte)Math.Round(r);
|
||||||
|
this.g = (byte)Math.Round(g);
|
||||||
|
this.b = (byte)Math.Round(b);
|
||||||
|
this.a = (byte)Math.Round(a);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Equals(Color32 other)
|
||||||
|
{
|
||||||
|
return other.rgba == rgba;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Color32 Interpolate(Color32 other, double ratio)
|
||||||
|
{
|
||||||
|
return ratio * this + (1 - ratio) * other;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds two colors.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static Color32 operator +(Color32 a, Color32 b) { return new Color32(a.r + b.r, a.g + b.g, a.b + b.b, a.a + b.a); }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Subtracts one color from another.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static Color32 operator -(Color32 a, Color32 b) { return new Color32(1f * a.r - b.r, a.g - b.g, a.b - b.b, a.a - b.a); }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Multiplies one color by another.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static Color32 operator *(Color32 a, Color32 b) { return new Color32(1f * a.r * b.r, 1f * a.g * b.g, 1f * a.b * b.b, 1f * a.a * b.a); }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Divides one color over another.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static Color32 operator /(Color32 a, Color32 b) { return new Color32(1f * a.r / b.r, 1f * a.g / b.g, 1f * a.b / b.b, 1f * a.a / b.a); }
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Multiplies a color by a number.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="a"></param>
|
||||||
|
/// <param name="d"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static Color32 operator *(Color32 a, float d) { return new Color32(d * a.r, d * a.g, d * a.b, d * a.a); }
|
||||||
|
|
||||||
|
public static Color32 operator *(Color32 a, double d) { return new Color32(d * a.r, d * a.g, d * a.b, d * a.a); }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Multiplies a color by a number.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static Color32 operator *(float d, Color32 a) { return new Color32(d * a.r, d * a.g, d * a.b, d * a.a); }
|
||||||
|
|
||||||
|
public static Color32 operator *(double d, Color32 a) { return new Color32(d * a.r, d * a.g, d * a.b, d * a.a); }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Divides a color by a number.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static Color32 operator /(Color32 a, float d) { return new Color32(1f * a.r / d, 1f * a.g / d, 1f * a.b / d, 1f * a.a / d); }
|
||||||
|
}
|
||||||
|
}
|
||||||
347
LightlessSync/ThirdParty/Nanomesh/Base/FfxivVertexAttribute.cs
vendored
Normal file
347
LightlessSync/ThirdParty/Nanomesh/Base/FfxivVertexAttribute.cs
vendored
Normal file
@@ -0,0 +1,347 @@
|
|||||||
|
using System;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace Nanomesh
|
||||||
|
{
|
||||||
|
[Flags]
|
||||||
|
public enum FfxivAttributeFlags : uint
|
||||||
|
{
|
||||||
|
None = 0,
|
||||||
|
Normal = 1u << 0,
|
||||||
|
Tangent1 = 1u << 1,
|
||||||
|
Tangent2 = 1u << 2,
|
||||||
|
Color = 1u << 3,
|
||||||
|
BoneWeights = 1u << 4,
|
||||||
|
PositionW = 1u << 5,
|
||||||
|
NormalW = 1u << 6,
|
||||||
|
Uv0 = 1u << 7,
|
||||||
|
Uv1 = 1u << 8,
|
||||||
|
Uv2 = 1u << 9,
|
||||||
|
Uv3 = 1u << 10,
|
||||||
|
}
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
public readonly struct FfxivVertexAttribute : IEquatable<FfxivVertexAttribute>, IInterpolable<FfxivVertexAttribute>
|
||||||
|
{
|
||||||
|
public readonly Vector3F normal;
|
||||||
|
public readonly Vector4F tangent1;
|
||||||
|
public readonly Vector4F tangent2;
|
||||||
|
public readonly Vector2F uv0;
|
||||||
|
public readonly Vector2F uv1;
|
||||||
|
public readonly Vector2F uv2;
|
||||||
|
public readonly Vector2F uv3;
|
||||||
|
public readonly Vector4F color;
|
||||||
|
public readonly BoneWeight boneWeight;
|
||||||
|
public readonly float positionW;
|
||||||
|
public readonly float normalW;
|
||||||
|
public readonly FfxivAttributeFlags flags;
|
||||||
|
|
||||||
|
public FfxivVertexAttribute(
|
||||||
|
FfxivAttributeFlags flags,
|
||||||
|
Vector3F normal,
|
||||||
|
Vector4F tangent1,
|
||||||
|
Vector4F tangent2,
|
||||||
|
Vector2F uv0,
|
||||||
|
Vector2F uv1,
|
||||||
|
Vector2F uv2,
|
||||||
|
Vector2F uv3,
|
||||||
|
Vector4F color,
|
||||||
|
BoneWeight boneWeight,
|
||||||
|
float positionW,
|
||||||
|
float normalW)
|
||||||
|
{
|
||||||
|
this.flags = flags;
|
||||||
|
this.normal = normal;
|
||||||
|
this.tangent1 = tangent1;
|
||||||
|
this.tangent2 = tangent2;
|
||||||
|
this.uv0 = uv0;
|
||||||
|
this.uv1 = uv1;
|
||||||
|
this.uv2 = uv2;
|
||||||
|
this.uv3 = uv3;
|
||||||
|
this.color = color;
|
||||||
|
this.boneWeight = boneWeight;
|
||||||
|
this.positionW = positionW;
|
||||||
|
this.normalW = normalW;
|
||||||
|
}
|
||||||
|
|
||||||
|
public FfxivVertexAttribute Interpolate(FfxivVertexAttribute other, double ratio)
|
||||||
|
{
|
||||||
|
var t = (float)ratio;
|
||||||
|
var inv = 1f - t;
|
||||||
|
var combinedFlags = flags | other.flags;
|
||||||
|
|
||||||
|
var normal = (combinedFlags & FfxivAttributeFlags.Normal) != 0
|
||||||
|
? NormalizeVector3(new Vector3F(
|
||||||
|
(this.normal.x * inv) + (other.normal.x * t),
|
||||||
|
(this.normal.y * inv) + (other.normal.y * t),
|
||||||
|
(this.normal.z * inv) + (other.normal.z * t)))
|
||||||
|
: default;
|
||||||
|
|
||||||
|
var tangent1 = (combinedFlags & FfxivAttributeFlags.Tangent1) != 0
|
||||||
|
? BlendTangent(this.tangent1, other.tangent1, t)
|
||||||
|
: default;
|
||||||
|
|
||||||
|
var tangent2 = (combinedFlags & FfxivAttributeFlags.Tangent2) != 0
|
||||||
|
? BlendTangent(this.tangent2, other.tangent2, t)
|
||||||
|
: default;
|
||||||
|
|
||||||
|
var uv0 = (combinedFlags & FfxivAttributeFlags.Uv0) != 0
|
||||||
|
? Vector2F.LerpUnclamped(this.uv0, other.uv0, t)
|
||||||
|
: default;
|
||||||
|
|
||||||
|
var uv1 = (combinedFlags & FfxivAttributeFlags.Uv1) != 0
|
||||||
|
? Vector2F.LerpUnclamped(this.uv1, other.uv1, t)
|
||||||
|
: default;
|
||||||
|
|
||||||
|
var uv2 = (combinedFlags & FfxivAttributeFlags.Uv2) != 0
|
||||||
|
? Vector2F.LerpUnclamped(this.uv2, other.uv2, t)
|
||||||
|
: default;
|
||||||
|
|
||||||
|
var uv3 = (combinedFlags & FfxivAttributeFlags.Uv3) != 0
|
||||||
|
? Vector2F.LerpUnclamped(this.uv3, other.uv3, t)
|
||||||
|
: default;
|
||||||
|
|
||||||
|
var color = (combinedFlags & FfxivAttributeFlags.Color) != 0
|
||||||
|
? new Vector4F(
|
||||||
|
(this.color.x * inv) + (other.color.x * t),
|
||||||
|
(this.color.y * inv) + (other.color.y * t),
|
||||||
|
(this.color.z * inv) + (other.color.z * t),
|
||||||
|
(this.color.w * inv) + (other.color.w * t))
|
||||||
|
: default;
|
||||||
|
|
||||||
|
var boneWeight = (combinedFlags & FfxivAttributeFlags.BoneWeights) != 0
|
||||||
|
? BlendBoneWeights(this.boneWeight, other.boneWeight, t)
|
||||||
|
: default;
|
||||||
|
|
||||||
|
var positionW = (combinedFlags & FfxivAttributeFlags.PositionW) != 0
|
||||||
|
? (this.positionW * inv) + (other.positionW * t)
|
||||||
|
: 0f;
|
||||||
|
|
||||||
|
var normalW = (combinedFlags & FfxivAttributeFlags.NormalW) != 0
|
||||||
|
? (this.normalW * inv) + (other.normalW * t)
|
||||||
|
: 0f;
|
||||||
|
|
||||||
|
return new FfxivVertexAttribute(
|
||||||
|
combinedFlags,
|
||||||
|
normal,
|
||||||
|
tangent1,
|
||||||
|
tangent2,
|
||||||
|
uv0,
|
||||||
|
uv1,
|
||||||
|
uv2,
|
||||||
|
uv3,
|
||||||
|
color,
|
||||||
|
boneWeight,
|
||||||
|
positionW,
|
||||||
|
normalW);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Equals(FfxivVertexAttribute other)
|
||||||
|
{
|
||||||
|
if (flags != other.flags)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((flags & FfxivAttributeFlags.Normal) != 0 && !normal.Equals(other.normal))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((flags & FfxivAttributeFlags.Tangent1) != 0 && !tangent1.Equals(other.tangent1))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((flags & FfxivAttributeFlags.Tangent2) != 0 && !tangent2.Equals(other.tangent2))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((flags & FfxivAttributeFlags.Uv0) != 0 && !uv0.Equals(other.uv0))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((flags & FfxivAttributeFlags.Uv1) != 0 && !uv1.Equals(other.uv1))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((flags & FfxivAttributeFlags.Uv2) != 0 && !uv2.Equals(other.uv2))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((flags & FfxivAttributeFlags.Uv3) != 0 && !uv3.Equals(other.uv3))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((flags & FfxivAttributeFlags.Color) != 0 && !color.Equals(other.color))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((flags & FfxivAttributeFlags.BoneWeights) != 0 && !boneWeight.Equals(other.boneWeight))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((flags & FfxivAttributeFlags.PositionW) != 0 && positionW != other.positionW)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((flags & FfxivAttributeFlags.NormalW) != 0 && normalW != other.normalW)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool Equals(object? obj)
|
||||||
|
=> obj is FfxivVertexAttribute other && Equals(other);
|
||||||
|
|
||||||
|
public override int GetHashCode()
|
||||||
|
{
|
||||||
|
var hash = new HashCode();
|
||||||
|
hash.Add(normal);
|
||||||
|
hash.Add(tangent1);
|
||||||
|
hash.Add(tangent2);
|
||||||
|
hash.Add(uv0);
|
||||||
|
hash.Add(uv1);
|
||||||
|
hash.Add(uv2);
|
||||||
|
hash.Add(uv3);
|
||||||
|
hash.Add(color);
|
||||||
|
hash.Add(boneWeight);
|
||||||
|
hash.Add(positionW);
|
||||||
|
hash.Add(normalW);
|
||||||
|
hash.Add(flags);
|
||||||
|
return hash.ToHashCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Vector3F NormalizeVector3(in Vector3F value)
|
||||||
|
{
|
||||||
|
var length = Vector3F.Magnitude(value);
|
||||||
|
return length > 0f ? value / length : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Vector4F BlendTangent(in Vector4F a, in Vector4F b, float t)
|
||||||
|
{
|
||||||
|
var inv = 1f - t;
|
||||||
|
var blended = new Vector3F(
|
||||||
|
(a.x * inv) + (b.x * t),
|
||||||
|
(a.y * inv) + (b.y * t),
|
||||||
|
(a.z * inv) + (b.z * t));
|
||||||
|
blended = NormalizeVector3(blended);
|
||||||
|
|
||||||
|
var w = t >= 0.5f ? b.w : a.w;
|
||||||
|
if (w != 0f)
|
||||||
|
{
|
||||||
|
w = w >= 0f ? 1f : -1f;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Vector4F(blended.x, blended.y, blended.z, w);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BoneWeight BlendBoneWeights(in BoneWeight a, in BoneWeight b, float ratio)
|
||||||
|
{
|
||||||
|
Span<int> indices = stackalloc int[8];
|
||||||
|
Span<float> weights = stackalloc float[8];
|
||||||
|
var count = 0;
|
||||||
|
|
||||||
|
static void AddWeight(Span<int> indices, Span<float> weights, ref int count, int index, float weight)
|
||||||
|
{
|
||||||
|
if (weight <= 0f)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
if (indices[i] == index)
|
||||||
|
{
|
||||||
|
weights[i] += weight;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count < indices.Length)
|
||||||
|
{
|
||||||
|
indices[count] = index;
|
||||||
|
weights[count] = weight;
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var inv = 1f - ratio;
|
||||||
|
var sumA = a.weight0 + a.weight1 + a.weight2 + a.weight3;
|
||||||
|
var sumB = b.weight0 + b.weight1 + b.weight2 + b.weight3;
|
||||||
|
var targetSum = (sumA * inv) + (sumB * ratio);
|
||||||
|
AddWeight(indices, weights, ref count, a.index0, a.weight0 * inv);
|
||||||
|
AddWeight(indices, weights, ref count, a.index1, a.weight1 * inv);
|
||||||
|
AddWeight(indices, weights, ref count, a.index2, a.weight2 * inv);
|
||||||
|
AddWeight(indices, weights, ref count, a.index3, a.weight3 * inv);
|
||||||
|
AddWeight(indices, weights, ref count, b.index0, b.weight0 * ratio);
|
||||||
|
AddWeight(indices, weights, ref count, b.index1, b.weight1 * ratio);
|
||||||
|
AddWeight(indices, weights, ref count, b.index2, b.weight2 * ratio);
|
||||||
|
AddWeight(indices, weights, ref count, b.index3, b.weight3 * ratio);
|
||||||
|
|
||||||
|
if (count == 0)
|
||||||
|
{
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
|
||||||
|
Span<int> topIndices = stackalloc int[4];
|
||||||
|
Span<float> topWeights = stackalloc float[4];
|
||||||
|
for (var i = 0; i < 4; i++)
|
||||||
|
{
|
||||||
|
topIndices[i] = -1;
|
||||||
|
topWeights[i] = 0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
var weight = weights[i];
|
||||||
|
var index = indices[i];
|
||||||
|
for (var slot = 0; slot < 4; slot++)
|
||||||
|
{
|
||||||
|
if (weight > topWeights[slot])
|
||||||
|
{
|
||||||
|
for (var shift = 3; shift > slot; shift--)
|
||||||
|
{
|
||||||
|
topWeights[shift] = topWeights[shift - 1];
|
||||||
|
topIndices[shift] = topIndices[shift - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
topWeights[slot] = weight;
|
||||||
|
topIndices[slot] = index;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var sum = topWeights[0] + topWeights[1] + topWeights[2] + topWeights[3];
|
||||||
|
if (sum > 0f)
|
||||||
|
{
|
||||||
|
var scale = targetSum > 0f ? targetSum / sum : 0f;
|
||||||
|
for (var i = 0; i < 4; i++)
|
||||||
|
{
|
||||||
|
topWeights[i] *= scale;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new BoneWeight(
|
||||||
|
topIndices[0] < 0 ? 0 : topIndices[0],
|
||||||
|
topIndices[1] < 0 ? 0 : topIndices[1],
|
||||||
|
topIndices[2] < 0 ? 0 : topIndices[2],
|
||||||
|
topIndices[3] < 0 ? 0 : topIndices[3],
|
||||||
|
topWeights[0],
|
||||||
|
topWeights[1],
|
||||||
|
topWeights[2],
|
||||||
|
topWeights[3]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
7
LightlessSync/ThirdParty/Nanomesh/Base/IInterpolable.cs
vendored
Normal file
7
LightlessSync/ThirdParty/Nanomesh/Base/IInterpolable.cs
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Nanomesh
|
||||||
|
{
|
||||||
|
public interface IInterpolable<T>
|
||||||
|
{
|
||||||
|
T Interpolate(T other, double ratio);
|
||||||
|
}
|
||||||
|
}
|
||||||
356
LightlessSync/ThirdParty/Nanomesh/Base/MathF.cs
vendored
Normal file
356
LightlessSync/ThirdParty/Nanomesh/Base/MathF.cs
vendored
Normal file
@@ -0,0 +1,356 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Nanomesh
|
||||||
|
{
|
||||||
|
public static partial class MathF
|
||||||
|
{
|
||||||
|
// Returns the sine of angle /f/ in radians.
|
||||||
|
public static float Sin(float f) { return (float)Math.Sin(f); }
|
||||||
|
|
||||||
|
// Returns the cosine of angle /f/ in radians.
|
||||||
|
public static float Cos(float f) { return (float)Math.Cos(f); }
|
||||||
|
|
||||||
|
// Returns the tangent of angle /f/ in radians.
|
||||||
|
public static float Tan(float f) { return (float)Math.Tan(f); }
|
||||||
|
|
||||||
|
// Returns the arc-sine of /f/ - the angle in radians whose sine is /f/.
|
||||||
|
public static float Asin(float f) { return (float)Math.Asin(f); }
|
||||||
|
|
||||||
|
// Returns the arc-cosine of /f/ - the angle in radians whose cosine is /f/.
|
||||||
|
public static float Acos(float f) { return (float)Math.Acos(f); }
|
||||||
|
|
||||||
|
// Returns the arc-tangent of /f/ - the angle in radians whose tangent is /f/.
|
||||||
|
public static float Atan(float f) { return (float)Math.Atan(f); }
|
||||||
|
|
||||||
|
// Returns the angle in radians whose ::ref::Tan is @@y/x@@.
|
||||||
|
public static float Atan2(float y, float x) { return (float)Math.Atan2(y, x); }
|
||||||
|
|
||||||
|
// Returns square root of /f/.
|
||||||
|
public static float Sqrt(float f) { return (float)Math.Sqrt(f); }
|
||||||
|
|
||||||
|
// Returns the absolute value of /f/.
|
||||||
|
public static float Abs(float f) { return (float)Math.Abs(f); }
|
||||||
|
|
||||||
|
// Returns the absolute value of /value/.
|
||||||
|
public static int Abs(int value) { return Math.Abs(value); }
|
||||||
|
|
||||||
|
/// *listonly*
|
||||||
|
public static float Min(float a, float b) { return a < b ? a : b; }
|
||||||
|
// Returns the smallest of two or more values.
|
||||||
|
public static float Min(params float[] values)
|
||||||
|
{
|
||||||
|
int len = values.Length;
|
||||||
|
if (len == 0)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
float m = values[0];
|
||||||
|
for (int i = 1; i < len; i++)
|
||||||
|
{
|
||||||
|
if (values[i] < m)
|
||||||
|
{
|
||||||
|
m = values[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// *listonly*
|
||||||
|
public static int Min(int a, int b) { return a < b ? a : b; }
|
||||||
|
// Returns the smallest of two or more values.
|
||||||
|
public static int Min(params int[] values)
|
||||||
|
{
|
||||||
|
int len = values.Length;
|
||||||
|
if (len == 0)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int m = values[0];
|
||||||
|
for (int i = 1; i < len; i++)
|
||||||
|
{
|
||||||
|
if (values[i] < m)
|
||||||
|
{
|
||||||
|
m = values[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// *listonly*
|
||||||
|
public static float Max(float a, float b) { return a > b ? a : b; }
|
||||||
|
// Returns largest of two or more values.
|
||||||
|
public static float Max(params float[] values)
|
||||||
|
{
|
||||||
|
int len = values.Length;
|
||||||
|
if (len == 0)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
float m = values[0];
|
||||||
|
for (int i = 1; i < len; i++)
|
||||||
|
{
|
||||||
|
if (values[i] > m)
|
||||||
|
{
|
||||||
|
m = values[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// *listonly*
|
||||||
|
public static int Max(int a, int b) { return a > b ? a : b; }
|
||||||
|
// Returns the largest of two or more values.
|
||||||
|
public static int Max(params int[] values)
|
||||||
|
{
|
||||||
|
int len = values.Length;
|
||||||
|
if (len == 0)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int m = values[0];
|
||||||
|
for (int i = 1; i < len; i++)
|
||||||
|
{
|
||||||
|
if (values[i] > m)
|
||||||
|
{
|
||||||
|
m = values[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns /f/ raised to power /p/.
|
||||||
|
public static float Pow(float f, float p) { return (float)Math.Pow(f, p); }
|
||||||
|
|
||||||
|
// Returns e raised to the specified power.
|
||||||
|
public static float Exp(float power) { return (float)Math.Exp(power); }
|
||||||
|
|
||||||
|
// Returns the logarithm of a specified number in a specified base.
|
||||||
|
public static float Log(float f, float p) { return (float)Math.Log(f, p); }
|
||||||
|
|
||||||
|
// Returns the natural (base e) logarithm of a specified number.
|
||||||
|
public static float Log(float f) { return (float)Math.Log(f); }
|
||||||
|
|
||||||
|
// Returns the base 10 logarithm of a specified number.
|
||||||
|
public static float Log10(float f) { return (float)Math.Log10(f); }
|
||||||
|
|
||||||
|
// Returns the smallest integer greater to or equal to /f/.
|
||||||
|
public static float Ceil(float f) { return (float)Math.Ceiling(f); }
|
||||||
|
|
||||||
|
// Returns the largest integer smaller to or equal to /f/.
|
||||||
|
public static float Floor(float f) { return (float)Math.Floor(f); }
|
||||||
|
|
||||||
|
// Returns /f/ rounded to the nearest integer.
|
||||||
|
public static float Round(float f) { return (float)Math.Round(f); }
|
||||||
|
|
||||||
|
// Returns the smallest integer greater to or equal to /f/.
|
||||||
|
public static int CeilToInt(float f) { return (int)Math.Ceiling(f); }
|
||||||
|
|
||||||
|
// Returns the largest integer smaller to or equal to /f/.
|
||||||
|
public static int FloorToInt(float f) { return (int)Math.Floor(f); }
|
||||||
|
|
||||||
|
// Returns /f/ rounded to the nearest integer.
|
||||||
|
public static int RoundToInt(float f) { return (int)Math.Round(f); }
|
||||||
|
|
||||||
|
// Returns the sign of /f/.
|
||||||
|
public static float Sign(float f) { return f >= 0F ? 1F : -1F; }
|
||||||
|
|
||||||
|
// The infamous ''3.14159265358979...'' value (RO).
|
||||||
|
public const float PI = (float)Math.PI;
|
||||||
|
|
||||||
|
// A representation of positive infinity (RO).
|
||||||
|
public const float Infinity = float.PositiveInfinity;
|
||||||
|
|
||||||
|
// A representation of negative infinity (RO).
|
||||||
|
public const float NegativeInfinity = float.NegativeInfinity;
|
||||||
|
|
||||||
|
// Degrees-to-radians conversion constant (RO).
|
||||||
|
public const float Deg2Rad = PI * 2F / 360F;
|
||||||
|
|
||||||
|
// Radians-to-degrees conversion constant (RO).
|
||||||
|
public const float Rad2Deg = 1F / Deg2Rad;
|
||||||
|
|
||||||
|
// Clamps a value between a minimum float and maximum float value.
|
||||||
|
public static double Clamp(double value, double min, double max)
|
||||||
|
{
|
||||||
|
if (value < min)
|
||||||
|
{
|
||||||
|
value = min;
|
||||||
|
}
|
||||||
|
else if (value > max)
|
||||||
|
{
|
||||||
|
value = max;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clamps a value between a minimum float and maximum float value.
|
||||||
|
public static float Clamp(float value, float min, float max)
|
||||||
|
{
|
||||||
|
if (value < min)
|
||||||
|
{
|
||||||
|
value = min;
|
||||||
|
}
|
||||||
|
else if (value > max)
|
||||||
|
{
|
||||||
|
value = max;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clamps value between min and max and returns value.
|
||||||
|
// Set the position of the transform to be that of the time
|
||||||
|
// but never less than 1 or more than 3
|
||||||
|
//
|
||||||
|
public static int Clamp(int value, int min, int max)
|
||||||
|
{
|
||||||
|
if (value < min)
|
||||||
|
{
|
||||||
|
value = min;
|
||||||
|
}
|
||||||
|
else if (value > max)
|
||||||
|
{
|
||||||
|
value = max;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clamps value between 0 and 1 and returns value
|
||||||
|
public static float Clamp01(float value)
|
||||||
|
{
|
||||||
|
if (value < 0F)
|
||||||
|
{
|
||||||
|
return 0F;
|
||||||
|
}
|
||||||
|
else if (value > 1F)
|
||||||
|
{
|
||||||
|
return 1F;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interpolates between /a/ and /b/ by /t/. /t/ is clamped between 0 and 1.
|
||||||
|
public static float Lerp(float a, float b, float t)
|
||||||
|
{
|
||||||
|
return a + (b - a) * Clamp01(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interpolates between /a/ and /b/ by /t/ without clamping the interpolant.
|
||||||
|
public static float LerpUnclamped(float a, float b, float t)
|
||||||
|
{
|
||||||
|
return a + (b - a) * t;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Same as ::ref::Lerp but makes sure the values interpolate correctly when they wrap around 360 degrees.
|
||||||
|
public static float LerpAngle(float a, float b, float t)
|
||||||
|
{
|
||||||
|
float delta = Repeat((b - a), 360);
|
||||||
|
if (delta > 180)
|
||||||
|
{
|
||||||
|
delta -= 360;
|
||||||
|
}
|
||||||
|
|
||||||
|
return a + delta * Clamp01(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Moves a value /current/ towards /target/.
|
||||||
|
public static float MoveTowards(float current, float target, float maxDelta)
|
||||||
|
{
|
||||||
|
if (MathF.Abs(target - current) <= maxDelta)
|
||||||
|
{
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
return current + MathF.Sign(target - current) * maxDelta;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Same as ::ref::MoveTowards but makes sure the values interpolate correctly when they wrap around 360 degrees.
|
||||||
|
public static float MoveTowardsAngle(float current, float target, float maxDelta)
|
||||||
|
{
|
||||||
|
float deltaAngle = DeltaAngle(current, target);
|
||||||
|
if (-maxDelta < deltaAngle && deltaAngle < maxDelta)
|
||||||
|
{
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
target = current + deltaAngle;
|
||||||
|
return MoveTowards(current, target, maxDelta);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interpolates between /min/ and /max/ with smoothing at the limits.
|
||||||
|
public static float SmoothStep(float from, float to, float t)
|
||||||
|
{
|
||||||
|
t = MathF.Clamp01(t);
|
||||||
|
t = -2.0F * t * t * t + 3.0F * t * t;
|
||||||
|
return to * t + from * (1F - t);
|
||||||
|
}
|
||||||
|
|
||||||
|
//*undocumented
|
||||||
|
public static float Gamma(float value, float absmax, float gamma)
|
||||||
|
{
|
||||||
|
bool negative = value < 0F;
|
||||||
|
float absval = Abs(value);
|
||||||
|
if (absval > absmax)
|
||||||
|
{
|
||||||
|
return negative ? -absval : absval;
|
||||||
|
}
|
||||||
|
|
||||||
|
float result = Pow(absval / absmax, gamma) * absmax;
|
||||||
|
return negative ? -result : result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loops the value t, so that it is never larger than length and never smaller than 0.
|
||||||
|
public static float Repeat(float t, float length)
|
||||||
|
{
|
||||||
|
return Clamp(t - MathF.Floor(t / length) * length, 0.0f, length);
|
||||||
|
}
|
||||||
|
|
||||||
|
// PingPongs the value t, so that it is never larger than length and never smaller than 0.
|
||||||
|
public static float PingPong(float t, float length)
|
||||||
|
{
|
||||||
|
t = Repeat(t, length * 2F);
|
||||||
|
return length - MathF.Abs(t - length);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculates the ::ref::Lerp parameter between of two values.
|
||||||
|
public static float InverseLerp(float a, float b, float value)
|
||||||
|
{
|
||||||
|
if (a != b)
|
||||||
|
{
|
||||||
|
return Clamp01((value - a) / (b - a));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return 0.0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculates the shortest difference between two given angles.
|
||||||
|
public static float DeltaAngle(float current, float target)
|
||||||
|
{
|
||||||
|
float delta = MathF.Repeat((target - current), 360.0F);
|
||||||
|
if (delta > 180.0F)
|
||||||
|
{
|
||||||
|
delta -= 360.0F;
|
||||||
|
}
|
||||||
|
|
||||||
|
return delta;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static long RandomToLong(System.Random r)
|
||||||
|
{
|
||||||
|
byte[] buffer = new byte[8];
|
||||||
|
r.NextBytes(buffer);
|
||||||
|
return (long)(System.BitConverter.ToUInt64(buffer, 0) & long.MaxValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
114
LightlessSync/ThirdParty/Nanomesh/Base/MathUtils.cs
vendored
Normal file
114
LightlessSync/ThirdParty/Nanomesh/Base/MathUtils.cs
vendored
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
namespace Nanomesh
|
||||||
|
{
|
||||||
|
public static class MathUtils
|
||||||
|
{
|
||||||
|
public const float EpsilonFloat = 1e-15f;
|
||||||
|
public const double EpsilonDouble = 1e-40f;
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public static float DivideSafe(float numerator, float denominator)
|
||||||
|
{
|
||||||
|
return (denominator > -EpsilonFloat && denominator < EpsilonFloat) ? 0f : numerator / denominator;
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public static double DivideSafe(double numerator, double denominator)
|
||||||
|
{
|
||||||
|
return (denominator > -EpsilonDouble && denominator < EpsilonDouble) ? 0d : numerator / denominator;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void SelectMin<T>(double e1, double e2, double e3, in T v1, in T v2, in T v3, out double e, out T v)
|
||||||
|
{
|
||||||
|
if (e1 < e2)
|
||||||
|
{
|
||||||
|
if (e1 < e3)
|
||||||
|
{
|
||||||
|
e = e1;
|
||||||
|
v = v1;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
e = e3;
|
||||||
|
v = v3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (e2 < e3)
|
||||||
|
{
|
||||||
|
e = e2;
|
||||||
|
v = v2;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
e = e3;
|
||||||
|
v = v3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void SelectMin<T>(double e1, double e2, double e3, double e4, in T v1, in T v2, in T v3, in T v4, out double e, out T v)
|
||||||
|
{
|
||||||
|
if (e1 < e2)
|
||||||
|
{
|
||||||
|
if (e1 < e3)
|
||||||
|
{
|
||||||
|
if (e1 < e4)
|
||||||
|
{
|
||||||
|
e = e1;
|
||||||
|
v = v1;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
e = e4;
|
||||||
|
v = v4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (e3 < e4)
|
||||||
|
{
|
||||||
|
e = e3;
|
||||||
|
v = v3;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
e = e4;
|
||||||
|
v = v4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (e2 < e3)
|
||||||
|
{
|
||||||
|
if (e2 < e4)
|
||||||
|
{
|
||||||
|
e = e2;
|
||||||
|
v = v2;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
e = e4;
|
||||||
|
v = v4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (e3 < e4)
|
||||||
|
{
|
||||||
|
e = e3;
|
||||||
|
v = v3;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
e = e4;
|
||||||
|
v = v4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
50
LightlessSync/ThirdParty/Nanomesh/Base/Profiling.cs
vendored
Normal file
50
LightlessSync/ThirdParty/Nanomesh/Base/Profiling.cs
vendored
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace Nanomesh
|
||||||
|
{
|
||||||
|
public static class Profiling
|
||||||
|
{
|
||||||
|
private static readonly Dictionary<string, Stopwatch> stopwatches = new Dictionary<string, Stopwatch>();
|
||||||
|
|
||||||
|
public static void Start(string key)
|
||||||
|
{
|
||||||
|
if (!stopwatches.ContainsKey(key))
|
||||||
|
{
|
||||||
|
stopwatches.Add(key, Stopwatch.StartNew());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
stopwatches[key] = Stopwatch.StartNew();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string End(string key)
|
||||||
|
{
|
||||||
|
TimeSpan time = EndTimer(key);
|
||||||
|
return $"{key} done in {time.ToString("mm':'ss':'fff")}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TimeSpan EndTimer(string key)
|
||||||
|
{
|
||||||
|
if (!stopwatches.ContainsKey(key))
|
||||||
|
{
|
||||||
|
return TimeSpan.MinValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Stopwatch sw = stopwatches[key];
|
||||||
|
sw.Stop();
|
||||||
|
stopwatches.Remove(key);
|
||||||
|
return sw.Elapsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TimeSpan Time(Action toTime)
|
||||||
|
{
|
||||||
|
Stopwatch timer = Stopwatch.StartNew();
|
||||||
|
toTime();
|
||||||
|
timer.Stop();
|
||||||
|
return timer.Elapsed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user