Compare commits
6 Commits
dev
...
collection
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
699535b68b | ||
|
|
828705cbfb | ||
|
|
d8b4122ec3 | ||
|
|
f6a5c85c2d | ||
|
|
9fcbd68ca2 | ||
|
|
73dee6d9a5 |
@@ -1,18 +0,0 @@
|
|||||||
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,15 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,270 +0,0 @@
|
|||||||
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,10 +22,6 @@ 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
|
||||||
@@ -120,30 +116,6 @@ 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,6 +21,7 @@ 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);
|
||||||
|
|
||||||
@@ -68,6 +69,9 @@ 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
|
||||||
@@ -91,6 +95,9 @@ 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; }
|
||||||
@@ -98,14 +105,36 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
public void HaltScan(string source)
|
public void HaltScan(string source)
|
||||||
{
|
{
|
||||||
if (!HaltScanLocks.ContainsKey(source)) HaltScanLocks[source] = 0;
|
HaltScanLocks.AddOrUpdate(source, 1, (_, v) => v + 1);
|
||||||
HaltScanLocks[source]++;
|
Interlocked.Increment(ref _haltCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
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 Dictionary<string, WatcherChange> _watcherChanges = new(StringComparer.OrdinalIgnoreCase);
|
private readonly object _penumbraGate = new();
|
||||||
private readonly Dictionary<string, WatcherChange> _lightlessChanges = new(StringComparer.OrdinalIgnoreCase);
|
private Dictionary<string, WatcherChange> _watcherChanges = 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()
|
||||||
{
|
{
|
||||||
@@ -168,7 +197,7 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
if (!HasAllowedExtension(e.FullPath)) return;
|
if (!HasAllowedExtension(e.FullPath)) return;
|
||||||
|
|
||||||
lock (_watcherChanges)
|
lock (_lightlessChanges)
|
||||||
{
|
{
|
||||||
_lightlessChanges[e.FullPath] = new(e.ChangeType);
|
_lightlessChanges[e.FullPath] = new(e.ChangeType);
|
||||||
}
|
}
|
||||||
@@ -281,65 +310,56 @@ 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
|
||||||
{
|
{
|
||||||
do
|
await Task.Delay(TimeSpan.FromSeconds(10), token).ConfigureAwait(false);
|
||||||
{
|
while (IsHalted() && !token.IsCancellationRequested)
|
||||||
await Task.Delay(delay, token).ConfigureAwait(false);
|
await Task.Delay(250, token).ConfigureAwait(false);
|
||||||
} while (HaltScanLocks.Any(f => f.Value > 0));
|
|
||||||
}
|
|
||||||
catch (TaskCanceledException)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
catch (TaskCanceledException) { return; }
|
||||||
|
|
||||||
lock (_lightlessChanges)
|
var changes = DrainLightlessChanges();
|
||||||
{
|
if (changes.Count > 0)
|
||||||
foreach (var key in changes.Keys)
|
_ = HandleChangesAsync(changes, token);
|
||||||
{
|
|
||||||
_lightlessChanges.Remove(key);
|
|
||||||
}
|
}
|
||||||
}
|
private async Task HandleChangesAsync(Dictionary<string, WatcherChange> changes, CancellationToken token)
|
||||||
|
|
||||||
HandleChanges(changes);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void HandleChanges(Dictionary<string, WatcherChange> changes)
|
|
||||||
{
|
{
|
||||||
lock (_fileDbManager)
|
await _dbGate.WaitAsync(token).ConfigureAwait(false);
|
||||||
|
try
|
||||||
{
|
{
|
||||||
var deletedEntries = changes.Where(c => c.Value.ChangeType == WatcherChangeTypes.Deleted).Select(c => c.Key);
|
var deleted = changes.Where(c => c.Value.ChangeType == WatcherChangeTypes.Deleted).Select(c => c.Key);
|
||||||
var renamedEntries = changes.Where(c => c.Value.ChangeType == WatcherChangeTypes.Renamed);
|
var renamed = changes.Where(c => c.Value.ChangeType == WatcherChangeTypes.Renamed);
|
||||||
var remainingEntries = changes.Where(c => c.Value.ChangeType != WatcherChangeTypes.Deleted).Select(c => c.Key);
|
var remaining = changes.Where(c => c.Value.ChangeType != WatcherChangeTypes.Deleted).Select(c => c.Key);
|
||||||
|
|
||||||
foreach (var entry in deletedEntries)
|
foreach (var entry in deleted)
|
||||||
{
|
{
|
||||||
Logger.LogDebug("FSW Change: Deletion - {val}", entry);
|
Logger.LogDebug("FSW Change: Deletion - {val}", entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var entry in renamedEntries)
|
foreach (var entry in renamed)
|
||||||
{
|
{
|
||||||
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 remainingEntries)
|
foreach (var entry in remaining)
|
||||||
{
|
{
|
||||||
Logger.LogDebug("FSW Change: Creation or Change - {val}", entry);
|
Logger.LogDebug("FSW Change: Creation or Change - {val}", entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
var allChanges = deletedEntries
|
var allChanges = deleted
|
||||||
.Concat(renamedEntries.Select(c => c.Value.OldPath!))
|
.Concat(renamed.Select(c => c.Value.OldPath!))
|
||||||
.Concat(renamedEntries.Select(c => c.Key))
|
.Concat(renamed.Select(c => c.Key))
|
||||||
.Concat(remainingEntries)
|
.Concat(remaining)
|
||||||
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
.ToArray();
|
.ToArray();
|
||||||
|
|
||||||
_ = _fileDbManager.GetFileCachesByPaths(allChanges);
|
_ = _fileDbManager.GetFileCachesByPaths(allChanges);
|
||||||
|
await _fileDbManager.WriteOutFullCsvAsync(token).ConfigureAwait(false);
|
||||||
_fileDbManager.WriteOutFullCsv();
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_dbGate.Release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -347,77 +367,97 @@ 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
|
||||||
{
|
{
|
||||||
do
|
await Task.Delay(TimeSpan.FromSeconds(10), token).ConfigureAwait(false);
|
||||||
{
|
while (IsHalted() && !token.IsCancellationRequested)
|
||||||
await Task.Delay(delay, token).ConfigureAwait(false);
|
await Task.Delay(250, token).ConfigureAwait(false);
|
||||||
} while (HaltScanLocks.Any(f => f.Value > 0));
|
|
||||||
}
|
|
||||||
catch (TaskCanceledException)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
catch (TaskCanceledException) { return; }
|
||||||
|
|
||||||
lock (_watcherChanges)
|
var changes = DrainPenumbraChanges();
|
||||||
{
|
if (changes.Count > 0)
|
||||||
foreach (var key in changes.Keys)
|
_ = HandleChangesAsync(changes, token);
|
||||||
{
|
|
||||||
_watcherChanges.Remove(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
HandleChanges(changes);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void InvokeScan()
|
public void InvokeScan()
|
||||||
{
|
{
|
||||||
TotalFiles = 0;
|
TotalFiles = 0;
|
||||||
_currentFileProgress = 0;
|
TotalFilesStorage = 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 () =>
|
||||||
{
|
{
|
||||||
Logger.LogDebug("Starting Full File Scan");
|
TaskCompletionSource scanTcs = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
TotalFiles = 0;
|
|
||||||
_currentFileProgress = 0;
|
try
|
||||||
while (_dalamudUtil.IsOnFrameworkThread)
|
|
||||||
{
|
{
|
||||||
Logger.LogWarning("Scanner is on framework, waiting for leaving thread before continuing");
|
Logger.LogDebug("Starting Full File Scan");
|
||||||
|
|
||||||
|
while (IsHalted() && !token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
Logger.LogDebug("Scan is halted, waiting...");
|
||||||
await Task.Delay(250, token).ConfigureAwait(false);
|
await Task.Delay(250, token).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
Thread scanThread = new(() =>
|
var scanThread = new Thread(() =>
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_performanceCollector.LogPerformance(this, $"FullFileScan", () => FullFileScan(token));
|
token.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
_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 Task.Delay(250, token).ConfigureAwait(false);
|
|
||||||
|
await scanTcs.Task.ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
catch (TaskCanceledException)
|
||||||
|
{
|
||||||
|
Logger.LogInformation("Full File Scan was canceled.");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError(ex, "Unexpected error in InvokeScan task");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
TotalFiles = 0;
|
TotalFiles = 0;
|
||||||
_currentFileProgress = 0;
|
TotalFilesStorage = 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))
|
||||||
{
|
{
|
||||||
@@ -594,10 +634,20 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
public void ResumeScan(string source)
|
public void ResumeScan(string source)
|
||||||
{
|
{
|
||||||
if (!HaltScanLocks.ContainsKey(source)) HaltScanLocks[source] = 0;
|
int delta = 0;
|
||||||
|
|
||||||
HaltScanLocks[source]--;
|
HaltScanLocks.AddOrUpdate(source,
|
||||||
if (HaltScanLocks[source] < 0) HaltScanLocks[source] = 0;
|
addValueFactory: _ => 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)
|
||||||
@@ -621,97 +671,81 @@ 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;
|
||||||
bool penDirExists = true;
|
var cacheFolder = _configService.Current.CacheFolder;
|
||||||
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.");
|
||||||
}
|
|
||||||
if (string.IsNullOrEmpty(_configService.Current.CacheFolder) || !Directory.Exists(_configService.Current.CacheFolder))
|
|
||||||
{
|
|
||||||
cacheDirExists = false;
|
|
||||||
Logger.LogWarning("Lightless Cache directory is not set or does not exist.");
|
|
||||||
}
|
|
||||||
if (!penDirExists || !cacheDirExists)
|
|
||||||
{
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var previousThreadPriority = Thread.CurrentThread.Priority;
|
if (string.IsNullOrEmpty(cacheFolder) || !Directory.Exists(cacheFolder))
|
||||||
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!))
|
|
||||||
{
|
{
|
||||||
|
Logger.LogWarning("Lightless Cache directory is not set or does not exist.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var prevPriority = Thread.CurrentThread.Priority;
|
||||||
|
Thread.CurrentThread.Priority = ThreadPriority.Lowest;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
penumbraFiles[folder] =
|
Logger.LogDebug("Getting files from {penumbra} and {storage}", penumbraDir, cacheFolder);
|
||||||
[
|
|
||||||
.. Directory.GetFiles(folder, "*.*", SearchOption.AllDirectories)
|
var onDiskPaths = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
.AsParallel()
|
|
||||||
.Where(f => HasAllowedExtension(f)
|
static bool IsExcludedPenumbraPath(string path)
|
||||||
&& !f.Contains(@"\bg\", StringComparison.OrdinalIgnoreCase)
|
=> path.Contains(@"\bg\", StringComparison.OrdinalIgnoreCase)
|
||||||
&& !f.Contains(@"\bgcommon\", StringComparison.OrdinalIgnoreCase)
|
|| path.Contains(@"\bgcommon\", StringComparison.OrdinalIgnoreCase)
|
||||||
&& !f.Contains(@"\ui\", StringComparison.OrdinalIgnoreCase)),
|
|| path.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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var allCacheFiles = Directory.GetFiles(_configService.Current.CacheFolder, "*.*", SearchOption.TopDirectoryOnly)
|
foreach (var file in Directory.EnumerateFiles(cacheFolder, "*.*", SearchOption.TopDirectoryOnly))
|
||||||
.AsParallel()
|
|
||||||
.Where(f =>
|
|
||||||
{
|
{
|
||||||
var val = f.Split('\\')[^1];
|
ct.ThrowIfCancellationRequested();
|
||||||
return val.Length == 40 || (val.Split('.').FirstOrDefault()?.Length ?? 0) == 40;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (ct.IsCancellationRequested) return;
|
var name = Path.GetFileName(file);
|
||||||
|
var stem = Path.GetFileNameWithoutExtension(file);
|
||||||
|
|
||||||
var allScannedFiles = (penumbraFiles.SelectMany(k => k.Value))
|
if (name.Length == 40 || stem.Length == 40)
|
||||||
.Concat(allCacheFiles)
|
onDiskPaths.Add(file);
|
||||||
.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);
|
||||||
|
|
||||||
List<FileCacheEntity> entitiesToRemove = [];
|
var fileCacheList = _fileDbManager.GetAllFileCaches();
|
||||||
List<FileCacheEntity> entitiesToUpdate = [];
|
var fileCaches = new ConcurrentQueue<FileCacheEntity>(fileCacheList);
|
||||||
Lock sync = new();
|
|
||||||
Thread[] workerThreads = new Thread[threadCount];
|
|
||||||
|
|
||||||
ConcurrentQueue<FileCacheEntity> fileCaches = new(_fileDbManager.GetAllFileCaches());
|
|
||||||
|
|
||||||
TotalFilesStorage = fileCaches.Count;
|
TotalFilesStorage = fileCaches.Count;
|
||||||
|
TotalFiles = onDiskPaths.Count + TotalFilesStorage;
|
||||||
|
|
||||||
for (int i = 0; i < threadCount; i++)
|
var validOrPresentInDb = new ConcurrentDictionary<string, byte>(StringComparer.OrdinalIgnoreCase);
|
||||||
{
|
var entitiesToUpdate = new ConcurrentBag<FileCacheEntity>();
|
||||||
Logger.LogTrace("Creating Thread {i}", i);
|
var entitiesToRemove = new ConcurrentBag<FileCacheEntity>();
|
||||||
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)
|
||||||
{
|
{
|
||||||
@@ -719,35 +753,53 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var validatedCacheResult = _fileDbManager.ValidateFileCacheEntity(workload);
|
Thread[] workerThreads = new Thread[threadCount];
|
||||||
if (validatedCacheResult.State != FileState.RequireDeletion)
|
for (int i = 0; i < threadCount; i++)
|
||||||
{
|
{
|
||||||
lock (sync) { allScannedFiles[validatedCacheResult.FileCache.ResolvedFilepath] = true; }
|
workerThreads[i] = new Thread(tcounter =>
|
||||||
|
{
|
||||||
|
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}", validatedCacheResult.FileCache.ResolvedFilepath);
|
Logger.LogTrace("To update: {path}", validated.FileCache.ResolvedFilepath);
|
||||||
lock (sync) { entitiesToUpdate.Add(validatedCacheResult.FileCache); }
|
entitiesToUpdate.Add(validated.FileCache);
|
||||||
}
|
}
|
||||||
else if (validatedCacheResult.State == FileState.RequireDeletion)
|
else if (validated.State == FileState.RequireDeletion)
|
||||||
{
|
{
|
||||||
Logger.LogTrace("To delete: {path}", validatedCacheResult.FileCache.ResolvedFilepath);
|
Logger.LogTrace("To delete: {path}", validated.FileCache.ResolvedFilepath);
|
||||||
lock (sync) { entitiesToRemove.Add(validatedCacheResult.FileCache); }
|
entitiesToRemove.Add(validated.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);
|
||||||
})
|
})
|
||||||
@@ -755,39 +807,17 @@ 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(u => u.IsAlive))
|
while (!ct.IsCancellationRequested && workerThreads.Any(t => t.IsAlive))
|
||||||
{
|
{
|
||||||
Thread.Sleep(1000);
|
ct.WaitHandle.WaitOne(250);
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
||||||
@@ -796,26 +826,40 @@ 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 = allScannedFiles.Where(c => !c.Value).Select(c => c.Key).ToList();
|
var newFiles = onDiskPaths.Where(p => !validOrPresentInDb.ContainsKey(p)).ToList();
|
||||||
foreach (var cachePath in newFiles)
|
|
||||||
|
foreach (var path in newFiles)
|
||||||
{
|
{
|
||||||
if (ct.IsCancellationRequested) break;
|
if (ct.IsCancellationRequested) break;
|
||||||
ProcessOne(cachePath);
|
ProcessOne(path);
|
||||||
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? cachePath)
|
void ProcessOne(string? filePath)
|
||||||
{
|
{
|
||||||
if (_fileDbManager == null || _ipcManager?.Penumbra == null || cachePath == null)
|
if (filePath == 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)
|
||||||
{
|
{
|
||||||
@@ -825,31 +869,45 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var entry = _fileDbManager.CreateFileEntry(cachePath);
|
var entry = _fileDbManager.CreateFileEntry(filePath);
|
||||||
if (entry == null) _ = _fileDbManager.CreateCacheEntry(cachePath);
|
if (entry == null)
|
||||||
|
_ = _fileDbManager.CreateCacheEntry(filePath);
|
||||||
}
|
}
|
||||||
catch (IOException ioex)
|
catch (IOException ioex)
|
||||||
{
|
{
|
||||||
Logger.LogDebug(ioex, "File busy or locked: {file}", cachePath);
|
Logger.LogDebug(ioex, "File busy or locked: {file}", filePath);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Logger.LogWarning(ex, "Failed adding {file}", cachePath);
|
Logger.LogWarning(ex, "Failed adding {file}", filePath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,241 +0,0 @@
|
|||||||
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,35 +115,6 @@ 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)
|
||||||
@@ -317,11 +288,6 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
using LightlessSync.Services.Compactor;
|
using LightlessSync.LightlessConfiguration;
|
||||||
|
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;
|
||||||
@@ -18,8 +20,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 ICompactorContext _context;
|
private readonly LightlessConfigService _lightlessConfigService;
|
||||||
private readonly ICompactionExecutor _compactionExecutor;
|
private readonly DalamudUtilService _dalamudUtilService;
|
||||||
|
|
||||||
private readonly Channel<string> _compactionQueue;
|
private readonly Channel<string> _compactionQueue;
|
||||||
private readonly CancellationTokenSource _compactionCts = new();
|
private readonly CancellationTokenSource _compactionCts = new();
|
||||||
@@ -57,12 +59,12 @@ public sealed partial class FileCompactor : IDisposable
|
|||||||
XPRESS16K = 3
|
XPRESS16K = 3
|
||||||
}
|
}
|
||||||
|
|
||||||
public FileCompactor(ILogger<FileCompactor> logger, ICompactorContext context, ICompactionExecutor compactionExecutor)
|
public FileCompactor(ILogger<FileCompactor> logger, LightlessConfigService lightlessConfigService, DalamudUtilService dalamudUtilService)
|
||||||
{
|
{
|
||||||
_pendingCompactions = new(StringComparer.OrdinalIgnoreCase);
|
_pendingCompactions = new(StringComparer.OrdinalIgnoreCase);
|
||||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
_logger = logger;
|
||||||
_context = context ?? throw new ArgumentNullException(nameof(context));
|
_lightlessConfigService = lightlessConfigService;
|
||||||
_compactionExecutor = compactionExecutor ?? throw new ArgumentNullException(nameof(compactionExecutor));
|
_dalamudUtilService = dalamudUtilService;
|
||||||
_isWindows = OperatingSystem.IsWindows();
|
_isWindows = OperatingSystem.IsWindows();
|
||||||
|
|
||||||
_compactionQueue = Channel.CreateUnbounded<string>(new UnboundedChannelOptions
|
_compactionQueue = Channel.CreateUnbounded<string>(new UnboundedChannelOptions
|
||||||
@@ -92,7 +94,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: _context.IsWine,
|
useShell: _dalamudUtilService.IsWine,
|
||||||
log: _logger,
|
log: _logger,
|
||||||
batchSize: 64,
|
batchSize: 64,
|
||||||
flushMs: 25,
|
flushMs: 25,
|
||||||
@@ -116,7 +118,7 @@ public sealed partial class FileCompactor : IDisposable
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var folder = _context.CacheFolder;
|
var folder = _lightlessConfigService.Current.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))
|
||||||
@@ -125,7 +127,7 @@ public sealed partial class FileCompactor : IDisposable
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var files = Directory.EnumerateFiles(folder, "*", SearchOption.AllDirectories).ToArray();
|
var files = Directory.EnumerateFiles(folder).ToArray();
|
||||||
var total = files.Length;
|
var total = files.Length;
|
||||||
Progress = $"0/{total}";
|
Progress = $"0/{total}";
|
||||||
if (total == 0) return;
|
if (total == 0) return;
|
||||||
@@ -153,7 +155,7 @@ public sealed partial class FileCompactor : IDisposable
|
|||||||
{
|
{
|
||||||
if (compress)
|
if (compress)
|
||||||
{
|
{
|
||||||
if (_context.UseCompactor)
|
if (_lightlessConfigService.Current.UseCompactor)
|
||||||
CompactFile(file, workerId);
|
CompactFile(file, workerId);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -211,7 +213,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)
|
public async Task WriteAllBytesAsync(string filePath, byte[] bytes, CancellationToken token, bool enqueueCompaction = true)
|
||||||
{
|
{
|
||||||
var dir = Path.GetDirectoryName(filePath);
|
var dir = Path.GetDirectoryName(filePath);
|
||||||
if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
|
if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
|
||||||
@@ -219,52 +221,25 @@ public sealed partial class FileCompactor : IDisposable
|
|||||||
|
|
||||||
await File.WriteAllBytesAsync(filePath, bytes, token).ConfigureAwait(false);
|
await File.WriteAllBytesAsync(filePath, bytes, token).ConfigureAwait(false);
|
||||||
|
|
||||||
if (_context.UseCompactor)
|
if (enqueueCompaction && _lightlessConfigService.Current.UseCompactor)
|
||||||
EnqueueCompaction(filePath);
|
EnqueueCompaction(filePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
public void RequestCompaction(string filePath)
|
||||||
/// 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, _context.IsWine);
|
var fsType = GetFilesystemType(fileInfo.FullName, _dalamudUtilService.IsWine);
|
||||||
|
|
||||||
if (fsType == FilesystemType.NTFS && !_context.IsWine)
|
if (fsType == FilesystemType.NTFS && !_dalamudUtilService.IsWine)
|
||||||
{
|
{
|
||||||
(bool flowControl, long value) = GetFileSizeNTFS(fileInfo);
|
(bool flowControl, long value) = GetFileSizeNTFS(fileInfo);
|
||||||
if (!flowControl)
|
if (!flowControl)
|
||||||
@@ -321,7 +296,7 @@ public sealed partial class FileCompactor : IDisposable
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var blockSize = GetBlockSizeForPath(fileInfo.FullName, _logger, _context.IsWine);
|
var blockSize = GetBlockSizeForPath(fileInfo.FullName, _logger, _dalamudUtilService.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}");
|
||||||
|
|
||||||
@@ -361,7 +336,7 @@ public sealed partial class FileCompactor : IDisposable
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var fsType = GetFilesystemType(filePath, _context.IsWine);
|
var fsType = GetFilesystemType(filePath, _dalamudUtilService.IsWine);
|
||||||
var oldSize = fi.Length;
|
var oldSize = fi.Length;
|
||||||
int blockSize = (int)(GetFileSizeOnDisk(fi) / 512);
|
int blockSize = (int)(GetFileSizeOnDisk(fi) / 512);
|
||||||
|
|
||||||
@@ -377,7 +352,7 @@ public sealed partial class FileCompactor : IDisposable
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fsType == FilesystemType.NTFS && !_context.IsWine)
|
if (fsType == FilesystemType.NTFS && !_dalamudUtilService.IsWine)
|
||||||
{
|
{
|
||||||
if (!IsWOFCompactedFile(filePath))
|
if (!IsWOFCompactedFile(filePath))
|
||||||
{
|
{
|
||||||
@@ -433,9 +408,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, _context.IsWine);
|
var fsType = GetFilesystemType(filePath, _dalamudUtilService.IsWine);
|
||||||
|
|
||||||
if (fsType == FilesystemType.NTFS && !_context.IsWine)
|
if (fsType == FilesystemType.NTFS && !_dalamudUtilService.IsWine)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -479,7 +454,7 @@ public sealed partial class FileCompactor : IDisposable
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
bool isWine = _context.IsWine;
|
bool isWine = _dalamudUtilService?.IsWine ?? false;
|
||||||
string linuxPath = isWine ? ToLinuxPathIfWine(path, isWine) : path;
|
string linuxPath = isWine ? ToLinuxPathIfWine(path, isWine) : path;
|
||||||
|
|
||||||
var opts = GetMountOptionsForPath(linuxPath);
|
var opts = GetMountOptionsForPath(linuxPath);
|
||||||
@@ -992,7 +967,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 = _context.IsWine;
|
bool isWine = _dalamudUtilService?.IsWine ?? false;
|
||||||
if (!isWine)
|
if (!isWine)
|
||||||
{
|
{
|
||||||
try { proc.WaitForExit(); } catch { /* ignore quirks */ }
|
try { proc.WaitForExit(); } catch { /* ignore quirks */ }
|
||||||
@@ -1036,7 +1011,7 @@ public sealed partial class FileCompactor : IDisposable
|
|||||||
if (string.IsNullOrWhiteSpace(filePath))
|
if (string.IsNullOrWhiteSpace(filePath))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (!_context.UseCompactor)
|
if (!_lightlessConfigService.Current.UseCompactor)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (!File.Exists(filePath))
|
if (!File.Exists(filePath))
|
||||||
@@ -1048,7 +1023,7 @@ public sealed partial class FileCompactor : IDisposable
|
|||||||
bool enqueued = false;
|
bool enqueued = false;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
bool isWine = _context.IsWine;
|
bool isWine = _dalamudUtilService?.IsWine ?? false;
|
||||||
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.
|
||||||
@@ -1101,12 +1076,9 @@ public sealed partial class FileCompactor : IDisposable
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (_context.UseCompactor && File.Exists(filePath))
|
if (_lightlessConfigService.Current.UseCompactor && File.Exists(filePath))
|
||||||
{
|
|
||||||
if (!_compactionExecutor.TryCompact(filePath))
|
|
||||||
CompactFile(filePath, workerId);
|
CompactFile(filePath, workerId);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
_globalGate.Release();
|
_globalGate.Release();
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
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,6 +25,7 @@ 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();
|
||||||
@@ -41,6 +42,8 @@ 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));
|
||||||
@@ -318,7 +321,7 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
foreach (var handler in _playerRelatedPointers)
|
foreach (var handler in _playerRelatedPointers)
|
||||||
{
|
{
|
||||||
var address = (nint)handler.Address;
|
var address = handler.Address;
|
||||||
if (address != nint.Zero)
|
if (address != nint.Zero)
|
||||||
{
|
{
|
||||||
tempMap[address] = handler;
|
tempMap[address] = handler;
|
||||||
@@ -520,51 +523,46 @@ 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;
|
||||||
var filePath = msg.FilePath;
|
if (!_cachedFrameAddresses.TryGetValue(gameObjectAddress, out var objectKind))
|
||||||
|
|
||||||
// ignore files already processed this frame
|
|
||||||
if (_cachedHandledPaths.Contains(gamePath)) return;
|
|
||||||
|
|
||||||
lock (_cacheAdditionLock)
|
|
||||||
{
|
{
|
||||||
_cachedHandledPaths.Add(gamePath);
|
if (_actorObjectService.TryGetOwnedKind(gameObjectAddress, out var ownedKind))
|
||||||
}
|
|
||||||
|
|
||||||
// replace individual mtrl stuff
|
|
||||||
if (filePath.StartsWith("|", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
{
|
||||||
filePath = filePath.Split("|")[2];
|
objectKind = ownedKind;
|
||||||
}
|
}
|
||||||
// replace filepath
|
else
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var gamePath = NormalizeGamePath(msg.GamePath);
|
||||||
|
if (string.IsNullOrEmpty(gamePath))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ignore files already processed this frame
|
||||||
|
lock (_cacheAdditionLock)
|
||||||
|
{
|
||||||
|
if (!_cachedHandledPaths.Add(gamePath))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ignore files to not handle
|
// ignore files to not handle
|
||||||
var handledTypes = IsTransientRecording ? _handledRecordingFileTypes.Concat(_handledFileTypes) : _handledFileTypes;
|
var handledTypes = IsTransientRecording ? _handledFileTypesWithRecording : _handledFileTypes;
|
||||||
if (!handledTypes.Any(type => gamePath.EndsWith(type, StringComparison.OrdinalIgnoreCase)))
|
if (!HasHandledFileType(gamePath, handledTypes))
|
||||||
{
|
{
|
||||||
lock (_cacheAdditionLock)
|
|
||||||
{
|
|
||||||
_cachedHandledPaths.Add(gamePath);
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ignore files not belonging to anything player related
|
var filePath = NormalizeFilePath(msg.FilePath);
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -579,13 +577,12 @@ 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(replacedGamePath);
|
bool transientContains = transientResources.Contains(gamePath);
|
||||||
bool semiTransientContains = SemiTransientResources.SelectMany(k => k.Value)
|
bool semiTransientContains = SemiTransientResources.Values.Any(value => value.Contains(gamePath));
|
||||||
.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}", replacedGamePath, filePath,
|
Logger.LogTrace("Not adding {replacedPath} => {filePath}, Reason: Transient: {contains}, SemiTransient: {contains2}", gamePath, filePath,
|
||||||
transientContains, semiTransientContains);
|
transientContains, semiTransientContains);
|
||||||
alreadyTransient = true;
|
alreadyTransient = true;
|
||||||
}
|
}
|
||||||
@@ -593,10 +590,10 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
if (!IsTransientRecording)
|
if (!IsTransientRecording)
|
||||||
{
|
{
|
||||||
bool isAdded = transientResources.Add(replacedGamePath);
|
bool isAdded = transientResources.Add(gamePath);
|
||||||
if (isAdded)
|
if (isAdded)
|
||||||
{
|
{
|
||||||
Logger.LogDebug("Adding {replacedGamePath} for {gameObject} ({filePath})", replacedGamePath, owner?.ToString() ?? gameObjectAddress.ToString("X"), filePath);
|
Logger.LogDebug("Adding {replacedGamePath} for {gameObject} ({filePath})", gamePath, owner?.ToString() ?? gameObjectAddress.ToString("X"), filePath);
|
||||||
SendTransients(gameObjectAddress, objectKind);
|
SendTransients(gameObjectAddress, objectKind);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -604,7 +601,7 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
if (owner != null && IsTransientRecording)
|
if (owner != null && IsTransientRecording)
|
||||||
{
|
{
|
||||||
_recordedTransients.Add(new TransientRecord(owner, replacedGamePath, filePath, alreadyTransient) { AddTransient = !alreadyTransient });
|
_recordedTransients.Add(new TransientRecord(owner, gamePath, filePath, alreadyTransient) { AddTransient = !alreadyTransient });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
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,6 +4,7 @@ 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;
|
||||||
@@ -35,7 +36,8 @@ public sealed class IpcCallerPenumbra : IpcServiceBase
|
|||||||
IDalamudPluginInterface pluginInterface,
|
IDalamudPluginInterface pluginInterface,
|
||||||
DalamudUtilService dalamudUtil,
|
DalamudUtilService dalamudUtil,
|
||||||
LightlessMediator mediator,
|
LightlessMediator mediator,
|
||||||
RedrawManager redrawManager) : base(logger, mediator, pluginInterface, PenumbraDescriptor)
|
RedrawManager redrawManager,
|
||||||
|
ActorObjectService actorObjectService) : base(logger, mediator, pluginInterface, PenumbraDescriptor)
|
||||||
{
|
{
|
||||||
_penumbraEnabled = new GetEnabledState(pluginInterface);
|
_penumbraEnabled = new GetEnabledState(pluginInterface);
|
||||||
_penumbraGetModDirectory = new GetModDirectory(pluginInterface);
|
_penumbraGetModDirectory = new GetModDirectory(pluginInterface);
|
||||||
@@ -44,7 +46,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));
|
_resources = RegisterInterop(new PenumbraResource(logger, pluginInterface, dalamudUtil, mediator, actorObjectService));
|
||||||
_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));
|
||||||
|
|
||||||
@@ -79,7 +81,10 @@ 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);
|
=> _collections.SetTemporaryModsAsync(logger, applicationId, collectionId, modPaths, "Player");
|
||||||
|
|
||||||
|
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);
|
||||||
@@ -102,11 +107,8 @@ 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 void RequestImmediateRedraw(int objectIndex, RedrawType redrawType)
|
public Task ConvertTextureFiles(ILogger logger, IReadOnlyList<TextureConversionJob> jobs, IProgress<TextureConversionProgress>? progress, CancellationToken token)
|
||||||
=> _redraw.RequestImmediateRedraw(objectIndex, redrawType);
|
=> _textures.ConvertTextureFilesAsync(logger, jobs, progress, token);
|
||||||
|
|
||||||
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,8 +1,10 @@
|
|||||||
|
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;
|
||||||
@@ -14,6 +16,10 @@ 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,
|
||||||
@@ -26,6 +32,7 @@ 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";
|
||||||
@@ -55,11 +62,16 @@ 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}";
|
||||||
var createResult = _createNamedTemporaryCollection.Invoke(name, name, out var tempCollectionId);
|
_createNamedTemporaryCollection.Invoke(name, name, out var tempCollectionId);
|
||||||
logger.LogTrace("Creating Temp Collection {CollectionName}, GUID: {CollectionId}, Result: {Result}", name, tempCollectionId, createResult);
|
logger.LogTrace("Creating Temp Collection {CollectionName}, GUID: {CollectionId}", name, tempCollectionId);
|
||||||
return (tempCollectionId, name);
|
return (tempCollectionId, name);
|
||||||
}).ConfigureAwait(false);
|
}).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (collectionId != Guid.Empty)
|
||||||
|
{
|
||||||
|
_activeTemporaryCollections[collectionId] = collectionName;
|
||||||
|
}
|
||||||
|
|
||||||
return collectionId;
|
return collectionId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,27 +89,46 @@ 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(ILogger logger, Guid applicationId, Guid collectionId, Dictionary<string, string> modPaths)
|
public async Task SetTemporaryModsAsync(
|
||||||
|
ILogger logger,
|
||||||
|
Guid applicationId,
|
||||||
|
Guid collectionId,
|
||||||
|
Dictionary<string, string> modPaths,
|
||||||
|
string scope)
|
||||||
{
|
{
|
||||||
if (!IsAvailable || collectionId == Guid.Empty)
|
if (!IsAvailable || collectionId == Guid.Empty)
|
||||||
{
|
|
||||||
return;
|
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))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var gamePath = kvp.Key.Replace('\\', '/').ToLowerInvariant();
|
||||||
|
normalized[gamePath] = kvp.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
await DalamudUtil.RunOnFrameworkThread(() =>
|
await DalamudUtil.RunOnFrameworkThread(() =>
|
||||||
{
|
{
|
||||||
foreach (var mod in modPaths)
|
foreach (var mod in normalized)
|
||||||
{
|
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("LightlessChara_Files", collectionId, 0);
|
var removeResult = _removeTemporaryMod.Invoke(modName, collectionId, 0);
|
||||||
logger.LogTrace("[{ApplicationId}] Removing temp files mod for {CollectionId}, Success: {Result}", applicationId, collectionId, removeResult);
|
logger.LogTrace("[{ApplicationId}] Removing temp mod {ModName} for {CollectionId}, Success: {Result}",
|
||||||
|
applicationId, modName, collectionId, removeResult);
|
||||||
|
|
||||||
var addResult = _addTemporaryMod.Invoke("LightlessChara_Files", collectionId, modPaths, string.Empty, 0);
|
if (normalized.Count == 0)
|
||||||
logger.LogTrace("[{ApplicationId}] Setting temp files mod for {CollectionId}, Success: {Result}", applicationId, collectionId, addResult);
|
return;
|
||||||
|
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,5 +149,67 @@ 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,10 +2,9 @@ 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;
|
||||||
|
|
||||||
@@ -13,6 +12,7 @@ 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,8 +24,10 @@ public sealed class PenumbraResource : PenumbraBase
|
|||||||
ILogger logger,
|
ILogger logger,
|
||||||
IDalamudPluginInterface pluginInterface,
|
IDalamudPluginInterface pluginInterface,
|
||||||
DalamudUtilService dalamudUtil,
|
DalamudUtilService dalamudUtil,
|
||||||
LightlessMediator mediator) : base(logger, pluginInterface, dalamudUtil, mediator)
|
LightlessMediator 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);
|
||||||
@@ -43,33 +45,17 @@ public sealed class PenumbraResource : PenumbraBase
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var requestId = Guid.NewGuid();
|
return await DalamudUtil.RunOnFrameworkThread(() =>
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.LogTrace("[{requestId}] Invoking Penumbra.GetGameObjectResourcePaths for index {index}", requestId, idx.Value);
|
return _gameObjectResourcePaths.Invoke(idx.Value)[0];
|
||||||
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()
|
||||||
@@ -93,10 +79,22 @@ 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 && string.Compare(gamePath, resolvedPath, ignoreCase: true, CultureInfo.InvariantCulture) != 0)
|
if (ptr == nint.Zero)
|
||||||
{
|
{
|
||||||
Mediator.Publish(new PenumbraResourceLoadMessage(ptr, gamePath, resolvedPath));
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!_actorObjectService.TryGetOwnedKind(ptr, out _))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.Compare(gamePath, resolvedPath, StringComparison.OrdinalIgnoreCase) == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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, bool requestRedraw)
|
public async Task ConvertTextureFilesAsync(ILogger logger, IReadOnlyList<TextureConversionJob> jobs, IProgress<TextureConversionProgress>? progress, CancellationToken token)
|
||||||
{
|
{
|
||||||
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 (requestRedraw && completedJobs > 0 && !token.IsCancellationRequested)
|
if (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.LogDebug("Converting texture {Input} -> {Output} ({Target})", job.InputFile, job.OutputFile, job.TargetType);
|
logger.LogInformation("Converting texture {Input} -> {Output} ({Target})", job.InputFile, job.OutputFile, job.TargetType);
|
||||||
var convertTask = _convertTextureFile.Invoke(job.InputFile, job.OutputFile, job.TargetType, job.IncludeMipMaps);
|
var convertTask = _convertTextureFile.Invoke(job.InputFile, job.OutputFile, job.TargetType, job.IncludeMipMaps);
|
||||||
await convertTask.ConfigureAwait(false);
|
await convertTask.ConfigureAwait(false);
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,11 @@
|
|||||||
using System.Globalization;
|
using LightlessSync.WebAPI;
|
||||||
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, TempCollectionConfigService tempCollectionConfigService,
|
ServerConfigService serverConfigService) : IHostedService
|
||||||
LightlessConfigService lightlessConfigService) : IHostedService
|
|
||||||
{
|
{
|
||||||
private readonly ILogger<ConfigurationMigrator> _logger = logger;
|
private readonly ILogger<ConfigurationMigrator> _logger = logger;
|
||||||
|
|
||||||
@@ -57,8 +51,6 @@ 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)
|
||||||
@@ -71,273 +63,4 @@ 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,10 +72,7 @@ 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);
|
||||||
@@ -107,14 +104,13 @@ 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 = !isTempCollections
|
WriteIndented = true
|
||||||
})).ConfigureAwait(false);
|
})).ConfigureAwait(false);
|
||||||
File.Move(temp, config.ConfigurationPath, true);
|
File.Move(temp, config.ConfigurationPath, true);
|
||||||
config.UpdateLastWriteTime();
|
config.UpdateLastWriteTime();
|
||||||
|
|||||||
@@ -12,9 +12,6 @@ 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;
|
||||||
@@ -26,9 +23,6 @@ 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,8 +32,6 @@ 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;
|
||||||
@@ -162,5 +160,6 @@ 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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,156 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace LightlessSync.LightlessConfiguration.Configurations;
|
||||||
|
|
||||||
|
public class PenumbraJanitorConfig : ILightlessConfiguration
|
||||||
|
{
|
||||||
|
public int Version { get; set; } = 0;
|
||||||
|
|
||||||
|
public HashSet<Guid> OrphanableTempCollections { get; set; } = [];
|
||||||
|
}
|
||||||
@@ -21,26 +21,16 @@ 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; } = ModelDecimationDefaults.EnableAutoDecimation;
|
public bool EnableModelDecimation { get; set; } = false;
|
||||||
public int ModelDecimationTriangleThreshold { get; set; } = ModelDecimationDefaults.TriangleThreshold;
|
public int ModelDecimationTriangleThreshold { get; set; } = 20_000;
|
||||||
public double ModelDecimationTargetRatio { get; set; } = ModelDecimationDefaults.TargetRatio;
|
public double ModelDecimationTargetRatio { get; set; } = 0.8;
|
||||||
public bool ModelDecimationNormalizeTangents { get; set; } = ModelDecimationDefaults.NormalizeTangents;
|
public bool KeepOriginalModelFiles { get; set; } = true;
|
||||||
public bool ModelDecimationAvoidBodyIntersection { get; set; } = ModelDecimationDefaults.AvoidBodyIntersection;
|
public bool SkipModelDecimationForPreferredPairs { get; set; } = true;
|
||||||
public ModelDecimationAdvancedSettings ModelDecimationAdvanced { get; set; } = new();
|
public bool ModelDecimationAllowBody { get; set; } = false;
|
||||||
public int BatchModelDecimationTriangleThreshold { get; set; } = ModelDecimationDefaults.BatchTriangleThreshold;
|
public bool ModelDecimationAllowFaceHead { get; set; } = false;
|
||||||
public double BatchModelDecimationTargetRatio { get; set; } = ModelDecimationDefaults.BatchTargetRatio;
|
public bool ModelDecimationAllowTail { get; set; } = false;
|
||||||
public bool BatchModelDecimationNormalizeTangents { get; set; } = ModelDecimationDefaults.BatchNormalizeTangents;
|
public bool ModelDecimationAllowClothing { get; set; } = true;
|
||||||
public bool BatchModelDecimationAvoidBodyIntersection { get; set; } = ModelDecimationDefaults.BatchAvoidBodyIntersection;
|
public bool ModelDecimationAllowAccessories { get; set; } = true;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
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; } = [];
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
namespace LightlessSync.LightlessConfiguration.Models;
|
|
||||||
|
|
||||||
public sealed class OrphanableTempCollectionEntry
|
|
||||||
{
|
|
||||||
public Guid Id { get; set; }
|
|
||||||
public DateTime RegisteredAtUtc { get; set; } = DateTime.MinValue;
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
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.2.83</Version>
|
<Version>2.0.3</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,8 +85,6 @@
|
|||||||
|
|
||||||
<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" />
|
||||||
@@ -111,12 +109,4 @@
|
|||||||
<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,7 +19,6 @@ 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,
|
||||||
@@ -30,8 +29,7 @@ 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;
|
||||||
@@ -42,7 +40,6 @@ public class FileDownloadManagerFactory
|
|||||||
_textureDownscaleService = textureDownscaleService;
|
_textureDownscaleService = textureDownscaleService;
|
||||||
_modelDecimationService = modelDecimationService;
|
_modelDecimationService = modelDecimationService;
|
||||||
_textureMetadataHelper = textureMetadataHelper;
|
_textureMetadataHelper = textureMetadataHelper;
|
||||||
_downloadDeduplicator = downloadDeduplicator;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public FileDownloadManager Create()
|
public FileDownloadManager Create()
|
||||||
@@ -56,7 +53,6 @@ public class FileDownloadManagerFactory
|
|||||||
_configService,
|
_configService,
|
||||||
_textureDownscaleService,
|
_textureDownscaleService,
|
||||||
_modelDecimationService,
|
_modelDecimationService,
|
||||||
_textureMetadataHelper,
|
_textureMetadataHelper);
|
||||||
_downloadDeduplicator);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using LightlessSync.API.Data.Enum;
|
using Dalamud.Plugin.Services;
|
||||||
|
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;
|
||||||
@@ -11,6 +12,7 @@ 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;
|
||||||
|
|
||||||
@@ -18,12 +20,14 @@ 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)
|
||||||
@@ -36,6 +40,7 @@ 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,4 +1,5 @@
|
|||||||
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;
|
||||||
@@ -34,7 +35,7 @@ public class PlayerDataFactory
|
|||||||
private const int _maxTransientResolvedEntries = 1000;
|
private const int _maxTransientResolvedEntries = 1000;
|
||||||
|
|
||||||
// Character build caches
|
// Character build caches
|
||||||
private readonly TaskRegistry<nint> _characterBuildInflight = new();
|
private readonly ConcurrentDictionary<nint, Task<CharacterDataFragment>> _characterBuildInflight = new();
|
||||||
private readonly ConcurrentDictionary<nint, CacheEntry> _characterBuildCache = new();
|
private readonly ConcurrentDictionary<nint, CacheEntry> _characterBuildCache = new();
|
||||||
|
|
||||||
// Time out thresholds
|
// Time out thresholds
|
||||||
@@ -119,30 +120,29 @@ public class PlayerDataFactory
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static readonly int _drawObjectOffset =
|
private static readonly int _characterGameObjectOffset =
|
||||||
|
(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(() =>
|
=> await _dalamudUtil.RunOnFrameworkThread(() => CheckForNullDrawObjectSafe(playerPointer))
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
private static bool CheckForNullDrawObjectSafe(nint playerPointer)
|
||||||
{
|
{
|
||||||
nint basePtr = playerPointer;
|
if (playerPointer == nint.Zero)
|
||||||
|
|
||||||
if (!PtrGuard.LooksLikePtr(basePtr))
|
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
nint drawObjAddr = basePtr + _drawObjectOffset;
|
var drawObjPtrAddress = playerPointer + _characterGameObjectOffset + _gameObjectDrawObjectOffset;
|
||||||
|
|
||||||
if (!PtrGuard.IsReadable(drawObjAddr, (nuint)IntPtr.Size))
|
// Read the DrawObject pointer from memory
|
||||||
|
if (!MemoryProcessProbe.TryReadIntPtr(drawObjPtrAddress, out var drawObj))
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
if (!PtrGuard.TryReadIntPtr(drawObjAddr, out var drawObj))
|
return drawObj == nint.Zero;
|
||||||
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 CacheEntry cached) && IsCacheFresh(cached) && !_characterBuildInflight.TryGetExisting(key, out _))
|
if (_characterBuildCache.TryGetValue(key, out var cached) && IsCacheFresh(cached) && !_characterBuildInflight.ContainsKey(key))
|
||||||
return cached.Fragment;
|
return cached.Fragment;
|
||||||
|
|
||||||
Task<CharacterDataFragment> buildTask = _characterBuildInflight.GetOrStart(key, () => BuildAndCacheAsync(obj, key));
|
var buildTask = _characterBuildInflight.GetOrAdd(key, valueFactory: k => BuildAndCacheAsync(obj, k));
|
||||||
|
|
||||||
if (_characterBuildCache.TryGetValue(key, out cached))
|
if (_characterBuildCache.TryGetValue(key, out cached))
|
||||||
{
|
{
|
||||||
@@ -172,15 +172,22 @@ 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);
|
||||||
CharacterDataFragment fragment = await CreateCharacterDataInternal(obj, cts.Token).ConfigureAwait(false);
|
var 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()
|
||||||
{
|
{
|
||||||
@@ -234,28 +241,7 @@ public class PlayerDataFactory
|
|||||||
getMoodlesData = _ipcManager.Moodles.GetStatusAsync(playerRelatedObject.Address);
|
getMoodlesData = _ipcManager.Moodles.GetStatusAsync(playerRelatedObject.Address);
|
||||||
}
|
}
|
||||||
|
|
||||||
Guid penumbraRequestId = Guid.Empty;
|
var resolvedPaths = await _ipcManager.Penumbra.GetCharacterData(_logger, playerRelatedObject).ConfigureAwait(false) ?? throw new InvalidOperationException("Penumbra returned null data; couldn't proceed with character");
|
||||||
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);
|
||||||
@@ -474,7 +460,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(transientPaths, new HashSet<string>(StringComparer.Ordinal))
|
var resolved = await GetFileReplacementsFromPaths(obj, transientPaths, new HashSet<string>(StringComparer.Ordinal))
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
if (_maxTransientResolvedEntries > 0 && resolved.Count > _maxTransientResolvedEntries)
|
if (_maxTransientResolvedEntries > 0 && resolved.Count > _maxTransientResolvedEntries)
|
||||||
@@ -676,6 +662,7 @@ 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)
|
||||||
{
|
{
|
||||||
@@ -690,6 +677,59 @@ 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,46 +1,68 @@
|
|||||||
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
using Dalamud.Game.ClientState.Objects.Enums;
|
||||||
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
using Dalamud.Game.ClientState.Objects.Types;
|
||||||
|
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 object _frameworkUpdateGate = new();
|
private readonly Lock _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();
|
||||||
|
|
||||||
public GameObjectHandler(ILogger<GameObjectHandler> logger, PerformanceCollectorService performanceCollector,
|
/// <summary>
|
||||||
LightlessMediator mediator, DalamudUtilService dalamudUtil, ObjectKind objectKind, Func<IntPtr> getAddress, bool ownedObject = true) : base(logger, mediator)
|
/// Constructor for GameObjectHandler
|
||||||
|
/// </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)
|
||||||
{
|
{
|
||||||
@@ -50,43 +72,36 @@ 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, (_) =>
|
Mediator.Subscribe<CutsceneStartMessage>(this, _ => _haltProcessing = true);
|
||||||
{
|
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)
|
if (msg.Address == Address) _haltProcessing = true;
|
||||||
{
|
|
||||||
_haltProcessing = true;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
Mediator.Subscribe<PenumbraEndRedrawMessage>(this, (msg) =>
|
Mediator.Subscribe<PenumbraEndRedrawMessage>(this, msg =>
|
||||||
{
|
{
|
||||||
if (msg.Address == Address)
|
if (msg.Address == Address) _haltProcessing = false;
|
||||||
{
|
|
||||||
_haltProcessing = false;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
Mediator.Publish(new GameObjectHandlerCreatedMessage(this, _isOwnedObject));
|
Mediator.Publish(new GameObjectHandlerCreatedMessage(this, _isOwnedObject));
|
||||||
|
|
||||||
_dalamudUtil.RunOnFrameworkThread(CheckAndUpdateObject).GetAwaiter().GetResult();
|
_dalamudUtil.EnsureIsOnFramework();
|
||||||
|
CheckAndUpdateObject(allowPublish: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Draw Condition Enum
|
||||||
|
/// </summary>
|
||||||
public enum DrawCondition
|
public enum DrawCondition
|
||||||
{
|
{
|
||||||
None,
|
None,
|
||||||
@@ -97,6 +112,7 @@ 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; }
|
||||||
@@ -107,18 +123,21 @@ 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];
|
|
||||||
|
|
||||||
public async Task ActOnFrameworkAfterEnsureNoDrawAsync(Action<Dalamud.Game.ClientState.Objects.Types.ICharacter> act, CancellationToken token)
|
/// <summary>
|
||||||
|
/// 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 Dalamud.Game.ClientState.Objects.Types.ICharacter chara)
|
if (gameObj is ICharacter chara)
|
||||||
{
|
{
|
||||||
act.Invoke(chara);
|
act.Invoke(chara);
|
||||||
}
|
}
|
||||||
@@ -129,6 +148,11 @@ 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))
|
||||||
@@ -141,11 +165,18 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Dalamud.Game.ClientState.Objects.Types.IGameObject? GetGameObject()
|
/// <summary>
|
||||||
|
/// 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;
|
||||||
@@ -154,182 +185,203 @@ 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);
|
||||||
|
|
||||||
private unsafe void CheckAndUpdateObject(bool allowPublish)
|
/// <summary>
|
||||||
|
/// 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;
|
||||||
|
|
||||||
var nextAddr = _getAddress();
|
Address = _getAddress();
|
||||||
|
|
||||||
if (nextAddr != IntPtr.Zero && !PtrGuard.LooksLikePtr(nextAddr))
|
IGameObject? obj = null;
|
||||||
|
ICharacter? chara = null;
|
||||||
|
|
||||||
|
if (Address != nint.Zero)
|
||||||
{
|
{
|
||||||
nextAddr = IntPtr.Zero;
|
// Try get object
|
||||||
}
|
obj = TryGetObjectByAddress(Address);
|
||||||
|
|
||||||
if (nextAddr != IntPtr.Zero &&
|
if (obj is not null)
|
||||||
!PtrGuard.IsReadable(nextAddr, (nuint)sizeof(FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject)))
|
|
||||||
{
|
{
|
||||||
nextAddr = IntPtr.Zero;
|
EntityId = obj.EntityId;
|
||||||
}
|
|
||||||
|
|
||||||
Address = nextAddr;
|
DrawObjectAddress = Address;
|
||||||
|
|
||||||
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 = IntPtr.Zero;
|
DrawObjectAddress = nint.Zero;
|
||||||
EntityId = uint.MaxValue;
|
EntityId = uint.MaxValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
CurrentDrawCondition = (Address != IntPtr.Zero && DrawObjectAddress != IntPtr.Zero)
|
// Update draw condition
|
||||||
? IsBeingDrawnUnsafe()
|
CurrentDrawCondition = IsBeingDrawnSafe(obj, chara);
|
||||||
: 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;
|
||||||
|
|
||||||
if (Address != IntPtr.Zero && DrawObjectAddress != IntPtr.Zero
|
// Name change check
|
||||||
&& PtrGuard.IsReadable(Address, (nuint)sizeof(Character))
|
bool nameChange = false;
|
||||||
&& PtrGuard.IsReadable(DrawObjectAddress, (nuint)sizeof(DrawObject)))
|
if (nameString is not null)
|
||||||
{
|
{
|
||||||
var chara = (Character*)Address;
|
nameChange = !string.Equals(nameString, Name, StringComparison.Ordinal);
|
||||||
var drawObj = (DrawObject*)DrawObjectAddress;
|
|
||||||
|
|
||||||
var objType = drawObj->Object.GetObjectType();
|
|
||||||
var isHuman = objType == ObjectType.CharacterBase
|
|
||||||
&& ((CharacterBase*)drawObj)->GetModelType() == CharacterBase.ModelType.Human;
|
|
||||||
|
|
||||||
nameString ??= chara->GameObject.NameString;
|
|
||||||
var nameChange = !string.Equals(nameString, Name, StringComparison.Ordinal);
|
|
||||||
if (nameChange) Name = nameString;
|
if (nameChange) Name = nameString;
|
||||||
|
}
|
||||||
|
|
||||||
bool equipDiff = false;
|
// Customize data change check
|
||||||
|
bool customizeDiff = false;
|
||||||
if (isHuman)
|
if (chara is not null)
|
||||||
{
|
{
|
||||||
if (PtrGuard.IsReadable(DrawObjectAddress, (nuint)sizeof(Human)))
|
// Class job change check
|
||||||
{
|
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 = classJob;
|
_classJob = (byte)classJob;
|
||||||
Mediator.Publish(new ClassJobChangedMessage(this));
|
Mediator.Publish(new ClassJobChangedMessage(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
equipDiff = CompareAndUpdateEquipByteData((byte*)&((Human*)drawObj)->Head);
|
// Customize data comparison
|
||||||
|
customizeDiff = CompareAndUpdateCustomizeData(chara.Customize);
|
||||||
|
|
||||||
ref var mh = ref chara->DrawData.Weapon(WeaponSlot.MainHand);
|
// Census update publish
|
||||||
ref var oh = ref chara->DrawData.Weapon(WeaponSlot.OffHand);
|
if (_isOwnedObject && ObjectKind == ObjectKind.Player && chara.Customize.Length > (int)CustomizeIndex.Tribe)
|
||||||
|
|
||||||
equipDiff |= CompareAndUpdateMainHand((Weapon*)mh.DrawObject);
|
|
||||||
equipDiff |= CompareAndUpdateOffHand((Weapon*)oh.DrawObject);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
isHuman = false;
|
var gender = chara.Customize[(int)CustomizeIndex.Gender];
|
||||||
}
|
var raceId = chara.Customize[(int)CustomizeIndex.Race];
|
||||||
}
|
var tribeId = chara.Customize[(int)CustomizeIndex.Tribe];
|
||||||
|
|
||||||
if (!isHuman)
|
if (gender != Gender || raceId != RaceId || tribeId != TribeId)
|
||||||
{
|
|
||||||
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;
|
||||||
|
|
||||||
private unsafe bool CompareAndUpdateCustomizeData(Span<byte> customizeData)
|
// Draw Object check
|
||||||
|
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;
|
||||||
|
|
||||||
for (int i = 0; i < customizeData.Length; i++)
|
// Resize if needed
|
||||||
|
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)
|
||||||
@@ -342,54 +394,9 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
|||||||
return hasChanges;
|
return hasChanges;
|
||||||
}
|
}
|
||||||
|
|
||||||
private unsafe bool CompareAndUpdateEquipByteData(byte* equipSlotData)
|
/// <summary>
|
||||||
{
|
/// Framework update method
|
||||||
bool hasChanges = false;
|
/// </summary>
|
||||||
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
|
||||||
@@ -403,6 +410,10 @@ 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();
|
||||||
@@ -417,6 +428,9 @@ 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)
|
||||||
@@ -425,6 +439,9 @@ 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)
|
||||||
@@ -439,24 +456,9 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private unsafe DrawCondition IsBeingDrawnUnsafe()
|
/// <summary>
|
||||||
{
|
/// Zone switch end handling
|
||||||
if (Address == IntPtr.Zero) return DrawCondition.ObjectZero;
|
/// </summary>
|
||||||
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;
|
||||||
@@ -467,7 +469,7 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
|||||||
}
|
}
|
||||||
catch (ObjectDisposedException)
|
catch (ObjectDisposedException)
|
||||||
{
|
{
|
||||||
// ignore
|
// ignore canelled after disposed
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -475,6 +477,9 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Zone switch start handling
|
||||||
|
/// </summary>
|
||||||
private void ZoneSwitchStart()
|
private void ZoneSwitchStart()
|
||||||
{
|
{
|
||||||
if (!_isOwnedObject) return;
|
if (!_isOwnedObject) return;
|
||||||
|
|||||||
461
LightlessSync/PlayerData/Handlers/OwnedObjectHandler.cs
Normal file
461
LightlessSync/PlayerData/Handlers/OwnedObjectHandler.cs
Normal file
@@ -0,0 +1,461 @@
|
|||||||
|
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,4 +1,5 @@
|
|||||||
using LightlessSync.API.Data;
|
using LightlessSync.API.Data;
|
||||||
|
using LightlessSync.API.Data.Enum;
|
||||||
|
|
||||||
namespace LightlessSync.PlayerData.Pairs;
|
namespace LightlessSync.PlayerData.Pairs;
|
||||||
|
|
||||||
@@ -16,25 +17,51 @@ 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);
|
||||||
Task EnsurePerformanceMetricsAsync(CancellationToken cancellationToken);
|
void HardReapplyLastData();
|
||||||
bool FetchPerformanceMetricsFromCache();
|
bool FetchPerformanceMetricsFromCache();
|
||||||
void LoadCachedCharacterData(CharacterData data);
|
void LoadCachedCharacterData(CharacterData data);
|
||||||
void SetUploading(bool uploading);
|
void SetUploading(bool uploading);
|
||||||
|
|||||||
@@ -82,33 +82,69 @@ 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;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
if (args.Target is not MenuTargetDefault target || target.TargetObjectId != handler.PlayerCharacterId)
|
if (args.Target is not MenuTargetDefault target)
|
||||||
{
|
|
||||||
return;
|
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)
|
||||||
|
{
|
||||||
if (!IsPaused)
|
if (!IsPaused)
|
||||||
{
|
{
|
||||||
UiSharedService.AddContextMenuItem(args, name: "Open Profile", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () =>
|
UiSharedService.AddContextMenuItem(
|
||||||
|
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(args, name: "Reapply last data", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () =>
|
UiSharedService.AddContextMenuItem(
|
||||||
|
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(args, name: "Change Permissions", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () =>
|
UiSharedService.AddContextMenuItem(
|
||||||
|
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;
|
||||||
@@ -116,7 +152,12 @@ public class Pair
|
|||||||
|
|
||||||
if (IsPaused)
|
if (IsPaused)
|
||||||
{
|
{
|
||||||
UiSharedService.AddContextMenuItem(args, name: "Toggle Unpause State", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () =>
|
UiSharedService.AddContextMenuItem(
|
||||||
|
args,
|
||||||
|
name: "Toggle Unpause State",
|
||||||
|
prefixChar: 'L',
|
||||||
|
colorMenuItem: _lightlessPrefixColor,
|
||||||
|
onClick: () =>
|
||||||
{
|
{
|
||||||
_ = _apiController.Value.UnpauseAsync(UserData);
|
_ = _apiController.Value.UnpauseAsync(UserData);
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
@@ -124,18 +165,31 @@ public class Pair
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
UiSharedService.AddContextMenuItem(args, name: "Toggle Pause State", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () =>
|
UiSharedService.AddContextMenuItem(
|
||||||
|
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(args, name: "Cycle Pause State", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () =>
|
UiSharedService.AddContextMenuItem(
|
||||||
|
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)
|
||||||
@@ -160,6 +214,18 @@ 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();
|
||||||
@@ -217,6 +283,12 @@ 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,
|
||||||
@@ -225,6 +297,9 @@ 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,
|
||||||
@@ -235,6 +310,17 @@ 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, forceDisposal: true);
|
_ = _handlerRegistry.DeregisterOfflinePair(registrationResult.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
_mediator.Publish(new ClearProfileUserDataMessage(user));
|
_mediator.Publish(new ClearProfileUserDataMessage(user));
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ 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,
|
||||||
@@ -18,25 +21,50 @@ 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(
|
||||||
false,
|
HasHandler: false,
|
||||||
false,
|
HandlerInitialized: false,
|
||||||
false,
|
HandlerVisible: false,
|
||||||
false,
|
HandlerScheduledForDeletion: false,
|
||||||
null,
|
LastDataReceivedAt: null,
|
||||||
null,
|
LastApplyAttemptAt: null,
|
||||||
null,
|
LastSuccessfulApplyAt: null,
|
||||||
null,
|
InvisibleSinceUtc: null,
|
||||||
Array.Empty<string>(),
|
VisibilityEvictionDueAtUtc: null,
|
||||||
false,
|
VisibilityEvictionRemainingSeconds: null,
|
||||||
false,
|
LastFailureReason: null,
|
||||||
0,
|
BlockingConditions: [],
|
||||||
0,
|
IsApplying: false,
|
||||||
false,
|
IsDownloading: false,
|
||||||
false,
|
PendingDownloadCount: 0,
|
||||||
0,
|
ForbiddenDownloadCount: 0,
|
||||||
0,
|
PendingModReapply: false,
|
||||||
0);
|
ModApplyDeferred: false,
|
||||||
|
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,6 +40,7 @@ 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,
|
||||||
@@ -63,7 +64,8 @@ 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;
|
||||||
@@ -87,6 +89,7 @@ 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)
|
||||||
@@ -105,6 +108,7 @@ internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory
|
|||||||
_pluginWarningNotificationManager,
|
_pluginWarningNotificationManager,
|
||||||
dalamudUtilService,
|
dalamudUtilService,
|
||||||
_framework,
|
_framework,
|
||||||
|
_objectTable,
|
||||||
actorObjectService,
|
actorObjectService,
|
||||||
_lifetime,
|
_lifetime,
|
||||||
_fileCacheManager,
|
_fileCacheManager,
|
||||||
|
|||||||
@@ -136,7 +136,6 @@ 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)
|
||||||
@@ -357,7 +356,6 @@ public sealed class PairHandlerRegistry : IDisposable
|
|||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
_pairPerformanceMetricsCache.Clear(handler.Ident);
|
_pairPerformanceMetricsCache.Clear(handler.Ident);
|
||||||
_pairStateCache.Clear(handler.Ident);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -379,7 +377,6 @@ public sealed class PairHandlerRegistry : IDisposable
|
|||||||
{
|
{
|
||||||
handler.Dispose();
|
handler.Dispose();
|
||||||
_pairPerformanceMetricsCache.Clear(handler.Ident);
|
_pairPerformanceMetricsCache.Clear(handler.Ident);
|
||||||
_pairStateCache.Clear(handler.Ident);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -404,7 +401,6 @@ public sealed class PairHandlerRegistry : IDisposable
|
|||||||
if (TryFinalizeHandlerRemoval(handler))
|
if (TryFinalizeHandlerRemoval(handler))
|
||||||
{
|
{
|
||||||
handler.Dispose();
|
handler.Dispose();
|
||||||
_pairStateCache.Clear(handler.Ident);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -271,20 +271,7 @@ public sealed class PairLedger : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_ = Task.Run(async () =>
|
handler.ApplyLastReceivedData(forced: true);
|
||||||
{
|
|
||||||
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,9 +160,8 @@ 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), ident));
|
return PairOperationResult<PairRegistration>.Ok(new PairRegistration(new PairUniqueIdentifier(user.UID), connection.Ident));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -531,7 +530,6 @@ public sealed class PairManager
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var ident = connection.Ident;
|
|
||||||
if (connection.IsOnline)
|
if (connection.IsOnline)
|
||||||
{
|
{
|
||||||
connection.SetOffline();
|
connection.SetOffline();
|
||||||
@@ -544,7 +542,7 @@ public sealed class PairManager
|
|||||||
shell.Users.Remove(userId);
|
shell.Users.Remove(userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new PairRegistration(new PairUniqueIdentifier(userId), ident);
|
return new PairRegistration(new PairUniqueIdentifier(userId), connection.Ident);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static PairConnection CreateFromFullData(UserFullPairDto dto)
|
public static PairConnection CreateFromFullData(UserFullPairDto dto)
|
||||||
|
|||||||
@@ -76,7 +76,6 @@ 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,6 +110,7 @@ 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
|
||||||
@@ -125,21 +126,16 @@ 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>();
|
||||||
@@ -336,7 +332,8 @@ 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>>(),
|
||||||
@@ -431,8 +428,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));
|
||||||
@@ -445,8 +442,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>());
|
||||||
@@ -522,7 +519,6 @@ 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,6 +14,7 @@ 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;
|
||||||
|
|
||||||
@@ -57,6 +58,8 @@ 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,
|
||||||
@@ -74,7 +77,6 @@ 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;
|
||||||
@@ -93,11 +95,9 @@ 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,9 +342,21 @@ public sealed class ActorObjectService : IHostedService, IDisposable, IMediatorS
|
|||||||
|
|
||||||
public Task StopAsync(CancellationToken cancellationToken)
|
public Task StopAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
_warmStartRan = 0;
|
||||||
|
|
||||||
DisposeHooks();
|
DisposeHooks();
|
||||||
ClearTrackingState();
|
_activePlayers.Clear();
|
||||||
|
_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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -496,7 +508,10 @@ 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(GameObject* gameObject, DalamudObjectKind objectKind, bool isLocalPlayer)
|
private unsafe (LightlessObjectKind? OwnedKind, uint OwnerEntityId) DetermineOwnedKind(
|
||||||
|
GameObject* gameObject,
|
||||||
|
DalamudObjectKind objectKind,
|
||||||
|
bool isLocalPlayer)
|
||||||
{
|
{
|
||||||
if (gameObject == null)
|
if (gameObject == null)
|
||||||
return (null, 0);
|
return (null, 0);
|
||||||
@@ -508,6 +523,7 @@ 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);
|
||||||
@@ -519,9 +535,7 @@ 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
|
if (expectedMinionOrMount != nint.Zero && (nint)gameObject == expectedMinionOrMount)
|
||||||
&& (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);
|
||||||
@@ -531,20 +545,16 @@ public sealed class ActorObjectService : IHostedService, IDisposable, IMediatorS
|
|||||||
if (objectKind != DalamudObjectKind.BattleNpc)
|
if (objectKind != DalamudObjectKind.BattleNpc)
|
||||||
return (null, ownerId);
|
return (null, ownerId);
|
||||||
|
|
||||||
if (ownerId != localEntityId)
|
if (ownerId != 0 && ownerId != localEntityId)
|
||||||
return (null, ownerId);
|
return (null, ownerId);
|
||||||
|
|
||||||
var expectedPet = GetPetAddress(localPlayerAddress, localEntityId);
|
var expectedPet = GetPetAddress(localPlayerAddress, localEntityId);
|
||||||
if (expectedPet != nint.Zero
|
if (expectedPet != nint.Zero && (nint)gameObject == expectedPet)
|
||||||
&& (nint)gameObject == expectedPet
|
return (LightlessObjectKind.Pet, ownerId != 0 ? ownerId : localEntityId);
|
||||||
&& IsPlayerRelatedOwnedAddress(expectedPet, LightlessObjectKind.Pet))
|
|
||||||
return (LightlessObjectKind.Pet, ownerId);
|
|
||||||
|
|
||||||
var expectedCompanion = GetCompanionAddress(localPlayerAddress, localEntityId);
|
var expectedCompanion = GetCompanionAddress(localPlayerAddress, localEntityId);
|
||||||
if (expectedCompanion != nint.Zero
|
if (expectedCompanion != nint.Zero && (nint)gameObject == expectedCompanion)
|
||||||
&& (nint)gameObject == expectedCompanion
|
return (LightlessObjectKind.Companion, ownerId != 0 ? ownerId : localEntityId);
|
||||||
&& IsPlayerRelatedOwnedAddress(expectedCompanion, LightlessObjectKind.Companion))
|
|
||||||
return (LightlessObjectKind.Companion, ownerId);
|
|
||||||
|
|
||||||
return (null, ownerId);
|
return (null, ownerId);
|
||||||
}
|
}
|
||||||
@@ -571,19 +581,124 @@ 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;
|
||||||
|
|
||||||
var playerObject = (GameObject*)localPlayerAddress;
|
foreach (var obj in _objectTable)
|
||||||
var candidateAddress = _objectTable.GetObjectAddress(playerObject->ObjectIndex + 1);
|
{
|
||||||
if (candidateAddress == nint.Zero)
|
if (obj is null || obj.Address == nint.Zero || obj.Address == localPlayerAddress)
|
||||||
return nint.Zero;
|
continue;
|
||||||
|
|
||||||
var candidate = (GameObject*)candidateAddress;
|
if (obj.ObjectKind is not (DalamudObjectKind.MountType or DalamudObjectKind.Companion))
|
||||||
var candidateKind = (DalamudObjectKind)candidate->ObjectKind;
|
continue;
|
||||||
return candidateKind is DalamudObjectKind.MountType or DalamudObjectKind.Companion
|
|
||||||
? candidateAddress
|
var candidate = (GameObject*)obj.Address;
|
||||||
: nint.Zero;
|
if (ResolveOwnerId(candidate) == ownerEntityId)
|
||||||
|
return obj.Address;
|
||||||
|
}
|
||||||
|
|
||||||
|
return nint.Zero;
|
||||||
|
}
|
||||||
|
|
||||||
|
public unsafe bool TryFindOwnedObject(uint ownerEntityId, LightlessObjectKind kind, out nint address)
|
||||||
|
{
|
||||||
|
address = nint.Zero;
|
||||||
|
if (ownerEntityId == 0) return false;
|
||||||
|
|
||||||
|
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)
|
||||||
@@ -603,6 +718,22 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -622,6 +753,23 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1018,22 +1166,6 @@ 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();
|
||||||
@@ -1173,21 +1305,19 @@ 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)
|
if (address == nint.Zero) return false;
|
||||||
return false;
|
|
||||||
|
|
||||||
var gameObject = (GameObject*)address;
|
var gameObject = (GameObject*)address;
|
||||||
if (gameObject == null)
|
if (gameObject == null) return false;
|
||||||
return false;
|
|
||||||
|
|
||||||
var drawObject = gameObject->DrawObject;
|
var drawObject = gameObject->DrawObject;
|
||||||
if (drawObject == null)
|
if (drawObject == null) return false;
|
||||||
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;
|
||||||
|
|
||||||
@@ -1197,6 +1327,7 @@ 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);
|
||||||
|
|||||||
@@ -1,93 +0,0 @@
|
|||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using System.Collections.Concurrent;
|
|
||||||
|
|
||||||
namespace LightlessSync.Services;
|
|
||||||
|
|
||||||
public sealed class AssetProcessingQueue : IDisposable
|
|
||||||
{
|
|
||||||
private readonly BlockingCollection<WorkItem> _queue = new();
|
|
||||||
private readonly Thread _worker;
|
|
||||||
private readonly ILogger _logger;
|
|
||||||
private bool _disposed;
|
|
||||||
|
|
||||||
public AssetProcessingQueue(ILogger logger, string name)
|
|
||||||
{
|
|
||||||
_logger = logger;
|
|
||||||
_worker = new Thread(Run)
|
|
||||||
{
|
|
||||||
IsBackground = true,
|
|
||||||
Name = string.IsNullOrWhiteSpace(name) ? "LightlessSync.AssetProcessing" : name
|
|
||||||
};
|
|
||||||
_worker.Start();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task Enqueue(Func<CancellationToken, Task> work, CancellationToken token = default)
|
|
||||||
{
|
|
||||||
if (work is null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(work));
|
|
||||||
}
|
|
||||||
|
|
||||||
var completion = new TaskCompletionSource<object?>(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
||||||
if (token.IsCancellationRequested)
|
|
||||||
{
|
|
||||||
completion.TrySetCanceled(token);
|
|
||||||
return completion.Task;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_queue.IsAddingCompleted || _disposed)
|
|
||||||
{
|
|
||||||
completion.TrySetException(new ObjectDisposedException(nameof(AssetProcessingQueue)));
|
|
||||||
return completion.Task;
|
|
||||||
}
|
|
||||||
|
|
||||||
_queue.Add(new WorkItem(work, token, completion));
|
|
||||||
return completion.Task;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Run()
|
|
||||||
{
|
|
||||||
foreach (var item in _queue.GetConsumingEnumerable())
|
|
||||||
{
|
|
||||||
if (item.Token.IsCancellationRequested)
|
|
||||||
{
|
|
||||||
item.Completion.TrySetCanceled(item.Token);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
item.Work(item.Token).GetAwaiter().GetResult();
|
|
||||||
item.Completion.TrySetResult(null);
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException ex)
|
|
||||||
{
|
|
||||||
var token = ex.CancellationToken.IsCancellationRequested ? ex.CancellationToken : item.Token;
|
|
||||||
item.Completion.TrySetCanceled(token);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogWarning(ex, "Asset processing job failed.");
|
|
||||||
item.Completion.TrySetException(ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
if (_disposed)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_disposed = true;
|
|
||||||
_queue.CompleteAdding();
|
|
||||||
_worker.Join(TimeSpan.FromSeconds(2));
|
|
||||||
_queue.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly record struct WorkItem(
|
|
||||||
Func<CancellationToken, Task> Work,
|
|
||||||
CancellationToken Token,
|
|
||||||
TaskCompletionSource<object?> Completion);
|
|
||||||
}
|
|
||||||
@@ -106,7 +106,7 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable
|
|||||||
_baseAnalysisCts.Dispose();
|
_baseAnalysisCts.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task UpdateFileEntriesAsync(IEnumerable<string> filePaths, CancellationToken token, bool force = false)
|
public async Task UpdateFileEntriesAsync(IEnumerable<string> filePaths, CancellationToken token)
|
||||||
{
|
{
|
||||||
var normalized = new HashSet<string>(
|
var normalized = new HashSet<string>(
|
||||||
filePaths.Where(path => !string.IsNullOrWhiteSpace(path)),
|
filePaths.Where(path => !string.IsNullOrWhiteSpace(path)),
|
||||||
@@ -115,8 +115,6 @@ 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)
|
||||||
@@ -126,26 +124,9 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
token.ThrowIfCancellationRequested();
|
token.ThrowIfCancellationRequested();
|
||||||
await entry.ComputeSizes(_fileCacheManager, token, force).ConfigureAwait(false);
|
await entry.ComputeSizes(_fileCacheManager, token).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)
|
||||||
@@ -330,10 +311,6 @@ 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);
|
||||||
@@ -349,7 +326,6 @@ 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,26 +8,18 @@ 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 = 200;
|
private const int MaxMessageHistory = 150;
|
||||||
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;
|
||||||
@@ -384,7 +376,6 @@ 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());
|
||||||
@@ -1009,22 +1000,11 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
|||||||
|
|
||||||
private void OnChatMessageReceived(ChatMessageDto dto)
|
private void OnChatMessageReceived(ChatMessageDto dto)
|
||||||
{
|
{
|
||||||
ChatChannelDescriptor descriptor = dto.Channel.WithNormalizedCustomKey();
|
var descriptor = dto.Channel.WithNormalizedCustomKey();
|
||||||
string key = descriptor.Type == ChatChannelType.Zone ? ZoneChannelKey : BuildChannelKey(descriptor);
|
var key = descriptor.Type == ChatChannelType.Zone ? ZoneChannelKey : BuildChannelKey(descriptor);
|
||||||
bool fromSelf = IsMessageFromSelf(dto, key);
|
var fromSelf = IsMessageFromSelf(dto, key);
|
||||||
ChatMessageEntry message = BuildMessage(dto, fromSelf);
|
var 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())
|
||||||
{
|
{
|
||||||
@@ -1062,12 +1042,6 @@ 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;
|
||||||
@@ -1084,29 +1058,10 @@ 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())
|
||||||
@@ -1153,113 +1108,6 @@ 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);
|
||||||
@@ -1516,313 +1364,6 @@ 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)
|
||||||
@@ -1859,12 +1400,4 @@ 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,8 +22,10 @@ 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;
|
||||||
@@ -227,28 +229,6 @@ 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);
|
||||||
@@ -424,7 +404,38 @@ 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;
|
||||||
return _objectTable.GetObjectAddress(((GameObject*)playerAddress)->ObjectIndex + 1);
|
var ownerEntityId = ((Character*)playerAddress)->EntityId;
|
||||||
|
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)
|
||||||
@@ -454,7 +465,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return IntPtr.Zero;
|
return FindOwnedPet(ownerEntityId, ownerAddress);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IntPtr> GetPetAsync(IntPtr? playerPointer = null)
|
public async Task<IntPtr> GetPetAsync(IntPtr? playerPointer = null)
|
||||||
@@ -462,6 +473,69 @@ 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)
|
||||||
@@ -560,37 +634,6 @@ 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();
|
||||||
@@ -701,23 +744,7 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -809,6 +836,7 @@ 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)
|
||||||
@@ -827,12 +855,10 @@ 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;
|
||||||
@@ -841,7 +867,6 @@ 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());
|
||||||
});
|
});
|
||||||
@@ -855,40 +880,49 @@ 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(ILogger logger, GameObjectHandler handler, Guid redrawId, int timeOut = 5000, CancellationToken? ct = null)
|
public async Task WaitWhileCharacterIsDrawing(
|
||||||
|
ILogger logger,
|
||||||
|
GameObjectHandler handler,
|
||||||
|
Guid redrawId,
|
||||||
|
int timeOut = 5000,
|
||||||
|
CancellationToken? ct = null)
|
||||||
{
|
{
|
||||||
if (!_clientState.IsLoggedIn) return;
|
if (!_clientState.IsLoggedIn) return;
|
||||||
|
|
||||||
if (ct == null)
|
var token = ct ?? CancellationToken.None;
|
||||||
ct = CancellationToken.None;
|
|
||||||
|
|
||||||
const int tick = 250;
|
const int tick = 250;
|
||||||
int curWaitTime = 0;
|
const int initialSettle = 50;
|
||||||
|
|
||||||
|
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;
|
|
||||||
|
|
||||||
while ((!ct.Value.IsCancellationRequested)
|
await Task.Delay(initialSettle, token).ConfigureAwait(false);
|
||||||
&& curWaitTime < timeOut
|
|
||||||
&& await handler.IsBeingDrawnRunOnFrameworkAsync().ConfigureAwait(false)) // 0b100000000000 is "still rendering" or something
|
while (!token.IsCancellationRequested
|
||||||
|
&& 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);
|
||||||
curWaitTime += tick;
|
await Task.Delay(tick, token).ConfigureAwait(false);
|
||||||
await Task.Delay(tick, ct.Value).ConfigureAwait(true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.LogTrace("[{redrawId}] Finished drawing after {curWaitTime}ms", redrawId, curWaitTime);
|
logger.LogTrace("[{redrawId}] Finished drawing after {ms}ms", redrawId, sw.ElapsedMilliseconds);
|
||||||
}
|
}
|
||||||
catch (AccessViolationException ex)
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
// 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);
|
||||||
}
|
}
|
||||||
@@ -938,21 +972,92 @@ 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)
|
||||||
{
|
{
|
||||||
isDrawing = ((CharacterBase*)drawObj)->HasModelInSlotLoaded != 0;
|
var charBase = (CharacterBase*)drawObj;
|
||||||
|
if (charBase != null && IsValidPointer((nint)charBase))
|
||||||
|
{
|
||||||
|
isDrawing = charBase->HasModelInSlotLoaded != 0;
|
||||||
if (!isDrawing)
|
if (!isDrawing)
|
||||||
{
|
{
|
||||||
isDrawing = ((CharacterBase*)drawObj)->HasModelFilesInSlotLoaded != 0;
|
isDrawing = charBase->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))
|
||||||
{
|
{
|
||||||
@@ -972,6 +1077,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (!string.Equals(_lastGlobalBlockPlayer, characterName, StringComparison.Ordinal)
|
if (!string.Equals(_lastGlobalBlockPlayer, characterName, StringComparison.Ordinal)
|
||||||
@@ -999,39 +1105,21 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
|
|
||||||
private unsafe void FrameworkOnUpdateInternal()
|
private unsafe void FrameworkOnUpdateInternal()
|
||||||
{
|
{
|
||||||
var localPlayer = _objectTable.LocalPlayer;
|
if (!_clientState.IsLoggedIn || _objectTable.LocalPlayer == null)
|
||||||
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",
|
||||||
() =>
|
() =>
|
||||||
{
|
{
|
||||||
@@ -1040,24 +1128,23 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
_actorObjectService.RefreshTrackedActors();
|
_actorObjectService.RefreshTrackedActors();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_clientState.IsLoggedIn && localPlayer != null)
|
|
||||||
{
|
|
||||||
var playerDescriptors = _actorObjectService.PlayerDescriptors;
|
var playerDescriptors = _actorObjectService.PlayerDescriptors;
|
||||||
for (var i = 0; i < playerDescriptors.Count; i++)
|
var descriptorCount = playerDescriptors.Count;
|
||||||
|
|
||||||
|
for (var i = 0; i < descriptorCount; 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)
|
if (playerAddress == nint.Zero || !IsValidPointer(playerAddress))
|
||||||
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"));
|
||||||
@@ -1066,21 +1153,16 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
|
|
||||||
if (!IsAnythingDrawing)
|
if (!IsAnythingDrawing)
|
||||||
{
|
{
|
||||||
var charaName = player.Name.TextValue;
|
if (!_objectTable.Any(o => o?.Address == playerAddress))
|
||||||
if (string.IsNullOrEmpty(charaName))
|
|
||||||
{
|
{
|
||||||
charaName = actor.Name;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
CheckCharacterForDrawing(playerAddress, charaName);
|
CheckCharacterForDrawing(playerAddress, actor.Name);
|
||||||
|
|
||||||
if (IsAnythingDrawing)
|
if (IsAnythingDrawing)
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1205,6 +1287,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var localPlayer = _objectTable.LocalPlayer;
|
||||||
if (localPlayer != null)
|
if (localPlayer != null)
|
||||||
{
|
{
|
||||||
_classJobId = localPlayer.ClassJob.RowId;
|
_classJobId = localPlayer.ClassJob.RowId;
|
||||||
@@ -1226,6 +1309,22 @@ 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,6 +68,7 @@ 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,4 +1,3 @@
|
|||||||
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;
|
||||||
@@ -109,144 +108,6 @@ 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,12 +21,6 @@ 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;
|
||||||
@@ -144,5 +138,6 @@ 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
@@ -1,132 +0,0 @@
|
|||||||
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,8 +1,5 @@
|
|||||||
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;
|
||||||
@@ -20,10 +17,9 @@ 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 TaskRegistry<string> _decimationDeduplicator = new();
|
private readonly ConcurrentDictionary<string, Task> _activeJobs = new(StringComparer.OrdinalIgnoreCase);
|
||||||
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);
|
||||||
|
|
||||||
@@ -32,15 +28,13 @@ 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)
|
||||||
@@ -50,16 +44,16 @@ public sealed class ModelDecimationService
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_decimatedPaths.ContainsKey(hash) || _failedHashes.ContainsKey(hash) || _decimationDeduplicator.TryGetExisting(hash, out _))
|
if (_decimatedPaths.ContainsKey(hash) || _failedHashes.ContainsKey(hash) || _activeJobs.ContainsKey(hash))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogDebug("Queued model decimation for {Hash}", hash);
|
_logger.LogInformation("Queued model decimation for {Hash}", hash);
|
||||||
|
|
||||||
_decimationDeduplicator.GetOrStart(hash, () => _processingQueue.Enqueue(async token =>
|
_activeJobs[hash] = Task.Run(async () =>
|
||||||
{
|
{
|
||||||
await _decimationSemaphore.WaitAsync(token).ConfigureAwait(false);
|
await _decimationSemaphore.WaitAsync().ConfigureAwait(false);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await DecimateInternalAsync(hash, filePath).ConfigureAwait(false);
|
await DecimateInternalAsync(hash, filePath).ConfigureAwait(false);
|
||||||
@@ -72,54 +66,16 @@ 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, threshold);
|
&& !ShouldSkipByTriangleCache(hash);
|
||||||
}
|
|
||||||
|
|
||||||
public string GetPreferredPath(string hash, string originalPath)
|
public string GetPreferredPath(string hash, string originalPath)
|
||||||
{
|
{
|
||||||
@@ -160,7 +116,7 @@ public sealed class ModelDecimationService
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_decimationDeduplicator.TryGetExisting(hash, out var job))
|
if (_activeJobs.TryGetValue(hash, out var job))
|
||||||
{
|
{
|
||||||
pending.Add(job);
|
pending.Add(job);
|
||||||
}
|
}
|
||||||
@@ -175,23 +131,6 @@ 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))
|
||||||
{
|
{
|
||||||
@@ -200,48 +139,30 @@ public sealed class ModelDecimationService
|
|||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryNormalizeSettings(settings, out var normalized))
|
if (!TryGetDecimationSettings(out var triangleThreshold, out var targetRatio))
|
||||||
{
|
{
|
||||||
_logger.LogDebug("Model decimation skipped for {Hash}; invalid settings.", hash);
|
_logger.LogInformation("Model decimation disabled or invalid settings for {Hash}", hash);
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogDebug(
|
_logger.LogInformation("Starting model decimation for {Hash} (threshold {Threshold}, ratio {Ratio:0.##})", hash, triangleThreshold, targetRatio);
|
||||||
"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 = destinationOverride ?? Path.Combine(GetDecimatedDirectory(), $"{hash}.mdl");
|
var destination = Path.Combine(GetDecimatedDirectory(), $"{hash}.mdl");
|
||||||
var inPlace = string.Equals(destination, sourcePath, StringComparison.OrdinalIgnoreCase);
|
if (File.Exists(destination))
|
||||||
if (!inPlace && File.Exists(destination))
|
|
||||||
{
|
|
||||||
if (allowExisting)
|
|
||||||
{
|
|
||||||
if (registerDecimatedPath)
|
|
||||||
{
|
{
|
||||||
RegisterDecimatedModel(hash, sourcePath, destination);
|
RegisterDecimatedModel(hash, sourcePath, destination);
|
||||||
}
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
TryDelete(destination);
|
if (!MdlDecimator.TryDecimate(sourcePath, destination, triangleThreshold, targetRatio, _logger))
|
||||||
}
|
|
||||||
|
|
||||||
if (!MdlDecimator.TryDecimate(sourcePath, destination, normalized, _logger))
|
|
||||||
{
|
{
|
||||||
_failedHashes[hash] = 1;
|
_failedHashes[hash] = 1;
|
||||||
_logger.LogDebug("Model decimation skipped for {Hash}", hash);
|
_logger.LogInformation("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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -329,7 +250,7 @@ public sealed class ModelDecimationService
|
|||||||
private bool IsDecimationEnabled()
|
private bool IsDecimationEnabled()
|
||||||
=> _performanceConfigService.Current.EnableModelDecimation;
|
=> _performanceConfigService.Current.EnableModelDecimation;
|
||||||
|
|
||||||
private bool ShouldSkipByTriangleCache(string hash, int triangleThreshold)
|
private bool ShouldSkipByTriangleCache(string hash)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(hash))
|
if (string.IsNullOrEmpty(hash))
|
||||||
{
|
{
|
||||||
@@ -341,7 +262,7 @@ public sealed class ModelDecimationService
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var threshold = Math.Max(0, triangleThreshold);
|
var threshold = Math.Max(0, _performanceConfigService.Current.ModelDecimationTriangleThreshold);
|
||||||
return threshold > 0 && cachedTris < threshold;
|
return threshold > 0 && cachedTris < threshold;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -352,48 +273,50 @@ public sealed class ModelDecimationService
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
var normalized = ModelDecimationFilters.NormalizePath(gamePath);
|
var normalized = NormalizeGamePath(gamePath);
|
||||||
if (ModelDecimationFilters.IsHairPath(normalized))
|
if (normalized.Contains("/hair/", StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ModelDecimationFilters.IsClothingPath(normalized))
|
if (normalized.Contains("/chara/equipment/", StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
return _performanceConfigService.Current.ModelDecimationAllowClothing;
|
return _performanceConfigService.Current.ModelDecimationAllowClothing;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ModelDecimationFilters.IsAccessoryPath(normalized))
|
if (normalized.Contains("/chara/accessory/", StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
return _performanceConfigService.Current.ModelDecimationAllowAccessories;
|
return _performanceConfigService.Current.ModelDecimationAllowAccessories;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ModelDecimationFilters.IsBodyPath(normalized))
|
if (normalized.Contains("/chara/human/", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
if (normalized.Contains("/body/", StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
return _performanceConfigService.Current.ModelDecimationAllowBody;
|
return _performanceConfigService.Current.ModelDecimationAllowBody;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ModelDecimationFilters.IsFaceHeadPath(normalized))
|
if (normalized.Contains("/face/", StringComparison.Ordinal) || normalized.Contains("/head/", StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
return _performanceConfigService.Current.ModelDecimationAllowFaceHead;
|
return _performanceConfigService.Current.ModelDecimationAllowFaceHead;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ModelDecimationFilters.IsTailOrEarPath(normalized))
|
if (normalized.Contains("/tail/", StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
return _performanceConfigService.Current.ModelDecimationAllowTail;
|
return _performanceConfigService.Current.ModelDecimationAllowTail;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool TryGetDecimationSettings(out ModelDecimationSettings settings)
|
private static string NormalizeGamePath(string path)
|
||||||
|
=> path.Replace('\\', '/').ToLowerInvariant();
|
||||||
|
|
||||||
|
private bool TryGetDecimationSettings(out int triangleThreshold, out double targetRatio)
|
||||||
{
|
{
|
||||||
settings = new ModelDecimationSettings(
|
triangleThreshold = 15_000;
|
||||||
ModelDecimationDefaults.TriangleThreshold,
|
targetRatio = 0.8;
|
||||||
ModelDecimationDefaults.TargetRatio,
|
|
||||||
ModelDecimationDefaults.NormalizeTangents,
|
|
||||||
ModelDecimationDefaults.AvoidBodyIntersection,
|
|
||||||
new ModelDecimationAdvancedSettings());
|
|
||||||
|
|
||||||
var config = _performanceConfigService.Current;
|
var config = _performanceConfigService.Current;
|
||||||
if (!config.EnableModelDecimation)
|
if (!config.EnableModelDecimation)
|
||||||
@@ -401,86 +324,14 @@ public sealed class ModelDecimationService
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var advanced = NormalizeAdvancedSettings(config.ModelDecimationAdvanced);
|
triangleThreshold = Math.Max(0, config.ModelDecimationTriangleThreshold);
|
||||||
settings = new ModelDecimationSettings(
|
targetRatio = config.ModelDecimationTargetRatio;
|
||||||
Math.Max(0, config.ModelDecimationTriangleThreshold),
|
if (double.IsNaN(targetRatio) || double.IsInfinity(targetRatio))
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryNormalizeSettings(settings, out _))
|
targetRatio = Math.Clamp(targetRatio, MinTargetRatio, MaxTargetRatio);
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
using LightlessSync.LightlessConfiguration.Configurations;
|
|
||||||
|
|
||||||
namespace LightlessSync.Services.ModelDecimation;
|
|
||||||
|
|
||||||
public readonly record struct ModelDecimationSettings(
|
|
||||||
int TriangleThreshold,
|
|
||||||
double TargetRatio,
|
|
||||||
bool NormalizeTangents,
|
|
||||||
bool AvoidBodyIntersection,
|
|
||||||
ModelDecimationAdvancedSettings Advanced);
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace LightlessSync.Services;
|
|
||||||
|
|
||||||
public sealed class ModelProcessingQueue : IDisposable
|
|
||||||
{
|
|
||||||
private readonly AssetProcessingQueue _queue;
|
|
||||||
|
|
||||||
public ModelProcessingQueue(ILogger<ModelProcessingQueue> logger)
|
|
||||||
{
|
|
||||||
_queue = new AssetProcessingQueue(logger, "LightlessSync.ModelProcessing");
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task Enqueue(Func<CancellationToken, Task> work, CancellationToken token = default)
|
|
||||||
=> _queue.Enqueue(work, token);
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
=> _queue.Dispose();
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
using System.Linq;
|
using LightlessSync.Interop.Ipc;
|
||||||
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;
|
||||||
@@ -10,18 +8,14 @@ namespace LightlessSync.Services;
|
|||||||
public sealed class PenumbraTempCollectionJanitor : DisposableMediatorSubscriberBase
|
public sealed class PenumbraTempCollectionJanitor : DisposableMediatorSubscriberBase
|
||||||
{
|
{
|
||||||
private readonly IpcManager _ipc;
|
private readonly IpcManager _ipc;
|
||||||
private readonly TempCollectionConfigService _config;
|
private readonly PenumbraJanitorConfigService _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,
|
||||||
TempCollectionConfigService config) : base(logger, mediator)
|
PenumbraJanitorConfigService config) : base(logger, mediator)
|
||||||
{
|
{
|
||||||
_ipc = ipc;
|
_ipc = ipc;
|
||||||
_config = config;
|
_config = config;
|
||||||
@@ -32,42 +26,16 @@ 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;
|
||||||
var changed = false;
|
if (_config.Current.OrphanableTempCollections.Add(id))
|
||||||
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;
|
||||||
var config = _config.Current;
|
if (_config.Current.OrphanableTempCollections.Remove(id))
|
||||||
var changed = RemoveEntry(config.OrphanableTempCollectionEntries, id) > 0;
|
|
||||||
if (changed)
|
|
||||||
{
|
|
||||||
_config.Save();
|
_config.Save();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private void CleanupOrphansOnBoot()
|
private void CleanupOrphansOnBoot()
|
||||||
{
|
{
|
||||||
@@ -77,139 +45,30 @@ public sealed class PenumbraTempCollectionJanitor : DisposableMediatorSubscriber
|
|||||||
if (!_ipc.Penumbra.APIAvailable)
|
if (!_ipc.Penumbra.APIAvailable)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
_ = Task.Run(async () =>
|
var ids = _config.Current.OrphanableTempCollections.ToArray();
|
||||||
{
|
if (ids.Length == 0)
|
||||||
try
|
|
||||||
{
|
|
||||||
await CleanupOrphansOnBootAsync(_cleanupCts.Token).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Logger.LogError(ex, "Error cleaning orphaned temp collections");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task CleanupOrphansOnBootAsync(CancellationToken token)
|
|
||||||
{
|
|
||||||
var config = _config.Current;
|
|
||||||
var entries = config.OrphanableTempCollectionEntries;
|
|
||||||
if (entries.Count == 0)
|
|
||||||
return;
|
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();
|
var appId = Guid.NewGuid();
|
||||||
Logger.LogInformation("Cleaning up {count} orphaned Lightless temp collections older than {delay}", expired.Count, OrphanCleanupDelay);
|
Logger.LogInformation("Cleaning up {count} orphaned Lightless temp collections found in configuration", ids.Length);
|
||||||
|
|
||||||
List<Guid> removedIds = [];
|
foreach (var id in ids)
|
||||||
foreach (var id in expired)
|
|
||||||
{
|
{
|
||||||
if (token.IsCancellationRequested)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await _ipc.Penumbra.RemoveTemporaryCollectionAsync(Logger, appId, id).ConfigureAwait(false);
|
_ipc.Penumbra.RemoveTemporaryCollectionAsync(Logger, appId, id)
|
||||||
|
.GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Logger.LogDebug(ex, "Failed removing orphaned temp collection {id}", id);
|
Logger.LogDebug(ex, "Failed removing orphaned temp collection {id}", id);
|
||||||
}
|
}
|
||||||
|
|
||||||
removedIds.Add(id);
|
|
||||||
if (removedIds.Count % CleanupBatchSize == 0)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await Task.Delay(CleanupBatchDelay, token).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (removedIds.Count == 0)
|
_config.Current.OrphanableTempCollections.Clear();
|
||||||
{
|
|
||||||
if (changed)
|
|
||||||
{
|
|
||||||
_config.Save();
|
_config.Save();
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var id in removedIds)
|
// Notify cleanup complete
|
||||||
{
|
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,10 +131,7 @@ public sealed class PerformanceCollectorService : IHostedService
|
|||||||
DrawSeparator(sb, longestCounterName);
|
DrawSeparator(sb, longestCounterName);
|
||||||
}
|
}
|
||||||
|
|
||||||
var snapshot = entry.Value.Snapshot();
|
var pastEntries = limitBySeconds > 0 ? entry.Value.Where(e => e.Item1.AddMinutes(limitBySeconds / 60.0d) >= TimeOnly.FromDateTime(DateTime.Now)).ToList() : [.. entry.Value];
|
||||||
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())
|
||||||
{
|
{
|
||||||
@@ -192,11 +189,7 @@ public sealed class PerformanceCollectorService : IHostedService
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!entries.Value.TryGetLast(out var last))
|
var last = entries.Value.ToList()[^1];
|
||||||
{
|
|
||||||
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,7 +2,6 @@ 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;
|
||||||
|
|
||||||
@@ -28,9 +27,7 @@ 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)
|
||||||
{
|
{
|
||||||
@@ -51,7 +48,7 @@ public sealed class TextureCompressionService
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
await RunPenumbraConversionAsync(request, textureType, total, completed, progress, token, requestRedraw, includeMipMaps).ConfigureAwait(false);
|
await RunPenumbraConversionAsync(request, textureType, total, completed, progress, token).ConfigureAwait(false);
|
||||||
|
|
||||||
completed++;
|
completed++;
|
||||||
}
|
}
|
||||||
@@ -68,16 +65,14 @@ 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: includeMipMaps,
|
IncludeMipMaps: true,
|
||||||
request.DuplicateFilePaths);
|
request.DuplicateFilePaths);
|
||||||
|
|
||||||
var backupPath = CreateBackupCopy(primaryPath);
|
var backupPath = CreateBackupCopy(primaryPath);
|
||||||
@@ -88,7 +83,7 @@ public sealed class TextureCompressionService
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
WaitForAccess(primaryPath);
|
WaitForAccess(primaryPath);
|
||||||
await _ipcManager.Penumbra.ConvertTextureFiles(_logger, new[] { conversionJob }, null, token, requestRedraw).ConfigureAwait(false);
|
await _ipcManager.Penumbra.ConvertTextureFiles(_logger, new[] { conversionJob }, null, token).ConfigureAwait(false);
|
||||||
|
|
||||||
if (!IsValidConversionResult(displayJob.OutputFile))
|
if (!IsValidConversionResult(displayJob.OutputFile))
|
||||||
{
|
{
|
||||||
@@ -133,47 +128,20 @@ 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)
|
||||||
{
|
{
|
||||||
if (hasExpectedHash)
|
entry = _fileCacheManager.CreateFileEntry(path);
|
||||||
{
|
|
||||||
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);
|
||||||
@@ -181,35 +149,6 @@ 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,12 +4,9 @@ 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;
|
||||||
@@ -33,13 +30,10 @@ 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 TaskRegistry<string> _downscaleDeduplicator = new();
|
private readonly ConcurrentDictionary<string, Task> _activeJobs = new(StringComparer.OrdinalIgnoreCase);
|
||||||
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>
|
||||||
{
|
{
|
||||||
@@ -74,16 +68,12 @@ 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)
|
||||||
@@ -92,9 +82,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 (_downscaleDeduplicator.TryGetExisting(hash, out _)) return;
|
if (_activeJobs.ContainsKey(hash)) return;
|
||||||
|
|
||||||
_downscaleDeduplicator.GetOrStart(hash, () => _processingQueue.Enqueue(async token =>
|
_activeJobs[hash] = Task.Run(async () =>
|
||||||
{
|
{
|
||||||
TextureMapKind mapKind;
|
TextureMapKind mapKind;
|
||||||
try
|
try
|
||||||
@@ -108,7 +98,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)
|
||||||
@@ -117,9 +107,7 @@ public sealed class TextureDownscaleService
|
|||||||
return false;
|
return false;
|
||||||
|
|
||||||
var performanceConfig = _playerPerformanceConfigService.Current;
|
var performanceConfig = _playerPerformanceConfigService.Current;
|
||||||
return performanceConfig.EnableNonIndexTextureMipTrim
|
return performanceConfig.EnableNonIndexTextureMipTrim || performanceConfig.EnableIndexTextureDownscale;
|
||||||
|| performanceConfig.EnableIndexTextureDownscale
|
|
||||||
|| performanceConfig.EnableUncompressedTextureCompression;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public string GetPreferredPath(string hash, string originalPath)
|
public string GetPreferredPath(string hash, string originalPath)
|
||||||
@@ -156,7 +144,7 @@ public sealed class TextureDownscaleService
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_downscaleDeduplicator.TryGetExisting(hash, out var job))
|
if (_activeJobs.TryGetValue(hash, out var job))
|
||||||
{
|
{
|
||||||
pending.Add(job);
|
pending.Add(job);
|
||||||
}
|
}
|
||||||
@@ -194,18 +182,10 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -216,7 +196,6 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,7 +206,6 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,7 +213,6 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,7 +222,6 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -253,12 +229,10 @@ 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;
|
||||||
@@ -274,40 +248,17 @@ 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);
|
||||||
if (!TryConvertForSave(resizedScratch, sourceFormat, out var finalScratch, canReencodeWithPenumbra))
|
using var finalScratch = resizedScratch.Convert(DXGIFormat.B8G8R8A8UNorm);
|
||||||
{
|
|
||||||
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);
|
||||||
@@ -326,6 +277,7 @@ public sealed class TextureDownscaleService
|
|||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
_downscaleSemaphore.Release();
|
_downscaleSemaphore.Release();
|
||||||
|
_activeJobs.TryRemove(hash, out _);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -378,164 +330,6 @@ 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;
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace LightlessSync.Services;
|
|
||||||
|
|
||||||
public sealed class TextureProcessingQueue : IDisposable
|
|
||||||
{
|
|
||||||
private readonly AssetProcessingQueue _queue;
|
|
||||||
|
|
||||||
public TextureProcessingQueue(ILogger<TextureProcessingQueue> logger)
|
|
||||||
{
|
|
||||||
_queue = new AssetProcessingQueue(logger, "LightlessSync.TextureProcessing");
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task Enqueue(Func<CancellationToken, Task> work, CancellationToken token = default)
|
|
||||||
=> _queue.Enqueue(work, token);
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
=> _queue.Dispose();
|
|
||||||
}
|
|
||||||
@@ -13,20 +13,16 @@ 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, DalamudUtilService dalamudUtilService, WindowSystem windowSystem,
|
LightlessConfigService lightlessConfigService, 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)
|
||||||
@@ -35,7 +31,6 @@ 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;
|
||||||
@@ -48,7 +43,6 @@ public sealed class UiService : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
foreach (var window in windows)
|
foreach (var window in windows)
|
||||||
{
|
{
|
||||||
_registeredWindows.Add(window);
|
|
||||||
_windowSystem.AddWindow(window);
|
_windowSystem.AddWindow(window);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,8 +176,6 @@ 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();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -227,10 +219,7 @@ 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
|
||||||
@@ -238,61 +227,4 @@ 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,20 +480,6 @@ 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)
|
||||||
|
|||||||
169
LightlessSync/ThirdParty/MeshDecimator/Algorithms/DecimationAlgorithm.cs
vendored
Normal file
169
LightlessSync/ThirdParty/MeshDecimator/Algorithms/DecimationAlgorithm.cs
vendored
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
#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
|
||||||
|
}
|
||||||
|
}
|
||||||
1549
LightlessSync/ThirdParty/MeshDecimator/Algorithms/FastQuadricMeshSimplification.cs
vendored
Normal file
1549
LightlessSync/ThirdParty/MeshDecimator/Algorithms/FastQuadricMeshSimplification.cs
vendored
Normal file
File diff suppressed because it is too large
Load Diff
249
LightlessSync/ThirdParty/MeshDecimator/BoneWeight.cs
vendored
Normal file
249
LightlessSync/ThirdParty/MeshDecimator/BoneWeight.cs
vendored
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
#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
|
||||||
|
}
|
||||||
|
}
|
||||||
179
LightlessSync/ThirdParty/MeshDecimator/Collections/ResizableArray.cs
vendored
Normal file
179
LightlessSync/ThirdParty/MeshDecimator/Collections/ResizableArray.cs
vendored
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
#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
|
||||||
|
}
|
||||||
|
}
|
||||||
79
LightlessSync/ThirdParty/MeshDecimator/Collections/UVChannels.cs
vendored
Normal file
79
LightlessSync/ThirdParty/MeshDecimator/Collections/UVChannels.cs
vendored
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
21
LightlessSync/ThirdParty/MeshDecimator/LICENSE.md
vendored
Normal file
21
LightlessSync/ThirdParty/MeshDecimator/LICENSE.md
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
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.
|
||||||
286
LightlessSync/ThirdParty/MeshDecimator/Math/MathHelper.cs
vendored
Normal file
286
LightlessSync/ThirdParty/MeshDecimator/Math/MathHelper.cs
vendored
Normal file
@@ -0,0 +1,286 @@
|
|||||||
|
#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
|
||||||
|
}
|
||||||
|
}
|
||||||
303
LightlessSync/ThirdParty/MeshDecimator/Math/SymmetricMatrix.cs
vendored
Normal file
303
LightlessSync/ThirdParty/MeshDecimator/Math/SymmetricMatrix.cs
vendored
Normal file
@@ -0,0 +1,303 @@
|
|||||||
|
#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
|
||||||
|
}
|
||||||
|
}
|
||||||
425
LightlessSync/ThirdParty/MeshDecimator/Math/Vector2.cs
vendored
Normal file
425
LightlessSync/ThirdParty/MeshDecimator/Math/Vector2.cs
vendored
Normal file
@@ -0,0 +1,425 @@
|
|||||||
|
#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
|
||||||
|
}
|
||||||
|
}
|
||||||
425
LightlessSync/ThirdParty/MeshDecimator/Math/Vector2d.cs
vendored
Normal file
425
LightlessSync/ThirdParty/MeshDecimator/Math/Vector2d.cs
vendored
Normal file
@@ -0,0 +1,425 @@
|
|||||||
|
#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
|
||||||
|
}
|
||||||
|
}
|
||||||
348
LightlessSync/ThirdParty/MeshDecimator/Math/Vector2i.cs
vendored
Normal file
348
LightlessSync/ThirdParty/MeshDecimator/Math/Vector2i.cs
vendored
Normal file
@@ -0,0 +1,348 @@
|
|||||||
|
#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
|
||||||
|
}
|
||||||
|
}
|
||||||
494
LightlessSync/ThirdParty/MeshDecimator/Math/Vector3.cs
vendored
Normal file
494
LightlessSync/ThirdParty/MeshDecimator/Math/Vector3.cs
vendored
Normal file
@@ -0,0 +1,494 @@
|
|||||||
|
#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
|
||||||
|
}
|
||||||
|
}
|
||||||
481
LightlessSync/ThirdParty/MeshDecimator/Math/Vector3d.cs
vendored
Normal file
481
LightlessSync/ThirdParty/MeshDecimator/Math/Vector3d.cs
vendored
Normal file
@@ -0,0 +1,481 @@
|
|||||||
|
#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
|
||||||
|
}
|
||||||
|
}
|
||||||
368
LightlessSync/ThirdParty/MeshDecimator/Math/Vector3i.cs
vendored
Normal file
368
LightlessSync/ThirdParty/MeshDecimator/Math/Vector3i.cs
vendored
Normal file
@@ -0,0 +1,368 @@
|
|||||||
|
#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
|
||||||
|
}
|
||||||
|
}
|
||||||
467
LightlessSync/ThirdParty/MeshDecimator/Math/Vector4.cs
vendored
Normal file
467
LightlessSync/ThirdParty/MeshDecimator/Math/Vector4.cs
vendored
Normal file
@@ -0,0 +1,467 @@
|
|||||||
|
#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
|
||||||
|
}
|
||||||
|
}
|
||||||
467
LightlessSync/ThirdParty/MeshDecimator/Math/Vector4d.cs
vendored
Normal file
467
LightlessSync/ThirdParty/MeshDecimator/Math/Vector4d.cs
vendored
Normal file
@@ -0,0 +1,467 @@
|
|||||||
|
#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
|
||||||
|
}
|
||||||
|
}
|
||||||
388
LightlessSync/ThirdParty/MeshDecimator/Math/Vector4i.cs
vendored
Normal file
388
LightlessSync/ThirdParty/MeshDecimator/Math/Vector4i.cs
vendored
Normal file
@@ -0,0 +1,388 @@
|
|||||||
|
#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
Normal file
955
LightlessSync/ThirdParty/MeshDecimator/Mesh.cs
vendored
Normal file
@@ -0,0 +1,955 @@
|
|||||||
|
#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
|
||||||
|
}
|
||||||
|
}
|
||||||
180
LightlessSync/ThirdParty/MeshDecimator/MeshDecimation.cs
vendored
Normal file
180
LightlessSync/ThirdParty/MeshDecimator/MeshDecimation.cs
vendored
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
#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
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,88 +0,0 @@
|
|||||||
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}>";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
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
144
LightlessSync/ThirdParty/Nanomesh/Base/BoneWeight.cs
vendored
@@ -1,144 +0,0 @@
|
|||||||
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
110
LightlessSync/ThirdParty/Nanomesh/Base/Color32.cs
vendored
@@ -1,110 +0,0 @@
|
|||||||
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); }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,347 +0,0 @@
|
|||||||
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]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
namespace Nanomesh
|
|
||||||
{
|
|
||||||
public interface IInterpolable<T>
|
|
||||||
{
|
|
||||||
T Interpolate(T other, double ratio);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
356
LightlessSync/ThirdParty/Nanomesh/Base/MathF.cs
vendored
356
LightlessSync/ThirdParty/Nanomesh/Base/MathF.cs
vendored
@@ -1,356 +0,0 @@
|
|||||||
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
114
LightlessSync/ThirdParty/Nanomesh/Base/MathUtils.cs
vendored
@@ -1,114 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
632
LightlessSync/ThirdParty/Nanomesh/Base/Quaternion.cs
vendored
632
LightlessSync/ThirdParty/Nanomesh/Base/Quaternion.cs
vendored
@@ -1,632 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
|
|
||||||
namespace Nanomesh
|
|
||||||
{
|
|
||||||
[StructLayout(LayoutKind.Sequential)]
|
|
||||||
public partial struct Quaternion : IEquatable<Quaternion>
|
|
||||||
{
|
|
||||||
private const double radToDeg = 180.0 / Math.PI;
|
|
||||||
private const double degToRad = Math.PI / 180.0;
|
|
||||||
|
|
||||||
public const double kEpsilon = 1E-20; // should probably be used in the 0 tests in LookRotation or Slerp
|
|
||||||
|
|
||||||
public Vector3 xyz
|
|
||||||
{
|
|
||||||
set
|
|
||||||
{
|
|
||||||
x = value.x;
|
|
||||||
y = value.y;
|
|
||||||
z = value.z;
|
|
||||||
}
|
|
||||||
get => new Vector3(x, y, z);
|
|
||||||
}
|
|
||||||
|
|
||||||
public double x;
|
|
||||||
|
|
||||||
public double y;
|
|
||||||
|
|
||||||
public double z;
|
|
||||||
|
|
||||||
public double w;
|
|
||||||
|
|
||||||
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 Quaternion index: " + index + ", can use only 0,1,2,3");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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 Quaternion index: " + index + ", can use only 0,1,2,3");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/// <summary>
|
|
||||||
/// <para>The identity rotation (RO).</para>
|
|
||||||
/// </summary>
|
|
||||||
public static Quaternion identity => new Quaternion(0, 0, 0, 1);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the length (magnitude) of the quaternion.
|
|
||||||
/// </summary>
|
|
||||||
/// <seealso cref="LengthSquared"/>
|
|
||||||
public double Length => (double)System.Math.Sqrt(x * x + y * y + z * z + w * w);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the square of the quaternion length (magnitude).
|
|
||||||
/// </summary>
|
|
||||||
public double LengthSquared => x * x + y * y + z * z + w * w;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// <para>Constructs new Quaternion with given x,y,z,w components.</para>
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="x"></param>
|
|
||||||
/// <param name="y"></param>
|
|
||||||
/// <param name="z"></param>
|
|
||||||
/// <param name="w"></param>
|
|
||||||
public Quaternion(double x, double y, double z, double w)
|
|
||||||
{
|
|
||||||
this.x = x;
|
|
||||||
this.y = y;
|
|
||||||
this.z = z;
|
|
||||||
this.w = w;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Construct a new Quaternion from vector and w components
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="v">The vector part</param>
|
|
||||||
/// <param name="w">The w part</param>
|
|
||||||
public Quaternion(Vector3 v, double w)
|
|
||||||
{
|
|
||||||
x = v.x;
|
|
||||||
y = v.y;
|
|
||||||
z = v.z;
|
|
||||||
this.w = w;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// <para>Set x, y, z and w components of an existing Quaternion.</para>
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="new_x"></param>
|
|
||||||
/// <param name="new_y"></param>
|
|
||||||
/// <param name="new_z"></param>
|
|
||||||
/// <param name="new_w"></param>
|
|
||||||
public void Set(double new_x, double new_y, double new_z, double new_w)
|
|
||||||
{
|
|
||||||
x = new_x;
|
|
||||||
y = new_y;
|
|
||||||
z = new_z;
|
|
||||||
w = new_w;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Scales the Quaternion to unit length.
|
|
||||||
/// </summary>
|
|
||||||
public static Quaternion Normalize(Quaternion q)
|
|
||||||
{
|
|
||||||
double mag = Math.Sqrt(Dot(q, q));
|
|
||||||
|
|
||||||
if (mag < kEpsilon)
|
|
||||||
{
|
|
||||||
return Quaternion.identity;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Quaternion(q.x / mag, q.y / mag, q.z / mag, q.w / mag);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Scale the given quaternion to unit length
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="q">The quaternion to normalize</param>
|
|
||||||
/// <param name="result">The normalized quaternion</param>
|
|
||||||
public void Normalize()
|
|
||||||
{
|
|
||||||
this = Normalize(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// <para>The dot product between two rotations.</para>
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="a"></param>
|
|
||||||
/// <param name="b"></param>
|
|
||||||
public static double Dot(Quaternion a, Quaternion b)
|
|
||||||
{
|
|
||||||
return a.x * b.x + a.y * b.y + a.z * b.z + a.w * b.w;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// <para>Creates a rotation which rotates /angle/ degrees around /axis/.</para>
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="angle"></param>
|
|
||||||
/// <param name="axis"></param>
|
|
||||||
public static Quaternion AngleAxis(double angle, Vector3 axis)
|
|
||||||
{
|
|
||||||
return Quaternion.AngleAxis(angle, ref axis);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Quaternion AngleAxis(double degress, ref Vector3 axis)
|
|
||||||
{
|
|
||||||
if (axis.LengthSquared == 0.0)
|
|
||||||
{
|
|
||||||
return identity;
|
|
||||||
}
|
|
||||||
|
|
||||||
Quaternion result = identity;
|
|
||||||
double radians = degress * degToRad;
|
|
||||||
radians *= 0.5;
|
|
||||||
axis = axis.Normalized;
|
|
||||||
axis = axis * Math.Sin(radians);
|
|
||||||
result.x = axis.x;
|
|
||||||
result.y = axis.y;
|
|
||||||
result.z = axis.z;
|
|
||||||
result.w = Math.Cos(radians);
|
|
||||||
|
|
||||||
return Normalize(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ToAngleAxis(out double angle, out Vector3 axis)
|
|
||||||
{
|
|
||||||
Quaternion.ToAxisAngleRad(this, out axis, out angle);
|
|
||||||
angle *= radToDeg;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// <para>Creates a rotation which rotates from /fromDirection/ to /toDirection/.</para>
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="fromDirection"></param>
|
|
||||||
/// <param name="toDirection"></param>
|
|
||||||
public static Quaternion FromToRotation(Vector3 fromDirection, Vector3 toDirection)
|
|
||||||
{
|
|
||||||
return RotateTowards(LookRotation(fromDirection), LookRotation(toDirection), double.MaxValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// <para>Creates a rotation which rotates from /fromDirection/ to /toDirection/.</para>
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="fromDirection"></param>
|
|
||||||
/// <param name="toDirection"></param>
|
|
||||||
public void SetFromToRotation(Vector3 fromDirection, Vector3 toDirection)
|
|
||||||
{
|
|
||||||
this = Quaternion.FromToRotation(fromDirection, toDirection);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// <para>Creates a rotation with the specified /forward/ and /upwards/ directions.</para>
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="forward">The direction to look in.</param>
|
|
||||||
/// <param name="upwards">The vector that defines in which direction up is.</param>
|
|
||||||
public static Quaternion LookRotation(Vector3 forward, Vector3 upwards)
|
|
||||||
{
|
|
||||||
return Quaternion.LookRotation(ref forward, ref upwards);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Quaternion LookRotation(Vector3 forward)
|
|
||||||
{
|
|
||||||
Vector3 up = new Vector3(1, 0, 0);
|
|
||||||
return Quaternion.LookRotation(ref forward, ref up);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Quaternion LookRotation(ref Vector3 forward, ref Vector3 up)
|
|
||||||
{
|
|
||||||
forward = Vector3.Normalize(forward);
|
|
||||||
Vector3 right = Vector3.Normalize(Vector3.Cross(up, forward));
|
|
||||||
up = Vector3.Cross(forward, right);
|
|
||||||
double m00 = right.x;
|
|
||||||
double m01 = right.y;
|
|
||||||
double m02 = right.z;
|
|
||||||
double m10 = up.x;
|
|
||||||
double m11 = up.y;
|
|
||||||
double m12 = up.z;
|
|
||||||
double m20 = forward.x;
|
|
||||||
double m21 = forward.y;
|
|
||||||
double m22 = forward.z;
|
|
||||||
|
|
||||||
double num8 = (m00 + m11) + m22;
|
|
||||||
Quaternion quaternion = new Quaternion();
|
|
||||||
if (num8 > 0)
|
|
||||||
{
|
|
||||||
double num = Math.Sqrt(num8 + 1);
|
|
||||||
quaternion.w = num * 0.5;
|
|
||||||
num = 0.5 / num;
|
|
||||||
quaternion.x = (m12 - m21) * num;
|
|
||||||
quaternion.y = (m20 - m02) * num;
|
|
||||||
quaternion.z = (m01 - m10) * num;
|
|
||||||
return quaternion;
|
|
||||||
}
|
|
||||||
if ((m00 >= m11) && (m00 >= m22))
|
|
||||||
{
|
|
||||||
double num7 = Math.Sqrt(((1 + m00) - m11) - m22);
|
|
||||||
double num4 = 0.5 / num7;
|
|
||||||
quaternion.x = 0.5 * num7;
|
|
||||||
quaternion.y = (m01 + m10) * num4;
|
|
||||||
quaternion.z = (m02 + m20) * num4;
|
|
||||||
quaternion.w = (m12 - m21) * num4;
|
|
||||||
return quaternion;
|
|
||||||
}
|
|
||||||
if (m11 > m22)
|
|
||||||
{
|
|
||||||
double num6 = Math.Sqrt(((1 + m11) - m00) - m22);
|
|
||||||
double num3 = 0.5 / num6;
|
|
||||||
quaternion.x = (m10 + m01) * num3;
|
|
||||||
quaternion.y = 0.5 * num6;
|
|
||||||
quaternion.z = (m21 + m12) * num3;
|
|
||||||
quaternion.w = (m20 - m02) * num3;
|
|
||||||
return quaternion;
|
|
||||||
}
|
|
||||||
double num5 = Math.Sqrt(((1 + m22) - m00) - m11);
|
|
||||||
double num2 = 0.5 / num5;
|
|
||||||
quaternion.x = (m20 + m02) * num2;
|
|
||||||
quaternion.y = (m21 + m12) * num2;
|
|
||||||
quaternion.z = 0.5 * num5;
|
|
||||||
quaternion.w = (m01 - m10) * num2;
|
|
||||||
return quaternion;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SetLookRotation(Vector3 view)
|
|
||||||
{
|
|
||||||
Vector3 up = new Vector3(1, 0, 0);
|
|
||||||
SetLookRotation(view, up);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// <para>Creates a rotation with the specified /forward/ and /upwards/ directions.</para>
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="view">The direction to look in.</param>
|
|
||||||
/// <param name="up">The vector that defines in which direction up is.</param>
|
|
||||||
public void SetLookRotation(Vector3 view, Vector3 up)
|
|
||||||
{
|
|
||||||
this = Quaternion.LookRotation(view, up);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// <para>Spherically interpolates between /a/ and /b/ by t. The parameter /t/ is clamped to the range [0, 1].</para>
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="a"></param>
|
|
||||||
/// <param name="b"></param>
|
|
||||||
/// <param name="t"></param>
|
|
||||||
public static Quaternion Slerp(Quaternion a, Quaternion b, double t)
|
|
||||||
{
|
|
||||||
return Quaternion.Slerp(ref a, ref b, t);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Quaternion Slerp(ref Quaternion a, ref Quaternion b, double t)
|
|
||||||
{
|
|
||||||
if (t > 1)
|
|
||||||
{
|
|
||||||
t = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (t < 0)
|
|
||||||
{
|
|
||||||
t = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return SlerpUnclamped(ref a, ref b, t);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// <para>Spherically interpolates between /a/ and /b/ by t. The parameter /t/ is not clamped.</para>
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="a"></param>
|
|
||||||
/// <param name="b"></param>
|
|
||||||
/// <param name="t"></param>
|
|
||||||
public static Quaternion SlerpUnclamped(Quaternion a, Quaternion b, double t)
|
|
||||||
{
|
|
||||||
|
|
||||||
return Quaternion.SlerpUnclamped(ref a, ref b, t);
|
|
||||||
}
|
|
||||||
private static Quaternion SlerpUnclamped(ref Quaternion a, ref Quaternion b, double t)
|
|
||||||
{
|
|
||||||
// if either input is zero, return the other.
|
|
||||||
if (a.LengthSquared == 0.0)
|
|
||||||
{
|
|
||||||
if (b.LengthSquared == 0.0)
|
|
||||||
{
|
|
||||||
return identity;
|
|
||||||
}
|
|
||||||
return b;
|
|
||||||
}
|
|
||||||
else if (b.LengthSquared == 0.0)
|
|
||||||
{
|
|
||||||
return a;
|
|
||||||
}
|
|
||||||
|
|
||||||
double cosHalfAngle = a.w * b.w + Vector3.Dot(a.xyz, b.xyz);
|
|
||||||
|
|
||||||
if (cosHalfAngle >= 1.0 || cosHalfAngle <= -1.0)
|
|
||||||
{
|
|
||||||
// angle = 0.0f, so just return one input.
|
|
||||||
return a;
|
|
||||||
}
|
|
||||||
else if (cosHalfAngle < 0.0)
|
|
||||||
{
|
|
||||||
b.xyz = -b.xyz;
|
|
||||||
b.w = -b.w;
|
|
||||||
cosHalfAngle = -cosHalfAngle;
|
|
||||||
}
|
|
||||||
|
|
||||||
double blendA;
|
|
||||||
double blendB;
|
|
||||||
if (cosHalfAngle < 0.99)
|
|
||||||
{
|
|
||||||
// do proper slerp for big angles
|
|
||||||
double halfAngle = Math.Acos(cosHalfAngle);
|
|
||||||
double sinHalfAngle = Math.Sin(halfAngle);
|
|
||||||
double oneOverSinHalfAngle = 1.0 / sinHalfAngle;
|
|
||||||
blendA = Math.Sin(halfAngle * (1.0 - t)) * oneOverSinHalfAngle;
|
|
||||||
blendB = Math.Sin(halfAngle * t) * oneOverSinHalfAngle;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// do lerp if angle is really small.
|
|
||||||
blendA = 1.0f - t;
|
|
||||||
blendB = t;
|
|
||||||
}
|
|
||||||
|
|
||||||
Quaternion result = new Quaternion(blendA * a.xyz + blendB * b.xyz, blendA * a.w + blendB * b.w);
|
|
||||||
if (result.LengthSquared > 0.0)
|
|
||||||
{
|
|
||||||
return Normalize(result);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return identity;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// <para>Interpolates between /a/ and /b/ by /t/ and normalizes the result afterwards. The parameter /t/ is clamped to the range [0, 1].</para>
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="a"></param>
|
|
||||||
/// <param name="b"></param>
|
|
||||||
/// <param name="t"></param>
|
|
||||||
public static Quaternion Lerp(Quaternion a, Quaternion b, double t)
|
|
||||||
{
|
|
||||||
if (t > 1)
|
|
||||||
{
|
|
||||||
t = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (t < 0)
|
|
||||||
{
|
|
||||||
t = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Slerp(ref a, ref b, t); // TODO: use lerp not slerp, "Because quaternion works in 4D. Rotation in 4D are linear" ???
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// <para>Interpolates between /a/ and /b/ by /t/ and normalizes the result afterwards. The parameter /t/ is not clamped.</para>
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="a"></param>
|
|
||||||
/// <param name="b"></param>
|
|
||||||
/// <param name="t"></param>
|
|
||||||
public static Quaternion LerpUnclamped(Quaternion a, Quaternion b, double t)
|
|
||||||
{
|
|
||||||
return Slerp(ref a, ref b, t);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// <para>Rotates a rotation /from/ towards /to/.</para>
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="from"></param>
|
|
||||||
/// <param name="to"></param>
|
|
||||||
/// <param name="maxDegreesDelta"></param>
|
|
||||||
public static Quaternion RotateTowards(Quaternion from, Quaternion to, double maxDegreesDelta)
|
|
||||||
{
|
|
||||||
double num = Quaternion.Angle(from, to);
|
|
||||||
if (num == 0)
|
|
||||||
{
|
|
||||||
return to;
|
|
||||||
}
|
|
||||||
double t = Math.Min(1, maxDegreesDelta / num);
|
|
||||||
return Quaternion.SlerpUnclamped(from, to, t);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// <para>Returns the Inverse of /rotation/.</para>
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="rotation"></param>
|
|
||||||
public static Quaternion Inverse(Quaternion rotation)
|
|
||||||
{
|
|
||||||
double lengthSq = rotation.LengthSquared;
|
|
||||||
if (lengthSq != 0.0)
|
|
||||||
{
|
|
||||||
double i = 1.0 / lengthSq;
|
|
||||||
return new Quaternion(rotation.xyz * -i, rotation.w * i);
|
|
||||||
}
|
|
||||||
return rotation;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// <para>Returns a nicely formatted string of the Quaternion.</para>
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="format"></param>
|
|
||||||
public override string ToString()
|
|
||||||
{
|
|
||||||
return $"{x}, {y}, {z}, {w}";
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// <para>Returns a nicely formatted string of the Quaternion.</para>
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="format"></param>
|
|
||||||
public string ToString(string format)
|
|
||||||
{
|
|
||||||
return string.Format("({0}, {1}, {2}, {3})", x.ToString(format), y.ToString(format), z.ToString(format), w.ToString(format));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// <para>Returns the angle in degrees between two rotations /a/ and /b/.</para>
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="a"></param>
|
|
||||||
/// <param name="b"></param>
|
|
||||||
public static double Angle(Quaternion a, Quaternion b)
|
|
||||||
{
|
|
||||||
double f = Quaternion.Dot(a, b);
|
|
||||||
return Math.Acos(Math.Min(Math.Abs(f), 1)) * 2 * radToDeg;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// <para>Returns a rotation that rotates z degrees around the z axis, x degrees around the x axis, and y degrees around the y axis (in that order).</para>
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="x"></param>
|
|
||||||
/// <param name="y"></param>
|
|
||||||
/// <param name="z"></param>
|
|
||||||
public static Quaternion Euler(double x, double y, double z)
|
|
||||||
{
|
|
||||||
return Quaternion.FromEulerRad(new Vector3((double)x, (double)y, (double)z) * degToRad);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// <para>Returns a rotation that rotates z degrees around the z axis, x degrees around the x axis, and y degrees around the y axis (in that order).</para>
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="euler"></param>
|
|
||||||
public static Quaternion Euler(Vector3 euler)
|
|
||||||
{
|
|
||||||
return Quaternion.FromEulerRad(euler * degToRad);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static double NormalizeAngle(double angle)
|
|
||||||
{
|
|
||||||
while (angle > 360)
|
|
||||||
{
|
|
||||||
angle -= 360;
|
|
||||||
}
|
|
||||||
|
|
||||||
while (angle < 0)
|
|
||||||
{
|
|
||||||
angle += 360;
|
|
||||||
}
|
|
||||||
|
|
||||||
return angle;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Quaternion FromEulerRad(Vector3 euler)
|
|
||||||
{
|
|
||||||
double yaw = euler.x;
|
|
||||||
double pitch = euler.y;
|
|
||||||
double roll = euler.z;
|
|
||||||
double rollOver2 = roll * 0.5;
|
|
||||||
double sinRollOver2 = (double)System.Math.Sin((double)rollOver2);
|
|
||||||
double cosRollOver2 = (double)System.Math.Cos((double)rollOver2);
|
|
||||||
double pitchOver2 = pitch * 0.5;
|
|
||||||
double sinPitchOver2 = (double)System.Math.Sin((double)pitchOver2);
|
|
||||||
double cosPitchOver2 = (double)System.Math.Cos((double)pitchOver2);
|
|
||||||
double yawOver2 = yaw * 0.5;
|
|
||||||
double sinYawOver2 = (double)System.Math.Sin((double)yawOver2);
|
|
||||||
double cosYawOver2 = (double)System.Math.Cos((double)yawOver2);
|
|
||||||
Quaternion result;
|
|
||||||
result.x = cosYawOver2 * cosPitchOver2 * cosRollOver2 + sinYawOver2 * sinPitchOver2 * sinRollOver2;
|
|
||||||
result.y = cosYawOver2 * cosPitchOver2 * sinRollOver2 - sinYawOver2 * sinPitchOver2 * cosRollOver2;
|
|
||||||
result.z = cosYawOver2 * sinPitchOver2 * cosRollOver2 + sinYawOver2 * cosPitchOver2 * sinRollOver2;
|
|
||||||
result.w = sinYawOver2 * cosPitchOver2 * cosRollOver2 - cosYawOver2 * sinPitchOver2 * sinRollOver2;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ToAxisAngleRad(Quaternion q, out Vector3 axis, out double angle)
|
|
||||||
{
|
|
||||||
if (System.Math.Abs(q.w) > 1.0)
|
|
||||||
{
|
|
||||||
q.Normalize();
|
|
||||||
}
|
|
||||||
|
|
||||||
angle = 2.0f * (double)System.Math.Acos(q.w); // angle
|
|
||||||
double den = (double)System.Math.Sqrt(1.0 - q.w * q.w);
|
|
||||||
if (den > 0.0001)
|
|
||||||
{
|
|
||||||
axis = q.xyz / den;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// This occurs when the angle is zero.
|
|
||||||
// Not a problem: just set an arbitrary normalized axis.
|
|
||||||
axis = new Vector3(1, 0, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override int GetHashCode()
|
|
||||||
{
|
|
||||||
return x.GetHashCode() ^ y.GetHashCode() << 2 ^ z.GetHashCode() >> 2 ^ w.GetHashCode() >> 1;
|
|
||||||
}
|
|
||||||
public override bool Equals(object other)
|
|
||||||
{
|
|
||||||
if (!(other is Quaternion))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
Quaternion quaternion = (Quaternion)other;
|
|
||||||
return x.Equals(quaternion.x) && y.Equals(quaternion.y) && z.Equals(quaternion.z) && w.Equals(quaternion.w);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Equals(Quaternion other)
|
|
||||||
{
|
|
||||||
return x.Equals(other.x) && y.Equals(other.y) && z.Equals(other.z) && w.Equals(other.w);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Quaternion operator *(Quaternion lhs, Quaternion rhs)
|
|
||||||
{
|
|
||||||
return new Quaternion(lhs.w * rhs.x + lhs.x * rhs.w + lhs.y * rhs.z - lhs.z * rhs.y, lhs.w * rhs.y + lhs.y * rhs.w + lhs.z * rhs.x - lhs.x * rhs.z, lhs.w * rhs.z + lhs.z * rhs.w + lhs.x * rhs.y - lhs.y * rhs.x, lhs.w * rhs.w - lhs.x * rhs.x - lhs.y * rhs.y - lhs.z * rhs.z);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Vector3 operator *(Quaternion rotation, Vector3 point)
|
|
||||||
{
|
|
||||||
double num = rotation.x * 2;
|
|
||||||
double num2 = rotation.y * 2;
|
|
||||||
double num3 = rotation.z * 2;
|
|
||||||
double num4 = rotation.x * num;
|
|
||||||
double num5 = rotation.y * num2;
|
|
||||||
double num6 = rotation.z * num3;
|
|
||||||
double num7 = rotation.x * num2;
|
|
||||||
double num8 = rotation.x * num3;
|
|
||||||
double num9 = rotation.y * num3;
|
|
||||||
double num10 = rotation.w * num;
|
|
||||||
double num11 = rotation.w * num2;
|
|
||||||
double num12 = rotation.w * num3;
|
|
||||||
|
|
||||||
return new Vector3(
|
|
||||||
(1 - (num5 + num6)) * point.x + (num7 - num12) * point.y + (num8 + num11) * point.z,
|
|
||||||
(num7 + num12) * point.x + (1 - (num4 + num6)) * point.y + (num9 - num10) * point.z,
|
|
||||||
(num8 - num11) * point.x + (num9 + num10) * point.y + (1 - (num4 + num5)) * point.z);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool operator ==(Quaternion lhs, Quaternion rhs)
|
|
||||||
{
|
|
||||||
return Quaternion.Dot(lhs, rhs) > 0.999999999;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool operator !=(Quaternion lhs, Quaternion rhs)
|
|
||||||
{
|
|
||||||
return Quaternion.Dot(lhs, rhs) <= 0.999999999;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user