Compare commits

..

69 Commits

Author SHA1 Message Date
17dd8a307b fix access violation 2026-01-06 00:42:44 +09:00
24d0c38f59 animated emotes and fix clipper in chat window 2026-01-05 22:43:50 +09:00
4da2548e03 just misc 2026-01-05 07:48:14 +09:00
b6b9c81a57 tighten the check 2026-01-03 13:53:55 +09:00
a824d94ffe slight adjustments and fixes 2026-01-03 10:20:07 +09:00
4b13dfe8d4 skip decimation for direct pairs and make it a toggle in settings 2026-01-03 03:19:10 +09:00
979443d9bb *atomize* download status! 2026-01-02 11:30:36 +09:00
92cb861710 readjust cache clean up and add keep originals setting for models 2026-01-02 11:04:15 +09:00
aeed8503c2 highly experimental runtime model decimation + file cache adjustment to clean up processed file copies 2026-01-02 09:54:34 +09:00
44bb53023e improvements to data analysis ui 2026-01-01 13:04:52 +09:00
05b91ed243 Merge remote-tracking branch 'origin/2.0.3' into just-experimenting 2026-01-01 10:04:38 +09:00
7e61954541 Location Sharing 2.0 (#125)
Need: Lightless-Sync/LightlessServer#49
Authored-by: Tsubasahane <wozaiha@gmail.com>
Reviewed-on: #125
Reviewed-by: cake <cake@noreply.git.lightless-sync.org>
Co-authored-by: Tsubasa <tsubasa@noreply.git.lightless-sync.org>
Co-committed-by: Tsubasa <tsubasa@noreply.git.lightless-sync.org>
2025-12-31 17:31:31 +00:00
cfc5c1e0f3 "improving" pair handler clean up and some other stuff 2026-01-01 00:33:24 +09:00
bbb3375661 2.0.3 staaato 2025-12-31 02:44:31 +00:00
ed7932ab83 2.0.2
All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 2m3s
Reviewed-on: #106
2025-12-31 02:29:36 +00:00
4eaaaf694c Merge pull request 'Complete Decompression after try.' (#122) from decompression-bullshit into 2.0.2
Reviewed-on: #122
2025-12-30 15:25:55 +00:00
defnotken
c32c89d1a8 Complete Decompression after try. 2025-12-30 08:52:59 -06:00
a8b58d05d6 Merge pull request 'pair-adapter-debug' (#121) from pair-adapter-debug into 2.0.2
Reviewed-on: #121
2025-12-30 14:29:53 +00:00
9ea0571e82 Lower Time out 2025-12-30 14:29:38 +00:00
cake
308c220735 Fixed auto prune options locked 2025-12-30 02:08:54 +01:00
defnotken
27d4da4615 thought a variable was unused. 2025-12-29 08:47:51 -06:00
defnotken
6b49c92ef9 Add a timeout to prevent deadlock of application data 2025-12-29 08:41:32 -06:00
cake
6d20995dbf Added decompression gate to decompress files 2025-12-29 02:50:49 +01:00
cake
cf495dc826 Merge branch '2.0.2' of https://git.lightless-sync.org/Lightless-Sync/LightlessClient into 2.0.2 2025-12-28 16:28:37 +01:00
cake
08050614da Own profiles are shown as online now. 2025-12-28 16:28:27 +01:00
94f520d0e7 Add Serious Warning about nameplates (#118)
Co-authored-by: defnotken <itsdefnotken@gmail.com>
Reviewed-on: #118
Co-authored-by: defnotken <defnotken@noreply.git.lightless-sync.org>
Co-committed-by: defnotken <defnotken@noreply.git.lightless-sync.org>
2025-12-28 15:13:40 +00:00
474fd5ef11 Merge pull request 'Fix Async hiccup in chat.' (#117) from quick-chat-fix into 2.0.2
Reviewed-on: #117
Reviewed-by: cake <cake@noreply.git.lightless-sync.org>
2025-12-28 03:58:35 +00:00
defnotken
759066731e Fix Async hiccup in chat. 2025-12-27 21:57:01 -06:00
defnotken
ff88e5f856 Merge branch '2.0.2' of https://git.lightless-sync.org/Lightless-Sync/LightlessClient into 2.0.2 2025-12-27 21:38:44 -06:00
defnotken
61bac0d39d Changelog update 2025-12-27 21:38:11 -06:00
5b3d00b90a API14 Updates - Migrate to IPlayerState (#113)
- use IPlayerState for DalamudUtilService and make things less async
- make LocationInfo work with ContentFinderData

Co-authored-by: Tsubasahane <wozaiha@gmail.com>
Co-authored-by: defnotken <itsdefnotken@gmail.com>
Reviewed-on: #113
Reviewed-by: cake <cake@noreply.git.lightless-sync.org>
Co-authored-by: Tsubasa <tsubasa@noreply.git.lightless-sync.org>
Co-committed-by: Tsubasa <tsubasa@noreply.git.lightless-sync.org>
2025-12-28 03:26:07 +00:00
e14d50674d Update World Data/Job Data to client lang (#115)
Co-authored-by: defnotken <itsdefnotken@gmail.com>
Reviewed-on: #115
Reviewed-by: cake <cake@noreply.git.lightless-sync.org>
Co-authored-by: defnotken <defnotken@noreply.git.lightless-sync.org>
Co-committed-by: defnotken <defnotken@noreply.git.lightless-sync.org>
2025-12-28 02:46:47 +00:00
129cf14151 Merge pull request 'Fixed chat input not clearing after sending.' (#116) from fix-chat-send into 2.0.2
Reviewed-on: #116
2025-12-28 02:46:21 +00:00
cake
dba04d740b Fixed chat input not clearing after sending. 2025-12-28 03:45:02 +01:00
04d7a66317 Merge pull request 'Fixed some occlusion checking on invincible elements, added debug mode imgui lightfinder, added caching file cache.' (#114) from debug-mode-lightfinder-imgui into 2.0.2
Reviewed-on: #114
2025-12-28 02:24:50 +00:00
cake
2abc92fc61 Fixed warnings 2025-12-28 03:17:27 +01:00
cake
a3ea48c6e1 Fixed some comments 2025-12-28 03:15:15 +01:00
cake
deb99628f6 Added debug mode for lightfinder IMGUI, added caching of file cache entries to reduce load of loading all entries again. 2025-12-28 03:01:02 +01:00
f69effb8a3 fix syncing.. 2025-12-28 10:48:40 +09:00
8f32b375dd boom 2025-12-28 05:24:12 +09:00
1632258c4f Merge pull request 'mcdf-background-creation' (#112) from mcdf-background-creation into 2.0.2
Reviewed-on: #112
2025-12-27 01:33:31 +00:00
a5786e1d5b Merge branch '2.0.2' into mcdf-background-creation 2025-12-26 21:20:09 +00:00
0b32639f99 Added chat notification pair request send (#111)
Co-authored-by: cake <admin@cakeandbanana.nl>
Reviewed-on: #111
Reviewed-by: defnotken <defnotken@noreply.git.lightless-sync.org>
2025-12-26 20:43:19 +00:00
65dea18f5f Added count to lightfinder label (#110)
[[https://lightless.media/u/3J6Um2OI.png](url)](https://lightless.media/u/3J6Um2OI.png)

Co-authored-by: cake <admin@cakeandbanana.nl>
Reviewed-on: #110
Reviewed-by: defnotken <defnotken@noreply.git.lightless-sync.org>
2025-12-26 20:43:09 +00:00
6546a658f3 Added temporary storage of guids of collections to be wiped on bootup when crash/reload (#109)
Co-authored-by: cake <admin@cakeandbanana.nl>
Reviewed-on: #109
Reviewed-by: defnotken <defnotken@noreply.git.lightless-sync.org>
2025-12-26 20:43:01 +00:00
8a41baa88b Fix context menu option from settings. (#108)
Co-authored-by: cake <admin@cakeandbanana.nl>
Reviewed-on: #108
Reviewed-by: defnotken <defnotken@noreply.git.lightless-sync.org>
2025-12-26 20:42:51 +00:00
88cb778791 Refactored most of file download, redone it so correct usage of slots and better thread management. (#107)
Before: https://lightless.media/u/n5DhLTPR.mp4

After: https://lightless.media/u/sqvDR0Ho.mp4

Usage of the locks is way more optimized.

Co-authored-by: cake <admin@cakeandbanana.nl>
Reviewed-on: #107
Reviewed-by: defnotken <defnotken@noreply.git.lightless-sync.org>
2025-12-26 20:42:43 +00:00
defnotken
6892d81041 Add notifications 2025-12-24 01:34:37 -06:00
defnotken
a47ca4452a oops 2025-12-24 01:06:49 -06:00
defnotken
32df21bf4a Make mcdf safe in the background. WIP: Progress notif 2025-12-24 01:05:21 -06:00
defnotken
1a2885fd74 ver bump 2025-12-23 20:12:39 -06:00
e470222fe6 Merge pull request '2.0.1' (#99) from 2.0.1 into master
All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 2m3s
Reviewed-on: #99
2025-12-24 02:11:07 +00:00
defnotken
eb83ca90cb Changelog 2025-12-23 20:06:39 -06:00
defnotken
35f0f6da5e removing unused call
Some checks failed
Tag and Release Lightless / tag-and-release (push) Has been cancelled
2025-12-23 17:46:13 -06:00
7d151dac2b hopefully it's fine now? 2025-12-24 07:43:23 +09:00
defnotken
2eba5a1f30 Bumping API 2025-12-23 11:20:52 -06:00
0a6cb05883 Merge pull request 'Fix bug with GPose Actors Page not showing up' (#105) from brio-actor-page-fix into 2.0.1
Reviewed-on: #105
2025-12-22 16:01:57 +00:00
Minmoose
838495810e Update DalamudUtilService.cs 2025-12-22 09:59:35 -06:00
a207c8994b Merge pull request 'Added documentation/comments, Added gpose detection.' (#104) from documentation-gpose-lightfinder-plate into 2.0.1
Reviewed-on: #104
2025-12-22 15:00:12 +00:00
cake
9b4e48ad3e Added documentation/comments, Added gpose detection. 2025-12-22 15:40:31 +01:00
fb4810980e Merge pull request 'Fixed many issues with lightfinder, added back icon support' (#102) from lightfinder-changes-picto into 2.0.1
Reviewed-on: #102
2025-12-22 03:34:27 +00:00
51e107d30a Merge pull request 'Added null check on GetCid if it is empty, would return zero' (#103) from fix-cid-settings into 2.0.1
Reviewed-on: #103
Reviewed-by: defnotken <defnotken@noreply.git.lightless-sync.org>
2025-12-22 03:09:39 +00:00
cake
cc011743af Added null check on GetCid if it is empty, would return zero 2025-12-22 03:36:06 +01:00
cake
f47fbda0d9 Fixed many issues with lightfinder, added back icon support 2025-12-22 03:06:07 +01:00
fd3b42eff1 Merge pull request 'Null reference exception fix for setting' (#101) from null-reference-validation into 2.0.1
Reviewed-on: #101
2025-12-21 22:53:44 +00:00
3262664d1c Merge branch '2.0.1' into null-reference-validation 2025-12-21 22:42:50 +00:00
afa0d9f101 Merge pull request 'imgui push/pop' (#100) from updatenotes-changes into 2.0.1
Reviewed-on: #100
2025-12-21 22:41:50 +00:00
defnotken
a66a9407f5 Null reference exception fix for setting 2025-12-21 16:36:23 -06:00
choco
34bbc34b5b imgui push/pop 2025-12-21 20:56:15 +01:00
111 changed files with 20270 additions and 2772 deletions

3
.gitignore vendored
View File

@@ -348,3 +348,6 @@ MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/
# idea
/.idea

View File

@@ -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. Youll 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 arent 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: ""

View File

@@ -103,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);
@@ -441,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++;
}
}
@@ -559,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;

View File

@@ -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);
}

View File

@@ -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)

View File

@@ -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,22 +349,24 @@ 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)
@@ -375,18 +374,15 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
if (descriptor.IsInGpose)
return;
if (!TryResolveObjectKind(descriptor, out var resolvedKind))
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)
{
@@ -465,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;
}
@@ -523,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;
}
@@ -539,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);
}
}
@@ -550,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 });
}
}
@@ -622,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);

View File

@@ -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;

View File

@@ -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()

View File

@@ -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;
}
}

View File

@@ -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);
}

View File

@@ -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()

View File

@@ -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");

View File

@@ -10,6 +10,8 @@ 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;

View File

@@ -51,6 +51,7 @@ public class LightlessConfig : ILightlessConfiguration
public bool PreferNotesOverNamesForVisible { get; set; } = false;
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;
@@ -140,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;
@@ -154,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; } = [];
}

View File

@@ -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;
}

View File

@@ -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();
}
}
}

View File

@@ -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;
}

View File

@@ -74,6 +74,7 @@ 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;
@@ -81,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;
}
@@ -108,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;
}

View File

@@ -3,7 +3,7 @@
<PropertyGroup>
<Authors></Authors>
<Company></Company>
<Version>2.0.1</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" />

View File

@@ -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);
}
}

View File

@@ -146,7 +146,8 @@ 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();
@@ -194,7 +195,9 @@ 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);
if (logDebug)
{
@@ -373,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);
}
}

View File

@@ -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;

View File

@@ -25,6 +25,11 @@
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; }

View File

@@ -16,4 +16,5 @@ public interface IPairPerformanceSubject
long LastAppliedApproximateVRAMBytes { get; set; }
long LastAppliedApproximateEffectiveVRAMBytes { get; set; }
long LastAppliedDataTris { get; set; }
long LastAppliedApproximateEffectiveTris { get; set; }
}

View File

@@ -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;
@@ -218,6 +239,11 @@ public class Pair
handler.IsApplying,
handler.IsDownloading,
handler.PendingDownloadCount,
handler.ForbiddenDownloadCount);
handler.ForbiddenDownloadCount,
handler.PendingModReapply,
handler.ModApplyDeferred,
handler.MissingCriticalMods,
handler.MissingNonCriticalMods,
handler.MissingForbiddenMods);
}
}

View File

@@ -125,6 +125,7 @@ public sealed partial class PairCoordinator
}
}
_mediator.Publish(new PairOnlineMessage(new PairUniqueIdentifier(dto.User.UID)));
PublishPairDataChanged();
}

View File

@@ -16,7 +16,12 @@ public sealed record PairDebugInfo(
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,
@@ -34,5 +39,10 @@ public sealed record PairDebugInfo(
false,
false,
0,
0,
false,
false,
0,
0,
0);
}

File diff suppressed because it is too large Load Diff

View File

@@ -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);
}
}

View File

@@ -89,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);
}

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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)];
}

View File

@@ -40,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;
@@ -51,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))
@@ -104,7 +106,9 @@ 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);
@@ -115,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>();
@@ -133,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));
@@ -171,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>>(),
@@ -201,6 +212,7 @@ public sealed class Plugin : IDalamudPlugin
gameInteropProvider,
objectTable,
clientState,
condition,
sp.GetRequiredService<LightlessMediator>()));
services.AddSingleton(sp => new DalamudUtilService(
@@ -213,6 +225,7 @@ public sealed class Plugin : IDalamudPlugin
gameData,
targetManager,
gameConfig,
playerState,
sp.GetRequiredService<ActorObjectService>(),
sp.GetRequiredService<BlockedCharacterHandler>(),
sp.GetRequiredService<LightlessMediator>(),
@@ -267,6 +280,7 @@ public sealed class Plugin : IDalamudPlugin
sp.GetRequiredService<ILogger<LightFinderPlateHandler>>(),
addonLifecycle,
gameGui,
clientState,
sp.GetRequiredService<LightlessConfigService>(),
sp.GetRequiredService<LightlessMediator>(),
objectTable,
@@ -274,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(
@@ -297,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(
@@ -458,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>>(),
@@ -527,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>());
@@ -548,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

View File

@@ -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);

View File

@@ -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)

View File

@@ -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);

View File

@@ -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;

View File

@@ -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
{

View 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();
}
}
}

View File

@@ -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;
@@ -24,6 +26,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
private readonly ActorObjectService _actorObjectService;
private readonly PairUiService _pairUiService;
private readonly ChatConfigService _chatConfigService;
private readonly ServerConfigurationManager _serverConfigurationManager;
private readonly Lock _sync = new();
@@ -36,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;
@@ -51,7 +57,8 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
ApiController apiController,
DalamudUtilService dalamudUtilService,
ActorObjectService actorObjectService,
PairUiService pairUiService)
PairUiService pairUiService,
ServerConfigurationManager serverConfigurationManager)
: base(logger, mediator)
{
_apiController = apiController;
@@ -59,6 +66,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
_actorObjectService = actorObjectService;
_pairUiService = pairUiService;
_chatConfigService = chatConfigService;
_serverConfigurationManager = serverConfigurationManager;
_isLoggedIn = _dalamudUtilService.IsLoggedIn;
_isConnected = _apiController.IsConnected;
@@ -69,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)
{
@@ -98,6 +111,8 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
state.Messages.ToList()));
}
_cachedChannelSnapshots = snapshots;
_channelsSnapshotDirty = false;
return snapshots;
}
}
@@ -135,6 +150,8 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
state.UnreadCount = 0;
_lastReadCounts[key] = state.Messages.Count;
}
MarkChannelsSnapshotDirtyLocked();
}
}
@@ -186,6 +203,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
if (!wasEnabled)
{
_chatEnabled = true;
MarkChannelsSnapshotDirtyLocked();
}
}
@@ -231,6 +249,8 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
state.IsAvailable = false;
state.StatusText = "Chat services disabled";
}
MarkChannelsSnapshotDirtyLocked();
}
UnregisterChatHandler();
@@ -556,7 +576,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
try
{
var location = await _dalamudUtilService.GetMapDataAsync().ConfigureAwait(false);
var location = _dalamudUtilService.GetMapData();
var territoryId = (ushort)location.TerritoryId;
var worldId = (ushort)location.ServerId;
@@ -682,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)
@@ -717,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))
@@ -761,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)
{
@@ -776,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
{
@@ -801,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);
}
}
}
@@ -853,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,
@@ -992,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;
}
@@ -1023,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));
@@ -1126,7 +1161,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
{
try
{
return _dalamudUtilService.GetPlayerNameAsync().ConfigureAwait(false).GetAwaiter().GetResult();
return _dalamudUtilService.GetPlayerName();
}
catch (Exception ex)
{
@@ -1136,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;
}
@@ -1204,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)
{
@@ -1249,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();
}
@@ -1262,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);
@@ -1276,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)

View File

@@ -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);
}
}

View File

@@ -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,6 +278,7 @@ 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];
@@ -245,7 +291,9 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
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; }
@@ -264,7 +312,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
return false;
}
if (!TerritoryData.Value.TryGetValue(territoryId, out var name) || string.IsNullOrWhiteSpace(name))
if (!TerritoryDataEnglish.Value.TryGetValue(territoryId, out var name) || string.IsNullOrWhiteSpace(name))
{
return false;
}
@@ -327,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)
@@ -339,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()
@@ -355,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)
{
@@ -366,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)
@@ -373,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)
@@ -388,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)
@@ -425,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)
@@ -472,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)
@@ -534,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)
@@ -593,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);
@@ -780,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;
@@ -846,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];
@@ -860,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;
@@ -990,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)

View File

@@ -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

View File

@@ -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;

View File

@@ -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)

View 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);
}
}
}
}

View File

@@ -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

File diff suppressed because it is too large Load Diff

View 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
}
}
}

View File

@@ -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);

View 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();
}
}

View 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();
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}

View File

@@ -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);

View File

@@ -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);
}

View File

@@ -105,6 +105,7 @@ public class UiFactory
groupData: groupData,
isLightfinderContext: isLightfinderContext,
lightfinderCid: lightfinderCid,
performanceCollector: _performanceCollectorService);
performanceCollector: _performanceCollectorService,
_apiController);
}
}

View File

@@ -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;

View 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
}
}

File diff suppressed because it is too large Load Diff

View 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
}
}

View 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
}
}

View 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
}
}

View 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.

View 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
}
}

View 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
}
}

View 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
}
}

View 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
}
}

View 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
}
}

View 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
}
}

View 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
}
}

View 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
}
}

View 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
}
}

View 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
}
}

View 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
}
}

View 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
}
}

View 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
}
}

View File

@@ -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;
@@ -944,6 +944,7 @@ 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),

View File

@@ -326,6 +326,7 @@ public class DrawFolderTag : DrawFolderBase
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",
};

View File

@@ -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

View File

@@ -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));

View File

@@ -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);

View File

@@ -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()

View File

@@ -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;

View File

@@ -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()
@@ -380,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);

View File

@@ -21,6 +21,7 @@ public sealed record PairUiEntry(
IndividualPairStatus? PairStatus,
long LastAppliedDataBytes,
long LastAppliedDataTris,
long LastAppliedApproximateEffectiveTris,
long LastAppliedApproximateVramBytes,
long LastAppliedApproximateEffectiveVramBytes,
IPairHandlerAdapter? Handler)

View File

@@ -0,0 +1,8 @@
namespace LightlessSync.UI.Models;
public enum TextureFormatSortMode
{
None = 0,
CompressedFirst = 1,
UncompressedFirst = 2
}

View File

@@ -7,4 +7,5 @@ public enum VisiblePairSortMode
EffectiveVramUsage = 2,
TriangleCount = 3,
PreferredDirectPairs = 4,
EffectiveTriangleCount = 5,
}

File diff suppressed because it is too large Load Diff

View File

@@ -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>

View File

@@ -116,7 +116,7 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
var drawList = ImGui.GetWindowDrawList();
var purple = UIColors.Get("LightlessPurple");
var gradLeft = purple.WithAlpha(0.0f);
var gradLeft = purple.WithAlpha(0.0f);
var gradRight = purple.WithAlpha(0.85f);
uint colTopLeft = ImGui.ColorConvertFloat4ToU32(gradLeft);
@@ -162,7 +162,7 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
var subtitlePos = new Vector2(
pMin.X + 12f * scale,
titlePos.Y + titleHeight - 2f * scale);
titlePos.Y + titleHeight - 2f * scale);
ImGui.SetCursorScreenPos(subtitlePos);
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey);
@@ -297,6 +297,25 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
var ownerTab = ImRaii.TabItem("Owner Settings");
if (ownerTab)
{
bool isChatDisabled = perm.IsDisableChat();
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("Syncshell Chat");
_uiSharedService.BooleanToColoredIcon(!isChatDisabled);
ImGui.SameLine(230);
using (ImRaii.PushColor(ImGuiCol.Text, isChatDisabled ? UIColors.Get("PairBlue") : UIColors.Get("DimRed")))
{
if (_uiSharedService.IconTextButton(
isChatDisabled ? FontAwesomeIcon.Comment : FontAwesomeIcon.Ban,
isChatDisabled ? "Enable syncshell chat" : "Disable syncshell chat"))
{
perm.SetDisableChat(!isChatDisabled);
_ = _apiController.GroupChangeGroupPermissionState(new(GroupFullInfo.Group, perm));
}
}
UiSharedService.AttachToolTip("Disables syncshell chat for all members.");
ImGuiHelpers.ScaledDummy(6f);
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("New Password");
var availableWidth = ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X;
@@ -373,25 +392,27 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
}
UiSharedService.AttachToolTip("When enabled, inactive non-pinned, non-moderator users will be pruned automatically on the server.");
ImGui.SameLine();
ImGui.SetNextItemWidth(150);
using (ImRaii.Disabled(!_autoPruneEnabled))
{
_uiSharedService.DrawCombo(
"Day(s) of inactivity",
[1, 3, 7, 14, 30, 90],
days => $"{days} day(s)",
selected =>
{
_autoPruneDays = selected;
SavePruneSettings();
},
_autoPruneDays);
}
if (!_autoPruneEnabled)
{
ImGui.BeginDisabled();
}
ImGui.SameLine();
ImGui.SetNextItemWidth(150);
_uiSharedService.DrawCombo(
"Day(s) of inactivity (gets checked hourly)",
[0, 1, 3, 7, 14, 30, 90],
(count) => count == 0 ? "2 hours(s)" : count + " day(s)",
selected =>
{
_autoPruneDays = selected;
SavePruneSettings();
},
_autoPruneDays);
if (!_autoPruneEnabled)
{
ImGui.EndDisabled();
UiSharedService.ColorTextWrapped(
"Automatic prune is currently disabled. Enable it and choose an inactivity threshold to let the server clean up inactive users automatically.",
ImGuiColors.DalamudGrey);
@@ -574,7 +595,7 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
_uiSharedService.DrawCombo(
"Day(s) of inactivity",
[0, 1, 3, 7, 14, 30, 90],
(count) => count == 0 ? "15 minute(s)" : count + " day(s)",
(count) => count == 0 ? "2 hours(s)" : count + " day(s)",
(selected) =>
{
_pruneDays = selected;
@@ -644,8 +665,8 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
var style = ImGui.GetStyle();
float fullW = ImGui.GetContentRegionAvail().X;
float colIdentity = fullW * 0.45f;
float colMeta = fullW * 0.35f;
float colIdentity = fullW * 0.45f;
float colMeta = fullW * 0.35f;
float colActions = fullW - colIdentity - colMeta - style.ItemSpacing.X * 2.0f;
// Header
@@ -854,7 +875,7 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
var boolcolor = UiSharedService.GetBoolColor(pair.IsOnline);
UiSharedService.ColorText(text, boolcolor);
if (ImGui.IsItemClicked())
ImGui.SetClipboardText(pair.UserData.AliasOrUID);
@@ -1074,6 +1095,7 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
ImGui.Dummy(new Vector2(0, 4 * ImGuiHelpers.GlobalScale));
}
private void SavePruneSettings()
{
if (_autoPruneDays <= 0)
@@ -1081,8 +1103,7 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
_autoPruneEnabled = false;
}
var enabled = _autoPruneEnabled && _autoPruneDays > 0;
var dto = new GroupPruneSettingsDto(Group: GroupFullInfo.Group, AutoPruneEnabled: enabled, AutoPruneDays: enabled ? _autoPruneDays : 0);
var dto = new GroupPruneSettingsDto(Group: GroupFullInfo.Group, AutoPruneEnabled: _autoPruneEnabled, AutoPruneDays: _autoPruneDays);
try
{

View File

@@ -140,19 +140,10 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
return;
}
string? myHashedCid = null;
try
{
var cid = _dalamudUtilService.GetCID();
myHashedCid = cid.ToString().GetHash256();
}
catch (Exception ex)
{
_logger.LogDebug(ex, "Failed to get CID, not excluding own broadcast.");
}
var broadcasts = _broadcastScannerService.GetActiveSyncshellBroadcasts().Where(b => !string.Equals(b.HashedCID, myHashedCid, StringComparison.Ordinal)).ToList() ?? [];
var broadcasts = _broadcastScannerService.GetActiveSyncshellBroadcasts().ToList() ?? [];
_broadcastScannerService.TryGetLocalHashedCid(out var localHashedCid);
var cardData = new List<(GroupJoinDto Shell, string BroadcasterName)>();
var cardData = new List<(GroupJoinDto Shell, string BroadcasterName, bool IsSelfBroadcast)>();
foreach (var shell in _nearbySyncshells)
{
@@ -185,9 +176,15 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
broadcasterName = !string.IsNullOrEmpty(worldName)
? $"{name} ({worldName})"
: name;
var isSelfBroadcast = !string.IsNullOrEmpty(localHashedCid)
&& string.Equals(broadcast.HashedCID, localHashedCid, StringComparison.Ordinal);
cardData.Add((shell, broadcasterName, isSelfBroadcast));
continue;
}
cardData.Add((shell, broadcasterName));
cardData.Add((shell, broadcasterName, false));
}
if (cardData.Count == 0)
@@ -210,7 +207,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
DrawConfirmation();
}
private void DrawSyncshellList(List<(GroupJoinDto Shell, string BroadcasterName)> listData)
private void DrawSyncshellList(List<(GroupJoinDto Shell, string BroadcasterName, bool IsSelfBroadcast)> listData)
{
const int shellsPerPage = 3;
var totalPages = (int)Math.Ceiling(listData.Count / (float)shellsPerPage);
@@ -227,7 +224,10 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
for (int index = firstIndex; index < lastExclusive; index++)
{
var (shell, broadcasterName) = listData[index];
var (shell, broadcasterName, isSelfBroadcast) = listData[index];
var broadcasterLabel = string.IsNullOrEmpty(broadcasterName)
? (isSelfBroadcast ? "You" : string.Empty)
: (isSelfBroadcast ? $"{broadcasterName} (You)" : broadcasterName);
ImGui.PushID(shell.Group.GID);
float rowHeight = 74f * ImGuiHelpers.GlobalScale;
@@ -239,7 +239,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
var style = ImGui.GetStyle();
float startX = ImGui.GetCursorPosX();
float regionW = ImGui.GetContentRegionAvail().X;
float rightTxtW = ImGui.CalcTextSize(broadcasterName).X;
float rightTxtW = ImGui.CalcTextSize(broadcasterLabel).X;
_uiSharedService.MediumText(displayName, UIColors.Get("LightlessPurple"));
if (ImGui.IsItemHovered())
@@ -252,7 +252,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
float rightX = startX + regionW - rightTxtW - style.ItemSpacing.X;
ImGui.SameLine();
ImGui.SetCursorPosX(rightX);
ImGui.TextUnformatted(broadcasterName);
ImGui.TextUnformatted(broadcasterLabel);
if (ImGui.IsItemHovered())
ImGui.SetTooltip("Broadcaster of the syncshell.");
@@ -291,7 +291,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
float joinX = rowStartLocal.X + (tagsWidth > 0 ? tagsWidth + style.ItemSpacing.X : 0f);
ImGui.SetCursorPos(new Vector2(joinX, btnBaselineY));
DrawJoinButton(shell);
DrawJoinButton(shell, isSelfBroadcast);
float btnHeight = ImGui.GetFrameHeightWithSpacing();
float rowHeightUsed = MathF.Max(tagsHeight, btnHeight);
@@ -311,7 +311,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
DrawPagination(totalPages);
}
private void DrawSyncshellGrid(List<(GroupJoinDto Shell, string BroadcasterName)> cardData)
private void DrawSyncshellGrid(List<(GroupJoinDto Shell, string BroadcasterName, bool IsSelfBroadcast)> cardData)
{
const int shellsPerPage = 4;
var totalPages = (int)Math.Ceiling(cardData.Count / (float)shellsPerPage);
@@ -336,7 +336,10 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
for (int index = firstIndex; index < lastExclusive; index++)
{
var localIndex = index - firstIndex;
var (shell, broadcasterName) = cardData[index];
var (shell, broadcasterName, isSelfBroadcast) = cardData[index];
var broadcasterLabel = string.IsNullOrEmpty(broadcasterName)
? (isSelfBroadcast ? "You" : string.Empty)
: (isSelfBroadcast ? $"{broadcasterName} (You)" : broadcasterName);
if (localIndex % 2 != 0)
ImGui.SameLine();
@@ -373,17 +376,17 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
float maxBroadcasterWidth = regionRightX - minBroadcasterX;
string broadcasterToShow = broadcasterName;
string broadcasterToShow = broadcasterLabel;
if (!string.IsNullOrEmpty(broadcasterName) && maxBroadcasterWidth > 0f)
if (!string.IsNullOrEmpty(broadcasterLabel) && maxBroadcasterWidth > 0f)
{
float bcFullWidth = ImGui.CalcTextSize(broadcasterName).X;
float bcFullWidth = ImGui.CalcTextSize(broadcasterLabel).X;
string toolTip;
if (bcFullWidth > maxBroadcasterWidth)
{
broadcasterToShow = TruncateTextToWidth(broadcasterName, maxBroadcasterWidth);
toolTip = broadcasterName + Environment.NewLine + Environment.NewLine + "Broadcaster of the syncshell.";
broadcasterToShow = TruncateTextToWidth(broadcasterLabel, maxBroadcasterWidth);
toolTip = broadcasterLabel + Environment.NewLine + Environment.NewLine + "Broadcaster of the syncshell.";
}
else
{
@@ -443,7 +446,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
if (remainingY > 0)
ImGui.Dummy(new Vector2(0, remainingY));
DrawJoinButton(shell);
DrawJoinButton(shell, isSelfBroadcast);
ImGui.EndChild();
ImGui.EndGroup();
@@ -489,7 +492,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
}
}
private void DrawJoinButton(dynamic shell)
private void DrawJoinButton(GroupJoinDto shell, bool isSelfBroadcast)
{
const string visibleLabel = "Join";
var label = $"{visibleLabel}##{shell.Group.GID}";
@@ -517,7 +520,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
buttonSize = new Vector2(-1, 0);
}
if (!isAlreadyMember && !isRecentlyJoined)
if (!isAlreadyMember && !isRecentlyJoined && !isSelfBroadcast)
{
ImGui.PushStyleColor(ImGuiCol.Button, UIColors.Get("LightlessGreen"));
ImGui.PushStyleColor(ImGuiCol.ButtonHovered, UIColors.Get("LightlessGreen").WithAlpha(0.85f));
@@ -567,7 +570,9 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
ImGui.Button(label, buttonSize);
}
UiSharedService.AttachToolTip("Already a member or owner of this Syncshell.");
UiSharedService.AttachToolTip(isSelfBroadcast
? "This is your own Syncshell."
: "Already a member or owner of this Syncshell.");
}
ImGui.PopStyleColor(3);

View File

@@ -440,7 +440,7 @@ public class TopTabMenu
try
{
var myCidHash = (await _dalamudUtilService.GetCIDAsync().ConfigureAwait(false)).ToString().GetHash256();
var myCidHash = _dalamudUtilService.GetCID().ToString().GetHash256();
await _apiController.TryPairWithContentId(request.HashedCid).ConfigureAwait(false);
_pairRequestService.RemoveRequest(request.HashedCid);
@@ -781,7 +781,8 @@ public class TopTabMenu
{
var buttonX = (availableWidth - (spacingX)) / 2f;
if (_uiSharedService.IconTextButton(FontAwesomeIcon.PersonCirclePlus, "Lightfinder", buttonX, center: true))
var lightFinderLabel = GetLightfinderFinderLabel();
if (_uiSharedService.IconTextButton(FontAwesomeIcon.PersonCirclePlus, lightFinderLabel, buttonX, center: true))
{
_lightlessMediator.Publish(new UiToggleMessage(typeof(LightFinderUI)));
}
@@ -795,27 +796,28 @@ public class TopTabMenu
}
}
private string GetLightfinderFinderLabel()
{
string label = "Lightfinder";
if (_lightFinderService.IsBroadcasting)
{
var hashExclude = _dalamudUtilService.GetCID().ToString().GetHash256();
var nearbyCount = _lightFinderScannerService.GetActiveBroadcasts(hashExclude).Count;
return $"{label} ({nearbyCount})";
}
return label;
}
private string GetSyncshellFinderLabel()
{
if (!_lightFinderService.IsBroadcasting)
return "Syncshell Finder";
string? myHashedCid = null;
try
{
var cid = _dalamudUtilService.GetCID();
myHashedCid = cid.ToString().GetHash256();
}
catch (Exception)
{
// Couldnt get own CID, log and return default table
}
var nearbyCount = _lightFinderScannerService
.GetActiveSyncshellBroadcasts()
.Where(b =>
!string.IsNullOrEmpty(b.GID) &&
!string.Equals(b.HashedCID, myHashedCid, StringComparison.Ordinal))
.GetActiveSyncshellBroadcasts(excludeLocal: true)
.Where(b => !string.IsNullOrEmpty(b.GID))
.Select(b => b.GID!)
.Distinct(StringComparer.Ordinal)
.Count();

View File

@@ -947,13 +947,16 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
}
}
if (_discordOAuthCheck != null && _discordOAuthCheck.IsCompleted)
if (_discordOAuthCheck != null && _discordOAuthCheck.IsCompleted && _discordOAuthCheck.Result != null)
{
if (IconTextButton(FontAwesomeIcon.ArrowRight, "Authenticate with Server"))
if (_discordOAuthGetCode == null)
{
_discordOAuthGetCode = _serverConfigurationManager.GetDiscordOAuthToken(_discordOAuthCheck.Result!, selectedServer.ServerUri, _discordOAuthGetCts.Token);
if (IconTextButton(FontAwesomeIcon.ArrowRight, "Authenticate with Server"))
{
_discordOAuthGetCode = _serverConfigurationManager.GetDiscordOAuthToken(_discordOAuthCheck.Result, selectedServer.ServerUri, _discordOAuthGetCts.Token);
}
}
else if (_discordOAuthGetCode != null && !_discordOAuthGetCode.IsCompleted)
else if (!_discordOAuthGetCode.IsCompleted)
{
TextWrapped("A browser window has been opened, follow it to authenticate. Click the button below if you accidentally closed the window and need to restart the authentication.");
if (IconTextButton(FontAwesomeIcon.Ban, "Cancel Authentication"))
@@ -962,7 +965,7 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
_discordOAuthGetCode = null;
}
}
else if (_discordOAuthGetCode != null && _discordOAuthGetCode.IsCompleted)
else
{
TextWrapped("Discord OAuth is completed, status: ");
ImGui.SameLine();

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More