Compare commits
13 Commits
ban-admin-
...
2.0.2.69-D
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cca23f6e05 | ||
|
|
3205e6e0c3 | ||
|
|
d16e46200d | ||
|
|
5fc13647ae | ||
|
|
39d5d9d7c1 | ||
|
|
c19db58ead | ||
| 30717ba200 | |||
| e0b8070aa8 | |||
| 3241b9222b | |||
|
|
de9c9955ef | ||
|
|
df33a0f0a2 | ||
| c439d1c822 | |||
|
|
fb58d8657d |
@@ -103,6 +103,7 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
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 Dictionary<string, WatcherChange> _watcherChanges = new(StringComparer.OrdinalIgnoreCase);
|
private readonly Dictionary<string, WatcherChange> _watcherChanges = new(StringComparer.OrdinalIgnoreCase);
|
||||||
private readonly Dictionary<string, WatcherChange> _lightlessChanges = new(StringComparer.OrdinalIgnoreCase);
|
private readonly Dictionary<string, WatcherChange> _lightlessChanges = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
@@ -441,116 +442,40 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
|||||||
Logger.LogWarning(ex, "Could not determine drive size for storage folder {folder}", _configService.Current.CacheFolder);
|
Logger.LogWarning(ex, "Could not determine drive size for storage folder {folder}", _configService.Current.CacheFolder);
|
||||||
}
|
}
|
||||||
|
|
||||||
var files = Directory.EnumerateFiles(_configService.Current.CacheFolder)
|
var cacheFolder = _configService.Current.CacheFolder;
|
||||||
.Select(f => new FileInfo(f))
|
var candidates = new List<CacheEvictionCandidate>();
|
||||||
.OrderBy(f => f.LastAccessTime)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
long totalSize = 0;
|
long totalSize = 0;
|
||||||
|
totalSize += AddFolderCandidates(cacheFolder, candidates, token, isWine);
|
||||||
foreach (var f in files)
|
totalSize += AddFolderCandidates(Path.Combine(cacheFolder, "downscaled"), candidates, token, isWine);
|
||||||
{
|
totalSize += AddFolderCandidates(Path.Combine(cacheFolder, "decimated"), candidates, token, isWine);
|
||||||
token.ThrowIfCancellationRequested();
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
long size = 0;
|
|
||||||
|
|
||||||
if (!isWine)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
size = _fileCompactor.GetFileSizeOnDisk(f);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Logger.LogTrace(ex, "GetFileSizeOnDisk failed for {file}, using fallback length", f.FullName);
|
|
||||||
size = f.Length;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
size = f.Length;
|
|
||||||
}
|
|
||||||
|
|
||||||
totalSize += size;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Logger.LogTrace(ex, "Error getting size for {file}", f.FullName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FileCacheSize = totalSize;
|
FileCacheSize = totalSize;
|
||||||
|
|
||||||
if (Directory.Exists(_configService.Current.CacheFolder + "/downscaled"))
|
|
||||||
{
|
|
||||||
var filesDownscaled = Directory.EnumerateFiles(_configService.Current.CacheFolder + "/downscaled").Select(f => new FileInfo(f)).OrderBy(f => f.LastAccessTime).ToList();
|
|
||||||
|
|
||||||
long totalSizeDownscaled = 0;
|
|
||||||
|
|
||||||
foreach (var f in filesDownscaled)
|
|
||||||
{
|
|
||||||
token.ThrowIfCancellationRequested();
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
long size = 0;
|
|
||||||
|
|
||||||
if (!isWine)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
size = _fileCompactor.GetFileSizeOnDisk(f);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Logger.LogTrace(ex, "GetFileSizeOnDisk failed for {file}, using fallback length", f.FullName);
|
|
||||||
size = f.Length;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
size = f.Length;
|
|
||||||
}
|
|
||||||
|
|
||||||
totalSizeDownscaled += size;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Logger.LogTrace(ex, "Error getting size for {file}", f.FullName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FileCacheSize = (totalSize + totalSizeDownscaled);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
FileCacheSize = totalSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
var maxCacheInBytes = (long)(_configService.Current.MaxLocalCacheInGiB * 1024d * 1024d * 1024d);
|
var maxCacheInBytes = (long)(_configService.Current.MaxLocalCacheInGiB * 1024d * 1024d * 1024d);
|
||||||
if (FileCacheSize < maxCacheInBytes)
|
if (FileCacheSize < maxCacheInBytes)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var maxCacheBuffer = maxCacheInBytes * 0.05d;
|
var maxCacheBuffer = maxCacheInBytes * 0.05d;
|
||||||
|
|
||||||
while (FileCacheSize > maxCacheInBytes - (long)maxCacheBuffer && files.Count > 0)
|
candidates.Sort(static (a, b) => a.LastAccessTime.CompareTo(b.LastAccessTime));
|
||||||
|
|
||||||
|
var evictionTarget = maxCacheInBytes - (long)maxCacheBuffer;
|
||||||
|
var index = 0;
|
||||||
|
while (FileCacheSize > evictionTarget && index < candidates.Count)
|
||||||
{
|
{
|
||||||
var oldestFile = files[0];
|
var oldestFile = candidates[index];
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
long fileSize = oldestFile.Length;
|
EvictCacheCandidate(oldestFile, cacheFolder);
|
||||||
File.Delete(oldestFile.FullName);
|
FileCacheSize -= oldestFile.Size;
|
||||||
FileCacheSize -= fileSize;
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Logger.LogTrace(ex, "Failed to delete old file {file}", oldestFile.FullName);
|
Logger.LogTrace(ex, "Failed to delete old file {file}", oldestFile.FullPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
files.RemoveAt(0);
|
index++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -559,6 +484,114 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
|||||||
HaltScanLocks.Clear();
|
HaltScanLocks.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private long AddFolderCandidates(string directory, List<CacheEvictionCandidate> candidates, CancellationToken token, bool isWine)
|
||||||
|
{
|
||||||
|
if (!Directory.Exists(directory))
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
long totalSize = 0;
|
||||||
|
foreach (var path in Directory.EnumerateFiles(directory))
|
||||||
|
{
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var file = new FileInfo(path);
|
||||||
|
var size = GetFileSizeOnDisk(file, isWine);
|
||||||
|
totalSize += size;
|
||||||
|
candidates.Add(new CacheEvictionCandidate(file.FullName, size, file.LastAccessTime));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogTrace(ex, "Error getting size for {file}", path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return totalSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
private long GetFileSizeOnDisk(FileInfo file, bool isWine)
|
||||||
|
{
|
||||||
|
if (isWine)
|
||||||
|
{
|
||||||
|
return file.Length;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return _fileCompactor.GetFileSizeOnDisk(file);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogTrace(ex, "GetFileSizeOnDisk failed for {file}, using fallback length", file.FullName);
|
||||||
|
return file.Length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EvictCacheCandidate(CacheEvictionCandidate candidate, string cacheFolder)
|
||||||
|
{
|
||||||
|
if (TryGetCacheHashAndPrefixedPath(candidate.FullPath, cacheFolder, out var hash, out var prefixedPath))
|
||||||
|
{
|
||||||
|
_fileDbManager.RemoveHashedFile(hash, prefixedPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (File.Exists(candidate.FullPath))
|
||||||
|
{
|
||||||
|
File.Delete(candidate.FullPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogTrace(ex, "Failed to delete old file {file}", candidate.FullPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryGetCacheHashAndPrefixedPath(string filePath, string cacheFolder, out string hash, out string prefixedPath)
|
||||||
|
{
|
||||||
|
hash = string.Empty;
|
||||||
|
prefixedPath = string.Empty;
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(cacheFolder))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var fileName = Path.GetFileNameWithoutExtension(filePath);
|
||||||
|
if (string.IsNullOrEmpty(fileName) || !IsSha1Hash(fileName))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var relative = Path.GetRelativePath(cacheFolder, filePath)
|
||||||
|
.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
|
||||||
|
var sanitizedRelative = relative.TrimStart(Path.DirectorySeparatorChar);
|
||||||
|
prefixedPath = Path.Combine(FileCacheManager.CachePrefix, sanitizedRelative);
|
||||||
|
hash = fileName;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsSha1Hash(string value)
|
||||||
|
{
|
||||||
|
if (value.Length != 40)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var ch in value)
|
||||||
|
{
|
||||||
|
if (!Uri.IsHexDigit(ch))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
public void ResumeScan(string source)
|
public void ResumeScan(string source)
|
||||||
{
|
{
|
||||||
if (!HaltScanLocks.ContainsKey(source)) HaltScanLocks[source] = 0;
|
if (!HaltScanLocks.ContainsKey(source)) HaltScanLocks[source] = 0;
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ public sealed class FileCacheManager : IHostedService
|
|||||||
private readonly ConcurrentDictionary<string, ConcurrentDictionary<string, FileCacheEntity>> _fileCaches = new(StringComparer.Ordinal);
|
private readonly ConcurrentDictionary<string, ConcurrentDictionary<string, FileCacheEntity>> _fileCaches = new(StringComparer.Ordinal);
|
||||||
private readonly ConcurrentDictionary<string, FileCacheEntity> _fileCachesByPrefixedPath = new(StringComparer.OrdinalIgnoreCase);
|
private readonly ConcurrentDictionary<string, FileCacheEntity> _fileCachesByPrefixedPath = new(StringComparer.OrdinalIgnoreCase);
|
||||||
private readonly SemaphoreSlim _getCachesByPathsSemaphore = new(1, 1);
|
private readonly SemaphoreSlim _getCachesByPathsSemaphore = new(1, 1);
|
||||||
|
private readonly SemaphoreSlim _evictSemaphore = new(1, 1);
|
||||||
private readonly Lock _fileWriteLock = new();
|
private readonly Lock _fileWriteLock = new();
|
||||||
private readonly IpcManager _ipcManager;
|
private readonly IpcManager _ipcManager;
|
||||||
private readonly ILogger<FileCacheManager> _logger;
|
private readonly ILogger<FileCacheManager> _logger;
|
||||||
@@ -226,13 +227,23 @@ public sealed class FileCacheManager : IHostedService
|
|||||||
var compressed = LZ4Wrapper.WrapHC(raw, 0, raw.Length);
|
var compressed = LZ4Wrapper.WrapHC(raw, 0, raw.Length);
|
||||||
|
|
||||||
var tmpPath = compressedPath + ".tmp";
|
var tmpPath = compressedPath + ".tmp";
|
||||||
await File.WriteAllBytesAsync(tmpPath, compressed, token).ConfigureAwait(false);
|
try
|
||||||
File.Move(tmpPath, compressedPath, overwrite: true);
|
{
|
||||||
|
await File.WriteAllBytesAsync(tmpPath, compressed, token).ConfigureAwait(false);
|
||||||
|
File.Move(tmpPath, compressedPath, overwrite: true);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
try { if (File.Exists(tmpPath)) File.Delete(tmpPath); } catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
|
||||||
var compressedSize = compressed.LongLength;
|
var compressedSize = new FileInfo(compressedPath).Length;
|
||||||
SetSizeInfo(hash, originalSize, compressedSize);
|
SetSizeInfo(hash, originalSize, compressedSize);
|
||||||
UpdateEntitiesSizes(hash, originalSize, compressedSize);
|
UpdateEntitiesSizes(hash, originalSize, compressedSize);
|
||||||
|
|
||||||
|
var maxBytes = GiBToBytes(_configService.Current.MaxLocalCacheInGiB);
|
||||||
|
await EnforceCacheLimitAsync(maxBytes, token).ConfigureAwait(false);
|
||||||
|
|
||||||
return compressed;
|
return compressed;
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
@@ -280,6 +291,26 @@ public sealed class FileCacheManager : IHostedService
|
|||||||
return CreateFileEntity(cacheFolder, CachePrefix, fi);
|
return CreateFileEntity(cacheFolder, CachePrefix, fi);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public FileCacheEntity? CreateCacheEntryWithKnownHash(string path, string hash)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(hash))
|
||||||
|
{
|
||||||
|
return CreateCacheEntry(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
FileInfo fi = new(path);
|
||||||
|
if (!fi.Exists) return null;
|
||||||
|
_logger.LogTrace("Creating cache entry for {path} using provided hash", path);
|
||||||
|
var cacheFolder = _configService.Current.CacheFolder;
|
||||||
|
if (string.IsNullOrEmpty(cacheFolder)) return null;
|
||||||
|
if (!TryBuildPrefixedPath(fi.FullName, cacheFolder, CachePrefix, out var prefixedPath, out _))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return CreateFileCacheEntity(fi, prefixedPath, hash);
|
||||||
|
}
|
||||||
|
|
||||||
public FileCacheEntity? CreateFileEntry(string path)
|
public FileCacheEntity? CreateFileEntry(string path)
|
||||||
{
|
{
|
||||||
FileInfo fi = new(path);
|
FileInfo fi = new(path);
|
||||||
@@ -562,9 +593,10 @@ public sealed class FileCacheManager : IHostedService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RemoveHashedFile(string hash, string prefixedFilePath)
|
public void RemoveHashedFile(string hash, string prefixedFilePath, bool removeDerivedFiles = true)
|
||||||
{
|
{
|
||||||
var normalizedPath = NormalizePrefixedPathKey(prefixedFilePath);
|
var normalizedPath = NormalizePrefixedPathKey(prefixedFilePath);
|
||||||
|
var removedHash = false;
|
||||||
|
|
||||||
if (_fileCaches.TryGetValue(hash, out var caches))
|
if (_fileCaches.TryGetValue(hash, out var caches))
|
||||||
{
|
{
|
||||||
@@ -577,11 +609,16 @@ public sealed class FileCacheManager : IHostedService
|
|||||||
|
|
||||||
if (caches.IsEmpty)
|
if (caches.IsEmpty)
|
||||||
{
|
{
|
||||||
_fileCaches.TryRemove(hash, out _);
|
removedHash = _fileCaches.TryRemove(hash, out _);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_fileCachesByPrefixedPath.TryRemove(normalizedPath, out _);
|
_fileCachesByPrefixedPath.TryRemove(normalizedPath, out _);
|
||||||
|
|
||||||
|
if (removeDerivedFiles && removedHash)
|
||||||
|
{
|
||||||
|
RemoveDerivedCacheFiles(hash);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void UpdateHashedFile(FileCacheEntity fileCache, bool computeProperties = true)
|
public void UpdateHashedFile(FileCacheEntity fileCache, bool computeProperties = true)
|
||||||
@@ -597,7 +634,8 @@ public sealed class FileCacheManager : IHostedService
|
|||||||
fileCache.Hash = Crypto.ComputeFileHash(fileCache.ResolvedFilepath, Crypto.HashAlgo.Sha1);
|
fileCache.Hash = Crypto.ComputeFileHash(fileCache.ResolvedFilepath, Crypto.HashAlgo.Sha1);
|
||||||
fileCache.LastModifiedDateTicks = fi.LastWriteTimeUtc.Ticks.ToString(CultureInfo.InvariantCulture);
|
fileCache.LastModifiedDateTicks = fi.LastWriteTimeUtc.Ticks.ToString(CultureInfo.InvariantCulture);
|
||||||
}
|
}
|
||||||
RemoveHashedFile(oldHash, prefixedPath);
|
var removeDerivedFiles = !string.Equals(oldHash, fileCache.Hash, StringComparison.OrdinalIgnoreCase);
|
||||||
|
RemoveHashedFile(oldHash, prefixedPath, removeDerivedFiles);
|
||||||
AddHashedFile(fileCache);
|
AddHashedFile(fileCache);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -747,7 +785,7 @@ public sealed class FileCacheManager : IHostedService
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
RemoveHashedFile(fileCache.Hash, fileCache.PrefixedFilePath);
|
RemoveHashedFile(fileCache.Hash, fileCache.PrefixedFilePath, removeDerivedFiles: false);
|
||||||
var extensionPath = fileCache.ResolvedFilepath.ToUpper(CultureInfo.InvariantCulture) + "." + ext;
|
var extensionPath = fileCache.ResolvedFilepath.ToUpper(CultureInfo.InvariantCulture) + "." + ext;
|
||||||
File.Move(fileCache.ResolvedFilepath, extensionPath, overwrite: true);
|
File.Move(fileCache.ResolvedFilepath, extensionPath, overwrite: true);
|
||||||
var newHashedEntity = new FileCacheEntity(fileCache.Hash, fileCache.PrefixedFilePath + "." + ext, DateTime.UtcNow.Ticks.ToString(CultureInfo.InvariantCulture));
|
var newHashedEntity = new FileCacheEntity(fileCache.Hash, fileCache.PrefixedFilePath + "." + ext, DateTime.UtcNow.Ticks.ToString(CultureInfo.InvariantCulture));
|
||||||
@@ -764,6 +802,33 @@ public sealed class FileCacheManager : IHostedService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void RemoveDerivedCacheFiles(string hash)
|
||||||
|
{
|
||||||
|
var cacheFolder = _configService.Current.CacheFolder;
|
||||||
|
if (string.IsNullOrWhiteSpace(cacheFolder))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
TryDeleteDerivedCacheFile(Path.Combine(cacheFolder, "downscaled", $"{hash}.tex"));
|
||||||
|
TryDeleteDerivedCacheFile(Path.Combine(cacheFolder, "decimated", $"{hash}.mdl"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TryDeleteDerivedCacheFile(string path)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (File.Exists(path))
|
||||||
|
{
|
||||||
|
File.Delete(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogTrace(ex, "Failed to delete derived cache file {path}", path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void AddHashedFile(FileCacheEntity fileCache)
|
private void AddHashedFile(FileCacheEntity fileCache)
|
||||||
{
|
{
|
||||||
var normalizedPath = NormalizePrefixedPathKey(fileCache.PrefixedFilePath);
|
var normalizedPath = NormalizePrefixedPathKey(fileCache.PrefixedFilePath);
|
||||||
@@ -877,6 +942,83 @@ public sealed class FileCacheManager : IHostedService
|
|||||||
}, token).ConfigureAwait(false);
|
}, token).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task EnforceCacheLimitAsync(long maxBytes, CancellationToken token)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(CacheFolder) || maxBytes <= 0) return;
|
||||||
|
|
||||||
|
await _evictSemaphore.WaitAsync(token).ConfigureAwait(false);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(CacheFolder);
|
||||||
|
|
||||||
|
foreach (var tmp in Directory.EnumerateFiles(CacheFolder, "*" + _compressedCacheExtension + ".tmp"))
|
||||||
|
{
|
||||||
|
try { File.Delete(tmp); } catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
var files = Directory.EnumerateFiles(CacheFolder, "*" + _compressedCacheExtension, SearchOption.TopDirectoryOnly)
|
||||||
|
.Select(p => new FileInfo(p))
|
||||||
|
.Where(fi => fi.Exists)
|
||||||
|
.OrderBy(fi => fi.LastWriteTimeUtc)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
long total = files.Sum(f => f.Length);
|
||||||
|
if (total <= maxBytes) return;
|
||||||
|
|
||||||
|
foreach (var fi in files)
|
||||||
|
{
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
if (total <= maxBytes) break;
|
||||||
|
|
||||||
|
var hash = Path.GetFileNameWithoutExtension(fi.Name);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var len = fi.Length;
|
||||||
|
fi.Delete();
|
||||||
|
total -= len;
|
||||||
|
_sizeCache.TryRemove(hash, out _);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Failed to evict cache file {file}", fi.FullName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_evictSemaphore.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long GiBToBytes(double gib)
|
||||||
|
{
|
||||||
|
if (double.IsNaN(gib) || double.IsInfinity(gib) || gib <= 0)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
var bytes = gib * 1024d * 1024d * 1024d;
|
||||||
|
|
||||||
|
if (bytes >= long.MaxValue) return long.MaxValue;
|
||||||
|
|
||||||
|
return (long)Math.Round(bytes, MidpointRounding.AwayFromZero);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CleanupOrphanCompressedCache()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(CacheFolder) || !Directory.Exists(CacheFolder))
|
||||||
|
return;
|
||||||
|
|
||||||
|
foreach (var path in Directory.EnumerateFiles(CacheFolder, "*" + _compressedCacheExtension))
|
||||||
|
{
|
||||||
|
var hash = Path.GetFileNameWithoutExtension(path);
|
||||||
|
if (!_fileCaches.ContainsKey(hash))
|
||||||
|
{
|
||||||
|
try { File.Delete(path); }
|
||||||
|
catch (Exception ex) { _logger.LogWarning(ex, "Failed deleting orphan {file}", path); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async Task StartAsync(CancellationToken cancellationToken)
|
public async Task StartAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Starting FileCacheManager");
|
_logger.LogInformation("Starting FileCacheManager");
|
||||||
@@ -1060,6 +1202,8 @@ public sealed class FileCacheManager : IHostedService
|
|||||||
{
|
{
|
||||||
await WriteOutFullCsvAsync(cancellationToken).ConfigureAwait(false);
|
await WriteOutFullCsvAsync(cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CleanupOrphanCompressedCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogInformation("Started FileCacheManager");
|
_logger.LogInformation("Started FileCacheManager");
|
||||||
|
|||||||
@@ -297,7 +297,7 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
private void DalamudUtil_FrameworkUpdate()
|
private void DalamudUtil_FrameworkUpdate()
|
||||||
{
|
{
|
||||||
RefreshPlayerRelatedAddressMap();
|
_ = Task.Run(() => RefreshPlayerRelatedAddressMap());
|
||||||
|
|
||||||
lock (_cacheAdditionLock)
|
lock (_cacheAdditionLock)
|
||||||
{
|
{
|
||||||
@@ -306,20 +306,64 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
if (_lastClassJobId != _dalamudUtil.ClassJobId)
|
if (_lastClassJobId != _dalamudUtil.ClassJobId)
|
||||||
{
|
{
|
||||||
_lastClassJobId = _dalamudUtil.ClassJobId;
|
UpdateClassJobCache();
|
||||||
if (SemiTransientResources.TryGetValue(ObjectKind.Pet, out HashSet<string>? value))
|
|
||||||
{
|
|
||||||
value?.Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
PlayerConfig.JobSpecificCache.TryGetValue(_dalamudUtil.ClassJobId, out var jobSpecificData);
|
|
||||||
SemiTransientResources[ObjectKind.Player] = PlayerConfig.GlobalPersistentCache.Concat(jobSpecificData ?? []).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
|
||||||
PlayerConfig.JobSpecificPetCache.TryGetValue(_dalamudUtil.ClassJobId, out var petSpecificData);
|
|
||||||
SemiTransientResources[ObjectKind.Pet] = new HashSet<string>(
|
|
||||||
petSpecificData ?? [],
|
|
||||||
StringComparer.OrdinalIgnoreCase);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CleanupAbsentObjects();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RefreshPlayerRelatedAddressMap()
|
||||||
|
{
|
||||||
|
var tempMap = new ConcurrentDictionary<nint, GameObjectHandler>();
|
||||||
|
var updatedFrameAddresses = new ConcurrentDictionary<nint, ObjectKind>();
|
||||||
|
|
||||||
|
lock (_playerRelatedLock)
|
||||||
|
{
|
||||||
|
foreach (var handler in _playerRelatedPointers)
|
||||||
|
{
|
||||||
|
var address = (nint)handler.Address;
|
||||||
|
if (address != nint.Zero)
|
||||||
|
{
|
||||||
|
tempMap[address] = handler;
|
||||||
|
updatedFrameAddresses[address] = handler.ObjectKind;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_playerRelatedByAddress.Clear();
|
||||||
|
foreach (var kvp in tempMap)
|
||||||
|
{
|
||||||
|
_playerRelatedByAddress[kvp.Key] = kvp.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
_cachedFrameAddresses.Clear();
|
||||||
|
foreach (var kvp in updatedFrameAddresses)
|
||||||
|
{
|
||||||
|
_cachedFrameAddresses[kvp.Key] = kvp.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateClassJobCache()
|
||||||
|
{
|
||||||
|
_lastClassJobId = _dalamudUtil.ClassJobId;
|
||||||
|
if (SemiTransientResources.TryGetValue(ObjectKind.Pet, out HashSet<string>? value))
|
||||||
|
{
|
||||||
|
value?.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
PlayerConfig.JobSpecificCache.TryGetValue(_dalamudUtil.ClassJobId, out var jobSpecificData);
|
||||||
|
SemiTransientResources[ObjectKind.Player] = PlayerConfig.GlobalPersistentCache
|
||||||
|
.Concat(jobSpecificData ?? [])
|
||||||
|
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
PlayerConfig.JobSpecificPetCache.TryGetValue(_dalamudUtil.ClassJobId, out var petSpecificData);
|
||||||
|
SemiTransientResources[ObjectKind.Pet] = new HashSet<string>(
|
||||||
|
petSpecificData ?? [],
|
||||||
|
StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CleanupAbsentObjects()
|
||||||
|
{
|
||||||
foreach (var kind in Enum.GetValues(typeof(ObjectKind)).Cast<ObjectKind>())
|
foreach (var kind in Enum.GetValues(typeof(ObjectKind)).Cast<ObjectKind>())
|
||||||
{
|
{
|
||||||
if (!_cachedFrameAddresses.Any(k => k.Value == kind) && TransientResources.Remove(kind, out _))
|
if (!_cachedFrameAddresses.Any(k => k.Value == kind) && TransientResources.Remove(kind, out _))
|
||||||
@@ -349,26 +393,6 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
|
|||||||
_semiTransientResources = null;
|
_semiTransientResources = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RefreshPlayerRelatedAddressMap()
|
|
||||||
{
|
|
||||||
_playerRelatedByAddress.Clear();
|
|
||||||
var updatedFrameAddresses = new ConcurrentDictionary<nint, ObjectKind>();
|
|
||||||
lock (_playerRelatedLock)
|
|
||||||
{
|
|
||||||
foreach (var handler in _playerRelatedPointers)
|
|
||||||
{
|
|
||||||
var address = (nint)handler.Address;
|
|
||||||
if (address != nint.Zero)
|
|
||||||
{
|
|
||||||
_playerRelatedByAddress[address] = handler;
|
|
||||||
updatedFrameAddresses[address] = handler.ObjectKind;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_cachedFrameAddresses = updatedFrameAddresses;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void HandleActorTracked(ActorObjectService.ActorDescriptor descriptor)
|
private void HandleActorTracked(ActorObjectService.ActorDescriptor descriptor)
|
||||||
{
|
{
|
||||||
if (descriptor.IsInGpose)
|
if (descriptor.IsInGpose)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Dalamud.Plugin.Services;
|
using Dalamud.Plugin.Services;
|
||||||
|
using Dalamud.Game.ClientState.Objects.SubKinds;
|
||||||
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
||||||
using FFXIVClientStructs.FFXIV.Client.UI.Info;
|
using FFXIVClientStructs.FFXIV.Client.UI.Info;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@@ -11,24 +12,35 @@ public unsafe class BlockedCharacterHandler
|
|||||||
private readonly Dictionary<CharaData, bool> _blockedCharacterCache = new();
|
private readonly Dictionary<CharaData, bool> _blockedCharacterCache = new();
|
||||||
|
|
||||||
private readonly ILogger<BlockedCharacterHandler> _logger;
|
private readonly ILogger<BlockedCharacterHandler> _logger;
|
||||||
|
private readonly IObjectTable _objectTable;
|
||||||
|
|
||||||
public BlockedCharacterHandler(ILogger<BlockedCharacterHandler> logger, IGameInteropProvider gameInteropProvider)
|
public BlockedCharacterHandler(ILogger<BlockedCharacterHandler> logger, IGameInteropProvider gameInteropProvider, IObjectTable objectTable)
|
||||||
{
|
{
|
||||||
gameInteropProvider.InitializeFromAttributes(this);
|
gameInteropProvider.InitializeFromAttributes(this);
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
_objectTable = objectTable;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static CharaData GetIdsFromPlayerPointer(nint ptr)
|
private CharaData? TryGetIdsFromPlayerPointer(nint ptr, ushort objectIndex)
|
||||||
{
|
{
|
||||||
if (ptr == nint.Zero) return new(0, 0);
|
if (ptr == nint.Zero || objectIndex >= 200)
|
||||||
var castChar = ((BattleChara*)ptr);
|
return null;
|
||||||
|
|
||||||
|
var obj = _objectTable[objectIndex];
|
||||||
|
if (obj is not IPlayerCharacter player || player.Address != ptr)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var castChar = (BattleChara*)player.Address;
|
||||||
return new(castChar->Character.AccountId, castChar->Character.ContentId);
|
return new(castChar->Character.AccountId, castChar->Character.ContentId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsCharacterBlocked(nint ptr, out bool firstTime)
|
public bool IsCharacterBlocked(nint ptr, ushort objectIndex, out bool firstTime)
|
||||||
{
|
{
|
||||||
firstTime = false;
|
firstTime = false;
|
||||||
var combined = GetIdsFromPlayerPointer(ptr);
|
var combined = TryGetIdsFromPlayerPointer(ptr, objectIndex);
|
||||||
|
if (combined == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
if (_blockedCharacterCache.TryGetValue(combined, out var isBlocked))
|
if (_blockedCharacterCache.TryGetValue(combined, out var isBlocked))
|
||||||
return isBlocked;
|
return isBlocked;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
namespace Lifestream.Enums;
|
||||||
|
|
||||||
|
public enum ResidentialAetheryteKind
|
||||||
|
{
|
||||||
|
None = -1,
|
||||||
|
Uldah = 9,
|
||||||
|
Gridania = 2,
|
||||||
|
Limsa = 8,
|
||||||
|
Foundation = 70,
|
||||||
|
Kugane = 111,
|
||||||
|
}
|
||||||
1
LightlessSync/Interop/InteropModel/GlobalModels.cs
Normal file
1
LightlessSync/Interop/InteropModel/GlobalModels.cs
Normal file
@@ -0,0 +1 @@
|
|||||||
|
global using AddressBookEntryTuple = (string Name, int World, int City, int Ward, int PropertyType, int Plot, int Apartment, bool ApartmentSubdivision, bool AliasEnabled, string Alias);
|
||||||
129
LightlessSync/Interop/Ipc/IpcCallerLifestream.cs
Normal file
129
LightlessSync/Interop/Ipc/IpcCallerLifestream.cs
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
using Dalamud.Plugin;
|
||||||
|
using Dalamud.Plugin.Ipc;
|
||||||
|
using Lifestream.Enums;
|
||||||
|
using LightlessSync.Interop.Ipc.Framework;
|
||||||
|
using LightlessSync.Services.Mediator;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
|
||||||
|
namespace LightlessSync.Interop.Ipc;
|
||||||
|
|
||||||
|
public sealed class IpcCallerLifestream : IpcServiceBase
|
||||||
|
{
|
||||||
|
private static readonly IpcServiceDescriptor LifestreamDescriptor = new("Lifestream", "Lifestream", new Version(0, 0, 0, 0));
|
||||||
|
|
||||||
|
private readonly ICallGateSubscriber<string, object> _executeLifestreamCommand;
|
||||||
|
private readonly ICallGateSubscriber<AddressBookEntryTuple, bool> _isHere;
|
||||||
|
private readonly ICallGateSubscriber<AddressBookEntryTuple, object> _goToHousingAddress;
|
||||||
|
private readonly ICallGateSubscriber<bool> _isBusy;
|
||||||
|
private readonly ICallGateSubscriber<object> _abort;
|
||||||
|
private readonly ICallGateSubscriber<string, bool> _changeWorld;
|
||||||
|
private readonly ICallGateSubscriber<uint, bool> _changeWorldById;
|
||||||
|
private readonly ICallGateSubscriber<string, bool> _aetheryteTeleport;
|
||||||
|
private readonly ICallGateSubscriber<uint, bool> _aetheryteTeleportById;
|
||||||
|
private readonly ICallGateSubscriber<bool> _canChangeInstance;
|
||||||
|
private readonly ICallGateSubscriber<int> _getCurrentInstance;
|
||||||
|
private readonly ICallGateSubscriber<int> _getNumberOfInstances;
|
||||||
|
private readonly ICallGateSubscriber<int, object> _changeInstance;
|
||||||
|
private readonly ICallGateSubscriber<(ResidentialAetheryteKind, int, int)> _getCurrentPlotInfo;
|
||||||
|
|
||||||
|
public IpcCallerLifestream(IDalamudPluginInterface pi, LightlessMediator lightlessMediator, ILogger<IpcCallerLifestream> logger)
|
||||||
|
: base(logger, lightlessMediator, pi, LifestreamDescriptor)
|
||||||
|
{
|
||||||
|
_executeLifestreamCommand = pi.GetIpcSubscriber<string, object>("Lifestream.ExecuteCommand");
|
||||||
|
_isHere = pi.GetIpcSubscriber<AddressBookEntryTuple, bool>("Lifestream.IsHere");
|
||||||
|
_goToHousingAddress = pi.GetIpcSubscriber<AddressBookEntryTuple, object>("Lifestream.GoToHousingAddress");
|
||||||
|
_isBusy = pi.GetIpcSubscriber<bool>("Lifestream.IsBusy");
|
||||||
|
_abort = pi.GetIpcSubscriber<object>("Lifestream.Abort");
|
||||||
|
_changeWorld = pi.GetIpcSubscriber<string, bool>("Lifestream.ChangeWorld");
|
||||||
|
_changeWorldById = pi.GetIpcSubscriber<uint, bool>("Lifestream.ChangeWorldById");
|
||||||
|
_aetheryteTeleport = pi.GetIpcSubscriber<string, bool>("Lifestream.AetheryteTeleport");
|
||||||
|
_aetheryteTeleportById = pi.GetIpcSubscriber<uint, bool>("Lifestream.AetheryteTeleportById");
|
||||||
|
_canChangeInstance = pi.GetIpcSubscriber<bool>("Lifestream.CanChangeInstance");
|
||||||
|
_getCurrentInstance = pi.GetIpcSubscriber<int>("Lifestream.GetCurrentInstance");
|
||||||
|
_getNumberOfInstances = pi.GetIpcSubscriber<int>("Lifestream.GetNumberOfInstances");
|
||||||
|
_changeInstance = pi.GetIpcSubscriber<int, object>("Lifestream.ChangeInstance");
|
||||||
|
_getCurrentPlotInfo = pi.GetIpcSubscriber<(ResidentialAetheryteKind, int, int)>("Lifestream.GetCurrentPlotInfo");
|
||||||
|
CheckAPI();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ExecuteLifestreamCommand(string command)
|
||||||
|
{
|
||||||
|
if (!APIAvailable) return;
|
||||||
|
_executeLifestreamCommand.InvokeAction(command);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsHere(AddressBookEntryTuple entry)
|
||||||
|
{
|
||||||
|
if (!APIAvailable) return false;
|
||||||
|
return _isHere.InvokeFunc(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void GoToHousingAddress(AddressBookEntryTuple entry)
|
||||||
|
{
|
||||||
|
if (!APIAvailable) return;
|
||||||
|
_goToHousingAddress.InvokeAction(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsBusy()
|
||||||
|
{
|
||||||
|
if (!APIAvailable) return false;
|
||||||
|
return _isBusy.InvokeFunc();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Abort()
|
||||||
|
{
|
||||||
|
if (!APIAvailable) return;
|
||||||
|
_abort.InvokeAction();
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool ChangeWorld(string worldName)
|
||||||
|
{
|
||||||
|
if (!APIAvailable) return false;
|
||||||
|
return _changeWorld.InvokeFunc(worldName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool AetheryteTeleport(string aetheryteName)
|
||||||
|
{
|
||||||
|
if (!APIAvailable) return false;
|
||||||
|
return _aetheryteTeleport.InvokeFunc(aetheryteName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool ChangeWorldById(uint worldId)
|
||||||
|
{
|
||||||
|
if (!APIAvailable) return false;
|
||||||
|
return _changeWorldById.InvokeFunc(worldId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool AetheryteTeleportById(uint aetheryteId)
|
||||||
|
{
|
||||||
|
if (!APIAvailable) return false;
|
||||||
|
return _aetheryteTeleportById.InvokeFunc(aetheryteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool CanChangeInstance()
|
||||||
|
{
|
||||||
|
if (!APIAvailable) return false;
|
||||||
|
return _canChangeInstance.InvokeFunc();
|
||||||
|
}
|
||||||
|
public int GetCurrentInstance()
|
||||||
|
{
|
||||||
|
if (!APIAvailable) return -1;
|
||||||
|
return _getCurrentInstance.InvokeFunc();
|
||||||
|
}
|
||||||
|
public int GetNumberOfInstances()
|
||||||
|
{
|
||||||
|
if (!APIAvailable) return -1;
|
||||||
|
return _getNumberOfInstances.InvokeFunc();
|
||||||
|
}
|
||||||
|
public void ChangeInstance(int instanceNumber)
|
||||||
|
{
|
||||||
|
if (!APIAvailable) return;
|
||||||
|
_changeInstance.InvokeAction(instanceNumber);
|
||||||
|
}
|
||||||
|
public (ResidentialAetheryteKind, int, int)? GetCurrentPlotInfo()
|
||||||
|
{
|
||||||
|
if (!APIAvailable) return (ResidentialAetheryteKind.None, -1, -1);
|
||||||
|
return _getCurrentPlotInfo.InvokeFunc();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,9 +5,12 @@ namespace LightlessSync.Interop.Ipc;
|
|||||||
|
|
||||||
public sealed partial class IpcManager : DisposableMediatorSubscriberBase
|
public sealed partial class IpcManager : DisposableMediatorSubscriberBase
|
||||||
{
|
{
|
||||||
|
private bool _wasInitialized;
|
||||||
|
|
||||||
public IpcManager(ILogger<IpcManager> logger, LightlessMediator mediator,
|
public IpcManager(ILogger<IpcManager> logger, LightlessMediator mediator,
|
||||||
IpcCallerPenumbra penumbraIpc, IpcCallerGlamourer glamourerIpc, IpcCallerCustomize customizeIpc, IpcCallerHeels heelsIpc,
|
IpcCallerPenumbra penumbraIpc, IpcCallerGlamourer glamourerIpc, IpcCallerCustomize customizeIpc, IpcCallerHeels heelsIpc,
|
||||||
IpcCallerHonorific honorificIpc, IpcCallerMoodles moodlesIpc, IpcCallerPetNames ipcCallerPetNames, IpcCallerBrio ipcCallerBrio) : base(logger, mediator)
|
IpcCallerHonorific honorificIpc, IpcCallerMoodles moodlesIpc, IpcCallerPetNames ipcCallerPetNames, IpcCallerBrio ipcCallerBrio,
|
||||||
|
IpcCallerLifestream ipcCallerLifestream) : base(logger, mediator)
|
||||||
{
|
{
|
||||||
CustomizePlus = customizeIpc;
|
CustomizePlus = customizeIpc;
|
||||||
Heels = heelsIpc;
|
Heels = heelsIpc;
|
||||||
@@ -17,8 +20,10 @@ public sealed partial class IpcManager : DisposableMediatorSubscriberBase
|
|||||||
Moodles = moodlesIpc;
|
Moodles = moodlesIpc;
|
||||||
PetNames = ipcCallerPetNames;
|
PetNames = ipcCallerPetNames;
|
||||||
Brio = ipcCallerBrio;
|
Brio = ipcCallerBrio;
|
||||||
|
Lifestream = ipcCallerLifestream;
|
||||||
|
|
||||||
if (Initialized)
|
_wasInitialized = Initialized;
|
||||||
|
if (_wasInitialized)
|
||||||
{
|
{
|
||||||
Mediator.Publish(new PenumbraInitializedMessage());
|
Mediator.Publish(new PenumbraInitializedMessage());
|
||||||
}
|
}
|
||||||
@@ -44,8 +49,8 @@ public sealed partial class IpcManager : DisposableMediatorSubscriberBase
|
|||||||
public IpcCallerPenumbra Penumbra { get; }
|
public IpcCallerPenumbra Penumbra { get; }
|
||||||
public IpcCallerMoodles Moodles { get; }
|
public IpcCallerMoodles Moodles { get; }
|
||||||
public IpcCallerPetNames PetNames { get; }
|
public IpcCallerPetNames PetNames { get; }
|
||||||
|
|
||||||
public IpcCallerBrio Brio { get; }
|
public IpcCallerBrio Brio { get; }
|
||||||
|
public IpcCallerLifestream Lifestream { get; }
|
||||||
|
|
||||||
private void PeriodicApiStateCheck()
|
private void PeriodicApiStateCheck()
|
||||||
{
|
{
|
||||||
@@ -58,5 +63,14 @@ public sealed partial class IpcManager : DisposableMediatorSubscriberBase
|
|||||||
Moodles.CheckAPI();
|
Moodles.CheckAPI();
|
||||||
PetNames.CheckAPI();
|
PetNames.CheckAPI();
|
||||||
Brio.CheckAPI();
|
Brio.CheckAPI();
|
||||||
|
|
||||||
|
var initialized = Initialized;
|
||||||
|
if (initialized && !_wasInitialized)
|
||||||
|
{
|
||||||
|
Mediator.Publish(new PenumbraInitializedMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
_wasInitialized = initialized;
|
||||||
|
Lifestream.CheckAPI();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,6 +4,7 @@ using LightlessSync.LightlessConfiguration.Models;
|
|||||||
using LightlessSync.UI;
|
using LightlessSync.UI;
|
||||||
using LightlessSync.UI.Models;
|
using LightlessSync.UI.Models;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using LightlessSync.PlayerData.Factories;
|
||||||
|
|
||||||
namespace LightlessSync.LightlessConfiguration.Configurations;
|
namespace LightlessSync.LightlessConfiguration.Configurations;
|
||||||
|
|
||||||
@@ -51,6 +52,7 @@ public class LightlessConfig : ILightlessConfiguration
|
|||||||
public bool PreferNotesOverNamesForVisible { get; set; } = false;
|
public bool PreferNotesOverNamesForVisible { get; set; } = false;
|
||||||
public VisiblePairSortMode VisiblePairSortMode { get; set; } = VisiblePairSortMode.Alphabetical;
|
public VisiblePairSortMode VisiblePairSortMode { get; set; } = VisiblePairSortMode.Alphabetical;
|
||||||
public OnlinePairSortMode OnlinePairSortMode { get; set; } = OnlinePairSortMode.Alphabetical;
|
public OnlinePairSortMode OnlinePairSortMode { get; set; } = OnlinePairSortMode.Alphabetical;
|
||||||
|
public TextureFormatSortMode TextureFormatSortMode { get; set; } = TextureFormatSortMode.None;
|
||||||
public float ProfileDelay { get; set; } = 1.5f;
|
public float ProfileDelay { get; set; } = 1.5f;
|
||||||
public bool ProfilePopoutRight { get; set; } = false;
|
public bool ProfilePopoutRight { get; set; } = false;
|
||||||
public bool ProfilesAllowNsfw { get; set; } = false;
|
public bool ProfilesAllowNsfw { get; set; } = false;
|
||||||
@@ -157,4 +159,8 @@ public class LightlessConfig : ILightlessConfiguration
|
|||||||
public string LastSeenVersion { get; set; } = string.Empty;
|
public string LastSeenVersion { get; set; } = string.Empty;
|
||||||
public bool EnableParticleEffects { get; set; } = true;
|
public bool EnableParticleEffects { get; set; } = true;
|
||||||
public HashSet<Guid> OrphanableTempCollections { get; set; } = [];
|
public HashSet<Guid> OrphanableTempCollections { get; set; } = [];
|
||||||
|
public AnimationValidationMode AnimationValidationMode { get; set; } = AnimationValidationMode.Safe;
|
||||||
|
public bool AnimationAllowOneBasedShift { get; set; } = true;
|
||||||
|
|
||||||
|
public bool AnimationAllowNeighborIndexTolerance { get; set; } = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,4 +22,15 @@ public class PlayerPerformanceConfig : ILightlessConfiguration
|
|||||||
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 KeepOriginalTextureFiles { get; set; } = false;
|
public bool KeepOriginalTextureFiles { get; set; } = false;
|
||||||
|
public bool SkipTextureDownscaleForPreferredPairs { get; set; } = true;
|
||||||
|
public bool EnableModelDecimation { get; set; } = false;
|
||||||
|
public int ModelDecimationTriangleThreshold { get; set; } = 20_000;
|
||||||
|
public double ModelDecimationTargetRatio { get; set; } = 0.8;
|
||||||
|
public bool KeepOriginalModelFiles { get; set; } = true;
|
||||||
|
public bool SkipModelDecimationForPreferredPairs { get; set; } = true;
|
||||||
|
public bool ModelDecimationAllowBody { get; set; } = false;
|
||||||
|
public bool ModelDecimationAllowFaceHead { get; set; } = false;
|
||||||
|
public bool ModelDecimationAllowTail { get; set; } = false;
|
||||||
|
public bool ModelDecimationAllowClothing { get; set; } = true;
|
||||||
|
public bool ModelDecimationAllowAccessories { get; set; } = true;
|
||||||
}
|
}
|
||||||
@@ -5,6 +5,7 @@ namespace LightlessSync.LightlessConfiguration.Configurations;
|
|||||||
public class XivDataStorageConfig : ILightlessConfiguration
|
public class XivDataStorageConfig : ILightlessConfiguration
|
||||||
{
|
{
|
||||||
public ConcurrentDictionary<string, long> TriangleDictionary { get; set; } = new(StringComparer.OrdinalIgnoreCase);
|
public ConcurrentDictionary<string, long> TriangleDictionary { get; set; } = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
public ConcurrentDictionary<string, long> EffectiveTriangleDictionary { get; set; } = new(StringComparer.OrdinalIgnoreCase);
|
||||||
public ConcurrentDictionary<string, Dictionary<string, List<ushort>>> BonesDictionary { get; set; } = new(StringComparer.OrdinalIgnoreCase);
|
public ConcurrentDictionary<string, Dictionary<string, List<ushort>>> BonesDictionary { get; set; } = new(StringComparer.OrdinalIgnoreCase);
|
||||||
public int Version { get; set; } = 0;
|
public int Version { get; set; } = 0;
|
||||||
}
|
}
|
||||||
@@ -74,6 +74,7 @@ public class LightlessPlugin : MediatorSubscriberBase, IHostedService
|
|||||||
private readonly DalamudUtilService _dalamudUtil;
|
private readonly DalamudUtilService _dalamudUtil;
|
||||||
private readonly LightlessConfigService _lightlessConfigService;
|
private readonly LightlessConfigService _lightlessConfigService;
|
||||||
private readonly ServerConfigurationManager _serverConfigurationManager;
|
private readonly ServerConfigurationManager _serverConfigurationManager;
|
||||||
|
private readonly PairHandlerRegistry _pairHandlerRegistry;
|
||||||
private readonly IServiceScopeFactory _serviceScopeFactory;
|
private readonly IServiceScopeFactory _serviceScopeFactory;
|
||||||
private IServiceScope? _runtimeServiceScope;
|
private IServiceScope? _runtimeServiceScope;
|
||||||
private Task? _launchTask = null;
|
private Task? _launchTask = null;
|
||||||
@@ -81,11 +82,13 @@ public class LightlessPlugin : MediatorSubscriberBase, IHostedService
|
|||||||
public LightlessPlugin(ILogger<LightlessPlugin> logger, LightlessConfigService lightlessConfigService,
|
public LightlessPlugin(ILogger<LightlessPlugin> logger, LightlessConfigService lightlessConfigService,
|
||||||
ServerConfigurationManager serverConfigurationManager,
|
ServerConfigurationManager serverConfigurationManager,
|
||||||
DalamudUtilService dalamudUtil,
|
DalamudUtilService dalamudUtil,
|
||||||
|
PairHandlerRegistry pairHandlerRegistry,
|
||||||
IServiceScopeFactory serviceScopeFactory, LightlessMediator mediator) : base(logger, mediator)
|
IServiceScopeFactory serviceScopeFactory, LightlessMediator mediator) : base(logger, mediator)
|
||||||
{
|
{
|
||||||
_lightlessConfigService = lightlessConfigService;
|
_lightlessConfigService = lightlessConfigService;
|
||||||
_serverConfigurationManager = serverConfigurationManager;
|
_serverConfigurationManager = serverConfigurationManager;
|
||||||
_dalamudUtil = dalamudUtil;
|
_dalamudUtil = dalamudUtil;
|
||||||
|
_pairHandlerRegistry = pairHandlerRegistry;
|
||||||
_serviceScopeFactory = serviceScopeFactory;
|
_serviceScopeFactory = serviceScopeFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,12 +111,20 @@ public class LightlessPlugin : MediatorSubscriberBase, IHostedService
|
|||||||
|
|
||||||
public Task StopAsync(CancellationToken cancellationToken)
|
public Task StopAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
Logger.LogDebug("Halting LightlessPlugin");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_pairHandlerRegistry.ResetAllHandlers();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogWarning(ex, "Failed to reset pair handlers on shutdown");
|
||||||
|
}
|
||||||
|
|
||||||
UnsubscribeAll();
|
UnsubscribeAll();
|
||||||
|
|
||||||
DalamudUtilOnLogOut();
|
DalamudUtilOnLogOut();
|
||||||
|
|
||||||
Logger.LogDebug("Halting LightlessPlugin");
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Authors></Authors>
|
<Authors></Authors>
|
||||||
<Company></Company>
|
<Company></Company>
|
||||||
<Version>2.0.3</Version>
|
<Version>2.0.2.69</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>
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace LightlessSync.PlayerData.Factories
|
||||||
|
{
|
||||||
|
public enum AnimationValidationMode
|
||||||
|
{
|
||||||
|
Unsafe = 0,
|
||||||
|
Safe = 1,
|
||||||
|
Safest = 2,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
using LightlessSync.FileCache;
|
using LightlessSync.FileCache;
|
||||||
using LightlessSync.LightlessConfiguration;
|
using LightlessSync.LightlessConfiguration;
|
||||||
using LightlessSync.Services.Mediator;
|
using LightlessSync.Services.Mediator;
|
||||||
|
using LightlessSync.Services.ModelDecimation;
|
||||||
using LightlessSync.Services.TextureCompression;
|
using LightlessSync.Services.TextureCompression;
|
||||||
using LightlessSync.WebAPI.Files;
|
using LightlessSync.WebAPI.Files;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@@ -16,6 +17,7 @@ public class FileDownloadManagerFactory
|
|||||||
private readonly FileCompactor _fileCompactor;
|
private readonly FileCompactor _fileCompactor;
|
||||||
private readonly LightlessConfigService _configService;
|
private readonly LightlessConfigService _configService;
|
||||||
private readonly TextureDownscaleService _textureDownscaleService;
|
private readonly TextureDownscaleService _textureDownscaleService;
|
||||||
|
private readonly ModelDecimationService _modelDecimationService;
|
||||||
private readonly TextureMetadataHelper _textureMetadataHelper;
|
private readonly TextureMetadataHelper _textureMetadataHelper;
|
||||||
|
|
||||||
public FileDownloadManagerFactory(
|
public FileDownloadManagerFactory(
|
||||||
@@ -26,6 +28,7 @@ public class FileDownloadManagerFactory
|
|||||||
FileCompactor fileCompactor,
|
FileCompactor fileCompactor,
|
||||||
LightlessConfigService configService,
|
LightlessConfigService configService,
|
||||||
TextureDownscaleService textureDownscaleService,
|
TextureDownscaleService textureDownscaleService,
|
||||||
|
ModelDecimationService modelDecimationService,
|
||||||
TextureMetadataHelper textureMetadataHelper)
|
TextureMetadataHelper textureMetadataHelper)
|
||||||
{
|
{
|
||||||
_loggerFactory = loggerFactory;
|
_loggerFactory = loggerFactory;
|
||||||
@@ -35,6 +38,7 @@ public class FileDownloadManagerFactory
|
|||||||
_fileCompactor = fileCompactor;
|
_fileCompactor = fileCompactor;
|
||||||
_configService = configService;
|
_configService = configService;
|
||||||
_textureDownscaleService = textureDownscaleService;
|
_textureDownscaleService = textureDownscaleService;
|
||||||
|
_modelDecimationService = modelDecimationService;
|
||||||
_textureMetadataHelper = textureMetadataHelper;
|
_textureMetadataHelper = textureMetadataHelper;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,6 +52,7 @@ public class FileDownloadManagerFactory
|
|||||||
_fileCompactor,
|
_fileCompactor,
|
||||||
_configService,
|
_configService,
|
||||||
_textureDownscaleService,
|
_textureDownscaleService,
|
||||||
|
_modelDecimationService,
|
||||||
_textureMetadataHelper);
|
_textureMetadataHelper);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,15 @@
|
|||||||
using LightlessSync.API.Data.Enum;
|
using LightlessSync.API.Data.Enum;
|
||||||
using LightlessSync.FileCache;
|
using LightlessSync.FileCache;
|
||||||
using LightlessSync.Interop.Ipc;
|
using LightlessSync.Interop.Ipc;
|
||||||
|
using LightlessSync.LightlessConfiguration;
|
||||||
using LightlessSync.LightlessConfiguration.Models;
|
using LightlessSync.LightlessConfiguration.Models;
|
||||||
using LightlessSync.PlayerData.Data;
|
using LightlessSync.PlayerData.Data;
|
||||||
using LightlessSync.PlayerData.Handlers;
|
using LightlessSync.PlayerData.Handlers;
|
||||||
using LightlessSync.Services;
|
using LightlessSync.Services;
|
||||||
using LightlessSync.Services.Mediator;
|
using LightlessSync.Services.Mediator;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
namespace LightlessSync.PlayerData.Factories;
|
namespace LightlessSync.PlayerData.Factories;
|
||||||
|
|
||||||
@@ -18,13 +21,34 @@ public class PlayerDataFactory
|
|||||||
private readonly IpcManager _ipcManager;
|
private readonly IpcManager _ipcManager;
|
||||||
private readonly ILogger<PlayerDataFactory> _logger;
|
private readonly ILogger<PlayerDataFactory> _logger;
|
||||||
private readonly PerformanceCollectorService _performanceCollector;
|
private readonly PerformanceCollectorService _performanceCollector;
|
||||||
|
private readonly LightlessConfigService _configService;
|
||||||
private readonly XivDataAnalyzer _modelAnalyzer;
|
private readonly XivDataAnalyzer _modelAnalyzer;
|
||||||
private readonly LightlessMediator _lightlessMediator;
|
private readonly LightlessMediator _lightlessMediator;
|
||||||
private readonly TransientResourceManager _transientResourceManager;
|
private readonly TransientResourceManager _transientResourceManager;
|
||||||
|
private static readonly SemaphoreSlim _papParseLimiter = new(1, 1);
|
||||||
|
|
||||||
public PlayerDataFactory(ILogger<PlayerDataFactory> logger, DalamudUtilService dalamudUtil, IpcManager ipcManager,
|
// Transient resolved entries threshold
|
||||||
TransientResourceManager transientResourceManager, FileCacheManager fileReplacementFactory,
|
private const int _maxTransientResolvedEntries = 1000;
|
||||||
PerformanceCollectorService performanceCollector, XivDataAnalyzer modelAnalyzer, LightlessMediator lightlessMediator)
|
|
||||||
|
// Character build caches
|
||||||
|
private readonly ConcurrentDictionary<nint, Task<CharacterDataFragment>> _characterBuildInflight = new();
|
||||||
|
private readonly ConcurrentDictionary<nint, CacheEntry> _characterBuildCache = new();
|
||||||
|
|
||||||
|
// Time out thresholds
|
||||||
|
private static readonly TimeSpan _characterCacheTtl = TimeSpan.FromMilliseconds(750);
|
||||||
|
private static readonly TimeSpan _softReturnIfBusyAfter = TimeSpan.FromMilliseconds(250);
|
||||||
|
private static readonly TimeSpan _hardBuildTimeout = TimeSpan.FromSeconds(30);
|
||||||
|
|
||||||
|
public PlayerDataFactory(
|
||||||
|
ILogger<PlayerDataFactory> logger,
|
||||||
|
DalamudUtilService dalamudUtil,
|
||||||
|
IpcManager ipcManager,
|
||||||
|
TransientResourceManager transientResourceManager,
|
||||||
|
FileCacheManager fileReplacementFactory,
|
||||||
|
PerformanceCollectorService performanceCollector,
|
||||||
|
XivDataAnalyzer modelAnalyzer,
|
||||||
|
LightlessMediator lightlessMediator,
|
||||||
|
LightlessConfigService configService)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_dalamudUtil = dalamudUtil;
|
_dalamudUtil = dalamudUtil;
|
||||||
@@ -34,15 +58,15 @@ public class PlayerDataFactory
|
|||||||
_performanceCollector = performanceCollector;
|
_performanceCollector = performanceCollector;
|
||||||
_modelAnalyzer = modelAnalyzer;
|
_modelAnalyzer = modelAnalyzer;
|
||||||
_lightlessMediator = lightlessMediator;
|
_lightlessMediator = lightlessMediator;
|
||||||
|
_configService = configService;
|
||||||
_logger.LogTrace("Creating {this}", nameof(PlayerDataFactory));
|
_logger.LogTrace("Creating {this}", nameof(PlayerDataFactory));
|
||||||
}
|
}
|
||||||
|
private sealed record CacheEntry(CharacterDataFragment Fragment, DateTime CreatedUtc);
|
||||||
|
|
||||||
public async Task<CharacterDataFragment?> BuildCharacterData(GameObjectHandler playerRelatedObject, CancellationToken token)
|
public async Task<CharacterDataFragment?> BuildCharacterData(GameObjectHandler playerRelatedObject, CancellationToken token)
|
||||||
{
|
{
|
||||||
if (!_ipcManager.Initialized)
|
if (!_ipcManager.Initialized)
|
||||||
{
|
|
||||||
throw new InvalidOperationException("Penumbra or Glamourer is not connected");
|
throw new InvalidOperationException("Penumbra or Glamourer is not connected");
|
||||||
}
|
|
||||||
|
|
||||||
if (playerRelatedObject == null) return null;
|
if (playerRelatedObject == null) return null;
|
||||||
|
|
||||||
@@ -67,16 +91,17 @@ public class PlayerDataFactory
|
|||||||
|
|
||||||
if (pointerIsZero)
|
if (pointerIsZero)
|
||||||
{
|
{
|
||||||
_logger.LogTrace("Pointer was zero for {objectKind}", playerRelatedObject.ObjectKind);
|
_logger.LogTrace("Pointer was zero for {objectKind}; couldn't build character", playerRelatedObject.ObjectKind);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return await _performanceCollector.LogPerformance(this, $"CreateCharacterData>{playerRelatedObject.ObjectKind}", async () =>
|
return await _performanceCollector.LogPerformance(
|
||||||
{
|
this,
|
||||||
return await CreateCharacterData(playerRelatedObject, token).ConfigureAwait(false);
|
$"CreateCharacterData>{playerRelatedObject.ObjectKind}",
|
||||||
}).ConfigureAwait(true);
|
async () => await CreateCharacterData(playerRelatedObject, token).ConfigureAwait(false)
|
||||||
|
).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
@@ -92,114 +117,192 @@ public class PlayerDataFactory
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async Task<bool> CheckForNullDrawObject(IntPtr playerPointer)
|
private async Task<bool> CheckForNullDrawObject(IntPtr playerPointer)
|
||||||
{
|
=> await _dalamudUtil.RunOnFrameworkThread(() => CheckForNullDrawObjectUnsafe(playerPointer)).ConfigureAwait(false);
|
||||||
return await _dalamudUtil.RunOnFrameworkThread(() => CheckForNullDrawObjectUnsafe(playerPointer)).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private unsafe bool CheckForNullDrawObjectUnsafe(IntPtr playerPointer)
|
private unsafe static bool CheckForNullDrawObjectUnsafe(IntPtr playerPointer)
|
||||||
{
|
{
|
||||||
if (playerPointer == IntPtr.Zero)
|
if (playerPointer == IntPtr.Zero)
|
||||||
return true;
|
return true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var character = (Character*)playerPointer;
|
||||||
|
if (character == null)
|
||||||
|
return true;
|
||||||
|
|
||||||
var character = (Character*)playerPointer;
|
var gameObject = &character->GameObject;
|
||||||
|
if (gameObject == null)
|
||||||
|
return true;
|
||||||
|
|
||||||
if (character == null)
|
return gameObject->DrawObject == null;
|
||||||
|
}
|
||||||
|
catch (AccessViolationException)
|
||||||
|
{
|
||||||
return true;
|
return true;
|
||||||
|
}
|
||||||
var gameObject = &character->GameObject;
|
|
||||||
if (gameObject == null)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
return gameObject->DrawObject == null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<CharacterDataFragment> CreateCharacterData(GameObjectHandler playerRelatedObject, CancellationToken ct)
|
private static bool IsCacheFresh(CacheEntry entry)
|
||||||
|
=> (DateTime.UtcNow - entry.CreatedUtc) <= _characterCacheTtl;
|
||||||
|
|
||||||
|
private Task<CharacterDataFragment> CreateCharacterData(GameObjectHandler playerRelatedObject, CancellationToken ct)
|
||||||
|
=> CreateCharacterDataCoalesced(playerRelatedObject, ct);
|
||||||
|
|
||||||
|
private async Task<CharacterDataFragment> CreateCharacterDataCoalesced(GameObjectHandler obj, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var key = obj.Address;
|
||||||
|
|
||||||
|
if (_characterBuildCache.TryGetValue(key, out var cached) && IsCacheFresh(cached) && !_characterBuildInflight.ContainsKey(key))
|
||||||
|
return cached.Fragment;
|
||||||
|
|
||||||
|
var buildTask = _characterBuildInflight.GetOrAdd(key, _ => BuildAndCacheAsync(obj, key));
|
||||||
|
|
||||||
|
if (_characterBuildCache.TryGetValue(key, out cached))
|
||||||
|
{
|
||||||
|
var completed = await Task.WhenAny(buildTask, Task.Delay(_softReturnIfBusyAfter, ct)).ConfigureAwait(false);
|
||||||
|
if (completed != buildTask && (DateTime.UtcNow - cached.CreatedUtc) <= TimeSpan.FromSeconds(5))
|
||||||
|
{
|
||||||
|
return cached.Fragment;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return await WithCancellation(buildTask, ct).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<CharacterDataFragment> BuildAndCacheAsync(GameObjectHandler obj, nint key)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var cts = new CancellationTokenSource(_hardBuildTimeout);
|
||||||
|
var fragment = await CreateCharacterDataInternal(obj, cts.Token).ConfigureAwait(false);
|
||||||
|
|
||||||
|
_characterBuildCache[key] = new CacheEntry(fragment, DateTime.UtcNow);
|
||||||
|
PruneCharacterCacheIfNeeded();
|
||||||
|
|
||||||
|
return fragment;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_characterBuildInflight.TryRemove(key, out _);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PruneCharacterCacheIfNeeded()
|
||||||
|
{
|
||||||
|
if (_characterBuildCache.Count < 2048) return;
|
||||||
|
|
||||||
|
var cutoff = DateTime.UtcNow - TimeSpan.FromSeconds(10);
|
||||||
|
foreach (var kv in _characterBuildCache)
|
||||||
|
{
|
||||||
|
if (kv.Value.CreatedUtc < cutoff)
|
||||||
|
_characterBuildCache.TryRemove(kv.Key, out _);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<T> WithCancellation<T>(Task<T> task, CancellationToken ct)
|
||||||
|
=> await task.WaitAsync(ct).ConfigureAwait(false);
|
||||||
|
|
||||||
|
private async Task<CharacterDataFragment> CreateCharacterDataInternal(GameObjectHandler playerRelatedObject, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var objectKind = playerRelatedObject.ObjectKind;
|
var objectKind = playerRelatedObject.ObjectKind;
|
||||||
CharacterDataFragment fragment = objectKind == ObjectKind.Player ? new CharacterDataFragmentPlayer() : new();
|
CharacterDataFragment fragment = objectKind == ObjectKind.Player ? new CharacterDataFragmentPlayer() : new();
|
||||||
|
|
||||||
_logger.LogDebug("Building character data for {obj}", playerRelatedObject);
|
|
||||||
var logDebug = _logger.IsEnabled(LogLevel.Debug);
|
var logDebug = _logger.IsEnabled(LogLevel.Debug);
|
||||||
|
var sw = Stopwatch.StartNew();
|
||||||
|
|
||||||
// wait until chara is not drawing and present so nothing spontaneously explodes
|
_logger.LogDebug("Building character data for {obj}", playerRelatedObject);
|
||||||
await _dalamudUtil.WaitWhileCharacterIsDrawing(_logger, playerRelatedObject, Guid.NewGuid(), 30000, ct: ct).ConfigureAwait(false);
|
|
||||||
int totalWaitTime = 10000;
|
await EnsureObjectPresentAsync(playerRelatedObject, ct).ConfigureAwait(false);
|
||||||
while (!await _dalamudUtil.IsObjectPresentAsync(await _dalamudUtil.CreateGameObjectAsync(playerRelatedObject.Address).ConfigureAwait(false)).ConfigureAwait(false) && totalWaitTime > 0)
|
ct.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
var waitRecordingTask = _transientResourceManager.WaitForRecording(ct);
|
||||||
|
|
||||||
|
await _dalamudUtil.WaitWhileCharacterIsDrawing(_logger, playerRelatedObject, Guid.NewGuid(), 30000, ct: ct)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
// get all remaining paths and resolve them
|
||||||
|
ct.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
if (await CheckForNullDrawObject(playerRelatedObject.Address).ConfigureAwait(false))
|
||||||
|
throw new InvalidOperationException("DrawObject became null during build (actor despawned)");
|
||||||
|
|
||||||
|
Task<string> getGlamourerData = _ipcManager.Glamourer.GetCharacterCustomizationAsync(playerRelatedObject.Address);
|
||||||
|
Task<string?> getCustomizeData = _ipcManager.CustomizePlus.GetScaleAsync(playerRelatedObject.Address);
|
||||||
|
Task<string?>? getMoodlesData = null;
|
||||||
|
Task<string>? getHeelsOffset = null;
|
||||||
|
Task<string>? getHonorificTitle = null;
|
||||||
|
|
||||||
|
if (objectKind == ObjectKind.Player)
|
||||||
{
|
{
|
||||||
_logger.LogTrace("Character is null but it shouldn't be, waiting");
|
getHeelsOffset = _ipcManager.Heels.GetOffsetAsync();
|
||||||
await Task.Delay(50, ct).ConfigureAwait(false);
|
getHonorificTitle = _ipcManager.Honorific.GetTitle();
|
||||||
totalWaitTime -= 50;
|
getMoodlesData = _ipcManager.Moodles.GetStatusAsync(playerRelatedObject.Address);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var resolvedPaths = await _ipcManager.Penumbra.GetCharacterData(_logger, playerRelatedObject).ConfigureAwait(false) ?? throw new InvalidOperationException("Penumbra returned null data; couldn't proceed with character");
|
||||||
ct.ThrowIfCancellationRequested();
|
ct.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
DateTime start = DateTime.UtcNow;
|
var staticBuildTask = Task.Run(() => BuildStaticReplacements(resolvedPaths), ct);
|
||||||
|
|
||||||
// penumbra call, it's currently broken
|
fragment.FileReplacements = await staticBuildTask.ConfigureAwait(false);
|
||||||
Dictionary<string, HashSet<string>>? resolvedPaths;
|
|
||||||
|
|
||||||
resolvedPaths = (await _ipcManager.Penumbra.GetCharacterData(_logger, playerRelatedObject).ConfigureAwait(false));
|
|
||||||
if (resolvedPaths == null) throw new InvalidOperationException("Penumbra returned null data");
|
|
||||||
|
|
||||||
ct.ThrowIfCancellationRequested();
|
|
||||||
|
|
||||||
fragment.FileReplacements =
|
|
||||||
new HashSet<FileReplacement>(resolvedPaths.Select(c => new FileReplacement([.. c.Value], c.Key)), FileReplacementComparer.Instance)
|
|
||||||
.Where(p => p.HasFileReplacement).ToHashSet();
|
|
||||||
fragment.FileReplacements.RemoveWhere(c => c.GamePaths.Any(g => !CacheMonitor.AllowedFileExtensions.Any(e => g.EndsWith(e, StringComparison.OrdinalIgnoreCase))));
|
|
||||||
|
|
||||||
ct.ThrowIfCancellationRequested();
|
|
||||||
|
|
||||||
if (logDebug)
|
if (logDebug)
|
||||||
{
|
{
|
||||||
_logger.LogDebug("== Static Replacements ==");
|
_logger.LogDebug("== Static Replacements ==");
|
||||||
foreach (var replacement in fragment.FileReplacements.Where(i => i.HasFileReplacement).OrderBy(i => i.GamePaths.First(), StringComparer.OrdinalIgnoreCase))
|
foreach (var replacement in fragment.FileReplacements
|
||||||
|
.Where(i => i.HasFileReplacement)
|
||||||
|
.OrderBy(i => i.GamePaths.First(), StringComparer.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
_logger.LogDebug("=> {repl}", replacement);
|
_logger.LogDebug("=> {repl}", replacement);
|
||||||
ct.ThrowIfCancellationRequested();
|
ct.ThrowIfCancellationRequested();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
|
||||||
|
var staticReplacements = new HashSet<FileReplacement>(fragment.FileReplacements, FileReplacementComparer.Instance);
|
||||||
|
|
||||||
|
var transientTask = ResolveTransientReplacementsAsync(
|
||||||
|
playerRelatedObject,
|
||||||
|
objectKind,
|
||||||
|
staticReplacements,
|
||||||
|
waitRecordingTask,
|
||||||
|
ct);
|
||||||
|
|
||||||
|
fragment.GlamourerString = await getGlamourerData.ConfigureAwait(false);
|
||||||
|
_logger.LogDebug("Glamourer is now: {data}", fragment.GlamourerString);
|
||||||
|
|
||||||
|
var customizeScale = await getCustomizeData.ConfigureAwait(false);
|
||||||
|
fragment.CustomizePlusScale = customizeScale ?? string.Empty;
|
||||||
|
_logger.LogDebug("Customize is now: {data}", fragment.CustomizePlusScale);
|
||||||
|
|
||||||
|
if (objectKind == ObjectKind.Player)
|
||||||
{
|
{
|
||||||
foreach (var replacement in fragment.FileReplacements.Where(i => i.HasFileReplacement))
|
CharacterDataFragmentPlayer? playerFragment = fragment as CharacterDataFragmentPlayer ?? throw new InvalidOperationException("Failed to cast CharacterDataFragment to Player variant");
|
||||||
{
|
|
||||||
ct.ThrowIfCancellationRequested();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await _transientResourceManager.WaitForRecording(ct).ConfigureAwait(false);
|
playerFragment.ManipulationString = _ipcManager.Penumbra.GetMetaManipulations();
|
||||||
|
playerFragment.HonorificData = await getHonorificTitle!.ConfigureAwait(false);
|
||||||
|
_logger.LogDebug("Honorific is now: {data}", playerFragment!.HonorificData);
|
||||||
|
|
||||||
// if it's pet then it's summoner, if it's summoner we actually want to keep all filereplacements alive at all times
|
playerFragment.PetNamesData = _ipcManager.PetNames.GetLocalNames();
|
||||||
// or we get into redraw city for every change and nothing works properly
|
_logger.LogDebug("Pet Nicknames is now: {petnames}", playerFragment!.PetNamesData);
|
||||||
if (objectKind == ObjectKind.Pet)
|
|
||||||
{
|
|
||||||
foreach (var item in fragment.FileReplacements.Where(i => i.HasFileReplacement).SelectMany(p => p.GamePaths))
|
|
||||||
{
|
|
||||||
if (_transientResourceManager.AddTransientResource(objectKind, item))
|
|
||||||
{
|
|
||||||
_logger.LogDebug("Marking static {item} for Pet as transient", item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.LogTrace("Clearing {count} Static Replacements for Pet", fragment.FileReplacements.Count);
|
playerFragment.HeelsData = await getHeelsOffset!.ConfigureAwait(false);
|
||||||
fragment.FileReplacements.Clear();
|
_logger.LogDebug("Heels is now: {heels}", playerFragment!.HeelsData);
|
||||||
|
|
||||||
|
playerFragment.MoodlesData = (await getMoodlesData!.ConfigureAwait(false)) ?? string.Empty;
|
||||||
|
_logger.LogDebug("Moodles is now: {moodles}", playerFragment!.MoodlesData);
|
||||||
}
|
}
|
||||||
|
|
||||||
ct.ThrowIfCancellationRequested();
|
ct.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
_logger.LogDebug("Handling transient update for {obj}", playerRelatedObject);
|
var (resolvedTransientPaths, clearedForPet) = await transientTask.ConfigureAwait(false);
|
||||||
|
if (clearedForPet != null)
|
||||||
// remove all potentially gathered paths from the transient resource manager that are resolved through static resolving
|
fragment.FileReplacements.Clear();
|
||||||
_transientResourceManager.ClearTransientPaths(objectKind, fragment.FileReplacements.SelectMany(c => c.GamePaths).ToList());
|
|
||||||
|
|
||||||
// get all remaining paths and resolve them
|
|
||||||
var transientPaths = ManageSemiTransientData(objectKind);
|
|
||||||
var resolvedTransientPaths = await GetFileReplacementsFromPaths(playerRelatedObject, transientPaths, new HashSet<string>(StringComparer.Ordinal)).ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (logDebug)
|
if (logDebug)
|
||||||
{
|
{
|
||||||
_logger.LogDebug("== Transient Replacements ==");
|
_logger.LogDebug("== Transient Replacements ==");
|
||||||
foreach (var replacement in resolvedTransientPaths.Select(c => new FileReplacement([.. c.Value], c.Key)).OrderBy(f => f.ResolvedPath, StringComparer.Ordinal))
|
foreach (var replacement in resolvedTransientPaths
|
||||||
|
.Select(c => new FileReplacement([.. c.Value], c.Key))
|
||||||
|
.OrderBy(f => f.ResolvedPath, StringComparer.Ordinal))
|
||||||
{
|
{
|
||||||
_logger.LogDebug("=> {repl}", replacement);
|
_logger.LogDebug("=> {repl}", replacement);
|
||||||
fragment.FileReplacements.Add(replacement);
|
fragment.FileReplacements.Add(replacement);
|
||||||
@@ -208,85 +311,64 @@ public class PlayerDataFactory
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
foreach (var replacement in resolvedTransientPaths.Select(c => new FileReplacement([.. c.Value], c.Key)))
|
foreach (var replacement in resolvedTransientPaths.Select(c => new FileReplacement([.. c.Value], c.Key)))
|
||||||
{
|
|
||||||
fragment.FileReplacements.Add(replacement);
|
fragment.FileReplacements.Add(replacement);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// clean up all semi transient resources that don't have any file replacement (aka null resolve)
|
|
||||||
_transientResourceManager.CleanUpSemiTransientResources(objectKind, [.. fragment.FileReplacements]);
|
_transientResourceManager.CleanUpSemiTransientResources(objectKind, [.. fragment.FileReplacements]);
|
||||||
|
|
||||||
ct.ThrowIfCancellationRequested();
|
fragment.FileReplacements = new HashSet<FileReplacement>(
|
||||||
|
fragment.FileReplacements
|
||||||
// make sure we only return data that actually has file replacements
|
.Where(v => v.HasFileReplacement)
|
||||||
fragment.FileReplacements = new HashSet<FileReplacement>(fragment.FileReplacements.Where(v => v.HasFileReplacement).OrderBy(v => v.ResolvedPath, StringComparer.Ordinal), FileReplacementComparer.Instance);
|
.OrderBy(v => v.ResolvedPath, StringComparer.Ordinal),
|
||||||
|
FileReplacementComparer.Instance);
|
||||||
// gather up data from ipc
|
|
||||||
Task<string> getHeelsOffset = _ipcManager.Heels.GetOffsetAsync();
|
|
||||||
Task<string> getGlamourerData = _ipcManager.Glamourer.GetCharacterCustomizationAsync(playerRelatedObject.Address);
|
|
||||||
Task<string?> getCustomizeData = _ipcManager.CustomizePlus.GetScaleAsync(playerRelatedObject.Address);
|
|
||||||
Task<string> getHonorificTitle = _ipcManager.Honorific.GetTitle();
|
|
||||||
fragment.GlamourerString = await getGlamourerData.ConfigureAwait(false);
|
|
||||||
_logger.LogDebug("Glamourer is now: {data}", fragment.GlamourerString);
|
|
||||||
var customizeScale = await getCustomizeData.ConfigureAwait(false);
|
|
||||||
fragment.CustomizePlusScale = customizeScale ?? string.Empty;
|
|
||||||
_logger.LogDebug("Customize is now: {data}", fragment.CustomizePlusScale);
|
|
||||||
|
|
||||||
if (objectKind == ObjectKind.Player)
|
|
||||||
{
|
|
||||||
var playerFragment = (fragment as CharacterDataFragmentPlayer)!;
|
|
||||||
playerFragment.ManipulationString = _ipcManager.Penumbra.GetMetaManipulations();
|
|
||||||
|
|
||||||
playerFragment!.HonorificData = await getHonorificTitle.ConfigureAwait(false);
|
|
||||||
_logger.LogDebug("Honorific is now: {data}", playerFragment!.HonorificData);
|
|
||||||
|
|
||||||
playerFragment!.HeelsData = await getHeelsOffset.ConfigureAwait(false);
|
|
||||||
_logger.LogDebug("Heels is now: {heels}", playerFragment!.HeelsData);
|
|
||||||
|
|
||||||
playerFragment!.MoodlesData = await _ipcManager.Moodles.GetStatusAsync(playerRelatedObject.Address).ConfigureAwait(false) ?? string.Empty;
|
|
||||||
_logger.LogDebug("Moodles is now: {moodles}", playerFragment!.MoodlesData);
|
|
||||||
|
|
||||||
playerFragment!.PetNamesData = _ipcManager.PetNames.GetLocalNames();
|
|
||||||
_logger.LogDebug("Pet Nicknames is now: {petnames}", playerFragment!.PetNamesData);
|
|
||||||
}
|
|
||||||
|
|
||||||
ct.ThrowIfCancellationRequested();
|
ct.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
var toCompute = fragment.FileReplacements.Where(f => !f.IsFileSwap).ToArray();
|
var toCompute = fragment.FileReplacements.Where(f => !f.IsFileSwap).ToArray();
|
||||||
_logger.LogDebug("Getting Hashes for {amount} Files", toCompute.Length);
|
_logger.LogDebug("Getting Hashes for {amount} Files", toCompute.Length);
|
||||||
var computedPaths = _fileCacheManager.GetFileCachesByPaths(toCompute.Select(c => c.ResolvedPath).ToArray());
|
|
||||||
foreach (var file in toCompute)
|
await Task.Run(() =>
|
||||||
{
|
{
|
||||||
ct.ThrowIfCancellationRequested();
|
var computedPaths = _fileCacheManager.GetFileCachesByPaths([.. toCompute.Select(c => c.ResolvedPath)]);
|
||||||
file.Hash = computedPaths[file.ResolvedPath]?.Hash ?? string.Empty;
|
foreach (var file in toCompute)
|
||||||
}
|
{
|
||||||
|
ct.ThrowIfCancellationRequested();
|
||||||
|
file.Hash = computedPaths[file.ResolvedPath]?.Hash ?? string.Empty;
|
||||||
|
}
|
||||||
|
}, ct).ConfigureAwait(false);
|
||||||
|
|
||||||
var removed = fragment.FileReplacements.RemoveWhere(f => !f.IsFileSwap && string.IsNullOrEmpty(f.Hash));
|
var removed = fragment.FileReplacements.RemoveWhere(f => !f.IsFileSwap && string.IsNullOrEmpty(f.Hash));
|
||||||
if (removed > 0)
|
if (removed > 0)
|
||||||
{
|
|
||||||
_logger.LogDebug("Removed {amount} of invalid files", removed);
|
_logger.LogDebug("Removed {amount} of invalid files", removed);
|
||||||
}
|
|
||||||
|
|
||||||
ct.ThrowIfCancellationRequested();
|
ct.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
Dictionary<string, List<ushort>>? boneIndices = null;
|
Dictionary<string, List<ushort>>? boneIndices = null;
|
||||||
var hasPapFiles = false;
|
var hasPapFiles = false;
|
||||||
if (objectKind == ObjectKind.Player)
|
|
||||||
{
|
|
||||||
hasPapFiles = fragment.FileReplacements.Any(f =>
|
|
||||||
!f.IsFileSwap && f.GamePaths.First().EndsWith("pap", StringComparison.OrdinalIgnoreCase));
|
|
||||||
if (hasPapFiles)
|
|
||||||
{
|
|
||||||
boneIndices = await _dalamudUtil.RunOnFrameworkThread(() => _modelAnalyzer.GetSkeletonBoneIndices(playerRelatedObject)).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (objectKind == ObjectKind.Player)
|
if (objectKind == ObjectKind.Player)
|
||||||
{
|
{
|
||||||
|
hasPapFiles = fragment.FileReplacements.Any(f =>
|
||||||
|
!f.IsFileSwap && f.GamePaths.Any(p => p.EndsWith(".pap", StringComparison.OrdinalIgnoreCase)));
|
||||||
|
|
||||||
|
if (hasPapFiles)
|
||||||
|
{
|
||||||
|
boneIndices = await _dalamudUtil
|
||||||
|
.RunOnFrameworkThread(() => _modelAnalyzer.GetSkeletonBoneIndices(playerRelatedObject))
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
#if DEBUG
|
||||||
|
if (hasPapFiles && boneIndices != null)
|
||||||
|
_modelAnalyzer.DumpLocalSkeletonIndices(playerRelatedObject);
|
||||||
|
#endif
|
||||||
|
|
||||||
if (hasPapFiles)
|
if (hasPapFiles)
|
||||||
{
|
{
|
||||||
await VerifyPlayerAnimationBones(boneIndices, (fragment as CharacterDataFragmentPlayer)!, ct).ConfigureAwait(false);
|
await VerifyPlayerAnimationBones(boneIndices, (CharacterDataFragmentPlayer)fragment, ct)
|
||||||
|
.ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException e)
|
catch (OperationCanceledException e)
|
||||||
@@ -300,105 +382,277 @@ public class PlayerDataFactory
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogInformation("Building character data for {obj} took {time}ms", objectKind, TimeSpan.FromTicks(DateTime.UtcNow.Ticks - start.Ticks).TotalMilliseconds);
|
_logger.LogInformation("Building character data for {obj} took {time}ms",
|
||||||
|
objectKind, sw.Elapsed.TotalMilliseconds);
|
||||||
|
|
||||||
return fragment;
|
return fragment;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task VerifyPlayerAnimationBones(Dictionary<string, List<ushort>>? boneIndices, CharacterDataFragmentPlayer fragment, CancellationToken ct)
|
private async Task EnsureObjectPresentAsync(GameObjectHandler handler, CancellationToken ct)
|
||||||
{
|
{
|
||||||
if (boneIndices == null) return;
|
var remaining = 10000;
|
||||||
|
while (remaining > 0)
|
||||||
if (_logger.IsEnabled(LogLevel.Debug))
|
|
||||||
{
|
|
||||||
foreach (var kvp in boneIndices)
|
|
||||||
{
|
|
||||||
_logger.LogDebug("Found {skellyname} ({idx} bone indices) on player: {bones}", kvp.Key, kvp.Value.Any() ? kvp.Value.Max() : 0, string.Join(',', kvp.Value));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var maxPlayerBoneIndex = boneIndices.SelectMany(kvp => kvp.Value).DefaultIfEmpty().Max();
|
|
||||||
if (maxPlayerBoneIndex <= 0) return;
|
|
||||||
|
|
||||||
int noValidationFailed = 0;
|
|
||||||
foreach (var file in fragment.FileReplacements.Where(f => !f.IsFileSwap && f.GamePaths.First().EndsWith("pap", StringComparison.OrdinalIgnoreCase)).ToList())
|
|
||||||
{
|
{
|
||||||
ct.ThrowIfCancellationRequested();
|
ct.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
var skeletonIndices = await _dalamudUtil.RunOnFrameworkThread(() => _modelAnalyzer.GetBoneIndicesFromPap(file.Hash)).ConfigureAwait(false);
|
var obj = await _dalamudUtil.CreateGameObjectAsync(handler.Address).ConfigureAwait(false);
|
||||||
bool validationFailed = false;
|
if (await _dalamudUtil.IsObjectPresentAsync(obj).ConfigureAwait(false))
|
||||||
if (skeletonIndices != null)
|
return;
|
||||||
|
|
||||||
|
_logger.LogTrace("Character is null but it shouldn't be, waiting");
|
||||||
|
await Task.Delay(50, ct).ConfigureAwait(false);
|
||||||
|
remaining -= 50;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static HashSet<FileReplacement> BuildStaticReplacements(Dictionary<string, HashSet<string>> resolvedPaths)
|
||||||
|
{
|
||||||
|
var set = new HashSet<FileReplacement>(FileReplacementComparer.Instance);
|
||||||
|
|
||||||
|
foreach (var kvp in resolvedPaths)
|
||||||
|
{
|
||||||
|
var fr = new FileReplacement([.. kvp.Value], kvp.Key);
|
||||||
|
if (!fr.HasFileReplacement) continue;
|
||||||
|
|
||||||
|
var allAllowed = fr.GamePaths.All(g =>
|
||||||
|
CacheMonitor.AllowedFileExtensions.Any(e => g.EndsWith(e, StringComparison.OrdinalIgnoreCase)));
|
||||||
|
|
||||||
|
if (!allAllowed) continue;
|
||||||
|
|
||||||
|
set.Add(fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
return set;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<(IReadOnlyDictionary<string, string[]> ResolvedPaths, HashSet<FileReplacement>? ClearedReplacements)>
|
||||||
|
ResolveTransientReplacementsAsync(
|
||||||
|
GameObjectHandler obj,
|
||||||
|
ObjectKind objectKind,
|
||||||
|
HashSet<FileReplacement> staticReplacements,
|
||||||
|
Task waitRecordingTask,
|
||||||
|
CancellationToken ct)
|
||||||
|
{
|
||||||
|
await waitRecordingTask.ConfigureAwait(false);
|
||||||
|
|
||||||
|
HashSet<FileReplacement>? clearedReplacements = null;
|
||||||
|
|
||||||
|
if (objectKind == ObjectKind.Pet)
|
||||||
|
{
|
||||||
|
foreach (var item in staticReplacements.Where(i => i.HasFileReplacement).SelectMany(p => p.GamePaths))
|
||||||
{
|
{
|
||||||
// 105 is the maximum vanilla skellington spoopy bone index
|
if (_transientResourceManager.AddTransientResource(objectKind, item))
|
||||||
if (skeletonIndices.All(k => k.Value.Max() <= 105))
|
_logger.LogDebug("Marking static {item} for Pet as transient", item);
|
||||||
{
|
}
|
||||||
_logger.LogTrace("All indices of {path} are <= 105, ignoring", file.ResolvedPath);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.LogDebug("Verifying bone indices for {path}, found {x} skeletons", file.ResolvedPath, skeletonIndices.Count);
|
_logger.LogTrace("Clearing {count} Static Replacements for Pet", staticReplacements.Count);
|
||||||
|
clearedReplacements = staticReplacements;
|
||||||
|
}
|
||||||
|
|
||||||
foreach (var boneCount in skeletonIndices)
|
ct.ThrowIfCancellationRequested();
|
||||||
{
|
|
||||||
var maxAnimationIndex = boneCount.Value.DefaultIfEmpty().Max();
|
_transientResourceManager.ClearTransientPaths(objectKind, [.. staticReplacements.SelectMany(c => c.GamePaths)]);
|
||||||
if (maxAnimationIndex > maxPlayerBoneIndex)
|
|
||||||
|
var transientPaths = ManageSemiTransientData(objectKind);
|
||||||
|
if (transientPaths.Count == 0)
|
||||||
|
return (new Dictionary<string, string[]>(StringComparer.Ordinal), clearedReplacements);
|
||||||
|
|
||||||
|
var resolved = await GetFileReplacementsFromPaths(obj, transientPaths, new HashSet<string>(StringComparer.Ordinal))
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (_maxTransientResolvedEntries > 0 && resolved.Count > _maxTransientResolvedEntries)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Transient entries ({resolved}) are above the threshold {max}; Please consider disable some mods (VFX have heavy load) to reduce transient load",
|
||||||
|
resolved.Count,
|
||||||
|
_maxTransientResolvedEntries);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (resolved, clearedReplacements);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private async Task VerifyPlayerAnimationBones(
|
||||||
|
Dictionary<string, List<ushort>>? playerBoneIndices,
|
||||||
|
CharacterDataFragmentPlayer fragment,
|
||||||
|
CancellationToken ct)
|
||||||
|
{
|
||||||
|
var mode = _configService.Current.AnimationValidationMode;
|
||||||
|
var allowBasedShift = _configService.Current.AnimationAllowOneBasedShift;
|
||||||
|
var allownNightIndex = _configService.Current.AnimationAllowNeighborIndexTolerance;
|
||||||
|
|
||||||
|
if (mode == AnimationValidationMode.Unsafe)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (playerBoneIndices == null || playerBoneIndices.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var localBoneSets = new Dictionary<string, HashSet<ushort>>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
foreach (var (rawLocalKey, indices) in playerBoneIndices)
|
||||||
|
{
|
||||||
|
if (indices is not { Count: > 0 })
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var key = XivDataAnalyzer.CanonicalizeSkeletonKey(rawLocalKey);
|
||||||
|
if (string.IsNullOrEmpty(key))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (!localBoneSets.TryGetValue(key, out var set))
|
||||||
|
localBoneSets[key] = set = [];
|
||||||
|
|
||||||
|
foreach (var idx in indices)
|
||||||
|
set.Add(idx);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (localBoneSets.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (_logger.IsEnabled(LogLevel.Debug))
|
||||||
|
{
|
||||||
|
_logger.LogDebug("SEND local buckets: {b}",
|
||||||
|
string.Join(", ", localBoneSets.Keys.Order(StringComparer.Ordinal)));
|
||||||
|
|
||||||
|
foreach (var kvp in localBoneSets.OrderBy(k => k.Key, StringComparer.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
var min = kvp.Value.Count > 0 ? kvp.Value.Min() : 0;
|
||||||
|
var max = kvp.Value.Count > 0 ? kvp.Value.Max() : 0;
|
||||||
|
_logger.LogDebug("Local bucket {bucket}: count={count} min={min} max={max}",
|
||||||
|
kvp.Key, kvp.Value.Count, min, max);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var papGroups = fragment.FileReplacements
|
||||||
|
.Where(f => !f.IsFileSwap
|
||||||
|
&& !string.IsNullOrEmpty(f.Hash)
|
||||||
|
&& f.GamePaths is { Count: > 0 }
|
||||||
|
&& f.GamePaths.Any(p => p.EndsWith(".pap", StringComparison.OrdinalIgnoreCase)))
|
||||||
|
.GroupBy(f => f.Hash!, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
int noValidationFailed = 0;
|
||||||
|
|
||||||
|
foreach (var g in papGroups)
|
||||||
|
{
|
||||||
|
ct.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
var hash = g.Key;
|
||||||
|
|
||||||
|
Dictionary<string, List<ushort>>? papIndices = null;
|
||||||
|
|
||||||
|
await _papParseLimiter.WaitAsync(ct).ConfigureAwait(false);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
papIndices = await Task.Run(() => _modelAnalyzer.GetBoneIndicesFromPap(hash), ct)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_papParseLimiter.Release();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (papIndices == null || papIndices.Count == 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (papIndices.All(k => k.Value.DefaultIfEmpty().Max() <= 105))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (_logger.IsEnabled(LogLevel.Debug))
|
||||||
|
{
|
||||||
|
var papBuckets = papIndices
|
||||||
|
.Select(kvp => new
|
||||||
{
|
{
|
||||||
_logger.LogWarning("Found more bone indices on the animation {path} skeleton {skl} (max indice {idx}) than on any player related skeleton (max indice {idx2})",
|
Raw = kvp.Key,
|
||||||
file.ResolvedPath, boneCount.Key, maxAnimationIndex, maxPlayerBoneIndex);
|
Key = XivDataAnalyzer.CanonicalizeSkeletonKey(kvp.Key),
|
||||||
validationFailed = true;
|
Indices = kvp.Value
|
||||||
break;
|
})
|
||||||
}
|
.Where(x => x.Indices is { Count: > 0 })
|
||||||
}
|
.GroupBy(x => string.IsNullOrEmpty(x.Key) ? x.Raw : x.Key!, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.Select(grp =>
|
||||||
|
{
|
||||||
|
var all = grp.SelectMany(v => v.Indices).ToList();
|
||||||
|
var min = all.Count > 0 ? all.Min() : 0;
|
||||||
|
var max = all.Count > 0 ? all.Max() : 0;
|
||||||
|
var raws = string.Join(',', grp.Select(v => v.Raw).Distinct(StringComparer.OrdinalIgnoreCase));
|
||||||
|
return $"{grp.Key}(min={min},max={max},raw=[{raws}])";
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
_logger.LogDebug("SEND pap buckets for hash={hash}: {b}",
|
||||||
|
hash,
|
||||||
|
string.Join(" | ", papBuckets));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (validationFailed)
|
if (XivDataAnalyzer.IsPapCompatible(localBoneSets, papIndices, mode, allowBasedShift, allownNightIndex, out var reason))
|
||||||
{
|
continue;
|
||||||
noValidationFailed++;
|
|
||||||
_logger.LogDebug("Removing {file} from sent file replacements and transient data", file.ResolvedPath);
|
|
||||||
fragment.FileReplacements.Remove(file);
|
|
||||||
foreach (var gamePath in file.GamePaths)
|
|
||||||
{
|
|
||||||
_transientResourceManager.RemoveTransientResource(ObjectKind.Player, gamePath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
noValidationFailed++;
|
||||||
|
|
||||||
|
_logger.LogWarning(
|
||||||
|
"Animation PAP hash {hash} is not compatible with local skeletons; dropping all mappings for this hash. Reason: {reason}",
|
||||||
|
hash,
|
||||||
|
reason);
|
||||||
|
|
||||||
|
var removedGamePaths = fragment.FileReplacements
|
||||||
|
.Where(fr => !fr.IsFileSwap
|
||||||
|
&& string.Equals(fr.Hash, hash, StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& fr.GamePaths.Any(p => p.EndsWith(".pap", StringComparison.OrdinalIgnoreCase)))
|
||||||
|
.SelectMany(fr => fr.GamePaths.Where(p => p.EndsWith(".pap", StringComparison.OrdinalIgnoreCase)))
|
||||||
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
fragment.FileReplacements.RemoveWhere(fr =>
|
||||||
|
!fr.IsFileSwap
|
||||||
|
&& string.Equals(fr.Hash, hash, StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& fr.GamePaths.Any(p => p.EndsWith(".pap", StringComparison.OrdinalIgnoreCase)));
|
||||||
|
|
||||||
|
foreach (var gp in removedGamePaths)
|
||||||
|
_transientResourceManager.RemoveTransientResource(ObjectKind.Player, gp);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (noValidationFailed > 0)
|
if (noValidationFailed > 0)
|
||||||
{
|
{
|
||||||
_lightlessMediator.Publish(new NotificationMessage("Invalid Skeleton Setup",
|
_lightlessMediator.Publish(new NotificationMessage(
|
||||||
$"Your client is attempting to send {noValidationFailed} animation files with invalid bone data. Those animation files have been removed from your sent data. " +
|
"Invalid Skeleton Setup",
|
||||||
$"Verify that you are using the correct skeleton for those animation files (Check /xllog for more information).",
|
$"Your client is attempting to send {noValidationFailed} animation files that don't match your current skeleton validation mode ({mode}). " +
|
||||||
NotificationType.Warning, TimeSpan.FromSeconds(10)));
|
"Please adjust your skeleton/mods or change the validation mode if this is unexpected. " +
|
||||||
|
"Those animation files have been removed from your sent (player) data. (Check /xllog for details).",
|
||||||
|
NotificationType.Warning,
|
||||||
|
TimeSpan.FromSeconds(10)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<IReadOnlyDictionary<string, string[]>> GetFileReplacementsFromPaths(GameObjectHandler handler, HashSet<string> forwardResolve, HashSet<string> reverseResolve)
|
|
||||||
|
private async Task<IReadOnlyDictionary<string, string[]>> GetFileReplacementsFromPaths(
|
||||||
|
GameObjectHandler handler,
|
||||||
|
HashSet<string> forwardResolve,
|
||||||
|
HashSet<string> reverseResolve)
|
||||||
{
|
{
|
||||||
var forwardPaths = forwardResolve.ToArray();
|
var forwardPaths = forwardResolve.ToArray();
|
||||||
var reversePaths = reverseResolve.ToArray();
|
var reversePaths = reverseResolve.ToArray();
|
||||||
Dictionary<string, List<string>> resolvedPaths = new(StringComparer.Ordinal);
|
if (forwardPaths.Length == 0 && reversePaths.Length == 0)
|
||||||
|
{
|
||||||
|
return new Dictionary<string, string[]>(StringComparer.OrdinalIgnoreCase).AsReadOnly();
|
||||||
|
}
|
||||||
|
|
||||||
|
var forwardPathsLower = forwardPaths.Length == 0 ? Array.Empty<string>() : forwardPaths.Select(p => p.ToLowerInvariant()).ToArray();
|
||||||
|
var reversePathsLower = reversePaths.Length == 0 ? Array.Empty<string>() : reversePaths.Select(p => p.ToLowerInvariant()).ToArray();
|
||||||
|
|
||||||
|
Dictionary<string, List<string>> resolvedPaths = new(forwardPaths.Length + reversePaths.Length, StringComparer.Ordinal);
|
||||||
if (handler.ObjectKind != ObjectKind.Player)
|
if (handler.ObjectKind != ObjectKind.Player)
|
||||||
{
|
{
|
||||||
var (objectIndex, forwardResolved, reverseResolved) = await _dalamudUtil.RunOnFrameworkThread(() =>
|
var (objectIndex, forwardResolved, reverseResolved) = await _dalamudUtil.RunOnFrameworkThread(() =>
|
||||||
{
|
{
|
||||||
var idx = handler.GetGameObject()?.ObjectIndex;
|
var idx = handler.GetGameObject()?.ObjectIndex;
|
||||||
if (!idx.HasValue)
|
if (!idx.HasValue)
|
||||||
{
|
|
||||||
return ((int?)null, Array.Empty<string>(), Array.Empty<string[]>());
|
return ((int?)null, Array.Empty<string>(), Array.Empty<string[]>());
|
||||||
}
|
|
||||||
|
|
||||||
var resolvedForward = new string[forwardPaths.Length];
|
var resolvedForward = new string[forwardPaths.Length];
|
||||||
for (int i = 0; i < forwardPaths.Length; i++)
|
for (int i = 0; i < forwardPaths.Length; i++)
|
||||||
{
|
|
||||||
resolvedForward[i] = _ipcManager.Penumbra.ResolveGameObjectPath(forwardPaths[i], idx.Value);
|
resolvedForward[i] = _ipcManager.Penumbra.ResolveGameObjectPath(forwardPaths[i], idx.Value);
|
||||||
}
|
|
||||||
|
|
||||||
var resolvedReverse = new string[reversePaths.Length][];
|
var resolvedReverse = new string[reversePaths.Length][];
|
||||||
for (int i = 0; i < reversePaths.Length; i++)
|
for (int i = 0; i < reversePaths.Length; i++)
|
||||||
{
|
|
||||||
resolvedReverse[i] = _ipcManager.Penumbra.ReverseResolveGameObjectPath(reversePaths[i], idx.Value);
|
resolvedReverse[i] = _ipcManager.Penumbra.ReverseResolveGameObjectPath(reversePaths[i], idx.Value);
|
||||||
}
|
|
||||||
|
|
||||||
return (idx, resolvedForward, resolvedReverse);
|
return (idx, resolvedForward, resolvedReverse);
|
||||||
}).ConfigureAwait(false);
|
}).ConfigureAwait(false);
|
||||||
@@ -409,14 +663,10 @@ public class PlayerDataFactory
|
|||||||
{
|
{
|
||||||
var filePath = forwardResolved[i]?.ToLowerInvariant();
|
var filePath = forwardResolved[i]?.ToLowerInvariant();
|
||||||
if (string.IsNullOrEmpty(filePath))
|
if (string.IsNullOrEmpty(filePath))
|
||||||
{
|
|
||||||
continue;
|
continue;
|
||||||
}
|
|
||||||
|
|
||||||
if (resolvedPaths.TryGetValue(filePath, out var list))
|
if (resolvedPaths.TryGetValue(filePath, out var list))
|
||||||
{
|
|
||||||
list.Add(forwardPaths[i].ToLowerInvariant());
|
list.Add(forwardPaths[i].ToLowerInvariant());
|
||||||
}
|
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
resolvedPaths[filePath] = [forwardPaths[i].ToLowerInvariant()];
|
resolvedPaths[filePath] = [forwardPaths[i].ToLowerInvariant()];
|
||||||
@@ -425,15 +675,16 @@ public class PlayerDataFactory
|
|||||||
|
|
||||||
for (int i = 0; i < reversePaths.Length; i++)
|
for (int i = 0; i < reversePaths.Length; i++)
|
||||||
{
|
{
|
||||||
var filePath = reversePaths[i].ToLowerInvariant();
|
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))
|
if (resolvedPaths.TryGetValue(filePath, out var list))
|
||||||
{
|
|
||||||
list.AddRange(reverseResolved[i].Select(c => c.ToLowerInvariant()));
|
list.AddRange(reverseResolved[i].Select(c => c.ToLowerInvariant()));
|
||||||
}
|
|
||||||
else
|
else
|
||||||
{
|
resolvedPaths[filePath] = [.. reverseResolved[i].Select(c => c.ToLowerInvariant()).ToList()];
|
||||||
resolvedPaths[filePath] = new List<string>(reverseResolved[i].Select(c => c.ToLowerInvariant()).ToList());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return resolvedPaths.ToDictionary(k => k.Key, k => k.Value.ToArray(), StringComparer.OrdinalIgnoreCase).AsReadOnly();
|
return resolvedPaths.ToDictionary(k => k.Key, k => k.Value.ToArray(), StringComparer.OrdinalIgnoreCase).AsReadOnly();
|
||||||
@@ -441,30 +692,28 @@ public class PlayerDataFactory
|
|||||||
}
|
}
|
||||||
|
|
||||||
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++)
|
||||||
{
|
{
|
||||||
var filePath = forward[i].ToLowerInvariant();
|
var filePath = forward[i].ToLowerInvariant();
|
||||||
if (resolvedPaths.TryGetValue(filePath, out var list))
|
if (resolvedPaths.TryGetValue(filePath, out var list))
|
||||||
{
|
|
||||||
list.Add(forwardPaths[i].ToLowerInvariant());
|
list.Add(forwardPaths[i].ToLowerInvariant());
|
||||||
}
|
|
||||||
else
|
else
|
||||||
{
|
|
||||||
resolvedPaths[filePath] = [forwardPaths[i].ToLowerInvariant()];
|
resolvedPaths[filePath] = [forwardPaths[i].ToLowerInvariant()];
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int i = 0; i < reversePaths.Length; i++)
|
for (int i = 0; i < reversePaths.Length; i++)
|
||||||
{
|
{
|
||||||
var filePath = reversePaths[i].ToLowerInvariant();
|
var filePath = reversePathsLower[i];
|
||||||
|
var reverseResolvedLower = new string[reverse[i].Length];
|
||||||
|
for (var j = 0; j < reverseResolvedLower.Length; j++)
|
||||||
|
{
|
||||||
|
reverseResolvedLower[j] = reverse[i][j].ToLowerInvariant();
|
||||||
|
}
|
||||||
if (resolvedPaths.TryGetValue(filePath, out var list))
|
if (resolvedPaths.TryGetValue(filePath, out var list))
|
||||||
{
|
|
||||||
list.AddRange(reverse[i].Select(c => c.ToLowerInvariant()));
|
list.AddRange(reverse[i].Select(c => c.ToLowerInvariant()));
|
||||||
}
|
|
||||||
else
|
else
|
||||||
{
|
resolvedPaths[filePath] = [.. reverse[i].Select(c => c.ToLowerInvariant()).ToList()];
|
||||||
resolvedPaths[filePath] = new List<string>(reverse[i].Select(c => c.ToLowerInvariant()).ToList());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return resolvedPaths.ToDictionary(k => k.Key, k => k.Value.ToArray(), StringComparer.OrdinalIgnoreCase).AsReadOnly();
|
return resolvedPaths.ToDictionary(k => k.Key, k => k.Value.ToArray(), StringComparer.OrdinalIgnoreCase).AsReadOnly();
|
||||||
@@ -475,11 +724,29 @@ public class PlayerDataFactory
|
|||||||
_transientResourceManager.PersistTransientResources(objectKind);
|
_transientResourceManager.PersistTransientResources(objectKind);
|
||||||
|
|
||||||
HashSet<string> pathsToResolve = new(StringComparer.Ordinal);
|
HashSet<string> pathsToResolve = new(StringComparer.Ordinal);
|
||||||
foreach (var path in _transientResourceManager.GetSemiTransientResources(objectKind).Where(path => !string.IsNullOrEmpty(path)))
|
|
||||||
|
int scanned = 0, skippedEmpty = 0, skippedVfx = 0;
|
||||||
|
|
||||||
|
foreach (var path in _transientResourceManager.GetSemiTransientResources(objectKind))
|
||||||
{
|
{
|
||||||
|
scanned++;
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(path))
|
||||||
|
{
|
||||||
|
skippedEmpty++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
pathsToResolve.Add(path);
|
pathsToResolve.Add(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_logger.IsEnabled(LogLevel.Debug))
|
||||||
|
{
|
||||||
|
_logger.LogDebug(
|
||||||
|
"ManageSemiTransientData({kind}): scanned={scanned}, added={added}, skippedEmpty={skippedEmpty}, skippedVfx={skippedVfx}",
|
||||||
|
objectKind, scanned, pathsToResolve.Count, skippedEmpty, skippedVfx);
|
||||||
|
}
|
||||||
|
|
||||||
return pathsToResolve;
|
return pathsToResolve;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
|||||||
if (msg.Address == Address)
|
if (msg.Address == Address)
|
||||||
{
|
{
|
||||||
_haltProcessing = false;
|
_haltProcessing = false;
|
||||||
|
Refresh();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -176,30 +177,36 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
|||||||
Mediator.Publish(new GameObjectHandlerDestroyedMessage(this, _isOwnedObject));
|
Mediator.Publish(new GameObjectHandlerDestroyedMessage(this, _isOwnedObject));
|
||||||
}
|
}
|
||||||
|
|
||||||
private unsafe void CheckAndUpdateObject()
|
private void CheckAndUpdateObject() => CheckAndUpdateObject(allowPublish: true);
|
||||||
|
|
||||||
|
private unsafe void CheckAndUpdateObject(bool allowPublish = true)
|
||||||
{
|
{
|
||||||
var prevAddr = Address;
|
var prevAddr = Address;
|
||||||
var prevDrawObj = DrawObjectAddress;
|
var prevDrawObj = DrawObjectAddress;
|
||||||
|
|
||||||
Address = _getAddress();
|
Address = _getAddress();
|
||||||
|
|
||||||
if (Address != IntPtr.Zero)
|
if (Address != IntPtr.Zero)
|
||||||
{
|
{
|
||||||
var gameObject = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)Address;
|
var gameObject = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)Address;
|
||||||
var drawObjAddr = (IntPtr)gameObject->DrawObject;
|
DrawObjectAddress = (IntPtr)gameObject->DrawObject;
|
||||||
DrawObjectAddress = drawObjAddr;
|
|
||||||
EntityId = gameObject->EntityId;
|
EntityId = gameObject->EntityId;
|
||||||
CurrentDrawCondition = DrawCondition.None;
|
|
||||||
|
var chara = (Character*)Address;
|
||||||
|
var newName = chara->GameObject.NameString;
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(newName) && !string.Equals(newName, Name, StringComparison.Ordinal))
|
||||||
|
Name = newName;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
DrawObjectAddress = IntPtr.Zero;
|
DrawObjectAddress = IntPtr.Zero;
|
||||||
EntityId = uint.MaxValue;
|
EntityId = uint.MaxValue;
|
||||||
CurrentDrawCondition = DrawCondition.DrawObjectZero;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
CurrentDrawCondition = IsBeingDrawnUnsafe();
|
CurrentDrawCondition = IsBeingDrawnUnsafe();
|
||||||
|
|
||||||
if (_haltProcessing) return;
|
if (_haltProcessing || !allowPublish) return;
|
||||||
|
|
||||||
bool drawObjDiff = DrawObjectAddress != prevDrawObj;
|
bool drawObjDiff = DrawObjectAddress != prevDrawObj;
|
||||||
bool addrDiff = Address != prevAddr;
|
bool addrDiff = Address != prevAddr;
|
||||||
@@ -356,12 +363,10 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
|||||||
|
|
||||||
private void FrameworkUpdate()
|
private void FrameworkUpdate()
|
||||||
{
|
{
|
||||||
if (!_delayedZoningTask?.IsCompleted ?? false) return;
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_performanceCollector.LogPerformance(this, $"CheckAndUpdateObject>{(_isOwnedObject ? "Self" : "Other")}+{ObjectKind}/{(string.IsNullOrEmpty(Name) ? "Unk" : Name)}"
|
var zoningDelayActive = !(_delayedZoningTask?.IsCompleted ?? true);
|
||||||
+ $"+{Address.ToString("X")}", CheckAndUpdateObject);
|
_performanceCollector.LogPerformance(this, $"CheckAndUpdateObject>{(_isOwnedObject ? "Self" : "Other")}+{ObjectKind}/{(string.IsNullOrEmpty(Name) ? "Unk" : Name)}", () => CheckAndUpdateObject(allowPublish: !zoningDelayActive));
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -462,6 +467,6 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
|||||||
Logger.LogDebug("[{this}] Delay after zoning complete", this);
|
Logger.LogDebug("[{this}] Delay after zoning complete", this);
|
||||||
_zoningCts.Dispose();
|
_zoningCts.Dispose();
|
||||||
}
|
}
|
||||||
});
|
}, _zoningCts.Token);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -16,4 +16,5 @@ public interface IPairPerformanceSubject
|
|||||||
long LastAppliedApproximateVRAMBytes { get; set; }
|
long LastAppliedApproximateVRAMBytes { get; set; }
|
||||||
long LastAppliedApproximateEffectiveVRAMBytes { get; set; }
|
long LastAppliedApproximateEffectiveVRAMBytes { get; set; }
|
||||||
long LastAppliedDataTris { get; set; }
|
long LastAppliedDataTris { get; set; }
|
||||||
|
long LastAppliedApproximateEffectiveTris { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ public class Pair
|
|||||||
public string? PlayerName => TryGetHandler()?.PlayerName ?? UserPair.User.AliasOrUID;
|
public string? PlayerName => TryGetHandler()?.PlayerName ?? UserPair.User.AliasOrUID;
|
||||||
public long LastAppliedDataBytes => TryGetHandler()?.LastAppliedDataBytes ?? -1;
|
public long LastAppliedDataBytes => TryGetHandler()?.LastAppliedDataBytes ?? -1;
|
||||||
public long LastAppliedDataTris => TryGetHandler()?.LastAppliedDataTris ?? -1;
|
public long LastAppliedDataTris => TryGetHandler()?.LastAppliedDataTris ?? -1;
|
||||||
|
public long LastAppliedApproximateEffectiveTris => TryGetHandler()?.LastAppliedApproximateEffectiveTris ?? -1;
|
||||||
public long LastAppliedApproximateVRAMBytes => TryGetHandler()?.LastAppliedApproximateVRAMBytes ?? -1;
|
public long LastAppliedApproximateVRAMBytes => TryGetHandler()?.LastAppliedApproximateVRAMBytes ?? -1;
|
||||||
public long LastAppliedApproximateEffectiveVRAMBytes => TryGetHandler()?.LastAppliedApproximateEffectiveVRAMBytes ?? -1;
|
public long LastAppliedApproximateEffectiveVRAMBytes => TryGetHandler()?.LastAppliedApproximateEffectiveVRAMBytes ?? -1;
|
||||||
public string Ident => TryGetHandler()?.Ident ?? TryGetConnection()?.Ident ?? string.Empty;
|
public string Ident => TryGetHandler()?.Ident ?? TryGetConnection()?.Ident ?? string.Empty;
|
||||||
|
|||||||
@@ -125,6 +125,7 @@ public sealed partial class PairCoordinator
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_mediator.Publish(new PairOnlineMessage(new PairUniqueIdentifier(dto.User.UID)));
|
||||||
PublishPairDataChanged();
|
PublishPairDataChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,12 +1,15 @@
|
|||||||
using LightlessSync.FileCache;
|
using LightlessSync.FileCache;
|
||||||
using LightlessSync.Interop.Ipc;
|
using LightlessSync.Interop.Ipc;
|
||||||
|
using LightlessSync.LightlessConfiguration;
|
||||||
using LightlessSync.PlayerData.Factories;
|
using LightlessSync.PlayerData.Factories;
|
||||||
using LightlessSync.Services;
|
using LightlessSync.Services;
|
||||||
using LightlessSync.Services.ActorTracking;
|
using LightlessSync.Services.ActorTracking;
|
||||||
using LightlessSync.Services.Mediator;
|
using LightlessSync.Services.Mediator;
|
||||||
|
using LightlessSync.Services.ModelDecimation;
|
||||||
using LightlessSync.Services.PairProcessing;
|
using LightlessSync.Services.PairProcessing;
|
||||||
using LightlessSync.Services.ServerConfiguration;
|
using LightlessSync.Services.ServerConfiguration;
|
||||||
using LightlessSync.Services.TextureCompression;
|
using LightlessSync.Services.TextureCompression;
|
||||||
|
using Dalamud.Plugin.Services;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@@ -25,13 +28,18 @@ internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory
|
|||||||
private readonly IServiceProvider _serviceProvider;
|
private readonly IServiceProvider _serviceProvider;
|
||||||
private readonly IHostApplicationLifetime _lifetime;
|
private readonly IHostApplicationLifetime _lifetime;
|
||||||
private readonly FileCacheManager _fileCacheManager;
|
private readonly FileCacheManager _fileCacheManager;
|
||||||
|
private readonly PlayerPerformanceConfigService _playerPerformanceConfigService;
|
||||||
private readonly PlayerPerformanceService _playerPerformanceService;
|
private readonly PlayerPerformanceService _playerPerformanceService;
|
||||||
private readonly PairProcessingLimiter _pairProcessingLimiter;
|
private readonly PairProcessingLimiter _pairProcessingLimiter;
|
||||||
private readonly ServerConfigurationManager _serverConfigManager;
|
private readonly ServerConfigurationManager _serverConfigManager;
|
||||||
private readonly TextureDownscaleService _textureDownscaleService;
|
private readonly TextureDownscaleService _textureDownscaleService;
|
||||||
|
private readonly ModelDecimationService _modelDecimationService;
|
||||||
private readonly PairStateCache _pairStateCache;
|
private readonly PairStateCache _pairStateCache;
|
||||||
private readonly PairPerformanceMetricsCache _pairPerformanceMetricsCache;
|
private readonly PairPerformanceMetricsCache _pairPerformanceMetricsCache;
|
||||||
private readonly PenumbraTempCollectionJanitor _tempCollectionJanitor;
|
private readonly PenumbraTempCollectionJanitor _tempCollectionJanitor;
|
||||||
|
private readonly LightlessConfigService _configService;
|
||||||
|
private readonly XivDataAnalyzer _modelAnalyzer;
|
||||||
|
private readonly IFramework _framework;
|
||||||
|
|
||||||
public PairHandlerAdapterFactory(
|
public PairHandlerAdapterFactory(
|
||||||
ILoggerFactory loggerFactory,
|
ILoggerFactory loggerFactory,
|
||||||
@@ -42,15 +50,20 @@ internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory
|
|||||||
FileDownloadManagerFactory fileDownloadManagerFactory,
|
FileDownloadManagerFactory fileDownloadManagerFactory,
|
||||||
PluginWarningNotificationService pluginWarningNotificationManager,
|
PluginWarningNotificationService pluginWarningNotificationManager,
|
||||||
IServiceProvider serviceProvider,
|
IServiceProvider serviceProvider,
|
||||||
|
IFramework framework,
|
||||||
IHostApplicationLifetime lifetime,
|
IHostApplicationLifetime lifetime,
|
||||||
FileCacheManager fileCacheManager,
|
FileCacheManager fileCacheManager,
|
||||||
|
PlayerPerformanceConfigService playerPerformanceConfigService,
|
||||||
PlayerPerformanceService playerPerformanceService,
|
PlayerPerformanceService playerPerformanceService,
|
||||||
PairProcessingLimiter pairProcessingLimiter,
|
PairProcessingLimiter pairProcessingLimiter,
|
||||||
ServerConfigurationManager serverConfigManager,
|
ServerConfigurationManager serverConfigManager,
|
||||||
TextureDownscaleService textureDownscaleService,
|
TextureDownscaleService textureDownscaleService,
|
||||||
|
ModelDecimationService modelDecimationService,
|
||||||
PairStateCache pairStateCache,
|
PairStateCache pairStateCache,
|
||||||
PairPerformanceMetricsCache pairPerformanceMetricsCache,
|
PairPerformanceMetricsCache pairPerformanceMetricsCache,
|
||||||
PenumbraTempCollectionJanitor tempCollectionJanitor)
|
PenumbraTempCollectionJanitor tempCollectionJanitor,
|
||||||
|
XivDataAnalyzer modelAnalyzer,
|
||||||
|
LightlessConfigService configService)
|
||||||
{
|
{
|
||||||
_loggerFactory = loggerFactory;
|
_loggerFactory = loggerFactory;
|
||||||
_mediator = mediator;
|
_mediator = mediator;
|
||||||
@@ -60,15 +73,20 @@ internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory
|
|||||||
_fileDownloadManagerFactory = fileDownloadManagerFactory;
|
_fileDownloadManagerFactory = fileDownloadManagerFactory;
|
||||||
_pluginWarningNotificationManager = pluginWarningNotificationManager;
|
_pluginWarningNotificationManager = pluginWarningNotificationManager;
|
||||||
_serviceProvider = serviceProvider;
|
_serviceProvider = serviceProvider;
|
||||||
|
_framework = framework;
|
||||||
_lifetime = lifetime;
|
_lifetime = lifetime;
|
||||||
_fileCacheManager = fileCacheManager;
|
_fileCacheManager = fileCacheManager;
|
||||||
|
_playerPerformanceConfigService = playerPerformanceConfigService;
|
||||||
_playerPerformanceService = playerPerformanceService;
|
_playerPerformanceService = playerPerformanceService;
|
||||||
_pairProcessingLimiter = pairProcessingLimiter;
|
_pairProcessingLimiter = pairProcessingLimiter;
|
||||||
_serverConfigManager = serverConfigManager;
|
_serverConfigManager = serverConfigManager;
|
||||||
_textureDownscaleService = textureDownscaleService;
|
_textureDownscaleService = textureDownscaleService;
|
||||||
|
_modelDecimationService = modelDecimationService;
|
||||||
_pairStateCache = pairStateCache;
|
_pairStateCache = pairStateCache;
|
||||||
_pairPerformanceMetricsCache = pairPerformanceMetricsCache;
|
_pairPerformanceMetricsCache = pairPerformanceMetricsCache;
|
||||||
_tempCollectionJanitor = tempCollectionJanitor;
|
_tempCollectionJanitor = tempCollectionJanitor;
|
||||||
|
_modelAnalyzer = modelAnalyzer;
|
||||||
|
_configService = configService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public IPairHandlerAdapter Create(string ident)
|
public IPairHandlerAdapter Create(string ident)
|
||||||
@@ -86,15 +104,20 @@ internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory
|
|||||||
downloadManager,
|
downloadManager,
|
||||||
_pluginWarningNotificationManager,
|
_pluginWarningNotificationManager,
|
||||||
dalamudUtilService,
|
dalamudUtilService,
|
||||||
|
_framework,
|
||||||
actorObjectService,
|
actorObjectService,
|
||||||
_lifetime,
|
_lifetime,
|
||||||
_fileCacheManager,
|
_fileCacheManager,
|
||||||
|
_playerPerformanceConfigService,
|
||||||
_playerPerformanceService,
|
_playerPerformanceService,
|
||||||
_pairProcessingLimiter,
|
_pairProcessingLimiter,
|
||||||
_serverConfigManager,
|
_serverConfigManager,
|
||||||
_textureDownscaleService,
|
_textureDownscaleService,
|
||||||
|
_modelDecimationService,
|
||||||
_pairStateCache,
|
_pairStateCache,
|
||||||
_pairPerformanceMetricsCache,
|
_pairPerformanceMetricsCache,
|
||||||
_tempCollectionJanitor);
|
_tempCollectionJanitor,
|
||||||
|
_modelAnalyzer,
|
||||||
|
_configService);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ public sealed class PairHandlerRegistry : IDisposable
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (handler.LastReceivedCharacterData is not null &&
|
if (handler.LastReceivedCharacterData is not null &&
|
||||||
(handler.LastAppliedApproximateVRAMBytes < 0 || handler.LastAppliedDataTris < 0))
|
(handler.LastAppliedApproximateVRAMBytes < 0 || handler.LastAppliedDataTris < 0 || handler.LastAppliedApproximateEffectiveTris < 0))
|
||||||
{
|
{
|
||||||
handler.ApplyLastReceivedData(forced: true);
|
handler.ApplyLastReceivedData(forced: true);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -258,7 +258,8 @@ public sealed class PairLedger : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
if (handler.LastAppliedApproximateVRAMBytes >= 0
|
if (handler.LastAppliedApproximateVRAMBytes >= 0
|
||||||
&& handler.LastAppliedDataTris >= 0
|
&& handler.LastAppliedDataTris >= 0
|
||||||
&& handler.LastAppliedApproximateEffectiveVRAMBytes >= 0)
|
&& handler.LastAppliedApproximateEffectiveVRAMBytes >= 0
|
||||||
|
&& handler.LastAppliedApproximateEffectiveTris >= 0)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ namespace LightlessSync.PlayerData.Pairs;
|
|||||||
public readonly record struct PairPerformanceMetrics(
|
public readonly record struct PairPerformanceMetrics(
|
||||||
long TriangleCount,
|
long TriangleCount,
|
||||||
long ApproximateVramBytes,
|
long ApproximateVramBytes,
|
||||||
long ApproximateEffectiveVramBytes);
|
long ApproximateEffectiveVramBytes,
|
||||||
|
long ApproximateEffectiveTris);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// caches performance metrics keyed by pair ident
|
/// caches performance metrics keyed by pair ident
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ public class VisibleUserDataDistributor : DisposableMediatorSubscriberBase
|
|||||||
});
|
});
|
||||||
|
|
||||||
Mediator.Subscribe<ConnectedMessage>(this, (_) => PushToAllVisibleUsers());
|
Mediator.Subscribe<ConnectedMessage>(this, (_) => PushToAllVisibleUsers());
|
||||||
|
Mediator.Subscribe<PairOnlineMessage>(this, (msg) => HandlePairOnline(msg.PairIdent));
|
||||||
Mediator.Subscribe<DisconnectedMessage>(this, (_) =>
|
Mediator.Subscribe<DisconnectedMessage>(this, (_) =>
|
||||||
{
|
{
|
||||||
_fileTransferManager.CancelUpload();
|
_fileTransferManager.CancelUpload();
|
||||||
@@ -111,6 +112,20 @@ public class VisibleUserDataDistributor : DisposableMediatorSubscriberBase
|
|||||||
_ = PushCharacterDataAsync(forced);
|
_ = PushCharacterDataAsync(forced);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void HandlePairOnline(PairUniqueIdentifier pairIdent)
|
||||||
|
{
|
||||||
|
if (!_apiController.IsConnected || !_pairLedger.IsPairVisible(pairIdent))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_pairLedger.GetHandler(pairIdent)?.UserData is { } user)
|
||||||
|
{
|
||||||
|
_usersToPushDataTo.Add(user);
|
||||||
|
PushCharacterData(forced: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async Task PushCharacterDataAsync(bool forced = false)
|
private async Task PushCharacterDataAsync(bool forced = false)
|
||||||
{
|
{
|
||||||
await _pushLock.WaitAsync(_runtimeCts.Token).ConfigureAwait(false);
|
await _pushLock.WaitAsync(_runtimeCts.Token).ConfigureAwait(false);
|
||||||
@@ -152,5 +167,6 @@ public class VisibleUserDataDistributor : DisposableMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<UserData> GetVisibleUsers() => [.. _pairLedger.GetVisiblePairs().Select(connection => connection.User)];
|
private List<UserData> GetVisibleUsers()
|
||||||
|
=> [.. _pairLedger.GetVisiblePairs().Where(connection => connection.IsOnline).Select(connection => connection.User)];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ using System.Reflection;
|
|||||||
using OtterTex;
|
using OtterTex;
|
||||||
using LightlessSync.Services.LightFinder;
|
using LightlessSync.Services.LightFinder;
|
||||||
using LightlessSync.Services.PairProcessing;
|
using LightlessSync.Services.PairProcessing;
|
||||||
|
using LightlessSync.Services.ModelDecimation;
|
||||||
using LightlessSync.UI.Models;
|
using LightlessSync.UI.Models;
|
||||||
|
|
||||||
namespace LightlessSync;
|
namespace LightlessSync;
|
||||||
@@ -105,6 +106,7 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
services.AddSingleton(new WindowSystem("LightlessSync"));
|
services.AddSingleton(new WindowSystem("LightlessSync"));
|
||||||
services.AddSingleton<FileDialogManager>();
|
services.AddSingleton<FileDialogManager>();
|
||||||
services.AddSingleton(new Dalamud.Localization("LightlessSync.Localization.", string.Empty, useEmbedded: true));
|
services.AddSingleton(new Dalamud.Localization("LightlessSync.Localization.", string.Empty, useEmbedded: true));
|
||||||
|
services.AddSingleton(framework);
|
||||||
services.AddSingleton(gameGui);
|
services.AddSingleton(gameGui);
|
||||||
services.AddSingleton(gameInteropProvider);
|
services.AddSingleton(gameInteropProvider);
|
||||||
services.AddSingleton(addonLifecycle);
|
services.AddSingleton(addonLifecycle);
|
||||||
@@ -125,6 +127,7 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
services.AddSingleton<LightlessProfileManager>();
|
services.AddSingleton<LightlessProfileManager>();
|
||||||
services.AddSingleton<TextureCompressionService>();
|
services.AddSingleton<TextureCompressionService>();
|
||||||
services.AddSingleton<TextureDownscaleService>();
|
services.AddSingleton<TextureDownscaleService>();
|
||||||
|
services.AddSingleton<ModelDecimationService>();
|
||||||
services.AddSingleton<GameObjectHandlerFactory>();
|
services.AddSingleton<GameObjectHandlerFactory>();
|
||||||
services.AddSingleton<FileDownloadManagerFactory>();
|
services.AddSingleton<FileDownloadManagerFactory>();
|
||||||
services.AddSingleton<PairProcessingLimiter>();
|
services.AddSingleton<PairProcessingLimiter>();
|
||||||
@@ -177,7 +180,8 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
|
|
||||||
services.AddSingleton(sp => new BlockedCharacterHandler(
|
services.AddSingleton(sp => new BlockedCharacterHandler(
|
||||||
sp.GetRequiredService<ILogger<BlockedCharacterHandler>>(),
|
sp.GetRequiredService<ILogger<BlockedCharacterHandler>>(),
|
||||||
gameInteropProvider));
|
gameInteropProvider,
|
||||||
|
objectTable));
|
||||||
|
|
||||||
services.AddSingleton(sp => new IpcProvider(
|
services.AddSingleton(sp => new IpcProvider(
|
||||||
sp.GetRequiredService<ILogger<IpcProvider>>(),
|
sp.GetRequiredService<ILogger<IpcProvider>>(),
|
||||||
@@ -373,6 +377,11 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
sp.GetRequiredService<DalamudUtilService>(),
|
sp.GetRequiredService<DalamudUtilService>(),
|
||||||
sp.GetRequiredService<LightlessMediator>()));
|
sp.GetRequiredService<LightlessMediator>()));
|
||||||
|
|
||||||
|
services.AddSingleton(sp => new IpcCallerLifestream(
|
||||||
|
pluginInterface,
|
||||||
|
sp.GetRequiredService<LightlessMediator>(),
|
||||||
|
sp.GetRequiredService<ILogger<IpcCallerLifestream>>()));
|
||||||
|
|
||||||
services.AddSingleton(sp => new IpcManager(
|
services.AddSingleton(sp => new IpcManager(
|
||||||
sp.GetRequiredService<ILogger<IpcManager>>(),
|
sp.GetRequiredService<ILogger<IpcManager>>(),
|
||||||
sp.GetRequiredService<LightlessMediator>(),
|
sp.GetRequiredService<LightlessMediator>(),
|
||||||
@@ -383,7 +392,9 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
sp.GetRequiredService<IpcCallerHonorific>(),
|
sp.GetRequiredService<IpcCallerHonorific>(),
|
||||||
sp.GetRequiredService<IpcCallerMoodles>(),
|
sp.GetRequiredService<IpcCallerMoodles>(),
|
||||||
sp.GetRequiredService<IpcCallerPetNames>(),
|
sp.GetRequiredService<IpcCallerPetNames>(),
|
||||||
sp.GetRequiredService<IpcCallerBrio>()));
|
sp.GetRequiredService<IpcCallerBrio>(),
|
||||||
|
sp.GetRequiredService<IpcCallerLifestream>()
|
||||||
|
));
|
||||||
|
|
||||||
// Notifications / HTTP
|
// Notifications / HTTP
|
||||||
services.AddSingleton(sp => new NotificationService(
|
services.AddSingleton(sp => new NotificationService(
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ using FFXIVClientStructs.Interop;
|
|||||||
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
||||||
using FFXIVClientStructs.FFXIV.Client.Game.Object;
|
using FFXIVClientStructs.FFXIV.Client.Game.Object;
|
||||||
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
||||||
|
using LightlessSync.PlayerData.Handlers;
|
||||||
using LightlessSync.Services.Mediator;
|
using LightlessSync.Services.Mediator;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@@ -16,7 +17,7 @@ using LightlessObjectKind = LightlessSync.API.Data.Enum.ObjectKind;
|
|||||||
|
|
||||||
namespace LightlessSync.Services.ActorTracking;
|
namespace LightlessSync.Services.ActorTracking;
|
||||||
|
|
||||||
public sealed class ActorObjectService : IHostedService, IDisposable
|
public sealed class ActorObjectService : IHostedService, IDisposable, IMediatorSubscriber
|
||||||
{
|
{
|
||||||
public readonly record struct ActorDescriptor(
|
public readonly record struct ActorDescriptor(
|
||||||
string Name,
|
string Name,
|
||||||
@@ -36,6 +37,8 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
|||||||
private readonly IClientState _clientState;
|
private readonly IClientState _clientState;
|
||||||
private readonly ICondition _condition;
|
private readonly ICondition _condition;
|
||||||
private readonly LightlessMediator _mediator;
|
private readonly LightlessMediator _mediator;
|
||||||
|
private readonly object _playerRelatedHandlerLock = new();
|
||||||
|
private readonly HashSet<GameObjectHandler> _playerRelatedHandlers = [];
|
||||||
|
|
||||||
private readonly ConcurrentDictionary<nint, ActorDescriptor> _activePlayers = new();
|
private readonly ConcurrentDictionary<nint, ActorDescriptor> _activePlayers = new();
|
||||||
private readonly ConcurrentDictionary<nint, ActorDescriptor> _gposePlayers = new();
|
private readonly ConcurrentDictionary<nint, ActorDescriptor> _gposePlayers = new();
|
||||||
@@ -71,6 +74,25 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
|||||||
_clientState = clientState;
|
_clientState = clientState;
|
||||||
_condition = condition;
|
_condition = condition;
|
||||||
_mediator = mediator;
|
_mediator = mediator;
|
||||||
|
|
||||||
|
_mediator.Subscribe<GameObjectHandlerCreatedMessage>(this, (msg) =>
|
||||||
|
{
|
||||||
|
if (!msg.OwnedObject) return;
|
||||||
|
lock (_playerRelatedHandlerLock)
|
||||||
|
{
|
||||||
|
_playerRelatedHandlers.Add(msg.GameObjectHandler);
|
||||||
|
}
|
||||||
|
RefreshTrackedActors(force: true);
|
||||||
|
});
|
||||||
|
_mediator.Subscribe<GameObjectHandlerDestroyedMessage>(this, (msg) =>
|
||||||
|
{
|
||||||
|
if (!msg.OwnedObject) return;
|
||||||
|
lock (_playerRelatedHandlerLock)
|
||||||
|
{
|
||||||
|
_playerRelatedHandlers.Remove(msg.GameObjectHandler);
|
||||||
|
}
|
||||||
|
RefreshTrackedActors(force: true);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool IsZoning => _condition[ConditionFlag.BetweenAreas] || _condition[ConditionFlag.BetweenAreas51];
|
private bool IsZoning => _condition[ConditionFlag.BetweenAreas] || _condition[ConditionFlag.BetweenAreas51];
|
||||||
@@ -84,6 +106,7 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
|||||||
public IReadOnlyList<ActorDescriptor> PlayerDescriptors => Snapshot.PlayerDescriptors;
|
public IReadOnlyList<ActorDescriptor> PlayerDescriptors => Snapshot.PlayerDescriptors;
|
||||||
public IReadOnlyList<ActorDescriptor> OwnedDescriptors => Snapshot.OwnedDescriptors;
|
public IReadOnlyList<ActorDescriptor> OwnedDescriptors => Snapshot.OwnedDescriptors;
|
||||||
public IReadOnlyList<ActorDescriptor> GposeDescriptors => CurrentGposeSnapshot.GposeDescriptors;
|
public IReadOnlyList<ActorDescriptor> GposeDescriptors => CurrentGposeSnapshot.GposeDescriptors;
|
||||||
|
public LightlessMediator Mediator => _mediator;
|
||||||
|
|
||||||
public bool TryGetActorByHash(string hash, out ActorDescriptor descriptor) => _actorsByHash.TryGetValue(hash, out descriptor);
|
public bool TryGetActorByHash(string hash, out ActorDescriptor descriptor) => _actorsByHash.TryGetValue(hash, out descriptor);
|
||||||
public bool TryGetValidatedActorByHash(string hash, out ActorDescriptor descriptor)
|
public bool TryGetValidatedActorByHash(string hash, out ActorDescriptor descriptor)
|
||||||
@@ -213,18 +236,25 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task WaitForFullyLoadedAsync(nint address, CancellationToken cancellationToken = default)
|
public async Task<bool> WaitForFullyLoadedAsync(nint address, CancellationToken cancellationToken = default, int timeOutMs = 30000)
|
||||||
{
|
{
|
||||||
if (address == nint.Zero)
|
if (address == nint.Zero)
|
||||||
throw new ArgumentException("Address cannot be zero.", nameof(address));
|
throw new ArgumentException("Address cannot be zero.", nameof(address));
|
||||||
|
|
||||||
|
var timeoutAt = timeOutMs > 0 ? Environment.TickCount64 + timeOutMs : long.MaxValue;
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
var isLoaded = await _framework.RunOnFrameworkThread(() => IsObjectFullyLoaded(address)).ConfigureAwait(false);
|
var loadState = await _framework.RunOnFrameworkThread(() => GetObjectLoadState(address)).ConfigureAwait(false);
|
||||||
if (!IsZoning && isLoaded)
|
if (!loadState.IsValid)
|
||||||
return;
|
return false;
|
||||||
|
|
||||||
|
if (!IsZoning && loadState.IsLoaded)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (Environment.TickCount64 >= timeoutAt)
|
||||||
|
return false;
|
||||||
|
|
||||||
await Task.Delay(100, cancellationToken).ConfigureAwait(false);
|
await Task.Delay(100, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
@@ -317,6 +347,11 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
|||||||
_actorsByHash.Clear();
|
_actorsByHash.Clear();
|
||||||
_actorsByName.Clear();
|
_actorsByName.Clear();
|
||||||
_pendingHashResolutions.Clear();
|
_pendingHashResolutions.Clear();
|
||||||
|
_mediator.UnsubscribeAll(this);
|
||||||
|
lock (_playerRelatedHandlerLock)
|
||||||
|
{
|
||||||
|
_playerRelatedHandlers.Clear();
|
||||||
|
}
|
||||||
Volatile.Write(ref _snapshot, ActorSnapshot.Empty);
|
Volatile.Write(ref _snapshot, ActorSnapshot.Empty);
|
||||||
Volatile.Write(ref _gposeSnapshot, GposeSnapshot.Empty);
|
Volatile.Write(ref _gposeSnapshot, GposeSnapshot.Empty);
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
@@ -493,7 +528,9 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
|||||||
if (objectKind is DalamudObjectKind.MountType or DalamudObjectKind.Companion)
|
if (objectKind is DalamudObjectKind.MountType or DalamudObjectKind.Companion)
|
||||||
{
|
{
|
||||||
var expectedMinionOrMount = GetMinionOrMountAddress(localPlayerAddress, localEntityId);
|
var expectedMinionOrMount = GetMinionOrMountAddress(localPlayerAddress, localEntityId);
|
||||||
if (expectedMinionOrMount != nint.Zero && (nint)gameObject == expectedMinionOrMount)
|
if (expectedMinionOrMount != nint.Zero
|
||||||
|
&& (nint)gameObject == expectedMinionOrMount
|
||||||
|
&& IsPlayerRelatedOwnedAddress(expectedMinionOrMount, LightlessObjectKind.MinionOrMount))
|
||||||
{
|
{
|
||||||
var resolvedOwner = ownerId != 0 ? ownerId : localEntityId;
|
var resolvedOwner = ownerId != 0 ? ownerId : localEntityId;
|
||||||
return (LightlessObjectKind.MinionOrMount, resolvedOwner);
|
return (LightlessObjectKind.MinionOrMount, resolvedOwner);
|
||||||
@@ -507,16 +544,37 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
|||||||
return (null, ownerId);
|
return (null, ownerId);
|
||||||
|
|
||||||
var expectedPet = GetPetAddress(localPlayerAddress, localEntityId);
|
var expectedPet = GetPetAddress(localPlayerAddress, localEntityId);
|
||||||
if (expectedPet != nint.Zero && (nint)gameObject == expectedPet)
|
if (expectedPet != nint.Zero
|
||||||
|
&& (nint)gameObject == expectedPet
|
||||||
|
&& IsPlayerRelatedOwnedAddress(expectedPet, LightlessObjectKind.Pet))
|
||||||
return (LightlessObjectKind.Pet, ownerId);
|
return (LightlessObjectKind.Pet, ownerId);
|
||||||
|
|
||||||
var expectedCompanion = GetCompanionAddress(localPlayerAddress, localEntityId);
|
var expectedCompanion = GetCompanionAddress(localPlayerAddress, localEntityId);
|
||||||
if (expectedCompanion != nint.Zero && (nint)gameObject == expectedCompanion)
|
if (expectedCompanion != nint.Zero
|
||||||
|
&& (nint)gameObject == expectedCompanion
|
||||||
|
&& IsPlayerRelatedOwnedAddress(expectedCompanion, LightlessObjectKind.Companion))
|
||||||
return (LightlessObjectKind.Companion, ownerId);
|
return (LightlessObjectKind.Companion, ownerId);
|
||||||
|
|
||||||
return (null, ownerId);
|
return (null, ownerId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool IsPlayerRelatedOwnedAddress(nint address, LightlessObjectKind expectedKind)
|
||||||
|
{
|
||||||
|
if (address == nint.Zero)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
lock (_playerRelatedHandlerLock)
|
||||||
|
{
|
||||||
|
foreach (var handler in _playerRelatedHandlers)
|
||||||
|
{
|
||||||
|
if (handler.Address == address && handler.ObjectKind == expectedKind)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
private unsafe nint GetMinionOrMountAddress(nint localPlayerAddress, uint ownerEntityId)
|
private unsafe nint GetMinionOrMountAddress(nint localPlayerAddress, uint ownerEntityId)
|
||||||
{
|
{
|
||||||
if (localPlayerAddress == nint.Zero)
|
if (localPlayerAddress == nint.Zero)
|
||||||
@@ -524,20 +582,20 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
|||||||
|
|
||||||
var playerObject = (GameObject*)localPlayerAddress;
|
var playerObject = (GameObject*)localPlayerAddress;
|
||||||
var candidateAddress = _objectTable.GetObjectAddress(playerObject->ObjectIndex + 1);
|
var candidateAddress = _objectTable.GetObjectAddress(playerObject->ObjectIndex + 1);
|
||||||
|
if (ownerEntityId == 0)
|
||||||
|
return nint.Zero;
|
||||||
|
|
||||||
if (candidateAddress != nint.Zero)
|
if (candidateAddress != nint.Zero)
|
||||||
{
|
{
|
||||||
var candidate = (GameObject*)candidateAddress;
|
var candidate = (GameObject*)candidateAddress;
|
||||||
var candidateKind = (DalamudObjectKind)candidate->ObjectKind;
|
var candidateKind = (DalamudObjectKind)candidate->ObjectKind;
|
||||||
if (candidateKind is DalamudObjectKind.MountType or DalamudObjectKind.Companion)
|
if (candidateKind is DalamudObjectKind.MountType or DalamudObjectKind.Companion)
|
||||||
{
|
{
|
||||||
if (ownerEntityId == 0 || ResolveOwnerId(candidate) == ownerEntityId)
|
if (ResolveOwnerId(candidate) == ownerEntityId)
|
||||||
return candidateAddress;
|
return candidateAddress;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ownerEntityId == 0)
|
|
||||||
return candidateAddress;
|
|
||||||
|
|
||||||
foreach (var obj in _objectTable)
|
foreach (var obj in _objectTable)
|
||||||
{
|
{
|
||||||
if (obj is null || obj.Address == nint.Zero || obj.Address == localPlayerAddress)
|
if (obj is null || obj.Address == nint.Zero || obj.Address == localPlayerAddress)
|
||||||
@@ -551,7 +609,7 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
|||||||
return obj.Address;
|
return obj.Address;
|
||||||
}
|
}
|
||||||
|
|
||||||
return candidateAddress;
|
return nint.Zero;
|
||||||
}
|
}
|
||||||
|
|
||||||
private unsafe nint GetPetAddress(nint localPlayerAddress, uint ownerEntityId)
|
private unsafe nint GetPetAddress(nint localPlayerAddress, uint ownerEntityId)
|
||||||
@@ -1022,6 +1080,7 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
|||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
DisposeHooks();
|
DisposeHooks();
|
||||||
|
_mediator.UnsubscribeAll(this);
|
||||||
GC.SuppressFinalize(this);
|
GC.SuppressFinalize(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1143,6 +1202,18 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
|||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private LoadState GetObjectLoadState(nint address)
|
||||||
|
{
|
||||||
|
if (address == nint.Zero)
|
||||||
|
return LoadState.Invalid;
|
||||||
|
|
||||||
|
var obj = _objectTable.CreateObjectReference(address);
|
||||||
|
if (obj is null || obj.Address != address)
|
||||||
|
return LoadState.Invalid;
|
||||||
|
|
||||||
|
return new LoadState(true, IsObjectFullyLoaded(address));
|
||||||
|
}
|
||||||
|
|
||||||
private static unsafe bool IsObjectFullyLoaded(nint address)
|
private static unsafe bool IsObjectFullyLoaded(nint address)
|
||||||
{
|
{
|
||||||
if (address == nint.Zero)
|
if (address == nint.Zero)
|
||||||
@@ -1169,6 +1240,11 @@ public sealed class ActorObjectService : IHostedService, IDisposable
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private readonly record struct LoadState(bool IsValid, bool IsLoaded)
|
||||||
|
{
|
||||||
|
public static LoadState Invalid => new(false, false);
|
||||||
|
}
|
||||||
|
|
||||||
private sealed record OwnedObjectSnapshot(
|
private sealed record OwnedObjectSnapshot(
|
||||||
IReadOnlyList<nint> RenderedPlayers,
|
IReadOnlyList<nint> RenderedPlayers,
|
||||||
IReadOnlyList<nint> RenderedCompanions,
|
IReadOnlyList<nint> RenderedCompanions,
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable
|
|||||||
{
|
{
|
||||||
_baseAnalysisCts = _baseAnalysisCts.CancelRecreate();
|
_baseAnalysisCts = _baseAnalysisCts.CancelRecreate();
|
||||||
var token = _baseAnalysisCts.Token;
|
var token = _baseAnalysisCts.Token;
|
||||||
_ = BaseAnalysis(msg.CharacterData, token);
|
_ = Task.Run(async () => await BaseAnalysis(msg.CharacterData, token).ConfigureAwait(false), token);
|
||||||
});
|
});
|
||||||
_fileCacheManager = fileCacheManager;
|
_fileCacheManager = fileCacheManager;
|
||||||
_xivDataAnalyzer = modelAnalyzer;
|
_xivDataAnalyzer = modelAnalyzer;
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -843,31 +845,41 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
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 initialSettle = 50;
|
||||||
|
|
||||||
|
var sw = Stopwatch.StartNew();
|
||||||
|
|
||||||
const int tick = 250;
|
|
||||||
int curWaitTime = 0;
|
|
||||||
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 (OperationCanceledException)
|
||||||
|
{
|
||||||
|
// ignore
|
||||||
}
|
}
|
||||||
catch (AccessViolationException ex)
|
catch (AccessViolationException ex)
|
||||||
{
|
{
|
||||||
@@ -1032,7 +1044,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
if (actor.ObjectIndex >= 200)
|
if (actor.ObjectIndex >= 200)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (_blockedCharacterHandler.IsCharacterBlocked(playerAddress, 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"));
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ public record HubClosedMessage(Exception? Exception) : SameThreadMessage;
|
|||||||
public record ResumeScanMessage(string Source) : MessageBase;
|
public record ResumeScanMessage(string Source) : MessageBase;
|
||||||
public record FileCacheInitializedMessage : MessageBase;
|
public record FileCacheInitializedMessage : MessageBase;
|
||||||
public record DownloadReadyMessage(Guid RequestId) : MessageBase;
|
public record DownloadReadyMessage(Guid RequestId) : MessageBase;
|
||||||
public record DownloadStartedMessage(GameObjectHandler DownloadId, Dictionary<string, FileDownloadStatus> DownloadStatus) : MessageBase;
|
public record DownloadStartedMessage(GameObjectHandler DownloadId, IReadOnlyDictionary<string, FileDownloadStatus> DownloadStatus) : MessageBase;
|
||||||
public record DownloadFinishedMessage(GameObjectHandler DownloadId) : MessageBase;
|
public record DownloadFinishedMessage(GameObjectHandler DownloadId) : MessageBase;
|
||||||
public record UiToggleMessage(Type UiType) : MessageBase;
|
public record UiToggleMessage(Type UiType) : MessageBase;
|
||||||
public record PlayerUploadingMessage(GameObjectHandler Handler, bool IsUploading) : MessageBase;
|
public record PlayerUploadingMessage(GameObjectHandler Handler, bool IsUploading) : MessageBase;
|
||||||
@@ -104,6 +104,7 @@ public record PairUiUpdatedMessage(PairUiSnapshot Snapshot) : MessageBase;
|
|||||||
public record CensusUpdateMessage(byte Gender, byte RaceId, byte TribeId) : MessageBase;
|
public record CensusUpdateMessage(byte Gender, byte RaceId, byte TribeId) : MessageBase;
|
||||||
public record TargetPairMessage(Pair Pair) : MessageBase;
|
public record TargetPairMessage(Pair Pair) : MessageBase;
|
||||||
public record PairFocusCharacterMessage(Pair Pair) : SameThreadMessage;
|
public record PairFocusCharacterMessage(Pair Pair) : SameThreadMessage;
|
||||||
|
public record PairOnlineMessage(PairUniqueIdentifier PairIdent) : MessageBase;
|
||||||
public record CombatStartMessage : MessageBase;
|
public record CombatStartMessage : MessageBase;
|
||||||
public record CombatEndMessage : MessageBase;
|
public record CombatEndMessage : MessageBase;
|
||||||
public record PerformanceStartMessage : MessageBase;
|
public record PerformanceStartMessage : MessageBase;
|
||||||
@@ -138,4 +139,4 @@ 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;
|
||||||
#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
|
||||||
|
|||||||
1462
LightlessSync/Services/ModelDecimation/MdlDecimator.cs
Normal file
1462
LightlessSync/Services/ModelDecimation/MdlDecimator.cs
Normal file
File diff suppressed because it is too large
Load Diff
381
LightlessSync/Services/ModelDecimation/ModelDecimationService.cs
Normal file
381
LightlessSync/Services/ModelDecimation/ModelDecimationService.cs
Normal file
@@ -0,0 +1,381 @@
|
|||||||
|
using LightlessSync.FileCache;
|
||||||
|
using LightlessSync.LightlessConfiguration;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Globalization;
|
||||||
|
|
||||||
|
namespace LightlessSync.Services.ModelDecimation;
|
||||||
|
|
||||||
|
public sealed class ModelDecimationService
|
||||||
|
{
|
||||||
|
private const int MaxConcurrentJobs = 1;
|
||||||
|
private const double MinTargetRatio = 0.01;
|
||||||
|
private const double MaxTargetRatio = 0.99;
|
||||||
|
|
||||||
|
private readonly ILogger<ModelDecimationService> _logger;
|
||||||
|
private readonly LightlessConfigService _configService;
|
||||||
|
private readonly FileCacheManager _fileCacheManager;
|
||||||
|
private readonly PlayerPerformanceConfigService _performanceConfigService;
|
||||||
|
private readonly XivDataStorageService _xivDataStorageService;
|
||||||
|
private readonly SemaphoreSlim _decimationSemaphore = new(MaxConcurrentJobs);
|
||||||
|
|
||||||
|
private readonly ConcurrentDictionary<string, Task> _activeJobs = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
private readonly ConcurrentDictionary<string, string> _decimatedPaths = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
private readonly ConcurrentDictionary<string, byte> _failedHashes = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
public ModelDecimationService(
|
||||||
|
ILogger<ModelDecimationService> logger,
|
||||||
|
LightlessConfigService configService,
|
||||||
|
FileCacheManager fileCacheManager,
|
||||||
|
PlayerPerformanceConfigService performanceConfigService,
|
||||||
|
XivDataStorageService xivDataStorageService)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_configService = configService;
|
||||||
|
_fileCacheManager = fileCacheManager;
|
||||||
|
_performanceConfigService = performanceConfigService;
|
||||||
|
_xivDataStorageService = xivDataStorageService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ScheduleDecimation(string hash, string filePath, string? gamePath = null)
|
||||||
|
{
|
||||||
|
if (!ShouldScheduleDecimation(hash, filePath, gamePath))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_decimatedPaths.ContainsKey(hash) || _failedHashes.ContainsKey(hash) || _activeJobs.ContainsKey(hash))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("Queued model decimation for {Hash}", hash);
|
||||||
|
|
||||||
|
_activeJobs[hash] = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
await _decimationSemaphore.WaitAsync().ConfigureAwait(false);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await DecimateInternalAsync(hash, filePath).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_failedHashes[hash] = 1;
|
||||||
|
_logger.LogWarning(ex, "Model decimation failed for {Hash}", hash);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_decimationSemaphore.Release();
|
||||||
|
_activeJobs.TryRemove(hash, out _);
|
||||||
|
}
|
||||||
|
}, CancellationToken.None);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool ShouldScheduleDecimation(string hash, string filePath, string? gamePath = null)
|
||||||
|
=> IsDecimationEnabled()
|
||||||
|
&& filePath.EndsWith(".mdl", StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& IsDecimationAllowed(gamePath)
|
||||||
|
&& !ShouldSkipByTriangleCache(hash);
|
||||||
|
|
||||||
|
public string GetPreferredPath(string hash, string originalPath)
|
||||||
|
{
|
||||||
|
if (!IsDecimationEnabled())
|
||||||
|
{
|
||||||
|
return originalPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_decimatedPaths.TryGetValue(hash, out var existing) && File.Exists(existing))
|
||||||
|
{
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
|
||||||
|
var resolved = GetExistingDecimatedPath(hash);
|
||||||
|
if (!string.IsNullOrEmpty(resolved))
|
||||||
|
{
|
||||||
|
_decimatedPaths[hash] = resolved;
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
|
||||||
|
return originalPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task WaitForPendingJobsAsync(IEnumerable<string>? hashes, CancellationToken token)
|
||||||
|
{
|
||||||
|
if (hashes is null)
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
var pending = new List<Task>();
|
||||||
|
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
foreach (var hash in hashes)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(hash) || !seen.Add(hash))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_activeJobs.TryGetValue(hash, out var job))
|
||||||
|
{
|
||||||
|
pending.Add(job);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pending.Count == 0)
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.WhenAll(pending).WaitAsync(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task DecimateInternalAsync(string hash, string sourcePath)
|
||||||
|
{
|
||||||
|
if (!File.Exists(sourcePath))
|
||||||
|
{
|
||||||
|
_failedHashes[hash] = 1;
|
||||||
|
_logger.LogWarning("Cannot decimate model {Hash}; source path missing: {Path}", hash, sourcePath);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryGetDecimationSettings(out var triangleThreshold, out var targetRatio))
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Model decimation disabled or invalid settings for {Hash}", hash);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("Starting model decimation for {Hash} (threshold {Threshold}, ratio {Ratio:0.##})", hash, triangleThreshold, targetRatio);
|
||||||
|
|
||||||
|
var destination = Path.Combine(GetDecimatedDirectory(), $"{hash}.mdl");
|
||||||
|
if (File.Exists(destination))
|
||||||
|
{
|
||||||
|
RegisterDecimatedModel(hash, sourcePath, destination);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!MdlDecimator.TryDecimate(sourcePath, destination, triangleThreshold, targetRatio, _logger))
|
||||||
|
{
|
||||||
|
_failedHashes[hash] = 1;
|
||||||
|
_logger.LogInformation("Model decimation skipped for {Hash}", hash);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
RegisterDecimatedModel(hash, sourcePath, destination);
|
||||||
|
_logger.LogInformation("Decimated model {Hash} -> {Path}", hash, destination);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RegisterDecimatedModel(string hash, string sourcePath, string destination)
|
||||||
|
{
|
||||||
|
_decimatedPaths[hash] = destination;
|
||||||
|
|
||||||
|
var performanceConfig = _performanceConfigService.Current;
|
||||||
|
if (performanceConfig.KeepOriginalModelFiles)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.Equals(sourcePath, destination, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryReplaceCacheEntryWithDecimated(hash, sourcePath, destination))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
TryDelete(sourcePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryReplaceCacheEntryWithDecimated(string hash, string sourcePath, string destination)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var cacheEntry = _fileCacheManager.GetFileCacheByHash(hash);
|
||||||
|
if (cacheEntry is null || !cacheEntry.IsCacheEntry)
|
||||||
|
{
|
||||||
|
return File.Exists(sourcePath) ? false : true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var cacheFolder = _configService.Current.CacheFolder;
|
||||||
|
if (string.IsNullOrEmpty(cacheFolder))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!destination.StartsWith(cacheFolder, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var info = new FileInfo(destination);
|
||||||
|
if (!info.Exists)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var relative = Path.GetRelativePath(cacheFolder, destination)
|
||||||
|
.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
|
||||||
|
var sanitizedRelative = relative.TrimStart(Path.DirectorySeparatorChar);
|
||||||
|
var prefixed = Path.Combine(FileCacheManager.CachePrefix, sanitizedRelative);
|
||||||
|
|
||||||
|
var replacement = new FileCacheEntity(
|
||||||
|
hash,
|
||||||
|
prefixed,
|
||||||
|
info.LastWriteTimeUtc.Ticks.ToString(CultureInfo.InvariantCulture),
|
||||||
|
info.Length,
|
||||||
|
cacheEntry.CompressedSize);
|
||||||
|
replacement.SetResolvedFilePath(destination);
|
||||||
|
|
||||||
|
if (!string.Equals(cacheEntry.PrefixedFilePath, prefixed, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
_fileCacheManager.RemoveHashedFile(cacheEntry.Hash, cacheEntry.PrefixedFilePath, removeDerivedFiles: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
_fileCacheManager.UpdateHashedFile(replacement, computeProperties: false);
|
||||||
|
_fileCacheManager.WriteOutFullCsv();
|
||||||
|
|
||||||
|
_logger.LogTrace("Replaced cache entry for model {Hash} to decimated path {Path}", hash, destination);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogTrace(ex, "Failed to replace cache entry for model {Hash}", hash);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsDecimationEnabled()
|
||||||
|
=> _performanceConfigService.Current.EnableModelDecimation;
|
||||||
|
|
||||||
|
private bool ShouldSkipByTriangleCache(string hash)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(hash))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_xivDataStorageService.Current.TriangleDictionary.TryGetValue(hash, out var cachedTris) || cachedTris <= 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var threshold = Math.Max(0, _performanceConfigService.Current.ModelDecimationTriangleThreshold);
|
||||||
|
return threshold > 0 && cachedTris < threshold;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsDecimationAllowed(string? gamePath)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(gamePath))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var normalized = NormalizeGamePath(gamePath);
|
||||||
|
if (normalized.Contains("/hair/", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalized.Contains("/chara/equipment/", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return _performanceConfigService.Current.ModelDecimationAllowClothing;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalized.Contains("/chara/accessory/", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return _performanceConfigService.Current.ModelDecimationAllowAccessories;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalized.Contains("/chara/human/", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
if (normalized.Contains("/body/", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return _performanceConfigService.Current.ModelDecimationAllowBody;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalized.Contains("/face/", StringComparison.Ordinal) || normalized.Contains("/head/", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return _performanceConfigService.Current.ModelDecimationAllowFaceHead;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalized.Contains("/tail/", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return _performanceConfigService.Current.ModelDecimationAllowTail;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string NormalizeGamePath(string path)
|
||||||
|
=> path.Replace('\\', '/').ToLowerInvariant();
|
||||||
|
|
||||||
|
private bool TryGetDecimationSettings(out int triangleThreshold, out double targetRatio)
|
||||||
|
{
|
||||||
|
triangleThreshold = 15_000;
|
||||||
|
targetRatio = 0.8;
|
||||||
|
|
||||||
|
var config = _performanceConfigService.Current;
|
||||||
|
if (!config.EnableModelDecimation)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
triangleThreshold = Math.Max(0, config.ModelDecimationTriangleThreshold);
|
||||||
|
targetRatio = config.ModelDecimationTargetRatio;
|
||||||
|
if (double.IsNaN(targetRatio) || double.IsInfinity(targetRatio))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
targetRatio = Math.Clamp(targetRatio, MinTargetRatio, MaxTargetRatio);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string? GetExistingDecimatedPath(string hash)
|
||||||
|
{
|
||||||
|
var candidate = Path.Combine(GetDecimatedDirectory(), $"{hash}.mdl");
|
||||||
|
return File.Exists(candidate) ? candidate : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetDecimatedDirectory()
|
||||||
|
{
|
||||||
|
var directory = Path.Combine(_configService.Current.CacheFolder, "decimated");
|
||||||
|
if (!Directory.Exists(directory))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(directory);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogTrace(ex, "Failed to create decimated directory {Directory}", directory);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return directory;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void TryDelete(string? path)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(path))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (File.Exists(path))
|
||||||
|
{
|
||||||
|
File.Delete(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignored
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ using LightlessSync.LightlessConfiguration;
|
|||||||
using LightlessSync.PlayerData.Pairs;
|
using LightlessSync.PlayerData.Pairs;
|
||||||
using LightlessSync.Services.Events;
|
using LightlessSync.Services.Events;
|
||||||
using LightlessSync.Services.Mediator;
|
using LightlessSync.Services.Mediator;
|
||||||
|
using LightlessSync.Services.ModelDecimation;
|
||||||
using LightlessSync.Services.TextureCompression;
|
using LightlessSync.Services.TextureCompression;
|
||||||
using LightlessSync.UI;
|
using LightlessSync.UI;
|
||||||
using LightlessSync.WebAPI.Files.Models;
|
using LightlessSync.WebAPI.Files.Models;
|
||||||
@@ -18,12 +19,14 @@ public class PlayerPerformanceService
|
|||||||
private readonly ILogger<PlayerPerformanceService> _logger;
|
private readonly ILogger<PlayerPerformanceService> _logger;
|
||||||
private readonly LightlessMediator _mediator;
|
private readonly LightlessMediator _mediator;
|
||||||
private readonly PlayerPerformanceConfigService _playerPerformanceConfigService;
|
private readonly PlayerPerformanceConfigService _playerPerformanceConfigService;
|
||||||
|
private readonly ModelDecimationService _modelDecimationService;
|
||||||
private readonly TextureDownscaleService _textureDownscaleService;
|
private readonly TextureDownscaleService _textureDownscaleService;
|
||||||
private readonly Dictionary<string, bool> _warnedForPlayers = new(StringComparer.Ordinal);
|
private readonly Dictionary<string, bool> _warnedForPlayers = new(StringComparer.Ordinal);
|
||||||
|
|
||||||
public PlayerPerformanceService(ILogger<PlayerPerformanceService> logger, LightlessMediator mediator,
|
public PlayerPerformanceService(ILogger<PlayerPerformanceService> logger, LightlessMediator mediator,
|
||||||
PlayerPerformanceConfigService playerPerformanceConfigService, FileCacheManager fileCacheManager,
|
PlayerPerformanceConfigService playerPerformanceConfigService, FileCacheManager fileCacheManager,
|
||||||
XivDataAnalyzer xivDataAnalyzer, TextureDownscaleService textureDownscaleService)
|
XivDataAnalyzer xivDataAnalyzer, TextureDownscaleService textureDownscaleService,
|
||||||
|
ModelDecimationService modelDecimationService)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_mediator = mediator;
|
_mediator = mediator;
|
||||||
@@ -31,6 +34,7 @@ public class PlayerPerformanceService
|
|||||||
_fileCacheManager = fileCacheManager;
|
_fileCacheManager = fileCacheManager;
|
||||||
_xivDataAnalyzer = xivDataAnalyzer;
|
_xivDataAnalyzer = xivDataAnalyzer;
|
||||||
_textureDownscaleService = textureDownscaleService;
|
_textureDownscaleService = textureDownscaleService;
|
||||||
|
_modelDecimationService = modelDecimationService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> CheckBothThresholds(IPairPerformanceSubject pairHandler, CharacterData charaData)
|
public async Task<bool> CheckBothThresholds(IPairPerformanceSubject pairHandler, CharacterData charaData)
|
||||||
@@ -111,10 +115,12 @@ public class PlayerPerformanceService
|
|||||||
var config = _playerPerformanceConfigService.Current;
|
var config = _playerPerformanceConfigService.Current;
|
||||||
|
|
||||||
long triUsage = 0;
|
long triUsage = 0;
|
||||||
|
long effectiveTriUsage = 0;
|
||||||
|
|
||||||
if (!charaData.FileReplacements.TryGetValue(API.Data.Enum.ObjectKind.Player, out List<FileReplacementData>? playerReplacements))
|
if (!charaData.FileReplacements.TryGetValue(API.Data.Enum.ObjectKind.Player, out List<FileReplacementData>? playerReplacements))
|
||||||
{
|
{
|
||||||
pairHandler.LastAppliedDataTris = 0;
|
pairHandler.LastAppliedDataTris = 0;
|
||||||
|
pairHandler.LastAppliedApproximateEffectiveTris = 0;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,14 +129,40 @@ public class PlayerPerformanceService
|
|||||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
|
var skipDecimation = config.SkipModelDecimationForPreferredPairs && pairHandler.IsDirectlyPaired && pairHandler.HasStickyPermissions;
|
||||||
|
|
||||||
foreach (var hash in moddedModelHashes)
|
foreach (var hash in moddedModelHashes)
|
||||||
{
|
{
|
||||||
triUsage += await _xivDataAnalyzer.GetTrianglesByHash(hash).ConfigureAwait(false);
|
var tris = await _xivDataAnalyzer.GetTrianglesByHash(hash).ConfigureAwait(false);
|
||||||
|
triUsage += tris;
|
||||||
|
|
||||||
|
long effectiveTris = tris;
|
||||||
|
var fileEntry = _fileCacheManager.GetFileCacheByHash(hash);
|
||||||
|
if (fileEntry != null)
|
||||||
|
{
|
||||||
|
var preferredPath = fileEntry.ResolvedFilepath;
|
||||||
|
if (!skipDecimation)
|
||||||
|
{
|
||||||
|
preferredPath = _modelDecimationService.GetPreferredPath(hash, fileEntry.ResolvedFilepath);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.Equals(preferredPath, fileEntry.ResolvedFilepath, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
var decimatedTris = await _xivDataAnalyzer.GetEffectiveTrianglesByHash(hash, preferredPath).ConfigureAwait(false);
|
||||||
|
if (decimatedTris > 0)
|
||||||
|
{
|
||||||
|
effectiveTris = decimatedTris;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
effectiveTriUsage += effectiveTris;
|
||||||
}
|
}
|
||||||
|
|
||||||
pairHandler.LastAppliedDataTris = triUsage;
|
pairHandler.LastAppliedDataTris = triUsage;
|
||||||
|
pairHandler.LastAppliedApproximateEffectiveTris = effectiveTriUsage;
|
||||||
|
|
||||||
_logger.LogDebug("Calculated VRAM usage for {p}", pairHandler);
|
_logger.LogDebug("Calculated triangle usage for {p}", pairHandler);
|
||||||
|
|
||||||
// no warning of any kind on ignored pairs
|
// no warning of any kind on ignored pairs
|
||||||
if (config.UIDsToIgnore
|
if (config.UIDsToIgnore
|
||||||
@@ -167,7 +199,9 @@ public class PlayerPerformanceService
|
|||||||
public bool ComputeAndAutoPauseOnVRAMUsageThresholds(IPairPerformanceSubject pairHandler, CharacterData charaData, List<DownloadFileTransfer> toDownloadFiles)
|
public bool ComputeAndAutoPauseOnVRAMUsageThresholds(IPairPerformanceSubject pairHandler, CharacterData charaData, List<DownloadFileTransfer> toDownloadFiles)
|
||||||
{
|
{
|
||||||
var config = _playerPerformanceConfigService.Current;
|
var config = _playerPerformanceConfigService.Current;
|
||||||
bool skipDownscale = pairHandler.IsDirectlyPaired && pairHandler.HasStickyPermissions;
|
bool skipDownscale = config.SkipTextureDownscaleForPreferredPairs
|
||||||
|
&& pairHandler.IsDirectlyPaired
|
||||||
|
&& pairHandler.HasStickyPermissions;
|
||||||
|
|
||||||
long vramUsage = 0;
|
long vramUsage = 0;
|
||||||
long effectiveVramUsage = 0;
|
long effectiveVramUsage = 0;
|
||||||
@@ -274,4 +308,4 @@ public class PlayerPerformanceService
|
|||||||
|
|
||||||
private static bool CheckForThreshold(bool thresholdEnabled, long threshold, long value, bool checkForPrefPerm, bool isPrefPerm) =>
|
private static bool CheckForThreshold(bool thresholdEnabled, long threshold, long value, bool checkForPrefPerm, bool isPrefPerm) =>
|
||||||
thresholdEnabled && threshold > 0 && threshold < value && ((checkForPrefPerm && isPrefPerm) || !isPrefPerm);
|
thresholdEnabled && threshold > 0 && threshold < value && ((checkForPrefPerm && isPrefPerm) || !isPrefPerm);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,16 +77,39 @@ public sealed class TextureDownscaleService
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void ScheduleDownscale(string hash, string filePath, TextureMapKind mapKind)
|
public void ScheduleDownscale(string hash, string filePath, TextureMapKind mapKind)
|
||||||
|
=> ScheduleDownscale(hash, filePath, () => mapKind);
|
||||||
|
|
||||||
|
public void ScheduleDownscale(string hash, string filePath, Func<TextureMapKind> mapKindFactory)
|
||||||
{
|
{
|
||||||
if (!filePath.EndsWith(".tex", StringComparison.OrdinalIgnoreCase)) return;
|
if (!filePath.EndsWith(".tex", StringComparison.OrdinalIgnoreCase)) return;
|
||||||
if (_activeJobs.ContainsKey(hash)) return;
|
if (_activeJobs.ContainsKey(hash)) return;
|
||||||
|
|
||||||
_activeJobs[hash] = Task.Run(async () =>
|
_activeJobs[hash] = Task.Run(async () =>
|
||||||
{
|
{
|
||||||
|
TextureMapKind mapKind;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
mapKind = mapKindFactory();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Failed to determine texture map kind for {Hash}; skipping downscale", hash);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await DownscaleInternalAsync(hash, filePath, mapKind).ConfigureAwait(false);
|
await DownscaleInternalAsync(hash, filePath, mapKind).ConfigureAwait(false);
|
||||||
}, CancellationToken.None);
|
}, CancellationToken.None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool ShouldScheduleDownscale(string filePath)
|
||||||
|
{
|
||||||
|
if (!filePath.EndsWith(".tex", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var performanceConfig = _playerPerformanceConfigService.Current;
|
||||||
|
return performanceConfig.EnableNonIndexTextureMipTrim || performanceConfig.EnableIndexTextureDownscale;
|
||||||
|
}
|
||||||
|
|
||||||
public string GetPreferredPath(string hash, string originalPath)
|
public string GetPreferredPath(string hash, string originalPath)
|
||||||
{
|
{
|
||||||
if (_downscaledPaths.TryGetValue(hash, out var existing) && File.Exists(existing))
|
if (_downscaledPaths.TryGetValue(hash, out var existing) && File.Exists(existing))
|
||||||
@@ -655,7 +678,7 @@ public sealed class TextureDownscaleService
|
|||||||
|
|
||||||
if (!string.Equals(cacheEntry.PrefixedFilePath, prefixed, StringComparison.OrdinalIgnoreCase))
|
if (!string.Equals(cacheEntry.PrefixedFilePath, prefixed, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
_fileCacheManager.RemoveHashedFile(cacheEntry.Hash, cacheEntry.PrefixedFilePath);
|
_fileCacheManager.RemoveHashedFile(cacheEntry.Hash, cacheEntry.PrefixedFilePath, removeDerivedFiles: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
_fileCacheManager.UpdateHashedFile(replacement, computeProperties: false);
|
_fileCacheManager.UpdateHashedFile(replacement, computeProperties: false);
|
||||||
|
|||||||
@@ -6,18 +6,22 @@ using FFXIVClientStructs.Havok.Common.Serialize.Util;
|
|||||||
using LightlessSync.FileCache;
|
using LightlessSync.FileCache;
|
||||||
using LightlessSync.Interop.GameModel;
|
using LightlessSync.Interop.GameModel;
|
||||||
using LightlessSync.LightlessConfiguration;
|
using LightlessSync.LightlessConfiguration;
|
||||||
|
using LightlessSync.PlayerData.Factories;
|
||||||
using LightlessSync.PlayerData.Handlers;
|
using LightlessSync.PlayerData.Handlers;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
namespace LightlessSync.Services;
|
namespace LightlessSync.Services;
|
||||||
|
|
||||||
public sealed class XivDataAnalyzer
|
public sealed partial class XivDataAnalyzer
|
||||||
{
|
{
|
||||||
private readonly ILogger<XivDataAnalyzer> _logger;
|
private readonly ILogger<XivDataAnalyzer> _logger;
|
||||||
private readonly FileCacheManager _fileCacheManager;
|
private readonly FileCacheManager _fileCacheManager;
|
||||||
private readonly XivDataStorageService _configService;
|
private readonly XivDataStorageService _configService;
|
||||||
private readonly List<string> _failedCalculatedTris = [];
|
private readonly List<string> _failedCalculatedTris = [];
|
||||||
|
private readonly List<string> _failedCalculatedEffectiveTris = [];
|
||||||
|
|
||||||
public XivDataAnalyzer(ILogger<XivDataAnalyzer> logger, FileCacheManager fileCacheManager,
|
public XivDataAnalyzer(ILogger<XivDataAnalyzer> logger, FileCacheManager fileCacheManager,
|
||||||
XivDataStorageService configService)
|
XivDataStorageService configService)
|
||||||
@@ -29,127 +33,441 @@ public sealed class XivDataAnalyzer
|
|||||||
|
|
||||||
public unsafe Dictionary<string, List<ushort>>? GetSkeletonBoneIndices(GameObjectHandler handler)
|
public unsafe Dictionary<string, List<ushort>>? GetSkeletonBoneIndices(GameObjectHandler handler)
|
||||||
{
|
{
|
||||||
if (handler.Address == nint.Zero) return null;
|
if (handler is null || handler.Address == nint.Zero)
|
||||||
var chara = (CharacterBase*)(((Character*)handler.Address)->GameObject.DrawObject);
|
return null;
|
||||||
if (chara->GetModelType() != CharacterBase.ModelType.Human) return null;
|
|
||||||
var resHandles = chara->Skeleton->SkeletonResourceHandles;
|
Dictionary<string, HashSet<ushort>> sets = new(StringComparer.OrdinalIgnoreCase);
|
||||||
Dictionary<string, List<ushort>> outputIndices = [];
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
for (int i = 0; i < chara->Skeleton->PartialSkeletonCount; i++)
|
var drawObject = ((Character*)handler.Address)->GameObject.DrawObject;
|
||||||
|
if (drawObject == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var chara = (CharacterBase*)drawObject;
|
||||||
|
if (chara->GetModelType() != CharacterBase.ModelType.Human)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var skeleton = chara->Skeleton;
|
||||||
|
if (skeleton == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var resHandles = skeleton->SkeletonResourceHandles;
|
||||||
|
var partialCount = skeleton->PartialSkeletonCount;
|
||||||
|
if (partialCount <= 0)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
for (int i = 0; i < partialCount; i++)
|
||||||
{
|
{
|
||||||
var handle = *(resHandles + i);
|
var handle = *(resHandles + i);
|
||||||
_logger.LogTrace("Iterating over SkeletonResourceHandle #{i}:{x}", i, ((nint)handle).ToString("X"));
|
if ((nint)handle == nint.Zero)
|
||||||
if ((nint)handle == nint.Zero) continue;
|
continue;
|
||||||
var curBones = handle->BoneCount;
|
|
||||||
// this is unrealistic, the filename shouldn't ever be that long
|
if (handle->FileName.Length > 1024)
|
||||||
if (handle->FileName.Length > 1024) continue;
|
continue;
|
||||||
var skeletonName = handle->FileName.ToString();
|
|
||||||
if (string.IsNullOrEmpty(skeletonName)) continue;
|
var rawName = handle->FileName.ToString();
|
||||||
outputIndices[skeletonName] = [];
|
if (string.IsNullOrWhiteSpace(rawName))
|
||||||
for (ushort boneIdx = 0; boneIdx < curBones; boneIdx++)
|
continue;
|
||||||
|
|
||||||
|
var skeletonKey = CanonicalizeSkeletonKey(rawName);
|
||||||
|
if (string.IsNullOrEmpty(skeletonKey))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var boneCount = handle->BoneCount;
|
||||||
|
if (boneCount == 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var havokSkel = handle->HavokSkeleton;
|
||||||
|
if ((nint)havokSkel == nint.Zero)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (!sets.TryGetValue(skeletonKey, out var set))
|
||||||
{
|
{
|
||||||
var boneName = handle->HavokSkeleton->Bones[boneIdx].Name.String;
|
set = [];
|
||||||
if (boneName == null) continue;
|
sets[skeletonKey] = set;
|
||||||
outputIndices[skeletonName].Add((ushort)(boneIdx + 1));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
uint maxExclusive = boneCount;
|
||||||
|
uint ushortExclusive = (uint)ushort.MaxValue + 1u;
|
||||||
|
if (maxExclusive > ushortExclusive)
|
||||||
|
maxExclusive = ushortExclusive;
|
||||||
|
|
||||||
|
for (uint boneIdx = 0; boneIdx < maxExclusive; boneIdx++)
|
||||||
|
{
|
||||||
|
var name = havokSkel->Bones[boneIdx].Name.String;
|
||||||
|
if (name == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
set.Add((ushort)boneIdx);
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogTrace("Local skeleton raw file='{raw}', key='{key}', boneCount={count}",
|
||||||
|
rawName, skeletonKey, boneCount);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogWarning(ex, "Could not process skeleton data");
|
_logger.LogWarning(ex, "Could not process skeleton data");
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (outputIndices.Count != 0 && outputIndices.Values.All(u => u.Count > 0)) ? outputIndices : null;
|
if (sets.Count == 0)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var output = new Dictionary<string, List<ushort>>(sets.Count, StringComparer.OrdinalIgnoreCase);
|
||||||
|
foreach (var (key, set) in sets)
|
||||||
|
{
|
||||||
|
if (set.Count == 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var list = set.ToList();
|
||||||
|
list.Sort();
|
||||||
|
output[key] = list;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (output.Count != 0 && output.Values.All(v => v.Count > 0)) ? output : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public unsafe Dictionary<string, List<ushort>>? GetBoneIndicesFromPap(string hash)
|
public unsafe Dictionary<string, List<ushort>>? GetBoneIndicesFromPap(string hash, bool persistToConfig = true)
|
||||||
{
|
{
|
||||||
if (_configService.Current.BonesDictionary.TryGetValue(hash, out var bones)) return bones;
|
if (string.IsNullOrWhiteSpace(hash))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (_configService.Current.BonesDictionary.TryGetValue(hash, out var cached) && cached is not null)
|
||||||
|
return cached;
|
||||||
|
|
||||||
var cacheEntity = _fileCacheManager.GetFileCacheByHash(hash);
|
var cacheEntity = _fileCacheManager.GetFileCacheByHash(hash);
|
||||||
if (cacheEntity == null) return null;
|
if (cacheEntity == null || string.IsNullOrEmpty(cacheEntity.ResolvedFilepath) || !File.Exists(cacheEntity.ResolvedFilepath))
|
||||||
|
return null;
|
||||||
|
|
||||||
using BinaryReader reader = new(File.Open(cacheEntity.ResolvedFilepath, FileMode.Open, FileAccess.Read, FileShare.Read));
|
using var fs = File.Open(cacheEntity.ResolvedFilepath, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||||
|
using var reader = new BinaryReader(fs);
|
||||||
|
|
||||||
// most of this shit is from vfxeditor, surely nothing will change in the pap format :copium:
|
// PAP header (mostly from vfxeditor)
|
||||||
reader.ReadInt32(); // ignore
|
_ = reader.ReadInt32(); // ignore
|
||||||
reader.ReadInt32(); // ignore
|
_ = reader.ReadInt32(); // ignore
|
||||||
reader.ReadInt16(); // read 2 (num animations)
|
_ = reader.ReadInt16(); // num animations
|
||||||
reader.ReadInt16(); // read 2 (modelid)
|
_ = reader.ReadInt16(); // modelid
|
||||||
var type = reader.ReadByte();// read 1 (type)
|
|
||||||
if (type != 0) return null; // it's not human, just ignore it, whatever
|
var type = reader.ReadByte(); // type
|
||||||
|
if (type != 0)
|
||||||
|
return null; // not human
|
||||||
|
|
||||||
|
_ = reader.ReadByte(); // variant
|
||||||
|
_ = reader.ReadInt32(); // ignore
|
||||||
|
|
||||||
reader.ReadByte(); // read 1 (variant)
|
|
||||||
reader.ReadInt32(); // ignore
|
|
||||||
var havokPosition = reader.ReadInt32();
|
var havokPosition = reader.ReadInt32();
|
||||||
var footerPosition = reader.ReadInt32();
|
var footerPosition = reader.ReadInt32();
|
||||||
var havokDataSize = footerPosition - havokPosition;
|
|
||||||
|
// sanity checks
|
||||||
|
if (havokPosition <= 0 || footerPosition <= havokPosition || footerPosition > fs.Length)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var havokDataSizeLong = (long)footerPosition - havokPosition;
|
||||||
|
if (havokDataSizeLong <= 8 || havokDataSizeLong > int.MaxValue)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var havokDataSize = (int)havokDataSizeLong;
|
||||||
|
|
||||||
reader.BaseStream.Position = havokPosition;
|
reader.BaseStream.Position = havokPosition;
|
||||||
var havokData = reader.ReadBytes(havokDataSize);
|
var havokData = reader.ReadBytes(havokDataSize);
|
||||||
if (havokData.Length <= 8) return null; // no havok data
|
if (havokData.Length <= 8)
|
||||||
|
return null;
|
||||||
|
|
||||||
var output = new Dictionary<string, List<ushort>>(StringComparer.OrdinalIgnoreCase);
|
var tempSets = new Dictionary<string, HashSet<ushort>>(StringComparer.OrdinalIgnoreCase);
|
||||||
var tempHavokDataPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()) + ".hkx";
|
|
||||||
var tempHavokDataPathAnsi = Marshal.StringToHGlobalAnsi(tempHavokDataPath);
|
var tempHavokDataPath = Path.Combine(Path.GetTempPath(), $"lightless_{Guid.NewGuid():N}.hkx");
|
||||||
|
IntPtr tempHavokDataPathAnsi = IntPtr.Zero;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
File.WriteAllBytes(tempHavokDataPath, havokData);
|
File.WriteAllBytes(tempHavokDataPath, havokData);
|
||||||
|
|
||||||
|
if (!File.Exists(tempHavokDataPath))
|
||||||
|
{
|
||||||
|
_logger.LogTrace("Temporary havok file did not exist when attempting to load: {path}", tempHavokDataPath);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
tempHavokDataPathAnsi = Marshal.StringToHGlobalAnsi(tempHavokDataPath);
|
||||||
|
|
||||||
var loadoptions = stackalloc hkSerializeUtil.LoadOptions[1];
|
var loadoptions = stackalloc hkSerializeUtil.LoadOptions[1];
|
||||||
loadoptions->TypeInfoRegistry = hkBuiltinTypeRegistry.Instance()->GetTypeInfoRegistry();
|
loadoptions->TypeInfoRegistry = hkBuiltinTypeRegistry.Instance()->GetTypeInfoRegistry();
|
||||||
loadoptions->ClassNameRegistry = hkBuiltinTypeRegistry.Instance()->GetClassNameRegistry();
|
loadoptions->ClassNameRegistry = hkBuiltinTypeRegistry.Instance()->GetClassNameRegistry();
|
||||||
loadoptions->Flags = new hkFlags<hkSerializeUtil.LoadOptionBits, int>
|
loadoptions->Flags = new hkFlags<hkSerializeUtil.LoadOptionBits, int>
|
||||||
{
|
{
|
||||||
Storage = (int)(hkSerializeUtil.LoadOptionBits.Default)
|
Storage = (int)hkSerializeUtil.LoadOptionBits.Default
|
||||||
};
|
};
|
||||||
|
|
||||||
var resource = hkSerializeUtil.LoadFromFile((byte*)tempHavokDataPathAnsi, null, loadoptions);
|
var resource = hkSerializeUtil.LoadFromFile((byte*)tempHavokDataPathAnsi, null, loadoptions);
|
||||||
if (resource == null)
|
if (resource == null)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException("Resource was null after loading");
|
_logger.LogWarning("Havok resource was null after loading from {path}", tempHavokDataPath);
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var rootLevelName = @"hkRootLevelContainer"u8;
|
var rootLevelName = @"hkRootLevelContainer"u8;
|
||||||
fixed (byte* n1 = rootLevelName)
|
fixed (byte* n1 = rootLevelName)
|
||||||
{
|
{
|
||||||
var container = (hkRootLevelContainer*)resource->GetContentsPointer(n1, hkBuiltinTypeRegistry.Instance()->GetTypeInfoRegistry());
|
var container = (hkRootLevelContainer*)resource->GetContentsPointer(n1, hkBuiltinTypeRegistry.Instance()->GetTypeInfoRegistry());
|
||||||
|
if (container == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
var animationName = @"hkaAnimationContainer"u8;
|
var animationName = @"hkaAnimationContainer"u8;
|
||||||
fixed (byte* n2 = animationName)
|
fixed (byte* n2 = animationName)
|
||||||
{
|
{
|
||||||
var animContainer = (hkaAnimationContainer*)container->findObjectByName(n2, null);
|
var animContainer = (hkaAnimationContainer*)container->findObjectByName(n2, null);
|
||||||
|
if (animContainer == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
for (int i = 0; i < animContainer->Bindings.Length; i++)
|
for (int i = 0; i < animContainer->Bindings.Length; i++)
|
||||||
{
|
{
|
||||||
var binding = animContainer->Bindings[i].ptr;
|
var binding = animContainer->Bindings[i].ptr;
|
||||||
|
if (binding == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var rawSkel = binding->OriginalSkeletonName.String;
|
||||||
|
var skeletonKey = CanonicalizeSkeletonKey(rawSkel);
|
||||||
|
if (string.IsNullOrEmpty(skeletonKey))
|
||||||
|
continue;
|
||||||
|
|
||||||
var boneTransform = binding->TransformTrackToBoneIndices;
|
var boneTransform = binding->TransformTrackToBoneIndices;
|
||||||
string name = binding->OriginalSkeletonName.String! + "_" + i;
|
if (boneTransform.Length <= 0)
|
||||||
output[name] = [];
|
continue;
|
||||||
|
|
||||||
|
if (!tempSets.TryGetValue(skeletonKey, out var set))
|
||||||
|
{
|
||||||
|
set = [];
|
||||||
|
tempSets[skeletonKey] = set;
|
||||||
|
}
|
||||||
|
|
||||||
for (int boneIdx = 0; boneIdx < boneTransform.Length; boneIdx++)
|
for (int boneIdx = 0; boneIdx < boneTransform.Length; boneIdx++)
|
||||||
{
|
{
|
||||||
output[name].Add((ushort)boneTransform[boneIdx]);
|
var v = boneTransform[boneIdx];
|
||||||
|
if (v < 0) continue;
|
||||||
|
set.Add((ushort)v);
|
||||||
}
|
}
|
||||||
output[name].Sort();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogWarning(ex, "Could not load havok file in {path}", tempHavokDataPath);
|
_logger.LogWarning(ex, "Could not load havok file in {path}", tempHavokDataPath);
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
Marshal.FreeHGlobal(tempHavokDataPathAnsi);
|
if (tempHavokDataPathAnsi != IntPtr.Zero)
|
||||||
File.Delete(tempHavokDataPath);
|
Marshal.FreeHGlobal(tempHavokDataPathAnsi);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (File.Exists(tempHavokDataPath))
|
||||||
|
File.Delete(tempHavokDataPath);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogTrace(ex, "Could not delete temporary havok file: {path}", tempHavokDataPath);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (tempSets.Count == 0)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var output = new Dictionary<string, List<ushort>>(tempSets.Count, StringComparer.OrdinalIgnoreCase);
|
||||||
|
foreach (var (key, set) in tempSets)
|
||||||
|
{
|
||||||
|
if (set.Count == 0) continue;
|
||||||
|
|
||||||
|
var list = set.ToList();
|
||||||
|
list.Sort();
|
||||||
|
output[key] = list;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (output.Count == 0)
|
||||||
|
return null;
|
||||||
|
|
||||||
_configService.Current.BonesDictionary[hash] = output;
|
_configService.Current.BonesDictionary[hash] = output;
|
||||||
_configService.Save();
|
|
||||||
|
if (persistToConfig)
|
||||||
|
_configService.Save();
|
||||||
|
|
||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static string CanonicalizeSkeletonKey(string? raw)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(raw))
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
var s = raw.Replace('\\', '/').Trim();
|
||||||
|
|
||||||
|
var underscore = s.LastIndexOf('_');
|
||||||
|
if (underscore > 0 && underscore + 1 < s.Length && char.IsDigit(s[underscore + 1]))
|
||||||
|
s = s[..underscore];
|
||||||
|
|
||||||
|
if (s.StartsWith("skeleton", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return "skeleton";
|
||||||
|
|
||||||
|
var m = _bucketPathRegex.Match(s);
|
||||||
|
if (m.Success)
|
||||||
|
return m.Groups["bucket"].Value.ToLowerInvariant();
|
||||||
|
|
||||||
|
m = _bucketSklRegex.Match(s);
|
||||||
|
if (m.Success)
|
||||||
|
return m.Groups["bucket"].Value.ToLowerInvariant();
|
||||||
|
|
||||||
|
m = _bucketLooseRegex.Match(s);
|
||||||
|
if (m.Success)
|
||||||
|
return m.Groups["bucket"].Value.ToLowerInvariant();
|
||||||
|
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool ContainsIndexCompat(
|
||||||
|
HashSet<ushort> available,
|
||||||
|
ushort idx,
|
||||||
|
bool papLikelyOneBased,
|
||||||
|
bool allowOneBasedShift,
|
||||||
|
bool allowNeighborTolerance)
|
||||||
|
{
|
||||||
|
Span<ushort> candidates = stackalloc ushort[2];
|
||||||
|
int count = 0;
|
||||||
|
|
||||||
|
candidates[count++] = idx;
|
||||||
|
|
||||||
|
if (allowOneBasedShift && papLikelyOneBased && idx > 0)
|
||||||
|
candidates[count++] = (ushort)(idx - 1);
|
||||||
|
|
||||||
|
for (int i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
var c = candidates[i];
|
||||||
|
|
||||||
|
if (available.Contains(c))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (allowNeighborTolerance)
|
||||||
|
{
|
||||||
|
if (c > 0 && available.Contains((ushort)(c - 1)))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (c < ushort.MaxValue && available.Contains((ushort)(c + 1)))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsPapCompatible(
|
||||||
|
IReadOnlyDictionary<string, HashSet<ushort>> localBoneSets,
|
||||||
|
IReadOnlyDictionary<string, List<ushort>> papBoneIndices,
|
||||||
|
AnimationValidationMode mode,
|
||||||
|
bool allowOneBasedShift,
|
||||||
|
bool allowNeighborTolerance,
|
||||||
|
out string reason)
|
||||||
|
{
|
||||||
|
reason = string.Empty;
|
||||||
|
|
||||||
|
if (mode == AnimationValidationMode.Unsafe)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
var papBuckets = papBoneIndices.Keys
|
||||||
|
.Select(CanonicalizeSkeletonKey)
|
||||||
|
.Where(k => !string.IsNullOrEmpty(k))
|
||||||
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (papBuckets.Count == 0)
|
||||||
|
{
|
||||||
|
reason = "No skeleton bucket bindings found in the PAP";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mode == AnimationValidationMode.Safe)
|
||||||
|
{
|
||||||
|
if (papBuckets.Any(b => localBoneSets.ContainsKey(b)))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
reason = $"No matching skeleton bucket between PAP [{string.Join(", ", papBuckets)}] and local [{string.Join(", ", localBoneSets.Keys.Order())}].";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var bucket in papBuckets)
|
||||||
|
{
|
||||||
|
if (!localBoneSets.TryGetValue(bucket, out var available))
|
||||||
|
{
|
||||||
|
reason = $"Missing skeleton bucket '{bucket}' on local actor.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var indices = papBoneIndices
|
||||||
|
.Where(kvp => string.Equals(CanonicalizeSkeletonKey(kvp.Key), bucket, StringComparison.OrdinalIgnoreCase))
|
||||||
|
.SelectMany(kvp => kvp.Value ?? Enumerable.Empty<ushort>())
|
||||||
|
.Distinct()
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (indices.Count == 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
bool has0 = false, has1 = false;
|
||||||
|
ushort min = ushort.MaxValue;
|
||||||
|
foreach (var v in indices)
|
||||||
|
{
|
||||||
|
if (v == 0) has0 = true;
|
||||||
|
if (v == 1) has1 = true;
|
||||||
|
if (v < min) min = v;
|
||||||
|
}
|
||||||
|
bool papLikelyOneBased = allowOneBasedShift && (min == 1) && has1 && !has0;
|
||||||
|
|
||||||
|
foreach (var idx in indices)
|
||||||
|
{
|
||||||
|
if (!ContainsIndexCompat(available, idx, papLikelyOneBased, allowOneBasedShift, allowNeighborTolerance))
|
||||||
|
{
|
||||||
|
reason = $"No compatible local skeleton for PAP '{bucket}': missing bone index {idx}.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DumpLocalSkeletonIndices(GameObjectHandler handler, string? filter = null)
|
||||||
|
{
|
||||||
|
var skels = GetSkeletonBoneIndices(handler);
|
||||||
|
if (skels == null)
|
||||||
|
{
|
||||||
|
_logger.LogTrace("DumpLocalSkeletonIndices: local skeleton indices are null or not found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var keys = skels.Keys
|
||||||
|
.Order(StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
_logger.LogTrace("Local skeleton indices found ({count}): {keys}",
|
||||||
|
keys.Length,
|
||||||
|
string.Join(", ", keys));
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(filter))
|
||||||
|
{
|
||||||
|
var hits = keys.Where(k =>
|
||||||
|
k.Equals(filter, StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
k.StartsWith(filter + "_", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
filter.StartsWith(k + "_", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
k.Contains(filter, StringComparison.OrdinalIgnoreCase))
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
_logger.LogTrace("Matches found for '{filter}': {hits}",
|
||||||
|
filter,
|
||||||
|
hits.Length == 0 ? "<none>" : string.Join(", ", hits));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<long> GetTrianglesByHash(string hash)
|
public async Task<long> GetTrianglesByHash(string hash)
|
||||||
{
|
{
|
||||||
if (_configService.Current.TriangleDictionary.TryGetValue(hash, out var cachedTris) && cachedTris > 0)
|
if (_configService.Current.TriangleDictionary.TryGetValue(hash, out var cachedTris) && cachedTris > 0)
|
||||||
@@ -162,16 +480,41 @@ public sealed class XivDataAnalyzer
|
|||||||
if (path == null || !path.ResolvedFilepath.EndsWith(".mdl", StringComparison.OrdinalIgnoreCase))
|
if (path == null || !path.ResolvedFilepath.EndsWith(".mdl", StringComparison.OrdinalIgnoreCase))
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
var filePath = path.ResolvedFilepath;
|
return CalculateTrianglesFromPath(hash, path.ResolvedFilepath, _configService.Current.TriangleDictionary, _failedCalculatedTris);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<long> GetEffectiveTrianglesByHash(string hash, string filePath)
|
||||||
|
{
|
||||||
|
if (_configService.Current.EffectiveTriangleDictionary.TryGetValue(hash, out var cachedTris) && cachedTris > 0)
|
||||||
|
return cachedTris;
|
||||||
|
|
||||||
|
if (_failedCalculatedEffectiveTris.Contains(hash, StringComparer.Ordinal))
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(filePath)
|
||||||
|
|| !filePath.EndsWith(".mdl", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| !File.Exists(filePath))
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return CalculateTrianglesFromPath(hash, filePath, _configService.Current.EffectiveTriangleDictionary, _failedCalculatedEffectiveTris);
|
||||||
|
}
|
||||||
|
|
||||||
|
private long CalculateTrianglesFromPath(
|
||||||
|
string hash,
|
||||||
|
string filePath,
|
||||||
|
ConcurrentDictionary<string, long> cache,
|
||||||
|
List<string> failedList)
|
||||||
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_logger.LogDebug("Detected Model File {path}, calculating Tris", filePath);
|
_logger.LogDebug("Detected Model File {path}, calculating Tris", filePath);
|
||||||
var file = new MdlFile(filePath);
|
var file = new MdlFile(filePath);
|
||||||
if (file.LodCount <= 0)
|
if (file.LodCount <= 0)
|
||||||
{
|
{
|
||||||
_failedCalculatedTris.Add(hash);
|
failedList.Add(hash);
|
||||||
_configService.Current.TriangleDictionary[hash] = 0;
|
cache[hash] = 0;
|
||||||
_configService.Save();
|
_configService.Save();
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -195,7 +538,7 @@ public sealed class XivDataAnalyzer
|
|||||||
if (tris > 0)
|
if (tris > 0)
|
||||||
{
|
{
|
||||||
_logger.LogDebug("TriAnalysis: {filePath} => {tris} triangles", filePath, tris);
|
_logger.LogDebug("TriAnalysis: {filePath} => {tris} triangles", filePath, tris);
|
||||||
_configService.Current.TriangleDictionary[hash] = tris;
|
cache[hash] = tris;
|
||||||
_configService.Save();
|
_configService.Save();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -205,11 +548,30 @@ public sealed class XivDataAnalyzer
|
|||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
_failedCalculatedTris.Add(hash);
|
failedList.Add(hash);
|
||||||
_configService.Current.TriangleDictionary[hash] = 0;
|
cache[hash] = 0;
|
||||||
_configService.Save();
|
_configService.Save();
|
||||||
_logger.LogWarning(e, "Could not parse file {file}", filePath);
|
_logger.LogWarning(e, "Could not parse file {file}", filePath);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Regexes for canonicalizing skeleton keys
|
||||||
|
private static readonly Regex _bucketPathRegex =
|
||||||
|
BucketRegex();
|
||||||
|
|
||||||
|
private static readonly Regex _bucketSklRegex =
|
||||||
|
SklRegex();
|
||||||
|
|
||||||
|
private static readonly Regex _bucketLooseRegex =
|
||||||
|
LooseBucketRegex();
|
||||||
|
|
||||||
|
[GeneratedRegex(@"(?i)(?:^|/)(?<bucket>c\d{4})(?:/|$)", RegexOptions.Compiled, "en-NL")]
|
||||||
|
private static partial Regex BucketRegex();
|
||||||
|
|
||||||
|
[GeneratedRegex(@"(?i)\bskl_(?<bucket>c\d{4})[a-z]\d{4}\b", RegexOptions.Compiled, "en-NL")]
|
||||||
|
private static partial Regex SklRegex();
|
||||||
|
|
||||||
|
[GeneratedRegex(@"(?i)(?<![a-z0-9])(?<bucket>c\d{4})(?!\d)", RegexOptions.Compiled, "en-NL")]
|
||||||
|
private static partial Regex LooseBucketRegex();
|
||||||
}
|
}
|
||||||
|
|||||||
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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -52,8 +52,8 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
private readonly LightlessConfigService _configService;
|
private readonly LightlessConfigService _configService;
|
||||||
private readonly LightlessMediator _lightlessMediator;
|
private readonly LightlessMediator _lightlessMediator;
|
||||||
private readonly PairLedger _pairLedger;
|
private readonly PairLedger _pairLedger;
|
||||||
private readonly PairUiService _pairUiService;
|
|
||||||
private readonly PlayerPerformanceConfigService _playerPerformanceConfig;
|
private readonly PlayerPerformanceConfigService _playerPerformanceConfig;
|
||||||
|
private readonly PairUiService _pairUiService;
|
||||||
private readonly ServerConfigurationManager _serverManager;
|
private readonly ServerConfigurationManager _serverManager;
|
||||||
private readonly TagHandler _tagHandler;
|
private readonly TagHandler _tagHandler;
|
||||||
private readonly UiSharedService _uiSharedService;
|
private readonly UiSharedService _uiSharedService;
|
||||||
@@ -167,9 +167,12 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
Mediator.Subscribe<SwitchToIntroUiMessage>(this, (_) => IsOpen = false);
|
Mediator.Subscribe<SwitchToIntroUiMessage>(this, (_) => IsOpen = false);
|
||||||
Mediator.Subscribe<CutsceneStartMessage>(this, (_) => UiSharedService_GposeStart());
|
Mediator.Subscribe<CutsceneStartMessage>(this, (_) => UiSharedService_GposeStart());
|
||||||
Mediator.Subscribe<CutsceneEndMessage>(this, (_) => UiSharedService_GposeEnd());
|
Mediator.Subscribe<CutsceneEndMessage>(this, (_) => UiSharedService_GposeEnd());
|
||||||
Mediator.Subscribe<DownloadStartedMessage>(this, (msg) => _currentDownloads[msg.DownloadId] = msg.DownloadStatus);
|
Mediator.Subscribe<DownloadStartedMessage>(this, msg =>
|
||||||
|
{
|
||||||
|
_currentDownloads[msg.DownloadId] = new Dictionary<string, FileDownloadStatus>(msg.DownloadStatus, StringComparer.Ordinal);
|
||||||
|
});
|
||||||
Mediator.Subscribe<DownloadFinishedMessage>(this, (msg) => _currentDownloads.TryRemove(msg.DownloadId, out _));
|
Mediator.Subscribe<DownloadFinishedMessage>(this, (msg) => _currentDownloads.TryRemove(msg.DownloadId, out _));
|
||||||
Mediator.Subscribe<RefreshUiMessage>(this, (msg) => _drawFolders = DrawFolders.ToList());
|
Mediator.Subscribe<RefreshUiMessage>(this, (msg) => _drawFolders = [.. DrawFolders]);
|
||||||
|
|
||||||
_characterAnalyzer = characterAnalyzer;
|
_characterAnalyzer = characterAnalyzer;
|
||||||
_playerPerformanceConfig = playerPerformanceConfig;
|
_playerPerformanceConfig = playerPerformanceConfig;
|
||||||
@@ -991,6 +994,7 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
VisiblePairSortMode.VramUsage => SortVisibleByMetric(entryList, e => e.LastAppliedApproximateVramBytes),
|
VisiblePairSortMode.VramUsage => SortVisibleByMetric(entryList, e => e.LastAppliedApproximateVramBytes),
|
||||||
VisiblePairSortMode.EffectiveVramUsage => SortVisibleByMetric(entryList, e => e.LastAppliedApproximateEffectiveVramBytes),
|
VisiblePairSortMode.EffectiveVramUsage => SortVisibleByMetric(entryList, e => e.LastAppliedApproximateEffectiveVramBytes),
|
||||||
VisiblePairSortMode.TriangleCount => SortVisibleByMetric(entryList, e => e.LastAppliedDataTris),
|
VisiblePairSortMode.TriangleCount => SortVisibleByMetric(entryList, e => e.LastAppliedDataTris),
|
||||||
|
VisiblePairSortMode.EffectiveTriangleCount => SortVisibleByMetric(entryList, e => e.LastAppliedApproximateEffectiveTris),
|
||||||
VisiblePairSortMode.Alphabetical => [.. entryList.OrderBy(e => AlphabeticalSortKey(e), StringComparer.OrdinalIgnoreCase)],
|
VisiblePairSortMode.Alphabetical => [.. entryList.OrderBy(e => AlphabeticalSortKey(e), StringComparer.OrdinalIgnoreCase)],
|
||||||
VisiblePairSortMode.PreferredDirectPairs => SortVisibleByPreferred(entryList),
|
VisiblePairSortMode.PreferredDirectPairs => SortVisibleByPreferred(entryList),
|
||||||
_ => SortEntries(entryList),
|
_ => SortEntries(entryList),
|
||||||
|
|||||||
@@ -326,6 +326,7 @@ public class DrawFolderTag : DrawFolderBase
|
|||||||
VisiblePairSortMode.VramUsage => "VRAM usage (descending)",
|
VisiblePairSortMode.VramUsage => "VRAM usage (descending)",
|
||||||
VisiblePairSortMode.EffectiveVramUsage => "Effective VRAM usage (descending)",
|
VisiblePairSortMode.EffectiveVramUsage => "Effective VRAM usage (descending)",
|
||||||
VisiblePairSortMode.TriangleCount => "Triangle count (descending)",
|
VisiblePairSortMode.TriangleCount => "Triangle count (descending)",
|
||||||
|
VisiblePairSortMode.EffectiveTriangleCount => "Effective triangle count (descending)",
|
||||||
VisiblePairSortMode.PreferredDirectPairs => "Preferred permissions & Direct pairs",
|
VisiblePairSortMode.PreferredDirectPairs => "Preferred permissions & Direct pairs",
|
||||||
_ => "Default",
|
_ => "Default",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -429,6 +429,7 @@ public class DrawUserPair
|
|||||||
_pair.LastAppliedApproximateVRAMBytes,
|
_pair.LastAppliedApproximateVRAMBytes,
|
||||||
_pair.LastAppliedApproximateEffectiveVRAMBytes,
|
_pair.LastAppliedApproximateEffectiveVRAMBytes,
|
||||||
_pair.LastAppliedDataTris,
|
_pair.LastAppliedDataTris,
|
||||||
|
_pair.LastAppliedApproximateEffectiveTris,
|
||||||
_pair.IsPaired,
|
_pair.IsPaired,
|
||||||
groupDisplays is null ? ImmutableArray<string>.Empty : ImmutableArray.CreateRange(groupDisplays));
|
groupDisplays is null ? ImmutableArray<string>.Empty : ImmutableArray.CreateRange(groupDisplays));
|
||||||
|
|
||||||
@@ -444,6 +445,8 @@ public class DrawUserPair
|
|||||||
private static string BuildTooltip(in TooltipSnapshot snapshot)
|
private static string BuildTooltip(in TooltipSnapshot snapshot)
|
||||||
{
|
{
|
||||||
var builder = new StringBuilder(256);
|
var builder = new StringBuilder(256);
|
||||||
|
static string FormatTriangles(long count) =>
|
||||||
|
count > 1000 ? (count / 1000d).ToString("0.0'k'") : count.ToString();
|
||||||
|
|
||||||
if (snapshot.IsPaused)
|
if (snapshot.IsPaused)
|
||||||
{
|
{
|
||||||
@@ -510,9 +513,13 @@ public class DrawUserPair
|
|||||||
{
|
{
|
||||||
builder.Append(Environment.NewLine);
|
builder.Append(Environment.NewLine);
|
||||||
builder.Append("Approx. Triangle Count (excl. Vanilla): ");
|
builder.Append("Approx. Triangle Count (excl. Vanilla): ");
|
||||||
builder.Append(snapshot.LastAppliedDataTris > 1000
|
builder.Append(FormatTriangles(snapshot.LastAppliedDataTris));
|
||||||
? (snapshot.LastAppliedDataTris / 1000d).ToString("0.0'k'")
|
if (snapshot.LastAppliedApproximateEffectiveTris >= 0)
|
||||||
: snapshot.LastAppliedDataTris);
|
{
|
||||||
|
builder.Append(" (Effective: ");
|
||||||
|
builder.Append(FormatTriangles(snapshot.LastAppliedApproximateEffectiveTris));
|
||||||
|
builder.Append(')');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -544,11 +551,12 @@ public class DrawUserPair
|
|||||||
long LastAppliedApproximateVRAMBytes,
|
long LastAppliedApproximateVRAMBytes,
|
||||||
long LastAppliedApproximateEffectiveVRAMBytes,
|
long LastAppliedApproximateEffectiveVRAMBytes,
|
||||||
long LastAppliedDataTris,
|
long LastAppliedDataTris,
|
||||||
|
long LastAppliedApproximateEffectiveTris,
|
||||||
bool IsPaired,
|
bool IsPaired,
|
||||||
ImmutableArray<string> GroupDisplays)
|
ImmutableArray<string> GroupDisplays)
|
||||||
{
|
{
|
||||||
public static TooltipSnapshot Empty { get; } =
|
public static TooltipSnapshot Empty { get; } =
|
||||||
new(false, false, false, IndividualPairStatus.None, string.Empty, string.Empty, -1, -1, -1, -1, false, ImmutableArray<string>.Empty);
|
new(false, false, false, IndividualPairStatus.None, string.Empty, string.Empty, -1, -1, -1, -1, -1, false, ImmutableArray<string>.Empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawPairedClientMenu()
|
private void DrawPairedClientMenu()
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ using LightlessSync.LightlessConfiguration;
|
|||||||
using LightlessSync.Services;
|
using LightlessSync.Services;
|
||||||
using LightlessSync.Services.Mediator;
|
using LightlessSync.Services.Mediator;
|
||||||
using LightlessSync.Services.TextureCompression;
|
using LightlessSync.Services.TextureCompression;
|
||||||
|
using LightlessSync.UI.Models;
|
||||||
using LightlessSync.Utils;
|
using LightlessSync.Utils;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using OtterTex;
|
using OtterTex;
|
||||||
@@ -34,12 +35,15 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
|
|||||||
private const float TextureDetailSplitterWidth = 12f;
|
private const float TextureDetailSplitterWidth = 12f;
|
||||||
private const float TextureDetailSplitterCollapsedWidth = 18f;
|
private const float TextureDetailSplitterCollapsedWidth = 18f;
|
||||||
private const float SelectedFilePanelLogicalHeight = 90f;
|
private const float SelectedFilePanelLogicalHeight = 90f;
|
||||||
|
private const float TextureHoverPreviewDelaySeconds = 1.75f;
|
||||||
|
private const float TextureHoverPreviewSize = 350f;
|
||||||
private static readonly Vector4 SelectedTextureRowTextColor = new(0f, 0f, 0f, 1f);
|
private static readonly Vector4 SelectedTextureRowTextColor = new(0f, 0f, 0f, 1f);
|
||||||
|
|
||||||
private readonly CharacterAnalyzer _characterAnalyzer;
|
private readonly CharacterAnalyzer _characterAnalyzer;
|
||||||
private readonly Progress<TextureConversionProgress> _conversionProgress = new();
|
private readonly Progress<TextureConversionProgress> _conversionProgress = new();
|
||||||
private readonly IpcManager _ipcManager;
|
private readonly IpcManager _ipcManager;
|
||||||
private readonly UiSharedService _uiSharedService;
|
private readonly UiSharedService _uiSharedService;
|
||||||
|
private readonly LightlessConfigService _configService;
|
||||||
private readonly PlayerPerformanceConfigService _playerPerformanceConfig;
|
private readonly PlayerPerformanceConfigService _playerPerformanceConfig;
|
||||||
private readonly TransientResourceManager _transientResourceManager;
|
private readonly TransientResourceManager _transientResourceManager;
|
||||||
private readonly TransientConfigService _transientConfigService;
|
private readonly TransientConfigService _transientConfigService;
|
||||||
@@ -77,6 +81,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
|
|||||||
private string _selectedJobEntry = string.Empty;
|
private string _selectedJobEntry = string.Empty;
|
||||||
private string _filterGamePath = string.Empty;
|
private string _filterGamePath = string.Empty;
|
||||||
private string _filterFilePath = string.Empty;
|
private string _filterFilePath = string.Empty;
|
||||||
|
private string _textureHoverKey = string.Empty;
|
||||||
|
|
||||||
private int _conversionCurrentFileProgress = 0;
|
private int _conversionCurrentFileProgress = 0;
|
||||||
private int _conversionTotalJobs;
|
private int _conversionTotalJobs;
|
||||||
@@ -87,6 +92,11 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
|
|||||||
private bool _textureRowsDirty = true;
|
private bool _textureRowsDirty = true;
|
||||||
private bool _textureDetailCollapsed = false;
|
private bool _textureDetailCollapsed = false;
|
||||||
private bool _conversionFailed;
|
private bool _conversionFailed;
|
||||||
|
private double _textureHoverStartTime = 0;
|
||||||
|
#if DEBUG
|
||||||
|
private bool _debugCompressionModalOpen = false;
|
||||||
|
private TextureConversionProgress? _debugConversionProgress;
|
||||||
|
#endif
|
||||||
private bool _showAlreadyAddedTransients = false;
|
private bool _showAlreadyAddedTransients = false;
|
||||||
private bool _acknowledgeReview = false;
|
private bool _acknowledgeReview = false;
|
||||||
|
|
||||||
@@ -98,10 +108,12 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
|
|||||||
private TextureUsageCategory? _textureCategoryFilter = null;
|
private TextureUsageCategory? _textureCategoryFilter = null;
|
||||||
private TextureMapKind? _textureMapFilter = null;
|
private TextureMapKind? _textureMapFilter = null;
|
||||||
private TextureCompressionTarget? _textureTargetFilter = null;
|
private TextureCompressionTarget? _textureTargetFilter = null;
|
||||||
|
private TextureFormatSortMode _textureFormatSortMode = TextureFormatSortMode.None;
|
||||||
|
|
||||||
public DataAnalysisUi(ILogger<DataAnalysisUi> logger, LightlessMediator mediator,
|
public DataAnalysisUi(ILogger<DataAnalysisUi> logger, LightlessMediator mediator,
|
||||||
CharacterAnalyzer characterAnalyzer, IpcManager ipcManager,
|
CharacterAnalyzer characterAnalyzer, IpcManager ipcManager,
|
||||||
PerformanceCollectorService performanceCollectorService, UiSharedService uiSharedService,
|
PerformanceCollectorService performanceCollectorService, UiSharedService uiSharedService,
|
||||||
|
LightlessConfigService configService,
|
||||||
PlayerPerformanceConfigService playerPerformanceConfig, TransientResourceManager transientResourceManager,
|
PlayerPerformanceConfigService playerPerformanceConfig, TransientResourceManager transientResourceManager,
|
||||||
TransientConfigService transientConfigService, TextureCompressionService textureCompressionService,
|
TransientConfigService transientConfigService, TextureCompressionService textureCompressionService,
|
||||||
TextureMetadataHelper textureMetadataHelper)
|
TextureMetadataHelper textureMetadataHelper)
|
||||||
@@ -110,6 +122,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
|
|||||||
_characterAnalyzer = characterAnalyzer;
|
_characterAnalyzer = characterAnalyzer;
|
||||||
_ipcManager = ipcManager;
|
_ipcManager = ipcManager;
|
||||||
_uiSharedService = uiSharedService;
|
_uiSharedService = uiSharedService;
|
||||||
|
_configService = configService;
|
||||||
_playerPerformanceConfig = playerPerformanceConfig;
|
_playerPerformanceConfig = playerPerformanceConfig;
|
||||||
_transientResourceManager = transientResourceManager;
|
_transientResourceManager = transientResourceManager;
|
||||||
_transientConfigService = transientConfigService;
|
_transientConfigService = transientConfigService;
|
||||||
@@ -135,21 +148,30 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
|
|||||||
|
|
||||||
private void HandleConversionModal()
|
private void HandleConversionModal()
|
||||||
{
|
{
|
||||||
if (_conversionTask == null)
|
bool hasConversion = _conversionTask != null;
|
||||||
|
#if DEBUG
|
||||||
|
bool showDebug = _debugCompressionModalOpen && !hasConversion;
|
||||||
|
#else
|
||||||
|
const bool showDebug = false;
|
||||||
|
#endif
|
||||||
|
if (!hasConversion && !showDebug)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_conversionTask.IsCompleted)
|
if (hasConversion && _conversionTask!.IsCompleted)
|
||||||
{
|
{
|
||||||
ResetConversionModalState();
|
ResetConversionModalState();
|
||||||
return;
|
if (!showDebug)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_showModal = true;
|
_showModal = true;
|
||||||
if (ImGui.BeginPopupModal("Texture Compression in Progress", ImGuiWindowFlags.AlwaysAutoResize))
|
if (ImGui.BeginPopupModal("Texture Compression in Progress", UiSharedService.PopupWindowFlags))
|
||||||
{
|
{
|
||||||
DrawConversionModalContent();
|
DrawConversionModalContent(showDebug);
|
||||||
ImGui.EndPopup();
|
ImGui.EndPopup();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -164,31 +186,190 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawConversionModalContent()
|
private void DrawConversionModalContent(bool isDebugPreview)
|
||||||
{
|
{
|
||||||
var progress = _lastConversionProgress;
|
var scale = ImGuiHelpers.GlobalScale;
|
||||||
|
TextureConversionProgress? progress;
|
||||||
|
#if DEBUG
|
||||||
|
progress = isDebugPreview ? _debugConversionProgress : _lastConversionProgress;
|
||||||
|
#else
|
||||||
|
progress = _lastConversionProgress;
|
||||||
|
#endif
|
||||||
var total = progress?.Total ?? Math.Max(_conversionTotalJobs, 1);
|
var total = progress?.Total ?? Math.Max(_conversionTotalJobs, 1);
|
||||||
var completed = progress != null
|
var completed = progress != null
|
||||||
? Math.Min(progress.Completed + 1, total)
|
? Math.Clamp(progress.Completed + 1, 0, total)
|
||||||
: _conversionCurrentFileProgress;
|
: Math.Clamp(_conversionCurrentFileProgress, 0, total);
|
||||||
var currentLabel = !string.IsNullOrEmpty(_conversionCurrentFileName)
|
var percent = total > 0 ? Math.Clamp(completed / (float)total, 0f, 1f) : 0f;
|
||||||
? _conversionCurrentFileName
|
|
||||||
: "Preparing...";
|
|
||||||
|
|
||||||
ImGui.TextUnformatted($"Compressing textures ({completed}/{total})");
|
var job = progress?.CurrentJob;
|
||||||
UiSharedService.TextWrapped("Current file: " + currentLabel);
|
var inputPath = job?.InputFile ?? string.Empty;
|
||||||
|
var targetLabel = job != null ? job.TargetType.ToString() : "Unknown";
|
||||||
|
var currentLabel = !string.IsNullOrEmpty(inputPath)
|
||||||
|
? Path.GetFileName(inputPath)
|
||||||
|
: !string.IsNullOrEmpty(_conversionCurrentFileName) ? _conversionCurrentFileName : "Preparing...";
|
||||||
|
var mapKind = !string.IsNullOrEmpty(inputPath)
|
||||||
|
? _textureMetadataHelper.DetermineMapKind(inputPath)
|
||||||
|
: TextureMapKind.Unknown;
|
||||||
|
|
||||||
if (_conversionFailed)
|
var accent = UIColors.Get("LightlessPurple");
|
||||||
|
var accentBg = new Vector4(accent.X, accent.Y, accent.Z, 0.18f);
|
||||||
|
var accentBorder = new Vector4(accent.X, accent.Y, accent.Z, 0.4f);
|
||||||
|
var headerHeight = MathF.Max(ImGui.GetTextLineHeightWithSpacing() * 2.6f, 46f * scale);
|
||||||
|
|
||||||
|
using (ImRaii.PushStyle(ImGuiStyleVar.ChildRounding, 6f * scale))
|
||||||
|
using (ImRaii.PushStyle(ImGuiStyleVar.ChildBorderSize, MathF.Max(1f, ImGui.GetStyle().ChildBorderSize)))
|
||||||
|
using (ImRaii.PushStyle(ImGuiStyleVar.WindowPadding, new Vector2(12f * scale, 6f * scale)))
|
||||||
|
using (ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(12f * scale, 2f * scale)))
|
||||||
|
using (ImRaii.PushColor(ImGuiCol.ChildBg, UiSharedService.Color(accentBg)))
|
||||||
|
using (ImRaii.PushColor(ImGuiCol.Border, UiSharedService.Color(accentBorder)))
|
||||||
|
using (var header = ImRaii.Child("compressionHeader", new Vector2(-1f, headerHeight), true, ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse))
|
||||||
{
|
{
|
||||||
UiSharedService.ColorText("Conversion encountered errors. Please review the log for details.", ImGuiColors.DalamudRed);
|
if (header)
|
||||||
|
{
|
||||||
|
if (ImGui.BeginTable("compressionHeaderTable", 2,
|
||||||
|
ImGuiTableFlags.SizingStretchProp | ImGuiTableFlags.NoBordersInBody | ImGuiTableFlags.NoHostExtendX))
|
||||||
|
{
|
||||||
|
ImGui.TableNextRow();
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
DrawCompressionTitle(accent, scale);
|
||||||
|
|
||||||
|
var statusText = isDebugPreview ? "Preview mode" : "Working...";
|
||||||
|
var statusColor = isDebugPreview ? UIColors.Get("LightlessYellow") : ImGuiColors.DalamudGrey;
|
||||||
|
UiSharedService.ColorText(statusText, statusColor);
|
||||||
|
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
var progressText = $"{completed}/{total}";
|
||||||
|
var percentText = $"{percent * 100f:0}%";
|
||||||
|
var summaryText = $"{progressText} ({percentText})";
|
||||||
|
var summaryWidth = ImGui.CalcTextSize(summaryText).X;
|
||||||
|
ImGui.SetCursorPosX(ImGui.GetCursorPosX() + MathF.Max(0f, ImGui.GetColumnWidth() - summaryWidth));
|
||||||
|
UiSharedService.ColorText(summaryText, ImGuiColors.DalamudGrey);
|
||||||
|
|
||||||
|
ImGui.EndTable();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.StopCircle, "Cancel conversion"))
|
ImGuiHelpers.ScaledDummy(6);
|
||||||
|
|
||||||
|
using (ImRaii.PushStyle(ImGuiStyleVar.FrameRounding, 4f * scale))
|
||||||
|
using (ImRaii.PushStyle(ImGuiStyleVar.FramePadding, new Vector2(0f, 4f * scale)))
|
||||||
|
using (ImRaii.PushColor(ImGuiCol.FrameBg, UiSharedService.Color(new Vector4(0.15f, 0.15f, 0.18f, 1f))))
|
||||||
|
using (ImRaii.PushColor(ImGuiCol.PlotHistogram, UiSharedService.Color(accent)))
|
||||||
{
|
{
|
||||||
_conversionCancellationTokenSource.Cancel();
|
ImGui.ProgressBar(percent, new Vector2(-1f, 0f), $"{percent * 100f:0}%");
|
||||||
}
|
}
|
||||||
|
|
||||||
UiSharedService.SetScaledWindowSize(520);
|
ImGuiHelpers.ScaledDummy(6);
|
||||||
|
|
||||||
|
var infoAccent = UIColors.Get("LightlessBlue");
|
||||||
|
var infoBg = new Vector4(infoAccent.X, infoAccent.Y, infoAccent.Z, 0.12f);
|
||||||
|
var infoBorder = new Vector4(infoAccent.X, infoAccent.Y, infoAccent.Z, 0.32f);
|
||||||
|
const int detailRows = 3;
|
||||||
|
var detailHeight = MathF.Max(ImGui.GetTextLineHeightWithSpacing() * (detailRows + 1.2f), 72f * scale);
|
||||||
|
|
||||||
|
using (ImRaii.PushStyle(ImGuiStyleVar.ChildRounding, 5f * scale))
|
||||||
|
using (ImRaii.PushStyle(ImGuiStyleVar.ChildBorderSize, MathF.Max(1f, ImGui.GetStyle().ChildBorderSize)))
|
||||||
|
using (ImRaii.PushStyle(ImGuiStyleVar.WindowPadding, new Vector2(10f * scale, 6f * scale)))
|
||||||
|
using (ImRaii.PushColor(ImGuiCol.ChildBg, UiSharedService.Color(infoBg)))
|
||||||
|
using (ImRaii.PushColor(ImGuiCol.Border, UiSharedService.Color(infoBorder)))
|
||||||
|
using (var details = ImRaii.Child("compressionDetail", new Vector2(-1f, detailHeight), true, ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse))
|
||||||
|
{
|
||||||
|
if (details)
|
||||||
|
{
|
||||||
|
if (ImGui.BeginTable("compressionDetailTable", 2,
|
||||||
|
ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.NoBordersInBody | ImGuiTableFlags.PadOuterX))
|
||||||
|
{
|
||||||
|
DrawDetailRow("Current file", currentLabel, inputPath);
|
||||||
|
DrawDetailRow("Target format", targetLabel, null);
|
||||||
|
DrawDetailRow("Map type", mapKind.ToString(), null);
|
||||||
|
ImGui.EndTable();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_conversionFailed && !isDebugPreview)
|
||||||
|
{
|
||||||
|
ImGuiHelpers.ScaledDummy(4);
|
||||||
|
_uiSharedService.IconText(FontAwesomeIcon.ExclamationTriangle, ImGuiColors.DalamudRed);
|
||||||
|
ImGui.SameLine(0f, 6f * scale);
|
||||||
|
UiSharedService.TextWrapped("Conversion encountered errors. Please review the log for details.", color: ImGuiColors.DalamudRed);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGuiHelpers.ScaledDummy(6);
|
||||||
|
if (!isDebugPreview)
|
||||||
|
{
|
||||||
|
if (_uiSharedService.IconTextButton(FontAwesomeIcon.StopCircle, "Cancel conversion"))
|
||||||
|
{
|
||||||
|
_conversionCancellationTokenSource.Cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
#if DEBUG
|
||||||
|
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Times, "Close preview"))
|
||||||
|
{
|
||||||
|
CloseDebugCompressionModal();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
UiSharedService.SetScaledWindowSize(600);
|
||||||
|
|
||||||
|
void DrawDetailRow(string label, string value, string? tooltip)
|
||||||
|
{
|
||||||
|
ImGui.TableNextRow();
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudGrey))
|
||||||
|
{
|
||||||
|
ImGui.TextUnformatted(label);
|
||||||
|
}
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
ImGui.TextUnformatted(value);
|
||||||
|
if (!string.IsNullOrEmpty(tooltip))
|
||||||
|
{
|
||||||
|
UiSharedService.AttachToolTip(tooltip);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DrawCompressionTitle(Vector4 iconColor, float localScale)
|
||||||
|
{
|
||||||
|
const string title = "Texture Compression";
|
||||||
|
var spacing = 6f * localScale;
|
||||||
|
|
||||||
|
var iconText = FontAwesomeIcon.CompressArrowsAlt.ToIconString();
|
||||||
|
Vector2 iconSize;
|
||||||
|
using (_uiSharedService.IconFont.Push())
|
||||||
|
{
|
||||||
|
iconSize = ImGui.CalcTextSize(iconText);
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector2 titleSize;
|
||||||
|
using (_uiSharedService.MediumFont.Push())
|
||||||
|
{
|
||||||
|
titleSize = ImGui.CalcTextSize(title);
|
||||||
|
}
|
||||||
|
|
||||||
|
var lineHeight = MathF.Max(iconSize.Y, titleSize.Y);
|
||||||
|
var iconOffsetY = (lineHeight - iconSize.Y) / 2f;
|
||||||
|
var textOffsetY = (lineHeight - titleSize.Y) / 2f;
|
||||||
|
|
||||||
|
var start = ImGui.GetCursorScreenPos();
|
||||||
|
var drawList = ImGui.GetWindowDrawList();
|
||||||
|
|
||||||
|
using (_uiSharedService.IconFont.Push())
|
||||||
|
{
|
||||||
|
drawList.AddText(new Vector2(start.X, start.Y + iconOffsetY), UiSharedService.Color(iconColor), iconText);
|
||||||
|
}
|
||||||
|
|
||||||
|
using (_uiSharedService.MediumFont.Push())
|
||||||
|
{
|
||||||
|
var textPos = new Vector2(start.X + iconSize.X + spacing, start.Y + textOffsetY);
|
||||||
|
drawList.AddText(textPos, ImGui.GetColorU32(ImGuiCol.Text), title);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.Dummy(new Vector2(iconSize.X + spacing + titleSize.X, lineHeight));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ResetConversionModalState()
|
private void ResetConversionModalState()
|
||||||
@@ -202,6 +383,41 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
|
|||||||
_conversionTotalJobs = 0;
|
_conversionTotalJobs = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
private void OpenCompressionDebugModal()
|
||||||
|
{
|
||||||
|
if (_conversionTask != null && !_conversionTask.IsCompleted)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_debugCompressionModalOpen = true;
|
||||||
|
_debugConversionProgress = new TextureConversionProgress(
|
||||||
|
Completed: 3,
|
||||||
|
Total: 10,
|
||||||
|
CurrentJob: new TextureConversionJob(
|
||||||
|
@"C:\Lightless\Mods\Textures\example_diffuse.tex",
|
||||||
|
@"C:\Lightless\Mods\Textures\example_diffuse_bc7.tex",
|
||||||
|
Penumbra.Api.Enums.TextureType.Bc7Tex));
|
||||||
|
_showModal = true;
|
||||||
|
_modalOpen = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ResetDebugCompressionModalState()
|
||||||
|
{
|
||||||
|
_debugCompressionModalOpen = false;
|
||||||
|
_debugConversionProgress = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CloseDebugCompressionModal()
|
||||||
|
{
|
||||||
|
ResetDebugCompressionModalState();
|
||||||
|
_showModal = false;
|
||||||
|
_modalOpen = false;
|
||||||
|
ImGui.CloseCurrentPopup();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
private void RefreshAnalysisCache()
|
private void RefreshAnalysisCache()
|
||||||
{
|
{
|
||||||
if (!_hasUpdate)
|
if (!_hasUpdate)
|
||||||
@@ -757,6 +973,16 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
|
|||||||
ResetTextureFilters();
|
ResetTextureFilters();
|
||||||
InvalidateTextureRows();
|
InvalidateTextureRows();
|
||||||
_conversionFailed = false;
|
_conversionFailed = false;
|
||||||
|
#if DEBUG
|
||||||
|
ResetDebugCompressionModalState();
|
||||||
|
#endif
|
||||||
|
var savedFormatSort = _configService.Current.TextureFormatSortMode;
|
||||||
|
if (!Enum.IsDefined(typeof(TextureFormatSortMode), savedFormatSort))
|
||||||
|
{
|
||||||
|
savedFormatSort = TextureFormatSortMode.None;
|
||||||
|
}
|
||||||
|
|
||||||
|
SetTextureFormatSortMode(savedFormatSort, persist: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Dispose(bool disposing)
|
protected override void Dispose(bool disposing)
|
||||||
@@ -1955,6 +2181,17 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
InvalidateTextureRows();
|
InvalidateTextureRows();
|
||||||
}
|
}
|
||||||
|
#if DEBUG
|
||||||
|
ImGui.SameLine();
|
||||||
|
using (ImRaii.Disabled(conversionRunning || !UiSharedService.CtrlPressed()))
|
||||||
|
{
|
||||||
|
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Cog, "Preview popup (debug)", 200f * scale))
|
||||||
|
{
|
||||||
|
OpenCompressionDebugModal();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
UiSharedService.AttachToolTip("Hold CTRL to open the compression popup preview.");
|
||||||
|
#endif
|
||||||
|
|
||||||
TextureRow? lastSelected = null;
|
TextureRow? lastSelected = null;
|
||||||
using (var table = ImRaii.Table("textureDataTable", 9,
|
using (var table = ImRaii.Table("textureDataTable", 9,
|
||||||
@@ -1973,26 +2210,56 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
|
|||||||
ImGui.TableSetupColumn("Original", ImGuiTableColumnFlags.PreferSortDescending);
|
ImGui.TableSetupColumn("Original", ImGuiTableColumnFlags.PreferSortDescending);
|
||||||
ImGui.TableSetupColumn("Compressed", ImGuiTableColumnFlags.PreferSortDescending);
|
ImGui.TableSetupColumn("Compressed", ImGuiTableColumnFlags.PreferSortDescending);
|
||||||
ImGui.TableSetupScrollFreeze(0, 1);
|
ImGui.TableSetupScrollFreeze(0, 1);
|
||||||
ImGui.TableHeadersRow();
|
DrawTextureTableHeaderRow();
|
||||||
|
|
||||||
var targets = _textureCompressionService.SelectableTargets;
|
var targets = _textureCompressionService.SelectableTargets;
|
||||||
|
|
||||||
IEnumerable<TextureRow> orderedRows = rows;
|
IEnumerable<TextureRow> orderedRows = rows;
|
||||||
var sortSpecs = ImGui.TableGetSortSpecs();
|
var sortSpecs = ImGui.TableGetSortSpecs();
|
||||||
|
var sizeSortColumn = -1;
|
||||||
|
var sizeSortDirection = ImGuiSortDirection.Ascending;
|
||||||
if (sortSpecs.SpecsCount > 0)
|
if (sortSpecs.SpecsCount > 0)
|
||||||
{
|
{
|
||||||
var spec = sortSpecs.Specs[0];
|
var spec = sortSpecs.Specs[0];
|
||||||
orderedRows = spec.ColumnIndex switch
|
if (spec.ColumnIndex is 7 or 8)
|
||||||
{
|
{
|
||||||
7 => spec.SortDirection == ImGuiSortDirection.Ascending
|
sizeSortColumn = spec.ColumnIndex;
|
||||||
? rows.OrderBy(r => r.OriginalSize)
|
sizeSortDirection = spec.SortDirection;
|
||||||
: rows.OrderByDescending(r => r.OriginalSize),
|
}
|
||||||
8 => spec.SortDirection == ImGuiSortDirection.Ascending
|
}
|
||||||
? rows.OrderBy(r => r.CompressedSize)
|
|
||||||
: rows.OrderByDescending(r => r.CompressedSize),
|
|
||||||
_ => rows
|
|
||||||
};
|
|
||||||
|
|
||||||
|
var hasSizeSort = sizeSortColumn != -1;
|
||||||
|
var indexedRows = rows.Select((row, idx) => (row, idx));
|
||||||
|
|
||||||
|
if (_textureFormatSortMode != TextureFormatSortMode.None)
|
||||||
|
{
|
||||||
|
bool compressedFirst = _textureFormatSortMode == TextureFormatSortMode.CompressedFirst;
|
||||||
|
int GroupKey(TextureRow row) => row.IsAlreadyCompressed == compressedFirst ? 0 : 1;
|
||||||
|
long SizeKey(TextureRow row) => sizeSortColumn == 7 ? row.OriginalSize : row.CompressedSize;
|
||||||
|
|
||||||
|
var ordered = indexedRows.OrderBy(pair => GroupKey(pair.row));
|
||||||
|
if (hasSizeSort)
|
||||||
|
{
|
||||||
|
ordered = sizeSortDirection == ImGuiSortDirection.Ascending
|
||||||
|
? ordered.ThenBy(pair => SizeKey(pair.row))
|
||||||
|
: ordered.ThenByDescending(pair => SizeKey(pair.row));
|
||||||
|
}
|
||||||
|
|
||||||
|
orderedRows = ordered
|
||||||
|
.ThenBy(pair => pair.idx)
|
||||||
|
.Select(pair => pair.row);
|
||||||
|
}
|
||||||
|
else if (hasSizeSort)
|
||||||
|
{
|
||||||
|
long SizeKey(TextureRow row) => sizeSortColumn == 7 ? row.OriginalSize : row.CompressedSize;
|
||||||
|
|
||||||
|
orderedRows = sizeSortDirection == ImGuiSortDirection.Ascending
|
||||||
|
? indexedRows.OrderBy(pair => SizeKey(pair.row)).ThenBy(pair => pair.idx).Select(pair => pair.row)
|
||||||
|
: indexedRows.OrderByDescending(pair => SizeKey(pair.row)).ThenBy(pair => pair.idx).Select(pair => pair.row);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sortSpecs.SpecsCount > 0)
|
||||||
|
{
|
||||||
sortSpecs.SpecsDirty = false;
|
sortSpecs.SpecsDirty = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2034,6 +2301,79 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void DrawTextureTableHeaderRow()
|
||||||
|
{
|
||||||
|
ImGui.TableNextRow(ImGuiTableRowFlags.Headers);
|
||||||
|
|
||||||
|
DrawHeaderCell(0, "##select");
|
||||||
|
DrawHeaderCell(1, "Texture");
|
||||||
|
DrawHeaderCell(2, "Slot");
|
||||||
|
DrawHeaderCell(3, "Map");
|
||||||
|
DrawFormatHeaderCell();
|
||||||
|
DrawHeaderCell(5, "Recommended");
|
||||||
|
DrawHeaderCell(6, "Target");
|
||||||
|
DrawHeaderCell(7, "Original");
|
||||||
|
DrawHeaderCell(8, "Compressed");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void DrawHeaderCell(int columnIndex, string label)
|
||||||
|
{
|
||||||
|
ImGui.TableSetColumnIndex(columnIndex);
|
||||||
|
ImGui.TableHeader(label);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawFormatHeaderCell()
|
||||||
|
{
|
||||||
|
ImGui.TableSetColumnIndex(4);
|
||||||
|
ImGui.TableHeader(GetFormatHeaderLabel());
|
||||||
|
|
||||||
|
if (ImGui.IsItemClicked(ImGuiMouseButton.Left))
|
||||||
|
{
|
||||||
|
CycleTextureFormatSortMode();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
{
|
||||||
|
ImGui.SetTooltip("Click to cycle sort: normal, compressed first, uncompressed first.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetFormatHeaderLabel()
|
||||||
|
=> _textureFormatSortMode switch
|
||||||
|
{
|
||||||
|
TextureFormatSortMode.CompressedFirst => "Format (C)##formatHeader",
|
||||||
|
TextureFormatSortMode.UncompressedFirst => "Format (U)##formatHeader",
|
||||||
|
_ => "Format##formatHeader"
|
||||||
|
};
|
||||||
|
|
||||||
|
private void SetTextureFormatSortMode(TextureFormatSortMode mode, bool persist = true)
|
||||||
|
{
|
||||||
|
if (_textureFormatSortMode == mode)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_textureFormatSortMode = mode;
|
||||||
|
if (persist)
|
||||||
|
{
|
||||||
|
_configService.Current.TextureFormatSortMode = mode;
|
||||||
|
_configService.Save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CycleTextureFormatSortMode()
|
||||||
|
{
|
||||||
|
var nextMode = _textureFormatSortMode switch
|
||||||
|
{
|
||||||
|
TextureFormatSortMode.None => TextureFormatSortMode.CompressedFirst,
|
||||||
|
TextureFormatSortMode.CompressedFirst => TextureFormatSortMode.UncompressedFirst,
|
||||||
|
_ => TextureFormatSortMode.None
|
||||||
|
};
|
||||||
|
|
||||||
|
SetTextureFormatSortMode(nextMode);
|
||||||
|
}
|
||||||
|
|
||||||
private void StartTextureConversion()
|
private void StartTextureConversion()
|
||||||
{
|
{
|
||||||
if (_conversionTask != null && !_conversionTask.IsCompleted)
|
if (_conversionTask != null && !_conversionTask.IsCompleted)
|
||||||
@@ -2335,11 +2675,30 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
if (_texturePreviews.TryGetValue(key, out var state))
|
if (_texturePreviews.TryGetValue(key, out var state))
|
||||||
{
|
{
|
||||||
|
var loadTask = state.LoadTask;
|
||||||
|
if (loadTask is { IsCompleted: false })
|
||||||
|
{
|
||||||
|
_ = loadTask.ContinueWith(_ =>
|
||||||
|
{
|
||||||
|
state.Texture?.Dispose();
|
||||||
|
}, TaskScheduler.Default);
|
||||||
|
}
|
||||||
|
|
||||||
state.Texture?.Dispose();
|
state.Texture?.Dispose();
|
||||||
_texturePreviews.Remove(key);
|
_texturePreviews.Remove(key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ClearHoverPreview(TextureRow row)
|
||||||
|
{
|
||||||
|
if (string.Equals(_selectedTextureKey, row.Key, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ResetPreview(row.Key);
|
||||||
|
}
|
||||||
|
|
||||||
private TextureResolutionInfo? GetTextureResolution(TextureRow row)
|
private TextureResolutionInfo? GetTextureResolution(TextureRow row)
|
||||||
{
|
{
|
||||||
if (_textureResolutionCache.TryGetValue(row.Key, out var cached))
|
if (_textureResolutionCache.TryGetValue(row.Key, out var cached))
|
||||||
@@ -2440,7 +2799,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
|
|||||||
UiSharedService.AttachToolTip("Already stored in a compressed format; additional compression is disabled.");
|
UiSharedService.AttachToolTip("Already stored in a compressed format; additional compression is disabled.");
|
||||||
}
|
}
|
||||||
|
|
||||||
DrawSelectableColumn(isSelected, () =>
|
var nameHovered = DrawSelectableColumn(isSelected, () =>
|
||||||
{
|
{
|
||||||
var selectableLabel = $"{row.DisplayName}##texName{index}";
|
var selectableLabel = $"{row.DisplayName}##texName{index}";
|
||||||
if (ImGui.Selectable(selectableLabel, isSelected))
|
if (ImGui.Selectable(selectableLabel, isSelected))
|
||||||
@@ -2448,20 +2807,20 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
|
|||||||
_selectedTextureKey = isSelected ? string.Empty : key;
|
_selectedTextureKey = isSelected ? string.Empty : key;
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => UiSharedService.AttachToolTip($"{row.PrimaryFilePath}{UiSharedService.TooltipSeparator}{string.Join(Environment.NewLine, row.GamePaths)}");
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
DrawSelectableColumn(isSelected, () =>
|
_ = DrawSelectableColumn(isSelected, () =>
|
||||||
{
|
{
|
||||||
ImGui.TextUnformatted(row.Slot);
|
ImGui.TextUnformatted(row.Slot);
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
DrawSelectableColumn(isSelected, () =>
|
_ = DrawSelectableColumn(isSelected, () =>
|
||||||
{
|
{
|
||||||
ImGui.TextUnformatted(row.MapKind.ToString());
|
ImGui.TextUnformatted(row.MapKind.ToString());
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
DrawSelectableColumn(isSelected, () =>
|
_ = DrawSelectableColumn(isSelected, () =>
|
||||||
{
|
{
|
||||||
Action? tooltipAction = null;
|
Action? tooltipAction = null;
|
||||||
ImGui.TextUnformatted(row.Format);
|
ImGui.TextUnformatted(row.Format);
|
||||||
@@ -2475,7 +2834,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
|
|||||||
return tooltipAction;
|
return tooltipAction;
|
||||||
});
|
});
|
||||||
|
|
||||||
DrawSelectableColumn(isSelected, () =>
|
_ = DrawSelectableColumn(isSelected, () =>
|
||||||
{
|
{
|
||||||
if (row.SuggestedTarget.HasValue)
|
if (row.SuggestedTarget.HasValue)
|
||||||
{
|
{
|
||||||
@@ -2537,19 +2896,21 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
|
|||||||
UiSharedService.AttachToolTip("This texture is already compressed and cannot be processed again.");
|
UiSharedService.AttachToolTip("This texture is already compressed and cannot be processed again.");
|
||||||
}
|
}
|
||||||
|
|
||||||
DrawSelectableColumn(isSelected, () =>
|
_ = DrawSelectableColumn(isSelected, () =>
|
||||||
{
|
{
|
||||||
ImGui.TextUnformatted(UiSharedService.ByteToString(row.OriginalSize));
|
ImGui.TextUnformatted(UiSharedService.ByteToString(row.OriginalSize));
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
DrawSelectableColumn(isSelected, () =>
|
_ = DrawSelectableColumn(isSelected, () =>
|
||||||
{
|
{
|
||||||
ImGui.TextUnformatted(UiSharedService.ByteToString(row.CompressedSize));
|
ImGui.TextUnformatted(UiSharedService.ByteToString(row.CompressedSize));
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
DrawTextureRowHoverTooltip(row, nameHovered);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void DrawSelectableColumn(bool isSelected, Func<Action?> draw)
|
private static bool DrawSelectableColumn(bool isSelected, Func<Action?> draw)
|
||||||
{
|
{
|
||||||
ImGui.TableNextColumn();
|
ImGui.TableNextColumn();
|
||||||
if (isSelected)
|
if (isSelected)
|
||||||
@@ -2558,6 +2919,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
var after = draw();
|
var after = draw();
|
||||||
|
var hovered = ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled | ImGuiHoveredFlags.AllowWhenBlockedByActiveItem);
|
||||||
|
|
||||||
if (isSelected)
|
if (isSelected)
|
||||||
{
|
{
|
||||||
@@ -2565,6 +2927,127 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
after?.Invoke();
|
after?.Invoke();
|
||||||
|
return hovered;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawTextureRowHoverTooltip(TextureRow row, bool isHovered)
|
||||||
|
{
|
||||||
|
if (!isHovered)
|
||||||
|
{
|
||||||
|
if (string.Equals(_textureHoverKey, row.Key, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
_textureHoverKey = string.Empty;
|
||||||
|
_textureHoverStartTime = 0;
|
||||||
|
ClearHoverPreview(row);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var now = ImGui.GetTime();
|
||||||
|
if (!string.Equals(_textureHoverKey, row.Key, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
_textureHoverKey = row.Key;
|
||||||
|
_textureHoverStartTime = now;
|
||||||
|
}
|
||||||
|
|
||||||
|
var elapsed = now - _textureHoverStartTime;
|
||||||
|
if (elapsed < TextureHoverPreviewDelaySeconds)
|
||||||
|
{
|
||||||
|
var progress = (float)Math.Clamp(elapsed / TextureHoverPreviewDelaySeconds, 0f, 1f);
|
||||||
|
DrawTextureRowTextTooltip(row, progress);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DrawTextureRowPreviewTooltip(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawTextureRowTextTooltip(TextureRow row, float progress)
|
||||||
|
{
|
||||||
|
ImGui.BeginTooltip();
|
||||||
|
ImGui.SetWindowFontScale(1f);
|
||||||
|
DrawTextureRowTooltipBody(row);
|
||||||
|
ImGuiHelpers.ScaledDummy(4);
|
||||||
|
DrawTextureHoverProgressBar(progress, GetTooltipContentWidth());
|
||||||
|
ImGui.EndTooltip();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawTextureRowPreviewTooltip(TextureRow row)
|
||||||
|
{
|
||||||
|
ImGui.BeginTooltip();
|
||||||
|
ImGui.SetWindowFontScale(1f);
|
||||||
|
|
||||||
|
DrawTextureRowTooltipBody(row);
|
||||||
|
ImGuiHelpers.ScaledDummy(4);
|
||||||
|
|
||||||
|
var previewSize = new Vector2(TextureHoverPreviewSize * ImGuiHelpers.GlobalScale);
|
||||||
|
var (previewTexture, previewLoading, previewError) = GetTexturePreview(row);
|
||||||
|
if (previewTexture != null)
|
||||||
|
{
|
||||||
|
ImGui.Image(previewTexture.Handle, previewSize);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
using (ImRaii.Child("textureHoverPreview", previewSize, true))
|
||||||
|
{
|
||||||
|
UiSharedService.TextWrapped(previewLoading ? "Loading preview..." : previewError ?? "Preview unavailable.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui.EndTooltip();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void DrawTextureRowTooltipBody(TextureRow row)
|
||||||
|
{
|
||||||
|
var text = row.GamePaths.Count > 0
|
||||||
|
? $"{row.PrimaryFilePath}{UiSharedService.TooltipSeparator}{string.Join(Environment.NewLine, row.GamePaths)}"
|
||||||
|
: row.PrimaryFilePath;
|
||||||
|
|
||||||
|
var wrapWidth = GetTextureHoverTooltipWidth();
|
||||||
|
ImGui.PushTextWrapPos(ImGui.GetCursorPosX() + wrapWidth);
|
||||||
|
if (text.Contains(UiSharedService.TooltipSeparator, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
var splitText = text.Split(UiSharedService.TooltipSeparator, StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
for (int i = 0; i < splitText.Length; i++)
|
||||||
|
{
|
||||||
|
ImGui.TextUnformatted(splitText[i]);
|
||||||
|
if (i != splitText.Length - 1)
|
||||||
|
{
|
||||||
|
ImGui.Separator();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ImGui.TextUnformatted(text);
|
||||||
|
}
|
||||||
|
ImGui.PopTextWrapPos();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void DrawTextureHoverProgressBar(float progress, float width)
|
||||||
|
{
|
||||||
|
var scale = ImGuiHelpers.GlobalScale;
|
||||||
|
var barHeight = 4f * scale;
|
||||||
|
var barWidth = width > 0f ? width : -1f;
|
||||||
|
using (ImRaii.PushStyle(ImGuiStyleVar.FrameRounding, 3f * scale))
|
||||||
|
using (ImRaii.PushStyle(ImGuiStyleVar.FramePadding, Vector2.Zero))
|
||||||
|
using (ImRaii.PushColor(ImGuiCol.PlotHistogram, UiSharedService.Color(UIColors.Get("LightlessPurple"))))
|
||||||
|
{
|
||||||
|
ImGui.ProgressBar(progress, new Vector2(barWidth, barHeight), string.Empty);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static float GetTextureHoverTooltipWidth()
|
||||||
|
=> ImGui.GetFontSize() * 35f;
|
||||||
|
|
||||||
|
private static float GetTooltipContentWidth()
|
||||||
|
{
|
||||||
|
var min = ImGui.GetWindowContentRegionMin();
|
||||||
|
var max = ImGui.GetWindowContentRegionMax();
|
||||||
|
var width = max.X - min.X;
|
||||||
|
if (width <= 0f)
|
||||||
|
{
|
||||||
|
width = ImGui.GetContentRegionAvail().X;
|
||||||
|
}
|
||||||
|
return width;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ApplyTextureRowBackground(TextureRow row, bool isSelected)
|
private static void ApplyTextureRowBackground(TextureRow row, bool isSelected)
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ namespace LightlessSync.UI;
|
|||||||
public class DownloadUi : WindowMediatorSubscriberBase
|
public class DownloadUi : WindowMediatorSubscriberBase
|
||||||
{
|
{
|
||||||
private readonly LightlessConfigService _configService;
|
private readonly LightlessConfigService _configService;
|
||||||
private readonly ConcurrentDictionary<GameObjectHandler, Dictionary<string, FileDownloadStatus>> _currentDownloads = new();
|
private readonly ConcurrentDictionary<GameObjectHandler, IReadOnlyDictionary<string, FileDownloadStatus>> _currentDownloads = new();
|
||||||
private readonly DalamudUtilService _dalamudUtilService;
|
private readonly DalamudUtilService _dalamudUtilService;
|
||||||
private readonly FileUploadManager _fileTransferManager;
|
private readonly FileUploadManager _fileTransferManager;
|
||||||
private readonly UiSharedService _uiShared;
|
private readonly UiSharedService _uiShared;
|
||||||
@@ -25,6 +25,8 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
|||||||
private readonly ConcurrentDictionary<GameObjectHandler, bool> _uploadingPlayers = new();
|
private readonly ConcurrentDictionary<GameObjectHandler, bool> _uploadingPlayers = new();
|
||||||
private readonly Dictionary<GameObjectHandler, Vector2> _smoothed = [];
|
private readonly Dictionary<GameObjectHandler, Vector2> _smoothed = [];
|
||||||
private readonly Dictionary<GameObjectHandler, DownloadSpeedTracker> _downloadSpeeds = [];
|
private readonly Dictionary<GameObjectHandler, DownloadSpeedTracker> _downloadSpeeds = [];
|
||||||
|
private readonly Dictionary<GameObjectHandler, (int TotalFiles, long TotalBytes)> _downloadInitialTotals = [];
|
||||||
|
|
||||||
|
|
||||||
private byte _transferBoxTransparency = 100;
|
private byte _transferBoxTransparency = 100;
|
||||||
private bool _notificationDismissed = true;
|
private bool _notificationDismissed = true;
|
||||||
@@ -63,9 +65,15 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
|||||||
|
|
||||||
IsOpen = true;
|
IsOpen = true;
|
||||||
|
|
||||||
Mediator.Subscribe<DownloadStartedMessage>(this, (msg) =>
|
Mediator.Subscribe<DownloadStartedMessage>(this, msg =>
|
||||||
{
|
{
|
||||||
_currentDownloads[msg.DownloadId] = msg.DownloadStatus;
|
_currentDownloads[msg.DownloadId] = msg.DownloadStatus;
|
||||||
|
|
||||||
|
var snap = msg.DownloadStatus.ToArray();
|
||||||
|
var totalFiles = snap.Sum(kv => kv.Value?.TotalFiles ?? 0);
|
||||||
|
var totalBytes = snap.Sum(kv => kv.Value?.TotalBytes ?? 0);
|
||||||
|
|
||||||
|
_downloadInitialTotals[msg.DownloadId] = (totalFiles, totalBytes);
|
||||||
_notificationDismissed = false;
|
_notificationDismissed = false;
|
||||||
});
|
});
|
||||||
Mediator.Subscribe<DownloadFinishedMessage>(this, (msg) =>
|
Mediator.Subscribe<DownloadFinishedMessage>(this, (msg) =>
|
||||||
@@ -73,7 +81,7 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
|||||||
_currentDownloads.TryRemove(msg.DownloadId, out _);
|
_currentDownloads.TryRemove(msg.DownloadId, out _);
|
||||||
|
|
||||||
// Dismiss notification if all downloads are complete
|
// Dismiss notification if all downloads are complete
|
||||||
if (!_currentDownloads.Any() && !_notificationDismissed)
|
if (_currentDownloads.IsEmpty && !_notificationDismissed)
|
||||||
{
|
{
|
||||||
Mediator.Publish(new LightlessNotificationDismissMessage("pair_download_progress"));
|
Mediator.Publish(new LightlessNotificationDismissMessage("pair_download_progress"));
|
||||||
_notificationDismissed = true;
|
_notificationDismissed = true;
|
||||||
@@ -164,10 +172,10 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
|||||||
const float rounding = 6f;
|
const float rounding = 6f;
|
||||||
var shadowOffset = new Vector2(2, 2);
|
var shadowOffset = new Vector2(2, 2);
|
||||||
|
|
||||||
List<KeyValuePair<GameObjectHandler, Dictionary<string, FileDownloadStatus>>> transfers;
|
List<KeyValuePair<GameObjectHandler, IReadOnlyDictionary<string, FileDownloadStatus>>> transfers;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
transfers = _currentDownloads.ToList();
|
transfers = [.. _currentDownloads];
|
||||||
}
|
}
|
||||||
catch (ArgumentException)
|
catch (ArgumentException)
|
||||||
{
|
{
|
||||||
@@ -206,12 +214,16 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
|||||||
var dlQueue = 0;
|
var dlQueue = 0;
|
||||||
var dlProg = 0;
|
var dlProg = 0;
|
||||||
var dlDecomp = 0;
|
var dlDecomp = 0;
|
||||||
|
var dlComplete = 0;
|
||||||
|
|
||||||
foreach (var entry in transfer.Value)
|
foreach (var entry in transfer.Value)
|
||||||
{
|
{
|
||||||
var fileStatus = entry.Value;
|
var fileStatus = entry.Value;
|
||||||
switch (fileStatus.DownloadStatus)
|
switch (fileStatus.DownloadStatus)
|
||||||
{
|
{
|
||||||
|
case DownloadStatus.Initializing:
|
||||||
|
dlQueue++;
|
||||||
|
break;
|
||||||
case DownloadStatus.WaitingForSlot:
|
case DownloadStatus.WaitingForSlot:
|
||||||
dlSlot++;
|
dlSlot++;
|
||||||
break;
|
break;
|
||||||
@@ -224,15 +236,20 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
|||||||
case DownloadStatus.Decompressing:
|
case DownloadStatus.Decompressing:
|
||||||
dlDecomp++;
|
dlDecomp++;
|
||||||
break;
|
break;
|
||||||
|
case DownloadStatus.Completed:
|
||||||
|
dlComplete++;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var isAllComplete = dlComplete > 0 && dlProg == 0 && dlDecomp == 0 && dlQueue == 0 && dlSlot == 0;
|
||||||
|
|
||||||
string statusText;
|
string statusText;
|
||||||
if (dlProg > 0)
|
if (dlProg > 0)
|
||||||
{
|
{
|
||||||
statusText = "Downloading";
|
statusText = "Downloading";
|
||||||
}
|
}
|
||||||
else if (dlDecomp > 0 || (totalBytes > 0 && transferredBytes >= totalBytes))
|
else if (dlDecomp > 0)
|
||||||
{
|
{
|
||||||
statusText = "Decompressing";
|
statusText = "Decompressing";
|
||||||
}
|
}
|
||||||
@@ -244,6 +261,10 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
statusText = "Waiting for slot";
|
statusText = "Waiting for slot";
|
||||||
}
|
}
|
||||||
|
else if (isAllComplete)
|
||||||
|
{
|
||||||
|
statusText = "Completed";
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
statusText = "Waiting";
|
statusText = "Waiting";
|
||||||
@@ -309,7 +330,7 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
|||||||
fillPercent = transferredBytes / (double)totalBytes;
|
fillPercent = transferredBytes / (double)totalBytes;
|
||||||
showFill = true;
|
showFill = true;
|
||||||
}
|
}
|
||||||
else if (dlDecomp > 0 || transferredBytes >= totalBytes)
|
else if (dlDecomp > 0 || dlComplete > 0 || transferredBytes >= totalBytes)
|
||||||
{
|
{
|
||||||
fillPercent = 1.0;
|
fillPercent = 1.0;
|
||||||
showFill = true;
|
showFill = true;
|
||||||
@@ -341,10 +362,14 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
|||||||
downloadText =
|
downloadText =
|
||||||
$"{statusText} {UiSharedService.ByteToString(transferredBytes, addSuffix: false)}/{UiSharedService.ByteToString(totalBytes)}";
|
$"{statusText} {UiSharedService.ByteToString(transferredBytes, addSuffix: false)}/{UiSharedService.ByteToString(totalBytes)}";
|
||||||
}
|
}
|
||||||
else if ((dlDecomp > 0 || transferredBytes >= totalBytes) && hasValidSize)
|
else if (dlDecomp > 0)
|
||||||
{
|
{
|
||||||
downloadText = "Decompressing";
|
downloadText = "Decompressing";
|
||||||
}
|
}
|
||||||
|
else if (isAllComplete)
|
||||||
|
{
|
||||||
|
downloadText = "Completed";
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Waiting states
|
// Waiting states
|
||||||
@@ -417,6 +442,7 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
|||||||
var totalDlQueue = 0;
|
var totalDlQueue = 0;
|
||||||
var totalDlProg = 0;
|
var totalDlProg = 0;
|
||||||
var totalDlDecomp = 0;
|
var totalDlDecomp = 0;
|
||||||
|
var totalDlComplete = 0;
|
||||||
|
|
||||||
var perPlayer = new List<(
|
var perPlayer = new List<(
|
||||||
string Name,
|
string Name,
|
||||||
@@ -428,16 +454,21 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
|||||||
int DlSlot,
|
int DlSlot,
|
||||||
int DlQueue,
|
int DlQueue,
|
||||||
int DlProg,
|
int DlProg,
|
||||||
int DlDecomp)>();
|
int DlDecomp,
|
||||||
|
int DlComplete)>();
|
||||||
|
|
||||||
foreach (var transfer in _currentDownloads)
|
foreach (var transfer in _currentDownloads)
|
||||||
{
|
{
|
||||||
var handler = transfer.Key;
|
var handler = transfer.Key;
|
||||||
var statuses = transfer.Value.Values;
|
var statuses = transfer.Value.Values;
|
||||||
|
|
||||||
var playerTotalFiles = statuses.Sum(s => s.TotalFiles);
|
var (playerTotalFiles, playerTotalBytes) = _downloadInitialTotals.TryGetValue(handler, out var totals)
|
||||||
var playerTransferredFiles = statuses.Sum(s => s.TransferredFiles);
|
? totals
|
||||||
var playerTotalBytes = statuses.Sum(s => s.TotalBytes);
|
: (statuses.Sum(s => s.TotalFiles), statuses.Sum(s => s.TotalBytes));
|
||||||
|
|
||||||
|
var playerTransferredFiles = statuses.Count(s =>
|
||||||
|
s.DownloadStatus == DownloadStatus.Decompressing ||
|
||||||
|
s.TransferredBytes >= s.TotalBytes);
|
||||||
var playerTransferredBytes = statuses.Sum(s => s.TransferredBytes);
|
var playerTransferredBytes = statuses.Sum(s => s.TransferredBytes);
|
||||||
|
|
||||||
totalFiles += playerTotalFiles;
|
totalFiles += playerTotalFiles;
|
||||||
@@ -445,25 +476,27 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
|||||||
totalBytes += playerTotalBytes;
|
totalBytes += playerTotalBytes;
|
||||||
transferredBytes += playerTransferredBytes;
|
transferredBytes += playerTransferredBytes;
|
||||||
|
|
||||||
// per-player W/Q/P/D
|
// per-player W/Q/P/D/C
|
||||||
var playerDlSlot = 0;
|
var playerDlSlot = 0;
|
||||||
var playerDlQueue = 0;
|
var playerDlQueue = 0;
|
||||||
var playerDlProg = 0;
|
var playerDlProg = 0;
|
||||||
var playerDlDecomp = 0;
|
var playerDlDecomp = 0;
|
||||||
|
var playerDlComplete = 0;
|
||||||
|
|
||||||
foreach (var entry in transfer.Value)
|
foreach (var entry in transfer.Value)
|
||||||
{
|
{
|
||||||
var fileStatus = entry.Value;
|
var fileStatus = entry.Value;
|
||||||
switch (fileStatus.DownloadStatus)
|
switch (fileStatus.DownloadStatus)
|
||||||
{
|
{
|
||||||
case DownloadStatus.WaitingForSlot:
|
case DownloadStatus.Initializing:
|
||||||
playerDlSlot++;
|
|
||||||
totalDlSlot++;
|
|
||||||
break;
|
|
||||||
case DownloadStatus.WaitingForQueue:
|
case DownloadStatus.WaitingForQueue:
|
||||||
playerDlQueue++;
|
playerDlQueue++;
|
||||||
totalDlQueue++;
|
totalDlQueue++;
|
||||||
break;
|
break;
|
||||||
|
case DownloadStatus.WaitingForSlot:
|
||||||
|
playerDlSlot++;
|
||||||
|
totalDlSlot++;
|
||||||
|
break;
|
||||||
case DownloadStatus.Downloading:
|
case DownloadStatus.Downloading:
|
||||||
playerDlProg++;
|
playerDlProg++;
|
||||||
totalDlProg++;
|
totalDlProg++;
|
||||||
@@ -472,6 +505,10 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
|||||||
playerDlDecomp++;
|
playerDlDecomp++;
|
||||||
totalDlDecomp++;
|
totalDlDecomp++;
|
||||||
break;
|
break;
|
||||||
|
case DownloadStatus.Completed:
|
||||||
|
playerDlComplete++;
|
||||||
|
totalDlComplete++;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -497,7 +534,8 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
|||||||
playerDlSlot,
|
playerDlSlot,
|
||||||
playerDlQueue,
|
playerDlQueue,
|
||||||
playerDlProg,
|
playerDlProg,
|
||||||
playerDlDecomp
|
playerDlDecomp,
|
||||||
|
playerDlComplete
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -511,17 +549,12 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
|||||||
if (totalFiles == 0 || totalBytes == 0)
|
if (totalFiles == 0 || totalBytes == 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// max speed for per-player bar scale (clamped)
|
|
||||||
double maxSpeed = perPlayer.Count > 0 ? perPlayer.Max(p => p.SpeedBytesPerSecond) : 0;
|
|
||||||
if (maxSpeed <= 0)
|
|
||||||
maxSpeed = 1;
|
|
||||||
|
|
||||||
var drawList = ImGui.GetBackgroundDrawList();
|
var drawList = ImGui.GetBackgroundDrawList();
|
||||||
var windowPos = ImGui.GetWindowPos();
|
var windowPos = ImGui.GetWindowPos();
|
||||||
|
|
||||||
// Overall texts
|
// Overall texts
|
||||||
var headerText =
|
var headerText =
|
||||||
$"Downloading {transferredFiles}/{totalFiles} files [W:{totalDlSlot}/Q:{totalDlQueue}/P:{totalDlProg}/D:{totalDlDecomp}]";
|
$"Downloading {transferredFiles}/{totalFiles} files [W:{totalDlSlot}/Q:{totalDlQueue}/P:{totalDlProg}/D:{totalDlDecomp}/C:{totalDlComplete}]";
|
||||||
|
|
||||||
var bytesText =
|
var bytesText =
|
||||||
$"{UiSharedService.ByteToString(transferredBytes, addSuffix: false)}/{UiSharedService.ByteToString(totalBytes)}";
|
$"{UiSharedService.ByteToString(transferredBytes, addSuffix: false)}/{UiSharedService.ByteToString(totalBytes)}";
|
||||||
@@ -544,7 +577,7 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
|||||||
foreach (var p in perPlayer)
|
foreach (var p in perPlayer)
|
||||||
{
|
{
|
||||||
var line =
|
var line =
|
||||||
$"{p.Name} [W:{p.DlSlot}/Q:{p.DlQueue}/P:{p.DlProg}/D:{p.DlDecomp}] {p.TransferredFiles}/{p.TotalFiles}";
|
$"{p.Name} [W:{p.DlSlot}/Q:{p.DlQueue}/P:{p.DlProg}/D:{p.DlDecomp}/C:{p.DlComplete}] {p.TransferredFiles}/{p.TotalFiles}";
|
||||||
|
|
||||||
var lineSize = ImGui.CalcTextSize(line);
|
var lineSize = ImGui.CalcTextSize(line);
|
||||||
if (lineSize.X > contentWidth)
|
if (lineSize.X > contentWidth)
|
||||||
@@ -662,7 +695,7 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
|||||||
&& p.TransferredBytes > 0;
|
&& p.TransferredBytes > 0;
|
||||||
|
|
||||||
var labelLine =
|
var labelLine =
|
||||||
$"{p.Name} [W:{p.DlSlot}/Q:{p.DlQueue}/P:{p.DlProg}/D:{p.DlDecomp}] {p.TransferredFiles}/{p.TotalFiles}";
|
$"{p.Name} [W:{p.DlSlot}/Q:{p.DlQueue}/P:{p.DlProg}/D:{p.DlDecomp}/C:{p.DlComplete}] {p.TransferredFiles}/{p.TotalFiles}";
|
||||||
|
|
||||||
if (!showBar)
|
if (!showBar)
|
||||||
{
|
{
|
||||||
@@ -721,13 +754,18 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
|||||||
// Text inside bar: downloading vs decompressing
|
// Text inside bar: downloading vs decompressing
|
||||||
string barText;
|
string barText;
|
||||||
|
|
||||||
var isDecompressing = p.DlDecomp > 0 && p.TransferredBytes >= p.TotalBytes && p.TotalBytes > 0;
|
var isDecompressing = p.DlDecomp > 0;
|
||||||
|
var isAllComplete = p.DlComplete > 0 && p.DlProg == 0 && p.DlDecomp == 0 && p.DlQueue == 0 && p.DlSlot == 0;
|
||||||
|
|
||||||
if (isDecompressing)
|
if (isDecompressing)
|
||||||
{
|
{
|
||||||
// Keep bar full, static text showing decompressing
|
// Keep bar full, static text showing decompressing
|
||||||
barText = "Decompressing...";
|
barText = "Decompressing...";
|
||||||
}
|
}
|
||||||
|
else if (isAllComplete)
|
||||||
|
{
|
||||||
|
barText = "Completed";
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var bytesInside =
|
var bytesInside =
|
||||||
@@ -808,6 +846,7 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
|||||||
var dlQueue = 0;
|
var dlQueue = 0;
|
||||||
var dlProg = 0;
|
var dlProg = 0;
|
||||||
var dlDecomp = 0;
|
var dlDecomp = 0;
|
||||||
|
var dlComplete = 0;
|
||||||
long totalBytes = 0;
|
long totalBytes = 0;
|
||||||
long transferredBytes = 0;
|
long transferredBytes = 0;
|
||||||
|
|
||||||
@@ -817,22 +856,29 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
|||||||
var fileStatus = entry.Value;
|
var fileStatus = entry.Value;
|
||||||
switch (fileStatus.DownloadStatus)
|
switch (fileStatus.DownloadStatus)
|
||||||
{
|
{
|
||||||
|
case DownloadStatus.Initializing: dlQueue++; break;
|
||||||
case DownloadStatus.WaitingForSlot: dlSlot++; break;
|
case DownloadStatus.WaitingForSlot: dlSlot++; break;
|
||||||
case DownloadStatus.WaitingForQueue: dlQueue++; break;
|
case DownloadStatus.WaitingForQueue: dlQueue++; break;
|
||||||
case DownloadStatus.Downloading: dlProg++; break;
|
case DownloadStatus.Downloading: dlProg++; break;
|
||||||
case DownloadStatus.Decompressing: dlDecomp++; break;
|
case DownloadStatus.Decompressing: dlDecomp++; break;
|
||||||
|
case DownloadStatus.Completed: dlComplete++; break;
|
||||||
}
|
}
|
||||||
totalBytes += fileStatus.TotalBytes;
|
totalBytes += fileStatus.TotalBytes;
|
||||||
transferredBytes += fileStatus.TransferredBytes;
|
transferredBytes += fileStatus.TransferredBytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
var progress = totalBytes > 0 ? (float)transferredBytes / totalBytes : 0f;
|
var progress = totalBytes > 0 ? (float)transferredBytes / totalBytes : 0f;
|
||||||
|
if (dlComplete > 0 && dlProg == 0 && dlDecomp == 0 && dlQueue == 0 && dlSlot == 0)
|
||||||
|
{
|
||||||
|
progress = 1f;
|
||||||
|
}
|
||||||
|
|
||||||
string status;
|
string status;
|
||||||
if (dlDecomp > 0) status = "decompressing";
|
if (dlDecomp > 0) status = "decompressing";
|
||||||
else if (dlProg > 0) status = "downloading";
|
else if (dlProg > 0) status = "downloading";
|
||||||
else if (dlQueue > 0) status = "queued";
|
else if (dlQueue > 0) status = "queued";
|
||||||
else if (dlSlot > 0) status = "waiting";
|
else if (dlSlot > 0) status = "waiting";
|
||||||
|
else if (dlComplete > 0) status = "completed";
|
||||||
else status = "completed";
|
else status = "completed";
|
||||||
|
|
||||||
downloadStatus.Add((item.Key.Name, progress, status));
|
downloadStatus.Add((item.Key.Name, progress, status));
|
||||||
|
|||||||
@@ -217,6 +217,7 @@ public class DrawEntityFactory
|
|||||||
entry.PairStatus,
|
entry.PairStatus,
|
||||||
handler?.LastAppliedDataBytes ?? -1,
|
handler?.LastAppliedDataBytes ?? -1,
|
||||||
handler?.LastAppliedDataTris ?? -1,
|
handler?.LastAppliedDataTris ?? -1,
|
||||||
|
handler?.LastAppliedApproximateEffectiveTris ?? -1,
|
||||||
handler?.LastAppliedApproximateVRAMBytes ?? -1,
|
handler?.LastAppliedApproximateVRAMBytes ?? -1,
|
||||||
handler?.LastAppliedApproximateEffectiveVRAMBytes ?? -1,
|
handler?.LastAppliedApproximateEffectiveVRAMBytes ?? -1,
|
||||||
handler);
|
handler);
|
||||||
|
|||||||
@@ -103,10 +103,19 @@ public sealed class DtrEntry : IDisposable, IHostedService
|
|||||||
|
|
||||||
public async Task StopAsync(CancellationToken cancellationToken)
|
public async Task StopAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
await _cancellationTokenSource.CancelAsync().ConfigureAwait(false);
|
_cancellationTokenSource.Cancel();
|
||||||
|
|
||||||
|
if (_dalamudUtilService.IsOnFrameworkThread)
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Skipping Lightfinder DTR wait on framework thread during shutdown.");
|
||||||
|
_cancellationTokenSource.Dispose();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await _runTask!.ConfigureAwait(false);
|
if (_runTask != null)
|
||||||
|
await _runTask.ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -415,7 +415,9 @@ public class IdDisplayHandler
|
|||||||
var vramBytes = pair.LastAppliedApproximateEffectiveVRAMBytes >= 0
|
var vramBytes = pair.LastAppliedApproximateEffectiveVRAMBytes >= 0
|
||||||
? pair.LastAppliedApproximateEffectiveVRAMBytes
|
? pair.LastAppliedApproximateEffectiveVRAMBytes
|
||||||
: pair.LastAppliedApproximateVRAMBytes;
|
: pair.LastAppliedApproximateVRAMBytes;
|
||||||
var triangleCount = pair.LastAppliedDataTris;
|
var triangleCount = pair.LastAppliedApproximateEffectiveTris >= 0
|
||||||
|
? pair.LastAppliedApproximateEffectiveTris
|
||||||
|
: pair.LastAppliedDataTris;
|
||||||
if (vramBytes < 0 && triangleCount < 0)
|
if (vramBytes < 0 && triangleCount < 0)
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ public sealed record PairUiEntry(
|
|||||||
IndividualPairStatus? PairStatus,
|
IndividualPairStatus? PairStatus,
|
||||||
long LastAppliedDataBytes,
|
long LastAppliedDataBytes,
|
||||||
long LastAppliedDataTris,
|
long LastAppliedDataTris,
|
||||||
|
long LastAppliedApproximateEffectiveTris,
|
||||||
long LastAppliedApproximateVramBytes,
|
long LastAppliedApproximateVramBytes,
|
||||||
long LastAppliedApproximateEffectiveVramBytes,
|
long LastAppliedApproximateEffectiveVramBytes,
|
||||||
IPairHandlerAdapter? Handler)
|
IPairHandlerAdapter? Handler)
|
||||||
|
|||||||
8
LightlessSync/UI/Models/TextureFormatSortMode.cs
Normal file
8
LightlessSync/UI/Models/TextureFormatSortMode.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
namespace LightlessSync.UI.Models;
|
||||||
|
|
||||||
|
public enum TextureFormatSortMode
|
||||||
|
{
|
||||||
|
None = 0,
|
||||||
|
CompressedFirst = 1,
|
||||||
|
UncompressedFirst = 2
|
||||||
|
}
|
||||||
@@ -7,4 +7,5 @@ public enum VisiblePairSortMode
|
|||||||
EffectiveVramUsage = 2,
|
EffectiveVramUsage = 2,
|
||||||
TriangleCount = 3,
|
TriangleCount = 3,
|
||||||
PreferredDirectPairs = 4,
|
PreferredDirectPairs = 4,
|
||||||
|
EffectiveTriangleCount = 5,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using Dalamud.Interface.Colors;
|
|||||||
using Dalamud.Interface.Utility;
|
using Dalamud.Interface.Utility;
|
||||||
using Dalamud.Interface.Utility.Raii;
|
using Dalamud.Interface.Utility.Raii;
|
||||||
using Dalamud.Utility;
|
using Dalamud.Utility;
|
||||||
|
using Lifestream.Enums;
|
||||||
using LightlessSync.API.Data;
|
using LightlessSync.API.Data;
|
||||||
using LightlessSync.API.Data.Comparer;
|
using LightlessSync.API.Data.Comparer;
|
||||||
using LightlessSync.API.Data.Enum;
|
using LightlessSync.API.Data.Enum;
|
||||||
@@ -14,6 +15,7 @@ using LightlessSync.Interop.Ipc;
|
|||||||
using LightlessSync.LightlessConfiguration;
|
using LightlessSync.LightlessConfiguration;
|
||||||
using LightlessSync.LightlessConfiguration.Configurations;
|
using LightlessSync.LightlessConfiguration.Configurations;
|
||||||
using LightlessSync.LightlessConfiguration.Models;
|
using LightlessSync.LightlessConfiguration.Models;
|
||||||
|
using LightlessSync.PlayerData.Factories;
|
||||||
using LightlessSync.PlayerData.Handlers;
|
using LightlessSync.PlayerData.Handlers;
|
||||||
using LightlessSync.PlayerData.Pairs;
|
using LightlessSync.PlayerData.Pairs;
|
||||||
using LightlessSync.Services;
|
using LightlessSync.Services;
|
||||||
@@ -40,6 +42,7 @@ using System.Globalization;
|
|||||||
using System.Net.Http.Headers;
|
using System.Net.Http.Headers;
|
||||||
using System.Net.Http.Json;
|
using System.Net.Http.Json;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|
||||||
@@ -51,7 +54,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
private readonly CacheMonitor _cacheMonitor;
|
private readonly CacheMonitor _cacheMonitor;
|
||||||
private readonly LightlessConfigService _configService;
|
private readonly LightlessConfigService _configService;
|
||||||
private readonly UiThemeConfigService _themeConfigService;
|
private readonly UiThemeConfigService _themeConfigService;
|
||||||
private readonly ConcurrentDictionary<GameObjectHandler, Dictionary<string, FileDownloadStatus>> _currentDownloads = new();
|
private readonly ConcurrentDictionary<GameObjectHandler, IReadOnlyDictionary<string, FileDownloadStatus>> _currentDownloads = new();
|
||||||
private readonly DalamudUtilService _dalamudUtilService;
|
private readonly DalamudUtilService _dalamudUtilService;
|
||||||
private readonly HttpClient _httpClient;
|
private readonly HttpClient _httpClient;
|
||||||
private readonly FileCacheManager _fileCacheManager;
|
private readonly FileCacheManager _fileCacheManager;
|
||||||
@@ -107,8 +110,8 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
};
|
};
|
||||||
private readonly UiSharedService.TabOption<TransferSettingsTab>[] _transferTabOptions = new UiSharedService.TabOption<TransferSettingsTab>[2];
|
private readonly UiSharedService.TabOption<TransferSettingsTab>[] _transferTabOptions = new UiSharedService.TabOption<TransferSettingsTab>[2];
|
||||||
private readonly List<UiSharedService.TabOption<ServerSettingsTab>> _serverTabOptions = new(4);
|
private readonly List<UiSharedService.TabOption<ServerSettingsTab>> _serverTabOptions = new(4);
|
||||||
private readonly string[] _generalTreeNavOrder = new[]
|
private readonly string[] _generalTreeNavOrder =
|
||||||
{
|
[
|
||||||
"Import & Export",
|
"Import & Export",
|
||||||
"Popup & Auto Fill",
|
"Popup & Auto Fill",
|
||||||
"Behavior",
|
"Behavior",
|
||||||
@@ -118,7 +121,8 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
"Colors",
|
"Colors",
|
||||||
"Server Info Bar",
|
"Server Info Bar",
|
||||||
"Nameplate",
|
"Nameplate",
|
||||||
};
|
"Animation & Bones"
|
||||||
|
];
|
||||||
private static readonly HashSet<string> _generalNavSeparatorAfter = new(StringComparer.Ordinal)
|
private static readonly HashSet<string> _generalNavSeparatorAfter = new(StringComparer.Ordinal)
|
||||||
{
|
{
|
||||||
"Popup & Auto Fill",
|
"Popup & Auto Fill",
|
||||||
@@ -580,6 +584,94 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void DrawTriangleDecimationCounters()
|
||||||
|
{
|
||||||
|
HashSet<Pair> trackedPairs = new();
|
||||||
|
|
||||||
|
var snapshot = _pairUiService.GetSnapshot();
|
||||||
|
|
||||||
|
foreach (var pair in snapshot.DirectPairs)
|
||||||
|
{
|
||||||
|
trackedPairs.Add(pair);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var group in snapshot.GroupPairs.Values)
|
||||||
|
{
|
||||||
|
foreach (var pair in group)
|
||||||
|
{
|
||||||
|
trackedPairs.Add(pair);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
long totalOriginalTris = 0;
|
||||||
|
long totalEffectiveTris = 0;
|
||||||
|
var hasData = false;
|
||||||
|
|
||||||
|
foreach (var pair in trackedPairs)
|
||||||
|
{
|
||||||
|
if (!pair.IsVisible)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var original = pair.LastAppliedDataTris;
|
||||||
|
var effective = pair.LastAppliedApproximateEffectiveTris;
|
||||||
|
|
||||||
|
if (original >= 0)
|
||||||
|
{
|
||||||
|
hasData = true;
|
||||||
|
totalOriginalTris += original;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (effective >= 0)
|
||||||
|
{
|
||||||
|
hasData = true;
|
||||||
|
totalEffectiveTris += effective;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasData)
|
||||||
|
{
|
||||||
|
ImGui.TextDisabled("Triangle usage has not been calculated yet.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var savedTris = Math.Max(0L, totalOriginalTris - totalEffectiveTris);
|
||||||
|
var originalText = FormatTriangleCount(totalOriginalTris);
|
||||||
|
var effectiveText = FormatTriangleCount(totalEffectiveTris);
|
||||||
|
var savedText = FormatTriangleCount(savedTris);
|
||||||
|
|
||||||
|
ImGui.TextUnformatted($"Total triangle usage (original): {originalText}");
|
||||||
|
ImGui.TextUnformatted($"Total triangle usage (effective): {effectiveText}");
|
||||||
|
|
||||||
|
if (savedTris > 0)
|
||||||
|
{
|
||||||
|
UiSharedService.ColorText($"Triangles saved by decimation: {savedText}", UIColors.Get("LightlessGreen"));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ImGui.TextUnformatted($"Triangles saved by decimation: {savedText}");
|
||||||
|
}
|
||||||
|
|
||||||
|
static string FormatTriangleCount(long triangleCount)
|
||||||
|
{
|
||||||
|
if (triangleCount < 0)
|
||||||
|
{
|
||||||
|
return "n/a";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (triangleCount >= 1_000_000)
|
||||||
|
{
|
||||||
|
return FormattableString.Invariant($"{triangleCount / 1_000_000d:0.#}m tris");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (triangleCount >= 1_000)
|
||||||
|
{
|
||||||
|
return FormattableString.Invariant($"{triangleCount / 1_000d:0.#}k tris");
|
||||||
|
}
|
||||||
|
|
||||||
|
return $"{triangleCount} tris";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void DrawThemeVectorRow(MainStyle.StyleVector2Option option)
|
private void DrawThemeVectorRow(MainStyle.StyleVector2Option option)
|
||||||
{
|
{
|
||||||
ImGui.TableNextRow();
|
ImGui.TableNextRow();
|
||||||
@@ -869,10 +961,11 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
|
|
||||||
_uiShared.DrawHelpText(
|
_uiShared.DrawHelpText(
|
||||||
$"The download window will show the current progress of outstanding downloads.{Environment.NewLine}{Environment.NewLine}" +
|
$"The download window will show the current progress of outstanding downloads.{Environment.NewLine}{Environment.NewLine}" +
|
||||||
$"What do W/Q/P/D stand for?{Environment.NewLine}W = Waiting for Slot (see Maximum Parallel Downloads){Environment.NewLine}" +
|
$"What do W/Q/P/D/C stand for?{Environment.NewLine}W = Waiting for Slot (see Maximum Parallel Downloads){Environment.NewLine}" +
|
||||||
$"Q = Queued on Server, waiting for queue ready signal{Environment.NewLine}" +
|
$"Q = Queued on Server, waiting for queue ready signal{Environment.NewLine}" +
|
||||||
$"P = Processing download (aka downloading){Environment.NewLine}" +
|
$"P = Processing download (aka downloading){Environment.NewLine}" +
|
||||||
$"D = Decompressing download");
|
$"D = Decompressing download{Environment.NewLine}" +
|
||||||
|
$"C = Completed download");
|
||||||
if (!_configService.Current.ShowTransferWindow) ImGui.BeginDisabled();
|
if (!_configService.Current.ShowTransferWindow) ImGui.BeginDisabled();
|
||||||
ImGui.Indent();
|
ImGui.Indent();
|
||||||
|
|
||||||
@@ -1147,7 +1240,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
|
|
||||||
private async Task<List<string>?> RunSpeedTest(List<string> servers, CancellationToken token)
|
private async Task<List<string>?> RunSpeedTest(List<string> servers, CancellationToken token)
|
||||||
{
|
{
|
||||||
List<string> speedTestResults = new();
|
List<string> speedTestResults = [];
|
||||||
foreach (var server in servers)
|
foreach (var server in servers)
|
||||||
{
|
{
|
||||||
HttpResponseMessage? result = null;
|
HttpResponseMessage? result = null;
|
||||||
@@ -1249,7 +1342,6 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
|
|
||||||
ImGui.TreePop();
|
ImGui.TreePop();
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
if (_uiShared.IconTextButton(FontAwesomeIcon.Copy, "[DEBUG] Copy Last created Character Data to clipboard"))
|
if (_uiShared.IconTextButton(FontAwesomeIcon.Copy, "[DEBUG] Copy Last created Character Data to clipboard"))
|
||||||
{
|
{
|
||||||
if (LastCreatedCharacterData != null)
|
if (LastCreatedCharacterData != null)
|
||||||
@@ -1265,6 +1357,39 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
|
|
||||||
UiSharedService.AttachToolTip("Use this when reporting mods being rejected from the server.");
|
UiSharedService.AttachToolTip("Use this when reporting mods being rejected from the server.");
|
||||||
|
|
||||||
|
if (_uiShared.IconTextButton(FontAwesomeIcon.Home, "Teleport to Limsa [LIFESTREAM TEST]") && _ipcManager.Lifestream.APIAvailable)
|
||||||
|
{
|
||||||
|
_ipcManager.Lifestream.ExecuteLifestreamCommand("limsa");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_uiShared.IconTextButton(FontAwesomeIcon.Home, "Teleport to JoyHouse [LIFESTREAM TEST]") && _ipcManager.Lifestream.APIAvailable)
|
||||||
|
{
|
||||||
|
var twintania = _dalamudUtilService.WorldData.Value
|
||||||
|
.FirstOrDefault(kvp => kvp.Value.Equals("Twintania", StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
int ward = 29;
|
||||||
|
int plot = 7;
|
||||||
|
|
||||||
|
AddressBookEntryTuple addressEntry = (
|
||||||
|
Name: "",
|
||||||
|
World: (int)twintania.Key,
|
||||||
|
City: (int)ResidentialAetheryteKind.Kugane,
|
||||||
|
Ward: ward,
|
||||||
|
PropertyType: 0,
|
||||||
|
Plot: plot,
|
||||||
|
Apartment: 1,
|
||||||
|
ApartmentSubdivision: false,
|
||||||
|
AliasEnabled: false,
|
||||||
|
Alias: ""
|
||||||
|
);
|
||||||
|
|
||||||
|
_logger.LogInformation("going to: {address}", addressEntry);
|
||||||
|
|
||||||
|
_ipcManager.Lifestream.GoToHousingAddress(addressEntry);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
_uiShared.DrawCombo("Log Level", Enum.GetValues<LogLevel>(), (l) => l.ToString(), (l) =>
|
_uiShared.DrawCombo("Log Level", Enum.GetValues<LogLevel>(), (l) => l.ToString(), (l) =>
|
||||||
{
|
{
|
||||||
_configService.Current.LogLevel = l;
|
_configService.Current.LogLevel = l;
|
||||||
@@ -1500,6 +1625,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
DrawPairPropertyRow("Approx. VRAM", FormatBytes(pair.LastAppliedApproximateVRAMBytes));
|
DrawPairPropertyRow("Approx. VRAM", FormatBytes(pair.LastAppliedApproximateVRAMBytes));
|
||||||
DrawPairPropertyRow("Effective VRAM", FormatBytes(pair.LastAppliedApproximateEffectiveVRAMBytes));
|
DrawPairPropertyRow("Effective VRAM", FormatBytes(pair.LastAppliedApproximateEffectiveVRAMBytes));
|
||||||
DrawPairPropertyRow("Last Triangles", pair.LastAppliedDataTris < 0 ? "n/a" : pair.LastAppliedDataTris.ToString(CultureInfo.InvariantCulture));
|
DrawPairPropertyRow("Last Triangles", pair.LastAppliedDataTris < 0 ? "n/a" : pair.LastAppliedDataTris.ToString(CultureInfo.InvariantCulture));
|
||||||
|
DrawPairPropertyRow("Effective Triangles", pair.LastAppliedApproximateEffectiveTris < 0 ? "n/a" : pair.LastAppliedApproximateEffectiveTris.ToString(CultureInfo.InvariantCulture));
|
||||||
ImGui.EndTable();
|
ImGui.EndTable();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1931,14 +2057,25 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
using (ImRaii.PushIndent(20f))
|
using (ImRaii.PushIndent(20f))
|
||||||
{
|
{
|
||||||
if (_validationTask.IsCompleted)
|
if (_validationTask.IsCompletedSuccessfully)
|
||||||
{
|
{
|
||||||
UiSharedService.TextWrapped(
|
UiSharedService.TextWrapped(
|
||||||
$"The storage validation has completed and removed {_validationTask.Result.Count} invalid files from storage.");
|
$"The storage validation has completed and removed {_validationTask.Result.Count} invalid files from storage.");
|
||||||
}
|
}
|
||||||
|
else if (_validationTask.IsCanceled)
|
||||||
|
{
|
||||||
|
UiSharedService.ColorTextWrapped(
|
||||||
|
"Storage validation was cancelled.",
|
||||||
|
UIColors.Get("LightlessYellow"));
|
||||||
|
}
|
||||||
|
else if (_validationTask.IsFaulted)
|
||||||
|
{
|
||||||
|
UiSharedService.ColorTextWrapped(
|
||||||
|
"Storage validation failed with an error.",
|
||||||
|
UIColors.Get("DimRed"));
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
||||||
UiSharedService.TextWrapped(
|
UiSharedService.TextWrapped(
|
||||||
$"Storage validation is running: {_currentProgress.Item1}/{_currentProgress.Item2}");
|
$"Storage validation is running: {_currentProgress.Item1}/{_currentProgress.Item2}");
|
||||||
if (_currentProgress.Item3 != null)
|
if (_currentProgress.Item3 != null)
|
||||||
@@ -3094,10 +3231,102 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
ImGui.Separator();
|
ImGui.Separator();
|
||||||
|
ImGui.Dummy(new Vector2(10));
|
||||||
|
_uiShared.BigText("Animation");
|
||||||
|
|
||||||
|
using (var animationTree = BeginGeneralTree("Animation & Bones", UIColors.Get("LightlessPurple")))
|
||||||
|
{
|
||||||
|
if (animationTree.Visible)
|
||||||
|
{
|
||||||
|
ImGui.TextUnformatted("Animation Options");
|
||||||
|
|
||||||
|
var modes = new[]
|
||||||
|
{
|
||||||
|
AnimationValidationMode.Unsafe,
|
||||||
|
AnimationValidationMode.Safe,
|
||||||
|
AnimationValidationMode.Safest,
|
||||||
|
};
|
||||||
|
|
||||||
|
var labels = new[]
|
||||||
|
{
|
||||||
|
"Unsafe",
|
||||||
|
"Safe (Race)",
|
||||||
|
"Safest (Race + Bones)",
|
||||||
|
};
|
||||||
|
|
||||||
|
var tooltips = new[]
|
||||||
|
{
|
||||||
|
"No validation. Fastest, but may allow incompatible animations (riskier).",
|
||||||
|
"Validates skeleton race + modded skeleton check (recommended).",
|
||||||
|
"Requires matching skeleton race + bone compatibility (strictest).",
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
var currentMode = _configService.Current.AnimationValidationMode;
|
||||||
|
int selectedIndex = Array.IndexOf(modes, currentMode);
|
||||||
|
if (selectedIndex < 0) selectedIndex = 1;
|
||||||
|
|
||||||
|
ImGui.SetNextItemWidth(140 * ImGuiHelpers.GlobalScale);
|
||||||
|
|
||||||
|
bool open = ImGui.BeginCombo("Animation validation", labels[selectedIndex]);
|
||||||
|
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
ImGui.SetTooltip(tooltips[selectedIndex]);
|
||||||
|
|
||||||
|
if (open)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < modes.Length; i++)
|
||||||
|
{
|
||||||
|
bool isSelected = (i == selectedIndex);
|
||||||
|
|
||||||
|
if (ImGui.Selectable(labels[i], isSelected))
|
||||||
|
{
|
||||||
|
selectedIndex = i;
|
||||||
|
_configService.Current.AnimationValidationMode = modes[i];
|
||||||
|
_configService.Save();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
ImGui.SetTooltip(tooltips[i]);
|
||||||
|
|
||||||
|
if (isSelected)
|
||||||
|
ImGui.SetItemDefaultFocus();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.EndCombo();
|
||||||
|
}
|
||||||
|
|
||||||
|
UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f);
|
||||||
|
|
||||||
|
var cfg = _configService.Current;
|
||||||
|
|
||||||
|
bool oneBased = cfg.AnimationAllowOneBasedShift;
|
||||||
|
if (ImGui.Checkbox("Treat 1-based PAP indices as compatible", ref oneBased))
|
||||||
|
{
|
||||||
|
cfg.AnimationAllowOneBasedShift = oneBased;
|
||||||
|
_configService.Save();
|
||||||
|
}
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
ImGui.SetTooltip("Fixes off-by-one PAPs (one bone differance in bones and PAP). Can also increase crashing, toggle off if alot of crashing is happening");
|
||||||
|
|
||||||
|
bool neighbor = cfg.AnimationAllowNeighborIndexTolerance;
|
||||||
|
if (ImGui.Checkbox("Allow 1+- bone index tolerance", ref neighbor))
|
||||||
|
{
|
||||||
|
cfg.AnimationAllowNeighborIndexTolerance = neighbor;
|
||||||
|
_configService.Save();
|
||||||
|
}
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
ImGui.SetTooltip("Looser matching on bone matching. Can reduce false blocks happening but also reduces safety and more prone to crashing.");
|
||||||
|
|
||||||
|
ImGui.TreePop();
|
||||||
|
animationTree.MarkContentEnd();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ImGui.EndChild();
|
ImGui.EndChild();
|
||||||
ImGui.EndGroup();
|
ImGui.EndGroup();
|
||||||
|
|
||||||
|
ImGui.Separator();
|
||||||
generalSelune.DrawHighlightOnly(ImGui.GetIO().DeltaTime);
|
generalSelune.DrawHighlightOnly(ImGui.GetIO().DeltaTime);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3187,6 +3416,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
return 1f - (elapsed / GeneralTreeHighlightDuration);
|
return 1f - (elapsed / GeneralTreeHighlightDuration);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Auto)]
|
||||||
private struct GeneralTreeScope : IDisposable
|
private struct GeneralTreeScope : IDisposable
|
||||||
{
|
{
|
||||||
private readonly bool _visible;
|
private readonly bool _visible;
|
||||||
@@ -3494,7 +3724,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
_uiShared.DrawHelpText("Controls whether Lightless reduces index textures that exceed the size limit.");
|
_uiShared.DrawHelpText("Controls whether Lightless reduces index textures that exceed the size limit.");
|
||||||
|
|
||||||
var dimensionOptions = new[] { 512, 1024, 2048, 4096 };
|
var dimensionOptions = new[] { 512, 1024, 2048, 4096 };
|
||||||
var optionLabels = dimensionOptions.Select(static value => value.ToString()).ToArray();
|
var optionLabels = dimensionOptions.Select(selector: static value => value.ToString()).ToArray();
|
||||||
var currentDimension = textureConfig.TextureDownscaleMaxDimension;
|
var currentDimension = textureConfig.TextureDownscaleMaxDimension;
|
||||||
var selectedIndex = Array.IndexOf(dimensionOptions, currentDimension);
|
var selectedIndex = Array.IndexOf(dimensionOptions, currentDimension);
|
||||||
if (selectedIndex < 0)
|
if (selectedIndex < 0)
|
||||||
@@ -3520,6 +3750,14 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
_uiShared.DrawNoteLine("! ", UIColors.Get("LightlessYellow"), new SeStringUtils.RichTextEntry("If disabled, saved + effective VRAM usage information will not work.", UIColors.Get("LightlessYellow")));
|
_uiShared.DrawNoteLine("! ", UIColors.Get("LightlessYellow"), new SeStringUtils.RichTextEntry("If disabled, saved + effective VRAM usage information will not work.", UIColors.Get("LightlessYellow")));
|
||||||
|
|
||||||
|
var skipPreferredDownscale = textureConfig.SkipTextureDownscaleForPreferredPairs;
|
||||||
|
if (ImGui.Checkbox("Skip downscale for preferred/direct pairs", ref skipPreferredDownscale))
|
||||||
|
{
|
||||||
|
textureConfig.SkipTextureDownscaleForPreferredPairs = skipPreferredDownscale;
|
||||||
|
_playerPerformanceConfigService.Save();
|
||||||
|
}
|
||||||
|
_uiShared.DrawHelpText("When enabled, textures for direct pairs with preferred permissions are left untouched.");
|
||||||
|
|
||||||
if (!textureConfig.EnableNonIndexTextureMipTrim && !textureConfig.EnableIndexTextureDownscale)
|
if (!textureConfig.EnableNonIndexTextureMipTrim && !textureConfig.EnableIndexTextureDownscale)
|
||||||
{
|
{
|
||||||
UiSharedService.ColorTextWrapped("Both trimming and downscale are disabled. Lightless will keep original textures regardless of size.", UIColors.Get("DimRed"));
|
UiSharedService.ColorTextWrapped("Both trimming and downscale are disabled. Lightless will keep original textures regardless of size.", UIColors.Get("DimRed"));
|
||||||
@@ -3547,6 +3785,160 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
ImGui.TreePop();
|
ImGui.TreePop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ImGui.Separator();
|
||||||
|
|
||||||
|
if (_uiShared.MediumTreeNode("Model Optimization", UIColors.Get("DimRed")))
|
||||||
|
{
|
||||||
|
_uiShared.MediumText("Warning", UIColors.Get("DimRed"));
|
||||||
|
_uiShared.DrawNoteLine("! ", UIColors.Get("DimRed"),
|
||||||
|
new SeStringUtils.RichTextEntry("Model decimation is a "),
|
||||||
|
new SeStringUtils.RichTextEntry("destructive", UIColors.Get("DimRed"), true),
|
||||||
|
new SeStringUtils.RichTextEntry(" process and may cause broken or incorrect character appearances."));
|
||||||
|
|
||||||
|
|
||||||
|
_uiShared.DrawNoteLine("! ", UIColors.Get("DimRed"),
|
||||||
|
new SeStringUtils.RichTextEntry("This feature is encouraged to help "),
|
||||||
|
new SeStringUtils.RichTextEntry("lower-end systems with limited VRAM", UIColors.Get("LightlessYellow"), true),
|
||||||
|
new SeStringUtils.RichTextEntry(" and for use in "),
|
||||||
|
new SeStringUtils.RichTextEntry("performance-critical scenarios", UIColors.Get("LightlessYellow"), true),
|
||||||
|
new SeStringUtils.RichTextEntry("."));
|
||||||
|
|
||||||
|
_uiShared.DrawNoteLine("! ", UIColors.Get("DimRed"),
|
||||||
|
new SeStringUtils.RichTextEntry("Runtime decimation "),
|
||||||
|
new SeStringUtils.RichTextEntry("MAY", UIColors.Get("DimRed"), true),
|
||||||
|
new SeStringUtils.RichTextEntry(" cause higher load on the system when processing downloads."));
|
||||||
|
|
||||||
|
_uiShared.DrawNoteLine("!!! ", UIColors.Get("DimRed"),
|
||||||
|
new SeStringUtils.RichTextEntry("When enabled, we cannot provide support for appearance issues caused by this setting!", UIColors.Get("DimRed"), true));
|
||||||
|
|
||||||
|
ImGui.Dummy(new Vector2(15));
|
||||||
|
|
||||||
|
_uiShared.DrawNoteLine("! ", UIColors.Get("LightlessGreen"),
|
||||||
|
new SeStringUtils.RichTextEntry("If a mesh exceeds the "),
|
||||||
|
new SeStringUtils.RichTextEntry("triangle threshold", UIColors.Get("LightlessGreen"), true),
|
||||||
|
new SeStringUtils.RichTextEntry(", it will be decimated automatically to the set "),
|
||||||
|
new SeStringUtils.RichTextEntry("target triangle ratio", UIColors.Get("LightlessGreen"), true),
|
||||||
|
new SeStringUtils.RichTextEntry(". This will reduce quality of the mesh or may break it's intended structure."));
|
||||||
|
|
||||||
|
|
||||||
|
var performanceConfig = _playerPerformanceConfigService.Current;
|
||||||
|
var enableDecimation = performanceConfig.EnableModelDecimation;
|
||||||
|
if (ImGui.Checkbox("Enable model decimation", ref enableDecimation))
|
||||||
|
{
|
||||||
|
performanceConfig.EnableModelDecimation = enableDecimation;
|
||||||
|
_playerPerformanceConfigService.Save();
|
||||||
|
}
|
||||||
|
_uiShared.DrawHelpText("When enabled, Lightless generates a decimated copy of given model after download.");
|
||||||
|
|
||||||
|
var keepOriginalModels = performanceConfig.KeepOriginalModelFiles;
|
||||||
|
if (ImGui.Checkbox("Keep original model files", ref keepOriginalModels))
|
||||||
|
{
|
||||||
|
performanceConfig.KeepOriginalModelFiles = keepOriginalModels;
|
||||||
|
_playerPerformanceConfigService.Save();
|
||||||
|
}
|
||||||
|
_uiShared.DrawHelpText("When disabled, Lightless removes the original model after a decimated copy is created.");
|
||||||
|
ImGui.SameLine();
|
||||||
|
_uiShared.DrawNoteLine("! ", UIColors.Get("LightlessYellow"), new SeStringUtils.RichTextEntry("If disabled, saved + effective triangle usage information will not work.", UIColors.Get("LightlessYellow")));
|
||||||
|
|
||||||
|
var skipPreferredDecimation = performanceConfig.SkipModelDecimationForPreferredPairs;
|
||||||
|
if (ImGui.Checkbox("Skip decimation for preferred/direct pairs", ref skipPreferredDecimation))
|
||||||
|
{
|
||||||
|
performanceConfig.SkipModelDecimationForPreferredPairs = skipPreferredDecimation;
|
||||||
|
_playerPerformanceConfigService.Save();
|
||||||
|
}
|
||||||
|
_uiShared.DrawHelpText("When enabled, models for direct pairs with preferred permissions are left untouched.");
|
||||||
|
|
||||||
|
var triangleThreshold = performanceConfig.ModelDecimationTriangleThreshold;
|
||||||
|
ImGui.SetNextItemWidth(300 * ImGuiHelpers.GlobalScale);
|
||||||
|
if (ImGui.SliderInt("Decimate models above", ref triangleThreshold, 8_000, 100_000))
|
||||||
|
{
|
||||||
|
performanceConfig.ModelDecimationTriangleThreshold = Math.Clamp(triangleThreshold, 8_000, 100_000);
|
||||||
|
_playerPerformanceConfigService.Save();
|
||||||
|
}
|
||||||
|
ImGui.SameLine();
|
||||||
|
ImGui.Text("triangles");
|
||||||
|
_uiShared.DrawHelpText($"Models below this triangle count are left untouched.{UiSharedService.TooltipSeparator}Default: 50,000");
|
||||||
|
|
||||||
|
var targetPercent = (float)(performanceConfig.ModelDecimationTargetRatio * 100.0);
|
||||||
|
var clampedPercent = Math.Clamp(targetPercent, 60f, 99f);
|
||||||
|
if (Math.Abs(clampedPercent - targetPercent) > float.Epsilon)
|
||||||
|
{
|
||||||
|
performanceConfig.ModelDecimationTargetRatio = clampedPercent / 100.0;
|
||||||
|
_playerPerformanceConfigService.Save();
|
||||||
|
targetPercent = clampedPercent;
|
||||||
|
}
|
||||||
|
ImGui.SetNextItemWidth(300 * ImGuiHelpers.GlobalScale);
|
||||||
|
if (ImGui.SliderFloat("Target triangle ratio", ref targetPercent, 60f, 99f, "%.0f%%"))
|
||||||
|
{
|
||||||
|
performanceConfig.ModelDecimationTargetRatio = Math.Clamp(targetPercent / 100f, 0.6f, 0.99f);
|
||||||
|
_playerPerformanceConfigService.Save();
|
||||||
|
}
|
||||||
|
_uiShared.DrawHelpText($"Target ratio relative to original triangle count (80% keeps 80% of triangles).{UiSharedService.TooltipSeparator}Default: 80%");
|
||||||
|
|
||||||
|
ImGui.Dummy(new Vector2(15));
|
||||||
|
ImGui.TextUnformatted("Decimation targets");
|
||||||
|
_uiShared.DrawHelpText("Hair mods are always excluded from decimation.");
|
||||||
|
|
||||||
|
_uiShared.DrawNoteLine("! ", UIColors.Get("LightlessGreen"),
|
||||||
|
new SeStringUtils.RichTextEntry("Automatic decimation will only target the selected "),
|
||||||
|
new SeStringUtils.RichTextEntry("decimation targets", UIColors.Get("LightlessGreen"), true),
|
||||||
|
new SeStringUtils.RichTextEntry("."));
|
||||||
|
|
||||||
|
_uiShared.DrawNoteLine("! ", UIColors.Get("LightlessYellow"),
|
||||||
|
new SeStringUtils.RichTextEntry("It is advised to not decimate any body related meshes which includes: "),
|
||||||
|
new SeStringUtils.RichTextEntry("facial mods + sculpts, chest, legs, hands and feet", UIColors.Get("LightlessYellow"), true),
|
||||||
|
new SeStringUtils.RichTextEntry("."));
|
||||||
|
|
||||||
|
_uiShared.DrawNoteLine("!!! ", UIColors.Get("DimRed"),
|
||||||
|
new SeStringUtils.RichTextEntry("Remember, automatic decimation is not perfect and can cause meshes to be ruined, especially hair mods.", UIColors.Get("DimRed"), true));
|
||||||
|
|
||||||
|
var allowBody = performanceConfig.ModelDecimationAllowBody;
|
||||||
|
if (ImGui.Checkbox("Body", ref allowBody))
|
||||||
|
{
|
||||||
|
performanceConfig.ModelDecimationAllowBody = allowBody;
|
||||||
|
_playerPerformanceConfigService.Save();
|
||||||
|
}
|
||||||
|
|
||||||
|
var allowFaceHead = performanceConfig.ModelDecimationAllowFaceHead;
|
||||||
|
if (ImGui.Checkbox("Face/head", ref allowFaceHead))
|
||||||
|
{
|
||||||
|
performanceConfig.ModelDecimationAllowFaceHead = allowFaceHead;
|
||||||
|
_playerPerformanceConfigService.Save();
|
||||||
|
}
|
||||||
|
|
||||||
|
var allowTail = performanceConfig.ModelDecimationAllowTail;
|
||||||
|
if (ImGui.Checkbox("Tails/Ears", ref allowTail))
|
||||||
|
{
|
||||||
|
performanceConfig.ModelDecimationAllowTail = allowTail;
|
||||||
|
_playerPerformanceConfigService.Save();
|
||||||
|
}
|
||||||
|
|
||||||
|
var allowClothing = performanceConfig.ModelDecimationAllowClothing;
|
||||||
|
if (ImGui.Checkbox("Clothing (body/legs/shoes/gloves/hats)", ref allowClothing))
|
||||||
|
{
|
||||||
|
performanceConfig.ModelDecimationAllowClothing = allowClothing;
|
||||||
|
_playerPerformanceConfigService.Save();
|
||||||
|
}
|
||||||
|
|
||||||
|
var allowAccessories = performanceConfig.ModelDecimationAllowAccessories;
|
||||||
|
if (ImGui.Checkbox("Accessories (earring/rings/bracelet/necklace)", ref allowAccessories))
|
||||||
|
{
|
||||||
|
performanceConfig.ModelDecimationAllowAccessories = allowAccessories;
|
||||||
|
_playerPerformanceConfigService.Save();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.Dummy(new Vector2(5));
|
||||||
|
|
||||||
|
UiSharedService.ColoredSeparator(UIColors.Get("LightlessGrey"), 3f);
|
||||||
|
|
||||||
|
ImGui.Dummy(new Vector2(5));
|
||||||
|
DrawTriangleDecimationCounters();
|
||||||
|
ImGui.Dummy(new Vector2(5));
|
||||||
|
|
||||||
|
UiSharedService.ColoredSeparator(UIColors.Get("DimRed"), 1.5f);
|
||||||
|
ImGui.TreePop();
|
||||||
|
}
|
||||||
|
|
||||||
ImGui.Separator();
|
ImGui.Separator();
|
||||||
ImGui.Dummy(new Vector2(10));
|
ImGui.Dummy(new Vector2(10));
|
||||||
|
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
|
|||||||
private readonly Dictionary<string, DateTime> _oauthTokenExpiry = [];
|
private readonly Dictionary<string, DateTime> _oauthTokenExpiry = [];
|
||||||
private bool _penumbraExists = false;
|
private bool _penumbraExists = false;
|
||||||
private bool _petNamesExists = false;
|
private bool _petNamesExists = false;
|
||||||
|
private bool _lifestreamExists = false;
|
||||||
private int _serverSelectionIndex = -1;
|
private int _serverSelectionIndex = -1;
|
||||||
public UiSharedService(ILogger<UiSharedService> logger, IpcManager ipcManager, ApiController apiController,
|
public UiSharedService(ILogger<UiSharedService> logger, IpcManager ipcManager, ApiController apiController,
|
||||||
CacheMonitor cacheMonitor, FileDialogManager fileDialogManager,
|
CacheMonitor cacheMonitor, FileDialogManager fileDialogManager,
|
||||||
@@ -112,6 +113,7 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
|
|||||||
_moodlesExists = _ipcManager.Moodles.APIAvailable;
|
_moodlesExists = _ipcManager.Moodles.APIAvailable;
|
||||||
_petNamesExists = _ipcManager.PetNames.APIAvailable;
|
_petNamesExists = _ipcManager.PetNames.APIAvailable;
|
||||||
_brioExists = _ipcManager.Brio.APIAvailable;
|
_brioExists = _ipcManager.Brio.APIAvailable;
|
||||||
|
_lifestreamExists = _ipcManager.Lifestream.APIAvailable;
|
||||||
});
|
});
|
||||||
|
|
||||||
UidFont = _pluginInterface.UiBuilder.FontAtlas.NewDelegateFontHandle(e =>
|
UidFont = _pluginInterface.UiBuilder.FontAtlas.NewDelegateFontHandle(e =>
|
||||||
@@ -1105,6 +1107,10 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
|
|||||||
ColorText("Brio", GetBoolColor(_brioExists));
|
ColorText("Brio", GetBoolColor(_brioExists));
|
||||||
AttachToolTip(BuildPluginTooltip("Brio", _brioExists, _ipcManager.Brio.State));
|
AttachToolTip(BuildPluginTooltip("Brio", _brioExists, _ipcManager.Brio.State));
|
||||||
|
|
||||||
|
ImGui.SameLine();
|
||||||
|
ColorText("Lifestream", GetBoolColor(_lifestreamExists));
|
||||||
|
AttachToolTip(BuildPluginTooltip("Lifestream", _lifestreamExists, _ipcManager.Lifestream.State));
|
||||||
|
|
||||||
if (!_penumbraExists || !_glamourerExists)
|
if (!_penumbraExists || !_glamourerExists)
|
||||||
{
|
{
|
||||||
ImGui.TextColored(ImGuiColors.DalamudRed, "You need to install both Penumbra and Glamourer and keep them up to date to use Lightless Sync.");
|
ImGui.TextColored(ImGuiColors.DalamudRed, "You need to install both Penumbra and Glamourer and keep them up to date to use Lightless Sync.");
|
||||||
|
|||||||
@@ -205,10 +205,8 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
|||||||
|
|
||||||
private void ApplyUiVisibilitySettings()
|
private void ApplyUiVisibilitySettings()
|
||||||
{
|
{
|
||||||
var config = _chatConfigService.Current;
|
|
||||||
_uiBuilder.DisableUserUiHide = true;
|
_uiBuilder.DisableUserUiHide = true;
|
||||||
_uiBuilder.DisableCutsceneUiHide = config.ShowInCutscenes;
|
_uiBuilder.DisableCutsceneUiHide = true;
|
||||||
_uiBuilder.DisableGposeUiHide = config.ShowInGpose;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool ShouldHide()
|
private bool ShouldHide()
|
||||||
@@ -220,6 +218,16 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!config.ShowInGpose && _dalamudUtilService.IsInGpose)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!config.ShowInCutscenes && _dalamudUtilService.IsInCutscene)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
if (config.HideInCombat && _dalamudUtilService.IsInCombat)
|
if (config.HideInCombat && _dalamudUtilService.IsInCombat)
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -57,7 +57,8 @@ public static class VariousExtensions
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static Dictionary<ObjectKind, HashSet<PlayerChanges>> CheckUpdatedData(this CharacterData newData, Guid applicationBase,
|
public static Dictionary<ObjectKind, HashSet<PlayerChanges>> CheckUpdatedData(this CharacterData newData, Guid applicationBase,
|
||||||
CharacterData? oldData, ILogger logger, IPairPerformanceSubject cachedPlayer, bool forceApplyCustomization, bool forceApplyMods)
|
CharacterData? oldData, ILogger logger, IPairPerformanceSubject cachedPlayer, bool forceApplyCustomization, bool forceApplyMods,
|
||||||
|
bool suppressForcedRedrawOnForcedModApply = false)
|
||||||
{
|
{
|
||||||
oldData ??= new();
|
oldData ??= new();
|
||||||
|
|
||||||
@@ -78,6 +79,7 @@ public static class VariousExtensions
|
|||||||
|
|
||||||
bool hasNewAndOldFileReplacements = newFileReplacements != null && existingFileReplacements != null;
|
bool hasNewAndOldFileReplacements = newFileReplacements != null && existingFileReplacements != null;
|
||||||
bool hasNewAndOldGlamourerData = newGlamourerData != null && existingGlamourerData != null;
|
bool hasNewAndOldGlamourerData = newGlamourerData != null && existingGlamourerData != null;
|
||||||
|
var forceRedrawOnForcedApply = forceApplyMods && !suppressForcedRedrawOnForcedModApply;
|
||||||
|
|
||||||
if (hasNewButNotOldFileReplacements || hasOldButNotNewFileReplacements || hasNewButNotOldGlamourerData || hasOldButNotNewGlamourerData)
|
if (hasNewButNotOldFileReplacements || hasOldButNotNewFileReplacements || hasNewButNotOldGlamourerData || hasOldButNotNewGlamourerData)
|
||||||
{
|
{
|
||||||
@@ -100,7 +102,7 @@ public static class VariousExtensions
|
|||||||
{
|
{
|
||||||
logger.LogDebug("[BASE-{appBase}] Updating {object}/{kind} (FileReplacements not equal) => {change}", applicationBase, cachedPlayer, objectKind, PlayerChanges.ModFiles);
|
logger.LogDebug("[BASE-{appBase}] Updating {object}/{kind} (FileReplacements not equal) => {change}", applicationBase, cachedPlayer, objectKind, PlayerChanges.ModFiles);
|
||||||
charaDataToUpdate[objectKind].Add(PlayerChanges.ModFiles);
|
charaDataToUpdate[objectKind].Add(PlayerChanges.ModFiles);
|
||||||
if (forceApplyMods || objectKind != ObjectKind.Player)
|
if (objectKind != ObjectKind.Player || forceRedrawOnForcedApply)
|
||||||
{
|
{
|
||||||
charaDataToUpdate[objectKind].Add(PlayerChanges.ForcedRedraw);
|
charaDataToUpdate[objectKind].Add(PlayerChanges.ForcedRedraw);
|
||||||
}
|
}
|
||||||
@@ -167,7 +169,7 @@ public static class VariousExtensions
|
|||||||
if (objectKind != ObjectKind.Player) continue;
|
if (objectKind != ObjectKind.Player) continue;
|
||||||
|
|
||||||
bool manipDataDifferent = !string.Equals(oldData.ManipulationData, newData.ManipulationData, StringComparison.Ordinal);
|
bool manipDataDifferent = !string.Equals(oldData.ManipulationData, newData.ManipulationData, StringComparison.Ordinal);
|
||||||
if (manipDataDifferent || forceApplyMods)
|
if (manipDataDifferent || forceRedrawOnForcedApply)
|
||||||
{
|
{
|
||||||
logger.LogDebug("[BASE-{appBase}] Updating {object}/{kind} (Diff manip data) => {change}", applicationBase, cachedPlayer, objectKind, PlayerChanges.ModManip);
|
logger.LogDebug("[BASE-{appBase}] Updating {object}/{kind} (Diff manip data) => {change}", applicationBase, cachedPlayer, objectKind, PlayerChanges.ModManip);
|
||||||
charaDataToUpdate[objectKind].Add(PlayerChanges.ModManip);
|
charaDataToUpdate[objectKind].Add(PlayerChanges.ModManip);
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ using LightlessSync.FileCache;
|
|||||||
using LightlessSync.LightlessConfiguration;
|
using LightlessSync.LightlessConfiguration;
|
||||||
using LightlessSync.PlayerData.Handlers;
|
using LightlessSync.PlayerData.Handlers;
|
||||||
using LightlessSync.Services.Mediator;
|
using LightlessSync.Services.Mediator;
|
||||||
|
using LightlessSync.Services.ModelDecimation;
|
||||||
using LightlessSync.Services.TextureCompression;
|
using LightlessSync.Services.TextureCompression;
|
||||||
using LightlessSync.WebAPI.Files.Models;
|
using LightlessSync.WebAPI.Files.Models;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@@ -17,19 +18,21 @@ namespace LightlessSync.WebAPI.Files;
|
|||||||
|
|
||||||
public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
||||||
{
|
{
|
||||||
private readonly Dictionary<string, FileDownloadStatus> _downloadStatus;
|
private readonly ConcurrentDictionary<string, FileDownloadStatus> _downloadStatus;
|
||||||
private readonly object _downloadStatusLock = new();
|
|
||||||
|
|
||||||
private readonly FileCompactor _fileCompactor;
|
private readonly FileCompactor _fileCompactor;
|
||||||
private readonly FileCacheManager _fileDbManager;
|
private readonly FileCacheManager _fileDbManager;
|
||||||
private readonly FileTransferOrchestrator _orchestrator;
|
private readonly FileTransferOrchestrator _orchestrator;
|
||||||
private readonly LightlessConfigService _configService;
|
private readonly LightlessConfigService _configService;
|
||||||
private readonly TextureDownscaleService _textureDownscaleService;
|
private readonly TextureDownscaleService _textureDownscaleService;
|
||||||
|
private readonly ModelDecimationService _modelDecimationService;
|
||||||
private readonly TextureMetadataHelper _textureMetadataHelper;
|
private readonly TextureMetadataHelper _textureMetadataHelper;
|
||||||
|
|
||||||
private readonly ConcurrentDictionary<ThrottledStream, byte> _activeDownloadStreams;
|
private readonly ConcurrentDictionary<ThrottledStream, byte> _activeDownloadStreams;
|
||||||
private readonly SemaphoreSlim _decompressGate =
|
private readonly SemaphoreSlim _decompressGate =
|
||||||
new(Math.Max(1, Environment.ProcessorCount / 2), Math.Max(1, Environment.ProcessorCount / 2));
|
new(Math.Max(1, Environment.ProcessorCount / 2), Math.Max(1, Environment.ProcessorCount / 2));
|
||||||
|
|
||||||
|
private readonly ConcurrentQueue<string> _deferredCompressionQueue = new();
|
||||||
|
|
||||||
private volatile bool _disableDirectDownloads;
|
private volatile bool _disableDirectDownloads;
|
||||||
private int _consecutiveDirectDownloadFailures;
|
private int _consecutiveDirectDownloadFailures;
|
||||||
@@ -43,14 +46,16 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
FileCompactor fileCompactor,
|
FileCompactor fileCompactor,
|
||||||
LightlessConfigService configService,
|
LightlessConfigService configService,
|
||||||
TextureDownscaleService textureDownscaleService,
|
TextureDownscaleService textureDownscaleService,
|
||||||
|
ModelDecimationService modelDecimationService,
|
||||||
TextureMetadataHelper textureMetadataHelper) : base(logger, mediator)
|
TextureMetadataHelper textureMetadataHelper) : base(logger, mediator)
|
||||||
{
|
{
|
||||||
_downloadStatus = new Dictionary<string, FileDownloadStatus>(StringComparer.Ordinal);
|
_downloadStatus = new ConcurrentDictionary<string, FileDownloadStatus>(StringComparer.Ordinal);
|
||||||
_orchestrator = orchestrator;
|
_orchestrator = orchestrator;
|
||||||
_fileDbManager = fileCacheManager;
|
_fileDbManager = fileCacheManager;
|
||||||
_fileCompactor = fileCompactor;
|
_fileCompactor = fileCompactor;
|
||||||
_configService = configService;
|
_configService = configService;
|
||||||
_textureDownscaleService = textureDownscaleService;
|
_textureDownscaleService = textureDownscaleService;
|
||||||
|
_modelDecimationService = modelDecimationService;
|
||||||
_textureMetadataHelper = textureMetadataHelper;
|
_textureMetadataHelper = textureMetadataHelper;
|
||||||
_activeDownloadStreams = new();
|
_activeDownloadStreams = new();
|
||||||
_lastConfigDirectDownloadsState = _configService.Current.EnableDirectDownloads;
|
_lastConfigDirectDownloadsState = _configService.Current.EnableDirectDownloads;
|
||||||
@@ -84,19 +89,16 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
public void ClearDownload()
|
public void ClearDownload()
|
||||||
{
|
{
|
||||||
CurrentDownloads.Clear();
|
CurrentDownloads.Clear();
|
||||||
lock (_downloadStatusLock)
|
_downloadStatus.Clear();
|
||||||
{
|
|
||||||
_downloadStatus.Clear();
|
|
||||||
}
|
|
||||||
CurrentOwnerToken = null;
|
CurrentOwnerToken = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task DownloadFiles(GameObjectHandler? gameObject, List<FileReplacementData> fileReplacementDto, CancellationToken ct, bool skipDownscale = false)
|
public async Task DownloadFiles(GameObjectHandler? gameObject, List<FileReplacementData> fileReplacementDto, CancellationToken ct, bool skipDownscale = false, bool skipDecimation = false)
|
||||||
{
|
{
|
||||||
Mediator.Publish(new HaltScanMessage(nameof(DownloadFiles)));
|
Mediator.Publish(new HaltScanMessage(nameof(DownloadFiles)));
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await DownloadFilesInternal(gameObject, fileReplacementDto, ct, skipDownscale).ConfigureAwait(false);
|
await DownloadFilesInternal(gameObject, fileReplacementDto, ct, skipDownscale, skipDecimation).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
@@ -154,29 +156,20 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
private void SetStatus(string key, DownloadStatus status)
|
private void SetStatus(string key, DownloadStatus status)
|
||||||
{
|
{
|
||||||
lock (_downloadStatusLock)
|
if (_downloadStatus.TryGetValue(key, out var st))
|
||||||
{
|
st.DownloadStatus = status;
|
||||||
if (_downloadStatus.TryGetValue(key, out var st))
|
|
||||||
st.DownloadStatus = status;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddTransferredBytes(string key, long delta)
|
private void AddTransferredBytes(string key, long delta)
|
||||||
{
|
{
|
||||||
lock (_downloadStatusLock)
|
if (_downloadStatus.TryGetValue(key, out var st))
|
||||||
{
|
st.AddTransferredBytes(delta);
|
||||||
if (_downloadStatus.TryGetValue(key, out var st))
|
|
||||||
st.TransferredBytes += delta;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void MarkTransferredFiles(string key, int files)
|
private void MarkTransferredFiles(string key, int files)
|
||||||
{
|
{
|
||||||
lock (_downloadStatusLock)
|
if (_downloadStatus.TryGetValue(key, out var st))
|
||||||
{
|
st.SetTransferredFiles(files);
|
||||||
if (_downloadStatus.TryGetValue(key, out var st))
|
|
||||||
st.TransferredFiles = files;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static byte MungeByte(int byteOrEof)
|
private static byte MungeByte(int byteOrEof)
|
||||||
@@ -404,76 +397,32 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
private async Task WaitForDownloadReady(List<DownloadFileTransfer> downloadFileTransfer, Guid requestId, CancellationToken downloadCt)
|
private async Task WaitForDownloadReady(List<DownloadFileTransfer> downloadFileTransfer, Guid requestId, CancellationToken downloadCt)
|
||||||
{
|
{
|
||||||
bool alreadyCancelled = false;
|
while (true)
|
||||||
try
|
|
||||||
{
|
{
|
||||||
CancellationTokenSource localTimeoutCts = new();
|
downloadCt.ThrowIfCancellationRequested();
|
||||||
localTimeoutCts.CancelAfter(TimeSpan.FromSeconds(5));
|
|
||||||
CancellationTokenSource composite = CancellationTokenSource.CreateLinkedTokenSource(downloadCt, localTimeoutCts.Token);
|
|
||||||
|
|
||||||
while (!_orchestrator.IsDownloadReady(requestId))
|
if (_orchestrator.IsDownloadReady(requestId))
|
||||||
|
break;
|
||||||
|
|
||||||
|
using var resp = await _orchestrator.SendRequestAsync(
|
||||||
|
HttpMethod.Get,
|
||||||
|
LightlessFiles.RequestCheckQueueFullPath(downloadFileTransfer[0].DownloadUri, requestId),
|
||||||
|
downloadFileTransfer.Select(t => t.Hash).ToList(),
|
||||||
|
downloadCt).ConfigureAwait(false);
|
||||||
|
|
||||||
|
resp.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
var body = (await resp.Content.ReadAsStringAsync(downloadCt).ConfigureAwait(false)).Trim();
|
||||||
|
if (string.Equals(body, "true", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
body.Contains("\"ready\":true", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
try
|
break;
|
||||||
{
|
|
||||||
await Task.Delay(250, composite.Token).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
catch (TaskCanceledException)
|
|
||||||
{
|
|
||||||
if (downloadCt.IsCancellationRequested) throw;
|
|
||||||
|
|
||||||
var req = await _orchestrator.SendRequestAsync(
|
|
||||||
HttpMethod.Get,
|
|
||||||
LightlessFiles.RequestCheckQueueFullPath(downloadFileTransfer[0].DownloadUri, requestId),
|
|
||||||
downloadFileTransfer.Select(c => c.Hash).ToList(),
|
|
||||||
downloadCt).ConfigureAwait(false);
|
|
||||||
|
|
||||||
req.EnsureSuccessStatusCode();
|
|
||||||
|
|
||||||
localTimeoutCts.Dispose();
|
|
||||||
composite.Dispose();
|
|
||||||
|
|
||||||
localTimeoutCts = new();
|
|
||||||
localTimeoutCts.CancelAfter(TimeSpan.FromSeconds(5));
|
|
||||||
composite = CancellationTokenSource.CreateLinkedTokenSource(downloadCt, localTimeoutCts.Token);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
localTimeoutCts.Dispose();
|
await Task.Delay(250, downloadCt).ConfigureAwait(false);
|
||||||
composite.Dispose();
|
|
||||||
|
|
||||||
Logger.LogDebug("Download {requestId} ready", requestId);
|
|
||||||
}
|
}
|
||||||
catch (TaskCanceledException)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await _orchestrator.SendRequestAsync(HttpMethod.Get, LightlessFiles.RequestCancelFullPath(downloadFileTransfer[0].DownloadUri, requestId))
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
alreadyCancelled = true;
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
|
|
||||||
throw;
|
_orchestrator.ClearDownloadRequest(requestId);
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
if (downloadCt.IsCancellationRequested && !alreadyCancelled)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await _orchestrator.SendRequestAsync(HttpMethod.Get, LightlessFiles.RequestCancelFullPath(downloadFileTransfer[0].DownloadUri, requestId))
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_orchestrator.ClearDownloadRequest(requestId);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task DownloadQueuedBlockFileAsync(
|
private async Task DownloadQueuedBlockFileAsync(
|
||||||
@@ -502,21 +451,15 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RemoveStatus(string key)
|
|
||||||
{
|
|
||||||
lock (_downloadStatusLock)
|
|
||||||
{
|
|
||||||
_downloadStatus.Remove(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task DecompressBlockFileAsync(
|
private async Task DecompressBlockFileAsync(
|
||||||
string downloadStatusKey,
|
string downloadStatusKey,
|
||||||
string blockFilePath,
|
string blockFilePath,
|
||||||
Dictionary<string, (string Extension, string GamePath)> replacementLookup,
|
Dictionary<string, (string Extension, string GamePath)> replacementLookup,
|
||||||
|
IReadOnlyDictionary<string, long> rawSizeLookup,
|
||||||
string downloadLabel,
|
string downloadLabel,
|
||||||
CancellationToken ct,
|
CancellationToken ct,
|
||||||
bool skipDownscale)
|
bool skipDownscale,
|
||||||
|
bool skipDecimation)
|
||||||
{
|
{
|
||||||
SetStatus(downloadStatusKey, DownloadStatus.Decompressing);
|
SetStatus(downloadStatusKey, DownloadStatus.Decompressing);
|
||||||
MarkTransferredFiles(downloadStatusKey, 1);
|
MarkTransferredFiles(downloadStatusKey, 1);
|
||||||
@@ -532,52 +475,59 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// sanity check length
|
|
||||||
if (fileLengthBytes < 0 || fileLengthBytes > int.MaxValue)
|
if (fileLengthBytes < 0 || fileLengthBytes > int.MaxValue)
|
||||||
throw new InvalidDataException($"Invalid block entry length: {fileLengthBytes}");
|
throw new InvalidDataException($"Invalid block entry length: {fileLengthBytes}");
|
||||||
|
|
||||||
// safe cast after check
|
|
||||||
var len = checked((int)fileLengthBytes);
|
var len = checked((int)fileLengthBytes);
|
||||||
|
|
||||||
if (!replacementLookup.TryGetValue(fileHash, out var repl))
|
if (!replacementLookup.TryGetValue(fileHash, out var repl))
|
||||||
{
|
{
|
||||||
Logger.LogWarning("{dlName}: No replacement mapping for {fileHash}", downloadLabel, fileHash);
|
Logger.LogWarning("{dlName}: No replacement mapping for {fileHash}", downloadLabel, fileHash);
|
||||||
fileBlockStream.Seek(len, SeekOrigin.Current);
|
// still need to skip bytes:
|
||||||
|
var skip = checked((int)fileLengthBytes);
|
||||||
|
fileBlockStream.Position += skip;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// decompress
|
|
||||||
var filePath = _fileDbManager.GetCacheFilePath(fileHash, repl.Extension);
|
var filePath = _fileDbManager.GetCacheFilePath(fileHash, repl.Extension);
|
||||||
Logger.LogTrace("{dlName}: Decompressing {file}:{len} => {dest}", downloadLabel, fileHash, fileLengthBytes, filePath);
|
Logger.LogTrace("{dlName}: Decompressing {file}:{len} => {dest}", downloadLabel, fileHash, fileLengthBytes, filePath);
|
||||||
|
|
||||||
// read compressed data
|
|
||||||
var compressed = new byte[len];
|
var compressed = new byte[len];
|
||||||
|
|
||||||
await ReadExactlyAsync(fileBlockStream, compressed.AsMemory(0, len), ct).ConfigureAwait(false);
|
await ReadExactlyAsync(fileBlockStream, compressed.AsMemory(0, len), ct).ConfigureAwait(false);
|
||||||
|
|
||||||
if (len == 0)
|
MungeBuffer(compressed);
|
||||||
|
var decompressed = LZ4Wrapper.Unwrap(compressed);
|
||||||
|
|
||||||
|
if (rawSizeLookup.TryGetValue(fileHash, out var expectedRawSize)
|
||||||
|
&& expectedRawSize > 0
|
||||||
|
&& decompressed.LongLength != expectedRawSize)
|
||||||
{
|
{
|
||||||
await _fileCompactor.WriteAllBytesAsync(filePath, Array.Empty<byte>(), ct).ConfigureAwait(false);
|
await _fileCompactor.WriteAllBytesAsync(filePath, Array.Empty<byte>(), ct).ConfigureAwait(false);
|
||||||
PersistFileToStorage(fileHash, filePath, repl.GamePath, skipDownscale);
|
PersistFileToStorage(fileHash, filePath, repl.GamePath, skipDownscale, skipDecimation);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
MungeBuffer(compressed);
|
MungeBuffer(compressed);
|
||||||
|
|
||||||
// limit concurrent decompressions
|
|
||||||
await _decompressGate.WaitAsync(ct).ConfigureAwait(false);
|
await _decompressGate.WaitAsync(ct).ConfigureAwait(false);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var sw = System.Diagnostics.Stopwatch.StartNew();
|
// offload CPU-intensive decompression to threadpool to free up worker
|
||||||
|
await Task.Run(async () =>
|
||||||
|
{
|
||||||
|
var sw = System.Diagnostics.Stopwatch.StartNew();
|
||||||
|
|
||||||
// decompress
|
// decompress
|
||||||
var decompressed = LZ4Wrapper.Unwrap(compressed);
|
var decompressed = LZ4Wrapper.Unwrap(compressed);
|
||||||
|
|
||||||
Logger.LogTrace("{dlName}: Unwrap {fileHash} took {ms}ms (compressed {c} bytes, decompressed {d} bytes)",
|
Logger.LogTrace("{dlName}: Unwrap {fileHash} took {ms}ms (compressed {c} bytes, decompressed {d} bytes)",
|
||||||
downloadLabel, fileHash, sw.ElapsedMilliseconds, compressed.Length, decompressed?.Length ?? -1);
|
downloadLabel, fileHash, sw.ElapsedMilliseconds, compressed.Length, decompressed?.Length ?? -1);
|
||||||
|
|
||||||
// write to file
|
// write to file without compacting during download
|
||||||
await _fileCompactor.WriteAllBytesAsync(filePath, decompressed, ct).ConfigureAwait(false);
|
await File.WriteAllBytesAsync(filePath, decompressed, ct).ConfigureAwait(false);
|
||||||
PersistFileToStorage(fileHash, filePath, repl.GamePath, skipDownscale);
|
PersistFileToStorage(fileHash, filePath, repl.GamePath, skipDownscale, skipDecimation);
|
||||||
|
}, ct).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -594,6 +544,8 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SetStatus(downloadStatusKey, DownloadStatus.Completed);
|
||||||
}
|
}
|
||||||
catch (EndOfStreamException)
|
catch (EndOfStreamException)
|
||||||
{
|
{
|
||||||
@@ -603,10 +555,6 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
Logger.LogError(ex, "{dlName}: Error during block file read", downloadLabel);
|
Logger.LogError(ex, "{dlName}: Error during block file read", downloadLabel);
|
||||||
}
|
}
|
||||||
finally
|
|
||||||
{
|
|
||||||
RemoveStatus(downloadStatusKey);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<List<DownloadFileTransfer>> InitiateDownloadList(
|
public async Task<List<DownloadFileTransfer>> InitiateDownloadList(
|
||||||
@@ -644,21 +592,25 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
.. await FilesGetSizes(hashes, ct).ConfigureAwait(false),
|
.. await FilesGetSizes(hashes, ct).ConfigureAwait(false),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
Logger.LogDebug("Files with size 0 or less: {files}",
|
||||||
|
string.Join(", ", downloadFileInfoFromService.Where(f => f.Size <= 0).Select(f => f.Hash)));
|
||||||
|
|
||||||
foreach (var dto in downloadFileInfoFromService.Where(c => c.IsForbidden))
|
foreach (var dto in downloadFileInfoFromService.Where(c => c.IsForbidden))
|
||||||
{
|
{
|
||||||
if (!_orchestrator.ForbiddenTransfers.Exists(f => string.Equals(f.Hash, dto.Hash, StringComparison.Ordinal)))
|
if (!_orchestrator.ForbiddenTransfers.Exists(f => string.Equals(f.Hash, dto.Hash, StringComparison.Ordinal)))
|
||||||
_orchestrator.ForbiddenTransfers.Add(new DownloadFileTransfer(dto));
|
_orchestrator.ForbiddenTransfers.Add(new DownloadFileTransfer(dto));
|
||||||
}
|
}
|
||||||
|
|
||||||
CurrentDownloads = [.. downloadFileInfoFromService
|
CurrentDownloads = downloadFileInfoFromService
|
||||||
.Distinct()
|
.Distinct()
|
||||||
.Select(d => new DownloadFileTransfer(d))
|
.Select(d => new DownloadFileTransfer(d))
|
||||||
.Where(d => d.CanBeTransferred)];
|
.Where(d => d.CanBeTransferred)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
return CurrentDownloads;
|
return CurrentDownloads;
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed record BatchChunk(string Key, List<DownloadFileTransfer> Items);
|
private sealed record BatchChunk(string HostKey, string StatusKey, List<DownloadFileTransfer> Items);
|
||||||
|
|
||||||
private static IEnumerable<List<T>> ChunkList<T>(List<T> items, int chunkSize)
|
private static IEnumerable<List<T>> ChunkList<T>(List<T> items, int chunkSize)
|
||||||
{
|
{
|
||||||
@@ -666,7 +618,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
yield return items.GetRange(i, Math.Min(chunkSize, items.Count - i));
|
yield return items.GetRange(i, Math.Min(chunkSize, items.Count - i));
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task DownloadFilesInternal(GameObjectHandler? gameObjectHandler, List<FileReplacementData> fileReplacement, CancellationToken ct, bool skipDownscale)
|
private async Task DownloadFilesInternal(GameObjectHandler? gameObjectHandler, List<FileReplacementData> fileReplacement, CancellationToken ct, bool skipDownscale, bool skipDecimation)
|
||||||
{
|
{
|
||||||
var objectName = gameObjectHandler?.Name ?? "Unknown";
|
var objectName = gameObjectHandler?.Name ?? "Unknown";
|
||||||
|
|
||||||
@@ -684,6 +636,20 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
var allowDirectDownloads = ShouldUseDirectDownloads();
|
var allowDirectDownloads = ShouldUseDirectDownloads();
|
||||||
var replacementLookup = BuildReplacementLookup(fileReplacement);
|
var replacementLookup = BuildReplacementLookup(fileReplacement);
|
||||||
|
var rawSizeLookup = new Dictionary<string, long>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
foreach (var download in CurrentDownloads)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(download.Hash))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rawSizeLookup.TryGetValue(download.Hash, out var existing) || existing <= 0)
|
||||||
|
{
|
||||||
|
rawSizeLookup[download.Hash] = download.TotalRaw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var directDownloads = new List<DownloadFileTransfer>();
|
var directDownloads = new List<DownloadFileTransfer>();
|
||||||
var batchDownloads = new List<DownloadFileTransfer>();
|
var batchDownloads = new List<DownloadFileTransfer>();
|
||||||
@@ -708,39 +674,36 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
var chunkSize = (int)Math.Ceiling(list.Count / (double)chunkCount);
|
var chunkSize = (int)Math.Ceiling(list.Count / (double)chunkCount);
|
||||||
|
|
||||||
return ChunkList(list, chunkSize)
|
return ChunkList(list, chunkSize)
|
||||||
.Select(chunk => new BatchChunk(g.Key, chunk));
|
.Select((chunk, index) => new BatchChunk(g.Key, $"{g.Key}#{index + 1}", chunk));
|
||||||
})
|
})
|
||||||
.ToArray();
|
.ToArray();
|
||||||
|
|
||||||
// init statuses
|
// init statuses
|
||||||
lock (_downloadStatusLock)
|
_downloadStatus.Clear();
|
||||||
|
|
||||||
|
// direct downloads and batch downloads tracked separately
|
||||||
|
foreach (var d in directDownloads)
|
||||||
{
|
{
|
||||||
_downloadStatus.Clear();
|
_downloadStatus[d.DirectDownloadUrl!] = new FileDownloadStatus
|
||||||
|
|
||||||
// direct downloads and batch downloads tracked separately
|
|
||||||
foreach (var d in directDownloads)
|
|
||||||
{
|
{
|
||||||
_downloadStatus[d.DirectDownloadUrl!] = new FileDownloadStatus
|
DownloadStatus = DownloadStatus.WaitingForSlot,
|
||||||
{
|
TotalBytes = d.Total,
|
||||||
DownloadStatus = DownloadStatus.Initializing,
|
TotalFiles = 1,
|
||||||
TotalBytes = d.Total,
|
TransferredBytes = 0,
|
||||||
TotalFiles = 1,
|
TransferredFiles = 0
|
||||||
TransferredBytes = 0,
|
};
|
||||||
TransferredFiles = 0
|
}
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var g in batchChunks.GroupBy(c => c.Key, StringComparer.Ordinal))
|
foreach (var chunk in batchChunks)
|
||||||
|
{
|
||||||
|
_downloadStatus[chunk.StatusKey] = new FileDownloadStatus
|
||||||
{
|
{
|
||||||
_downloadStatus[g.Key] = new FileDownloadStatus
|
DownloadStatus = DownloadStatus.WaitingForQueue,
|
||||||
{
|
TotalBytes = chunk.Items.Sum(x => x.Total),
|
||||||
DownloadStatus = DownloadStatus.Initializing,
|
TotalFiles = 1,
|
||||||
TotalBytes = g.SelectMany(x => x.Items).Sum(x => x.Total),
|
TransferredBytes = 0,
|
||||||
TotalFiles = 1,
|
TransferredFiles = 0
|
||||||
TransferredBytes = 0,
|
};
|
||||||
TransferredFiles = 0
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (directDownloads.Count > 0 || batchChunks.Length > 0)
|
if (directDownloads.Count > 0 || batchChunks.Length > 0)
|
||||||
@@ -752,30 +715,47 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
if (gameObjectHandler is not null)
|
if (gameObjectHandler is not null)
|
||||||
Mediator.Publish(new DownloadStartedMessage(gameObjectHandler, _downloadStatus));
|
Mediator.Publish(new DownloadStartedMessage(gameObjectHandler, _downloadStatus));
|
||||||
|
|
||||||
|
// work based on cpu count and slots
|
||||||
|
var coreCount = Environment.ProcessorCount;
|
||||||
|
var baseWorkers = Math.Min(slots, coreCount);
|
||||||
|
|
||||||
|
// only add buffer if decompression has capacity AND we have cores to spare
|
||||||
|
var availableDecompressSlots = _decompressGate.CurrentCount;
|
||||||
|
var extraWorkers = (availableDecompressSlots > 0 && coreCount >= 6) ? 2 : 0;
|
||||||
|
|
||||||
// allow some extra workers so downloads can continue while earlier items decompress.
|
// allow some extra workers so downloads can continue while earlier items decompress.
|
||||||
var workerDop = Math.Clamp(slots * 2, 2, 16);
|
var workerDop = Math.Clamp(baseWorkers + extraWorkers, 2, coreCount);
|
||||||
|
|
||||||
// batch downloads
|
// batch downloads
|
||||||
Task batchTask = batchChunks.Length == 0
|
Task batchTask = batchChunks.Length == 0
|
||||||
? Task.CompletedTask
|
? Task.CompletedTask
|
||||||
: Parallel.ForEachAsync(batchChunks, new ParallelOptions { MaxDegreeOfParallelism = workerDop, CancellationToken = ct },
|
: Parallel.ForEachAsync(batchChunks, new ParallelOptions { MaxDegreeOfParallelism = workerDop, CancellationToken = ct },
|
||||||
async (chunk, token) => await ProcessBatchChunkAsync(chunk, replacementLookup, token, skipDownscale).ConfigureAwait(false));
|
async (chunk, token) => await ProcessBatchChunkAsync(chunk, replacementLookup, rawSizeLookup, token, skipDownscale, skipDecimation).ConfigureAwait(false));
|
||||||
|
|
||||||
// direct downloads
|
// direct downloads
|
||||||
Task directTask = directDownloads.Count == 0
|
Task directTask = directDownloads.Count == 0
|
||||||
? Task.CompletedTask
|
? Task.CompletedTask
|
||||||
: Parallel.ForEachAsync(directDownloads, new ParallelOptions { MaxDegreeOfParallelism = workerDop, CancellationToken = ct },
|
: Parallel.ForEachAsync(directDownloads, new ParallelOptions { MaxDegreeOfParallelism = workerDop, CancellationToken = ct },
|
||||||
async (d, token) => await ProcessDirectAsync(d, replacementLookup, token, skipDownscale).ConfigureAwait(false));
|
async (d, token) => await ProcessDirectAsync(d, replacementLookup, rawSizeLookup, token, skipDownscale, skipDecimation).ConfigureAwait(false));
|
||||||
|
|
||||||
await Task.WhenAll(batchTask, directTask).ConfigureAwait(false);
|
await Task.WhenAll(batchTask, directTask).ConfigureAwait(false);
|
||||||
|
|
||||||
|
// process deferred compressions after all downloads complete
|
||||||
|
await ProcessDeferredCompressionsAsync(ct).ConfigureAwait(false);
|
||||||
|
|
||||||
Logger.LogDebug("Download end: {id}", objectName);
|
Logger.LogDebug("Download end: {id}", objectName);
|
||||||
ClearDownload();
|
ClearDownload();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task ProcessBatchChunkAsync(BatchChunk chunk, Dictionary<string, (string Extension, string GamePath)> replacementLookup, CancellationToken ct, bool skipDownscale)
|
private async Task ProcessBatchChunkAsync(
|
||||||
|
BatchChunk chunk,
|
||||||
|
Dictionary<string, (string Extension, string GamePath)> replacementLookup,
|
||||||
|
IReadOnlyDictionary<string, long> rawSizeLookup,
|
||||||
|
CancellationToken ct,
|
||||||
|
bool skipDownscale,
|
||||||
|
bool skipDecimation)
|
||||||
{
|
{
|
||||||
var statusKey = chunk.Key;
|
var statusKey = chunk.StatusKey;
|
||||||
|
|
||||||
// enqueue (no slot)
|
// enqueue (no slot)
|
||||||
SetStatus(statusKey, DownloadStatus.WaitingForQueue);
|
SetStatus(statusKey, DownloadStatus.WaitingForQueue);
|
||||||
@@ -793,7 +773,6 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// download (with slot)
|
|
||||||
var progress = CreateInlineProgress(bytes => AddTransferredBytes(statusKey, bytes));
|
var progress = CreateInlineProgress(bytes => AddTransferredBytes(statusKey, bytes));
|
||||||
|
|
||||||
// Download slot held on get
|
// Download slot held on get
|
||||||
@@ -803,10 +782,11 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
if (!File.Exists(blockFile))
|
if (!File.Exists(blockFile))
|
||||||
{
|
{
|
||||||
Logger.LogWarning("{dlName}: Block file missing before extraction, skipping", fi.Name);
|
Logger.LogWarning("{dlName}: Block file missing before extraction, skipping", fi.Name);
|
||||||
|
SetStatus(statusKey, DownloadStatus.Completed);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await DecompressBlockFileAsync(statusKey, blockFile, replacementLookup, fi.Name, ct, skipDownscale).ConfigureAwait(false);
|
await DecompressBlockFileAsync(statusKey, blockFile, replacementLookup, rawSizeLookup, fi.Name, ct, skipDownscale, skipDecimation).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
@@ -823,7 +803,13 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task ProcessDirectAsync(DownloadFileTransfer directDownload, Dictionary<string, (string Extension, string GamePath)> replacementLookup, CancellationToken ct, bool skipDownscale)
|
private async Task ProcessDirectAsync(
|
||||||
|
DownloadFileTransfer directDownload,
|
||||||
|
Dictionary<string, (string Extension, string GamePath)> replacementLookup,
|
||||||
|
IReadOnlyDictionary<string, long> rawSizeLookup,
|
||||||
|
CancellationToken ct,
|
||||||
|
bool skipDownscale,
|
||||||
|
bool skipDecimation)
|
||||||
{
|
{
|
||||||
var progress = CreateInlineProgress(bytes =>
|
var progress = CreateInlineProgress(bytes =>
|
||||||
{
|
{
|
||||||
@@ -833,7 +819,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
if (!ShouldUseDirectDownloads() || string.IsNullOrEmpty(directDownload.DirectDownloadUrl))
|
if (!ShouldUseDirectDownloads() || string.IsNullOrEmpty(directDownload.DirectDownloadUrl))
|
||||||
{
|
{
|
||||||
await ProcessDirectAsQueuedFallbackAsync(directDownload, replacementLookup, progress, ct, skipDownscale).ConfigureAwait(false);
|
await ProcessDirectAsQueuedFallbackAsync(directDownload, replacementLookup, rawSizeLookup, progress, ct, skipDownscale, skipDecimation).ConfigureAwait(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -861,6 +847,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
if (!replacementLookup.TryGetValue(directDownload.Hash, out var repl))
|
if (!replacementLookup.TryGetValue(directDownload.Hash, out var repl))
|
||||||
{
|
{
|
||||||
Logger.LogWarning("{hash}: No replacement data found for direct download.", directDownload.Hash);
|
Logger.LogWarning("{hash}: No replacement data found for direct download.", directDownload.Hash);
|
||||||
|
SetStatus(directDownload.DirectDownloadUrl!, DownloadStatus.Completed);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -873,13 +860,18 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
byte[] compressedBytes = await File.ReadAllBytesAsync(tempFilename, ct).ConfigureAwait(false);
|
byte[] compressedBytes = await File.ReadAllBytesAsync(tempFilename, ct).ConfigureAwait(false);
|
||||||
var decompressedBytes = LZ4Wrapper.Unwrap(compressedBytes);
|
var decompressedBytes = LZ4Wrapper.Unwrap(compressedBytes);
|
||||||
|
|
||||||
|
if (directDownload.TotalRaw > 0 && decompressedBytes.LongLength != directDownload.TotalRaw)
|
||||||
|
{
|
||||||
|
throw new InvalidDataException(
|
||||||
|
$"{directDownload.Hash}: Decompressed size mismatch (expected {directDownload.TotalRaw}, got {decompressedBytes.LongLength})");
|
||||||
|
}
|
||||||
|
|
||||||
await _fileCompactor.WriteAllBytesAsync(finalFilename, decompressedBytes, ct).ConfigureAwait(false);
|
await _fileCompactor.WriteAllBytesAsync(finalFilename, decompressedBytes, ct).ConfigureAwait(false);
|
||||||
PersistFileToStorage(directDownload.Hash, finalFilename, repl.GamePath, skipDownscale);
|
PersistFileToStorage(directDownload.Hash, finalFilename, repl.GamePath, skipDownscale, skipDecimation);
|
||||||
|
|
||||||
MarkTransferredFiles(directDownload.DirectDownloadUrl!, 1);
|
MarkTransferredFiles(directDownload.DirectDownloadUrl!, 1);
|
||||||
|
SetStatus(directDownload.DirectDownloadUrl!, DownloadStatus.Completed);
|
||||||
Logger.LogDebug("Finished direct download of {hash}.", directDownload.Hash);
|
Logger.LogDebug("Finished direct download of {hash}.", directDownload.Hash);
|
||||||
|
|
||||||
RemoveStatus(directDownload.DirectDownloadUrl!);
|
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException ex)
|
catch (OperationCanceledException ex)
|
||||||
{
|
{
|
||||||
@@ -902,7 +894,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await ProcessDirectAsQueuedFallbackAsync(directDownload, replacementLookup, progress, ct, skipDownscale).ConfigureAwait(false);
|
await ProcessDirectAsQueuedFallbackAsync(directDownload, replacementLookup, rawSizeLookup, progress, ct, skipDownscale, skipDecimation).ConfigureAwait(false);
|
||||||
|
|
||||||
if (!expectedDirectDownloadFailure && failureCount >= 3 && !_disableDirectDownloads)
|
if (!expectedDirectDownloadFailure && failureCount >= 3 && !_disableDirectDownloads)
|
||||||
{
|
{
|
||||||
@@ -929,9 +921,11 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
private async Task ProcessDirectAsQueuedFallbackAsync(
|
private async Task ProcessDirectAsQueuedFallbackAsync(
|
||||||
DownloadFileTransfer directDownload,
|
DownloadFileTransfer directDownload,
|
||||||
Dictionary<string, (string Extension, string GamePath)> replacementLookup,
|
Dictionary<string, (string Extension, string GamePath)> replacementLookup,
|
||||||
|
IReadOnlyDictionary<string, long> rawSizeLookup,
|
||||||
IProgress<long> progress,
|
IProgress<long> progress,
|
||||||
CancellationToken ct,
|
CancellationToken ct,
|
||||||
bool skipDownscale)
|
bool skipDownscale,
|
||||||
|
bool skipDecimation)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(directDownload.DirectDownloadUrl))
|
if (string.IsNullOrEmpty(directDownload.DirectDownloadUrl))
|
||||||
throw new InvalidOperationException("Direct download fallback requested without a direct download URL.");
|
throw new InvalidOperationException("Direct download fallback requested without a direct download URL.");
|
||||||
@@ -956,7 +950,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
if (!File.Exists(blockFile))
|
if (!File.Exists(blockFile))
|
||||||
throw new FileNotFoundException("Block file missing after direct download fallback.", blockFile);
|
throw new FileNotFoundException("Block file missing after direct download fallback.", blockFile);
|
||||||
|
|
||||||
await DecompressBlockFileAsync(statusKey, blockFile, replacementLookup, $"fallback-{directDownload.Hash}", ct, skipDownscale)
|
await DecompressBlockFileAsync(statusKey, blockFile, replacementLookup, rawSizeLookup, $"fallback-{directDownload.Hash}", ct, skipDownscale, skipDecimation)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
@@ -974,18 +968,16 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
if (!_orchestrator.IsInitialized)
|
if (!_orchestrator.IsInitialized)
|
||||||
throw new InvalidOperationException("FileTransferManager is not initialized");
|
throw new InvalidOperationException("FileTransferManager is not initialized");
|
||||||
|
|
||||||
// batch request
|
|
||||||
var response = await _orchestrator.SendRequestAsync(
|
var response = await _orchestrator.SendRequestAsync(
|
||||||
HttpMethod.Get,
|
HttpMethod.Get,
|
||||||
LightlessFiles.ServerFilesGetSizesFullPath(_orchestrator.FilesCdnUri!),
|
LightlessFiles.ServerFilesGetSizesFullPath(_orchestrator.FilesCdnUri!),
|
||||||
hashes,
|
hashes,
|
||||||
ct).ConfigureAwait(false);
|
ct).ConfigureAwait(false);
|
||||||
|
|
||||||
// ensure success
|
|
||||||
return await response.Content.ReadFromJsonAsync<List<DownloadFileDto>>(cancellationToken: ct).ConfigureAwait(false) ?? [];
|
return await response.Content.ReadFromJsonAsync<List<DownloadFileDto>>(cancellationToken: ct).ConfigureAwait(false) ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
private void PersistFileToStorage(string fileHash, string filePath, string gamePath, bool skipDownscale)
|
private void PersistFileToStorage(string fileHash, string filePath, string gamePath, bool skipDownscale, bool skipDecimation)
|
||||||
{
|
{
|
||||||
var fi = new FileInfo(filePath);
|
var fi = new FileInfo(filePath);
|
||||||
|
|
||||||
@@ -1001,13 +993,26 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
fi.LastAccessTime = DateTime.Today;
|
fi.LastAccessTime = DateTime.Today;
|
||||||
fi.LastWriteTime = RandomDayInThePast().Invoke();
|
fi.LastWriteTime = RandomDayInThePast().Invoke();
|
||||||
|
|
||||||
|
// queue file for deferred compression instead of compressing immediately
|
||||||
|
if (_configService.Current.UseCompactor)
|
||||||
|
_deferredCompressionQueue.Enqueue(filePath);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var entry = _fileDbManager.CreateCacheEntry(filePath);
|
var entry = _fileDbManager.CreateCacheEntryWithKnownHash(filePath, fileHash);
|
||||||
var mapKind = _textureMetadataHelper.DetermineMapKind(gamePath, filePath);
|
|
||||||
|
|
||||||
if (!skipDownscale)
|
if (!skipDownscale && _textureDownscaleService.ShouldScheduleDownscale(filePath))
|
||||||
_textureDownscaleService.ScheduleDownscale(fileHash, filePath, mapKind);
|
{
|
||||||
|
_textureDownscaleService.ScheduleDownscale(
|
||||||
|
fileHash,
|
||||||
|
filePath,
|
||||||
|
() => _textureMetadataHelper.DetermineMapKind(gamePath, filePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!skipDecimation && _modelDecimationService.ShouldScheduleDecimation(fileHash, filePath, gamePath))
|
||||||
|
{
|
||||||
|
_modelDecimationService.ScheduleDecimation(fileHash, filePath, gamePath);
|
||||||
|
}
|
||||||
|
|
||||||
if (entry != null && !string.Equals(entry.Hash, fileHash, StringComparison.OrdinalIgnoreCase))
|
if (entry != null && !string.Equals(entry.Hash, fileHash, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
@@ -1026,6 +1031,52 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
private static IProgress<long> CreateInlineProgress(Action<long> callback) => new InlineProgress(callback);
|
private static IProgress<long> CreateInlineProgress(Action<long> callback) => new InlineProgress(callback);
|
||||||
|
|
||||||
|
private async Task ProcessDeferredCompressionsAsync(CancellationToken ct)
|
||||||
|
{
|
||||||
|
if (_deferredCompressionQueue.IsEmpty)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var filesToCompress = new List<string>();
|
||||||
|
while (_deferredCompressionQueue.TryDequeue(out var filePath))
|
||||||
|
{
|
||||||
|
if (File.Exists(filePath))
|
||||||
|
filesToCompress.Add(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filesToCompress.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Logger.LogDebug("Starting deferred compression of {count} files", filesToCompress.Count);
|
||||||
|
|
||||||
|
var compressionWorkers = Math.Clamp(Environment.ProcessorCount / 4, 2, 4);
|
||||||
|
|
||||||
|
await Parallel.ForEachAsync(filesToCompress,
|
||||||
|
new ParallelOptions
|
||||||
|
{
|
||||||
|
MaxDegreeOfParallelism = compressionWorkers,
|
||||||
|
CancellationToken = ct
|
||||||
|
},
|
||||||
|
async (filePath, token) =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await Task.Yield();
|
||||||
|
if (_configService.Current.UseCompactor && File.Exists(filePath))
|
||||||
|
{
|
||||||
|
var bytes = await File.ReadAllBytesAsync(filePath, token).ConfigureAwait(false);
|
||||||
|
await _fileCompactor.WriteAllBytesAsync(filePath, bytes, token).ConfigureAwait(false);
|
||||||
|
Logger.LogTrace("Compressed file: {filePath}", filePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogWarning(ex, "Failed to compress file: {filePath}", filePath);
|
||||||
|
}
|
||||||
|
}).ConfigureAwait(false);
|
||||||
|
|
||||||
|
Logger.LogDebug("Completed deferred compression of {count} files", filesToCompress.Count);
|
||||||
|
}
|
||||||
|
|
||||||
private sealed class InlineProgress : IProgress<long>
|
private sealed class InlineProgress : IProgress<long>
|
||||||
{
|
{
|
||||||
private readonly Action<long> _callback;
|
private readonly Action<long> _callback;
|
||||||
|
|||||||
@@ -6,5 +6,6 @@ public enum DownloadStatus
|
|||||||
WaitingForSlot,
|
WaitingForSlot,
|
||||||
WaitingForQueue,
|
WaitingForQueue,
|
||||||
Downloading,
|
Downloading,
|
||||||
Decompressing
|
Decompressing,
|
||||||
|
Completed
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,46 @@
|
|||||||
namespace LightlessSync.WebAPI.Files.Models;
|
using System.Threading;
|
||||||
|
|
||||||
|
namespace LightlessSync.WebAPI.Files.Models;
|
||||||
|
|
||||||
public class FileDownloadStatus
|
public class FileDownloadStatus
|
||||||
{
|
{
|
||||||
public DownloadStatus DownloadStatus { get; set; }
|
private int _downloadStatus;
|
||||||
public long TotalBytes { get; set; }
|
private long _totalBytes;
|
||||||
public int TotalFiles { get; set; }
|
private int _totalFiles;
|
||||||
public long TransferredBytes { get; set; }
|
private long _transferredBytes;
|
||||||
public int TransferredFiles { get; set; }
|
private int _transferredFiles;
|
||||||
}
|
|
||||||
|
public DownloadStatus DownloadStatus
|
||||||
|
{
|
||||||
|
get => (DownloadStatus)Volatile.Read(ref _downloadStatus);
|
||||||
|
set => Volatile.Write(ref _downloadStatus, (int)value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public long TotalBytes
|
||||||
|
{
|
||||||
|
get => Interlocked.Read(ref _totalBytes);
|
||||||
|
set => Interlocked.Exchange(ref _totalBytes, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int TotalFiles
|
||||||
|
{
|
||||||
|
get => Volatile.Read(ref _totalFiles);
|
||||||
|
set => Volatile.Write(ref _totalFiles, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public long TransferredBytes
|
||||||
|
{
|
||||||
|
get => Interlocked.Read(ref _transferredBytes);
|
||||||
|
set => Interlocked.Exchange(ref _transferredBytes, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int TransferredFiles
|
||||||
|
{
|
||||||
|
get => Volatile.Read(ref _transferredFiles);
|
||||||
|
set => Volatile.Write(ref _transferredFiles, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddTransferredBytes(long delta) => Interlocked.Add(ref _transferredBytes, delta);
|
||||||
|
|
||||||
|
public void SetTransferredFiles(int files) => Volatile.Write(ref _transferredFiles, files);
|
||||||
|
}
|
||||||
|
|||||||
@@ -76,6 +76,19 @@
|
|||||||
"Microsoft.AspNetCore.SignalR.Common": "10.0.1"
|
"Microsoft.AspNetCore.SignalR.Common": "10.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Microsoft.Extensions.Caching.Memory": {
|
||||||
|
"type": "Direct",
|
||||||
|
"requested": "[10.0.1, )",
|
||||||
|
"resolved": "10.0.1",
|
||||||
|
"contentHash": "NxqSP0Ky4dZ5ybszdZCqs1X2C70s+dXflqhYBUh/vhcQVTIooNCXIYnLVbafoAFGZMs51d9+rHxveXs0ZC3SQQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.Extensions.Caching.Abstractions": "10.0.1",
|
||||||
|
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.1",
|
||||||
|
"Microsoft.Extensions.Logging.Abstractions": "10.0.1",
|
||||||
|
"Microsoft.Extensions.Options": "10.0.1",
|
||||||
|
"Microsoft.Extensions.Primitives": "10.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"Microsoft.Extensions.Hosting": {
|
"Microsoft.Extensions.Hosting": {
|
||||||
"type": "Direct",
|
"type": "Direct",
|
||||||
"requested": "[10.0.1, )",
|
"requested": "[10.0.1, )",
|
||||||
@@ -233,6 +246,14 @@
|
|||||||
"Microsoft.AspNetCore.SignalR.Common": "10.0.1"
|
"Microsoft.AspNetCore.SignalR.Common": "10.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Microsoft.Extensions.Caching.Abstractions": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "10.0.1",
|
||||||
|
"contentHash": "Vb1vVAQDxHpXVdL9fpOX2BzeV7bbhzG4pAcIKRauRl0/VfkE8mq0f+fYC+gWICh3dlzTZInJ/cTeBS2MgU/XvQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.Extensions.Primitives": "10.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"Microsoft.Extensions.Configuration": {
|
"Microsoft.Extensions.Configuration": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "10.0.1",
|
"resolved": "10.0.1",
|
||||||
@@ -618,7 +639,7 @@
|
|||||||
"FlatSharp.Compiler": "[7.9.0, )",
|
"FlatSharp.Compiler": "[7.9.0, )",
|
||||||
"FlatSharp.Runtime": "[7.9.0, )",
|
"FlatSharp.Runtime": "[7.9.0, )",
|
||||||
"OtterGui": "[1.0.0, )",
|
"OtterGui": "[1.0.0, )",
|
||||||
"Penumbra.Api": "[5.13.0, )",
|
"Penumbra.Api": "[5.13.1, )",
|
||||||
"Penumbra.String": "[1.0.7, )"
|
"Penumbra.String": "[1.0.7, )"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user