Compare commits
82 Commits
master
...
2.0.2.74-D
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
92772cf334 | ||
|
|
0395e81a9f | ||
|
|
9b9010ab8e | ||
|
|
7734a7bf7e | ||
|
|
db2d19bb1e | ||
|
|
032201ed9e | ||
|
|
775b128cf3 | ||
|
|
4bb8db8c03 | ||
|
|
f307c65c66 | ||
|
|
ab305a249c | ||
|
|
9d104a9dd8 | ||
|
|
4eec363cd2 | ||
|
|
d00df84ed6 | ||
|
|
bcd3bd5ca2 | ||
|
|
9048b3bd87 | ||
|
|
c1829a9837 | ||
|
|
a2ed9f8d2b | ||
| 8e08da7471 | |||
|
|
cca23f6e05 | ||
|
|
3205e6e0c3 | ||
|
|
d16e46200d | ||
|
|
5fc13647ae | ||
|
|
39d5d9d7c1 | ||
|
|
c19db58ead | ||
| 30717ba200 | |||
| e0b8070aa8 | |||
| 3241b9222b | |||
| 80b082240f | |||
| b8c8f3dffd | |||
| 543ea6c865 | |||
|
|
de9c9955ef | ||
|
|
2eb0c463e3 | ||
|
|
cd510f93af | ||
|
|
3bbda69699 | ||
|
|
deb7f67e59 | ||
|
|
9ba45670c5 | ||
|
|
f7bb73bcd1 | ||
|
|
4c07162ee3 | ||
|
|
a4d62af73d | ||
|
|
5fba3c01e7 | ||
|
|
df33a0f0a2 | ||
| c439d1c822 | |||
|
|
906dda3885 | ||
|
|
f812b6d09e | ||
| 7e61954541 | |||
|
|
89f59a98f5 | ||
|
|
fb58d8657d | ||
| bbb3375661 | |||
|
|
e95a2c3352 | ||
|
|
a8340c3279 | ||
|
|
e25979e089 | ||
|
|
ca7375b9c3 | ||
|
|
f8752fcb4d | ||
|
|
d1c955c74f | ||
|
|
91e60694ad | ||
|
|
f37fdefddd | ||
|
|
18fa0a47b1 | ||
|
|
9f5cc9e0d1 | ||
|
|
b02db4c1e1 | ||
|
|
d6b31ed5b9 | ||
|
|
9e600bfae0 | ||
|
|
1a73d5a4d9 | ||
|
|
a933330418 | ||
|
|
ea34b18f40 | ||
|
|
67dc215e83 | ||
|
|
baf3869cec | ||
|
|
eeda5aeb66 | ||
|
|
754df95071 | ||
|
|
24fca31606 | ||
|
|
a99c1c01b0 | ||
|
|
85999fab8f | ||
|
|
70745613e1 | ||
|
|
5c8e239a7b | ||
|
|
5eed65149a | ||
|
|
1ab4e2f94b | ||
|
|
f792bc1954 | ||
|
|
ced72ab9eb | ||
|
|
6c1cc77aaa | ||
|
|
5b81caf5a8 | ||
|
|
4e03b381dc | ||
|
|
3222133aa0 | ||
|
|
0ec423e65c |
Submodule LightlessAPI updated: 56566003e0...4ecd5375e6
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -11,6 +11,7 @@ public sealed class ChatConfig : ILightlessConfiguration
|
|||||||
public bool ShowRulesOverlayOnOpen { get; set; } = true;
|
public bool ShowRulesOverlayOnOpen { get; set; } = true;
|
||||||
public bool ShowMessageTimestamps { get; set; } = true;
|
public bool ShowMessageTimestamps { get; set; } = true;
|
||||||
public bool ShowNotesInSyncshellChat { get; set; } = true;
|
public bool ShowNotesInSyncshellChat { get; set; } = true;
|
||||||
|
public bool EnableAnimatedEmotes { get; set; } = true;
|
||||||
public float ChatWindowOpacity { get; set; } = .97f;
|
public float ChatWindowOpacity { get; set; } = .97f;
|
||||||
public bool FadeWhenUnfocused { get; set; } = false;
|
public bool FadeWhenUnfocused { get; set; } = false;
|
||||||
public float UnfocusedWindowOpacity { get; set; } = 0.6f;
|
public float UnfocusedWindowOpacity { get; set; } = 0.6f;
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -155,5 +157,10 @@ public class LightlessConfig : ILightlessConfiguration
|
|||||||
public bool SyncshellFinderEnabled { get; set; } = false;
|
public bool SyncshellFinderEnabled { get; set; } = false;
|
||||||
public string? SelectedFinderSyncshell { get; set; } = null;
|
public string? SelectedFinderSyncshell { get; set; } = null;
|
||||||
public string LastSeenVersion { get; set; } = string.Empty;
|
public string LastSeenVersion { get; set; } = string.Empty;
|
||||||
|
public bool EnableParticleEffects { get; set; } = true;
|
||||||
public 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.2</Version>
|
<Version>2.0.2.74</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>
|
||||||
@@ -37,6 +37,7 @@
|
|||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="10.0.1" />
|
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="10.0.1" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="10.0.1" />
|
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="10.0.1" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="10.0.1" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.1" />
|
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.1" />
|
||||||
<PackageReference Include="Glamourer.Api" Version="2.8.0" />
|
<PackageReference Include="Glamourer.Api" Version="2.8.0" />
|
||||||
<PackageReference Include="NReco.Logging.File" Version="1.3.1" />
|
<PackageReference Include="NReco.Logging.File" Version="1.3.1" />
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,19 @@
|
|||||||
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
using Dalamud.Utility;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
||||||
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;
|
||||||
|
using System.Runtime.ExceptionServices;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
namespace LightlessSync.PlayerData.Factories;
|
namespace LightlessSync.PlayerData.Factories;
|
||||||
|
|
||||||
@@ -18,13 +24,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 +61,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 +94,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,17 +120,17 @@ 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;
|
||||||
|
|
||||||
var character = (Character*)playerPointer;
|
if (!IsPointerValid(playerPointer))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
var character = (Character*)playerPointer;
|
||||||
if (character == null)
|
if (character == null)
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
@@ -110,96 +138,190 @@ public class PlayerDataFactory
|
|||||||
if (gameObject == null)
|
if (gameObject == null)
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
|
if (!IsPointerValid((IntPtr)gameObject))
|
||||||
|
return true;
|
||||||
|
|
||||||
return gameObject->DrawObject == null;
|
return gameObject->DrawObject == null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<CharacterDataFragment> CreateCharacterData(GameObjectHandler playerRelatedObject, CancellationToken ct)
|
private static bool IsPointerValid(IntPtr ptr)
|
||||||
|
{
|
||||||
|
if (ptr == IntPtr.Zero)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_ = Marshal.ReadByte(ptr);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 +330,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 +401,327 @@ 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("Clearing {count} Static Replacements for Pet", staticReplacements.Count);
|
||||||
|
clearedReplacements = staticReplacements;
|
||||||
|
}
|
||||||
|
|
||||||
|
ct.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
_transientResourceManager.ClearTransientPaths(objectKind, [.. staticReplacements.SelectMany(c => c.GamePaths)]);
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
var resolvedPath = g.Select(f => f.ResolvedPath).Distinct(StringComparer.OrdinalIgnoreCase);
|
||||||
|
var papPathSummary = string.Join(", ", resolvedPath);
|
||||||
|
if (papPathSummary.IsNullOrEmpty())
|
||||||
|
papPathSummary = "<unknown pap path>";
|
||||||
|
|
||||||
|
Dictionary<string, List<ushort>>? papIndices = null;
|
||||||
|
|
||||||
|
await _papParseLimiter.WaitAsync(ct).ConfigureAwait(false);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
try
|
||||||
{
|
{
|
||||||
_logger.LogTrace("All indices of {path} are <= 105, ignoring", file.ResolvedPath);
|
papIndices = await Task.Run(() => _modelAnalyzer.GetBoneIndicesFromPap(hash, persistToConfig: false), ct)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (SEHException ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "SEH exception while parsing PAP file (hash={hash}, path={path}). Error code: 0x{code:X}. Skipping this animation.", hash, papPathSummary, ex.ErrorCode);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
_logger.LogDebug("Verifying bone indices for {path}, found {x} skeletons", file.ResolvedPath, skeletonIndices.Count);
|
|
||||||
|
|
||||||
foreach (var boneCount in skeletonIndices)
|
|
||||||
{
|
{
|
||||||
var maxAnimationIndex = boneCount.Value.DefaultIfEmpty().Max();
|
_logger.LogError(ex, "Unexpected error parsing PAP file (hash={hash}, path={path}). Skipping this animation.", hash, papPathSummary);
|
||||||
if (maxAnimationIndex > maxPlayerBoneIndex)
|
continue;
|
||||||
{
|
|
||||||
_logger.LogWarning("Found more bone indices on the animation {path} skeleton {skl} (max indice {idx}) than on any player related skeleton (max indice {idx2})",
|
|
||||||
file.ResolvedPath, boneCount.Key, maxAnimationIndex, maxPlayerBoneIndex);
|
|
||||||
validationFailed = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
if (validationFailed)
|
|
||||||
{
|
{
|
||||||
noValidationFailed++;
|
_papParseLimiter.Release();
|
||||||
_logger.LogDebug("Removing {file} from sent file replacements and transient data", file.ResolvedPath);
|
}
|
||||||
fragment.FileReplacements.Remove(file);
|
|
||||||
foreach (var gamePath in file.GamePaths)
|
if (papIndices == null || papIndices.Count == 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
bool hasValidIndices = false;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
hasValidIndices = papIndices.All(k => k.Value != null && k.Value.DefaultIfEmpty().Max() <= 105);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Error validating bone indices for PAP (hash={hash}, path={path}). Skipping.", hash, papPathSummary);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasValidIndices)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (_logger.IsEnabled(LogLevel.Debug))
|
||||||
|
{
|
||||||
|
try
|
||||||
{
|
{
|
||||||
_transientResourceManager.RemoveTransientResource(ObjectKind.Player, gamePath);
|
var papBuckets = papIndices
|
||||||
|
.Where(kvp => kvp.Value is { Count: > 0 })
|
||||||
|
.Select(kvp => new
|
||||||
|
{
|
||||||
|
Raw = kvp.Key,
|
||||||
|
Key = XivDataAnalyzer.CanonicalizeSkeletonKey(kvp.Key),
|
||||||
|
Indices = kvp.Value
|
||||||
|
})
|
||||||
|
.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));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogDebug(ex, "Error logging PAP bucket details for hash={hash}", hash);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool isCompatible = false;
|
||||||
|
string reason = string.Empty;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
isCompatible = XivDataAnalyzer.IsPapCompatible(localBoneSets, papIndices, mode, allowBasedShift, allownNightIndex, out reason);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Error checking PAP compatibility for hash={hash}, path={path}. Treating as incompatible.", hash, papPathSummary);
|
||||||
|
reason = $"Exception during compatibility check: {ex.Message}";
|
||||||
|
isCompatible = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isCompatible)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
noValidationFailed++;
|
||||||
|
|
||||||
|
_logger.LogWarning(
|
||||||
|
"Animation PAP is not compatible with local skeletons; dropping mappings for {papPath}. Reason: {reason}",
|
||||||
|
papPathSummary,
|
||||||
|
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,31 +732,28 @@ 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] = [forwardPathsLower[i]];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 +761,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 +793,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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -113,16 +113,16 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
|||||||
public async Task ActOnFrameworkAfterEnsureNoDrawAsync(Action<Dalamud.Game.ClientState.Objects.Types.ICharacter> act, CancellationToken token)
|
public async Task ActOnFrameworkAfterEnsureNoDrawAsync(Action<Dalamud.Game.ClientState.Objects.Types.ICharacter> act, CancellationToken token)
|
||||||
{
|
{
|
||||||
while (await _dalamudUtil.RunOnFrameworkThread(() =>
|
while (await _dalamudUtil.RunOnFrameworkThread(() =>
|
||||||
{
|
{
|
||||||
EnsureLatestObjectState();
|
EnsureLatestObjectState();
|
||||||
if (CurrentDrawCondition != DrawCondition.None) return true;
|
if (CurrentDrawCondition != DrawCondition.None) return true;
|
||||||
var gameObj = _dalamudUtil.CreateGameObject(Address);
|
var gameObj = _dalamudUtil.CreateGameObject(Address);
|
||||||
if (gameObj is Dalamud.Game.ClientState.Objects.Types.ICharacter chara)
|
if (gameObj is Dalamud.Game.ClientState.Objects.Types.ICharacter chara)
|
||||||
{
|
{
|
||||||
act.Invoke(chara);
|
act.Invoke(chara);
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}).ConfigureAwait(false))
|
}).ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
await Task.Delay(250, token).ConfigureAwait(false);
|
await Task.Delay(250, token).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
@@ -169,37 +169,36 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
|||||||
return $"{owned}/{ObjectKind}:{Name} ({Address:X},{DrawObjectAddress:X})";
|
return $"{owned}/{ObjectKind}:{Name} ({Address:X},{DrawObjectAddress:X})";
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Dispose(bool disposing)
|
private void CheckAndUpdateObject() => CheckAndUpdateObject(allowPublish: true);
|
||||||
{
|
|
||||||
base.Dispose(disposing);
|
|
||||||
|
|
||||||
Mediator.Publish(new GameObjectHandlerDestroyedMessage(this, _isOwnedObject));
|
private unsafe void CheckAndUpdateObject(bool allowPublish)
|
||||||
}
|
|
||||||
|
|
||||||
private unsafe void CheckAndUpdateObject()
|
|
||||||
{
|
{
|
||||||
var prevAddr = Address;
|
var prevAddr = Address;
|
||||||
var prevDrawObj = DrawObjectAddress;
|
var prevDrawObj = DrawObjectAddress;
|
||||||
|
string? nameString = null;
|
||||||
|
|
||||||
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;
|
||||||
|
nameString = chara->GameObject.NameString;
|
||||||
|
if (!string.IsNullOrEmpty(nameString) && !string.Equals(nameString, Name, StringComparison.Ordinal))
|
||||||
|
Name = nameString;
|
||||||
}
|
}
|
||||||
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;
|
||||||
@@ -207,16 +206,18 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
|||||||
if (Address != IntPtr.Zero && DrawObjectAddress != IntPtr.Zero)
|
if (Address != IntPtr.Zero && DrawObjectAddress != IntPtr.Zero)
|
||||||
{
|
{
|
||||||
var chara = (Character*)Address;
|
var chara = (Character*)Address;
|
||||||
var name = chara->GameObject.NameString;
|
var drawObj = (DrawObject*)DrawObjectAddress;
|
||||||
bool nameChange = !string.Equals(name, Name, StringComparison.Ordinal);
|
var objType = drawObj->Object.GetObjectType();
|
||||||
if (nameChange)
|
var isHuman = objType == ObjectType.CharacterBase
|
||||||
{
|
&& ((CharacterBase*)drawObj)->GetModelType() == CharacterBase.ModelType.Human;
|
||||||
Name = name;
|
|
||||||
}
|
nameString ??= ((Character*)Address)->GameObject.NameString;
|
||||||
|
var nameChange = !string.Equals(nameString, Name, StringComparison.Ordinal);
|
||||||
|
if (nameChange) Name = nameString;
|
||||||
|
|
||||||
bool equipDiff = false;
|
bool equipDiff = false;
|
||||||
|
|
||||||
if (((DrawObject*)DrawObjectAddress)->Object.GetObjectType() == ObjectType.CharacterBase
|
if (isHuman)
|
||||||
&& ((CharacterBase*)DrawObjectAddress)->GetModelType() == CharacterBase.ModelType.Human)
|
|
||||||
{
|
{
|
||||||
var classJob = chara->CharacterData.ClassJob;
|
var classJob = chara->CharacterData.ClassJob;
|
||||||
if (classJob != _classJob)
|
if (classJob != _classJob)
|
||||||
@@ -226,7 +227,7 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
|||||||
Mediator.Publish(new ClassJobChangedMessage(this));
|
Mediator.Publish(new ClassJobChangedMessage(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
equipDiff = CompareAndUpdateEquipByteData((byte*)&((Human*)DrawObjectAddress)->Head);
|
equipDiff = CompareAndUpdateEquipByteData((byte*)&((Human*)drawObj)->Head);
|
||||||
|
|
||||||
ref var mh = ref chara->DrawData.Weapon(WeaponSlot.MainHand);
|
ref var mh = ref chara->DrawData.Weapon(WeaponSlot.MainHand);
|
||||||
ref var oh = ref chara->DrawData.Weapon(WeaponSlot.OffHand);
|
ref var oh = ref chara->DrawData.Weapon(WeaponSlot.OffHand);
|
||||||
@@ -251,12 +252,11 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
|||||||
|
|
||||||
bool customizeDiff = false;
|
bool customizeDiff = false;
|
||||||
|
|
||||||
if (((DrawObject*)DrawObjectAddress)->Object.GetObjectType() == ObjectType.CharacterBase
|
if (isHuman)
|
||||||
&& ((CharacterBase*)DrawObjectAddress)->GetModelType() == CharacterBase.ModelType.Human)
|
|
||||||
{
|
{
|
||||||
var gender = ((Human*)DrawObjectAddress)->Customize.Sex;
|
var gender = ((Human*)drawObj)->Customize.Sex;
|
||||||
var raceId = ((Human*)DrawObjectAddress)->Customize.Race;
|
var raceId = ((Human*)drawObj)->Customize.Race;
|
||||||
var tribeId = ((Human*)DrawObjectAddress)->Customize.Tribe;
|
var tribeId = ((Human*)drawObj)->Customize.Tribe;
|
||||||
|
|
||||||
if (_isOwnedObject && ObjectKind == ObjectKind.Player
|
if (_isOwnedObject && ObjectKind == ObjectKind.Player
|
||||||
&& (gender != Gender || raceId != RaceId || tribeId != TribeId))
|
&& (gender != Gender || raceId != RaceId || tribeId != TribeId))
|
||||||
@@ -267,7 +267,7 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
|||||||
TribeId = tribeId;
|
TribeId = tribeId;
|
||||||
}
|
}
|
||||||
|
|
||||||
customizeDiff = CompareAndUpdateCustomizeData(((Human*)DrawObjectAddress)->Customize.Data);
|
customizeDiff = CompareAndUpdateCustomizeData(((Human*)drawObj)->Customize.Data);
|
||||||
if (customizeDiff)
|
if (customizeDiff)
|
||||||
Logger.LogTrace("Checking [{this}] customize data as human from draw obj, result: {diff}", this, customizeDiff);
|
Logger.LogTrace("Checking [{this}] customize data as human from draw obj, result: {diff}", this, customizeDiff);
|
||||||
}
|
}
|
||||||
@@ -356,12 +356,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 +460,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>();
|
||||||
@@ -140,6 +143,7 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
services.AddSingleton<IdDisplayHandler>();
|
services.AddSingleton<IdDisplayHandler>();
|
||||||
services.AddSingleton<PlayerPerformanceService>();
|
services.AddSingleton<PlayerPerformanceService>();
|
||||||
services.AddSingleton<PenumbraTempCollectionJanitor>();
|
services.AddSingleton<PenumbraTempCollectionJanitor>();
|
||||||
|
services.AddSingleton<LocationShareService>();
|
||||||
|
|
||||||
services.AddSingleton<TextureMetadataHelper>(sp =>
|
services.AddSingleton<TextureMetadataHelper>(sp =>
|
||||||
new TextureMetadataHelper(sp.GetRequiredService<ILogger<TextureMetadataHelper>>(), gameData));
|
new TextureMetadataHelper(sp.GetRequiredService<ILogger<TextureMetadataHelper>>(), gameData));
|
||||||
@@ -176,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>>(),
|
||||||
@@ -372,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>(),
|
||||||
@@ -382,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(
|
||||||
@@ -480,19 +492,11 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
sp.GetRequiredService<UiSharedService>(),
|
sp.GetRequiredService<UiSharedService>(),
|
||||||
sp.GetRequiredService<ApiController>(),
|
sp.GetRequiredService<ApiController>(),
|
||||||
sp.GetRequiredService<LightFinderScannerService>(),
|
sp.GetRequiredService<LightFinderScannerService>(),
|
||||||
sp.GetRequiredService<LightFinderPlateHandler>()));
|
|
||||||
|
|
||||||
services.AddScoped<WindowMediatorSubscriberBase, SyncshellFinderUI>(sp => new SyncshellFinderUI(
|
|
||||||
sp.GetRequiredService<ILogger<SyncshellFinderUI>>(),
|
|
||||||
sp.GetRequiredService<LightlessMediator>(),
|
|
||||||
sp.GetRequiredService<PerformanceCollectorService>(),
|
|
||||||
sp.GetRequiredService<LightFinderService>(),
|
|
||||||
sp.GetRequiredService<UiSharedService>(),
|
|
||||||
sp.GetRequiredService<ApiController>(),
|
|
||||||
sp.GetRequiredService<LightFinderScannerService>(),
|
|
||||||
sp.GetRequiredService<PairUiService>(),
|
sp.GetRequiredService<PairUiService>(),
|
||||||
sp.GetRequiredService<DalamudUtilService>(),
|
sp.GetRequiredService<DalamudUtilService>(),
|
||||||
sp.GetRequiredService<LightlessProfileManager>()));
|
sp.GetRequiredService<LightlessProfileManager>(),
|
||||||
|
sp.GetRequiredService<ActorObjectService>(),
|
||||||
|
sp.GetRequiredService<LightFinderPlateHandler>()));
|
||||||
|
|
||||||
services.AddScoped<IPopupHandler, BanUserPopupHandler>();
|
services.AddScoped<IPopupHandler, BanUserPopupHandler>();
|
||||||
services.AddScoped<IPopupHandler, CensusPopupHandler>();
|
services.AddScoped<IPopupHandler, CensusPopupHandler>();
|
||||||
@@ -578,7 +582,6 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
_host.StopAsync().GetAwaiter().GetResult();
|
_host.StopAsync().ContinueWith(_ => _host.Dispose()).Wait(TimeSpan.FromSeconds(5));
|
||||||
_host.Dispose();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -1,29 +1,41 @@
|
|||||||
using Dalamud.Interface.Textures.TextureWraps;
|
using Dalamud.Interface.Textures.TextureWraps;
|
||||||
|
using LightlessSync.LightlessConfiguration;
|
||||||
using LightlessSync.UI;
|
using LightlessSync.UI;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using SixLabors.ImageSharp;
|
||||||
|
using SixLabors.ImageSharp.Formats.Gif;
|
||||||
|
using SixLabors.ImageSharp.Formats.Webp;
|
||||||
|
using SixLabors.ImageSharp.Metadata;
|
||||||
|
using SixLabors.ImageSharp.PixelFormats;
|
||||||
|
using SixLabors.ImageSharp.Processing;
|
||||||
|
|
||||||
namespace LightlessSync.Services.Chat;
|
namespace LightlessSync.Services.Chat;
|
||||||
|
|
||||||
public sealed class ChatEmoteService : IDisposable
|
public sealed class ChatEmoteService : IDisposable
|
||||||
{
|
{
|
||||||
private const string GlobalEmoteSetUrl = "https://7tv.io/v3/emote-sets/global";
|
private const string GlobalEmoteSetUrl = "https://7tv.io/v3/emote-sets/global";
|
||||||
|
private const int DefaultFrameDelayMs = 100;
|
||||||
|
private const int MinFrameDelayMs = 20;
|
||||||
|
|
||||||
private readonly ILogger<ChatEmoteService> _logger;
|
private readonly ILogger<ChatEmoteService> _logger;
|
||||||
private readonly HttpClient _httpClient;
|
private readonly HttpClient _httpClient;
|
||||||
private readonly UiSharedService _uiSharedService;
|
private readonly UiSharedService _uiSharedService;
|
||||||
|
private readonly ChatConfigService _chatConfigService;
|
||||||
private readonly ConcurrentDictionary<string, EmoteEntry> _emotes = new(StringComparer.Ordinal);
|
private readonly ConcurrentDictionary<string, EmoteEntry> _emotes = new(StringComparer.Ordinal);
|
||||||
private readonly SemaphoreSlim _downloadGate = new(3, 3);
|
private readonly SemaphoreSlim _downloadGate = new(3, 3);
|
||||||
|
|
||||||
private readonly object _loadLock = new();
|
private readonly object _loadLock = new();
|
||||||
private Task? _loadTask;
|
private Task? _loadTask;
|
||||||
|
|
||||||
public ChatEmoteService(ILogger<ChatEmoteService> logger, HttpClient httpClient, UiSharedService uiSharedService)
|
public ChatEmoteService(ILogger<ChatEmoteService> logger, HttpClient httpClient, UiSharedService uiSharedService, ChatConfigService chatConfigService)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_httpClient = httpClient;
|
_httpClient = httpClient;
|
||||||
_uiSharedService = uiSharedService;
|
_uiSharedService = uiSharedService;
|
||||||
|
_chatConfigService = chatConfigService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void EnsureGlobalEmotesLoaded()
|
public void EnsureGlobalEmotesLoaded()
|
||||||
@@ -62,13 +74,17 @@ public sealed class ChatEmoteService : IDisposable
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (entry.Texture is not null)
|
var allowAnimation = _chatConfigService.Current.EnableAnimatedEmotes;
|
||||||
|
if (entry.TryGetTexture(allowAnimation, out texture))
|
||||||
{
|
{
|
||||||
texture = entry.Texture;
|
if (allowAnimation && entry.NeedsAnimationLoad && !entry.HasAttemptedAnimation)
|
||||||
|
{
|
||||||
|
entry.EnsureLoading(allowAnimation, QueueEmoteDownload, allowWhenStaticLoaded: true);
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
entry.EnsureLoading(QueueEmoteDownload);
|
entry.EnsureLoading(allowAnimation, QueueEmoteDownload);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,7 +92,7 @@ public sealed class ChatEmoteService : IDisposable
|
|||||||
{
|
{
|
||||||
foreach (var entry in _emotes.Values)
|
foreach (var entry in _emotes.Values)
|
||||||
{
|
{
|
||||||
entry.Texture?.Dispose();
|
entry.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
_downloadGate.Dispose();
|
_downloadGate.Dispose();
|
||||||
@@ -108,13 +124,13 @@ public sealed class ChatEmoteService : IDisposable
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var url = TryBuildEmoteUrl(emoteElement);
|
var source = TryBuildEmoteSource(emoteElement);
|
||||||
if (string.IsNullOrWhiteSpace(url))
|
if (source is null || (!source.Value.HasStatic && !source.Value.HasAnimation))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
_emotes.TryAdd(name, new EmoteEntry(url));
|
_emotes.TryAdd(name, new EmoteEntry(name, source.Value));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -123,7 +139,7 @@ public sealed class ChatEmoteService : IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string? TryBuildEmoteUrl(JsonElement emoteElement)
|
private static EmoteSource? TryBuildEmoteSource(JsonElement emoteElement)
|
||||||
{
|
{
|
||||||
if (!emoteElement.TryGetProperty("data", out var dataElement))
|
if (!emoteElement.TryGetProperty("data", out var dataElement))
|
||||||
{
|
{
|
||||||
@@ -156,29 +172,38 @@ public sealed class ChatEmoteService : IDisposable
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var fileName = PickBestStaticFile(filesElement);
|
var files = ReadEmoteFiles(filesElement);
|
||||||
if (string.IsNullOrWhiteSpace(fileName))
|
if (files.Count == 0)
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return baseUrl.TrimEnd('/') + "/" + fileName;
|
var animatedFile = PickBestAnimatedFile(files);
|
||||||
|
var animatedUrl = animatedFile is null ? null : BuildEmoteUrl(baseUrl, animatedFile.Value.Name);
|
||||||
|
|
||||||
|
var staticName = animatedFile?.StaticName;
|
||||||
|
if (string.IsNullOrWhiteSpace(staticName))
|
||||||
|
{
|
||||||
|
staticName = PickBestStaticFileName(files);
|
||||||
|
}
|
||||||
|
|
||||||
|
var staticUrl = string.IsNullOrWhiteSpace(staticName) ? null : BuildEmoteUrl(baseUrl, staticName);
|
||||||
|
if (string.IsNullOrWhiteSpace(animatedUrl) && string.IsNullOrWhiteSpace(staticUrl))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new EmoteSource(staticUrl, animatedUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string? PickBestStaticFile(JsonElement filesElement)
|
private static string BuildEmoteUrl(string baseUrl, string fileName)
|
||||||
{
|
=> baseUrl.TrimEnd('/') + "/" + fileName;
|
||||||
string? png1x = null;
|
|
||||||
string? webp1x = null;
|
|
||||||
string? pngFallback = null;
|
|
||||||
string? webpFallback = null;
|
|
||||||
|
|
||||||
|
private static List<EmoteFile> ReadEmoteFiles(JsonElement filesElement)
|
||||||
|
{
|
||||||
|
var files = new List<EmoteFile>();
|
||||||
foreach (var file in filesElement.EnumerateArray())
|
foreach (var file in filesElement.EnumerateArray())
|
||||||
{
|
{
|
||||||
if (file.TryGetProperty("static", out var staticElement) && staticElement.ValueKind == JsonValueKind.False)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!file.TryGetProperty("name", out var nameElement))
|
if (!file.TryGetProperty("name", out var nameElement))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
@@ -190,6 +215,88 @@ public sealed class ChatEmoteService : IDisposable
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
string? staticName = null;
|
||||||
|
if (file.TryGetProperty("static_name", out var staticNameElement) && staticNameElement.ValueKind == JsonValueKind.String)
|
||||||
|
{
|
||||||
|
staticName = staticNameElement.GetString();
|
||||||
|
}
|
||||||
|
|
||||||
|
var frameCount = 1;
|
||||||
|
if (file.TryGetProperty("frame_count", out var frameCountElement) && frameCountElement.ValueKind == JsonValueKind.Number)
|
||||||
|
{
|
||||||
|
frameCountElement.TryGetInt32(out frameCount);
|
||||||
|
frameCount = Math.Max(frameCount, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
string? format = null;
|
||||||
|
if (file.TryGetProperty("format", out var formatElement) && formatElement.ValueKind == JsonValueKind.String)
|
||||||
|
{
|
||||||
|
format = formatElement.GetString();
|
||||||
|
}
|
||||||
|
|
||||||
|
files.Add(new EmoteFile(name, staticName, frameCount, format));
|
||||||
|
}
|
||||||
|
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static EmoteFile? PickBestAnimatedFile(IReadOnlyList<EmoteFile> files)
|
||||||
|
{
|
||||||
|
EmoteFile? webp1x = null;
|
||||||
|
EmoteFile? gif1x = null;
|
||||||
|
EmoteFile? webpFallback = null;
|
||||||
|
EmoteFile? gifFallback = null;
|
||||||
|
|
||||||
|
foreach (var file in files)
|
||||||
|
{
|
||||||
|
if (file.FrameCount <= 1 || !IsAnimatedFormatSupported(file))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file.Name.Equals("1x.webp", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
webp1x = file;
|
||||||
|
}
|
||||||
|
else if (file.Name.Equals("1x.gif", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
gif1x = file;
|
||||||
|
}
|
||||||
|
else if (file.Name.EndsWith(".webp", StringComparison.OrdinalIgnoreCase) && webpFallback is null)
|
||||||
|
{
|
||||||
|
webpFallback = file;
|
||||||
|
}
|
||||||
|
else if (file.Name.EndsWith(".gif", StringComparison.OrdinalIgnoreCase) && gifFallback is null)
|
||||||
|
{
|
||||||
|
gifFallback = file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return webp1x ?? gif1x ?? webpFallback ?? gifFallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? PickBestStaticFileName(IReadOnlyList<EmoteFile> files)
|
||||||
|
{
|
||||||
|
string? png1x = null;
|
||||||
|
string? webp1x = null;
|
||||||
|
string? gif1x = null;
|
||||||
|
string? pngFallback = null;
|
||||||
|
string? webpFallback = null;
|
||||||
|
string? gifFallback = null;
|
||||||
|
|
||||||
|
foreach (var file in files)
|
||||||
|
{
|
||||||
|
if (file.FrameCount > 1)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var name = file.StaticName ?? file.Name;
|
||||||
|
if (string.IsNullOrWhiteSpace(name))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (name.Equals("1x.png", StringComparison.OrdinalIgnoreCase))
|
if (name.Equals("1x.png", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
png1x = name;
|
png1x = name;
|
||||||
@@ -198,6 +305,10 @@ public sealed class ChatEmoteService : IDisposable
|
|||||||
{
|
{
|
||||||
webp1x = name;
|
webp1x = name;
|
||||||
}
|
}
|
||||||
|
else if (name.Equals("1x.gif", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
gif1x = name;
|
||||||
|
}
|
||||||
else if (name.EndsWith(".png", StringComparison.OrdinalIgnoreCase) && pngFallback is null)
|
else if (name.EndsWith(".png", StringComparison.OrdinalIgnoreCase) && pngFallback is null)
|
||||||
{
|
{
|
||||||
pngFallback = name;
|
pngFallback = name;
|
||||||
@@ -206,25 +317,80 @@ public sealed class ChatEmoteService : IDisposable
|
|||||||
{
|
{
|
||||||
webpFallback = name;
|
webpFallback = name;
|
||||||
}
|
}
|
||||||
|
else if (name.EndsWith(".gif", StringComparison.OrdinalIgnoreCase) && gifFallback is null)
|
||||||
|
{
|
||||||
|
gifFallback = name;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return png1x ?? webp1x ?? pngFallback ?? webpFallback;
|
return png1x ?? webp1x ?? gif1x ?? pngFallback ?? webpFallback ?? gifFallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void QueueEmoteDownload(EmoteEntry entry)
|
private static bool IsAnimatedFormatSupported(EmoteFile file)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(file.Format))
|
||||||
|
{
|
||||||
|
return file.Format.Equals("WEBP", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| file.Format.Equals("GIF", StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
return file.Name.EndsWith(".webp", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| file.Name.EndsWith(".gif", StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly record struct EmoteSource(string? StaticUrl, string? AnimatedUrl)
|
||||||
|
{
|
||||||
|
public bool HasStatic => !string.IsNullOrWhiteSpace(StaticUrl);
|
||||||
|
public bool HasAnimation => !string.IsNullOrWhiteSpace(AnimatedUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly record struct EmoteFile(string Name, string? StaticName, int FrameCount, string? Format);
|
||||||
|
|
||||||
|
private void QueueEmoteDownload(EmoteEntry entry, bool allowAnimation)
|
||||||
{
|
{
|
||||||
_ = Task.Run(async () =>
|
_ = Task.Run(async () =>
|
||||||
{
|
{
|
||||||
await _downloadGate.WaitAsync().ConfigureAwait(false);
|
await _downloadGate.WaitAsync().ConfigureAwait(false);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var data = await _httpClient.GetByteArrayAsync(entry.Url).ConfigureAwait(false);
|
if (allowAnimation)
|
||||||
var texture = _uiSharedService.LoadImage(data);
|
{
|
||||||
entry.SetTexture(texture);
|
if (entry.HasAnimatedSource)
|
||||||
|
{
|
||||||
|
entry.MarkAnimationAttempted();
|
||||||
|
if (await TryLoadAnimatedEmoteAsync(entry).ConfigureAwait(false))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.HasStaticSource && !entry.HasStaticTexture && await TryLoadStaticEmoteAsync(entry).ConfigureAwait(false))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (entry.HasStaticSource && await TryLoadStaticEmoteAsync(entry).ConfigureAwait(false))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.HasAnimatedSource)
|
||||||
|
{
|
||||||
|
entry.MarkAnimationAttempted();
|
||||||
|
if (await TryLoadAnimatedEmoteAsync(entry).ConfigureAwait(false))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
entry.MarkFailed();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogDebug(ex, "Failed to load 7TV emote {Url}", entry.Url);
|
_logger.LogDebug(ex, "Failed to load 7TV emote {Emote}", entry.Code);
|
||||||
entry.MarkFailed();
|
entry.MarkFailed();
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
@@ -234,21 +400,334 @@ public sealed class ChatEmoteService : IDisposable
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class EmoteEntry
|
private async Task<bool> TryLoadAnimatedEmoteAsync(EmoteEntry entry)
|
||||||
{
|
{
|
||||||
private int _loadingState;
|
if (string.IsNullOrWhiteSpace(entry.AnimatedUrl))
|
||||||
|
|
||||||
public EmoteEntry(string url)
|
|
||||||
{
|
{
|
||||||
Url = url;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string Url { get; }
|
try
|
||||||
public IDalamudTextureWrap? Texture { get; private set; }
|
|
||||||
|
|
||||||
public void EnsureLoading(Action<EmoteEntry> queueDownload)
|
|
||||||
{
|
{
|
||||||
if (Texture is not null)
|
var data = await _httpClient.GetByteArrayAsync(entry.AnimatedUrl).ConfigureAwait(false);
|
||||||
|
var isWebp = entry.AnimatedUrl.EndsWith(".webp", StringComparison.OrdinalIgnoreCase);
|
||||||
|
if (!TryDecodeAnimation(data, isWebp, out var animation))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
entry.SetAnimation(animation);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogDebug(ex, "Failed to decode animated 7TV emote {Emote}", entry.Code);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<bool> TryLoadStaticEmoteAsync(EmoteEntry entry)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(entry.StaticUrl))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var data = await _httpClient.GetByteArrayAsync(entry.StaticUrl).ConfigureAwait(false);
|
||||||
|
var texture = _uiSharedService.LoadImage(data);
|
||||||
|
entry.SetStaticTexture(texture);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogDebug(ex, "Failed to decode static 7TV emote {Emote}", entry.Code);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryDecodeAnimation(byte[] data, bool isWebp, out EmoteAnimation? animation)
|
||||||
|
{
|
||||||
|
animation = null;
|
||||||
|
List<EmoteFrame>? frames = null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Image<Rgba32> image;
|
||||||
|
if (isWebp)
|
||||||
|
{
|
||||||
|
using var stream = new MemoryStream(data);
|
||||||
|
image = WebpDecoder.Instance.Decode<Rgba32>(
|
||||||
|
new WebpDecoderOptions { BackgroundColorHandling = BackgroundColorHandling.Ignore },
|
||||||
|
stream);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
image = Image.Load<Rgba32>(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
using (image)
|
||||||
|
{
|
||||||
|
if (image.Frames.Count <= 1)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
using var composite = new Image<Rgba32>(image.Width, image.Height, Color.Transparent);
|
||||||
|
Image<Rgba32>? restoreCanvas = null;
|
||||||
|
GifDisposalMethod? pendingGifDisposal = null;
|
||||||
|
WebpDisposalMethod? pendingWebpDisposal = null;
|
||||||
|
|
||||||
|
frames = new List<EmoteFrame>(image.Frames.Count);
|
||||||
|
for (var i = 0; i < image.Frames.Count; i++)
|
||||||
|
{
|
||||||
|
var frameMetadata = image.Frames[i].Metadata;
|
||||||
|
var delayMs = GetFrameDelayMs(frameMetadata);
|
||||||
|
|
||||||
|
ApplyDisposal(composite, ref restoreCanvas, pendingGifDisposal, pendingWebpDisposal);
|
||||||
|
|
||||||
|
GifDisposalMethod? currentGifDisposal = null;
|
||||||
|
WebpDisposalMethod? currentWebpDisposal = null;
|
||||||
|
var blendMethod = WebpBlendMethod.Over;
|
||||||
|
|
||||||
|
if (isWebp)
|
||||||
|
{
|
||||||
|
if (frameMetadata.TryGetWebpFrameMetadata(out var webpMetadata))
|
||||||
|
{
|
||||||
|
currentWebpDisposal = webpMetadata.DisposalMethod;
|
||||||
|
blendMethod = webpMetadata.BlendMethod;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (frameMetadata.TryGetGifMetadata(out var gifMetadata))
|
||||||
|
{
|
||||||
|
currentGifDisposal = gifMetadata.DisposalMethod;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentGifDisposal == GifDisposalMethod.RestoreToPrevious)
|
||||||
|
{
|
||||||
|
restoreCanvas?.Dispose();
|
||||||
|
restoreCanvas = composite.Clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
using var frameImage = image.Frames.CloneFrame(i);
|
||||||
|
var alphaMode = blendMethod == WebpBlendMethod.Source
|
||||||
|
? PixelAlphaCompositionMode.Src
|
||||||
|
: PixelAlphaCompositionMode.SrcOver;
|
||||||
|
composite.Mutate(ctx => ctx.DrawImage(frameImage, PixelColorBlendingMode.Normal, alphaMode, 1f));
|
||||||
|
|
||||||
|
using var renderedFrame = composite.Clone();
|
||||||
|
using var ms = new MemoryStream();
|
||||||
|
renderedFrame.SaveAsPng(ms);
|
||||||
|
|
||||||
|
var texture = _uiSharedService.LoadImage(ms.ToArray());
|
||||||
|
frames.Add(new EmoteFrame(texture, delayMs));
|
||||||
|
|
||||||
|
pendingGifDisposal = currentGifDisposal;
|
||||||
|
pendingWebpDisposal = currentWebpDisposal;
|
||||||
|
}
|
||||||
|
|
||||||
|
restoreCanvas?.Dispose();
|
||||||
|
|
||||||
|
animation = new EmoteAnimation(frames);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
if (frames is not null)
|
||||||
|
{
|
||||||
|
foreach (var frame in frames)
|
||||||
|
{
|
||||||
|
frame.Texture.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int GetFrameDelayMs(ImageFrameMetadata metadata)
|
||||||
|
{
|
||||||
|
if (metadata.TryGetGifMetadata(out var gifMetadata))
|
||||||
|
{
|
||||||
|
var delayMs = (long)gifMetadata.FrameDelay * 10L;
|
||||||
|
return NormalizeFrameDelayMs(delayMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (metadata.TryGetWebpFrameMetadata(out var webpMetadata))
|
||||||
|
{
|
||||||
|
return NormalizeFrameDelayMs(webpMetadata.FrameDelay);
|
||||||
|
}
|
||||||
|
|
||||||
|
return DefaultFrameDelayMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int NormalizeFrameDelayMs(long delayMs)
|
||||||
|
{
|
||||||
|
if (delayMs <= 0)
|
||||||
|
{
|
||||||
|
return DefaultFrameDelayMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
var clamped = delayMs > int.MaxValue ? int.MaxValue : (int)delayMs;
|
||||||
|
return Math.Max(clamped, MinFrameDelayMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ApplyDisposal(
|
||||||
|
Image<Rgba32> composite,
|
||||||
|
ref Image<Rgba32>? restoreCanvas,
|
||||||
|
GifDisposalMethod? gifDisposal,
|
||||||
|
WebpDisposalMethod? webpDisposal)
|
||||||
|
{
|
||||||
|
if (gifDisposal is not null)
|
||||||
|
{
|
||||||
|
switch (gifDisposal)
|
||||||
|
{
|
||||||
|
case GifDisposalMethod.RestoreToBackground:
|
||||||
|
composite.Mutate(ctx => ctx.BackgroundColor(Color.Transparent));
|
||||||
|
break;
|
||||||
|
case GifDisposalMethod.RestoreToPrevious:
|
||||||
|
if (restoreCanvas is not null)
|
||||||
|
{
|
||||||
|
composite.Mutate(ctx => ctx.BackgroundColor(Color.Transparent));
|
||||||
|
var restoreSnapshot = restoreCanvas;
|
||||||
|
composite.Mutate(ctx => ctx.DrawImage(restoreSnapshot, PixelColorBlendingMode.Normal, PixelAlphaCompositionMode.Src, 1f));
|
||||||
|
restoreCanvas.Dispose();
|
||||||
|
restoreCanvas = null;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (webpDisposal == WebpDisposalMethod.RestoreToBackground)
|
||||||
|
{
|
||||||
|
composite.Mutate(ctx => ctx.BackgroundColor(Color.Transparent));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class EmoteAnimation : IDisposable
|
||||||
|
{
|
||||||
|
private readonly EmoteFrame[] _frames;
|
||||||
|
private readonly int _durationMs;
|
||||||
|
private readonly long _startTimestamp;
|
||||||
|
|
||||||
|
public EmoteAnimation(IReadOnlyList<EmoteFrame> frames)
|
||||||
|
{
|
||||||
|
_frames = frames.ToArray();
|
||||||
|
_durationMs = Math.Max(1, frames.Sum(frame => frame.DurationMs));
|
||||||
|
_startTimestamp = Stopwatch.GetTimestamp();
|
||||||
|
}
|
||||||
|
|
||||||
|
public IDalamudTextureWrap? GetCurrentFrame()
|
||||||
|
{
|
||||||
|
if (_frames.Length == 0)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_frames.Length == 1)
|
||||||
|
{
|
||||||
|
return _frames[0].Texture;
|
||||||
|
}
|
||||||
|
|
||||||
|
var elapsedTicks = Stopwatch.GetTimestamp() - _startTimestamp;
|
||||||
|
var elapsedMs = (elapsedTicks * 1000L) / Stopwatch.Frequency;
|
||||||
|
var targetMs = (int)(elapsedMs % _durationMs);
|
||||||
|
var accumulated = 0;
|
||||||
|
|
||||||
|
foreach (var frame in _frames)
|
||||||
|
{
|
||||||
|
accumulated += frame.DurationMs;
|
||||||
|
if (targetMs < accumulated)
|
||||||
|
{
|
||||||
|
return frame.Texture;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return _frames[^1].Texture;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IDalamudTextureWrap? GetStaticFrame()
|
||||||
|
{
|
||||||
|
if (_frames.Length == 0)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return _frames[0].Texture;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
foreach (var frame in _frames)
|
||||||
|
{
|
||||||
|
frame.Texture.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly record struct EmoteFrame(IDalamudTextureWrap Texture, int DurationMs);
|
||||||
|
|
||||||
|
private sealed class EmoteEntry : IDisposable
|
||||||
|
{
|
||||||
|
private int _loadingState;
|
||||||
|
private int _animationAttempted;
|
||||||
|
private IDalamudTextureWrap? _staticTexture;
|
||||||
|
private EmoteAnimation? _animation;
|
||||||
|
|
||||||
|
public EmoteEntry(string code, EmoteSource source)
|
||||||
|
{
|
||||||
|
Code = code;
|
||||||
|
StaticUrl = source.StaticUrl;
|
||||||
|
AnimatedUrl = source.AnimatedUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Code { get; }
|
||||||
|
public string? StaticUrl { get; }
|
||||||
|
public string? AnimatedUrl { get; }
|
||||||
|
public bool HasStaticSource => !string.IsNullOrWhiteSpace(StaticUrl);
|
||||||
|
public bool HasAnimatedSource => !string.IsNullOrWhiteSpace(AnimatedUrl);
|
||||||
|
public bool HasStaticTexture => _staticTexture is not null;
|
||||||
|
public bool HasAttemptedAnimation => Interlocked.CompareExchange(ref _animationAttempted, 0, 0) != 0;
|
||||||
|
public bool NeedsAnimationLoad => _animation is null && HasAnimatedSource;
|
||||||
|
|
||||||
|
public void MarkAnimationAttempted()
|
||||||
|
{
|
||||||
|
Interlocked.Exchange(ref _animationAttempted, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryGetTexture(bool allowAnimation, out IDalamudTextureWrap? texture)
|
||||||
|
{
|
||||||
|
if (allowAnimation && _animation is not null)
|
||||||
|
{
|
||||||
|
texture = _animation.GetCurrentFrame();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_staticTexture is not null)
|
||||||
|
{
|
||||||
|
texture = _staticTexture;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!allowAnimation && _animation is not null)
|
||||||
|
{
|
||||||
|
texture = _animation.GetStaticFrame();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
texture = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void EnsureLoading(bool allowAnimation, Action<EmoteEntry, bool> queueDownload, bool allowWhenStaticLoaded = false)
|
||||||
|
{
|
||||||
|
if (_animation is not null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!allowWhenStaticLoaded && _staticTexture is not null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -258,12 +737,22 @@ public sealed class ChatEmoteService : IDisposable
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
queueDownload(this);
|
queueDownload(this, allowAnimation);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetTexture(IDalamudTextureWrap texture)
|
public void SetAnimation(EmoteAnimation animation)
|
||||||
{
|
{
|
||||||
Texture = texture;
|
_staticTexture?.Dispose();
|
||||||
|
_staticTexture = null;
|
||||||
|
_animation?.Dispose();
|
||||||
|
_animation = animation;
|
||||||
|
Interlocked.Exchange(ref _loadingState, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetStaticTexture(IDalamudTextureWrap texture)
|
||||||
|
{
|
||||||
|
_staticTexture?.Dispose();
|
||||||
|
_staticTexture = texture;
|
||||||
Interlocked.Exchange(ref _loadingState, 0);
|
Interlocked.Exchange(ref _loadingState, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -271,5 +760,11 @@ public sealed class ChatEmoteService : IDisposable
|
|||||||
{
|
{
|
||||||
Interlocked.Exchange(ref _loadingState, 0);
|
Interlocked.Exchange(ref _loadingState, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_animation?.Dispose();
|
||||||
|
_staticTexture?.Dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ using LightlessSync.UI;
|
|||||||
using LightlessSync.UI.Services;
|
using LightlessSync.UI.Services;
|
||||||
using LightlessSync.Utils;
|
using LightlessSync.Utils;
|
||||||
using LightlessSync.WebAPI;
|
using LightlessSync.WebAPI;
|
||||||
using Lumina.Excel.Sheets;
|
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
@@ -172,9 +171,8 @@ internal class ContextMenuService : IHostedService
|
|||||||
_logger.LogTrace("Cannot send pair request to {TargetName}@{World} while in PvP or GPose.", target.TargetName, target.TargetHomeWorld.RowId);
|
_logger.LogTrace("Cannot send pair request to {TargetName}@{World} while in PvP or GPose.", target.TargetName, target.TargetHomeWorld.RowId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var world = GetWorld(target.TargetHomeWorld.RowId);
|
if (!IsWorldValid(target.TargetHomeWorld.RowId))
|
||||||
if (!IsWorldValid(world))
|
|
||||||
{
|
{
|
||||||
_logger.LogTrace("Target player {TargetName}@{World} is on an invalid world.", target.TargetName, target.TargetHomeWorld.RowId);
|
_logger.LogTrace("Target player {TargetName}@{World} is on an invalid world.", target.TargetName, target.TargetHomeWorld.RowId);
|
||||||
return;
|
return;
|
||||||
@@ -226,9 +224,8 @@ internal class ContextMenuService : IHostedService
|
|||||||
{
|
{
|
||||||
if (args.Target is not MenuTargetDefault target)
|
if (args.Target is not MenuTargetDefault target)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var world = GetWorld(target.TargetHomeWorld.RowId);
|
if (!target.TargetHomeWorld.IsValid || !IsWorldValid(target.TargetHomeWorld.RowId))
|
||||||
if (!IsWorldValid(world))
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
try
|
try
|
||||||
@@ -237,7 +234,7 @@ internal class ContextMenuService : IHostedService
|
|||||||
|
|
||||||
if (targetData == null || targetData.Address == nint.Zero)
|
if (targetData == null || targetData.Address == nint.Zero)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("Target player {TargetName}@{World} not found in object table.", target.TargetName, world.Name);
|
_logger.LogWarning("Target player {TargetName}@{World} not found in object table.", target.TargetName, target.TargetHomeWorld.Value.Name);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,7 +249,7 @@ internal class ContextMenuService : IHostedService
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Notify in chat when NotificationService is disabled
|
// Notify in chat when NotificationService is disabled
|
||||||
NotifyInChat($"Pair request sent to {target.TargetName}@{world.Name}.", NotificationType.Info);
|
NotifyInChat($"Pair request sent to {target.TargetName}@{target.TargetHomeWorld.Value.Name}.", NotificationType.Info);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -312,37 +309,8 @@ internal class ContextMenuService : IHostedService
|
|||||||
p.HomeWorld.RowId == target.TargetHomeWorld.RowId);
|
p.HomeWorld.RowId == target.TargetHomeWorld.RowId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private World GetWorld(uint worldId)
|
private bool IsWorldValid(uint worldId)
|
||||||
{
|
{
|
||||||
var sheet = _gameData.GetExcelSheet<World>()!;
|
return _dalamudUtil.WorldData.Value.ContainsKey((ushort)worldId);
|
||||||
var luminaWorlds = sheet.Where(x =>
|
|
||||||
{
|
|
||||||
var dc = x.DataCenter.ValueNullable;
|
|
||||||
var name = x.Name.ExtractText();
|
|
||||||
var internalName = x.InternalName.ExtractText();
|
|
||||||
|
|
||||||
if (dc == null || dc.Value.Region == 0 || string.IsNullOrWhiteSpace(dc.Value.Name.ExtractText()))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(name) || string.IsNullOrWhiteSpace(internalName))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (name.Contains('-', StringComparison.Ordinal) || name.Contains('_', StringComparison.Ordinal))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
return x.DataCenter.Value.Region != 5 || x.RowId > 3001 && x.RowId != 1200 && IsChineseJapaneseKoreanString(name);
|
|
||||||
});
|
|
||||||
|
|
||||||
return luminaWorlds.FirstOrDefault(x => x.RowId == worldId);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool IsChineseJapaneseKoreanString(string text) => text.All(IsChineseJapaneseKoreanCharacter);
|
|
||||||
|
|
||||||
private static bool IsChineseJapaneseKoreanCharacter(char c) => c >= 0x4E00 && c <= 0x9FFF;
|
|
||||||
|
|
||||||
public static bool IsWorldValid(World world)
|
|
||||||
{
|
|
||||||
var name = world.Name.ToString();
|
|
||||||
return !string.IsNullOrWhiteSpace(name) && char.IsUpper(name[0]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
using Dalamud.Game.ClientState.Conditions;
|
using Dalamud.Game.ClientState.Conditions;
|
||||||
using Dalamud.Game.ClientState.Objects.SubKinds;
|
using Dalamud.Game.ClientState.Objects.SubKinds;
|
||||||
using Dalamud.Game.ClientState.Objects.Types;
|
using Dalamud.Game.ClientState.Objects.Types;
|
||||||
|
using Dalamud.Game.Text;
|
||||||
using Dalamud.Plugin.Services;
|
using Dalamud.Plugin.Services;
|
||||||
using Dalamud.Utility;
|
using Dalamud.Utility;
|
||||||
using FFXIVClientStructs.FFXIV.Client.Game;
|
using FFXIVClientStructs.FFXIV.Client.Game;
|
||||||
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
||||||
using FFXIVClientStructs.FFXIV.Client.Game.Control;
|
using FFXIVClientStructs.FFXIV.Client.Game.Control;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.Game.UI;
|
||||||
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
||||||
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||||
using LightlessSync.API.Dto.CharaData;
|
using LightlessSync.API.Dto.CharaData;
|
||||||
@@ -20,12 +22,15 @@ 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;
|
||||||
using GameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject;
|
using GameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject;
|
||||||
|
using Map = Lumina.Excel.Sheets.Map;
|
||||||
using VisibilityFlags = FFXIVClientStructs.FFXIV.Client.Game.Object.VisibilityFlags;
|
using VisibilityFlags = FFXIVClientStructs.FFXIV.Client.Game.Object.VisibilityFlags;
|
||||||
|
|
||||||
namespace LightlessSync.Services;
|
namespace LightlessSync.Services;
|
||||||
@@ -57,6 +62,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
private string _lastGlobalBlockReason = string.Empty;
|
private string _lastGlobalBlockReason = string.Empty;
|
||||||
private ushort _lastZone = 0;
|
private ushort _lastZone = 0;
|
||||||
private ushort _lastWorldId = 0;
|
private ushort _lastWorldId = 0;
|
||||||
|
private uint _lastMapId = 0;
|
||||||
private bool _sentBetweenAreas = false;
|
private bool _sentBetweenAreas = false;
|
||||||
private Lazy<ulong> _cid;
|
private Lazy<ulong> _cid;
|
||||||
|
|
||||||
@@ -86,7 +92,8 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
WorldData = new(() =>
|
WorldData = new(() =>
|
||||||
{
|
{
|
||||||
return gameData.GetExcelSheet<Lumina.Excel.Sheets.World>(clientLanguage)!
|
return gameData.GetExcelSheet<Lumina.Excel.Sheets.World>(clientLanguage)!
|
||||||
.Where(w => !w.Name.IsEmpty && w.DataCenter.RowId != 0 && (w.IsPublic || char.IsUpper(w.Name.ToString()[0])))
|
.Where(w => !w.Name.IsEmpty && w.DataCenter.RowId != 0 && (w.IsPublic || char.IsUpper(w.Name.ToString()[0])
|
||||||
|
|| w is { RowId: > 1000, Region: 101 or 201 }))
|
||||||
.ToDictionary(w => (ushort)w.RowId, w => w.Name.ToString());
|
.ToDictionary(w => (ushort)w.RowId, w => w.Name.ToString());
|
||||||
});
|
});
|
||||||
JobData = new(() =>
|
JobData = new(() =>
|
||||||
@@ -659,7 +666,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
|
|
||||||
var location = new LocationInfo();
|
var location = new LocationInfo();
|
||||||
location.ServerId = _playerState.CurrentWorld.RowId;
|
location.ServerId = _playerState.CurrentWorld.RowId;
|
||||||
//location.InstanceId = UIState.Instance()->PublicInstance.InstanceId; //TODO:Need API update first
|
location.InstanceId = UIState.Instance()->PublicInstance.InstanceId;
|
||||||
location.TerritoryId = _clientState.TerritoryType;
|
location.TerritoryId = _clientState.TerritoryType;
|
||||||
location.MapId = _clientState.MapId;
|
location.MapId = _clientState.MapId;
|
||||||
if (houseMan != null)
|
if (houseMan != null)
|
||||||
@@ -685,20 +692,20 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
var outside = houseMan->OutdoorTerritory;
|
var outside = houseMan->OutdoorTerritory;
|
||||||
var house = outside->HouseId;
|
var house = outside->HouseId;
|
||||||
location.WardId = house.WardIndex + 1u;
|
location.WardId = house.WardIndex + 1u;
|
||||||
location.HouseId = (uint)houseMan->GetCurrentPlot() + 1;
|
//location.HouseId = (uint)houseMan->GetCurrentPlot() + 1;
|
||||||
location.DivisionId = houseMan->GetCurrentDivision();
|
location.DivisionId = houseMan->GetCurrentDivision();
|
||||||
}
|
}
|
||||||
//_logger.LogWarning(LocationToString(location));
|
//_logger.LogWarning(LocationToString(location));
|
||||||
}
|
}
|
||||||
return location;
|
return location;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string LocationToString(LocationInfo location)
|
public string LocationToString(LocationInfo location)
|
||||||
{
|
{
|
||||||
if (location.ServerId is 0 || location.TerritoryId is 0) return String.Empty;
|
if (location.ServerId is 0 || location.TerritoryId is 0) return String.Empty;
|
||||||
var str = WorldData.Value[(ushort)location.ServerId];
|
var str = WorldData.Value[(ushort)location.ServerId];
|
||||||
|
|
||||||
if (ContentFinderData.Value.TryGetValue(location.TerritoryId , out var dutyName))
|
if (ContentFinderData.Value.TryGetValue(location.TerritoryId, out var dutyName))
|
||||||
{
|
{
|
||||||
str += $" - [In Duty]{dutyName}";
|
str += $" - [In Duty]{dutyName}";
|
||||||
}
|
}
|
||||||
@@ -713,10 +720,10 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
str += $" - {MapData.Value[(ushort)location.MapId].MapName}";
|
str += $" - {MapData.Value[(ushort)location.MapId].MapName}";
|
||||||
}
|
}
|
||||||
|
|
||||||
// if (location.InstanceId is not 0)
|
if (location.InstanceId is not 0)
|
||||||
// {
|
{
|
||||||
// str += ((SeIconChar)(57520 + location.InstanceId)).ToIconString();
|
str += ((SeIconChar)(57520 + location.InstanceId)).ToIconString();
|
||||||
// }
|
}
|
||||||
|
|
||||||
if (location.WardId is not 0)
|
if (location.WardId is not 0)
|
||||||
{
|
{
|
||||||
@@ -838,33 +845,43 @@ 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 tick = 250;
|
||||||
int curWaitTime = 0;
|
const int initialSettle = 50;
|
||||||
|
|
||||||
|
var sw = Stopwatch.StartNew();
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
logger.LogTrace("[{redrawId}] Starting wait for {handler} to draw", redrawId, handler);
|
logger.LogTrace("[{redrawId}] Starting wait for {handler} to draw", redrawId, handler);
|
||||||
await Task.Delay(tick, ct.Value).ConfigureAwait(true);
|
|
||||||
curWaitTime += tick;
|
|
||||||
|
|
||||||
while ((!ct.Value.IsCancellationRequested)
|
await Task.Delay(initialSettle, token).ConfigureAwait(false);
|
||||||
&& curWaitTime < timeOut
|
|
||||||
&& await handler.IsBeingDrawnRunOnFrameworkAsync().ConfigureAwait(false)) // 0b100000000000 is "still rendering" or something
|
while (!token.IsCancellationRequested
|
||||||
|
&& sw.ElapsedMilliseconds < timeOut
|
||||||
|
&& await handler.IsBeingDrawnRunOnFrameworkAsync().ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
logger.LogTrace("[{redrawId}] Waiting for {handler} to finish drawing", redrawId, handler);
|
logger.LogTrace("[{redrawId}] Waiting for {handler} to finish drawing", redrawId, handler);
|
||||||
curWaitTime += tick;
|
await Task.Delay(tick, token).ConfigureAwait(false);
|
||||||
await Task.Delay(tick, ct.Value).ConfigureAwait(true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.LogTrace("[{redrawId}] Finished drawing after {curWaitTime}ms", redrawId, curWaitTime);
|
logger.LogTrace("[{redrawId}] Finished drawing after {ms}ms", redrawId, sw.ElapsedMilliseconds);
|
||||||
}
|
}
|
||||||
catch (AccessViolationException ex)
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
logger.LogWarning(ex, "Error accessing {handler}, object does not exist anymore?", handler);
|
logger.LogWarning(ex, "Error accessing {handler}, object does not exist anymore?", handler);
|
||||||
}
|
}
|
||||||
@@ -905,46 +922,118 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
public string? GetWorldNameFromPlayerAddress(nint address)
|
public string? GetWorldNameFromPlayerAddress(nint address)
|
||||||
{
|
{
|
||||||
if (address == nint.Zero) return null;
|
if (address == nint.Zero) return null;
|
||||||
|
|
||||||
EnsureIsOnFramework();
|
EnsureIsOnFramework();
|
||||||
var playerCharacter = _objectTable.OfType<IPlayerCharacter>().FirstOrDefault(p => p.Address == address);
|
var playerCharacter = _objectTable.OfType<IPlayerCharacter>().FirstOrDefault(p => p.Address == address);
|
||||||
if (playerCharacter == null) return null;
|
if (playerCharacter == null) return null;
|
||||||
|
|
||||||
var worldId = (ushort)playerCharacter.HomeWorld.RowId;
|
var worldId = (ushort)playerCharacter.HomeWorld.RowId;
|
||||||
return WorldData.Value.TryGetValue(worldId, out var worldName) ? worldName : null;
|
return WorldData.Value.TryGetValue(worldId, out var worldName) ? worldName : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void TargetPlayerByAddress(nint address)
|
||||||
|
{
|
||||||
|
if (address == nint.Zero) return;
|
||||||
|
if (_clientState.IsPvP) return;
|
||||||
|
|
||||||
|
_ = RunOnFrameworkThread(() =>
|
||||||
|
{
|
||||||
|
var gameObject = CreateGameObject(address);
|
||||||
|
if (gameObject is null) return;
|
||||||
|
|
||||||
|
var useFocusTarget = _configService.Current.UseFocusTarget;
|
||||||
|
if (useFocusTarget)
|
||||||
|
{
|
||||||
|
_targetManager.FocusTarget = gameObject;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_targetManager.Target = gameObject;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
|
private static extern bool IsBadReadPtr(IntPtr ptr, UIntPtr size);
|
||||||
|
|
||||||
|
private static bool IsValidPointer(nint ptr, int size = 8)
|
||||||
|
{
|
||||||
|
if (ptr == nint.Zero)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!Util.IsWine())
|
||||||
|
{
|
||||||
|
return !IsBadReadPtr(ptr, (UIntPtr)size);
|
||||||
|
}
|
||||||
|
return ptr != nint.Zero && (ptr % IntPtr.Size) == 0;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private unsafe void CheckCharacterForDrawing(nint address, string characterName)
|
private unsafe void CheckCharacterForDrawing(nint address, string characterName)
|
||||||
{
|
{
|
||||||
|
if (address == nint.Zero)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!IsValidPointer(address))
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Invalid pointer for character {name} at {addr}", characterName, address.ToString("X"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var gameObj = (GameObject*)address;
|
var gameObj = (GameObject*)address;
|
||||||
|
|
||||||
|
if (gameObj == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!_objectTable.Any(o => o?.Address == address))
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Character {name} at {addr} no longer in object table", characterName, address.ToString("X"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gameObj->ObjectKind == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
var drawObj = gameObj->DrawObject;
|
var drawObj = gameObj->DrawObject;
|
||||||
bool isDrawing = false;
|
bool isDrawing = false;
|
||||||
bool isDrawingChanged = false;
|
bool isDrawingChanged = false;
|
||||||
if ((nint)drawObj != IntPtr.Zero)
|
|
||||||
|
if ((nint)drawObj != IntPtr.Zero && IsValidPointer((nint)drawObj))
|
||||||
{
|
{
|
||||||
isDrawing = gameObj->RenderFlags == (VisibilityFlags)0b100000000000;
|
isDrawing = gameObj->RenderFlags == (VisibilityFlags)0b100000000000;
|
||||||
|
|
||||||
if (!isDrawing)
|
if (!isDrawing)
|
||||||
{
|
{
|
||||||
isDrawing = ((CharacterBase*)drawObj)->HasModelInSlotLoaded != 0;
|
var charBase = (CharacterBase*)drawObj;
|
||||||
if (!isDrawing)
|
if (charBase != null && IsValidPointer((nint)charBase))
|
||||||
{
|
{
|
||||||
isDrawing = ((CharacterBase*)drawObj)->HasModelFilesInSlotLoaded != 0;
|
isDrawing = charBase->HasModelInSlotLoaded != 0;
|
||||||
if (isDrawing && !string.Equals(_lastGlobalBlockPlayer, characterName, StringComparison.Ordinal)
|
if (!isDrawing)
|
||||||
&& !string.Equals(_lastGlobalBlockReason, "HasModelFilesInSlotLoaded", StringComparison.Ordinal))
|
|
||||||
{
|
{
|
||||||
_lastGlobalBlockPlayer = characterName;
|
isDrawing = charBase->HasModelFilesInSlotLoaded != 0;
|
||||||
_lastGlobalBlockReason = "HasModelFilesInSlotLoaded";
|
if (isDrawing && !string.Equals(_lastGlobalBlockPlayer, characterName, StringComparison.Ordinal)
|
||||||
isDrawingChanged = true;
|
&& !string.Equals(_lastGlobalBlockReason, "HasModelFilesInSlotLoaded", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
_lastGlobalBlockPlayer = characterName;
|
||||||
|
_lastGlobalBlockReason = "HasModelFilesInSlotLoaded";
|
||||||
|
isDrawingChanged = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
else
|
||||||
else
|
|
||||||
{
|
|
||||||
if (!string.Equals(_lastGlobalBlockPlayer, characterName, StringComparison.Ordinal)
|
|
||||||
&& !string.Equals(_lastGlobalBlockReason, "HasModelInSlotLoaded", StringComparison.Ordinal))
|
|
||||||
{
|
{
|
||||||
_lastGlobalBlockPlayer = characterName;
|
if (!string.Equals(_lastGlobalBlockPlayer, characterName, StringComparison.Ordinal)
|
||||||
_lastGlobalBlockReason = "HasModelInSlotLoaded";
|
&& !string.Equals(_lastGlobalBlockReason, "HasModelInSlotLoaded", StringComparison.Ordinal))
|
||||||
isDrawingChanged = true;
|
{
|
||||||
|
_lastGlobalBlockPlayer = characterName;
|
||||||
|
_lastGlobalBlockReason = "HasModelInSlotLoaded";
|
||||||
|
isDrawingChanged = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -975,6 +1064,11 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
|
|
||||||
private unsafe void FrameworkOnUpdateInternal()
|
private unsafe void FrameworkOnUpdateInternal()
|
||||||
{
|
{
|
||||||
|
if (!_clientState.IsLoggedIn || _objectTable.LocalPlayer == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if ((_objectTable.LocalPlayer?.IsDead ?? false) && _condition[ConditionFlag.BoundByDuty])
|
if ((_objectTable.LocalPlayer?.IsDead ?? false) && _condition[ConditionFlag.BoundByDuty])
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
@@ -994,18 +1088,23 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
}
|
}
|
||||||
|
|
||||||
var playerDescriptors = _actorObjectService.PlayerDescriptors;
|
var playerDescriptors = _actorObjectService.PlayerDescriptors;
|
||||||
for (var i = 0; i < playerDescriptors.Count; i++)
|
var descriptorCount = playerDescriptors.Count;
|
||||||
|
|
||||||
|
for (var i = 0; i < descriptorCount; i++)
|
||||||
{
|
{
|
||||||
|
if (i >= playerDescriptors.Count)
|
||||||
|
break;
|
||||||
|
|
||||||
var actor = playerDescriptors[i];
|
var actor = playerDescriptors[i];
|
||||||
|
|
||||||
var playerAddress = actor.Address;
|
var playerAddress = actor.Address;
|
||||||
if (playerAddress == nint.Zero)
|
if (playerAddress == nint.Zero || !IsValidPointer(playerAddress))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (actor.ObjectIndex >= 200)
|
if (actor.ObjectIndex >= 200)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
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;
|
||||||
@@ -1013,17 +1112,16 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
|
|
||||||
if (!IsAnythingDrawing)
|
if (!IsAnythingDrawing)
|
||||||
{
|
{
|
||||||
var gameObj = (GameObject*)playerAddress;
|
if (!_objectTable.Any(o => o?.Address == playerAddress))
|
||||||
var currentName = gameObj != null ? gameObj->NameString ?? string.Empty : string.Empty;
|
{
|
||||||
var charaName = string.IsNullOrEmpty(currentName) ? actor.Name : currentName;
|
continue;
|
||||||
CheckCharacterForDrawing(playerAddress, charaName);
|
}
|
||||||
|
|
||||||
|
CheckCharacterForDrawing(playerAddress, actor.Name);
|
||||||
|
|
||||||
if (IsAnythingDrawing)
|
if (IsAnythingDrawing)
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1092,7 +1190,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Cutscene
|
// Cutscene
|
||||||
HandleStateTransition(() => IsInCutscene,v => IsInCutscene = v, shouldBeInCutscene, "Cutscene",
|
HandleStateTransition(() => IsInCutscene, v => IsInCutscene = v, shouldBeInCutscene, "Cutscene",
|
||||||
onEnter: () =>
|
onEnter: () =>
|
||||||
{
|
{
|
||||||
Mediator.Publish(new CutsceneStartMessage());
|
Mediator.Publish(new CutsceneStartMessage());
|
||||||
@@ -1136,6 +1234,18 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
Mediator.Publish(new ResumeScanMessage(nameof(ConditionFlag.BetweenAreas)));
|
Mediator.Publish(new ResumeScanMessage(nameof(ConditionFlag.BetweenAreas)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Map
|
||||||
|
if (!_sentBetweenAreas)
|
||||||
|
{
|
||||||
|
var mapid = _clientState.MapId;
|
||||||
|
if (mapid != _lastMapId)
|
||||||
|
{
|
||||||
|
_lastMapId = mapid;
|
||||||
|
Mediator.Publish(new MapChangedMessage(mapid));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
var localPlayer = _objectTable.LocalPlayer;
|
var localPlayer = _objectTable.LocalPlayer;
|
||||||
if (localPlayer != null)
|
if (localPlayer != null)
|
||||||
{
|
{
|
||||||
@@ -1220,4 +1330,4 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
|||||||
onExit();
|
onExit();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
using Dalamud.Plugin.Services;
|
using Dalamud.Plugin.Services;
|
||||||
using LightlessSync.API.Dto.User;
|
using LightlessSync.API.Dto.User;
|
||||||
using LightlessSync.Services.ActorTracking;
|
using LightlessSync.Services.ActorTracking;
|
||||||
using LightlessSync.Services.Mediator;
|
using LightlessSync.Services.Mediator;
|
||||||
@@ -23,6 +23,7 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase
|
|||||||
private readonly HashSet<string> _syncshellCids = [];
|
private readonly HashSet<string> _syncshellCids = [];
|
||||||
private volatile bool _pendingLocalBroadcast;
|
private volatile bool _pendingLocalBroadcast;
|
||||||
private TimeSpan? _pendingLocalTtl;
|
private TimeSpan? _pendingLocalTtl;
|
||||||
|
private string? _pendingLocalGid;
|
||||||
|
|
||||||
private static readonly TimeSpan _maxAllowedTtl = TimeSpan.FromMinutes(4);
|
private static readonly TimeSpan _maxAllowedTtl = TimeSpan.FromMinutes(4);
|
||||||
private static readonly TimeSpan _retryDelay = TimeSpan.FromMinutes(1);
|
private static readonly TimeSpan _retryDelay = TimeSpan.FromMinutes(1);
|
||||||
@@ -36,6 +37,7 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase
|
|||||||
private const int _maxQueueSize = 100;
|
private const int _maxQueueSize = 100;
|
||||||
|
|
||||||
private volatile bool _batchRunning = false;
|
private volatile bool _batchRunning = false;
|
||||||
|
private volatile bool _disposed = false;
|
||||||
|
|
||||||
public IReadOnlyDictionary<string, BroadcastEntry> BroadcastCache => _broadcastCache;
|
public IReadOnlyDictionary<string, BroadcastEntry> BroadcastCache => _broadcastCache;
|
||||||
public readonly record struct BroadcastEntry(bool IsBroadcasting, DateTime ExpiryTime, string? GID);
|
public readonly record struct BroadcastEntry(bool IsBroadcasting, DateTime ExpiryTime, string? GID);
|
||||||
@@ -68,6 +70,9 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
public void Update()
|
public void Update()
|
||||||
{
|
{
|
||||||
|
if (_disposed)
|
||||||
|
return;
|
||||||
|
|
||||||
_frameCounter++;
|
_frameCounter++;
|
||||||
var lookupsThisFrame = 0;
|
var lookupsThisFrame = 0;
|
||||||
|
|
||||||
@@ -78,12 +83,12 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
var now = DateTime.UtcNow;
|
var now = DateTime.UtcNow;
|
||||||
|
|
||||||
foreach (var address in _actorTracker.PlayerAddresses)
|
foreach (var descriptor in _actorTracker.PlayerDescriptors)
|
||||||
{
|
{
|
||||||
if (address == nint.Zero)
|
if (string.IsNullOrEmpty(descriptor.HashedContentId))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
var cid = DalamudUtilService.GetHashedCIDFromPlayerPointer(address);
|
var cid = descriptor.HashedContentId;
|
||||||
var isStale = !_broadcastCache.TryGetValue(cid, out var entry) || entry.ExpiryTime <= now;
|
var isStale = !_broadcastCache.TryGetValue(cid, out var entry) || entry.ExpiryTime <= now;
|
||||||
|
|
||||||
if (isStale && _lookupQueuedCids.Add(cid) && _lookupQueue.Count < _maxQueueSize)
|
if (isStale && _lookupQueuedCids.Add(cid) && _lookupQueue.Count < _maxQueueSize)
|
||||||
@@ -111,7 +116,14 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
private async Task BatchUpdateBroadcastCacheAsync(List<string> cids)
|
private async Task BatchUpdateBroadcastCacheAsync(List<string> cids)
|
||||||
{
|
{
|
||||||
|
if (_disposed)
|
||||||
|
return;
|
||||||
|
|
||||||
var results = await _broadcastService.AreUsersBroadcastingAsync(cids).ConfigureAwait(false);
|
var results = await _broadcastService.AreUsersBroadcastingAsync(cids).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (_disposed)
|
||||||
|
return;
|
||||||
|
|
||||||
var now = DateTime.UtcNow;
|
var now = DateTime.UtcNow;
|
||||||
|
|
||||||
foreach (var (cid, info) in results)
|
foreach (var (cid, info) in results)
|
||||||
@@ -130,6 +142,9 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase
|
|||||||
(_, old) => new BroadcastEntry(info.IsBroadcasting, expiry, info.GID));
|
(_, old) => new BroadcastEntry(info.IsBroadcasting, expiry, info.GID));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_disposed)
|
||||||
|
return;
|
||||||
|
|
||||||
var activeCids = _broadcastCache
|
var activeCids = _broadcastCache
|
||||||
.Where(e => e.Value.IsBroadcasting && e.Value.ExpiryTime > now)
|
.Where(e => e.Value.IsBroadcasting && e.Value.ExpiryTime > now)
|
||||||
.Select(e => e.Key)
|
.Select(e => e.Key)
|
||||||
@@ -142,6 +157,9 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
private void OnBroadcastStatusChanged(BroadcastStatusChangedMessage msg)
|
private void OnBroadcastStatusChanged(BroadcastStatusChangedMessage msg)
|
||||||
{
|
{
|
||||||
|
if (_disposed)
|
||||||
|
return;
|
||||||
|
|
||||||
if (!msg.Enabled)
|
if (!msg.Enabled)
|
||||||
{
|
{
|
||||||
_broadcastCache.Clear();
|
_broadcastCache.Clear();
|
||||||
@@ -158,6 +176,7 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
_pendingLocalBroadcast = true;
|
_pendingLocalBroadcast = true;
|
||||||
_pendingLocalTtl = msg.Ttl;
|
_pendingLocalTtl = msg.Ttl;
|
||||||
|
_pendingLocalGid = msg.Gid;
|
||||||
TryPrimeLocalBroadcastCache();
|
TryPrimeLocalBroadcastCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,11 +192,12 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase
|
|||||||
var expiry = DateTime.UtcNow + ttl;
|
var expiry = DateTime.UtcNow + ttl;
|
||||||
|
|
||||||
_broadcastCache.AddOrUpdate(localCid,
|
_broadcastCache.AddOrUpdate(localCid,
|
||||||
new BroadcastEntry(true, expiry, null),
|
new BroadcastEntry(true, expiry, _pendingLocalGid),
|
||||||
(_, old) => new BroadcastEntry(true, expiry, old.GID));
|
(_, old) => new BroadcastEntry(true, expiry, _pendingLocalGid ?? old.GID));
|
||||||
|
|
||||||
_pendingLocalBroadcast = false;
|
_pendingLocalBroadcast = false;
|
||||||
_pendingLocalTtl = null;
|
_pendingLocalTtl = null;
|
||||||
|
_pendingLocalGid = null;
|
||||||
|
|
||||||
var now = DateTime.UtcNow;
|
var now = DateTime.UtcNow;
|
||||||
var activeCids = _broadcastCache
|
var activeCids = _broadcastCache
|
||||||
@@ -187,10 +207,14 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
_lightFinderPlateHandler.UpdateBroadcastingCids(activeCids);
|
_lightFinderPlateHandler.UpdateBroadcastingCids(activeCids);
|
||||||
_lightFinderNativePlateHandler.UpdateBroadcastingCids(activeCids);
|
_lightFinderNativePlateHandler.UpdateBroadcastingCids(activeCids);
|
||||||
|
UpdateSyncshellBroadcasts();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateSyncshellBroadcasts()
|
private void UpdateSyncshellBroadcasts()
|
||||||
{
|
{
|
||||||
|
if (_disposed)
|
||||||
|
return;
|
||||||
|
|
||||||
var now = DateTime.UtcNow;
|
var now = DateTime.UtcNow;
|
||||||
var nearbyCids = GetNearbyHashedCids(out _);
|
var nearbyCids = GetNearbyHashedCids(out _);
|
||||||
var newSet = nearbyCids.Count == 0
|
var newSet = nearbyCids.Count == 0
|
||||||
@@ -324,17 +348,35 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase
|
|||||||
|
|
||||||
protected override void Dispose(bool disposing)
|
protected override void Dispose(bool disposing)
|
||||||
{
|
{
|
||||||
|
_disposed = true;
|
||||||
base.Dispose(disposing);
|
base.Dispose(disposing);
|
||||||
_framework.Update -= OnFrameworkUpdate;
|
_framework.Update -= OnFrameworkUpdate;
|
||||||
if (_cleanupTask != null)
|
|
||||||
|
try
|
||||||
{
|
{
|
||||||
_cleanupTask?.Wait(100, _cleanupCts.Token);
|
_cleanupCts.Cancel();
|
||||||
|
}
|
||||||
|
catch (ObjectDisposedException)
|
||||||
|
{
|
||||||
|
// Already disposed, can be ignored :)
|
||||||
}
|
}
|
||||||
|
|
||||||
_cleanupCts.Cancel();
|
try
|
||||||
_cleanupCts.Dispose();
|
{
|
||||||
|
_cleanupTask?.Wait(100);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
// Task may have already completed or been cancelled?
|
||||||
|
}
|
||||||
|
|
||||||
_cleanupTask?.Wait(100);
|
try
|
||||||
_cleanupCts.Dispose();
|
{
|
||||||
|
_cleanupCts.Dispose();
|
||||||
|
}
|
||||||
|
catch (ObjectDisposedException)
|
||||||
|
{
|
||||||
|
// Already disposed, ignore
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using Dalamud.Interface;
|
using Dalamud.Interface;
|
||||||
using LightlessSync.API.Dto.Group;
|
using LightlessSync.API.Dto.Group;
|
||||||
using LightlessSync.API.Dto.User;
|
using LightlessSync.API.Dto.User;
|
||||||
using LightlessSync.LightlessConfiguration;
|
using LightlessSync.LightlessConfiguration;
|
||||||
@@ -121,7 +121,10 @@ public class LightFinderService : IHostedService, IMediatorSubscriber
|
|||||||
_waitingForTtlFetch = false;
|
_waitingForTtlFetch = false;
|
||||||
|
|
||||||
if (!wasEnabled || previousRemaining != validTtl)
|
if (!wasEnabled || previousRemaining != validTtl)
|
||||||
_mediator.Publish(new BroadcastStatusChangedMessage(true, validTtl));
|
{
|
||||||
|
var gid = _config.Current.SyncshellFinderEnabled ? _config.Current.SelectedFinderSyncshell : null;
|
||||||
|
_mediator.Publish(new BroadcastStatusChangedMessage(true, validTtl, gid));
|
||||||
|
}
|
||||||
|
|
||||||
_logger.LogInformation("Lightfinder broadcast enabled ({Context}), TTL: {TTL}", context, validTtl);
|
_logger.LogInformation("Lightfinder broadcast enabled ({Context}), TTL: {TTL}", context, validTtl);
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
137
LightlessSync/Services/LocationShareService.cs
Normal file
137
LightlessSync/Services/LocationShareService.cs
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
using LightlessSync.API.Data;
|
||||||
|
using LightlessSync.API.Dto.CharaData;
|
||||||
|
using LightlessSync.API.Dto.User;
|
||||||
|
using LightlessSync.Services.Mediator;
|
||||||
|
using LightlessSync.WebAPI;
|
||||||
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Primitives;
|
||||||
|
|
||||||
|
namespace LightlessSync.Services
|
||||||
|
{
|
||||||
|
public class LocationShareService : DisposableMediatorSubscriberBase
|
||||||
|
{
|
||||||
|
private readonly DalamudUtilService _dalamudUtilService;
|
||||||
|
private readonly ApiController _apiController;
|
||||||
|
private IMemoryCache _locations = new MemoryCache(new MemoryCacheOptions());
|
||||||
|
private IMemoryCache _sharingStatus = new MemoryCache(new MemoryCacheOptions());
|
||||||
|
private CancellationTokenSource _resetToken = new CancellationTokenSource();
|
||||||
|
|
||||||
|
public LocationShareService(ILogger<LocationShareService> logger, LightlessMediator mediator, DalamudUtilService dalamudUtilService, ApiController apiController) : base(logger, mediator)
|
||||||
|
{
|
||||||
|
_dalamudUtilService = dalamudUtilService;
|
||||||
|
_apiController = apiController;
|
||||||
|
|
||||||
|
|
||||||
|
Mediator.Subscribe<DisconnectedMessage>(this, (msg) =>
|
||||||
|
{
|
||||||
|
_resetToken.Cancel();
|
||||||
|
_resetToken.Dispose();
|
||||||
|
_resetToken = new CancellationTokenSource();
|
||||||
|
});
|
||||||
|
Mediator.Subscribe<ConnectedMessage>(this, (msg) =>
|
||||||
|
{
|
||||||
|
_ = _apiController.UpdateLocation(new LocationDto(new UserData(_apiController.UID, apiController.DisplayName), _dalamudUtilService.GetMapData()));
|
||||||
|
_ = RequestAllLocation();
|
||||||
|
} );
|
||||||
|
Mediator.Subscribe<LocationSharingMessage>(this, UpdateLocationList);
|
||||||
|
Mediator.Subscribe<MapChangedMessage>(this,
|
||||||
|
msg => _ = _apiController.UpdateLocation(new LocationDto(new UserData(_apiController.UID, _apiController.DisplayName), _dalamudUtilService.GetMapData())));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateLocationList(LocationSharingMessage msg)
|
||||||
|
{
|
||||||
|
if (_locations.TryGetValue(msg.User.UID, out _) && msg.LocationInfo.ServerId is 0)
|
||||||
|
{
|
||||||
|
_locations.Remove(msg.User.UID);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( msg.LocationInfo.ServerId is not 0 && msg.ExpireAt > DateTime.UtcNow)
|
||||||
|
{
|
||||||
|
AddLocationInfo(msg.User.UID, msg.LocationInfo, msg.ExpireAt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddLocationInfo(string uid, LocationInfo location, DateTimeOffset expireAt)
|
||||||
|
{
|
||||||
|
var options = new MemoryCacheEntryOptions()
|
||||||
|
.SetAbsoluteExpiration(expireAt)
|
||||||
|
.AddExpirationToken(new CancellationChangeToken(_resetToken.Token));
|
||||||
|
_locations.Set(uid, location, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task RequestAllLocation()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var (data, status) = await _apiController.RequestAllLocationInfo().ConfigureAwait(false);
|
||||||
|
foreach (var dto in data)
|
||||||
|
{
|
||||||
|
AddLocationInfo(dto.LocationDto.User.UID, dto.LocationDto.Location, dto.ExpireAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var dto in status)
|
||||||
|
{
|
||||||
|
AddStatus(dto.User.UID, dto.ExpireAt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Logger.LogError(e,"RequestAllLocation error : ");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddStatus(string uid, DateTimeOffset expireAt)
|
||||||
|
{
|
||||||
|
var options = new MemoryCacheEntryOptions()
|
||||||
|
.SetAbsoluteExpiration(expireAt)
|
||||||
|
.AddExpirationToken(new CancellationChangeToken(_resetToken.Token));
|
||||||
|
_sharingStatus.Set(uid, expireAt, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetUserLocation(string uid)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_locations.TryGetValue<LocationInfo>(uid, out var location))
|
||||||
|
{
|
||||||
|
return _dalamudUtilService.LocationToString(location);
|
||||||
|
}
|
||||||
|
return String.Empty;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Logger.LogError(e,"GetUserLocation error : ");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public DateTimeOffset GetSharingStatus(string uid)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_sharingStatus.TryGetValue<DateTimeOffset>(uid, out var expireAt))
|
||||||
|
{
|
||||||
|
return expireAt;
|
||||||
|
}
|
||||||
|
return DateTimeOffset.MinValue;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Logger.LogError(e,"GetSharingStatus error : ");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateSharingStatus(List<string> users, DateTimeOffset expireAt)
|
||||||
|
{
|
||||||
|
foreach (var user in users)
|
||||||
|
{
|
||||||
|
AddStatus(user, expireAt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -63,23 +63,31 @@ public sealed class LightlessMediator : IHostedService
|
|||||||
|
|
||||||
_ = Task.Run(async () =>
|
_ = Task.Run(async () =>
|
||||||
{
|
{
|
||||||
while (!_loopCts.Token.IsCancellationRequested)
|
try
|
||||||
{
|
{
|
||||||
while (!_processQueue)
|
while (!_loopCts.Token.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
|
while (!_processQueue)
|
||||||
|
{
|
||||||
|
await Task.Delay(100, _loopCts.Token).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
await Task.Delay(100, _loopCts.Token).ConfigureAwait(false);
|
await Task.Delay(100, _loopCts.Token).ConfigureAwait(false);
|
||||||
|
|
||||||
|
HashSet<MessageBase> processedMessages = [];
|
||||||
|
while (_messageQueue.TryDequeue(out var message))
|
||||||
|
{
|
||||||
|
if (processedMessages.Contains(message)) { continue; }
|
||||||
|
|
||||||
|
processedMessages.Add(message);
|
||||||
|
|
||||||
|
ExecuteMessage(message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
await Task.Delay(100, _loopCts.Token).ConfigureAwait(false);
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
HashSet<MessageBase> processedMessages = [];
|
_logger.LogInformation("LightlessMediator stopped");
|
||||||
while (_messageQueue.TryDequeue(out var message))
|
|
||||||
{
|
|
||||||
if (processedMessages.Contains(message)) { continue; }
|
|
||||||
processedMessages.Add(message);
|
|
||||||
|
|
||||||
ExecuteMessage(message);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -123,7 +124,7 @@ public record GPoseLobbyReceivePoseData(UserData UserData, PoseData PoseData) :
|
|||||||
public record GPoseLobbyReceiveWorldData(UserData UserData, WorldData WorldData) : MessageBase;
|
public record GPoseLobbyReceiveWorldData(UserData UserData, WorldData WorldData) : MessageBase;
|
||||||
public record OpenCharaDataHubWithFilterMessage(UserData UserData) : MessageBase;
|
public record OpenCharaDataHubWithFilterMessage(UserData UserData) : MessageBase;
|
||||||
public record EnableBroadcastMessage(string HashedCid, bool Enabled) : MessageBase;
|
public record EnableBroadcastMessage(string HashedCid, bool Enabled) : MessageBase;
|
||||||
public record BroadcastStatusChangedMessage(bool Enabled, TimeSpan? Ttl) : MessageBase;
|
public record BroadcastStatusChangedMessage(bool Enabled, TimeSpan? Ttl, string? Gid = null) : MessageBase;
|
||||||
public record UserLeftSyncshell(string gid) : MessageBase;
|
public record UserLeftSyncshell(string gid) : MessageBase;
|
||||||
public record UserJoinedSyncshell(string gid) : MessageBase;
|
public record UserJoinedSyncshell(string gid) : MessageBase;
|
||||||
public record SyncshellBroadcastsUpdatedMessage : MessageBase;
|
public record SyncshellBroadcastsUpdatedMessage : MessageBase;
|
||||||
@@ -135,5 +136,7 @@ public record ChatChannelsUpdated : MessageBase;
|
|||||||
public record ChatChannelMessageAdded(string ChannelKey, ChatMessageEntry Message) : MessageBase;
|
public record ChatChannelMessageAdded(string ChannelKey, ChatMessageEntry Message) : MessageBase;
|
||||||
public record GroupCollectionChangedMessage : MessageBase;
|
public record GroupCollectionChangedMessage : MessageBase;
|
||||||
public record OpenUserProfileMessage(UserData User) : MessageBase;
|
public record OpenUserProfileMessage(UserData User) : MessageBase;
|
||||||
|
public record LocationSharingMessage(UserData User, LocationInfo LocationInfo, DateTimeOffset ExpireAt) : MessageBase;
|
||||||
|
public record 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);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ using LightlessSync.UI.Tags;
|
|||||||
using LightlessSync.WebAPI;
|
using LightlessSync.WebAPI;
|
||||||
using LightlessSync.UI.Services;
|
using LightlessSync.UI.Services;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using LightlessSync.PlayerData.Factories;
|
||||||
|
|
||||||
namespace LightlessSync.Services;
|
namespace LightlessSync.Services;
|
||||||
|
|
||||||
@@ -23,6 +24,7 @@ public class UiFactory
|
|||||||
private readonly PerformanceCollectorService _performanceCollectorService;
|
private readonly PerformanceCollectorService _performanceCollectorService;
|
||||||
private readonly ProfileTagService _profileTagService;
|
private readonly ProfileTagService _profileTagService;
|
||||||
private readonly DalamudUtilService _dalamudUtilService;
|
private readonly DalamudUtilService _dalamudUtilService;
|
||||||
|
private readonly PairFactory _pairFactory;
|
||||||
|
|
||||||
public UiFactory(
|
public UiFactory(
|
||||||
ILoggerFactory loggerFactory,
|
ILoggerFactory loggerFactory,
|
||||||
@@ -34,7 +36,8 @@ public class UiFactory
|
|||||||
LightlessProfileManager lightlessProfileManager,
|
LightlessProfileManager lightlessProfileManager,
|
||||||
PerformanceCollectorService performanceCollectorService,
|
PerformanceCollectorService performanceCollectorService,
|
||||||
ProfileTagService profileTagService,
|
ProfileTagService profileTagService,
|
||||||
DalamudUtilService dalamudUtilService)
|
DalamudUtilService dalamudUtilService,
|
||||||
|
PairFactory pairFactory)
|
||||||
{
|
{
|
||||||
_loggerFactory = loggerFactory;
|
_loggerFactory = loggerFactory;
|
||||||
_lightlessMediator = lightlessMediator;
|
_lightlessMediator = lightlessMediator;
|
||||||
@@ -46,6 +49,7 @@ public class UiFactory
|
|||||||
_performanceCollectorService = performanceCollectorService;
|
_performanceCollectorService = performanceCollectorService;
|
||||||
_profileTagService = profileTagService;
|
_profileTagService = profileTagService;
|
||||||
_dalamudUtilService = dalamudUtilService;
|
_dalamudUtilService = dalamudUtilService;
|
||||||
|
_pairFactory = pairFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
public SyncshellAdminUI CreateSyncshellAdminUi(GroupFullInfoDto dto)
|
public SyncshellAdminUI CreateSyncshellAdminUi(GroupFullInfoDto dto)
|
||||||
@@ -58,7 +62,8 @@ public class UiFactory
|
|||||||
_pairUiService,
|
_pairUiService,
|
||||||
dto,
|
dto,
|
||||||
_performanceCollectorService,
|
_performanceCollectorService,
|
||||||
_lightlessProfileManager);
|
_lightlessProfileManager,
|
||||||
|
_pairFactory);
|
||||||
}
|
}
|
||||||
|
|
||||||
public StandaloneProfileUi CreateStandaloneProfileUi(Pair pair)
|
public StandaloneProfileUi CreateStandaloneProfileUi(Pair pair)
|
||||||
|
|||||||
@@ -1,23 +1,28 @@
|
|||||||
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
||||||
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
||||||
|
using FFXIVClientStructs.Havok.Common.Serialize.Resource;
|
||||||
using FFXIVClientStructs.Havok.Animation;
|
using FFXIVClientStructs.Havok.Animation;
|
||||||
using FFXIVClientStructs.Havok.Common.Base.Types;
|
using FFXIVClientStructs.Havok.Common.Base.Types;
|
||||||
using FFXIVClientStructs.Havok.Common.Serialize.Util;
|
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,125 +34,580 @@ 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:
|
|
||||||
reader.ReadInt32(); // ignore
|
|
||||||
reader.ReadInt32(); // ignore
|
|
||||||
reader.ReadInt16(); // read 2 (num animations)
|
|
||||||
reader.ReadInt16(); // read 2 (modelid)
|
|
||||||
var type = reader.ReadByte();// read 1 (type)
|
|
||||||
if (type != 0) return null; // it's not human, just ignore it, whatever
|
|
||||||
|
|
||||||
reader.ReadByte(); // read 1 (variant)
|
|
||||||
reader.ReadInt32(); // ignore
|
|
||||||
var havokPosition = reader.ReadInt32();
|
|
||||||
var footerPosition = reader.ReadInt32();
|
|
||||||
var havokDataSize = footerPosition - havokPosition;
|
|
||||||
reader.BaseStream.Position = havokPosition;
|
|
||||||
var havokData = reader.ReadBytes(havokDataSize);
|
|
||||||
if (havokData.Length <= 8) return null; // no havok data
|
|
||||||
|
|
||||||
var output = new Dictionary<string, List<ushort>>(StringComparer.OrdinalIgnoreCase);
|
|
||||||
var tempHavokDataPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()) + ".hkx";
|
|
||||||
var tempHavokDataPathAnsi = Marshal.StringToHGlobalAnsi(tempHavokDataPath);
|
|
||||||
|
|
||||||
|
// PAP header (mostly from vfxeditor)
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
File.WriteAllBytes(tempHavokDataPath, havokData);
|
_ = reader.ReadInt32(); // ignore
|
||||||
|
_ = reader.ReadInt32(); // ignore
|
||||||
|
var numAnimations = reader.ReadInt16(); // num animations
|
||||||
|
var modelId = reader.ReadInt16(); // modelid
|
||||||
|
|
||||||
var loadoptions = stackalloc hkSerializeUtil.LoadOptions[1];
|
if (numAnimations < 0 || numAnimations > 1000)
|
||||||
loadoptions->TypeInfoRegistry = hkBuiltinTypeRegistry.Instance()->GetTypeInfoRegistry();
|
|
||||||
loadoptions->ClassNameRegistry = hkBuiltinTypeRegistry.Instance()->GetClassNameRegistry();
|
|
||||||
loadoptions->Flags = new hkFlags<hkSerializeUtil.LoadOptionBits, int>
|
|
||||||
{
|
{
|
||||||
Storage = (int)(hkSerializeUtil.LoadOptionBits.Default)
|
_logger.LogWarning("PAP file {hash} has invalid animation count {count}, skipping", hash, numAnimations);
|
||||||
};
|
return null;
|
||||||
|
|
||||||
var resource = hkSerializeUtil.LoadFromFile((byte*)tempHavokDataPathAnsi, null, loadoptions);
|
|
||||||
if (resource == null)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("Resource was null after loading");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var rootLevelName = @"hkRootLevelContainer"u8;
|
var type = reader.ReadByte(); // type
|
||||||
fixed (byte* n1 = rootLevelName)
|
if (type != 0)
|
||||||
|
return null; // not human
|
||||||
|
|
||||||
|
_ = reader.ReadByte(); // variant
|
||||||
|
_ = reader.ReadInt32(); // ignore
|
||||||
|
|
||||||
|
var havokPosition = reader.ReadInt32();
|
||||||
|
var footerPosition = reader.ReadInt32();
|
||||||
|
|
||||||
|
if (havokPosition <= 0 || footerPosition <= havokPosition ||
|
||||||
|
footerPosition > fs.Length || havokPosition >= fs.Length)
|
||||||
{
|
{
|
||||||
var container = (hkRootLevelContainer*)resource->GetContentsPointer(n1, hkBuiltinTypeRegistry.Instance()->GetTypeInfoRegistry());
|
_logger.LogWarning("PAP file {hash} has invalid offsets (havok={havok}, footer={footer}, length={length})",
|
||||||
var animationName = @"hkaAnimationContainer"u8;
|
hash, havokPosition, footerPosition, fs.Length);
|
||||||
fixed (byte* n2 = animationName)
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var havokDataSizeLong = (long)footerPosition - havokPosition;
|
||||||
|
if (havokDataSizeLong <= 8 || havokDataSizeLong > int.MaxValue)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("PAP file {hash} has invalid Havok data size {size}", hash, havokDataSizeLong);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var havokDataSize = (int)havokDataSizeLong;
|
||||||
|
|
||||||
|
reader.BaseStream.Position = havokPosition;
|
||||||
|
|
||||||
|
var havokData = new byte[havokDataSize];
|
||||||
|
var bytesRead = reader.Read(havokData, 0, havokDataSize);
|
||||||
|
if (bytesRead != havokDataSize)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("PAP file {hash}: Expected to read {expected} bytes but got {actual}",
|
||||||
|
hash, havokDataSize, bytesRead);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (havokData.Length < 8)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var tempSets = new Dictionary<string, HashSet<ushort>>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
var tempFileName = $"lightless_pap_{Guid.NewGuid():N}_{hash.Substring(0, Math.Min(8, hash.Length))}.hkx";
|
||||||
|
var tempHavokDataPath = Path.Combine(Path.GetTempPath(), tempFileName);
|
||||||
|
IntPtr tempHavokDataPathAnsi = IntPtr.Zero;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var tempDir = Path.GetDirectoryName(tempHavokDataPath);
|
||||||
|
if (!Directory.Exists(tempDir))
|
||||||
{
|
{
|
||||||
var animContainer = (hkaAnimationContainer*)container->findObjectByName(n2, null);
|
_logger.LogWarning("Temp directory {dir} doesn't exist", tempDir);
|
||||||
for (int i = 0; i < animContainer->Bindings.Length; i++)
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
File.WriteAllBytes(tempHavokDataPath, havokData);
|
||||||
|
|
||||||
|
if (!File.Exists(tempHavokDataPath))
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Temporary havok file was not created at {path}", tempHavokDataPath);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var writtenFileInfo = new FileInfo(tempHavokDataPath);
|
||||||
|
if (writtenFileInfo.Length != havokData.Length)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Written temp file size mismatch: expected {expected}, got {actual}",
|
||||||
|
havokData.Length, writtenFileInfo.Length);
|
||||||
|
File.Delete(tempHavokDataPath);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
tempHavokDataPathAnsi = Marshal.StringToHGlobalAnsi(tempHavokDataPath);
|
||||||
|
|
||||||
|
var loadoptions = stackalloc hkSerializeUtil.LoadOptions[1];
|
||||||
|
loadoptions->TypeInfoRegistry = hkBuiltinTypeRegistry.Instance()->GetTypeInfoRegistry();
|
||||||
|
loadoptions->ClassNameRegistry = hkBuiltinTypeRegistry.Instance()->GetClassNameRegistry();
|
||||||
|
loadoptions->Flags = new hkFlags<hkSerializeUtil.LoadOptionBits, int>
|
||||||
|
{
|
||||||
|
Storage = (int)hkSerializeUtil.LoadOptionBits.Default
|
||||||
|
};
|
||||||
|
|
||||||
|
hkResource* resource = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
resource = hkSerializeUtil.LoadFromFile((byte*)tempHavokDataPathAnsi, null, loadoptions);
|
||||||
|
}
|
||||||
|
catch (SEHException ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "SEH exception loading Havok file from {path} (hash={hash}). Native error code: 0x{code:X}",
|
||||||
|
tempHavokDataPath, hash, ex.ErrorCode);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resource == null)
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Havok resource was null after loading from {path} (hash={hash})", tempHavokDataPath, hash);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((nint)resource == nint.Zero || !IsValidPointer((IntPtr)resource))
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Havok resource pointer is invalid (hash={hash})", hash);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var rootLevelName = @"hkRootLevelContainer"u8;
|
||||||
|
fixed (byte* n1 = rootLevelName)
|
||||||
|
{
|
||||||
|
var container = (hkRootLevelContainer*)resource->GetContentsPointer(n1, hkBuiltinTypeRegistry.Instance()->GetTypeInfoRegistry());
|
||||||
|
if (container == null)
|
||||||
{
|
{
|
||||||
var binding = animContainer->Bindings[i].ptr;
|
_logger.LogDebug("hkRootLevelContainer is null (hash={hash})", hash);
|
||||||
var boneTransform = binding->TransformTrackToBoneIndices;
|
return null;
|
||||||
string name = binding->OriginalSkeletonName.String! + "_" + i;
|
|
||||||
output[name] = [];
|
|
||||||
for (int boneIdx = 0; boneIdx < boneTransform.Length; boneIdx++)
|
|
||||||
{
|
|
||||||
output[name].Add((ushort)boneTransform[boneIdx]);
|
|
||||||
}
|
|
||||||
output[name].Sort();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ((nint)container == nint.Zero || !IsValidPointer((IntPtr)container))
|
||||||
|
{
|
||||||
|
_logger.LogDebug("hkRootLevelContainer pointer is invalid (hash={hash})", hash);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var animationName = @"hkaAnimationContainer"u8;
|
||||||
|
fixed (byte* n2 = animationName)
|
||||||
|
{
|
||||||
|
var animContainer = (hkaAnimationContainer*)container->findObjectByName(n2, null);
|
||||||
|
if (animContainer == null)
|
||||||
|
{
|
||||||
|
_logger.LogDebug("hkaAnimationContainer is null (hash={hash})", hash);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((nint)animContainer == nint.Zero || !IsValidPointer((IntPtr)animContainer))
|
||||||
|
{
|
||||||
|
_logger.LogDebug("hkaAnimationContainer pointer is invalid (hash={hash})", hash);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (animContainer->Bindings.Length < 0 || animContainer->Bindings.Length > 10000)
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Invalid bindings count {count} (hash={hash})", animContainer->Bindings.Length, hash);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < animContainer->Bindings.Length; i++)
|
||||||
|
{
|
||||||
|
var binding = animContainer->Bindings[i].ptr;
|
||||||
|
if (binding == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if ((nint)binding == nint.Zero || !IsValidPointer((IntPtr)binding))
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Skipping invalid binding at index {index} (hash={hash})", i, hash);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var rawSkel = binding->OriginalSkeletonName.String;
|
||||||
|
var skeletonKey = CanonicalizeSkeletonKey(rawSkel);
|
||||||
|
if (string.IsNullOrEmpty(skeletonKey))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var boneTransform = binding->TransformTrackToBoneIndices;
|
||||||
|
if (boneTransform.Length <= 0 || boneTransform.Length > 10000)
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Invalid bone transform length {length} for skeleton {skel} (hash={hash})",
|
||||||
|
boneTransform.Length, skeletonKey, hash);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!tempSets.TryGetValue(skeletonKey, out var set))
|
||||||
|
{
|
||||||
|
set = [];
|
||||||
|
tempSets[skeletonKey] = set;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int boneIdx = 0; boneIdx < boneTransform.Length; boneIdx++)
|
||||||
|
{
|
||||||
|
var v = boneTransform[boneIdx];
|
||||||
|
if (v < 0 || v > ushort.MaxValue)
|
||||||
|
continue;
|
||||||
|
set.Add((ushort)v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
catch (SEHException ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "SEH exception processing PAP file {hash} from {path}. Error code: 0x{code:X}",
|
||||||
|
hash, tempHavokDataPath, ex.ErrorCode);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Managed exception loading havok file {hash} from {path}", hash, tempHavokDataPath);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (tempHavokDataPathAnsi != IntPtr.Zero)
|
||||||
|
Marshal.FreeHGlobal(tempHavokDataPathAnsi);
|
||||||
|
|
||||||
|
int retryCount = 3;
|
||||||
|
while (retryCount > 0 && File.Exists(tempHavokDataPath))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.Delete(tempHavokDataPath);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
catch (IOException ex)
|
||||||
|
{
|
||||||
|
retryCount--;
|
||||||
|
if (retryCount == 0)
|
||||||
|
{
|
||||||
|
_logger.LogDebug(ex, "Failed to delete temporary havok file after retries: {path}", tempHavokDataPath);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Thread.Sleep(50);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogDebug(ex, "Unexpected error deleting temporary havok file: {path}", tempHavokDataPath);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tempSets.Count == 0)
|
||||||
|
{
|
||||||
|
_logger.LogDebug("No bone sets found in PAP file (hash={hash})", hash);
|
||||||
|
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;
|
||||||
|
|
||||||
|
if (persistToConfig)
|
||||||
|
_configService.Save();
|
||||||
|
|
||||||
|
return output;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogWarning(ex, "Could not load havok file in {path}", tempHavokDataPath);
|
_logger.LogError(ex, "Outer exception reading PAP file (hash={hash})", hash);
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
finally
|
}
|
||||||
|
|
||||||
|
private static bool IsValidPointer(IntPtr ptr)
|
||||||
|
{
|
||||||
|
if (ptr == IntPtr.Zero)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
try
|
||||||
{
|
{
|
||||||
Marshal.FreeHGlobal(tempHavokDataPathAnsi);
|
_ = Marshal.ReadByte(ptr);
|
||||||
File.Delete(tempHavokDataPath);
|
return true;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_configService.Current.BonesDictionary[hash] = output;
|
return false;
|
||||||
_configService.Save();
|
}
|
||||||
return output;
|
|
||||||
|
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)
|
||||||
@@ -162,16 +622,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 +680,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 +690,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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -34,44 +34,65 @@ namespace LightlessSync.UI;
|
|||||||
|
|
||||||
public class CompactUi : WindowMediatorSubscriberBase
|
public class CompactUi : WindowMediatorSubscriberBase
|
||||||
{
|
{
|
||||||
private readonly CharacterAnalyzer _characterAnalyzer;
|
#region Constants
|
||||||
|
|
||||||
|
private const float ConnectButtonHighlightThickness = 14f;
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Services
|
||||||
|
|
||||||
private readonly ApiController _apiController;
|
private readonly ApiController _apiController;
|
||||||
|
private readonly CharacterAnalyzer _characterAnalyzer;
|
||||||
|
private readonly DalamudUtilService _dalamudUtilService;
|
||||||
|
private readonly DrawEntityFactory _drawEntityFactory;
|
||||||
|
private readonly FileUploadManager _fileTransferManager;
|
||||||
|
private readonly IpcManager _ipcManager;
|
||||||
|
private readonly LightFinderService _broadcastService;
|
||||||
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 ConcurrentDictionary<GameObjectHandler, Dictionary<string, FileDownloadStatus>> _currentDownloads = new();
|
|
||||||
private readonly DrawEntityFactory _drawEntityFactory;
|
|
||||||
private readonly FileUploadManager _fileTransferManager;
|
|
||||||
private readonly PlayerPerformanceConfigService _playerPerformanceConfig;
|
private readonly PlayerPerformanceConfigService _playerPerformanceConfig;
|
||||||
private readonly PairUiService _pairUiService;
|
private readonly PairUiService _pairUiService;
|
||||||
private readonly SelectTagForPairUi _selectTagForPairUi;
|
|
||||||
private readonly SelectTagForSyncshellUi _selectTagForSyncshellUi;
|
|
||||||
private readonly SelectSyncshellForTagUi _selectSyncshellForTagUi;
|
|
||||||
private readonly RenameSyncshellTagUi _renameSyncshellTagUi;
|
|
||||||
private readonly SelectPairForTagUi _selectPairsForGroupUi;
|
|
||||||
private readonly RenamePairTagUi _renamePairTagUi;
|
|
||||||
private readonly IpcManager _ipcManager;
|
|
||||||
private readonly ServerConfigurationManager _serverManager;
|
private readonly ServerConfigurationManager _serverManager;
|
||||||
private readonly TopTabMenu _tabMenu;
|
|
||||||
private readonly TagHandler _tagHandler;
|
private readonly TagHandler _tagHandler;
|
||||||
private readonly UiSharedService _uiSharedService;
|
private readonly UiSharedService _uiSharedService;
|
||||||
private readonly LightFinderService _broadcastService;
|
|
||||||
private readonly DalamudUtilService _dalamudUtilService;
|
#endregion
|
||||||
|
|
||||||
|
#region UI Components
|
||||||
|
|
||||||
|
private readonly AnimatedHeader _animatedHeader = new();
|
||||||
|
private readonly RenamePairTagUi _renamePairTagUi;
|
||||||
|
private readonly RenameSyncshellTagUi _renameSyncshellTagUi;
|
||||||
|
private readonly SelectPairForTagUi _selectPairsForGroupUi;
|
||||||
|
private readonly SelectSyncshellForTagUi _selectSyncshellForTagUi;
|
||||||
|
private readonly SelectTagForPairUi _selectTagForPairUi;
|
||||||
|
private readonly SelectTagForSyncshellUi _selectTagForSyncshellUi;
|
||||||
|
private readonly SeluneBrush _seluneBrush = new();
|
||||||
|
private readonly TopTabMenu _tabMenu;
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region State
|
||||||
|
|
||||||
|
private readonly ConcurrentDictionary<GameObjectHandler, Dictionary<string, FileDownloadStatus>> _currentDownloads = new();
|
||||||
private List<IDrawFolder> _drawFolders;
|
private List<IDrawFolder> _drawFolders;
|
||||||
|
private Pair? _focusedPair;
|
||||||
private Pair? _lastAddedUser;
|
private Pair? _lastAddedUser;
|
||||||
private string _lastAddedUserComment = string.Empty;
|
private string _lastAddedUserComment = string.Empty;
|
||||||
private Vector2 _lastPosition = Vector2.One;
|
private Vector2 _lastPosition = Vector2.One;
|
||||||
private Vector2 _lastSize = Vector2.One;
|
private Vector2 _lastSize = Vector2.One;
|
||||||
|
private int _pendingFocusFrame = -1;
|
||||||
|
private Pair? _pendingFocusPair;
|
||||||
private bool _showModalForUserAddition;
|
private bool _showModalForUserAddition;
|
||||||
private float _transferPartHeight;
|
private float _transferPartHeight;
|
||||||
private bool _wasOpen;
|
private bool _wasOpen;
|
||||||
private float _windowContentWidth;
|
private float _windowContentWidth;
|
||||||
private readonly SeluneBrush _seluneBrush = new();
|
|
||||||
private const float _connectButtonHighlightThickness = 14f;
|
#endregion
|
||||||
private Pair? _focusedPair;
|
|
||||||
private Pair? _pendingFocusPair;
|
#region Constructor
|
||||||
private int _pendingFocusFrame = -1;
|
|
||||||
|
|
||||||
public CompactUi(
|
public CompactUi(
|
||||||
ILogger<CompactUi> logger,
|
ILogger<CompactUi> logger,
|
||||||
@@ -127,6 +148,11 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
.Apply();
|
.Apply();
|
||||||
|
|
||||||
_drawFolders = [.. DrawFolders];
|
_drawFolders = [.. DrawFolders];
|
||||||
|
|
||||||
|
_animatedHeader.Height = 120f;
|
||||||
|
_animatedHeader.EnableBottomGradient = true;
|
||||||
|
_animatedHeader.GradientHeight = 250f;
|
||||||
|
_animatedHeader.EnableParticles = _configService.Current.EnableParticleEffects;
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
string dev = "Dev Build";
|
string dev = "Dev Build";
|
||||||
@@ -141,18 +167,26 @@ 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;
|
||||||
_lightlessMediator = mediator;
|
_lightlessMediator = mediator;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Lifecycle
|
||||||
|
|
||||||
public override void OnClose()
|
public override void OnClose()
|
||||||
{
|
{
|
||||||
ForceReleaseFocus();
|
ForceReleaseFocus();
|
||||||
|
_animatedHeader.ClearParticles();
|
||||||
base.OnClose();
|
base.OnClose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,6 +198,13 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
using var selune = Selune.Begin(_seluneBrush, drawList, windowPos, windowSize);
|
using var selune = Selune.Begin(_seluneBrush, drawList, windowPos, windowSize);
|
||||||
|
|
||||||
_windowContentWidth = UiSharedService.GetWindowContentRegionWidth();
|
_windowContentWidth = UiSharedService.GetWindowContentRegionWidth();
|
||||||
|
|
||||||
|
// Draw animated header background (just the gradient/particles, content drawn by existing methods)
|
||||||
|
var startCursorY = ImGui.GetCursorPosY();
|
||||||
|
_animatedHeader.Draw(_windowContentWidth, (_, _) => { });
|
||||||
|
// Reset cursor to draw content on top of the header background
|
||||||
|
ImGui.SetCursorPosY(startCursorY);
|
||||||
|
|
||||||
if (!_apiController.IsCurrentVersion)
|
if (!_apiController.IsCurrentVersion)
|
||||||
{
|
{
|
||||||
var ver = _apiController.CurrentClientVersion;
|
var ver = _apiController.CurrentClientVersion;
|
||||||
@@ -209,17 +250,11 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
using (ImRaii.PushId("header")) DrawUIDHeader();
|
using (ImRaii.PushId("header")) DrawUIDHeader();
|
||||||
_uiSharedService.RoundedSeparator(UIColors.Get("LightlessPurple"), 2.5f, 1f, 12f);
|
|
||||||
using (ImRaii.PushId("serverstatus"))
|
|
||||||
{
|
|
||||||
DrawServerStatus();
|
|
||||||
}
|
|
||||||
selune.DrawHighlightOnly(ImGui.GetIO().DeltaTime);
|
selune.DrawHighlightOnly(ImGui.GetIO().DeltaTime);
|
||||||
var style = ImGui.GetStyle();
|
var style = ImGui.GetStyle();
|
||||||
var contentMinY = windowPos.Y + ImGui.GetWindowContentRegionMin().Y;
|
var contentMinY = windowPos.Y + ImGui.GetWindowContentRegionMin().Y;
|
||||||
var gradientInset = 4f * ImGuiHelpers.GlobalScale;
|
var gradientInset = 4f * ImGuiHelpers.GlobalScale;
|
||||||
var gradientTop = MathF.Max(contentMinY, ImGui.GetCursorScreenPos().Y - style.ItemSpacing.Y + gradientInset);
|
var gradientTop = MathF.Max(contentMinY, ImGui.GetCursorScreenPos().Y - style.ItemSpacing.Y + gradientInset);
|
||||||
ImGui.Separator();
|
|
||||||
|
|
||||||
if (_apiController.ServerState is ServerState.Connected)
|
if (_apiController.ServerState is ServerState.Connected)
|
||||||
{
|
{
|
||||||
@@ -227,7 +262,6 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
|
|
||||||
using (ImRaii.PushId("global-topmenu")) _tabMenu.Draw(pairSnapshot);
|
using (ImRaii.PushId("global-topmenu")) _tabMenu.Draw(pairSnapshot);
|
||||||
using (ImRaii.PushId("pairlist")) DrawPairs();
|
using (ImRaii.PushId("pairlist")) DrawPairs();
|
||||||
ImGui.Separator();
|
|
||||||
var transfersTop = ImGui.GetCursorScreenPos().Y;
|
var transfersTop = ImGui.GetCursorScreenPos().Y;
|
||||||
var gradientBottom = MathF.Max(gradientTop, transfersTop - style.ItemSpacing.Y - gradientInset);
|
var gradientBottom = MathF.Max(gradientTop, transfersTop - style.ItemSpacing.Y - gradientInset);
|
||||||
selune.DrawGradient(gradientTop, gradientBottom, ImGui.GetIO().DeltaTime);
|
selune.DrawGradient(gradientTop, gradientBottom, ImGui.GetIO().DeltaTime);
|
||||||
@@ -290,6 +324,10 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Content Drawing
|
||||||
|
|
||||||
private void DrawPairs()
|
private void DrawPairs()
|
||||||
{
|
{
|
||||||
float ySize = Math.Abs(_transferPartHeight) < 0.0001f
|
float ySize = Math.Abs(_transferPartHeight) < 0.0001f
|
||||||
@@ -308,95 +346,6 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
ImGui.EndChild();
|
ImGui.EndChild();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawServerStatus()
|
|
||||||
{
|
|
||||||
var buttonSize = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Link);
|
|
||||||
var userCount = _apiController.OnlineUsers.ToString(CultureInfo.InvariantCulture);
|
|
||||||
var userSize = ImGui.CalcTextSize(userCount);
|
|
||||||
var textSize = ImGui.CalcTextSize("Users Online");
|
|
||||||
#if DEBUG
|
|
||||||
string shardConnection = $"Shard: {_apiController.ServerInfo.ShardName}";
|
|
||||||
#else
|
|
||||||
string shardConnection = string.Equals(_apiController.ServerInfo.ShardName, "Main", StringComparison.OrdinalIgnoreCase) ? string.Empty : $"Shard: {_apiController.ServerInfo.ShardName}";
|
|
||||||
#endif
|
|
||||||
var shardTextSize = ImGui.CalcTextSize(shardConnection);
|
|
||||||
var printShard = !string.IsNullOrEmpty(_apiController.ServerInfo.ShardName) && shardConnection != string.Empty;
|
|
||||||
|
|
||||||
if (_apiController.ServerState is ServerState.Connected)
|
|
||||||
{
|
|
||||||
ImGui.SetCursorPosX((ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth()) / 2 - (userSize.X + textSize.X) / 2 - ImGui.GetStyle().ItemSpacing.X / 2);
|
|
||||||
if (!printShard) ImGui.AlignTextToFramePadding();
|
|
||||||
ImGui.TextColored(UIColors.Get("LightlessPurple"), userCount);
|
|
||||||
ImGui.SameLine();
|
|
||||||
if (!printShard) ImGui.AlignTextToFramePadding();
|
|
||||||
ImGui.TextUnformatted("Users Online");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
ImGui.AlignTextToFramePadding();
|
|
||||||
ImGui.TextColored(UIColors.Get("DimRed"), "Not connected to any server");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (printShard)
|
|
||||||
{
|
|
||||||
ImGui.SetCursorPosY(ImGui.GetCursorPosY() - ImGui.GetStyle().ItemSpacing.Y);
|
|
||||||
ImGui.SetCursorPosX((ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth()) / 2 - shardTextSize.X / 2);
|
|
||||||
ImGui.TextUnformatted(shardConnection);
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui.SameLine();
|
|
||||||
if (printShard)
|
|
||||||
{
|
|
||||||
ImGui.SetCursorPosY(ImGui.GetCursorPosY() - ((userSize.Y + textSize.Y) / 2 + shardTextSize.Y) / 2 - ImGui.GetStyle().ItemSpacing.Y + buttonSize.Y / 2);
|
|
||||||
}
|
|
||||||
bool isConnectingOrConnected = _apiController.ServerState is ServerState.Connected or ServerState.Connecting or ServerState.Reconnecting;
|
|
||||||
var color = UiSharedService.GetBoolColor(!isConnectingOrConnected);
|
|
||||||
var connectedIcon = isConnectingOrConnected ? FontAwesomeIcon.Unlink : FontAwesomeIcon.Link;
|
|
||||||
|
|
||||||
ImGui.SameLine(ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth() - buttonSize.X);
|
|
||||||
if (printShard)
|
|
||||||
{
|
|
||||||
ImGui.SetCursorPosY(ImGui.GetCursorPosY() - ((userSize.Y + textSize.Y) / 2 + shardTextSize.Y) / 2 - ImGui.GetStyle().ItemSpacing.Y + buttonSize.Y / 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_apiController.ServerState is not (ServerState.Reconnecting or ServerState.Disconnecting))
|
|
||||||
{
|
|
||||||
using (ImRaii.PushColor(ImGuiCol.Text, color))
|
|
||||||
{
|
|
||||||
if (_uiSharedService.IconButton(connectedIcon))
|
|
||||||
{
|
|
||||||
if (isConnectingOrConnected && !_serverManager.CurrentServer.FullPause)
|
|
||||||
{
|
|
||||||
_serverManager.CurrentServer.FullPause = true;
|
|
||||||
_serverManager.Save();
|
|
||||||
}
|
|
||||||
else if (!isConnectingOrConnected && _serverManager.CurrentServer.FullPause)
|
|
||||||
{
|
|
||||||
_serverManager.CurrentServer.FullPause = false;
|
|
||||||
_serverManager.Save();
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = _apiController.CreateConnectionsAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenBlockedByActiveItem) || ImGui.IsItemActive())
|
|
||||||
{
|
|
||||||
Selune.RegisterHighlight(
|
|
||||||
ImGui.GetItemRectMin(),
|
|
||||||
ImGui.GetItemRectMax(),
|
|
||||||
SeluneHighlightMode.Both,
|
|
||||||
borderOnly: true,
|
|
||||||
borderThicknessOverride: _connectButtonHighlightThickness,
|
|
||||||
exactSize: true,
|
|
||||||
clipToElement: true,
|
|
||||||
roundingOverride: ImGui.GetStyle().FrameRounding);
|
|
||||||
}
|
|
||||||
|
|
||||||
UiSharedService.AttachToolTip(isConnectingOrConnected ? "Disconnect from " + _serverManager.CurrentServer.ServerName : "Connect to " + _serverManager.CurrentServer.ServerName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DrawTransfers()
|
private void DrawTransfers()
|
||||||
{
|
{
|
||||||
var currentUploads = _fileTransferManager.GetCurrentUploadsSnapshot();
|
var currentUploads = _fileTransferManager.GetCurrentUploadsSnapshot();
|
||||||
@@ -492,11 +441,9 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
return new DownloadSummary(totalFiles, transferredFiles, transferredBytes, totalBytes);
|
return new DownloadSummary(totalFiles, transferredFiles, transferredBytes, totalBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
[StructLayout(LayoutKind.Auto)]
|
#endregion
|
||||||
private readonly record struct DownloadSummary(int TotalFiles, int TransferredFiles, long TransferredBytes, long TotalBytes)
|
|
||||||
{
|
#region Header Drawing
|
||||||
public bool HasDownloads => TotalFiles > 0 || TotalBytes > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DrawUIDHeader()
|
private void DrawUIDHeader()
|
||||||
{
|
{
|
||||||
@@ -532,21 +479,52 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
using (_uiSharedService.IconFont.Push())
|
using (_uiSharedService.IconFont.Push())
|
||||||
iconSize = ImGui.CalcTextSize(FontAwesomeIcon.PersonCirclePlus.ToIconString());
|
iconSize = ImGui.CalcTextSize(FontAwesomeIcon.PersonCirclePlus.ToIconString());
|
||||||
|
|
||||||
float contentWidth = ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X;
|
float uidStartX = 25f;
|
||||||
float uidStartX = (contentWidth - uidTextSize.X) / 2f;
|
|
||||||
float cursorY = ImGui.GetCursorPosY();
|
float cursorY = ImGui.GetCursorPosY();
|
||||||
|
|
||||||
|
ImGui.SetCursorPosY(cursorY);
|
||||||
|
ImGui.SetCursorPosX(uidStartX);
|
||||||
|
|
||||||
|
bool headerItemClicked;
|
||||||
|
using (_uiSharedService.UidFont.Push())
|
||||||
|
{
|
||||||
|
if (useVanityColors)
|
||||||
|
{
|
||||||
|
var seString = SeStringUtils.BuildFormattedPlayerName(uidText, vanityTextColor, vanityGlowColor);
|
||||||
|
var cursorPos = ImGui.GetCursorScreenPos();
|
||||||
|
var targetFontSize = ImGui.GetFontSize();
|
||||||
|
var font = ImGui.GetFont();
|
||||||
|
SeStringUtils.RenderSeStringWithHitbox(seString, cursorPos, targetFontSize ,font , "uid-header");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ImGui.TextColored(uidColor, uidText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the actual rendered text rect for proper icon alignment
|
||||||
|
var uidTextRect = ImGui.GetItemRectMax() - ImGui.GetItemRectMin();
|
||||||
|
var uidTextRectMin = ImGui.GetItemRectMin();
|
||||||
|
var uidTextHovered = ImGui.IsItemHovered();
|
||||||
|
headerItemClicked = ImGui.IsItemClicked();
|
||||||
|
|
||||||
|
// Track position for icons next to UID text
|
||||||
|
// Use uidTextSize.Y (actual font height) for vertical centering, not hitbox height
|
||||||
|
float nextIconX = uidTextRectMin.X + uidTextRect.X + 10f;
|
||||||
|
float iconYOffset = (uidTextSize.Y - iconSize.Y) * 0.5f;
|
||||||
|
float textVerticalOffset = (uidTextRect.Y - uidTextSize.Y) * 0.5f;
|
||||||
|
var buttonSize = new Vector2(iconSize.X, uidTextSize.Y);
|
||||||
|
|
||||||
if (_configService.Current.BroadcastEnabled && _apiController.IsConnected)
|
if (_configService.Current.BroadcastEnabled && _apiController.IsConnected)
|
||||||
{
|
{
|
||||||
float iconYOffset = (uidTextSize.Y - iconSize.Y) * 0.5f;
|
ImGui.SetCursorScreenPos(new Vector2(nextIconX, uidTextRectMin.Y + textVerticalOffset));
|
||||||
var buttonSize = new Vector2(iconSize.X, uidTextSize.Y);
|
|
||||||
|
|
||||||
ImGui.SetCursorPos(new Vector2(ImGui.GetStyle().ItemSpacing.X + 5f, cursorY));
|
|
||||||
ImGui.InvisibleButton("BroadcastIcon", buttonSize);
|
ImGui.InvisibleButton("BroadcastIcon", buttonSize);
|
||||||
|
|
||||||
var iconPos = ImGui.GetItemRectMin() + new Vector2(0f, iconYOffset);
|
var iconPos = ImGui.GetItemRectMin() + new Vector2(0f, iconYOffset);
|
||||||
using (_uiSharedService.IconFont.Push())
|
using (_uiSharedService.IconFont.Push())
|
||||||
ImGui.GetWindowDrawList().AddText(iconPos, ImGui.GetColorU32(UIColors.Get("LightlessGreen")), FontAwesomeIcon.PersonCirclePlus.ToIconString());
|
ImGui.GetWindowDrawList().AddText(iconPos, ImGui.GetColorU32(UIColors.Get("LightlessGreen")), FontAwesomeIcon.Wifi.ToIconString());
|
||||||
|
|
||||||
|
nextIconX = ImGui.GetItemRectMax().X + 6f;
|
||||||
|
|
||||||
|
|
||||||
if (ImGui.IsItemHovered())
|
if (ImGui.IsItemHovered())
|
||||||
@@ -618,50 +596,8 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
if (ImGui.IsItemClicked())
|
if (ImGui.IsItemClicked())
|
||||||
_lightlessMediator.Publish(new UiToggleMessage(typeof(LightFinderUI)));
|
_lightlessMediator.Publish(new UiToggleMessage(typeof(LightFinderUI)));
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui.SetCursorPosY(cursorY);
|
// Warning threshold icon (next to lightfinder or UID text)
|
||||||
ImGui.SetCursorPosX(uidStartX);
|
|
||||||
|
|
||||||
bool headerItemClicked;
|
|
||||||
using (_uiSharedService.UidFont.Push())
|
|
||||||
{
|
|
||||||
if (useVanityColors)
|
|
||||||
{
|
|
||||||
var seString = SeStringUtils.BuildFormattedPlayerName(uidText, vanityTextColor, vanityGlowColor);
|
|
||||||
var cursorPos = ImGui.GetCursorScreenPos();
|
|
||||||
var targetFontSize = ImGui.GetFontSize();
|
|
||||||
var font = ImGui.GetFont();
|
|
||||||
SeStringUtils.RenderSeStringWithHitbox(seString, cursorPos, targetFontSize ,font , "uid-header");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
ImGui.TextColored(uidColor, uidText);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ImGui.IsItemHovered())
|
|
||||||
{
|
|
||||||
var padding = new Vector2(35f * ImGuiHelpers.GlobalScale);
|
|
||||||
Selune.RegisterHighlight(
|
|
||||||
ImGui.GetItemRectMin() - padding,
|
|
||||||
ImGui.GetItemRectMax() + padding,
|
|
||||||
SeluneHighlightMode.Point,
|
|
||||||
exactSize: true,
|
|
||||||
clipToElement: true,
|
|
||||||
clipPadding: padding,
|
|
||||||
highlightColorOverride: vanityGlowColor,
|
|
||||||
highlightAlphaOverride: 0.05f);
|
|
||||||
}
|
|
||||||
|
|
||||||
headerItemClicked = ImGui.IsItemClicked();
|
|
||||||
|
|
||||||
if (headerItemClicked)
|
|
||||||
{
|
|
||||||
ImGui.SetClipboardText(uidText);
|
|
||||||
}
|
|
||||||
|
|
||||||
UiSharedService.AttachToolTip("Click to copy");
|
|
||||||
|
|
||||||
if (_apiController.ServerState is ServerState.Connected && analysisSummary.HasData)
|
if (_apiController.ServerState is ServerState.Connected && analysisSummary.HasData)
|
||||||
{
|
{
|
||||||
var objectSummary = analysisSummary.Objects.Values.FirstOrDefault(summary => summary.HasEntries);
|
var objectSummary = analysisSummary.Objects.Values.FirstOrDefault(summary => summary.HasEntries);
|
||||||
@@ -675,24 +611,30 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
|
|
||||||
if ((isOverTriHold || isOverVRAMUsage) && _playerPerformanceConfig.Current.WarnOnExceedingThresholds)
|
if ((isOverTriHold || isOverVRAMUsage) && _playerPerformanceConfig.Current.WarnOnExceedingThresholds)
|
||||||
{
|
{
|
||||||
ImGui.SameLine();
|
ImGui.SetCursorScreenPos(new Vector2(nextIconX, uidTextRectMin.Y + textVerticalOffset));
|
||||||
ImGui.SetCursorPosY(cursorY + 15f);
|
|
||||||
_uiSharedService.IconText(FontAwesomeIcon.ExclamationTriangle, UIColors.Get("LightlessYellow"));
|
ImGui.InvisibleButton("WarningThresholdIcon", buttonSize);
|
||||||
|
var warningIconPos = ImGui.GetItemRectMin() + new Vector2(0f, iconYOffset);
|
||||||
|
using (_uiSharedService.IconFont.Push())
|
||||||
|
ImGui.GetWindowDrawList().AddText(warningIconPos, ImGui.GetColorU32(UIColors.Get("LightlessYellow")), FontAwesomeIcon.ExclamationTriangle.ToIconString());
|
||||||
|
|
||||||
string warningMessage = "";
|
if (ImGui.IsItemHovered())
|
||||||
if (isOverTriHold)
|
|
||||||
{
|
{
|
||||||
warningMessage += $"You exceed your own triangles threshold by " +
|
string warningMessage = "";
|
||||||
$"{actualTriCount - _playerPerformanceConfig.Current.TrisWarningThresholdThousands * 1000} triangles.";
|
if (isOverTriHold)
|
||||||
warningMessage += Environment.NewLine;
|
{
|
||||||
|
warningMessage += $"You exceed your own triangles threshold by " +
|
||||||
|
$"{actualTriCount - _playerPerformanceConfig.Current.TrisWarningThresholdThousands * 1000} triangles.";
|
||||||
|
warningMessage += Environment.NewLine;
|
||||||
|
}
|
||||||
|
if (isOverVRAMUsage)
|
||||||
|
{
|
||||||
|
warningMessage += $"You exceed your own VRAM threshold by " +
|
||||||
|
$"{UiSharedService.ByteToString(actualVramUsage - (_playerPerformanceConfig.Current.VRAMSizeWarningThresholdMiB * 1024 * 1024))}.";
|
||||||
|
}
|
||||||
|
UiSharedService.AttachToolTip(warningMessage);
|
||||||
}
|
}
|
||||||
if (isOverVRAMUsage)
|
|
||||||
{
|
|
||||||
warningMessage += $"You exceed your own VRAM threshold by " +
|
|
||||||
$"{UiSharedService.ByteToString(actualVramUsage - (_playerPerformanceConfig.Current.VRAMSizeWarningThresholdMiB * 1024 * 1024))}.";
|
|
||||||
}
|
|
||||||
UiSharedService.AttachToolTip(warningMessage);
|
|
||||||
if (ImGui.IsItemClicked())
|
if (ImGui.IsItemClicked())
|
||||||
{
|
{
|
||||||
_lightlessMediator.Publish(new UiToggleMessage(typeof(DataAnalysisUi)));
|
_lightlessMediator.Publish(new UiToggleMessage(typeof(DataAnalysisUi)));
|
||||||
@@ -701,6 +643,34 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (uidTextHovered)
|
||||||
|
{
|
||||||
|
var padding = new Vector2(35f * ImGuiHelpers.GlobalScale);
|
||||||
|
Selune.RegisterHighlight(
|
||||||
|
uidTextRectMin - padding,
|
||||||
|
uidTextRectMin + uidTextRect + padding,
|
||||||
|
SeluneHighlightMode.Point,
|
||||||
|
exactSize: true,
|
||||||
|
clipToElement: true,
|
||||||
|
clipPadding: padding,
|
||||||
|
highlightColorOverride: vanityGlowColor,
|
||||||
|
highlightAlphaOverride: 0.05f);
|
||||||
|
|
||||||
|
ImGui.SetTooltip("Click to copy");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (headerItemClicked)
|
||||||
|
{
|
||||||
|
ImGui.SetClipboardText(uidText);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect/Disconnect button next to big UID (use screen pos to avoid affecting layout)
|
||||||
|
DrawConnectButton(uidTextRectMin.Y + textVerticalOffset, uidTextSize.Y);
|
||||||
|
|
||||||
|
// Add spacing below the big UID
|
||||||
|
ImGuiHelpers.ScaledDummy(5f);
|
||||||
|
|
||||||
if (_apiController.ServerState is ServerState.Connected)
|
if (_apiController.ServerState is ServerState.Connected)
|
||||||
{
|
{
|
||||||
if (headerItemClicked)
|
if (headerItemClicked)
|
||||||
@@ -708,10 +678,12 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
ImGui.SetClipboardText(_apiController.DisplayName);
|
ImGui.SetClipboardText(_apiController.DisplayName);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.Equals(_apiController.DisplayName, _apiController.UID, StringComparison.Ordinal))
|
// Only show smaller UID line if DisplayName differs from UID (custom vanity name)
|
||||||
|
bool hasCustomName = !string.Equals(_apiController.DisplayName, _apiController.UID, StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
if (hasCustomName)
|
||||||
{
|
{
|
||||||
var origTextSize = ImGui.CalcTextSize(_apiController.UID);
|
ImGui.SetCursorPosX(uidStartX);
|
||||||
ImGui.SetCursorPosX((ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X) / 2 - (origTextSize.X / 2));
|
|
||||||
|
|
||||||
if (useVanityColors)
|
if (useVanityColors)
|
||||||
{
|
{
|
||||||
@@ -746,14 +718,88 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
ImGui.SetClipboardText(_apiController.UID);
|
ImGui.SetClipboardText(_apiController.UID);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Users Online on same line as smaller UID (with separator)
|
||||||
|
ImGui.SameLine();
|
||||||
|
ImGui.AlignTextToFramePadding();
|
||||||
|
ImGui.TextUnformatted("|");
|
||||||
|
ImGui.SameLine();
|
||||||
|
ImGui.TextColored(UIColors.Get("LightlessGreen"), _apiController.OnlineUsers.ToString(CultureInfo.InvariantCulture));
|
||||||
|
ImGui.SameLine();
|
||||||
|
ImGui.TextUnformatted("Users Online");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// No custom name - just show Users Online aligned to uidStartX
|
||||||
|
ImGui.SetCursorPosX(uidStartX);
|
||||||
|
ImGui.TextColored(UIColors.Get("LightlessGreen"), _apiController.OnlineUsers.ToString(CultureInfo.InvariantCulture));
|
||||||
|
ImGui.SameLine();
|
||||||
|
ImGui.TextUnformatted("Users Online");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
ImGui.SetCursorPosX(uidStartX);
|
||||||
UiSharedService.ColorTextWrapped(_apiController.ServerState.GetServerError(_apiController.AuthFailureMessage), uidColor);
|
UiSharedService.ColorTextWrapped(_apiController.ServerState.GetServerError(_apiController.AuthFailureMessage), uidColor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void DrawConnectButton(float screenY, float textHeight)
|
||||||
|
{
|
||||||
|
var buttonSize = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Link);
|
||||||
|
bool isConnectingOrConnected = _apiController.ServerState is ServerState.Connected or ServerState.Connecting or ServerState.Reconnecting;
|
||||||
|
var color = UiSharedService.GetBoolColor(!isConnectingOrConnected);
|
||||||
|
var connectedIcon = isConnectingOrConnected ? FontAwesomeIcon.Unlink : FontAwesomeIcon.Link;
|
||||||
|
|
||||||
|
// Position on right side, vertically centered with text
|
||||||
|
if (_apiController.ServerState is not (ServerState.Reconnecting or ServerState.Disconnecting))
|
||||||
|
{
|
||||||
|
var windowPos = ImGui.GetWindowPos();
|
||||||
|
var screenX = windowPos.X + UiSharedService.GetWindowContentRegionWidth() - buttonSize.X - 13f;
|
||||||
|
var yOffset = (textHeight - buttonSize.Y) * 0.5f;
|
||||||
|
ImGui.SetCursorScreenPos(new Vector2(screenX, screenY + yOffset));
|
||||||
|
|
||||||
|
using (ImRaii.PushColor(ImGuiCol.Text, color))
|
||||||
|
using (ImRaii.PushColor(ImGuiCol.Button, ImGui.ColorConvertFloat4ToU32(new(0, 0, 0, 0))))
|
||||||
|
{
|
||||||
|
if (_uiSharedService.IconButton(connectedIcon, buttonSize.Y))
|
||||||
|
{
|
||||||
|
if (isConnectingOrConnected && !_serverManager.CurrentServer.FullPause)
|
||||||
|
{
|
||||||
|
_serverManager.CurrentServer.FullPause = true;
|
||||||
|
_serverManager.Save();
|
||||||
|
}
|
||||||
|
else if (!isConnectingOrConnected && _serverManager.CurrentServer.FullPause)
|
||||||
|
{
|
||||||
|
_serverManager.CurrentServer.FullPause = false;
|
||||||
|
_serverManager.Save();
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = _apiController.CreateConnectionsAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenBlockedByActiveItem) || ImGui.IsItemActive())
|
||||||
|
{
|
||||||
|
Selune.RegisterHighlight(
|
||||||
|
ImGui.GetItemRectMin(),
|
||||||
|
ImGui.GetItemRectMax(),
|
||||||
|
SeluneHighlightMode.Both,
|
||||||
|
borderOnly: true,
|
||||||
|
borderThicknessOverride: ConnectButtonHighlightThickness,
|
||||||
|
exactSize: true,
|
||||||
|
clipToElement: true,
|
||||||
|
roundingOverride: ImGui.GetStyle().FrameRounding);
|
||||||
|
}
|
||||||
|
|
||||||
|
UiSharedService.AttachToolTip(isConnectingOrConnected ? "Disconnect from " + _serverManager.CurrentServer.ServerName : "Connect to " + _serverManager.CurrentServer.ServerName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Folder Building
|
||||||
|
|
||||||
private IEnumerable<IDrawFolder> DrawFolders
|
private IEnumerable<IDrawFolder> DrawFolders
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
@@ -889,6 +935,10 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Filtering & Sorting
|
||||||
|
|
||||||
private static bool PassesFilter(PairUiEntry entry, string filter)
|
private static bool PassesFilter(PairUiEntry entry, string filter)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(filter)) return true;
|
if (string.IsNullOrEmpty(filter)) return true;
|
||||||
@@ -944,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),
|
||||||
@@ -1032,10 +1083,11 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
return SortGroupEntries(entries, group);
|
return SortGroupEntries(entries, group);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UiSharedService_GposeEnd()
|
#endregion
|
||||||
{
|
|
||||||
IsOpen = _wasOpen;
|
#region GPose Handlers
|
||||||
}
|
|
||||||
|
private void UiSharedService_GposeEnd() => IsOpen = _wasOpen;
|
||||||
|
|
||||||
private void UiSharedService_GposeStart()
|
private void UiSharedService_GposeStart()
|
||||||
{
|
{
|
||||||
@@ -1043,6 +1095,10 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
IsOpen = false;
|
IsOpen = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Focus Tracking
|
||||||
|
|
||||||
private void RegisterFocusCharacter(Pair pair)
|
private void RegisterFocusCharacter(Pair pair)
|
||||||
{
|
{
|
||||||
_pendingFocusPair = pair;
|
_pendingFocusPair = pair;
|
||||||
@@ -1088,4 +1144,16 @@ public class CompactUi : WindowMediatorSubscriberBase
|
|||||||
_pendingFocusPair = null;
|
_pendingFocusPair = null;
|
||||||
_pendingFocusFrame = -1;
|
_pendingFocusFrame = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Helper Types
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Auto)]
|
||||||
|
private readonly record struct DownloadSummary(int TotalFiles, int TransferredFiles, long TransferredBytes, long TotalBytes)
|
||||||
|
{
|
||||||
|
public bool HasDownloads => TotalFiles > 0 || TotalBytes > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ public class DrawUserPair
|
|||||||
private readonly UiSharedService _uiSharedService;
|
private readonly UiSharedService _uiSharedService;
|
||||||
private readonly PlayerPerformanceConfigService _performanceConfigService;
|
private readonly PlayerPerformanceConfigService _performanceConfigService;
|
||||||
private readonly LightlessConfigService _configService;
|
private readonly LightlessConfigService _configService;
|
||||||
|
private readonly LocationShareService _locationShareService;
|
||||||
private readonly CharaDataManager _charaDataManager;
|
private readonly CharaDataManager _charaDataManager;
|
||||||
private readonly PairLedger _pairLedger;
|
private readonly PairLedger _pairLedger;
|
||||||
private float _menuWidth = -1;
|
private float _menuWidth = -1;
|
||||||
@@ -57,6 +58,7 @@ public class DrawUserPair
|
|||||||
UiSharedService uiSharedService,
|
UiSharedService uiSharedService,
|
||||||
PlayerPerformanceConfigService performanceConfigService,
|
PlayerPerformanceConfigService performanceConfigService,
|
||||||
LightlessConfigService configService,
|
LightlessConfigService configService,
|
||||||
|
LocationShareService locationShareService,
|
||||||
CharaDataManager charaDataManager,
|
CharaDataManager charaDataManager,
|
||||||
PairLedger pairLedger)
|
PairLedger pairLedger)
|
||||||
{
|
{
|
||||||
@@ -74,6 +76,7 @@ public class DrawUserPair
|
|||||||
_uiSharedService = uiSharedService;
|
_uiSharedService = uiSharedService;
|
||||||
_performanceConfigService = performanceConfigService;
|
_performanceConfigService = performanceConfigService;
|
||||||
_configService = configService;
|
_configService = configService;
|
||||||
|
_locationShareService = locationShareService;
|
||||||
_charaDataManager = charaDataManager;
|
_charaDataManager = charaDataManager;
|
||||||
_pairLedger = pairLedger;
|
_pairLedger = pairLedger;
|
||||||
}
|
}
|
||||||
@@ -216,6 +219,48 @@ public class DrawUserPair
|
|||||||
_ = _apiController.UserSetPairPermissions(new UserPermissionsDto(_pair.UserData, permissions));
|
_ = _apiController.UserSetPairPermissions(new UserPermissionsDto(_pair.UserData, permissions));
|
||||||
}
|
}
|
||||||
UiSharedService.AttachToolTip("Changes VFX sync permissions with this user." + (individual ? individualText : string.Empty));
|
UiSharedService.AttachToolTip("Changes VFX sync permissions with this user." + (individual ? individualText : string.Empty));
|
||||||
|
|
||||||
|
ImGui.SetCursorPosX(10f);
|
||||||
|
_uiSharedService.IconText(FontAwesomeIcon.Globe);
|
||||||
|
ImGui.SameLine();
|
||||||
|
if (ImGui.BeginMenu("Toggle Location sharing"))
|
||||||
|
{
|
||||||
|
if (ImGui.MenuItem("Share for 30 Mins"))
|
||||||
|
{
|
||||||
|
_ = ToggleLocationSharing([_pair.UserData.UID], DateTimeOffset.UtcNow.AddMinutes(30));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ImGui.MenuItem("Share for 1 Hour"))
|
||||||
|
{
|
||||||
|
_ = ToggleLocationSharing([_pair.UserData.UID], DateTimeOffset.UtcNow.AddHours(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ImGui.MenuItem("Share for 3 Hours"))
|
||||||
|
{
|
||||||
|
_ = ToggleLocationSharing([_pair.UserData.UID], DateTimeOffset.UtcNow.AddHours(3));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ImGui.MenuItem("Share until manually stop"))
|
||||||
|
{
|
||||||
|
_ = ToggleLocationSharing([_pair.UserData.UID], DateTimeOffset.MaxValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.Separator();
|
||||||
|
if (ImGui.MenuItem("Stop Sharing"))
|
||||||
|
{
|
||||||
|
_ = ToggleLocationSharing([_pair.UserData.UID], DateTimeOffset.MinValue);
|
||||||
|
}
|
||||||
|
ImGui.EndMenu();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ToggleLocationSharing(List<string> users, DateTimeOffset expireAt)
|
||||||
|
{
|
||||||
|
var updated = await _apiController.ToggleLocationSharing(new LocationSharingToggleDto(users, expireAt)).ConfigureAwait(false);
|
||||||
|
if (updated)
|
||||||
|
{
|
||||||
|
_locationShareService.UpdateSharingStatus(users, expireAt);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawIndividualMenu()
|
private void DrawIndividualMenu()
|
||||||
@@ -384,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));
|
||||||
|
|
||||||
@@ -399,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)
|
||||||
{
|
{
|
||||||
@@ -465,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(')');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -499,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()
|
||||||
@@ -574,6 +627,71 @@ public class DrawUserPair
|
|||||||
var individualVFXDisabled = (_pair.UserPair?.OwnPermissions.IsDisableVFX() ?? false) || (_pair.UserPair?.OtherPermissions.IsDisableVFX() ?? false);
|
var individualVFXDisabled = (_pair.UserPair?.OwnPermissions.IsDisableVFX() ?? false) || (_pair.UserPair?.OtherPermissions.IsDisableVFX() ?? false);
|
||||||
var individualIsSticky = _pair.UserPair!.OwnPermissions.IsSticky();
|
var individualIsSticky = _pair.UserPair!.OwnPermissions.IsSticky();
|
||||||
var individualIcon = individualIsSticky ? FontAwesomeIcon.ArrowCircleUp : FontAwesomeIcon.InfoCircle;
|
var individualIcon = individualIsSticky ? FontAwesomeIcon.ArrowCircleUp : FontAwesomeIcon.InfoCircle;
|
||||||
|
|
||||||
|
var shareLocationIcon = FontAwesomeIcon.Globe;
|
||||||
|
var location = _locationShareService.GetUserLocation(_pair.UserPair!.User.UID);
|
||||||
|
var shareLocation = !string.IsNullOrEmpty(location);
|
||||||
|
var expireAt = _locationShareService.GetSharingStatus(_pair.UserPair!.User.UID);
|
||||||
|
var shareLocationToOther = expireAt > DateTimeOffset.UtcNow;
|
||||||
|
var shareColor = shareLocation switch
|
||||||
|
{
|
||||||
|
true when shareLocationToOther => UIColors.Get("LightlessGreen"),
|
||||||
|
true when !shareLocationToOther => UIColors.Get("LightlessBlue"),
|
||||||
|
_ => UIColors.Get("LightlessYellow"),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (shareLocation || shareLocationToOther)
|
||||||
|
{
|
||||||
|
currentRightSide -= (_uiSharedService.GetIconSize(shareLocationIcon).X + spacingX);
|
||||||
|
ImGui.SameLine(currentRightSide);
|
||||||
|
using (ImRaii.PushColor(ImGuiCol.Text, shareColor, shareLocation || shareLocationToOther))
|
||||||
|
_uiSharedService.IconText(shareLocationIcon);
|
||||||
|
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
{
|
||||||
|
ImGui.BeginTooltip();
|
||||||
|
|
||||||
|
if (_pair.IsOnline)
|
||||||
|
{
|
||||||
|
if (shareLocation)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(location))
|
||||||
|
{
|
||||||
|
_uiSharedService.IconText(FontAwesomeIcon.LocationArrow);
|
||||||
|
ImGui.SameLine();
|
||||||
|
ImGui.TextUnformatted(location);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ImGui.TextUnformatted("Location info not updated, reconnect or wait for update.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ImGui.TextUnformatted("NOT Sharing location with you. o(TヘTo)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ImGui.TextUnformatted("User not online. (´・ω・`)?");
|
||||||
|
}
|
||||||
|
ImGui.Separator();
|
||||||
|
|
||||||
|
if (shareLocationToOther)
|
||||||
|
{
|
||||||
|
ImGui.TextUnformatted("Sharing your location. ヾ(•ω•`)o");
|
||||||
|
if (expireAt != DateTimeOffset.MaxValue)
|
||||||
|
{
|
||||||
|
ImGui.TextUnformatted("Expires at " + expireAt.ToLocalTime().ToString("g"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ImGui.TextUnformatted("NOT sharing your location.  ̄へ ̄");
|
||||||
|
}
|
||||||
|
ImGui.EndTooltip();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (individualAnimDisabled || individualSoundsDisabled || individualVFXDisabled || individualIsSticky)
|
if (individualAnimDisabled || individualSoundsDisabled || individualVFXDisabled || individualIsSticky)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -2183,7 +2523,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
|
|||||||
bool toggleClicked = false;
|
bool toggleClicked = false;
|
||||||
if (showToggle)
|
if (showToggle)
|
||||||
{
|
{
|
||||||
var icon = isCollapsed ? FontAwesomeIcon.ChevronRight : FontAwesomeIcon.ChevronLeft;
|
var icon = !isCollapsed ? FontAwesomeIcon.ChevronRight : FontAwesomeIcon.ChevronLeft;
|
||||||
Vector2 iconSize;
|
Vector2 iconSize;
|
||||||
using (_uiSharedService.IconFont.Push())
|
using (_uiSharedService.IconFont.Push())
|
||||||
{
|
{
|
||||||
@@ -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));
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ public class DrawEntityFactory
|
|||||||
private readonly LightlessConfigService _configService;
|
private readonly LightlessConfigService _configService;
|
||||||
private readonly UiSharedService _uiSharedService;
|
private readonly UiSharedService _uiSharedService;
|
||||||
private readonly PlayerPerformanceConfigService _playerPerformanceConfigService;
|
private readonly PlayerPerformanceConfigService _playerPerformanceConfigService;
|
||||||
|
private readonly LocationShareService _locationShareService;
|
||||||
private readonly CharaDataManager _charaDataManager;
|
private readonly CharaDataManager _charaDataManager;
|
||||||
private readonly SelectTagForPairUi _selectTagForPairUi;
|
private readonly SelectTagForPairUi _selectTagForPairUi;
|
||||||
private readonly RenamePairTagUi _renamePairTagUi;
|
private readonly RenamePairTagUi _renamePairTagUi;
|
||||||
@@ -53,6 +54,7 @@ public class DrawEntityFactory
|
|||||||
LightlessConfigService configService,
|
LightlessConfigService configService,
|
||||||
UiSharedService uiSharedService,
|
UiSharedService uiSharedService,
|
||||||
PlayerPerformanceConfigService playerPerformanceConfigService,
|
PlayerPerformanceConfigService playerPerformanceConfigService,
|
||||||
|
LocationShareService locationShareService,
|
||||||
CharaDataManager charaDataManager,
|
CharaDataManager charaDataManager,
|
||||||
SelectTagForSyncshellUi selectTagForSyncshellUi,
|
SelectTagForSyncshellUi selectTagForSyncshellUi,
|
||||||
RenameSyncshellTagUi renameSyncshellTagUi,
|
RenameSyncshellTagUi renameSyncshellTagUi,
|
||||||
@@ -72,6 +74,7 @@ public class DrawEntityFactory
|
|||||||
_configService = configService;
|
_configService = configService;
|
||||||
_uiSharedService = uiSharedService;
|
_uiSharedService = uiSharedService;
|
||||||
_playerPerformanceConfigService = playerPerformanceConfigService;
|
_playerPerformanceConfigService = playerPerformanceConfigService;
|
||||||
|
_locationShareService = locationShareService;
|
||||||
_charaDataManager = charaDataManager;
|
_charaDataManager = charaDataManager;
|
||||||
_selectTagForSyncshellUi = selectTagForSyncshellUi;
|
_selectTagForSyncshellUi = selectTagForSyncshellUi;
|
||||||
_renameSyncshellTagUi = renameSyncshellTagUi;
|
_renameSyncshellTagUi = renameSyncshellTagUi;
|
||||||
@@ -162,6 +165,7 @@ public class DrawEntityFactory
|
|||||||
_uiSharedService,
|
_uiSharedService,
|
||||||
_playerPerformanceConfigService,
|
_playerPerformanceConfigService,
|
||||||
_configService,
|
_configService,
|
||||||
|
_locationShareService,
|
||||||
_charaDataManager,
|
_charaDataManager,
|
||||||
_pairLedger);
|
_pairLedger);
|
||||||
}
|
}
|
||||||
@@ -213,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;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||||
@@ -69,6 +72,8 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
private readonly UiSharedService _uiShared;
|
private readonly UiSharedService _uiShared;
|
||||||
private readonly IProgress<(int, int, FileCacheEntity)> _validationProgress;
|
private readonly IProgress<(int, int, FileCacheEntity)> _validationProgress;
|
||||||
private readonly NameplateService _nameplateService;
|
private readonly NameplateService _nameplateService;
|
||||||
|
private readonly AnimatedHeader _animatedHeader = new();
|
||||||
|
|
||||||
private (int, int, FileCacheEntity) _currentProgress;
|
private (int, int, FileCacheEntity) _currentProgress;
|
||||||
private bool _deleteAccountPopupModalShown = false;
|
private bool _deleteAccountPopupModalShown = false;
|
||||||
private bool _deleteFilesPopupModalShown = false;
|
private bool _deleteFilesPopupModalShown = false;
|
||||||
@@ -105,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",
|
||||||
@@ -116,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",
|
||||||
@@ -205,7 +211,10 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
_nameplateService = nameplateService;
|
_nameplateService = nameplateService;
|
||||||
_actorObjectService = actorObjectService;
|
_actorObjectService = actorObjectService;
|
||||||
_validationProgress = new Progress<(int, int, FileCacheEntity)>(v => _currentProgress = v);
|
_validationProgress = new Progress<(int, int, FileCacheEntity)>(v => _currentProgress = v);
|
||||||
|
_animatedHeader.Height = 120f;
|
||||||
|
_animatedHeader.EnableBottomGradient = true;
|
||||||
|
_animatedHeader.GradientHeight = 250f;
|
||||||
|
_animatedHeader.EnableParticles = _configService.Current.EnableParticleEffects;
|
||||||
WindowBuilder.For(this)
|
WindowBuilder.For(this)
|
||||||
.AllowPinning(true)
|
.AllowPinning(true)
|
||||||
.AllowClickthrough(false)
|
.AllowClickthrough(false)
|
||||||
@@ -241,6 +250,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
|
|
||||||
public override void OnClose()
|
public override void OnClose()
|
||||||
{
|
{
|
||||||
|
_animatedHeader.ClearParticles();
|
||||||
_uiShared.EditTrackerPosition = false;
|
_uiShared.EditTrackerPosition = false;
|
||||||
_uidToAddForIgnore = string.Empty;
|
_uidToAddForIgnore = string.Empty;
|
||||||
_secretKeysConversionCts = _secretKeysConversionCts.CancelRecreate();
|
_secretKeysConversionCts = _secretKeysConversionCts.CancelRecreate();
|
||||||
@@ -255,8 +265,8 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
|
|
||||||
protected override void DrawInternal()
|
protected override void DrawInternal()
|
||||||
{
|
{
|
||||||
|
_animatedHeader.Draw(ImGui.GetContentRegionAvail().X, (_, _) => { });
|
||||||
_ = _uiShared.DrawOtherPluginState();
|
_ = _uiShared.DrawOtherPluginState();
|
||||||
|
|
||||||
DrawSettingsContent();
|
DrawSettingsContent();
|
||||||
}
|
}
|
||||||
private static Vector3 PackedColorToVector3(uint color)
|
private static Vector3 PackedColorToVector3(uint color)
|
||||||
@@ -574,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();
|
||||||
@@ -863,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();
|
||||||
|
|
||||||
@@ -1141,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;
|
||||||
@@ -1243,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)
|
||||||
@@ -1259,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;
|
||||||
@@ -1494,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();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1925,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)
|
||||||
@@ -2089,7 +2232,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
|
|
||||||
ImGui.Separator();
|
ImGui.Separator();
|
||||||
var openPopupOnAddition = _configService.Current.OpenPopupOnAdd;
|
var openPopupOnAddition = _configService.Current.OpenPopupOnAdd;
|
||||||
|
|
||||||
using (var popupTree = BeginGeneralTree("Popup & Auto Fill", UIColors.Get("LightlessPurple")))
|
using (var popupTree = BeginGeneralTree("Popup & Auto Fill", UIColors.Get("LightlessPurple")))
|
||||||
{
|
{
|
||||||
if (popupTree.Visible)
|
if (popupTree.Visible)
|
||||||
@@ -2146,11 +2289,20 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
var groupInVisible = _configService.Current.ShowSyncshellUsersInVisible;
|
var groupInVisible = _configService.Current.ShowSyncshellUsersInVisible;
|
||||||
var syncshellOfflineSeparate = _configService.Current.ShowSyncshellOfflineUsersSeparately;
|
var syncshellOfflineSeparate = _configService.Current.ShowSyncshellOfflineUsersSeparately;
|
||||||
var greenVisiblePair = _configService.Current.ShowVisiblePairsGreenEye;
|
var greenVisiblePair = _configService.Current.ShowVisiblePairsGreenEye;
|
||||||
|
var enableParticleEffects = _configService.Current.EnableParticleEffects;
|
||||||
|
|
||||||
using (var behaviorTree = BeginGeneralTree("Behavior", UIColors.Get("LightlessPurple")))
|
using (var behaviorTree = BeginGeneralTree("Behavior", UIColors.Get("LightlessPurple")))
|
||||||
{
|
{
|
||||||
if (behaviorTree.Visible)
|
if (behaviorTree.Visible)
|
||||||
{
|
{
|
||||||
|
if (ImGui.Checkbox("Enable Particle Effects", ref enableParticleEffects))
|
||||||
|
{
|
||||||
|
_configService.Current.EnableParticleEffects = enableParticleEffects;
|
||||||
|
_configService.Save();
|
||||||
|
}
|
||||||
|
|
||||||
|
_uiShared.DrawHelpText("This will enable particle effects in the UI.");
|
||||||
|
|
||||||
if (ImGui.Checkbox("Enable Game Right Click Menu Entries", ref enableRightClickMenu))
|
if (ImGui.Checkbox("Enable Game Right Click Menu Entries", ref enableRightClickMenu))
|
||||||
{
|
{
|
||||||
_configService.Current.EnableRightClickMenus = enableRightClickMenu;
|
_configService.Current.EnableRightClickMenus = enableRightClickMenu;
|
||||||
@@ -2859,16 +3011,21 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
|||||||
|
|
||||||
var colorNames = new[]
|
var colorNames = new[]
|
||||||
{
|
{
|
||||||
("LightlessPurple", "Primary Purple", "Section titles and dividers"),
|
("LightlessPurple", "Primary Purple", "Section titles and dividers"),
|
||||||
("LightlessPurpleActive", "Primary Purple (Active)", "Active tabs and hover highlights"),
|
("LightlessPurpleActive", "Primary Purple (Active)", "Active tabs and hover highlights"),
|
||||||
("LightlessPurpleDefault", "Primary Purple (Inactive)", "Inactive tabs and default dividers"),
|
("LightlessPurpleDefault", "Primary Purple (Inactive)", "Inactive tabs and default dividers"),
|
||||||
("LightlessBlue", "Secondary Blue", "Secondary title colors, visable pairs"),
|
("LightlessBlue", "Secondary Blue", "Secondary title colors, visable pairs"),
|
||||||
("LightlessGreen", "Success Green", "Join buttons and success messages"),
|
("LightlessGreen", "Success Green", "Join buttons and success messages"),
|
||||||
("LightlessYellow", "Warning Yellow", "Warning colors"),
|
("LightlessYellow", "Warning Yellow", "Warning colors"),
|
||||||
("LightlessOrange", "Performance Orange", "Performance notifications and warnings"),
|
("LightlessOrange", "Performance Orange", "Performance notifications and warnings"),
|
||||||
("PairBlue", "Syncshell Blue", "Syncshell headers, toggle highlights, and moderator actions"),
|
("PairBlue", "Syncshell Blue", "Syncshell headers, toggle highlights, and moderator actions"),
|
||||||
("DimRed", "Error Red", "Error and offline colors")
|
("DimRed", "Error Red", "Error and offline colors"),
|
||||||
};
|
("HeaderGradientTop", "Header Gradient (Top)", "Top color of the animated header background"),
|
||||||
|
("HeaderGradientBottom", "Header Gradient (Bottom)", "Bottom color of the animated header background"),
|
||||||
|
("HeaderStaticStar", "Header Stars", "Tint color for the static background stars in the header"),
|
||||||
|
("HeaderShootingStar", "Header Shooting Star", "Tint color for the shooting star effect"),
|
||||||
|
};
|
||||||
|
|
||||||
if (ImGui.BeginTable("##ColorTable", 3,
|
if (ImGui.BeginTable("##ColorTable", 3,
|
||||||
ImGuiTableFlags.Borders | ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit))
|
ImGuiTableFlags.Borders | ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit))
|
||||||
{
|
{
|
||||||
@@ -3074,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3167,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;
|
||||||
@@ -3474,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)
|
||||||
@@ -3500,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"));
|
||||||
@@ -3527,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));
|
||||||
|
|
||||||
|
|||||||
@@ -43,10 +43,23 @@ public class AnimatedHeader
|
|||||||
private const float _extendedParticleHeight = 40f;
|
private const float _extendedParticleHeight = 40f;
|
||||||
|
|
||||||
public float Height { get; set; } = 150f;
|
public float Height { get; set; } = 150f;
|
||||||
|
|
||||||
|
// Color keys for theming
|
||||||
|
public string? TopColorKey { get; set; } = "HeaderGradientTop";
|
||||||
|
public string? BottomColorKey { get; set; } = "HeaderGradientBottom";
|
||||||
|
public string? StaticStarColorKey { get; set; } = "HeaderStaticStar";
|
||||||
|
public string? ShootingStarColorKey { get; set; } = "HeaderShootingStar";
|
||||||
|
|
||||||
|
// Fallbacks if the color keys are not found
|
||||||
public Vector4 TopColor { get; set; } = new(0.08f, 0.05f, 0.15f, 1.0f);
|
public Vector4 TopColor { get; set; } = new(0.08f, 0.05f, 0.15f, 1.0f);
|
||||||
public Vector4 BottomColor { get; set; } = new(0.12f, 0.08f, 0.20f, 1.0f);
|
public Vector4 BottomColor { get; set; } = new(0.12f, 0.08f, 0.20f, 1.0f);
|
||||||
|
public Vector4 StaticStarColor { get; set; } = new(1f, 1f, 1f, 1f);
|
||||||
|
public Vector4 ShootingStarColor { get; set; } = new(0.4f, 0.8f, 1.0f, 1.0f);
|
||||||
|
|
||||||
public bool EnableParticles { get; set; } = true;
|
public bool EnableParticles { get; set; } = true;
|
||||||
public bool EnableBottomGradient { get; set; } = true;
|
public bool EnableBottomGradient { get; set; } = true;
|
||||||
|
|
||||||
|
public float GradientHeight { get; set; } = 60f;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Draws the animated header with some customizable content
|
/// Draws the animated header with some customizable content
|
||||||
@@ -146,16 +159,21 @@ public class AnimatedHeader
|
|||||||
{
|
{
|
||||||
var drawList = ImGui.GetWindowDrawList();
|
var drawList = ImGui.GetWindowDrawList();
|
||||||
|
|
||||||
|
var top = ResolveColor(TopColorKey, TopColor);
|
||||||
|
var bottom = ResolveColor(BottomColorKey, BottomColor);
|
||||||
|
|
||||||
drawList.AddRectFilledMultiColor(
|
drawList.AddRectFilledMultiColor(
|
||||||
headerStart,
|
headerStart,
|
||||||
headerEnd,
|
headerEnd,
|
||||||
ImGui.GetColorU32(TopColor),
|
ImGui.GetColorU32(top),
|
||||||
ImGui.GetColorU32(TopColor),
|
ImGui.GetColorU32(top),
|
||||||
ImGui.GetColorU32(BottomColor),
|
ImGui.GetColorU32(bottom),
|
||||||
ImGui.GetColorU32(BottomColor)
|
ImGui.GetColorU32(bottom)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Draw static background stars
|
// Draw static background stars
|
||||||
|
var starBase = ResolveColor(StaticStarColorKey, StaticStarColor);
|
||||||
|
|
||||||
var random = new Random(42);
|
var random = new Random(42);
|
||||||
for (int i = 0; i < 50; i++)
|
for (int i = 0; i < 50; i++)
|
||||||
{
|
{
|
||||||
@@ -164,23 +182,28 @@ public class AnimatedHeader
|
|||||||
(float)random.NextDouble() * (headerEnd.Y - headerStart.Y)
|
(float)random.NextDouble() * (headerEnd.Y - headerStart.Y)
|
||||||
);
|
);
|
||||||
var brightness = 0.3f + (float)random.NextDouble() * 0.4f;
|
var brightness = 0.3f + (float)random.NextDouble() * 0.4f;
|
||||||
drawList.AddCircleFilled(starPos, 1f, ImGui.GetColorU32(new Vector4(1f, 1f, 1f, brightness)));
|
var starColor = starBase with { W = starBase.W * brightness };
|
||||||
|
|
||||||
|
drawList.AddCircleFilled(starPos, 1f, ImGui.GetColorU32(starColor));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawBottomGradient(Vector2 headerStart, Vector2 headerEnd, float width)
|
private void DrawBottomGradient(Vector2 headerStart, Vector2 headerEnd, float width)
|
||||||
{
|
{
|
||||||
var drawList = ImGui.GetWindowDrawList();
|
var drawList = ImGui.GetWindowDrawList();
|
||||||
var gradientHeight = 60f;
|
var gradientHeight = GradientHeight;
|
||||||
|
var bottom = ResolveColor(BottomColorKey, BottomColor);
|
||||||
|
|
||||||
for (int i = 0; i < gradientHeight; i++)
|
for (int i = 0; i < gradientHeight; i++)
|
||||||
{
|
{
|
||||||
var progress = i / gradientHeight;
|
var progress = i / gradientHeight;
|
||||||
var smoothProgress = progress * progress;
|
var smoothProgress = progress * progress;
|
||||||
var r = BottomColor.X + (0.0f - BottomColor.X) * smoothProgress;
|
|
||||||
var g = BottomColor.Y + (0.0f - BottomColor.Y) * smoothProgress;
|
var r = bottom.X + (0.0f - bottom.X) * smoothProgress;
|
||||||
var b = BottomColor.Z + (0.0f - BottomColor.Z) * smoothProgress;
|
var g = bottom.Y + (0.0f - bottom.Y) * smoothProgress;
|
||||||
|
var b = bottom.Z + (0.0f - bottom.Z) * smoothProgress;
|
||||||
var alpha = 1f - smoothProgress;
|
var alpha = 1f - smoothProgress;
|
||||||
|
|
||||||
var gradientColor = new Vector4(r, g, b, alpha);
|
var gradientColor = new Vector4(r, g, b, alpha);
|
||||||
drawList.AddLine(
|
drawList.AddLine(
|
||||||
new Vector2(headerStart.X, headerEnd.Y + i),
|
new Vector2(headerStart.X, headerEnd.Y + i),
|
||||||
@@ -308,9 +331,11 @@ public class AnimatedHeader
|
|||||||
? baseAlpha * (0.6f + 0.4f * MathF.Sin(particle.Twinkle))
|
? baseAlpha * (0.6f + 0.4f * MathF.Sin(particle.Twinkle))
|
||||||
: baseAlpha;
|
: baseAlpha;
|
||||||
|
|
||||||
|
var shootingBase = ResolveColor(ShootingStarColorKey, ShootingStarColor);
|
||||||
|
|
||||||
if (particle.Type == ParticleType.ShootingStar && particle.Trail != null && particle.Trail.Count > 1)
|
if (particle.Type == ParticleType.ShootingStar && particle.Trail != null && particle.Trail.Count > 1)
|
||||||
{
|
{
|
||||||
var cyanColor = new Vector4(0.4f, 0.8f, 1.0f, 1.0f);
|
var baseColor = shootingBase;
|
||||||
|
|
||||||
for (int t = 1; t < particle.Trail.Count; t++)
|
for (int t = 1; t < particle.Trail.Count; t++)
|
||||||
{
|
{
|
||||||
@@ -319,17 +344,18 @@ public class AnimatedHeader
|
|||||||
var trailWidth = (1f - trailProgress) * 3f + 1f;
|
var trailWidth = (1f - trailProgress) * 3f + 1f;
|
||||||
|
|
||||||
var glowAlpha = trailAlpha * 0.4f;
|
var glowAlpha = trailAlpha * 0.4f;
|
||||||
|
|
||||||
drawList.AddLine(
|
drawList.AddLine(
|
||||||
bannerStart + particle.Trail[t - 1],
|
bannerStart + particle.Trail[t - 1],
|
||||||
bannerStart + particle.Trail[t],
|
bannerStart + particle.Trail[t],
|
||||||
ImGui.GetColorU32(cyanColor with { W = glowAlpha }),
|
ImGui.GetColorU32(baseColor with { W = glowAlpha }),
|
||||||
trailWidth + 4f
|
trailWidth + 4f
|
||||||
);
|
);
|
||||||
|
|
||||||
drawList.AddLine(
|
drawList.AddLine(
|
||||||
bannerStart + particle.Trail[t - 1],
|
bannerStart + particle.Trail[t - 1],
|
||||||
bannerStart + particle.Trail[t],
|
bannerStart + particle.Trail[t],
|
||||||
ImGui.GetColorU32(cyanColor with { W = trailAlpha }),
|
ImGui.GetColorU32(baseColor with { W = trailAlpha }),
|
||||||
trailWidth
|
trailWidth
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -448,6 +474,13 @@ public class AnimatedHeader
|
|||||||
Hue = 270f
|
Hue = 270f
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
private static Vector4 ResolveColor(string? key, Vector4 fallback)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(key))
|
||||||
|
return fallback;
|
||||||
|
|
||||||
|
return UIColors.Get(key);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Clears all active particles. Useful when closing or hiding a window with an animated header.
|
/// Clears all active particles. Useful when closing or hiding a window with an animated header.
|
||||||
|
|||||||
@@ -40,9 +40,10 @@ internal static class MainStyle
|
|||||||
new("color.frameBg", "Frame Background", () => Rgba(40, 40, 40, 255), ImGuiCol.FrameBg),
|
new("color.frameBg", "Frame Background", () => Rgba(40, 40, 40, 255), ImGuiCol.FrameBg),
|
||||||
new("color.frameBgHovered", "Frame Background (Hover)", () => Rgba(50, 50, 50, 100), ImGuiCol.FrameBgHovered),
|
new("color.frameBgHovered", "Frame Background (Hover)", () => Rgba(50, 50, 50, 100), ImGuiCol.FrameBgHovered),
|
||||||
new("color.frameBgActive", "Frame Background (Active)", () => Rgba(30, 30, 30, 255), ImGuiCol.FrameBgActive),
|
new("color.frameBgActive", "Frame Background (Active)", () => Rgba(30, 30, 30, 255), ImGuiCol.FrameBgActive),
|
||||||
new("color.titleBg", "Title Background", () => Rgba(24, 24, 24, 232), ImGuiCol.TitleBg),
|
new("color.titleBg", "Title Background", () => Rgba(22, 14, 41, 255), ImGuiCol.TitleBg),
|
||||||
new("color.titleBgActive", "Title Background (Active)", () => Rgba(30, 30, 30, 255), ImGuiCol.TitleBgActive),
|
new("color.titleBgActive", "Title Background (Active)", () => Rgba(22, 14, 41, 255), ImGuiCol.TitleBgActive),
|
||||||
new("color.titleBgCollapsed", "Title Background (Collapsed)", () => Rgba(27, 27, 27, 255), ImGuiCol.TitleBgCollapsed),
|
new("color.titleBgCollapsed", "Title Background (Collapsed)", () => Rgba(22, 14, 41, 255), ImGuiCol.TitleBgCollapsed),
|
||||||
|
|
||||||
new("color.menuBarBg", "Menu Bar Background", () => Rgba(36, 36, 36, 255), ImGuiCol.MenuBarBg),
|
new("color.menuBarBg", "Menu Bar Background", () => Rgba(36, 36, 36, 255), ImGuiCol.MenuBarBg),
|
||||||
new("color.scrollbarBg", "Scrollbar Background", () => Rgba(0, 0, 0, 0), ImGuiCol.ScrollbarBg),
|
new("color.scrollbarBg", "Scrollbar Background", () => Rgba(0, 0, 0, 0), ImGuiCol.ScrollbarBg),
|
||||||
new("color.scrollbarGrab", "Scrollbar Grab", () => Rgba(62, 62, 62, 255), ImGuiCol.ScrollbarGrab),
|
new("color.scrollbarGrab", "Scrollbar Grab", () => Rgba(62, 62, 62, 255), ImGuiCol.ScrollbarGrab),
|
||||||
|
|||||||
@@ -4,9 +4,11 @@ using Dalamud.Interface.Colors;
|
|||||||
using Dalamud.Interface.Textures.TextureWraps;
|
using Dalamud.Interface.Textures.TextureWraps;
|
||||||
using Dalamud.Interface.Utility;
|
using Dalamud.Interface.Utility;
|
||||||
using Dalamud.Interface.Utility.Raii;
|
using Dalamud.Interface.Utility.Raii;
|
||||||
|
using LightlessSync.API.Data;
|
||||||
using LightlessSync.API.Data.Enum;
|
using LightlessSync.API.Data.Enum;
|
||||||
using LightlessSync.API.Data.Extensions;
|
using LightlessSync.API.Data.Extensions;
|
||||||
using LightlessSync.API.Dto.Group;
|
using LightlessSync.API.Dto.Group;
|
||||||
|
using LightlessSync.PlayerData.Factories;
|
||||||
using LightlessSync.PlayerData.Pairs;
|
using LightlessSync.PlayerData.Pairs;
|
||||||
using LightlessSync.Services;
|
using LightlessSync.Services;
|
||||||
using LightlessSync.Services.Mediator;
|
using LightlessSync.Services.Mediator;
|
||||||
@@ -42,13 +44,32 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
|
|||||||
private Task<int>? _pruneTask;
|
private Task<int>? _pruneTask;
|
||||||
private int _pruneDays = 14;
|
private int _pruneDays = 14;
|
||||||
|
|
||||||
|
// Ban management fields
|
||||||
|
private Task<List<BannedGroupUserDto>>? _bannedUsersTask;
|
||||||
|
private bool _bannedUsersLoaded;
|
||||||
|
private string? _bannedUsersLoadError;
|
||||||
|
|
||||||
|
private string _newBanUid = string.Empty;
|
||||||
|
private string _newBanReason = string.Empty;
|
||||||
|
private Task? _newBanTask;
|
||||||
|
private string? _newBanError;
|
||||||
|
private DateTime _newBanBusyUntilUtc;
|
||||||
|
|
||||||
|
// Ban editing fields
|
||||||
|
private string? _editingBanUid;
|
||||||
|
private readonly Dictionary<string, string> _banReasonEdits = new(StringComparer.Ordinal);
|
||||||
|
|
||||||
|
private Task? _banEditTask;
|
||||||
|
private string? _banEditError;
|
||||||
|
|
||||||
private Task<GroupPruneSettingsDto>? _pruneSettingsTask;
|
private Task<GroupPruneSettingsDto>? _pruneSettingsTask;
|
||||||
private bool _pruneSettingsLoaded;
|
private bool _pruneSettingsLoaded;
|
||||||
private bool _autoPruneEnabled;
|
private bool _autoPruneEnabled;
|
||||||
private int _autoPruneDays = 14;
|
private int _autoPruneDays = 14;
|
||||||
|
private readonly PairFactory _pairFactory;
|
||||||
|
|
||||||
public SyncshellAdminUI(ILogger<SyncshellAdminUI> logger, LightlessMediator mediator, ApiController apiController,
|
public SyncshellAdminUI(ILogger<SyncshellAdminUI> logger, LightlessMediator mediator, ApiController apiController,
|
||||||
UiSharedService uiSharedService, PairUiService pairUiService, GroupFullInfoDto groupFullInfo, PerformanceCollectorService performanceCollectorService, LightlessProfileManager lightlessProfileManager)
|
UiSharedService uiSharedService, PairUiService pairUiService, GroupFullInfoDto groupFullInfo, PerformanceCollectorService performanceCollectorService, LightlessProfileManager lightlessProfileManager, PairFactory pairFactory)
|
||||||
: base(logger, mediator, "Syncshell Admin Panel (" + groupFullInfo.GroupAliasOrGID + ")", performanceCollectorService)
|
: base(logger, mediator, "Syncshell Admin Panel (" + groupFullInfo.GroupAliasOrGID + ")", performanceCollectorService)
|
||||||
{
|
{
|
||||||
GroupFullInfo = groupFullInfo;
|
GroupFullInfo = groupFullInfo;
|
||||||
@@ -76,6 +97,7 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
|
|||||||
MaximumSize = new(700, 2000),
|
MaximumSize = new(700, 2000),
|
||||||
};
|
};
|
||||||
_pairUiService = pairUiService;
|
_pairUiService = pairUiService;
|
||||||
|
_pairFactory = pairFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
public GroupFullInfoDto GroupFullInfo { get; private set; }
|
public GroupFullInfoDto GroupFullInfo { get; private set; }
|
||||||
@@ -654,34 +676,345 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
|
|||||||
_uiSharedService.MediumText("User Bans", UIColors.Get("LightlessYellow"));
|
_uiSharedService.MediumText("User Bans", UIColors.Get("LightlessYellow"));
|
||||||
ImGuiHelpers.ScaledDummy(3f);
|
ImGuiHelpers.ScaledDummy(3f);
|
||||||
|
|
||||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Retweet, "Refresh Banlist from Server"))
|
EnsureBanListLoaded();
|
||||||
|
|
||||||
|
DrawNewBanEntryRow();
|
||||||
|
|
||||||
|
ImGuiHelpers.ScaledDummy(4f);
|
||||||
|
|
||||||
|
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Retweet, "Refresh Banlist"))
|
||||||
{
|
{
|
||||||
_bannedUsers = _apiController.GroupGetBannedUsers(new GroupDto(GroupFullInfo.Group)).Result;
|
QueueBanListRefresh(force: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGuiHelpers.ScaledDummy(2f);
|
ImGuiHelpers.ScaledDummy(2f);
|
||||||
|
|
||||||
|
if (!_bannedUsersLoaded)
|
||||||
|
{
|
||||||
|
UiSharedService.ColorTextWrapped("Loading banlist from server...", ImGuiColors.DalamudGrey);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(_bannedUsersLoadError))
|
||||||
|
{
|
||||||
|
UiSharedService.ColorTextWrapped(_bannedUsersLoadError!, ImGuiColors.DalamudRed);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
ImGui.BeginChild("bannedListScroll#" + GroupFullInfo.GID, new Vector2(0, 0), true);
|
ImGui.BeginChild("bannedListScroll#" + GroupFullInfo.GID, new Vector2(0, 0), true);
|
||||||
|
|
||||||
var style = ImGui.GetStyle();
|
var style = ImGui.GetStyle();
|
||||||
float fullW = ImGui.GetContentRegionAvail().X;
|
float fullW = ImGui.GetContentRegionAvail().X;
|
||||||
|
float scale = ImGuiHelpers.GlobalScale;
|
||||||
|
|
||||||
|
float frame = ImGui.GetFrameHeight();
|
||||||
|
float actionIcons = 3;
|
||||||
|
float colActions = actionIcons * frame + (actionIcons - 1) * style.ItemSpacing.X + 10f * scale;
|
||||||
|
|
||||||
float colIdentity = fullW * 0.45f;
|
|
||||||
float colMeta = fullW * 0.35f;
|
float colMeta = fullW * 0.35f;
|
||||||
float colActions = fullW - colIdentity - colMeta - style.ItemSpacing.X * 2.0f;
|
|
||||||
|
|
||||||
// Header
|
float colIdentity = fullW - colMeta - colActions - style.ItemSpacing.X * 2.0f;
|
||||||
|
|
||||||
|
float minIdentity = fullW * 0.40f;
|
||||||
|
if (colIdentity < minIdentity)
|
||||||
|
{
|
||||||
|
colIdentity = minIdentity;
|
||||||
|
colMeta = fullW - colIdentity - colActions - style.ItemSpacing.X * 2.0f;
|
||||||
|
if (colMeta < 80f * scale) colMeta = 80f * scale;
|
||||||
|
}
|
||||||
|
|
||||||
DrawBannedListHeader(colIdentity, colMeta);
|
DrawBannedListHeader(colIdentity, colMeta);
|
||||||
|
|
||||||
int rowIndex = 0;
|
int rowIndex = 0;
|
||||||
foreach (var bannedUser in _bannedUsers.ToList())
|
foreach (var bannedUser in _bannedUsers.ToList())
|
||||||
{
|
{
|
||||||
// Each row
|
|
||||||
DrawBannedRow(bannedUser, rowIndex++, colIdentity, colMeta, colActions);
|
DrawBannedRow(bannedUser, rowIndex++, colIdentity, colMeta, colActions);
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui.EndChild();
|
ImGui.EndChild();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void DrawNewBanEntryRow()
|
||||||
|
{
|
||||||
|
ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("LightlessYellow"));
|
||||||
|
ImGui.TextUnformatted("Add new ban");
|
||||||
|
ImGui.PopStyleColor();
|
||||||
|
|
||||||
|
UiSharedService.TextWrapped("Enter a UID (Not Alias!) and optional reason. (Hold CTRL to enable the ban button.)");
|
||||||
|
|
||||||
|
var style = ImGui.GetStyle();
|
||||||
|
float fullW = ImGui.GetContentRegionAvail().X;
|
||||||
|
|
||||||
|
float uidW = fullW * 0.35f;
|
||||||
|
float reasonW = fullW * 0.50f;
|
||||||
|
float btnW = fullW - uidW - reasonW - style.ItemSpacing.X * 2f;
|
||||||
|
|
||||||
|
// UID
|
||||||
|
ImGui.SetNextItemWidth(uidW);
|
||||||
|
ImGui.InputTextWithHint("##newBanUid", "UID...", ref _newBanUid, 128);
|
||||||
|
|
||||||
|
// Reason
|
||||||
|
ImGui.SameLine(0f, style.ItemSpacing.X);
|
||||||
|
ImGui.SetNextItemWidth(reasonW);
|
||||||
|
ImGui.InputTextWithHint("##newBanReason", "Reason (optional)...", ref _newBanReason, 256);
|
||||||
|
|
||||||
|
// Ban button
|
||||||
|
ImGui.SameLine(0f, style.ItemSpacing.X);
|
||||||
|
|
||||||
|
var trimmedUid = (_newBanUid ?? string.Empty).Trim();
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
bool taskRunning = _newBanTask != null && !_newBanTask.IsCompleted;
|
||||||
|
bool busyLatched = now < _newBanBusyUntilUtc;
|
||||||
|
bool busy = taskRunning || busyLatched;
|
||||||
|
|
||||||
|
bool canBan = UiSharedService.CtrlPressed()
|
||||||
|
&& !string.IsNullOrWhiteSpace(_newBanUid)
|
||||||
|
&& !busy;
|
||||||
|
|
||||||
|
using (ImRaii.Disabled(!canBan))
|
||||||
|
using (ImRaii.PushColor(ImGuiCol.Text, UIColors.Get("DimRed")))
|
||||||
|
{
|
||||||
|
ImGui.SetNextItemWidth(btnW);
|
||||||
|
if (_uiSharedService.IconTextButton(FontAwesomeIcon.UserSlash, "Ban"))
|
||||||
|
{
|
||||||
|
_newBanError = null;
|
||||||
|
|
||||||
|
_newBanBusyUntilUtc = DateTime.UtcNow.AddMilliseconds(750);
|
||||||
|
|
||||||
|
_newBanTask = SubmitNewBanByUidAsync(trimmedUid, _newBanReason);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
UiSharedService.AttachToolTip("Hold CTRL to enable banning by UID.");
|
||||||
|
|
||||||
|
if (busy)
|
||||||
|
{
|
||||||
|
UiSharedService.ColorTextWrapped("Banning user...", ImGuiColors.DalamudGrey);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_newBanTask != null && _newBanTask.IsCompleted && DateTime.UtcNow >= _newBanBusyUntilUtc)
|
||||||
|
{
|
||||||
|
if (_newBanTask.IsFaulted)
|
||||||
|
{
|
||||||
|
var _ = _newBanTask.Exception;
|
||||||
|
_newBanError ??= "Ban failed (see log).";
|
||||||
|
}
|
||||||
|
|
||||||
|
QueueBanListRefresh(force: true);
|
||||||
|
_newBanTask = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SubmitNewBanByUidAsync(string uidOrAlias, string reason)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await Task.Yield();
|
||||||
|
|
||||||
|
uidOrAlias = (uidOrAlias ?? string.Empty).Trim();
|
||||||
|
reason = (reason ?? string.Empty).Trim();
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(uidOrAlias))
|
||||||
|
{
|
||||||
|
_newBanError = "UID is empty.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string targetUid = uidOrAlias;
|
||||||
|
string? typedAlias = null;
|
||||||
|
|
||||||
|
var snap = _pairUiService.GetSnapshot();
|
||||||
|
if (snap.GroupPairs.TryGetValue(GroupFullInfo, out var pairs))
|
||||||
|
{
|
||||||
|
var match = pairs.FirstOrDefault(p =>
|
||||||
|
string.Equals(p.UserData.UID, uidOrAlias, StringComparison.Ordinal) ||
|
||||||
|
string.Equals(p.UserData.AliasOrUID, uidOrAlias, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
if (match != null)
|
||||||
|
{
|
||||||
|
targetUid = match.UserData.UID;
|
||||||
|
typedAlias = match.UserData.Alias;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
typedAlias = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var userData = new UserData(UID: targetUid, Alias: typedAlias);
|
||||||
|
|
||||||
|
await _apiController
|
||||||
|
.GroupBanUser(new GroupPairDto(GroupFullInfo.Group, userData), reason)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
_newBanUid = string.Empty;
|
||||||
|
_newBanReason = string.Empty;
|
||||||
|
_newBanError = null;
|
||||||
|
|
||||||
|
QueueBanListRefresh(force: true);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Failed to ban '{uidOrAlias}' in group {gid}", uidOrAlias, GroupFullInfo.Group.GID);
|
||||||
|
_newBanError = "Failed to ban user (see log).";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SaveBanReasonViaBanUserAsync(string uid)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!_banReasonEdits.TryGetValue(uid, out var newReason))
|
||||||
|
newReason = string.Empty;
|
||||||
|
|
||||||
|
newReason = (newReason ?? string.Empty).Trim();
|
||||||
|
|
||||||
|
var userData = new UserData(uid.Trim());
|
||||||
|
|
||||||
|
await _apiController
|
||||||
|
.GroupBanUser(new GroupPairDto(GroupFullInfo.Group, userData), newReason)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
_editingBanUid = null;
|
||||||
|
_banEditError = null;
|
||||||
|
|
||||||
|
await Task.Delay(450).ConfigureAwait(false);
|
||||||
|
|
||||||
|
QueueBanListRefresh(force: true);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Failed to edit ban reason for {uid} in group {gid}", uid, GroupFullInfo.Group.GID);
|
||||||
|
_banEditError = "Failed to update reason (see log).";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawBannedRow(BannedGroupUserDto bannedUser, int rowIndex, float colIdentity, float colMeta, float colActions)
|
||||||
|
{
|
||||||
|
using var id = ImRaii.PushId("banRow_" + bannedUser.UID);
|
||||||
|
|
||||||
|
var style = ImGui.GetStyle();
|
||||||
|
float x0 = ImGui.GetCursorPosX();
|
||||||
|
|
||||||
|
if (rowIndex % 2 == 0)
|
||||||
|
{
|
||||||
|
var drawList = ImGui.GetWindowDrawList();
|
||||||
|
var pMin = ImGui.GetCursorScreenPos();
|
||||||
|
var rowHeight = ImGui.GetTextLineHeightWithSpacing() * 2.6f;
|
||||||
|
var pMax = new Vector2(
|
||||||
|
pMin.X + colIdentity + colMeta + colActions + style.ItemSpacing.X * 2.0f,
|
||||||
|
pMin.Y + rowHeight);
|
||||||
|
|
||||||
|
var bgColor = UIColors.Get("FullBlack").WithAlpha(0.10f);
|
||||||
|
drawList.AddRectFilled(pMin, pMax, ImGui.ColorConvertFloat4ToU32(bgColor));
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.SetCursorPosX(x0);
|
||||||
|
ImGui.AlignTextToFramePadding();
|
||||||
|
|
||||||
|
string alias = bannedUser.UserAlias ?? string.Empty;
|
||||||
|
string line1 = string.IsNullOrEmpty(alias)
|
||||||
|
? bannedUser.UID
|
||||||
|
: $"{alias} ({bannedUser.UID})";
|
||||||
|
|
||||||
|
ImGui.TextUnformatted(line1);
|
||||||
|
|
||||||
|
var fullReason = bannedUser.Reason ?? string.Empty;
|
||||||
|
|
||||||
|
if (string.Equals(_editingBanUid, bannedUser.UID, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
_banReasonEdits.TryGetValue(bannedUser.UID, out var editReason);
|
||||||
|
editReason ??= StripAliasSuffix(fullReason);
|
||||||
|
|
||||||
|
ImGui.SetCursorPosX(x0);
|
||||||
|
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey);
|
||||||
|
ImGui.SetNextItemWidth(colIdentity);
|
||||||
|
ImGui.InputTextWithHint("##banReasonEdit", "Reason...", ref editReason, 255);
|
||||||
|
ImGui.PopStyleColor();
|
||||||
|
|
||||||
|
_banReasonEdits[bannedUser.UID] = editReason;
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(_banEditError))
|
||||||
|
UiSharedService.ColorTextWrapped(_banEditError!, ImGuiColors.DalamudRed);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(fullReason))
|
||||||
|
{
|
||||||
|
ImGui.SetCursorPosX(x0);
|
||||||
|
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey);
|
||||||
|
|
||||||
|
ImGui.PushTextWrapPos(x0 + colIdentity);
|
||||||
|
UiSharedService.TextWrapped(fullReason);
|
||||||
|
ImGui.PopTextWrapPos();
|
||||||
|
|
||||||
|
ImGui.PopStyleColor();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.SameLine();
|
||||||
|
ImGui.SetCursorPosX(x0 + colIdentity + style.ItemSpacing.X);
|
||||||
|
|
||||||
|
ImGui.AlignTextToFramePadding();
|
||||||
|
ImGui.TextUnformatted($"By: {bannedUser.BannedBy}");
|
||||||
|
|
||||||
|
var dateText = bannedUser.BannedOn.ToLocalTime().ToString(CultureInfo.CurrentCulture);
|
||||||
|
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey);
|
||||||
|
ImGui.TextUnformatted(dateText);
|
||||||
|
ImGui.PopStyleColor();
|
||||||
|
ImGui.SameLine();
|
||||||
|
|
||||||
|
float frame = ImGui.GetFrameHeight();
|
||||||
|
float actionsX0 = x0 + colIdentity + colMeta + style.ItemSpacing.X * 2.0f;
|
||||||
|
|
||||||
|
ImGui.SameLine();
|
||||||
|
ImGui.SetCursorPosX(actionsX0);
|
||||||
|
|
||||||
|
bool isEditing = string.Equals(_editingBanUid, bannedUser.UID, StringComparison.Ordinal);
|
||||||
|
int actionCount = 1 + (isEditing ? 2 : 1);
|
||||||
|
|
||||||
|
float totalW = actionCount * frame + (actionCount - 1) * style.ItemSpacing.X;
|
||||||
|
float startX = actionsX0 + MathF.Max(0, colActions - totalW) - 36f;
|
||||||
|
ImGui.SetCursorPosX(startX);
|
||||||
|
|
||||||
|
if (_uiSharedService.IconButton(FontAwesomeIcon.Check))
|
||||||
|
{
|
||||||
|
_apiController.GroupUnbanUser(bannedUser);
|
||||||
|
_bannedUsers.RemoveAll(b => string.Equals(b.UID, bannedUser.UID, StringComparison.Ordinal));
|
||||||
|
}
|
||||||
|
UiSharedService.AttachToolTip("Unban");
|
||||||
|
|
||||||
|
ImGui.SameLine(0f, style.ItemSpacing.X);
|
||||||
|
|
||||||
|
if (!isEditing)
|
||||||
|
{
|
||||||
|
if (_uiSharedService.IconButton(FontAwesomeIcon.Edit))
|
||||||
|
{
|
||||||
|
_banEditError = null;
|
||||||
|
_editingBanUid = bannedUser.UID;
|
||||||
|
_banReasonEdits[bannedUser.UID] = StripAliasSuffix(bannedUser.Reason ?? string.Empty);
|
||||||
|
}
|
||||||
|
UiSharedService.AttachToolTip("Edit reason");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (_uiSharedService.IconButton(FontAwesomeIcon.Save))
|
||||||
|
{
|
||||||
|
_banEditError = null;
|
||||||
|
_banEditTask = SaveBanReasonViaBanUserAsync(bannedUser.UID);
|
||||||
|
}
|
||||||
|
UiSharedService.AttachToolTip("Save");
|
||||||
|
|
||||||
|
ImGui.SameLine(0f, style.ItemSpacing.X);
|
||||||
|
|
||||||
|
if (_uiSharedService.IconButton(FontAwesomeIcon.Times))
|
||||||
|
{
|
||||||
|
_banEditError = null;
|
||||||
|
_editingBanUid = null;
|
||||||
|
}
|
||||||
|
UiSharedService.AttachToolTip("Cancel");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void DrawInvites(GroupPermissions perm)
|
private void DrawInvites(GroupPermissions perm)
|
||||||
{
|
{
|
||||||
var inviteTab = ImRaii.TabItem("Invites");
|
var inviteTab = ImRaii.TabItem("Invites");
|
||||||
@@ -902,7 +1235,9 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
|
|||||||
if (buttonCount == 0)
|
if (buttonCount == 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
float totalWidth = buttonCount * frameH + (buttonCount - 1) * style.ItemSpacing.X;
|
float totalWidth = _isOwner
|
||||||
|
? buttonCount * frameH + buttonCount * style.ItemSpacing.X + 20f
|
||||||
|
: buttonCount * frameH + buttonCount * style.ItemSpacing.X;
|
||||||
|
|
||||||
float curX = ImGui.GetCursorPosX();
|
float curX = ImGui.GetCursorPosX();
|
||||||
float avail = ImGui.GetContentRegionAvail().X;
|
float avail = ImGui.GetContentRegionAvail().X;
|
||||||
@@ -1031,69 +1366,40 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
|
|||||||
UiSharedService.ColoredSeparator(UIColors.Get("LightlessYellow"), 1.0f);
|
UiSharedService.ColoredSeparator(UIColors.Get("LightlessYellow"), 1.0f);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawBannedRow(BannedGroupUserDto bannedUser, int rowIndex, float colIdentity, float colMeta, float colActions)
|
private void QueueBanListRefresh(bool force = false)
|
||||||
{
|
{
|
||||||
using var id = ImRaii.PushId("banRow_" + bannedUser.UID);
|
if (!force)
|
||||||
|
|
||||||
var style = ImGui.GetStyle();
|
|
||||||
float x0 = ImGui.GetCursorPosX();
|
|
||||||
|
|
||||||
if (rowIndex % 2 == 0)
|
|
||||||
{
|
{
|
||||||
var drawList = ImGui.GetWindowDrawList();
|
if (_bannedUsersTask != null && !_bannedUsersTask.IsCompleted)
|
||||||
var pMin = ImGui.GetCursorScreenPos();
|
return;
|
||||||
var rowHeight = ImGui.GetTextLineHeightWithSpacing() * 2.6f;
|
|
||||||
var pMax = new Vector2(
|
|
||||||
pMin.X + colIdentity + colMeta + colActions + style.ItemSpacing.X * 2.0f,
|
|
||||||
pMin.Y + rowHeight);
|
|
||||||
|
|
||||||
var bgColor = UIColors.Get("FullBlack").WithAlpha(0.10f);
|
|
||||||
drawList.AddRectFilled(pMin, pMax, ImGui.ColorConvertFloat4ToU32(bgColor));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui.SetCursorPosX(x0);
|
_bannedUsersLoaded = false;
|
||||||
ImGui.AlignTextToFramePadding();
|
_bannedUsersLoadError = null;
|
||||||
|
|
||||||
string alias = bannedUser.UserAlias ?? string.Empty;
|
_bannedUsersTask = _apiController.GroupGetBannedUsers(new GroupDto(GroupFullInfo.Group));
|
||||||
string line1 = string.IsNullOrEmpty(alias)
|
}
|
||||||
? bannedUser.UID
|
|
||||||
: $"{alias} ({bannedUser.UID})";
|
|
||||||
|
|
||||||
ImGui.TextUnformatted(line1);
|
private void EnsureBanListLoaded()
|
||||||
|
{
|
||||||
|
_bannedUsersTask ??= _apiController.GroupGetBannedUsers(new GroupDto(GroupFullInfo.Group));
|
||||||
|
|
||||||
var reason = bannedUser.Reason ?? string.Empty;
|
if (_bannedUsersLoaded || _bannedUsersTask == null)
|
||||||
if (!string.IsNullOrWhiteSpace(reason))
|
return;
|
||||||
|
|
||||||
|
if (!_bannedUsersTask.IsCompleted)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (_bannedUsersTask.IsFaulted || _bannedUsersTask.IsCanceled)
|
||||||
{
|
{
|
||||||
var reasonPos = new Vector2(x0, ImGui.GetCursorPosY());
|
_bannedUsersLoadError = "Failed to load banlist from server.";
|
||||||
ImGui.SetCursorPos(reasonPos);
|
_bannedUsers = [];
|
||||||
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey);
|
_bannedUsersLoaded = true;
|
||||||
UiSharedService.TextWrapped(reason);
|
return;
|
||||||
ImGui.PopStyleColor();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui.SameLine();
|
_bannedUsers = _bannedUsersTask.GetAwaiter().GetResult() ?? [];
|
||||||
ImGui.SetCursorPosX(x0 + colIdentity + style.ItemSpacing.X);
|
_bannedUsersLoaded = true;
|
||||||
|
|
||||||
ImGui.AlignTextToFramePadding();
|
|
||||||
ImGui.TextUnformatted($"By: {bannedUser.BannedBy}");
|
|
||||||
|
|
||||||
var dateText = bannedUser.BannedOn.ToLocalTime().ToString(CultureInfo.CurrentCulture);
|
|
||||||
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey);
|
|
||||||
ImGui.TextUnformatted(dateText);
|
|
||||||
ImGui.PopStyleColor();
|
|
||||||
|
|
||||||
ImGui.SameLine();
|
|
||||||
ImGui.SetCursorPosX(x0 + colIdentity + colMeta + style.ItemSpacing.X * 2.0f);
|
|
||||||
|
|
||||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Check, "Unban"))
|
|
||||||
{
|
|
||||||
_apiController.GroupUnbanUser(bannedUser);
|
|
||||||
_bannedUsers.RemoveAll(b => string.Equals(b.UID, bannedUser.UID, StringComparison.Ordinal));
|
|
||||||
}
|
|
||||||
|
|
||||||
UiSharedService.AttachToolTip($"Unban {alias} ({bannedUser.UID}) from this Syncshell");
|
|
||||||
|
|
||||||
ImGui.Dummy(new Vector2(0, 4 * ImGuiHelpers.GlobalScale));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SavePruneSettings()
|
private void SavePruneSettings()
|
||||||
@@ -1116,6 +1422,13 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string StripAliasSuffix(string reason)
|
||||||
|
{
|
||||||
|
const string marker = " (Alias at time of ban:";
|
||||||
|
var idx = reason.IndexOf(marker, StringComparison.Ordinal);
|
||||||
|
return idx >= 0 ? reason[..idx] : reason;
|
||||||
|
}
|
||||||
|
|
||||||
private static bool MatchesUserFilter(Pair pair, string filterLower)
|
private static bool MatchesUserFilter(Pair pair, string filterLower)
|
||||||
{
|
{
|
||||||
var note = pair.GetNote() ?? string.Empty;
|
var note = pair.GetNote() ?? string.Empty;
|
||||||
@@ -1127,6 +1440,11 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
|
|||||||
|| alias.Contains(filterLower, StringComparison.OrdinalIgnoreCase);
|
|| alias.Contains(filterLower, StringComparison.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override void OnOpen()
|
||||||
|
{
|
||||||
|
base.OnOpen();
|
||||||
|
QueueBanListRefresh(force: true);
|
||||||
|
}
|
||||||
public override void OnClose()
|
public override void OnClose()
|
||||||
{
|
{
|
||||||
Mediator.Publish(new RemoveWindowMessage(this));
|
Mediator.Publish(new RemoveWindowMessage(this));
|
||||||
|
|||||||
@@ -1,855 +0,0 @@
|
|||||||
using Dalamud.Bindings.ImGui;
|
|
||||||
using Dalamud.Interface;
|
|
||||||
using Dalamud.Interface.Colors;
|
|
||||||
using Dalamud.Interface.Textures.TextureWraps;
|
|
||||||
using Dalamud.Interface.Utility;
|
|
||||||
using Dalamud.Interface.Utility.Raii;
|
|
||||||
using Dalamud.Plugin.Services;
|
|
||||||
using LightlessSync.API.Data;
|
|
||||||
using LightlessSync.API.Data.Enum;
|
|
||||||
using LightlessSync.API.Data.Extensions;
|
|
||||||
using LightlessSync.API.Dto;
|
|
||||||
using LightlessSync.API.Dto.Group;
|
|
||||||
using LightlessSync.Services;
|
|
||||||
using LightlessSync.Services.LightFinder;
|
|
||||||
using LightlessSync.Services.Mediator;
|
|
||||||
using LightlessSync.UI.Services;
|
|
||||||
using LightlessSync.UI.Tags;
|
|
||||||
using LightlessSync.Utils;
|
|
||||||
using LightlessSync.WebAPI;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using System.Numerics;
|
|
||||||
|
|
||||||
namespace LightlessSync.UI;
|
|
||||||
|
|
||||||
public class SyncshellFinderUI : WindowMediatorSubscriberBase
|
|
||||||
{
|
|
||||||
private readonly ApiController _apiController;
|
|
||||||
private readonly LightFinderService _broadcastService;
|
|
||||||
private readonly UiSharedService _uiSharedService;
|
|
||||||
private readonly LightFinderScannerService _broadcastScannerService;
|
|
||||||
private readonly PairUiService _pairUiService;
|
|
||||||
private readonly DalamudUtilService _dalamudUtilService;
|
|
||||||
|
|
||||||
private Vector4 _tagBackgroundColor = new(0.18f, 0.18f, 0.18f, 0.95f);
|
|
||||||
private Vector4 _tagBorderColor = new(0.35f, 0.35f, 0.35f, 0.4f);
|
|
||||||
|
|
||||||
private readonly List<SeStringUtils.SeStringSegment> _seResolvedSegments = new();
|
|
||||||
private readonly List<GroupJoinDto> _nearbySyncshells = [];
|
|
||||||
private List<GroupFullInfoDto> _currentSyncshells = [];
|
|
||||||
private int _selectedNearbyIndex = -1;
|
|
||||||
private int _syncshellPageIndex = 0;
|
|
||||||
private readonly HashSet<string> _recentlyJoined = new(StringComparer.Ordinal);
|
|
||||||
|
|
||||||
private GroupJoinDto? _joinDto;
|
|
||||||
private GroupJoinInfoDto? _joinInfo;
|
|
||||||
private DefaultPermissionsDto _ownPermissions = null!;
|
|
||||||
private bool _useTestSyncshells = false;
|
|
||||||
|
|
||||||
private bool _compactView = false;
|
|
||||||
private readonly LightlessProfileManager _lightlessProfileManager;
|
|
||||||
|
|
||||||
public SyncshellFinderUI(
|
|
||||||
ILogger<SyncshellFinderUI> logger,
|
|
||||||
LightlessMediator mediator,
|
|
||||||
PerformanceCollectorService performanceCollectorService,
|
|
||||||
LightFinderService broadcastService,
|
|
||||||
UiSharedService uiShared,
|
|
||||||
ApiController apiController,
|
|
||||||
LightFinderScannerService broadcastScannerService,
|
|
||||||
PairUiService pairUiService,
|
|
||||||
DalamudUtilService dalamudUtilService,
|
|
||||||
LightlessProfileManager lightlessProfileManager) : base(logger, mediator, "Shellfinder###LightlessSyncshellFinderUI", performanceCollectorService)
|
|
||||||
{
|
|
||||||
_broadcastService = broadcastService;
|
|
||||||
_uiSharedService = uiShared;
|
|
||||||
_apiController = apiController;
|
|
||||||
_broadcastScannerService = broadcastScannerService;
|
|
||||||
_pairUiService = pairUiService;
|
|
||||||
_dalamudUtilService = dalamudUtilService;
|
|
||||||
_lightlessProfileManager = lightlessProfileManager;
|
|
||||||
|
|
||||||
IsOpen = false;
|
|
||||||
WindowBuilder.For(this)
|
|
||||||
.SetSizeConstraints(new Vector2(600, 400), new Vector2(600, 550))
|
|
||||||
.Apply();
|
|
||||||
|
|
||||||
Mediator.Subscribe<SyncshellBroadcastsUpdatedMessage>(this, async _ => await RefreshSyncshellsAsync().ConfigureAwait(false));
|
|
||||||
Mediator.Subscribe<BroadcastStatusChangedMessage>(this, async _ => await RefreshSyncshellsAsync().ConfigureAwait(false));
|
|
||||||
Mediator.Subscribe<UserLeftSyncshell>(this, async _ => await RefreshSyncshellsAsync(_.gid).ConfigureAwait(false));
|
|
||||||
Mediator.Subscribe<UserJoinedSyncshell>(this, async _ => await RefreshSyncshellsAsync(_.gid).ConfigureAwait(false));
|
|
||||||
}
|
|
||||||
|
|
||||||
public override async void OnOpen()
|
|
||||||
{
|
|
||||||
_ownPermissions = _apiController.DefaultPermissions.DeepClone()!;
|
|
||||||
await RefreshSyncshellsAsync().ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void DrawInternal()
|
|
||||||
{
|
|
||||||
ImGui.BeginGroup();
|
|
||||||
_uiSharedService.MediumText("Nearby Syncshells", UIColors.Get("LightlessPurple"));
|
|
||||||
|
|
||||||
#if DEBUG
|
|
||||||
if (ImGui.SmallButton("Show test syncshells"))
|
|
||||||
{
|
|
||||||
_useTestSyncshells = !_useTestSyncshells;
|
|
||||||
_ = Task.Run(async () => await RefreshSyncshellsAsync().ConfigureAwait(false));
|
|
||||||
}
|
|
||||||
ImGui.SameLine();
|
|
||||||
#endif
|
|
||||||
|
|
||||||
string checkboxLabel = "Compact view";
|
|
||||||
float availWidth = ImGui.GetContentRegionAvail().X;
|
|
||||||
float checkboxWidth = ImGui.CalcTextSize(checkboxLabel).X + ImGui.GetFrameHeight();
|
|
||||||
|
|
||||||
float rightX = ImGui.GetCursorPosX() + availWidth - checkboxWidth - 4.0f;
|
|
||||||
ImGui.SetCursorPosX(rightX);
|
|
||||||
ImGui.Checkbox(checkboxLabel, ref _compactView);
|
|
||||||
ImGui.EndGroup();
|
|
||||||
|
|
||||||
UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"));
|
|
||||||
ImGui.Dummy(new Vector2(0, 2 * ImGuiHelpers.GlobalScale));
|
|
||||||
if (_nearbySyncshells.Count == 0)
|
|
||||||
{
|
|
||||||
ImGui.TextColored(ImGuiColors.DalamudGrey, "No nearby syncshells are being broadcasted.");
|
|
||||||
|
|
||||||
if (!_broadcastService.IsBroadcasting)
|
|
||||||
{
|
|
||||||
|
|
||||||
UiSharedService.ColoredSeparator(UIColors.Get("LightlessYellow"));
|
|
||||||
|
|
||||||
ImGui.TextColored(UIColors.Get("LightlessYellow"), "Lightfinder is currently disabled, to locate nearby syncshells, Lightfinder must be active.");
|
|
||||||
ImGuiHelpers.ScaledDummy(0.5f);
|
|
||||||
|
|
||||||
ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, 10.0f);
|
|
||||||
ImGui.PushStyleColor(ImGuiCol.Button, UIColors.Get("LightlessPurple"));
|
|
||||||
|
|
||||||
if (ImGui.Button("Open Lightfinder", new Vector2(200 * ImGuiHelpers.GlobalScale, 0)))
|
|
||||||
{
|
|
||||||
Mediator.Publish(new UiToggleMessage(typeof(LightFinderUI)));
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui.PopStyleColor();
|
|
||||||
ImGui.PopStyleVar();
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var broadcasts = _broadcastScannerService.GetActiveSyncshellBroadcasts().ToList() ?? [];
|
|
||||||
_broadcastScannerService.TryGetLocalHashedCid(out var localHashedCid);
|
|
||||||
|
|
||||||
var cardData = new List<(GroupJoinDto Shell, string BroadcasterName, bool IsSelfBroadcast)>();
|
|
||||||
|
|
||||||
foreach (var shell in _nearbySyncshells)
|
|
||||||
{
|
|
||||||
string broadcasterName;
|
|
||||||
|
|
||||||
if (shell?.Group == null || string.IsNullOrEmpty(shell.Group.GID))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (_useTestSyncshells)
|
|
||||||
{
|
|
||||||
var displayName = !string.IsNullOrEmpty(shell.Group.Alias)
|
|
||||||
? shell.Group.Alias
|
|
||||||
: shell.Group.GID;
|
|
||||||
|
|
||||||
broadcasterName = $"{displayName} (Tester of TestWorld)";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var broadcast = broadcasts
|
|
||||||
.FirstOrDefault(b => string.Equals(b.GID, shell.Group.GID, StringComparison.Ordinal));
|
|
||||||
|
|
||||||
if (broadcast == null)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
var (name, address) = _dalamudUtilService.FindPlayerByNameHash(broadcast.HashedCID);
|
|
||||||
if (string.IsNullOrEmpty(name))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
var worldName = _dalamudUtilService.GetWorldNameFromPlayerAddress(address);
|
|
||||||
broadcasterName = !string.IsNullOrEmpty(worldName)
|
|
||||||
? $"{name} ({worldName})"
|
|
||||||
: name;
|
|
||||||
|
|
||||||
var isSelfBroadcast = !string.IsNullOrEmpty(localHashedCid)
|
|
||||||
&& string.Equals(broadcast.HashedCID, localHashedCid, StringComparison.Ordinal);
|
|
||||||
|
|
||||||
cardData.Add((shell, broadcasterName, isSelfBroadcast));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
cardData.Add((shell, broadcasterName, false));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cardData.Count == 0)
|
|
||||||
{
|
|
||||||
ImGui.TextColored(ImGuiColors.DalamudGrey, "No nearby syncshells are being broadcasted.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_compactView)
|
|
||||||
{
|
|
||||||
DrawSyncshellGrid(cardData);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
DrawSyncshellList(cardData);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (_joinDto != null && _joinInfo != null && _joinInfo.Success)
|
|
||||||
DrawConfirmation();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DrawSyncshellList(List<(GroupJoinDto Shell, string BroadcasterName, bool IsSelfBroadcast)> listData)
|
|
||||||
{
|
|
||||||
const int shellsPerPage = 3;
|
|
||||||
var totalPages = (int)Math.Ceiling(listData.Count / (float)shellsPerPage);
|
|
||||||
if (totalPages <= 0)
|
|
||||||
totalPages = 1;
|
|
||||||
|
|
||||||
_syncshellPageIndex = Math.Clamp(_syncshellPageIndex, 0, totalPages - 1);
|
|
||||||
|
|
||||||
var firstIndex = _syncshellPageIndex * shellsPerPage;
|
|
||||||
var lastExclusive = Math.Min(firstIndex + shellsPerPage, listData.Count);
|
|
||||||
|
|
||||||
ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, 8.0f);
|
|
||||||
ImGui.PushStyleVar(ImGuiStyleVar.FrameBorderSize, 1.0f);
|
|
||||||
|
|
||||||
for (int index = firstIndex; index < lastExclusive; index++)
|
|
||||||
{
|
|
||||||
var (shell, broadcasterName, isSelfBroadcast) = listData[index];
|
|
||||||
var broadcasterLabel = string.IsNullOrEmpty(broadcasterName)
|
|
||||||
? (isSelfBroadcast ? "You" : string.Empty)
|
|
||||||
: (isSelfBroadcast ? $"{broadcasterName} (You)" : broadcasterName);
|
|
||||||
|
|
||||||
ImGui.PushID(shell.Group.GID);
|
|
||||||
float rowHeight = 74f * ImGuiHelpers.GlobalScale;
|
|
||||||
|
|
||||||
ImGui.BeginChild($"ShellRow##{shell.Group.GID}", new Vector2(-1, rowHeight), border: true);
|
|
||||||
|
|
||||||
var displayName = !string.IsNullOrEmpty(shell.Group.Alias) ? shell.Group.Alias : shell.Group.GID;
|
|
||||||
|
|
||||||
var style = ImGui.GetStyle();
|
|
||||||
float startX = ImGui.GetCursorPosX();
|
|
||||||
float regionW = ImGui.GetContentRegionAvail().X;
|
|
||||||
float rightTxtW = ImGui.CalcTextSize(broadcasterLabel).X;
|
|
||||||
|
|
||||||
_uiSharedService.MediumText(displayName, UIColors.Get("LightlessPurple"));
|
|
||||||
if (ImGui.IsItemHovered())
|
|
||||||
ImGui.SetTooltip("Click to open profile.");
|
|
||||||
if (ImGui.IsItemClicked())
|
|
||||||
{
|
|
||||||
Mediator.Publish(new GroupProfileOpenStandaloneMessage(shell.Group));
|
|
||||||
}
|
|
||||||
|
|
||||||
float rightX = startX + regionW - rightTxtW - style.ItemSpacing.X;
|
|
||||||
ImGui.SameLine();
|
|
||||||
ImGui.SetCursorPosX(rightX);
|
|
||||||
ImGui.TextUnformatted(broadcasterLabel);
|
|
||||||
if (ImGui.IsItemHovered())
|
|
||||||
ImGui.SetTooltip("Broadcaster of the syncshell.");
|
|
||||||
|
|
||||||
UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"));
|
|
||||||
|
|
||||||
var groupProfile = _lightlessProfileManager.GetLightlessGroupProfile(shell.Group);
|
|
||||||
|
|
||||||
IReadOnlyList<ProfileTagDefinition> groupTags =
|
|
||||||
groupProfile != null && groupProfile.Tags.Count > 0
|
|
||||||
? ProfileTagService.ResolveTags(groupProfile.Tags)
|
|
||||||
: [];
|
|
||||||
|
|
||||||
var limitedTags = groupTags.Count > 3
|
|
||||||
? [.. groupTags.Take(3)]
|
|
||||||
: groupTags;
|
|
||||||
|
|
||||||
float tagScale = ImGuiHelpers.GlobalScale * 0.9f;
|
|
||||||
|
|
||||||
Vector2 rowStartLocal = ImGui.GetCursorPos();
|
|
||||||
|
|
||||||
float tagsWidth = 0f;
|
|
||||||
float tagsHeight = 0f;
|
|
||||||
|
|
||||||
if (limitedTags.Count > 0)
|
|
||||||
{
|
|
||||||
(tagsWidth, tagsHeight) = RenderProfileTagsSingleRow(limitedTags, tagScale);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
ImGui.SetCursorPosX(startX);
|
|
||||||
ImGui.TextDisabled("-- No tags set --");
|
|
||||||
ImGui.Dummy(new Vector2(0, 4 * ImGuiHelpers.GlobalScale));
|
|
||||||
}
|
|
||||||
|
|
||||||
float btnBaselineY = rowStartLocal.Y;
|
|
||||||
float joinX = rowStartLocal.X + (tagsWidth > 0 ? tagsWidth + style.ItemSpacing.X : 0f);
|
|
||||||
|
|
||||||
ImGui.SetCursorPos(new Vector2(joinX, btnBaselineY));
|
|
||||||
DrawJoinButton(shell, isSelfBroadcast);
|
|
||||||
|
|
||||||
float btnHeight = ImGui.GetFrameHeightWithSpacing();
|
|
||||||
float rowHeightUsed = MathF.Max(tagsHeight, btnHeight);
|
|
||||||
|
|
||||||
ImGui.SetCursorPos(new Vector2(
|
|
||||||
rowStartLocal.X,
|
|
||||||
rowStartLocal.Y + rowHeightUsed));
|
|
||||||
|
|
||||||
ImGui.EndChild();
|
|
||||||
ImGui.PopID();
|
|
||||||
|
|
||||||
ImGui.Dummy(new Vector2(0, 2 * ImGuiHelpers.GlobalScale));
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui.PopStyleVar(2);
|
|
||||||
|
|
||||||
DrawPagination(totalPages);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DrawSyncshellGrid(List<(GroupJoinDto Shell, string BroadcasterName, bool IsSelfBroadcast)> cardData)
|
|
||||||
{
|
|
||||||
const int shellsPerPage = 4;
|
|
||||||
var totalPages = (int)Math.Ceiling(cardData.Count / (float)shellsPerPage);
|
|
||||||
if (totalPages <= 0)
|
|
||||||
totalPages = 1;
|
|
||||||
|
|
||||||
_syncshellPageIndex = Math.Clamp(_syncshellPageIndex, 0, totalPages - 1);
|
|
||||||
|
|
||||||
var firstIndex = _syncshellPageIndex * shellsPerPage;
|
|
||||||
var lastExclusive = Math.Min(firstIndex + shellsPerPage, cardData.Count);
|
|
||||||
|
|
||||||
var avail = ImGui.GetContentRegionAvail();
|
|
||||||
var spacing = ImGui.GetStyle().ItemSpacing;
|
|
||||||
|
|
||||||
var cardWidth = (avail.X - spacing.X) / 2.0f;
|
|
||||||
var cardHeight = (avail.Y - spacing.Y - (ImGui.GetFrameHeightWithSpacing() * 2.0f)) / 2.0f;
|
|
||||||
cardHeight = MathF.Max(110f * ImGuiHelpers.GlobalScale, cardHeight);
|
|
||||||
|
|
||||||
ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, 8.0f);
|
|
||||||
ImGui.PushStyleVar(ImGuiStyleVar.FrameBorderSize, 1.0f);
|
|
||||||
|
|
||||||
for (int index = firstIndex; index < lastExclusive; index++)
|
|
||||||
{
|
|
||||||
var localIndex = index - firstIndex;
|
|
||||||
var (shell, broadcasterName, isSelfBroadcast) = cardData[index];
|
|
||||||
var broadcasterLabel = string.IsNullOrEmpty(broadcasterName)
|
|
||||||
? (isSelfBroadcast ? "You" : string.Empty)
|
|
||||||
: (isSelfBroadcast ? $"{broadcasterName} (You)" : broadcasterName);
|
|
||||||
|
|
||||||
if (localIndex % 2 != 0)
|
|
||||||
ImGui.SameLine();
|
|
||||||
|
|
||||||
ImGui.PushID(shell.Group.GID);
|
|
||||||
|
|
||||||
ImGui.BeginGroup();
|
|
||||||
_ = ImGui.BeginChild("ShellCard##" + shell.Group.GID, new Vector2(cardWidth, cardHeight), border: true);
|
|
||||||
|
|
||||||
var displayName = !string.IsNullOrEmpty(shell.Group.Alias)
|
|
||||||
? shell.Group.Alias
|
|
||||||
: shell.Group.GID;
|
|
||||||
|
|
||||||
var style = ImGui.GetStyle();
|
|
||||||
float startX = ImGui.GetCursorPosX();
|
|
||||||
float availW = ImGui.GetContentRegionAvail().X;
|
|
||||||
|
|
||||||
ImGui.BeginGroup();
|
|
||||||
|
|
||||||
_uiSharedService.MediumText(displayName, UIColors.Get("LightlessPurple"));
|
|
||||||
if (ImGui.IsItemHovered())
|
|
||||||
ImGui.SetTooltip("Click to open profile.");
|
|
||||||
if (ImGui.IsItemClicked())
|
|
||||||
{
|
|
||||||
Mediator.Publish(new GroupProfileOpenStandaloneMessage(shell.Group));
|
|
||||||
}
|
|
||||||
|
|
||||||
float nameRightX = ImGui.GetItemRectMax().X;
|
|
||||||
|
|
||||||
var regionMinScreen = ImGui.GetCursorScreenPos();
|
|
||||||
float regionRightX = regionMinScreen.X + availW;
|
|
||||||
|
|
||||||
float minBroadcasterX = nameRightX + style.ItemSpacing.X;
|
|
||||||
|
|
||||||
float maxBroadcasterWidth = regionRightX - minBroadcasterX;
|
|
||||||
|
|
||||||
string broadcasterToShow = broadcasterLabel;
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(broadcasterLabel) && maxBroadcasterWidth > 0f)
|
|
||||||
{
|
|
||||||
float bcFullWidth = ImGui.CalcTextSize(broadcasterLabel).X;
|
|
||||||
string toolTip;
|
|
||||||
|
|
||||||
if (bcFullWidth > maxBroadcasterWidth)
|
|
||||||
{
|
|
||||||
broadcasterToShow = TruncateTextToWidth(broadcasterLabel, maxBroadcasterWidth);
|
|
||||||
toolTip = broadcasterLabel + Environment.NewLine + Environment.NewLine + "Broadcaster of the syncshell.";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
toolTip = "Broadcaster of the syncshell.";
|
|
||||||
}
|
|
||||||
|
|
||||||
float bcWidth = ImGui.CalcTextSize(broadcasterToShow).X;
|
|
||||||
|
|
||||||
float broadX = regionRightX - bcWidth;
|
|
||||||
|
|
||||||
broadX = MathF.Max(broadX, minBroadcasterX);
|
|
||||||
|
|
||||||
ImGui.SameLine();
|
|
||||||
var curPos = ImGui.GetCursorPos();
|
|
||||||
ImGui.SetCursorPos(new Vector2(broadX - regionMinScreen.X + startX, curPos.Y + 3f * ImGuiHelpers.GlobalScale));
|
|
||||||
ImGui.TextUnformatted(broadcasterToShow);
|
|
||||||
if (ImGui.IsItemHovered())
|
|
||||||
ImGui.SetTooltip(toolTip);
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui.EndGroup();
|
|
||||||
|
|
||||||
UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"));
|
|
||||||
|
|
||||||
ImGui.Dummy(new Vector2(0, 6 * ImGuiHelpers.GlobalScale));
|
|
||||||
|
|
||||||
var groupProfile = _lightlessProfileManager.GetLightlessGroupProfile(shell.Group);
|
|
||||||
|
|
||||||
IReadOnlyList<ProfileTagDefinition> groupTags =
|
|
||||||
groupProfile != null && groupProfile.Tags.Count > 0
|
|
||||||
? ProfileTagService.ResolveTags(groupProfile.Tags)
|
|
||||||
: [];
|
|
||||||
|
|
||||||
float tagScale = ImGuiHelpers.GlobalScale * 0.9f;
|
|
||||||
|
|
||||||
if (groupTags.Count > 0)
|
|
||||||
{
|
|
||||||
var limitedTags = groupTags.Count > 2
|
|
||||||
? [.. groupTags.Take(2)]
|
|
||||||
: groupTags;
|
|
||||||
|
|
||||||
ImGui.SetCursorPosX(startX);
|
|
||||||
|
|
||||||
var (_, tagsHeight) = RenderProfileTagsSingleRow(limitedTags, tagScale);
|
|
||||||
|
|
||||||
ImGui.Dummy(new Vector2(0, 4 * ImGuiHelpers.GlobalScale));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
ImGui.SetCursorPosX(startX);
|
|
||||||
ImGui.TextDisabled("-- No tags set --");
|
|
||||||
ImGui.Dummy(new Vector2(0, 4 * ImGuiHelpers.GlobalScale));
|
|
||||||
}
|
|
||||||
|
|
||||||
var buttonHeight = ImGui.GetFrameHeightWithSpacing();
|
|
||||||
var remainingY = ImGui.GetContentRegionAvail().Y - buttonHeight;
|
|
||||||
if (remainingY > 0)
|
|
||||||
ImGui.Dummy(new Vector2(0, remainingY));
|
|
||||||
|
|
||||||
DrawJoinButton(shell, isSelfBroadcast);
|
|
||||||
|
|
||||||
ImGui.EndChild();
|
|
||||||
ImGui.EndGroup();
|
|
||||||
|
|
||||||
ImGui.PopID();
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui.Dummy(new Vector2(0, 2 * ImGuiHelpers.GlobalScale));
|
|
||||||
ImGui.PopStyleVar(2);
|
|
||||||
|
|
||||||
DrawPagination(totalPages);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DrawPagination(int totalPages)
|
|
||||||
{
|
|
||||||
if (totalPages > 1)
|
|
||||||
{
|
|
||||||
UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"));
|
|
||||||
|
|
||||||
var style = ImGui.GetStyle();
|
|
||||||
string pageLabel = $"Page {_syncshellPageIndex + 1}/{totalPages}";
|
|
||||||
|
|
||||||
float prevWidth = ImGui.CalcTextSize("<").X + style.FramePadding.X * 2;
|
|
||||||
float nextWidth = ImGui.CalcTextSize(">").X + style.FramePadding.X * 2;
|
|
||||||
float textWidth = ImGui.CalcTextSize(pageLabel).X;
|
|
||||||
|
|
||||||
float totalWidth = prevWidth + textWidth + nextWidth + style.ItemSpacing.X * 2;
|
|
||||||
|
|
||||||
float availWidth = ImGui.GetContentRegionAvail().X;
|
|
||||||
float offsetX = (availWidth - totalWidth) * 0.5f;
|
|
||||||
|
|
||||||
ImGui.SetCursorPosX(ImGui.GetCursorPosX() + offsetX);
|
|
||||||
|
|
||||||
if (ImGui.Button("<##PrevSyncshellPage") && _syncshellPageIndex > 0)
|
|
||||||
_syncshellPageIndex--;
|
|
||||||
|
|
||||||
ImGui.SameLine();
|
|
||||||
ImGui.Text(pageLabel);
|
|
||||||
|
|
||||||
ImGui.SameLine();
|
|
||||||
if (ImGui.Button(">##NextSyncshellPage") && _syncshellPageIndex < totalPages - 1)
|
|
||||||
_syncshellPageIndex++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DrawJoinButton(GroupJoinDto shell, bool isSelfBroadcast)
|
|
||||||
{
|
|
||||||
const string visibleLabel = "Join";
|
|
||||||
var label = $"{visibleLabel}##{shell.Group.GID}";
|
|
||||||
|
|
||||||
var isAlreadyMember = _currentSyncshells.Exists(g => string.Equals(g.GID, shell.GID, StringComparison.Ordinal));
|
|
||||||
var isRecentlyJoined = _recentlyJoined.Contains(shell.GID);
|
|
||||||
|
|
||||||
Vector2 buttonSize;
|
|
||||||
|
|
||||||
if (!_compactView)
|
|
||||||
{
|
|
||||||
var style = ImGui.GetStyle();
|
|
||||||
var textSize = ImGui.CalcTextSize(visibleLabel);
|
|
||||||
|
|
||||||
var width = textSize.X + style.FramePadding.X * 20f;
|
|
||||||
buttonSize = new Vector2(width, 30f);
|
|
||||||
|
|
||||||
float availX = ImGui.GetContentRegionAvail().X;
|
|
||||||
float curX = ImGui.GetCursorPosX();
|
|
||||||
float newX = curX + (availX - buttonSize.X);
|
|
||||||
ImGui.SetCursorPosX(newX);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
buttonSize = new Vector2(-1, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isAlreadyMember && !isRecentlyJoined && !isSelfBroadcast)
|
|
||||||
{
|
|
||||||
ImGui.PushStyleColor(ImGuiCol.Button, UIColors.Get("LightlessGreen"));
|
|
||||||
ImGui.PushStyleColor(ImGuiCol.ButtonHovered, UIColors.Get("LightlessGreen").WithAlpha(0.85f));
|
|
||||||
ImGui.PushStyleColor(ImGuiCol.ButtonActive, UIColors.Get("LightlessGreen").WithAlpha(0.75f));
|
|
||||||
if (ImGui.Button(label, buttonSize))
|
|
||||||
{
|
|
||||||
_logger.LogInformation($"Join requested for Syncshell {shell.Group.GID} ({shell.Group.Alias})");
|
|
||||||
|
|
||||||
_ = Task.Run(async () =>
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var info = await _apiController.GroupJoinHashed(new GroupJoinHashedDto(
|
|
||||||
shell.Group,
|
|
||||||
shell.Password,
|
|
||||||
shell.GroupUserPreferredPermissions
|
|
||||||
)).ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (info != null && info.Success)
|
|
||||||
{
|
|
||||||
_joinDto = new GroupJoinDto(shell.Group, shell.Password, shell.GroupUserPreferredPermissions);
|
|
||||||
_joinInfo = info;
|
|
||||||
_ownPermissions = _apiController.DefaultPermissions.DeepClone()!;
|
|
||||||
|
|
||||||
_logger.LogInformation($"Fetched join info for {shell.Group.GID}");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_logger.LogWarning($"Failed to join {shell.Group.GID}: info was null or unsuccessful");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, $"Join failed for {shell.Group.GID}");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
ImGui.PushStyleColor(ImGuiCol.Button, UIColors.Get("DimRed"));
|
|
||||||
ImGui.PushStyleColor(ImGuiCol.ButtonHovered, UIColors.Get("DimRed").WithAlpha(0.85f));
|
|
||||||
ImGui.PushStyleColor(ImGuiCol.ButtonActive, UIColors.Get("DimRed").WithAlpha(0.75f));
|
|
||||||
|
|
||||||
using (ImRaii.Disabled())
|
|
||||||
{
|
|
||||||
ImGui.Button(label, buttonSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
UiSharedService.AttachToolTip(isSelfBroadcast
|
|
||||||
? "This is your own Syncshell."
|
|
||||||
: "Already a member or owner of this Syncshell.");
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui.PopStyleColor(3);
|
|
||||||
}
|
|
||||||
|
|
||||||
private (float widthUsed, float rowHeight) RenderProfileTagsSingleRow(IReadOnlyList<ProfileTagDefinition> tags, float scale)
|
|
||||||
{
|
|
||||||
if (tags == null || tags.Count == 0)
|
|
||||||
return (0f, 0f);
|
|
||||||
|
|
||||||
var drawList = ImGui.GetWindowDrawList();
|
|
||||||
var style = ImGui.GetStyle();
|
|
||||||
var defaultTextColorU32 = ImGui.GetColorU32(ImGuiCol.Text);
|
|
||||||
|
|
||||||
var baseLocal = ImGui.GetCursorPos();
|
|
||||||
var baseScreen = ImGui.GetCursorScreenPos();
|
|
||||||
float availableWidth = ImGui.GetContentRegionAvail().X;
|
|
||||||
if (availableWidth <= 0f)
|
|
||||||
availableWidth = 1f;
|
|
||||||
|
|
||||||
float cursorLocalX = baseLocal.X;
|
|
||||||
float cursorScreenX = baseScreen.X;
|
|
||||||
float rowHeight = 0f;
|
|
||||||
|
|
||||||
for (int i = 0; i < tags.Count; i++)
|
|
||||||
{
|
|
||||||
var tag = tags[i];
|
|
||||||
if (!tag.HasContent)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
var tagSize = ProfileTagRenderer.MeasureTag(tag, scale, style, _tagBackgroundColor, _tagBorderColor, defaultTextColorU32, _seResolvedSegments, GetIconWrap, _logger);
|
|
||||||
|
|
||||||
float tagWidth = tagSize.X;
|
|
||||||
float tagHeight = tagSize.Y;
|
|
||||||
|
|
||||||
if (cursorLocalX > baseLocal.X && cursorLocalX + tagWidth > baseLocal.X + availableWidth)
|
|
||||||
break;
|
|
||||||
|
|
||||||
var tagScreenPos = new Vector2(cursorScreenX, baseScreen.Y);
|
|
||||||
ImGui.SetCursorScreenPos(tagScreenPos);
|
|
||||||
ImGui.InvisibleButton($"##profileTagInline_{i}", tagSize);
|
|
||||||
|
|
||||||
ProfileTagRenderer.RenderTag(tag, tagScreenPos, scale, drawList, style, _tagBackgroundColor, _tagBorderColor, defaultTextColorU32, _seResolvedSegments, GetIconWrap, _logger);
|
|
||||||
|
|
||||||
cursorLocalX += tagWidth + style.ItemSpacing.X;
|
|
||||||
cursorScreenX += tagWidth + style.ItemSpacing.X;
|
|
||||||
rowHeight = MathF.Max(rowHeight, tagHeight);
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui.SetCursorPos(new Vector2(baseLocal.X, baseLocal.Y + rowHeight));
|
|
||||||
|
|
||||||
float widthUsed = cursorLocalX - baseLocal.X;
|
|
||||||
return (widthUsed, rowHeight);
|
|
||||||
}
|
|
||||||
private static string TruncateTextToWidth(string text, float maxWidth)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(text))
|
|
||||||
return text;
|
|
||||||
|
|
||||||
const string ellipsis = "...";
|
|
||||||
float ellipsisWidth = ImGui.CalcTextSize(ellipsis).X;
|
|
||||||
|
|
||||||
if (maxWidth <= ellipsisWidth)
|
|
||||||
return ellipsis;
|
|
||||||
|
|
||||||
int low = 0;
|
|
||||||
int high = text.Length;
|
|
||||||
string best = ellipsis;
|
|
||||||
|
|
||||||
while (low <= high)
|
|
||||||
{
|
|
||||||
int mid = (low + high) / 2;
|
|
||||||
string candidate = string.Concat(text.AsSpan(0, mid), ellipsis);
|
|
||||||
float width = ImGui.CalcTextSize(candidate).X;
|
|
||||||
|
|
||||||
if (width <= maxWidth)
|
|
||||||
{
|
|
||||||
best = candidate;
|
|
||||||
low = mid + 1;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
high = mid - 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return best;
|
|
||||||
}
|
|
||||||
|
|
||||||
private IDalamudTextureWrap? GetIconWrap(uint iconId)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (_uiSharedService.TryGetIcon(iconId, out var wrap) && wrap != null)
|
|
||||||
return wrap;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogDebug(ex, "Failed to resolve icon {IconId} for profile tags", iconId);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DrawConfirmation()
|
|
||||||
{
|
|
||||||
if (_joinDto != null && _joinInfo != null)
|
|
||||||
{
|
|
||||||
ImGui.Separator();
|
|
||||||
ImGui.TextUnformatted($"Join Syncshell: {_joinDto.Group.AliasOrGID} by {_joinInfo.OwnerAliasOrUID}");
|
|
||||||
ImGuiHelpers.ScaledDummy(2f);
|
|
||||||
ImGui.TextUnformatted("Suggested Syncshell Permissions:");
|
|
||||||
|
|
||||||
DrawPermissionRow("Sounds", _joinInfo.GroupPermissions.IsPreferDisableSounds(), _ownPermissions.DisableGroupSounds, v => _ownPermissions.DisableGroupSounds = v);
|
|
||||||
DrawPermissionRow("Animations", _joinInfo.GroupPermissions.IsPreferDisableAnimations(), _ownPermissions.DisableGroupAnimations, v => _ownPermissions.DisableGroupAnimations = v);
|
|
||||||
DrawPermissionRow("VFX", _joinInfo.GroupPermissions.IsPreferDisableVFX(), _ownPermissions.DisableGroupVFX, v => _ownPermissions.DisableGroupVFX = v);
|
|
||||||
|
|
||||||
ImGui.NewLine();
|
|
||||||
ImGui.NewLine();
|
|
||||||
|
|
||||||
if (_uiSharedService.IconTextButton(Dalamud.Interface.FontAwesomeIcon.Plus, $"Finalize and join {_joinDto.Group.AliasOrGID}"))
|
|
||||||
{
|
|
||||||
var finalPermissions = GroupUserPreferredPermissions.NoneSet;
|
|
||||||
finalPermissions.SetDisableSounds(_ownPermissions.DisableGroupSounds);
|
|
||||||
finalPermissions.SetDisableAnimations(_ownPermissions.DisableGroupAnimations);
|
|
||||||
finalPermissions.SetDisableVFX(_ownPermissions.DisableGroupVFX);
|
|
||||||
|
|
||||||
_ = _apiController.GroupJoinFinalize(new GroupJoinDto(_joinDto.Group, _joinDto.Password, finalPermissions));
|
|
||||||
|
|
||||||
_recentlyJoined.Add(_joinDto.Group.GID);
|
|
||||||
|
|
||||||
_joinDto = null;
|
|
||||||
_joinInfo = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DrawPermissionRow(string label, bool suggested, bool current, Action<bool> apply)
|
|
||||||
{
|
|
||||||
ImGui.AlignTextToFramePadding();
|
|
||||||
ImGui.TextUnformatted($"- {label}");
|
|
||||||
|
|
||||||
ImGui.SameLine(150 * ImGuiHelpers.GlobalScale);
|
|
||||||
ImGui.TextUnformatted("Current:");
|
|
||||||
ImGui.SameLine();
|
|
||||||
_uiSharedService.BooleanToColoredIcon(!current);
|
|
||||||
|
|
||||||
ImGui.SameLine(300 * ImGuiHelpers.GlobalScale);
|
|
||||||
ImGui.TextUnformatted("Suggested:");
|
|
||||||
ImGui.SameLine();
|
|
||||||
_uiSharedService.BooleanToColoredIcon(!suggested);
|
|
||||||
|
|
||||||
ImGui.SameLine(450 * ImGuiHelpers.GlobalScale);
|
|
||||||
using var id = ImRaii.PushId(label);
|
|
||||||
if (current != suggested)
|
|
||||||
{
|
|
||||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.ArrowRight, "Apply"))
|
|
||||||
apply(suggested);
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui.NewLine();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task RefreshSyncshellsAsync(string? gid = null)
|
|
||||||
{
|
|
||||||
var syncshellBroadcasts = _broadcastScannerService.GetActiveSyncshellBroadcasts();
|
|
||||||
var snapshot = _pairUiService.GetSnapshot();
|
|
||||||
_currentSyncshells = [.. snapshot.GroupPairs.Keys];
|
|
||||||
|
|
||||||
_recentlyJoined.RemoveWhere(gid =>
|
|
||||||
_currentSyncshells.Exists(s => string.Equals(s.GID, gid, StringComparison.Ordinal)));
|
|
||||||
|
|
||||||
List<GroupJoinDto>? updatedList = [];
|
|
||||||
|
|
||||||
if (_useTestSyncshells)
|
|
||||||
{
|
|
||||||
updatedList = BuildTestSyncshells();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (syncshellBroadcasts.Count == 0)
|
|
||||||
{
|
|
||||||
ClearSyncshells();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var groups = await _apiController.GetBroadcastedGroups(syncshellBroadcasts)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
updatedList = groups?.DistinctBy(g => g.Group.GID).ToList();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Failed to refresh broadcasted syncshells.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (updatedList == null || updatedList.Count == 0)
|
|
||||||
{
|
|
||||||
ClearSyncshells();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (gid != null && _recentlyJoined.Contains(gid))
|
|
||||||
{
|
|
||||||
_recentlyJoined.Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
var previousGid = GetSelectedGid();
|
|
||||||
|
|
||||||
_nearbySyncshells.Clear();
|
|
||||||
_nearbySyncshells.AddRange(updatedList);
|
|
||||||
|
|
||||||
if (previousGid != null)
|
|
||||||
{
|
|
||||||
var newIndex = _nearbySyncshells.FindIndex(s =>
|
|
||||||
string.Equals(s.Group.GID, previousGid, StringComparison.Ordinal));
|
|
||||||
|
|
||||||
if (newIndex >= 0)
|
|
||||||
{
|
|
||||||
_selectedNearbyIndex = newIndex;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ClearSelection();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static List<GroupJoinDto> BuildTestSyncshells()
|
|
||||||
{
|
|
||||||
var testGroup1 = new GroupData("TEST-ALPHA", "Alpha Shell");
|
|
||||||
var testGroup2 = new GroupData("TEST-BETA", "Beta Shell");
|
|
||||||
var testGroup3 = new GroupData("TEST-GAMMA", "Gamma Shell");
|
|
||||||
var testGroup4 = new GroupData("TEST-DELTA", "Delta Shell");
|
|
||||||
var testGroup5 = new GroupData("TEST-CHARLIE", "Charlie Shell");
|
|
||||||
var testGroup6 = new GroupData("TEST-OMEGA", "Omega Shell");
|
|
||||||
var testGroup7 = new GroupData("TEST-POINT", "Point Shell");
|
|
||||||
var testGroup8 = new GroupData("TEST-HOTEL", "Hotel Shell");
|
|
||||||
|
|
||||||
return
|
|
||||||
[
|
|
||||||
new(testGroup1, "", GroupUserPreferredPermissions.NoneSet),
|
|
||||||
new(testGroup2, "", GroupUserPreferredPermissions.NoneSet),
|
|
||||||
new(testGroup3, "", GroupUserPreferredPermissions.NoneSet),
|
|
||||||
new(testGroup4, "", GroupUserPreferredPermissions.NoneSet),
|
|
||||||
new(testGroup5, "", GroupUserPreferredPermissions.NoneSet),
|
|
||||||
new(testGroup6, "", GroupUserPreferredPermissions.NoneSet),
|
|
||||||
new(testGroup7, "", GroupUserPreferredPermissions.NoneSet),
|
|
||||||
new(testGroup8, "", GroupUserPreferredPermissions.NoneSet),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ClearSyncshells()
|
|
||||||
{
|
|
||||||
if (_nearbySyncshells.Count == 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
_nearbySyncshells.Clear();
|
|
||||||
ClearSelection();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ClearSelection()
|
|
||||||
{
|
|
||||||
_selectedNearbyIndex = -1;
|
|
||||||
_syncshellPageIndex = 0;
|
|
||||||
_joinDto = null;
|
|
||||||
_joinInfo = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private string? GetSelectedGid()
|
|
||||||
{
|
|
||||||
if (_selectedNearbyIndex < 0 || _selectedNearbyIndex >= _nearbySyncshells.Count)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return _nearbySyncshells[_selectedNearbyIndex].Group.GID;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -162,24 +162,32 @@ public class TopTabMenu
|
|||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
using (ImRaii.PushFont(UiBuilder.IconFont))
|
using (ImRaii.PushFont(UiBuilder.IconFont))
|
||||||
{
|
{
|
||||||
var x = ImGui.GetCursorScreenPos();
|
|
||||||
if (ImGui.Button(FontAwesomeIcon.Compass.ToIconString(), buttonSize))
|
if (ImGui.Button(FontAwesomeIcon.Compass.ToIconString(), buttonSize))
|
||||||
{
|
{
|
||||||
TabSelection = TabSelection == SelectedTab.Lightfinder ? SelectedTab.None : SelectedTab.Lightfinder;
|
_lightlessMediator.Publish(new UiToggleMessage(typeof(LightFinderUI)));
|
||||||
}
|
}
|
||||||
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenBlockedByActiveItem) || ImGui.IsItemActive())
|
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenBlockedByActiveItem) || ImGui.IsItemActive())
|
||||||
{
|
{
|
||||||
Selune.RegisterHighlight(ImGui.GetItemRectMin(), ImGui.GetItemRectMax(), SeluneHighlightMode.Both, true, buttonBorderThickness, exactSize: true, clipToElement: true, roundingOverride: buttonRounding);
|
Selune.RegisterHighlight(ImGui.GetItemRectMin(), ImGui.GetItemRectMax(), SeluneHighlightMode.Both, true, buttonBorderThickness, exactSize: true, clipToElement: true, roundingOverride: buttonRounding);
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui.SameLine();
|
|
||||||
var xAfter = ImGui.GetCursorScreenPos();
|
|
||||||
if (TabSelection == SelectedTab.Lightfinder)
|
|
||||||
drawList.AddLine(x with { Y = x.Y + buttonSize.Y + spacing.Y },
|
|
||||||
xAfter with { Y = xAfter.Y + buttonSize.Y + spacing.Y, X = xAfter.X - spacing.X },
|
|
||||||
underlineColor, 2);
|
|
||||||
}
|
}
|
||||||
UiSharedService.AttachToolTip("Lightfinder");
|
|
||||||
|
var nearbyCount = GetNearbySyncshellCount();
|
||||||
|
if (nearbyCount > 0)
|
||||||
|
{
|
||||||
|
var buttonMax = ImGui.GetItemRectMax();
|
||||||
|
var badgeRadius = 8f * ImGuiHelpers.GlobalScale;
|
||||||
|
var badgeCenter = new Vector2(buttonMax.X - badgeRadius * 1.3f, buttonMax.Y - buttonSize.Y + badgeRadius * 0.5f);
|
||||||
|
var badgeText = nearbyCount > 99 ? "99+" : nearbyCount.ToString();
|
||||||
|
var textSize = ImGui.CalcTextSize(badgeText);
|
||||||
|
|
||||||
|
drawList.AddCircleFilled(badgeCenter, badgeRadius + 1f, ImGui.GetColorU32(new Vector4(0, 0, 0, 0.6f)));
|
||||||
|
drawList.AddCircleFilled(badgeCenter, badgeRadius, ImGui.GetColorU32(UIColors.Get("LightlessPurple")));
|
||||||
|
|
||||||
|
var textPos = new Vector2(badgeCenter.X - textSize.X * 0.45f, badgeCenter.Y - textSize.Y * 0.55f);
|
||||||
|
drawList.AddText(textPos, ImGui.GetColorU32(new Vector4(1, 1, 1, 1)), badgeText);
|
||||||
|
}
|
||||||
|
UiSharedService.AttachToolTip(nearbyCount > 0 ? $"Lightfinder ({nearbyCount} nearby)" : "Open Lightfinder");
|
||||||
|
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
using (ImRaii.PushFont(UiBuilder.IconFont))
|
using (ImRaii.PushFont(UiBuilder.IconFont))
|
||||||
@@ -234,10 +242,7 @@ public class TopTabMenu
|
|||||||
DrawSyncshellMenu(availableWidth, spacing.X);
|
DrawSyncshellMenu(availableWidth, spacing.X);
|
||||||
DrawGlobalSyncshellButtons(availableWidth, spacing.X);
|
DrawGlobalSyncshellButtons(availableWidth, spacing.X);
|
||||||
}
|
}
|
||||||
else if (TabSelection == SelectedTab.Lightfinder)
|
|
||||||
{
|
|
||||||
DrawLightfinderMenu(availableWidth, spacing.X);
|
|
||||||
}
|
|
||||||
else if (TabSelection == SelectedTab.UserConfig)
|
else if (TabSelection == SelectedTab.UserConfig)
|
||||||
{
|
{
|
||||||
DrawUserConfig(availableWidth, spacing.X);
|
DrawUserConfig(availableWidth, spacing.X);
|
||||||
@@ -776,53 +781,22 @@ public class TopTabMenu
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawLightfinderMenu(float availableWidth, float spacingX)
|
private int GetNearbySyncshellCount()
|
||||||
{
|
|
||||||
var buttonX = (availableWidth - (spacingX)) / 2f;
|
|
||||||
|
|
||||||
var lightFinderLabel = GetLightfinderFinderLabel();
|
|
||||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.PersonCirclePlus, lightFinderLabel, buttonX, center: true))
|
|
||||||
{
|
|
||||||
_lightlessMediator.Publish(new UiToggleMessage(typeof(LightFinderUI)));
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui.SameLine();
|
|
||||||
|
|
||||||
var syncshellFinderLabel = GetSyncshellFinderLabel();
|
|
||||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Globe, syncshellFinderLabel, buttonX, center: true))
|
|
||||||
{
|
|
||||||
_lightlessMediator.Publish(new UiToggleMessage(typeof(SyncshellFinderUI)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private string GetLightfinderFinderLabel()
|
|
||||||
{
|
|
||||||
string label = "Lightfinder";
|
|
||||||
|
|
||||||
if (_lightFinderService.IsBroadcasting)
|
|
||||||
{
|
|
||||||
var hashExclude = _dalamudUtilService.GetCID().ToString().GetHash256();
|
|
||||||
var nearbyCount = _lightFinderScannerService.GetActiveBroadcasts(hashExclude).Count;
|
|
||||||
return $"{label} ({nearbyCount})";
|
|
||||||
}
|
|
||||||
|
|
||||||
return label;
|
|
||||||
}
|
|
||||||
|
|
||||||
private string GetSyncshellFinderLabel()
|
|
||||||
{
|
{
|
||||||
if (!_lightFinderService.IsBroadcasting)
|
if (!_lightFinderService.IsBroadcasting)
|
||||||
return "Syncshell Finder";
|
return 0;
|
||||||
|
|
||||||
var nearbyCount = _lightFinderScannerService
|
var myHashedCid = _dalamudUtilService.GetCID().ToString().GetHash256();
|
||||||
.GetActiveSyncshellBroadcasts(excludeLocal: true)
|
|
||||||
.Where(b => !string.IsNullOrEmpty(b.GID))
|
return _lightFinderScannerService
|
||||||
|
.GetActiveSyncshellBroadcasts()
|
||||||
|
.Where(b =>
|
||||||
|
!string.IsNullOrEmpty(b.GID) &&
|
||||||
|
!string.Equals(b.HashedCID, myHashedCid, StringComparison.Ordinal))
|
||||||
.Select(b => b.GID!)
|
.Select(b => b.GID!)
|
||||||
.Distinct(StringComparer.Ordinal)
|
.Distinct(StringComparer.Ordinal)
|
||||||
.Count();
|
.Count();
|
||||||
|
|
||||||
return nearbyCount > 0 ? $"Syncshell Finder ({nearbyCount})" : "Syncshell Finder";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawUserConfig(float availableWidth, float spacingX)
|
private void DrawUserConfig(float availableWidth, float spacingX)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ namespace LightlessSync.UI
|
|||||||
{
|
{
|
||||||
internal static class UIColors
|
internal static class UIColors
|
||||||
{
|
{
|
||||||
private static readonly Dictionary<string, string> DefaultHexColors = new(StringComparer.OrdinalIgnoreCase)
|
private static readonly Dictionary<string, string> _defaultHexColors = new(StringComparer.OrdinalIgnoreCase)
|
||||||
{
|
{
|
||||||
{ "LightlessPurple", "#ad8af5" },
|
{ "LightlessPurple", "#ad8af5" },
|
||||||
{ "LightlessPurpleActive", "#be9eff" },
|
{ "LightlessPurpleActive", "#be9eff" },
|
||||||
@@ -31,6 +31,12 @@ namespace LightlessSync.UI
|
|||||||
|
|
||||||
{ "ProfileBodyGradientTop", "#2f283fff" },
|
{ "ProfileBodyGradientTop", "#2f283fff" },
|
||||||
{ "ProfileBodyGradientBottom", "#372d4d00" },
|
{ "ProfileBodyGradientBottom", "#372d4d00" },
|
||||||
|
|
||||||
|
{ "HeaderGradientTop", "#140D26FF" },
|
||||||
|
{ "HeaderGradientBottom", "#1F1433FF" },
|
||||||
|
|
||||||
|
{ "HeaderStaticStar", "#FFFFFFFF" },
|
||||||
|
{ "HeaderShootingStar", "#66CCFFFF" },
|
||||||
};
|
};
|
||||||
|
|
||||||
private static LightlessConfigService? _configService;
|
private static LightlessConfigService? _configService;
|
||||||
@@ -45,7 +51,7 @@ namespace LightlessSync.UI
|
|||||||
if (_configService?.Current.CustomUIColors.TryGetValue(name, out var customColorHex) == true)
|
if (_configService?.Current.CustomUIColors.TryGetValue(name, out var customColorHex) == true)
|
||||||
return HexToRgba(customColorHex);
|
return HexToRgba(customColorHex);
|
||||||
|
|
||||||
if (!DefaultHexColors.TryGetValue(name, out var hex))
|
if (!_defaultHexColors.TryGetValue(name, out var hex))
|
||||||
throw new ArgumentException($"Color '{name}' not found in UIColors.", nameof(name));
|
throw new ArgumentException($"Color '{name}' not found in UIColors.", nameof(name));
|
||||||
|
|
||||||
return HexToRgba(hex);
|
return HexToRgba(hex);
|
||||||
@@ -53,7 +59,7 @@ namespace LightlessSync.UI
|
|||||||
|
|
||||||
public static void Set(string name, Vector4 color)
|
public static void Set(string name, Vector4 color)
|
||||||
{
|
{
|
||||||
if (!DefaultHexColors.ContainsKey(name))
|
if (!_defaultHexColors.ContainsKey(name))
|
||||||
throw new ArgumentException($"Color '{name}' not found in UIColors.", nameof(name));
|
throw new ArgumentException($"Color '{name}' not found in UIColors.", nameof(name));
|
||||||
|
|
||||||
if (_configService != null)
|
if (_configService != null)
|
||||||
@@ -83,7 +89,7 @@ namespace LightlessSync.UI
|
|||||||
|
|
||||||
public static Vector4 GetDefault(string name)
|
public static Vector4 GetDefault(string name)
|
||||||
{
|
{
|
||||||
if (!DefaultHexColors.TryGetValue(name, out var hex))
|
if (!_defaultHexColors.TryGetValue(name, out var hex))
|
||||||
throw new ArgumentException($"Color '{name}' not found in UIColors.", nameof(name));
|
throw new ArgumentException($"Color '{name}' not found in UIColors.", nameof(name));
|
||||||
|
|
||||||
return HexToRgba(hex);
|
return HexToRgba(hex);
|
||||||
@@ -96,7 +102,7 @@ namespace LightlessSync.UI
|
|||||||
|
|
||||||
public static IEnumerable<string> GetColorNames()
|
public static IEnumerable<string> GetColorNames()
|
||||||
{
|
{
|
||||||
return DefaultHexColors.Keys;
|
return _defaultHexColors.Keys;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Vector4 HexToRgba(string hexColor)
|
public static Vector4 HexToRgba(string hexColor)
|
||||||
|
|||||||
@@ -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.");
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase
|
|||||||
logger.LogInformation("UpdateNotesUi constructor called");
|
logger.LogInformation("UpdateNotesUi constructor called");
|
||||||
_uiShared = uiShared;
|
_uiShared = uiShared;
|
||||||
_configService = configService;
|
_configService = configService;
|
||||||
|
_animatedHeader.EnableParticles = _configService.Current.EnableParticleEffects;
|
||||||
|
|
||||||
RespectCloseHotkey = true;
|
RespectCloseHotkey = true;
|
||||||
ShowCloseButton = true;
|
ShowCloseButton = true;
|
||||||
@@ -48,7 +49,8 @@ public class UpdateNotesUi : WindowMediatorSubscriberBase
|
|||||||
ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoMove;
|
ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoMove;
|
||||||
|
|
||||||
PositionCondition = ImGuiCond.Always;
|
PositionCondition = ImGuiCond.Always;
|
||||||
|
|
||||||
|
|
||||||
WindowBuilder.For(this)
|
WindowBuilder.For(this)
|
||||||
.AllowPinning(false)
|
.AllowPinning(false)
|
||||||
.AllowClickthrough(false)
|
.AllowClickthrough(false)
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ using LightlessSync.UI.Services;
|
|||||||
using LightlessSync.UI.Style;
|
using LightlessSync.UI.Style;
|
||||||
using LightlessSync.Utils;
|
using LightlessSync.Utils;
|
||||||
using Dalamud.Interface.Textures.TextureWraps;
|
using Dalamud.Interface.Textures.TextureWraps;
|
||||||
using OtterGui.Text;
|
|
||||||
using LightlessSync.WebAPI;
|
using LightlessSync.WebAPI;
|
||||||
using LightlessSync.WebAPI.SignalR.Utils;
|
using LightlessSync.WebAPI.SignalR.Utils;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@@ -205,10 +204,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 +217,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;
|
||||||
@@ -421,150 +428,182 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var itemHeight = ImGui.GetTextLineHeightWithSpacing();
|
var messageCount = channel.Messages.Count;
|
||||||
using var clipper = ImUtf8.ListClipper(channel.Messages.Count, itemHeight);
|
var contentMaxX = ImGui.GetWindowContentRegionMax().X;
|
||||||
while (clipper.Step())
|
var cursorStartX = ImGui.GetCursorPosX();
|
||||||
|
var lineHeightWithSpacing = ImGui.GetTextLineHeightWithSpacing();
|
||||||
|
var prefix = new float[messageCount + 1];
|
||||||
|
var totalHeight = 0f;
|
||||||
|
|
||||||
|
for (var i = 0; i < messageCount; i++)
|
||||||
{
|
{
|
||||||
for (var i = clipper.DisplayStart; i < clipper.DisplayEnd; i++)
|
var messageHeight = MeasureMessageHeight(channel, channel.Messages[i], showTimestamps, cursorStartX, contentMaxX, itemSpacing, ref pairSnapshot);
|
||||||
|
if (messageHeight <= 0f)
|
||||||
{
|
{
|
||||||
var message = channel.Messages[i];
|
messageHeight = lineHeightWithSpacing;
|
||||||
ImGui.PushID(i);
|
}
|
||||||
|
|
||||||
if (message.IsSystem)
|
totalHeight += messageHeight;
|
||||||
|
prefix[i + 1] = totalHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
var scrollY = ImGui.GetScrollY();
|
||||||
|
var windowHeight = ImGui.GetWindowHeight();
|
||||||
|
var startIndex = Math.Max(0, UpperBound(prefix, scrollY) - 1);
|
||||||
|
var endIndex = Math.Min(messageCount, LowerBound(prefix, scrollY + windowHeight));
|
||||||
|
startIndex = Math.Max(0, startIndex - 2);
|
||||||
|
endIndex = Math.Min(messageCount, endIndex + 2);
|
||||||
|
|
||||||
|
if (startIndex > 0)
|
||||||
|
{
|
||||||
|
ImGui.Dummy(new Vector2(1f, prefix[startIndex]));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = startIndex; i < endIndex; i++)
|
||||||
|
{
|
||||||
|
var message = channel.Messages[i];
|
||||||
|
ImGui.PushID(i);
|
||||||
|
|
||||||
|
if (message.IsSystem)
|
||||||
|
{
|
||||||
|
DrawSystemEntry(message);
|
||||||
|
ImGui.PopID();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.Payload is not { } payload)
|
||||||
|
{
|
||||||
|
ImGui.PopID();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var timestampText = string.Empty;
|
||||||
|
if (showTimestamps)
|
||||||
|
{
|
||||||
|
timestampText = $"[{message.ReceivedAtUtc.ToLocalTime().ToString("HH:mm", CultureInfo.InvariantCulture)}] ";
|
||||||
|
}
|
||||||
|
var color = message.FromSelf ? UIColors.Get("LightlessBlue") : ImGuiColors.DalamudWhite;
|
||||||
|
var showRoleIcons = false;
|
||||||
|
var isOwner = false;
|
||||||
|
var isModerator = false;
|
||||||
|
var isPinned = false;
|
||||||
|
|
||||||
|
if (channel.Type == ChatChannelType.Group
|
||||||
|
&& payload.Sender.Kind == ChatSenderKind.IdentifiedUser
|
||||||
|
&& payload.Sender.User is not null)
|
||||||
|
{
|
||||||
|
pairSnapshot ??= _pairUiService.GetSnapshot();
|
||||||
|
var groupId = channel.Descriptor.CustomKey;
|
||||||
|
if (!string.IsNullOrWhiteSpace(groupId)
|
||||||
|
&& pairSnapshot.GroupsByGid.TryGetValue(groupId, out var groupInfo))
|
||||||
{
|
{
|
||||||
DrawSystemEntry(message);
|
var senderUid = payload.Sender.User.UID;
|
||||||
ImGui.PopID();
|
isOwner = string.Equals(senderUid, groupInfo.OwnerUID, StringComparison.Ordinal);
|
||||||
continue;
|
if (groupInfo.GroupPairUserInfos.TryGetValue(senderUid, out var info))
|
||||||
|
{
|
||||||
|
isModerator = info.IsModerator();
|
||||||
|
isPinned = info.IsPinned();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (message.Payload is not { } payload)
|
showRoleIcons = isOwner || isModerator || isPinned;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.BeginGroup();
|
||||||
|
ImGui.PushStyleColor(ImGuiCol.Text, color);
|
||||||
|
if (showRoleIcons)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(timestampText))
|
||||||
{
|
{
|
||||||
ImGui.PopID();
|
ImGui.TextUnformatted(timestampText);
|
||||||
continue;
|
ImGui.SameLine(0f, 0f);
|
||||||
}
|
}
|
||||||
|
|
||||||
var timestampText = string.Empty;
|
var hasIcon = false;
|
||||||
if (showTimestamps)
|
if (isModerator)
|
||||||
{
|
{
|
||||||
timestampText = $"[{message.ReceivedAtUtc.ToLocalTime().ToString("HH:mm", CultureInfo.InvariantCulture)}] ";
|
_uiSharedService.IconText(FontAwesomeIcon.UserShield, UIColors.Get("LightlessPurple"));
|
||||||
}
|
UiSharedService.AttachToolTip("Moderator");
|
||||||
var color = message.FromSelf ? UIColors.Get("LightlessBlue") : ImGuiColors.DalamudWhite;
|
hasIcon = true;
|
||||||
var showRoleIcons = false;
|
|
||||||
var isOwner = false;
|
|
||||||
var isModerator = false;
|
|
||||||
var isPinned = false;
|
|
||||||
|
|
||||||
if (channel.Type == ChatChannelType.Group
|
|
||||||
&& payload.Sender.Kind == ChatSenderKind.IdentifiedUser
|
|
||||||
&& payload.Sender.User is not null)
|
|
||||||
{
|
|
||||||
pairSnapshot ??= _pairUiService.GetSnapshot();
|
|
||||||
var groupId = channel.Descriptor.CustomKey;
|
|
||||||
if (!string.IsNullOrWhiteSpace(groupId)
|
|
||||||
&& pairSnapshot.GroupsByGid.TryGetValue(groupId, out var groupInfo))
|
|
||||||
{
|
|
||||||
var senderUid = payload.Sender.User.UID;
|
|
||||||
isOwner = string.Equals(senderUid, groupInfo.OwnerUID, StringComparison.Ordinal);
|
|
||||||
if (groupInfo.GroupPairUserInfos.TryGetValue(senderUid, out var info))
|
|
||||||
{
|
|
||||||
isModerator = info.IsModerator();
|
|
||||||
isPinned = info.IsPinned();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
showRoleIcons = isOwner || isModerator || isPinned;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui.BeginGroup();
|
if (isOwner)
|
||||||
ImGui.PushStyleColor(ImGuiCol.Text, color);
|
|
||||||
if (showRoleIcons)
|
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrEmpty(timestampText))
|
|
||||||
{
|
|
||||||
ImGui.TextUnformatted(timestampText);
|
|
||||||
ImGui.SameLine(0f, 0f);
|
|
||||||
}
|
|
||||||
|
|
||||||
var hasIcon = false;
|
|
||||||
if (isModerator)
|
|
||||||
{
|
|
||||||
_uiSharedService.IconText(FontAwesomeIcon.UserShield, UIColors.Get("LightlessPurple"));
|
|
||||||
UiSharedService.AttachToolTip("Moderator");
|
|
||||||
hasIcon = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isOwner)
|
|
||||||
{
|
|
||||||
if (hasIcon)
|
|
||||||
{
|
|
||||||
ImGui.SameLine(0f, itemSpacing);
|
|
||||||
}
|
|
||||||
|
|
||||||
_uiSharedService.IconText(FontAwesomeIcon.Crown, UIColors.Get("LightlessYellow"));
|
|
||||||
UiSharedService.AttachToolTip("Owner");
|
|
||||||
hasIcon = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isPinned)
|
|
||||||
{
|
|
||||||
if (hasIcon)
|
|
||||||
{
|
|
||||||
ImGui.SameLine(0f, itemSpacing);
|
|
||||||
}
|
|
||||||
|
|
||||||
_uiSharedService.IconText(FontAwesomeIcon.Thumbtack, UIColors.Get("LightlessBlue"));
|
|
||||||
UiSharedService.AttachToolTip("Pinned");
|
|
||||||
hasIcon = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasIcon)
|
if (hasIcon)
|
||||||
{
|
{
|
||||||
ImGui.SameLine(0f, itemSpacing);
|
ImGui.SameLine(0f, itemSpacing);
|
||||||
}
|
}
|
||||||
|
|
||||||
var messageStartX = ImGui.GetCursorPosX();
|
_uiSharedService.IconText(FontAwesomeIcon.Crown, UIColors.Get("LightlessYellow"));
|
||||||
DrawChatMessageWithEmotes($"{message.DisplayName}: ", payload.Message, messageStartX);
|
UiSharedService.AttachToolTip("Owner");
|
||||||
|
hasIcon = true;
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
var messageStartX = ImGui.GetCursorPosX();
|
|
||||||
DrawChatMessageWithEmotes($"{timestampText}{message.DisplayName}: ", payload.Message, messageStartX);
|
|
||||||
}
|
|
||||||
ImGui.PopStyleColor();
|
|
||||||
ImGui.EndGroup();
|
|
||||||
|
|
||||||
ImGui.SetNextWindowSizeConstraints(
|
if (isPinned)
|
||||||
new Vector2(190f * ImGuiHelpers.GlobalScale, 0f),
|
|
||||||
new Vector2(float.MaxValue, float.MaxValue));
|
|
||||||
if (ImGui.BeginPopupContextItem($"chat_msg_ctx##{channel.Key}_{i}"))
|
|
||||||
{
|
{
|
||||||
var contextLocalTimestamp = payload.SentAtUtc.ToLocalTime();
|
if (hasIcon)
|
||||||
var contextTimestampText = contextLocalTimestamp.ToString("yyyy-MM-dd HH:mm:ss 'UTC'z", CultureInfo.InvariantCulture);
|
|
||||||
ImGui.TextDisabled(contextTimestampText);
|
|
||||||
if (channel.Type == ChatChannelType.Group
|
|
||||||
&& payload.Sender.Kind == ChatSenderKind.IdentifiedUser
|
|
||||||
&& payload.Sender.User is not null)
|
|
||||||
{
|
{
|
||||||
var aliasOrUid = payload.Sender.User.AliasOrUID;
|
ImGui.SameLine(0f, itemSpacing);
|
||||||
if (!string.IsNullOrWhiteSpace(aliasOrUid)
|
|
||||||
&& !string.Equals(message.DisplayName, aliasOrUid, StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
ImGui.TextDisabled(aliasOrUid);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ImGui.Separator();
|
|
||||||
|
|
||||||
var actionIndex = 0;
|
|
||||||
foreach (var action in GetContextMenuActions(channel, message))
|
|
||||||
{
|
|
||||||
DrawContextMenuAction(action, actionIndex++);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui.EndPopup();
|
_uiSharedService.IconText(FontAwesomeIcon.Thumbtack, UIColors.Get("LightlessBlue"));
|
||||||
|
UiSharedService.AttachToolTip("Pinned");
|
||||||
|
hasIcon = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui.PopID();
|
if (hasIcon)
|
||||||
|
{
|
||||||
|
ImGui.SameLine(0f, itemSpacing);
|
||||||
|
}
|
||||||
|
|
||||||
|
var messageStartX = ImGui.GetCursorPosX();
|
||||||
|
DrawChatMessageWithEmotes($"{message.DisplayName}: ", payload.Message, messageStartX);
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var messageStartX = ImGui.GetCursorPosX();
|
||||||
|
DrawChatMessageWithEmotes($"{timestampText}{message.DisplayName}: ", payload.Message, messageStartX);
|
||||||
|
}
|
||||||
|
ImGui.PopStyleColor();
|
||||||
|
ImGui.EndGroup();
|
||||||
|
|
||||||
|
ImGui.SetNextWindowSizeConstraints(
|
||||||
|
new Vector2(190f * ImGuiHelpers.GlobalScale, 0f),
|
||||||
|
new Vector2(float.MaxValue, float.MaxValue));
|
||||||
|
if (ImGui.BeginPopupContextItem($"chat_msg_ctx##{channel.Key}_{i}"))
|
||||||
|
{
|
||||||
|
var contextLocalTimestamp = payload.SentAtUtc.ToLocalTime();
|
||||||
|
var contextTimestampText = contextLocalTimestamp.ToString("yyyy-MM-dd HH:mm:ss 'UTC'z", CultureInfo.InvariantCulture);
|
||||||
|
ImGui.TextDisabled(contextTimestampText);
|
||||||
|
if (channel.Type == ChatChannelType.Group
|
||||||
|
&& payload.Sender.Kind == ChatSenderKind.IdentifiedUser
|
||||||
|
&& payload.Sender.User is not null)
|
||||||
|
{
|
||||||
|
var aliasOrUid = payload.Sender.User.AliasOrUID;
|
||||||
|
if (!string.IsNullOrWhiteSpace(aliasOrUid)
|
||||||
|
&& !string.Equals(message.DisplayName, aliasOrUid, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
ImGui.TextDisabled(aliasOrUid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui.Separator();
|
||||||
|
|
||||||
|
var actionIndex = 0;
|
||||||
|
foreach (var action in GetContextMenuActions(channel, message))
|
||||||
|
{
|
||||||
|
DrawContextMenuAction(action, actionIndex++);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.EndPopup();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.PopID();
|
||||||
|
}
|
||||||
|
|
||||||
|
var remainingHeight = totalHeight - prefix[endIndex];
|
||||||
|
if (remainingHeight > 0f)
|
||||||
|
{
|
||||||
|
ImGui.Dummy(new Vector2(1f, remainingHeight));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -700,7 +739,20 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
|||||||
var clicked = false;
|
var clicked = false;
|
||||||
if (texture is not null)
|
if (texture is not null)
|
||||||
{
|
{
|
||||||
clicked = ImGui.ImageButton(texture.Handle, new Vector2(emoteSize));
|
var buttonSize = new Vector2(itemWidth, itemHeight);
|
||||||
|
clicked = ImGui.InvisibleButton("##emote_button", buttonSize);
|
||||||
|
var drawList = ImGui.GetWindowDrawList();
|
||||||
|
var itemMin = ImGui.GetItemRectMin();
|
||||||
|
var itemMax = ImGui.GetItemRectMax();
|
||||||
|
var bgColor = ImGui.IsItemActive()
|
||||||
|
? ImGui.GetColorU32(ImGuiCol.ButtonActive)
|
||||||
|
: ImGui.IsItemHovered()
|
||||||
|
? ImGui.GetColorU32(ImGuiCol.ButtonHovered)
|
||||||
|
: ImGui.GetColorU32(ImGuiCol.Button);
|
||||||
|
drawList.AddRectFilled(itemMin, itemMax, bgColor, style.FrameRounding);
|
||||||
|
var imageMin = itemMin + style.FramePadding;
|
||||||
|
var imageMax = imageMin + new Vector2(emoteSize);
|
||||||
|
drawList.AddImage(texture.Handle, imageMin, imageMax);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -870,7 +922,232 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
|||||||
|
|
||||||
private static bool IsEmoteChar(char value)
|
private static bool IsEmoteChar(char value)
|
||||||
{
|
{
|
||||||
return char.IsLetterOrDigit(value) || value == '_' || value == '-' || value == '!';
|
return char.IsLetterOrDigit(value) || value == '_' || value == '-' || value == '!' || value == '(' || value == ')';
|
||||||
|
}
|
||||||
|
|
||||||
|
private float MeasureMessageHeight(
|
||||||
|
ChatChannelSnapshot channel,
|
||||||
|
ChatMessageEntry message,
|
||||||
|
bool showTimestamps,
|
||||||
|
float cursorStartX,
|
||||||
|
float contentMaxX,
|
||||||
|
float itemSpacing,
|
||||||
|
ref PairUiSnapshot? pairSnapshot)
|
||||||
|
{
|
||||||
|
if (message.IsSystem)
|
||||||
|
{
|
||||||
|
return MeasureSystemEntryHeight(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.Payload is not { } payload)
|
||||||
|
{
|
||||||
|
return 0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
var timestampText = string.Empty;
|
||||||
|
if (showTimestamps)
|
||||||
|
{
|
||||||
|
timestampText = $"[{message.ReceivedAtUtc.ToLocalTime().ToString("HH:mm", CultureInfo.InvariantCulture)}] ";
|
||||||
|
}
|
||||||
|
|
||||||
|
var showRoleIcons = false;
|
||||||
|
var isOwner = false;
|
||||||
|
var isModerator = false;
|
||||||
|
var isPinned = false;
|
||||||
|
|
||||||
|
if (channel.Type == ChatChannelType.Group
|
||||||
|
&& payload.Sender.Kind == ChatSenderKind.IdentifiedUser
|
||||||
|
&& payload.Sender.User is not null)
|
||||||
|
{
|
||||||
|
pairSnapshot ??= _pairUiService.GetSnapshot();
|
||||||
|
var groupId = channel.Descriptor.CustomKey;
|
||||||
|
if (!string.IsNullOrWhiteSpace(groupId)
|
||||||
|
&& pairSnapshot.GroupsByGid.TryGetValue(groupId, out var groupInfo))
|
||||||
|
{
|
||||||
|
var senderUid = payload.Sender.User.UID;
|
||||||
|
isOwner = string.Equals(senderUid, groupInfo.OwnerUID, StringComparison.Ordinal);
|
||||||
|
if (groupInfo.GroupPairUserInfos.TryGetValue(senderUid, out var info))
|
||||||
|
{
|
||||||
|
isModerator = info.IsModerator();
|
||||||
|
isPinned = info.IsPinned();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showRoleIcons = isOwner || isModerator || isPinned;
|
||||||
|
}
|
||||||
|
|
||||||
|
var lineStartX = cursorStartX;
|
||||||
|
string prefix;
|
||||||
|
if (showRoleIcons)
|
||||||
|
{
|
||||||
|
lineStartX += MeasureRolePrefixWidth(timestampText, isOwner, isModerator, isPinned, itemSpacing);
|
||||||
|
prefix = $"{message.DisplayName}: ";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
prefix = $"{timestampText}{message.DisplayName}: ";
|
||||||
|
}
|
||||||
|
|
||||||
|
var lines = MeasureChatMessageLines(prefix, payload.Message, lineStartX, contentMaxX);
|
||||||
|
return Math.Max(1, lines) * ImGui.GetTextLineHeightWithSpacing();
|
||||||
|
}
|
||||||
|
|
||||||
|
private int MeasureChatMessageLines(string prefix, string message, float lineStartX, float contentMaxX)
|
||||||
|
{
|
||||||
|
var segments = BuildChatSegments(prefix, message);
|
||||||
|
if (segments.Count == 0)
|
||||||
|
{
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
var emoteWidth = ImGui.GetTextLineHeight();
|
||||||
|
var availableWidth = Math.Max(1f, contentMaxX - lineStartX);
|
||||||
|
var remainingWidth = availableWidth;
|
||||||
|
var firstOnLine = true;
|
||||||
|
var lines = 1;
|
||||||
|
|
||||||
|
foreach (var segment in segments)
|
||||||
|
{
|
||||||
|
if (segment.IsLineBreak)
|
||||||
|
{
|
||||||
|
lines++;
|
||||||
|
firstOnLine = true;
|
||||||
|
remainingWidth = availableWidth;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (segment.IsWhitespace && firstOnLine)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var segmentWidth = segment.IsEmote ? emoteWidth : ImGui.CalcTextSize(segment.Text).X;
|
||||||
|
if (!firstOnLine)
|
||||||
|
{
|
||||||
|
if (segmentWidth > remainingWidth)
|
||||||
|
{
|
||||||
|
lines++;
|
||||||
|
firstOnLine = true;
|
||||||
|
remainingWidth = availableWidth;
|
||||||
|
if (segment.IsWhitespace)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
remainingWidth -= segmentWidth;
|
||||||
|
firstOnLine = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
private float MeasureRolePrefixWidth(string timestampText, bool isOwner, bool isModerator, bool isPinned, float itemSpacing)
|
||||||
|
{
|
||||||
|
var width = 0f;
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(timestampText))
|
||||||
|
{
|
||||||
|
width += ImGui.CalcTextSize(timestampText).X;
|
||||||
|
}
|
||||||
|
|
||||||
|
var hasIcon = false;
|
||||||
|
if (isModerator)
|
||||||
|
{
|
||||||
|
width += MeasureIconWidth(FontAwesomeIcon.UserShield);
|
||||||
|
hasIcon = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isOwner)
|
||||||
|
{
|
||||||
|
if (hasIcon)
|
||||||
|
{
|
||||||
|
width += itemSpacing;
|
||||||
|
}
|
||||||
|
|
||||||
|
width += MeasureIconWidth(FontAwesomeIcon.Crown);
|
||||||
|
hasIcon = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPinned)
|
||||||
|
{
|
||||||
|
if (hasIcon)
|
||||||
|
{
|
||||||
|
width += itemSpacing;
|
||||||
|
}
|
||||||
|
|
||||||
|
width += MeasureIconWidth(FontAwesomeIcon.Thumbtack);
|
||||||
|
hasIcon = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasIcon)
|
||||||
|
{
|
||||||
|
width += itemSpacing;
|
||||||
|
}
|
||||||
|
|
||||||
|
return width;
|
||||||
|
}
|
||||||
|
|
||||||
|
private float MeasureIconWidth(FontAwesomeIcon icon)
|
||||||
|
{
|
||||||
|
using var font = _uiSharedService.IconFont.Push();
|
||||||
|
return ImGui.CalcTextSize(icon.ToIconString()).X;
|
||||||
|
}
|
||||||
|
|
||||||
|
private float MeasureSystemEntryHeight(ChatMessageEntry entry)
|
||||||
|
{
|
||||||
|
_ = entry;
|
||||||
|
var spacing = ImGui.GetStyle().ItemSpacing.Y;
|
||||||
|
var lineHeightWithSpacing = ImGui.GetTextLineHeightWithSpacing();
|
||||||
|
var separatorHeight = Math.Max(1f, ImGuiHelpers.GlobalScale);
|
||||||
|
|
||||||
|
var height = spacing;
|
||||||
|
height += lineHeightWithSpacing;
|
||||||
|
height += spacing * 0.35f;
|
||||||
|
height += separatorHeight;
|
||||||
|
height += spacing;
|
||||||
|
return height;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int LowerBound(float[] values, float target)
|
||||||
|
{
|
||||||
|
var low = 0;
|
||||||
|
var high = values.Length;
|
||||||
|
while (low < high)
|
||||||
|
{
|
||||||
|
var mid = (low + high) / 2;
|
||||||
|
if (values[mid] < target)
|
||||||
|
{
|
||||||
|
low = mid + 1;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
high = mid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return low;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int UpperBound(float[] values, float target)
|
||||||
|
{
|
||||||
|
var low = 0;
|
||||||
|
var high = values.Length;
|
||||||
|
while (low < high)
|
||||||
|
{
|
||||||
|
var mid = (low + high) / 2;
|
||||||
|
if (values[mid] <= target)
|
||||||
|
{
|
||||||
|
low = mid + 1;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
high = mid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return low;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawEmoteTooltip(string name, IDalamudTextureWrap? texture)
|
private void DrawEmoteTooltip(string name, IDalamudTextureWrap? texture)
|
||||||
@@ -2084,6 +2361,17 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
|
|||||||
ImGui.SetTooltip("When enabled, your notes replace user names in syncshell chat.");
|
ImGui.SetTooltip("When enabled, your notes replace user names in syncshell chat.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var enableAnimatedEmotes = chatConfig.EnableAnimatedEmotes;
|
||||||
|
if (ImGui.Checkbox("Enable animated emotes", ref enableAnimatedEmotes))
|
||||||
|
{
|
||||||
|
chatConfig.EnableAnimatedEmotes = enableAnimatedEmotes;
|
||||||
|
_chatConfigService.Save();
|
||||||
|
}
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
{
|
||||||
|
ImGui.SetTooltip("When disabled, emotes render as static images.");
|
||||||
|
}
|
||||||
|
|
||||||
ImGui.Separator();
|
ImGui.Separator();
|
||||||
ImGui.TextUnformatted("Chat Visibility");
|
ImGui.TextUnformatted("Chat Visibility");
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|||||||
@@ -200,5 +200,21 @@ public partial class ApiController
|
|||||||
|
|
||||||
await UserPushData(new(visibleCharacters, character, censusDto)).ConfigureAwait(false);
|
await UserPushData(new(visibleCharacters, character, censusDto)).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task UpdateLocation(LocationDto locationDto, bool offline = false)
|
||||||
|
{
|
||||||
|
if (!IsConnected) return;
|
||||||
|
await _lightlessHub!.SendAsync(nameof(UpdateLocation), locationDto, offline).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
public async Task<(List<LocationWithTimeDto>, List<SharingStatusDto>)> RequestAllLocationInfo()
|
||||||
|
{
|
||||||
|
if (!IsConnected) return ([],[]);
|
||||||
|
return await _lightlessHub!.InvokeAsync<(List<LocationWithTimeDto>, List<SharingStatusDto>)>(nameof(RequestAllLocationInfo)).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
public async Task<bool> ToggleLocationSharing(LocationSharingToggleDto dto)
|
||||||
|
{
|
||||||
|
if (!IsConnected) return false;
|
||||||
|
return await _lightlessHub!.InvokeAsync<bool>(nameof(ToggleLocationSharing), dto).ConfigureAwait(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
#pragma warning restore MA0040
|
#pragma warning restore MA0040
|
||||||
@@ -259,6 +259,13 @@ public partial class ApiController
|
|||||||
ExecuteSafely(() => Mediator.Publish(new GPoseLobbyReceiveWorldData(userData, worldData)));
|
ExecuteSafely(() => Mediator.Publish(new GPoseLobbyReceiveWorldData(userData, worldData)));
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Task Client_SendLocationToClient(LocationDto locationDto, DateTimeOffset expireAt)
|
||||||
|
{
|
||||||
|
Logger.LogDebug($"{nameof(Client_SendLocationToClient)}: {locationDto.User} {expireAt}");
|
||||||
|
ExecuteSafely(() => Mediator.Publish(new LocationSharingMessage(locationDto.User, locationDto.Location, expireAt)));
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
public void OnDownloadReady(Action<Guid> act)
|
public void OnDownloadReady(Action<Guid> act)
|
||||||
{
|
{
|
||||||
@@ -441,6 +448,12 @@ public partial class ApiController
|
|||||||
_lightlessHub!.On(nameof(Client_GposeLobbyPushWorldData), act);
|
_lightlessHub!.On(nameof(Client_GposeLobbyPushWorldData), act);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void OnReceiveLocation(Action<LocationDto, DateTimeOffset> act)
|
||||||
|
{
|
||||||
|
if (_initialized) return;
|
||||||
|
_lightlessHub!.On(nameof(Client_SendLocationToClient), act);
|
||||||
|
}
|
||||||
|
|
||||||
private void ExecuteSafely(Action act)
|
private void ExecuteSafely(Action act)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|||||||
@@ -606,6 +606,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
|
|||||||
OnGposeLobbyPushCharacterData((dto) => _ = Client_GposeLobbyPushCharacterData(dto));
|
OnGposeLobbyPushCharacterData((dto) => _ = Client_GposeLobbyPushCharacterData(dto));
|
||||||
OnGposeLobbyPushPoseData((dto, data) => _ = Client_GposeLobbyPushPoseData(dto, data));
|
OnGposeLobbyPushPoseData((dto, data) => _ = Client_GposeLobbyPushPoseData(dto, data));
|
||||||
OnGposeLobbyPushWorldData((dto, data) => _ = Client_GposeLobbyPushWorldData(dto, data));
|
OnGposeLobbyPushWorldData((dto, data) => _ = Client_GposeLobbyPushWorldData(dto, data));
|
||||||
|
OnReceiveLocation((dto, time) => _ = Client_SendLocationToClient(dto, time));
|
||||||
|
|
||||||
_healthCheckTokenSource?.Cancel();
|
_healthCheckTokenSource?.Cancel();
|
||||||
_healthCheckTokenSource?.Dispose();
|
_healthCheckTokenSource?.Dispose();
|
||||||
@@ -774,5 +775,6 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
|
|||||||
|
|
||||||
ServerState = state;
|
ServerState = state;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
#pragma warning restore MA0040
|
#pragma warning restore MA0040
|
||||||
|
|||||||
@@ -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