Compare commits
72 Commits
detached
...
just-exper
| Author | SHA1 | Date | |
|---|---|---|---|
| 17dd8a307b | |||
| 24d0c38f59 | |||
| 4da2548e03 | |||
| b6b9c81a57 | |||
| a824d94ffe | |||
| 4b13dfe8d4 | |||
| 979443d9bb | |||
| 92cb861710 | |||
| aeed8503c2 | |||
| 44bb53023e | |||
| 05b91ed243 | |||
| 7e61954541 | |||
| cfc5c1e0f3 | |||
| bbb3375661 | |||
| ed7932ab83 | |||
| 4eaaaf694c | |||
|
|
c32c89d1a8 | ||
| a8b58d05d6 | |||
| 9ea0571e82 | |||
|
|
308c220735 | ||
|
|
27d4da4615 | ||
|
|
6b49c92ef9 | ||
|
|
6d20995dbf | ||
|
|
cf495dc826 | ||
|
|
08050614da | ||
| 94f520d0e7 | |||
| 474fd5ef11 | |||
|
|
759066731e | ||
|
|
ff88e5f856 | ||
|
|
61bac0d39d | ||
| 5b3d00b90a | |||
| e14d50674d | |||
| 129cf14151 | |||
|
|
dba04d740b | ||
| 04d7a66317 | |||
|
|
2abc92fc61 | ||
|
|
a3ea48c6e1 | ||
|
|
deb99628f6 | ||
| f69effb8a3 | |||
| 8f32b375dd | |||
| 1632258c4f | |||
| a5786e1d5b | |||
| 0b32639f99 | |||
| 65dea18f5f | |||
| 6546a658f3 | |||
| 8a41baa88b | |||
| 88cb778791 | |||
|
|
6892d81041 | ||
|
|
a47ca4452a | ||
|
|
32df21bf4a | ||
|
|
1a2885fd74 | ||
| e470222fe6 | |||
|
|
eb83ca90cb | ||
|
|
35f0f6da5e | ||
| 7d151dac2b | |||
|
|
2eba5a1f30 | ||
| 0a6cb05883 | |||
|
|
838495810e | ||
| a207c8994b | |||
|
|
9b4e48ad3e | ||
| fb4810980e | |||
| 51e107d30a | |||
|
|
cc011743af | ||
|
|
f47fbda0d9 | ||
| fd3b42eff1 | |||
| 3262664d1c | |||
| afa0d9f101 | |||
|
|
a66a9407f5 | ||
|
|
34bbc34b5b | ||
|
|
be068ed6d1 | ||
|
|
3c3c8fd90b | ||
| 835a0a637d |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -348,3 +348,6 @@ MigrationBackup/
|
||||
|
||||
# Ionide (cross platform F# VS Code tools) working folder
|
||||
.ionide/
|
||||
|
||||
# idea
|
||||
/.idea
|
||||
|
||||
Submodule LightlessAPI updated: 8e4432af45...4ecd5375e6
@@ -1,11 +1,72 @@
|
||||
tagline: "Lightless Sync v2.0.0"
|
||||
tagline: "Lightless Sync v2.0.1"
|
||||
subline: "LIGHTLESS IS EVOLVING!!"
|
||||
changelog:
|
||||
- name: "v2.0.2"
|
||||
tagline: "Last update of 2025!... ... ... If Nothing breaks"
|
||||
date: "December 28 2025"
|
||||
# be sure to set this every new version
|
||||
isCurrent: true
|
||||
versions:
|
||||
- number: "Chat"
|
||||
icon: ""
|
||||
items:
|
||||
- "Added a 7TV emote picker to chat. You’ll now see a new button next to Send that opens an emote selector."
|
||||
- "Pin User, Remove User, and Ban User (including Syncshell) have been added when you right click a user in chat."
|
||||
- "Chatters now show status icons/labels in the Syncshell (e.g., Owner, Moderator, and Pinned when applicable)."
|
||||
- "The Rules page no longer blocks input for other open Lightless UI windows."
|
||||
- number: "LightFinder"
|
||||
icon: ""
|
||||
items:
|
||||
- "If the ImGui Lightfinder icons aren’t working correctly, you can switch back to the Nameplate signature hook. Important warning - USE AT YOUR OWN RISK: The native nameplate hook can crash the game if multiple plugins hook the nameplate function at the same time. We will not provide support about Nameplate crashes, nor will the Dalamud team, **DO NOT BOTHER THEM.**"
|
||||
- "The LightFinder label in the menu has a counter next to it showing the number of broadcasting users."
|
||||
- "There is less interference of hidden UI elements for the imGui renderer of LightFinder."
|
||||
- number: "Miscellaneous fixes"
|
||||
icon: ""
|
||||
items:
|
||||
- "Overhauled transient resources in an attempt to mitigate mount and minion problems."
|
||||
- "Some file cache entries will now be cached to reduce load on your game."
|
||||
- "Downloading and decompressing have been redone to fix the locking issues."
|
||||
- "Disabling the context menu will now hide the context menu on right clicks again. (Thanks @infiniti)"
|
||||
- "Temporary collections that were not cleared before will now be cleared when the plugin starts."
|
||||
- "Pair requests will now appear in chat if notifications are not enabled or on chat mode."
|
||||
- "Fixed an instance were an object may be null in the Download UI."
|
||||
- "API 14 - Migrate to IPlayerState service"
|
||||
- name: "v2.0.1"
|
||||
tagline: "Some Fixes"
|
||||
date: "December 23 2025"
|
||||
versions:
|
||||
- number: "Chat"
|
||||
icon: ""
|
||||
items:
|
||||
- "You can turn off the syncshell chat as Owner by going to the Syncshell Admin panel -> Owner -> Enable/Disable Chat."
|
||||
- "Fixed an issue where you can't chat due to regions being in a different language."
|
||||
- number: "LightFinder"
|
||||
icon: ""
|
||||
items:
|
||||
- "The icon/Lightfinder Text will be hidden when Game UI is hidden and behind game elements/UI"
|
||||
- "Able to select an icon for the selected list or a custom glyph if you know the code."
|
||||
- "Smoothing and reducing jitter on the icon/Lightfinder Text."
|
||||
- "Fixed so higher scaled UI options (100/150/200% UI scale) wouldn't break the element."
|
||||
- "Detects if GPose is active, wouldn't render the elements"
|
||||
- number: "Miscellaneous fixes"
|
||||
icon: ""
|
||||
items:
|
||||
- "Fixed the null error given on GetCID when transferring between zones/housing."
|
||||
- "Added push/pop on certain ImGUI elements to remove them after being used. "
|
||||
- "Having all tabs open in the Main UI wouldn't lag out the game anymore."
|
||||
- "Cycle pause has been adjusted to the old function. There is a separate button to pause normally, now called 'Toggle (Un)Pause State'."
|
||||
- "Changes have been made to the character redraw to address the issues with the building character data constantly being redrawn and the redrawn behavior with Honorific titles."
|
||||
- "GPose characters should appear again in the actor screen"
|
||||
- "Lightspeed download console messages are no longer shown as warnings."
|
||||
- number: "Server Updates"
|
||||
icon: ""
|
||||
items:
|
||||
- "Changes have been made to the disabling of your profile. It should save again."
|
||||
- "Ability added to toggle chats from syncshell to be disabled."
|
||||
- "Files are continuously being deleted due to high volumes in storage, potentially causing MCDOs to have missing files. We have increased the limit of the storage in our configurations to see if that helps."
|
||||
- name: "v2.0.0"
|
||||
tagline: "Thank you for 4 months!"
|
||||
date: "December 2025"
|
||||
# be sure to set this every new version
|
||||
isCurrent: true
|
||||
versions:
|
||||
- number: "Lightless Chat"
|
||||
icon: ""
|
||||
|
||||
@@ -6,6 +6,7 @@ using LightlessSync.Utils;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Immutable;
|
||||
using System.IO;
|
||||
|
||||
namespace LightlessSync.FileCache;
|
||||
|
||||
@@ -21,6 +22,7 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
||||
private CancellationTokenSource _scanCancellationTokenSource = new();
|
||||
private readonly CancellationTokenSource _periodicCalculationTokenSource = new();
|
||||
public static readonly IImmutableList<string> AllowedFileExtensions = [".mdl", ".tex", ".mtrl", ".tmb", ".pap", ".avfx", ".atex", ".sklb", ".eid", ".phyb", ".pbd", ".scd", ".skp", ".shpk", ".kdb"];
|
||||
private static readonly HashSet<string> AllowedFileExtensionSet = new(AllowedFileExtensions, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public CacheMonitor(ILogger<CacheMonitor> logger, IpcManager ipcManager, LightlessConfigService configService,
|
||||
FileCacheManager fileDbManager, LightlessMediator mediator, PerformanceCollectorService performanceCollector, DalamudUtilService dalamudUtil,
|
||||
@@ -101,6 +103,7 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
||||
}
|
||||
|
||||
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> _lightlessChanges = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
@@ -163,7 +166,7 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
||||
{
|
||||
Logger.LogTrace("Lightless FSW: FileChanged: {change} => {path}", e.ChangeType, e.FullPath);
|
||||
|
||||
if (!AllowedFileExtensions.Any(ext => e.FullPath.EndsWith(ext, StringComparison.OrdinalIgnoreCase))) return;
|
||||
if (!HasAllowedExtension(e.FullPath)) return;
|
||||
|
||||
lock (_watcherChanges)
|
||||
{
|
||||
@@ -207,7 +210,7 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
||||
private void Fs_Changed(object sender, FileSystemEventArgs e)
|
||||
{
|
||||
if (Directory.Exists(e.FullPath)) return;
|
||||
if (!AllowedFileExtensions.Any(ext => e.FullPath.EndsWith(ext, StringComparison.OrdinalIgnoreCase))) return;
|
||||
if (!HasAllowedExtension(e.FullPath)) return;
|
||||
|
||||
if (e.ChangeType is not (WatcherChangeTypes.Changed or WatcherChangeTypes.Deleted or WatcherChangeTypes.Created))
|
||||
return;
|
||||
@@ -231,7 +234,7 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
||||
{
|
||||
foreach (var file in directoryFiles)
|
||||
{
|
||||
if (!AllowedFileExtensions.Any(ext => file.EndsWith(ext, StringComparison.OrdinalIgnoreCase))) continue;
|
||||
if (!HasAllowedExtension(file)) continue;
|
||||
var oldPath = file.Replace(e.FullPath, e.OldFullPath, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
_watcherChanges.Remove(oldPath);
|
||||
@@ -243,7 +246,7 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!AllowedFileExtensions.Any(ext => e.FullPath.EndsWith(ext, StringComparison.OrdinalIgnoreCase))) return;
|
||||
if (!HasAllowedExtension(e.FullPath)) return;
|
||||
|
||||
lock (_watcherChanges)
|
||||
{
|
||||
@@ -263,6 +266,17 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
||||
public FileSystemWatcher? PenumbraWatcher { get; private set; }
|
||||
public FileSystemWatcher? LightlessWatcher { get; private set; }
|
||||
|
||||
private static bool HasAllowedExtension(string path)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var extension = Path.GetExtension(path);
|
||||
return !string.IsNullOrEmpty(extension) && AllowedFileExtensionSet.Contains(extension);
|
||||
}
|
||||
|
||||
private async Task LightlessWatcherExecution()
|
||||
{
|
||||
_lightlessFswCts = _lightlessFswCts.CancelRecreate();
|
||||
@@ -428,116 +442,40 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
||||
Logger.LogWarning(ex, "Could not determine drive size for storage folder {folder}", _configService.Current.CacheFolder);
|
||||
}
|
||||
|
||||
var files = Directory.EnumerateFiles(_configService.Current.CacheFolder)
|
||||
.Select(f => new FileInfo(f))
|
||||
.OrderBy(f => f.LastAccessTime)
|
||||
.ToList();
|
||||
|
||||
var cacheFolder = _configService.Current.CacheFolder;
|
||||
var candidates = new List<CacheEvictionCandidate>();
|
||||
long totalSize = 0;
|
||||
|
||||
foreach (var f in files)
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
totalSize += AddFolderCandidates(cacheFolder, candidates, token, isWine);
|
||||
totalSize += AddFolderCandidates(Path.Combine(cacheFolder, "downscaled"), candidates, token, isWine);
|
||||
totalSize += AddFolderCandidates(Path.Combine(cacheFolder, "decimated"), candidates, token, isWine);
|
||||
|
||||
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);
|
||||
if (FileCacheSize < maxCacheInBytes)
|
||||
return;
|
||||
|
||||
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
|
||||
{
|
||||
long fileSize = oldestFile.Length;
|
||||
File.Delete(oldestFile.FullName);
|
||||
FileCacheSize -= fileSize;
|
||||
EvictCacheCandidate(oldestFile, cacheFolder);
|
||||
FileCacheSize -= oldestFile.Size;
|
||||
}
|
||||
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++;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -546,6 +484,114 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
||||
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)
|
||||
{
|
||||
if (!HaltScanLocks.ContainsKey(source)) HaltScanLocks[source] = 0;
|
||||
@@ -606,7 +652,7 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
|
||||
[
|
||||
.. Directory.GetFiles(folder, "*.*", SearchOption.AllDirectories)
|
||||
.AsParallel()
|
||||
.Where(f => AllowedFileExtensions.Any(e => f.EndsWith(e, StringComparison.OrdinalIgnoreCase))
|
||||
.Where(f => HasAllowedExtension(f)
|
||||
&& !f.Contains(@"\bg\", StringComparison.OrdinalIgnoreCase)
|
||||
&& !f.Contains(@"\bgcommon\", StringComparison.OrdinalIgnoreCase)
|
||||
&& !f.Contains(@"\ui\", StringComparison.OrdinalIgnoreCase)),
|
||||
|
||||
@@ -1,15 +1,23 @@
|
||||
#nullable disable
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace LightlessSync.FileCache;
|
||||
|
||||
public class FileCacheEntity
|
||||
{
|
||||
public FileCacheEntity(string hash, string path, string lastModifiedDateTicks, long? size = null, long? compressedSize = null)
|
||||
[JsonConstructor]
|
||||
public FileCacheEntity(
|
||||
string hash,
|
||||
string prefixedFilePath,
|
||||
string lastModifiedDateTicks,
|
||||
long? size = null,
|
||||
long? compressedSize = null)
|
||||
{
|
||||
Size = size;
|
||||
CompressedSize = compressedSize;
|
||||
Hash = hash;
|
||||
PrefixedFilePath = path;
|
||||
PrefixedFilePath = prefixedFilePath;
|
||||
LastModifiedDateTicks = lastModifiedDateTicks;
|
||||
}
|
||||
|
||||
@@ -23,7 +31,5 @@ public class FileCacheEntity
|
||||
public long? Size { get; set; }
|
||||
|
||||
public void SetResolvedFilePath(string filePath)
|
||||
{
|
||||
ResolvedFilepath = filePath.ToLowerInvariant().Replace("\\\\", "\\", StringComparison.Ordinal);
|
||||
}
|
||||
=> ResolvedFilepath = filePath.ToLowerInvariant().Replace("\\\\", "\\", StringComparison.Ordinal);
|
||||
}
|
||||
@@ -7,6 +7,8 @@ using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
namespace LightlessSync.FileCache;
|
||||
@@ -31,6 +33,14 @@ public sealed class FileCacheManager : IHostedService
|
||||
private bool _csvHeaderEnsured;
|
||||
public string CacheFolder => _configService.Current.CacheFolder;
|
||||
|
||||
private const string _compressedCacheExtension = ".llz4";
|
||||
private readonly ConcurrentDictionary<string, SemaphoreSlim> _compressLocks = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly ConcurrentDictionary<string, SizeInfo> _sizeCache =
|
||||
new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
[StructLayout(LayoutKind.Auto)]
|
||||
public readonly record struct SizeInfo(long Original, long Compressed);
|
||||
|
||||
public FileCacheManager(ILogger<FileCacheManager> logger, IpcManager ipcManager, LightlessConfigService configService, LightlessMediator lightlessMediator)
|
||||
{
|
||||
_logger = logger;
|
||||
@@ -45,6 +55,18 @@ public sealed class FileCacheManager : IHostedService
|
||||
private static string NormalizeSeparators(string path) => path.Replace("/", "\\", StringComparison.Ordinal)
|
||||
.Replace("\\\\", "\\", StringComparison.Ordinal);
|
||||
|
||||
private SemaphoreSlim GetCompressLock(string hash)
|
||||
=> _compressLocks.GetOrAdd(hash, _ => new SemaphoreSlim(1, 1));
|
||||
|
||||
public void SetSizeInfo(string hash, long original, long compressed)
|
||||
=> _sizeCache[hash] = new SizeInfo(original, compressed);
|
||||
|
||||
public bool TryGetSizeInfo(string hash, out SizeInfo info)
|
||||
=> _sizeCache.TryGetValue(hash, out info);
|
||||
|
||||
private string GetCompressedCachePath(string hash)
|
||||
=> Path.Combine(CacheFolder, hash + _compressedCacheExtension);
|
||||
|
||||
private static string NormalizePrefixedPathKey(string prefixedPath)
|
||||
{
|
||||
if (string.IsNullOrEmpty(prefixedPath))
|
||||
@@ -111,6 +133,114 @@ public sealed class FileCacheManager : IHostedService
|
||||
return int.TryParse(versionSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out version);
|
||||
}
|
||||
|
||||
public void UpdateSizeInfo(string hash, long? original = null, long? compressed = null)
|
||||
{
|
||||
_sizeCache.AddOrUpdate(
|
||||
hash,
|
||||
_ => new SizeInfo(original ?? 0, compressed ?? 0),
|
||||
(_, old) => new SizeInfo(original ?? old.Original, compressed ?? old.Compressed));
|
||||
}
|
||||
|
||||
private void UpdateEntitiesSizes(string hash, long original, long compressed)
|
||||
{
|
||||
if (_fileCaches.TryGetValue(hash, out var dict))
|
||||
{
|
||||
foreach (var e in dict.Values)
|
||||
{
|
||||
e.Size = original;
|
||||
e.CompressedSize = compressed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void ApplySizesToEntries(IEnumerable<FileCacheEntity?> entries, long original, long compressed)
|
||||
{
|
||||
foreach (var e in entries)
|
||||
{
|
||||
if (e == null) continue;
|
||||
e.Size = original;
|
||||
e.CompressedSize = compressed > 0 ? compressed : null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<long> GetCompressedSizeAsync(string hash, CancellationToken token)
|
||||
{
|
||||
if (_sizeCache.TryGetValue(hash, out var info) && info.Compressed > 0)
|
||||
return info.Compressed;
|
||||
|
||||
if (_fileCaches.TryGetValue(hash, out var dict))
|
||||
{
|
||||
var any = dict.Values.FirstOrDefault();
|
||||
if (any != null && any.CompressedSize > 0)
|
||||
{
|
||||
UpdateSizeInfo(hash, original: any.Size > 0 ? any.Size : null, compressed: any.CompressedSize);
|
||||
return (long)any.CompressedSize;
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(CacheFolder))
|
||||
{
|
||||
var path = GetCompressedCachePath(hash);
|
||||
if (File.Exists(path))
|
||||
{
|
||||
var len = new FileInfo(path).Length;
|
||||
UpdateSizeInfo(hash, compressed: len);
|
||||
return len;
|
||||
}
|
||||
|
||||
var bytes = await EnsureCompressedCacheBytesAsync(hash, token).ConfigureAwait(false);
|
||||
return bytes.LongLength;
|
||||
}
|
||||
|
||||
var fallback = await GetCompressedFileData(hash, token).ConfigureAwait(false);
|
||||
return fallback.Item2.LongLength;
|
||||
}
|
||||
|
||||
private async Task<byte[]> EnsureCompressedCacheBytesAsync(string hash, CancellationToken token)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(CacheFolder))
|
||||
throw new InvalidOperationException("CacheFolder is not set; cannot persist compressed cache.");
|
||||
|
||||
Directory.CreateDirectory(CacheFolder);
|
||||
|
||||
var compressedPath = GetCompressedCachePath(hash);
|
||||
|
||||
if (File.Exists(compressedPath))
|
||||
return await File.ReadAllBytesAsync(compressedPath, token).ConfigureAwait(false);
|
||||
|
||||
var sem = GetCompressLock(hash);
|
||||
await sem.WaitAsync(token).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
if (File.Exists(compressedPath))
|
||||
return await File.ReadAllBytesAsync(compressedPath, token).ConfigureAwait(false);
|
||||
|
||||
var entity = GetFileCacheByHash(hash);
|
||||
if (entity == null || string.IsNullOrWhiteSpace(entity.ResolvedFilepath))
|
||||
throw new InvalidOperationException($"No local file cache found for hash {hash}.");
|
||||
|
||||
var sourcePath = entity.ResolvedFilepath;
|
||||
var originalSize = new FileInfo(sourcePath).Length;
|
||||
|
||||
var raw = await File.ReadAllBytesAsync(sourcePath, token).ConfigureAwait(false);
|
||||
var compressed = LZ4Wrapper.WrapHC(raw, 0, raw.Length);
|
||||
|
||||
var tmpPath = compressedPath + ".tmp";
|
||||
await File.WriteAllBytesAsync(tmpPath, compressed, token).ConfigureAwait(false);
|
||||
File.Move(tmpPath, compressedPath, overwrite: true);
|
||||
|
||||
var compressedSize = compressed.LongLength;
|
||||
SetSizeInfo(hash, originalSize, compressedSize);
|
||||
UpdateEntitiesSizes(hash, originalSize, compressedSize);
|
||||
|
||||
return compressed;
|
||||
}
|
||||
finally
|
||||
{
|
||||
sem.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private string NormalizeToPrefixedPath(string path)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path)) return string.Empty;
|
||||
@@ -150,6 +280,26 @@ public sealed class FileCacheManager : IHostedService
|
||||
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)
|
||||
{
|
||||
FileInfo fi = new(path);
|
||||
@@ -318,9 +468,18 @@ public sealed class FileCacheManager : IHostedService
|
||||
|
||||
public async Task<(string, byte[])> GetCompressedFileData(string fileHash, CancellationToken uploadToken)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(CacheFolder))
|
||||
{
|
||||
var bytes = await EnsureCompressedCacheBytesAsync(fileHash, uploadToken).ConfigureAwait(false);
|
||||
UpdateSizeInfo(fileHash, compressed: bytes.LongLength);
|
||||
return (fileHash, bytes);
|
||||
}
|
||||
|
||||
var fileCache = GetFileCacheByHash(fileHash)!.ResolvedFilepath;
|
||||
return (fileHash, LZ4Wrapper.WrapHC(await File.ReadAllBytesAsync(fileCache, uploadToken).ConfigureAwait(false), 0,
|
||||
(int)new FileInfo(fileCache).Length));
|
||||
var raw = await File.ReadAllBytesAsync(fileCache, uploadToken).ConfigureAwait(false);
|
||||
var compressed = LZ4Wrapper.WrapHC(raw, 0, raw.Length);
|
||||
UpdateSizeInfo(fileHash, original: raw.LongLength, compressed: compressed.LongLength);
|
||||
return (fileHash, compressed);
|
||||
}
|
||||
|
||||
public FileCacheEntity? GetFileCacheByHash(string hash)
|
||||
@@ -423,9 +582,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 removedHash = false;
|
||||
|
||||
if (_fileCaches.TryGetValue(hash, out var caches))
|
||||
{
|
||||
@@ -438,11 +598,16 @@ public sealed class FileCacheManager : IHostedService
|
||||
|
||||
if (caches.IsEmpty)
|
||||
{
|
||||
_fileCaches.TryRemove(hash, out _);
|
||||
removedHash = _fileCaches.TryRemove(hash, out _);
|
||||
}
|
||||
}
|
||||
|
||||
_fileCachesByPrefixedPath.TryRemove(normalizedPath, out _);
|
||||
|
||||
if (removeDerivedFiles && removedHash)
|
||||
{
|
||||
RemoveDerivedCacheFiles(hash);
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateHashedFile(FileCacheEntity fileCache, bool computeProperties = true)
|
||||
@@ -458,7 +623,8 @@ public sealed class FileCacheManager : IHostedService
|
||||
fileCache.Hash = Crypto.ComputeFileHash(fileCache.ResolvedFilepath, Crypto.HashAlgo.Sha1);
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -608,7 +774,7 @@ public sealed class FileCacheManager : IHostedService
|
||||
{
|
||||
try
|
||||
{
|
||||
RemoveHashedFile(fileCache.Hash, fileCache.PrefixedFilePath);
|
||||
RemoveHashedFile(fileCache.Hash, fileCache.PrefixedFilePath, removeDerivedFiles: false);
|
||||
var extensionPath = fileCache.ResolvedFilepath.ToUpper(CultureInfo.InvariantCulture) + "." + ext;
|
||||
File.Move(fileCache.ResolvedFilepath, extensionPath, overwrite: true);
|
||||
var newHashedEntity = new FileCacheEntity(fileCache.Hash, fileCache.PrefixedFilePath + "." + ext, DateTime.UtcNow.Ticks.ToString(CultureInfo.InvariantCulture));
|
||||
@@ -625,6 +791,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)
|
||||
{
|
||||
var normalizedPath = NormalizePrefixedPathKey(fileCache.PrefixedFilePath);
|
||||
@@ -891,6 +1084,14 @@ public sealed class FileCacheManager : IHostedService
|
||||
compressed = resultCompressed;
|
||||
}
|
||||
}
|
||||
|
||||
if (size > 0 || compressed > 0)
|
||||
{
|
||||
UpdateSizeInfo(hash,
|
||||
original: size > 0 ? size : null,
|
||||
compressed: compressed > 0 ? compressed : null);
|
||||
}
|
||||
|
||||
AddHashedFile(ReplacePathPrefixes(new FileCacheEntity(hash, path, time, size, compressed)));
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
@@ -10,9 +10,6 @@ using LightlessSync.Services.Mediator;
|
||||
using LightlessSync.Utils;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using System.Linq;
|
||||
using DalamudObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind;
|
||||
|
||||
namespace LightlessSync.FileCache;
|
||||
@@ -28,7 +25,10 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
|
||||
private readonly object _ownedHandlerLock = new();
|
||||
private readonly string[] _handledFileTypes = ["tmb", "pap", "avfx", "atex", "sklb", "eid", "phyb", "scd", "skp", "shpk", "kdb"];
|
||||
private readonly string[] _handledRecordingFileTypes = ["tex", "mdl", "mtrl"];
|
||||
private readonly string[] _handledFileTypesWithRecording;
|
||||
private readonly HashSet<GameObjectHandler> _playerRelatedPointers = [];
|
||||
private readonly object _playerRelatedLock = new();
|
||||
private readonly ConcurrentDictionary<nint, GameObjectHandler> _playerRelatedByAddress = new();
|
||||
private readonly Dictionary<nint, GameObjectHandler> _ownedHandlers = new();
|
||||
private ConcurrentDictionary<nint, ObjectKind> _cachedFrameAddresses = new();
|
||||
private ConcurrentDictionary<ObjectKind, HashSet<string>>? _semiTransientResources = null;
|
||||
@@ -42,6 +42,7 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
|
||||
_dalamudUtil = dalamudUtil;
|
||||
_actorObjectService = actorObjectService;
|
||||
_gameObjectHandlerFactory = gameObjectHandlerFactory;
|
||||
_handledFileTypesWithRecording = _handledRecordingFileTypes.Concat(_handledFileTypes).ToArray();
|
||||
|
||||
Mediator.Subscribe<PenumbraResourceLoadMessage>(this, Manager_PenumbraResourceLoadEvent);
|
||||
Mediator.Subscribe<ActorTrackedMessage>(this, msg => HandleActorTracked(msg.Descriptor));
|
||||
@@ -51,15 +52,21 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
|
||||
Mediator.Subscribe<GameObjectHandlerCreatedMessage>(this, (msg) =>
|
||||
{
|
||||
if (!msg.OwnedObject) return;
|
||||
_playerRelatedPointers.Add(msg.GameObjectHandler);
|
||||
lock (_playerRelatedLock)
|
||||
{
|
||||
_playerRelatedPointers.Add(msg.GameObjectHandler);
|
||||
}
|
||||
});
|
||||
Mediator.Subscribe<GameObjectHandlerDestroyedMessage>(this, (msg) =>
|
||||
{
|
||||
if (!msg.OwnedObject) return;
|
||||
_playerRelatedPointers.Remove(msg.GameObjectHandler);
|
||||
lock (_playerRelatedLock)
|
||||
{
|
||||
_playerRelatedPointers.Remove(msg.GameObjectHandler);
|
||||
}
|
||||
});
|
||||
|
||||
foreach (var descriptor in _actorObjectService.PlayerDescriptors)
|
||||
foreach (var descriptor in _actorObjectService.ObjectDescriptors)
|
||||
{
|
||||
HandleActorTracked(descriptor);
|
||||
}
|
||||
@@ -78,7 +85,7 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
|
||||
}
|
||||
}
|
||||
|
||||
private string PlayerPersistentDataKey => _dalamudUtil.GetPlayerNameAsync().GetAwaiter().GetResult() + "_" + _dalamudUtil.GetHomeWorldIdAsync().GetAwaiter().GetResult();
|
||||
private string PlayerPersistentDataKey => _dalamudUtil.GetPlayerName() + "_" + _dalamudUtil.GetHomeWorldId();
|
||||
private ConcurrentDictionary<ObjectKind, HashSet<string>> SemiTransientResources
|
||||
{
|
||||
get
|
||||
@@ -87,9 +94,12 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
|
||||
{
|
||||
_semiTransientResources = new();
|
||||
PlayerConfig.JobSpecificCache.TryGetValue(_dalamudUtil.ClassJobId, out var jobSpecificData);
|
||||
_semiTransientResources[ObjectKind.Player] = PlayerConfig.GlobalPersistentCache.Concat(jobSpecificData ?? []).ToHashSet(StringComparer.Ordinal);
|
||||
_semiTransientResources[ObjectKind.Player] = PlayerConfig.GlobalPersistentCache.Concat(jobSpecificData ?? [])
|
||||
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
PlayerConfig.JobSpecificPetCache.TryGetValue(_dalamudUtil.ClassJobId, out var petSpecificData);
|
||||
_semiTransientResources[ObjectKind.Pet] = [.. petSpecificData ?? []];
|
||||
_semiTransientResources[ObjectKind.Pet] = new HashSet<string>(
|
||||
petSpecificData ?? [],
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
return _semiTransientResources;
|
||||
@@ -127,14 +137,14 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
|
||||
{
|
||||
SemiTransientResources.TryGetValue(objectKind, out var result);
|
||||
|
||||
return result ?? new HashSet<string>(StringComparer.Ordinal);
|
||||
return result ?? new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public void PersistTransientResources(ObjectKind objectKind)
|
||||
{
|
||||
if (!SemiTransientResources.TryGetValue(objectKind, out HashSet<string>? semiTransientResources))
|
||||
{
|
||||
SemiTransientResources[objectKind] = semiTransientResources = new(StringComparer.Ordinal);
|
||||
SemiTransientResources[objectKind] = semiTransientResources = new(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
if (!TransientResources.TryGetValue(objectKind, out var resources))
|
||||
@@ -152,7 +162,7 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
|
||||
List<string> newlyAddedGamePaths;
|
||||
lock (semiTransientResources)
|
||||
{
|
||||
newlyAddedGamePaths = transientResources.Except(semiTransientResources, StringComparer.Ordinal).ToList();
|
||||
newlyAddedGamePaths = transientResources.Except(semiTransientResources, StringComparer.OrdinalIgnoreCase).ToList();
|
||||
foreach (var gamePath in transientResources)
|
||||
{
|
||||
semiTransientResources.Add(gamePath);
|
||||
@@ -197,12 +207,13 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
|
||||
|
||||
public void RemoveTransientResource(ObjectKind objectKind, string path)
|
||||
{
|
||||
var normalizedPath = NormalizeGamePath(path);
|
||||
if (SemiTransientResources.TryGetValue(objectKind, out var resources))
|
||||
{
|
||||
resources.RemoveWhere(f => string.Equals(path, f, StringComparison.Ordinal));
|
||||
resources.Remove(normalizedPath);
|
||||
if (objectKind == ObjectKind.Player)
|
||||
{
|
||||
PlayerConfig.RemovePath(path, objectKind);
|
||||
PlayerConfig.RemovePath(normalizedPath, objectKind);
|
||||
Logger.LogTrace("Saving transient.json from {method}", nameof(RemoveTransientResource));
|
||||
_configurationService.Save();
|
||||
}
|
||||
@@ -211,16 +222,17 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
|
||||
|
||||
internal bool AddTransientResource(ObjectKind objectKind, string item)
|
||||
{
|
||||
if (SemiTransientResources.TryGetValue(objectKind, out var semiTransient) && semiTransient != null && semiTransient.Contains(item))
|
||||
var normalizedItem = NormalizeGamePath(item);
|
||||
if (SemiTransientResources.TryGetValue(objectKind, out var semiTransient) && semiTransient != null && semiTransient.Contains(normalizedItem))
|
||||
return false;
|
||||
|
||||
if (!TransientResources.TryGetValue(objectKind, out HashSet<string>? transientResource))
|
||||
{
|
||||
transientResource = new HashSet<string>(StringComparer.Ordinal);
|
||||
transientResource = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
TransientResources[objectKind] = transientResource;
|
||||
}
|
||||
|
||||
return transientResource.Add(item.ToLowerInvariant());
|
||||
return transientResource.Add(normalizedItem);
|
||||
}
|
||||
|
||||
internal void ClearTransientPaths(ObjectKind objectKind, List<string> list)
|
||||
@@ -285,33 +297,13 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
|
||||
|
||||
private void DalamudUtil_FrameworkUpdate()
|
||||
{
|
||||
RefreshPlayerRelatedAddressMap();
|
||||
|
||||
lock (_cacheAdditionLock)
|
||||
{
|
||||
_cachedHandledPaths.Clear();
|
||||
}
|
||||
|
||||
var activeDescriptors = new Dictionary<nint, ObjectKind>();
|
||||
foreach (var descriptor in _actorObjectService.PlayerDescriptors)
|
||||
{
|
||||
if (TryResolveObjectKind(descriptor, out var resolvedKind))
|
||||
{
|
||||
activeDescriptors[descriptor.Address] = resolvedKind;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var address in _cachedFrameAddresses.Keys.ToList())
|
||||
{
|
||||
if (!activeDescriptors.ContainsKey(address))
|
||||
{
|
||||
_cachedFrameAddresses.TryRemove(address, out _);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var descriptor in activeDescriptors)
|
||||
{
|
||||
_cachedFrameAddresses[descriptor.Key] = descriptor.Value;
|
||||
}
|
||||
|
||||
if (_lastClassJobId != _dalamudUtil.ClassJobId)
|
||||
{
|
||||
_lastClassJobId = _dalamudUtil.ClassJobId;
|
||||
@@ -323,7 +315,9 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
|
||||
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] = [.. petSpecificData ?? []];
|
||||
SemiTransientResources[ObjectKind.Pet] = new HashSet<string>(
|
||||
petSpecificData ?? [],
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
foreach (var kind in Enum.GetValues(typeof(ObjectKind)).Cast<ObjectKind>())
|
||||
@@ -340,9 +334,12 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
|
||||
_ = Task.Run(() =>
|
||||
{
|
||||
Logger.LogDebug("Penumbra Mod Settings changed, verifying SemiTransientResources");
|
||||
foreach (var item in _playerRelatedPointers)
|
||||
lock (_playerRelatedLock)
|
||||
{
|
||||
Mediator.Publish(new TransientResourceChangedMessage(item.Address));
|
||||
foreach (var item in _playerRelatedPointers)
|
||||
{
|
||||
Mediator.Publish(new TransientResourceChangedMessage(item.Address));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -352,38 +349,40 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
|
||||
_semiTransientResources = null;
|
||||
}
|
||||
|
||||
private static bool TryResolveObjectKind(ActorObjectService.ActorDescriptor descriptor, out ObjectKind resolvedKind)
|
||||
private void RefreshPlayerRelatedAddressMap()
|
||||
{
|
||||
if (descriptor.OwnedKind is ObjectKind ownedKind)
|
||||
_playerRelatedByAddress.Clear();
|
||||
var updatedFrameAddresses = new ConcurrentDictionary<nint, ObjectKind>();
|
||||
lock (_playerRelatedLock)
|
||||
{
|
||||
resolvedKind = ownedKind;
|
||||
return true;
|
||||
foreach (var handler in _playerRelatedPointers)
|
||||
{
|
||||
var address = (nint)handler.Address;
|
||||
if (address != nint.Zero)
|
||||
{
|
||||
_playerRelatedByAddress[address] = handler;
|
||||
updatedFrameAddresses[address] = handler.ObjectKind;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (descriptor.ObjectKind == DalamudObjectKind.Player)
|
||||
{
|
||||
resolvedKind = ObjectKind.Player;
|
||||
return true;
|
||||
}
|
||||
|
||||
resolvedKind = default;
|
||||
return false;
|
||||
_cachedFrameAddresses = updatedFrameAddresses;
|
||||
}
|
||||
|
||||
private void HandleActorTracked(ActorObjectService.ActorDescriptor descriptor)
|
||||
{
|
||||
if (!TryResolveObjectKind(descriptor, out var resolvedKind))
|
||||
if (descriptor.IsInGpose)
|
||||
return;
|
||||
|
||||
if (descriptor.OwnedKind is not ObjectKind ownedKind)
|
||||
return;
|
||||
|
||||
if (Logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
Logger.LogDebug("ActorObject tracked: {kind} addr={address:X} name={name}", resolvedKind, descriptor.Address, descriptor.Name);
|
||||
Logger.LogDebug("ActorObject tracked: {kind} addr={address:X} name={name}", ownedKind, descriptor.Address, descriptor.Name);
|
||||
}
|
||||
|
||||
_cachedFrameAddresses[descriptor.Address] = resolvedKind;
|
||||
|
||||
if (descriptor.OwnedKind is not ObjectKind ownedKind)
|
||||
return;
|
||||
_cachedFrameAddresses[descriptor.Address] = ownedKind;
|
||||
|
||||
lock (_ownedHandlerLock)
|
||||
{
|
||||
@@ -462,53 +461,84 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
|
||||
}
|
||||
}
|
||||
|
||||
private static string NormalizeGamePath(string path)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path))
|
||||
return string.Empty;
|
||||
|
||||
return path.Replace("\\", "/", StringComparison.Ordinal).ToLowerInvariant();
|
||||
}
|
||||
|
||||
private static string NormalizeFilePath(string path)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path))
|
||||
return string.Empty;
|
||||
|
||||
if (path.StartsWith("|", StringComparison.Ordinal))
|
||||
{
|
||||
var lastPipe = path.LastIndexOf('|');
|
||||
if (lastPipe >= 0 && lastPipe + 1 < path.Length)
|
||||
{
|
||||
path = path[(lastPipe + 1)..];
|
||||
}
|
||||
}
|
||||
|
||||
return NormalizeGamePath(path);
|
||||
}
|
||||
|
||||
private static bool HasHandledFileType(string gamePath, string[] handledTypes)
|
||||
{
|
||||
for (var i = 0; i < handledTypes.Length; i++)
|
||||
{
|
||||
if (gamePath.EndsWith(handledTypes[i], StringComparison.Ordinal))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void Manager_PenumbraResourceLoadEvent(PenumbraResourceLoadMessage msg)
|
||||
{
|
||||
var gamePath = msg.GamePath.ToLowerInvariant();
|
||||
var gameObjectAddress = msg.GameObject;
|
||||
var filePath = msg.FilePath;
|
||||
|
||||
// ignore files already processed this frame
|
||||
if (_cachedHandledPaths.Contains(gamePath)) return;
|
||||
|
||||
lock (_cacheAdditionLock)
|
||||
if (!_cachedFrameAddresses.TryGetValue(gameObjectAddress, out var objectKind))
|
||||
{
|
||||
_cachedHandledPaths.Add(gamePath);
|
||||
if (_actorObjectService.TryGetOwnedKind(gameObjectAddress, out var ownedKind))
|
||||
{
|
||||
objectKind = ownedKind;
|
||||
}
|
||||
else
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// replace individual mtrl stuff
|
||||
if (filePath.StartsWith("|", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
filePath = filePath.Split("|")[2];
|
||||
}
|
||||
// replace filepath
|
||||
filePath = filePath.ToLowerInvariant().Replace("\\", "/", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
// ignore files that are the same
|
||||
var replacedGamePath = gamePath.ToLowerInvariant().Replace("\\", "/", StringComparison.OrdinalIgnoreCase);
|
||||
if (string.Equals(filePath, replacedGamePath, StringComparison.OrdinalIgnoreCase))
|
||||
var gamePath = NormalizeGamePath(msg.GamePath);
|
||||
if (string.IsNullOrEmpty(gamePath))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// ignore files already processed this frame
|
||||
lock (_cacheAdditionLock)
|
||||
{
|
||||
if (!_cachedHandledPaths.Add(gamePath))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// ignore files to not handle
|
||||
var handledTypes = IsTransientRecording ? _handledRecordingFileTypes.Concat(_handledFileTypes) : _handledFileTypes;
|
||||
if (!handledTypes.Any(type => gamePath.EndsWith(type, StringComparison.OrdinalIgnoreCase)))
|
||||
var handledTypes = IsTransientRecording ? _handledFileTypesWithRecording : _handledFileTypes;
|
||||
if (!HasHandledFileType(gamePath, handledTypes))
|
||||
{
|
||||
lock (_cacheAdditionLock)
|
||||
{
|
||||
_cachedHandledPaths.Add(gamePath);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// ignore files not belonging to anything player related
|
||||
if (!_cachedFrameAddresses.TryGetValue(gameObjectAddress, out var objectKind))
|
||||
var filePath = NormalizeFilePath(msg.FilePath);
|
||||
|
||||
// ignore files that are the same
|
||||
if (string.Equals(filePath, gamePath, StringComparison.Ordinal))
|
||||
{
|
||||
lock (_cacheAdditionLock)
|
||||
{
|
||||
_cachedHandledPaths.Add(gamePath);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -520,15 +550,15 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
|
||||
TransientResources[objectKind] = transientResources;
|
||||
}
|
||||
|
||||
var owner = _playerRelatedPointers.FirstOrDefault(f => f.Address == gameObjectAddress);
|
||||
_playerRelatedByAddress.TryGetValue(gameObjectAddress, out var owner);
|
||||
bool alreadyTransient = false;
|
||||
|
||||
bool transientContains = transientResources.Contains(replacedGamePath);
|
||||
bool semiTransientContains = SemiTransientResources.SelectMany(k => k.Value).Any(f => string.Equals(f, gamePath, StringComparison.OrdinalIgnoreCase));
|
||||
bool transientContains = transientResources.Contains(gamePath);
|
||||
bool semiTransientContains = SemiTransientResources.Values.Any(value => value.Contains(gamePath));
|
||||
if (transientContains || semiTransientContains)
|
||||
{
|
||||
if (!IsTransientRecording)
|
||||
Logger.LogTrace("Not adding {replacedPath} => {filePath}, Reason: Transient: {contains}, SemiTransient: {contains2}", replacedGamePath, filePath,
|
||||
Logger.LogTrace("Not adding {replacedPath} => {filePath}, Reason: Transient: {contains}, SemiTransient: {contains2}", gamePath, filePath,
|
||||
transientContains, semiTransientContains);
|
||||
alreadyTransient = true;
|
||||
}
|
||||
@@ -536,10 +566,10 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
|
||||
{
|
||||
if (!IsTransientRecording)
|
||||
{
|
||||
bool isAdded = transientResources.Add(replacedGamePath);
|
||||
bool isAdded = transientResources.Add(gamePath);
|
||||
if (isAdded)
|
||||
{
|
||||
Logger.LogDebug("Adding {replacedGamePath} for {gameObject} ({filePath})", replacedGamePath, owner?.ToString() ?? gameObjectAddress.ToString("X"), filePath);
|
||||
Logger.LogDebug("Adding {replacedGamePath} for {gameObject} ({filePath})", gamePath, owner?.ToString() ?? gameObjectAddress.ToString("X"), filePath);
|
||||
SendTransients(gameObjectAddress, objectKind);
|
||||
}
|
||||
}
|
||||
@@ -547,7 +577,7 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
|
||||
|
||||
if (owner != null && IsTransientRecording)
|
||||
{
|
||||
_recordedTransients.Add(new TransientRecord(owner, replacedGamePath, filePath, alreadyTransient) { AddTransient = !alreadyTransient });
|
||||
_recordedTransients.Add(new TransientRecord(owner, gamePath, filePath, alreadyTransient) { AddTransient = !alreadyTransient });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -619,7 +649,7 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
|
||||
if (!item.AddTransient || item.AlreadyTransient) continue;
|
||||
if (!TransientResources.TryGetValue(item.Owner.ObjectKind, out var transient))
|
||||
{
|
||||
TransientResources[item.Owner.ObjectKind] = transient = [];
|
||||
TransientResources[item.Owner.ObjectKind] = transient = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
Logger.LogTrace("Adding recorded: {gamePath} => {filePath}", item.GamePath, item.FilePath);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Dalamud.Plugin.Services;
|
||||
using Dalamud.Game.ClientState.Objects.SubKinds;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Info;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -11,24 +12,35 @@ public unsafe class BlockedCharacterHandler
|
||||
private readonly Dictionary<CharaData, bool> _blockedCharacterCache = new();
|
||||
|
||||
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);
|
||||
_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);
|
||||
var castChar = ((BattleChara*)ptr);
|
||||
if (ptr == nint.Zero || objectIndex >= 200)
|
||||
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);
|
||||
}
|
||||
|
||||
public bool IsCharacterBlocked(nint ptr, out bool firstTime)
|
||||
public bool IsCharacterBlocked(nint ptr, ushort objectIndex, out bool firstTime)
|
||||
{
|
||||
firstTime = false;
|
||||
var combined = GetIdsFromPlayerPointer(ptr);
|
||||
var combined = TryGetIdsFromPlayerPointer(ptr, objectIndex);
|
||||
if (combined == null)
|
||||
return false;
|
||||
|
||||
if (_blockedCharacterCache.TryGetValue(combined, out var isBlocked))
|
||||
return isBlocked;
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ namespace LightlessSync.Interop.Ipc;
|
||||
|
||||
public sealed class IpcCallerBrio : IpcServiceBase
|
||||
{
|
||||
private static readonly IpcServiceDescriptor BrioDescriptor = new("Brio", "Brio", new Version(3, 0, 0, 0));
|
||||
private static readonly IpcServiceDescriptor BrioDescriptor = new("Brio", "Brio", new Version(0, 0, 0, 0));
|
||||
|
||||
private readonly ILogger<IpcCallerBrio> _logger;
|
||||
private readonly DalamudUtilService _dalamudUtilService;
|
||||
@@ -144,7 +144,7 @@ public sealed class IpcCallerBrio : IpcServiceBase
|
||||
try
|
||||
{
|
||||
var version = _apiVersion.Invoke();
|
||||
return version.Item1 == 3 && version.Item2 >= 0
|
||||
return version.Breaking == 3 && version.Feature >= 0
|
||||
? IpcConnectionState.Available
|
||||
: IpcConnectionState.VersionMismatch;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using Dalamud.Game.ClientState.Objects.SubKinds;
|
||||
using Dalamud.Plugin;
|
||||
using Dalamud.Plugin;
|
||||
using Dalamud.Plugin.Ipc;
|
||||
using LightlessSync.Interop.Ipc.Framework;
|
||||
using LightlessSync.Services;
|
||||
@@ -13,7 +12,7 @@ public sealed class IpcCallerMoodles : IpcServiceBase
|
||||
private static readonly IpcServiceDescriptor MoodlesDescriptor = new("Moodles", "Moodles", new Version(0, 0, 0, 0));
|
||||
|
||||
private readonly ICallGateSubscriber<int> _moodlesApiVersion;
|
||||
private readonly ICallGateSubscriber<IPlayerCharacter, object> _moodlesOnChange;
|
||||
private readonly ICallGateSubscriber<nint, object> _moodlesOnChange;
|
||||
private readonly ICallGateSubscriber<nint, string> _moodlesGetStatus;
|
||||
private readonly ICallGateSubscriber<nint, string, object> _moodlesSetStatus;
|
||||
private readonly ICallGateSubscriber<nint, object> _moodlesRevertStatus;
|
||||
@@ -29,7 +28,7 @@ public sealed class IpcCallerMoodles : IpcServiceBase
|
||||
_lightlessMediator = lightlessMediator;
|
||||
|
||||
_moodlesApiVersion = pi.GetIpcSubscriber<int>("Moodles.Version");
|
||||
_moodlesOnChange = pi.GetIpcSubscriber<IPlayerCharacter, object>("Moodles.StatusManagerModified");
|
||||
_moodlesOnChange = pi.GetIpcSubscriber<nint, object>("Moodles.StatusManagerModified");
|
||||
_moodlesGetStatus = pi.GetIpcSubscriber<nint, string>("Moodles.GetStatusManagerByPtrV2");
|
||||
_moodlesSetStatus = pi.GetIpcSubscriber<nint, string, object>("Moodles.SetStatusManagerByPtrV2");
|
||||
_moodlesRevertStatus = pi.GetIpcSubscriber<nint, object>("Moodles.ClearStatusManagerByPtrV2");
|
||||
@@ -39,9 +38,9 @@ public sealed class IpcCallerMoodles : IpcServiceBase
|
||||
CheckAPI();
|
||||
}
|
||||
|
||||
private void OnMoodlesChange(IPlayerCharacter character)
|
||||
private void OnMoodlesChange(nint address)
|
||||
{
|
||||
_lightlessMediator.Publish(new MoodlesMessage(character.Address));
|
||||
_lightlessMediator.Publish(new MoodlesMessage(address));
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
@@ -107,7 +106,7 @@ public sealed class IpcCallerMoodles : IpcServiceBase
|
||||
|
||||
try
|
||||
{
|
||||
return _moodlesApiVersion.InvokeFunc() == 3
|
||||
return _moodlesApiVersion.InvokeFunc() >= 4
|
||||
? IpcConnectionState.Available
|
||||
: IpcConnectionState.VersionMismatch;
|
||||
}
|
||||
|
||||
@@ -95,6 +95,12 @@ public sealed class IpcCallerPenumbra : IpcServiceBase
|
||||
public Task<(string[] forward, string[][] reverse)> ResolvePathsAsync(string[] forward, string[] reverse)
|
||||
=> _resources.ResolvePathsAsync(forward, reverse);
|
||||
|
||||
public string ResolveGameObjectPath(string gamePath, int objectIndex)
|
||||
=> _resources.ResolveGameObjectPath(gamePath, objectIndex);
|
||||
|
||||
public string[] ReverseResolveGameObjectPath(string moddedPath, int objectIndex)
|
||||
=> _resources.ReverseResolveGameObjectPath(moddedPath, objectIndex);
|
||||
|
||||
public Task RedrawAsync(ILogger logger, GameObjectHandler handler, Guid applicationId, CancellationToken token)
|
||||
=> _redraw.RedrawAsync(logger, handler, applicationId, token);
|
||||
|
||||
@@ -171,11 +177,6 @@ public sealed class IpcCallerPenumbra : IpcServiceBase
|
||||
});
|
||||
|
||||
Mediator.Subscribe<DalamudLoginMessage>(this, _ => _shownPenumbraUnavailable = false);
|
||||
|
||||
Mediator.Subscribe<ActorTrackedMessage>(this, msg => _resources.TrackActor(msg.Descriptor.Address));
|
||||
Mediator.Subscribe<ActorUntrackedMessage>(this, msg => _resources.UntrackActor(msg.Descriptor.Address));
|
||||
Mediator.Subscribe<GameObjectHandlerCreatedMessage>(this, msg => _resources.TrackActor(msg.GameObjectHandler.Address));
|
||||
Mediator.Subscribe<GameObjectHandlerDestroyedMessage>(this, msg => _resources.UntrackActor(msg.GameObjectHandler.Address));
|
||||
}
|
||||
|
||||
private void HandlePenumbraInitialized()
|
||||
|
||||
@@ -5,6 +5,8 @@ namespace LightlessSync.Interop.Ipc;
|
||||
|
||||
public sealed partial class IpcManager : DisposableMediatorSubscriberBase
|
||||
{
|
||||
private bool _wasInitialized;
|
||||
|
||||
public IpcManager(ILogger<IpcManager> logger, LightlessMediator mediator,
|
||||
IpcCallerPenumbra penumbraIpc, IpcCallerGlamourer glamourerIpc, IpcCallerCustomize customizeIpc, IpcCallerHeels heelsIpc,
|
||||
IpcCallerHonorific honorificIpc, IpcCallerMoodles moodlesIpc, IpcCallerPetNames ipcCallerPetNames, IpcCallerBrio ipcCallerBrio) : base(logger, mediator)
|
||||
@@ -18,7 +20,8 @@ public sealed partial class IpcManager : DisposableMediatorSubscriberBase
|
||||
PetNames = ipcCallerPetNames;
|
||||
Brio = ipcCallerBrio;
|
||||
|
||||
if (Initialized)
|
||||
_wasInitialized = Initialized;
|
||||
if (_wasInitialized)
|
||||
{
|
||||
Mediator.Publish(new PenumbraInitializedMessage());
|
||||
}
|
||||
@@ -58,5 +61,13 @@ public sealed partial class IpcManager : DisposableMediatorSubscriberBase
|
||||
Moodles.CheckAPI();
|
||||
PetNames.CheckAPI();
|
||||
Brio.CheckAPI();
|
||||
|
||||
var initialized = Initialized;
|
||||
if (initialized && !_wasInitialized)
|
||||
{
|
||||
Mediator.Publish(new PenumbraInitializedMessage());
|
||||
}
|
||||
|
||||
_wasInitialized = initialized;
|
||||
}
|
||||
}
|
||||
@@ -92,7 +92,7 @@ public sealed class PenumbraCollections : PenumbraBase
|
||||
_activeTemporaryCollections.TryRemove(collectionId, out _);
|
||||
}
|
||||
|
||||
public async Task SetTemporaryModsAsync(ILogger logger, Guid applicationId, Guid collectionId, IReadOnlyDictionary<string, string> modPaths)
|
||||
public async Task SetTemporaryModsAsync(ILogger logger, Guid applicationId, Guid collectionId, Dictionary<string, string> modPaths)
|
||||
{
|
||||
if (!IsAvailable || collectionId == Guid.Empty)
|
||||
{
|
||||
@@ -109,7 +109,7 @@ public sealed class PenumbraCollections : PenumbraBase
|
||||
var removeResult = _removeTemporaryMod.Invoke("LightlessChara_Files", collectionId, 0);
|
||||
logger.LogTrace("[{ApplicationId}] Removing temp files mod for {CollectionId}, Success: {Result}", applicationId, collectionId, removeResult);
|
||||
|
||||
var addResult = _addTemporaryMod.Invoke("LightlessChara_Files", collectionId, new Dictionary<string, string>(modPaths), string.Empty, 0);
|
||||
var addResult = _addTemporaryMod.Invoke("LightlessChara_Files", collectionId, modPaths, string.Empty, 0);
|
||||
logger.LogTrace("[{ApplicationId}] Setting temp files mod for {CollectionId}, Success: {Result}", applicationId, collectionId, addResult);
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using System.Collections.Concurrent;
|
||||
using Dalamud.Plugin;
|
||||
using LightlessSync.Interop.Ipc.Framework;
|
||||
using LightlessSync.PlayerData.Handlers;
|
||||
@@ -15,10 +14,11 @@ public sealed class PenumbraResource : PenumbraBase
|
||||
{
|
||||
private readonly ActorObjectService _actorObjectService;
|
||||
private readonly GetGameObjectResourcePaths _gameObjectResourcePaths;
|
||||
private readonly ResolveGameObjectPath _resolveGameObjectPath;
|
||||
private readonly ReverseResolveGameObjectPath _reverseResolveGameObjectPath;
|
||||
private readonly ResolvePlayerPathsAsync _resolvePlayerPaths;
|
||||
private readonly GetPlayerMetaManipulations _getPlayerMetaManipulations;
|
||||
private readonly EventSubscriber<nint, string, string> _gameObjectResourcePathResolved;
|
||||
private readonly ConcurrentDictionary<IntPtr, byte> _trackedActors = new();
|
||||
|
||||
public PenumbraResource(
|
||||
ILogger logger,
|
||||
@@ -29,14 +29,11 @@ public sealed class PenumbraResource : PenumbraBase
|
||||
{
|
||||
_actorObjectService = actorObjectService;
|
||||
_gameObjectResourcePaths = new GetGameObjectResourcePaths(pluginInterface);
|
||||
_resolveGameObjectPath = new ResolveGameObjectPath(pluginInterface);
|
||||
_reverseResolveGameObjectPath = new ReverseResolveGameObjectPath(pluginInterface);
|
||||
_resolvePlayerPaths = new ResolvePlayerPathsAsync(pluginInterface);
|
||||
_getPlayerMetaManipulations = new GetPlayerMetaManipulations(pluginInterface);
|
||||
_gameObjectResourcePathResolved = GameObjectResourcePathResolved.Subscriber(pluginInterface, HandleResourceLoaded);
|
||||
|
||||
foreach (var descriptor in _actorObjectService.PlayerDescriptors)
|
||||
{
|
||||
TrackActor(descriptor.Address);
|
||||
}
|
||||
}
|
||||
|
||||
public override string Name => "Penumbra.Resources";
|
||||
@@ -74,63 +71,34 @@ public sealed class PenumbraResource : PenumbraBase
|
||||
return await _resolvePlayerPaths.Invoke(forwardPaths, reversePaths).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public void TrackActor(nint address)
|
||||
{
|
||||
if (address != nint.Zero)
|
||||
{
|
||||
_trackedActors[(IntPtr)address] = 0;
|
||||
}
|
||||
}
|
||||
public string ResolveGameObjectPath(string gamePath, int gameObjectIndex)
|
||||
=> IsAvailable ? _resolveGameObjectPath.Invoke(gamePath, gameObjectIndex) : gamePath;
|
||||
|
||||
public void UntrackActor(nint address)
|
||||
{
|
||||
if (address != nint.Zero)
|
||||
{
|
||||
_trackedActors.TryRemove((IntPtr)address, out _);
|
||||
}
|
||||
}
|
||||
public string[] ReverseResolveGameObjectPath(string moddedPath, int gameObjectIndex)
|
||||
=> IsAvailable ? _reverseResolveGameObjectPath.Invoke(moddedPath, gameObjectIndex) : Array.Empty<string>();
|
||||
|
||||
private void HandleResourceLoaded(nint ptr, string resolvedPath, string gamePath)
|
||||
private void HandleResourceLoaded(nint ptr, string gamePath, string resolvedPath)
|
||||
{
|
||||
if (ptr == nint.Zero)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_trackedActors.ContainsKey(ptr))
|
||||
{
|
||||
var descriptor = _actorObjectService.PlayerDescriptors.FirstOrDefault(d => d.Address == ptr);
|
||||
if (descriptor.Address != nint.Zero)
|
||||
{
|
||||
_trackedActors[ptr] = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (string.Compare(resolvedPath, gamePath, StringComparison.OrdinalIgnoreCase) == 0)
|
||||
if (!_actorObjectService.TryGetOwnedKind(ptr, out _))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Mediator.Publish(new PenumbraResourceLoadMessage(ptr, resolvedPath, gamePath));
|
||||
if (string.Compare(gamePath, resolvedPath, StringComparison.OrdinalIgnoreCase) == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Mediator.Publish(new PenumbraResourceLoadMessage(ptr, gamePath, resolvedPath));
|
||||
}
|
||||
|
||||
protected override void HandleStateChange(IpcConnectionState previous, IpcConnectionState current)
|
||||
{
|
||||
if (current != IpcConnectionState.Available)
|
||||
{
|
||||
_trackedActors.Clear();
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var descriptor in _actorObjectService.PlayerDescriptors)
|
||||
{
|
||||
TrackActor(descriptor.Address);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override void Dispose()
|
||||
|
||||
@@ -19,6 +19,27 @@ public class ConfigurationMigrator(ILogger<ConfigurationMigrator> logger, Transi
|
||||
transientConfigService.Save();
|
||||
}
|
||||
|
||||
if (transientConfigService.Current.Version == 1)
|
||||
{
|
||||
_logger.LogInformation("Migrating Transient Config V1 => V2");
|
||||
var totalRemoved = 0;
|
||||
var configCount = 0;
|
||||
var changedCount = 0;
|
||||
foreach (var config in transientConfigService.Current.TransientConfigs.Values)
|
||||
{
|
||||
if (config.NormalizePaths(out var removed))
|
||||
changedCount++;
|
||||
|
||||
totalRemoved += removed;
|
||||
|
||||
configCount++;
|
||||
}
|
||||
|
||||
_logger.LogInformation("Transient config normalization: processed {count} entries, updated {updated}, removed {removed} paths", configCount, changedCount, totalRemoved);
|
||||
transientConfigService.Current.Version = 2;
|
||||
transientConfigService.Save();
|
||||
}
|
||||
|
||||
if (serverConfigService.Current.Version == 1)
|
||||
{
|
||||
_logger.LogInformation("Migrating Server Config V1 => V2");
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace LightlessSync.LightlessConfiguration.Configurations;
|
||||
|
||||
@@ -9,8 +10,19 @@ public sealed class ChatConfig : ILightlessConfiguration
|
||||
public bool AutoEnableChatOnLogin { get; set; } = false;
|
||||
public bool ShowRulesOverlayOnOpen { get; set; } = true;
|
||||
public bool ShowMessageTimestamps { get; set; } = true;
|
||||
public bool ShowNotesInSyncshellChat { get; set; } = true;
|
||||
public bool EnableAnimatedEmotes { get; set; } = true;
|
||||
public float ChatWindowOpacity { get; set; } = .97f;
|
||||
public bool FadeWhenUnfocused { get; set; } = false;
|
||||
public float UnfocusedWindowOpacity { get; set; } = 0.6f;
|
||||
public bool IsWindowPinned { get; set; } = false;
|
||||
public bool AutoOpenChatOnPluginLoad { get; set; } = false;
|
||||
public float ChatFontScale { get; set; } = 1.0f;
|
||||
public bool HideInCombat { get; set; } = false;
|
||||
public bool HideInDuty { get; set; } = false;
|
||||
public bool ShowWhenUiHidden { get; set; } = true;
|
||||
public bool ShowInCutscenes { get; set; } = true;
|
||||
public bool ShowInGpose { get; set; } = true;
|
||||
public List<string> ChannelOrder { get; set; } = new();
|
||||
public Dictionary<string, bool> PreferNotesForChannels { get; set; } = new(StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
@@ -49,7 +49,9 @@ public class LightlessConfig : ILightlessConfiguration
|
||||
public int DownloadSpeedLimitInBytes { get; set; } = 0;
|
||||
public DownloadSpeeds DownloadSpeedType { get; set; } = DownloadSpeeds.MBps;
|
||||
public bool PreferNotesOverNamesForVisible { get; set; } = false;
|
||||
public VisiblePairSortMode VisiblePairSortMode { get; set; } = VisiblePairSortMode.Default;
|
||||
public VisiblePairSortMode VisiblePairSortMode { get; set; } = VisiblePairSortMode.Alphabetical;
|
||||
public OnlinePairSortMode OnlinePairSortMode { get; set; } = OnlinePairSortMode.Alphabetical;
|
||||
public TextureFormatSortMode TextureFormatSortMode { get; set; } = TextureFormatSortMode.None;
|
||||
public float ProfileDelay { get; set; } = 1.5f;
|
||||
public bool ProfilePopoutRight { get; set; } = false;
|
||||
public bool ProfilesAllowNsfw { get; set; } = false;
|
||||
@@ -139,6 +141,7 @@ public class LightlessConfig : ILightlessConfiguration
|
||||
public bool useColoredUIDs { get; set; } = true;
|
||||
public bool BroadcastEnabled { get; set; } = false;
|
||||
public bool LightfinderAutoEnableOnConnect { get; set; } = false;
|
||||
public LightfinderLabelRenderer LightfinderLabelRenderer { get; set; } = LightfinderLabelRenderer.Pictomancy;
|
||||
public short LightfinderLabelOffsetX { get; set; } = 0;
|
||||
public short LightfinderLabelOffsetY { get; set; } = 0;
|
||||
public bool LightfinderLabelUseIcon { get; set; } = false;
|
||||
@@ -153,4 +156,5 @@ public class LightlessConfig : ILightlessConfiguration
|
||||
public bool SyncshellFinderEnabled { get; set; } = false;
|
||||
public string? SelectedFinderSyncshell { get; set; } = null;
|
||||
public string LastSeenVersion { get; set; } = string.Empty;
|
||||
public HashSet<Guid> OrphanableTempCollections { get; set; } = [];
|
||||
}
|
||||
|
||||
@@ -22,4 +22,15 @@ public class PlayerPerformanceConfig : ILightlessConfiguration
|
||||
public int TextureDownscaleMaxDimension { get; set; } = 2048;
|
||||
public bool OnlyDownscaleUncompressedTextures { get; set; } = true;
|
||||
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,7 +5,7 @@ namespace LightlessSync.LightlessConfiguration.Configurations;
|
||||
public class TransientConfig : ILightlessConfiguration
|
||||
{
|
||||
public Dictionary<string, TransientPlayerConfig> TransientConfigs { get; set; } = [];
|
||||
public int Version { get; set; } = 1;
|
||||
public int Version { get; set; } = 2;
|
||||
|
||||
public class TransientPlayerConfig
|
||||
{
|
||||
@@ -88,5 +88,70 @@ public class TransientConfig : ILightlessConfiguration
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool NormalizePaths(out int removedEntries)
|
||||
{
|
||||
bool changed = false;
|
||||
removedEntries = 0;
|
||||
|
||||
GlobalPersistentCache = NormalizeList(GlobalPersistentCache, ref changed, ref removedEntries);
|
||||
|
||||
foreach (var jobId in JobSpecificCache.Keys.ToList())
|
||||
{
|
||||
JobSpecificCache[jobId] = NormalizeList(JobSpecificCache[jobId], ref changed, ref removedEntries);
|
||||
}
|
||||
|
||||
foreach (var jobId in JobSpecificPetCache.Keys.ToList())
|
||||
{
|
||||
JobSpecificPetCache[jobId] = NormalizeList(JobSpecificPetCache[jobId], ref changed, ref removedEntries);
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
private static List<string> NormalizeList(List<string> entries, ref bool changed, ref int removedEntries)
|
||||
{
|
||||
if (entries.Count == 0)
|
||||
return entries;
|
||||
|
||||
var result = new List<string>(entries.Count);
|
||||
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
var normalized = NormalizePath(entry);
|
||||
if (string.IsNullOrEmpty(normalized))
|
||||
{
|
||||
changed = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!string.Equals(entry, normalized, StringComparison.Ordinal))
|
||||
{
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (seen.Add(normalized))
|
||||
{
|
||||
result.Add(normalized);
|
||||
}
|
||||
else
|
||||
{
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
removedEntries += entries.Count - result.Count;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static string NormalizePath(string path)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path))
|
||||
return string.Empty;
|
||||
|
||||
return path.Replace("\\", "/", StringComparison.Ordinal).ToLowerInvariant();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace LightlessSync.LightlessConfiguration.Configurations;
|
||||
public class XivDataStorageConfig : ILightlessConfiguration
|
||||
{
|
||||
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 int Version { get; set; } = 0;
|
||||
}
|
||||
@@ -69,63 +69,12 @@ namespace LightlessSync;
|
||||
#pragma warning restore S125 // Sections of code should not be commented out
|
||||
// thank you dark 🙏
|
||||
|
||||
/*
|
||||
---------
|
||||
-------------------------
|
||||
-------------- ##++++ -------------
|
||||
----------+##+++++++++++++++++++###--------
|
||||
+------ ####++++++++++++++++++++++++##### -----
|
||||
------- #####++++++++++++++++++++++++++++++###### +++++
|
||||
------+#####++++++++++++++++++++++++++++++++++########+++++
|
||||
------######++++++++++++++++++++++++++++++++++++++#########++++
|
||||
------######+++++++++++++++++++++++++++++++++++++##############+++
|
||||
-----#######+++++++++++++++++++++++++++++++#######################+++
|
||||
---- #######++++++++++++++++++++++++++++############################ ++
|
||||
-----########+++++++++++++++++++++++++++##################################
|
||||
----+########+++++++++++++++++++++++++#######################################
|
||||
---- ########+++++++++++++++++++++++-+#########################################
|
||||
-----#########++++++++++++++++++++--+###########################################
|
||||
----#########+++++++++++++++++++---+#######+++++++++#############################
|
||||
----##########+++++++++++++++++----###+--------.......---+####################### #
|
||||
---###########+++++++++++++++-----#+------..................-#######################
|
||||
----###########++++++++++++++----------..----..................-+####################
|
||||
---+###########+++++++++++++--------.-+########+-................-###################
|
||||
----############++++++++++++--------+##############+................+#################
|
||||
----############++++---+---------.-#################+................-################
|
||||
---+####+++++-------------------..###################+...............--###############
|
||||
----+#############++-------------.+####################-...............--###############
|
||||
----+##############++++++++------.+####################+...............---##############
|
||||
----+##############+++++++++-----.+####################+...............---+#############
|
||||
---++##############++++++++-------####################.................---#############
|
||||
----+################++++++++-----+##################-......-..........---+############
|
||||
----++################+++++++-------################-......+...........---+############
|
||||
----++##################++++++--------+##########+-......-#-..........----+###########
|
||||
--- ++##################+++++--------..--+++---........+#-.........------+###########
|
||||
+++++++####################++----------.............-+#+-......-.--------+##########
|
||||
+++ +++######################+---------..........+###+-......-----------+##########
|
||||
++++-+++##########################+++------+++######-......--------.----##########
|
||||
++++----+########################################+-.......------------+#########
|
||||
++++ ----+####################################--.....----------------#########
|
||||
+++ -----++###############################+------------------------########
|
||||
+++ ------++###########++##+++++#+++----------------------------+#######
|
||||
+++ ----------++########-----------...------------------------+#######
|
||||
+++ -------------------------------------------------------#######
|
||||
++# ---------------------------------------------------+######
|
||||
## ------------------------------------------------######
|
||||
### -------------------------------------------+#####
|
||||
### --------------------------------------+#####
|
||||
# -------------------------------####
|
||||
----------------------++###
|
||||
------------#
|
||||
*/
|
||||
|
||||
// Just a shitty fork, still with love.
|
||||
|
||||
public class LightlessPlugin : MediatorSubscriberBase, IHostedService
|
||||
{
|
||||
private readonly DalamudUtilService _dalamudUtil;
|
||||
private readonly LightlessConfigService _lightlessConfigService;
|
||||
private readonly ServerConfigurationManager _serverConfigurationManager;
|
||||
private readonly PairHandlerRegistry _pairHandlerRegistry;
|
||||
private readonly IServiceScopeFactory _serviceScopeFactory;
|
||||
private IServiceScope? _runtimeServiceScope;
|
||||
private Task? _launchTask = null;
|
||||
@@ -133,11 +82,13 @@ public class LightlessPlugin : MediatorSubscriberBase, IHostedService
|
||||
public LightlessPlugin(ILogger<LightlessPlugin> logger, LightlessConfigService lightlessConfigService,
|
||||
ServerConfigurationManager serverConfigurationManager,
|
||||
DalamudUtilService dalamudUtil,
|
||||
PairHandlerRegistry pairHandlerRegistry,
|
||||
IServiceScopeFactory serviceScopeFactory, LightlessMediator mediator) : base(logger, mediator)
|
||||
{
|
||||
_lightlessConfigService = lightlessConfigService;
|
||||
_serverConfigurationManager = serverConfigurationManager;
|
||||
_dalamudUtil = dalamudUtil;
|
||||
_pairHandlerRegistry = pairHandlerRegistry;
|
||||
_serviceScopeFactory = serviceScopeFactory;
|
||||
}
|
||||
|
||||
@@ -160,12 +111,20 @@ public class LightlessPlugin : MediatorSubscriberBase, IHostedService
|
||||
|
||||
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();
|
||||
|
||||
DalamudUtilOnLogOut();
|
||||
|
||||
Logger.LogDebug("Halting LightlessPlugin");
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<PropertyGroup>
|
||||
<Authors></Authors>
|
||||
<Company></Company>
|
||||
<Version>2.0.0</Version>
|
||||
<Version>2.0.3</Version>
|
||||
<Description></Description>
|
||||
<Copyright></Copyright>
|
||||
<PackageProjectUrl>https://github.com/Light-Public-Syncshells/LightlessClient</PackageProjectUrl>
|
||||
@@ -37,6 +37,7 @@
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" 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="Glamourer.Api" Version="2.8.0" />
|
||||
<PackageReference Include="NReco.Logging.File" Version="1.3.1" />
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using LightlessSync.FileCache;
|
||||
using LightlessSync.LightlessConfiguration;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using LightlessSync.Services.ModelDecimation;
|
||||
using LightlessSync.Services.TextureCompression;
|
||||
using LightlessSync.WebAPI.Files;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -16,6 +17,7 @@ public class FileDownloadManagerFactory
|
||||
private readonly FileCompactor _fileCompactor;
|
||||
private readonly LightlessConfigService _configService;
|
||||
private readonly TextureDownscaleService _textureDownscaleService;
|
||||
private readonly ModelDecimationService _modelDecimationService;
|
||||
private readonly TextureMetadataHelper _textureMetadataHelper;
|
||||
|
||||
public FileDownloadManagerFactory(
|
||||
@@ -26,6 +28,7 @@ public class FileDownloadManagerFactory
|
||||
FileCompactor fileCompactor,
|
||||
LightlessConfigService configService,
|
||||
TextureDownscaleService textureDownscaleService,
|
||||
ModelDecimationService modelDecimationService,
|
||||
TextureMetadataHelper textureMetadataHelper)
|
||||
{
|
||||
_loggerFactory = loggerFactory;
|
||||
@@ -35,6 +38,7 @@ public class FileDownloadManagerFactory
|
||||
_fileCompactor = fileCompactor;
|
||||
_configService = configService;
|
||||
_textureDownscaleService = textureDownscaleService;
|
||||
_modelDecimationService = modelDecimationService;
|
||||
_textureMetadataHelper = textureMetadataHelper;
|
||||
}
|
||||
|
||||
@@ -48,6 +52,7 @@ public class FileDownloadManagerFactory
|
||||
_fileCompactor,
|
||||
_configService,
|
||||
_textureDownscaleService,
|
||||
_modelDecimationService,
|
||||
_textureMetadataHelper);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,6 +119,7 @@ public class PlayerDataFactory
|
||||
CharacterDataFragment fragment = objectKind == ObjectKind.Player ? new CharacterDataFragmentPlayer() : new();
|
||||
|
||||
_logger.LogDebug("Building character data for {obj}", playerRelatedObject);
|
||||
var logDebug = _logger.IsEnabled(LogLevel.Debug);
|
||||
|
||||
// wait until chara is not drawing and present so nothing spontaneously explodes
|
||||
await _dalamudUtil.WaitWhileCharacterIsDrawing(_logger, playerRelatedObject, Guid.NewGuid(), 30000, ct: ct).ConfigureAwait(false);
|
||||
@@ -132,11 +133,6 @@ public class PlayerDataFactory
|
||||
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
Dictionary<string, List<ushort>>? boneIndices =
|
||||
objectKind != ObjectKind.Player
|
||||
? null
|
||||
: await _dalamudUtil.RunOnFrameworkThread(() => _modelAnalyzer.GetSkeletonBoneIndices(playerRelatedObject)).ConfigureAwait(false);
|
||||
|
||||
DateTime start = DateTime.UtcNow;
|
||||
|
||||
// penumbra call, it's currently broken
|
||||
@@ -150,15 +146,26 @@ public class PlayerDataFactory
|
||||
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))));
|
||||
var allowedExtensions = CacheMonitor.AllowedFileExtensions;
|
||||
fragment.FileReplacements.RemoveWhere(c => c.GamePaths.Any(g => !allowedExtensions.Any(e => g.EndsWith(e, StringComparison.OrdinalIgnoreCase))));
|
||||
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
_logger.LogDebug("== Static Replacements ==");
|
||||
foreach (var replacement in fragment.FileReplacements.Where(i => i.HasFileReplacement).OrderBy(i => i.GamePaths.First(), StringComparer.OrdinalIgnoreCase))
|
||||
if (logDebug)
|
||||
{
|
||||
_logger.LogDebug("=> {repl}", replacement);
|
||||
ct.ThrowIfCancellationRequested();
|
||||
_logger.LogDebug("== Static Replacements ==");
|
||||
foreach (var replacement in fragment.FileReplacements.Where(i => i.HasFileReplacement).OrderBy(i => i.GamePaths.First(), StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
_logger.LogDebug("=> {repl}", replacement);
|
||||
ct.ThrowIfCancellationRequested();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var replacement in fragment.FileReplacements.Where(i => i.HasFileReplacement))
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
}
|
||||
}
|
||||
|
||||
await _transientResourceManager.WaitForRecording(ct).ConfigureAwait(false);
|
||||
@@ -188,13 +195,25 @@ public class PlayerDataFactory
|
||||
|
||||
// get all remaining paths and resolve them
|
||||
var transientPaths = ManageSemiTransientData(objectKind);
|
||||
var resolvedTransientPaths = await GetFileReplacementsFromPaths(transientPaths, new HashSet<string>(StringComparer.Ordinal)).ConfigureAwait(false);
|
||||
var resolvedTransientPaths = transientPaths.Count == 0
|
||||
? new Dictionary<string, string[]>(StringComparer.OrdinalIgnoreCase).AsReadOnly()
|
||||
: await GetFileReplacementsFromPaths(playerRelatedObject, transientPaths, new HashSet<string>(StringComparer.Ordinal)).ConfigureAwait(false);
|
||||
|
||||
_logger.LogDebug("== Transient Replacements ==");
|
||||
foreach (var replacement in resolvedTransientPaths.Select(c => new FileReplacement([.. c.Value], c.Key)).OrderBy(f => f.ResolvedPath, StringComparer.Ordinal))
|
||||
if (logDebug)
|
||||
{
|
||||
_logger.LogDebug("=> {repl}", replacement);
|
||||
fragment.FileReplacements.Add(replacement);
|
||||
_logger.LogDebug("== Transient Replacements ==");
|
||||
foreach (var replacement in resolvedTransientPaths.Select(c => new FileReplacement([.. c.Value], c.Key)).OrderBy(f => f.ResolvedPath, StringComparer.Ordinal))
|
||||
{
|
||||
_logger.LogDebug("=> {repl}", replacement);
|
||||
fragment.FileReplacements.Add(replacement);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var replacement in resolvedTransientPaths.Select(c => new FileReplacement([.. c.Value], c.Key)))
|
||||
{
|
||||
fragment.FileReplacements.Add(replacement);
|
||||
}
|
||||
}
|
||||
|
||||
// clean up all semi transient resources that don't have any file replacement (aka null resolve)
|
||||
@@ -252,11 +271,26 @@ public class PlayerDataFactory
|
||||
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
Dictionary<string, List<ushort>>? boneIndices = null;
|
||||
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)
|
||||
{
|
||||
try
|
||||
{
|
||||
await VerifyPlayerAnimationBones(boneIndices, (fragment as CharacterDataFragmentPlayer)!, ct).ConfigureAwait(false);
|
||||
if (hasPapFiles)
|
||||
{
|
||||
await VerifyPlayerAnimationBones(boneIndices, (fragment as CharacterDataFragmentPlayer)!, ct).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException e)
|
||||
{
|
||||
@@ -278,12 +312,16 @@ public class PlayerDataFactory
|
||||
{
|
||||
if (boneIndices == null) return;
|
||||
|
||||
foreach (var kvp in boneIndices)
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
{
|
||||
_logger.LogDebug("Found {skellyname} ({idx} bone indices) on player: {bones}", kvp.Key, kvp.Value.Any() ? kvp.Value.Max() : 0, string.Join(',', kvp.Value));
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
if (boneIndices.All(u => u.Value.Count == 0)) return;
|
||||
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())
|
||||
@@ -303,12 +341,13 @@ public class PlayerDataFactory
|
||||
|
||||
_logger.LogDebug("Verifying bone indices for {path}, found {x} skeletons", file.ResolvedPath, skeletonIndices.Count);
|
||||
|
||||
foreach (var boneCount in skeletonIndices.Select(k => k).ToList())
|
||||
foreach (var boneCount in skeletonIndices)
|
||||
{
|
||||
if (boneCount.Value.Max() > boneIndices.SelectMany(b => b.Value).Max())
|
||||
var maxAnimationIndex = boneCount.Value.DefaultIfEmpty().Max();
|
||||
if (maxAnimationIndex > maxPlayerBoneIndex)
|
||||
{
|
||||
_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, boneCount.Value.Max(), boneIndices.SelectMany(b => b.Value).Max());
|
||||
file.ResolvedPath, boneCount.Key, maxAnimationIndex, maxPlayerBoneIndex);
|
||||
validationFailed = true;
|
||||
break;
|
||||
}
|
||||
@@ -337,35 +376,115 @@ public class PlayerDataFactory
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<IReadOnlyDictionary<string, string[]>> GetFileReplacementsFromPaths(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 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)
|
||||
{
|
||||
var (objectIndex, forwardResolved, reverseResolved) = await _dalamudUtil.RunOnFrameworkThread(() =>
|
||||
{
|
||||
var idx = handler.GetGameObject()?.ObjectIndex;
|
||||
if (!idx.HasValue)
|
||||
{
|
||||
return ((int?)null, Array.Empty<string>(), Array.Empty<string[]>());
|
||||
}
|
||||
|
||||
var resolvedForward = new string[forwardPaths.Length];
|
||||
for (int i = 0; i < forwardPaths.Length; i++)
|
||||
{
|
||||
resolvedForward[i] = _ipcManager.Penumbra.ResolveGameObjectPath(forwardPaths[i], idx.Value);
|
||||
}
|
||||
|
||||
var resolvedReverse = new string[reversePaths.Length][];
|
||||
for (int i = 0; i < reversePaths.Length; i++)
|
||||
{
|
||||
resolvedReverse[i] = _ipcManager.Penumbra.ReverseResolveGameObjectPath(reversePaths[i], idx.Value);
|
||||
}
|
||||
|
||||
return (idx, resolvedForward, resolvedReverse);
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
if (objectIndex.HasValue)
|
||||
{
|
||||
for (int i = 0; i < forwardPaths.Length; i++)
|
||||
{
|
||||
var filePath = forwardResolved[i]?.ToLowerInvariant();
|
||||
if (string.IsNullOrEmpty(filePath))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (resolvedPaths.TryGetValue(filePath, out var list))
|
||||
{
|
||||
list.Add(forwardPathsLower[i]);
|
||||
}
|
||||
else
|
||||
{
|
||||
resolvedPaths[filePath] = [forwardPathsLower[i]];
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < reversePaths.Length; i++)
|
||||
{
|
||||
var filePath = reversePathsLower[i];
|
||||
var reverseResolvedLower = new string[reverseResolved[i].Length];
|
||||
for (var j = 0; j < reverseResolvedLower.Length; j++)
|
||||
{
|
||||
reverseResolvedLower[j] = reverseResolved[i][j].ToLowerInvariant();
|
||||
}
|
||||
if (resolvedPaths.TryGetValue(filePath, out var list))
|
||||
{
|
||||
list.AddRange(reverseResolvedLower);
|
||||
}
|
||||
else
|
||||
{
|
||||
resolvedPaths[filePath] = new List<string>(reverseResolvedLower);
|
||||
}
|
||||
}
|
||||
|
||||
return resolvedPaths.ToDictionary(k => k.Key, k => k.Value.ToArray(), StringComparer.OrdinalIgnoreCase).AsReadOnly();
|
||||
}
|
||||
}
|
||||
|
||||
var (forward, reverse) = await _ipcManager.Penumbra.ResolvePathsAsync(forwardPaths, reversePaths).ConfigureAwait(false);
|
||||
for (int i = 0; i < forwardPaths.Length; i++)
|
||||
{
|
||||
var filePath = forward[i].ToLowerInvariant();
|
||||
if (resolvedPaths.TryGetValue(filePath, out var list))
|
||||
{
|
||||
list.Add(forwardPaths[i].ToLowerInvariant());
|
||||
list.Add(forwardPathsLower[i]);
|
||||
}
|
||||
else
|
||||
{
|
||||
resolvedPaths[filePath] = [forwardPaths[i].ToLowerInvariant()];
|
||||
resolvedPaths[filePath] = [forwardPathsLower[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))
|
||||
{
|
||||
list.AddRange(reverse[i].Select(c => c.ToLowerInvariant()));
|
||||
list.AddRange(reverseResolvedLower);
|
||||
}
|
||||
else
|
||||
{
|
||||
resolvedPaths[filePath] = new List<string>(reverse[i].Select(c => c.ToLowerInvariant()).ToList());
|
||||
resolvedPaths[filePath] = new List<string>(reverseResolvedLower);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,8 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
||||
private readonly Func<IntPtr> _getAddress;
|
||||
private readonly bool _isOwnedObject;
|
||||
private readonly PerformanceCollectorService _performanceCollector;
|
||||
private readonly object _frameworkUpdateGate = new();
|
||||
private bool _frameworkUpdateSubscribed;
|
||||
private byte _classJob = 0;
|
||||
private Task? _delayedZoningTask;
|
||||
private bool _haltProcessing = false;
|
||||
@@ -47,7 +49,10 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
||||
});
|
||||
}
|
||||
|
||||
Mediator.Subscribe<FrameworkUpdateMessage>(this, (_) => FrameworkUpdate());
|
||||
if (_isOwnedObject)
|
||||
{
|
||||
EnableFrameworkUpdates();
|
||||
}
|
||||
|
||||
Mediator.Subscribe<ZoneSwitchEndMessage>(this, (_) => ZoneSwitchEnd());
|
||||
Mediator.Subscribe<ZoneSwitchStartMessage>(this, (_) => ZoneSwitchStart());
|
||||
@@ -109,7 +114,7 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
||||
{
|
||||
while (await _dalamudUtil.RunOnFrameworkThread(() =>
|
||||
{
|
||||
if (_haltProcessing) CheckAndUpdateObject();
|
||||
EnsureLatestObjectState();
|
||||
if (CurrentDrawCondition != DrawCondition.None) return true;
|
||||
var gameObj = _dalamudUtil.CreateGameObject(Address);
|
||||
if (gameObj is Dalamud.Game.ClientState.Objects.Types.ICharacter chara)
|
||||
@@ -148,6 +153,11 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
||||
_haltProcessing = false;
|
||||
}
|
||||
|
||||
public void Refresh()
|
||||
{
|
||||
_dalamudUtil.RunOnFrameworkThread(CheckAndUpdateObject).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public async Task<bool> IsBeingDrawnRunOnFrameworkAsync()
|
||||
{
|
||||
return await _dalamudUtil.RunOnFrameworkThread(IsBeingDrawn).ConfigureAwait(false);
|
||||
@@ -361,7 +371,7 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
||||
|
||||
private bool IsBeingDrawn()
|
||||
{
|
||||
if (_haltProcessing) CheckAndUpdateObject();
|
||||
EnsureLatestObjectState();
|
||||
|
||||
if (_dalamudUtil.IsAnythingDrawing)
|
||||
{
|
||||
@@ -373,6 +383,28 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
|
||||
return CurrentDrawCondition != DrawCondition.None;
|
||||
}
|
||||
|
||||
private void EnsureLatestObjectState()
|
||||
{
|
||||
if (_haltProcessing || !_frameworkUpdateSubscribed)
|
||||
{
|
||||
CheckAndUpdateObject();
|
||||
}
|
||||
}
|
||||
|
||||
private void EnableFrameworkUpdates()
|
||||
{
|
||||
lock (_frameworkUpdateGate)
|
||||
{
|
||||
if (_frameworkUpdateSubscribed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Mediator.Subscribe<FrameworkUpdateMessage>(this, _ => FrameworkUpdate());
|
||||
_frameworkUpdateSubscribed = true;
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe DrawCondition IsBeingDrawnUnsafe()
|
||||
{
|
||||
if (Address == IntPtr.Zero) return DrawCondition.ObjectZero;
|
||||
|
||||
@@ -1,36 +1,43 @@
|
||||
using LightlessSync.API.Data;
|
||||
using LightlessSync.API.Data;
|
||||
|
||||
namespace LightlessSync.PlayerData.Pairs;
|
||||
namespace LightlessSync.PlayerData.Pairs;
|
||||
|
||||
/// <summary>
|
||||
/// orchestrates the lifecycle of a paired character
|
||||
/// </summary>
|
||||
public interface IPairHandlerAdapter : IDisposable, IPairPerformanceSubject
|
||||
{
|
||||
new string Ident { get; }
|
||||
bool Initialized { get; }
|
||||
bool IsVisible { get; }
|
||||
bool ScheduledForDeletion { get; set; }
|
||||
CharacterData? LastReceivedCharacterData { get; }
|
||||
long LastAppliedDataBytes { get; }
|
||||
new string? PlayerName { get; }
|
||||
string PlayerNameHash { get; }
|
||||
uint PlayerCharacterId { get; }
|
||||
DateTime? LastDataReceivedAt { get; }
|
||||
DateTime? LastApplyAttemptAt { get; }
|
||||
DateTime? LastSuccessfulApplyAt { get; }
|
||||
string? LastFailureReason { get; }
|
||||
IReadOnlyList<string> LastBlockingConditions { get; }
|
||||
bool IsApplying { get; }
|
||||
bool IsDownloading { get; }
|
||||
int PendingDownloadCount { get; }
|
||||
int ForbiddenDownloadCount { get; }
|
||||
/// <summary>
|
||||
/// orchestrates the lifecycle of a paired character
|
||||
/// </summary>
|
||||
public interface IPairHandlerAdapter : IDisposable, IPairPerformanceSubject
|
||||
{
|
||||
new string Ident { get; }
|
||||
bool Initialized { get; }
|
||||
bool IsVisible { get; }
|
||||
bool ScheduledForDeletion { get; set; }
|
||||
CharacterData? LastReceivedCharacterData { get; }
|
||||
long LastAppliedDataBytes { get; }
|
||||
new string? PlayerName { get; }
|
||||
string PlayerNameHash { get; }
|
||||
uint PlayerCharacterId { get; }
|
||||
DateTime? LastDataReceivedAt { get; }
|
||||
DateTime? LastApplyAttemptAt { get; }
|
||||
DateTime? LastSuccessfulApplyAt { get; }
|
||||
string? LastFailureReason { get; }
|
||||
IReadOnlyList<string> LastBlockingConditions { get; }
|
||||
bool IsApplying { get; }
|
||||
bool IsDownloading { get; }
|
||||
int PendingDownloadCount { get; }
|
||||
int ForbiddenDownloadCount { get; }
|
||||
bool PendingModReapply { get; }
|
||||
bool ModApplyDeferred { get; }
|
||||
int MissingCriticalMods { get; }
|
||||
int MissingNonCriticalMods { get; }
|
||||
int MissingForbiddenMods { get; }
|
||||
DateTime? InvisibleSinceUtc { get; }
|
||||
DateTime? VisibilityEvictionDueAtUtc { get; }
|
||||
|
||||
void Initialize();
|
||||
void ApplyData(CharacterData data);
|
||||
void ApplyLastReceivedData(bool forced = false);
|
||||
bool FetchPerformanceMetricsFromCache();
|
||||
void LoadCachedCharacterData(CharacterData data);
|
||||
void SetUploading(bool uploading);
|
||||
void SetPaused(bool paused);
|
||||
}
|
||||
void ApplyData(CharacterData data);
|
||||
void ApplyLastReceivedData(bool forced = false);
|
||||
bool FetchPerformanceMetricsFromCache();
|
||||
void LoadCachedCharacterData(CharacterData data);
|
||||
void SetUploading(bool uploading);
|
||||
void SetPaused(bool paused);
|
||||
}
|
||||
|
||||
@@ -16,4 +16,5 @@ public interface IPairPerformanceSubject
|
||||
long LastAppliedApproximateVRAMBytes { get; set; }
|
||||
long LastAppliedApproximateEffectiveVRAMBytes { 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 long LastAppliedDataBytes => TryGetHandler()?.LastAppliedDataBytes ?? -1;
|
||||
public long LastAppliedDataTris => TryGetHandler()?.LastAppliedDataTris ?? -1;
|
||||
public long LastAppliedApproximateEffectiveTris => TryGetHandler()?.LastAppliedApproximateEffectiveTris ?? -1;
|
||||
public long LastAppliedApproximateVRAMBytes => TryGetHandler()?.LastAppliedApproximateVRAMBytes ?? -1;
|
||||
public long LastAppliedApproximateEffectiveVRAMBytes => TryGetHandler()?.LastAppliedApproximateEffectiveVRAMBytes ?? -1;
|
||||
public string Ident => TryGetHandler()?.Ident ?? TryGetConnection()?.Ident ?? string.Empty;
|
||||
@@ -87,22 +88,25 @@ public class Pair
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.Target is not MenuTargetDefault target || target.TargetObjectId != handler.PlayerCharacterId || IsPaused)
|
||||
if (args.Target is not MenuTargetDefault target || target.TargetObjectId != handler.PlayerCharacterId)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
UiSharedService.AddContextMenuItem(args, name: "Open Profile", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () =>
|
||||
if (!IsPaused)
|
||||
{
|
||||
_mediator.Publish(new ProfileOpenStandaloneMessage(this));
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
UiSharedService.AddContextMenuItem(args, name: "Open Profile", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () =>
|
||||
{
|
||||
_mediator.Publish(new ProfileOpenStandaloneMessage(this));
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
|
||||
UiSharedService.AddContextMenuItem(args, name: "Reapply last data", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () =>
|
||||
{
|
||||
ApplyLastReceivedData(forced: true);
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
UiSharedService.AddContextMenuItem(args, name: "Reapply last data", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () =>
|
||||
{
|
||||
ApplyLastReceivedData(forced: true);
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
}
|
||||
|
||||
UiSharedService.AddContextMenuItem(args, name: "Change Permissions", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () =>
|
||||
{
|
||||
@@ -110,7 +114,24 @@ public class Pair
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
|
||||
UiSharedService.AddContextMenuItem(args, name: "Cycle pause state", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () =>
|
||||
if (IsPaused)
|
||||
{
|
||||
UiSharedService.AddContextMenuItem(args, name: "Toggle Unpause State", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () =>
|
||||
{
|
||||
_ = _apiController.Value.UnpauseAsync(UserData);
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
UiSharedService.AddContextMenuItem(args, name: "Toggle Pause State", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () =>
|
||||
{
|
||||
_ = _apiController.Value.PauseAsync(UserData);
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
}
|
||||
|
||||
UiSharedService.AddContextMenuItem(args, name: "Cycle Pause State", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () =>
|
||||
{
|
||||
TriggerCyclePause();
|
||||
return Task.CompletedTask;
|
||||
@@ -194,9 +215,13 @@ public class Pair
|
||||
{
|
||||
var handler = TryGetHandler();
|
||||
if (handler is null)
|
||||
{
|
||||
return PairDebugInfo.Empty;
|
||||
}
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
var dueAt = handler.VisibilityEvictionDueAtUtc;
|
||||
var remainingSeconds = dueAt.HasValue
|
||||
? Math.Max(0, (dueAt.Value - now).TotalSeconds)
|
||||
: (double?)null;
|
||||
|
||||
return new PairDebugInfo(
|
||||
true,
|
||||
@@ -206,11 +231,19 @@ public class Pair
|
||||
handler.LastDataReceivedAt,
|
||||
handler.LastApplyAttemptAt,
|
||||
handler.LastSuccessfulApplyAt,
|
||||
handler.InvisibleSinceUtc,
|
||||
handler.VisibilityEvictionDueAtUtc,
|
||||
remainingSeconds,
|
||||
handler.LastFailureReason,
|
||||
handler.LastBlockingConditions,
|
||||
handler.IsApplying,
|
||||
handler.IsDownloading,
|
||||
handler.PendingDownloadCount,
|
||||
handler.ForbiddenDownloadCount);
|
||||
handler.ForbiddenDownloadCount,
|
||||
handler.PendingModReapply,
|
||||
handler.ModApplyDeferred,
|
||||
handler.MissingCriticalMods,
|
||||
handler.MissingNonCriticalMods,
|
||||
handler.MissingForbiddenMods);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,6 +125,7 @@ public sealed partial class PairCoordinator
|
||||
}
|
||||
}
|
||||
|
||||
_mediator.Publish(new PairOnlineMessage(new PairUniqueIdentifier(dto.User.UID)));
|
||||
PublishPairDataChanged();
|
||||
}
|
||||
|
||||
|
||||
@@ -8,12 +8,20 @@ public sealed record PairDebugInfo(
|
||||
DateTime? LastDataReceivedAt,
|
||||
DateTime? LastApplyAttemptAt,
|
||||
DateTime? LastSuccessfulApplyAt,
|
||||
DateTime? InvisibleSinceUtc,
|
||||
DateTime? VisibilityEvictionDueAtUtc,
|
||||
double? VisibilityEvictionRemainingSeconds,
|
||||
string? LastFailureReason,
|
||||
IReadOnlyList<string> BlockingConditions,
|
||||
bool IsApplying,
|
||||
bool IsDownloading,
|
||||
int PendingDownloadCount,
|
||||
int ForbiddenDownloadCount)
|
||||
int ForbiddenDownloadCount,
|
||||
bool PendingModReapply,
|
||||
bool ModApplyDeferred,
|
||||
int MissingCriticalMods,
|
||||
int MissingNonCriticalMods,
|
||||
int MissingForbiddenMods)
|
||||
{
|
||||
public static PairDebugInfo Empty { get; } = new(
|
||||
false,
|
||||
@@ -24,9 +32,17 @@ public sealed record PairDebugInfo(
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
Array.Empty<string>(),
|
||||
false,
|
||||
false,
|
||||
0,
|
||||
0,
|
||||
false,
|
||||
false,
|
||||
0,
|
||||
0,
|
||||
0);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,11 +1,15 @@
|
||||
using LightlessSync.FileCache;
|
||||
using LightlessSync.Interop.Ipc;
|
||||
using LightlessSync.LightlessConfiguration;
|
||||
using LightlessSync.PlayerData.Factories;
|
||||
using LightlessSync.Services;
|
||||
using LightlessSync.Services.ActorTracking;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using LightlessSync.Services.ModelDecimation;
|
||||
using LightlessSync.Services.PairProcessing;
|
||||
using LightlessSync.Services.ServerConfiguration;
|
||||
using LightlessSync.Services.TextureCompression;
|
||||
using Dalamud.Plugin.Services;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -24,12 +28,16 @@ internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly IHostApplicationLifetime _lifetime;
|
||||
private readonly FileCacheManager _fileCacheManager;
|
||||
private readonly PlayerPerformanceConfigService _playerPerformanceConfigService;
|
||||
private readonly PlayerPerformanceService _playerPerformanceService;
|
||||
private readonly PairProcessingLimiter _pairProcessingLimiter;
|
||||
private readonly ServerConfigurationManager _serverConfigManager;
|
||||
private readonly TextureDownscaleService _textureDownscaleService;
|
||||
private readonly ModelDecimationService _modelDecimationService;
|
||||
private readonly PairStateCache _pairStateCache;
|
||||
private readonly PairPerformanceMetricsCache _pairPerformanceMetricsCache;
|
||||
private readonly PenumbraTempCollectionJanitor _tempCollectionJanitor;
|
||||
private readonly IFramework _framework;
|
||||
|
||||
public PairHandlerAdapterFactory(
|
||||
ILoggerFactory loggerFactory,
|
||||
@@ -40,14 +48,18 @@ internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory
|
||||
FileDownloadManagerFactory fileDownloadManagerFactory,
|
||||
PluginWarningNotificationService pluginWarningNotificationManager,
|
||||
IServiceProvider serviceProvider,
|
||||
IFramework framework,
|
||||
IHostApplicationLifetime lifetime,
|
||||
FileCacheManager fileCacheManager,
|
||||
PlayerPerformanceConfigService playerPerformanceConfigService,
|
||||
PlayerPerformanceService playerPerformanceService,
|
||||
PairProcessingLimiter pairProcessingLimiter,
|
||||
ServerConfigurationManager serverConfigManager,
|
||||
TextureDownscaleService textureDownscaleService,
|
||||
ModelDecimationService modelDecimationService,
|
||||
PairStateCache pairStateCache,
|
||||
PairPerformanceMetricsCache pairPerformanceMetricsCache)
|
||||
PairPerformanceMetricsCache pairPerformanceMetricsCache,
|
||||
PenumbraTempCollectionJanitor tempCollectionJanitor)
|
||||
{
|
||||
_loggerFactory = loggerFactory;
|
||||
_mediator = mediator;
|
||||
@@ -57,20 +69,25 @@ internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory
|
||||
_fileDownloadManagerFactory = fileDownloadManagerFactory;
|
||||
_pluginWarningNotificationManager = pluginWarningNotificationManager;
|
||||
_serviceProvider = serviceProvider;
|
||||
_framework = framework;
|
||||
_lifetime = lifetime;
|
||||
_fileCacheManager = fileCacheManager;
|
||||
_playerPerformanceConfigService = playerPerformanceConfigService;
|
||||
_playerPerformanceService = playerPerformanceService;
|
||||
_pairProcessingLimiter = pairProcessingLimiter;
|
||||
_serverConfigManager = serverConfigManager;
|
||||
_textureDownscaleService = textureDownscaleService;
|
||||
_modelDecimationService = modelDecimationService;
|
||||
_pairStateCache = pairStateCache;
|
||||
_pairPerformanceMetricsCache = pairPerformanceMetricsCache;
|
||||
_tempCollectionJanitor = tempCollectionJanitor;
|
||||
}
|
||||
|
||||
public IPairHandlerAdapter Create(string ident)
|
||||
{
|
||||
var downloadManager = _fileDownloadManagerFactory.Create();
|
||||
var dalamudUtilService = _serviceProvider.GetRequiredService<DalamudUtilService>();
|
||||
var actorObjectService = _serviceProvider.GetRequiredService<ActorObjectService>();
|
||||
return new PairHandlerAdapter(
|
||||
_loggerFactory.CreateLogger<PairHandlerAdapter>(),
|
||||
_mediator,
|
||||
@@ -81,13 +98,18 @@ internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory
|
||||
downloadManager,
|
||||
_pluginWarningNotificationManager,
|
||||
dalamudUtilService,
|
||||
_framework,
|
||||
actorObjectService,
|
||||
_lifetime,
|
||||
_fileCacheManager,
|
||||
_playerPerformanceConfigService,
|
||||
_playerPerformanceService,
|
||||
_pairProcessingLimiter,
|
||||
_serverConfigManager,
|
||||
_textureDownscaleService,
|
||||
_modelDecimationService,
|
||||
_pairStateCache,
|
||||
_pairPerformanceMetricsCache);
|
||||
_pairPerformanceMetricsCache,
|
||||
_tempCollectionJanitor);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,9 @@ public sealed class PairHandlerRegistry : IDisposable
|
||||
{
|
||||
private readonly object _gate = new();
|
||||
private readonly object _pendingGate = new();
|
||||
private readonly object _visibilityGate = new();
|
||||
private readonly Dictionary<string, PairHandlerEntry> _entriesByIdent = new(StringComparer.Ordinal);
|
||||
private readonly Dictionary<string, CancellationTokenSource> _pendingInvisibleEvictions = new(StringComparer.Ordinal);
|
||||
private readonly Dictionary<IPairHandlerAdapter, PairHandlerEntry> _entriesByHandler = new(ReferenceEqualityComparer.Instance);
|
||||
|
||||
private readonly IPairHandlerAdapterFactory _handlerFactory;
|
||||
@@ -87,7 +89,7 @@ public sealed class PairHandlerRegistry : IDisposable
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -144,6 +146,37 @@ public sealed class PairHandlerRegistry : IDisposable
|
||||
return PairOperationResult<PairUniqueIdentifier>.Ok(registration.PairIdent);
|
||||
}
|
||||
|
||||
private PairOperationResult CancelAllInvisibleEvictions()
|
||||
{
|
||||
List<CancellationTokenSource> snapshot;
|
||||
lock (_visibilityGate)
|
||||
{
|
||||
snapshot = [.. _pendingInvisibleEvictions.Values];
|
||||
_pendingInvisibleEvictions.Clear();
|
||||
}
|
||||
|
||||
List<string>? errors = null;
|
||||
|
||||
foreach (var cts in snapshot)
|
||||
{
|
||||
try { cts.Cancel(); }
|
||||
catch (Exception ex)
|
||||
{
|
||||
(errors ??= new List<string>()).Add($"Cancel: {ex.Message}");
|
||||
}
|
||||
|
||||
try { cts.Dispose(); }
|
||||
catch (Exception ex)
|
||||
{
|
||||
(errors ??= new List<string>()).Add($"Dispose: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
return errors is null
|
||||
? PairOperationResult.Ok()
|
||||
: PairOperationResult.Fail($"CancelAllInvisibleEvictions had error(s): {string.Join(" | ", errors)}");
|
||||
}
|
||||
|
||||
public PairOperationResult ApplyCharacterData(PairRegistration registration, OnlineUserCharaDataDto dto)
|
||||
{
|
||||
if (registration.CharacterIdent is null)
|
||||
@@ -300,6 +333,7 @@ public sealed class PairHandlerRegistry : IDisposable
|
||||
lock (_gate)
|
||||
{
|
||||
handlers = _entriesByHandler.Keys.ToList();
|
||||
CancelAllInvisibleEvictions();
|
||||
_entriesByIdent.Clear();
|
||||
_entriesByHandler.Clear();
|
||||
}
|
||||
@@ -332,6 +366,7 @@ public sealed class PairHandlerRegistry : IDisposable
|
||||
lock (_gate)
|
||||
{
|
||||
handlers = _entriesByHandler.Keys.ToList();
|
||||
CancelAllInvisibleEvictions();
|
||||
_entriesByIdent.Clear();
|
||||
_entriesByHandler.Clear();
|
||||
}
|
||||
|
||||
@@ -258,7 +258,8 @@ public sealed class PairLedger : DisposableMediatorSubscriberBase
|
||||
|
||||
if (handler.LastAppliedApproximateVRAMBytes >= 0
|
||||
&& handler.LastAppliedDataTris >= 0
|
||||
&& handler.LastAppliedApproximateEffectiveVRAMBytes >= 0)
|
||||
&& handler.LastAppliedApproximateEffectiveVRAMBytes >= 0
|
||||
&& handler.LastAppliedApproximateEffectiveTris >= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,8 @@ namespace LightlessSync.PlayerData.Pairs;
|
||||
public readonly record struct PairPerformanceMetrics(
|
||||
long TriangleCount,
|
||||
long ApproximateVramBytes,
|
||||
long ApproximateEffectiveVramBytes);
|
||||
long ApproximateEffectiveVramBytes,
|
||||
long ApproximateEffectiveTris);
|
||||
|
||||
/// <summary>
|
||||
/// caches performance metrics keyed by pair ident
|
||||
|
||||
@@ -50,6 +50,7 @@ public class VisibleUserDataDistributor : DisposableMediatorSubscriberBase
|
||||
});
|
||||
|
||||
Mediator.Subscribe<ConnectedMessage>(this, (_) => PushToAllVisibleUsers());
|
||||
Mediator.Subscribe<PairOnlineMessage>(this, (msg) => HandlePairOnline(msg.PairIdent));
|
||||
Mediator.Subscribe<DisconnectedMessage>(this, (_) =>
|
||||
{
|
||||
_fileTransferManager.CancelUpload();
|
||||
@@ -111,6 +112,20 @@ public class VisibleUserDataDistributor : DisposableMediatorSubscriberBase
|
||||
_ = 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)
|
||||
{
|
||||
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)];
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Dalamud.Game;
|
||||
using Dalamud.Game.ClientState.Objects;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.ImGuiFileDialog;
|
||||
using Dalamud.Interface.Windowing;
|
||||
using Dalamud.Plugin;
|
||||
@@ -39,6 +40,8 @@ using System.Reflection;
|
||||
using OtterTex;
|
||||
using LightlessSync.Services.LightFinder;
|
||||
using LightlessSync.Services.PairProcessing;
|
||||
using LightlessSync.Services.ModelDecimation;
|
||||
using LightlessSync.UI.Models;
|
||||
|
||||
namespace LightlessSync;
|
||||
|
||||
@@ -50,7 +53,7 @@ public sealed class Plugin : IDalamudPlugin
|
||||
IFramework framework, IObjectTable objectTable, IClientState clientState, ICondition condition, IChatGui chatGui,
|
||||
IGameGui gameGui, IDtrBar dtrBar, IPluginLog pluginLog, ITargetManager targetManager, INotificationManager notificationManager,
|
||||
ITextureProvider textureProvider, IContextMenu contextMenu, IGameInteropProvider gameInteropProvider, IGameConfig gameConfig,
|
||||
ISigScanner sigScanner, INamePlateGui namePlateGui, IAddonLifecycle addonLifecycle)
|
||||
ISigScanner sigScanner, INamePlateGui namePlateGui, IAddonLifecycle addonLifecycle, IPlayerState playerState)
|
||||
{
|
||||
NativeDll.Initialize(pluginInterface.AssemblyLocation.DirectoryName);
|
||||
if (!Directory.Exists(pluginInterface.ConfigDirectory.FullName))
|
||||
@@ -103,8 +106,11 @@ public sealed class Plugin : IDalamudPlugin
|
||||
services.AddSingleton(new WindowSystem("LightlessSync"));
|
||||
services.AddSingleton<FileDialogManager>();
|
||||
services.AddSingleton(new Dalamud.Localization("LightlessSync.Localization.", string.Empty, useEmbedded: true));
|
||||
services.AddSingleton(framework);
|
||||
services.AddSingleton(gameGui);
|
||||
services.AddSingleton(gameInteropProvider);
|
||||
services.AddSingleton(addonLifecycle);
|
||||
services.AddSingleton<IUiBuilder>(pluginInterface.UiBuilder);
|
||||
|
||||
// Core singletons
|
||||
services.AddSingleton<LightlessMediator>();
|
||||
@@ -113,13 +119,16 @@ public sealed class Plugin : IDalamudPlugin
|
||||
services.AddSingleton<ProfileTagService>();
|
||||
services.AddSingleton<ApiController>();
|
||||
services.AddSingleton<PerformanceCollectorService>();
|
||||
services.AddSingleton<NameplateUpdateHookService>();
|
||||
services.AddSingleton<HubFactory>();
|
||||
services.AddSingleton<FileUploadManager>();
|
||||
services.AddSingleton<FileTransferOrchestrator>();
|
||||
services.AddSingleton<FileDownloadDeduplicator>();
|
||||
services.AddSingleton<LightlessPlugin>();
|
||||
services.AddSingleton<LightlessProfileManager>();
|
||||
services.AddSingleton<TextureCompressionService>();
|
||||
services.AddSingleton<TextureDownscaleService>();
|
||||
services.AddSingleton<ModelDecimationService>();
|
||||
services.AddSingleton<GameObjectHandlerFactory>();
|
||||
services.AddSingleton<FileDownloadManagerFactory>();
|
||||
services.AddSingleton<PairProcessingLimiter>();
|
||||
@@ -131,8 +140,11 @@ public sealed class Plugin : IDalamudPlugin
|
||||
services.AddSingleton<TagHandler>();
|
||||
services.AddSingleton<PairRequestService>();
|
||||
services.AddSingleton<ZoneChatService>();
|
||||
services.AddSingleton<ChatEmoteService>();
|
||||
services.AddSingleton<IdDisplayHandler>();
|
||||
services.AddSingleton<PlayerPerformanceService>();
|
||||
services.AddSingleton<PenumbraTempCollectionJanitor>();
|
||||
services.AddSingleton<LocationShareService>();
|
||||
|
||||
services.AddSingleton<TextureMetadataHelper>(sp =>
|
||||
new TextureMetadataHelper(sp.GetRequiredService<ILogger<TextureMetadataHelper>>(), gameData));
|
||||
@@ -169,7 +181,8 @@ public sealed class Plugin : IDalamudPlugin
|
||||
|
||||
services.AddSingleton(sp => new BlockedCharacterHandler(
|
||||
sp.GetRequiredService<ILogger<BlockedCharacterHandler>>(),
|
||||
gameInteropProvider));
|
||||
gameInteropProvider,
|
||||
objectTable));
|
||||
|
||||
services.AddSingleton(sp => new IpcProvider(
|
||||
sp.GetRequiredService<ILogger<IpcProvider>>(),
|
||||
@@ -199,6 +212,7 @@ public sealed class Plugin : IDalamudPlugin
|
||||
gameInteropProvider,
|
||||
objectTable,
|
||||
clientState,
|
||||
condition,
|
||||
sp.GetRequiredService<LightlessMediator>()));
|
||||
|
||||
services.AddSingleton(sp => new DalamudUtilService(
|
||||
@@ -211,6 +225,7 @@ public sealed class Plugin : IDalamudPlugin
|
||||
gameData,
|
||||
targetManager,
|
||||
gameConfig,
|
||||
playerState,
|
||||
sp.GetRequiredService<ActorObjectService>(),
|
||||
sp.GetRequiredService<BlockedCharacterHandler>(),
|
||||
sp.GetRequiredService<LightlessMediator>(),
|
||||
@@ -265,6 +280,7 @@ public sealed class Plugin : IDalamudPlugin
|
||||
sp.GetRequiredService<ILogger<LightFinderPlateHandler>>(),
|
||||
addonLifecycle,
|
||||
gameGui,
|
||||
clientState,
|
||||
sp.GetRequiredService<LightlessConfigService>(),
|
||||
sp.GetRequiredService<LightlessMediator>(),
|
||||
objectTable,
|
||||
@@ -272,12 +288,22 @@ public sealed class Plugin : IDalamudPlugin
|
||||
pluginInterface,
|
||||
sp.GetRequiredService<PictomancyService>()));
|
||||
|
||||
services.AddSingleton(sp => new LightFinderNativePlateHandler(
|
||||
sp.GetRequiredService<ILogger<LightFinderNativePlateHandler>>(),
|
||||
clientState,
|
||||
sp.GetRequiredService<LightlessConfigService>(),
|
||||
sp.GetRequiredService<LightlessMediator>(),
|
||||
objectTable,
|
||||
sp.GetRequiredService<PairUiService>(),
|
||||
sp.GetRequiredService<NameplateUpdateHookService>()));
|
||||
|
||||
services.AddSingleton(sp => new LightFinderScannerService(
|
||||
sp.GetRequiredService<ILogger<LightFinderScannerService>>(),
|
||||
framework,
|
||||
sp.GetRequiredService<LightFinderService>(),
|
||||
sp.GetRequiredService<LightlessMediator>(),
|
||||
sp.GetRequiredService<LightFinderPlateHandler>(),
|
||||
sp.GetRequiredService<LightFinderNativePlateHandler>(),
|
||||
sp.GetRequiredService<ActorObjectService>()));
|
||||
|
||||
services.AddSingleton(sp => new ContextMenuService(
|
||||
@@ -295,7 +321,10 @@ public sealed class Plugin : IDalamudPlugin
|
||||
sp.GetRequiredService<LightFinderScannerService>(),
|
||||
sp.GetRequiredService<LightFinderService>(),
|
||||
sp.GetRequiredService<LightlessProfileManager>(),
|
||||
sp.GetRequiredService<LightlessMediator>()));
|
||||
sp.GetRequiredService<LightlessMediator>(),
|
||||
chatGui,
|
||||
sp.GetRequiredService<NotificationService>())
|
||||
);
|
||||
|
||||
// IPC callers / manager
|
||||
services.AddSingleton(sp => new IpcCallerPenumbra(
|
||||
@@ -456,7 +485,8 @@ public sealed class Plugin : IDalamudPlugin
|
||||
sp.GetRequiredService<LightlessConfigService>(),
|
||||
sp.GetRequiredService<UiSharedService>(),
|
||||
sp.GetRequiredService<ApiController>(),
|
||||
sp.GetRequiredService<LightFinderScannerService>()));
|
||||
sp.GetRequiredService<LightFinderScannerService>(),
|
||||
sp.GetRequiredService<LightFinderPlateHandler>()));
|
||||
|
||||
services.AddScoped<WindowMediatorSubscriberBase, SyncshellFinderUI>(sp => new SyncshellFinderUI(
|
||||
sp.GetRequiredService<ILogger<SyncshellFinderUI>>(),
|
||||
@@ -525,9 +555,9 @@ public sealed class Plugin : IDalamudPlugin
|
||||
clientState,
|
||||
gameGui,
|
||||
objectTable,
|
||||
gameInteropProvider,
|
||||
sp.GetRequiredService<LightlessMediator>(),
|
||||
sp.GetRequiredService<PairUiService>()));
|
||||
sp.GetRequiredService<PairUiService>(),
|
||||
sp.GetRequiredService<NameplateUpdateHookService>()));
|
||||
|
||||
// Hosted services
|
||||
services.AddHostedService(sp => sp.GetRequiredService<ConfigurationSaveService>());
|
||||
@@ -546,6 +576,7 @@ public sealed class Plugin : IDalamudPlugin
|
||||
services.AddHostedService(sp => sp.GetRequiredService<ContextMenuService>());
|
||||
services.AddHostedService(sp => sp.GetRequiredService<LightFinderService>());
|
||||
services.AddHostedService(sp => sp.GetRequiredService<LightFinderPlateHandler>());
|
||||
services.AddHostedService(sp => sp.GetRequiredService<LightFinderNativePlateHandler>());
|
||||
}).Build();
|
||||
|
||||
_ = _host.StartAsync();
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,13 +1,16 @@
|
||||
using Dalamud.Game.ClientState.Objects.SubKinds;
|
||||
using Dalamud.Game.ClientState.Objects.SubKinds;
|
||||
using K4os.Compression.LZ4.Legacy;
|
||||
using LightlessSync.API.Data;
|
||||
using LightlessSync.API.Data.Enum;
|
||||
using LightlessSync.API.Dto.CharaData;
|
||||
using LightlessSync.FileCache;
|
||||
using LightlessSync.LightlessConfiguration.Models;
|
||||
using LightlessSync.PlayerData.Factories;
|
||||
using LightlessSync.PlayerData.Handlers;
|
||||
using LightlessSync.Services.CharaData;
|
||||
using LightlessSync.Services.CharaData.Models;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using LightlessSync.UI.Models;
|
||||
using LightlessSync.Utils;
|
||||
using LightlessSync.WebAPI.Files;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -24,10 +27,11 @@ public sealed class CharaDataFileHandler : IDisposable
|
||||
private readonly ILogger<CharaDataFileHandler> _logger;
|
||||
private readonly LightlessCharaFileDataFactory _lightlessCharaFileDataFactory;
|
||||
private readonly PlayerDataFactory _playerDataFactory;
|
||||
private readonly NotificationService _notificationService;
|
||||
private int _globalFileCounter = 0;
|
||||
|
||||
public CharaDataFileHandler(ILogger<CharaDataFileHandler> logger, FileDownloadManagerFactory fileDownloadManagerFactory, FileUploadManager fileUploadManager, FileCacheManager fileCacheManager,
|
||||
DalamudUtilService dalamudUtilService, GameObjectHandlerFactory gameObjectHandlerFactory, PlayerDataFactory playerDataFactory)
|
||||
DalamudUtilService dalamudUtilService, GameObjectHandlerFactory gameObjectHandlerFactory, PlayerDataFactory playerDataFactory, NotificationService notificationService)
|
||||
{
|
||||
_fileDownloadManager = fileDownloadManagerFactory.Create();
|
||||
_logger = logger;
|
||||
@@ -36,6 +40,7 @@ public sealed class CharaDataFileHandler : IDisposable
|
||||
_dalamudUtilService = dalamudUtilService;
|
||||
_gameObjectHandlerFactory = gameObjectHandlerFactory;
|
||||
_playerDataFactory = playerDataFactory;
|
||||
_notificationService = notificationService;
|
||||
_lightlessCharaFileDataFactory = new(fileCacheManager);
|
||||
}
|
||||
|
||||
@@ -248,54 +253,161 @@ public sealed class CharaDataFileHandler : IDisposable
|
||||
}
|
||||
|
||||
internal async Task SaveCharaFileAsync(string description, string filePath)
|
||||
{
|
||||
var createPlayerDataStopwatch = System.Diagnostics.Stopwatch.StartNew();
|
||||
var data = await CreatePlayerData().ConfigureAwait(false);
|
||||
createPlayerDataStopwatch.Stop();
|
||||
_logger.LogInformation("CreatePlayerData took {elapsed}ms", createPlayerDataStopwatch.ElapsedMilliseconds);
|
||||
|
||||
if (data == null) return;
|
||||
|
||||
await Task.Run(async () => await SaveCharaFileAsyncInternal(description, filePath, data).ConfigureAwait(false)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task SaveCharaFileAsyncInternal(string description, string filePath, CharacterData data)
|
||||
{
|
||||
var tempFilePath = filePath + ".tmp";
|
||||
var overallStopwatch = System.Diagnostics.Stopwatch.StartNew();
|
||||
|
||||
try
|
||||
{
|
||||
var data = await CreatePlayerData().ConfigureAwait(false);
|
||||
if (data == null) return;
|
||||
|
||||
var lightlessCharaFileData = _lightlessCharaFileDataFactory.Create(description, data);
|
||||
LightlessCharaFileHeader output = new(LightlessCharaFileHeader.CurrentVersion, lightlessCharaFileData);
|
||||
|
||||
using var fs = new FileStream(tempFilePath, FileMode.Create, FileAccess.ReadWrite, FileShare.None);
|
||||
using var fs = new FileStream(tempFilePath, FileMode.Create, FileAccess.ReadWrite, FileShare.None, bufferSize: 65536, useAsync: false);
|
||||
using var lz4 = new LZ4Stream(fs, LZ4StreamMode.Compress, LZ4StreamFlags.HighCompression);
|
||||
using var writer = new BinaryWriter(lz4);
|
||||
output.WriteToStream(writer);
|
||||
|
||||
int fileIndex = 0;
|
||||
long totalBytesWritten = 0;
|
||||
long totalBytesToWrite = output.CharaFileData.Files.Sum(f => f.Length);
|
||||
var fileWriteStopwatch = System.Diagnostics.Stopwatch.StartNew();
|
||||
const long updateIntervalMs = 1000;
|
||||
|
||||
foreach (var item in output.CharaFileData.Files)
|
||||
{
|
||||
fileIndex++;
|
||||
var fileStopwatch = System.Diagnostics.Stopwatch.StartNew();
|
||||
|
||||
var file = _fileCacheManager.GetFileCacheByHash(item.Hash)!;
|
||||
_logger.LogDebug("Saving to MCDF: {hash}:{file}", item.Hash, file.ResolvedFilepath);
|
||||
_logger.LogDebug("Saving to MCDF [{fileNum}/{totalFiles}]: {hash}:{file}", fileIndex, output.CharaFileData.Files.Count, item.Hash, file.ResolvedFilepath);
|
||||
_logger.LogDebug("\tAssociated GamePaths:");
|
||||
foreach (var path in item.GamePaths)
|
||||
{
|
||||
_logger.LogDebug("\t{path}", path);
|
||||
}
|
||||
|
||||
var fsRead = File.OpenRead(file.ResolvedFilepath);
|
||||
await using (fsRead.ConfigureAwait(false))
|
||||
using var fsRead = File.OpenRead(file.ResolvedFilepath);
|
||||
using var br = new BinaryReader(fsRead);
|
||||
byte[] buffer = new byte[item.Length];
|
||||
int bytesRead = br.Read(buffer, 0, item.Length);
|
||||
|
||||
if (bytesRead != item.Length)
|
||||
{
|
||||
using var br = new BinaryReader(fsRead);
|
||||
byte[] buffer = new byte[item.Length];
|
||||
br.Read(buffer, 0, item.Length);
|
||||
writer.Write(buffer);
|
||||
_logger.LogWarning("Expected to read {expected} bytes but got {actual} bytes from {file}", item.Length, bytesRead, file.ResolvedFilepath);
|
||||
}
|
||||
|
||||
writer.Write(buffer);
|
||||
totalBytesWritten += bytesRead;
|
||||
|
||||
fileStopwatch.Stop();
|
||||
_logger.LogDebug("Wrote file [{fileNum}/{totalFiles}] in {elapsed}ms ({sizeKb}kb)", fileIndex, output.CharaFileData.Files.Count, fileStopwatch.ElapsedMilliseconds, item.Length / 1024);
|
||||
|
||||
if (fileWriteStopwatch.ElapsedMilliseconds >= updateIntervalMs && totalBytesToWrite > 0)
|
||||
{
|
||||
float progress = (float)totalBytesWritten / totalBytesToWrite;
|
||||
var elapsed = overallStopwatch.Elapsed;
|
||||
var eta = CalculateEta(elapsed, progress);
|
||||
|
||||
var notification = new LightlessNotification
|
||||
{
|
||||
Id = "chara_file_save_progress",
|
||||
Title = "Character Data",
|
||||
Message = $"Compressing and saving character file... {(progress * 100):F0}%\nETA: {FormatTimespan(eta)}",
|
||||
Type = NotificationType.Info,
|
||||
Duration = TimeSpan.FromMinutes(5),
|
||||
ShowProgress = true,
|
||||
Progress = progress
|
||||
};
|
||||
|
||||
_notificationService.Mediator.Publish(new LightlessNotificationMessage(notification));
|
||||
|
||||
fileWriteStopwatch.Restart();
|
||||
}
|
||||
}
|
||||
|
||||
var flushStopwatch = System.Diagnostics.Stopwatch.StartNew();
|
||||
writer.Flush();
|
||||
await lz4.FlushAsync().ConfigureAwait(false);
|
||||
await fs.FlushAsync().ConfigureAwait(false);
|
||||
lz4.Flush();
|
||||
fs.Flush();
|
||||
fs.Close();
|
||||
flushStopwatch.Stop();
|
||||
_logger.LogInformation("Flush operations took {elapsed}ms", flushStopwatch.ElapsedMilliseconds);
|
||||
|
||||
var moveStopwatch = System.Diagnostics.Stopwatch.StartNew();
|
||||
File.Move(tempFilePath, filePath, true);
|
||||
moveStopwatch.Stop();
|
||||
_logger.LogInformation("File move took {elapsed}ms", moveStopwatch.ElapsedMilliseconds);
|
||||
|
||||
overallStopwatch.Stop();
|
||||
_logger.LogInformation("SaveCharaFileAsync completed successfully in {elapsed}ms. Total bytes written: {totalBytes}mb", overallStopwatch.ElapsedMilliseconds, totalBytesWritten / (1024 * 1024));
|
||||
|
||||
_notificationService.ShowNotification(
|
||||
"Character Data",
|
||||
"Character file saved successfully!",
|
||||
NotificationType.Info,
|
||||
duration: TimeSpan.FromSeconds(5));
|
||||
|
||||
_notificationService.Mediator.Publish(new LightlessNotificationDismissMessage("chara_file_save_progress"));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failure Saving Lightless Chara File, deleting output");
|
||||
File.Delete(tempFilePath);
|
||||
overallStopwatch.Stop();
|
||||
_logger.LogError(ex, "Failure Saving Lightless Chara File after {elapsed}ms, deleting output", overallStopwatch.ElapsedMilliseconds);
|
||||
try
|
||||
{
|
||||
File.Delete(tempFilePath);
|
||||
}
|
||||
catch (Exception deleteEx)
|
||||
{
|
||||
_logger.LogError(deleteEx, "Failed to delete temporary file {file}", tempFilePath);
|
||||
}
|
||||
|
||||
_notificationService.ShowErrorNotification(
|
||||
"Character Data Save Failed",
|
||||
"Failed to save character file",
|
||||
ex);
|
||||
|
||||
_notificationService.Mediator.Publish(new LightlessNotificationDismissMessage("chara_file_save_progress"));
|
||||
}
|
||||
}
|
||||
|
||||
private static TimeSpan CalculateEta(TimeSpan elapsed, float progress)
|
||||
{
|
||||
if (progress <= 0 || elapsed.TotalSeconds < 0.1)
|
||||
return TimeSpan.Zero;
|
||||
|
||||
double totalSeconds = elapsed.TotalSeconds / progress;
|
||||
double remainingSeconds = totalSeconds - elapsed.TotalSeconds;
|
||||
|
||||
return TimeSpan.FromSeconds(Math.Max(0, remainingSeconds));
|
||||
}
|
||||
|
||||
private static string FormatTimespan(TimeSpan ts)
|
||||
{
|
||||
if (ts.TotalSeconds < 1)
|
||||
return "< 1s";
|
||||
|
||||
if (ts.TotalSeconds < 60)
|
||||
return $"{ts.TotalSeconds:F0}s";
|
||||
|
||||
if (ts.TotalMinutes < 60)
|
||||
return $"{ts.TotalMinutes:F1}m";
|
||||
|
||||
return $"{ts.TotalHours:F1}h";
|
||||
}
|
||||
|
||||
internal async Task<List<string>> UploadFiles(List<string> fileList, ValueProgress<string> uploadProgress, CancellationToken token)
|
||||
{
|
||||
return await _fileUploadManager.UploadFiles(fileList, uploadProgress, token).ConfigureAwait(false);
|
||||
|
||||
@@ -450,7 +450,7 @@ public class CharaDataGposeTogetherManager : DisposableMediatorSubscriberBase
|
||||
};
|
||||
}
|
||||
|
||||
var loc = await _dalamudUtil.GetMapDataAsync().ConfigureAwait(false);
|
||||
var loc = _dalamudUtil.GetMapData();
|
||||
worldData.LocationInfo = loc;
|
||||
|
||||
if (_forceResendWorldData || worldData != _lastWorldData)
|
||||
|
||||
@@ -254,7 +254,7 @@ public sealed partial class CharaDataManager : DisposableMediatorSubscriberBase
|
||||
|
||||
Logger.LogTrace("Attaching World data {data}", worldData);
|
||||
|
||||
worldData.LocationInfo = await _dalamudUtilService.GetMapDataAsync().ConfigureAwait(false);
|
||||
worldData.LocationInfo = _dalamudUtilService.GetMapData();
|
||||
|
||||
Logger.LogTrace("World data serialized: {data}", worldData);
|
||||
|
||||
|
||||
@@ -186,8 +186,8 @@ public sealed class CharaDataNearbyManager : DisposableMediatorSubscriberBase
|
||||
var previousPoses = _nearbyData.Keys.ToList();
|
||||
_nearbyData.Clear();
|
||||
|
||||
var ownLocation = await _dalamudUtilService.RunOnFrameworkThread(() => _dalamudUtilService.GetMapData()).ConfigureAwait(false);
|
||||
var player = await _dalamudUtilService.RunOnFrameworkThread(() => _dalamudUtilService.GetPlayerCharacter()).ConfigureAwait(false);
|
||||
var ownLocation = _dalamudUtilService.GetMapData();
|
||||
var player = await _dalamudUtilService.GetPlayerCharacterAsync().ConfigureAwait(false);
|
||||
var currentServer = player.CurrentWorld;
|
||||
var playerPos = player.Position;
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using FFXIVClientStructs.FFXIV.Client.Graphics.Render;
|
||||
using LightlessSync.API.Data;
|
||||
using LightlessSync.API.Data.Enum;
|
||||
using LightlessSync.FileCache;
|
||||
@@ -98,11 +99,13 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable
|
||||
_analysisCts = null;
|
||||
if (print) PrintAnalysis();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_analysisCts.CancelDispose();
|
||||
_baseAnalysisCts.Dispose();
|
||||
}
|
||||
|
||||
public async Task UpdateFileEntriesAsync(IEnumerable<string> filePaths, CancellationToken token)
|
||||
{
|
||||
var normalized = new HashSet<string>(
|
||||
@@ -125,6 +128,7 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task BaseAnalysis(CharacterData charaData, CancellationToken token)
|
||||
{
|
||||
if (string.Equals(charaData.DataHash.Value, _lastDataHash, StringComparison.Ordinal)) return;
|
||||
@@ -136,29 +140,47 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
|
||||
var fileCacheEntries = (await _fileCacheManager.GetAllFileCachesByHashAsync(fileEntry.Hash, ignoreCacheEntries: true, validate: false, token).ConfigureAwait(false)).ToList();
|
||||
if (fileCacheEntries.Count == 0) continue;
|
||||
var filePath = fileCacheEntries[0].ResolvedFilepath;
|
||||
FileInfo fi = new(filePath);
|
||||
string ext = "unk?";
|
||||
try
|
||||
{
|
||||
ext = fi.Extension[1..];
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogWarning(ex, "Could not identify extension for {path}", filePath);
|
||||
}
|
||||
var fileCacheEntries = (await _fileCacheManager
|
||||
.GetAllFileCachesByHashAsync(fileEntry.Hash, ignoreCacheEntries: true, validate: false, token)
|
||||
.ConfigureAwait(false))
|
||||
.ToList();
|
||||
|
||||
if (fileCacheEntries.Count == 0)
|
||||
continue;
|
||||
|
||||
var resolved = fileCacheEntries[0].ResolvedFilepath;
|
||||
|
||||
var extWithDot = Path.GetExtension(resolved);
|
||||
var ext = string.IsNullOrEmpty(extWithDot) ? "unk?" : extWithDot.TrimStart('.');
|
||||
|
||||
var tris = await _xivDataAnalyzer.GetTrianglesByHash(fileEntry.Hash).ConfigureAwait(false);
|
||||
foreach (var entry in fileCacheEntries)
|
||||
|
||||
var distinctFilePaths = fileCacheEntries
|
||||
.Select(c => c.ResolvedFilepath)
|
||||
.Where(p => !string.IsNullOrWhiteSpace(p))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
|
||||
long orig = 0, comp = 0;
|
||||
var first = fileCacheEntries[0];
|
||||
if (first.Size > 0) orig = first.Size.Value;
|
||||
if (first.CompressedSize > 0) comp = first.CompressedSize.Value;
|
||||
|
||||
if (_fileCacheManager.TryGetSizeInfo(fileEntry.Hash, out var cached))
|
||||
{
|
||||
data[fileEntry.Hash] = new FileDataEntry(fileEntry.Hash, ext,
|
||||
[.. fileEntry.GamePaths],
|
||||
[.. fileCacheEntries.Select(c => c.ResolvedFilepath).Distinct(StringComparer.Ordinal)],
|
||||
entry.Size > 0 ? entry.Size.Value : 0,
|
||||
entry.CompressedSize > 0 ? entry.CompressedSize.Value : 0,
|
||||
tris);
|
||||
if (orig <= 0 && cached.Original > 0) orig = cached.Original;
|
||||
if (comp <= 0 && cached.Compressed > 0) comp = cached.Compressed;
|
||||
}
|
||||
|
||||
data[fileEntry.Hash] = new FileDataEntry(
|
||||
fileEntry.Hash,
|
||||
ext,
|
||||
[.. fileEntry.GamePaths],
|
||||
distinctFilePaths,
|
||||
orig,
|
||||
comp,
|
||||
tris,
|
||||
fileCacheEntries);
|
||||
}
|
||||
LastAnalysis[obj.Key] = data;
|
||||
}
|
||||
@@ -167,6 +189,7 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable
|
||||
Mediator.Publish(new CharacterDataAnalyzedMessage());
|
||||
_lastDataHash = charaData.DataHash.Value;
|
||||
}
|
||||
|
||||
private void RecalculateSummary()
|
||||
{
|
||||
var builder = ImmutableDictionary.CreateBuilder<ObjectKind, CharacterAnalysisObjectSummary>();
|
||||
@@ -192,6 +215,7 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable
|
||||
|
||||
_latestSummary = new CharacterAnalysisSummary(builder.ToImmutable());
|
||||
}
|
||||
|
||||
private void PrintAnalysis()
|
||||
{
|
||||
if (LastAnalysis.Count == 0) return;
|
||||
@@ -235,42 +259,79 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable
|
||||
UiSharedService.ByteToString(LastAnalysis.Values.Sum(c => c.Values.Sum(v => v.CompressedSize))));
|
||||
Logger.LogInformation("IMPORTANT NOTES:\n\r- For Lightless up- and downloads only the compressed size is relevant.\n\r- An unusually high total files count beyond 200 and up will also increase your download time to others significantly.");
|
||||
}
|
||||
internal sealed record FileDataEntry(string Hash, string FileType, List<string> GamePaths, List<string> FilePaths, long OriginalSize, long CompressedSize, long Triangles)
|
||||
{
|
||||
public bool IsComputed => OriginalSize > 0 && CompressedSize > 0;
|
||||
public async Task ComputeSizes(FileCacheManager fileCacheManager, CancellationToken token)
|
||||
{
|
||||
var compressedsize = await fileCacheManager.GetCompressedFileData(Hash, token).ConfigureAwait(false);
|
||||
var normalSize = new FileInfo(FilePaths[0]).Length;
|
||||
var entries = await fileCacheManager.GetAllFileCachesByHashAsync(Hash, ignoreCacheEntries: true, validate: false, token).ConfigureAwait(false);
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
entry.Size = normalSize;
|
||||
entry.CompressedSize = compressedsize.Item2.LongLength;
|
||||
}
|
||||
OriginalSize = normalSize;
|
||||
CompressedSize = compressedsize.Item2.LongLength;
|
||||
RefreshFormat();
|
||||
}
|
||||
public long OriginalSize { get; private set; } = OriginalSize;
|
||||
public long CompressedSize { get; private set; } = CompressedSize;
|
||||
public long Triangles { get; private set; } = Triangles;
|
||||
public Lazy<string> Format => _format ??= CreateFormatValue();
|
||||
|
||||
internal sealed class FileDataEntry
|
||||
{
|
||||
public string Hash { get; }
|
||||
public string FileType { get; }
|
||||
public List<string> GamePaths { get; }
|
||||
public List<string> FilePaths { get; }
|
||||
|
||||
public long OriginalSize { get; private set; }
|
||||
public long CompressedSize { get; private set; }
|
||||
public long Triangles { get; private set; }
|
||||
|
||||
public IReadOnlyList<FileCacheEntity> CacheEntries { get; }
|
||||
|
||||
public bool IsComputed => OriginalSize > 0 && CompressedSize > 0;
|
||||
|
||||
public FileDataEntry(
|
||||
string hash,
|
||||
string fileType,
|
||||
List<string> gamePaths,
|
||||
List<string> filePaths,
|
||||
long originalSize,
|
||||
long compressedSize,
|
||||
long triangles,
|
||||
IReadOnlyList<FileCacheEntity> cacheEntries)
|
||||
{
|
||||
Hash = hash;
|
||||
FileType = fileType;
|
||||
GamePaths = gamePaths;
|
||||
FilePaths = filePaths;
|
||||
OriginalSize = originalSize;
|
||||
CompressedSize = compressedSize;
|
||||
Triangles = triangles;
|
||||
CacheEntries = cacheEntries;
|
||||
}
|
||||
|
||||
public async Task ComputeSizes(FileCacheManager fileCacheManager, CancellationToken token, bool force = false)
|
||||
{
|
||||
if (!force && IsComputed)
|
||||
return;
|
||||
|
||||
if (FilePaths.Count == 0 || string.IsNullOrWhiteSpace(FilePaths[0]))
|
||||
return;
|
||||
|
||||
var path = FilePaths[0];
|
||||
|
||||
if (!File.Exists(path))
|
||||
return;
|
||||
|
||||
var original = new FileInfo(path).Length;
|
||||
|
||||
var compressedLen = await fileCacheManager.GetCompressedSizeAsync(Hash, token).ConfigureAwait(false);
|
||||
|
||||
fileCacheManager.SetSizeInfo(Hash, original, compressedLen);
|
||||
FileCacheManager.ApplySizesToEntries(CacheEntries, original, compressedLen);
|
||||
|
||||
OriginalSize = original;
|
||||
CompressedSize = compressedLen;
|
||||
|
||||
if (string.Equals(FileType, "tex", StringComparison.OrdinalIgnoreCase))
|
||||
RefreshFormat();
|
||||
}
|
||||
|
||||
public Lazy<string> Format => _format ??= CreateFormatValue();
|
||||
private Lazy<string>? _format;
|
||||
|
||||
public void RefreshFormat()
|
||||
{
|
||||
_format = CreateFormatValue();
|
||||
}
|
||||
public void RefreshFormat() => _format = CreateFormatValue();
|
||||
|
||||
private Lazy<string> CreateFormatValue()
|
||||
=> new(() =>
|
||||
{
|
||||
if (!string.Equals(FileType, "tex", StringComparison.Ordinal))
|
||||
{
|
||||
if (!string.Equals(FileType, "tex", StringComparison.OrdinalIgnoreCase))
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
|
||||
770
LightlessSync/Services/Chat/ChatEmoteService.cs
Normal file
770
LightlessSync/Services/Chat/ChatEmoteService.cs
Normal file
@@ -0,0 +1,770 @@
|
||||
using Dalamud.Interface.Textures.TextureWraps;
|
||||
using LightlessSync.LightlessConfiguration;
|
||||
using LightlessSync.UI;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
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;
|
||||
|
||||
public sealed class ChatEmoteService : IDisposable
|
||||
{
|
||||
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 HttpClient _httpClient;
|
||||
private readonly UiSharedService _uiSharedService;
|
||||
private readonly ChatConfigService _chatConfigService;
|
||||
private readonly ConcurrentDictionary<string, EmoteEntry> _emotes = new(StringComparer.Ordinal);
|
||||
private readonly SemaphoreSlim _downloadGate = new(3, 3);
|
||||
|
||||
private readonly object _loadLock = new();
|
||||
private Task? _loadTask;
|
||||
|
||||
public ChatEmoteService(ILogger<ChatEmoteService> logger, HttpClient httpClient, UiSharedService uiSharedService, ChatConfigService chatConfigService)
|
||||
{
|
||||
_logger = logger;
|
||||
_httpClient = httpClient;
|
||||
_uiSharedService = uiSharedService;
|
||||
_chatConfigService = chatConfigService;
|
||||
}
|
||||
|
||||
public void EnsureGlobalEmotesLoaded()
|
||||
{
|
||||
lock (_loadLock)
|
||||
{
|
||||
if (_loadTask is not null && !_loadTask.IsCompleted)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_emotes.Count > 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_loadTask = Task.Run(LoadGlobalEmotesAsync);
|
||||
}
|
||||
}
|
||||
|
||||
public IReadOnlyList<string> GetEmoteNames()
|
||||
{
|
||||
EnsureGlobalEmotesLoaded();
|
||||
var names = _emotes.Keys.ToArray();
|
||||
Array.Sort(names, StringComparer.OrdinalIgnoreCase);
|
||||
return names;
|
||||
}
|
||||
|
||||
public bool TryGetEmote(string code, out IDalamudTextureWrap? texture)
|
||||
{
|
||||
texture = null;
|
||||
EnsureGlobalEmotesLoaded();
|
||||
|
||||
if (!_emotes.TryGetValue(code, out var entry))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var allowAnimation = _chatConfigService.Current.EnableAnimatedEmotes;
|
||||
if (entry.TryGetTexture(allowAnimation, out texture))
|
||||
{
|
||||
if (allowAnimation && entry.NeedsAnimationLoad && !entry.HasAttemptedAnimation)
|
||||
{
|
||||
entry.EnsureLoading(allowAnimation, QueueEmoteDownload, allowWhenStaticLoaded: true);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
entry.EnsureLoading(allowAnimation, QueueEmoteDownload);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var entry in _emotes.Values)
|
||||
{
|
||||
entry.Dispose();
|
||||
}
|
||||
|
||||
_downloadGate.Dispose();
|
||||
}
|
||||
|
||||
private async Task LoadGlobalEmotesAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
using var stream = await _httpClient.GetStreamAsync(GlobalEmoteSetUrl).ConfigureAwait(false);
|
||||
using var document = await JsonDocument.ParseAsync(stream).ConfigureAwait(false);
|
||||
|
||||
if (!document.RootElement.TryGetProperty("emotes", out var emotes))
|
||||
{
|
||||
_logger.LogWarning("7TV emote set response missing emotes array");
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var emoteElement in emotes.EnumerateArray())
|
||||
{
|
||||
if (!emoteElement.TryGetProperty("name", out var nameElement))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var name = nameElement.GetString();
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var source = TryBuildEmoteSource(emoteElement);
|
||||
if (source is null || (!source.Value.HasStatic && !source.Value.HasAnimation))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_emotes.TryAdd(name, new EmoteEntry(name, source.Value));
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to load 7TV emote set");
|
||||
}
|
||||
}
|
||||
|
||||
private static EmoteSource? TryBuildEmoteSource(JsonElement emoteElement)
|
||||
{
|
||||
if (!emoteElement.TryGetProperty("data", out var dataElement))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!dataElement.TryGetProperty("host", out var hostElement))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!hostElement.TryGetProperty("url", out var urlElement))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var baseUrl = urlElement.GetString();
|
||||
if (string.IsNullOrWhiteSpace(baseUrl))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (baseUrl.StartsWith("//", StringComparison.Ordinal))
|
||||
{
|
||||
baseUrl = "https:" + baseUrl;
|
||||
}
|
||||
|
||||
if (!hostElement.TryGetProperty("files", out var filesElement))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var files = ReadEmoteFiles(filesElement);
|
||||
if (files.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
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 BuildEmoteUrl(string baseUrl, string fileName)
|
||||
=> baseUrl.TrimEnd('/') + "/" + fileName;
|
||||
|
||||
private static List<EmoteFile> ReadEmoteFiles(JsonElement filesElement)
|
||||
{
|
||||
var files = new List<EmoteFile>();
|
||||
foreach (var file in filesElement.EnumerateArray())
|
||||
{
|
||||
if (!file.TryGetProperty("name", out var nameElement))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var name = nameElement.GetString();
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
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))
|
||||
{
|
||||
png1x = name;
|
||||
}
|
||||
else if (name.Equals("1x.webp", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
webp1x = name;
|
||||
}
|
||||
else if (name.Equals("1x.gif", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
gif1x = name;
|
||||
}
|
||||
else if (name.EndsWith(".png", StringComparison.OrdinalIgnoreCase) && pngFallback is null)
|
||||
{
|
||||
pngFallback = name;
|
||||
}
|
||||
else if (name.EndsWith(".webp", StringComparison.OrdinalIgnoreCase) && webpFallback is null)
|
||||
{
|
||||
webpFallback = name;
|
||||
}
|
||||
else if (name.EndsWith(".gif", StringComparison.OrdinalIgnoreCase) && gifFallback is null)
|
||||
{
|
||||
gifFallback = name;
|
||||
}
|
||||
}
|
||||
|
||||
return png1x ?? webp1x ?? gif1x ?? pngFallback ?? webpFallback ?? gifFallback;
|
||||
}
|
||||
|
||||
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 () =>
|
||||
{
|
||||
await _downloadGate.WaitAsync().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
if (allowAnimation)
|
||||
{
|
||||
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)
|
||||
{
|
||||
_logger.LogDebug(ex, "Failed to load 7TV emote {Emote}", entry.Code);
|
||||
entry.MarkFailed();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_downloadGate.Release();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async Task<bool> TryLoadAnimatedEmoteAsync(EmoteEntry entry)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(entry.AnimatedUrl))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
if (Interlocked.CompareExchange(ref _loadingState, 1, 0) != 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
queueDownload(this, allowAnimation);
|
||||
}
|
||||
|
||||
public void SetAnimation(EmoteAnimation animation)
|
||||
{
|
||||
_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);
|
||||
}
|
||||
|
||||
public void MarkFailed()
|
||||
{
|
||||
Interlocked.Exchange(ref _loadingState, 0);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_animation?.Dispose();
|
||||
_staticTexture?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
using LightlessSync.API.Dto.Chat;
|
||||
using LightlessSync.API.Data.Extensions;
|
||||
using LightlessSync.Services.ActorTracking;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using LightlessSync.Services.ServerConfiguration;
|
||||
using LightlessSync.WebAPI;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -23,6 +25,8 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
||||
private readonly DalamudUtilService _dalamudUtilService;
|
||||
private readonly ActorObjectService _actorObjectService;
|
||||
private readonly PairUiService _pairUiService;
|
||||
private readonly ChatConfigService _chatConfigService;
|
||||
private readonly ServerConfigurationManager _serverConfigurationManager;
|
||||
|
||||
private readonly Lock _sync = new();
|
||||
|
||||
@@ -35,6 +39,9 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
||||
private readonly Dictionary<string, bool> _lastPresenceStates = new(StringComparer.Ordinal);
|
||||
private readonly Dictionary<string, string> _selfTokens = new(StringComparer.Ordinal);
|
||||
private readonly List<PendingSelfMessage> _pendingSelfMessages = new();
|
||||
private readonly Dictionary<string, List<ChatMessageEntry>> _messageHistoryCache = new(StringComparer.Ordinal);
|
||||
private List<ChatChannelSnapshot>? _cachedChannelSnapshots;
|
||||
private bool _channelsSnapshotDirty = true;
|
||||
|
||||
private bool _isLoggedIn;
|
||||
private bool _isConnected;
|
||||
@@ -50,13 +57,16 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
||||
ApiController apiController,
|
||||
DalamudUtilService dalamudUtilService,
|
||||
ActorObjectService actorObjectService,
|
||||
PairUiService pairUiService)
|
||||
PairUiService pairUiService,
|
||||
ServerConfigurationManager serverConfigurationManager)
|
||||
: base(logger, mediator)
|
||||
{
|
||||
_apiController = apiController;
|
||||
_dalamudUtilService = dalamudUtilService;
|
||||
_actorObjectService = actorObjectService;
|
||||
_pairUiService = pairUiService;
|
||||
_chatConfigService = chatConfigService;
|
||||
_serverConfigurationManager = serverConfigurationManager;
|
||||
|
||||
_isLoggedIn = _dalamudUtilService.IsLoggedIn;
|
||||
_isConnected = _apiController.IsConnected;
|
||||
@@ -67,6 +77,11 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
||||
{
|
||||
using (_sync.EnterScope())
|
||||
{
|
||||
if (!_channelsSnapshotDirty && _cachedChannelSnapshots is not null)
|
||||
{
|
||||
return _cachedChannelSnapshots;
|
||||
}
|
||||
|
||||
var snapshots = new List<ChatChannelSnapshot>(_channelOrder.Count);
|
||||
foreach (var key in _channelOrder)
|
||||
{
|
||||
@@ -96,6 +111,8 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
||||
state.Messages.ToList()));
|
||||
}
|
||||
|
||||
_cachedChannelSnapshots = snapshots;
|
||||
_channelsSnapshotDirty = false;
|
||||
return snapshots;
|
||||
}
|
||||
}
|
||||
@@ -133,6 +150,44 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
||||
state.UnreadCount = 0;
|
||||
_lastReadCounts[key] = state.Messages.Count;
|
||||
}
|
||||
|
||||
MarkChannelsSnapshotDirtyLocked();
|
||||
}
|
||||
}
|
||||
|
||||
public void MoveChannel(string draggedKey, string targetKey)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(draggedKey) || string.IsNullOrWhiteSpace(targetKey))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
bool updated = false;
|
||||
using (_sync.EnterScope())
|
||||
{
|
||||
if (!_channels.ContainsKey(draggedKey) || !_channels.ContainsKey(targetKey))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var fromIndex = _channelOrder.IndexOf(draggedKey);
|
||||
var toIndex = _channelOrder.IndexOf(targetKey);
|
||||
if (fromIndex < 0 || toIndex < 0 || fromIndex == toIndex)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_channelOrder.RemoveAt(fromIndex);
|
||||
var insertIndex = Math.Clamp(toIndex, 0, _channelOrder.Count);
|
||||
_channelOrder.Insert(insertIndex, draggedKey);
|
||||
_chatConfigService.Current.ChannelOrder = new List<string>(_channelOrder);
|
||||
_chatConfigService.Save();
|
||||
updated = true;
|
||||
}
|
||||
|
||||
if (updated)
|
||||
{
|
||||
PublishChannelListChanged();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,6 +203,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
||||
if (!wasEnabled)
|
||||
{
|
||||
_chatEnabled = true;
|
||||
MarkChannelsSnapshotDirtyLocked();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -193,6 +249,8 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
||||
state.IsAvailable = false;
|
||||
state.StatusText = "Chat services disabled";
|
||||
}
|
||||
|
||||
MarkChannelsSnapshotDirtyLocked();
|
||||
}
|
||||
|
||||
UnregisterChatHandler();
|
||||
@@ -512,14 +570,15 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
||||
|
||||
if (!_isLoggedIn || !_apiController.IsConnected)
|
||||
{
|
||||
await LeaveCurrentZoneAsync(force, 0).ConfigureAwait(false);
|
||||
await LeaveCurrentZoneAsync(force, 0, 0).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var location = await _dalamudUtilService.GetMapDataAsync().ConfigureAwait(false);
|
||||
var location = _dalamudUtilService.GetMapData();
|
||||
var territoryId = (ushort)location.TerritoryId;
|
||||
var worldId = (ushort)location.ServerId;
|
||||
|
||||
string? zoneKey;
|
||||
ZoneChannelDefinition? definition = null;
|
||||
@@ -536,14 +595,14 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
||||
|
||||
if (definition is null)
|
||||
{
|
||||
await LeaveCurrentZoneAsync(force, territoryId).ConfigureAwait(false);
|
||||
await LeaveCurrentZoneAsync(force, territoryId, worldId).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
var descriptor = await BuildZoneDescriptorAsync(definition.Value).ConfigureAwait(false);
|
||||
if (descriptor is null)
|
||||
{
|
||||
await LeaveCurrentZoneAsync(force, territoryId).ConfigureAwait(false);
|
||||
await LeaveCurrentZoneAsync(force, territoryId, worldId).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -586,7 +645,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LeaveCurrentZoneAsync(bool force, ushort territoryId)
|
||||
private async Task LeaveCurrentZoneAsync(bool force, ushort territoryId, ushort worldId)
|
||||
{
|
||||
ChatChannelDescriptor? descriptor = null;
|
||||
|
||||
@@ -602,7 +661,27 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
||||
state.StatusText = !_chatEnabled
|
||||
? "Chat services disabled"
|
||||
: (_isConnected ? ZoneUnavailableMessage : "Disconnected from chat server");
|
||||
state.DisplayName = "Zone Chat";
|
||||
if (territoryId != 0
|
||||
&& _dalamudUtilService.TerritoryData.Value.TryGetValue(territoryId, out var territoryName)
|
||||
&& !string.IsNullOrWhiteSpace(territoryName))
|
||||
{
|
||||
state.DisplayName = territoryName;
|
||||
}
|
||||
else
|
||||
{
|
||||
state.DisplayName = "Zone Chat";
|
||||
}
|
||||
|
||||
if (worldId != 0)
|
||||
{
|
||||
state.Descriptor = new ChatChannelDescriptor
|
||||
{
|
||||
Type = ChatChannelType.Zone,
|
||||
WorldId = worldId,
|
||||
ZoneId = territoryId,
|
||||
CustomKey = string.Empty
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (string.Equals(_activeChannelKey, ZoneChannelKey, StringComparison.Ordinal))
|
||||
@@ -623,7 +702,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
||||
{
|
||||
try
|
||||
{
|
||||
var worldId = (ushort)await _dalamudUtilService.GetWorldIdAsync().ConfigureAwait(false);
|
||||
var worldId = (ushort)_dalamudUtilService.GetWorldId();
|
||||
return definition.Descriptor with { WorldId = worldId };
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -658,7 +737,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
||||
_zoneDefinitions[key] = new ZoneChannelDefinition(key, info.DisplayName ?? key, descriptor, territories);
|
||||
}
|
||||
|
||||
var territoryData = _dalamudUtilService.TerritoryData.Value;
|
||||
var territoryData = _dalamudUtilService.TerritoryDataEnglish.Value;
|
||||
foreach (var kvp in territoryData)
|
||||
{
|
||||
foreach (var variant in EnumerateTerritoryKeys(kvp.Value))
|
||||
@@ -702,6 +781,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
||||
using (_sync.EnterScope())
|
||||
{
|
||||
var remainingGroups = new HashSet<string>(_groupDefinitions.Keys, StringComparer.OrdinalIgnoreCase);
|
||||
var allowRemoval = _isConnected;
|
||||
|
||||
foreach (var info in infoList)
|
||||
{
|
||||
@@ -717,18 +797,19 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
||||
var key = BuildChannelKey(descriptor);
|
||||
if (!_channels.TryGetValue(key, out var state))
|
||||
{
|
||||
state = new ChatChannelState(key, ChatChannelType.Group, info.DisplayName ?? groupId, descriptor);
|
||||
state.IsConnected = _chatEnabled && _isConnected;
|
||||
state.IsAvailable = _chatEnabled && _isConnected;
|
||||
state.StatusText = !_chatEnabled
|
||||
? "Chat services disabled"
|
||||
: (_isConnected ? null : "Disconnected from chat server");
|
||||
_channels[key] = state;
|
||||
_lastReadCounts[key] = 0;
|
||||
if (_chatEnabled)
|
||||
{
|
||||
descriptorsToJoin.Add(descriptor);
|
||||
}
|
||||
state = new ChatChannelState(key, ChatChannelType.Group, info.DisplayName ?? groupId, descriptor);
|
||||
var restoredCount = RestoreCachedMessagesLocked(state);
|
||||
state.IsConnected = _chatEnabled && _isConnected;
|
||||
state.IsAvailable = _chatEnabled && _isConnected;
|
||||
state.StatusText = !_chatEnabled
|
||||
? "Chat services disabled"
|
||||
: (_isConnected ? null : "Disconnected from chat server");
|
||||
_channels[key] = state;
|
||||
_lastReadCounts[key] = restoredCount > 0 ? state.Messages.Count : 0;
|
||||
if (_chatEnabled)
|
||||
{
|
||||
descriptorsToJoin.Add(descriptor);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -742,26 +823,30 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var removedGroupId in remainingGroups)
|
||||
if (allowRemoval)
|
||||
{
|
||||
if (_groupDefinitions.TryGetValue(removedGroupId, out var definition))
|
||||
foreach (var removedGroupId in remainingGroups)
|
||||
{
|
||||
var key = BuildChannelKey(definition.Descriptor);
|
||||
if (_channels.TryGetValue(key, out var state))
|
||||
if (_groupDefinitions.TryGetValue(removedGroupId, out var definition))
|
||||
{
|
||||
descriptorsToLeave.Add(state.Descriptor);
|
||||
_channels.Remove(key);
|
||||
_lastReadCounts.Remove(key);
|
||||
_lastPresenceStates.Remove(BuildPresenceKey(state.Descriptor));
|
||||
_selfTokens.Remove(key);
|
||||
_pendingSelfMessages.RemoveAll(p => string.Equals(p.ChannelKey, key, StringComparison.Ordinal));
|
||||
if (string.Equals(_activeChannelKey, key, StringComparison.Ordinal))
|
||||
var key = BuildChannelKey(definition.Descriptor);
|
||||
if (_channels.TryGetValue(key, out var state))
|
||||
{
|
||||
_activeChannelKey = null;
|
||||
CacheMessagesLocked(state);
|
||||
descriptorsToLeave.Add(state.Descriptor);
|
||||
_channels.Remove(key);
|
||||
_lastReadCounts.Remove(key);
|
||||
_lastPresenceStates.Remove(BuildPresenceKey(state.Descriptor));
|
||||
_selfTokens.Remove(key);
|
||||
_pendingSelfMessages.RemoveAll(p => string.Equals(p.ChannelKey, key, StringComparison.Ordinal));
|
||||
if (string.Equals(_activeChannelKey, key, StringComparison.Ordinal))
|
||||
{
|
||||
_activeChannelKey = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_groupDefinitions.Remove(removedGroupId);
|
||||
_groupDefinitions.Remove(removedGroupId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -794,6 +879,12 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
||||
var infos = new List<GroupChatChannelInfoDto>(groups.Count);
|
||||
foreach (var group in groups)
|
||||
{
|
||||
// basically prune the channel if it's disabled
|
||||
if (group.GroupPermissions.IsDisableChat())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var descriptor = new ChatChannelDescriptor
|
||||
{
|
||||
Type = ChatChannelType.Group,
|
||||
@@ -933,13 +1024,14 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
||||
descriptor.Type,
|
||||
displayName,
|
||||
descriptor.Type == ChatChannelType.Zone ? (_lastZoneDescriptor ?? descriptor) : descriptor);
|
||||
var restoredCount = RestoreCachedMessagesLocked(state);
|
||||
|
||||
state.IsConnected = _isConnected;
|
||||
state.IsAvailable = descriptor.Type == ChatChannelType.Group && _isConnected;
|
||||
state.StatusText = descriptor.Type == ChatChannelType.Zone ? ZoneUnavailableMessage : (_isConnected ? null : "Disconnected from chat server");
|
||||
|
||||
_channels[key] = state;
|
||||
_lastReadCounts[key] = 0;
|
||||
_lastReadCounts[key] = restoredCount > 0 ? state.Messages.Count : 0;
|
||||
publishChannelList = true;
|
||||
}
|
||||
|
||||
@@ -964,6 +1056,8 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
||||
state.UnreadCount = Math.Min(Math.Max(unreadFromHistory, incrementalUnread), MaxUnreadCount);
|
||||
state.HasUnread = state.UnreadCount > 0;
|
||||
}
|
||||
|
||||
MarkChannelsSnapshotDirtyLocked();
|
||||
}
|
||||
|
||||
Mediator.Publish(new ChatChannelMessageAdded(key, message));
|
||||
@@ -1067,7 +1161,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
||||
{
|
||||
try
|
||||
{
|
||||
return _dalamudUtilService.GetPlayerNameAsync().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
return _dalamudUtilService.GetPlayerName();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -1077,6 +1171,15 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
||||
|
||||
if (dto.Sender.Kind == ChatSenderKind.IdentifiedUser && dto.Sender.User is not null)
|
||||
{
|
||||
if (dto.Channel.Type != ChatChannelType.Group || _chatConfigService.Current.ShowNotesInSyncshellChat)
|
||||
{
|
||||
var note = _serverConfigurationManager.GetNoteForUid(dto.Sender.User.UID);
|
||||
if (!string.IsNullOrWhiteSpace(note))
|
||||
{
|
||||
return note;
|
||||
}
|
||||
}
|
||||
|
||||
return dto.Sender.User.AliasOrUID;
|
||||
}
|
||||
|
||||
@@ -1092,17 +1195,50 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
||||
{
|
||||
_channelOrder.Clear();
|
||||
|
||||
if (_channels.ContainsKey(ZoneChannelKey))
|
||||
var configuredOrder = _chatConfigService.Current.ChannelOrder;
|
||||
if (configuredOrder.Count > 0)
|
||||
{
|
||||
_channelOrder.Add(ZoneChannelKey);
|
||||
var seen = new HashSet<string>(StringComparer.Ordinal);
|
||||
foreach (var key in configuredOrder)
|
||||
{
|
||||
if (_channels.ContainsKey(key) && seen.Add(key))
|
||||
{
|
||||
_channelOrder.Add(key);
|
||||
}
|
||||
}
|
||||
|
||||
var remaining = _channels.Values
|
||||
.Where(state => !seen.Contains(state.Key))
|
||||
.ToList();
|
||||
|
||||
if (remaining.Count > 0)
|
||||
{
|
||||
var zoneKeys = remaining
|
||||
.Where(state => state.Type == ChatChannelType.Zone)
|
||||
.Select(state => state.Key);
|
||||
var groupKeys = remaining
|
||||
.Where(state => state.Type == ChatChannelType.Group)
|
||||
.OrderBy(state => state.DisplayName, StringComparer.OrdinalIgnoreCase)
|
||||
.Select(state => state.Key);
|
||||
|
||||
_channelOrder.AddRange(zoneKeys);
|
||||
_channelOrder.AddRange(groupKeys);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_channels.ContainsKey(ZoneChannelKey))
|
||||
{
|
||||
_channelOrder.Add(ZoneChannelKey);
|
||||
}
|
||||
|
||||
var groups = _channels.Values
|
||||
.Where(state => state.Type == ChatChannelType.Group)
|
||||
.OrderBy(state => state.DisplayName, StringComparer.OrdinalIgnoreCase)
|
||||
.Select(state => state.Key);
|
||||
var groups = _channels.Values
|
||||
.Where(state => state.Type == ChatChannelType.Group)
|
||||
.OrderBy(state => state.DisplayName, StringComparer.OrdinalIgnoreCase)
|
||||
.Select(state => state.Key);
|
||||
|
||||
_channelOrder.AddRange(groups);
|
||||
_channelOrder.AddRange(groups);
|
||||
}
|
||||
|
||||
if (_activeChannelKey is null && _channelOrder.Count > 0)
|
||||
{
|
||||
@@ -1112,9 +1248,25 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
||||
{
|
||||
_activeChannelKey = _channelOrder.Count > 0 ? _channelOrder[0] : null;
|
||||
}
|
||||
|
||||
MarkChannelsSnapshotDirtyLocked();
|
||||
}
|
||||
|
||||
private void PublishChannelListChanged() => Mediator.Publish(new ChatChannelsUpdated());
|
||||
private void MarkChannelsSnapshotDirty()
|
||||
{
|
||||
using (_sync.EnterScope())
|
||||
{
|
||||
_channelsSnapshotDirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void MarkChannelsSnapshotDirtyLocked() => _channelsSnapshotDirty = true;
|
||||
|
||||
private void PublishChannelListChanged()
|
||||
{
|
||||
MarkChannelsSnapshotDirty();
|
||||
Mediator.Publish(new ChatChannelsUpdated());
|
||||
}
|
||||
|
||||
private static IEnumerable<string> EnumerateTerritoryKeys(string? value)
|
||||
{
|
||||
@@ -1157,11 +1309,12 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
||||
if (!_channels.TryGetValue(ZoneChannelKey, out var state))
|
||||
{
|
||||
state = new ChatChannelState(ZoneChannelKey, ChatChannelType.Zone, "Zone Chat", new ChatChannelDescriptor { Type = ChatChannelType.Zone });
|
||||
var restoredCount = RestoreCachedMessagesLocked(state);
|
||||
state.IsConnected = _chatEnabled && _isConnected;
|
||||
state.IsAvailable = false;
|
||||
state.StatusText = _chatEnabled ? ZoneUnavailableMessage : "Chat services disabled";
|
||||
_channels[ZoneChannelKey] = state;
|
||||
_lastReadCounts[ZoneChannelKey] = 0;
|
||||
_lastReadCounts[ZoneChannelKey] = restoredCount > 0 ? state.Messages.Count : 0;
|
||||
UpdateChannelOrderLocked();
|
||||
}
|
||||
|
||||
@@ -1170,6 +1323,11 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
||||
|
||||
private void RemoveZoneStateLocked()
|
||||
{
|
||||
if (_channels.TryGetValue(ZoneChannelKey, out var existing))
|
||||
{
|
||||
CacheMessagesLocked(existing);
|
||||
}
|
||||
|
||||
if (_channels.Remove(ZoneChannelKey))
|
||||
{
|
||||
_lastReadCounts.Remove(ZoneChannelKey);
|
||||
@@ -1184,6 +1342,28 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
|
||||
}
|
||||
}
|
||||
|
||||
private void CacheMessagesLocked(ChatChannelState state)
|
||||
{
|
||||
if (state.Messages.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_messageHistoryCache[state.Key] = new List<ChatMessageEntry>(state.Messages);
|
||||
}
|
||||
|
||||
private int RestoreCachedMessagesLocked(ChatChannelState state)
|
||||
{
|
||||
if (_messageHistoryCache.TryGetValue(state.Key, out var cached) && cached.Count > 0)
|
||||
{
|
||||
state.Messages.AddRange(cached);
|
||||
_messageHistoryCache.Remove(state.Key);
|
||||
return cached.Count;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private sealed class ChatChannelState
|
||||
{
|
||||
public ChatChannelState(string key, ChatChannelType type, string displayName, ChatChannelDescriptor descriptor)
|
||||
|
||||
@@ -49,7 +49,7 @@ public sealed class CommandManagerService : IDisposable
|
||||
"\t /light analyze - Opens the Lightless Character Data Analysis window" + Environment.NewLine +
|
||||
"\t /light settings - Opens the Lightless Settings window" + Environment.NewLine +
|
||||
"\t /light finder - Opens the Lightfinder window" + Environment.NewLine +
|
||||
"\t /light finder - Opens the Lightless Chat window"
|
||||
"\t /light chat - Opens the Lightless Chat window"
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -4,21 +4,21 @@ using Dalamud.Plugin;
|
||||
using Dalamud.Plugin.Services;
|
||||
using LightlessSync.LightlessConfiguration;
|
||||
using LightlessSync.LightlessConfiguration.Models;
|
||||
using LightlessSync.Services.LightFinder;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using LightlessSync.UI;
|
||||
using LightlessSync.UI.Services;
|
||||
using LightlessSync.Utils;
|
||||
using LightlessSync.WebAPI;
|
||||
using Lumina.Excel.Sheets;
|
||||
using LightlessSync.UI.Services;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using LightlessSync.UI;
|
||||
using LightlessSync.Services.LightFinder;
|
||||
|
||||
namespace LightlessSync.Services;
|
||||
|
||||
internal class ContextMenuService : IHostedService
|
||||
{
|
||||
private readonly IContextMenu _contextMenu;
|
||||
private readonly IChatGui _chatGui;
|
||||
private readonly IDalamudPluginInterface _pluginInterface;
|
||||
private readonly IDataManager _gameData;
|
||||
private readonly ILogger<ContextMenuService> _logger;
|
||||
@@ -29,6 +29,7 @@ internal class ContextMenuService : IHostedService
|
||||
private readonly ApiController _apiController;
|
||||
private readonly IObjectTable _objectTable;
|
||||
private readonly LightlessConfigService _configService;
|
||||
private readonly NotificationService _lightlessNotification;
|
||||
private readonly LightFinderScannerService _broadcastScannerService;
|
||||
private readonly LightFinderService _broadcastService;
|
||||
private readonly LightlessProfileManager _lightlessProfileManager;
|
||||
@@ -43,7 +44,7 @@ internal class ContextMenuService : IHostedService
|
||||
ILogger<ContextMenuService> logger,
|
||||
DalamudUtilService dalamudUtil,
|
||||
ApiController apiController,
|
||||
IObjectTable objectTable,
|
||||
IObjectTable objectTable,
|
||||
LightlessConfigService configService,
|
||||
PairRequestService pairRequestService,
|
||||
PairUiService pairUiService,
|
||||
@@ -51,7 +52,9 @@ internal class ContextMenuService : IHostedService
|
||||
LightFinderScannerService broadcastScannerService,
|
||||
LightFinderService broadcastService,
|
||||
LightlessProfileManager lightlessProfileManager,
|
||||
LightlessMediator mediator)
|
||||
LightlessMediator mediator,
|
||||
IChatGui chatGui,
|
||||
NotificationService lightlessNotification)
|
||||
{
|
||||
_contextMenu = contextMenu;
|
||||
_pluginInterface = pluginInterface;
|
||||
@@ -68,6 +71,8 @@ internal class ContextMenuService : IHostedService
|
||||
_broadcastService = broadcastService;
|
||||
_lightlessProfileManager = lightlessProfileManager;
|
||||
_mediator = mediator;
|
||||
_chatGui = chatGui;
|
||||
_lightlessNotification = lightlessNotification;
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
@@ -99,6 +104,12 @@ internal class ContextMenuService : IHostedService
|
||||
if (!_pluginInterface.UiBuilder.ShouldModifyUi)
|
||||
return;
|
||||
|
||||
if (!_configService.Current.EnableRightClickMenus)
|
||||
{
|
||||
_logger.LogTrace("Right-click menus are disabled in configuration.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.AddonName != null)
|
||||
{
|
||||
var addonName = args.AddonName;
|
||||
@@ -129,7 +140,6 @@ internal class ContextMenuService : IHostedService
|
||||
|
||||
var snapshot = _pairUiService.GetSnapshot();
|
||||
var pair = snapshot.PairsByUid.Values.FirstOrDefault(p =>
|
||||
p.IsVisible &&
|
||||
p.PlayerCharacterId != uint.MaxValue &&
|
||||
p.PlayerCharacterId == target.TargetObjectId);
|
||||
|
||||
@@ -161,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);
|
||||
return;
|
||||
}
|
||||
|
||||
var world = GetWorld(target.TargetHomeWorld.RowId);
|
||||
if (!IsWorldValid(world))
|
||||
|
||||
if (!IsWorldValid(target.TargetHomeWorld.RowId))
|
||||
{
|
||||
_logger.LogTrace("Target player {TargetName}@{World} is on an invalid world.", target.TargetName, target.TargetHomeWorld.RowId);
|
||||
return;
|
||||
@@ -199,13 +208,24 @@ internal class ContextMenuService : IHostedService
|
||||
.Where(p => p.IsVisible && p.PlayerCharacterId != uint.MaxValue)
|
||||
.Select(p => (ulong)p.PlayerCharacterId)];
|
||||
|
||||
private void NotifyInChat(string message, NotificationType type = NotificationType.Info)
|
||||
{
|
||||
if (!_configService.Current.UseLightlessNotifications || (_configService.Current.LightlessPairRequestNotification == NotificationLocation.Chat || _configService.Current.LightlessPairRequestNotification == NotificationLocation.ChatAndLightlessUi))
|
||||
{
|
||||
var chatMsg = $"[Lightless] {message}";
|
||||
if (type == NotificationType.Error)
|
||||
_chatGui.PrintError(chatMsg);
|
||||
else
|
||||
_chatGui.Print(chatMsg);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleSelection(IMenuArgs args)
|
||||
{
|
||||
if (args.Target is not MenuTargetDefault target)
|
||||
return;
|
||||
|
||||
var world = GetWorld(target.TargetHomeWorld.RowId);
|
||||
if (!IsWorldValid(world))
|
||||
|
||||
if (!target.TargetHomeWorld.IsValid || !IsWorldValid(target.TargetHomeWorld.RowId))
|
||||
return;
|
||||
|
||||
try
|
||||
@@ -214,11 +234,11 @@ internal class ContextMenuService : IHostedService
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
var senderCid = (await _dalamudUtil.GetCIDAsync().ConfigureAwait(false)).ToString().GetHash256();
|
||||
var senderCid = _dalamudUtil.GetCID().ToString().GetHash256();
|
||||
var receiverCid = DalamudUtilService.GetHashedCIDFromPlayerPointer(targetData.Address);
|
||||
|
||||
_logger.LogInformation("Sending pair request: sender {SenderCid}, receiver {ReceiverCid}", senderCid, receiverCid);
|
||||
@@ -227,6 +247,9 @@ internal class ContextMenuService : IHostedService
|
||||
{
|
||||
_pairRequestService.RemoveRequest(receiverCid);
|
||||
}
|
||||
|
||||
// Notify in chat when NotificationService is disabled
|
||||
NotifyInChat($"Pair request sent to {target.TargetName}@{target.TargetHomeWorld.Value.Name}.", NotificationType.Info);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -286,37 +309,8 @@ internal class ContextMenuService : IHostedService
|
||||
p.HomeWorld.RowId == target.TargetHomeWorld.RowId);
|
||||
}
|
||||
|
||||
private World GetWorld(uint worldId)
|
||||
private bool IsWorldValid(uint worldId)
|
||||
{
|
||||
var sheet = _gameData.GetExcelSheet<World>()!;
|
||||
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]);
|
||||
return _dalamudUtil.WorldData.Value.ContainsKey((ushort)worldId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
using Dalamud.Game.ClientState.Conditions;
|
||||
using Dalamud.Game.ClientState.Objects;
|
||||
using Dalamud.Game.ClientState.Objects.SubKinds;
|
||||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
using Dalamud.Game.Text;
|
||||
using Dalamud.Plugin.Services;
|
||||
using Dalamud.Utility;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.Control;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.UI;
|
||||
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||
using LightlessSync.API.Dto.CharaData;
|
||||
@@ -24,8 +25,10 @@ using Microsoft.Extensions.Logging;
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using BattleNpcSubKind = FFXIVClientStructs.FFXIV.Client.Game.Object.BattleNpcSubKind;
|
||||
using DalamudObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind;
|
||||
using GameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject;
|
||||
using Map = Lumina.Excel.Sheets.Map;
|
||||
using VisibilityFlags = FFXIVClientStructs.FFXIV.Client.Game.Object.VisibilityFlags;
|
||||
|
||||
namespace LightlessSync.Services;
|
||||
@@ -37,6 +40,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
private readonly ICondition _condition;
|
||||
private readonly IDataManager _gameData;
|
||||
private readonly IGameConfig _gameConfig;
|
||||
private readonly IPlayerState _playerState;
|
||||
private readonly BlockedCharacterHandler _blockedCharacterHandler;
|
||||
private readonly IFramework _framework;
|
||||
private readonly IGameGui _gameGui;
|
||||
@@ -56,11 +60,12 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
private string _lastGlobalBlockReason = string.Empty;
|
||||
private ushort _lastZone = 0;
|
||||
private ushort _lastWorldId = 0;
|
||||
private uint _lastMapId = 0;
|
||||
private bool _sentBetweenAreas = false;
|
||||
private Lazy<ulong> _cid;
|
||||
|
||||
public DalamudUtilService(ILogger<DalamudUtilService> logger, IClientState clientState, IObjectTable objectTable, IFramework framework,
|
||||
IGameGui gameGui, ICondition condition, IDataManager gameData, ITargetManager targetManager, IGameConfig gameConfig,
|
||||
IGameGui gameGui, ICondition condition, IDataManager gameData, ITargetManager targetManager, IGameConfig gameConfig, IPlayerState playerState,
|
||||
ActorObjectService actorObjectService, BlockedCharacterHandler blockedCharacterHandler, LightlessMediator mediator, PerformanceCollectorService performanceCollector,
|
||||
LightlessConfigService configService, PlayerPerformanceConfigService playerPerformanceConfigService, Lazy<PairFactory> pairFactory)
|
||||
{
|
||||
@@ -72,6 +77,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
_condition = condition;
|
||||
_gameData = gameData;
|
||||
_gameConfig = gameConfig;
|
||||
_playerState = playerState;
|
||||
_actorObjectService = actorObjectService;
|
||||
_targetManager = targetManager;
|
||||
_blockedCharacterHandler = blockedCharacterHandler;
|
||||
@@ -80,53 +86,27 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
_configService = configService;
|
||||
_playerPerformanceConfigService = playerPerformanceConfigService;
|
||||
_pairFactory = pairFactory;
|
||||
var clientLanguage = _clientState.ClientLanguage;
|
||||
WorldData = new(() =>
|
||||
{
|
||||
return gameData.GetExcelSheet<Lumina.Excel.Sheets.World>(Dalamud.Game.ClientLanguage.English)!
|
||||
.Where(w => !w.Name.IsEmpty && w.DataCenter.RowId != 0 && (w.IsPublic || char.IsUpper(w.Name.ToString()[0])))
|
||||
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])
|
||||
|| w is { RowId: > 1000, Region: 101 or 201 }))
|
||||
.ToDictionary(w => (ushort)w.RowId, w => w.Name.ToString());
|
||||
});
|
||||
JobData = new(() =>
|
||||
{
|
||||
return gameData.GetExcelSheet<ClassJob>(Dalamud.Game.ClientLanguage.English)!
|
||||
.ToDictionary(k => k.RowId, k => k.NameEnglish.ToString());
|
||||
return gameData.GetExcelSheet<ClassJob>(clientLanguage)!
|
||||
.ToDictionary(k => k.RowId, k => k.Name.ToString());
|
||||
});
|
||||
TerritoryData = new(() =>
|
||||
TerritoryData = new(() => BuildTerritoryData(clientLanguage));
|
||||
TerritoryDataEnglish = new(() => BuildTerritoryData(Dalamud.Game.ClientLanguage.English));
|
||||
MapData = new(() => BuildMapData(clientLanguage));
|
||||
ContentFinderData = new Lazy<Dictionary<uint, string>>(() =>
|
||||
{
|
||||
return gameData.GetExcelSheet<TerritoryType>(Dalamud.Game.ClientLanguage.English)!
|
||||
.Where(w => w.RowId != 0)
|
||||
.ToDictionary(w => w.RowId, w =>
|
||||
{
|
||||
StringBuilder sb = new();
|
||||
sb.Append(w.PlaceNameRegion.Value.Name);
|
||||
if (w.PlaceName.ValueNullable != null)
|
||||
{
|
||||
sb.Append(" - ");
|
||||
sb.Append(w.PlaceName.Value.Name);
|
||||
}
|
||||
return sb.ToString();
|
||||
});
|
||||
});
|
||||
MapData = new(() =>
|
||||
{
|
||||
return gameData.GetExcelSheet<Map>(Dalamud.Game.ClientLanguage.English)!
|
||||
.Where(w => w.RowId != 0)
|
||||
.ToDictionary(w => w.RowId, w =>
|
||||
{
|
||||
StringBuilder sb = new();
|
||||
sb.Append(w.PlaceNameRegion.Value.Name);
|
||||
if (w.PlaceName.ValueNullable != null)
|
||||
{
|
||||
sb.Append(" - ");
|
||||
sb.Append(w.PlaceName.Value.Name);
|
||||
}
|
||||
if (w.PlaceNameSub.ValueNullable != null && !string.IsNullOrEmpty(w.PlaceNameSub.Value.Name.ToString()))
|
||||
{
|
||||
sb.Append(" - ");
|
||||
sb.Append(w.PlaceNameSub.Value.Name);
|
||||
}
|
||||
return (w, sb.ToString());
|
||||
});
|
||||
return _gameData.GetExcelSheet<TerritoryType>()!
|
||||
.Where(w => w.RowId != 0 && !string.IsNullOrEmpty(w.ContentFinderCondition.ValueNullable?.Name.ToString()))
|
||||
.ToDictionary(w => w.RowId, w => w.ContentFinderCondition.Value.Name.ToString());
|
||||
});
|
||||
mediator.Subscribe<TargetPairMessage>(this, (msg) =>
|
||||
{
|
||||
@@ -158,6 +138,71 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
private Lazy<ulong> RebuildCID() => new(GetCID);
|
||||
|
||||
public bool IsWine { get; init; }
|
||||
private Dictionary<uint, string> BuildTerritoryData(Dalamud.Game.ClientLanguage language)
|
||||
{
|
||||
var placeNames = _gameData.GetExcelSheet<PlaceName>(language)!;
|
||||
return _gameData.GetExcelSheet<TerritoryType>(language)!
|
||||
.Where(w => w.RowId != 0)
|
||||
.ToDictionary(w => w.RowId, w =>
|
||||
{
|
||||
var regionName = GetPlaceName(placeNames, w.PlaceNameRegion.RowId);
|
||||
var placeName = GetPlaceName(placeNames, w.PlaceName.RowId);
|
||||
return BuildPlaceName(regionName, placeName, string.Empty);
|
||||
});
|
||||
}
|
||||
|
||||
private Dictionary<uint, (Map Map, string MapName)> BuildMapData(Dalamud.Game.ClientLanguage language)
|
||||
{
|
||||
var placeNames = _gameData.GetExcelSheet<PlaceName>(language)!;
|
||||
return _gameData.GetExcelSheet<Map>(language)!
|
||||
.Where(w => w.RowId != 0)
|
||||
.ToDictionary(w => w.RowId, w =>
|
||||
{
|
||||
var regionName = GetPlaceName(placeNames, w.PlaceNameRegion.RowId);
|
||||
var placeName = GetPlaceName(placeNames, w.PlaceName.RowId);
|
||||
var subPlaceName = GetPlaceName(placeNames, w.PlaceNameSub.RowId);
|
||||
var displayName = BuildPlaceName(regionName, placeName, subPlaceName);
|
||||
return (w, displayName);
|
||||
});
|
||||
}
|
||||
private static string GetPlaceName(Lumina.Excel.ExcelSheet<PlaceName> placeNames, uint rowId)
|
||||
{
|
||||
if (rowId == 0)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return placeNames.GetRow(rowId).Name.ToString();
|
||||
}
|
||||
|
||||
private static string BuildPlaceName(string regionName, string placeName, string subPlaceName)
|
||||
{
|
||||
StringBuilder sb = new();
|
||||
if (!string.IsNullOrWhiteSpace(regionName))
|
||||
{
|
||||
sb.Append(regionName);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(placeName))
|
||||
{
|
||||
if (sb.Length > 0)
|
||||
{
|
||||
sb.Append(" - ");
|
||||
}
|
||||
sb.Append(placeName);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(subPlaceName))
|
||||
{
|
||||
if (sb.Length > 0)
|
||||
{
|
||||
sb.Append(" - ");
|
||||
}
|
||||
sb.Append(subPlaceName);
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
private bool ResolvePairAddress(Pair pair, out Pair resolvedPair, out nint address)
|
||||
{
|
||||
resolvedPair = _pairFactory.Value.Create(pair.UniqueIdent) ?? pair;
|
||||
@@ -233,21 +278,51 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
public bool IsAnythingDrawing { get; private set; } = false;
|
||||
public bool IsInCutscene { get; private set; } = false;
|
||||
public bool IsInGpose { get; private set; } = false;
|
||||
public bool IsGameUiHidden => _gameGui.GameUiHidden;
|
||||
public bool IsLoggedIn { get; private set; }
|
||||
public bool IsOnFrameworkThread => _framework.IsInFrameworkUpdateThread;
|
||||
public bool IsZoning => _condition[ConditionFlag.BetweenAreas] || _condition[ConditionFlag.BetweenAreas51];
|
||||
public bool IsInCombat { get; private set; } = false;
|
||||
public bool IsPerforming { get; private set; } = false;
|
||||
public bool IsInInstance { get; private set; } = false;
|
||||
public bool IsInDuty => _condition[ConditionFlag.BoundByDuty];
|
||||
public bool HasModifiedGameFiles => _gameData.HasModifiedGameDataFiles;
|
||||
public uint ClassJobId => _classJobId!.Value;
|
||||
public Lazy<Dictionary<uint, string>> JobData { get; private set; }
|
||||
public Lazy<Dictionary<ushort, string>> WorldData { get; private set; }
|
||||
public Lazy<Dictionary<uint, string>> TerritoryData { get; private set; }
|
||||
public Lazy<Dictionary<uint, string>> TerritoryDataEnglish { get; private set; }
|
||||
public Lazy<Dictionary<uint, (Map Map, string MapName)>> MapData { get; private set; }
|
||||
public Lazy<Dictionary<uint, string>> ContentFinderData { get; private set; }
|
||||
public bool IsLodEnabled { get; private set; }
|
||||
public LightlessMediator Mediator { get; }
|
||||
|
||||
public bool IsInFieldOperation
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!IsInDuty)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var territoryId = _clientState.TerritoryType;
|
||||
if (territoryId == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TerritoryDataEnglish.Value.TryGetValue(territoryId, out var name) || string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return name.Contains("Eureka", StringComparison.OrdinalIgnoreCase)
|
||||
|| name.Contains("Bozja", StringComparison.OrdinalIgnoreCase)
|
||||
|| name.Contains("Zadnor", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
|
||||
public IGameObject? CreateGameObject(IntPtr reference)
|
||||
{
|
||||
EnsureIsOnFramework();
|
||||
@@ -300,8 +375,8 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
|
||||
public IEnumerable<ICharacter?> GetGposeCharactersFromObjectTable()
|
||||
{
|
||||
foreach (var actor in _actorObjectService.PlayerDescriptors
|
||||
.Where(a => a.ObjectKind == DalamudObjectKind.Player && a.ObjectIndex > 200))
|
||||
foreach (var actor in _objectTable
|
||||
.Where(a => a.ObjectIndex > 200 && a.ObjectKind == DalamudObjectKind.Player))
|
||||
{
|
||||
var character = _objectTable.CreateObjectReference(actor.Address) as ICharacter;
|
||||
if (character != null)
|
||||
@@ -312,7 +387,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
public bool GetIsPlayerPresent()
|
||||
{
|
||||
EnsureIsOnFramework();
|
||||
return _objectTable.LocalPlayer != null && _objectTable.LocalPlayer.IsValid();
|
||||
return _objectTable.LocalPlayer != null && _objectTable.LocalPlayer.IsValid() && _playerState.IsLoaded;
|
||||
}
|
||||
|
||||
public async Task<bool> GetIsPlayerPresentAsync()
|
||||
@@ -328,7 +403,8 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
|
||||
var playerAddress = playerPointer.Value;
|
||||
var ownerEntityId = ((Character*)playerAddress)->EntityId;
|
||||
if (ownerEntityId == 0) return IntPtr.Zero;
|
||||
var candidateAddress = _objectTable.GetObjectAddress(((GameObject*)playerAddress)->ObjectIndex + 1);
|
||||
if (ownerEntityId == 0) return candidateAddress;
|
||||
|
||||
if (playerAddress == _actorObjectService.LocalPlayerAddress)
|
||||
{
|
||||
@@ -339,6 +415,17 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
}
|
||||
}
|
||||
|
||||
if (candidateAddress != nint.Zero)
|
||||
{
|
||||
var candidate = (GameObject*)candidateAddress;
|
||||
var candidateKind = (DalamudObjectKind)candidate->ObjectKind;
|
||||
if ((candidateKind == DalamudObjectKind.MountType || candidateKind == DalamudObjectKind.Companion)
|
||||
&& ResolveOwnerId(candidate) == ownerEntityId)
|
||||
{
|
||||
return candidateAddress;
|
||||
}
|
||||
}
|
||||
|
||||
var ownedObject = FindOwnedObject(ownerEntityId, playerAddress, static kind =>
|
||||
kind == DalamudObjectKind.MountType || kind == DalamudObjectKind.Companion);
|
||||
if (ownedObject != nint.Zero)
|
||||
@@ -346,7 +433,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
return ownedObject;
|
||||
}
|
||||
|
||||
return _objectTable.GetObjectAddress(((GameObject*)playerAddress)->ObjectIndex + 1);
|
||||
return candidateAddress;
|
||||
}
|
||||
|
||||
public async Task<IntPtr> GetMinionOrMountAsync(IntPtr? playerPointer = null)
|
||||
@@ -361,7 +448,22 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
var mgr = CharacterManager.Instance();
|
||||
playerPointer ??= GetPlayerPtr();
|
||||
if (playerPointer == IntPtr.Zero || (IntPtr)mgr == IntPtr.Zero) return IntPtr.Zero;
|
||||
return (IntPtr)mgr->LookupPetByOwnerObject((BattleChara*)playerPointer);
|
||||
|
||||
var ownerAddress = playerPointer.Value;
|
||||
var ownerEntityId = ((Character*)ownerAddress)->EntityId;
|
||||
if (ownerEntityId == 0) return IntPtr.Zero;
|
||||
|
||||
var candidate = (IntPtr)mgr->LookupPetByOwnerObject((BattleChara*)ownerAddress);
|
||||
if (candidate != IntPtr.Zero)
|
||||
{
|
||||
var candidateObj = (GameObject*)candidate;
|
||||
if (IsPetMatch(candidateObj, ownerEntityId))
|
||||
{
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return FindOwnedPet(ownerEntityId, ownerAddress);
|
||||
}
|
||||
|
||||
public async Task<IntPtr> GetPetAsync(IntPtr? playerPointer = null)
|
||||
@@ -398,6 +500,60 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
return nint.Zero;
|
||||
}
|
||||
|
||||
private unsafe nint FindOwnedPet(uint ownerEntityId, nint ownerAddress)
|
||||
{
|
||||
if (ownerEntityId == 0)
|
||||
{
|
||||
return nint.Zero;
|
||||
}
|
||||
|
||||
foreach (var obj in _objectTable)
|
||||
{
|
||||
if (obj is null || obj.Address == nint.Zero || obj.Address == ownerAddress)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (obj.ObjectKind != DalamudObjectKind.BattleNpc)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var candidate = (GameObject*)obj.Address;
|
||||
if (candidate->BattleNpcSubKind != BattleNpcSubKind.Pet)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ResolveOwnerId(candidate) == ownerEntityId)
|
||||
{
|
||||
return obj.Address;
|
||||
}
|
||||
}
|
||||
|
||||
return nint.Zero;
|
||||
}
|
||||
|
||||
private static unsafe bool IsPetMatch(GameObject* candidate, uint ownerEntityId)
|
||||
{
|
||||
if (candidate == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((DalamudObjectKind)candidate->ObjectKind != DalamudObjectKind.BattleNpc)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (candidate->BattleNpcSubKind != BattleNpcSubKind.Pet)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return ResolveOwnerId(candidate) == ownerEntityId;
|
||||
}
|
||||
|
||||
private static unsafe uint ResolveOwnerId(GameObject* gameObject)
|
||||
{
|
||||
if (gameObject == null)
|
||||
@@ -445,30 +601,17 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
|
||||
public string GetPlayerName()
|
||||
{
|
||||
EnsureIsOnFramework();
|
||||
return _objectTable.LocalPlayer?.Name.ToString() ?? "--";
|
||||
}
|
||||
|
||||
public async Task<string> GetPlayerNameAsync()
|
||||
{
|
||||
return await RunOnFrameworkThread(GetPlayerName).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<ulong> GetCIDAsync()
|
||||
{
|
||||
return await RunOnFrameworkThread(GetCID).ConfigureAwait(false);
|
||||
return _playerState.CharacterName;
|
||||
}
|
||||
|
||||
public unsafe ulong GetCID()
|
||||
{
|
||||
EnsureIsOnFramework();
|
||||
var playerChar = GetPlayerCharacter();
|
||||
return ((BattleChara*)playerChar.Address)->Character.ContentId;
|
||||
return _playerState.ContentId;
|
||||
}
|
||||
|
||||
public async Task<string> GetPlayerNameHashedAsync()
|
||||
public string GetPlayerNameHashed()
|
||||
{
|
||||
return await RunOnFrameworkThread(() => _cid.Value.ToString().GetHash256()).ConfigureAwait(false);
|
||||
return _cid.Value.ToString().GetHash256();
|
||||
}
|
||||
|
||||
public static unsafe bool TryGetHashedCID(IPlayerCharacter? playerCharacter, out string hashedCid)
|
||||
@@ -507,54 +650,100 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
|
||||
public uint GetHomeWorldId()
|
||||
{
|
||||
EnsureIsOnFramework();
|
||||
return _objectTable.LocalPlayer?.HomeWorld.RowId ?? 0;
|
||||
return _playerState.HomeWorld.RowId;
|
||||
}
|
||||
|
||||
public uint GetWorldId()
|
||||
{
|
||||
EnsureIsOnFramework();
|
||||
return _objectTable.LocalPlayer!.CurrentWorld.RowId;
|
||||
return _playerState.CurrentWorld.RowId;
|
||||
}
|
||||
|
||||
public unsafe LocationInfo GetMapData()
|
||||
{
|
||||
EnsureIsOnFramework();
|
||||
var agentMap = AgentMap.Instance();
|
||||
var houseMan = HousingManager.Instance();
|
||||
uint serverId = 0;
|
||||
if (_objectTable.LocalPlayer == null) serverId = 0;
|
||||
else serverId = _objectTable.LocalPlayer.CurrentWorld.RowId;
|
||||
uint mapId = agentMap == null ? 0 : agentMap->CurrentMapId;
|
||||
uint territoryId = agentMap == null ? 0 : agentMap->CurrentTerritoryId;
|
||||
uint divisionId = houseMan == null ? 0 : (uint)(houseMan->GetCurrentDivision());
|
||||
uint wardId = houseMan == null ? 0 : (uint)(houseMan->GetCurrentWard() + 1);
|
||||
uint houseId = 0;
|
||||
var tempHouseId = houseMan == null ? 0 : (houseMan->GetCurrentPlot());
|
||||
if (!houseMan->IsInside()) tempHouseId = 0;
|
||||
if (tempHouseId < -1)
|
||||
{
|
||||
divisionId = tempHouseId == -127 ? 2 : (uint)1;
|
||||
tempHouseId = 100;
|
||||
}
|
||||
if (tempHouseId == -1) tempHouseId = 0;
|
||||
houseId = (uint)tempHouseId;
|
||||
if (houseId != 0)
|
||||
{
|
||||
territoryId = HousingManager.GetOriginalHouseTerritoryTypeId();
|
||||
}
|
||||
uint roomId = houseMan == null ? 0 : (uint)(houseMan->GetCurrentRoom());
|
||||
|
||||
return new LocationInfo()
|
||||
var location = new LocationInfo();
|
||||
location.ServerId = _playerState.CurrentWorld.RowId;
|
||||
location.InstanceId = UIState.Instance()->PublicInstance.InstanceId;
|
||||
location.TerritoryId = _clientState.TerritoryType;
|
||||
location.MapId = _clientState.MapId;
|
||||
if (houseMan != null)
|
||||
{
|
||||
ServerId = serverId,
|
||||
MapId = mapId,
|
||||
TerritoryId = territoryId,
|
||||
DivisionId = divisionId,
|
||||
WardId = wardId,
|
||||
HouseId = houseId,
|
||||
RoomId = roomId
|
||||
};
|
||||
if (houseMan->IsInside())
|
||||
{
|
||||
location.TerritoryId = HousingManager.GetOriginalHouseTerritoryTypeId();
|
||||
var house = houseMan->GetCurrentIndoorHouseId();
|
||||
location.WardId = house.WardIndex + 1u;
|
||||
location.HouseId = house.IsApartment ? 100 : house.PlotIndex + 1u;
|
||||
location.RoomId = (uint)house.RoomNumber;
|
||||
location.DivisionId = house.IsApartment ? house.ApartmentDivision + 1u : houseMan->GetCurrentDivision();
|
||||
}
|
||||
else if (houseMan->IsInWorkshop())
|
||||
{
|
||||
var workShop = houseMan->WorkshopTerritory;
|
||||
var house = workShop->HouseId;
|
||||
location.WardId = house.WardIndex + 1u;
|
||||
location.HouseId = house.PlotIndex + 1u;
|
||||
}
|
||||
else if (houseMan->IsOutside())
|
||||
{
|
||||
var outside = houseMan->OutdoorTerritory;
|
||||
var house = outside->HouseId;
|
||||
location.WardId = house.WardIndex + 1u;
|
||||
//location.HouseId = (uint)houseMan->GetCurrentPlot() + 1;
|
||||
location.DivisionId = houseMan->GetCurrentDivision();
|
||||
}
|
||||
//_logger.LogWarning(LocationToString(location));
|
||||
}
|
||||
return location;
|
||||
}
|
||||
|
||||
public string LocationToString(LocationInfo location)
|
||||
{
|
||||
if (location.ServerId is 0 || location.TerritoryId is 0) return String.Empty;
|
||||
var str = WorldData.Value[(ushort)location.ServerId];
|
||||
|
||||
if (ContentFinderData.Value.TryGetValue(location.TerritoryId , out var dutyName))
|
||||
{
|
||||
str += $" - [In Duty]{dutyName}";
|
||||
}
|
||||
else
|
||||
{
|
||||
if (location.HouseId is not 0 || location.MapId is 0) // Dont show mapName when in house/no map available
|
||||
{
|
||||
str += $" - {TerritoryData.Value[(ushort)location.TerritoryId]}";
|
||||
}
|
||||
else
|
||||
{
|
||||
str += $" - {MapData.Value[(ushort)location.MapId].MapName}";
|
||||
}
|
||||
|
||||
if (location.InstanceId is not 0)
|
||||
{
|
||||
str += ((SeIconChar)(57520 + location.InstanceId)).ToIconString();
|
||||
}
|
||||
|
||||
if (location.WardId is not 0)
|
||||
{
|
||||
str += $" Ward #{location.WardId}";
|
||||
}
|
||||
|
||||
if (location.HouseId is not 0 and not 100)
|
||||
{
|
||||
str += $" House #{location.HouseId}";
|
||||
}
|
||||
else if (location.HouseId is 100)
|
||||
{
|
||||
str += $" {(location.DivisionId == 2 ? "[Subdivision]" : "")} Apartment";
|
||||
}
|
||||
|
||||
if (location.RoomId is not 0)
|
||||
{
|
||||
str += $" Room #{location.RoomId}";
|
||||
}
|
||||
}
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
public unsafe void SetMarkerAndOpenMap(Vector3 position, Map map)
|
||||
@@ -566,21 +755,6 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
agentMap->SetFlagMapMarker(map.TerritoryType.RowId, map.RowId, position);
|
||||
}
|
||||
|
||||
public async Task<LocationInfo> GetMapDataAsync()
|
||||
{
|
||||
return await RunOnFrameworkThread(GetMapData).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<uint> GetWorldIdAsync()
|
||||
{
|
||||
return await RunOnFrameworkThread(GetWorldId).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<uint> GetHomeWorldIdAsync()
|
||||
{
|
||||
return await RunOnFrameworkThread(GetHomeWorldId).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public unsafe bool IsGameObjectPresent(IntPtr key)
|
||||
{
|
||||
return _objectTable.Any(f => f.Address == key);
|
||||
@@ -753,7 +927,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
bool isDrawingChanged = false;
|
||||
if ((nint)drawObj != IntPtr.Zero)
|
||||
{
|
||||
isDrawing = (gameObj->RenderFlags & VisibilityFlags.Nameplate) != VisibilityFlags.None;
|
||||
isDrawing = gameObj->RenderFlags == (VisibilityFlags)0b100000000000;
|
||||
if (!isDrawing)
|
||||
{
|
||||
isDrawing = ((CharacterBase*)drawObj)->HasModelInSlotLoaded != 0;
|
||||
@@ -819,9 +993,12 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
_performanceCollector.LogPerformance(this, $"TrackedActorsToState",
|
||||
() =>
|
||||
{
|
||||
_actorObjectService.RefreshTrackedActors();
|
||||
if (!_actorObjectService.HooksActive || !isNormalFrameworkUpdate || _actorObjectService.HasPendingHashResolutions)
|
||||
{
|
||||
_actorObjectService.RefreshTrackedActors();
|
||||
}
|
||||
|
||||
var playerDescriptors = _actorObjectService.PlayerCharacterDescriptors;
|
||||
var playerDescriptors = _actorObjectService.PlayerDescriptors;
|
||||
for (var i = 0; i < playerDescriptors.Count; i++)
|
||||
{
|
||||
var actor = playerDescriptors[i];
|
||||
@@ -833,7 +1010,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
if (actor.ObjectIndex >= 200)
|
||||
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"));
|
||||
continue;
|
||||
@@ -963,6 +1140,18 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
Mediator.Publish(new ZoneSwitchEndMessage());
|
||||
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;
|
||||
if (localPlayer != null)
|
||||
|
||||
@@ -0,0 +1,863 @@
|
||||
using Dalamud.Game.ClientState.Objects.Enums;
|
||||
using Dalamud.Plugin.Services;
|
||||
using FFXIVClientStructs.FFXIV.Client.System.Framework;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
using LightlessSync.LightlessConfiguration;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using LightlessSync.UI;
|
||||
using LightlessSync.UI.Services;
|
||||
using LightlessSync.Utils;
|
||||
using LightlessSync.UtilsEnum.Enum;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Collections.Immutable;
|
||||
using Task = System.Threading.Tasks.Task;
|
||||
|
||||
namespace LightlessSync.Services.LightFinder;
|
||||
|
||||
/// <summary>
|
||||
/// Native nameplate handler that injects LightFinder labels via the signature hook path.
|
||||
/// </summary>
|
||||
public unsafe class LightFinderNativePlateHandler : DisposableMediatorSubscriberBase, IHostedService
|
||||
{
|
||||
private const uint NameplateNodeIdBase = 0x7D99D500;
|
||||
private const string DefaultLabelText = "LightFinder";
|
||||
|
||||
private readonly ILogger<LightFinderNativePlateHandler> _logger;
|
||||
private readonly IClientState _clientState;
|
||||
private readonly IObjectTable _objectTable;
|
||||
private readonly LightlessConfigService _configService;
|
||||
private readonly PairUiService _pairUiService;
|
||||
private readonly NameplateUpdateHookService _nameplateUpdateHookService;
|
||||
|
||||
private readonly int[] _cachedNameplateTextWidths = new int[AddonNamePlate.NumNamePlateObjects];
|
||||
private readonly int[] _cachedNameplateTextHeights = new int[AddonNamePlate.NumNamePlateObjects];
|
||||
private readonly int[] _cachedNameplateContainerHeights = new int[AddonNamePlate.NumNamePlateObjects];
|
||||
private readonly int[] _cachedNameplateTextOffsets = new int[AddonNamePlate.NumNamePlateObjects];
|
||||
private readonly string?[] _lastLabelByIndex = new string?[AddonNamePlate.NumNamePlateObjects];
|
||||
|
||||
private ImmutableHashSet<string> _activeBroadcastingCids = [];
|
||||
private LightfinderLabelRenderer _lastRenderer;
|
||||
private uint _lastSignatureUpdateFrame;
|
||||
private bool _isUpdating;
|
||||
private string _lastLabelContent = DefaultLabelText;
|
||||
|
||||
public LightFinderNativePlateHandler(
|
||||
ILogger<LightFinderNativePlateHandler> logger,
|
||||
IClientState clientState,
|
||||
LightlessConfigService configService,
|
||||
LightlessMediator mediator,
|
||||
IObjectTable objectTable,
|
||||
PairUiService pairUiService,
|
||||
NameplateUpdateHookService nameplateUpdateHookService) : base(logger, mediator)
|
||||
{
|
||||
_logger = logger;
|
||||
_clientState = clientState;
|
||||
_configService = configService;
|
||||
_objectTable = objectTable;
|
||||
_pairUiService = pairUiService;
|
||||
_nameplateUpdateHookService = nameplateUpdateHookService;
|
||||
_lastRenderer = _configService.Current.LightfinderLabelRenderer;
|
||||
|
||||
Array.Fill(_cachedNameplateTextOffsets, int.MinValue);
|
||||
}
|
||||
|
||||
private bool IsSignatureMode => _configService.Current.LightfinderLabelRenderer == LightfinderLabelRenderer.SignatureHook;
|
||||
|
||||
/// <summary>
|
||||
/// Starts listening for nameplate updates from the hook service.
|
||||
/// </summary>
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_nameplateUpdateHookService.NameplateUpdated += OnNameplateUpdated;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops listening for nameplate updates and tears down any constructed nodes.
|
||||
/// </summary>
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_nameplateUpdateHookService.NameplateUpdated -= OnNameplateUpdated;
|
||||
UnsubscribeAll();
|
||||
TryDestroyNameplateNodes();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Triggered by the sig hook to refresh native nameplate labels.
|
||||
/// </summary>
|
||||
private void HandleNameplateUpdate(RaptureAtkModule* raptureAtkModule)
|
||||
{
|
||||
if (_isUpdating)
|
||||
return;
|
||||
|
||||
_isUpdating = true;
|
||||
try
|
||||
{
|
||||
RefreshRendererState();
|
||||
if (!IsSignatureMode)
|
||||
return;
|
||||
|
||||
if (raptureAtkModule == null)
|
||||
return;
|
||||
|
||||
var namePlateAddon = GetNamePlateAddon(raptureAtkModule);
|
||||
if (namePlateAddon == null)
|
||||
return;
|
||||
|
||||
if (_clientState.IsGPosing)
|
||||
{
|
||||
HideAllNameplateNodes(namePlateAddon);
|
||||
return;
|
||||
}
|
||||
|
||||
var fw = Framework.Instance();
|
||||
if (fw == null)
|
||||
return;
|
||||
|
||||
var frame = fw->FrameCounter;
|
||||
if (_lastSignatureUpdateFrame == frame)
|
||||
return;
|
||||
|
||||
_lastSignatureUpdateFrame = frame;
|
||||
UpdateNameplateNodes(namePlateAddon);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isUpdating = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Hook callback from the nameplate update signature.
|
||||
/// </summary>
|
||||
private void OnNameplateUpdated(RaptureAtkModule* raptureAtkModule, RaptureAtkModule.NamePlateInfo* namePlateInfo,
|
||||
NumberArrayData* numArray, StringArrayData* stringArray, BattleChara* battleChara, int numArrayIndex, int stringArrayIndex)
|
||||
{
|
||||
HandleNameplateUpdate(raptureAtkModule);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the active broadcasting CID set and requests a nameplate redraw.
|
||||
/// </summary>
|
||||
public void UpdateBroadcastingCids(IEnumerable<string> cids)
|
||||
{
|
||||
var newSet = cids.ToImmutableHashSet(StringComparer.Ordinal);
|
||||
if (ReferenceEquals(_activeBroadcastingCids, newSet) || _activeBroadcastingCids.SetEquals(newSet))
|
||||
return;
|
||||
|
||||
_activeBroadcastingCids = newSet;
|
||||
if (_logger.IsEnabled(LogLevel.Trace))
|
||||
_logger.LogTrace("Active broadcast IDs (native): {Cids}", string.Join(',', _activeBroadcastingCids));
|
||||
RequestNameplateRedraw();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sync renderer state with config and clear/remove native nodes if needed.
|
||||
/// </summary>
|
||||
private void RefreshRendererState()
|
||||
{
|
||||
var renderer = _configService.Current.LightfinderLabelRenderer;
|
||||
if (renderer == _lastRenderer)
|
||||
return;
|
||||
|
||||
_lastRenderer = renderer;
|
||||
|
||||
if (renderer == LightfinderLabelRenderer.SignatureHook)
|
||||
{
|
||||
ClearNameplateCaches();
|
||||
RequestNameplateRedraw();
|
||||
}
|
||||
else
|
||||
{
|
||||
TryDestroyNameplateNodes();
|
||||
ClearNameplateCaches();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Requests a full nameplate update through the native addon.
|
||||
/// </summary>
|
||||
private void RequestNameplateRedraw()
|
||||
{
|
||||
if (!IsSignatureMode)
|
||||
return;
|
||||
|
||||
var raptureAtkModule = GetRaptureAtkModule();
|
||||
if (raptureAtkModule == null)
|
||||
return;
|
||||
|
||||
var namePlateAddon = GetNamePlateAddon(raptureAtkModule);
|
||||
if (namePlateAddon == null)
|
||||
return;
|
||||
|
||||
namePlateAddon->DoFullUpdate = 1;
|
||||
}
|
||||
|
||||
private HashSet<ulong> VisibleUserIds
|
||||
=> [.. _pairUiService.GetSnapshot().PairsByUid.Values
|
||||
.Where(u => u.IsVisible && u.PlayerCharacterId != uint.MaxValue)
|
||||
.Select(u => (ulong)u.PlayerCharacterId)];
|
||||
|
||||
/// <summary>
|
||||
/// Creates/updates LightFinder label nodes for active broadcasts.
|
||||
/// </summary>
|
||||
private void UpdateNameplateNodes(AddonNamePlate* namePlateAddon)
|
||||
{
|
||||
if (namePlateAddon == null)
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
_logger.LogDebug("NamePlate addon unavailable during update, skipping label refresh.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!IsNameplateAddonVisible(namePlateAddon))
|
||||
return;
|
||||
|
||||
if (!IsSignatureMode)
|
||||
{
|
||||
HideAllNameplateNodes(namePlateAddon);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_activeBroadcastingCids.Count == 0)
|
||||
{
|
||||
HideAllNameplateNodes(namePlateAddon);
|
||||
return;
|
||||
}
|
||||
|
||||
var framework = Framework.Instance();
|
||||
if (framework == null)
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
_logger.LogDebug("Framework instance unavailable during nameplate update, skipping.");
|
||||
return;
|
||||
}
|
||||
|
||||
var uiModule = framework->GetUIModule();
|
||||
if (uiModule == null)
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
_logger.LogDebug("UI module unavailable during nameplate update, skipping.");
|
||||
return;
|
||||
}
|
||||
|
||||
var ui3DModule = uiModule->GetUI3DModule();
|
||||
if (ui3DModule == null)
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
_logger.LogDebug("UI3D module unavailable during nameplate update, skipping.");
|
||||
return;
|
||||
}
|
||||
|
||||
var vec = ui3DModule->NamePlateObjectInfoPointers;
|
||||
if (vec.IsEmpty)
|
||||
return;
|
||||
|
||||
var config = _configService.Current;
|
||||
var visibleUserIdsSnapshot = VisibleUserIds;
|
||||
var labelColor = UIColors.Get("Lightfinder");
|
||||
var edgeColor = UIColors.Get("LightfinderEdge");
|
||||
var scaleMultiplier = Math.Clamp(config.LightfinderLabelScale, 0.5f, 2.0f);
|
||||
var baseScale = config.LightfinderLabelUseIcon ? 1.0f : 0.5f;
|
||||
var effectiveScale = baseScale * scaleMultiplier;
|
||||
var labelContent = config.LightfinderLabelUseIcon
|
||||
? LightFinderPlateHandler.NormalizeIconGlyph(config.LightfinderLabelIconGlyph)
|
||||
: DefaultLabelText;
|
||||
|
||||
if (!config.LightfinderLabelUseIcon && (string.IsNullOrWhiteSpace(labelContent) || string.Equals(labelContent, "-", StringComparison.Ordinal)))
|
||||
labelContent = DefaultLabelText;
|
||||
|
||||
if (!string.Equals(_lastLabelContent, labelContent, StringComparison.Ordinal))
|
||||
{
|
||||
_lastLabelContent = labelContent;
|
||||
Array.Fill(_lastLabelByIndex, null);
|
||||
}
|
||||
|
||||
var desiredFontType = config.LightfinderLabelUseIcon ? FontType.Axis : FontType.MiedingerMed;
|
||||
var baseFontSize = config.LightfinderLabelUseIcon ? 36f : 24f;
|
||||
var desiredFontSize = (byte)Math.Clamp((int)Math.Round(baseFontSize * scaleMultiplier), 1, 255);
|
||||
var desiredFlags = config.LightfinderLabelUseIcon
|
||||
? TextFlags.Edge | TextFlags.Glare | TextFlags.AutoAdjustNodeSize
|
||||
: TextFlags.Edge | TextFlags.Glare;
|
||||
var desiredLineSpacing = (byte)Math.Clamp((int)Math.Round(24 * scaleMultiplier), 0, byte.MaxValue);
|
||||
var defaultNodeWidth = (int)Math.Round(AtkNodeHelpers.DefaultTextNodeWidth * effectiveScale);
|
||||
var defaultNodeHeight = (int)Math.Round(AtkNodeHelpers.DefaultTextNodeHeight * effectiveScale);
|
||||
|
||||
var safeCount = Math.Min(ui3DModule->NamePlateObjectInfoCount, vec.Length);
|
||||
var visibleIndices = new bool[AddonNamePlate.NumNamePlateObjects];
|
||||
|
||||
for (int i = 0; i < safeCount; ++i)
|
||||
{
|
||||
var objectInfoPtr = vec[i];
|
||||
if (objectInfoPtr == null)
|
||||
continue;
|
||||
|
||||
var objectInfo = objectInfoPtr.Value;
|
||||
if (objectInfo == null || objectInfo->GameObject == null)
|
||||
continue;
|
||||
|
||||
var nameplateIndex = objectInfo->NamePlateIndex;
|
||||
if (nameplateIndex < 0 || nameplateIndex >= AddonNamePlate.NumNamePlateObjects)
|
||||
continue;
|
||||
|
||||
var gameObject = objectInfo->GameObject;
|
||||
if ((ObjectKind)gameObject->ObjectKind != ObjectKind.Player)
|
||||
continue;
|
||||
|
||||
var cid = DalamudUtilService.GetHashedCIDFromPlayerPointer((nint)gameObject);
|
||||
if (cid == null || !_activeBroadcastingCids.Contains(cid))
|
||||
continue;
|
||||
|
||||
var local = _objectTable.LocalPlayer;
|
||||
if (!config.LightfinderLabelShowOwn && local != null &&
|
||||
objectInfo->GameObject->GetGameObjectId() == local.GameObjectId)
|
||||
continue;
|
||||
|
||||
var hidePaired = !config.LightfinderLabelShowPaired;
|
||||
var goId = (ulong)gameObject->GetGameObjectId();
|
||||
if (hidePaired && visibleUserIdsSnapshot.Contains(goId))
|
||||
continue;
|
||||
|
||||
var nameplateObject = namePlateAddon->NamePlateObjectArray[nameplateIndex];
|
||||
var root = nameplateObject.RootComponentNode;
|
||||
var nameContainer = nameplateObject.NameContainer;
|
||||
var nameText = nameplateObject.NameText;
|
||||
var marker = nameplateObject.MarkerIcon;
|
||||
|
||||
if (root == null || root->Component == null || nameContainer == null || nameText == null)
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
_logger.LogDebug("Nameplate {Index} missing required nodes during update, skipping.", nameplateIndex);
|
||||
continue;
|
||||
}
|
||||
|
||||
var nodeId = GetNameplateNodeId(nameplateIndex);
|
||||
var pNode = EnsureNameplateTextNode(nameContainer, root, nodeId, out var nodeCreated);
|
||||
if (pNode == null)
|
||||
continue;
|
||||
|
||||
bool isVisible =
|
||||
((marker != null) && marker->AtkResNode.IsVisible()) ||
|
||||
(nameContainer->IsVisible() && nameText->AtkResNode.IsVisible()) ||
|
||||
config.LightfinderLabelShowHidden;
|
||||
|
||||
if (!isVisible)
|
||||
continue;
|
||||
|
||||
if (!pNode->AtkResNode.IsVisible())
|
||||
pNode->AtkResNode.ToggleVisibility(enable: true);
|
||||
visibleIndices[nameplateIndex] = true;
|
||||
|
||||
if (nodeCreated)
|
||||
pNode->AtkResNode.SetUseDepthBasedPriority(enable: true);
|
||||
|
||||
var scaleMatches = NearlyEqual(pNode->AtkResNode.ScaleX, effectiveScale) &&
|
||||
NearlyEqual(pNode->AtkResNode.ScaleY, effectiveScale);
|
||||
if (!scaleMatches)
|
||||
pNode->AtkResNode.SetScale(effectiveScale, effectiveScale);
|
||||
|
||||
var fontTypeChanged = pNode->FontType != desiredFontType;
|
||||
if (fontTypeChanged)
|
||||
pNode->FontType = desiredFontType;
|
||||
|
||||
var fontSizeChanged = pNode->FontSize != desiredFontSize;
|
||||
if (fontSizeChanged)
|
||||
pNode->FontSize = desiredFontSize;
|
||||
|
||||
var needsTextUpdate = nodeCreated ||
|
||||
!string.Equals(_lastLabelByIndex[nameplateIndex], labelContent, StringComparison.Ordinal);
|
||||
if (needsTextUpdate)
|
||||
{
|
||||
pNode->SetText(labelContent);
|
||||
_lastLabelByIndex[nameplateIndex] = labelContent;
|
||||
}
|
||||
|
||||
var flagsChanged = pNode->TextFlags != desiredFlags;
|
||||
var nodeWidth = (int)pNode->AtkResNode.GetWidth();
|
||||
if (nodeWidth <= 0)
|
||||
nodeWidth = defaultNodeWidth;
|
||||
var nodeHeight = defaultNodeHeight;
|
||||
AlignmentType alignment;
|
||||
|
||||
var textScaleY = nameText->AtkResNode.ScaleY;
|
||||
if (textScaleY <= 0f)
|
||||
textScaleY = 1f;
|
||||
|
||||
var blockHeight = Math.Abs((int)nameplateObject.TextH);
|
||||
if (blockHeight > 0)
|
||||
{
|
||||
_cachedNameplateTextHeights[nameplateIndex] = blockHeight;
|
||||
}
|
||||
else
|
||||
{
|
||||
blockHeight = _cachedNameplateTextHeights[nameplateIndex];
|
||||
}
|
||||
|
||||
if (blockHeight <= 0)
|
||||
{
|
||||
blockHeight = GetScaledTextHeight(nameText);
|
||||
if (blockHeight <= 0)
|
||||
blockHeight = nodeHeight;
|
||||
|
||||
_cachedNameplateTextHeights[nameplateIndex] = blockHeight;
|
||||
}
|
||||
|
||||
var containerHeight = (int)nameContainer->Height;
|
||||
if (containerHeight > 0)
|
||||
{
|
||||
_cachedNameplateContainerHeights[nameplateIndex] = containerHeight;
|
||||
}
|
||||
else
|
||||
{
|
||||
containerHeight = _cachedNameplateContainerHeights[nameplateIndex];
|
||||
}
|
||||
|
||||
if (containerHeight <= 0)
|
||||
{
|
||||
containerHeight = blockHeight + (int)Math.Round(8 * textScaleY);
|
||||
if (containerHeight <= blockHeight)
|
||||
containerHeight = blockHeight + 1;
|
||||
|
||||
_cachedNameplateContainerHeights[nameplateIndex] = containerHeight;
|
||||
}
|
||||
|
||||
var blockTop = containerHeight - blockHeight;
|
||||
if (blockTop < 0)
|
||||
blockTop = 0;
|
||||
var verticalPadding = (int)Math.Round(4 * effectiveScale);
|
||||
|
||||
var positionY = blockTop - verticalPadding - nodeHeight;
|
||||
|
||||
var textWidth = Math.Abs((int)nameplateObject.TextW);
|
||||
if (textWidth <= 0)
|
||||
{
|
||||
textWidth = GetScaledTextWidth(nameText);
|
||||
if (textWidth <= 0)
|
||||
textWidth = nodeWidth;
|
||||
}
|
||||
|
||||
if (textWidth > 0)
|
||||
{
|
||||
_cachedNameplateTextWidths[nameplateIndex] = textWidth;
|
||||
}
|
||||
|
||||
var textOffset = (int)Math.Round(nameText->AtkResNode.X);
|
||||
var hasValidOffset = false;
|
||||
|
||||
if (Math.Abs((int)nameplateObject.TextW) > 0 || textOffset != 0)
|
||||
{
|
||||
_cachedNameplateTextOffsets[nameplateIndex] = textOffset;
|
||||
hasValidOffset = true;
|
||||
}
|
||||
else if (_cachedNameplateTextOffsets[nameplateIndex] != int.MinValue)
|
||||
{
|
||||
hasValidOffset = true;
|
||||
}
|
||||
|
||||
int positionX;
|
||||
|
||||
if (!config.LightfinderLabelUseIcon)
|
||||
{
|
||||
var needsWidthRefresh = nodeCreated || needsTextUpdate || !scaleMatches || fontTypeChanged || fontSizeChanged || flagsChanged;
|
||||
if (flagsChanged)
|
||||
pNode->TextFlags = desiredFlags;
|
||||
|
||||
if (needsWidthRefresh)
|
||||
{
|
||||
if (pNode->AtkResNode.Width != 0)
|
||||
pNode->AtkResNode.Width = 0;
|
||||
nodeWidth = (int)pNode->AtkResNode.GetWidth();
|
||||
if (nodeWidth <= 0)
|
||||
nodeWidth = defaultNodeWidth;
|
||||
}
|
||||
|
||||
if (pNode->AtkResNode.Width != (ushort)nodeWidth)
|
||||
pNode->AtkResNode.Width = (ushort)nodeWidth;
|
||||
}
|
||||
else
|
||||
{
|
||||
var needsWidthRefresh = nodeCreated || needsTextUpdate || !scaleMatches || fontTypeChanged || fontSizeChanged || flagsChanged;
|
||||
if (flagsChanged)
|
||||
pNode->TextFlags = desiredFlags;
|
||||
|
||||
if (needsWidthRefresh && pNode->AtkResNode.Width != 0)
|
||||
pNode->AtkResNode.Width = 0;
|
||||
nodeWidth = pNode->AtkResNode.GetWidth();
|
||||
}
|
||||
|
||||
if (config.LightfinderAutoAlign && nameContainer != null && hasValidOffset)
|
||||
{
|
||||
var nameplateWidth = (int)nameContainer->Width;
|
||||
|
||||
int leftPos = nameplateWidth / 8;
|
||||
int rightPos = nameplateWidth - nodeWidth - (nameplateWidth / 8);
|
||||
int centrePos = (nameplateWidth - nodeWidth) / 2;
|
||||
int staticMargin = 24;
|
||||
int calcMargin = (int)(nameplateWidth * 0.08f);
|
||||
|
||||
switch (config.LabelAlignment)
|
||||
{
|
||||
case LabelAlignment.Left:
|
||||
positionX = config.LightfinderLabelUseIcon ? leftPos + staticMargin : leftPos;
|
||||
alignment = AlignmentType.BottomLeft;
|
||||
break;
|
||||
case LabelAlignment.Right:
|
||||
positionX = config.LightfinderLabelUseIcon ? rightPos - staticMargin : nameplateWidth - nodeWidth + calcMargin;
|
||||
alignment = AlignmentType.BottomRight;
|
||||
break;
|
||||
default:
|
||||
positionX = config.LightfinderLabelUseIcon ? centrePos : centrePos + calcMargin;
|
||||
alignment = AlignmentType.Bottom;
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
positionX = 58 + config.LightfinderLabelOffsetX;
|
||||
alignment = AlignmentType.Bottom;
|
||||
}
|
||||
|
||||
positionY += config.LightfinderLabelOffsetY;
|
||||
|
||||
alignment = (AlignmentType)Math.Clamp((int)alignment, 0, 8);
|
||||
if (pNode->AtkResNode.Color.A != 255)
|
||||
pNode->AtkResNode.Color.A = 255;
|
||||
|
||||
var textR = (byte)(labelColor.X * 255);
|
||||
var textG = (byte)(labelColor.Y * 255);
|
||||
var textB = (byte)(labelColor.Z * 255);
|
||||
var textA = (byte)(labelColor.W * 255);
|
||||
|
||||
if (pNode->TextColor.R != textR || pNode->TextColor.G != textG ||
|
||||
pNode->TextColor.B != textB || pNode->TextColor.A != textA)
|
||||
{
|
||||
pNode->TextColor.R = textR;
|
||||
pNode->TextColor.G = textG;
|
||||
pNode->TextColor.B = textB;
|
||||
pNode->TextColor.A = textA;
|
||||
}
|
||||
|
||||
var edgeR = (byte)(edgeColor.X * 255);
|
||||
var edgeG = (byte)(edgeColor.Y * 255);
|
||||
var edgeB = (byte)(edgeColor.Z * 255);
|
||||
var edgeA = (byte)(edgeColor.W * 255);
|
||||
|
||||
if (pNode->EdgeColor.R != edgeR || pNode->EdgeColor.G != edgeG ||
|
||||
pNode->EdgeColor.B != edgeB || pNode->EdgeColor.A != edgeA)
|
||||
{
|
||||
pNode->EdgeColor.R = edgeR;
|
||||
pNode->EdgeColor.G = edgeG;
|
||||
pNode->EdgeColor.B = edgeB;
|
||||
pNode->EdgeColor.A = edgeA;
|
||||
}
|
||||
|
||||
var desiredAlignment = config.LightfinderLabelUseIcon ? alignment : AlignmentType.Bottom;
|
||||
if (pNode->AlignmentType != desiredAlignment)
|
||||
pNode->AlignmentType = desiredAlignment;
|
||||
|
||||
var desiredX = (short)Math.Clamp(positionX, short.MinValue, short.MaxValue);
|
||||
var desiredY = (short)Math.Clamp(positionY, short.MinValue, short.MaxValue);
|
||||
if (!NearlyEqual(pNode->AtkResNode.X, desiredX) || !NearlyEqual(pNode->AtkResNode.Y, desiredY))
|
||||
pNode->AtkResNode.SetPositionShort(desiredX, desiredY);
|
||||
|
||||
if (pNode->LineSpacing != desiredLineSpacing)
|
||||
pNode->LineSpacing = desiredLineSpacing;
|
||||
if (pNode->CharSpacing != 1)
|
||||
pNode->CharSpacing = 1;
|
||||
}
|
||||
|
||||
HideUnmarkedNodes(namePlateAddon, visibleIndices);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolve the current RaptureAtkModule for native UI access.
|
||||
/// </summary>
|
||||
private static RaptureAtkModule* GetRaptureAtkModule()
|
||||
{
|
||||
var framework = Framework.Instance();
|
||||
if (framework == null)
|
||||
return null;
|
||||
|
||||
var uiModule = framework->GetUIModule();
|
||||
if (uiModule == null)
|
||||
return null;
|
||||
|
||||
return uiModule->GetRaptureAtkModule();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolve the NamePlate addon from the given RaptureAtkModule.
|
||||
/// </summary>
|
||||
private static AddonNamePlate* GetNamePlateAddon(RaptureAtkModule* raptureAtkModule)
|
||||
{
|
||||
if (raptureAtkModule == null)
|
||||
return null;
|
||||
|
||||
var addon = raptureAtkModule->RaptureAtkUnitManager.GetAddonByName("NamePlate");
|
||||
return addon != null ? (AddonNamePlate*)addon : null;
|
||||
}
|
||||
|
||||
private static uint GetNameplateNodeId(int index)
|
||||
=> NameplateNodeIdBase + (uint)index;
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the NamePlate addon is visible and safe to touch.
|
||||
/// </summary>
|
||||
private static bool IsNameplateAddonVisible(AddonNamePlate* namePlateAddon)
|
||||
{
|
||||
if (namePlateAddon == null)
|
||||
return false;
|
||||
|
||||
var root = namePlateAddon->AtkUnitBase.RootNode;
|
||||
return root != null && root->IsVisible();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds a LightFinder text node by ID in the name container.
|
||||
/// </summary>
|
||||
private static AtkTextNode* FindNameplateTextNode(AtkResNode* nameContainer, uint nodeId)
|
||||
{
|
||||
if (nameContainer == null)
|
||||
return null;
|
||||
|
||||
var child = nameContainer->ChildNode;
|
||||
while (child != null)
|
||||
{
|
||||
if (child->NodeId == nodeId &&
|
||||
child->Type == NodeType.Text &&
|
||||
child->ParentNode == nameContainer)
|
||||
return (AtkTextNode*)child;
|
||||
|
||||
child = child->PrevSiblingNode;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensures a LightFinder text node exists for the given nameplate index.
|
||||
/// </summary>
|
||||
private static AtkTextNode* EnsureNameplateTextNode(AtkResNode* nameContainer, AtkComponentNode* root, uint nodeId, out bool created)
|
||||
{
|
||||
created = false;
|
||||
if (nameContainer == null || root == null || root->Component == null)
|
||||
return null;
|
||||
|
||||
var existing = FindNameplateTextNode(nameContainer, nodeId);
|
||||
if (existing != null)
|
||||
return existing;
|
||||
|
||||
if (nameContainer->ChildNode == null)
|
||||
return null;
|
||||
|
||||
var newNode = AtkNodeHelpers.CreateOrphanTextNode(nodeId, TextFlags.Edge | TextFlags.Glare);
|
||||
if (newNode == null)
|
||||
return null;
|
||||
|
||||
var lastChild = nameContainer->ChildNode;
|
||||
while (lastChild->PrevSiblingNode != null)
|
||||
lastChild = lastChild->PrevSiblingNode;
|
||||
|
||||
newNode->AtkResNode.NextSiblingNode = lastChild;
|
||||
newNode->AtkResNode.ParentNode = nameContainer;
|
||||
lastChild->PrevSiblingNode = (AtkResNode*)newNode;
|
||||
root->Component->UldManager.UpdateDrawNodeList();
|
||||
newNode->AtkResNode.SetUseDepthBasedPriority(true);
|
||||
|
||||
created = true;
|
||||
return newNode;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Hides all native LightFinder nodes on the nameplate addon.
|
||||
/// </summary>
|
||||
private static void HideAllNameplateNodes(AddonNamePlate* namePlateAddon)
|
||||
{
|
||||
if (namePlateAddon == null)
|
||||
return;
|
||||
|
||||
if (!IsNameplateAddonVisible(namePlateAddon))
|
||||
return;
|
||||
|
||||
for (int i = 0; i < AddonNamePlate.NumNamePlateObjects; ++i)
|
||||
{
|
||||
HideNameplateTextNode(namePlateAddon->NamePlateObjectArray[i], GetNameplateNodeId(i));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Hides all LightFinder nodes not marked as visible this frame.
|
||||
/// </summary>
|
||||
private static void HideUnmarkedNodes(AddonNamePlate* namePlateAddon, bool[] visibleIndices)
|
||||
{
|
||||
if (namePlateAddon == null)
|
||||
return;
|
||||
|
||||
if (!IsNameplateAddonVisible(namePlateAddon))
|
||||
return;
|
||||
|
||||
var visibleLength = visibleIndices.Length;
|
||||
for (int i = 0; i < AddonNamePlate.NumNamePlateObjects; ++i)
|
||||
{
|
||||
if (i < visibleLength && visibleIndices[i])
|
||||
continue;
|
||||
|
||||
HideNameplateTextNode(namePlateAddon->NamePlateObjectArray[i], GetNameplateNodeId(i));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Hides the LightFinder text node for a single nameplate object.
|
||||
/// </summary>
|
||||
private static void HideNameplateTextNode(AddonNamePlate.NamePlateObject nameplateObject, uint nodeId)
|
||||
{
|
||||
var nameContainer = nameplateObject.NameContainer;
|
||||
if (nameContainer == null)
|
||||
return;
|
||||
|
||||
var node = FindNameplateTextNode(nameContainer, nodeId);
|
||||
if (!IsValidNameplateTextNode(node, nameContainer))
|
||||
return;
|
||||
|
||||
node->AtkResNode.ToggleVisibility(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to destroy all constructed LightFinder nodes safely.
|
||||
/// </summary>
|
||||
private void TryDestroyNameplateNodes()
|
||||
{
|
||||
var raptureAtkModule = GetRaptureAtkModule();
|
||||
if (raptureAtkModule == null)
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
_logger.LogDebug("Unable to destroy nameplate nodes because the RaptureAtkModule is not available.");
|
||||
return;
|
||||
}
|
||||
|
||||
var namePlateAddon = GetNamePlateAddon(raptureAtkModule);
|
||||
if (namePlateAddon == null)
|
||||
{
|
||||
if (_logger.IsEnabled(LogLevel.Debug))
|
||||
_logger.LogDebug("Unable to destroy nameplate nodes because the NamePlate addon is not available.");
|
||||
return;
|
||||
}
|
||||
|
||||
DestroyNameplateNodes(namePlateAddon);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes all constructed LightFinder nodes from the given nameplate addon.
|
||||
/// </summary>
|
||||
private void DestroyNameplateNodes(AddonNamePlate* namePlateAddon)
|
||||
{
|
||||
if (namePlateAddon == null)
|
||||
return;
|
||||
|
||||
for (int i = 0; i < AddonNamePlate.NumNamePlateObjects; ++i)
|
||||
{
|
||||
var nameplateObject = namePlateAddon->NamePlateObjectArray[i];
|
||||
var root = nameplateObject.RootComponentNode;
|
||||
var nameContainer = nameplateObject.NameContainer;
|
||||
if (root == null || root->Component == null || nameContainer == null)
|
||||
continue;
|
||||
|
||||
var nodeId = GetNameplateNodeId(i);
|
||||
var textNode = FindNameplateTextNode(nameContainer, nodeId);
|
||||
if (!IsValidNameplateTextNode(textNode, nameContainer))
|
||||
continue;
|
||||
|
||||
try
|
||||
{
|
||||
var resNode = &textNode->AtkResNode;
|
||||
|
||||
if (resNode->PrevSiblingNode != null)
|
||||
resNode->PrevSiblingNode->NextSiblingNode = resNode->NextSiblingNode;
|
||||
if (resNode->NextSiblingNode != null)
|
||||
resNode->NextSiblingNode->PrevSiblingNode = resNode->PrevSiblingNode;
|
||||
|
||||
root->Component->UldManager.UpdateDrawNodeList();
|
||||
resNode->Destroy(true);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "Unknown error while removing text node 0x{Node:X} for nameplate {Index} on component node 0x{Component:X}", (IntPtr)textNode, i, (IntPtr)root);
|
||||
}
|
||||
}
|
||||
|
||||
ClearNameplateCaches();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates that a node is a LightFinder text node owned by the container.
|
||||
/// </summary>
|
||||
private static bool IsValidNameplateTextNode(AtkTextNode* node, AtkResNode* nameContainer)
|
||||
{
|
||||
if (node == null || nameContainer == null)
|
||||
return false;
|
||||
|
||||
var resNode = &node->AtkResNode;
|
||||
return resNode->Type == NodeType.Text && resNode->ParentNode == nameContainer;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Float comparison helper for UI values.
|
||||
/// </summary>
|
||||
private static bool NearlyEqual(float a, float b, float epsilon = 0.001f)
|
||||
=> Math.Abs(a - b) <= epsilon;
|
||||
|
||||
private static int GetScaledTextHeight(AtkTextNode* node)
|
||||
{
|
||||
if (node == null)
|
||||
return 0;
|
||||
|
||||
var resNode = &node->AtkResNode;
|
||||
var rawHeight = (int)resNode->GetHeight();
|
||||
if (rawHeight <= 0 && node->LineSpacing > 0)
|
||||
rawHeight = node->LineSpacing;
|
||||
if (rawHeight <= 0)
|
||||
rawHeight = AtkNodeHelpers.DefaultTextNodeHeight;
|
||||
|
||||
var scale = resNode->ScaleY;
|
||||
if (scale <= 0f)
|
||||
scale = 1f;
|
||||
|
||||
var computed = (int)Math.Round(rawHeight * scale);
|
||||
return Math.Max(1, computed);
|
||||
}
|
||||
|
||||
private static int GetScaledTextWidth(AtkTextNode* node)
|
||||
{
|
||||
if (node == null)
|
||||
return 0;
|
||||
|
||||
var resNode = &node->AtkResNode;
|
||||
var rawWidth = (int)resNode->GetWidth();
|
||||
if (rawWidth <= 0)
|
||||
rawWidth = AtkNodeHelpers.DefaultTextNodeWidth;
|
||||
|
||||
var scale = resNode->ScaleX;
|
||||
if (scale <= 0f)
|
||||
scale = 1f;
|
||||
|
||||
var computed = (int)Math.Round(rawWidth * scale);
|
||||
return Math.Max(1, computed);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears cached text sizing and label state for nameplates.
|
||||
/// </summary>
|
||||
public void ClearNameplateCaches()
|
||||
{
|
||||
Array.Clear(_cachedNameplateTextWidths, 0, _cachedNameplateTextWidths.Length);
|
||||
Array.Clear(_cachedNameplateTextHeights, 0, _cachedNameplateTextHeights.Length);
|
||||
Array.Clear(_cachedNameplateContainerHeights, 0, _cachedNameplateContainerHeights.Length);
|
||||
Array.Fill(_cachedNameplateTextOffsets, int.MinValue);
|
||||
Array.Fill(_lastLabelByIndex, null);
|
||||
}
|
||||
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -15,11 +15,14 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase
|
||||
|
||||
private readonly LightFinderService _broadcastService;
|
||||
private readonly LightFinderPlateHandler _lightFinderPlateHandler;
|
||||
private readonly LightFinderNativePlateHandler _lightFinderNativePlateHandler;
|
||||
|
||||
private readonly ConcurrentDictionary<string, BroadcastEntry> _broadcastCache = new(StringComparer.Ordinal);
|
||||
private readonly Queue<string> _lookupQueue = new();
|
||||
private readonly HashSet<string> _lookupQueuedCids = [];
|
||||
private readonly HashSet<string> _syncshellCids = [];
|
||||
private volatile bool _pendingLocalBroadcast;
|
||||
private TimeSpan? _pendingLocalTtl;
|
||||
|
||||
private static readonly TimeSpan _maxAllowedTtl = TimeSpan.FromMinutes(4);
|
||||
private static readonly TimeSpan _retryDelay = TimeSpan.FromMinutes(1);
|
||||
@@ -42,12 +45,14 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase
|
||||
LightFinderService broadcastService,
|
||||
LightlessMediator mediator,
|
||||
LightFinderPlateHandler lightFinderPlateHandler,
|
||||
LightFinderNativePlateHandler lightFinderNativePlateHandler,
|
||||
ActorObjectService actorTracker) : base(logger, mediator)
|
||||
{
|
||||
_logger = logger;
|
||||
_actorTracker = actorTracker;
|
||||
_broadcastService = broadcastService;
|
||||
_lightFinderPlateHandler = lightFinderPlateHandler;
|
||||
_lightFinderNativePlateHandler = lightFinderNativePlateHandler;
|
||||
|
||||
_logger = logger;
|
||||
_framework = framework;
|
||||
@@ -69,14 +74,16 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase
|
||||
if (!_broadcastService.IsBroadcasting)
|
||||
return;
|
||||
|
||||
TryPrimeLocalBroadcastCache();
|
||||
|
||||
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;
|
||||
|
||||
var cid = DalamudUtilService.GetHashedCIDFromPlayerPointer(address);
|
||||
var cid = descriptor.HashedContentId;
|
||||
var isStale = !_broadcastCache.TryGetValue(cid, out var entry) || entry.ExpiryTime <= now;
|
||||
|
||||
if (isStale && _lookupQueuedCids.Add(cid) && _lookupQueue.Count < _maxQueueSize)
|
||||
@@ -129,6 +136,7 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase
|
||||
.ToList();
|
||||
|
||||
_lightFinderPlateHandler.UpdateBroadcastingCids(activeCids);
|
||||
_lightFinderNativePlateHandler.UpdateBroadcastingCids(activeCids);
|
||||
UpdateSyncshellBroadcasts();
|
||||
}
|
||||
|
||||
@@ -140,18 +148,58 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase
|
||||
_lookupQueue.Clear();
|
||||
_lookupQueuedCids.Clear();
|
||||
_syncshellCids.Clear();
|
||||
_pendingLocalBroadcast = false;
|
||||
_pendingLocalTtl = null;
|
||||
|
||||
_lightFinderPlateHandler.UpdateBroadcastingCids([]);
|
||||
_lightFinderNativePlateHandler.UpdateBroadcastingCids([]);
|
||||
return;
|
||||
}
|
||||
|
||||
_pendingLocalBroadcast = true;
|
||||
_pendingLocalTtl = msg.Ttl;
|
||||
TryPrimeLocalBroadcastCache();
|
||||
}
|
||||
|
||||
private void TryPrimeLocalBroadcastCache()
|
||||
{
|
||||
if (!_pendingLocalBroadcast)
|
||||
return;
|
||||
|
||||
if (!TryGetLocalHashedCid(out var localCid))
|
||||
return;
|
||||
|
||||
var ttl = _pendingLocalTtl ?? _maxAllowedTtl;
|
||||
var expiry = DateTime.UtcNow + ttl;
|
||||
|
||||
_broadcastCache.AddOrUpdate(localCid,
|
||||
new BroadcastEntry(true, expiry, null),
|
||||
(_, old) => new BroadcastEntry(true, expiry, old.GID));
|
||||
|
||||
_pendingLocalBroadcast = false;
|
||||
_pendingLocalTtl = null;
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
var activeCids = _broadcastCache
|
||||
.Where(e => e.Value.IsBroadcasting && e.Value.ExpiryTime > now)
|
||||
.Select(e => e.Key)
|
||||
.ToList();
|
||||
|
||||
_lightFinderPlateHandler.UpdateBroadcastingCids(activeCids);
|
||||
_lightFinderNativePlateHandler.UpdateBroadcastingCids(activeCids);
|
||||
}
|
||||
|
||||
private void UpdateSyncshellBroadcasts()
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
var newSet = _broadcastCache
|
||||
.Where(e => e.Value.IsBroadcasting && e.Value.ExpiryTime > now && !string.IsNullOrEmpty(e.Value.GID))
|
||||
.Select(e => e.Key)
|
||||
.ToHashSet(StringComparer.Ordinal);
|
||||
var nearbyCids = GetNearbyHashedCids(out _);
|
||||
var newSet = nearbyCids.Count == 0
|
||||
? new HashSet<string>(StringComparer.Ordinal)
|
||||
: _broadcastCache
|
||||
.Where(e => e.Value.IsBroadcasting && e.Value.ExpiryTime > now && !string.IsNullOrEmpty(e.Value.GID))
|
||||
.Where(e => nearbyCids.Contains(e.Key))
|
||||
.Select(e => e.Key)
|
||||
.ToHashSet(StringComparer.Ordinal);
|
||||
|
||||
if (!_syncshellCids.SetEquals(newSet))
|
||||
{
|
||||
@@ -163,12 +211,17 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase
|
||||
}
|
||||
}
|
||||
|
||||
public List<BroadcastStatusInfoDto> GetActiveSyncshellBroadcasts()
|
||||
public List<BroadcastStatusInfoDto> GetActiveSyncshellBroadcasts(bool excludeLocal = false)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
var nearbyCids = GetNearbyHashedCids(out var localCid);
|
||||
if (nearbyCids.Count == 0)
|
||||
return [];
|
||||
|
||||
return [.. _broadcastCache
|
||||
.Where(e => e.Value.IsBroadcasting && e.Value.ExpiryTime > now && !string.IsNullOrEmpty(e.Value.GID))
|
||||
.Where(e => nearbyCids.Contains(e.Key))
|
||||
.Where(e => !excludeLocal || !string.Equals(e.Key, localCid, StringComparison.Ordinal))
|
||||
.Select(e => new BroadcastStatusInfoDto
|
||||
{
|
||||
HashedCID = e.Key,
|
||||
@@ -178,6 +231,47 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase
|
||||
})];
|
||||
}
|
||||
|
||||
public bool TryGetLocalHashedCid(out string hashedCid)
|
||||
{
|
||||
hashedCid = string.Empty;
|
||||
var descriptors = _actorTracker.PlayerDescriptors;
|
||||
if (descriptors.Count == 0)
|
||||
return false;
|
||||
|
||||
foreach (var descriptor in descriptors)
|
||||
{
|
||||
if (!descriptor.IsLocalPlayer || string.IsNullOrWhiteSpace(descriptor.HashedContentId))
|
||||
continue;
|
||||
|
||||
hashedCid = descriptor.HashedContentId;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private HashSet<string> GetNearbyHashedCids(out string? localCid)
|
||||
{
|
||||
localCid = null;
|
||||
var descriptors = _actorTracker.PlayerDescriptors;
|
||||
if (descriptors.Count == 0)
|
||||
return new HashSet<string>(StringComparer.Ordinal);
|
||||
|
||||
var set = new HashSet<string>(StringComparer.Ordinal);
|
||||
foreach (var descriptor in descriptors)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(descriptor.HashedContentId))
|
||||
continue;
|
||||
|
||||
if (descriptor.IsLocalPlayer)
|
||||
localCid = descriptor.HashedContentId;
|
||||
|
||||
set.Add(descriptor.HashedContentId);
|
||||
}
|
||||
|
||||
return set;
|
||||
}
|
||||
|
||||
private async Task ExpiredBroadcastCleanupLoop()
|
||||
{
|
||||
var token = _cleanupCts.Token;
|
||||
|
||||
@@ -67,7 +67,7 @@ public class LightFinderService : IHostedService, IMediatorSubscriber
|
||||
{
|
||||
try
|
||||
{
|
||||
var cid = await _dalamudUtil.GetCIDAsync().ConfigureAwait(false);
|
||||
var cid = _dalamudUtil.GetCID();
|
||||
return cid.ToString().GetHash256();
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -73,7 +73,7 @@ public record HubClosedMessage(Exception? Exception) : SameThreadMessage;
|
||||
public record ResumeScanMessage(string Source) : MessageBase;
|
||||
public record FileCacheInitializedMessage : 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 UiToggleMessage(Type UiType) : 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 TargetPairMessage(Pair Pair) : MessageBase;
|
||||
public record PairFocusCharacterMessage(Pair Pair) : SameThreadMessage;
|
||||
public record PairOnlineMessage(PairUniqueIdentifier PairIdent) : MessageBase;
|
||||
public record CombatStartMessage : MessageBase;
|
||||
public record CombatEndMessage : MessageBase;
|
||||
public record PerformanceStartMessage : MessageBase;
|
||||
@@ -135,5 +136,7 @@ public record ChatChannelsUpdated : MessageBase;
|
||||
public record ChatChannelMessageAdded(string ChannelKey, ChatMessageEntry Message) : MessageBase;
|
||||
public record GroupCollectionChangedMessage : 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 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,10 +2,8 @@ using Dalamud.Game.ClientState.Objects.Enums;
|
||||
using Dalamud.Game.ClientState.Objects.SubKinds;
|
||||
using Dalamud.Game.NativeWrapper;
|
||||
using Dalamud.Game.Text.SeStringHandling;
|
||||
using Dalamud.Hooking;
|
||||
using Dalamud.Plugin.Services;
|
||||
using Dalamud.Utility;
|
||||
using Dalamud.Utility.Signatures;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
@@ -24,27 +22,22 @@ namespace LightlessSync.Services;
|
||||
/// </summary>
|
||||
public unsafe class NameplateService : DisposableMediatorSubscriberBase
|
||||
{
|
||||
private delegate nint UpdateNameplateDelegate(RaptureAtkModule* raptureAtkModule, RaptureAtkModule.NamePlateInfo* namePlateInfo, NumberArrayData* numArray, StringArrayData* stringArray, BattleChara* battleChara, int numArrayIndex, int stringArrayIndex);
|
||||
|
||||
// Glyceri, Thanks :bow:
|
||||
[Signature("40 53 55 57 41 56 48 81 EC ?? ?? ?? ?? 48 8B 84 24", DetourName = nameof(UpdateNameplateDetour))]
|
||||
private readonly Hook<UpdateNameplateDelegate>? _nameplateHook = null;
|
||||
|
||||
private readonly ILogger<NameplateService> _logger;
|
||||
private readonly LightlessConfigService _configService;
|
||||
private readonly IClientState _clientState;
|
||||
private readonly IGameGui _gameGui;
|
||||
private readonly IObjectTable _objectTable;
|
||||
private readonly PairUiService _pairUiService;
|
||||
private readonly NameplateUpdateHookService _nameplateUpdateHookService;
|
||||
|
||||
public NameplateService(ILogger<NameplateService> logger,
|
||||
LightlessConfigService configService,
|
||||
IClientState clientState,
|
||||
IGameGui gameGui,
|
||||
IObjectTable objectTable,
|
||||
IGameInteropProvider interop,
|
||||
LightlessMediator lightlessMediator,
|
||||
PairUiService pairUiService) : base(logger, lightlessMediator)
|
||||
PairUiService pairUiService,
|
||||
NameplateUpdateHookService nameplateUpdateHookService) : base(logger, lightlessMediator)
|
||||
{
|
||||
_logger = logger;
|
||||
_configService = configService;
|
||||
@@ -52,21 +45,18 @@ public unsafe class NameplateService : DisposableMediatorSubscriberBase
|
||||
_gameGui = gameGui;
|
||||
_objectTable = objectTable;
|
||||
_pairUiService = pairUiService;
|
||||
_nameplateUpdateHookService = nameplateUpdateHookService;
|
||||
|
||||
interop.InitializeFromAttributes(this);
|
||||
_nameplateHook?.Enable();
|
||||
_nameplateUpdateHookService.NameplateUpdated += OnNameplateUpdated;
|
||||
Refresh();
|
||||
|
||||
Mediator.Subscribe<VisibilityChange>(this, (_) => Refresh());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Detour for the game's internal nameplate update function.
|
||||
/// This will be called whenever the client updates any nameplate.
|
||||
///
|
||||
/// We hook into it to apply our own nameplate coloring logic via <see cref="SetNameplate"/>,
|
||||
/// Nameplate update handler, triggered by the signature hook service.
|
||||
/// </summary>
|
||||
private nint UpdateNameplateDetour(RaptureAtkModule* raptureAtkModule, RaptureAtkModule.NamePlateInfo* namePlateInfo, NumberArrayData* numArray, StringArrayData* stringArray, BattleChara* battleChara, int numArrayIndex, int stringArrayIndex)
|
||||
private void OnNameplateUpdated(RaptureAtkModule* raptureAtkModule, RaptureAtkModule.NamePlateInfo* namePlateInfo, NumberArrayData* numArray, StringArrayData* stringArray, BattleChara* battleChara, int numArrayIndex, int stringArrayIndex)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -74,10 +64,8 @@ public unsafe class NameplateService : DisposableMediatorSubscriberBase
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "Error in NameplateService UpdateNameplateDetour");
|
||||
_logger.LogError(e, "Error in NameplateService OnNameplateUpdated");
|
||||
}
|
||||
|
||||
return _nameplateHook!.Original(raptureAtkModule, namePlateInfo, numArray, stringArray, battleChara, numArrayIndex, stringArrayIndex);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -246,7 +234,7 @@ public unsafe class NameplateService : DisposableMediatorSubscriberBase
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
_nameplateHook?.Dispose();
|
||||
_nameplateUpdateHookService.NameplateUpdated -= OnNameplateUpdated;
|
||||
}
|
||||
|
||||
base.Dispose(disposing);
|
||||
|
||||
57
LightlessSync/Services/NameplateUpdateHookService.cs
Normal file
57
LightlessSync/Services/NameplateUpdateHookService.cs
Normal file
@@ -0,0 +1,57 @@
|
||||
using Dalamud.Hooking;
|
||||
using Dalamud.Plugin.Services;
|
||||
using Dalamud.Utility.Signatures;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace LightlessSync.Services;
|
||||
|
||||
public unsafe sealed class NameplateUpdateHookService : IDisposable
|
||||
{
|
||||
private delegate nint UpdateNameplateDelegate(RaptureAtkModule* raptureAtkModule, RaptureAtkModule.NamePlateInfo* namePlateInfo,
|
||||
NumberArrayData* numArray, StringArrayData* stringArray, BattleChara* battleChara, int numArrayIndex, int stringArrayIndex);
|
||||
public delegate void NameplateUpdatedHandler(RaptureAtkModule* raptureAtkModule, RaptureAtkModule.NamePlateInfo* namePlateInfo,
|
||||
NumberArrayData* numArray, StringArrayData* stringArray, BattleChara* battleChara, int numArrayIndex, int stringArrayIndex);
|
||||
|
||||
// Glyceri, Thanks :bow:
|
||||
[Signature("40 53 55 57 41 56 48 81 EC ?? ?? ?? ?? 48 8B 84 24", DetourName = nameof(UpdateNameplateDetour))]
|
||||
private readonly Hook<UpdateNameplateDelegate>? _nameplateHook = null;
|
||||
|
||||
private readonly ILogger<NameplateUpdateHookService> _logger;
|
||||
|
||||
public NameplateUpdateHookService(ILogger<NameplateUpdateHookService> logger, IGameInteropProvider interop)
|
||||
{
|
||||
_logger = logger;
|
||||
|
||||
interop.InitializeFromAttributes(this);
|
||||
_nameplateHook?.Enable();
|
||||
}
|
||||
|
||||
public event NameplateUpdatedHandler? NameplateUpdated;
|
||||
|
||||
/// <summary>
|
||||
/// Detour for the game's internal nameplate update function.
|
||||
/// This will be called whenever the client updates any nameplate.
|
||||
/// </summary>
|
||||
private nint UpdateNameplateDetour(RaptureAtkModule* raptureAtkModule, RaptureAtkModule.NamePlateInfo* namePlateInfo,
|
||||
NumberArrayData* numArray, StringArrayData* stringArray, BattleChara* battleChara, int numArrayIndex, int stringArrayIndex)
|
||||
{
|
||||
try
|
||||
{
|
||||
NameplateUpdated?.Invoke(raptureAtkModule, namePlateInfo, numArray, stringArray, battleChara, numArrayIndex, stringArrayIndex);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "Error in NameplateUpdateHookService UpdateNameplateDetour");
|
||||
}
|
||||
|
||||
return _nameplateHook!.Original(raptureAtkModule, namePlateInfo, numArray, stringArray, battleChara, numArrayIndex, stringArrayIndex);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_nameplateHook?.Dispose();
|
||||
}
|
||||
}
|
||||
71
LightlessSync/Services/PenumbraTempCollectionJanitor.cs
Normal file
71
LightlessSync/Services/PenumbraTempCollectionJanitor.cs
Normal file
@@ -0,0 +1,71 @@
|
||||
using LightlessSync.Interop.Ipc;
|
||||
using LightlessSync.LightlessConfiguration;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace LightlessSync.Services;
|
||||
|
||||
public sealed class PenumbraTempCollectionJanitor : DisposableMediatorSubscriberBase
|
||||
{
|
||||
private readonly IpcManager _ipc;
|
||||
private readonly LightlessConfigService _config;
|
||||
private int _ran;
|
||||
|
||||
public PenumbraTempCollectionJanitor(
|
||||
ILogger<PenumbraTempCollectionJanitor> logger,
|
||||
LightlessMediator mediator,
|
||||
IpcManager ipc,
|
||||
LightlessConfigService config) : base(logger, mediator)
|
||||
{
|
||||
_ipc = ipc;
|
||||
_config = config;
|
||||
|
||||
Mediator.Subscribe<PenumbraInitializedMessage>(this, _ => CleanupOrphansOnBoot());
|
||||
}
|
||||
|
||||
public void Register(Guid id)
|
||||
{
|
||||
if (id == Guid.Empty) return;
|
||||
if (_config.Current.OrphanableTempCollections.Add(id))
|
||||
_config.Save();
|
||||
}
|
||||
|
||||
public void Unregister(Guid id)
|
||||
{
|
||||
if (id == Guid.Empty) return;
|
||||
if (_config.Current.OrphanableTempCollections.Remove(id))
|
||||
_config.Save();
|
||||
}
|
||||
|
||||
private void CleanupOrphansOnBoot()
|
||||
{
|
||||
if (Interlocked.Exchange(ref _ran, 1) == 1)
|
||||
return;
|
||||
|
||||
if (!_ipc.Penumbra.APIAvailable)
|
||||
return;
|
||||
|
||||
var ids = _config.Current.OrphanableTempCollections.ToArray();
|
||||
if (ids.Length == 0)
|
||||
return;
|
||||
|
||||
var appId = Guid.NewGuid();
|
||||
Logger.LogInformation("Cleaning up {count} orphaned Lightless temp collections found in configuration", ids.Length);
|
||||
|
||||
foreach (var id in ids)
|
||||
{
|
||||
try
|
||||
{
|
||||
_ipc.Penumbra.RemoveTemporaryCollectionAsync(Logger, appId, id)
|
||||
.GetAwaiter().GetResult();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogDebug(ex, "Failed removing orphaned temp collection {id}", id);
|
||||
}
|
||||
}
|
||||
|
||||
_config.Current.OrphanableTempCollections.Clear();
|
||||
_config.Save();
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ using LightlessSync.LightlessConfiguration;
|
||||
using LightlessSync.PlayerData.Pairs;
|
||||
using LightlessSync.Services.Events;
|
||||
using LightlessSync.Services.Mediator;
|
||||
using LightlessSync.Services.ModelDecimation;
|
||||
using LightlessSync.Services.TextureCompression;
|
||||
using LightlessSync.UI;
|
||||
using LightlessSync.WebAPI.Files.Models;
|
||||
@@ -18,12 +19,14 @@ public class PlayerPerformanceService
|
||||
private readonly ILogger<PlayerPerformanceService> _logger;
|
||||
private readonly LightlessMediator _mediator;
|
||||
private readonly PlayerPerformanceConfigService _playerPerformanceConfigService;
|
||||
private readonly ModelDecimationService _modelDecimationService;
|
||||
private readonly TextureDownscaleService _textureDownscaleService;
|
||||
private readonly Dictionary<string, bool> _warnedForPlayers = new(StringComparer.Ordinal);
|
||||
|
||||
public PlayerPerformanceService(ILogger<PlayerPerformanceService> logger, LightlessMediator mediator,
|
||||
PlayerPerformanceConfigService playerPerformanceConfigService, FileCacheManager fileCacheManager,
|
||||
XivDataAnalyzer xivDataAnalyzer, TextureDownscaleService textureDownscaleService)
|
||||
XivDataAnalyzer xivDataAnalyzer, TextureDownscaleService textureDownscaleService,
|
||||
ModelDecimationService modelDecimationService)
|
||||
{
|
||||
_logger = logger;
|
||||
_mediator = mediator;
|
||||
@@ -31,6 +34,7 @@ public class PlayerPerformanceService
|
||||
_fileCacheManager = fileCacheManager;
|
||||
_xivDataAnalyzer = xivDataAnalyzer;
|
||||
_textureDownscaleService = textureDownscaleService;
|
||||
_modelDecimationService = modelDecimationService;
|
||||
}
|
||||
|
||||
public async Task<bool> CheckBothThresholds(IPairPerformanceSubject pairHandler, CharacterData charaData)
|
||||
@@ -111,10 +115,12 @@ public class PlayerPerformanceService
|
||||
var config = _playerPerformanceConfigService.Current;
|
||||
|
||||
long triUsage = 0;
|
||||
long effectiveTriUsage = 0;
|
||||
|
||||
if (!charaData.FileReplacements.TryGetValue(API.Data.Enum.ObjectKind.Player, out List<FileReplacementData>? playerReplacements))
|
||||
{
|
||||
pairHandler.LastAppliedDataTris = 0;
|
||||
pairHandler.LastAppliedApproximateEffectiveTris = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -123,14 +129,40 @@ public class PlayerPerformanceService
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
|
||||
var skipDecimation = config.SkipModelDecimationForPreferredPairs && pairHandler.IsDirectlyPaired && pairHandler.HasStickyPermissions;
|
||||
|
||||
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.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
|
||||
if (config.UIDsToIgnore
|
||||
@@ -167,7 +199,9 @@ public class PlayerPerformanceService
|
||||
public bool ComputeAndAutoPauseOnVRAMUsageThresholds(IPairPerformanceSubject pairHandler, CharacterData charaData, List<DownloadFileTransfer> toDownloadFiles)
|
||||
{
|
||||
var config = _playerPerformanceConfigService.Current;
|
||||
bool skipDownscale = pairHandler.IsDirectlyPaired && pairHandler.HasStickyPermissions;
|
||||
bool skipDownscale = config.SkipTextureDownscaleForPreferredPairs
|
||||
&& pairHandler.IsDirectlyPaired
|
||||
&& pairHandler.HasStickyPermissions;
|
||||
|
||||
long vramUsage = 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) =>
|
||||
thresholdEnabled && threshold > 0 && threshold < value && ((checkForPrefPerm && isPrefPerm) || !isPrefPerm);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,9 +101,9 @@ public class ServerConfigurationManager
|
||||
}
|
||||
hasMulti = false;
|
||||
|
||||
var charaName = _dalamudUtil.GetPlayerNameAsync().GetAwaiter().GetResult();
|
||||
var worldId = _dalamudUtil.GetHomeWorldIdAsync().GetAwaiter().GetResult();
|
||||
var cid = _dalamudUtil.GetCIDAsync().GetAwaiter().GetResult();
|
||||
var charaName = _dalamudUtil.GetPlayerName();
|
||||
var worldId = _dalamudUtil.GetHomeWorldId();
|
||||
var cid = _dalamudUtil.GetCID();
|
||||
|
||||
var auth = currentServer.Authentications.FindAll(f => string.Equals(f.CharacterName, charaName) && f.WorldId == worldId);
|
||||
if (auth.Count >= 2)
|
||||
@@ -148,9 +148,9 @@ public class ServerConfigurationManager
|
||||
}
|
||||
hasMulti = false;
|
||||
|
||||
var charaName = _dalamudUtil.GetPlayerNameAsync().GetAwaiter().GetResult();
|
||||
var worldId = _dalamudUtil.GetHomeWorldIdAsync().GetAwaiter().GetResult();
|
||||
var cid = _dalamudUtil.GetCIDAsync().GetAwaiter().GetResult();
|
||||
var charaName = _dalamudUtil.GetPlayerName();
|
||||
var worldId = _dalamudUtil.GetHomeWorldId();
|
||||
var cid = _dalamudUtil.GetCID();
|
||||
if (!currentServer.Authentications.Any() && currentServer.SecretKeys.Any())
|
||||
{
|
||||
currentServer.Authentications.Add(new Authentication()
|
||||
@@ -268,16 +268,16 @@ public class ServerConfigurationManager
|
||||
{
|
||||
if (serverSelectionIndex == -1) serverSelectionIndex = CurrentServerIndex;
|
||||
var server = GetServerByIndex(serverSelectionIndex);
|
||||
if (server.Authentications.Exists(c => string.Equals(c.CharacterName, _dalamudUtil.GetPlayerNameAsync().GetAwaiter().GetResult(), StringComparison.Ordinal)
|
||||
&& c.WorldId == _dalamudUtil.GetHomeWorldIdAsync().GetAwaiter().GetResult()))
|
||||
if (server.Authentications.Exists(c => string.Equals(c.CharacterName, _dalamudUtil.GetPlayerName(), StringComparison.Ordinal)
|
||||
&& c.WorldId == _dalamudUtil.GetHomeWorldId()))
|
||||
return;
|
||||
|
||||
server.Authentications.Add(new Authentication()
|
||||
{
|
||||
CharacterName = _dalamudUtil.GetPlayerNameAsync().GetAwaiter().GetResult(),
|
||||
WorldId = _dalamudUtil.GetHomeWorldIdAsync().GetAwaiter().GetResult(),
|
||||
CharacterName = _dalamudUtil.GetPlayerName(),
|
||||
WorldId = _dalamudUtil.GetHomeWorldId(),
|
||||
SecretKeyIdx = !server.UseOAuth2 ? server.SecretKeys.Last().Key : -1,
|
||||
LastSeenCID = _dalamudUtil.GetCIDAsync().GetAwaiter().GetResult()
|
||||
LastSeenCID = _dalamudUtil.GetCID()
|
||||
});
|
||||
Save();
|
||||
}
|
||||
|
||||
@@ -77,16 +77,39 @@ public sealed class TextureDownscaleService
|
||||
}
|
||||
|
||||
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 (_activeJobs.ContainsKey(hash)) return;
|
||||
|
||||
_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);
|
||||
}, 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)
|
||||
{
|
||||
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))
|
||||
{
|
||||
_fileCacheManager.RemoveHashedFile(cacheEntry.Hash, cacheEntry.PrefixedFilePath);
|
||||
_fileCacheManager.RemoveHashedFile(cacheEntry.Hash, cacheEntry.PrefixedFilePath, removeDerivedFiles: false);
|
||||
}
|
||||
|
||||
_fileCacheManager.UpdateHashedFile(replacement, computeProperties: false);
|
||||
|
||||
@@ -126,11 +126,11 @@ public sealed class TextureMetadataHelper
|
||||
private const string TextureSegment = "/texture/";
|
||||
private const string MaterialSegment = "/material/";
|
||||
|
||||
private const uint NormalSamplerId = 0x0C5EC1F1u;
|
||||
private const uint IndexSamplerId = 0x565F8FD8u;
|
||||
private const uint SpecularSamplerId = 0x2B99E025u;
|
||||
private const uint DiffuseSamplerId = 0x115306BEu;
|
||||
private const uint MaskSamplerId = 0x8A4E82B6u;
|
||||
private const uint NormalSamplerId = ShpkFile.NormalSamplerId;
|
||||
private const uint IndexSamplerId = ShpkFile.IndexSamplerId;
|
||||
private const uint SpecularSamplerId = ShpkFile.SpecularSamplerId;
|
||||
private const uint DiffuseSamplerId = ShpkFile.DiffuseSamplerId;
|
||||
private const uint MaskSamplerId = ShpkFile.MaskSamplerId;
|
||||
|
||||
public TextureMetadataHelper(ILogger<TextureMetadataHelper> logger, IDataManager dataManager)
|
||||
{
|
||||
@@ -394,6 +394,21 @@ public sealed class TextureMetadataHelper
|
||||
if (string.IsNullOrEmpty(fileNameWithExtension) && string.IsNullOrEmpty(fileNameWithoutExtension))
|
||||
return TextureMapKind.Unknown;
|
||||
|
||||
if (normalized.Contains("/eye/eyelids_shadow.tex", StringComparison.Ordinal))
|
||||
return TextureMapKind.Normal;
|
||||
|
||||
if (normalized.Contains("/ui/map/", StringComparison.Ordinal) && !string.IsNullOrEmpty(fileNameWithoutExtension))
|
||||
{
|
||||
if (fileNameWithoutExtension.EndsWith("m_m", StringComparison.Ordinal)
|
||||
|| fileNameWithoutExtension.EndsWith("m_s", StringComparison.Ordinal))
|
||||
return TextureMapKind.Mask;
|
||||
|
||||
if (fileNameWithoutExtension.EndsWith("_m", StringComparison.Ordinal)
|
||||
|| fileNameWithoutExtension.EndsWith("_s", StringComparison.Ordinal)
|
||||
|| fileNameWithoutExtension.EndsWith("d", StringComparison.Ordinal))
|
||||
return TextureMapKind.Diffuse;
|
||||
}
|
||||
|
||||
foreach (var (kind, token) in MapTokens)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(fileNameWithExtension) &&
|
||||
@@ -563,7 +578,16 @@ public sealed class TextureMetadataHelper
|
||||
|
||||
var normalized = format.ToUpperInvariant();
|
||||
return normalized.Contains("A8", StringComparison.Ordinal)
|
||||
|| normalized.Contains("A1", StringComparison.Ordinal)
|
||||
|| normalized.Contains("A4", StringComparison.Ordinal)
|
||||
|| normalized.Contains("A16", StringComparison.Ordinal)
|
||||
|| normalized.Contains("A32", StringComparison.Ordinal)
|
||||
|| normalized.Contains("ARGB", StringComparison.Ordinal)
|
||||
|| normalized.Contains("RGBA", StringComparison.Ordinal)
|
||||
|| normalized.Contains("BGRA", StringComparison.Ordinal)
|
||||
|| normalized.Contains("DXT3", StringComparison.Ordinal)
|
||||
|| normalized.Contains("DXT5", StringComparison.Ordinal)
|
||||
|| normalized.Contains("BC2", StringComparison.Ordinal)
|
||||
|| normalized.Contains("BC3", StringComparison.Ordinal)
|
||||
|| normalized.Contains("BC7", StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
@@ -105,6 +105,7 @@ public class UiFactory
|
||||
groupData: groupData,
|
||||
isLightfinderContext: isLightfinderContext,
|
||||
lightfinderCid: lightfinderCid,
|
||||
performanceCollector: _performanceCollectorService);
|
||||
performanceCollector: _performanceCollectorService,
|
||||
_apiController);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ using LightlessSync.Interop.GameModel;
|
||||
using LightlessSync.LightlessConfiguration;
|
||||
using LightlessSync.PlayerData.Handlers;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace LightlessSync.Services;
|
||||
@@ -18,6 +19,7 @@ public sealed class XivDataAnalyzer
|
||||
private readonly FileCacheManager _fileCacheManager;
|
||||
private readonly XivDataStorageService _configService;
|
||||
private readonly List<string> _failedCalculatedTris = [];
|
||||
private readonly List<string> _failedCalculatedEffectiveTris = [];
|
||||
|
||||
public XivDataAnalyzer(ILogger<XivDataAnalyzer> logger, FileCacheManager fileCacheManager,
|
||||
XivDataStorageService configService)
|
||||
@@ -162,16 +164,41 @@ public sealed class XivDataAnalyzer
|
||||
if (path == null || !path.ResolvedFilepath.EndsWith(".mdl", StringComparison.OrdinalIgnoreCase))
|
||||
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
|
||||
{
|
||||
_logger.LogDebug("Detected Model File {path}, calculating Tris", filePath);
|
||||
var file = new MdlFile(filePath);
|
||||
if (file.LodCount <= 0)
|
||||
{
|
||||
_failedCalculatedTris.Add(hash);
|
||||
_configService.Current.TriangleDictionary[hash] = 0;
|
||||
failedList.Add(hash);
|
||||
cache[hash] = 0;
|
||||
_configService.Save();
|
||||
return 0;
|
||||
}
|
||||
@@ -195,7 +222,7 @@ public sealed class XivDataAnalyzer
|
||||
if (tris > 0)
|
||||
{
|
||||
_logger.LogDebug("TriAnalysis: {filePath} => {tris} triangles", filePath, tris);
|
||||
_configService.Current.TriangleDictionary[hash] = tris;
|
||||
cache[hash] = tris;
|
||||
_configService.Save();
|
||||
break;
|
||||
}
|
||||
@@ -205,8 +232,8 @@ public sealed class XivDataAnalyzer
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_failedCalculatedTris.Add(hash);
|
||||
_configService.Current.TriangleDictionary[hash] = 0;
|
||||
failedList.Add(hash);
|
||||
cache[hash] = 0;
|
||||
_configService.Save();
|
||||
_logger.LogWarning(e, "Could not parse file {file}", filePath);
|
||||
return 0;
|
||||
|
||||
169
LightlessSync/ThirdParty/MeshDecimator/Algorithms/DecimationAlgorithm.cs
vendored
Normal file
169
LightlessSync/ThirdParty/MeshDecimator/Algorithms/DecimationAlgorithm.cs
vendored
Normal file
@@ -0,0 +1,169 @@
|
||||
#region License
|
||||
/*
|
||||
MIT License
|
||||
|
||||
Copyright(c) 2017-2018 Mattias Edlund
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace MeshDecimator.Algorithms
|
||||
{
|
||||
/// <summary>
|
||||
/// A decimation algorithm.
|
||||
/// </summary>
|
||||
public abstract class DecimationAlgorithm
|
||||
{
|
||||
#region Delegates
|
||||
/// <summary>
|
||||
/// A callback for decimation status reports.
|
||||
/// </summary>
|
||||
/// <param name="iteration">The current iteration, starting at zero.</param>
|
||||
/// <param name="originalTris">The original count of triangles.</param>
|
||||
/// <param name="currentTris">The current count of triangles.</param>
|
||||
/// <param name="targetTris">The target count of triangles.</param>
|
||||
public delegate void StatusReportCallback(int iteration, int originalTris, int currentTris, int targetTris);
|
||||
#endregion
|
||||
|
||||
#region Fields
|
||||
private bool preserveBorders = false;
|
||||
private int maxVertexCount = 0;
|
||||
private bool verbose = false;
|
||||
|
||||
private StatusReportCallback statusReportInvoker = null;
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
/// <summary>
|
||||
/// Gets or sets if borders should be kept.
|
||||
/// Default value: false
|
||||
/// </summary>
|
||||
[Obsolete("Use the 'DecimationAlgorithm.PreserveBorders' property instead.", false)]
|
||||
public bool KeepBorders
|
||||
{
|
||||
get { return preserveBorders; }
|
||||
set { preserveBorders = value; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets if borders should be preserved.
|
||||
/// Default value: false
|
||||
/// </summary>
|
||||
public bool PreserveBorders
|
||||
{
|
||||
get { return preserveBorders; }
|
||||
set { preserveBorders = value; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets if linked vertices should be kept.
|
||||
/// Default value: false
|
||||
/// </summary>
|
||||
[Obsolete("This feature has been removed, for more details why please read the readme.", true)]
|
||||
public bool KeepLinkedVertices
|
||||
{
|
||||
get { return false; }
|
||||
set { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum vertex count. Set to zero for no limitation.
|
||||
/// Default value: 0 (no limitation)
|
||||
/// </summary>
|
||||
public int MaxVertexCount
|
||||
{
|
||||
get { return maxVertexCount; }
|
||||
set { maxVertexCount = Math.MathHelper.Max(value, 0); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets if verbose information should be printed in the console.
|
||||
/// Default value: false
|
||||
/// </summary>
|
||||
public bool Verbose
|
||||
{
|
||||
get { return verbose; }
|
||||
set { verbose = value; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the logger used for diagnostics.
|
||||
/// </summary>
|
||||
public ILogger? Logger { get; set; }
|
||||
#endregion
|
||||
|
||||
#region Events
|
||||
/// <summary>
|
||||
/// An event for status reports for this algorithm.
|
||||
/// </summary>
|
||||
public event StatusReportCallback StatusReport
|
||||
{
|
||||
add { statusReportInvoker += value; }
|
||||
remove { statusReportInvoker -= value; }
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Protected Methods
|
||||
/// <summary>
|
||||
/// Reports the current status of the decimation.
|
||||
/// </summary>
|
||||
/// <param name="iteration">The current iteration, starting at zero.</param>
|
||||
/// <param name="originalTris">The original count of triangles.</param>
|
||||
/// <param name="currentTris">The current count of triangles.</param>
|
||||
/// <param name="targetTris">The target count of triangles.</param>
|
||||
protected void ReportStatus(int iteration, int originalTris, int currentTris, int targetTris)
|
||||
{
|
||||
var statusReportInvoker = this.statusReportInvoker;
|
||||
if (statusReportInvoker != null)
|
||||
{
|
||||
statusReportInvoker.Invoke(iteration, originalTris, currentTris, targetTris);
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Public Methods
|
||||
/// <summary>
|
||||
/// Initializes the algorithm with the original mesh.
|
||||
/// </summary>
|
||||
/// <param name="mesh">The mesh.</param>
|
||||
public abstract void Initialize(Mesh mesh);
|
||||
|
||||
/// <summary>
|
||||
/// Decimates the mesh.
|
||||
/// </summary>
|
||||
/// <param name="targetTrisCount">The target triangle count.</param>
|
||||
public abstract void DecimateMesh(int targetTrisCount);
|
||||
|
||||
/// <summary>
|
||||
/// Decimates the mesh without losing any quality.
|
||||
/// </summary>
|
||||
public abstract void DecimateMeshLossless();
|
||||
|
||||
/// <summary>
|
||||
/// Returns the resulting mesh.
|
||||
/// </summary>
|
||||
/// <returns>The resulting mesh.</returns>
|
||||
public abstract Mesh ToMesh();
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
1549
LightlessSync/ThirdParty/MeshDecimator/Algorithms/FastQuadricMeshSimplification.cs
vendored
Normal file
1549
LightlessSync/ThirdParty/MeshDecimator/Algorithms/FastQuadricMeshSimplification.cs
vendored
Normal file
File diff suppressed because it is too large
Load Diff
249
LightlessSync/ThirdParty/MeshDecimator/BoneWeight.cs
vendored
Normal file
249
LightlessSync/ThirdParty/MeshDecimator/BoneWeight.cs
vendored
Normal file
@@ -0,0 +1,249 @@
|
||||
#region License
|
||||
/*
|
||||
MIT License
|
||||
|
||||
Copyright(c) 2017-2018 Mattias Edlund
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using MeshDecimator.Math;
|
||||
|
||||
namespace MeshDecimator
|
||||
{
|
||||
/// <summary>
|
||||
/// A bone weight.
|
||||
/// </summary>
|
||||
public struct BoneWeight : IEquatable<BoneWeight>
|
||||
{
|
||||
#region Fields
|
||||
/// <summary>
|
||||
/// The first bone index.
|
||||
/// </summary>
|
||||
public int boneIndex0;
|
||||
/// <summary>
|
||||
/// The second bone index.
|
||||
/// </summary>
|
||||
public int boneIndex1;
|
||||
/// <summary>
|
||||
/// The third bone index.
|
||||
/// </summary>
|
||||
public int boneIndex2;
|
||||
/// <summary>
|
||||
/// The fourth bone index.
|
||||
/// </summary>
|
||||
public int boneIndex3;
|
||||
|
||||
/// <summary>
|
||||
/// The first bone weight.
|
||||
/// </summary>
|
||||
public float boneWeight0;
|
||||
/// <summary>
|
||||
/// The second bone weight.
|
||||
/// </summary>
|
||||
public float boneWeight1;
|
||||
/// <summary>
|
||||
/// The third bone weight.
|
||||
/// </summary>
|
||||
public float boneWeight2;
|
||||
/// <summary>
|
||||
/// The fourth bone weight.
|
||||
/// </summary>
|
||||
public float boneWeight3;
|
||||
#endregion
|
||||
|
||||
#region Constructor
|
||||
/// <summary>
|
||||
/// Creates a new bone weight.
|
||||
/// </summary>
|
||||
/// <param name="boneIndex0">The first bone index.</param>
|
||||
/// <param name="boneIndex1">The second bone index.</param>
|
||||
/// <param name="boneIndex2">The third bone index.</param>
|
||||
/// <param name="boneIndex3">The fourth bone index.</param>
|
||||
/// <param name="boneWeight0">The first bone weight.</param>
|
||||
/// <param name="boneWeight1">The second bone weight.</param>
|
||||
/// <param name="boneWeight2">The third bone weight.</param>
|
||||
/// <param name="boneWeight3">The fourth bone weight.</param>
|
||||
public BoneWeight(int boneIndex0, int boneIndex1, int boneIndex2, int boneIndex3, float boneWeight0, float boneWeight1, float boneWeight2, float boneWeight3)
|
||||
{
|
||||
this.boneIndex0 = boneIndex0;
|
||||
this.boneIndex1 = boneIndex1;
|
||||
this.boneIndex2 = boneIndex2;
|
||||
this.boneIndex3 = boneIndex3;
|
||||
|
||||
this.boneWeight0 = boneWeight0;
|
||||
this.boneWeight1 = boneWeight1;
|
||||
this.boneWeight2 = boneWeight2;
|
||||
this.boneWeight3 = boneWeight3;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Operators
|
||||
/// <summary>
|
||||
/// Returns if two bone weights equals eachother.
|
||||
/// </summary>
|
||||
/// <param name="lhs">The left hand side bone weight.</param>
|
||||
/// <param name="rhs">The right hand side bone weight.</param>
|
||||
/// <returns>If equals.</returns>
|
||||
public static bool operator ==(BoneWeight lhs, BoneWeight rhs)
|
||||
{
|
||||
return (lhs.boneIndex0 == rhs.boneIndex0 && lhs.boneIndex1 == rhs.boneIndex1 && lhs.boneIndex2 == rhs.boneIndex2 && lhs.boneIndex3 == rhs.boneIndex3 &&
|
||||
new Vector4(lhs.boneWeight0, lhs.boneWeight1, lhs.boneWeight2, lhs.boneWeight3) == new Vector4(rhs.boneWeight0, rhs.boneWeight1, rhs.boneWeight2, rhs.boneWeight3));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if two bone weights don't equal eachother.
|
||||
/// </summary>
|
||||
/// <param name="lhs">The left hand side bone weight.</param>
|
||||
/// <param name="rhs">The right hand side bone weight.</param>
|
||||
/// <returns>If not equals.</returns>
|
||||
public static bool operator !=(BoneWeight lhs, BoneWeight rhs)
|
||||
{
|
||||
return !(lhs == rhs);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Private Methods
|
||||
private void MergeBoneWeight(int boneIndex, float weight)
|
||||
{
|
||||
if (boneIndex == boneIndex0)
|
||||
{
|
||||
boneWeight0 = (boneWeight0 + weight) * 0.5f;
|
||||
}
|
||||
else if (boneIndex == boneIndex1)
|
||||
{
|
||||
boneWeight1 = (boneWeight1 + weight) * 0.5f;
|
||||
}
|
||||
else if (boneIndex == boneIndex2)
|
||||
{
|
||||
boneWeight2 = (boneWeight2 + weight) * 0.5f;
|
||||
}
|
||||
else if (boneIndex == boneIndex3)
|
||||
{
|
||||
boneWeight3 = (boneWeight3 + weight) * 0.5f;
|
||||
}
|
||||
else if(boneWeight0 == 0f)
|
||||
{
|
||||
boneIndex0 = boneIndex;
|
||||
boneWeight0 = weight;
|
||||
}
|
||||
else if (boneWeight1 == 0f)
|
||||
{
|
||||
boneIndex1 = boneIndex;
|
||||
boneWeight1 = weight;
|
||||
}
|
||||
else if (boneWeight2 == 0f)
|
||||
{
|
||||
boneIndex2 = boneIndex;
|
||||
boneWeight2 = weight;
|
||||
}
|
||||
else if (boneWeight3 == 0f)
|
||||
{
|
||||
boneIndex3 = boneIndex;
|
||||
boneWeight3 = weight;
|
||||
}
|
||||
Normalize();
|
||||
}
|
||||
|
||||
private void Normalize()
|
||||
{
|
||||
float mag = (float)System.Math.Sqrt(boneWeight0 * boneWeight0 + boneWeight1 * boneWeight1 + boneWeight2 * boneWeight2 + boneWeight3 * boneWeight3);
|
||||
if (mag > float.Epsilon)
|
||||
{
|
||||
boneWeight0 /= mag;
|
||||
boneWeight1 /= mag;
|
||||
boneWeight2 /= mag;
|
||||
boneWeight3 /= mag;
|
||||
}
|
||||
else
|
||||
{
|
||||
boneWeight0 = boneWeight1 = boneWeight2 = boneWeight3 = 0f;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Public Methods
|
||||
#region Object
|
||||
/// <summary>
|
||||
/// Returns a hash code for this vector.
|
||||
/// </summary>
|
||||
/// <returns>The hash code.</returns>
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return boneIndex0.GetHashCode() ^ boneIndex1.GetHashCode() << 2 ^ boneIndex2.GetHashCode() >> 2 ^ boneIndex3.GetHashCode() >>
|
||||
1 ^ boneWeight0.GetHashCode() << 5 ^ boneWeight1.GetHashCode() << 4 ^ boneWeight2.GetHashCode() >> 4 ^ boneWeight3.GetHashCode() >> 3;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if this bone weight is equal to another object.
|
||||
/// </summary>
|
||||
/// <param name="obj">The other object to compare to.</param>
|
||||
/// <returns>If equals.</returns>
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
if (!(obj is BoneWeight))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
BoneWeight other = (BoneWeight)obj;
|
||||
return (boneIndex0 == other.boneIndex0 && boneIndex1 == other.boneIndex1 && boneIndex2 == other.boneIndex2 && boneIndex3 == other.boneIndex3 &&
|
||||
boneWeight0 == other.boneWeight0 && boneWeight1 == other.boneWeight1 && boneWeight2 == other.boneWeight2 && boneWeight3 == other.boneWeight3);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if this bone weight is equal to another one.
|
||||
/// </summary>
|
||||
/// <param name="other">The other bone weight to compare to.</param>
|
||||
/// <returns>If equals.</returns>
|
||||
public bool Equals(BoneWeight other)
|
||||
{
|
||||
return (boneIndex0 == other.boneIndex0 && boneIndex1 == other.boneIndex1 && boneIndex2 == other.boneIndex2 && boneIndex3 == other.boneIndex3 &&
|
||||
boneWeight0 == other.boneWeight0 && boneWeight1 == other.boneWeight1 && boneWeight2 == other.boneWeight2 && boneWeight3 == other.boneWeight3);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a nicely formatted string for this bone weight.
|
||||
/// </summary>
|
||||
/// <returns>The string.</returns>
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format("({0}:{4:F1}, {1}:{5:F1}, {2}:{6:F1}, {3}:{7:F1})",
|
||||
boneIndex0, boneIndex1, boneIndex2, boneIndex3, boneWeight0, boneWeight1, boneWeight2, boneWeight3);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Static
|
||||
/// <summary>
|
||||
/// Merges two bone weights and stores the merged result in the first parameter.
|
||||
/// </summary>
|
||||
/// <param name="a">The first bone weight, also stores result.</param>
|
||||
/// <param name="b">The second bone weight.</param>
|
||||
public static void Merge(ref BoneWeight a, ref BoneWeight b)
|
||||
{
|
||||
if (b.boneWeight0 > 0f) a.MergeBoneWeight(b.boneIndex0, b.boneWeight0);
|
||||
if (b.boneWeight1 > 0f) a.MergeBoneWeight(b.boneIndex1, b.boneWeight1);
|
||||
if (b.boneWeight2 > 0f) a.MergeBoneWeight(b.boneIndex2, b.boneWeight2);
|
||||
if (b.boneWeight3 > 0f) a.MergeBoneWeight(b.boneIndex3, b.boneWeight3);
|
||||
}
|
||||
#endregion
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
179
LightlessSync/ThirdParty/MeshDecimator/Collections/ResizableArray.cs
vendored
Normal file
179
LightlessSync/ThirdParty/MeshDecimator/Collections/ResizableArray.cs
vendored
Normal file
@@ -0,0 +1,179 @@
|
||||
#region License
|
||||
/*
|
||||
MIT License
|
||||
|
||||
Copyright(c) 2017-2018 Mattias Edlund
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
|
||||
namespace MeshDecimator.Collections
|
||||
{
|
||||
/// <summary>
|
||||
/// A resizable array.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The item type.</typeparam>
|
||||
internal sealed class ResizableArray<T>
|
||||
{
|
||||
#region Fields
|
||||
private T[] items = null;
|
||||
private int length = 0;
|
||||
|
||||
private static T[] emptyArr = new T[0];
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
/// <summary>
|
||||
/// Gets the length of this array.
|
||||
/// </summary>
|
||||
public int Length
|
||||
{
|
||||
get { return length; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the internal data buffer for this array.
|
||||
/// </summary>
|
||||
public T[] Data
|
||||
{
|
||||
get { return items; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the element value at a specific index.
|
||||
/// </summary>
|
||||
/// <param name="index">The element index.</param>
|
||||
/// <returns>The element value.</returns>
|
||||
public T this[int index]
|
||||
{
|
||||
get { return items[index]; }
|
||||
set { items[index] = value; }
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Constructor
|
||||
/// <summary>
|
||||
/// Creates a new resizable array.
|
||||
/// </summary>
|
||||
/// <param name="capacity">The initial array capacity.</param>
|
||||
public ResizableArray(int capacity)
|
||||
: this(capacity, 0)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new resizable array.
|
||||
/// </summary>
|
||||
/// <param name="capacity">The initial array capacity.</param>
|
||||
/// <param name="length">The initial length of the array.</param>
|
||||
public ResizableArray(int capacity, int length)
|
||||
{
|
||||
if (capacity < 0)
|
||||
throw new ArgumentOutOfRangeException("capacity");
|
||||
else if (length < 0 || length > capacity)
|
||||
throw new ArgumentOutOfRangeException("length");
|
||||
|
||||
if (capacity > 0)
|
||||
items = new T[capacity];
|
||||
else
|
||||
items = emptyArr;
|
||||
|
||||
this.length = length;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Private Methods
|
||||
private void IncreaseCapacity(int capacity)
|
||||
{
|
||||
T[] newItems = new T[capacity];
|
||||
Array.Copy(items, 0, newItems, 0, System.Math.Min(length, capacity));
|
||||
items = newItems;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Public Methods
|
||||
/// <summary>
|
||||
/// Clears this array.
|
||||
/// </summary>
|
||||
public void Clear()
|
||||
{
|
||||
Array.Clear(items, 0, length);
|
||||
length = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resizes this array.
|
||||
/// </summary>
|
||||
/// <param name="length">The new length.</param>
|
||||
/// <param name="trimExess">If exess memory should be trimmed.</param>
|
||||
public void Resize(int length, bool trimExess = false)
|
||||
{
|
||||
if (length < 0)
|
||||
throw new ArgumentOutOfRangeException("capacity");
|
||||
|
||||
if (length > items.Length)
|
||||
{
|
||||
IncreaseCapacity(length);
|
||||
}
|
||||
else if (length < this.length)
|
||||
{
|
||||
//Array.Clear(items, capacity, length - capacity);
|
||||
}
|
||||
|
||||
this.length = length;
|
||||
|
||||
if (trimExess)
|
||||
{
|
||||
TrimExcess();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Trims any excess memory for this array.
|
||||
/// </summary>
|
||||
public void TrimExcess()
|
||||
{
|
||||
if (items.Length == length) // Nothing to do
|
||||
return;
|
||||
|
||||
T[] newItems = new T[length];
|
||||
Array.Copy(items, 0, newItems, 0, length);
|
||||
items = newItems;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a new item to the end of this array.
|
||||
/// </summary>
|
||||
/// <param name="item">The new item.</param>
|
||||
public void Add(T item)
|
||||
{
|
||||
if (length >= items.Length)
|
||||
{
|
||||
IncreaseCapacity(items.Length << 1);
|
||||
}
|
||||
|
||||
items[length++] = item;
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
79
LightlessSync/ThirdParty/MeshDecimator/Collections/UVChannels.cs
vendored
Normal file
79
LightlessSync/ThirdParty/MeshDecimator/Collections/UVChannels.cs
vendored
Normal file
@@ -0,0 +1,79 @@
|
||||
using System;
|
||||
|
||||
namespace MeshDecimator.Collections
|
||||
{
|
||||
/// <summary>
|
||||
/// A collection of UV channels.
|
||||
/// </summary>
|
||||
/// <typeparam name="TVec">The UV vector type.</typeparam>
|
||||
internal sealed class UVChannels<TVec>
|
||||
{
|
||||
#region Fields
|
||||
private ResizableArray<TVec>[] channels = null;
|
||||
private TVec[][] channelsData = null;
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
/// <summary>
|
||||
/// Gets the channel collection data.
|
||||
/// </summary>
|
||||
public TVec[][] Data
|
||||
{
|
||||
get
|
||||
{
|
||||
for (int i = 0; i < Mesh.UVChannelCount; i++)
|
||||
{
|
||||
if (channels[i] != null)
|
||||
{
|
||||
channelsData[i] = channels[i].Data;
|
||||
}
|
||||
else
|
||||
{
|
||||
channelsData[i] = null;
|
||||
}
|
||||
}
|
||||
return channelsData;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a specific channel by index.
|
||||
/// </summary>
|
||||
/// <param name="index">The channel index.</param>
|
||||
public ResizableArray<TVec> this[int index]
|
||||
{
|
||||
get { return channels[index]; }
|
||||
set { channels[index] = value; }
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Constructor
|
||||
/// <summary>
|
||||
/// Creates a new collection of UV channels.
|
||||
/// </summary>
|
||||
public UVChannels()
|
||||
{
|
||||
channels = new ResizableArray<TVec>[Mesh.UVChannelCount];
|
||||
channelsData = new TVec[Mesh.UVChannelCount][];
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Public Methods
|
||||
/// <summary>
|
||||
/// Resizes all channels at once.
|
||||
/// </summary>
|
||||
/// <param name="capacity">The new capacity.</param>
|
||||
/// <param name="trimExess">If exess memory should be trimmed.</param>
|
||||
public void Resize(int capacity, bool trimExess = false)
|
||||
{
|
||||
for (int i = 0; i < Mesh.UVChannelCount; i++)
|
||||
{
|
||||
if (channels[i] != null)
|
||||
{
|
||||
channels[i].Resize(capacity, trimExess);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
21
LightlessSync/ThirdParty/MeshDecimator/LICENSE.md
vendored
Normal file
21
LightlessSync/ThirdParty/MeshDecimator/LICENSE.md
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2017-2018 Mattias Edlund
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
286
LightlessSync/ThirdParty/MeshDecimator/Math/MathHelper.cs
vendored
Normal file
286
LightlessSync/ThirdParty/MeshDecimator/Math/MathHelper.cs
vendored
Normal file
@@ -0,0 +1,286 @@
|
||||
#region License
|
||||
/*
|
||||
MIT License
|
||||
|
||||
Copyright(c) 2017-2018 Mattias Edlund
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
|
||||
namespace MeshDecimator.Math
|
||||
{
|
||||
/// <summary>
|
||||
/// Math helpers.
|
||||
/// </summary>
|
||||
public static class MathHelper
|
||||
{
|
||||
#region Consts
|
||||
/// <summary>
|
||||
/// The Pi constant.
|
||||
/// </summary>
|
||||
public const float PI = 3.14159274f;
|
||||
|
||||
/// <summary>
|
||||
/// The Pi constant.
|
||||
/// </summary>
|
||||
public const double PId = 3.1415926535897932384626433832795;
|
||||
|
||||
/// <summary>
|
||||
/// Degrees to radian constant.
|
||||
/// </summary>
|
||||
public const float Deg2Rad = PI / 180f;
|
||||
|
||||
/// <summary>
|
||||
/// Degrees to radian constant.
|
||||
/// </summary>
|
||||
public const double Deg2Radd = PId / 180.0;
|
||||
|
||||
/// <summary>
|
||||
/// Radians to degrees constant.
|
||||
/// </summary>
|
||||
public const float Rad2Deg = 180f / PI;
|
||||
|
||||
/// <summary>
|
||||
/// Radians to degrees constant.
|
||||
/// </summary>
|
||||
public const double Rad2Degd = 180.0 / PId;
|
||||
#endregion
|
||||
|
||||
#region Min
|
||||
/// <summary>
|
||||
/// Returns the minimum of two values.
|
||||
/// </summary>
|
||||
/// <param name="val1">The first value.</param>
|
||||
/// <param name="val2">The second value.</param>
|
||||
/// <returns>The minimum value.</returns>
|
||||
public static int Min(int val1, int val2)
|
||||
{
|
||||
return (val1 < val2 ? val1 : val2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the minimum of three values.
|
||||
/// </summary>
|
||||
/// <param name="val1">The first value.</param>
|
||||
/// <param name="val2">The second value.</param>
|
||||
/// <param name="val3">The third value.</param>
|
||||
/// <returns>The minimum value.</returns>
|
||||
public static int Min(int val1, int val2, int val3)
|
||||
{
|
||||
return (val1 < val2 ? (val1 < val3 ? val1 : val3) : (val2 < val3 ? val2 : val3));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the minimum of two values.
|
||||
/// </summary>
|
||||
/// <param name="val1">The first value.</param>
|
||||
/// <param name="val2">The second value.</param>
|
||||
/// <returns>The minimum value.</returns>
|
||||
public static float Min(float val1, float val2)
|
||||
{
|
||||
return (val1 < val2 ? val1 : val2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the minimum of three values.
|
||||
/// </summary>
|
||||
/// <param name="val1">The first value.</param>
|
||||
/// <param name="val2">The second value.</param>
|
||||
/// <param name="val3">The third value.</param>
|
||||
/// <returns>The minimum value.</returns>
|
||||
public static float Min(float val1, float val2, float val3)
|
||||
{
|
||||
return (val1 < val2 ? (val1 < val3 ? val1 : val3) : (val2 < val3 ? val2 : val3));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the minimum of two values.
|
||||
/// </summary>
|
||||
/// <param name="val1">The first value.</param>
|
||||
/// <param name="val2">The second value.</param>
|
||||
/// <returns>The minimum value.</returns>
|
||||
public static double Min(double val1, double val2)
|
||||
{
|
||||
return (val1 < val2 ? val1 : val2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the minimum of three values.
|
||||
/// </summary>
|
||||
/// <param name="val1">The first value.</param>
|
||||
/// <param name="val2">The second value.</param>
|
||||
/// <param name="val3">The third value.</param>
|
||||
/// <returns>The minimum value.</returns>
|
||||
public static double Min(double val1, double val2, double val3)
|
||||
{
|
||||
return (val1 < val2 ? (val1 < val3 ? val1 : val3) : (val2 < val3 ? val2 : val3));
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Max
|
||||
/// <summary>
|
||||
/// Returns the maximum of two values.
|
||||
/// </summary>
|
||||
/// <param name="val1">The first value.</param>
|
||||
/// <param name="val2">The second value.</param>
|
||||
/// <returns>The maximum value.</returns>
|
||||
public static int Max(int val1, int val2)
|
||||
{
|
||||
return (val1 > val2 ? val1 : val2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the maximum of three values.
|
||||
/// </summary>
|
||||
/// <param name="val1">The first value.</param>
|
||||
/// <param name="val2">The second value.</param>
|
||||
/// <param name="val3">The third value.</param>
|
||||
/// <returns>The maximum value.</returns>
|
||||
public static int Max(int val1, int val2, int val3)
|
||||
{
|
||||
return (val1 > val2 ? (val1 > val3 ? val1 : val3) : (val2 > val3 ? val2 : val3));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the maximum of two values.
|
||||
/// </summary>
|
||||
/// <param name="val1">The first value.</param>
|
||||
/// <param name="val2">The second value.</param>
|
||||
/// <returns>The maximum value.</returns>
|
||||
public static float Max(float val1, float val2)
|
||||
{
|
||||
return (val1 > val2 ? val1 : val2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the maximum of three values.
|
||||
/// </summary>
|
||||
/// <param name="val1">The first value.</param>
|
||||
/// <param name="val2">The second value.</param>
|
||||
/// <param name="val3">The third value.</param>
|
||||
/// <returns>The maximum value.</returns>
|
||||
public static float Max(float val1, float val2, float val3)
|
||||
{
|
||||
return (val1 > val2 ? (val1 > val3 ? val1 : val3) : (val2 > val3 ? val2 : val3));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the maximum of two values.
|
||||
/// </summary>
|
||||
/// <param name="val1">The first value.</param>
|
||||
/// <param name="val2">The second value.</param>
|
||||
/// <returns>The maximum value.</returns>
|
||||
public static double Max(double val1, double val2)
|
||||
{
|
||||
return (val1 > val2 ? val1 : val2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the maximum of three values.
|
||||
/// </summary>
|
||||
/// <param name="val1">The first value.</param>
|
||||
/// <param name="val2">The second value.</param>
|
||||
/// <param name="val3">The third value.</param>
|
||||
/// <returns>The maximum value.</returns>
|
||||
public static double Max(double val1, double val2, double val3)
|
||||
{
|
||||
return (val1 > val2 ? (val1 > val3 ? val1 : val3) : (val2 > val3 ? val2 : val3));
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Clamping
|
||||
/// <summary>
|
||||
/// Clamps a value between a minimum and a maximum value.
|
||||
/// </summary>
|
||||
/// <param name="value">The value to clamp.</param>
|
||||
/// <param name="min">The minimum value.</param>
|
||||
/// <param name="max">The maximum value.</param>
|
||||
/// <returns>The clamped value.</returns>
|
||||
public static float Clamp(float value, float min, float max)
|
||||
{
|
||||
return (value >= min ? (value <= max ? value : max) : min);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clamps a value between a minimum and a maximum value.
|
||||
/// </summary>
|
||||
/// <param name="value">The value to clamp.</param>
|
||||
/// <param name="min">The minimum value.</param>
|
||||
/// <param name="max">The maximum value.</param>
|
||||
/// <returns>The clamped value.</returns>
|
||||
public static double Clamp(double value, double min, double max)
|
||||
{
|
||||
return (value >= min ? (value <= max ? value : max) : min);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clamps the value between 0 and 1.
|
||||
/// </summary>
|
||||
/// <param name="value">The value to clamp.</param>
|
||||
/// <returns>The clamped value.</returns>
|
||||
public static float Clamp01(float value)
|
||||
{
|
||||
return (value > 0f ? (value < 1f ? value : 1f) : 0f);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clamps the value between 0 and 1.
|
||||
/// </summary>
|
||||
/// <param name="value">The value to clamp.</param>
|
||||
/// <returns>The clamped value.</returns>
|
||||
public static double Clamp01(double value)
|
||||
{
|
||||
return (value > 0.0 ? (value < 1.0 ? value : 1.0) : 0.0);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Triangle Area
|
||||
/// <summary>
|
||||
/// Calculates the area of a triangle.
|
||||
/// </summary>
|
||||
/// <param name="p0">The first point.</param>
|
||||
/// <param name="p1">The second point.</param>
|
||||
/// <param name="p2">The third point.</param>
|
||||
/// <returns>The triangle area.</returns>
|
||||
public static float TriangleArea(ref Vector3 p0, ref Vector3 p1, ref Vector3 p2)
|
||||
{
|
||||
var dx = p1 - p0;
|
||||
var dy = p2 - p0;
|
||||
return dx.Magnitude * ((float)System.Math.Sin(Vector3.Angle(ref dx, ref dy) * Deg2Rad) * dy.Magnitude) * 0.5f;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the area of a triangle.
|
||||
/// </summary>
|
||||
/// <param name="p0">The first point.</param>
|
||||
/// <param name="p1">The second point.</param>
|
||||
/// <param name="p2">The third point.</param>
|
||||
/// <returns>The triangle area.</returns>
|
||||
public static double TriangleArea(ref Vector3d p0, ref Vector3d p1, ref Vector3d p2)
|
||||
{
|
||||
var dx = p1 - p0;
|
||||
var dy = p2 - p0;
|
||||
return dx.Magnitude * (System.Math.Sin(Vector3d.Angle(ref dx, ref dy) * Deg2Radd) * dy.Magnitude) * 0.5f;
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
303
LightlessSync/ThirdParty/MeshDecimator/Math/SymmetricMatrix.cs
vendored
Normal file
303
LightlessSync/ThirdParty/MeshDecimator/Math/SymmetricMatrix.cs
vendored
Normal file
@@ -0,0 +1,303 @@
|
||||
#region License
|
||||
/*
|
||||
MIT License
|
||||
|
||||
Copyright(c) 2017-2018 Mattias Edlund
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
|
||||
namespace MeshDecimator.Math
|
||||
{
|
||||
/// <summary>
|
||||
/// A symmetric matrix.
|
||||
/// </summary>
|
||||
public struct SymmetricMatrix
|
||||
{
|
||||
#region Fields
|
||||
/// <summary>
|
||||
/// The m11 component.
|
||||
/// </summary>
|
||||
public double m0;
|
||||
/// <summary>
|
||||
/// The m12 component.
|
||||
/// </summary>
|
||||
public double m1;
|
||||
/// <summary>
|
||||
/// The m13 component.
|
||||
/// </summary>
|
||||
public double m2;
|
||||
/// <summary>
|
||||
/// The m14 component.
|
||||
/// </summary>
|
||||
public double m3;
|
||||
/// <summary>
|
||||
/// The m22 component.
|
||||
/// </summary>
|
||||
public double m4;
|
||||
/// <summary>
|
||||
/// The m23 component.
|
||||
/// </summary>
|
||||
public double m5;
|
||||
/// <summary>
|
||||
/// The m24 component.
|
||||
/// </summary>
|
||||
public double m6;
|
||||
/// <summary>
|
||||
/// The m33 component.
|
||||
/// </summary>
|
||||
public double m7;
|
||||
/// <summary>
|
||||
/// The m34 component.
|
||||
/// </summary>
|
||||
public double m8;
|
||||
/// <summary>
|
||||
/// The m44 component.
|
||||
/// </summary>
|
||||
public double m9;
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
/// <summary>
|
||||
/// Gets the component value with a specific index.
|
||||
/// </summary>
|
||||
/// <param name="index">The component index.</param>
|
||||
/// <returns>The value.</returns>
|
||||
public double this[int index]
|
||||
{
|
||||
get
|
||||
{
|
||||
switch (index)
|
||||
{
|
||||
case 0:
|
||||
return m0;
|
||||
case 1:
|
||||
return m1;
|
||||
case 2:
|
||||
return m2;
|
||||
case 3:
|
||||
return m3;
|
||||
case 4:
|
||||
return m4;
|
||||
case 5:
|
||||
return m5;
|
||||
case 6:
|
||||
return m6;
|
||||
case 7:
|
||||
return m7;
|
||||
case 8:
|
||||
return m8;
|
||||
case 9:
|
||||
return m9;
|
||||
default:
|
||||
throw new IndexOutOfRangeException();
|
||||
}
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Constructor
|
||||
/// <summary>
|
||||
/// Creates a symmetric matrix with a value in each component.
|
||||
/// </summary>
|
||||
/// <param name="c">The component value.</param>
|
||||
public SymmetricMatrix(double c)
|
||||
{
|
||||
this.m0 = c;
|
||||
this.m1 = c;
|
||||
this.m2 = c;
|
||||
this.m3 = c;
|
||||
this.m4 = c;
|
||||
this.m5 = c;
|
||||
this.m6 = c;
|
||||
this.m7 = c;
|
||||
this.m8 = c;
|
||||
this.m9 = c;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a symmetric matrix.
|
||||
/// </summary>
|
||||
/// <param name="m0">The m11 component.</param>
|
||||
/// <param name="m1">The m12 component.</param>
|
||||
/// <param name="m2">The m13 component.</param>
|
||||
/// <param name="m3">The m14 component.</param>
|
||||
/// <param name="m4">The m22 component.</param>
|
||||
/// <param name="m5">The m23 component.</param>
|
||||
/// <param name="m6">The m24 component.</param>
|
||||
/// <param name="m7">The m33 component.</param>
|
||||
/// <param name="m8">The m34 component.</param>
|
||||
/// <param name="m9">The m44 component.</param>
|
||||
public SymmetricMatrix(double m0, double m1, double m2, double m3,
|
||||
double m4, double m5, double m6, double m7, double m8, double m9)
|
||||
{
|
||||
this.m0 = m0;
|
||||
this.m1 = m1;
|
||||
this.m2 = m2;
|
||||
this.m3 = m3;
|
||||
this.m4 = m4;
|
||||
this.m5 = m5;
|
||||
this.m6 = m6;
|
||||
this.m7 = m7;
|
||||
this.m8 = m8;
|
||||
this.m9 = m9;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a symmetric matrix from a plane.
|
||||
/// </summary>
|
||||
/// <param name="a">The plane x-component.</param>
|
||||
/// <param name="b">The plane y-component</param>
|
||||
/// <param name="c">The plane z-component</param>
|
||||
/// <param name="d">The plane w-component</param>
|
||||
public SymmetricMatrix(double a, double b, double c, double d)
|
||||
{
|
||||
this.m0 = a * a;
|
||||
this.m1 = a * b;
|
||||
this.m2 = a * c;
|
||||
this.m3 = a * d;
|
||||
|
||||
this.m4 = b * b;
|
||||
this.m5 = b * c;
|
||||
this.m6 = b * d;
|
||||
|
||||
this.m7 = c * c;
|
||||
this.m8 = c * d;
|
||||
|
||||
this.m9 = d * d;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Operators
|
||||
/// <summary>
|
||||
/// Adds two matrixes together.
|
||||
/// </summary>
|
||||
/// <param name="a">The left hand side.</param>
|
||||
/// <param name="b">The right hand side.</param>
|
||||
/// <returns>The resulting matrix.</returns>
|
||||
public static SymmetricMatrix operator +(SymmetricMatrix a, SymmetricMatrix b)
|
||||
{
|
||||
return new SymmetricMatrix(
|
||||
a.m0 + b.m0, a.m1 + b.m1, a.m2 + b.m2, a.m3 + b.m3,
|
||||
a.m4 + b.m4, a.m5 + b.m5, a.m6 + b.m6,
|
||||
a.m7 + b.m7, a.m8 + b.m8,
|
||||
a.m9 + b.m9
|
||||
);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Internal Methods
|
||||
/// <summary>
|
||||
/// Determinant(0, 1, 2, 1, 4, 5, 2, 5, 7)
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
internal double Determinant1()
|
||||
{
|
||||
double det =
|
||||
m0 * m4 * m7 +
|
||||
m2 * m1 * m5 +
|
||||
m1 * m5 * m2 -
|
||||
m2 * m4 * m2 -
|
||||
m0 * m5 * m5 -
|
||||
m1 * m1 * m7;
|
||||
return det;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determinant(1, 2, 3, 4, 5, 6, 5, 7, 8)
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
internal double Determinant2()
|
||||
{
|
||||
double det =
|
||||
m1 * m5 * m8 +
|
||||
m3 * m4 * m7 +
|
||||
m2 * m6 * m5 -
|
||||
m3 * m5 * m5 -
|
||||
m1 * m6 * m7 -
|
||||
m2 * m4 * m8;
|
||||
return det;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determinant(0, 2, 3, 1, 5, 6, 2, 7, 8)
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
internal double Determinant3()
|
||||
{
|
||||
double det =
|
||||
m0 * m5 * m8 +
|
||||
m3 * m1 * m7 +
|
||||
m2 * m6 * m2 -
|
||||
m3 * m5 * m2 -
|
||||
m0 * m6 * m7 -
|
||||
m2 * m1 * m8;
|
||||
return det;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determinant(0, 1, 3, 1, 4, 6, 2, 5, 8)
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
internal double Determinant4()
|
||||
{
|
||||
double det =
|
||||
m0 * m4 * m8 +
|
||||
m3 * m1 * m5 +
|
||||
m1 * m6 * m2 -
|
||||
m3 * m4 * m2 -
|
||||
m0 * m6 * m5 -
|
||||
m1 * m1 * m8;
|
||||
return det;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Public Methods
|
||||
/// <summary>
|
||||
/// Computes the determinant of this matrix.
|
||||
/// </summary>
|
||||
/// <param name="a11">The a11 index.</param>
|
||||
/// <param name="a12">The a12 index.</param>
|
||||
/// <param name="a13">The a13 index.</param>
|
||||
/// <param name="a21">The a21 index.</param>
|
||||
/// <param name="a22">The a22 index.</param>
|
||||
/// <param name="a23">The a23 index.</param>
|
||||
/// <param name="a31">The a31 index.</param>
|
||||
/// <param name="a32">The a32 index.</param>
|
||||
/// <param name="a33">The a33 index.</param>
|
||||
/// <returns>The determinant value.</returns>
|
||||
public double Determinant(int a11, int a12, int a13,
|
||||
int a21, int a22, int a23,
|
||||
int a31, int a32, int a33)
|
||||
{
|
||||
double det =
|
||||
this[a11] * this[a22] * this[a33] +
|
||||
this[a13] * this[a21] * this[a32] +
|
||||
this[a12] * this[a23] * this[a31] -
|
||||
this[a13] * this[a22] * this[a31] -
|
||||
this[a11] * this[a23] * this[a32] -
|
||||
this[a12] * this[a21] * this[a33];
|
||||
return det;
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
425
LightlessSync/ThirdParty/MeshDecimator/Math/Vector2.cs
vendored
Normal file
425
LightlessSync/ThirdParty/MeshDecimator/Math/Vector2.cs
vendored
Normal file
@@ -0,0 +1,425 @@
|
||||
#region License
|
||||
/*
|
||||
MIT License
|
||||
|
||||
Copyright(c) 2017-2018 Mattias Edlund
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
|
||||
namespace MeshDecimator.Math
|
||||
{
|
||||
/// <summary>
|
||||
/// A single precision 2D vector.
|
||||
/// </summary>
|
||||
public struct Vector2 : IEquatable<Vector2>
|
||||
{
|
||||
#region Static Read-Only
|
||||
/// <summary>
|
||||
/// The zero vector.
|
||||
/// </summary>
|
||||
public static readonly Vector2 zero = new Vector2(0, 0);
|
||||
#endregion
|
||||
|
||||
#region Consts
|
||||
/// <summary>
|
||||
/// The vector epsilon.
|
||||
/// </summary>
|
||||
public const float Epsilon = 9.99999944E-11f;
|
||||
#endregion
|
||||
|
||||
#region Fields
|
||||
/// <summary>
|
||||
/// The x component.
|
||||
/// </summary>
|
||||
public float x;
|
||||
/// <summary>
|
||||
/// The y component.
|
||||
/// </summary>
|
||||
public float y;
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
/// <summary>
|
||||
/// Gets the magnitude of this vector.
|
||||
/// </summary>
|
||||
public float Magnitude
|
||||
{
|
||||
get { return (float)System.Math.Sqrt(x * x + y * y); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the squared magnitude of this vector.
|
||||
/// </summary>
|
||||
public float MagnitudeSqr
|
||||
{
|
||||
get { return (x * x + y * y); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a normalized vector from this vector.
|
||||
/// </summary>
|
||||
public Vector2 Normalized
|
||||
{
|
||||
get
|
||||
{
|
||||
Vector2 result;
|
||||
Normalize(ref this, out result);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a specific component by index in this vector.
|
||||
/// </summary>
|
||||
/// <param name="index">The component index.</param>
|
||||
public float this[int index]
|
||||
{
|
||||
get
|
||||
{
|
||||
switch (index)
|
||||
{
|
||||
case 0:
|
||||
return x;
|
||||
case 1:
|
||||
return y;
|
||||
default:
|
||||
throw new IndexOutOfRangeException("Invalid Vector2 index!");
|
||||
}
|
||||
}
|
||||
set
|
||||
{
|
||||
switch (index)
|
||||
{
|
||||
case 0:
|
||||
x = value;
|
||||
break;
|
||||
case 1:
|
||||
y = value;
|
||||
break;
|
||||
default:
|
||||
throw new IndexOutOfRangeException("Invalid Vector2 index!");
|
||||
}
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Constructor
|
||||
/// <summary>
|
||||
/// Creates a new vector with one value for all components.
|
||||
/// </summary>
|
||||
/// <param name="value">The value.</param>
|
||||
public Vector2(float value)
|
||||
{
|
||||
this.x = value;
|
||||
this.y = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new vector.
|
||||
/// </summary>
|
||||
/// <param name="x">The x value.</param>
|
||||
/// <param name="y">The y value.</param>
|
||||
public Vector2(float x, float y)
|
||||
{
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Operators
|
||||
/// <summary>
|
||||
/// Adds two vectors.
|
||||
/// </summary>
|
||||
/// <param name="a">The first vector.</param>
|
||||
/// <param name="b">The second vector.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector2 operator +(Vector2 a, Vector2 b)
|
||||
{
|
||||
return new Vector2(a.x + b.x, a.y + b.y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subtracts two vectors.
|
||||
/// </summary>
|
||||
/// <param name="a">The first vector.</param>
|
||||
/// <param name="b">The second vector.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector2 operator -(Vector2 a, Vector2 b)
|
||||
{
|
||||
return new Vector2(a.x - b.x, a.y - b.y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scales the vector uniformly.
|
||||
/// </summary>
|
||||
/// <param name="a">The vector.</param>
|
||||
/// <param name="d">The scaling value.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector2 operator *(Vector2 a, float d)
|
||||
{
|
||||
return new Vector2(a.x * d, a.y * d);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scales the vector uniformly.
|
||||
/// </summary>
|
||||
/// <param name="d">The scaling value.</param>
|
||||
/// <param name="a">The vector.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector2 operator *(float d, Vector2 a)
|
||||
{
|
||||
return new Vector2(a.x * d, a.y * d);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Divides the vector with a float.
|
||||
/// </summary>
|
||||
/// <param name="a">The vector.</param>
|
||||
/// <param name="d">The dividing float value.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector2 operator /(Vector2 a, float d)
|
||||
{
|
||||
return new Vector2(a.x / d, a.y / d);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subtracts the vector from a zero vector.
|
||||
/// </summary>
|
||||
/// <param name="a">The vector.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector2 operator -(Vector2 a)
|
||||
{
|
||||
return new Vector2(-a.x, -a.y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if two vectors equals eachother.
|
||||
/// </summary>
|
||||
/// <param name="lhs">The left hand side vector.</param>
|
||||
/// <param name="rhs">The right hand side vector.</param>
|
||||
/// <returns>If equals.</returns>
|
||||
public static bool operator ==(Vector2 lhs, Vector2 rhs)
|
||||
{
|
||||
return (lhs - rhs).MagnitudeSqr < Epsilon;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if two vectors don't equal eachother.
|
||||
/// </summary>
|
||||
/// <param name="lhs">The left hand side vector.</param>
|
||||
/// <param name="rhs">The right hand side vector.</param>
|
||||
/// <returns>If not equals.</returns>
|
||||
public static bool operator !=(Vector2 lhs, Vector2 rhs)
|
||||
{
|
||||
return (lhs - rhs).MagnitudeSqr >= Epsilon;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Explicitly converts from a double-precision vector into a single-precision vector.
|
||||
/// </summary>
|
||||
/// <param name="v">The double-precision vector.</param>
|
||||
public static explicit operator Vector2(Vector2d v)
|
||||
{
|
||||
return new Vector2((float)v.x, (float)v.y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implicitly converts from an integer vector into a single-precision vector.
|
||||
/// </summary>
|
||||
/// <param name="v">The integer vector.</param>
|
||||
public static implicit operator Vector2(Vector2i v)
|
||||
{
|
||||
return new Vector2(v.x, v.y);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Public Methods
|
||||
#region Instance
|
||||
/// <summary>
|
||||
/// Set x and y components of an existing vector.
|
||||
/// </summary>
|
||||
/// <param name="x">The x value.</param>
|
||||
/// <param name="y">The y value.</param>
|
||||
public void Set(float x, float y)
|
||||
{
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Multiplies with another vector component-wise.
|
||||
/// </summary>
|
||||
/// <param name="scale">The vector to multiply with.</param>
|
||||
public void Scale(ref Vector2 scale)
|
||||
{
|
||||
x *= scale.x;
|
||||
y *= scale.y;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes this vector.
|
||||
/// </summary>
|
||||
public void Normalize()
|
||||
{
|
||||
float mag = this.Magnitude;
|
||||
if (mag > Epsilon)
|
||||
{
|
||||
x /= mag;
|
||||
y /= mag;
|
||||
}
|
||||
else
|
||||
{
|
||||
x = y = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clamps this vector between a specific range.
|
||||
/// </summary>
|
||||
/// <param name="min">The minimum component value.</param>
|
||||
/// <param name="max">The maximum component value.</param>
|
||||
public void Clamp(float min, float max)
|
||||
{
|
||||
if (x < min) x = min;
|
||||
else if (x > max) x = max;
|
||||
|
||||
if (y < min) y = min;
|
||||
else if (y > max) y = max;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Object
|
||||
/// <summary>
|
||||
/// Returns a hash code for this vector.
|
||||
/// </summary>
|
||||
/// <returns>The hash code.</returns>
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return x.GetHashCode() ^ y.GetHashCode() << 2;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if this vector is equal to another one.
|
||||
/// </summary>
|
||||
/// <param name="other">The other vector to compare to.</param>
|
||||
/// <returns>If equals.</returns>
|
||||
public override bool Equals(object other)
|
||||
{
|
||||
if (!(other is Vector2))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
Vector2 vector = (Vector2)other;
|
||||
return (x == vector.x && y == vector.y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if this vector is equal to another one.
|
||||
/// </summary>
|
||||
/// <param name="other">The other vector to compare to.</param>
|
||||
/// <returns>If equals.</returns>
|
||||
public bool Equals(Vector2 other)
|
||||
{
|
||||
return (x == other.x && y == other.y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a nicely formatted string for this vector.
|
||||
/// </summary>
|
||||
/// <returns>The string.</returns>
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format("({0}, {1})",
|
||||
x.ToString("F1", CultureInfo.InvariantCulture),
|
||||
y.ToString("F1", CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a nicely formatted string for this vector.
|
||||
/// </summary>
|
||||
/// <param name="format">The float format.</param>
|
||||
/// <returns>The string.</returns>
|
||||
public string ToString(string format)
|
||||
{
|
||||
return string.Format("({0}, {1})",
|
||||
x.ToString(format, CultureInfo.InvariantCulture),
|
||||
y.ToString(format, CultureInfo.InvariantCulture));
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Static
|
||||
/// <summary>
|
||||
/// Dot Product of two vectors.
|
||||
/// </summary>
|
||||
/// <param name="lhs">The left hand side vector.</param>
|
||||
/// <param name="rhs">The right hand side vector.</param>
|
||||
public static float Dot(ref Vector2 lhs, ref Vector2 rhs)
|
||||
{
|
||||
return lhs.x * rhs.x + lhs.y * rhs.y;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs a linear interpolation between two vectors.
|
||||
/// </summary>
|
||||
/// <param name="a">The vector to interpolate from.</param>
|
||||
/// <param name="b">The vector to interpolate to.</param>
|
||||
/// <param name="t">The time fraction.</param>
|
||||
/// <param name="result">The resulting vector.</param>
|
||||
public static void Lerp(ref Vector2 a, ref Vector2 b, float t, out Vector2 result)
|
||||
{
|
||||
result = new Vector2(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Multiplies two vectors component-wise.
|
||||
/// </summary>
|
||||
/// <param name="a">The first vector.</param>
|
||||
/// <param name="b">The second vector.</param>
|
||||
/// <param name="result">The resulting vector.</param>
|
||||
public static void Scale(ref Vector2 a, ref Vector2 b, out Vector2 result)
|
||||
{
|
||||
result = new Vector2(a.x * b.x, a.y * b.y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes a vector.
|
||||
/// </summary>
|
||||
/// <param name="value">The vector to normalize.</param>
|
||||
/// <param name="result">The resulting normalized vector.</param>
|
||||
public static void Normalize(ref Vector2 value, out Vector2 result)
|
||||
{
|
||||
float mag = value.Magnitude;
|
||||
if (mag > Epsilon)
|
||||
{
|
||||
result = new Vector2(value.x / mag, value.y / mag);
|
||||
}
|
||||
else
|
||||
{
|
||||
result = Vector2.zero;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
425
LightlessSync/ThirdParty/MeshDecimator/Math/Vector2d.cs
vendored
Normal file
425
LightlessSync/ThirdParty/MeshDecimator/Math/Vector2d.cs
vendored
Normal file
@@ -0,0 +1,425 @@
|
||||
#region License
|
||||
/*
|
||||
MIT License
|
||||
|
||||
Copyright(c) 2017-2018 Mattias Edlund
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
|
||||
namespace MeshDecimator.Math
|
||||
{
|
||||
/// <summary>
|
||||
/// A double precision 2D vector.
|
||||
/// </summary>
|
||||
public struct Vector2d : IEquatable<Vector2d>
|
||||
{
|
||||
#region Static Read-Only
|
||||
/// <summary>
|
||||
/// The zero vector.
|
||||
/// </summary>
|
||||
public static readonly Vector2d zero = new Vector2d(0, 0);
|
||||
#endregion
|
||||
|
||||
#region Consts
|
||||
/// <summary>
|
||||
/// The vector epsilon.
|
||||
/// </summary>
|
||||
public const double Epsilon = double.Epsilon;
|
||||
#endregion
|
||||
|
||||
#region Fields
|
||||
/// <summary>
|
||||
/// The x component.
|
||||
/// </summary>
|
||||
public double x;
|
||||
/// <summary>
|
||||
/// The y component.
|
||||
/// </summary>
|
||||
public double y;
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
/// <summary>
|
||||
/// Gets the magnitude of this vector.
|
||||
/// </summary>
|
||||
public double Magnitude
|
||||
{
|
||||
get { return System.Math.Sqrt(x * x + y * y); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the squared magnitude of this vector.
|
||||
/// </summary>
|
||||
public double MagnitudeSqr
|
||||
{
|
||||
get { return (x * x + y * y); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a normalized vector from this vector.
|
||||
/// </summary>
|
||||
public Vector2d Normalized
|
||||
{
|
||||
get
|
||||
{
|
||||
Vector2d result;
|
||||
Normalize(ref this, out result);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a specific component by index in this vector.
|
||||
/// </summary>
|
||||
/// <param name="index">The component index.</param>
|
||||
public double this[int index]
|
||||
{
|
||||
get
|
||||
{
|
||||
switch (index)
|
||||
{
|
||||
case 0:
|
||||
return x;
|
||||
case 1:
|
||||
return y;
|
||||
default:
|
||||
throw new IndexOutOfRangeException("Invalid Vector2d index!");
|
||||
}
|
||||
}
|
||||
set
|
||||
{
|
||||
switch (index)
|
||||
{
|
||||
case 0:
|
||||
x = value;
|
||||
break;
|
||||
case 1:
|
||||
y = value;
|
||||
break;
|
||||
default:
|
||||
throw new IndexOutOfRangeException("Invalid Vector2d index!");
|
||||
}
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Constructor
|
||||
/// <summary>
|
||||
/// Creates a new vector with one value for all components.
|
||||
/// </summary>
|
||||
/// <param name="value">The value.</param>
|
||||
public Vector2d(double value)
|
||||
{
|
||||
this.x = value;
|
||||
this.y = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new vector.
|
||||
/// </summary>
|
||||
/// <param name="x">The x value.</param>
|
||||
/// <param name="y">The y value.</param>
|
||||
public Vector2d(double x, double y)
|
||||
{
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Operators
|
||||
/// <summary>
|
||||
/// Adds two vectors.
|
||||
/// </summary>
|
||||
/// <param name="a">The first vector.</param>
|
||||
/// <param name="b">The second vector.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector2d operator +(Vector2d a, Vector2d b)
|
||||
{
|
||||
return new Vector2d(a.x + b.x, a.y + b.y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subtracts two vectors.
|
||||
/// </summary>
|
||||
/// <param name="a">The first vector.</param>
|
||||
/// <param name="b">The second vector.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector2d operator -(Vector2d a, Vector2d b)
|
||||
{
|
||||
return new Vector2d(a.x - b.x, a.y - b.y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scales the vector uniformly.
|
||||
/// </summary>
|
||||
/// <param name="a">The vector.</param>
|
||||
/// <param name="d">The scaling value.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector2d operator *(Vector2d a, double d)
|
||||
{
|
||||
return new Vector2d(a.x * d, a.y * d);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scales the vector uniformly.
|
||||
/// </summary>
|
||||
/// <param name="d">The scaling value.</param>
|
||||
/// <param name="a">The vector.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector2d operator *(double d, Vector2d a)
|
||||
{
|
||||
return new Vector2d(a.x * d, a.y * d);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Divides the vector with a float.
|
||||
/// </summary>
|
||||
/// <param name="a">The vector.</param>
|
||||
/// <param name="d">The dividing float value.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector2d operator /(Vector2d a, double d)
|
||||
{
|
||||
return new Vector2d(a.x / d, a.y / d);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subtracts the vector from a zero vector.
|
||||
/// </summary>
|
||||
/// <param name="a">The vector.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector2d operator -(Vector2d a)
|
||||
{
|
||||
return new Vector2d(-a.x, -a.y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if two vectors equals eachother.
|
||||
/// </summary>
|
||||
/// <param name="lhs">The left hand side vector.</param>
|
||||
/// <param name="rhs">The right hand side vector.</param>
|
||||
/// <returns>If equals.</returns>
|
||||
public static bool operator ==(Vector2d lhs, Vector2d rhs)
|
||||
{
|
||||
return (lhs - rhs).MagnitudeSqr < Epsilon;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if two vectors don't equal eachother.
|
||||
/// </summary>
|
||||
/// <param name="lhs">The left hand side vector.</param>
|
||||
/// <param name="rhs">The right hand side vector.</param>
|
||||
/// <returns>If not equals.</returns>
|
||||
public static bool operator !=(Vector2d lhs, Vector2d rhs)
|
||||
{
|
||||
return (lhs - rhs).MagnitudeSqr >= Epsilon;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implicitly converts from a single-precision vector into a double-precision vector.
|
||||
/// </summary>
|
||||
/// <param name="v">The single-precision vector.</param>
|
||||
public static implicit operator Vector2d(Vector2 v)
|
||||
{
|
||||
return new Vector2d(v.x, v.y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implicitly converts from an integer vector into a double-precision vector.
|
||||
/// </summary>
|
||||
/// <param name="v">The integer vector.</param>
|
||||
public static implicit operator Vector2d(Vector2i v)
|
||||
{
|
||||
return new Vector2d(v.x, v.y);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Public Methods
|
||||
#region Instance
|
||||
/// <summary>
|
||||
/// Set x and y components of an existing vector.
|
||||
/// </summary>
|
||||
/// <param name="x">The x value.</param>
|
||||
/// <param name="y">The y value.</param>
|
||||
public void Set(double x, double y)
|
||||
{
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Multiplies with another vector component-wise.
|
||||
/// </summary>
|
||||
/// <param name="scale">The vector to multiply with.</param>
|
||||
public void Scale(ref Vector2d scale)
|
||||
{
|
||||
x *= scale.x;
|
||||
y *= scale.y;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes this vector.
|
||||
/// </summary>
|
||||
public void Normalize()
|
||||
{
|
||||
double mag = this.Magnitude;
|
||||
if (mag > Epsilon)
|
||||
{
|
||||
x /= mag;
|
||||
y /= mag;
|
||||
}
|
||||
else
|
||||
{
|
||||
x = y = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clamps this vector between a specific range.
|
||||
/// </summary>
|
||||
/// <param name="min">The minimum component value.</param>
|
||||
/// <param name="max">The maximum component value.</param>
|
||||
public void Clamp(double min, double max)
|
||||
{
|
||||
if (x < min) x = min;
|
||||
else if (x > max) x = max;
|
||||
|
||||
if (y < min) y = min;
|
||||
else if (y > max) y = max;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Object
|
||||
/// <summary>
|
||||
/// Returns a hash code for this vector.
|
||||
/// </summary>
|
||||
/// <returns>The hash code.</returns>
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return x.GetHashCode() ^ y.GetHashCode() << 2;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if this vector is equal to another one.
|
||||
/// </summary>
|
||||
/// <param name="other">The other vector to compare to.</param>
|
||||
/// <returns>If equals.</returns>
|
||||
public override bool Equals(object other)
|
||||
{
|
||||
if (!(other is Vector2d))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
Vector2d vector = (Vector2d)other;
|
||||
return (x == vector.x && y == vector.y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if this vector is equal to another one.
|
||||
/// </summary>
|
||||
/// <param name="other">The other vector to compare to.</param>
|
||||
/// <returns>If equals.</returns>
|
||||
public bool Equals(Vector2d other)
|
||||
{
|
||||
return (x == other.x && y == other.y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a nicely formatted string for this vector.
|
||||
/// </summary>
|
||||
/// <returns>The string.</returns>
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format("({0}, {1})",
|
||||
x.ToString("F1", CultureInfo.InvariantCulture),
|
||||
y.ToString("F1", CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a nicely formatted string for this vector.
|
||||
/// </summary>
|
||||
/// <param name="format">The float format.</param>
|
||||
/// <returns>The string.</returns>
|
||||
public string ToString(string format)
|
||||
{
|
||||
return string.Format("({0}, {1})",
|
||||
x.ToString(format, CultureInfo.InvariantCulture),
|
||||
y.ToString(format, CultureInfo.InvariantCulture));
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Static
|
||||
/// <summary>
|
||||
/// Dot Product of two vectors.
|
||||
/// </summary>
|
||||
/// <param name="lhs">The left hand side vector.</param>
|
||||
/// <param name="rhs">The right hand side vector.</param>
|
||||
public static double Dot(ref Vector2d lhs, ref Vector2d rhs)
|
||||
{
|
||||
return lhs.x * rhs.x + lhs.y * rhs.y;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs a linear interpolation between two vectors.
|
||||
/// </summary>
|
||||
/// <param name="a">The vector to interpolate from.</param>
|
||||
/// <param name="b">The vector to interpolate to.</param>
|
||||
/// <param name="t">The time fraction.</param>
|
||||
/// <param name="result">The resulting vector.</param>
|
||||
public static void Lerp(ref Vector2d a, ref Vector2d b, double t, out Vector2d result)
|
||||
{
|
||||
result = new Vector2d(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Multiplies two vectors component-wise.
|
||||
/// </summary>
|
||||
/// <param name="a">The first vector.</param>
|
||||
/// <param name="b">The second vector.</param>
|
||||
/// <param name="result">The resulting vector.</param>
|
||||
public static void Scale(ref Vector2d a, ref Vector2d b, out Vector2d result)
|
||||
{
|
||||
result = new Vector2d(a.x * b.x, a.y * b.y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes a vector.
|
||||
/// </summary>
|
||||
/// <param name="value">The vector to normalize.</param>
|
||||
/// <param name="result">The resulting normalized vector.</param>
|
||||
public static void Normalize(ref Vector2d value, out Vector2d result)
|
||||
{
|
||||
double mag = value.Magnitude;
|
||||
if (mag > Epsilon)
|
||||
{
|
||||
result = new Vector2d(value.x / mag, value.y / mag);
|
||||
}
|
||||
else
|
||||
{
|
||||
result = Vector2d.zero;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
348
LightlessSync/ThirdParty/MeshDecimator/Math/Vector2i.cs
vendored
Normal file
348
LightlessSync/ThirdParty/MeshDecimator/Math/Vector2i.cs
vendored
Normal file
@@ -0,0 +1,348 @@
|
||||
#region License
|
||||
/*
|
||||
MIT License
|
||||
|
||||
Copyright(c) 2017-2018 Mattias Edlund
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
|
||||
namespace MeshDecimator.Math
|
||||
{
|
||||
/// <summary>
|
||||
/// A 2D integer vector.
|
||||
/// </summary>
|
||||
public struct Vector2i : IEquatable<Vector2i>
|
||||
{
|
||||
#region Static Read-Only
|
||||
/// <summary>
|
||||
/// The zero vector.
|
||||
/// </summary>
|
||||
public static readonly Vector2i zero = new Vector2i(0, 0);
|
||||
#endregion
|
||||
|
||||
#region Fields
|
||||
/// <summary>
|
||||
/// The x component.
|
||||
/// </summary>
|
||||
public int x;
|
||||
/// <summary>
|
||||
/// The y component.
|
||||
/// </summary>
|
||||
public int y;
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
/// <summary>
|
||||
/// Gets the magnitude of this vector.
|
||||
/// </summary>
|
||||
public int Magnitude
|
||||
{
|
||||
get { return (int)System.Math.Sqrt(x * x + y * y); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the squared magnitude of this vector.
|
||||
/// </summary>
|
||||
public int MagnitudeSqr
|
||||
{
|
||||
get { return (x * x + y * y); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a specific component by index in this vector.
|
||||
/// </summary>
|
||||
/// <param name="index">The component index.</param>
|
||||
public int this[int index]
|
||||
{
|
||||
get
|
||||
{
|
||||
switch (index)
|
||||
{
|
||||
case 0:
|
||||
return x;
|
||||
case 1:
|
||||
return y;
|
||||
default:
|
||||
throw new IndexOutOfRangeException("Invalid Vector2i index!");
|
||||
}
|
||||
}
|
||||
set
|
||||
{
|
||||
switch (index)
|
||||
{
|
||||
case 0:
|
||||
x = value;
|
||||
break;
|
||||
case 1:
|
||||
y = value;
|
||||
break;
|
||||
default:
|
||||
throw new IndexOutOfRangeException("Invalid Vector2i index!");
|
||||
}
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Constructor
|
||||
/// <summary>
|
||||
/// Creates a new vector with one value for all components.
|
||||
/// </summary>
|
||||
/// <param name="value">The value.</param>
|
||||
public Vector2i(int value)
|
||||
{
|
||||
this.x = value;
|
||||
this.y = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new vector.
|
||||
/// </summary>
|
||||
/// <param name="x">The x value.</param>
|
||||
/// <param name="y">The y value.</param>
|
||||
public Vector2i(int x, int y)
|
||||
{
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Operators
|
||||
/// <summary>
|
||||
/// Adds two vectors.
|
||||
/// </summary>
|
||||
/// <param name="a">The first vector.</param>
|
||||
/// <param name="b">The second vector.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector2i operator +(Vector2i a, Vector2i b)
|
||||
{
|
||||
return new Vector2i(a.x + b.x, a.y + b.y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subtracts two vectors.
|
||||
/// </summary>
|
||||
/// <param name="a">The first vector.</param>
|
||||
/// <param name="b">The second vector.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector2i operator -(Vector2i a, Vector2i b)
|
||||
{
|
||||
return new Vector2i(a.x - b.x, a.y - b.y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scales the vector uniformly.
|
||||
/// </summary>
|
||||
/// <param name="a">The vector.</param>
|
||||
/// <param name="d">The scaling value.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector2i operator *(Vector2i a, int d)
|
||||
{
|
||||
return new Vector2i(a.x * d, a.y * d);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scales the vector uniformly.
|
||||
/// </summary>
|
||||
/// <param name="d">The scaling value.</param>
|
||||
/// <param name="a">The vector.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector2i operator *(int d, Vector2i a)
|
||||
{
|
||||
return new Vector2i(a.x * d, a.y * d);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Divides the vector with a float.
|
||||
/// </summary>
|
||||
/// <param name="a">The vector.</param>
|
||||
/// <param name="d">The dividing float value.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector2i operator /(Vector2i a, int d)
|
||||
{
|
||||
return new Vector2i(a.x / d, a.y / d);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subtracts the vector from a zero vector.
|
||||
/// </summary>
|
||||
/// <param name="a">The vector.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector2i operator -(Vector2i a)
|
||||
{
|
||||
return new Vector2i(-a.x, -a.y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if two vectors equals eachother.
|
||||
/// </summary>
|
||||
/// <param name="lhs">The left hand side vector.</param>
|
||||
/// <param name="rhs">The right hand side vector.</param>
|
||||
/// <returns>If equals.</returns>
|
||||
public static bool operator ==(Vector2i lhs, Vector2i rhs)
|
||||
{
|
||||
return (lhs.x == rhs.x && lhs.y == rhs.y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if two vectors don't equal eachother.
|
||||
/// </summary>
|
||||
/// <param name="lhs">The left hand side vector.</param>
|
||||
/// <param name="rhs">The right hand side vector.</param>
|
||||
/// <returns>If not equals.</returns>
|
||||
public static bool operator !=(Vector2i lhs, Vector2i rhs)
|
||||
{
|
||||
return (lhs.x != rhs.x || lhs.y != rhs.y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Explicitly converts from a single-precision vector into an integer vector.
|
||||
/// </summary>
|
||||
/// <param name="v">The single-precision vector.</param>
|
||||
public static explicit operator Vector2i(Vector2 v)
|
||||
{
|
||||
return new Vector2i((int)v.x, (int)v.y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Explicitly converts from a double-precision vector into an integer vector.
|
||||
/// </summary>
|
||||
/// <param name="v">The double-precision vector.</param>
|
||||
public static explicit operator Vector2i(Vector2d v)
|
||||
{
|
||||
return new Vector2i((int)v.x, (int)v.y);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Public Methods
|
||||
#region Instance
|
||||
/// <summary>
|
||||
/// Set x and y components of an existing vector.
|
||||
/// </summary>
|
||||
/// <param name="x">The x value.</param>
|
||||
/// <param name="y">The y value.</param>
|
||||
public void Set(int x, int y)
|
||||
{
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Multiplies with another vector component-wise.
|
||||
/// </summary>
|
||||
/// <param name="scale">The vector to multiply with.</param>
|
||||
public void Scale(ref Vector2i scale)
|
||||
{
|
||||
x *= scale.x;
|
||||
y *= scale.y;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clamps this vector between a specific range.
|
||||
/// </summary>
|
||||
/// <param name="min">The minimum component value.</param>
|
||||
/// <param name="max">The maximum component value.</param>
|
||||
public void Clamp(int min, int max)
|
||||
{
|
||||
if (x < min) x = min;
|
||||
else if (x > max) x = max;
|
||||
|
||||
if (y < min) y = min;
|
||||
else if (y > max) y = max;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Object
|
||||
/// <summary>
|
||||
/// Returns a hash code for this vector.
|
||||
/// </summary>
|
||||
/// <returns>The hash code.</returns>
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return x.GetHashCode() ^ y.GetHashCode() << 2;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if this vector is equal to another one.
|
||||
/// </summary>
|
||||
/// <param name="other">The other vector to compare to.</param>
|
||||
/// <returns>If equals.</returns>
|
||||
public override bool Equals(object other)
|
||||
{
|
||||
if (!(other is Vector2i))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
Vector2i vector = (Vector2i)other;
|
||||
return (x == vector.x && y == vector.y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if this vector is equal to another one.
|
||||
/// </summary>
|
||||
/// <param name="other">The other vector to compare to.</param>
|
||||
/// <returns>If equals.</returns>
|
||||
public bool Equals(Vector2i other)
|
||||
{
|
||||
return (x == other.x && y == other.y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a nicely formatted string for this vector.
|
||||
/// </summary>
|
||||
/// <returns>The string.</returns>
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format("({0}, {1})",
|
||||
x.ToString(CultureInfo.InvariantCulture),
|
||||
y.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a nicely formatted string for this vector.
|
||||
/// </summary>
|
||||
/// <param name="format">The integer format.</param>
|
||||
/// <returns>The string.</returns>
|
||||
public string ToString(string format)
|
||||
{
|
||||
return string.Format("({0}, {1})",
|
||||
x.ToString(format, CultureInfo.InvariantCulture),
|
||||
y.ToString(format, CultureInfo.InvariantCulture));
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Static
|
||||
/// <summary>
|
||||
/// Multiplies two vectors component-wise.
|
||||
/// </summary>
|
||||
/// <param name="a">The first vector.</param>
|
||||
/// <param name="b">The second vector.</param>
|
||||
/// <param name="result">The resulting vector.</param>
|
||||
public static void Scale(ref Vector2i a, ref Vector2i b, out Vector2i result)
|
||||
{
|
||||
result = new Vector2i(a.x * b.x, a.y * b.y);
|
||||
}
|
||||
#endregion
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
494
LightlessSync/ThirdParty/MeshDecimator/Math/Vector3.cs
vendored
Normal file
494
LightlessSync/ThirdParty/MeshDecimator/Math/Vector3.cs
vendored
Normal file
@@ -0,0 +1,494 @@
|
||||
#region License
|
||||
/*
|
||||
MIT License
|
||||
|
||||
Copyright(c) 2017-2018 Mattias Edlund
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
|
||||
namespace MeshDecimator.Math
|
||||
{
|
||||
/// <summary>
|
||||
/// A single precision 3D vector.
|
||||
/// </summary>
|
||||
public struct Vector3 : IEquatable<Vector3>
|
||||
{
|
||||
#region Static Read-Only
|
||||
/// <summary>
|
||||
/// The zero vector.
|
||||
/// </summary>
|
||||
public static readonly Vector3 zero = new Vector3(0, 0, 0);
|
||||
#endregion
|
||||
|
||||
#region Consts
|
||||
/// <summary>
|
||||
/// The vector epsilon.
|
||||
/// </summary>
|
||||
public const float Epsilon = 9.99999944E-11f;
|
||||
#endregion
|
||||
|
||||
#region Fields
|
||||
/// <summary>
|
||||
/// The x component.
|
||||
/// </summary>
|
||||
public float x;
|
||||
/// <summary>
|
||||
/// The y component.
|
||||
/// </summary>
|
||||
public float y;
|
||||
/// <summary>
|
||||
/// The z component.
|
||||
/// </summary>
|
||||
public float z;
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
/// <summary>
|
||||
/// Gets the magnitude of this vector.
|
||||
/// </summary>
|
||||
public float Magnitude
|
||||
{
|
||||
get { return (float)System.Math.Sqrt(x * x + y * y + z * z); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the squared magnitude of this vector.
|
||||
/// </summary>
|
||||
public float MagnitudeSqr
|
||||
{
|
||||
get { return (x * x + y * y + z * z); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a normalized vector from this vector.
|
||||
/// </summary>
|
||||
public Vector3 Normalized
|
||||
{
|
||||
get
|
||||
{
|
||||
Vector3 result;
|
||||
Normalize(ref this, out result);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a specific component by index in this vector.
|
||||
/// </summary>
|
||||
/// <param name="index">The component index.</param>
|
||||
public float this[int index]
|
||||
{
|
||||
get
|
||||
{
|
||||
switch (index)
|
||||
{
|
||||
case 0:
|
||||
return x;
|
||||
case 1:
|
||||
return y;
|
||||
case 2:
|
||||
return z;
|
||||
default:
|
||||
throw new IndexOutOfRangeException("Invalid Vector3 index!");
|
||||
}
|
||||
}
|
||||
set
|
||||
{
|
||||
switch (index)
|
||||
{
|
||||
case 0:
|
||||
x = value;
|
||||
break;
|
||||
case 1:
|
||||
y = value;
|
||||
break;
|
||||
case 2:
|
||||
z = value;
|
||||
break;
|
||||
default:
|
||||
throw new IndexOutOfRangeException("Invalid Vector3 index!");
|
||||
}
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Constructor
|
||||
/// <summary>
|
||||
/// Creates a new vector with one value for all components.
|
||||
/// </summary>
|
||||
/// <param name="value">The value.</param>
|
||||
public Vector3(float value)
|
||||
{
|
||||
this.x = value;
|
||||
this.y = value;
|
||||
this.z = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new vector.
|
||||
/// </summary>
|
||||
/// <param name="x">The x value.</param>
|
||||
/// <param name="y">The y value.</param>
|
||||
/// <param name="z">The z value.</param>
|
||||
public Vector3(float x, float y, float z)
|
||||
{
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new vector from a double precision vector.
|
||||
/// </summary>
|
||||
/// <param name="vector">The double precision vector.</param>
|
||||
public Vector3(Vector3d vector)
|
||||
{
|
||||
this.x = (float)vector.x;
|
||||
this.y = (float)vector.y;
|
||||
this.z = (float)vector.z;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Operators
|
||||
/// <summary>
|
||||
/// Adds two vectors.
|
||||
/// </summary>
|
||||
/// <param name="a">The first vector.</param>
|
||||
/// <param name="b">The second vector.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector3 operator +(Vector3 a, Vector3 b)
|
||||
{
|
||||
return new Vector3(a.x + b.x, a.y + b.y, a.z + b.z);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subtracts two vectors.
|
||||
/// </summary>
|
||||
/// <param name="a">The first vector.</param>
|
||||
/// <param name="b">The second vector.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector3 operator -(Vector3 a, Vector3 b)
|
||||
{
|
||||
return new Vector3(a.x - b.x, a.y - b.y, a.z - b.z);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scales the vector uniformly.
|
||||
/// </summary>
|
||||
/// <param name="a">The vector.</param>
|
||||
/// <param name="d">The scaling value.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector3 operator *(Vector3 a, float d)
|
||||
{
|
||||
return new Vector3(a.x * d, a.y * d, a.z * d);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scales the vector uniformly.
|
||||
/// </summary>
|
||||
/// <param name="d">The scaling value.</param>
|
||||
/// <param name="a">The vector.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector3 operator *(float d, Vector3 a)
|
||||
{
|
||||
return new Vector3(a.x * d, a.y * d, a.z * d);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Divides the vector with a float.
|
||||
/// </summary>
|
||||
/// <param name="a">The vector.</param>
|
||||
/// <param name="d">The dividing float value.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector3 operator /(Vector3 a, float d)
|
||||
{
|
||||
return new Vector3(a.x / d, a.y / d, a.z / d);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subtracts the vector from a zero vector.
|
||||
/// </summary>
|
||||
/// <param name="a">The vector.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector3 operator -(Vector3 a)
|
||||
{
|
||||
return new Vector3(-a.x, -a.y, -a.z);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if two vectors equals eachother.
|
||||
/// </summary>
|
||||
/// <param name="lhs">The left hand side vector.</param>
|
||||
/// <param name="rhs">The right hand side vector.</param>
|
||||
/// <returns>If equals.</returns>
|
||||
public static bool operator ==(Vector3 lhs, Vector3 rhs)
|
||||
{
|
||||
return (lhs - rhs).MagnitudeSqr < Epsilon;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if two vectors don't equal eachother.
|
||||
/// </summary>
|
||||
/// <param name="lhs">The left hand side vector.</param>
|
||||
/// <param name="rhs">The right hand side vector.</param>
|
||||
/// <returns>If not equals.</returns>
|
||||
public static bool operator !=(Vector3 lhs, Vector3 rhs)
|
||||
{
|
||||
return (lhs - rhs).MagnitudeSqr >= Epsilon;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Explicitly converts from a double-precision vector into a single-precision vector.
|
||||
/// </summary>
|
||||
/// <param name="v">The double-precision vector.</param>
|
||||
public static explicit operator Vector3(Vector3d v)
|
||||
{
|
||||
return new Vector3((float)v.x, (float)v.y, (float)v.z);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implicitly converts from an integer vector into a single-precision vector.
|
||||
/// </summary>
|
||||
/// <param name="v">The integer vector.</param>
|
||||
public static implicit operator Vector3(Vector3i v)
|
||||
{
|
||||
return new Vector3(v.x, v.y, v.z);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Public Methods
|
||||
#region Instance
|
||||
/// <summary>
|
||||
/// Set x, y and z components of an existing vector.
|
||||
/// </summary>
|
||||
/// <param name="x">The x value.</param>
|
||||
/// <param name="y">The y value.</param>
|
||||
/// <param name="z">The z value.</param>
|
||||
public void Set(float x, float y, float z)
|
||||
{
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Multiplies with another vector component-wise.
|
||||
/// </summary>
|
||||
/// <param name="scale">The vector to multiply with.</param>
|
||||
public void Scale(ref Vector3 scale)
|
||||
{
|
||||
x *= scale.x;
|
||||
y *= scale.y;
|
||||
z *= scale.z;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes this vector.
|
||||
/// </summary>
|
||||
public void Normalize()
|
||||
{
|
||||
float mag = this.Magnitude;
|
||||
if (mag > Epsilon)
|
||||
{
|
||||
x /= mag;
|
||||
y /= mag;
|
||||
z /= mag;
|
||||
}
|
||||
else
|
||||
{
|
||||
x = y = z = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clamps this vector between a specific range.
|
||||
/// </summary>
|
||||
/// <param name="min">The minimum component value.</param>
|
||||
/// <param name="max">The maximum component value.</param>
|
||||
public void Clamp(float min, float max)
|
||||
{
|
||||
if (x < min) x = min;
|
||||
else if (x > max) x = max;
|
||||
|
||||
if (y < min) y = min;
|
||||
else if (y > max) y = max;
|
||||
|
||||
if (z < min) z = min;
|
||||
else if (z > max) z = max;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Object
|
||||
/// <summary>
|
||||
/// Returns a hash code for this vector.
|
||||
/// </summary>
|
||||
/// <returns>The hash code.</returns>
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return x.GetHashCode() ^ y.GetHashCode() << 2 ^ z.GetHashCode() >> 2;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if this vector is equal to another one.
|
||||
/// </summary>
|
||||
/// <param name="other">The other vector to compare to.</param>
|
||||
/// <returns>If equals.</returns>
|
||||
public override bool Equals(object other)
|
||||
{
|
||||
if (!(other is Vector3))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
Vector3 vector = (Vector3)other;
|
||||
return (x == vector.x && y == vector.y && z == vector.z);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if this vector is equal to another one.
|
||||
/// </summary>
|
||||
/// <param name="other">The other vector to compare to.</param>
|
||||
/// <returns>If equals.</returns>
|
||||
public bool Equals(Vector3 other)
|
||||
{
|
||||
return (x == other.x && y == other.y && z == other.z);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a nicely formatted string for this vector.
|
||||
/// </summary>
|
||||
/// <returns>The string.</returns>
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format("({0}, {1}, {2})",
|
||||
x.ToString("F1", CultureInfo.InvariantCulture),
|
||||
y.ToString("F1", CultureInfo.InvariantCulture),
|
||||
z.ToString("F1", CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a nicely formatted string for this vector.
|
||||
/// </summary>
|
||||
/// <param name="format">The float format.</param>
|
||||
/// <returns>The string.</returns>
|
||||
public string ToString(string format)
|
||||
{
|
||||
return string.Format("({0}, {1}, {2})",
|
||||
x.ToString(format, CultureInfo.InvariantCulture),
|
||||
y.ToString(format, CultureInfo.InvariantCulture),
|
||||
z.ToString(format, CultureInfo.InvariantCulture));
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Static
|
||||
/// <summary>
|
||||
/// Dot Product of two vectors.
|
||||
/// </summary>
|
||||
/// <param name="lhs">The left hand side vector.</param>
|
||||
/// <param name="rhs">The right hand side vector.</param>
|
||||
public static float Dot(ref Vector3 lhs, ref Vector3 rhs)
|
||||
{
|
||||
return lhs.x * rhs.x + lhs.y * rhs.y + lhs.z * rhs.z;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cross Product of two vectors.
|
||||
/// </summary>
|
||||
/// <param name="lhs">The left hand side vector.</param>
|
||||
/// <param name="rhs">The right hand side vector.</param>
|
||||
/// <param name="result">The resulting vector.</param>
|
||||
public static void Cross(ref Vector3 lhs, ref Vector3 rhs, out Vector3 result)
|
||||
{
|
||||
result = new Vector3(lhs.y * rhs.z - lhs.z * rhs.y, lhs.z * rhs.x - lhs.x * rhs.z, lhs.x * rhs.y - lhs.y * rhs.x);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the angle between two vectors.
|
||||
/// </summary>
|
||||
/// <param name="from">The from vector.</param>
|
||||
/// <param name="to">The to vector.</param>
|
||||
/// <returns>The angle.</returns>
|
||||
public static float Angle(ref Vector3 from, ref Vector3 to)
|
||||
{
|
||||
Vector3 fromNormalized = from.Normalized;
|
||||
Vector3 toNormalized = to.Normalized;
|
||||
return (float)System.Math.Acos(MathHelper.Clamp(Vector3.Dot(ref fromNormalized, ref toNormalized), -1f, 1f)) * MathHelper.Rad2Deg;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs a linear interpolation between two vectors.
|
||||
/// </summary>
|
||||
/// <param name="a">The vector to interpolate from.</param>
|
||||
/// <param name="b">The vector to interpolate to.</param>
|
||||
/// <param name="t">The time fraction.</param>
|
||||
/// <param name="result">The resulting vector.</param>
|
||||
public static void Lerp(ref Vector3 a, ref Vector3 b, float t, out Vector3 result)
|
||||
{
|
||||
result = new Vector3(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t, a.z + (b.z - a.z) * t);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Multiplies two vectors component-wise.
|
||||
/// </summary>
|
||||
/// <param name="a">The first vector.</param>
|
||||
/// <param name="b">The second vector.</param>
|
||||
/// <param name="result">The resulting vector.</param>
|
||||
public static void Scale(ref Vector3 a, ref Vector3 b, out Vector3 result)
|
||||
{
|
||||
result = new Vector3(a.x * b.x, a.y * b.y, a.z * b.z);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes a vector.
|
||||
/// </summary>
|
||||
/// <param name="value">The vector to normalize.</param>
|
||||
/// <param name="result">The resulting normalized vector.</param>
|
||||
public static void Normalize(ref Vector3 value, out Vector3 result)
|
||||
{
|
||||
float mag = value.Magnitude;
|
||||
if (mag > Epsilon)
|
||||
{
|
||||
result = new Vector3(value.x / mag, value.y / mag, value.z / mag);
|
||||
}
|
||||
else
|
||||
{
|
||||
result = Vector3.zero;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes both vectors and makes them orthogonal to each other.
|
||||
/// </summary>
|
||||
/// <param name="normal">The normal vector.</param>
|
||||
/// <param name="tangent">The tangent.</param>
|
||||
public static void OrthoNormalize(ref Vector3 normal, ref Vector3 tangent)
|
||||
{
|
||||
normal.Normalize();
|
||||
Vector3 proj = normal * Vector3.Dot(ref tangent, ref normal);
|
||||
tangent -= proj;
|
||||
tangent.Normalize();
|
||||
}
|
||||
#endregion
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
481
LightlessSync/ThirdParty/MeshDecimator/Math/Vector3d.cs
vendored
Normal file
481
LightlessSync/ThirdParty/MeshDecimator/Math/Vector3d.cs
vendored
Normal file
@@ -0,0 +1,481 @@
|
||||
#region License
|
||||
/*
|
||||
MIT License
|
||||
|
||||
Copyright(c) 2017-2018 Mattias Edlund
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
|
||||
namespace MeshDecimator.Math
|
||||
{
|
||||
/// <summary>
|
||||
/// A double precision 3D vector.
|
||||
/// </summary>
|
||||
public struct Vector3d : IEquatable<Vector3d>
|
||||
{
|
||||
#region Static Read-Only
|
||||
/// <summary>
|
||||
/// The zero vector.
|
||||
/// </summary>
|
||||
public static readonly Vector3d zero = new Vector3d(0, 0, 0);
|
||||
#endregion
|
||||
|
||||
#region Consts
|
||||
/// <summary>
|
||||
/// The vector epsilon.
|
||||
/// </summary>
|
||||
public const double Epsilon = double.Epsilon;
|
||||
#endregion
|
||||
|
||||
#region Fields
|
||||
/// <summary>
|
||||
/// The x component.
|
||||
/// </summary>
|
||||
public double x;
|
||||
/// <summary>
|
||||
/// The y component.
|
||||
/// </summary>
|
||||
public double y;
|
||||
/// <summary>
|
||||
/// The z component.
|
||||
/// </summary>
|
||||
public double z;
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
/// <summary>
|
||||
/// Gets the magnitude of this vector.
|
||||
/// </summary>
|
||||
public double Magnitude
|
||||
{
|
||||
get { return System.Math.Sqrt(x * x + y * y + z * z); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the squared magnitude of this vector.
|
||||
/// </summary>
|
||||
public double MagnitudeSqr
|
||||
{
|
||||
get { return (x * x + y * y + z * z); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a normalized vector from this vector.
|
||||
/// </summary>
|
||||
public Vector3d Normalized
|
||||
{
|
||||
get
|
||||
{
|
||||
Vector3d result;
|
||||
Normalize(ref this, out result);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a specific component by index in this vector.
|
||||
/// </summary>
|
||||
/// <param name="index">The component index.</param>
|
||||
public double this[int index]
|
||||
{
|
||||
get
|
||||
{
|
||||
switch (index)
|
||||
{
|
||||
case 0:
|
||||
return x;
|
||||
case 1:
|
||||
return y;
|
||||
case 2:
|
||||
return z;
|
||||
default:
|
||||
throw new IndexOutOfRangeException("Invalid Vector3d index!");
|
||||
}
|
||||
}
|
||||
set
|
||||
{
|
||||
switch (index)
|
||||
{
|
||||
case 0:
|
||||
x = value;
|
||||
break;
|
||||
case 1:
|
||||
y = value;
|
||||
break;
|
||||
case 2:
|
||||
z = value;
|
||||
break;
|
||||
default:
|
||||
throw new IndexOutOfRangeException("Invalid Vector3d index!");
|
||||
}
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Constructor
|
||||
/// <summary>
|
||||
/// Creates a new vector with one value for all components.
|
||||
/// </summary>
|
||||
/// <param name="value">The value.</param>
|
||||
public Vector3d(double value)
|
||||
{
|
||||
this.x = value;
|
||||
this.y = value;
|
||||
this.z = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new vector.
|
||||
/// </summary>
|
||||
/// <param name="x">The x value.</param>
|
||||
/// <param name="y">The y value.</param>
|
||||
/// <param name="z">The z value.</param>
|
||||
public Vector3d(double x, double y, double z)
|
||||
{
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new vector from a single precision vector.
|
||||
/// </summary>
|
||||
/// <param name="vector">The single precision vector.</param>
|
||||
public Vector3d(Vector3 vector)
|
||||
{
|
||||
this.x = vector.x;
|
||||
this.y = vector.y;
|
||||
this.z = vector.z;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Operators
|
||||
/// <summary>
|
||||
/// Adds two vectors.
|
||||
/// </summary>
|
||||
/// <param name="a">The first vector.</param>
|
||||
/// <param name="b">The second vector.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector3d operator +(Vector3d a, Vector3d b)
|
||||
{
|
||||
return new Vector3d(a.x + b.x, a.y + b.y, a.z + b.z);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subtracts two vectors.
|
||||
/// </summary>
|
||||
/// <param name="a">The first vector.</param>
|
||||
/// <param name="b">The second vector.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector3d operator -(Vector3d a, Vector3d b)
|
||||
{
|
||||
return new Vector3d(a.x - b.x, a.y - b.y, a.z - b.z);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scales the vector uniformly.
|
||||
/// </summary>
|
||||
/// <param name="a">The vector.</param>
|
||||
/// <param name="d">The scaling value.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector3d operator *(Vector3d a, double d)
|
||||
{
|
||||
return new Vector3d(a.x * d, a.y * d, a.z * d);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scales the vector uniformly.
|
||||
/// </summary>
|
||||
/// <param name="d">The scaling value.</param>
|
||||
/// <param name="a">The vector.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector3d operator *(double d, Vector3d a)
|
||||
{
|
||||
return new Vector3d(a.x * d, a.y * d, a.z * d);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Divides the vector with a float.
|
||||
/// </summary>
|
||||
/// <param name="a">The vector.</param>
|
||||
/// <param name="d">The dividing float value.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector3d operator /(Vector3d a, double d)
|
||||
{
|
||||
return new Vector3d(a.x / d, a.y / d, a.z / d);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subtracts the vector from a zero vector.
|
||||
/// </summary>
|
||||
/// <param name="a">The vector.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector3d operator -(Vector3d a)
|
||||
{
|
||||
return new Vector3d(-a.x, -a.y, -a.z);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if two vectors equals eachother.
|
||||
/// </summary>
|
||||
/// <param name="lhs">The left hand side vector.</param>
|
||||
/// <param name="rhs">The right hand side vector.</param>
|
||||
/// <returns>If equals.</returns>
|
||||
public static bool operator ==(Vector3d lhs, Vector3d rhs)
|
||||
{
|
||||
return (lhs - rhs).MagnitudeSqr < Epsilon;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if two vectors don't equal eachother.
|
||||
/// </summary>
|
||||
/// <param name="lhs">The left hand side vector.</param>
|
||||
/// <param name="rhs">The right hand side vector.</param>
|
||||
/// <returns>If not equals.</returns>
|
||||
public static bool operator !=(Vector3d lhs, Vector3d rhs)
|
||||
{
|
||||
return (lhs - rhs).MagnitudeSqr >= Epsilon;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implicitly converts from a single-precision vector into a double-precision vector.
|
||||
/// </summary>
|
||||
/// <param name="v">The single-precision vector.</param>
|
||||
public static implicit operator Vector3d(Vector3 v)
|
||||
{
|
||||
return new Vector3d(v.x, v.y, v.z);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implicitly converts from an integer vector into a double-precision vector.
|
||||
/// </summary>
|
||||
/// <param name="v">The integer vector.</param>
|
||||
public static implicit operator Vector3d(Vector3i v)
|
||||
{
|
||||
return new Vector3d(v.x, v.y, v.z);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Public Methods
|
||||
#region Instance
|
||||
/// <summary>
|
||||
/// Set x, y and z components of an existing vector.
|
||||
/// </summary>
|
||||
/// <param name="x">The x value.</param>
|
||||
/// <param name="y">The y value.</param>
|
||||
/// <param name="z">The z value.</param>
|
||||
public void Set(double x, double y, double z)
|
||||
{
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Multiplies with another vector component-wise.
|
||||
/// </summary>
|
||||
/// <param name="scale">The vector to multiply with.</param>
|
||||
public void Scale(ref Vector3d scale)
|
||||
{
|
||||
x *= scale.x;
|
||||
y *= scale.y;
|
||||
z *= scale.z;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes this vector.
|
||||
/// </summary>
|
||||
public void Normalize()
|
||||
{
|
||||
double mag = this.Magnitude;
|
||||
if (mag > Epsilon)
|
||||
{
|
||||
x /= mag;
|
||||
y /= mag;
|
||||
z /= mag;
|
||||
}
|
||||
else
|
||||
{
|
||||
x = y = z = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clamps this vector between a specific range.
|
||||
/// </summary>
|
||||
/// <param name="min">The minimum component value.</param>
|
||||
/// <param name="max">The maximum component value.</param>
|
||||
public void Clamp(double min, double max)
|
||||
{
|
||||
if (x < min) x = min;
|
||||
else if (x > max) x = max;
|
||||
|
||||
if (y < min) y = min;
|
||||
else if (y > max) y = max;
|
||||
|
||||
if (z < min) z = min;
|
||||
else if (z > max) z = max;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Object
|
||||
/// <summary>
|
||||
/// Returns a hash code for this vector.
|
||||
/// </summary>
|
||||
/// <returns>The hash code.</returns>
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return x.GetHashCode() ^ y.GetHashCode() << 2 ^ z.GetHashCode() >> 2;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if this vector is equal to another one.
|
||||
/// </summary>
|
||||
/// <param name="other">The other vector to compare to.</param>
|
||||
/// <returns>If equals.</returns>
|
||||
public override bool Equals(object other)
|
||||
{
|
||||
if (!(other is Vector3d))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
Vector3d vector = (Vector3d)other;
|
||||
return (x == vector.x && y == vector.y && z == vector.z);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if this vector is equal to another one.
|
||||
/// </summary>
|
||||
/// <param name="other">The other vector to compare to.</param>
|
||||
/// <returns>If equals.</returns>
|
||||
public bool Equals(Vector3d other)
|
||||
{
|
||||
return (x == other.x && y == other.y && z == other.z);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a nicely formatted string for this vector.
|
||||
/// </summary>
|
||||
/// <returns>The string.</returns>
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format("({0}, {1}, {2})",
|
||||
x.ToString("F1", CultureInfo.InvariantCulture),
|
||||
y.ToString("F1", CultureInfo.InvariantCulture),
|
||||
z.ToString("F1", CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a nicely formatted string for this vector.
|
||||
/// </summary>
|
||||
/// <param name="format">The float format.</param>
|
||||
/// <returns>The string.</returns>
|
||||
public string ToString(string format)
|
||||
{
|
||||
return string.Format("({0}, {1}, {2})",
|
||||
x.ToString(format, CultureInfo.InvariantCulture),
|
||||
y.ToString(format, CultureInfo.InvariantCulture),
|
||||
z.ToString(format, CultureInfo.InvariantCulture));
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Static
|
||||
/// <summary>
|
||||
/// Dot Product of two vectors.
|
||||
/// </summary>
|
||||
/// <param name="lhs">The left hand side vector.</param>
|
||||
/// <param name="rhs">The right hand side vector.</param>
|
||||
public static double Dot(ref Vector3d lhs, ref Vector3d rhs)
|
||||
{
|
||||
return lhs.x * rhs.x + lhs.y * rhs.y + lhs.z * rhs.z;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cross Product of two vectors.
|
||||
/// </summary>
|
||||
/// <param name="lhs">The left hand side vector.</param>
|
||||
/// <param name="rhs">The right hand side vector.</param>
|
||||
/// <param name="result">The resulting vector.</param>
|
||||
public static void Cross(ref Vector3d lhs, ref Vector3d rhs, out Vector3d result)
|
||||
{
|
||||
result = new Vector3d(lhs.y * rhs.z - lhs.z * rhs.y, lhs.z * rhs.x - lhs.x * rhs.z, lhs.x * rhs.y - lhs.y * rhs.x);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the angle between two vectors.
|
||||
/// </summary>
|
||||
/// <param name="from">The from vector.</param>
|
||||
/// <param name="to">The to vector.</param>
|
||||
/// <returns>The angle.</returns>
|
||||
public static double Angle(ref Vector3d from, ref Vector3d to)
|
||||
{
|
||||
Vector3d fromNormalized = from.Normalized;
|
||||
Vector3d toNormalized = to.Normalized;
|
||||
return System.Math.Acos(MathHelper.Clamp(Vector3d.Dot(ref fromNormalized, ref toNormalized), -1.0, 1.0)) * MathHelper.Rad2Degd;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs a linear interpolation between two vectors.
|
||||
/// </summary>
|
||||
/// <param name="a">The vector to interpolate from.</param>
|
||||
/// <param name="b">The vector to interpolate to.</param>
|
||||
/// <param name="t">The time fraction.</param>
|
||||
/// <param name="result">The resulting vector.</param>
|
||||
public static void Lerp(ref Vector3d a, ref Vector3d b, double t, out Vector3d result)
|
||||
{
|
||||
result = new Vector3d(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t, a.z + (b.z - a.z) * t);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Multiplies two vectors component-wise.
|
||||
/// </summary>
|
||||
/// <param name="a">The first vector.</param>
|
||||
/// <param name="b">The second vector.</param>
|
||||
/// <param name="result">The resulting vector.</param>
|
||||
public static void Scale(ref Vector3d a, ref Vector3d b, out Vector3d result)
|
||||
{
|
||||
result = new Vector3d(a.x * b.x, a.y * b.y, a.z * b.z);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes a vector.
|
||||
/// </summary>
|
||||
/// <param name="value">The vector to normalize.</param>
|
||||
/// <param name="result">The resulting normalized vector.</param>
|
||||
public static void Normalize(ref Vector3d value, out Vector3d result)
|
||||
{
|
||||
double mag = value.Magnitude;
|
||||
if (mag > Epsilon)
|
||||
{
|
||||
result = new Vector3d(value.x / mag, value.y / mag, value.z / mag);
|
||||
}
|
||||
else
|
||||
{
|
||||
result = Vector3d.zero;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
368
LightlessSync/ThirdParty/MeshDecimator/Math/Vector3i.cs
vendored
Normal file
368
LightlessSync/ThirdParty/MeshDecimator/Math/Vector3i.cs
vendored
Normal file
@@ -0,0 +1,368 @@
|
||||
#region License
|
||||
/*
|
||||
MIT License
|
||||
|
||||
Copyright(c) 2017-2018 Mattias Edlund
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
|
||||
namespace MeshDecimator.Math
|
||||
{
|
||||
/// <summary>
|
||||
/// A 3D integer vector.
|
||||
/// </summary>
|
||||
public struct Vector3i : IEquatable<Vector3i>
|
||||
{
|
||||
#region Static Read-Only
|
||||
/// <summary>
|
||||
/// The zero vector.
|
||||
/// </summary>
|
||||
public static readonly Vector3i zero = new Vector3i(0, 0, 0);
|
||||
#endregion
|
||||
|
||||
#region Fields
|
||||
/// <summary>
|
||||
/// The x component.
|
||||
/// </summary>
|
||||
public int x;
|
||||
/// <summary>
|
||||
/// The y component.
|
||||
/// </summary>
|
||||
public int y;
|
||||
/// <summary>
|
||||
/// The z component.
|
||||
/// </summary>
|
||||
public int z;
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
/// <summary>
|
||||
/// Gets the magnitude of this vector.
|
||||
/// </summary>
|
||||
public int Magnitude
|
||||
{
|
||||
get { return (int)System.Math.Sqrt(x * x + y * y + z * z); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the squared magnitude of this vector.
|
||||
/// </summary>
|
||||
public int MagnitudeSqr
|
||||
{
|
||||
get { return (x * x + y * y + z * z); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a specific component by index in this vector.
|
||||
/// </summary>
|
||||
/// <param name="index">The component index.</param>
|
||||
public int this[int index]
|
||||
{
|
||||
get
|
||||
{
|
||||
switch (index)
|
||||
{
|
||||
case 0:
|
||||
return x;
|
||||
case 1:
|
||||
return y;
|
||||
case 2:
|
||||
return z;
|
||||
default:
|
||||
throw new IndexOutOfRangeException("Invalid Vector3i index!");
|
||||
}
|
||||
}
|
||||
set
|
||||
{
|
||||
switch (index)
|
||||
{
|
||||
case 0:
|
||||
x = value;
|
||||
break;
|
||||
case 1:
|
||||
y = value;
|
||||
break;
|
||||
case 2:
|
||||
z = value;
|
||||
break;
|
||||
default:
|
||||
throw new IndexOutOfRangeException("Invalid Vector3i index!");
|
||||
}
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Constructor
|
||||
/// <summary>
|
||||
/// Creates a new vector with one value for all components.
|
||||
/// </summary>
|
||||
/// <param name="value">The value.</param>
|
||||
public Vector3i(int value)
|
||||
{
|
||||
this.x = value;
|
||||
this.y = value;
|
||||
this.z = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new vector.
|
||||
/// </summary>
|
||||
/// <param name="x">The x value.</param>
|
||||
/// <param name="y">The y value.</param>
|
||||
/// <param name="z">The z value.</param>
|
||||
public Vector3i(int x, int y, int z)
|
||||
{
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Operators
|
||||
/// <summary>
|
||||
/// Adds two vectors.
|
||||
/// </summary>
|
||||
/// <param name="a">The first vector.</param>
|
||||
/// <param name="b">The second vector.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector3i operator +(Vector3i a, Vector3i b)
|
||||
{
|
||||
return new Vector3i(a.x + b.x, a.y + b.y, a.z + b.z);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subtracts two vectors.
|
||||
/// </summary>
|
||||
/// <param name="a">The first vector.</param>
|
||||
/// <param name="b">The second vector.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector3i operator -(Vector3i a, Vector3i b)
|
||||
{
|
||||
return new Vector3i(a.x - b.x, a.y - b.y, a.z - b.z);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scales the vector uniformly.
|
||||
/// </summary>
|
||||
/// <param name="a">The vector.</param>
|
||||
/// <param name="d">The scaling value.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector3i operator *(Vector3i a, int d)
|
||||
{
|
||||
return new Vector3i(a.x * d, a.y * d, a.z * d);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scales the vector uniformly.
|
||||
/// </summary>
|
||||
/// <param name="d">The scaling value.</param>
|
||||
/// <param name="a">The vector.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector3i operator *(int d, Vector3i a)
|
||||
{
|
||||
return new Vector3i(a.x * d, a.y * d, a.z * d);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Divides the vector with a float.
|
||||
/// </summary>
|
||||
/// <param name="a">The vector.</param>
|
||||
/// <param name="d">The dividing float value.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector3i operator /(Vector3i a, int d)
|
||||
{
|
||||
return new Vector3i(a.x / d, a.y / d, a.z / d);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subtracts the vector from a zero vector.
|
||||
/// </summary>
|
||||
/// <param name="a">The vector.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector3i operator -(Vector3i a)
|
||||
{
|
||||
return new Vector3i(-a.x, -a.y, -a.z);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if two vectors equals eachother.
|
||||
/// </summary>
|
||||
/// <param name="lhs">The left hand side vector.</param>
|
||||
/// <param name="rhs">The right hand side vector.</param>
|
||||
/// <returns>If equals.</returns>
|
||||
public static bool operator ==(Vector3i lhs, Vector3i rhs)
|
||||
{
|
||||
return (lhs.x == rhs.x && lhs.y == rhs.y && lhs.z == rhs.z);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if two vectors don't equal eachother.
|
||||
/// </summary>
|
||||
/// <param name="lhs">The left hand side vector.</param>
|
||||
/// <param name="rhs">The right hand side vector.</param>
|
||||
/// <returns>If not equals.</returns>
|
||||
public static bool operator !=(Vector3i lhs, Vector3i rhs)
|
||||
{
|
||||
return (lhs.x != rhs.x || lhs.y != rhs.y || lhs.z != rhs.z);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Explicitly converts from a single-precision vector into an integer vector.
|
||||
/// </summary>
|
||||
/// <param name="v">The single-precision vector.</param>
|
||||
public static implicit operator Vector3i(Vector3 v)
|
||||
{
|
||||
return new Vector3i((int)v.x, (int)v.y, (int)v.z);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Explicitly converts from a double-precision vector into an integer vector.
|
||||
/// </summary>
|
||||
/// <param name="v">The double-precision vector.</param>
|
||||
public static explicit operator Vector3i(Vector3d v)
|
||||
{
|
||||
return new Vector3i((int)v.x, (int)v.y, (int)v.z);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Public Methods
|
||||
#region Instance
|
||||
/// <summary>
|
||||
/// Set x, y and z components of an existing vector.
|
||||
/// </summary>
|
||||
/// <param name="x">The x value.</param>
|
||||
/// <param name="y">The y value.</param>
|
||||
/// <param name="z">The z value.</param>
|
||||
public void Set(int x, int y, int z)
|
||||
{
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Multiplies with another vector component-wise.
|
||||
/// </summary>
|
||||
/// <param name="scale">The vector to multiply with.</param>
|
||||
public void Scale(ref Vector3i scale)
|
||||
{
|
||||
x *= scale.x;
|
||||
y *= scale.y;
|
||||
z *= scale.z;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clamps this vector between a specific range.
|
||||
/// </summary>
|
||||
/// <param name="min">The minimum component value.</param>
|
||||
/// <param name="max">The maximum component value.</param>
|
||||
public void Clamp(int min, int max)
|
||||
{
|
||||
if (x < min) x = min;
|
||||
else if (x > max) x = max;
|
||||
|
||||
if (y < min) y = min;
|
||||
else if (y > max) y = max;
|
||||
|
||||
if (z < min) z = min;
|
||||
else if (z > max) z = max;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Object
|
||||
/// <summary>
|
||||
/// Returns a hash code for this vector.
|
||||
/// </summary>
|
||||
/// <returns>The hash code.</returns>
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return x.GetHashCode() ^ y.GetHashCode() << 2 ^ z.GetHashCode() >> 2;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if this vector is equal to another one.
|
||||
/// </summary>
|
||||
/// <param name="other">The other vector to compare to.</param>
|
||||
/// <returns>If equals.</returns>
|
||||
public override bool Equals(object other)
|
||||
{
|
||||
if (!(other is Vector3i))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
Vector3i vector = (Vector3i)other;
|
||||
return (x == vector.x && y == vector.y && z == vector.z);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if this vector is equal to another one.
|
||||
/// </summary>
|
||||
/// <param name="other">The other vector to compare to.</param>
|
||||
/// <returns>If equals.</returns>
|
||||
public bool Equals(Vector3i other)
|
||||
{
|
||||
return (x == other.x && y == other.y && z == other.z);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a nicely formatted string for this vector.
|
||||
/// </summary>
|
||||
/// <returns>The string.</returns>
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format("({0}, {1}, {2})",
|
||||
x.ToString(CultureInfo.InvariantCulture),
|
||||
y.ToString(CultureInfo.InvariantCulture),
|
||||
z.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a nicely formatted string for this vector.
|
||||
/// </summary>
|
||||
/// <param name="format">The integer format.</param>
|
||||
/// <returns>The string.</returns>
|
||||
public string ToString(string format)
|
||||
{
|
||||
return string.Format("({0}, {1}, {2})",
|
||||
x.ToString(format, CultureInfo.InvariantCulture),
|
||||
y.ToString(format, CultureInfo.InvariantCulture),
|
||||
z.ToString(format, CultureInfo.InvariantCulture));
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Static
|
||||
/// <summary>
|
||||
/// Multiplies two vectors component-wise.
|
||||
/// </summary>
|
||||
/// <param name="a">The first vector.</param>
|
||||
/// <param name="b">The second vector.</param>
|
||||
/// <param name="result">The resulting vector.</param>
|
||||
public static void Scale(ref Vector3i a, ref Vector3i b, out Vector3i result)
|
||||
{
|
||||
result = new Vector3i(a.x * b.x, a.y * b.y, a.z * b.z);
|
||||
}
|
||||
#endregion
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
467
LightlessSync/ThirdParty/MeshDecimator/Math/Vector4.cs
vendored
Normal file
467
LightlessSync/ThirdParty/MeshDecimator/Math/Vector4.cs
vendored
Normal file
@@ -0,0 +1,467 @@
|
||||
#region License
|
||||
/*
|
||||
MIT License
|
||||
|
||||
Copyright(c) 2017-2018 Mattias Edlund
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
|
||||
namespace MeshDecimator.Math
|
||||
{
|
||||
/// <summary>
|
||||
/// A single precision 4D vector.
|
||||
/// </summary>
|
||||
public struct Vector4 : IEquatable<Vector4>
|
||||
{
|
||||
#region Static Read-Only
|
||||
/// <summary>
|
||||
/// The zero vector.
|
||||
/// </summary>
|
||||
public static readonly Vector4 zero = new Vector4(0, 0, 0, 0);
|
||||
#endregion
|
||||
|
||||
#region Consts
|
||||
/// <summary>
|
||||
/// The vector epsilon.
|
||||
/// </summary>
|
||||
public const float Epsilon = 9.99999944E-11f;
|
||||
#endregion
|
||||
|
||||
#region Fields
|
||||
/// <summary>
|
||||
/// The x component.
|
||||
/// </summary>
|
||||
public float x;
|
||||
/// <summary>
|
||||
/// The y component.
|
||||
/// </summary>
|
||||
public float y;
|
||||
/// <summary>
|
||||
/// The z component.
|
||||
/// </summary>
|
||||
public float z;
|
||||
/// <summary>
|
||||
/// The w component.
|
||||
/// </summary>
|
||||
public float w;
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
/// <summary>
|
||||
/// Gets the magnitude of this vector.
|
||||
/// </summary>
|
||||
public float Magnitude
|
||||
{
|
||||
get { return (float)System.Math.Sqrt(x * x + y * y + z * z + w * w); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the squared magnitude of this vector.
|
||||
/// </summary>
|
||||
public float MagnitudeSqr
|
||||
{
|
||||
get { return (x * x + y * y + z * z + w * w); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a normalized vector from this vector.
|
||||
/// </summary>
|
||||
public Vector4 Normalized
|
||||
{
|
||||
get
|
||||
{
|
||||
Vector4 result;
|
||||
Normalize(ref this, out result);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a specific component by index in this vector.
|
||||
/// </summary>
|
||||
/// <param name="index">The component index.</param>
|
||||
public float this[int index]
|
||||
{
|
||||
get
|
||||
{
|
||||
switch (index)
|
||||
{
|
||||
case 0:
|
||||
return x;
|
||||
case 1:
|
||||
return y;
|
||||
case 2:
|
||||
return z;
|
||||
case 3:
|
||||
return w;
|
||||
default:
|
||||
throw new IndexOutOfRangeException("Invalid Vector4 index!");
|
||||
}
|
||||
}
|
||||
set
|
||||
{
|
||||
switch (index)
|
||||
{
|
||||
case 0:
|
||||
x = value;
|
||||
break;
|
||||
case 1:
|
||||
y = value;
|
||||
break;
|
||||
case 2:
|
||||
z = value;
|
||||
break;
|
||||
case 3:
|
||||
w = value;
|
||||
break;
|
||||
default:
|
||||
throw new IndexOutOfRangeException("Invalid Vector4 index!");
|
||||
}
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Constructor
|
||||
/// <summary>
|
||||
/// Creates a new vector with one value for all components.
|
||||
/// </summary>
|
||||
/// <param name="value">The value.</param>
|
||||
public Vector4(float value)
|
||||
{
|
||||
this.x = value;
|
||||
this.y = value;
|
||||
this.z = value;
|
||||
this.w = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new vector.
|
||||
/// </summary>
|
||||
/// <param name="x">The x value.</param>
|
||||
/// <param name="y">The y value.</param>
|
||||
/// <param name="z">The z value.</param>
|
||||
/// <param name="w">The w value.</param>
|
||||
public Vector4(float x, float y, float z, float w)
|
||||
{
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
this.w = w;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Operators
|
||||
/// <summary>
|
||||
/// Adds two vectors.
|
||||
/// </summary>
|
||||
/// <param name="a">The first vector.</param>
|
||||
/// <param name="b">The second vector.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector4 operator +(Vector4 a, Vector4 b)
|
||||
{
|
||||
return new Vector4(a.x + b.x, a.y + b.y, a.z + b.z, a.w + b.w);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subtracts two vectors.
|
||||
/// </summary>
|
||||
/// <param name="a">The first vector.</param>
|
||||
/// <param name="b">The second vector.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector4 operator -(Vector4 a, Vector4 b)
|
||||
{
|
||||
return new Vector4(a.x - b.x, a.y - b.y, a.z - b.z, a.w - b.w);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scales the vector uniformly.
|
||||
/// </summary>
|
||||
/// <param name="a">The vector.</param>
|
||||
/// <param name="d">The scaling value.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector4 operator *(Vector4 a, float d)
|
||||
{
|
||||
return new Vector4(a.x * d, a.y * d, a.z * d, a.w * d);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scales the vector uniformly.
|
||||
/// </summary>
|
||||
/// <param name="d">The scaling value.</param>
|
||||
/// <param name="a">The vector.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector4 operator *(float d, Vector4 a)
|
||||
{
|
||||
return new Vector4(a.x * d, a.y * d, a.z * d, a.w * d);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Divides the vector with a float.
|
||||
/// </summary>
|
||||
/// <param name="a">The vector.</param>
|
||||
/// <param name="d">The dividing float value.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector4 operator /(Vector4 a, float d)
|
||||
{
|
||||
return new Vector4(a.x / d, a.y / d, a.z / d, a.w / d);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subtracts the vector from a zero vector.
|
||||
/// </summary>
|
||||
/// <param name="a">The vector.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector4 operator -(Vector4 a)
|
||||
{
|
||||
return new Vector4(-a.x, -a.y, -a.z, -a.w);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if two vectors equals eachother.
|
||||
/// </summary>
|
||||
/// <param name="lhs">The left hand side vector.</param>
|
||||
/// <param name="rhs">The right hand side vector.</param>
|
||||
/// <returns>If equals.</returns>
|
||||
public static bool operator ==(Vector4 lhs, Vector4 rhs)
|
||||
{
|
||||
return (lhs - rhs).MagnitudeSqr < Epsilon;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if two vectors don't equal eachother.
|
||||
/// </summary>
|
||||
/// <param name="lhs">The left hand side vector.</param>
|
||||
/// <param name="rhs">The right hand side vector.</param>
|
||||
/// <returns>If not equals.</returns>
|
||||
public static bool operator !=(Vector4 lhs, Vector4 rhs)
|
||||
{
|
||||
return (lhs - rhs).MagnitudeSqr >= Epsilon;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Explicitly converts from a double-precision vector into a single-precision vector.
|
||||
/// </summary>
|
||||
/// <param name="v">The double-precision vector.</param>
|
||||
public static explicit operator Vector4(Vector4d v)
|
||||
{
|
||||
return new Vector4((float)v.x, (float)v.y, (float)v.z, (float)v.w);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implicitly converts from an integer vector into a single-precision vector.
|
||||
/// </summary>
|
||||
/// <param name="v">The integer vector.</param>
|
||||
public static implicit operator Vector4(Vector4i v)
|
||||
{
|
||||
return new Vector4(v.x, v.y, v.z, v.w);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Public Methods
|
||||
#region Instance
|
||||
/// <summary>
|
||||
/// Set x, y and z components of an existing vector.
|
||||
/// </summary>
|
||||
/// <param name="x">The x value.</param>
|
||||
/// <param name="y">The y value.</param>
|
||||
/// <param name="z">The z value.</param>
|
||||
/// <param name="w">The w value.</param>
|
||||
public void Set(float x, float y, float z, float w)
|
||||
{
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
this.w = w;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Multiplies with another vector component-wise.
|
||||
/// </summary>
|
||||
/// <param name="scale">The vector to multiply with.</param>
|
||||
public void Scale(ref Vector4 scale)
|
||||
{
|
||||
x *= scale.x;
|
||||
y *= scale.y;
|
||||
z *= scale.z;
|
||||
w *= scale.w;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes this vector.
|
||||
/// </summary>
|
||||
public void Normalize()
|
||||
{
|
||||
float mag = this.Magnitude;
|
||||
if (mag > Epsilon)
|
||||
{
|
||||
x /= mag;
|
||||
y /= mag;
|
||||
z /= mag;
|
||||
w /= mag;
|
||||
}
|
||||
else
|
||||
{
|
||||
x = y = z = w = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clamps this vector between a specific range.
|
||||
/// </summary>
|
||||
/// <param name="min">The minimum component value.</param>
|
||||
/// <param name="max">The maximum component value.</param>
|
||||
public void Clamp(float min, float max)
|
||||
{
|
||||
if (x < min) x = min;
|
||||
else if (x > max) x = max;
|
||||
|
||||
if (y < min) y = min;
|
||||
else if (y > max) y = max;
|
||||
|
||||
if (z < min) z = min;
|
||||
else if (z > max) z = max;
|
||||
|
||||
if (w < min) w = min;
|
||||
else if (w > max) w = max;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Object
|
||||
/// <summary>
|
||||
/// Returns a hash code for this vector.
|
||||
/// </summary>
|
||||
/// <returns>The hash code.</returns>
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return x.GetHashCode() ^ y.GetHashCode() << 2 ^ z.GetHashCode() >> 2 ^ w.GetHashCode() >> 1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if this vector is equal to another one.
|
||||
/// </summary>
|
||||
/// <param name="other">The other vector to compare to.</param>
|
||||
/// <returns>If equals.</returns>
|
||||
public override bool Equals(object other)
|
||||
{
|
||||
if (!(other is Vector4))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
Vector4 vector = (Vector4)other;
|
||||
return (x == vector.x && y == vector.y && z == vector.z && w == vector.w);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if this vector is equal to another one.
|
||||
/// </summary>
|
||||
/// <param name="other">The other vector to compare to.</param>
|
||||
/// <returns>If equals.</returns>
|
||||
public bool Equals(Vector4 other)
|
||||
{
|
||||
return (x == other.x && y == other.y && z == other.z && w == other.w);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a nicely formatted string for this vector.
|
||||
/// </summary>
|
||||
/// <returns>The string.</returns>
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format("({0}, {1}, {2}, {3})",
|
||||
x.ToString("F1", CultureInfo.InvariantCulture),
|
||||
y.ToString("F1", CultureInfo.InvariantCulture),
|
||||
z.ToString("F1", CultureInfo.InvariantCulture),
|
||||
w.ToString("F1", CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a nicely formatted string for this vector.
|
||||
/// </summary>
|
||||
/// <param name="format">The float format.</param>
|
||||
/// <returns>The string.</returns>
|
||||
public string ToString(string format)
|
||||
{
|
||||
return string.Format("({0}, {1}, {2}, {3})",
|
||||
x.ToString(format, CultureInfo.InvariantCulture),
|
||||
y.ToString(format, CultureInfo.InvariantCulture),
|
||||
z.ToString(format, CultureInfo.InvariantCulture),
|
||||
w.ToString(format, CultureInfo.InvariantCulture));
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Static
|
||||
/// <summary>
|
||||
/// Dot Product of two vectors.
|
||||
/// </summary>
|
||||
/// <param name="lhs">The left hand side vector.</param>
|
||||
/// <param name="rhs">The right hand side vector.</param>
|
||||
public static float Dot(ref Vector4 lhs, ref Vector4 rhs)
|
||||
{
|
||||
return lhs.x * rhs.x + lhs.y * rhs.y + lhs.z * rhs.z + lhs.w * rhs.w;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs a linear interpolation between two vectors.
|
||||
/// </summary>
|
||||
/// <param name="a">The vector to interpolate from.</param>
|
||||
/// <param name="b">The vector to interpolate to.</param>
|
||||
/// <param name="t">The time fraction.</param>
|
||||
/// <param name="result">The resulting vector.</param>
|
||||
public static void Lerp(ref Vector4 a, ref Vector4 b, float t, out Vector4 result)
|
||||
{
|
||||
result = new Vector4(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t, a.z + (b.z - a.z) * t, a.w + (b.w - a.w) * t);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Multiplies two vectors component-wise.
|
||||
/// </summary>
|
||||
/// <param name="a">The first vector.</param>
|
||||
/// <param name="b">The second vector.</param>
|
||||
/// <param name="result">The resulting vector.</param>
|
||||
public static void Scale(ref Vector4 a, ref Vector4 b, out Vector4 result)
|
||||
{
|
||||
result = new Vector4(a.x * b.x, a.y * b.y, a.z * b.z, a.w * b.w);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes a vector.
|
||||
/// </summary>
|
||||
/// <param name="value">The vector to normalize.</param>
|
||||
/// <param name="result">The resulting normalized vector.</param>
|
||||
public static void Normalize(ref Vector4 value, out Vector4 result)
|
||||
{
|
||||
float mag = value.Magnitude;
|
||||
if (mag > Epsilon)
|
||||
{
|
||||
result = new Vector4(value.x / mag, value.y / mag, value.z / mag, value.w / mag);
|
||||
}
|
||||
else
|
||||
{
|
||||
result = Vector4.zero;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
467
LightlessSync/ThirdParty/MeshDecimator/Math/Vector4d.cs
vendored
Normal file
467
LightlessSync/ThirdParty/MeshDecimator/Math/Vector4d.cs
vendored
Normal file
@@ -0,0 +1,467 @@
|
||||
#region License
|
||||
/*
|
||||
MIT License
|
||||
|
||||
Copyright(c) 2017-2018 Mattias Edlund
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
|
||||
namespace MeshDecimator.Math
|
||||
{
|
||||
/// <summary>
|
||||
/// A double precision 4D vector.
|
||||
/// </summary>
|
||||
public struct Vector4d : IEquatable<Vector4d>
|
||||
{
|
||||
#region Static Read-Only
|
||||
/// <summary>
|
||||
/// The zero vector.
|
||||
/// </summary>
|
||||
public static readonly Vector4d zero = new Vector4d(0, 0, 0, 0);
|
||||
#endregion
|
||||
|
||||
#region Consts
|
||||
/// <summary>
|
||||
/// The vector epsilon.
|
||||
/// </summary>
|
||||
public const double Epsilon = double.Epsilon;
|
||||
#endregion
|
||||
|
||||
#region Fields
|
||||
/// <summary>
|
||||
/// The x component.
|
||||
/// </summary>
|
||||
public double x;
|
||||
/// <summary>
|
||||
/// The y component.
|
||||
/// </summary>
|
||||
public double y;
|
||||
/// <summary>
|
||||
/// The z component.
|
||||
/// </summary>
|
||||
public double z;
|
||||
/// <summary>
|
||||
/// The w component.
|
||||
/// </summary>
|
||||
public double w;
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
/// <summary>
|
||||
/// Gets the magnitude of this vector.
|
||||
/// </summary>
|
||||
public double Magnitude
|
||||
{
|
||||
get { return System.Math.Sqrt(x * x + y * y + z * z + w * w); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the squared magnitude of this vector.
|
||||
/// </summary>
|
||||
public double MagnitudeSqr
|
||||
{
|
||||
get { return (x * x + y * y + z * z + w * w); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a normalized vector from this vector.
|
||||
/// </summary>
|
||||
public Vector4d Normalized
|
||||
{
|
||||
get
|
||||
{
|
||||
Vector4d result;
|
||||
Normalize(ref this, out result);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a specific component by index in this vector.
|
||||
/// </summary>
|
||||
/// <param name="index">The component index.</param>
|
||||
public double this[int index]
|
||||
{
|
||||
get
|
||||
{
|
||||
switch (index)
|
||||
{
|
||||
case 0:
|
||||
return x;
|
||||
case 1:
|
||||
return y;
|
||||
case 2:
|
||||
return z;
|
||||
case 3:
|
||||
return w;
|
||||
default:
|
||||
throw new IndexOutOfRangeException("Invalid Vector4d index!");
|
||||
}
|
||||
}
|
||||
set
|
||||
{
|
||||
switch (index)
|
||||
{
|
||||
case 0:
|
||||
x = value;
|
||||
break;
|
||||
case 1:
|
||||
y = value;
|
||||
break;
|
||||
case 2:
|
||||
z = value;
|
||||
break;
|
||||
case 3:
|
||||
w = value;
|
||||
break;
|
||||
default:
|
||||
throw new IndexOutOfRangeException("Invalid Vector4d index!");
|
||||
}
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Constructor
|
||||
/// <summary>
|
||||
/// Creates a new vector with one value for all components.
|
||||
/// </summary>
|
||||
/// <param name="value">The value.</param>
|
||||
public Vector4d(double value)
|
||||
{
|
||||
this.x = value;
|
||||
this.y = value;
|
||||
this.z = value;
|
||||
this.w = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new vector.
|
||||
/// </summary>
|
||||
/// <param name="x">The x value.</param>
|
||||
/// <param name="y">The y value.</param>
|
||||
/// <param name="z">The z value.</param>
|
||||
/// <param name="w">The w value.</param>
|
||||
public Vector4d(double x, double y, double z, double w)
|
||||
{
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
this.w = w;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Operators
|
||||
/// <summary>
|
||||
/// Adds two vectors.
|
||||
/// </summary>
|
||||
/// <param name="a">The first vector.</param>
|
||||
/// <param name="b">The second vector.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector4d operator +(Vector4d a, Vector4d b)
|
||||
{
|
||||
return new Vector4d(a.x + b.x, a.y + b.y, a.z + b.z, a.w + b.w);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subtracts two vectors.
|
||||
/// </summary>
|
||||
/// <param name="a">The first vector.</param>
|
||||
/// <param name="b">The second vector.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector4d operator -(Vector4d a, Vector4d b)
|
||||
{
|
||||
return new Vector4d(a.x - b.x, a.y - b.y, a.z - b.z, a.w - b.w);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scales the vector uniformly.
|
||||
/// </summary>
|
||||
/// <param name="a">The vector.</param>
|
||||
/// <param name="d">The scaling value.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector4d operator *(Vector4d a, double d)
|
||||
{
|
||||
return new Vector4d(a.x * d, a.y * d, a.z * d, a.w * d);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scales the vector uniformly.
|
||||
/// </summary>
|
||||
/// <param name="d">The scaling value.</param>
|
||||
/// <param name="a">The vector.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector4d operator *(double d, Vector4d a)
|
||||
{
|
||||
return new Vector4d(a.x * d, a.y * d, a.z * d, a.w * d);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Divides the vector with a float.
|
||||
/// </summary>
|
||||
/// <param name="a">The vector.</param>
|
||||
/// <param name="d">The dividing float value.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector4d operator /(Vector4d a, double d)
|
||||
{
|
||||
return new Vector4d(a.x / d, a.y / d, a.z / d, a.w / d);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subtracts the vector from a zero vector.
|
||||
/// </summary>
|
||||
/// <param name="a">The vector.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector4d operator -(Vector4d a)
|
||||
{
|
||||
return new Vector4d(-a.x, -a.y, -a.z, -a.w);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if two vectors equals eachother.
|
||||
/// </summary>
|
||||
/// <param name="lhs">The left hand side vector.</param>
|
||||
/// <param name="rhs">The right hand side vector.</param>
|
||||
/// <returns>If equals.</returns>
|
||||
public static bool operator ==(Vector4d lhs, Vector4d rhs)
|
||||
{
|
||||
return (lhs - rhs).MagnitudeSqr < Epsilon;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if two vectors don't equal eachother.
|
||||
/// </summary>
|
||||
/// <param name="lhs">The left hand side vector.</param>
|
||||
/// <param name="rhs">The right hand side vector.</param>
|
||||
/// <returns>If not equals.</returns>
|
||||
public static bool operator !=(Vector4d lhs, Vector4d rhs)
|
||||
{
|
||||
return (lhs - rhs).MagnitudeSqr >= Epsilon;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implicitly converts from a single-precision vector into a double-precision vector.
|
||||
/// </summary>
|
||||
/// <param name="v">The single-precision vector.</param>
|
||||
public static implicit operator Vector4d(Vector4 v)
|
||||
{
|
||||
return new Vector4d(v.x, v.y, v.z, v.w);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implicitly converts from an integer vector into a double-precision vector.
|
||||
/// </summary>
|
||||
/// <param name="v">The integer vector.</param>
|
||||
public static implicit operator Vector4d(Vector4i v)
|
||||
{
|
||||
return new Vector4d(v.x, v.y, v.z, v.w);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Public Methods
|
||||
#region Instance
|
||||
/// <summary>
|
||||
/// Set x, y and z components of an existing vector.
|
||||
/// </summary>
|
||||
/// <param name="x">The x value.</param>
|
||||
/// <param name="y">The y value.</param>
|
||||
/// <param name="z">The z value.</param>
|
||||
/// <param name="w">The w value.</param>
|
||||
public void Set(double x, double y, double z, double w)
|
||||
{
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
this.w = w;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Multiplies with another vector component-wise.
|
||||
/// </summary>
|
||||
/// <param name="scale">The vector to multiply with.</param>
|
||||
public void Scale(ref Vector4d scale)
|
||||
{
|
||||
x *= scale.x;
|
||||
y *= scale.y;
|
||||
z *= scale.z;
|
||||
w *= scale.w;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes this vector.
|
||||
/// </summary>
|
||||
public void Normalize()
|
||||
{
|
||||
double mag = this.Magnitude;
|
||||
if (mag > Epsilon)
|
||||
{
|
||||
x /= mag;
|
||||
y /= mag;
|
||||
z /= mag;
|
||||
w /= mag;
|
||||
}
|
||||
else
|
||||
{
|
||||
x = y = z = w = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clamps this vector between a specific range.
|
||||
/// </summary>
|
||||
/// <param name="min">The minimum component value.</param>
|
||||
/// <param name="max">The maximum component value.</param>
|
||||
public void Clamp(double min, double max)
|
||||
{
|
||||
if (x < min) x = min;
|
||||
else if (x > max) x = max;
|
||||
|
||||
if (y < min) y = min;
|
||||
else if (y > max) y = max;
|
||||
|
||||
if (z < min) z = min;
|
||||
else if (z > max) z = max;
|
||||
|
||||
if (w < min) w = min;
|
||||
else if (w > max) w = max;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Object
|
||||
/// <summary>
|
||||
/// Returns a hash code for this vector.
|
||||
/// </summary>
|
||||
/// <returns>The hash code.</returns>
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return x.GetHashCode() ^ y.GetHashCode() << 2 ^ z.GetHashCode() >> 2 ^ w.GetHashCode() >> 1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if this vector is equal to another one.
|
||||
/// </summary>
|
||||
/// <param name="other">The other vector to compare to.</param>
|
||||
/// <returns>If equals.</returns>
|
||||
public override bool Equals(object other)
|
||||
{
|
||||
if (!(other is Vector4d))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
Vector4d vector = (Vector4d)other;
|
||||
return (x == vector.x && y == vector.y && z == vector.z && w == vector.w);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if this vector is equal to another one.
|
||||
/// </summary>
|
||||
/// <param name="other">The other vector to compare to.</param>
|
||||
/// <returns>If equals.</returns>
|
||||
public bool Equals(Vector4d other)
|
||||
{
|
||||
return (x == other.x && y == other.y && z == other.z && w == other.w);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a nicely formatted string for this vector.
|
||||
/// </summary>
|
||||
/// <returns>The string.</returns>
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format("({0}, {1}, {2}, {3})",
|
||||
x.ToString("F1", CultureInfo.InvariantCulture),
|
||||
y.ToString("F1", CultureInfo.InvariantCulture),
|
||||
z.ToString("F1", CultureInfo.InvariantCulture),
|
||||
w.ToString("F1", CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a nicely formatted string for this vector.
|
||||
/// </summary>
|
||||
/// <param name="format">The float format.</param>
|
||||
/// <returns>The string.</returns>
|
||||
public string ToString(string format)
|
||||
{
|
||||
return string.Format("({0}, {1}, {2}, {3})",
|
||||
x.ToString(format, CultureInfo.InvariantCulture),
|
||||
y.ToString(format, CultureInfo.InvariantCulture),
|
||||
z.ToString(format, CultureInfo.InvariantCulture),
|
||||
w.ToString(format, CultureInfo.InvariantCulture));
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Static
|
||||
/// <summary>
|
||||
/// Dot Product of two vectors.
|
||||
/// </summary>
|
||||
/// <param name="lhs">The left hand side vector.</param>
|
||||
/// <param name="rhs">The right hand side vector.</param>
|
||||
public static double Dot(ref Vector4d lhs, ref Vector4d rhs)
|
||||
{
|
||||
return lhs.x * rhs.x + lhs.y * rhs.y + lhs.z * rhs.z + lhs.w * rhs.w;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs a linear interpolation between two vectors.
|
||||
/// </summary>
|
||||
/// <param name="a">The vector to interpolate from.</param>
|
||||
/// <param name="b">The vector to interpolate to.</param>
|
||||
/// <param name="t">The time fraction.</param>
|
||||
/// <param name="result">The resulting vector.</param>
|
||||
public static void Lerp(ref Vector4d a, ref Vector4d b, double t, out Vector4d result)
|
||||
{
|
||||
result = new Vector4d(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t, a.z + (b.z - a.z) * t, a.w + (b.w - a.w) * t);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Multiplies two vectors component-wise.
|
||||
/// </summary>
|
||||
/// <param name="a">The first vector.</param>
|
||||
/// <param name="b">The second vector.</param>
|
||||
/// <param name="result">The resulting vector.</param>
|
||||
public static void Scale(ref Vector4d a, ref Vector4d b, out Vector4d result)
|
||||
{
|
||||
result = new Vector4d(a.x * b.x, a.y * b.y, a.z * b.z, a.w * b.w);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes a vector.
|
||||
/// </summary>
|
||||
/// <param name="value">The vector to normalize.</param>
|
||||
/// <param name="result">The resulting normalized vector.</param>
|
||||
public static void Normalize(ref Vector4d value, out Vector4d result)
|
||||
{
|
||||
double mag = value.Magnitude;
|
||||
if (mag > Epsilon)
|
||||
{
|
||||
result = new Vector4d(value.x / mag, value.y / mag, value.z / mag, value.w / mag);
|
||||
}
|
||||
else
|
||||
{
|
||||
result = Vector4d.zero;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
388
LightlessSync/ThirdParty/MeshDecimator/Math/Vector4i.cs
vendored
Normal file
388
LightlessSync/ThirdParty/MeshDecimator/Math/Vector4i.cs
vendored
Normal file
@@ -0,0 +1,388 @@
|
||||
#region License
|
||||
/*
|
||||
MIT License
|
||||
|
||||
Copyright(c) 2017-2018 Mattias Edlund
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
|
||||
namespace MeshDecimator.Math
|
||||
{
|
||||
/// <summary>
|
||||
/// A 4D integer vector.
|
||||
/// </summary>
|
||||
public struct Vector4i : IEquatable<Vector4i>
|
||||
{
|
||||
#region Static Read-Only
|
||||
/// <summary>
|
||||
/// The zero vector.
|
||||
/// </summary>
|
||||
public static readonly Vector4i zero = new Vector4i(0, 0, 0, 0);
|
||||
#endregion
|
||||
|
||||
#region Fields
|
||||
/// <summary>
|
||||
/// The x component.
|
||||
/// </summary>
|
||||
public int x;
|
||||
/// <summary>
|
||||
/// The y component.
|
||||
/// </summary>
|
||||
public int y;
|
||||
/// <summary>
|
||||
/// The z component.
|
||||
/// </summary>
|
||||
public int z;
|
||||
/// <summary>
|
||||
/// The w component.
|
||||
/// </summary>
|
||||
public int w;
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
/// <summary>
|
||||
/// Gets the magnitude of this vector.
|
||||
/// </summary>
|
||||
public int Magnitude
|
||||
{
|
||||
get { return (int)System.Math.Sqrt(x * x + y * y + z * z + w * w); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the squared magnitude of this vector.
|
||||
/// </summary>
|
||||
public int MagnitudeSqr
|
||||
{
|
||||
get { return (x * x + y * y + z * z + w * w); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a specific component by index in this vector.
|
||||
/// </summary>
|
||||
/// <param name="index">The component index.</param>
|
||||
public int this[int index]
|
||||
{
|
||||
get
|
||||
{
|
||||
switch (index)
|
||||
{
|
||||
case 0:
|
||||
return x;
|
||||
case 1:
|
||||
return y;
|
||||
case 2:
|
||||
return z;
|
||||
case 3:
|
||||
return w;
|
||||
default:
|
||||
throw new IndexOutOfRangeException("Invalid Vector4i index!");
|
||||
}
|
||||
}
|
||||
set
|
||||
{
|
||||
switch (index)
|
||||
{
|
||||
case 0:
|
||||
x = value;
|
||||
break;
|
||||
case 1:
|
||||
y = value;
|
||||
break;
|
||||
case 2:
|
||||
z = value;
|
||||
break;
|
||||
case 3:
|
||||
w = value;
|
||||
break;
|
||||
default:
|
||||
throw new IndexOutOfRangeException("Invalid Vector4i index!");
|
||||
}
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Constructor
|
||||
/// <summary>
|
||||
/// Creates a new vector with one value for all components.
|
||||
/// </summary>
|
||||
/// <param name="value">The value.</param>
|
||||
public Vector4i(int value)
|
||||
{
|
||||
this.x = value;
|
||||
this.y = value;
|
||||
this.z = value;
|
||||
this.w = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new vector.
|
||||
/// </summary>
|
||||
/// <param name="x">The x value.</param>
|
||||
/// <param name="y">The y value.</param>
|
||||
/// <param name="z">The z value.</param>
|
||||
/// <param name="w">The w value.</param>
|
||||
public Vector4i(int x, int y, int z, int w)
|
||||
{
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
this.w = w;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Operators
|
||||
/// <summary>
|
||||
/// Adds two vectors.
|
||||
/// </summary>
|
||||
/// <param name="a">The first vector.</param>
|
||||
/// <param name="b">The second vector.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector4i operator +(Vector4i a, Vector4i b)
|
||||
{
|
||||
return new Vector4i(a.x + b.x, a.y + b.y, a.z + b.z, a.w + b.w);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subtracts two vectors.
|
||||
/// </summary>
|
||||
/// <param name="a">The first vector.</param>
|
||||
/// <param name="b">The second vector.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector4i operator -(Vector4i a, Vector4i b)
|
||||
{
|
||||
return new Vector4i(a.x - b.x, a.y - b.y, a.z - b.z, a.w - b.w);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scales the vector uniformly.
|
||||
/// </summary>
|
||||
/// <param name="a">The vector.</param>
|
||||
/// <param name="d">The scaling value.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector4i operator *(Vector4i a, int d)
|
||||
{
|
||||
return new Vector4i(a.x * d, a.y * d, a.z * d, a.w * d);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scales the vector uniformly.
|
||||
/// </summary>
|
||||
/// <param name="d">The scaling value.</param>
|
||||
/// <param name="a">The vector.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector4i operator *(int d, Vector4i a)
|
||||
{
|
||||
return new Vector4i(a.x * d, a.y * d, a.z * d, a.w * d);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Divides the vector with a float.
|
||||
/// </summary>
|
||||
/// <param name="a">The vector.</param>
|
||||
/// <param name="d">The dividing float value.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector4i operator /(Vector4i a, int d)
|
||||
{
|
||||
return new Vector4i(a.x / d, a.y / d, a.z / d, a.w / d);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subtracts the vector from a zero vector.
|
||||
/// </summary>
|
||||
/// <param name="a">The vector.</param>
|
||||
/// <returns>The resulting vector.</returns>
|
||||
public static Vector4i operator -(Vector4i a)
|
||||
{
|
||||
return new Vector4i(-a.x, -a.y, -a.z, -a.w);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if two vectors equals eachother.
|
||||
/// </summary>
|
||||
/// <param name="lhs">The left hand side vector.</param>
|
||||
/// <param name="rhs">The right hand side vector.</param>
|
||||
/// <returns>If equals.</returns>
|
||||
public static bool operator ==(Vector4i lhs, Vector4i rhs)
|
||||
{
|
||||
return (lhs.x == rhs.x && lhs.y == rhs.y && lhs.z == rhs.z && lhs.w == rhs.w);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if two vectors don't equal eachother.
|
||||
/// </summary>
|
||||
/// <param name="lhs">The left hand side vector.</param>
|
||||
/// <param name="rhs">The right hand side vector.</param>
|
||||
/// <returns>If not equals.</returns>
|
||||
public static bool operator !=(Vector4i lhs, Vector4i rhs)
|
||||
{
|
||||
return (lhs.x != rhs.x || lhs.y != rhs.y || lhs.z != rhs.z || lhs.w != rhs.w);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Explicitly converts from a single-precision vector into an integer vector.
|
||||
/// </summary>
|
||||
/// <param name="v">The single-precision vector.</param>
|
||||
public static explicit operator Vector4i(Vector4 v)
|
||||
{
|
||||
return new Vector4i((int)v.x, (int)v.y, (int)v.z, (int)v.w);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Explicitly converts from a double-precision vector into an integer vector.
|
||||
/// </summary>
|
||||
/// <param name="v">The double-precision vector.</param>
|
||||
public static explicit operator Vector4i(Vector4d v)
|
||||
{
|
||||
return new Vector4i((int)v.x, (int)v.y, (int)v.z, (int)v.w);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Public Methods
|
||||
#region Instance
|
||||
/// <summary>
|
||||
/// Set x, y and z components of an existing vector.
|
||||
/// </summary>
|
||||
/// <param name="x">The x value.</param>
|
||||
/// <param name="y">The y value.</param>
|
||||
/// <param name="z">The z value.</param>
|
||||
/// <param name="w">The w value.</param>
|
||||
public void Set(int x, int y, int z, int w)
|
||||
{
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
this.w = w;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Multiplies with another vector component-wise.
|
||||
/// </summary>
|
||||
/// <param name="scale">The vector to multiply with.</param>
|
||||
public void Scale(ref Vector4i scale)
|
||||
{
|
||||
x *= scale.x;
|
||||
y *= scale.y;
|
||||
z *= scale.z;
|
||||
w *= scale.w;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clamps this vector between a specific range.
|
||||
/// </summary>
|
||||
/// <param name="min">The minimum component value.</param>
|
||||
/// <param name="max">The maximum component value.</param>
|
||||
public void Clamp(int min, int max)
|
||||
{
|
||||
if (x < min) x = min;
|
||||
else if (x > max) x = max;
|
||||
|
||||
if (y < min) y = min;
|
||||
else if (y > max) y = max;
|
||||
|
||||
if (z < min) z = min;
|
||||
else if (z > max) z = max;
|
||||
|
||||
if (w < min) w = min;
|
||||
else if (w > max) w = max;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Object
|
||||
/// <summary>
|
||||
/// Returns a hash code for this vector.
|
||||
/// </summary>
|
||||
/// <returns>The hash code.</returns>
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return x.GetHashCode() ^ y.GetHashCode() << 2 ^ z.GetHashCode() >> 2 ^ w.GetHashCode() >> 1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if this vector is equal to another one.
|
||||
/// </summary>
|
||||
/// <param name="other">The other vector to compare to.</param>
|
||||
/// <returns>If equals.</returns>
|
||||
public override bool Equals(object other)
|
||||
{
|
||||
if (!(other is Vector4i))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
Vector4i vector = (Vector4i)other;
|
||||
return (x == vector.x && y == vector.y && z == vector.z && w == vector.w);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if this vector is equal to another one.
|
||||
/// </summary>
|
||||
/// <param name="other">The other vector to compare to.</param>
|
||||
/// <returns>If equals.</returns>
|
||||
public bool Equals(Vector4i other)
|
||||
{
|
||||
return (x == other.x && y == other.y && z == other.z && w == other.w);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a nicely formatted string for this vector.
|
||||
/// </summary>
|
||||
/// <returns>The string.</returns>
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format("({0}, {1}, {2}, {3})",
|
||||
x.ToString(CultureInfo.InvariantCulture),
|
||||
y.ToString(CultureInfo.InvariantCulture),
|
||||
z.ToString(CultureInfo.InvariantCulture),
|
||||
w.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a nicely formatted string for this vector.
|
||||
/// </summary>
|
||||
/// <param name="format">The integer format.</param>
|
||||
/// <returns>The string.</returns>
|
||||
public string ToString(string format)
|
||||
{
|
||||
return string.Format("({0}, {1}, {2}, {3})",
|
||||
x.ToString(format, CultureInfo.InvariantCulture),
|
||||
y.ToString(format, CultureInfo.InvariantCulture),
|
||||
z.ToString(format, CultureInfo.InvariantCulture),
|
||||
w.ToString(format, CultureInfo.InvariantCulture));
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Static
|
||||
/// <summary>
|
||||
/// Multiplies two vectors component-wise.
|
||||
/// </summary>
|
||||
/// <param name="a">The first vector.</param>
|
||||
/// <param name="b">The second vector.</param>
|
||||
/// <param name="result">The resulting vector.</param>
|
||||
public static void Scale(ref Vector4i a, ref Vector4i b, out Vector4i result)
|
||||
{
|
||||
result = new Vector4i(a.x * b.x, a.y * b.y, a.z * b.z, a.w * b.w);
|
||||
}
|
||||
#endregion
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
955
LightlessSync/ThirdParty/MeshDecimator/Mesh.cs
vendored
Normal file
955
LightlessSync/ThirdParty/MeshDecimator/Mesh.cs
vendored
Normal file
@@ -0,0 +1,955 @@
|
||||
#region License
|
||||
/*
|
||||
MIT License
|
||||
|
||||
Copyright(c) 2017-2018 Mattias Edlund
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using MeshDecimator.Math;
|
||||
|
||||
namespace MeshDecimator
|
||||
{
|
||||
/// <summary>
|
||||
/// A mesh.
|
||||
/// </summary>
|
||||
public sealed class Mesh
|
||||
{
|
||||
#region Consts
|
||||
/// <summary>
|
||||
/// The count of supported UV channels.
|
||||
/// </summary>
|
||||
public const int UVChannelCount = 4;
|
||||
#endregion
|
||||
|
||||
#region Fields
|
||||
private Vector3d[] vertices = null;
|
||||
private int[][] indices = null;
|
||||
private Vector3[] normals = null;
|
||||
private Vector4[] tangents = null;
|
||||
private Vector2[][] uvs2D = null;
|
||||
private Vector3[][] uvs3D = null;
|
||||
private Vector4[][] uvs4D = null;
|
||||
private Vector4[] colors = null;
|
||||
private BoneWeight[] boneWeights = null;
|
||||
|
||||
private static readonly int[] emptyIndices = new int[0];
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
/// <summary>
|
||||
/// Gets the count of vertices of this mesh.
|
||||
/// </summary>
|
||||
public int VertexCount
|
||||
{
|
||||
get { return vertices.Length; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the count of submeshes in this mesh.
|
||||
/// </summary>
|
||||
public int SubMeshCount
|
||||
{
|
||||
get { return indices.Length; }
|
||||
set
|
||||
{
|
||||
if (value <= 0)
|
||||
throw new ArgumentOutOfRangeException("value");
|
||||
|
||||
int[][] newIndices = new int[value][];
|
||||
Array.Copy(indices, 0, newIndices, 0, MathHelper.Min(indices.Length, newIndices.Length));
|
||||
indices = newIndices;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the total count of triangles in this mesh.
|
||||
/// </summary>
|
||||
public int TriangleCount
|
||||
{
|
||||
get
|
||||
{
|
||||
int triangleCount = 0;
|
||||
for (int i = 0; i < indices.Length; i++)
|
||||
{
|
||||
if (indices[i] != null)
|
||||
{
|
||||
triangleCount += indices[i].Length / 3;
|
||||
}
|
||||
}
|
||||
return triangleCount;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the vertices for this mesh. Note that this resets all other vertex attributes.
|
||||
/// </summary>
|
||||
public Vector3d[] Vertices
|
||||
{
|
||||
get { return vertices; }
|
||||
set
|
||||
{
|
||||
if (value == null)
|
||||
throw new ArgumentNullException("value");
|
||||
|
||||
vertices = value;
|
||||
ClearVertexAttributes();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the combined indices for this mesh. Once set, the sub-mesh count gets set to 1.
|
||||
/// </summary>
|
||||
public int[] Indices
|
||||
{
|
||||
get
|
||||
{
|
||||
if (indices.Length == 1)
|
||||
{
|
||||
return indices[0] ?? emptyIndices;
|
||||
}
|
||||
else
|
||||
{
|
||||
List<int> indexList = new List<int>(TriangleCount * 3);
|
||||
for (int i = 0; i < indices.Length; i++)
|
||||
{
|
||||
if (indices[i] != null)
|
||||
{
|
||||
indexList.AddRange(indices[i]);
|
||||
}
|
||||
}
|
||||
return indexList.ToArray();
|
||||
}
|
||||
}
|
||||
set
|
||||
{
|
||||
if (value == null)
|
||||
throw new ArgumentNullException("value");
|
||||
else if ((value.Length % 3) != 0)
|
||||
throw new ArgumentException("The index count must be multiple by 3.", "value");
|
||||
|
||||
SubMeshCount = 1;
|
||||
SetIndices(0, value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the normals for this mesh.
|
||||
/// </summary>
|
||||
public Vector3[] Normals
|
||||
{
|
||||
get { return normals; }
|
||||
set
|
||||
{
|
||||
if (value != null && value.Length != vertices.Length)
|
||||
throw new ArgumentException(string.Format("The vertex normals must be as many as the vertices. Assigned: {0} Require: {1}", value.Length, vertices.Length));
|
||||
|
||||
normals = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the tangents for this mesh.
|
||||
/// </summary>
|
||||
public Vector4[] Tangents
|
||||
{
|
||||
get { return tangents; }
|
||||
set
|
||||
{
|
||||
if (value != null && value.Length != vertices.Length)
|
||||
throw new ArgumentException(string.Format("The vertex tangents must be as many as the vertices. Assigned: {0} Require: {1}", value.Length, vertices.Length));
|
||||
|
||||
tangents = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the first UV set for this mesh.
|
||||
/// </summary>
|
||||
public Vector2[] UV1
|
||||
{
|
||||
get { return GetUVs2D(0); }
|
||||
set { SetUVs(0, value); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the second UV set for this mesh.
|
||||
/// </summary>
|
||||
public Vector2[] UV2
|
||||
{
|
||||
get { return GetUVs2D(1); }
|
||||
set { SetUVs(1, value); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the third UV set for this mesh.
|
||||
/// </summary>
|
||||
public Vector2[] UV3
|
||||
{
|
||||
get { return GetUVs2D(2); }
|
||||
set { SetUVs(2, value); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the fourth UV set for this mesh.
|
||||
/// </summary>
|
||||
public Vector2[] UV4
|
||||
{
|
||||
get { return GetUVs2D(3); }
|
||||
set { SetUVs(3, value); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the vertex colors for this mesh.
|
||||
/// </summary>
|
||||
public Vector4[] Colors
|
||||
{
|
||||
get { return colors; }
|
||||
set
|
||||
{
|
||||
if (value != null && value.Length != vertices.Length)
|
||||
throw new ArgumentException(string.Format("The vertex colors must be as many as the vertices. Assigned: {0} Require: {1}", value.Length, vertices.Length));
|
||||
|
||||
colors = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the vertex bone weights for this mesh.
|
||||
/// </summary>
|
||||
public BoneWeight[] BoneWeights
|
||||
{
|
||||
get { return boneWeights; }
|
||||
set
|
||||
{
|
||||
if (value != null && value.Length != vertices.Length)
|
||||
throw new ArgumentException(string.Format("The vertex bone weights must be as many as the vertices. Assigned: {0} Require: {1}", value.Length, vertices.Length));
|
||||
|
||||
boneWeights = value;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Constructor
|
||||
/// <summary>
|
||||
/// Creates a new mesh.
|
||||
/// </summary>
|
||||
/// <param name="vertices">The mesh vertices.</param>
|
||||
/// <param name="indices">The mesh indices.</param>
|
||||
public Mesh(Vector3d[] vertices, int[] indices)
|
||||
{
|
||||
if (vertices == null)
|
||||
throw new ArgumentNullException("vertices");
|
||||
else if (indices == null)
|
||||
throw new ArgumentNullException("indices");
|
||||
else if ((indices.Length % 3) != 0)
|
||||
throw new ArgumentException("The index count must be multiple by 3.", "indices");
|
||||
|
||||
this.vertices = vertices;
|
||||
this.indices = new int[1][];
|
||||
this.indices[0] = indices;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new mesh.
|
||||
/// </summary>
|
||||
/// <param name="vertices">The mesh vertices.</param>
|
||||
/// <param name="indices">The mesh indices.</param>
|
||||
public Mesh(Vector3d[] vertices, int[][] indices)
|
||||
{
|
||||
if (vertices == null)
|
||||
throw new ArgumentNullException("vertices");
|
||||
else if (indices == null)
|
||||
throw new ArgumentNullException("indices");
|
||||
|
||||
for (int i = 0; i < indices.Length; i++)
|
||||
{
|
||||
if (indices[i] != null && (indices[i].Length % 3) != 0)
|
||||
throw new ArgumentException(string.Format("The index count must be multiple by 3 at sub-mesh index {0}.", i), "indices");
|
||||
}
|
||||
|
||||
this.vertices = vertices;
|
||||
this.indices = indices;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Private Methods
|
||||
private void ClearVertexAttributes()
|
||||
{
|
||||
normals = null;
|
||||
tangents = null;
|
||||
uvs2D = null;
|
||||
uvs3D = null;
|
||||
uvs4D = null;
|
||||
colors = null;
|
||||
boneWeights = null;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Public Methods
|
||||
#region Recalculate Normals
|
||||
/// <summary>
|
||||
/// Recalculates the normals for this mesh smoothly.
|
||||
/// </summary>
|
||||
public void RecalculateNormals()
|
||||
{
|
||||
int vertexCount = vertices.Length;
|
||||
Vector3[] normals = new Vector3[vertexCount];
|
||||
|
||||
int subMeshCount = this.indices.Length;
|
||||
for (int subMeshIndex = 0; subMeshIndex < subMeshCount; subMeshIndex++)
|
||||
{
|
||||
int[] indices = this.indices[subMeshIndex];
|
||||
if (indices == null)
|
||||
continue;
|
||||
|
||||
int indexCount = indices.Length;
|
||||
for (int i = 0; i < indexCount; i += 3)
|
||||
{
|
||||
int i0 = indices[i];
|
||||
int i1 = indices[i + 1];
|
||||
int i2 = indices[i + 2];
|
||||
|
||||
var v0 = (Vector3)vertices[i0];
|
||||
var v1 = (Vector3)vertices[i1];
|
||||
var v2 = (Vector3)vertices[i2];
|
||||
|
||||
var nx = v1 - v0;
|
||||
var ny = v2 - v0;
|
||||
Vector3 normal;
|
||||
Vector3.Cross(ref nx, ref ny, out normal);
|
||||
normal.Normalize();
|
||||
|
||||
normals[i0] += normal;
|
||||
normals[i1] += normal;
|
||||
normals[i2] += normal;
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < vertexCount; i++)
|
||||
{
|
||||
normals[i].Normalize();
|
||||
}
|
||||
|
||||
this.normals = normals;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Recalculate Tangents
|
||||
/// <summary>
|
||||
/// Recalculates the tangents for this mesh.
|
||||
/// </summary>
|
||||
public void RecalculateTangents()
|
||||
{
|
||||
// Make sure we have the normals first
|
||||
if (normals == null)
|
||||
return;
|
||||
|
||||
// Also make sure that we have the first UV set
|
||||
bool uvIs2D = (uvs2D != null && uvs2D[0] != null);
|
||||
bool uvIs3D = (uvs3D != null && uvs3D[0] != null);
|
||||
bool uvIs4D = (uvs4D != null && uvs4D[0] != null);
|
||||
if (!uvIs2D && !uvIs3D && !uvIs4D)
|
||||
return;
|
||||
|
||||
int vertexCount = vertices.Length;
|
||||
|
||||
var tangents = new Vector4[vertexCount];
|
||||
var tan1 = new Vector3[vertexCount];
|
||||
var tan2 = new Vector3[vertexCount];
|
||||
|
||||
Vector2[] uv2D = (uvIs2D ? uvs2D[0] : null);
|
||||
Vector3[] uv3D = (uvIs3D ? uvs3D[0] : null);
|
||||
Vector4[] uv4D = (uvIs4D ? uvs4D[0] : null);
|
||||
|
||||
int subMeshCount = this.indices.Length;
|
||||
for (int subMeshIndex = 0; subMeshIndex < subMeshCount; subMeshIndex++)
|
||||
{
|
||||
int[] indices = this.indices[subMeshIndex];
|
||||
if (indices == null)
|
||||
continue;
|
||||
|
||||
int indexCount = indices.Length;
|
||||
for (int i = 0; i < indexCount; i += 3)
|
||||
{
|
||||
int i0 = indices[i];
|
||||
int i1 = indices[i + 1];
|
||||
int i2 = indices[i + 2];
|
||||
|
||||
var v0 = vertices[i0];
|
||||
var v1 = vertices[i1];
|
||||
var v2 = vertices[i2];
|
||||
|
||||
float s1, s2, t1, t2;
|
||||
if (uvIs2D)
|
||||
{
|
||||
var w0 = uv2D[i0];
|
||||
var w1 = uv2D[i1];
|
||||
var w2 = uv2D[i2];
|
||||
s1 = w1.x - w0.x;
|
||||
s2 = w2.x - w0.x;
|
||||
t1 = w1.y - w0.y;
|
||||
t2 = w2.y - w0.y;
|
||||
}
|
||||
else if (uvIs3D)
|
||||
{
|
||||
var w0 = uv3D[i0];
|
||||
var w1 = uv3D[i1];
|
||||
var w2 = uv3D[i2];
|
||||
s1 = w1.x - w0.x;
|
||||
s2 = w2.x - w0.x;
|
||||
t1 = w1.y - w0.y;
|
||||
t2 = w2.y - w0.y;
|
||||
}
|
||||
else
|
||||
{
|
||||
var w0 = uv4D[i0];
|
||||
var w1 = uv4D[i1];
|
||||
var w2 = uv4D[i2];
|
||||
s1 = w1.x - w0.x;
|
||||
s2 = w2.x - w0.x;
|
||||
t1 = w1.y - w0.y;
|
||||
t2 = w2.y - w0.y;
|
||||
}
|
||||
|
||||
|
||||
float x1 = (float)(v1.x - v0.x);
|
||||
float x2 = (float)(v2.x - v0.x);
|
||||
float y1 = (float)(v1.y - v0.y);
|
||||
float y2 = (float)(v2.y - v0.y);
|
||||
float z1 = (float)(v1.z - v0.z);
|
||||
float z2 = (float)(v2.z - v0.z);
|
||||
float r = 1f / (s1 * t2 - s2 * t1);
|
||||
|
||||
var sdir = new Vector3((t2 * x1 - t1 * x2) * r, (t2 * y1 - t1 * y2) * r, (t2 * z1 - t1 * z2) * r);
|
||||
var tdir = new Vector3((s1 * x2 - s2 * x1) * r, (s1 * y2 - s2 * y1) * r, (s1 * z2 - s2 * z1) * r);
|
||||
|
||||
tan1[i0] += sdir;
|
||||
tan1[i1] += sdir;
|
||||
tan1[i2] += sdir;
|
||||
tan2[i0] += tdir;
|
||||
tan2[i1] += tdir;
|
||||
tan2[i2] += tdir;
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < vertexCount; i++)
|
||||
{
|
||||
var n = normals[i];
|
||||
var t = tan1[i];
|
||||
|
||||
var tmp = (t - n * Vector3.Dot(ref n, ref t));
|
||||
tmp.Normalize();
|
||||
|
||||
Vector3 c;
|
||||
Vector3.Cross(ref n, ref t, out c);
|
||||
float dot = Vector3.Dot(ref c, ref tan2[i]);
|
||||
float w = (dot < 0f ? -1f : 1f);
|
||||
tangents[i] = new Vector4(tmp.x, tmp.y, tmp.z, w);
|
||||
}
|
||||
|
||||
this.tangents = tangents;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Triangles
|
||||
/// <summary>
|
||||
/// Returns the count of triangles for a specific sub-mesh in this mesh.
|
||||
/// </summary>
|
||||
/// <param name="subMeshIndex">The sub-mesh index.</param>
|
||||
/// <returns>The triangle count.</returns>
|
||||
public int GetTriangleCount(int subMeshIndex)
|
||||
{
|
||||
if (subMeshIndex < 0 || subMeshIndex >= indices.Length)
|
||||
throw new IndexOutOfRangeException();
|
||||
|
||||
return indices[subMeshIndex].Length / 3;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the triangle indices of a specific sub-mesh in this mesh.
|
||||
/// </summary>
|
||||
/// <param name="subMeshIndex">The sub-mesh index.</param>
|
||||
/// <returns>The triangle indices.</returns>
|
||||
public int[] GetIndices(int subMeshIndex)
|
||||
{
|
||||
if (subMeshIndex < 0 || subMeshIndex >= indices.Length)
|
||||
throw new IndexOutOfRangeException();
|
||||
|
||||
return indices[subMeshIndex] ?? emptyIndices;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the triangle indices for all sub-meshes in this mesh.
|
||||
/// </summary>
|
||||
/// <returns>The sub-mesh triangle indices.</returns>
|
||||
public int[][] GetSubMeshIndices()
|
||||
{
|
||||
var subMeshIndices = new int[indices.Length][];
|
||||
for (int subMeshIndex = 0; subMeshIndex < indices.Length; subMeshIndex++)
|
||||
{
|
||||
subMeshIndices[subMeshIndex] = indices[subMeshIndex] ?? emptyIndices;
|
||||
}
|
||||
return subMeshIndices;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the triangle indices of a specific sub-mesh in this mesh.
|
||||
/// </summary>
|
||||
/// <param name="subMeshIndex">The sub-mesh index.</param>
|
||||
/// <param name="indices">The triangle indices.</param>
|
||||
public void SetIndices(int subMeshIndex, int[] indices)
|
||||
{
|
||||
if (subMeshIndex < 0 || subMeshIndex >= this.indices.Length)
|
||||
throw new IndexOutOfRangeException();
|
||||
else if (indices == null)
|
||||
throw new ArgumentNullException("indices");
|
||||
else if ((indices.Length % 3) != 0)
|
||||
throw new ArgumentException("The index count must be multiple by 3.", "indices");
|
||||
|
||||
this.indices[subMeshIndex] = indices;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region UV Sets
|
||||
#region Getting
|
||||
/// <summary>
|
||||
/// Returns the UV dimension for a specific channel.
|
||||
/// </summary>
|
||||
/// <param name="channel"></param>
|
||||
/// <returns>The UV dimension count.</returns>
|
||||
public int GetUVDimension(int channel)
|
||||
{
|
||||
if (channel < 0 || channel >= UVChannelCount)
|
||||
throw new ArgumentOutOfRangeException("channel");
|
||||
|
||||
if (uvs2D != null && uvs2D[channel] != null)
|
||||
{
|
||||
return 2;
|
||||
}
|
||||
else if (uvs3D != null && uvs3D[channel] != null)
|
||||
{
|
||||
return 3;
|
||||
}
|
||||
else if (uvs4D != null && uvs4D[channel] != null)
|
||||
{
|
||||
return 4;
|
||||
}
|
||||
else
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the UVs (2D) from a specific channel.
|
||||
/// </summary>
|
||||
/// <param name="channel">The channel index.</param>
|
||||
/// <returns>The UVs.</returns>
|
||||
public Vector2[] GetUVs2D(int channel)
|
||||
{
|
||||
if (channel < 0 || channel >= UVChannelCount)
|
||||
throw new ArgumentOutOfRangeException("channel");
|
||||
|
||||
if (uvs2D != null && uvs2D[channel] != null)
|
||||
{
|
||||
return uvs2D[channel];
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the UVs (3D) from a specific channel.
|
||||
/// </summary>
|
||||
/// <param name="channel">The channel index.</param>
|
||||
/// <returns>The UVs.</returns>
|
||||
public Vector3[] GetUVs3D(int channel)
|
||||
{
|
||||
if (channel < 0 || channel >= UVChannelCount)
|
||||
throw new ArgumentOutOfRangeException("channel");
|
||||
|
||||
if (uvs3D != null && uvs3D[channel] != null)
|
||||
{
|
||||
return uvs3D[channel];
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the UVs (4D) from a specific channel.
|
||||
/// </summary>
|
||||
/// <param name="channel">The channel index.</param>
|
||||
/// <returns>The UVs.</returns>
|
||||
public Vector4[] GetUVs4D(int channel)
|
||||
{
|
||||
if (channel < 0 || channel >= UVChannelCount)
|
||||
throw new ArgumentOutOfRangeException("channel");
|
||||
|
||||
if (uvs4D != null && uvs4D[channel] != null)
|
||||
{
|
||||
return uvs4D[channel];
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the UVs (2D) from a specific channel.
|
||||
/// </summary>
|
||||
/// <param name="channel">The channel index.</param>
|
||||
/// <param name="uvs">The UVs.</param>
|
||||
public void GetUVs(int channel, List<Vector2> uvs)
|
||||
{
|
||||
if (channel < 0 || channel >= UVChannelCount)
|
||||
throw new ArgumentOutOfRangeException("channel");
|
||||
else if (uvs == null)
|
||||
throw new ArgumentNullException("uvs");
|
||||
|
||||
uvs.Clear();
|
||||
if (uvs2D != null && uvs2D[channel] != null)
|
||||
{
|
||||
var uvData = uvs2D[channel];
|
||||
if (uvData != null)
|
||||
{
|
||||
uvs.AddRange(uvData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the UVs (3D) from a specific channel.
|
||||
/// </summary>
|
||||
/// <param name="channel">The channel index.</param>
|
||||
/// <param name="uvs">The UVs.</param>
|
||||
public void GetUVs(int channel, List<Vector3> uvs)
|
||||
{
|
||||
if (channel < 0 || channel >= UVChannelCount)
|
||||
throw new ArgumentOutOfRangeException("channel");
|
||||
else if (uvs == null)
|
||||
throw new ArgumentNullException("uvs");
|
||||
|
||||
uvs.Clear();
|
||||
if (uvs3D != null && uvs3D[channel] != null)
|
||||
{
|
||||
var uvData = uvs3D[channel];
|
||||
if (uvData != null)
|
||||
{
|
||||
uvs.AddRange(uvData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the UVs (4D) from a specific channel.
|
||||
/// </summary>
|
||||
/// <param name="channel">The channel index.</param>
|
||||
/// <param name="uvs">The UVs.</param>
|
||||
public void GetUVs(int channel, List<Vector4> uvs)
|
||||
{
|
||||
if (channel < 0 || channel >= UVChannelCount)
|
||||
throw new ArgumentOutOfRangeException("channel");
|
||||
else if (uvs == null)
|
||||
throw new ArgumentNullException("uvs");
|
||||
|
||||
uvs.Clear();
|
||||
if (uvs4D != null && uvs4D[channel] != null)
|
||||
{
|
||||
var uvData = uvs4D[channel];
|
||||
if (uvData != null)
|
||||
{
|
||||
uvs.AddRange(uvData);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Setting
|
||||
/// <summary>
|
||||
/// Sets the UVs (2D) for a specific channel.
|
||||
/// </summary>
|
||||
/// <param name="channel">The channel index.</param>
|
||||
/// <param name="uvs">The UVs.</param>
|
||||
public void SetUVs(int channel, Vector2[] uvs)
|
||||
{
|
||||
if (channel < 0 || channel >= UVChannelCount)
|
||||
throw new ArgumentOutOfRangeException("channel");
|
||||
|
||||
if (uvs != null && uvs.Length > 0)
|
||||
{
|
||||
if (uvs.Length != vertices.Length)
|
||||
throw new ArgumentException(string.Format("The vertex UVs must be as many as the vertices. Assigned: {0} Require: {1}", uvs.Length, vertices.Length));
|
||||
|
||||
if (uvs2D == null)
|
||||
uvs2D = new Vector2[UVChannelCount][];
|
||||
|
||||
int uvCount = uvs.Length;
|
||||
var uvSet = new Vector2[uvCount];
|
||||
uvs2D[channel] = uvSet;
|
||||
uvs.CopyTo(uvSet, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (uvs2D != null)
|
||||
{
|
||||
uvs2D[channel] = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (uvs3D != null)
|
||||
{
|
||||
uvs3D[channel] = null;
|
||||
}
|
||||
if (uvs4D != null)
|
||||
{
|
||||
uvs4D[channel] = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the UVs (3D) for a specific channel.
|
||||
/// </summary>
|
||||
/// <param name="channel">The channel index.</param>
|
||||
/// <param name="uvs">The UVs.</param>
|
||||
public void SetUVs(int channel, Vector3[] uvs)
|
||||
{
|
||||
if (channel < 0 || channel >= UVChannelCount)
|
||||
throw new ArgumentOutOfRangeException("channel");
|
||||
|
||||
if (uvs != null && uvs.Length > 0)
|
||||
{
|
||||
int uvCount = uvs.Length;
|
||||
if (uvCount != vertices.Length)
|
||||
throw new ArgumentException(string.Format("The vertex UVs must be as many as the vertices. Assigned: {0} Require: {1}", uvCount, vertices.Length), "uvs");
|
||||
|
||||
if (uvs3D == null)
|
||||
uvs3D = new Vector3[UVChannelCount][];
|
||||
|
||||
var uvSet = new Vector3[uvCount];
|
||||
uvs3D[channel] = uvSet;
|
||||
uvs.CopyTo(uvSet, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (uvs3D != null)
|
||||
{
|
||||
uvs3D[channel] = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (uvs2D != null)
|
||||
{
|
||||
uvs2D[channel] = null;
|
||||
}
|
||||
if (uvs4D != null)
|
||||
{
|
||||
uvs4D[channel] = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the UVs (4D) for a specific channel.
|
||||
/// </summary>
|
||||
/// <param name="channel">The channel index.</param>
|
||||
/// <param name="uvs">The UVs.</param>
|
||||
public void SetUVs(int channel, Vector4[] uvs)
|
||||
{
|
||||
if (channel < 0 || channel >= UVChannelCount)
|
||||
throw new ArgumentOutOfRangeException("channel");
|
||||
|
||||
if (uvs != null && uvs.Length > 0)
|
||||
{
|
||||
int uvCount = uvs.Length;
|
||||
if (uvCount != vertices.Length)
|
||||
throw new ArgumentException(string.Format("The vertex UVs must be as many as the vertices. Assigned: {0} Require: {1}", uvCount, vertices.Length), "uvs");
|
||||
|
||||
if (uvs4D == null)
|
||||
uvs4D = new Vector4[UVChannelCount][];
|
||||
|
||||
var uvSet = new Vector4[uvCount];
|
||||
uvs4D[channel] = uvSet;
|
||||
uvs.CopyTo(uvSet, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (uvs4D != null)
|
||||
{
|
||||
uvs4D[channel] = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (uvs2D != null)
|
||||
{
|
||||
uvs2D[channel] = null;
|
||||
}
|
||||
if (uvs3D != null)
|
||||
{
|
||||
uvs3D[channel] = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the UVs (2D) for a specific channel.
|
||||
/// </summary>
|
||||
/// <param name="channel">The channel index.</param>
|
||||
/// <param name="uvs">The UVs.</param>
|
||||
public void SetUVs(int channel, List<Vector2> uvs)
|
||||
{
|
||||
if (channel < 0 || channel >= UVChannelCount)
|
||||
throw new ArgumentOutOfRangeException("channel");
|
||||
|
||||
if (uvs != null && uvs.Count > 0)
|
||||
{
|
||||
int uvCount = uvs.Count;
|
||||
if (uvCount != vertices.Length)
|
||||
throw new ArgumentException(string.Format("The vertex UVs must be as many as the vertices. Assigned: {0} Require: {1}", uvCount, vertices.Length), "uvs");
|
||||
|
||||
if (uvs2D == null)
|
||||
uvs2D = new Vector2[UVChannelCount][];
|
||||
|
||||
var uvSet = new Vector2[uvCount];
|
||||
uvs2D[channel] = uvSet;
|
||||
uvs.CopyTo(uvSet, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (uvs2D != null)
|
||||
{
|
||||
uvs2D[channel] = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (uvs3D != null)
|
||||
{
|
||||
uvs3D[channel] = null;
|
||||
}
|
||||
if (uvs4D != null)
|
||||
{
|
||||
uvs4D[channel] = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the UVs (3D) for a specific channel.
|
||||
/// </summary>
|
||||
/// <param name="channel">The channel index.</param>
|
||||
/// <param name="uvs">The UVs.</param>
|
||||
public void SetUVs(int channel, List<Vector3> uvs)
|
||||
{
|
||||
if (channel < 0 || channel >= UVChannelCount)
|
||||
throw new ArgumentOutOfRangeException("channel");
|
||||
|
||||
if (uvs != null && uvs.Count > 0)
|
||||
{
|
||||
int uvCount = uvs.Count;
|
||||
if (uvCount != vertices.Length)
|
||||
throw new ArgumentException(string.Format("The vertex UVs must be as many as the vertices. Assigned: {0} Require: {1}", uvCount, vertices.Length), "uvs");
|
||||
|
||||
if (uvs3D == null)
|
||||
uvs3D = new Vector3[UVChannelCount][];
|
||||
|
||||
var uvSet = new Vector3[uvCount];
|
||||
uvs3D[channel] = uvSet;
|
||||
uvs.CopyTo(uvSet, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (uvs3D != null)
|
||||
{
|
||||
uvs3D[channel] = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (uvs2D != null)
|
||||
{
|
||||
uvs2D[channel] = null;
|
||||
}
|
||||
if (uvs4D != null)
|
||||
{
|
||||
uvs4D[channel] = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the UVs (4D) for a specific channel.
|
||||
/// </summary>
|
||||
/// <param name="channel">The channel index.</param>
|
||||
/// <param name="uvs">The UVs.</param>
|
||||
public void SetUVs(int channel, List<Vector4> uvs)
|
||||
{
|
||||
if (channel < 0 || channel >= UVChannelCount)
|
||||
throw new ArgumentOutOfRangeException("channel");
|
||||
|
||||
if (uvs != null && uvs.Count > 0)
|
||||
{
|
||||
int uvCount = uvs.Count;
|
||||
if (uvCount != vertices.Length)
|
||||
throw new ArgumentException(string.Format("The vertex UVs must be as many as the vertices. Assigned: {0} Require: {1}", uvCount, vertices.Length), "uvs");
|
||||
|
||||
if (uvs4D == null)
|
||||
uvs4D = new Vector4[UVChannelCount][];
|
||||
|
||||
var uvSet = new Vector4[uvCount];
|
||||
uvs4D[channel] = uvSet;
|
||||
uvs.CopyTo(uvSet, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (uvs4D != null)
|
||||
{
|
||||
uvs4D[channel] = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (uvs2D != null)
|
||||
{
|
||||
uvs2D[channel] = null;
|
||||
}
|
||||
if (uvs3D != null)
|
||||
{
|
||||
uvs3D[channel] = null;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
#endregion
|
||||
|
||||
#region To String
|
||||
/// <summary>
|
||||
/// Returns the text-representation of this mesh.
|
||||
/// </summary>
|
||||
/// <returns>The text-representation.</returns>
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format("Vertices: {0}", vertices.Length);
|
||||
}
|
||||
#endregion
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
180
LightlessSync/ThirdParty/MeshDecimator/MeshDecimation.cs
vendored
Normal file
180
LightlessSync/ThirdParty/MeshDecimator/MeshDecimation.cs
vendored
Normal file
@@ -0,0 +1,180 @@
|
||||
#region License
|
||||
/*
|
||||
MIT License
|
||||
|
||||
Copyright(c) 2017-2018 Mattias Edlund
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
*/
|
||||
#endregion
|
||||
|
||||
using System;
|
||||
using MeshDecimator.Algorithms;
|
||||
|
||||
namespace MeshDecimator
|
||||
{
|
||||
#region Algorithm
|
||||
/// <summary>
|
||||
/// The decimation algorithms.
|
||||
/// </summary>
|
||||
public enum Algorithm
|
||||
{
|
||||
/// <summary>
|
||||
/// The default algorithm.
|
||||
/// </summary>
|
||||
Default,
|
||||
/// <summary>
|
||||
/// The fast quadric mesh simplification algorithm.
|
||||
/// </summary>
|
||||
FastQuadricMesh
|
||||
}
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// The mesh decimation API.
|
||||
/// </summary>
|
||||
public static class MeshDecimation
|
||||
{
|
||||
#region Public Methods
|
||||
#region Create Algorithm
|
||||
/// <summary>
|
||||
/// Creates a specific decimation algorithm.
|
||||
/// </summary>
|
||||
/// <param name="algorithm">The desired algorithm.</param>
|
||||
/// <returns>The decimation algorithm.</returns>
|
||||
public static DecimationAlgorithm CreateAlgorithm(Algorithm algorithm)
|
||||
{
|
||||
DecimationAlgorithm alg = null;
|
||||
|
||||
switch (algorithm)
|
||||
{
|
||||
case Algorithm.Default:
|
||||
case Algorithm.FastQuadricMesh:
|
||||
alg = new FastQuadricMeshSimplification();
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentException("The specified algorithm is not supported.", "algorithm");
|
||||
}
|
||||
|
||||
return alg;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Decimate Mesh
|
||||
/// <summary>
|
||||
/// Decimates a mesh.
|
||||
/// </summary>
|
||||
/// <param name="mesh">The mesh to decimate.</param>
|
||||
/// <param name="targetTriangleCount">The target triangle count.</param>
|
||||
/// <returns>The decimated mesh.</returns>
|
||||
public static Mesh DecimateMesh(Mesh mesh, int targetTriangleCount)
|
||||
{
|
||||
return DecimateMesh(Algorithm.Default, mesh, targetTriangleCount);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decimates a mesh.
|
||||
/// </summary>
|
||||
/// <param name="algorithm">The desired algorithm.</param>
|
||||
/// <param name="mesh">The mesh to decimate.</param>
|
||||
/// <param name="targetTriangleCount">The target triangle count.</param>
|
||||
/// <returns>The decimated mesh.</returns>
|
||||
public static Mesh DecimateMesh(Algorithm algorithm, Mesh mesh, int targetTriangleCount)
|
||||
{
|
||||
if (mesh == null)
|
||||
throw new ArgumentNullException("mesh");
|
||||
|
||||
var decimationAlgorithm = CreateAlgorithm(algorithm);
|
||||
return DecimateMesh(decimationAlgorithm, mesh, targetTriangleCount);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decimates a mesh.
|
||||
/// </summary>
|
||||
/// <param name="algorithm">The decimation algorithm.</param>
|
||||
/// <param name="mesh">The mesh to decimate.</param>
|
||||
/// <param name="targetTriangleCount">The target triangle count.</param>
|
||||
/// <returns>The decimated mesh.</returns>
|
||||
public static Mesh DecimateMesh(DecimationAlgorithm algorithm, Mesh mesh, int targetTriangleCount)
|
||||
{
|
||||
if (algorithm == null)
|
||||
throw new ArgumentNullException("algorithm");
|
||||
else if (mesh == null)
|
||||
throw new ArgumentNullException("mesh");
|
||||
|
||||
int currentTriangleCount = mesh.TriangleCount;
|
||||
if (targetTriangleCount > currentTriangleCount)
|
||||
targetTriangleCount = currentTriangleCount;
|
||||
else if (targetTriangleCount < 0)
|
||||
targetTriangleCount = 0;
|
||||
|
||||
algorithm.Initialize(mesh);
|
||||
algorithm.DecimateMesh(targetTriangleCount);
|
||||
return algorithm.ToMesh();
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Decimate Mesh Lossless
|
||||
/// <summary>
|
||||
/// Decimates a mesh without losing any quality.
|
||||
/// </summary>
|
||||
/// <param name="mesh">The mesh to decimate.</param>
|
||||
/// <returns>The decimated mesh.</returns>
|
||||
public static Mesh DecimateMeshLossless(Mesh mesh)
|
||||
{
|
||||
return DecimateMeshLossless(Algorithm.Default, mesh);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decimates a mesh without losing any quality.
|
||||
/// </summary>
|
||||
/// <param name="algorithm">The desired algorithm.</param>
|
||||
/// <param name="mesh">The mesh to decimate.</param>
|
||||
/// <returns>The decimated mesh.</returns>
|
||||
public static Mesh DecimateMeshLossless(Algorithm algorithm, Mesh mesh)
|
||||
{
|
||||
if (mesh == null)
|
||||
throw new ArgumentNullException("mesh");
|
||||
|
||||
var decimationAlgorithm = CreateAlgorithm(algorithm);
|
||||
return DecimateMeshLossless(decimationAlgorithm, mesh);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decimates a mesh without losing any quality.
|
||||
/// </summary>
|
||||
/// <param name="algorithm">The decimation algorithm.</param>
|
||||
/// <param name="mesh">The mesh to decimate.</param>
|
||||
/// <returns>The decimated mesh.</returns>
|
||||
public static Mesh DecimateMeshLossless(DecimationAlgorithm algorithm, Mesh mesh)
|
||||
{
|
||||
if (algorithm == null)
|
||||
throw new ArgumentNullException("algorithm");
|
||||
else if (mesh == null)
|
||||
throw new ArgumentNullException("mesh");
|
||||
|
||||
int currentTriangleCount = mesh.TriangleCount;
|
||||
algorithm.Initialize(mesh);
|
||||
algorithm.DecimateMeshLossless();
|
||||
return algorithm.ToMesh();
|
||||
}
|
||||
#endregion
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -39,7 +39,7 @@ public class CompactUi : WindowMediatorSubscriberBase
|
||||
private readonly LightlessConfigService _configService;
|
||||
private readonly LightlessMediator _lightlessMediator;
|
||||
private readonly PairLedger _pairLedger;
|
||||
private readonly ConcurrentDictionary<GameObjectHandler, Dictionary<string, FileDownloadStatus>> _currentDownloads = new();
|
||||
private readonly ConcurrentDictionary<GameObjectHandler, IReadOnlyDictionary<string, FileDownloadStatus>> _currentDownloads = new();
|
||||
private readonly DrawEntityFactory _drawEntityFactory;
|
||||
private readonly FileUploadManager _fileTransferManager;
|
||||
private readonly PlayerPerformanceConfigService _playerPerformanceConfig;
|
||||
@@ -843,12 +843,16 @@ public class CompactUi : WindowMediatorSubscriberBase
|
||||
|
||||
//Filter of not grouped/foldered and offline pairs
|
||||
var allOnlineNotTaggedPairs = SortEntries(allEntries.Where(FilterNotTaggedUsers));
|
||||
var onlineNotTaggedPairs = SortEntries(filteredEntries.Where(e => FilterNotTaggedUsers(e) && FilterOnlineOrPausedSelf(e)));
|
||||
|
||||
if (allOnlineNotTaggedPairs.Count > 0)
|
||||
{
|
||||
if (allOnlineNotTaggedPairs.Count > 0 && _configService.Current.ShowOfflineUsersSeparately) {
|
||||
var filteredOnlineEntries = SortOnlineEntries(filteredEntries.Where(e => FilterNotTaggedUsers(e) && FilterOnlineOrPausedSelf(e)));
|
||||
drawFolders.Add(_drawEntityFactory.CreateTagFolder(
|
||||
_configService.Current.ShowOfflineUsersSeparately ? TagHandler.CustomOnlineTag : TagHandler.CustomAllTag,
|
||||
TagHandler.CustomOnlineTag,
|
||||
filteredOnlineEntries,
|
||||
allOnlineNotTaggedPairs));
|
||||
} else if (allOnlineNotTaggedPairs.Count > 0 && !_configService.Current.ShowOfflineUsersSeparately) {
|
||||
var onlineNotTaggedPairs = SortEntries(filteredEntries.Where(FilterNotTaggedUsers));
|
||||
drawFolders.Add(_drawEntityFactory.CreateTagFolder(
|
||||
TagHandler.CustomAllTag,
|
||||
onlineNotTaggedPairs,
|
||||
allOnlineNotTaggedPairs));
|
||||
}
|
||||
@@ -885,7 +889,7 @@ public class CompactUi : WindowMediatorSubscriberBase
|
||||
}
|
||||
}
|
||||
|
||||
private bool PassesFilter(PairUiEntry entry, string filter)
|
||||
private static bool PassesFilter(PairUiEntry entry, string filter)
|
||||
{
|
||||
if (string.IsNullOrEmpty(filter)) return true;
|
||||
|
||||
@@ -940,12 +944,24 @@ public class CompactUi : WindowMediatorSubscriberBase
|
||||
VisiblePairSortMode.VramUsage => SortVisibleByMetric(entryList, e => e.LastAppliedApproximateVramBytes),
|
||||
VisiblePairSortMode.EffectiveVramUsage => SortVisibleByMetric(entryList, e => e.LastAppliedApproximateEffectiveVramBytes),
|
||||
VisiblePairSortMode.TriangleCount => SortVisibleByMetric(entryList, e => e.LastAppliedDataTris),
|
||||
VisiblePairSortMode.EffectiveTriangleCount => SortVisibleByMetric(entryList, e => e.LastAppliedApproximateEffectiveTris),
|
||||
VisiblePairSortMode.Alphabetical => [.. entryList.OrderBy(e => AlphabeticalSortKey(e), StringComparer.OrdinalIgnoreCase)],
|
||||
VisiblePairSortMode.PreferredDirectPairs => SortVisibleByPreferred(entryList),
|
||||
_ => SortEntries(entryList),
|
||||
};
|
||||
}
|
||||
|
||||
private ImmutableList<PairUiEntry> SortOnlineEntries(IEnumerable<PairUiEntry> entries)
|
||||
{
|
||||
var entryList = entries.ToList();
|
||||
return _configService.Current.OnlinePairSortMode switch
|
||||
{
|
||||
OnlinePairSortMode.Alphabetical => [.. entryList.OrderBy(e => AlphabeticalSortKey(e), StringComparer.OrdinalIgnoreCase)],
|
||||
OnlinePairSortMode.PreferredDirectPairs => SortVisibleByPreferred(entryList),
|
||||
_ => SortEntries(entryList),
|
||||
};
|
||||
}
|
||||
|
||||
private ImmutableList<PairUiEntry> SortVisibleByMetric(IEnumerable<PairUiEntry> entries, Func<PairUiEntry, long> selector)
|
||||
{
|
||||
return [.. entries
|
||||
|
||||
@@ -4,8 +4,8 @@ using Dalamud.Interface.Utility.Raii;
|
||||
using LightlessSync.UI.Handlers;
|
||||
using LightlessSync.UI.Models;
|
||||
using System.Collections.Immutable;
|
||||
using LightlessSync.UI;
|
||||
using LightlessSync.UI.Style;
|
||||
using OtterGui.Text;
|
||||
|
||||
namespace LightlessSync.UI.Components;
|
||||
|
||||
@@ -113,9 +113,13 @@ public abstract class DrawFolderBase : IDrawFolder
|
||||
using var indent = ImRaii.PushIndent(_uiSharedService.GetIconSize(FontAwesomeIcon.EllipsisV).X + ImGui.GetStyle().ItemSpacing.X, false);
|
||||
if (DrawPairs.Any())
|
||||
{
|
||||
foreach (var item in DrawPairs)
|
||||
using var clipper = ImUtf8.ListClipper(DrawPairs.Count, ImGui.GetFrameHeightWithSpacing());
|
||||
while (clipper.Step())
|
||||
{
|
||||
item.DrawPairedClient();
|
||||
for (var i = clipper.DisplayStart; i < clipper.DisplayEnd; i++)
|
||||
{
|
||||
DrawPairs[i].DrawPairedClient();
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
|
||||
@@ -169,11 +169,16 @@ public class DrawFolderTag : DrawFolderBase
|
||||
|
||||
protected override float DrawRightSide(float currentRightSideX)
|
||||
{
|
||||
if (_id == TagHandler.CustomVisibleTag)
|
||||
if (string.Equals(_id, TagHandler.CustomVisibleTag, StringComparison.Ordinal))
|
||||
{
|
||||
return DrawVisibleFilter(currentRightSideX);
|
||||
}
|
||||
|
||||
if (string.Equals(_id, TagHandler.CustomOnlineTag, StringComparison.Ordinal))
|
||||
{
|
||||
return DrawOnlineFilter(currentRightSideX);
|
||||
}
|
||||
|
||||
if (!RenderPause)
|
||||
{
|
||||
return currentRightSideX;
|
||||
@@ -254,7 +259,7 @@ public class DrawFolderTag : DrawFolderBase
|
||||
foreach (VisiblePairSortMode mode in Enum.GetValues<VisiblePairSortMode>())
|
||||
{
|
||||
var selected = _configService.Current.VisiblePairSortMode == mode;
|
||||
if (ImGui.MenuItem(GetSortLabel(mode), string.Empty, selected))
|
||||
if (ImGui.MenuItem(GetSortVisibleLabel(mode), string.Empty, selected))
|
||||
{
|
||||
if (!selected)
|
||||
{
|
||||
@@ -273,13 +278,63 @@ public class DrawFolderTag : DrawFolderBase
|
||||
return buttonStart - spacingX;
|
||||
}
|
||||
|
||||
private static string GetSortLabel(VisiblePairSortMode mode) => mode switch
|
||||
private float DrawOnlineFilter(float currentRightSideX)
|
||||
{
|
||||
var buttonSize = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Filter);
|
||||
var spacingX = ImGui.GetStyle().ItemSpacing.X;
|
||||
var buttonStart = currentRightSideX - buttonSize.X;
|
||||
|
||||
ImGui.SameLine(buttonStart);
|
||||
if (_uiSharedService.IconButton(FontAwesomeIcon.Filter))
|
||||
{
|
||||
SuppressNextRowToggle();
|
||||
ImGui.OpenPopup($"online-filter-{_id}");
|
||||
}
|
||||
|
||||
UiSharedService.AttachToolTip("Adjust how online pairs are ordered.");
|
||||
|
||||
if (ImGui.BeginPopup($"online-filter-{_id}"))
|
||||
{
|
||||
ImGui.TextUnformatted("Online Pair Ordering");
|
||||
ImGui.Separator();
|
||||
|
||||
foreach (OnlinePairSortMode mode in Enum.GetValues<OnlinePairSortMode>())
|
||||
{
|
||||
var selected = _configService.Current.OnlinePairSortMode == mode;
|
||||
if (ImGui.MenuItem(GetSortOnlineLabel(mode), string.Empty, selected))
|
||||
{
|
||||
if (!selected)
|
||||
{
|
||||
_configService.Current.OnlinePairSortMode = mode;
|
||||
_configService.Save();
|
||||
_mediator.Publish(new RefreshUiMessage());
|
||||
}
|
||||
|
||||
ImGui.CloseCurrentPopup();
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.EndPopup();
|
||||
}
|
||||
|
||||
return buttonStart - spacingX;
|
||||
}
|
||||
|
||||
private static string GetSortVisibleLabel(VisiblePairSortMode mode) => mode switch
|
||||
{
|
||||
VisiblePairSortMode.Alphabetical => "Alphabetical",
|
||||
VisiblePairSortMode.VramUsage => "VRAM usage (descending)",
|
||||
VisiblePairSortMode.EffectiveVramUsage => "Effective VRAM usage (descending)",
|
||||
VisiblePairSortMode.TriangleCount => "Triangle count (descending)",
|
||||
VisiblePairSortMode.EffectiveTriangleCount => "Effective triangle count (descending)",
|
||||
VisiblePairSortMode.PreferredDirectPairs => "Preferred permissions & Direct pairs",
|
||||
_ => "Default",
|
||||
};
|
||||
|
||||
private static string GetSortOnlineLabel(OnlinePairSortMode mode) => mode switch
|
||||
{
|
||||
OnlinePairSortMode.Alphabetical => "Alphabetical",
|
||||
OnlinePairSortMode.PreferredDirectPairs => "Preferred permissions & Direct pairs",
|
||||
_ => "Default",
|
||||
};
|
||||
}
|
||||
@@ -37,6 +37,7 @@ public class DrawUserPair
|
||||
private readonly UiSharedService _uiSharedService;
|
||||
private readonly PlayerPerformanceConfigService _performanceConfigService;
|
||||
private readonly LightlessConfigService _configService;
|
||||
private readonly LocationShareService _locationShareService;
|
||||
private readonly CharaDataManager _charaDataManager;
|
||||
private readonly PairLedger _pairLedger;
|
||||
private float _menuWidth = -1;
|
||||
@@ -57,6 +58,7 @@ public class DrawUserPair
|
||||
UiSharedService uiSharedService,
|
||||
PlayerPerformanceConfigService performanceConfigService,
|
||||
LightlessConfigService configService,
|
||||
LocationShareService locationShareService,
|
||||
CharaDataManager charaDataManager,
|
||||
PairLedger pairLedger)
|
||||
{
|
||||
@@ -74,6 +76,7 @@ public class DrawUserPair
|
||||
_uiSharedService = uiSharedService;
|
||||
_performanceConfigService = performanceConfigService;
|
||||
_configService = configService;
|
||||
_locationShareService = locationShareService;
|
||||
_charaDataManager = charaDataManager;
|
||||
_pairLedger = pairLedger;
|
||||
}
|
||||
@@ -133,6 +136,26 @@ public class DrawUserPair
|
||||
UiSharedService.AttachToolTip("This reapplies the last received character data to this character");
|
||||
}
|
||||
|
||||
var isPaused = _pair.UserPair!.OwnPermissions.IsPaused();
|
||||
if (!isPaused)
|
||||
{
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Pause, "Toggle Pause State", _menuWidth, true))
|
||||
{
|
||||
_ = _apiController.PauseAsync(_pair.UserData);
|
||||
ImGui.CloseCurrentPopup();
|
||||
}
|
||||
UiSharedService.AttachToolTip("Pauses syncing with this user.");
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Play, "Toggle Unpause State", _menuWidth, true))
|
||||
{
|
||||
_ = _apiController.UnpauseAsync(_pair.UserData);
|
||||
ImGui.CloseCurrentPopup();
|
||||
}
|
||||
UiSharedService.AttachToolTip("Resumes syncing with this user.");
|
||||
}
|
||||
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.PlayCircle, "Cycle pause state", _menuWidth, true))
|
||||
{
|
||||
_ = _apiController.CyclePauseAsync(_pair);
|
||||
@@ -196,6 +219,48 @@ public class DrawUserPair
|
||||
_ = _apiController.UserSetPairPermissions(new UserPermissionsDto(_pair.UserData, permissions));
|
||||
}
|
||||
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()
|
||||
@@ -364,6 +429,7 @@ public class DrawUserPair
|
||||
_pair.LastAppliedApproximateVRAMBytes,
|
||||
_pair.LastAppliedApproximateEffectiveVRAMBytes,
|
||||
_pair.LastAppliedDataTris,
|
||||
_pair.LastAppliedApproximateEffectiveTris,
|
||||
_pair.IsPaired,
|
||||
groupDisplays is null ? ImmutableArray<string>.Empty : ImmutableArray.CreateRange(groupDisplays));
|
||||
|
||||
@@ -379,6 +445,8 @@ public class DrawUserPair
|
||||
private static string BuildTooltip(in TooltipSnapshot snapshot)
|
||||
{
|
||||
var builder = new StringBuilder(256);
|
||||
static string FormatTriangles(long count) =>
|
||||
count > 1000 ? (count / 1000d).ToString("0.0'k'") : count.ToString();
|
||||
|
||||
if (snapshot.IsPaused)
|
||||
{
|
||||
@@ -445,9 +513,13 @@ public class DrawUserPair
|
||||
{
|
||||
builder.Append(Environment.NewLine);
|
||||
builder.Append("Approx. Triangle Count (excl. Vanilla): ");
|
||||
builder.Append(snapshot.LastAppliedDataTris > 1000
|
||||
? (snapshot.LastAppliedDataTris / 1000d).ToString("0.0'k'")
|
||||
: snapshot.LastAppliedDataTris);
|
||||
builder.Append(FormatTriangles(snapshot.LastAppliedDataTris));
|
||||
if (snapshot.LastAppliedApproximateEffectiveTris >= 0)
|
||||
{
|
||||
builder.Append(" (Effective: ");
|
||||
builder.Append(FormatTriangles(snapshot.LastAppliedApproximateEffectiveTris));
|
||||
builder.Append(')');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -479,11 +551,12 @@ public class DrawUserPair
|
||||
long LastAppliedApproximateVRAMBytes,
|
||||
long LastAppliedApproximateEffectiveVRAMBytes,
|
||||
long LastAppliedDataTris,
|
||||
long LastAppliedApproximateEffectiveTris,
|
||||
bool IsPaired,
|
||||
ImmutableArray<string> GroupDisplays)
|
||||
{
|
||||
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()
|
||||
@@ -554,6 +627,71 @@ public class DrawUserPair
|
||||
var individualVFXDisabled = (_pair.UserPair?.OwnPermissions.IsDisableVFX() ?? false) || (_pair.UserPair?.OtherPermissions.IsDisableVFX() ?? false);
|
||||
var individualIsSticky = _pair.UserPair!.OwnPermissions.IsSticky();
|
||||
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)
|
||||
{
|
||||
@@ -737,14 +875,19 @@ public class DrawUserPair
|
||||
}
|
||||
UiSharedService.AttachToolTip("Hold CTRL and click to remove user " + (_pair.UserData.AliasOrUID) + " from Syncshell");
|
||||
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.UserSlash, "Ban User", _menuWidth, true))
|
||||
var banEnabled = UiSharedService.CtrlPressed();
|
||||
var banLabel = banEnabled ? "Ban user" : "Ban user (Hold CTRL)";
|
||||
if (_uiSharedService.IconTextButton(FontAwesomeIcon.UserSlash, banLabel, _menuWidth, true) && banEnabled)
|
||||
{
|
||||
_mediator.Publish(new OpenBanUserPopupMessage(_pair, group));
|
||||
ImGui.CloseCurrentPopup();
|
||||
}
|
||||
UiSharedService.AttachToolTip("Ban user from this Syncshell");
|
||||
UiSharedService.AttachToolTip("Hold CTRL to ban user " + (_pair.UserData.AliasOrUID) + " from this Syncshell");
|
||||
|
||||
ImGui.Separator();
|
||||
if (showOwnerActions)
|
||||
{
|
||||
ImGui.Separator();
|
||||
}
|
||||
}
|
||||
|
||||
if (showOwnerActions)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -17,7 +17,7 @@ namespace LightlessSync.UI;
|
||||
public class DownloadUi : WindowMediatorSubscriberBase
|
||||
{
|
||||
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 FileUploadManager _fileTransferManager;
|
||||
private readonly UiSharedService _uiShared;
|
||||
@@ -164,9 +164,25 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
||||
const float rounding = 6f;
|
||||
var shadowOffset = new Vector2(2, 2);
|
||||
|
||||
foreach (var transfer in _currentDownloads.ToList())
|
||||
List<KeyValuePair<GameObjectHandler, IReadOnlyDictionary<string, FileDownloadStatus>>> transfers;
|
||||
try
|
||||
{
|
||||
transfers = _currentDownloads.ToList();
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
foreach (var transfer in transfers)
|
||||
{
|
||||
var transferKey = transfer.Key;
|
||||
|
||||
// Skip if no valid game object
|
||||
if (transferKey.GetGameObject() == null)
|
||||
continue;
|
||||
|
||||
var rawPos = _dalamudUtilService.WorldToScreen(transferKey.GetGameObject());
|
||||
|
||||
// If RawPos is zero, remove it from smoothed dictionary
|
||||
@@ -190,12 +206,16 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
||||
var dlQueue = 0;
|
||||
var dlProg = 0;
|
||||
var dlDecomp = 0;
|
||||
var dlComplete = 0;
|
||||
|
||||
foreach (var entry in transfer.Value)
|
||||
{
|
||||
var fileStatus = entry.Value;
|
||||
switch (fileStatus.DownloadStatus)
|
||||
{
|
||||
case DownloadStatus.Initializing:
|
||||
dlQueue++;
|
||||
break;
|
||||
case DownloadStatus.WaitingForSlot:
|
||||
dlSlot++;
|
||||
break;
|
||||
@@ -208,15 +228,20 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
||||
case DownloadStatus.Decompressing:
|
||||
dlDecomp++;
|
||||
break;
|
||||
case DownloadStatus.Completed:
|
||||
dlComplete++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var isAllComplete = dlComplete > 0 && dlProg == 0 && dlDecomp == 0 && dlQueue == 0 && dlSlot == 0;
|
||||
|
||||
string statusText;
|
||||
if (dlProg > 0)
|
||||
{
|
||||
statusText = "Downloading";
|
||||
}
|
||||
else if (dlDecomp > 0 || (totalBytes > 0 && transferredBytes >= totalBytes))
|
||||
else if (dlDecomp > 0)
|
||||
{
|
||||
statusText = "Decompressing";
|
||||
}
|
||||
@@ -228,6 +253,10 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
||||
{
|
||||
statusText = "Waiting for slot";
|
||||
}
|
||||
else if (isAllComplete)
|
||||
{
|
||||
statusText = "Completed";
|
||||
}
|
||||
else
|
||||
{
|
||||
statusText = "Waiting";
|
||||
@@ -293,7 +322,7 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
||||
fillPercent = transferredBytes / (double)totalBytes;
|
||||
showFill = true;
|
||||
}
|
||||
else if (dlDecomp > 0 || transferredBytes >= totalBytes)
|
||||
else if (dlDecomp > 0 || dlComplete > 0 || transferredBytes >= totalBytes)
|
||||
{
|
||||
fillPercent = 1.0;
|
||||
showFill = true;
|
||||
@@ -325,10 +354,14 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
||||
downloadText =
|
||||
$"{statusText} {UiSharedService.ByteToString(transferredBytes, addSuffix: false)}/{UiSharedService.ByteToString(totalBytes)}";
|
||||
}
|
||||
else if ((dlDecomp > 0 || transferredBytes >= totalBytes) && hasValidSize)
|
||||
else if (dlDecomp > 0)
|
||||
{
|
||||
downloadText = "Decompressing";
|
||||
}
|
||||
else if (isAllComplete)
|
||||
{
|
||||
downloadText = "Completed";
|
||||
}
|
||||
else
|
||||
{
|
||||
// Waiting states
|
||||
@@ -401,6 +434,7 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
||||
var totalDlQueue = 0;
|
||||
var totalDlProg = 0;
|
||||
var totalDlDecomp = 0;
|
||||
var totalDlComplete = 0;
|
||||
|
||||
var perPlayer = new List<(
|
||||
string Name,
|
||||
@@ -412,7 +446,8 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
||||
int DlSlot,
|
||||
int DlQueue,
|
||||
int DlProg,
|
||||
int DlDecomp)>();
|
||||
int DlDecomp,
|
||||
int DlComplete)>();
|
||||
|
||||
foreach (var transfer in _currentDownloads)
|
||||
{
|
||||
@@ -434,12 +469,17 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
||||
var playerDlQueue = 0;
|
||||
var playerDlProg = 0;
|
||||
var playerDlDecomp = 0;
|
||||
var playerDlComplete = 0;
|
||||
|
||||
foreach (var entry in transfer.Value)
|
||||
{
|
||||
var fileStatus = entry.Value;
|
||||
switch (fileStatus.DownloadStatus)
|
||||
{
|
||||
case DownloadStatus.Initializing:
|
||||
playerDlQueue++;
|
||||
totalDlQueue++;
|
||||
break;
|
||||
case DownloadStatus.WaitingForSlot:
|
||||
playerDlSlot++;
|
||||
totalDlSlot++;
|
||||
@@ -456,6 +496,10 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
||||
playerDlDecomp++;
|
||||
totalDlDecomp++;
|
||||
break;
|
||||
case DownloadStatus.Completed:
|
||||
playerDlComplete++;
|
||||
totalDlComplete++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -481,7 +525,8 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
||||
playerDlSlot,
|
||||
playerDlQueue,
|
||||
playerDlProg,
|
||||
playerDlDecomp
|
||||
playerDlDecomp,
|
||||
playerDlComplete
|
||||
));
|
||||
}
|
||||
|
||||
@@ -505,7 +550,7 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
||||
|
||||
// Overall texts
|
||||
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 =
|
||||
$"{UiSharedService.ByteToString(transferredBytes, addSuffix: false)}/{UiSharedService.ByteToString(totalBytes)}";
|
||||
@@ -528,7 +573,7 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
||||
foreach (var p in perPlayer)
|
||||
{
|
||||
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);
|
||||
if (lineSize.X > contentWidth)
|
||||
@@ -646,7 +691,7 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
||||
&& p.TransferredBytes > 0;
|
||||
|
||||
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)
|
||||
{
|
||||
@@ -705,13 +750,18 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
||||
// Text inside bar: downloading vs decompressing
|
||||
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)
|
||||
{
|
||||
// Keep bar full, static text showing decompressing
|
||||
barText = "Decompressing...";
|
||||
}
|
||||
else if (isAllComplete)
|
||||
{
|
||||
barText = "Completed";
|
||||
}
|
||||
else
|
||||
{
|
||||
var bytesInside =
|
||||
@@ -792,6 +842,7 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
||||
var dlQueue = 0;
|
||||
var dlProg = 0;
|
||||
var dlDecomp = 0;
|
||||
var dlComplete = 0;
|
||||
long totalBytes = 0;
|
||||
long transferredBytes = 0;
|
||||
|
||||
@@ -801,22 +852,29 @@ public class DownloadUi : WindowMediatorSubscriberBase
|
||||
var fileStatus = entry.Value;
|
||||
switch (fileStatus.DownloadStatus)
|
||||
{
|
||||
case DownloadStatus.Initializing: dlQueue++; break;
|
||||
case DownloadStatus.WaitingForSlot: dlSlot++; break;
|
||||
case DownloadStatus.WaitingForQueue: dlQueue++; break;
|
||||
case DownloadStatus.Downloading: dlProg++; break;
|
||||
case DownloadStatus.Decompressing: dlDecomp++; break;
|
||||
case DownloadStatus.Completed: dlComplete++; break;
|
||||
}
|
||||
totalBytes += fileStatus.TotalBytes;
|
||||
transferredBytes += fileStatus.TransferredBytes;
|
||||
}
|
||||
|
||||
var progress = totalBytes > 0 ? (float)transferredBytes / totalBytes : 0f;
|
||||
if (dlComplete > 0 && dlProg == 0 && dlDecomp == 0 && dlQueue == 0 && dlSlot == 0)
|
||||
{
|
||||
progress = 1f;
|
||||
}
|
||||
|
||||
string status;
|
||||
if (dlDecomp > 0) status = "decompressing";
|
||||
else if (dlProg > 0) status = "downloading";
|
||||
else if (dlQueue > 0) status = "queued";
|
||||
else if (dlSlot > 0) status = "waiting";
|
||||
else if (dlComplete > 0) status = "completed";
|
||||
else status = "completed";
|
||||
|
||||
downloadStatus.Add((item.Key.Name, progress, status));
|
||||
|
||||
@@ -29,6 +29,7 @@ public class DrawEntityFactory
|
||||
private readonly LightlessConfigService _configService;
|
||||
private readonly UiSharedService _uiSharedService;
|
||||
private readonly PlayerPerformanceConfigService _playerPerformanceConfigService;
|
||||
private readonly LocationShareService _locationShareService;
|
||||
private readonly CharaDataManager _charaDataManager;
|
||||
private readonly SelectTagForPairUi _selectTagForPairUi;
|
||||
private readonly RenamePairTagUi _renamePairTagUi;
|
||||
@@ -53,6 +54,7 @@ public class DrawEntityFactory
|
||||
LightlessConfigService configService,
|
||||
UiSharedService uiSharedService,
|
||||
PlayerPerformanceConfigService playerPerformanceConfigService,
|
||||
LocationShareService locationShareService,
|
||||
CharaDataManager charaDataManager,
|
||||
SelectTagForSyncshellUi selectTagForSyncshellUi,
|
||||
RenameSyncshellTagUi renameSyncshellTagUi,
|
||||
@@ -72,6 +74,7 @@ public class DrawEntityFactory
|
||||
_configService = configService;
|
||||
_uiSharedService = uiSharedService;
|
||||
_playerPerformanceConfigService = playerPerformanceConfigService;
|
||||
_locationShareService = locationShareService;
|
||||
_charaDataManager = charaDataManager;
|
||||
_selectTagForSyncshellUi = selectTagForSyncshellUi;
|
||||
_renameSyncshellTagUi = renameSyncshellTagUi;
|
||||
@@ -162,6 +165,7 @@ public class DrawEntityFactory
|
||||
_uiSharedService,
|
||||
_playerPerformanceConfigService,
|
||||
_configService,
|
||||
_locationShareService,
|
||||
_charaDataManager,
|
||||
_pairLedger);
|
||||
}
|
||||
@@ -213,6 +217,7 @@ public class DrawEntityFactory
|
||||
entry.PairStatus,
|
||||
handler?.LastAppliedDataBytes ?? -1,
|
||||
handler?.LastAppliedDataTris ?? -1,
|
||||
handler?.LastAppliedApproximateEffectiveTris ?? -1,
|
||||
handler?.LastAppliedApproximateVRAMBytes ?? -1,
|
||||
handler?.LastAppliedApproximateEffectiveVRAMBytes ?? -1,
|
||||
handler);
|
||||
|
||||
@@ -46,10 +46,12 @@ public sealed class DtrEntry : IDisposable, IHostedService
|
||||
private string? _lightfinderText;
|
||||
private string? _lightfinderTooltip;
|
||||
private Colors _lightfinderColors;
|
||||
private readonly object _localHashedCidLock = new();
|
||||
private string? _localHashedCid;
|
||||
private DateTime _localHashedCidFetchedAt = DateTime.MinValue;
|
||||
private DateTime _localHashedCidNextErrorLog = DateTime.MinValue;
|
||||
private DateTime _pairRequestNextErrorLog = DateTime.MinValue;
|
||||
private int _localHashedCidRefreshActive;
|
||||
|
||||
public DtrEntry(
|
||||
ILogger<DtrEntry> logger,
|
||||
@@ -101,10 +103,19 @@ public sealed class DtrEntry : IDisposable, IHostedService
|
||||
|
||||
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
|
||||
{
|
||||
await _runTask!.ConfigureAwait(false);
|
||||
if (_runTask != null)
|
||||
await _runTask.ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
@@ -339,29 +350,61 @@ public sealed class DtrEntry : IDisposable, IHostedService
|
||||
private string? GetLocalHashedCid()
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
if (_localHashedCid is not null && now - _localHashedCidFetchedAt < _localHashedCidCacheDuration)
|
||||
return _localHashedCid;
|
||||
|
||||
try
|
||||
lock (_localHashedCidLock)
|
||||
{
|
||||
var cid = _dalamudUtilService.GetCIDAsync().GetAwaiter().GetResult();
|
||||
var hashedCid = cid.ToString().GetHash256();
|
||||
_localHashedCid = hashedCid;
|
||||
_localHashedCidFetchedAt = now;
|
||||
return hashedCid;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (now >= _localHashedCidNextErrorLog)
|
||||
if (_localHashedCid is not null && now - _localHashedCidFetchedAt < _localHashedCidCacheDuration)
|
||||
{
|
||||
_logger.LogDebug(ex, "Failed to refresh local hashed CID for Lightfinder DTR entry.");
|
||||
_localHashedCidNextErrorLog = now + _localHashedCidErrorCooldown;
|
||||
return _localHashedCid;
|
||||
}
|
||||
|
||||
_localHashedCid = null;
|
||||
_localHashedCidFetchedAt = now;
|
||||
return null;
|
||||
}
|
||||
|
||||
QueueLocalHashedCidRefresh();
|
||||
|
||||
lock (_localHashedCidLock)
|
||||
{
|
||||
return _localHashedCid;
|
||||
}
|
||||
}
|
||||
|
||||
private void QueueLocalHashedCidRefresh()
|
||||
{
|
||||
if (Interlocked.Exchange(ref _localHashedCidRefreshActive, 1) != 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var cid = _dalamudUtilService.GetCID();
|
||||
var hashedCid = cid.ToString().GetHash256();
|
||||
lock (_localHashedCidLock)
|
||||
{
|
||||
_localHashedCid = hashedCid;
|
||||
_localHashedCidFetchedAt = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
lock (_localHashedCidLock)
|
||||
{
|
||||
if (now >= _localHashedCidNextErrorLog)
|
||||
{
|
||||
_logger.LogDebug(ex, "Failed to refresh local hashed CID for Lightfinder DTR entry.");
|
||||
_localHashedCidNextErrorLog = now + _localHashedCidErrorCooldown;
|
||||
}
|
||||
|
||||
_localHashedCid = null;
|
||||
_localHashedCidFetchedAt = now;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
Interlocked.Exchange(ref _localHashedCidRefreshActive, 0);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private List<string> GetNearbyBroadcasts()
|
||||
|
||||
@@ -415,7 +415,9 @@ public class IdDisplayHandler
|
||||
var vramBytes = pair.LastAppliedApproximateEffectiveVRAMBytes >= 0
|
||||
? pair.LastAppliedApproximateEffectiveVRAMBytes
|
||||
: pair.LastAppliedApproximateVRAMBytes;
|
||||
var triangleCount = pair.LastAppliedDataTris;
|
||||
var triangleCount = pair.LastAppliedApproximateEffectiveTris >= 0
|
||||
? pair.LastAppliedApproximateEffectiveTris
|
||||
: pair.LastAppliedDataTris;
|
||||
if (vramBytes < 0 && triangleCount < 0)
|
||||
{
|
||||
return null;
|
||||
|
||||
@@ -23,6 +23,7 @@ namespace LightlessSync.UI
|
||||
private readonly LightFinderService _broadcastService;
|
||||
private readonly UiSharedService _uiSharedService;
|
||||
private readonly LightFinderScannerService _broadcastScannerService;
|
||||
private readonly LightFinderPlateHandler _lightFinderPlateHandler;
|
||||
|
||||
private IReadOnlyList<GroupFullInfoDto> _allSyncshells = Array.Empty<GroupFullInfoDto>();
|
||||
private string _userUid = string.Empty;
|
||||
@@ -38,7 +39,8 @@ namespace LightlessSync.UI
|
||||
UiSharedService uiShared,
|
||||
ApiController apiController,
|
||||
LightFinderScannerService broadcastScannerService
|
||||
) : base(logger, mediator, "Lightfinder###LightlessLightfinderUI", performanceCollectorService)
|
||||
,
|
||||
LightFinderPlateHandler lightFinderPlateHandler) : base(logger, mediator, "Lightfinder###LightlessLightfinderUI", performanceCollectorService)
|
||||
{
|
||||
_broadcastService = broadcastService;
|
||||
_uiSharedService = uiShared;
|
||||
@@ -50,6 +52,7 @@ namespace LightlessSync.UI
|
||||
WindowBuilder.For(this)
|
||||
.SetSizeConstraints(new Vector2(600, 465), new Vector2(750, 525))
|
||||
.Apply();
|
||||
_lightFinderPlateHandler = lightFinderPlateHandler;
|
||||
}
|
||||
|
||||
private void RebuildSyncshellDropdownOptions()
|
||||
@@ -301,6 +304,14 @@ namespace LightlessSync.UI
|
||||
bool ShellFinderEnabled = _configService.Current.SyncshellFinderEnabled;
|
||||
bool isBroadcasting = _broadcastService.IsBroadcasting;
|
||||
|
||||
if (isBroadcasting)
|
||||
{
|
||||
var warningColor = UIColors.Get("LightlessYellow");
|
||||
_uiSharedService.DrawNoteLine("! ", warningColor,
|
||||
new SeStringUtils.RichTextEntry("Syncshell Finder can only be changed while Lightfinder is disabled.", warningColor));
|
||||
ImGuiHelpers.ScaledDummy(0.2f);
|
||||
}
|
||||
|
||||
if (isBroadcasting)
|
||||
ImGui.BeginDisabled();
|
||||
|
||||
@@ -372,9 +383,47 @@ namespace LightlessSync.UI
|
||||
#if DEBUG
|
||||
if (ImGui.BeginTabItem("Debug"))
|
||||
{
|
||||
if (ImGui.CollapsingHeader("LightFinder Plates", ImGuiTreeNodeFlags.DefaultOpen))
|
||||
{
|
||||
var h = _lightFinderPlateHandler;
|
||||
|
||||
var enabled = h.DebugEnabled;
|
||||
if (ImGui.Checkbox("Enable LightFinder debug", ref enabled))
|
||||
h.DebugEnabled = enabled;
|
||||
|
||||
if (h.DebugEnabled)
|
||||
{
|
||||
ImGui.Indent();
|
||||
|
||||
var disableOcc = h.DebugDisableOcclusion;
|
||||
if (ImGui.Checkbox("Disable occlusion (force draw)", ref disableOcc))
|
||||
h.DebugDisableOcclusion = disableOcc;
|
||||
|
||||
var drawUiRects = h.DebugDrawUiRects;
|
||||
if (ImGui.Checkbox("Draw UI rects", ref drawUiRects))
|
||||
h.DebugDrawUiRects = drawUiRects;
|
||||
|
||||
var drawLabelRects = h.DebugDrawLabelRects;
|
||||
if (ImGui.Checkbox("Draw label rects", ref drawLabelRects))
|
||||
h.DebugDrawLabelRects = drawLabelRects;
|
||||
|
||||
ImGui.Separator();
|
||||
ImGui.TextUnformatted($"Labels last frame: {h.DebugLabelCountLastFrame}");
|
||||
ImGui.TextUnformatted($"UI rects last frame: {h.DebugUiRectCountLastFrame}");
|
||||
ImGui.TextUnformatted($"Occluded last frame: {h.DebugOccludedCountLastFrame}");
|
||||
ImGui.TextUnformatted($"Last NamePlate frame: {h.DebugLastNameplateFrame}");
|
||||
|
||||
ImGui.Unindent();
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.Separator();
|
||||
|
||||
ImGui.Text("Broadcast Cache");
|
||||
|
||||
if (ImGui.BeginTable("##BroadcastCacheTable", 4, ImGuiTableFlags.RowBg | ImGuiTableFlags.Borders | ImGuiTableFlags.ScrollY, new Vector2(-1, 225f)))
|
||||
if (ImGui.BeginTable("##BroadcastCacheTable", 4,
|
||||
ImGuiTableFlags.RowBg | ImGuiTableFlags.Borders | ImGuiTableFlags.ScrollY,
|
||||
new Vector2(-1, 225f)))
|
||||
{
|
||||
ImGui.TableSetupColumn("CID", ImGuiTableColumnFlags.WidthStretch);
|
||||
ImGui.TableSetupColumn("IsBroadcasting", ImGuiTableColumnFlags.WidthStretch);
|
||||
|
||||
7
LightlessSync/UI/Models/OnlinePairSortMode.cs
Normal file
7
LightlessSync/UI/Models/OnlinePairSortMode.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace LightlessSync.UI.Models;
|
||||
|
||||
public enum OnlinePairSortMode
|
||||
{
|
||||
Alphabetical = 0,
|
||||
PreferredDirectPairs = 1,
|
||||
}
|
||||
@@ -21,6 +21,7 @@ public sealed record PairUiEntry(
|
||||
IndividualPairStatus? PairStatus,
|
||||
long LastAppliedDataBytes,
|
||||
long LastAppliedDataTris,
|
||||
long LastAppliedApproximateEffectiveTris,
|
||||
long LastAppliedApproximateVramBytes,
|
||||
long LastAppliedApproximateEffectiveVramBytes,
|
||||
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
|
||||
}
|
||||
@@ -2,10 +2,10 @@ namespace LightlessSync.UI.Models;
|
||||
|
||||
public enum VisiblePairSortMode
|
||||
{
|
||||
Default = 0,
|
||||
Alphabetical = 1,
|
||||
VramUsage = 2,
|
||||
EffectiveVramUsage = 3,
|
||||
TriangleCount = 4,
|
||||
PreferredDirectPairs = 5,
|
||||
Alphabetical = 0,
|
||||
VramUsage = 1,
|
||||
EffectiveVramUsage = 2,
|
||||
TriangleCount = 3,
|
||||
PreferredDirectPairs = 4,
|
||||
EffectiveTriangleCount = 5,
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -11,6 +11,7 @@ using LightlessSync.Services.ServerConfiguration;
|
||||
using LightlessSync.UI.Services;
|
||||
using LightlessSync.UI.Tags;
|
||||
using LightlessSync.Utils;
|
||||
using LightlessSync.WebAPI;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Numerics;
|
||||
|
||||
@@ -22,6 +23,7 @@ public class StandaloneProfileUi : WindowMediatorSubscriberBase
|
||||
private readonly PairUiService _pairUiService;
|
||||
private readonly ServerConfigurationManager _serverManager;
|
||||
private readonly ProfileTagService _profileTagService;
|
||||
private readonly ApiController _apiController;
|
||||
private readonly UiSharedService _uiSharedService;
|
||||
private readonly UserData? _userData;
|
||||
private readonly GroupData? _groupData;
|
||||
@@ -60,7 +62,8 @@ public class StandaloneProfileUi : WindowMediatorSubscriberBase
|
||||
GroupData? groupData,
|
||||
bool isLightfinderContext,
|
||||
string? lightfinderCid,
|
||||
PerformanceCollectorService performanceCollector)
|
||||
PerformanceCollectorService performanceCollector,
|
||||
ApiController apiController)
|
||||
: base(logger, mediator, BuildWindowTitle(
|
||||
userData,
|
||||
groupData,
|
||||
@@ -94,6 +97,7 @@ public class StandaloneProfileUi : WindowMediatorSubscriberBase
|
||||
.Apply();
|
||||
|
||||
IsOpen = true;
|
||||
_apiController = apiController;
|
||||
}
|
||||
|
||||
public Pair? Pair { get; }
|
||||
@@ -248,19 +252,33 @@ public class StandaloneProfileUi : WindowMediatorSubscriberBase
|
||||
ResetBannerTexture();
|
||||
_lastBannerPicture = bannerBytes;
|
||||
}
|
||||
|
||||
string? noteText = null;
|
||||
string statusLabel = _isLightfinderContext ? "Exploring" : "Offline";
|
||||
|
||||
var isSelfProfile = !_isLightfinderContext
|
||||
&& _userData is not null
|
||||
&& !string.IsNullOrEmpty(_apiController.UID)
|
||||
&& string.Equals(_userData.UID, _apiController.UID, StringComparison.Ordinal);
|
||||
|
||||
string statusLabel = _isLightfinderContext
|
||||
? "Exploring"
|
||||
: isSelfProfile ? "Online" : "Offline";
|
||||
|
||||
string? visiblePlayerName = null;
|
||||
bool directPair = false;
|
||||
bool youPaused = false;
|
||||
bool theyPaused = false;
|
||||
List<string> syncshellLines = [];
|
||||
|
||||
if (!_isLightfinderContext)
|
||||
{
|
||||
noteText = _serverManager.GetNoteForUid(_userData!.UID);
|
||||
}
|
||||
|
||||
if (!_isLightfinderContext && Pair != null)
|
||||
{
|
||||
var snapshot = _pairUiService.GetSnapshot();
|
||||
noteText = _serverManager.GetNoteForUid(Pair.UserData.UID);
|
||||
|
||||
statusLabel = Pair.IsVisible ? "Visible" : (Pair.IsOnline ? "Online" : "Offline");
|
||||
visiblePlayerName = Pair.IsVisible ? Pair.PlayerName : null;
|
||||
|
||||
@@ -282,11 +300,15 @@ public class StandaloneProfileUi : WindowMediatorSubscriberBase
|
||||
var groupLabel = snapshot.GroupsByGid.TryGetValue(gid, out var groupInfo)
|
||||
? groupInfo.GroupAliasOrGID
|
||||
: gid;
|
||||
|
||||
var groupNote = _serverManager.GetNoteForGid(gid);
|
||||
syncshellLines.Add(string.IsNullOrEmpty(groupNote) ? groupLabel : $"{groupNote} ({groupLabel})");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isSelfProfile)
|
||||
statusLabel = "Online";
|
||||
}
|
||||
|
||||
var presenceTokens = new List<PresenceToken>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user