Compare commits

38 Commits

Author SHA1 Message Date
e1d71ee33a Allow disable display self-analysis alert 2025-11-01 00:23:53 +01:00
89fa1a999f Fix bubble party + Download queue + Allow pause user in syncshell + add visual feature + clean log info 2025-10-19 01:29:57 +02:00
1f6e86ec2d Woops 2025-10-12 13:20:23 +02:00
d225a3844a Fix UI + Amélioration AutoDetect & Self Analyse + Update Penumbra API 2025-10-12 12:42:31 +02:00
d4a46910f9 Fix UI & BubbleType position 2025-10-12 01:00:08 +02:00
b59a579f56 Update 0.1.9.2 - Fix BubleChat 2025-10-04 19:15:27 +02:00
7706ef1fa7 Hotfix 0.1.9.1 - Update API & Double notification & Update Penumbra API Ref 2025-09-29 02:10:03 +02:00
fca730557e Update 0.1.9 - Correctif UI + Default Synchronisation settings + Detect TypeChat 2025-09-29 00:19:45 +02:00
6572fdcc27 Update 0.1.8.2 - Correctif UI, Correctif Nearby CompactUI, Add changelog 2025-09-27 10:02:43 +02:00
bf770f19d9 Update 0.1.8.1 - Changement UI & Ajout "vue sous" (cf.Suggestion Discord) 2025-09-23 23:41:45 +02:00
78089a9fc7 Update 0.1.8 - Fix interface & ajout syncshell perma/temp 2025-09-20 12:39:18 +02:00
3c81e1f243 Fix - Enforcing Unique groupe aliases 2025-09-19 23:58:31 +02:00
0808266887 Update 0.1.7 - Allow naming Syncshell 2025-09-19 23:18:09 +02:00
a2071b9c05 Update MareAPI submodule and config 2025-09-19 22:52:09 +02:00
612e7c88a2 Fix UI & Rotate Salt 2025-09-19 22:33:40 +02:00
1755b5cb54 Clean code 2025-09-14 00:21:30 +02:00
4a388dcfa9 Update 0.1.6 - UI change 2025-09-13 22:42:08 +02:00
a0957715a5 Update 0.1.6 - Fix UI settings & Delay Detection 2025-09-13 20:08:24 +02:00
04a8ee3186 Update 0.1.6 - Deploy AutoDetect, last debug and optimization 2025-09-13 13:41:00 +02:00
b79a51748f Update 0.1.4 - AutoDetect WIP Debug & Fix UI & Optimization 2025-09-11 22:37:29 +02:00
95d9f65068 Update 0.1.2 - AutoDetect Debug before release 2025-09-11 15:42:41 +02:00
a70968d30c Nearby (AutoDetect) Phase 1 — settings, window, compact section; UmbraSync theme; minor fixes 2025-09-11 11:30:09 +02:00
6ebb73040b earby (AutoDetect) Phase 1 — settings, window, compact section; UmbraSync theme; minor fixes 2025-09-11 11:28:48 +02:00
46f2443824 FINALY !!! 2025-09-05 21:34:17 +02:00
eeab8354b6 remove old ref + update gitsubmodule + update 0.1.0.0 + add NoSnapService + pimpmymod + Licence AGPLv3 2025-09-05 15:03:41 +02:00
b5d8f288f9 remove old ref + update gitsubmodule + update 0.1.0.0 + add NoSnapService + pimpmymod 2025-09-05 15:02:52 +02:00
3c2dab4d21 add submodules (MareAPI, Penumbra.Api, Glamourer.Api) 2025-09-05 13:39:35 +02:00
edb49f710a remove UmbraCrypt gitlink 2025-09-05 13:35:47 +02:00
9ff21dc341 purge UmbraCrypt submodule 2025-09-05 13:34:29 +02:00
17962a37b3 Uodate 0.0.6 - Change reference and clean 2025-09-04 22:54:55 +02:00
4495177f02 Update 0.0.5 - Change repo & services 2025-09-04 21:22:39 +02:00
14bb5c14f7 Update 0.0.5 - Change repo & services 2025-09-04 21:22:24 +02:00
6101686a33 Add PenumbraAPI & GlamourerAPI + Update API & Connector 2025-08-31 18:19:04 +02:00
bc6cde48de PimpMyMod 2025-08-31 12:27:50 +02:00
9ce213f949 OupsiDoupsi 2025-08-31 11:22:23 +02:00
cd5bf3f06f Change build version for beta 2025-08-31 10:44:42 +02:00
52eaa0cf95 Initial commit 2025-08-31 00:52:40 +02:00
6edb90f4d7 Initial commit 2025-08-31 00:52:32 +02:00
163 changed files with 6245 additions and 3309 deletions

6
.gitignore vendored
View File

@@ -2,15 +2,17 @@
## files generated by popular Visual Studio add-ons. ## files generated by popular Visual Studio add-ons.
## ##
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
.idea
# User-specific files # User-specific files
*.rsuser *.rsuser
*.suo *.suo
*.user *.user
*.userosscache *.userosscache
*.sln.docstates *.sln.docstates
*.bak
.DS_Store .DS_Store
MareSynchronos/.DS_Store
*.zip
UmbraServer_extracted/
# User-specific files (MonoDevelop/Xamarin Studio) # User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs *.userprefs

14
.gitmodules vendored
View File

@@ -1,12 +1,12 @@
[submodule "UmbraAPI"] [submodule "MareAPI"]
path = UmbraAPI path = MareAPI
url = https://git.umbra-sync.net/SirConstance/UmbraAPI.git url = ssh://git@git.umbra-sync.net:1222/Keda/UmbraAPI.git
branch = main
[submodule "Penumbra.Api"] [submodule "Penumbra.Api"]
path = Penumbra.Api path = Penumbra.Api
url = https://github.com/Ottermandias/Penumbra.Api.git url = https://github.com/Ottermandias/Penumbra.Api.git
branch = main
[submodule "Glamourer.Api"] [submodule "Glamourer.Api"]
path = Glamourer.Api path = Glamourer.Api
url = https://github.com/Ottermandias/Glamourer.Api url = https://github.com/Ottermandias/Glamourer.Api.git
[submodule "Chaos.NaCl"] branch = main
path = Chaos.NaCl
url = https://github.com/CodesInChaos/Chaos.NaCl.git

Submodule Chaos.NaCl deleted from 2c861348dc

1
Glamourer.Api Submodule

Submodule Glamourer.Api added at 59a7ab5fa9

View File

@@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) 2022 Mare Synchronos Copyright (c) 2022 Penumbra-Sync
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

1
MareAPI Submodule

Submodule MareAPI added at 0abb078c21

View File

@@ -5,7 +5,7 @@ VisualStudioVersion = 17.1.32328.378
MinimumVisualStudioVersion = 10.0.40219.1 MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MareSynchronos", "MareSynchronos\MareSynchronos.csproj", "{13C812E9-0D42-4B95-8646-40EEBF30636F}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MareSynchronos", "MareSynchronos\MareSynchronos.csproj", "{13C812E9-0D42-4B95-8646-40EEBF30636F}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MareSynchronos.API", "UmbraAPI\MareSynchronosAPI\MareSynchronos.API.csproj", "{5A0B7434-8D89-4E90-B55C-B4A7AE1A6ADE}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MareSynchronos.API", "MareAPI\MareSynchronosAPI\MareSynchronos.API.csproj", "{5A0B7434-8D89-4E90-B55C-B4A7AE1A6ADE}"
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{585B740D-BA2C-429B-9CF3-B2D223423748}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{585B740D-BA2C-429B-9CF3-B2D223423748}"
ProjectSection(SolutionItems) = preProject ProjectSection(SolutionItems) = preProject

View File

@@ -122,24 +122,24 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
public bool StorageisNTFS { get; private set; } = false; public bool StorageisNTFS { get; private set; } = false;
public void StartMareWatcher(string? marePath) public void StartMareWatcher(string? snowPath)
{ {
MareWatcher?.Dispose(); MareWatcher?.Dispose();
if (string.IsNullOrEmpty(marePath) || !Directory.Exists(marePath)) if (string.IsNullOrEmpty(snowPath) || !Directory.Exists(snowPath))
{ {
MareWatcher = null; MareWatcher = null;
Logger.LogWarning("Mare file path is not set, cannot start the FSW for Mare."); Logger.LogWarning("Umbra file path is not set, cannot start the FSW for Umbra.");
return; return;
} }
DriveInfo di = new(new DirectoryInfo(_configService.Current.CacheFolder).Root.FullName); DriveInfo di = new(new DirectoryInfo(_configService.Current.CacheFolder).Root.FullName);
StorageisNTFS = string.Equals("NTFS", di.DriveFormat, StringComparison.OrdinalIgnoreCase); StorageisNTFS = string.Equals("NTFS", di.DriveFormat, StringComparison.OrdinalIgnoreCase);
Logger.LogInformation("Mare Storage is on NTFS drive: {isNtfs}", StorageisNTFS); Logger.LogInformation("Umbra Storage is on NTFS drive: {isNtfs}", StorageisNTFS);
Logger.LogDebug("Initializing Mare FSW on {path}", marePath); Logger.LogDebug("Initializing Mare FSW on {path}", snowPath);
MareWatcher = new() MareWatcher = new()
{ {
Path = marePath, Path = snowPath,
InternalBufferSize = 8388608, InternalBufferSize = 8388608,
NotifyFilter = NotifyFilters.CreationTime NotifyFilter = NotifyFilters.CreationTime
| NotifyFilters.LastWrite | NotifyFilters.LastWrite
@@ -161,7 +161,7 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
if (string.IsNullOrEmpty(substPath)) if (string.IsNullOrEmpty(substPath))
{ {
SubstWatcher = null; SubstWatcher = null;
Logger.LogWarning("Mare file path is not set, cannot start the FSW for Mare."); Logger.LogWarning("Umbra file path is not set, cannot start the FSW for Umbra.");
return; return;
} }
@@ -197,7 +197,7 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
private void MareWatcher_FileChanged(object sender, FileSystemEventArgs e) private void MareWatcher_FileChanged(object sender, FileSystemEventArgs e)
{ {
Logger.LogTrace("Mare FSW: FileChanged: {change} => {path}", e.ChangeType, e.FullPath); Logger.LogTrace("Umbra FSW: FileChanged: {change} => {path}", e.ChangeType, e.FullPath);
if (!AllowedFileExtensions.Any(ext => e.FullPath.EndsWith(ext, StringComparison.OrdinalIgnoreCase))) return; if (!AllowedFileExtensions.Any(ext => e.FullPath.EndsWith(ext, StringComparison.OrdinalIgnoreCase))) return;
@@ -631,7 +631,7 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
if (string.IsNullOrEmpty(_configService.Current.CacheFolder) || !Directory.Exists(_configService.Current.CacheFolder)) if (string.IsNullOrEmpty(_configService.Current.CacheFolder) || !Directory.Exists(_configService.Current.CacheFolder))
{ {
cacheDirExists = false; cacheDirExists = false;
Logger.LogWarning("UmbraSync Cache directory is not set or does not exist."); Logger.LogWarning("Umbra Cache directory is not set or does not exist.");
} }
if (!penDirExists || !cacheDirExists) if (!penDirExists || !cacheDirExists)
{ {

View File

@@ -236,7 +236,6 @@ public sealed class FileCacheManager : IHostedService
foreach (var entry in cleanedPaths) foreach (var entry in cleanedPaths)
{ {
//_logger.LogDebug("Checking {path}", entry.Value);
if (dict.TryGetValue(entry.Value, out var entity)) if (dict.TryGetValue(entry.Value, out var entity))
{ {
@@ -366,7 +365,6 @@ public sealed class FileCacheManager : IHostedService
if (!entries.Exists(u => string.Equals(u.PrefixedFilePath, fileCache.PrefixedFilePath, StringComparison.OrdinalIgnoreCase))) if (!entries.Exists(u => string.Equals(u.PrefixedFilePath, fileCache.PrefixedFilePath, StringComparison.OrdinalIgnoreCase)))
{ {
//_logger.LogTrace("Adding to DB: {hash} => {path}", fileCache.Hash, fileCache.PrefixedFilePath);
entries.Add(fileCache); entries.Add(fileCache);
} }
} }
@@ -389,7 +387,6 @@ public sealed class FileCacheManager : IHostedService
private FileCacheEntity? GetValidatedFileCache(FileCacheEntity fileCache) private FileCacheEntity? GetValidatedFileCache(FileCacheEntity fileCache)
{ {
var resultingFileCache = ReplacePathPrefixes(fileCache); var resultingFileCache = ReplacePathPrefixes(fileCache);
//_logger.LogTrace("Validating {path}", fileCache.PrefixedFilePath);
resultingFileCache = Validate(resultingFileCache); resultingFileCache = Validate(resultingFileCache);
return resultingFileCache; return resultingFileCache;
} }
@@ -465,7 +462,7 @@ public sealed class FileCacheManager : IHostedService
if (!_ipcManager.Penumbra.APIAvailable || string.IsNullOrEmpty(_ipcManager.Penumbra.ModDirectory)) if (!_ipcManager.Penumbra.APIAvailable || string.IsNullOrEmpty(_ipcManager.Penumbra.ModDirectory))
{ {
_mareMediator.Publish(new NotificationMessage("Penumbra not connected", _mareMediator.Publish(new NotificationMessage("Penumbra not connected",
"Could not load local file cache data. Penumbra is not connected or not properly set up. Please enable and/or configure Penumbra properly to use UmbraSync. After, reload UmbraSync in the Plugin installer.", "Could not load local file cache data. Penumbra is not connected or not properly set up. Please enable and/or configure Penumbra properly to use Umbra. After, reload Umbra in the Plugin installer.",
MareConfiguration.Models.NotificationType.Error)); MareConfiguration.Models.NotificationType.Error));
} }

View File

@@ -95,8 +95,6 @@ public sealed class IpcCallerBrio : IIpcCaller
if (gameObject == null) return default; if (gameObject == null) return default;
var data = await _dalamudUtilService.RunOnFrameworkThread(() => _brioGetModelTransform.InvokeFunc(gameObject)).ConfigureAwait(false); var data = await _dalamudUtilService.RunOnFrameworkThread(() => _brioGetModelTransform.InvokeFunc(gameObject)).ConfigureAwait(false);
if (data.Item1 == null || data.Item2 == null || data.Item3 == null) return default; if (data.Item1 == null || data.Item2 == null || data.Item3 == null) return default;
//_logger.LogDebug("Getting Transform from Actor {actor}", gameObject.Name.TextValue);
return new WorldData() return new WorldData()
{ {
PositionX = data.Item1.Value.X, PositionX = data.Item1.Value.X,

View File

@@ -109,7 +109,7 @@ public sealed class IpcCallerGlamourer : DisposableMediatorSubscriberBase, IIpcC
if (!apiAvailable && !_shownGlamourerUnavailable) if (!apiAvailable && !_shownGlamourerUnavailable)
{ {
_shownGlamourerUnavailable = true; _shownGlamourerUnavailable = true;
_mareMediator.Publish(new NotificationMessage("Glamourer inactive", "Your Glamourer installation is not active or out of date. Update Glamourer to continue to use UmbraSync. If you just updated Glamourer, ignore this message.", _mareMediator.Publish(new NotificationMessage("Glamourer inactive", "Your Glamourer installation is not active or out of date. Update Glamourer to continue to use Umbra. If you just updated Glamourer, ignore this message.",
NotificationType.Error)); NotificationType.Error));
} }
} }

View File

@@ -27,9 +27,9 @@ public sealed class IpcCallerMoodles : IIpcCaller
_moodlesApiVersion = pi.GetIpcSubscriber<int>("Moodles.Version"); _moodlesApiVersion = pi.GetIpcSubscriber<int>("Moodles.Version");
_moodlesOnChange = pi.GetIpcSubscriber<IPlayerCharacter, object>("Moodles.StatusManagerModified"); _moodlesOnChange = pi.GetIpcSubscriber<IPlayerCharacter, object>("Moodles.StatusManagerModified");
_moodlesGetStatus = pi.GetIpcSubscriber<nint, string>("Moodles.GetStatusManagerByPtr"); _moodlesGetStatus = pi.GetIpcSubscriber<nint, string>("Moodles.GetStatusManagerByPtrV2");
_moodlesSetStatus = pi.GetIpcSubscriber<nint, string, object>("Moodles.SetStatusManagerByPtr"); _moodlesSetStatus = pi.GetIpcSubscriber<nint, string, object>("Moodles.SetStatusManagerByPtrV2");
_moodlesRevertStatus = pi.GetIpcSubscriber<nint, object>("Moodles.ClearStatusManagerByPtr"); _moodlesRevertStatus = pi.GetIpcSubscriber<nint, object>("Moodles.ClearStatusManagerByPtrV2");
_moodlesOnChange.Subscribe(OnMoodlesChange); _moodlesOnChange.Subscribe(OnMoodlesChange);
@@ -47,7 +47,7 @@ public sealed class IpcCallerMoodles : IIpcCaller
{ {
try try
{ {
APIAvailable = _moodlesApiVersion.InvokeFunc() == 1; APIAvailable = _moodlesApiVersion.InvokeFunc() == 3;
} }
catch catch
{ {

View File

@@ -114,7 +114,7 @@ public sealed class IpcCallerPenumbra : DisposableMediatorSubscriberBase, IIpcCa
bool penumbraAvailable = false; bool penumbraAvailable = false;
try try
{ {
penumbraAvailable = _pluginLoaded && _pluginVersion >= new Version(1, 0, 1, 0); penumbraAvailable = _pluginLoaded && _pluginVersion >= new Version(1, 5, 1, 0);
try try
{ {
penumbraAvailable &= _penumbraEnabled.Invoke(); penumbraAvailable &= _penumbraEnabled.Invoke();
@@ -136,7 +136,7 @@ public sealed class IpcCallerPenumbra : DisposableMediatorSubscriberBase, IIpcCa
{ {
_shownPenumbraUnavailable = true; _shownPenumbraUnavailable = true;
_mareMediator.Publish(new NotificationMessage("Penumbra inactive", _mareMediator.Publish(new NotificationMessage("Penumbra inactive",
"Your Penumbra installation is not active or out of date. Update Penumbra and/or the Enable Mods setting in Penumbra to continue to use UmbraSync. If you just updated Penumbra, ignore this message.", "Your Penumbra installation is not active or out of date. Update Penumbra and/or the Enable Mods setting in Penumbra to continue to use Umbra. If you just updated Penumbra, ignore this message.",
NotificationType.Error)); NotificationType.Error));
} }
} }
@@ -225,9 +225,15 @@ public sealed class IpcCallerPenumbra : DisposableMediatorSubscriberBase, IIpcCa
return await _dalamudUtil.RunOnFrameworkThread(() => return await _dalamudUtil.RunOnFrameworkThread(() =>
{ {
var collName = "UmbraSync_" + uid; Guid collId;
var collId = _penumbraCreateNamedTemporaryCollection.Invoke(collName); var collName = "ElfSync_" + uid;
PenumbraApiEc penEC = _penumbraCreateNamedTemporaryCollection.Invoke(uid, collName, out collId);
logger.LogTrace("Creating Temp Collection {collName}, GUID: {collId}", collName, collId); logger.LogTrace("Creating Temp Collection {collName}, GUID: {collId}", collName, collId);
if (penEC != PenumbraApiEc.Success)
{
logger.LogError("Failed to create temporary collection for {collName} with error code {penEC}. Please include this line in any error reports", collName, penEC);
return Guid.Empty;
}
return collId; return collId;
}).ConfigureAwait(false); }).ConfigureAwait(false);

View File

@@ -30,12 +30,12 @@ public sealed class IpcCallerPetNames : IIpcCaller
_dalamudUtil = dalamudUtil; _dalamudUtil = dalamudUtil;
_mareMediator = mareMediator; _mareMediator = mareMediator;
_petnamesReady = pi.GetIpcSubscriber<object>("PetRenamer.Ready"); _petnamesReady = pi.GetIpcSubscriber<object>("PetRenamer.OnReady");
_petnamesDisposing = pi.GetIpcSubscriber<object>("PetRenamer.Disposing"); _petnamesDisposing = pi.GetIpcSubscriber<object>("PetRenamer.OnDisposing");
_apiVersion = pi.GetIpcSubscriber<(uint, uint)>("PetRenamer.ApiVersion"); _apiVersion = pi.GetIpcSubscriber<(uint, uint)>("PetRenamer.ApiVersion");
_enabled = pi.GetIpcSubscriber<bool>("PetRenamer.Enabled"); _enabled = pi.GetIpcSubscriber<bool>("PetRenamer.IsEnabled");
_playerDataChanged = pi.GetIpcSubscriber<string, object>("PetRenamer.PlayerDataChanged"); _playerDataChanged = pi.GetIpcSubscriber<string, object>("PetRenamer.OnPlayerDataChanged");
_getPlayerData = pi.GetIpcSubscriber<string>("PetRenamer.GetPlayerData"); _getPlayerData = pi.GetIpcSubscriber<string>("PetRenamer.GetPlayerData");
_setPlayerData = pi.GetIpcSubscriber<string, object>("PetRenamer.SetPlayerData"); _setPlayerData = pi.GetIpcSubscriber<string, object>("PetRenamer.SetPlayerData");
_clearPlayerData = pi.GetIpcSubscriber<ushort, object>("PetRenamer.ClearPlayerData"); _clearPlayerData = pi.GetIpcSubscriber<ushort, object>("PetRenamer.ClearPlayerData");
@@ -56,7 +56,7 @@ public sealed class IpcCallerPetNames : IIpcCaller
APIAvailable = _enabled?.InvokeFunc() ?? false; APIAvailable = _enabled?.InvokeFunc() ?? false;
if (APIAvailable) if (APIAvailable)
{ {
APIAvailable = _apiVersion?.InvokeFunc() is { Item1: 3, Item2: >= 1 }; APIAvailable = _apiVersion?.InvokeFunc() is { Item1: 4, Item2: >= 0 };
} }
} }
catch catch

View File

@@ -66,11 +66,11 @@ public class IpcProvider : IHostedService, IMediatorSubscriber
public Task StartAsync(CancellationToken cancellationToken) public Task StartAsync(CancellationToken cancellationToken)
{ {
_logger.LogDebug("Starting IpcProvider Service"); _logger.LogDebug("Starting IpcProvider Service");
_loadFileProvider = _pi.GetIpcProvider<string, IGameObject, bool>("UmbraSyncSync.LoadMcdf"); _loadFileProvider = _pi.GetIpcProvider<string, IGameObject, bool>("ElfSync.LoadMcdf");
_loadFileProvider.RegisterFunc(LoadMcdf); _loadFileProvider.RegisterFunc(LoadMcdf);
_loadFileAsyncProvider = _pi.GetIpcProvider<string, IGameObject, Task<bool>>("UmbraSyncSync.LoadMcdfAsync"); _loadFileAsyncProvider = _pi.GetIpcProvider<string, IGameObject, Task<bool>>("UmbraSync.LoadMcdfAsync");
_loadFileAsyncProvider.RegisterFunc(LoadMcdfAsync); _loadFileAsyncProvider.RegisterFunc(LoadMcdfAsync);
_handledGameAddresses = _pi.GetIpcProvider<List<nint>>("UmbraSyncSync.GetHandledAddresses"); _handledGameAddresses = _pi.GetIpcProvider<List<nint>>("UmbraSync.GetHandledAddresses");
_handledGameAddresses.RegisterFunc(GetHandledAddresses); _handledGameAddresses.RegisterFunc(GetHandledAddresses);
_loadFileProviderMare = _pi.GetIpcProvider<string, IGameObject, bool>("MareSynchronos.LoadMcdf"); _loadFileProviderMare = _pi.GetIpcProvider<string, IGameObject, bool>("MareSynchronos.LoadMcdf");

View File

@@ -1,15 +1,72 @@
using MareSynchronos.WebAPI; using MareSynchronos.WebAPI;
using System.Text.Json;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace MareSynchronos.MareConfiguration; namespace MareSynchronos.MareConfiguration;
public class ConfigurationMigrator(ILogger<ConfigurationMigrator> logger) : IHostedService public class ConfigurationMigrator(ILogger<ConfigurationMigrator> logger, MareConfigService mareConfig) : IHostedService
{ {
private readonly ILogger<ConfigurationMigrator> _logger = logger; private readonly ILogger<ConfigurationMigrator> _logger = logger;
private readonly MareConfigService _mareConfig = mareConfig;
public void Migrate() public void Migrate()
{ {
try
{
var path = _mareConfig.ConfigurationPath;
if (!File.Exists(path)) return;
using var doc = JsonDocument.Parse(File.ReadAllText(path));
var root = doc.RootElement;
bool changed = false;
if (root.TryGetProperty("EnableAutoSyncDiscovery", out var enableAutoSync))
{
var val = enableAutoSync.GetBoolean();
if (_mareConfig.Current.EnableAutoDetectDiscovery != val)
{
_mareConfig.Current.EnableAutoDetectDiscovery = val;
changed = true;
}
}
if (root.TryGetProperty("AllowAutoSyncPairRequests", out var allowAutoSync))
{
var val = allowAutoSync.GetBoolean();
if (_mareConfig.Current.AllowAutoDetectPairRequests != val)
{
_mareConfig.Current.AllowAutoDetectPairRequests = val;
changed = true;
}
}
if (root.TryGetProperty("AutoSyncMaxDistanceMeters", out var maxDistSync) && maxDistSync.TryGetInt32(out var md))
{
if (_mareConfig.Current.AutoDetectMaxDistanceMeters != md)
{
_mareConfig.Current.AutoDetectMaxDistanceMeters = md;
changed = true;
}
}
if (root.TryGetProperty("AutoSyncMuteMinutes", out var muteSync) && muteSync.TryGetInt32(out var mm))
{
if (_mareConfig.Current.AutoDetectMuteMinutes != mm)
{
_mareConfig.Current.AutoDetectMuteMinutes = mm;
changed = true;
}
}
if (changed)
{
_logger.LogInformation("Migrated config: AutoSync -> AutoDetect fields");
_mareConfig.Save();
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Configuration migration failed");
}
} }
public Task StartAsync(CancellationToken cancellationToken) public Task StartAsync(CancellationToken cancellationToken)

View File

@@ -12,6 +12,7 @@ public class CharaDataConfig : IMareConfiguration
public bool NearbyOwnServerOnly { get; set; } = false; public bool NearbyOwnServerOnly { get; set; } = false;
public bool NearbyIgnoreHousingLimitations { get; set; } = false; public bool NearbyIgnoreHousingLimitations { get; set; } = false;
public bool NearbyDrawWisps { get; set; } = true; public bool NearbyDrawWisps { get; set; } = true;
public int NearbyMaxWisps { get; set; } = 20;
public int NearbyDistanceFilter { get; set; } = 100; public int NearbyDistanceFilter { get; set; } = 100;
public bool NearbyShowOwnData { get; set; } = false; public bool NearbyShowOwnData { get; set; } = false;
public bool ShowHelpTexts { get; set; } = true; public bool ShowHelpTexts { get; set; } = true;

View File

@@ -1,4 +1,5 @@
using MareSynchronos.MareConfiguration.Models; using System.Collections.Generic;
using MareSynchronos.MareConfiguration.Models;
using MareSynchronos.UI; using MareSynchronos.UI;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@@ -7,6 +8,8 @@ namespace MareSynchronos.MareConfiguration.Configurations;
[Serializable] [Serializable]
public class MareConfig : IMareConfiguration public class MareConfig : IMareConfiguration
{ {
public int ExpectedTOSVersion = 2;
public int AcceptedTOSVersion { get; set; } = 0;
public bool AcceptedAgreement { get; set; } = false; public bool AcceptedAgreement { get; set; } = false;
public string CacheFolder { get; set; } = string.Empty; public string CacheFolder { get; set; } = string.Empty;
public bool DisableOptionalPluginWarnings { get; set; } = false; public bool DisableOptionalPluginWarnings { get; set; } = false;
@@ -17,7 +20,7 @@ public class MareConfig : IMareConfiguration
public bool UseColorsInDtr { get; set; } = true; public bool UseColorsInDtr { get; set; } = true;
public DtrEntry.Colors DtrColorsDefault { get; set; } = default; public DtrEntry.Colors DtrColorsDefault { get; set; } = default;
public DtrEntry.Colors DtrColorsNotConnected { get; set; } = new(Glow: 0x0428FFu); public DtrEntry.Colors DtrColorsNotConnected { get; set; } = new(Glow: 0x0428FFu);
public DtrEntry.Colors DtrColorsPairsInRange { get; set; } = new(Glow: 0xFFBA47u); public DtrEntry.Colors DtrColorsPairsInRange { get; set; } = new(Glow: 0x8D37C0u);
public bool UseNameColors { get; set; } = false; public bool UseNameColors { get; set; } = false;
public DtrEntry.Colors NameColors { get; set; } = new(Foreground: 0x67EBF5u, Glow: 0x00303Cu); public DtrEntry.Colors NameColors { get; set; } = new(Foreground: 0x67EBF5u, Glow: 0x00303Cu);
public DtrEntry.Colors BlockedNameColors { get; set; } = new(Foreground: 0x8AADC7, Glow: 0x000080u); public DtrEntry.Colors BlockedNameColors { get; set; } = new(Foreground: 0x8AADC7, Glow: 0x000080u);
@@ -31,10 +34,11 @@ public class MareConfig : IMareConfiguration
public bool LogPerformance { get; set; } = false; public bool LogPerformance { get; set; } = false;
public bool LogEvents { get; set; } = true; public bool LogEvents { get; set; } = true;
public bool HoldCombatApplication { get; set; } = false; public bool HoldCombatApplication { get; set; } = false;
public double MaxLocalCacheInGiB { get; set; } = 20; public double MaxLocalCacheInGiB { get; set; } = 100;
public bool OpenGposeImportOnGposeStart { get; set; } = false; public bool OpenGposeImportOnGposeStart { get; set; } = false;
public bool OpenPopupOnAdd { get; set; } = true; public bool OpenPopupOnAdd { get; set; } = true;
public int ParallelDownloads { get; set; } = 10; public int ParallelDownloads { get; set; } = 10;
public bool EnableDownloadQueue { get; set; } = false;
public int DownloadSpeedLimitInBytes { get; set; } = 0; public int DownloadSpeedLimitInBytes { get; set; } = 0;
public DownloadSpeeds DownloadSpeedType { get; set; } = DownloadSpeeds.MBps; public DownloadSpeeds DownloadSpeedType { get; set; } = DownloadSpeeds.MBps;
[Obsolete] public bool PreferNotesOverNamesForVisible { get; set; } = false; [Obsolete] public bool PreferNotesOverNamesForVisible { get; set; } = false;
@@ -57,6 +61,16 @@ public class MareConfig : IMareConfiguration
public bool ShowUploading { get; set; } = true; public bool ShowUploading { get; set; } = true;
public bool ShowUploadingBigText { get; set; } = true; public bool ShowUploadingBigText { get; set; } = true;
public bool ShowVisibleUsersSeparately { get; set; } = true; public bool ShowVisibleUsersSeparately { get; set; } = true;
public string LastChangelogVersionSeen { get; set; } = string.Empty;
public bool DefaultDisableSounds { get; set; } = false;
public bool DefaultDisableAnimations { get; set; } = false;
public bool DefaultDisableVfx { get; set; } = false;
public Dictionary<string, SyncOverrideEntry> PairSyncOverrides { get; set; } = new(StringComparer.Ordinal);
public Dictionary<string, SyncOverrideEntry> GroupSyncOverrides { get; set; } = new(StringComparer.Ordinal);
public bool EnableAutoDetectDiscovery { get; set; } = true;
public bool AllowAutoDetectPairRequests { get; set; } = true;
public int AutoDetectMaxDistanceMeters { get; set; } = 40;
public int AutoDetectMuteMinutes { get; set; } = 5;
public int TimeSpanBetweenScansInSeconds { get; set; } = 30; public int TimeSpanBetweenScansInSeconds { get; set; } = 30;
public int TransferBarsHeight { get; set; } = 12; public int TransferBarsHeight { get; set; } = 12;
public bool TransferBarsShowText { get; set; } = true; public bool TransferBarsShowText { get; set; } = true;
@@ -71,6 +85,9 @@ public class MareConfig : IMareConfiguration
public int ChatLogKind { get; set; } = 1; // XivChatType.Debug public int ChatLogKind { get; set; } = 1; // XivChatType.Debug
public bool ExtraChatAPI { get; set; } = false; public bool ExtraChatAPI { get; set; } = false;
public bool ExtraChatTags { get; set; } = false; public bool ExtraChatTags { get; set; } = false;
public bool TypingIndicatorShowOnNameplates { get; set; } = true;
public bool TypingIndicatorShowOnPartyList { get; set; } = true;
public TypingIndicatorBubbleSize TypingIndicatorBubbleSize { get; set; } = TypingIndicatorBubbleSize.Large;
public bool MareAPI { get; set; } = true; public bool MareAPI { get; set; } = true;
} }

View File

@@ -5,11 +5,12 @@ namespace MareSynchronos.MareConfiguration.Configurations;
public class PlayerPerformanceConfig : IMareConfiguration public class PlayerPerformanceConfig : IMareConfiguration
{ {
public int Version { get; set; } = 1; public int Version { get; set; } = 1;
public bool AutoPausePlayersExceedingThresholds { get; set; } = false; public bool AutoPausePlayersExceedingThresholds { get; set; } = true;
public bool NotifyAutoPauseDirectPairs { get; set; } = true; public bool NotifyAutoPauseDirectPairs { get; set; } = true;
public bool NotifyAutoPauseGroupPairs { get; set; } = false; public bool NotifyAutoPauseGroupPairs { get; set; } = true;
public int VRAMSizeAutoPauseThresholdMiB { get; set; } = 550; public bool ShowSelfAnalysisWarnings { get; set; } = true;
public int TrisAutoPauseThresholdThousands { get; set; } = 375; public int VRAMSizeAutoPauseThresholdMiB { get; set; } = 500;
public int TrisAutoPauseThresholdThousands { get; set; } = 400;
public bool IgnoreDirectPairs { get; set; } = true; public bool IgnoreDirectPairs { get; set; } = true;
public TextureShrinkMode TextureShrinkMode { get; set; } = TextureShrinkMode.Default; public TextureShrinkMode TextureShrinkMode { get; set; } = TextureShrinkMode.Default;
public bool TextureShrinkDeleteOriginal { get; set; } = false; public bool TextureShrinkDeleteOriginal { get; set; } = false;

View File

@@ -10,7 +10,7 @@ public class ServerConfig : IMareConfiguration
public List<ServerStorage> ServerStorage { get; set; } = new() public List<ServerStorage> ServerStorage { get; set; } = new()
{ {
{ new ServerStorage() { ServerName = ApiController.UmbraSyncServer, ServerUri = ApiController.UmbraSyncServiceUri } }, { new ServerStorage() { ServerName = ApiController.UmbraServer, ServerUri = ApiController.UmbraServiceUri } },
}; };
public int Version { get; set; } = 1; public int Version { get; set; } = 1;

View File

@@ -0,0 +1,13 @@
using System;
namespace MareSynchronos.MareConfiguration.Models;
[Serializable]
public class SyncOverrideEntry
{
public bool? DisableSounds { get; set; }
public bool? DisableAnimations { get; set; }
public bool? DisableVfx { get; set; }
public bool IsEmpty => DisableSounds is null && DisableAnimations is null && DisableVfx is null;
}

View File

@@ -0,0 +1,8 @@
namespace MareSynchronos.MareConfiguration.Models;
public enum TypingIndicatorBubbleSize
{
Small,
Medium,
Large
}

View File

@@ -5,6 +5,7 @@ using MareSynchronos.PlayerData.Services;
using MareSynchronos.Services; using MareSynchronos.Services;
using MareSynchronos.Services.Mediator; using MareSynchronos.Services.Mediator;
using MareSynchronos.Services.ServerConfiguration; using MareSynchronos.Services.ServerConfiguration;
using MareSynchronos.Interop;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@@ -89,9 +90,9 @@ public class MarePlugin : MediatorSubscriberBase, IHostedService
public Task StartAsync(CancellationToken cancellationToken) public Task StartAsync(CancellationToken cancellationToken)
{ {
var version = Assembly.GetExecutingAssembly().GetName().Version!; var version = Assembly.GetExecutingAssembly().GetName().Version!;
Logger.LogInformation("Launching {name} {major}.{minor}.{build}.{rev}", "UmbraSync", version.Major, version.Minor, version.Build, version.Revision); Logger.LogInformation("Launching {name} {major}.{minor}.{build}.{rev}", "Umbra Sync", version.Major, version.Minor, version.Build, version.Revision);
Mediator.Publish(new EventMessage(new Services.Events.Event(nameof(MarePlugin), Services.Events.EventSeverity.Informational, Mediator.Publish(new EventMessage(new Services.Events.Event(nameof(MarePlugin), Services.Events.EventSeverity.Informational,
$"Starting UmbraSync {version.Major}.{version.Minor}.{version.Build}.{version.Revision}"))); $"Starting Umbra Sync {version.Major}.{version.Minor}.{version.Build}.{version.Revision}")));
Mediator.Subscribe<SwitchToMainUiMessage>(this, (msg) => { if (_launchTask == null || _launchTask.IsCompleted) _launchTask = Task.Run(WaitForPlayerAndLaunchCharacterManager); }); Mediator.Subscribe<SwitchToMainUiMessage>(this, (msg) => { if (_launchTask == null || _launchTask.IsCompleted) _launchTask = Task.Run(WaitForPlayerAndLaunchCharacterManager); });
Mediator.Subscribe<DalamudLoginMessage>(this, (_) => DalamudUtilOnLogIn()); Mediator.Subscribe<DalamudLoginMessage>(this, (_) => DalamudUtilOnLogIn());
@@ -150,14 +151,18 @@ public class MarePlugin : MediatorSubscriberBase, IHostedService
_runtimeServiceScope.ServiceProvider.GetRequiredService<TransientResourceManager>(); _runtimeServiceScope.ServiceProvider.GetRequiredService<TransientResourceManager>();
_runtimeServiceScope.ServiceProvider.GetRequiredService<OnlinePlayerManager>(); _runtimeServiceScope.ServiceProvider.GetRequiredService<OnlinePlayerManager>();
_runtimeServiceScope.ServiceProvider.GetRequiredService<NotificationService>(); _runtimeServiceScope.ServiceProvider.GetRequiredService<NotificationService>();
_runtimeServiceScope.ServiceProvider.GetRequiredService<SyncDefaultsService>();
_runtimeServiceScope.ServiceProvider.GetRequiredService<ChatService>(); _runtimeServiceScope.ServiceProvider.GetRequiredService<ChatService>();
_runtimeServiceScope.ServiceProvider.GetRequiredService<ChatTypingDetectionService>();
_runtimeServiceScope.ServiceProvider.GetRequiredService<GuiHookService>(); _runtimeServiceScope.ServiceProvider.GetRequiredService<GuiHookService>();
var characterAnalyzer = _runtimeServiceScope.ServiceProvider.GetRequiredService<CharacterAnalyzer>();
_ = characterAnalyzer.ComputeAnalysis(print: false);
#if !DEBUG #if !DEBUG
if (_mareConfigService.Current.LogLevel != LogLevel.Information) if (_mareConfigService.Current.LogLevel != LogLevel.Information)
{ {
Mediator.Publish(new NotificationMessage("Abnormal Log Level", Mediator.Publish(new NotificationMessage("Abnormal Log Level",
$"Your log level is set to '{_mareConfigService.Current.LogLevel}' which is not recommended for normal usage. Set it to '{LogLevel.Information}' in \"UmbraSync Settings -> Debug\" unless instructed otherwise.", $"Your log level is set to '{_mareConfigService.Current.LogLevel}' which is not recommended for normal usage. Set it to '{LogLevel.Information}' in \"Umbra Settings -> Debug\" unless instructed otherwise.",
MareConfiguration.Models.NotificationType.Error, TimeSpan.FromSeconds(15000))); MareConfiguration.Models.NotificationType.Error, TimeSpan.FromSeconds(15000)));
} }
#endif #endif

View File

@@ -1,10 +1,10 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Dalamud.NET.Sdk/13.0.0"> <Project Sdk="Dalamud.NET.Sdk/13.0.0">
<PropertyGroup> <PropertyGroup>
<AssemblyName>UmbraSync</AssemblyName> <AssemblyName>UmbraSync</AssemblyName>
<Version>1.0.0</Version> <RootNamespace>UmbraSync</RootNamespace>
<PackageProjectUrl></PackageProjectUrl> <Version>0.1.9.6</Version>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Compile Remove="PlayerData\Export\**" /> <Compile Remove="PlayerData\Export\**" />
@@ -13,37 +13,38 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Chaos.NaCl.Standard" Version="1.0.0" />
<PackageReference Include="Downloader" Version="3.3.4" /> <PackageReference Include="Downloader" Version="3.3.4" />
<PackageReference Include="K4os.Compression.LZ4.Legacy" Version="1.3.8" /> <PackageReference Include="K4os.Compression.LZ4.Legacy" Version="1.3.8" />
<PackageReference Include="K4os.Compression.LZ4.Streams" Version="1.3.8" /> <PackageReference Include="K4os.Compression.LZ4.Streams" Version="1.3.8" />
<PackageReference Include="Meziantou.Analyzer" Version="2.0.189"> <PackageReference Include="Meziantou.Analyzer" Version="2.0.212">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="9.0.3" /> <PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="9.0.8" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="9.0.3" /> <PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="9.0.8" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.3" /> <PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.8" />
<PackageReference Include="SonarAnalyzer.CSharp" Version="10.7.0.110445"> <PackageReference Include="SonarAnalyzer.CSharp" Version="10.15.0.120848">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.7.0" /> <PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.14.0" />
</ItemGroup> </ItemGroup>
<ItemGroup Condition="Exists('..\Penumbra.Api\Penumbra.Api.csproj')"> <ItemGroup Condition="Exists('.\Penumbra.Api\Penumbra.Api.csproj')">
<ProjectReference Include="..\Penumbra.Api\Penumbra.Api.csproj" /> <ProjectReference Include=".\Penumbra.Api\Penumbra.Api.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup Condition="!Exists('..\Penumbra.Api\Penumbra.Api.csproj')"> <ItemGroup Condition="!Exists('.\Penumbra.Api\Penumbra.Api.csproj')">
<PackageReference Include="Penumbra.Api" Version="5.6.1" /> <PackageReference Include="Penumbra.Api" Version="5.12.0" />
</ItemGroup> </ItemGroup>
<ItemGroup Condition="Exists('..\Glamourer.Api\Glamourer.Api.csproj')"> <ItemGroup Condition="Exists('.\Glamourer.Api\Glamourer.Api.csproj')">
<ProjectReference Include="..\Glamourer.Api\Glamourer.Api.csproj" /> <ProjectReference Include=".\Glamourer.Api\Glamourer.Api.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup Condition="!Exists('..\Glamourer.Api\Glamourer.Api.csproj')"> <ItemGroup Condition="!Exists('.\Glamourer.Api\Glamourer.Api.csproj')">
<PackageReference Include="Glamourer.Api" Version="2.4.1" /> <PackageReference Include="Glamourer.Api" Version="2.6.0" />
</ItemGroup> </ItemGroup>
<PropertyGroup> <PropertyGroup>
@@ -52,14 +53,11 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\UmbraAPI\MareSynchronosAPI\MareSynchronos.API.csproj" /> <ProjectReference Include="..\MareAPI\MareSynchronosAPI\MareSynchronos.API.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<None Include="..\.editorconfig" Link=".editorconfig" /> <None Include="..\.editorconfig" Link=".editorconfig" />
</ItemGroup> </ItemGroup>
<ItemGroup Condition="Exists('..\\Chaos.NaCl\\Chaos.NaCl\\Chaos.NaCl.csproj')">
<ProjectReference Include="..\\Chaos.NaCl\\Chaos.NaCl\\Chaos.NaCl.csproj" />
</ItemGroup>
</Project> </Project>

View File

@@ -1,4 +1,5 @@
using MareSynchronos.FileCache; using MareSynchronos.FileCache;
using MareSynchronos.MareConfiguration;
using MareSynchronos.Services.Mediator; using MareSynchronos.Services.Mediator;
using MareSynchronos.WebAPI.Files; using MareSynchronos.WebAPI.Files;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@@ -10,21 +11,23 @@ public class FileDownloadManagerFactory
private readonly FileCacheManager _fileCacheManager; private readonly FileCacheManager _fileCacheManager;
private readonly FileCompactor _fileCompactor; private readonly FileCompactor _fileCompactor;
private readonly FileTransferOrchestrator _fileTransferOrchestrator; private readonly FileTransferOrchestrator _fileTransferOrchestrator;
private readonly MareConfigService _mareConfigService;
private readonly ILoggerFactory _loggerFactory; private readonly ILoggerFactory _loggerFactory;
private readonly MareMediator _mareMediator; private readonly MareMediator _mareMediator;
public FileDownloadManagerFactory(ILoggerFactory loggerFactory, MareMediator mareMediator, FileTransferOrchestrator fileTransferOrchestrator, public FileDownloadManagerFactory(ILoggerFactory loggerFactory, MareMediator mareMediator, FileTransferOrchestrator fileTransferOrchestrator,
FileCacheManager fileCacheManager, FileCompactor fileCompactor) FileCacheManager fileCacheManager, FileCompactor fileCompactor, MareConfigService mareConfigService)
{ {
_loggerFactory = loggerFactory; _loggerFactory = loggerFactory;
_mareMediator = mareMediator; _mareMediator = mareMediator;
_fileTransferOrchestrator = fileTransferOrchestrator; _fileTransferOrchestrator = fileTransferOrchestrator;
_fileCacheManager = fileCacheManager; _fileCacheManager = fileCacheManager;
_fileCompactor = fileCompactor; _fileCompactor = fileCompactor;
_mareConfigService = mareConfigService;
} }
public FileDownloadManager Create() public FileDownloadManager Create()
{ {
return new FileDownloadManager(_loggerFactory.CreateLogger<FileDownloadManager>(), _mareMediator, _fileTransferOrchestrator, _fileCacheManager, _fileCompactor); return new FileDownloadManager(_loggerFactory.CreateLogger<FileDownloadManager>(), _mareMediator, _fileTransferOrchestrator, _fileCacheManager, _fileCompactor, _mareConfigService);
} }
} }

View File

@@ -27,14 +27,13 @@ public class PairHandlerFactory
private readonly PluginWarningNotificationService _pluginWarningNotificationManager; private readonly PluginWarningNotificationService _pluginWarningNotificationManager;
private readonly PairAnalyzerFactory _pairAnalyzerFactory; private readonly PairAnalyzerFactory _pairAnalyzerFactory;
private readonly VisibilityService _visibilityService; private readonly VisibilityService _visibilityService;
private readonly NoSnapService _noSnapService;
public PairHandlerFactory(ILoggerFactory loggerFactory, GameObjectHandlerFactory gameObjectHandlerFactory, IpcManager ipcManager, public PairHandlerFactory(ILoggerFactory loggerFactory, GameObjectHandlerFactory gameObjectHandlerFactory, IpcManager ipcManager,
FileDownloadManagerFactory fileDownloadManagerFactory, DalamudUtilService dalamudUtilService, FileDownloadManagerFactory fileDownloadManagerFactory, DalamudUtilService dalamudUtilService,
PluginWarningNotificationService pluginWarningNotificationManager, IHostApplicationLifetime hostApplicationLifetime, PluginWarningNotificationService pluginWarningNotificationManager, IHostApplicationLifetime hostApplicationLifetime,
FileCacheManager fileCacheManager, MareMediator mareMediator, PlayerPerformanceService playerPerformanceService, FileCacheManager fileCacheManager, MareMediator mareMediator, PlayerPerformanceService playerPerformanceService,
ServerConfigurationManager serverConfigManager, PairAnalyzerFactory pairAnalyzerFactory, ServerConfigurationManager serverConfigManager, PairAnalyzerFactory pairAnalyzerFactory,
MareConfigService configService, VisibilityService visibilityService, NoSnapService noSnapService) MareConfigService configService, VisibilityService visibilityService)
{ {
_loggerFactory = loggerFactory; _loggerFactory = loggerFactory;
_gameObjectHandlerFactory = gameObjectHandlerFactory; _gameObjectHandlerFactory = gameObjectHandlerFactory;
@@ -50,13 +49,12 @@ public class PairHandlerFactory
_pairAnalyzerFactory = pairAnalyzerFactory; _pairAnalyzerFactory = pairAnalyzerFactory;
_configService = configService; _configService = configService;
_visibilityService = visibilityService; _visibilityService = visibilityService;
_noSnapService = noSnapService;
} }
public PairHandler Create(Pair pair) public PairHandler Create(Pair pair)
{ {
return new PairHandler(_loggerFactory.CreateLogger<PairHandler>(), pair, _pairAnalyzerFactory.Create(pair), _gameObjectHandlerFactory, return new PairHandler(_loggerFactory.CreateLogger<PairHandler>(), pair, _pairAnalyzerFactory.Create(pair), _gameObjectHandlerFactory,
_ipcManager, _fileDownloadManagerFactory.Create(), _pluginWarningNotificationManager, _dalamudUtilService, _hostApplicationLifetime, _ipcManager, _fileDownloadManagerFactory.Create(), _pluginWarningNotificationManager, _dalamudUtilService, _hostApplicationLifetime,
_fileCacheManager, _mareMediator, _playerPerformanceService, _serverConfigManager, _configService, _visibilityService, _noSnapService); _fileCacheManager, _mareMediator, _playerPerformanceService, _serverConfigManager, _configService, _visibilityService);
} }
} }

View File

@@ -32,7 +32,6 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase
private readonly ServerConfigurationManager _serverConfigManager; private readonly ServerConfigurationManager _serverConfigManager;
private readonly PluginWarningNotificationService _pluginWarningNotificationManager; private readonly PluginWarningNotificationService _pluginWarningNotificationManager;
private readonly VisibilityService _visibilityService; private readonly VisibilityService _visibilityService;
private readonly NoSnapService _noSnapService;
private CancellationTokenSource? _applicationCancellationTokenSource = new(); private CancellationTokenSource? _applicationCancellationTokenSource = new();
private Guid _applicationId; private Guid _applicationId;
private Task? _applicationTask; private Task? _applicationTask;
@@ -55,8 +54,7 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase
FileCacheManager fileDbManager, MareMediator mediator, FileCacheManager fileDbManager, MareMediator mediator,
PlayerPerformanceService playerPerformanceService, PlayerPerformanceService playerPerformanceService,
ServerConfigurationManager serverConfigManager, ServerConfigurationManager serverConfigManager,
MareConfigService configService, VisibilityService visibilityService, MareConfigService configService, VisibilityService visibilityService) : base(logger, mediator)
NoSnapService noSnapService) : base(logger, mediator)
{ {
Pair = pair; Pair = pair;
PairAnalyzer = pairAnalyzer; PairAnalyzer = pairAnalyzer;
@@ -70,7 +68,6 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase
_serverConfigManager = serverConfigManager; _serverConfigManager = serverConfigManager;
_configService = configService; _configService = configService;
_visibilityService = visibilityService; _visibilityService = visibilityService;
_noSnapService = noSnapService;
_visibilityService.StartTracking(Pair.Ident); _visibilityService.StartTracking(Pair.Ident);
@@ -319,24 +316,6 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase
}); });
} }
private void RegisterGposeClones()
{
var name = PlayerName;
if (name == null)
return;
_ = _dalamudUtil.RunOnFrameworkThread(() =>
{
foreach (var actor in _dalamudUtil.GetGposeCharactersFromObjectTable())
{
if (actor == null) continue;
var gposeName = actor.Name.TextValue;
if (!name.Equals(gposeName, StringComparison.Ordinal))
continue;
_noSnapService.AddGposer(actor.ObjectIndex);
}
});
}
private async Task UndoApplicationAsync(Guid applicationId = default) private async Task UndoApplicationAsync(Guid applicationId = default)
{ {
Logger.LogDebug($"Undoing application of {Pair.UserPair}"); Logger.LogDebug($"Undoing application of {Pair.UserPair}");
@@ -353,7 +332,6 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase
{ {
await _ipcManager.Penumbra.RemoveTemporaryCollectionAsync(Logger, applicationId, _penumbraCollection).ConfigureAwait(false); await _ipcManager.Penumbra.RemoveTemporaryCollectionAsync(Logger, applicationId, _penumbraCollection).ConfigureAwait(false);
_penumbraCollection = Guid.Empty; _penumbraCollection = Guid.Empty;
RegisterGposeClones();
} }
if (_dalamudUtil is { IsZoning: false, IsInCutscene: false } && !string.IsNullOrEmpty(name)) if (_dalamudUtil is { IsZoning: false, IsInCutscene: false } && !string.IsNullOrEmpty(name))
@@ -385,10 +363,6 @@ public sealed class PairHandler : DisposableMediatorSubscriberBase
} }
} }
} }
else if (_dalamudUtil.IsInCutscene && !string.IsNullOrEmpty(name))
{
_noSnapService.AddGposerNamed(name);
}
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@@ -86,8 +86,8 @@ public class Pair : DisposableMediatorSubscriberBase
{ {
Name = name, Name = name,
OnClicked = action, OnClicked = action,
PrefixColor = 559, PrefixColor = 526,
PrefixChar = 'L' PrefixChar = 'S'
}); });
} }
@@ -171,11 +171,6 @@ public class Pair : DisposableMediatorSubscriberBase
if (_serverConfigurationManager.IsUidBlacklisted(UserData.UID)) if (_serverConfigurationManager.IsUidBlacklisted(UserData.UID))
HoldApplication("Blacklist", maxValue: 1); HoldApplication("Blacklist", maxValue: 1);
if (NoSnapService.AnyLoaded)
HoldApplication("NoSnap", maxValue: 1);
else
UnholdApplication("NoSnap", skipApplication: true);
CachedPlayer.ApplyCharacterData(Guid.NewGuid(), RemoveNotSyncedFiles(LastReceivedCharacterData.DeepClone())!, forced); CachedPlayer.ApplyCharacterData(Guid.NewGuid(), RemoveNotSyncedFiles(LastReceivedCharacterData.DeepClone())!, forced);
} }

View File

@@ -51,7 +51,7 @@ public sealed class PairManager : DisposableMediatorSubscriberBase
RecreateLazy(); RecreateLazy();
} }
public void AddGroupPair(GroupPairFullInfoDto dto) public void AddGroupPair(GroupPairFullInfoDto dto, bool isInitialLoad = false)
{ {
if (!_allClientPairs.ContainsKey(dto.User)) if (!_allClientPairs.ContainsKey(dto.User))
_allClientPairs[dto.User] = _pairFactory.Create(dto.User); _allClientPairs[dto.User] = _pairFactory.Create(dto.User);
@@ -59,6 +59,11 @@ public sealed class PairManager : DisposableMediatorSubscriberBase
var group = _allGroups[dto.Group]; var group = _allGroups[dto.Group];
_allClientPairs[dto.User].GroupPair[group] = dto; _allClientPairs[dto.User].GroupPair[group] = dto;
RecreateLazy(); RecreateLazy();
if (!isInitialLoad)
{
Mediator.Publish(new ApplyDefaultGroupPermissionsMessage(dto));
}
} }
public Pair? GetPairByUID(string uid) public Pair? GetPairByUID(string uid)
@@ -88,6 +93,11 @@ public sealed class PairManager : DisposableMediatorSubscriberBase
LastAddedUser = _allClientPairs[dto.User]; LastAddedUser = _allClientPairs[dto.User];
_allClientPairs[dto.User].ApplyLastReceivedData(); _allClientPairs[dto.User].ApplyLastReceivedData();
RecreateLazy(); RecreateLazy();
if (addToLastAddedUser)
{
Mediator.Publish(new ApplyDefaultPairPermissionsMessage(dto));
}
} }
public void ClearPairs() public void ClearPairs()
@@ -210,9 +220,16 @@ public sealed class PairManager : DisposableMediatorSubscriberBase
public void SetGroupInfo(GroupInfoDto dto) public void SetGroupInfo(GroupInfoDto dto)
{ {
_allGroups[dto.Group].Group = dto.Group; if (!_allGroups.TryGetValue(dto.Group, out var groupInfo))
_allGroups[dto.Group].Owner = dto.Owner; {
_allGroups[dto.Group].GroupPermissions = dto.GroupPermissions; return;
}
groupInfo.Group = dto.Group;
groupInfo.Owner = dto.Owner;
groupInfo.GroupPermissions = dto.GroupPermissions;
groupInfo.IsTemporary = dto.IsTemporary;
groupInfo.ExpiresAt = dto.ExpiresAt;
RecreateLazy(); RecreateLazy();
} }

View File

@@ -29,7 +29,7 @@ using MareSynchronos.Services.CharaData;
using MareSynchronos; using MareSynchronos;
namespace UmbraSyncSync; namespace Umbra;
public sealed class Plugin : IDalamudPlugin public sealed class Plugin : IDalamudPlugin
{ {
@@ -39,8 +39,6 @@ public sealed class Plugin : IDalamudPlugin
public static Plugin Self; public static Plugin Self;
#pragma warning restore CA2211, CS8618, MA0069, S1104, S2223 #pragma warning restore CA2211, CS8618, MA0069, S1104, S2223
public Action<IFramework>? RealOnFrameworkUpdate { get; set; } public Action<IFramework>? RealOnFrameworkUpdate { get; set; }
// Proxy function in the UmbraSyncSync namespace to avoid confusion in /xlstats
public void OnFrameworkUpdate(IFramework framework) public void OnFrameworkUpdate(IFramework framework)
{ {
RealOnFrameworkUpdate?.Invoke(framework); RealOnFrameworkUpdate?.Invoke(framework);
@@ -98,6 +96,12 @@ public sealed class Plugin : IDalamudPlugin
collection.AddSingleton<HubFactory>(); collection.AddSingleton<HubFactory>();
collection.AddSingleton<FileUploadManager>(); collection.AddSingleton<FileUploadManager>();
collection.AddSingleton<FileTransferOrchestrator>(); collection.AddSingleton<FileTransferOrchestrator>();
collection.AddSingleton<MareSynchronos.Services.AutoDetect.DiscoveryConfigProvider>();
collection.AddSingleton<MareSynchronos.WebAPI.AutoDetect.DiscoveryApiClient>();
collection.AddSingleton<MareSynchronos.Services.AutoDetect.AutoDetectRequestService>();
collection.AddSingleton<MareSynchronos.Services.AutoDetect.NearbyDiscoveryService>();
collection.AddSingleton<MareSynchronos.Services.AutoDetect.NearbyPendingService>();
collection.AddSingleton<MareSynchronos.Services.AutoDetect.AutoDetectSuppressionService>();
collection.AddSingleton<MarePlugin>(); collection.AddSingleton<MarePlugin>();
collection.AddSingleton<MareProfileManager>(); collection.AddSingleton<MareProfileManager>();
collection.AddSingleton<GameObjectHandlerFactory>(); collection.AddSingleton<GameObjectHandlerFactory>();
@@ -112,6 +116,7 @@ public sealed class Plugin : IDalamudPlugin
collection.AddSingleton<PluginWarningNotificationService>(); collection.AddSingleton<PluginWarningNotificationService>();
collection.AddSingleton<FileCompactor>(); collection.AddSingleton<FileCompactor>();
collection.AddSingleton<TagHandler>(); collection.AddSingleton<TagHandler>();
collection.AddSingleton<SyncDefaultsService>();
collection.AddSingleton<UidDisplayHandler>(); collection.AddSingleton<UidDisplayHandler>();
collection.AddSingleton<PluginWatcherService>(); collection.AddSingleton<PluginWatcherService>();
collection.AddSingleton<PlayerPerformanceService>(); collection.AddSingleton<PlayerPerformanceService>();
@@ -126,7 +131,6 @@ public sealed class Plugin : IDalamudPlugin
collection.AddSingleton<BlockedCharacterHandler>(); collection.AddSingleton<BlockedCharacterHandler>();
collection.AddSingleton<IpcProvider>(); collection.AddSingleton<IpcProvider>();
collection.AddSingleton<VisibilityService>(); collection.AddSingleton<VisibilityService>();
collection.AddSingleton<RepoChangeService>();
collection.AddSingleton<EventAggregator>(); collection.AddSingleton<EventAggregator>();
collection.AddSingleton<DalamudUtilService>(); collection.AddSingleton<DalamudUtilService>();
collection.AddSingleton<DtrEntry>(); collection.AddSingleton<DtrEntry>();
@@ -143,7 +147,10 @@ public sealed class Plugin : IDalamudPlugin
collection.AddSingleton<IpcCallerMare>(); collection.AddSingleton<IpcCallerMare>();
collection.AddSingleton<IpcManager>(); collection.AddSingleton<IpcManager>();
collection.AddSingleton<NotificationService>(); collection.AddSingleton<NotificationService>();
collection.AddSingleton<NoSnapService>(); collection.AddSingleton<TemporarySyncshellNotificationService>();
collection.AddSingleton<PartyListTypingService>();
collection.AddSingleton<TypingIndicatorStateService>();
collection.AddSingleton<ChatTwoCompatibilityService>();
collection.AddSingleton((s) => new MareConfigService(pluginInterface.ConfigDirectory.FullName)); collection.AddSingleton((s) => new MareConfigService(pluginInterface.ConfigDirectory.FullName));
collection.AddSingleton((s) => new ServerConfigService(pluginInterface.ConfigDirectory.FullName)); collection.AddSingleton((s) => new ServerConfigService(pluginInterface.ConfigDirectory.FullName));
@@ -169,7 +176,6 @@ public sealed class Plugin : IDalamudPlugin
collection.AddSingleton<IConfigService<IMareConfiguration>>(s => s.GetRequiredService<RemoteConfigCacheService>()); collection.AddSingleton<IConfigService<IMareConfiguration>>(s => s.GetRequiredService<RemoteConfigCacheService>());
collection.AddSingleton<ConfigurationMigrator>(); collection.AddSingleton<ConfigurationMigrator>();
collection.AddSingleton<ConfigurationSaveService>(); collection.AddSingleton<ConfigurationSaveService>();
collection.AddSingleton<RemoteConfigurationService>();
collection.AddSingleton<HubFactory>(); collection.AddSingleton<HubFactory>();
@@ -180,12 +186,15 @@ public sealed class Plugin : IDalamudPlugin
collection.AddScoped<WindowMediatorSubscriberBase, CompactUi>(); collection.AddScoped<WindowMediatorSubscriberBase, CompactUi>();
collection.AddScoped<WindowMediatorSubscriberBase, IntroUi>(); collection.AddScoped<WindowMediatorSubscriberBase, IntroUi>();
collection.AddScoped<WindowMediatorSubscriberBase, DownloadUi>(); collection.AddScoped<WindowMediatorSubscriberBase, DownloadUi>();
collection.AddScoped<WindowMediatorSubscriberBase, AutoDetectUi>();
collection.AddScoped<WindowMediatorSubscriberBase, ChangelogUi>();
collection.AddScoped<WindowMediatorSubscriberBase, PopoutProfileUi>(); collection.AddScoped<WindowMediatorSubscriberBase, PopoutProfileUi>();
collection.AddScoped<WindowMediatorSubscriberBase, DataAnalysisUi>(); collection.AddScoped<WindowMediatorSubscriberBase, DataAnalysisUi>();
collection.AddScoped<WindowMediatorSubscriberBase, EventViewerUI>(); collection.AddScoped<WindowMediatorSubscriberBase, EventViewerUI>();
collection.AddScoped<WindowMediatorSubscriberBase, CharaDataHubUi>(); collection.AddScoped<WindowMediatorSubscriberBase, CharaDataHubUi>();
collection.AddScoped<WindowMediatorSubscriberBase, EditProfileUi>(); collection.AddScoped<WindowMediatorSubscriberBase, EditProfileUi>();
collection.AddScoped<WindowMediatorSubscriberBase, PopupHandler>(); collection.AddScoped<WindowMediatorSubscriberBase, PopupHandler>();
collection.AddScoped<WindowMediatorSubscriberBase, TypingIndicatorOverlay>();
collection.AddScoped<IPopupHandler, ReportPopupHandler>(); collection.AddScoped<IPopupHandler, ReportPopupHandler>();
collection.AddScoped<IPopupHandler, BanUserPopupHandler>(); collection.AddScoped<IPopupHandler, BanUserPopupHandler>();
collection.AddScoped<CacheCreationService>(); collection.AddScoped<CacheCreationService>();
@@ -197,11 +206,13 @@ public sealed class Plugin : IDalamudPlugin
collection.AddScoped<UiSharedService>(); collection.AddScoped<UiSharedService>();
collection.AddScoped<ChatService>(); collection.AddScoped<ChatService>();
collection.AddScoped<GuiHookService>(); collection.AddScoped<GuiHookService>();
collection.AddScoped<ChatTypingDetectionService>();
collection.AddHostedService(p => p.GetRequiredService<PluginWatcherService>()); collection.AddHostedService(p => p.GetRequiredService<PluginWatcherService>());
collection.AddHostedService(p => p.GetRequiredService<ConfigurationSaveService>()); collection.AddHostedService(p => p.GetRequiredService<ConfigurationSaveService>());
collection.AddHostedService(p => p.GetRequiredService<MareMediator>()); collection.AddHostedService(p => p.GetRequiredService<MareMediator>());
collection.AddHostedService(p => p.GetRequiredService<NotificationService>()); collection.AddHostedService(p => p.GetRequiredService<NotificationService>());
collection.AddHostedService(p => p.GetRequiredService<TemporarySyncshellNotificationService>());
collection.AddHostedService(p => p.GetRequiredService<FileCacheManager>()); collection.AddHostedService(p => p.GetRequiredService<FileCacheManager>());
collection.AddHostedService(p => p.GetRequiredService<ConfigurationMigrator>()); collection.AddHostedService(p => p.GetRequiredService<ConfigurationMigrator>());
collection.AddHostedService(p => p.GetRequiredService<DalamudUtilService>()); collection.AddHostedService(p => p.GetRequiredService<DalamudUtilService>());
@@ -210,11 +221,22 @@ public sealed class Plugin : IDalamudPlugin
collection.AddHostedService(p => p.GetRequiredService<EventAggregator>()); collection.AddHostedService(p => p.GetRequiredService<EventAggregator>());
collection.AddHostedService(p => p.GetRequiredService<MarePlugin>()); collection.AddHostedService(p => p.GetRequiredService<MarePlugin>());
collection.AddHostedService(p => p.GetRequiredService<IpcProvider>()); collection.AddHostedService(p => p.GetRequiredService<IpcProvider>());
collection.AddHostedService(p => p.GetRequiredService<RepoChangeService>()); collection.AddHostedService(p => p.GetRequiredService<MareSynchronos.Services.AutoDetect.NearbyDiscoveryService>());
collection.AddHostedService(p => p.GetRequiredService<NoSnapService>()); collection.AddHostedService(p => p.GetRequiredService<ChatTwoCompatibilityService>());
collection.AddHostedService(p => p.GetRequiredService<MareSynchronos.Services.AutoDetect.AutoDetectSuppressionService>());
}) })
.Build(); .Build();
try
{
var partyListTypingService = _host.Services.GetRequiredService<PartyListTypingService>();
pluginInterface.UiBuilder.Draw += partyListTypingService.Draw;
}
catch (Exception e)
{
pluginLog.Warning(e, "Failed to initialize PartyListTypingService draw hook");
}
_ = Task.Run(async () => { _ = Task.Run(async () => {
try try
{ {

View File

@@ -0,0 +1,386 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Microsoft.Extensions.Logging;
using MareSynchronos.WebAPI.AutoDetect;
using MareSynchronos.MareConfiguration;
using MareSynchronos.Services;
using MareSynchronos.Services.Mediator;
using NotificationType = MareSynchronos.MareConfiguration.Models.NotificationType;
using MareSynchronos.WebAPI;
using MareSynchronos.API.Dto.User;
using MareSynchronos.API.Data;
namespace MareSynchronos.Services.AutoDetect;
public class AutoDetectRequestService
{
private readonly ILogger<AutoDetectRequestService> _logger;
private readonly DiscoveryConfigProvider _configProvider;
private readonly DiscoveryApiClient _client;
private readonly MareConfigService _configService;
private readonly DalamudUtilService _dalamud;
private readonly MareMediator _mediator;
private readonly object _syncRoot = new();
private readonly Dictionary<string, DateTime> _activeCooldowns = new(StringComparer.Ordinal);
private readonly Dictionary<string, RefusalTracker> _refusalTrackers = new(StringComparer.Ordinal);
private readonly ConcurrentDictionary<string, PendingRequestInfo> _pendingRequests = new(StringComparer.Ordinal);
private static readonly TimeSpan RequestCooldown = TimeSpan.FromMinutes(5);
private static readonly TimeSpan RefusalLockDuration = TimeSpan.FromMinutes(15);
private readonly ApiController _apiController;
public AutoDetectRequestService(ILogger<AutoDetectRequestService> logger, DiscoveryConfigProvider configProvider, DiscoveryApiClient client, MareConfigService configService, MareMediator mediator, DalamudUtilService dalamudUtilService, ApiController apiController)
{
_logger = logger;
_configProvider = configProvider;
_client = client;
_configService = configService;
_mediator = mediator;
_dalamud = dalamudUtilService;
_apiController = apiController;
}
public async Task<bool> SendRequestAsync(string? token, string? uid = null, string? targetDisplayName = null, CancellationToken ct = default)
{
if (!_configService.Current.AllowAutoDetectPairRequests)
{
_logger.LogDebug("Nearby request blocked: AllowAutoDetectPairRequests is disabled");
_mediator.Publish(new NotificationMessage("Nearby request blocked", "Enable 'Allow pair requests' in Settings to send requests.", NotificationType.Info));
return false;
}
if (string.IsNullOrEmpty(token) && string.IsNullOrEmpty(uid))
{
_logger.LogDebug("Nearby request blocked: no token or UID provided");
return false;
}
var targetKey = BuildTargetKey(uid, token, targetDisplayName);
if (!string.IsNullOrEmpty(targetKey))
{
var now = DateTime.UtcNow;
lock (_syncRoot)
{
if (_refusalTrackers.TryGetValue(targetKey, out var tracker))
{
if (tracker.LockUntil.HasValue && tracker.LockUntil.Value > now)
{
PublishLockNotification(tracker.LockUntil.Value - now);
return false;
}
if (tracker.LockUntil.HasValue && tracker.LockUntil.Value <= now)
{
tracker.LockUntil = null;
tracker.Count = 0;
if (tracker.Count == 0 && tracker.LockUntil == null)
{
_refusalTrackers.Remove(targetKey);
}
}
}
if (_activeCooldowns.TryGetValue(targetKey, out var lastSent))
{
var elapsed = now - lastSent;
if (elapsed < RequestCooldown)
{
PublishCooldownNotification(RequestCooldown - elapsed);
return false;
}
if (elapsed >= RequestCooldown)
{
_activeCooldowns.Remove(targetKey);
}
}
}
}
var endpoint = _configProvider.RequestEndpoint;
if (string.IsNullOrEmpty(endpoint))
{
_logger.LogDebug("No request endpoint configured");
_mediator.Publish(new NotificationMessage("Nearby request failed", "Server does not expose request endpoint.", NotificationType.Error));
return false;
}
string? displayName = null;
try
{
var me = await _dalamud.RunOnFrameworkThread(() => _dalamud.GetPlayerCharacter()).ConfigureAwait(false);
displayName = me?.Name.TextValue;
}
catch { }
var requestToken = string.IsNullOrEmpty(token) ? null : token;
var requestUid = requestToken == null ? uid : null;
_logger.LogInformation("Nearby: sending pair request via {endpoint}", endpoint);
var ok = await _client.SendRequestAsync(endpoint!, requestToken, requestUid, displayName, ct).ConfigureAwait(false);
if (ok)
{
if (!string.IsNullOrEmpty(targetKey))
{
lock (_syncRoot)
{
_activeCooldowns[targetKey] = DateTime.UtcNow;
if (_refusalTrackers.TryGetValue(targetKey, out var tracker))
{
tracker.Count = 0;
tracker.LockUntil = null;
if (tracker.Count == 0 && tracker.LockUntil == null)
{
_refusalTrackers.Remove(targetKey);
}
}
}
}
_mediator.Publish(new NotificationMessage("Nearby request sent", "The other user will receive a request notification.", NotificationType.Info));
var pendingKey = EnsureTargetKey(targetKey);
var label = !string.IsNullOrWhiteSpace(targetDisplayName)
? targetDisplayName!
: (!string.IsNullOrEmpty(uid) ? uid : (!string.IsNullOrEmpty(token) ? token : pendingKey));
_pendingRequests[pendingKey] = new PendingRequestInfo(pendingKey, uid, token, label, DateTime.UtcNow);
}
else
{
if (!string.IsNullOrEmpty(targetKey))
{
var now = DateTime.UtcNow;
lock (_syncRoot)
{
_activeCooldowns.Remove(targetKey);
if (!_refusalTrackers.TryGetValue(targetKey, out var tracker))
{
tracker = new RefusalTracker();
_refusalTrackers[targetKey] = tracker;
}
if (tracker.LockUntil.HasValue && tracker.LockUntil.Value <= now)
{
tracker.LockUntil = null;
tracker.Count = 0;
}
tracker.Count++;
if (tracker.Count >= 3)
{
tracker.LockUntil = now.Add(RefusalLockDuration);
}
}
_pendingRequests.TryRemove(targetKey, out _);
}
_mediator.Publish(new NotificationMessage("Nearby request failed", "The server rejected the request. Try again soon.", NotificationType.Warning));
}
return ok;
}
public async Task<bool> SendAcceptNotifyAsync(string targetUid, CancellationToken ct = default)
{
var endpoint = _configProvider.AcceptEndpoint;
if (string.IsNullOrEmpty(endpoint))
{
_logger.LogDebug("No accept endpoint configured");
return false;
}
string? displayName = null;
try
{
var me = await _dalamud.RunOnFrameworkThread(() => _dalamud.GetPlayerCharacter()).ConfigureAwait(false);
displayName = me?.Name.TextValue;
}
catch { }
_logger.LogInformation("Nearby: sending accept notify via {endpoint}", endpoint);
return await _client.SendAcceptAsync(endpoint!, targetUid, displayName, ct).ConfigureAwait(false);
}
public async Task<bool> SendDirectUidRequestAsync(string uid, string? targetDisplayName = null, CancellationToken ct = default)
{
if (!_configService.Current.AllowAutoDetectPairRequests)
{
_logger.LogDebug("Nearby request blocked: AllowAutoDetectPairRequests is disabled");
_mediator.Publish(new NotificationMessage("Nearby request blocked", "Enable 'Allow pair requests' in Settings to send requests.", NotificationType.Info));
return false;
}
if (string.IsNullOrEmpty(uid))
{
_logger.LogDebug("Direct pair request aborted: UID is empty");
return false;
}
var targetKey = BuildTargetKey(uid, null, targetDisplayName);
if (!string.IsNullOrEmpty(targetKey))
{
var now = DateTime.UtcNow;
lock (_syncRoot)
{
if (_refusalTrackers.TryGetValue(targetKey, out var tracker))
{
if (tracker.LockUntil.HasValue && tracker.LockUntil.Value > now)
{
PublishLockNotification(tracker.LockUntil.Value - now);
return false;
}
if (tracker.LockUntil.HasValue && tracker.LockUntil.Value <= now)
{
tracker.LockUntil = null;
tracker.Count = 0;
if (tracker.Count == 0 && tracker.LockUntil == null)
{
_refusalTrackers.Remove(targetKey);
}
}
}
if (_activeCooldowns.TryGetValue(targetKey, out var lastSent))
{
var elapsed = now - lastSent;
if (elapsed < RequestCooldown)
{
PublishCooldownNotification(RequestCooldown - elapsed);
return false;
}
if (elapsed >= RequestCooldown)
{
_activeCooldowns.Remove(targetKey);
}
}
}
}
try
{
await _apiController.UserAddPair(new UserDto(new UserData(uid))).ConfigureAwait(false);
if (!string.IsNullOrEmpty(targetKey))
{
lock (_syncRoot)
{
_activeCooldowns[targetKey] = DateTime.UtcNow;
if (_refusalTrackers.TryGetValue(targetKey, out var tracker))
{
tracker.Count = 0;
tracker.LockUntil = null;
if (tracker.Count == 0 && tracker.LockUntil == null)
{
_refusalTrackers.Remove(targetKey);
}
}
}
}
_mediator.Publish(new NotificationMessage("Nearby request sent", "The other user will receive a request notification.", NotificationType.Info));
var pendingKey = EnsureTargetKey(targetKey);
var label = !string.IsNullOrWhiteSpace(targetDisplayName) ? targetDisplayName! : uid;
_pendingRequests[pendingKey] = new PendingRequestInfo(pendingKey, uid, null, label, DateTime.UtcNow);
return true;
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Direct pair request failed for {uid}", uid);
if (!string.IsNullOrEmpty(targetKey))
{
var now = DateTime.UtcNow;
lock (_syncRoot)
{
_activeCooldowns.Remove(targetKey);
if (!_refusalTrackers.TryGetValue(targetKey, out var tracker))
{
tracker = new RefusalTracker();
_refusalTrackers[targetKey] = tracker;
}
if (tracker.LockUntil.HasValue && tracker.LockUntil.Value <= now)
{
tracker.LockUntil = null;
tracker.Count = 0;
}
tracker.Count++;
if (tracker.Count >= 3)
{
tracker.LockUntil = now.Add(RefusalLockDuration);
}
}
_pendingRequests.TryRemove(targetKey, out _);
}
_mediator.Publish(new NotificationMessage("Nearby request failed", "The server rejected the request. Try again soon.", NotificationType.Warning));
return false;
}
}
private static string? BuildTargetKey(string? uid, string? token, string? displayName)
{
if (!string.IsNullOrEmpty(uid)) return "uid:" + uid;
if (!string.IsNullOrEmpty(token)) return "token:" + token;
if (!string.IsNullOrEmpty(displayName)) return "name:" + displayName;
return null;
}
private void PublishCooldownNotification(TimeSpan remaining)
{
var durationText = FormatDuration(remaining);
_mediator.Publish(new NotificationMessage("Nearby request en attente", $"Nearby request déjà envoyée. Merci d'attendre environ {durationText} avant de réessayer.", NotificationType.Info, TimeSpan.FromSeconds(5)));
}
private void PublishLockNotification(TimeSpan remaining)
{
var durationText = FormatDuration(remaining);
_mediator.Publish(new NotificationMessage("Nearby request bloquée", $"Nearby request bloquée après plusieurs refus. Réessayez dans {durationText}.", NotificationType.Warning, TimeSpan.FromSeconds(5)));
}
private static string FormatDuration(TimeSpan remaining)
{
if (remaining.TotalMinutes >= 1)
{
var minutes = Math.Max(1, (int)Math.Ceiling(remaining.TotalMinutes));
return minutes == 1 ? "1 minute" : minutes + " minutes";
}
var seconds = Math.Max(1, (int)Math.Ceiling(remaining.TotalSeconds));
return seconds == 1 ? "1 seconde" : seconds + " secondes";
}
private sealed class RefusalTracker
{
public int Count;
public DateTime? LockUntil;
}
public IReadOnlyCollection<PendingRequestInfo> GetPendingRequestsSnapshot()
{
return _pendingRequests.Values.OrderByDescending(v => v.SentAt).ToList();
}
public void RemovePendingRequestByUid(string uid)
{
if (string.IsNullOrEmpty(uid)) return;
foreach (var kvp in _pendingRequests)
{
if (string.Equals(kvp.Value.Uid, uid, StringComparison.Ordinal))
{
_pendingRequests.TryRemove(kvp.Key, out _);
break;
}
}
}
public void RemovePendingRequestByKey(string key)
{
if (string.IsNullOrEmpty(key)) return;
_pendingRequests.TryRemove(key, out _);
}
private static string EnsureTargetKey(string? targetKey)
{
return !string.IsNullOrEmpty(targetKey) ? targetKey! : "target:" + Guid.NewGuid().ToString("N");
}
public sealed record PendingRequestInfo(string Key, string? Uid, string? Token, string TargetDisplayName, DateTime SentAt);
}

View File

@@ -0,0 +1,209 @@
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Dalamud.Plugin.Services;
using Lumina.Excel.Sheets;
using MareSynchronos.MareConfiguration;
using MareSynchronos.MareConfiguration.Models;
using MareSynchronos.Services;
using MareSynchronos.Services.Mediator;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace MareSynchronos.Services.AutoDetect;
public sealed class AutoDetectSuppressionService : IHostedService, IMediatorSubscriber
{
private static readonly string[] ContentTypeKeywords =
[
"dungeon",
"donjon",
"raid",
"trial",
"défi",
"front",
"frontline",
"pvp",
"jcj",
"conflict",
"conflit"
];
private readonly ILogger<AutoDetectSuppressionService> _logger;
private readonly MareConfigService _configService;
private readonly IClientState _clientState;
private readonly IDataManager _dataManager;
private readonly MareMediator _mediator;
private readonly DalamudUtilService _dalamudUtilService;
private bool _isSuppressed;
private bool _hasSavedState;
private bool _savedDiscoveryEnabled;
private bool _savedAllowRequests;
private bool _suppressionWarningShown;
public bool IsSuppressed => _isSuppressed;
public AutoDetectSuppressionService(ILogger<AutoDetectSuppressionService> logger,
MareConfigService configService, IClientState clientState,
IDataManager dataManager, DalamudUtilService dalamudUtilService, MareMediator mediator)
{
_logger = logger;
_configService = configService;
_clientState = clientState;
_dataManager = dataManager;
_dalamudUtilService = dalamudUtilService;
_mediator = mediator;
}
public MareMediator Mediator => _mediator;
public Task StartAsync(CancellationToken cancellationToken)
{
_mediator.Subscribe<ZoneSwitchEndMessage>(this, _ => UpdateSuppressionState());
_mediator.Subscribe<DalamudLoginMessage>(this, _ => UpdateSuppressionState());
_mediator.Subscribe<DalamudLogoutMessage>(this, _ => ClearSuppression());
UpdateSuppressionState();
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
_mediator.UnsubscribeAll(this);
return Task.CompletedTask;
}
private void UpdateSuppressionState()
{
_ = _dalamudUtilService.RunOnFrameworkThread(() =>
{
try
{
if (!_clientState.IsLoggedIn || _clientState.LocalPlayer == null)
{
ClearSuppression();
return;
}
uint territoryId = _clientState.TerritoryType;
bool shouldSuppress = ShouldSuppressForTerritory(territoryId);
ApplySuppression(shouldSuppress, territoryId);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to update AutoDetect suppression state");
}
});
}
private void ApplySuppression(bool shouldSuppress, uint territoryId)
{
if (shouldSuppress)
{
if (!_isSuppressed)
{
_savedDiscoveryEnabled = _configService.Current.EnableAutoDetectDiscovery;
_savedAllowRequests = _configService.Current.AllowAutoDetectPairRequests;
_hasSavedState = true;
_isSuppressed = true;
}
bool changed = false;
if (_configService.Current.EnableAutoDetectDiscovery)
{
_configService.Current.EnableAutoDetectDiscovery = false;
changed = true;
}
if (_configService.Current.AllowAutoDetectPairRequests)
{
_configService.Current.AllowAutoDetectPairRequests = false;
changed = true;
}
if (changed)
{
_logger.LogInformation("AutoDetect temporarily disabled in instanced content (territory {territoryId}).", territoryId);
if (!_suppressionWarningShown)
{
_suppressionWarningShown = true;
const string warningText = "Zone instanciée détectée : les fonctions AutoDetect/Nearby sont coupées pour économiser de la bande passante.";
_mediator.Publish(new DualNotificationMessage("AutoDetect désactivé",
warningText,
NotificationType.Warning, TimeSpan.FromSeconds(5)));
}
}
return;
}
else
{
if (!_isSuppressed) return;
bool changed = false;
bool wasSuppressed = _suppressionWarningShown;
if (_hasSavedState)
{
if (_configService.Current.EnableAutoDetectDiscovery != _savedDiscoveryEnabled)
{
_configService.Current.EnableAutoDetectDiscovery = _savedDiscoveryEnabled;
changed = true;
}
if (_configService.Current.AllowAutoDetectPairRequests != _savedAllowRequests)
{
_configService.Current.AllowAutoDetectPairRequests = _savedAllowRequests;
changed = true;
}
}
_isSuppressed = false;
_hasSavedState = false;
_suppressionWarningShown = false;
if (changed || wasSuppressed)
{
_logger.LogInformation("AutoDetect restored after leaving instanced content (territory {territoryId}).", territoryId);
const string restoredText = "Vous avez quitté la zone instanciée : AutoDetect/Nearby fonctionnent de nouveau.";
_mediator.Publish(new DualNotificationMessage("AutoDetect réactivé",
restoredText,
NotificationType.Info, TimeSpan.FromSeconds(5)));
}
}
}
private void ClearSuppression()
{
if (!_isSuppressed) return;
_isSuppressed = false;
if (_hasSavedState)
{
_configService.Current.EnableAutoDetectDiscovery = _savedDiscoveryEnabled;
_configService.Current.AllowAutoDetectPairRequests = _savedAllowRequests;
}
_hasSavedState = false;
_suppressionWarningShown = false;
}
private bool ShouldSuppressForTerritory(uint territoryId)
{
if (territoryId == 0) return false;
var cfcSheet = _dataManager.GetExcelSheet<ContentFinderCondition>();
if (cfcSheet == null) return false;
var cfc = cfcSheet.FirstOrDefault(c => c.TerritoryType.RowId == territoryId);
if (cfc.RowId == 0) return false;
if (MatchesSuppressionKeyword(cfc.Name.ToString())) return true;
var contentType = cfc.ContentType.Value;
if (contentType.RowId == 0) return false;
return MatchesSuppressionKeyword(contentType.Name.ToString());
}
private static bool MatchesSuppressionKeyword(string? text)
{
if (string.IsNullOrWhiteSpace(text)) return false;
return ContentTypeKeywords.Any(keyword => text.Contains(keyword, StringComparison.OrdinalIgnoreCase));
}
}

View File

@@ -0,0 +1,173 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.Logging;
using MareSynchronos.Services.Mediator;
using MareSynchronos.Services.ServerConfiguration;
using MareSynchronos.WebAPI.SignalR;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Reflection;
namespace MareSynchronos.Services.AutoDetect;
public class DiscoveryConfigProvider
{
private readonly ILogger<DiscoveryConfigProvider> _logger;
private readonly ServerConfigurationManager _serverManager;
private readonly TokenProvider _tokenProvider;
private WellKnownRoot? _config;
private DateTimeOffset _lastLoad = DateTimeOffset.MinValue;
public DiscoveryConfigProvider(ILogger<DiscoveryConfigProvider> logger, ServerConfigurationManager serverManager, TokenProvider tokenProvider)
{
_logger = logger;
_serverManager = serverManager;
_tokenProvider = tokenProvider;
}
public bool HasConfig => _config != null;
public bool NearbyEnabled => _config?.NearbyDiscovery?.Enabled ?? false;
public byte[]? Salt => _config?.NearbyDiscovery?.SaltBytes;
public string? SaltB64 => _config?.NearbyDiscovery?.SaltB64;
public DateTimeOffset? SaltExpiresAt => _config?.NearbyDiscovery?.SaltExpiresAt;
public int RefreshSec => _config?.NearbyDiscovery?.RefreshSec ?? 300;
public int MinQueryIntervalMs => _config?.NearbyDiscovery?.Policies?.MinQueryIntervalMs ?? 2000;
public int MaxQueryBatch => _config?.NearbyDiscovery?.Policies?.MaxQueryBatch ?? 100;
public string? PublishEndpoint => _config?.NearbyDiscovery?.Endpoints?.Publish;
public string? QueryEndpoint => _config?.NearbyDiscovery?.Endpoints?.Query;
public string? RequestEndpoint => _config?.NearbyDiscovery?.Endpoints?.Request;
public string? AcceptEndpoint => _config?.NearbyDiscovery?.Endpoints?.Accept;
public bool TryLoadFromStapled()
{
try
{
var json = _tokenProvider.GetStapledWellKnown(_serverManager.CurrentApiUrl);
if (string.IsNullOrEmpty(json)) return false;
var root = JsonSerializer.Deserialize<WellKnownRoot>(json!);
if (root == null) return false;
root.NearbyDiscovery?.Hydrate();
_config = root;
_lastLoad = DateTimeOffset.UtcNow;
_logger.LogDebug("Loaded Nearby well-known (stapled), enabled={enabled}, expires={exp}", NearbyEnabled, _config?.NearbyDiscovery?.SaltExpiresAt);
return true;
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to parse stapled well-known");
return false;
}
}
public async Task<bool> TryFetchFromServerAsync(CancellationToken ct = default)
{
try
{
var baseUrl = _serverManager.CurrentApiUrl
.Replace("wss://", "https://", StringComparison.OrdinalIgnoreCase)
.Replace("ws://", "http://", StringComparison.OrdinalIgnoreCase);
// Try likely candidates based on nginx config
string[] candidates =
[
"/.well-known/Umbra/client", // matches provided nginx
"/.well-known/umbra", // lowercase variant
];
using var http = new HttpClient();
try
{
var ver = Assembly.GetExecutingAssembly().GetName().Version!;
http.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("MareSynchronos", $"{ver.Major}.{ver.Minor}.{ver.Build}"));
}
catch { }
foreach (var path in candidates)
{
try
{
var uri = new Uri(new Uri(baseUrl), path);
var json = await http.GetStringAsync(uri, ct).ConfigureAwait(false);
if (string.IsNullOrEmpty(json)) continue;
var root = JsonSerializer.Deserialize<WellKnownRoot>(json);
if (root == null) continue;
root.NearbyDiscovery?.Hydrate();
_config = root;
_lastLoad = DateTimeOffset.UtcNow;
_logger.LogInformation("Loaded Nearby well-known (http {path}), enabled={enabled}", path, NearbyEnabled);
return true;
}
catch (Exception ex)
{
_logger.LogDebug(ex, "Nearby well-known fetch failed for {path}", path);
}
}
_logger.LogInformation("Nearby well-known not found via HTTP candidates");
return false;
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to fetch Nearby well-known via HTTP");
return false;
}
}
public bool IsExpired()
{
if (_config?.NearbyDiscovery?.SaltExpiresAt == null) return false;
return DateTimeOffset.UtcNow > _config.NearbyDiscovery.SaltExpiresAt;
}
// DTOs for well-known JSON
private sealed class WellKnownRoot
{
[JsonPropertyName("features")] public Features? Features { get; set; }
[JsonPropertyName("nearby_discovery")] public Nearby? NearbyDiscovery { get; set; }
}
private sealed class Features
{
[JsonPropertyName("nearby_discovery")] public bool NearbyDiscovery { get; set; }
}
private sealed class Nearby
{
[JsonPropertyName("enabled")] public bool Enabled { get; set; }
[JsonPropertyName("hash_algo")] public string? HashAlgo { get; set; }
[JsonPropertyName("salt_b64")] public string? SaltB64 { get; set; }
[JsonPropertyName("salt_expires_at")] public string? SaltExpiresAtRaw { get; set; }
[JsonPropertyName("refresh_sec")] public int RefreshSec { get; set; } = 300;
[JsonPropertyName("endpoints")] public Endpoints? Endpoints { get; set; }
[JsonPropertyName("policies")] public Policies? Policies { get; set; }
[JsonIgnore] public byte[]? SaltBytes { get; private set; }
[JsonIgnore] public DateTimeOffset? SaltExpiresAt { get; private set; }
public void Hydrate()
{
try { SaltBytes = string.IsNullOrEmpty(SaltB64) ? null : Convert.FromBase64String(SaltB64!); } catch { SaltBytes = null; }
if (DateTimeOffset.TryParse(SaltExpiresAtRaw, out var dto)) SaltExpiresAt = dto;
}
}
private sealed class Endpoints
{
[JsonPropertyName("publish")] public string? Publish { get; set; }
[JsonPropertyName("query")] public string? Query { get; set; }
[JsonPropertyName("request")] public string? Request { get; set; }
[JsonPropertyName("accept")] public string? Accept { get; set; }
}
private sealed class Policies
{
[JsonPropertyName("max_query_batch")] public int MaxQueryBatch { get; set; } = 100;
[JsonPropertyName("min_query_interval_ms")] public int MinQueryIntervalMs { get; set; } = 2000;
[JsonPropertyName("rate_limit_per_min")] public int RateLimitPerMin { get; set; } = 30;
[JsonPropertyName("token_ttl_sec")] public int TokenTtlSec { get; set; } = 120;
}
}

View File

@@ -0,0 +1,477 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Hosting;
using MareSynchronos.Services.Mediator;
using MareSynchronos.MareConfiguration;
using MareSynchronos.Services.ServerConfiguration;
using MareSynchronos.WebAPI.AutoDetect;
using Dalamud.Plugin.Services;
using System.Numerics;
using System.Linq;
using MareSynchronos.Utils;
namespace MareSynchronos.Services.AutoDetect;
public class NearbyDiscoveryService : IHostedService, IMediatorSubscriber
{
private readonly ILogger<NearbyDiscoveryService> _logger;
private readonly MareMediator _mediator;
private readonly MareConfigService _config;
private readonly DiscoveryConfigProvider _configProvider;
private readonly DalamudUtilService _dalamud;
private readonly IObjectTable _objectTable;
private readonly DiscoveryApiClient _api;
private CancellationTokenSource? _loopCts;
private string? _lastPublishedSignature;
private bool _loggedLocalOnly;
private int _lastLocalCount = -1;
private int _lastMatchCount = -1;
private bool _loggedConfigReady;
private string? _lastSnapshotSig;
private volatile bool _isConnected;
private bool _notifiedDisabled;
private bool _notifiedEnabled;
private bool _disableSent;
private bool _lastAutoDetectState;
private DateTime _lastHeartbeat = DateTime.MinValue;
private static readonly TimeSpan HeartbeatInterval = TimeSpan.FromSeconds(75);
public NearbyDiscoveryService(ILogger<NearbyDiscoveryService> logger, MareMediator mediator,
MareConfigService config, DiscoveryConfigProvider configProvider, DalamudUtilService dalamudUtilService,
IObjectTable objectTable, DiscoveryApiClient api)
{
_logger = logger;
_mediator = mediator;
_config = config;
_configProvider = configProvider;
_dalamud = dalamudUtilService;
_objectTable = objectTable;
_api = api;
}
public MareMediator Mediator => _mediator;
public Task StartAsync(CancellationToken cancellationToken)
{
_loopCts = new CancellationTokenSource();
_mediator.Subscribe<ConnectedMessage>(this, _ => { _isConnected = true; _configProvider.TryLoadFromStapled(); });
_mediator.Subscribe<DisconnectedMessage>(this, _ => { _isConnected = false; _lastPublishedSignature = null; });
_mediator.Subscribe<AllowPairRequestsToggled>(this, OnAllowPairRequestsToggled);
_ = Task.Run(() => Loop(_loopCts.Token));
_lastAutoDetectState = _config.Current.EnableAutoDetectDiscovery;
return Task.CompletedTask;
}
private async void OnAllowPairRequestsToggled(AllowPairRequestsToggled msg)
{
try
{
if (!_config.Current.EnableAutoDetectDiscovery) return;
// Force a publish now so the server immediately reflects the new allow/deny state
_lastPublishedSignature = null; // ensure next loop won't skip
await PublishSelfOnceAsync(CancellationToken.None).ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.LogDebug(ex, "OnAllowPairRequestsToggled failed");
}
}
private async Task PublishSelfOnceAsync(CancellationToken ct)
{
try
{
if (!_config.Current.EnableAutoDetectDiscovery || !_dalamud.IsLoggedIn || !_isConnected) return;
if (!_configProvider.HasConfig || _configProvider.IsExpired())
{
if (!_configProvider.TryLoadFromStapled())
{
await _configProvider.TryFetchFromServerAsync(ct).ConfigureAwait(false);
}
}
var ep = _configProvider.PublishEndpoint;
var saltBytes = _configProvider.Salt;
if (string.IsNullOrEmpty(ep) || saltBytes is not { Length: > 0 }) return;
var saltHex = Convert.ToHexString(saltBytes);
string? displayName = null;
ushort meWorld = 0;
try
{
var me = await _dalamud.RunOnFrameworkThread(() => _dalamud.GetPlayerCharacter()).ConfigureAwait(false);
if (me != null)
{
displayName = me.Name.TextValue;
if (me is Dalamud.Game.ClientState.Objects.SubKinds.IPlayerCharacter mePc)
meWorld = (ushort)mePc.HomeWorld.RowId;
}
}
catch { }
if (string.IsNullOrEmpty(displayName)) return;
var selfHash = (saltHex + displayName + meWorld.ToString()).GetHash256();
var ok = await _api.PublishAsync(ep!, new[] { selfHash }, displayName, ct, _config.Current.AllowAutoDetectPairRequests).ConfigureAwait(false);
_logger.LogInformation("Nearby publish (manual/immediate): {result}", ok ? "success" : "failed");
if (ok)
{
_lastPublishedSignature = selfHash;
_lastHeartbeat = DateTime.UtcNow;
}
}
catch (Exception ex)
{
_logger.LogDebug(ex, "Immediate publish failed");
}
}
public Task StopAsync(CancellationToken cancellationToken)
{
_mediator.UnsubscribeAll(this);
try { _loopCts?.Cancel(); } catch { }
return Task.CompletedTask;
}
private async Task Loop(CancellationToken ct)
{
_configProvider.TryLoadFromStapled();
while (!ct.IsCancellationRequested)
{
try
{
bool currentState = _config.Current.EnableAutoDetectDiscovery;
if (currentState != _lastAutoDetectState)
{
_lastAutoDetectState = currentState;
if (currentState)
{
// Force immediate publish on toggle ON
try
{
// Ensure well-known is present
if (!_configProvider.HasConfig || _configProvider.IsExpired())
{
if (!_configProvider.TryLoadFromStapled())
{
await _configProvider.TryFetchFromServerAsync(ct).ConfigureAwait(false);
}
}
var ep = _configProvider.PublishEndpoint;
var saltBytes = _configProvider.Salt;
if (!string.IsNullOrEmpty(ep) && saltBytes is { Length: > 0 })
{
var saltHex = Convert.ToHexString(saltBytes);
string? displayName = null;
ushort meWorld = 0;
try
{
var me = await _dalamud.RunOnFrameworkThread(() => _dalamud.GetPlayerCharacter()).ConfigureAwait(false);
if (me != null)
{
displayName = me.Name.TextValue;
if (me is Dalamud.Game.ClientState.Objects.SubKinds.IPlayerCharacter mePc)
meWorld = (ushort)mePc.HomeWorld.RowId;
}
}
catch { }
if (!string.IsNullOrEmpty(displayName))
{
var selfHash = (saltHex + displayName + meWorld.ToString()).GetHash256();
_lastPublishedSignature = null; // ensure future loop doesn't skip
var okNow = await _api.PublishAsync(ep!, new[] { selfHash }, displayName, ct, _config.Current.AllowAutoDetectPairRequests).ConfigureAwait(false);
_logger.LogInformation("Nearby immediate publish on toggle ON: {result}", okNow ? "success" : "failed");
}
}
}
catch (Exception ex)
{
_logger.LogDebug(ex, "Nearby immediate publish on toggle ON failed");
}
if (!_notifiedEnabled)
{
_mediator.Publish(new NotificationMessage("Nearby Detection", "AutoDetect enabled : you are now visible.", default));
_notifiedEnabled = true;
_notifiedDisabled = false;
_disableSent = false;
}
}
else
{
var ep = _configProvider.PublishEndpoint;
if (!string.IsNullOrEmpty(ep) && !_disableSent)
{
var disableUrl = ep.Replace("/publish", "/disable");
try { await _api.DisableAsync(disableUrl, ct).ConfigureAwait(false); _disableSent = true; } catch { }
}
if (!_notifiedDisabled)
{
_mediator.Publish(new NotificationMessage("Nearby Detection", "AutoDetect disabled : you are not visible.", default));
_notifiedDisabled = true;
_notifiedEnabled = false;
}
}
}
if (!_config.Current.EnableAutoDetectDiscovery || !_dalamud.IsLoggedIn || !_isConnected)
{
if (!_config.Current.EnableAutoDetectDiscovery && !string.IsNullOrEmpty(_configProvider.PublishEndpoint))
{
var disableUrl = _configProvider.PublishEndpoint.Replace("/publish", "/disable");
try
{
if (!_disableSent)
{
await _api.DisableAsync(disableUrl, ct).ConfigureAwait(false);
_disableSent = true;
}
}
catch { }
if (!_notifiedDisabled)
{
_mediator.Publish(new NotificationMessage("Nearby Detection", "AutoDetect disabled : you are not visible.", default));
_notifiedDisabled = true;
_notifiedEnabled = false;
}
}
await Task.Delay(1000, ct).ConfigureAwait(false);
continue;
}
if (!_configProvider.HasConfig || _configProvider.IsExpired())
{
if (!_configProvider.TryLoadFromStapled())
{
await _configProvider.TryFetchFromServerAsync(ct).ConfigureAwait(false);
}
}
else if (!_loggedConfigReady && _configProvider.NearbyEnabled)
{
_loggedConfigReady = true;
_logger.LogInformation("Nearby: well-known loaded and enabled; refresh={refresh}s, expires={exp}", _configProvider.RefreshSec, _configProvider.SaltExpiresAt);
}
var entries = await GetLocalNearbyAsync().ConfigureAwait(false);
// Log when local count changes (including 0) to indicate activity
if (entries.Count != _lastLocalCount)
{
_lastLocalCount = entries.Count;
_logger.LogTrace("Nearby: {count} players detected locally", _lastLocalCount);
}
// Try query server if config and endpoints are present
if (_configProvider.NearbyEnabled && !_configProvider.IsExpired() &&
_configProvider.Salt is { Length: > 0 })
{
try
{
var saltHex = Convert.ToHexString(_configProvider.Salt!);
// map hash->index for result matching
Dictionary<string, int> hashToIndex = new(StringComparer.Ordinal);
List<string> hashes = new(entries.Count);
foreach (var (entry, idx) in entries.Select((e, i) => (e, i)))
{
var h = (saltHex + entry.Name + entry.WorldId.ToString()).GetHash256();
hashToIndex[h] = idx;
hashes.Add(h);
}
try
{
var snapSig = string.Join(',', hashes.OrderBy(s => s, StringComparer.Ordinal)).GetHash256();
if (!string.Equals(snapSig, _lastSnapshotSig, StringComparison.Ordinal))
{
_lastSnapshotSig = snapSig;
var sample = entries.Take(5).Select(e =>
{
var hh = (saltHex + e.Name + e.WorldId.ToString()).GetHash256();
var shortH = hh.Length > 8 ? hh[..8] : hh;
return $"{e.Name}({e.WorldId})->{shortH}";
});
var saltShort = saltHex.Length > 8 ? saltHex[..8] : saltHex;
_logger.LogTrace("Nearby snapshot: {count} entries; salt={saltShort}…; samples=[{samples}]",
entries.Count, saltShort, string.Join(", ", sample));
}
}
catch { }
if (!string.IsNullOrEmpty(_configProvider.PublishEndpoint))
{
string? displayName = null;
string? selfHash = null;
try
{
var me = await _dalamud.RunOnFrameworkThread(() => _dalamud.GetPlayerCharacter()).ConfigureAwait(false);
if (me != null)
{
displayName = me.Name.TextValue;
ushort meWorld = 0;
if (me is Dalamud.Game.ClientState.Objects.SubKinds.IPlayerCharacter mePc)
meWorld = (ushort)mePc.HomeWorld.RowId;
_logger.LogTrace("Nearby self ident: {name} ({world})", displayName, meWorld);
selfHash = (saltHex + displayName + meWorld.ToString()).GetHash256();
}
}
catch { /* ignore */ }
if (!string.IsNullOrEmpty(selfHash))
{
var sig = selfHash!;
if (!string.Equals(sig, _lastPublishedSignature, StringComparison.Ordinal))
{
_lastPublishedSignature = sig;
var shortSelf = selfHash!.Length > 8 ? selfHash[..8] : selfHash;
_logger.LogDebug("Nearby publish: self presence updated (hash={hash})", shortSelf);
var ok = await _api.PublishAsync(_configProvider.PublishEndpoint!, new[] { selfHash! }, displayName, ct, _config.Current.AllowAutoDetectPairRequests).ConfigureAwait(false);
_logger.LogInformation("Nearby publish result: {result}", ok ? "success" : "failed");
if (ok) _lastHeartbeat = DateTime.UtcNow;
if (ok)
{
if (!_notifiedEnabled)
{
_mediator.Publish(new NotificationMessage("Nearby Detection", "AutoDetect enabled : you are now visible.", default));
_notifiedEnabled = true;
_notifiedDisabled = false;
_disableSent = false; // allow future /disable when turning off again
}
}
}
else
{
// No changes; perform heartbeat publish if interval elapsed
if (DateTime.UtcNow - _lastHeartbeat >= HeartbeatInterval)
{
var okHb = await _api.PublishAsync(_configProvider.PublishEndpoint!, new[] { selfHash! }, displayName, ct, _config.Current.AllowAutoDetectPairRequests).ConfigureAwait(false);
_logger.LogDebug("Nearby heartbeat publish: {result}", okHb ? "success" : "failed");
if (okHb) _lastHeartbeat = DateTime.UtcNow;
}
else
{
_logger.LogDebug("Nearby publish skipped (no changes)");
}
}
}
// else: no self character available; skip publish silently
}
// Query for matches if endpoint is available
if (!string.IsNullOrEmpty(_configProvider.QueryEndpoint))
{
// chunked queries
int batch = Math.Max(1, _configProvider.MaxQueryBatch);
List<ServerMatch> allMatches = new();
for (int i = 0; i < hashes.Count; i += batch)
{
var slice = hashes.Skip(i).Take(batch).ToArray();
var res = await _api.QueryAsync(_configProvider.QueryEndpoint!, slice, ct).ConfigureAwait(false);
if (res != null && res.Count > 0) allMatches.AddRange(res);
}
if (allMatches.Count > 0)
{
foreach (var m in allMatches)
{
if (hashToIndex.TryGetValue(m.Hash, out var idx))
{
var e = entries[idx];
entries[idx] = new NearbyEntry(e.Name, e.WorldId, e.Distance, true, m.Token, m.DisplayName, m.Uid);
}
}
}
if (allMatches.Count > 0)
{
_logger.LogInformation("Nearby: server returned {count} matches", allMatches.Count);
}
else
{
_logger.LogTrace("Nearby: server returned {count} matches", allMatches.Count);
}
// Log change in number of Umbra matches
int matchCount = entries.Count(e => e.IsMatch);
if (matchCount != _lastMatchCount)
{
_lastMatchCount = matchCount;
if (matchCount > 0)
{
var matchSamples = entries.Where(e => e.IsMatch).Take(5)
.Select(e => string.IsNullOrEmpty(e.DisplayName) ? e.Name : e.DisplayName!);
_logger.LogInformation("Nearby: {count} Umbra users nearby [{samples}]",
matchCount, string.Join(", ", matchSamples));
}
else
{
_logger.LogTrace("Nearby: {count} Umbra users nearby", matchCount);
}
}
}
}
catch (Exception ex)
{
_logger.LogDebug(ex, "Nearby query failed; falling back to local list");
if (ex.Message.Contains("DISCOVERY_SALT_EXPIRED", StringComparison.OrdinalIgnoreCase))
{
_logger.LogInformation("Nearby: salt expired, refetching well-known");
try { await _configProvider.TryFetchFromServerAsync(ct).ConfigureAwait(false); } catch { }
}
}
}
else
{
if (!_loggedLocalOnly)
{
_loggedLocalOnly = true;
_logger.LogDebug("Nearby: well-known not available or disabled; running in local-only mode");
}
}
_mediator.Publish(new DiscoveryListUpdated(entries));
var delayMs = Math.Max(1000, _configProvider.MinQueryIntervalMs);
if (entries.Count == 0) delayMs = Math.Max(delayMs, 5000);
await Task.Delay(delayMs, ct).ConfigureAwait(false);
}
catch (OperationCanceledException) { }
catch (Exception ex)
{
_logger.LogDebug(ex, "NearbyDiscoveryService loop error");
await Task.Delay(2000, ct).ConfigureAwait(false);
}
}
}
private async Task<List<NearbyEntry>> GetLocalNearbyAsync()
{
var list = new List<NearbyEntry>();
try
{
var local = await _dalamud.RunOnFrameworkThread(() => _dalamud.GetPlayerCharacter()).ConfigureAwait(false);
var localPos = local?.Position ?? Vector3.Zero;
int maxDist = Math.Clamp(_config.Current.AutoDetectMaxDistanceMeters, 5, 100);
int limit = Math.Min(200, _objectTable.Length);
for (int i = 0; i < limit; i++)
{
var obj = await _dalamud.RunOnFrameworkThread(() => _objectTable[i]).ConfigureAwait(false);
if (obj == null || obj.ObjectKind != Dalamud.Game.ClientState.Objects.Enums.ObjectKind.Player) continue;
if (local != null && obj.Address == local.Address) continue;
float dist = local == null ? float.NaN : Vector3.Distance(localPos, obj.Position);
if (!float.IsNaN(dist) && dist > maxDist) continue;
string name = obj.Name.TextValue;
ushort worldId = 0;
if (obj is Dalamud.Game.ClientState.Objects.SubKinds.IPlayerCharacter pc)
worldId = (ushort)pc.HomeWorld.RowId;
list.Add(new NearbyEntry(name, worldId, dist, false, null, null, null));
}
}
catch
{
// ignore
}
return list;
}
}

View File

@@ -0,0 +1,107 @@
using System;
using System.Collections.Concurrent;
using System.Text.RegularExpressions;
using MareSynchronos.MareConfiguration.Models;
using MareSynchronos.Services.Mediator;
using MareSynchronos.WebAPI;
using Microsoft.Extensions.Logging;
namespace MareSynchronos.Services.AutoDetect;
public sealed class NearbyPendingService : IMediatorSubscriber
{
private readonly ILogger<NearbyPendingService> _logger;
private readonly MareMediator _mediator;
private readonly ApiController _api;
private readonly AutoDetectRequestService _requestService;
private readonly ConcurrentDictionary<string, string> _pending = new(StringComparer.Ordinal);
private static readonly Regex ReqRegex = new(@"^Nearby Request: (.+) \[(?<uid>[A-Z0-9]+)\]$", RegexOptions.Compiled);
private static readonly Regex AcceptRegex = new(@"^Nearby Accept: (.+) \[(?<uid>[A-Z0-9]+)\]$", RegexOptions.Compiled);
public NearbyPendingService(ILogger<NearbyPendingService> logger, MareMediator mediator, ApiController api, AutoDetectRequestService requestService)
{
_logger = logger;
_mediator = mediator;
_api = api;
_requestService = requestService;
_mediator.Subscribe<NotificationMessage>(this, OnNotification);
_mediator.Subscribe<ManualPairInviteMessage>(this, OnManualPairInvite);
}
public MareMediator Mediator => _mediator;
public IReadOnlyDictionary<string, string> Pending => _pending;
private void OnNotification(NotificationMessage msg)
{
// Watch info messages for Nearby request pattern
if (msg.Type != MareSynchronos.MareConfiguration.Models.NotificationType.Info) return;
var ma = AcceptRegex.Match(msg.Message);
if (ma.Success)
{
var uidA = ma.Groups["uid"].Value;
if (!string.IsNullOrEmpty(uidA))
{
_ = _api.UserAddPair(new MareSynchronos.API.Dto.User.UserDto(new MareSynchronos.API.Data.UserData(uidA)));
_pending.TryRemove(uidA, out _);
_requestService.RemovePendingRequestByUid(uidA);
_logger.LogInformation("NearbyPending: auto-accepted pairing with {uid}", uidA);
}
return;
}
var m = ReqRegex.Match(msg.Message);
if (!m.Success) return;
var uid = m.Groups["uid"].Value;
if (string.IsNullOrEmpty(uid)) return;
// Try to extract name as everything before space and '['
var name = msg.Message;
try
{
var idx = msg.Message.IndexOf(':');
if (idx >= 0) name = msg.Message[(idx + 1)..].Trim();
var br = name.LastIndexOf('[');
if (br > 0) name = name[..br].Trim();
}
catch { name = uid; }
_pending[uid] = name;
_logger.LogInformation("NearbyPending: received request from {uid} ({name})", uid, name);
}
private void OnManualPairInvite(ManualPairInviteMessage msg)
{
if (!string.Equals(msg.TargetUid, _api.UID, StringComparison.Ordinal))
return;
var display = !string.IsNullOrWhiteSpace(msg.DisplayName)
? msg.DisplayName!
: (!string.IsNullOrWhiteSpace(msg.SourceAlias) ? msg.SourceAlias : msg.SourceUid);
_pending[msg.SourceUid] = display;
_logger.LogInformation("NearbyPending: received manual invite from {uid} ({name})", msg.SourceUid, display);
_mediator.Publish(new NotificationMessage("Nearby request", $"{display} vous a envoyé une invitation de pair.", NotificationType.Info, TimeSpan.FromSeconds(5)));
}
public void Remove(string uid)
{
_pending.TryRemove(uid, out _);
_requestService.RemovePendingRequestByUid(uid);
}
public async Task<bool> AcceptAsync(string uid)
{
try
{
await _api.UserAddPair(new MareSynchronos.API.Dto.User.UserDto(new MareSynchronos.API.Data.UserData(uid))).ConfigureAwait(false);
_pending.TryRemove(uid, out _);
_requestService.RemovePendingRequestByUid(uid);
_ = _requestService.SendAcceptNotifyAsync(uid);
return true;
}
catch (Exception ex)
{
_logger.LogWarning(ex, "NearbyPending: accept failed for {uid}", uid);
return false;
}
}
}

View File

@@ -13,20 +13,18 @@ public sealed class CharaDataCharacterHandler : DisposableMediatorSubscriberBase
private readonly GameObjectHandlerFactory _gameObjectHandlerFactory; private readonly GameObjectHandlerFactory _gameObjectHandlerFactory;
private readonly DalamudUtilService _dalamudUtilService; private readonly DalamudUtilService _dalamudUtilService;
private readonly IpcManager _ipcManager; private readonly IpcManager _ipcManager;
private readonly NoSnapService _noSnapService;
private readonly Dictionary<string, HandledCharaDataEntry> _handledCharaData = new(StringComparer.Ordinal); private readonly Dictionary<string, HandledCharaDataEntry> _handledCharaData = new(StringComparer.Ordinal);
public IReadOnlyDictionary<string, HandledCharaDataEntry> HandledCharaData => _handledCharaData; public IReadOnlyDictionary<string, HandledCharaDataEntry> HandledCharaData => _handledCharaData;
public CharaDataCharacterHandler(ILogger<CharaDataCharacterHandler> logger, MareMediator mediator, public CharaDataCharacterHandler(ILogger<CharaDataCharacterHandler> logger, MareMediator mediator,
GameObjectHandlerFactory gameObjectHandlerFactory, DalamudUtilService dalamudUtilService, GameObjectHandlerFactory gameObjectHandlerFactory, DalamudUtilService dalamudUtilService,
IpcManager ipcManager, NoSnapService noSnapService) IpcManager ipcManager)
: base(logger, mediator) : base(logger, mediator)
{ {
_gameObjectHandlerFactory = gameObjectHandlerFactory; _gameObjectHandlerFactory = gameObjectHandlerFactory;
_dalamudUtilService = dalamudUtilService; _dalamudUtilService = dalamudUtilService;
_ipcManager = ipcManager; _ipcManager = ipcManager;
_noSnapService = noSnapService;
mediator.Subscribe<GposeEndMessage>(this, msg => mediator.Subscribe<GposeEndMessage>(this, msg =>
{ {
foreach (var chara in _handledCharaData) foreach (var chara in _handledCharaData)
@@ -94,7 +92,6 @@ public sealed class CharaDataCharacterHandler : DisposableMediatorSubscriberBase
_handledCharaData.Remove(handled.Name); _handledCharaData.Remove(handled.Name);
await _dalamudUtilService.RunOnFrameworkThread(async () => await _dalamudUtilService.RunOnFrameworkThread(async () =>
{ {
RemoveGposer(handled);
await RevertChara(handled.Name, handled.CustomizePlus).ConfigureAwait(false); await RevertChara(handled.Name, handled.CustomizePlus).ConfigureAwait(false);
}).ConfigureAwait(false); }).ConfigureAwait(false);
return true; return true;
@@ -103,7 +100,6 @@ public sealed class CharaDataCharacterHandler : DisposableMediatorSubscriberBase
internal void AddHandledChara(HandledCharaDataEntry handledCharaDataEntry) internal void AddHandledChara(HandledCharaDataEntry handledCharaDataEntry)
{ {
_handledCharaData.Add(handledCharaDataEntry.Name, handledCharaDataEntry); _handledCharaData.Add(handledCharaDataEntry.Name, handledCharaDataEntry);
_ = _dalamudUtilService.RunOnFrameworkThread(() => AddGposer(handledCharaDataEntry));
} }
public void UpdateHandledData(Dictionary<string, CharaDataMetaInfoExtendedDto?> newData) public void UpdateHandledData(Dictionary<string, CharaDataMetaInfoExtendedDto?> newData)
@@ -134,23 +130,4 @@ public sealed class CharaDataCharacterHandler : DisposableMediatorSubscriberBase
if (handler.Address == nint.Zero) return null; if (handler.Address == nint.Zero) return null;
return handler; return handler;
} }
private int GetGposerObjectIndex(string name)
{
return _dalamudUtilService.GetGposeCharacterFromObjectTableByName(name, _dalamudUtilService.IsInGpose)?.ObjectIndex ?? -1;
}
private void AddGposer(HandledCharaDataEntry handled)
{
int objectIndex = GetGposerObjectIndex(handled.Name);
if (objectIndex > 0)
_noSnapService.AddGposer(objectIndex);
}
private void RemoveGposer(HandledCharaDataEntry handled)
{
int objectIndex = GetGposerObjectIndex(handled.Name);
if (objectIndex > 0)
_noSnapService.RemoveGposer(objectIndex);
}
} }

View File

@@ -201,7 +201,7 @@ public sealed class CharaDataNearbyManager : DisposableMediatorSubscriberBase
foreach (var data in _metaInfoCache.Where(d => (string.IsNullOrWhiteSpace(UserNoteFilter) foreach (var data in _metaInfoCache.Where(d => (string.IsNullOrWhiteSpace(UserNoteFilter)
|| ((d.Key.Alias ?? string.Empty).Contains(UserNoteFilter, StringComparison.OrdinalIgnoreCase) || ((d.Key.Alias ?? string.Empty).Contains(UserNoteFilter, StringComparison.OrdinalIgnoreCase)
|| d.Key.UID.Contains(UserNoteFilter, StringComparison.OrdinalIgnoreCase) || d.Key.UID.Contains(UserNoteFilter, StringComparison.OrdinalIgnoreCase)
|| (_serverConfigurationManager.GetNoteForUid(UserNoteFilter) ?? string.Empty).Contains(UserNoteFilter, StringComparison.OrdinalIgnoreCase)))) || (_serverConfigurationManager.GetNoteForUid(d.Key.UID) ?? string.Empty).Contains(UserNoteFilter, StringComparison.OrdinalIgnoreCase))))
.ToDictionary(k => k.Key, k => k.Value)) .ToDictionary(k => k.Key, k => k.Value))
{ {
// filter all poses based on territory, that always must be correct // filter all poses based on territory, that always must be correct
@@ -266,26 +266,47 @@ public sealed class CharaDataNearbyManager : DisposableMediatorSubscriberBase
private void ManageWispsNearby(List<PoseEntryExtended> previousPoses) private void ManageWispsNearby(List<PoseEntryExtended> previousPoses)
{ {
foreach (var data in _nearbyData.Keys) int maxWisps = _charaDataConfigService.Current.NearbyMaxWisps;
if (maxWisps <= 0)
{ {
if (_poseVfx.TryGetValue(data, out var _)) continue; ClearAllVfx();
return;
}
Guid? vfxGuid; const int hardLimit = 200;
if (data.MetaInfo.IsOwnData) if (maxWisps > hardLimit) maxWisps = hardLimit;
var orderedAllowedPoses = _nearbyData
.OrderBy(kvp => kvp.Value.Distance)
.Take(maxWisps)
.Select(kvp => kvp.Key)
.ToList();
var allowedPoseSet = orderedAllowedPoses.ToHashSet();
foreach (var data in orderedAllowedPoses)
{ {
vfxGuid = _vfxSpawnManager.SpawnObject(data.Position, data.Rotation, Vector3.One * 2, 0.8f, 0.5f, 0.0f, 0.7f); if (_poseVfx.TryGetValue(data, out _)) continue;
}
else Guid? vfxGuid = data.MetaInfo.IsOwnData
{ ? _vfxSpawnManager.SpawnObject(data.Position, data.Rotation, Vector3.One * 2, 0.8f, 0.5f, 0.0f, 0.7f)
vfxGuid = _vfxSpawnManager.SpawnObject(data.Position, data.Rotation, Vector3.One * 2); : _vfxSpawnManager.SpawnObject(data.Position, data.Rotation, Vector3.One * 2);
}
if (vfxGuid != null) if (vfxGuid != null)
{ {
_poseVfx[data] = vfxGuid.Value; _poseVfx[data] = vfxGuid.Value;
} }
} }
foreach (var data in previousPoses.Except(_nearbyData.Keys)) foreach (var data in previousPoses.Except(allowedPoseSet))
{
if (_poseVfx.Remove(data, out var guid))
{
_vfxSpawnManager.DespawnObject(guid);
}
}
foreach (var data in _poseVfx.Keys.Except(allowedPoseSet).ToList())
{ {
if (_poseVfx.Remove(data, out var guid)) if (_poseVfx.Remove(data, out var guid))
{ {

View File

@@ -1,7 +1,10 @@
using Lumina.Data.Files; using System;
using Lumina.Data.Files;
using MareSynchronos.API.Data; using MareSynchronos.API.Data;
using MareSynchronos.API.Data.Enum; using MareSynchronos.API.Data.Enum;
using MareSynchronos.FileCache; using MareSynchronos.FileCache;
using MareSynchronos.MareConfiguration;
using MareSynchronos.MareConfiguration.Models;
using MareSynchronos.Services.Mediator; using MareSynchronos.Services.Mediator;
using MareSynchronos.UI; using MareSynchronos.UI;
using MareSynchronos.Utils; using MareSynchronos.Utils;
@@ -13,11 +16,22 @@ public sealed class CharacterAnalyzer : DisposableMediatorSubscriberBase
{ {
private readonly FileCacheManager _fileCacheManager; private readonly FileCacheManager _fileCacheManager;
private readonly XivDataAnalyzer _xivDataAnalyzer; private readonly XivDataAnalyzer _xivDataAnalyzer;
private readonly PlayerPerformanceConfigService _playerPerformanceConfigService;
private CancellationTokenSource? _analysisCts; private CancellationTokenSource? _analysisCts;
private CancellationTokenSource _baseAnalysisCts = new(); private CancellationTokenSource _baseAnalysisCts = new();
private string _lastDataHash = string.Empty; private string _lastDataHash = string.Empty;
private CharacterAnalysisSummary _previousSummary = CharacterAnalysisSummary.Empty;
private DateTime _lastAutoAnalysis = DateTime.MinValue;
private string _lastAutoAnalysisHash = string.Empty;
private const int AutoAnalysisFileDeltaThreshold = 25;
private const long AutoAnalysisSizeDeltaThreshold = 50L * 1024 * 1024;
private static readonly TimeSpan AutoAnalysisCooldown = TimeSpan.FromMinutes(2);
private const long NotificationSizeThreshold = 300L * 1024 * 1024;
private const long NotificationTriangleThreshold = 150_000;
private bool _sizeWarningShown;
private bool _triangleWarningShown;
public CharacterAnalyzer(ILogger<CharacterAnalyzer> logger, MareMediator mediator, FileCacheManager fileCacheManager, XivDataAnalyzer modelAnalyzer) public CharacterAnalyzer(ILogger<CharacterAnalyzer> logger, MareMediator mediator, FileCacheManager fileCacheManager, XivDataAnalyzer modelAnalyzer, PlayerPerformanceConfigService playerPerformanceConfigService)
: base(logger, mediator) : base(logger, mediator)
{ {
Mediator.Subscribe<CharacterDataCreatedMessage>(this, (msg) => Mediator.Subscribe<CharacterDataCreatedMessage>(this, (msg) =>
@@ -28,11 +42,14 @@ public sealed class CharacterAnalyzer : DisposableMediatorSubscriberBase
}); });
_fileCacheManager = fileCacheManager; _fileCacheManager = fileCacheManager;
_xivDataAnalyzer = modelAnalyzer; _xivDataAnalyzer = modelAnalyzer;
_playerPerformanceConfigService = playerPerformanceConfigService;
} }
public int CurrentFile { get; internal set; } public int CurrentFile { get; internal set; }
public bool IsAnalysisRunning => _analysisCts != null; public bool IsAnalysisRunning => _analysisCts != null;
public int TotalFiles { get; internal set; } public int TotalFiles { get; internal set; }
public CharacterAnalysisSummary CurrentSummary { get; private set; } = CharacterAnalysisSummary.Empty;
public DateTime? LastCompletedAnalysis { get; private set; }
internal Dictionary<ObjectKind, Dictionary<string, FileDataEntry>> LastAnalysis { get; } = []; internal Dictionary<ObjectKind, Dictionary<string, FileDataEntry>> LastAnalysis { get; } = [];
public void CancelAnalyze() public void CancelAnalyze()
@@ -80,8 +97,15 @@ public sealed class CharacterAnalyzer : DisposableMediatorSubscriberBase
} }
} }
RefreshSummary(false, _lastDataHash);
Mediator.Publish(new CharacterDataAnalyzedMessage()); Mediator.Publish(new CharacterDataAnalyzedMessage());
if (!cancelToken.IsCancellationRequested)
{
LastCompletedAnalysis = DateTime.UtcNow;
}
_analysisCts.CancelDispose(); _analysisCts.CancelDispose();
_analysisCts = null; _analysisCts = null;
@@ -142,9 +166,11 @@ public sealed class CharacterAnalyzer : DisposableMediatorSubscriberBase
LastAnalysis[obj.Key] = data; LastAnalysis[obj.Key] = data;
} }
_lastDataHash = charaData.DataHash.Value;
RefreshSummary(true, _lastDataHash);
Mediator.Publish(new CharacterDataAnalyzedMessage()); Mediator.Publish(new CharacterDataAnalyzedMessage());
_lastDataHash = charaData.DataHash.Value;
} }
private void PrintAnalysis() private void PrintAnalysis()
@@ -193,6 +219,168 @@ public sealed class CharacterAnalyzer : DisposableMediatorSubscriberBase
Logger.LogInformation("IMPORTANT NOTES:\n\r- For uploads 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."); Logger.LogInformation("IMPORTANT NOTES:\n\r- For uploads 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.");
} }
private void RefreshSummary(bool evaluateAutoAnalysis, string dataHash)
{
var summary = CalculateSummary();
CurrentSummary = summary;
if (evaluateAutoAnalysis)
{
EvaluateAutoAnalysis(summary, dataHash);
}
else
{
_previousSummary = summary;
if (!summary.HasUncomputedEntries && string.Equals(_lastAutoAnalysisHash, dataHash, StringComparison.Ordinal))
{
_lastAutoAnalysisHash = string.Empty;
}
}
EvaluateThresholdNotifications(summary);
}
private CharacterAnalysisSummary CalculateSummary()
{
if (LastAnalysis.Count == 0)
{
return CharacterAnalysisSummary.Empty;
}
long original = 0;
long compressed = 0;
long triangles = 0;
int files = 0;
bool hasUncomputed = false;
foreach (var obj in LastAnalysis.Values)
{
foreach (var entry in obj.Values)
{
files++;
original += entry.OriginalSize;
compressed += entry.CompressedSize;
triangles += entry.Triangles;
hasUncomputed |= !entry.IsComputed;
}
}
return new CharacterAnalysisSummary(files, original, compressed, triangles, hasUncomputed);
}
private void EvaluateAutoAnalysis(CharacterAnalysisSummary newSummary, string dataHash)
{
var previous = _previousSummary;
_previousSummary = newSummary;
if (newSummary.TotalFiles == 0)
{
return;
}
if (string.Equals(_lastAutoAnalysisHash, dataHash, StringComparison.Ordinal))
{
return;
}
if (IsAnalysisRunning)
{
return;
}
var now = DateTime.UtcNow;
if (now - _lastAutoAnalysis < AutoAnalysisCooldown)
{
return;
}
bool firstSummary = previous.TotalFiles == 0;
bool filesIncreased = newSummary.TotalFiles - previous.TotalFiles >= AutoAnalysisFileDeltaThreshold;
bool sizeIncreased = newSummary.TotalCompressedSize - previous.TotalCompressedSize >= AutoAnalysisSizeDeltaThreshold;
bool needsCompute = newSummary.HasUncomputedEntries;
if (!firstSummary && !filesIncreased && !sizeIncreased && !needsCompute)
{
return;
}
_lastAutoAnalysis = now;
_lastAutoAnalysisHash = dataHash;
_ = ComputeAnalysis(print: false);
}
private void EvaluateThresholdNotifications(CharacterAnalysisSummary summary)
{
if (summary.IsEmpty || summary.HasUncomputedEntries)
{
ResetThresholdFlagsIfNeeded(summary);
return;
}
if (!_playerPerformanceConfigService.Current.ShowSelfAnalysisWarnings)
{
ResetThresholdFlagsIfNeeded(summary);
return;
}
bool sizeExceeded = summary.TotalCompressedSize >= NotificationSizeThreshold;
bool trianglesExceeded = summary.TotalTriangles >= NotificationTriangleThreshold;
List<string> exceededReasons = new();
if (sizeExceeded && !_sizeWarningShown)
{
exceededReasons.Add($"un poids partagé de {UiSharedService.ByteToString(summary.TotalCompressedSize)} (≥ 300 MiB)");
_sizeWarningShown = true;
}
else if (!sizeExceeded && _sizeWarningShown)
{
_sizeWarningShown = false;
}
if (trianglesExceeded && !_triangleWarningShown)
{
exceededReasons.Add($"un total de {UiSharedService.TrisToString(summary.TotalTriangles)} triangles (≥ 150k)");
_triangleWarningShown = true;
}
else if (!trianglesExceeded && _triangleWarningShown)
{
_triangleWarningShown = false;
}
if (exceededReasons.Count == 0) return;
string combined = string.Join(" et ", exceededReasons);
string message = $"Attention : votre self-analysis indique {combined}. Des joueurs risquent de ne pas vous voir et UmbraSync peut activer un auto-pause. Pensez à réduire textures ou modèles lourds.";
Mediator.Publish(new DualNotificationMessage("Self Analysis", message, NotificationType.Warning));
}
private void ResetThresholdFlagsIfNeeded(CharacterAnalysisSummary summary)
{
if (summary.IsEmpty)
{
_sizeWarningShown = false;
_triangleWarningShown = false;
return;
}
if (summary.TotalCompressedSize < NotificationSizeThreshold)
{
_sizeWarningShown = false;
}
if (summary.TotalTriangles < NotificationTriangleThreshold)
{
_triangleWarningShown = false;
}
}
public readonly record struct CharacterAnalysisSummary(int TotalFiles, long TotalOriginalSize, long TotalCompressedSize, long TotalTriangles, bool HasUncomputedEntries)
{
public static CharacterAnalysisSummary Empty => new();
public bool IsEmpty => TotalFiles == 0 && TotalOriginalSize == 0 && TotalCompressedSize == 0 && TotalTriangles == 0;
}
internal sealed record FileDataEntry(string Hash, string FileType, List<string> GamePaths, List<string> FilePaths, long OriginalSize, long CompressedSize, long Triangles) 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 bool IsComputed => OriginalSize > 0 && CompressedSize > 0;

View File

@@ -1,3 +1,6 @@
using System;
using System.Text;
using System.Threading;
using Dalamud.Game.Text; using Dalamud.Game.Text;
using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Game.Text.SeStringHandling.Payloads; using Dalamud.Game.Text.SeStringHandling.Payloads;
@@ -18,6 +21,7 @@ namespace MareSynchronos.Services;
public class ChatService : DisposableMediatorSubscriberBase public class ChatService : DisposableMediatorSubscriberBase
{ {
public const int DefaultColor = 710; public const int DefaultColor = 710;
private const string ManualPairInvitePrefix = "[UmbraPairInvite|";
public const int CommandMaxNumber = 50; public const int CommandMaxNumber = 50;
private readonly ILogger<ChatService> _logger; private readonly ILogger<ChatService> _logger;
@@ -30,6 +34,13 @@ public class ChatService : DisposableMediatorSubscriberBase
private readonly Lazy<GameChatHooks> _gameChatHooks; private readonly Lazy<GameChatHooks> _gameChatHooks;
private readonly object _typingLock = new();
private CancellationTokenSource? _typingCts;
private bool _isTypingAnnounced;
private DateTime _lastTypingSent = DateTime.MinValue;
private static readonly TimeSpan TypingIdle = TimeSpan.FromSeconds(2);
private static readonly TimeSpan TypingResendInterval = TimeSpan.FromMilliseconds(750);
public ChatService(ILogger<ChatService> logger, DalamudUtilService dalamudUtil, MareMediator mediator, ApiController apiController, public ChatService(ILogger<ChatService> logger, DalamudUtilService dalamudUtil, MareMediator mediator, ApiController apiController,
PairManager pairManager, ILoggerFactory loggerFactory, IGameInteropProvider gameInteropProvider, IChatGui chatGui, PairManager pairManager, ILoggerFactory loggerFactory, IGameInteropProvider gameInteropProvider, IChatGui chatGui,
MareConfigService mareConfig, ServerConfigurationManager serverConfigurationManager) : base(logger, mediator) MareConfigService mareConfig, ServerConfigurationManager serverConfigurationManager) : base(logger, mediator)
@@ -46,13 +57,12 @@ public class ChatService : DisposableMediatorSubscriberBase
Mediator.Subscribe<GroupChatMsgMessage>(this, HandleGroupChat); Mediator.Subscribe<GroupChatMsgMessage>(this, HandleGroupChat);
_gameChatHooks = new(() => new GameChatHooks(loggerFactory.CreateLogger<GameChatHooks>(), gameInteropProvider, SendChatShell)); _gameChatHooks = new(() => new GameChatHooks(loggerFactory.CreateLogger<GameChatHooks>(), gameInteropProvider, SendChatShell));
// Initialize chat hooks in advance
_ = Task.Run(() => _ = Task.Run(() =>
{ {
try try
{ {
_ = _gameChatHooks.Value; _ = _gameChatHooks.Value;
_isTypingAnnounced = false;
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -64,15 +74,86 @@ public class ChatService : DisposableMediatorSubscriberBase
protected override void Dispose(bool disposing) protected override void Dispose(bool disposing)
{ {
base.Dispose(disposing); base.Dispose(disposing);
_typingCts?.Cancel();
_typingCts?.Dispose();
if (_gameChatHooks.IsValueCreated) if (_gameChatHooks.IsValueCreated)
_gameChatHooks.Value!.Dispose(); _gameChatHooks.Value!.Dispose();
} }
public void NotifyTypingKeystroke()
{
lock (_typingLock)
{
var now = DateTime.UtcNow;
if (!_isTypingAnnounced || (now - _lastTypingSent) >= TypingResendInterval)
{
_ = Task.Run(async () =>
{
try { await _apiController.UserSetTypingState(true).ConfigureAwait(false); }
catch (Exception ex) { _logger.LogDebug(ex, "NotifyTypingKeystroke: failed to send typing=true"); }
});
_isTypingAnnounced = true;
_lastTypingSent = now;
}
_typingCts?.Cancel();
_typingCts?.Dispose();
_typingCts = new CancellationTokenSource();
var token = _typingCts.Token;
_ = Task.Run(async () =>
{
try
{
await Task.Delay(TypingIdle, token).ConfigureAwait(false);
await _apiController.UserSetTypingState(false).ConfigureAwait(false);
}
catch (TaskCanceledException)
{
// reset timer
}
catch (Exception ex)
{
_logger.LogDebug(ex, "NotifyTypingKeystroke: failed to send typing=false");
}
finally
{
lock (_typingLock)
{
if (!token.IsCancellationRequested)
{
_isTypingAnnounced = false;
_lastTypingSent = DateTime.MinValue;
}
}
}
});
}
}
public void ClearTypingState()
{
lock (_typingLock)
{
_typingCts?.Cancel();
_typingCts?.Dispose();
_typingCts = null;
if (_isTypingAnnounced)
{
_ = Task.Run(async () =>
{
try { await _apiController.UserSetTypingState(false).ConfigureAwait(false); }
catch (Exception ex) { _logger.LogDebug(ex, "ClearTypingState: failed to send typing=false"); }
});
_isTypingAnnounced = false;
_lastTypingSent = DateTime.MinValue;
}
}
}
private void HandleUserChat(UserChatMsgMessage message) private void HandleUserChat(UserChatMsgMessage message)
{ {
var chatMsg = message.ChatMsg; var chatMsg = message.ChatMsg;
var prefix = new SeStringBuilder(); var prefix = new SeStringBuilder();
prefix.AddText("[BnnuyChat] "); prefix.AddText("[UmbraChat] ");
_chatGui.Print(new XivChatEntry{ _chatGui.Print(new XivChatEntry{
MessageBytes = [..prefix.Build().Encode(), ..message.ChatMsg.PayloadContent], MessageBytes = [..prefix.Build().Encode(), ..message.ChatMsg.PayloadContent],
Name = chatMsg.SenderName, Name = chatMsg.SenderName,
@@ -113,6 +194,10 @@ public class ChatService : DisposableMediatorSubscriberBase
var extraChatTags = _mareConfig.Current.ExtraChatTags; var extraChatTags = _mareConfig.Current.ExtraChatTags;
var logKind = ResolveShellLogKind(shellConfig.LogKind); var logKind = ResolveShellLogKind(shellConfig.LogKind);
var payload = SeString.Parse(message.ChatMsg.PayloadContent);
if (TryHandleManualPairInvite(message, payload))
return;
var msg = new SeStringBuilder(); var msg = new SeStringBuilder();
if (extraChatTags) if (extraChatTags)
{ {
@@ -124,7 +209,6 @@ public class ChatService : DisposableMediatorSubscriberBase
msg.AddText($"[SS{shellNumber}]<"); msg.AddText($"[SS{shellNumber}]<");
if (message.ChatMsg.Sender.UID.Equals(_apiController.UID, StringComparison.Ordinal)) if (message.ChatMsg.Sender.UID.Equals(_apiController.UID, StringComparison.Ordinal))
{ {
// Don't link to your own character
msg.AddText(chatMsg.SenderName); msg.AddText(chatMsg.SenderName);
} }
else else
@@ -132,7 +216,7 @@ public class ChatService : DisposableMediatorSubscriberBase
msg.Add(new PlayerPayload(chatMsg.SenderName, chatMsg.SenderHomeWorldId)); msg.Add(new PlayerPayload(chatMsg.SenderName, chatMsg.SenderHomeWorldId));
} }
msg.AddText("> "); msg.AddText("> ");
msg.Append(SeString.Parse(message.ChatMsg.PayloadContent)); msg.Append(payload);
if (color != 0) if (color != 0)
msg.AddUiForegroundOff(); msg.AddUiForegroundOff();
@@ -143,7 +227,51 @@ public class ChatService : DisposableMediatorSubscriberBase
}); });
} }
// Print an example message to the configured global chat channel private bool TryHandleManualPairInvite(GroupChatMsgMessage message, SeString payload)
{
var textValue = payload.TextValue;
if (string.IsNullOrEmpty(textValue) || !textValue.StartsWith(ManualPairInvitePrefix, StringComparison.Ordinal))
return false;
var content = textValue[ManualPairInvitePrefix.Length..];
if (content.EndsWith("]", StringComparison.Ordinal))
{
content = content[..^1];
}
var parts = content.Split('|');
if (parts.Length < 4)
return true;
var sourceUid = parts[0];
var sourceAlias = DecodeInviteField(parts[1]);
var targetUid = parts[2];
var displayName = DecodeInviteField(parts[3]);
var inviteId = parts.Length > 4 ? parts[4] : Guid.NewGuid().ToString("N");
if (!string.Equals(targetUid, _apiController.UID, StringComparison.Ordinal))
return true;
Mediator.Publish(new ManualPairInviteMessage(sourceUid, sourceAlias, targetUid, string.IsNullOrEmpty(displayName) ? null : displayName, inviteId));
_logger.LogDebug("Received manual pair invite from {source} via syncshell", sourceUid);
return true;
}
private static string DecodeInviteField(string encoded)
{
if (string.IsNullOrEmpty(encoded)) return string.Empty;
try
{
var bytes = Convert.FromBase64String(encoded);
return Encoding.UTF8.GetString(bytes);
}
catch
{
return encoded;
}
}
public void PrintChannelExample(string message, string gid = "") public void PrintChannelExample(string message, string gid = "")
{ {
int chatType = _mareConfig.Current.ChatLogKind; int chatType = _mareConfig.Current.ChatLogKind;
@@ -164,8 +292,6 @@ public class ChatService : DisposableMediatorSubscriberBase
Type = (XivChatType)chatType Type = (XivChatType)chatType
}); });
} }
// Called to update the active chat shell name if its renamed
public void MaybeUpdateShellName(int shellNumber) public void MaybeUpdateShellName(int shellNumber)
{ {
if (_mareConfig.Current.DisableSyncshellChat) if (_mareConfig.Current.DisableSyncshellChat)
@@ -178,7 +304,6 @@ public class ChatService : DisposableMediatorSubscriberBase
{ {
if (_gameChatHooks.IsValueCreated && _gameChatHooks.Value.ChatChannelOverride != null) if (_gameChatHooks.IsValueCreated && _gameChatHooks.Value.ChatChannelOverride != null)
{ {
// Very dumb and won't handle re-numbering -- need to identify the active chat channel more reliably later
if (_gameChatHooks.Value.ChatChannelOverride.ChannelName.StartsWith($"SS [{shellNumber}]", StringComparison.Ordinal)) if (_gameChatHooks.Value.ChatChannelOverride.ChannelName.StartsWith($"SS [{shellNumber}]", StringComparison.Ordinal))
SwitchChatShell(shellNumber); SwitchChatShell(shellNumber);
} }
@@ -197,7 +322,6 @@ public class ChatService : DisposableMediatorSubscriberBase
if (shellConfig.Enabled && shellConfig.ShellNumber == shellNumber) if (shellConfig.Enabled && shellConfig.ShellNumber == shellNumber)
{ {
var name = _serverConfigurationManager.GetNoteForGid(group.Key.GID) ?? group.Key.AliasOrGID; var name = _serverConfigurationManager.GetNoteForGid(group.Key.GID) ?? group.Key.AliasOrGID;
// BUG: This doesn't always update the chat window e.g. when renaming a group
_gameChatHooks.Value.ChatChannelOverride = new() _gameChatHooks.Value.ChatChannelOverride = new()
{ {
ChannelName = $"SS [{shellNumber}]: {name}", ChannelName = $"SS [{shellNumber}]: {name}",
@@ -207,7 +331,7 @@ public class ChatService : DisposableMediatorSubscriberBase
} }
} }
_chatGui.PrintError($"[UmbraSyncSync] Syncshell number #{shellNumber} not found"); _chatGui.PrintError($"[UmbraSync] Syncshell number #{shellNumber} not found");
} }
public void SendChatShell(int shellNumber, byte[] chatBytes) public void SendChatShell(int shellNumber, byte[] chatBytes)
@@ -221,7 +345,6 @@ public class ChatService : DisposableMediatorSubscriberBase
if (shellConfig.Enabled && shellConfig.ShellNumber == shellNumber) if (shellConfig.Enabled && shellConfig.ShellNumber == shellNumber)
{ {
_ = Task.Run(async () => { _ = Task.Run(async () => {
// Should cache the name and home world instead of fetching it every time
var chatMsg = await _dalamudUtil.RunOnFrameworkThread(() => { var chatMsg = await _dalamudUtil.RunOnFrameworkThread(() => {
return new ChatMessage() return new ChatMessage()
{ {
@@ -230,12 +353,13 @@ public class ChatService : DisposableMediatorSubscriberBase
PayloadContent = chatBytes PayloadContent = chatBytes
}; };
}).ConfigureAwait(false); }).ConfigureAwait(false);
ClearTypingState();
await _apiController.GroupChatSendMsg(new(group.Key), chatMsg).ConfigureAwait(false); await _apiController.GroupChatSendMsg(new(group.Key), chatMsg).ConfigureAwait(false);
}).ConfigureAwait(false); }).ConfigureAwait(false);
return; return;
} }
} }
_chatGui.PrintError($"[UmbraSyncSync] Syncshell number #{shellNumber} not found"); _chatGui.PrintError($"[UmbraSync] Syncshell number #{shellNumber} not found");
} }
} }

View File

@@ -0,0 +1,68 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Dalamud.Plugin;
using MareSynchronos.MareConfiguration.Models;
using MareSynchronos.Services.Mediator;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace MareSynchronos.Services;
public sealed class ChatTwoCompatibilityService : MediatorSubscriberBase, IHostedService
{
private const string ChatTwoInternalName = "ChatTwo";
private readonly IDalamudPluginInterface _pluginInterface;
private bool _warningShown;
public ChatTwoCompatibilityService(ILogger<ChatTwoCompatibilityService> logger, IDalamudPluginInterface pluginInterface, MareMediator mediator)
: base(logger, mediator)
{
_pluginInterface = pluginInterface;
Mediator.SubscribeKeyed<PluginChangeMessage>(this, ChatTwoInternalName, OnChatTwoStateChanged);
}
public Task StartAsync(CancellationToken cancellationToken)
{
try
{
var initialState = PluginWatcherService.GetInitialPluginState(_pluginInterface, ChatTwoInternalName);
if (initialState?.IsLoaded == true)
{
ShowWarning();
}
}
catch (Exception ex)
{
Logger.LogDebug(ex, "Failed to inspect ChatTwo initial state");
}
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
Mediator.UnsubscribeAll(this);
return Task.CompletedTask;
}
private void OnChatTwoStateChanged(PluginChangeMessage message)
{
if (message.IsLoaded)
{
ShowWarning();
}
}
private void ShowWarning()
{
if (_warningShown) return;
_warningShown = true;
const string warningTitle = "ChatTwo détecté";
const string warningBody = "Actuellement, le plugin ChatTwo n'est pas compatible avec la bulle d'écriture d'UmbraSync. Désactivez ChatTwo si vous souhaitez conserver l'indicateur de saisie.";
Mediator.Publish(new NotificationMessage(warningTitle, warningBody, NotificationType.Warning, TimeSpan.FromSeconds(10)));
}
}

View File

@@ -0,0 +1,294 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using Dalamud.Plugin.Services;
using Dalamud.Utility;
using Dalamud.Game.Text;
using FFXIVClientStructs.FFXIV.Component.GUI;
using FFXIVClientStructs.FFXIV.Client.UI.Shell;
using Microsoft.Extensions.Logging;
using MareSynchronos.PlayerData.Pairs;
using MareSynchronos.WebAPI;
namespace MareSynchronos.Services;
public sealed class ChatTypingDetectionService : IDisposable
{
private readonly ILogger<ChatTypingDetectionService> _logger;
private readonly IFramework _framework;
private readonly IClientState _clientState;
private readonly IGameGui _gameGui;
private readonly ChatService _chatService;
private readonly TypingIndicatorStateService _typingStateService;
private readonly ApiController _apiController;
private readonly PairManager _pairManager;
private readonly IPartyList _partyList;
private string _lastChatText = string.Empty;
private bool _isTyping;
private bool _notifyingRemote;
private bool _serverSupportWarnLogged;
private bool _remoteNotificationsEnabled;
public ChatTypingDetectionService(ILogger<ChatTypingDetectionService> logger, IFramework framework,
IClientState clientState, IGameGui gameGui, ChatService chatService, PairManager pairManager, IPartyList partyList,
TypingIndicatorStateService typingStateService, ApiController apiController)
{
_logger = logger;
_framework = framework;
_clientState = clientState;
_gameGui = gameGui;
_chatService = chatService;
_pairManager = pairManager;
_partyList = partyList;
_typingStateService = typingStateService;
_apiController = apiController;
_framework.Update += OnFrameworkUpdate;
_logger.LogInformation("ChatTypingDetectionService initialized");
}
public void Dispose()
{
_framework.Update -= OnFrameworkUpdate;
ResetTypingState();
}
private void OnFrameworkUpdate(IFramework framework)
{
try
{
if (!_clientState.IsLoggedIn)
{
ResetTypingState();
return;
}
if (!TryGetChatInput(out var chatText) || string.IsNullOrEmpty(chatText))
{
ResetTypingState();
return;
}
if (IsIgnoredCommand(chatText))
{
ResetTypingState();
return;
}
var notifyRemote = ShouldNotifyRemote();
UpdateRemoteNotificationLogState(notifyRemote);
if (!notifyRemote && _notifyingRemote)
{
_chatService.ClearTypingState();
_notifyingRemote = false;
}
if (!_isTyping || !string.Equals(chatText, _lastChatText, StringComparison.Ordinal))
{
if (notifyRemote)
{
_chatService.NotifyTypingKeystroke();
_notifyingRemote = true;
}
_typingStateService.SetSelfTypingLocal(true);
_isTyping = true;
}
_lastChatText = chatText;
}
catch (Exception ex)
{
_logger.LogTrace(ex, "ChatTypingDetectionService tick failed");
}
}
private void ResetTypingState()
{
if (!_isTyping)
{
_lastChatText = string.Empty;
return;
}
_isTyping = false;
_lastChatText = string.Empty;
_chatService.ClearTypingState();
_notifyingRemote = false;
_typingStateService.SetSelfTypingLocal(false);
}
private static bool IsIgnoredCommand(string chatText)
{
if (string.IsNullOrWhiteSpace(chatText))
return false;
var trimmed = chatText.TrimStart();
if (!trimmed.StartsWith('/'))
return false;
var firstTokenEnd = trimmed.IndexOf(' ');
var command = firstTokenEnd >= 0 ? trimmed[..firstTokenEnd] : trimmed;
command = command.TrimEnd();
var comparison = StringComparison.OrdinalIgnoreCase;
return command.StartsWith("/tell", comparison)
|| command.StartsWith("/t", comparison)
|| command.StartsWith("/xllog", comparison)
|| command.StartsWith("/umbra", comparison)
|| command.StartsWith("/fc", comparison)
|| command.StartsWith("/freecompany", comparison);
}
private unsafe bool ShouldNotifyRemote()
{
try
{
var supportsTypingState = _apiController.SystemInfoDto.SupportsTypingState;
var connected = _apiController.IsConnected;
if (!connected || !supportsTypingState)
{
if (!_serverSupportWarnLogged)
{
_logger.LogDebug("TypingDetection: server support unavailable (connected={connected}, supports={supports})", connected, supportsTypingState);
_serverSupportWarnLogged = true;
}
return false;
}
_serverSupportWarnLogged = false;
var shellModule = RaptureShellModule.Instance();
if (shellModule == null)
{
_logger.LogDebug("TypingDetection: shell module null");
return true;
}
var chatType = (XivChatType)shellModule->ChatType;
switch (chatType)
{
case XivChatType.Say:
case XivChatType.Shout:
case XivChatType.Yell:
return true;
case XivChatType.Party:
case XivChatType.CrossParty:
var eligible = PartyContainsPairedMember();
return eligible;
case XivChatType.Debug:
return true;
default:
_logger.LogTrace("TypingDetection: channel {type} rejected", chatType);
return false;
}
}
catch (Exception ex)
{
_logger.LogTrace(ex, "ChatTypingDetectionService: failed to evaluate chat channel");
}
return true;
}
private bool PartyContainsPairedMember()
{
try
{
var pairedNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var pair in _pairManager.GetOnlineUserPairs())
{
if (!string.IsNullOrEmpty(pair.PlayerName))
pairedNames.Add(pair.PlayerName);
}
if (pairedNames.Count == 0)
{
_logger.LogDebug("TypingDetection: no paired names online");
return false;
}
foreach (var member in _partyList)
{
var name = member?.Name?.TextValue;
if (string.IsNullOrEmpty(name))
continue;
if (pairedNames.Contains(name))
{
return true;
}
}
}
catch (Exception ex)
{
_logger.LogDebug(ex, "ChatTypingDetectionService: failed to check party composition");
}
_logger.LogDebug("TypingDetection: no paired members in party");
return false;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private unsafe bool TryGetChatInput(out string chatText)
{
chatText = string.Empty;
var addon = _gameGui.GetAddonByName("ChatLog", 1);
if (addon.Address == nint.Zero)
return false;
var chatLog = (AtkUnitBase*)addon.Address;
if (chatLog == null || !chatLog->IsVisible)
return false;
var textInputNode = chatLog->UldManager.NodeList[16];
if (textInputNode == null)
return false;
var componentNode = textInputNode->GetAsAtkComponentNode();
if (componentNode == null || componentNode->Component == null)
return false;
var cursorNode = componentNode->Component->UldManager.NodeList[14];
if (cursorNode == null)
return false;
var cursorVisible = cursorNode->IsVisible();
if (!cursorVisible)
{
return false;
}
var chatInputNode = componentNode->Component->UldManager.NodeList[1];
if (chatInputNode == null)
return false;
var textNode = chatInputNode->GetAsAtkTextNode();
if (textNode == null)
return false;
var rawText = textNode->GetText();
if (rawText == null)
return false;
chatText = rawText.AsDalamudSeString().ToString();
return true;
}
private void UpdateRemoteNotificationLogState(bool notifyRemote)
{
if (notifyRemote && !_remoteNotificationsEnabled)
{
_remoteNotificationsEnabled = true;
_logger.LogInformation("TypingDetection: remote notifications enabled");
}
else if (!notifyRemote && _remoteNotificationsEnabled)
{
_remoteNotificationsEnabled = false;
_logger.LogInformation("TypingDetection: remote notifications disabled");
}
}
}

View File

@@ -9,15 +9,15 @@ using MareSynchronos.UI;
using MareSynchronos.WebAPI; using MareSynchronos.WebAPI;
using System.Globalization; using System.Globalization;
using System.Text; using System.Text;
using System.Numerics;
namespace MareSynchronos.Services; namespace MareSynchronos.Services;
public sealed class CommandManagerService : IDisposable public sealed class CommandManagerService : IDisposable
{ {
private const string _commandName = "/sync"; private const string _commandName = "/usync";
private const string _commandName2 = "/loporrit"; private const string _autoDetectCommand = "/autodetect";
private const string _ssCommandPrefix = "/ums";
private const string _ssCommandPrefix = "/ss";
private readonly ApiController _apiController; private readonly ApiController _apiController;
private readonly ICommandManager _commandManager; private readonly ICommandManager _commandManager;
@@ -44,9 +44,10 @@ public sealed class CommandManagerService : IDisposable
{ {
HelpMessage = "Opens the UmbraSync UI" HelpMessage = "Opens the UmbraSync UI"
}); });
_commandManager.AddHandler(_commandName2, new CommandInfo(OnCommand)
_commandManager.AddHandler(_autoDetectCommand, new CommandInfo(OnAutoDetectCommand)
{ {
HelpMessage = "Opens the UmbraSync UI" HelpMessage = "Opens the AutoDetect window"
}); });
// Lazy registration of all possible /ss# commands which tbf is what the game does for linkshells anyway // Lazy registration of all possible /ss# commands which tbf is what the game does for linkshells anyway
@@ -62,12 +63,21 @@ public sealed class CommandManagerService : IDisposable
public void Dispose() public void Dispose()
{ {
_commandManager.RemoveHandler(_commandName); _commandManager.RemoveHandler(_commandName);
_commandManager.RemoveHandler(_commandName2); _commandManager.RemoveHandler(_autoDetectCommand);
for (int i = 1; i <= ChatService.CommandMaxNumber; ++i) for (int i = 1; i <= ChatService.CommandMaxNumber; ++i)
_commandManager.RemoveHandler($"{_ssCommandPrefix}{i}"); _commandManager.RemoveHandler($"{_ssCommandPrefix}{i}");
} }
private void OnAutoDetectCommand(string command, string args)
{
UiSharedService.AccentColor = new Vector4(0x8D / 255f, 0x37 / 255f, 0xC0 / 255f, 1f);
UiSharedService.AccentHoverColor = new Vector4(0x3A / 255f, 0x15 / 255f, 0x50 / 255f, 1f);
UiSharedService.AccentActiveColor = UiSharedService.AccentHoverColor;
_mediator.Publish(new UiToggleMessage(typeof(AutoDetectUi)));
}
private void OnCommand(string command, string args) private void OnCommand(string command, string args)
{ {
var splitArgs = args.ToLowerInvariant().Trim().Split(" ", StringSplitOptions.RemoveEmptyEntries); var splitArgs = args.ToLowerInvariant().Trim().Split(" ", StringSplitOptions.RemoveEmptyEntries);
@@ -86,7 +96,7 @@ public sealed class CommandManagerService : IDisposable
{ {
if (_apiController.ServerState == WebAPI.SignalR.Utils.ServerState.Disconnecting) if (_apiController.ServerState == WebAPI.SignalR.Utils.ServerState.Disconnecting)
{ {
_mediator.Publish(new NotificationMessage("UmbraSync disconnecting", "Cannot use /toggle while UmbraSync is still disconnecting", _mediator.Publish(new NotificationMessage("Umbra disconnecting", "Cannot use /toggle while Umbra is still disconnecting",
NotificationType.Error)); NotificationType.Error));
} }
@@ -147,7 +157,6 @@ public sealed class CommandManagerService : IDisposable
} }
else else
{ {
// FIXME: Chat content seems to already be stripped of any special characters here?
byte[] chatBytes = Encoding.UTF8.GetBytes(args); byte[] chatBytes = Encoding.UTF8.GetBytes(args);
_chatService.SendChatShell(shellNumber, chatBytes); _chatService.SendChatShell(shellNumber, chatBytes);
} }

View File

@@ -20,6 +20,8 @@ using Microsoft.Extensions.Logging;
using System.Numerics; using System.Numerics;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Text; using System.Text;
using System;
using System.Collections.Generic;
using GameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject; using GameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject;
using DalamudGameObject = Dalamud.Game.ClientState.Objects.Types.IGameObject; using DalamudGameObject = Dalamud.Game.ClientState.Objects.Types.IGameObject;
@@ -52,6 +54,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
private readonly ILogger<DalamudUtilService> _logger; private readonly ILogger<DalamudUtilService> _logger;
private readonly IObjectTable _objectTable; private readonly IObjectTable _objectTable;
private readonly PerformanceCollectorService _performanceCollector; private readonly PerformanceCollectorService _performanceCollector;
private readonly Dictionary<string, ConditionFlag> _conditionLookup = new(StringComparer.OrdinalIgnoreCase);
private uint? _classJobId = 0; private uint? _classJobId = 0;
private DateTime _delayedFrameworkUpdateCheck = DateTime.UtcNow; private DateTime _delayedFrameworkUpdateCheck = DateTime.UtcNow;
private string _lastGlobalBlockPlayer = string.Empty; private string _lastGlobalBlockPlayer = string.Empty;
@@ -172,6 +175,20 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
public bool IsInCombatOrPerforming { get; private set; } = false; public bool IsInCombatOrPerforming { get; private set; } = false;
public bool HasModifiedGameFiles => _gameData.HasModifiedGameDataFiles; public bool HasModifiedGameFiles => _gameData.HasModifiedGameDataFiles;
public bool IsConditionActive(string flagName)
{
if (_conditionLookup.TryGetValue(flagName, out var cachedFlag))
return _condition[cachedFlag];
if (Enum.TryParse<ConditionFlag>(flagName, true, out var flag))
{
_conditionLookup[flagName] = flag;
return _condition[flag];
}
return false;
}
public Lazy<Dictionary<ushort, string>> WorldData { get; private set; } public Lazy<Dictionary<ushort, string>> WorldData { get; private set; }
public Lazy<Dictionary<int, Lumina.Excel.Sheets.UIColor>> UiColors { get; private set; } public Lazy<Dictionary<int, Lumina.Excel.Sheets.UIColor>> UiColors { get; private set; }
public Lazy<Dictionary<uint, string>> TerritoryData { get; private set; } public Lazy<Dictionary<uint, string>> TerritoryData { get; private set; }
@@ -462,9 +479,9 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
{ {
_logger.LogInformation("Starting DalamudUtilService"); _logger.LogInformation("Starting DalamudUtilService");
#pragma warning disable S2696 // Instance members should not write to "static" fields #pragma warning disable S2696 // Instance members should not write to "static" fields
UmbraSyncSync.Plugin.Self.RealOnFrameworkUpdate = this.FrameworkOnUpdate; Umbra.Plugin.Self.RealOnFrameworkUpdate = this.FrameworkOnUpdate;
#pragma warning restore S2696 #pragma warning restore S2696
_framework.Update += UmbraSyncSync.Plugin.Self.OnFrameworkUpdate; _framework.Update += Umbra.Plugin.Self.OnFrameworkUpdate;
if (IsLoggedIn) if (IsLoggedIn)
{ {
_classJobId = _clientState.LocalPlayer!.ClassJob.RowId; _classJobId = _clientState.LocalPlayer!.ClassJob.RowId;
@@ -479,7 +496,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
_logger.LogTrace("Stopping {type}", GetType()); _logger.LogTrace("Stopping {type}", GetType());
Mediator.UnsubscribeAll(this); Mediator.UnsubscribeAll(this);
_framework.Update -= UmbraSyncSync.Plugin.Self.OnFrameworkUpdate; _framework.Update -= Umbra.Plugin.Self.OnFrameworkUpdate;
return Task.CompletedTask; return Task.CompletedTask;
} }

View File

@@ -1,12 +1,16 @@
using System;
using Dalamud.Game.Gui.NamePlate; using Dalamud.Game.Gui.NamePlate;
using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Game.Text.SeStringHandling.Payloads; using Dalamud.Game.Text.SeStringHandling.Payloads;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using MareSynchronos.API.Dto.User;
using MareSynchronos.MareConfiguration; using MareSynchronos.MareConfiguration;
using MareSynchronos.PlayerData.Pairs; using MareSynchronos.PlayerData.Pairs;
using MareSynchronos.Services.Mediator; using MareSynchronos.Services.Mediator;
using MareSynchronos.UI; using MareSynchronos.UI;
using MareSynchronos.WebAPI;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Dalamud.Game.Text;
namespace MareSynchronos.Services; namespace MareSynchronos.Services;
@@ -19,12 +23,18 @@ public class GuiHookService : DisposableMediatorSubscriberBase
private readonly IGameConfig _gameConfig; private readonly IGameConfig _gameConfig;
private readonly IPartyList _partyList; private readonly IPartyList _partyList;
private readonly PairManager _pairManager; private readonly PairManager _pairManager;
private readonly IClientState _clientState;
private readonly ApiController _apiController;
private readonly TypingIndicatorStateService _typingStateService;
private static readonly TimeSpan TypingDisplayTime = TimeSpan.FromSeconds(2);
private bool _isModified = false; private bool _isModified = false;
private bool _namePlateRoleColorsEnabled = false; private bool _namePlateRoleColorsEnabled = false;
public GuiHookService(ILogger<GuiHookService> logger, DalamudUtilService dalamudUtil, MareMediator mediator, MareConfigService configService, public GuiHookService(ILogger<GuiHookService> logger, DalamudUtilService dalamudUtil, MareMediator mediator, MareConfigService configService,
INamePlateGui namePlateGui, IGameConfig gameConfig, IPartyList partyList, PairManager pairManager) INamePlateGui namePlateGui, IGameConfig gameConfig, IPartyList partyList, PairManager pairManager, ApiController apiController,
IClientState clientState, TypingIndicatorStateService typingStateService)
: base(logger, mediator) : base(logger, mediator)
{ {
_logger = logger; _logger = logger;
@@ -34,6 +44,9 @@ public class GuiHookService : DisposableMediatorSubscriberBase
_gameConfig = gameConfig; _gameConfig = gameConfig;
_partyList = partyList; _partyList = partyList;
_pairManager = pairManager; _pairManager = pairManager;
_apiController = apiController;
_clientState = clientState;
_typingStateService = typingStateService;
_namePlateGui.OnNamePlateUpdate += OnNamePlateUpdate; _namePlateGui.OnNamePlateUpdate += OnNamePlateUpdate;
_namePlateGui.RequestRedraw(); _namePlateGui.RequestRedraw();
@@ -41,16 +54,24 @@ public class GuiHookService : DisposableMediatorSubscriberBase
Mediator.Subscribe<DelayedFrameworkUpdateMessage>(this, (_) => GameSettingsCheck()); Mediator.Subscribe<DelayedFrameworkUpdateMessage>(this, (_) => GameSettingsCheck());
Mediator.Subscribe<PairHandlerVisibleMessage>(this, (_) => RequestRedraw()); Mediator.Subscribe<PairHandlerVisibleMessage>(this, (_) => RequestRedraw());
Mediator.Subscribe<NameplateRedrawMessage>(this, (_) => RequestRedraw()); Mediator.Subscribe<NameplateRedrawMessage>(this, (_) => RequestRedraw());
Mediator.Subscribe<UserTypingStateMessage>(this, (_) => RequestRedraw());
} }
public void RequestRedraw(bool force = false) public void RequestRedraw(bool force = false)
{ {
if (!_configService.Current.UseNameColors) var useColors = _configService.Current.UseNameColors;
var showTyping = _configService.Current.TypingIndicatorShowOnNameplates;
if (!useColors && !showTyping)
{ {
if (!_isModified && !force) if (!_isModified && !force)
return; return;
_isModified = false; _isModified = false;
} }
else if (!useColors)
{
_isModified = false;
}
_ = Task.Run(async () => { _ = Task.Run(async () => {
await _dalamudUtil.RunOnFrameworkThread(() => _namePlateGui.RequestRedraw()).ConfigureAwait(false); await _dalamudUtil.RunOnFrameworkThread(() => _namePlateGui.RequestRedraw()).ConfigureAwait(false);
@@ -69,7 +90,9 @@ public class GuiHookService : DisposableMediatorSubscriberBase
private void OnNamePlateUpdate(INamePlateUpdateContext context, IReadOnlyList<INamePlateUpdateHandler> handlers) private void OnNamePlateUpdate(INamePlateUpdateContext context, IReadOnlyList<INamePlateUpdateHandler> handlers)
{ {
if (!_configService.Current.UseNameColors) var applyColors = _configService.Current.UseNameColors;
var showTypingIndicator = _configService.Current.TypingIndicatorShowOnNameplates;
if (!applyColors && !showTypingIndicator)
return; return;
var visibleUsers = _pairManager.GetOnlineUserPairs().Where(u => u.IsVisible && u.PlayerCharacterId != uint.MaxValue); var visibleUsers = _pairManager.GetOnlineUserPairs().Where(u => u.IsVisible && u.PlayerCharacterId != uint.MaxValue);
@@ -82,6 +105,11 @@ public class GuiHookService : DisposableMediatorSubscriberBase
for (int i = 0; i < _partyList.Count; ++i) for (int i = 0; i < _partyList.Count; ++i)
partyMembers[i] = _partyList[i]?.GameObject?.Address ?? nint.MaxValue; partyMembers[i] = _partyList[i]?.GameObject?.Address ?? nint.MaxValue;
var now = DateTime.UtcNow;
var activeTypers = _typingStateService.GetActiveTypers(TypingDisplayTime);
var selfTypingActive = showTypingIndicator && _typingStateService.TryGetSelfTyping(TypingDisplayTime, out _, out _);
var localPlayerAddress = selfTypingActive ? _clientState.LocalPlayer?.Address ?? nint.Zero : nint.Zero;
foreach (var handler in handlers) foreach (var handler in handlers)
{ {
if (handler != null && visibleUsersIds.Contains(handler.GameObjectId)) if (handler != null && visibleUsersIds.Contains(handler.GameObjectId))
@@ -89,6 +117,8 @@ public class GuiHookService : DisposableMediatorSubscriberBase
if (_namePlateRoleColorsEnabled && partyMembers.Contains(handler.GameObject?.Address ?? nint.MaxValue)) if (_namePlateRoleColorsEnabled && partyMembers.Contains(handler.GameObject?.Address ?? nint.MaxValue))
continue; continue;
var pair = visibleUsersDict[handler.GameObjectId]; var pair = visibleUsersDict[handler.GameObjectId];
if (applyColors)
{
var colors = !pair.IsApplicationBlocked ? _configService.Current.NameColors : _configService.Current.BlockedNameColors; var colors = !pair.IsApplicationBlocked ? _configService.Current.NameColors : _configService.Current.BlockedNameColors;
handler.NameParts.TextWrap = ( handler.NameParts.TextWrap = (
BuildColorStartSeString(colors), BuildColorStartSeString(colors),
@@ -96,6 +126,9 @@ public class GuiHookService : DisposableMediatorSubscriberBase
); );
_isModified = true; _isModified = true;
} }
}
} }
} }

View File

@@ -3,6 +3,7 @@ using MareSynchronos.API.Data;
using MareSynchronos.API.Dto; using MareSynchronos.API.Dto;
using MareSynchronos.API.Dto.CharaData; using MareSynchronos.API.Dto.CharaData;
using MareSynchronos.API.Dto.Group; using MareSynchronos.API.Dto.Group;
using MareSynchronos.API.Dto.User;
using MareSynchronos.MareConfiguration.Models; using MareSynchronos.MareConfiguration.Models;
using MareSynchronos.PlayerData.Handlers; using MareSynchronos.PlayerData.Handlers;
using MareSynchronos.PlayerData.Pairs; using MareSynchronos.PlayerData.Pairs;
@@ -12,7 +13,7 @@ using System.Numerics;
namespace MareSynchronos.Services.Mediator; namespace MareSynchronos.Services.Mediator;
#pragma warning disable MA0048 // File name must match type name #pragma warning disable MA0048
#pragma warning disable S2094 #pragma warning disable S2094
public record SwitchToIntroUiMessage : MessageBase; public record SwitchToIntroUiMessage : MessageBase;
public record SwitchToMainUiMessage : MessageBase; public record SwitchToMainUiMessage : MessageBase;
@@ -52,6 +53,7 @@ public record HaltScanMessage(string Source) : MessageBase;
public record ResumeScanMessage(string Source) : MessageBase; public record ResumeScanMessage(string Source) : MessageBase;
public record NotificationMessage public record NotificationMessage
(string Title, string Message, NotificationType Type, TimeSpan? TimeShownOnScreen = null) : MessageBase; (string Title, string Message, NotificationType Type, TimeSpan? TimeShownOnScreen = null) : MessageBase;
public record DualNotificationMessage(string Title, string Message, NotificationType Type, TimeSpan? ToastDuration = null) : MessageBase;
public record CreateCacheForObjectMessage(GameObjectHandler ObjectToCreateFor) : MessageBase; public record CreateCacheForObjectMessage(GameObjectHandler ObjectToCreateFor) : MessageBase;
public record ClearCacheForObjectMessage(GameObjectHandler ObjectToCreateFor) : MessageBase; public record ClearCacheForObjectMessage(GameObjectHandler ObjectToCreateFor) : MessageBase;
public record CharacterDataCreatedMessage(CharacterData CharacterData) : SameThreadMessage; public record CharacterDataCreatedMessage(CharacterData CharacterData) : SameThreadMessage;
@@ -90,6 +92,7 @@ public record PenumbraDirectoryChangedMessage(string? ModDirectory) : MessageBas
public record PenumbraRedrawCharacterMessage(ICharacter Character) : SameThreadMessage; public record PenumbraRedrawCharacterMessage(ICharacter Character) : SameThreadMessage;
public record UserChatMsgMessage(SignedChatMessage ChatMsg) : MessageBase; public record UserChatMsgMessage(SignedChatMessage ChatMsg) : MessageBase;
public record GroupChatMsgMessage(GroupDto GroupInfo, SignedChatMessage ChatMsg) : MessageBase; public record GroupChatMsgMessage(GroupDto GroupInfo, SignedChatMessage ChatMsg) : MessageBase;
public record UserTypingStateMessage(TypingStateDto Typing) : MessageBase;
public record RecalculatePerformanceMessage(string? UID) : MessageBase; public record RecalculatePerformanceMessage(string? UID) : MessageBase;
public record NameplateRedrawMessage : MessageBase; public record NameplateRedrawMessage : MessageBase;
public record HoldPairApplicationMessage(string UID, string Source) : KeyedMessage(UID); public record HoldPairApplicationMessage(string UID, string Source) : KeyedMessage(UID);
@@ -108,6 +111,17 @@ public record GPoseLobbyReceiveCharaData(CharaDataDownloadDto CharaDataDownloadD
public record GPoseLobbyReceivePoseData(UserData UserData, PoseData PoseData) : MessageBase; public record GPoseLobbyReceivePoseData(UserData UserData, PoseData PoseData) : MessageBase;
public record GPoseLobbyReceiveWorldData(UserData UserData, WorldData WorldData) : MessageBase; public record GPoseLobbyReceiveWorldData(UserData UserData, WorldData WorldData) : MessageBase;
public record NearbyEntry(string Name, ushort WorldId, float Distance, bool IsMatch, string? Token, string? DisplayName, string? Uid, bool AcceptPairRequests = true);
public record DiscoveryListUpdated(List<NearbyEntry> Entries) : MessageBase;
public record NearbyDetectionToggled(bool Enabled) : MessageBase;
public record AllowPairRequestsToggled(bool Enabled) : MessageBase;
public record ManualPairInviteMessage(string SourceUid, string SourceAlias, string TargetUid, string? DisplayName, string InviteId) : MessageBase;
public record ApplyDefaultPairPermissionsMessage(UserPairDto Pair) : MessageBase;
public record ApplyDefaultGroupPermissionsMessage(GroupPairFullInfoDto GroupPair) : MessageBase;
public record ApplyDefaultsToAllSyncsMessage(string? Context = null, bool? Disabled = null) : MessageBase;
public record PairSyncOverrideChanged(string Uid, bool? DisableSounds, bool? DisableAnimations, bool? DisableVfx) : MessageBase;
public record GroupSyncOverrideChanged(string Gid, bool? DisableSounds, bool? DisableAnimations, bool? DisableVfx) : MessageBase;
public record PluginChangeMessage(string InternalName, Version Version, bool IsLoaded) : KeyedMessage(InternalName); public record PluginChangeMessage(string InternalName, Version Version, bool IsLoaded) : KeyedMessage(InternalName);
#pragma warning restore S2094 #pragma warning restore S2094
#pragma warning restore MA0048 // File name must match type name #pragma warning restore MA0048

View File

@@ -1,226 +0,0 @@
using Dalamud.Plugin;
using MareSynchronos.Interop.Ipc;
using MareSynchronos.MareConfiguration.Models;
using MareSynchronos.Services.Mediator;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System.Text.Json.Serialization;
namespace MareSynchronos.Services;
public sealed class NoSnapService : IHostedService, IMediatorSubscriber
{
private record NoSnapConfig
{
[JsonPropertyName("listOfPlugins")]
public string[]? ListOfPlugins { get; set; }
}
private readonly ILogger<NoSnapService> _logger;
private readonly IDalamudPluginInterface _pluginInterface;
private readonly Dictionary<string, bool> _listOfPlugins = new(StringComparer.Ordinal)
{
["Snapper"] = false,
["Snappy"] = false,
["Meddle.Plugin"] = false,
};
private static readonly HashSet<int> _gposers = new();
private static readonly HashSet<string> _gposersNamed = new(StringComparer.Ordinal);
private readonly IHostApplicationLifetime _hostApplicationLifetime;
private readonly DalamudUtilService _dalamudUtilService;
private readonly IpcManager _ipcManager;
private readonly RemoteConfigurationService _remoteConfig;
public static bool AnyLoaded { get; private set; } = false;
public static string ActivePlugins { get; private set; } = string.Empty;
public MareMediator Mediator { get; init; }
public NoSnapService(ILogger<NoSnapService> logger, IDalamudPluginInterface pluginInterface, MareMediator mediator,
IHostApplicationLifetime hostApplicationLifetime, DalamudUtilService dalamudUtilService, IpcManager ipcManager,
RemoteConfigurationService remoteConfig)
{
_logger = logger;
_pluginInterface = pluginInterface;
Mediator = mediator;
_hostApplicationLifetime = hostApplicationLifetime;
_dalamudUtilService = dalamudUtilService;
_ipcManager = ipcManager;
_remoteConfig = remoteConfig;
Mediator.Subscribe<GposeEndMessage>(this, msg => ClearGposeList());
Mediator.Subscribe<CutsceneEndMessage>(this, msg => ClearGposeList());
}
public void AddGposer(int objectIndex)
{
if (AnyLoaded || _hostApplicationLifetime.ApplicationStopping.IsCancellationRequested)
{
_logger.LogTrace("Immediately reverting object index {id}", objectIndex);
RevertAndRedraw(objectIndex);
return;
}
_logger.LogTrace("Registering gposer object index {id}", objectIndex);
lock (_gposers)
_gposers.Add(objectIndex);
}
public void RemoveGposer(int objectIndex)
{
_logger.LogTrace("Un-registering gposer object index {id}", objectIndex);
lock (_gposers)
_gposers.Remove(objectIndex);
}
public void AddGposerNamed(string name)
{
if (AnyLoaded || _hostApplicationLifetime.ApplicationStopping.IsCancellationRequested)
{
_logger.LogTrace("Immediately reverting {name}", name);
RevertAndRedraw(name);
return;
}
_logger.LogTrace("Registering gposer {name}", name);
lock (_gposers)
_gposersNamed.Add(name);
}
private void ClearGposeList()
{
if (_gposers.Count > 0 || _gposersNamed.Count > 0)
_logger.LogTrace("Clearing gposer list");
lock (_gposers)
_gposers.Clear();
lock (_gposersNamed)
_gposersNamed.Clear();
}
private void RevertAndRedraw(int objIndex, Guid applicationId = default)
{
if (applicationId == default)
applicationId = Guid.NewGuid();
try
{
_ipcManager.Glamourer.RevertNow(_logger, applicationId, objIndex);
_ipcManager.Penumbra.RedrawNow(_logger, applicationId, objIndex);
}
catch { }
}
private void RevertAndRedraw(string name, Guid applicationId = default)
{
if (applicationId == default)
applicationId = Guid.NewGuid();
try
{
_ipcManager.Glamourer.RevertByNameNow(_logger, applicationId, name);
var addr = _dalamudUtilService.GetPlayerCharacterFromCachedTableByName(name);
if (addr != 0)
{
var obj = _dalamudUtilService.CreateGameObject(addr);
if (obj != null)
_ipcManager.Penumbra.RedrawNow(_logger, applicationId, obj.ObjectIndex);
}
}
catch { }
}
private void RevertGposers()
{
List<int>? gposersList = null;
List<string>? gposersList2 = null;
lock (_gposers)
{
if (_gposers.Count > 0)
{
gposersList = _gposers.ToList();
_gposers.Clear();
}
}
lock (_gposersNamed)
{
if (_gposersNamed.Count > 0)
{
gposersList2 = _gposersNamed.ToList();
_gposersNamed.Clear();
}
}
if (gposersList == null && gposersList2 == null)
return;
_logger.LogInformation("Reverting gposers");
_dalamudUtilService.RunOnFrameworkThread(() =>
{
Guid applicationId = Guid.NewGuid();
foreach (var gposer in gposersList ?? [])
RevertAndRedraw(gposer, applicationId);
foreach (var gposerName in gposersList2 ?? [])
RevertAndRedraw(gposerName, applicationId);
}).GetAwaiter().GetResult();
}
public async Task StartAsync(CancellationToken cancellationToken)
{
var config = await _remoteConfig.GetConfigAsync<NoSnapConfig>("noSnap").ConfigureAwait(false) ?? new();
if (config.ListOfPlugins != null)
{
_listOfPlugins.Clear();
foreach (var pluginName in config.ListOfPlugins)
_listOfPlugins.TryAdd(pluginName, value: false);
}
foreach (var pluginName in _listOfPlugins.Keys)
{
_listOfPlugins[pluginName] = PluginWatcherService.GetInitialPluginState(_pluginInterface, pluginName)?.IsLoaded ?? false;
Mediator.SubscribeKeyed<PluginChangeMessage>(this, pluginName, (msg) =>
{
_listOfPlugins[pluginName] = msg.IsLoaded;
_logger.LogDebug("{pluginName} isLoaded = {isLoaded}", pluginName, msg.IsLoaded);
Update();
});
}
Update();
}
public Task StopAsync(CancellationToken cancellationToken)
{
RevertGposers();
return Task.CompletedTask;
}
private void Update()
{
bool anyLoadedNow = _listOfPlugins.Values.Any(p => p);
if (AnyLoaded != anyLoadedNow)
{
AnyLoaded = anyLoadedNow;
Mediator.Publish(new RecalculatePerformanceMessage(null));
if (AnyLoaded)
{
RevertGposers();
var pluginList = string.Join(", ", _listOfPlugins.Where(p => p.Value).Select(p => p.Key));
Mediator.Publish(new NotificationMessage("Incompatible plugin loaded", $"Synced player appearances will not apply until incompatible plugins are disabled: {pluginList}.",
NotificationType.Error));
ActivePlugins = pluginList;
}
else
{
ActivePlugins = string.Empty;
}
}
}
}

View File

@@ -1,4 +1,5 @@
using Dalamud.Game.Text.SeStringHandling; using System;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Interface.ImGuiNotification; using Dalamud.Interface.ImGuiNotification;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using MareSynchronos.MareConfiguration; using MareSynchronos.MareConfiguration;
@@ -31,6 +32,7 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ
public Task StartAsync(CancellationToken cancellationToken) public Task StartAsync(CancellationToken cancellationToken)
{ {
Mediator.Subscribe<NotificationMessage>(this, ShowNotification); Mediator.Subscribe<NotificationMessage>(this, ShowNotification);
Mediator.Subscribe<DualNotificationMessage>(this, ShowDualNotification);
return Task.CompletedTask; return Task.CompletedTask;
} }
@@ -41,19 +43,19 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ
private void PrintErrorChat(string? message) private void PrintErrorChat(string? message)
{ {
SeStringBuilder se = new SeStringBuilder().AddText("[UmbraSyncSync] Error: " + message); SeStringBuilder se = new SeStringBuilder().AddText("[UmbraSync] Error: " + message);
_chatGui.PrintError(se.BuiltString); _chatGui.PrintError(se.BuiltString);
} }
private void PrintInfoChat(string? message) private void PrintInfoChat(string? message)
{ {
SeStringBuilder se = new SeStringBuilder().AddText("[UmbraSyncSync] Info: ").AddItalics(message ?? string.Empty); SeStringBuilder se = new SeStringBuilder().AddText("[UmbraSync] Info: ").AddItalics(message ?? string.Empty);
_chatGui.Print(se.BuiltString); _chatGui.Print(se.BuiltString);
} }
private void PrintWarnChat(string? message) private void PrintWarnChat(string? message)
{ {
SeStringBuilder se = new SeStringBuilder().AddText("[UmbraSyncSync] ").AddUiForeground("Warning: " + (message ?? string.Empty), 31).AddUiForegroundOff(); SeStringBuilder se = new SeStringBuilder().AddText("[UmbraSync] ").AddUiForeground("Warning: " + (message ?? string.Empty), 31).AddUiForegroundOff();
_chatGui.Print(se.BuiltString); _chatGui.Print(se.BuiltString);
} }
@@ -81,41 +83,103 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ
if (!_dalamudUtilService.IsLoggedIn) return; if (!_dalamudUtilService.IsLoggedIn) return;
switch (msg.Type) bool appendInstruction;
bool forceChat = ShouldForceChat(msg, out appendInstruction);
var effectiveMessage = forceChat && appendInstruction ? AppendAutoDetectInstruction(msg.Message) : msg.Message;
var adjustedMsg = forceChat && appendInstruction ? msg with { Message = effectiveMessage } : msg;
switch (adjustedMsg.Type)
{ {
case NotificationType.Info: case NotificationType.Info:
ShowNotificationLocationBased(msg, _configurationService.Current.InfoNotification); ShowNotificationLocationBased(adjustedMsg, _configurationService.Current.InfoNotification, forceChat);
break; break;
case NotificationType.Warning: case NotificationType.Warning:
ShowNotificationLocationBased(msg, _configurationService.Current.WarningNotification); ShowNotificationLocationBased(adjustedMsg, _configurationService.Current.WarningNotification, forceChat);
break; break;
case NotificationType.Error: case NotificationType.Error:
ShowNotificationLocationBased(msg, _configurationService.Current.ErrorNotification); ShowNotificationLocationBased(adjustedMsg, _configurationService.Current.ErrorNotification, forceChat);
break; break;
} }
} }
private void ShowNotificationLocationBased(NotificationMessage msg, NotificationLocation location) private void ShowDualNotification(DualNotificationMessage message)
{ {
switch (location) if (!_dalamudUtilService.IsLoggedIn) return;
var baseMsg = new NotificationMessage(message.Title, message.Message, message.Type, message.ToastDuration);
ShowToast(baseMsg);
ShowChat(baseMsg);
}
private static bool ShouldForceChat(NotificationMessage msg, out bool appendInstruction)
{
appendInstruction = false;
bool IsNearbyRequestText(string? text)
{
if (string.IsNullOrEmpty(text)) return false;
return text.Contains("Nearby request", StringComparison.OrdinalIgnoreCase)
|| text.Contains("Nearby Request", StringComparison.Ordinal);
}
bool IsNearbyAcceptText(string? text)
{
if (string.IsNullOrEmpty(text)) return false;
return text.Contains("Nearby Accept", StringComparison.OrdinalIgnoreCase);
}
bool isAccept = IsNearbyAcceptText(msg.Title) || IsNearbyAcceptText(msg.Message);
if (isAccept)
return false;
bool isRequest = IsNearbyRequestText(msg.Title) || IsNearbyRequestText(msg.Message);
if (isRequest)
{
appendInstruction = !IsRequestSentConfirmation(msg);
return true;
}
return false;
}
private static bool IsRequestSentConfirmation(NotificationMessage msg)
{
if (string.Equals(msg.Title, "Nearby request sent", StringComparison.OrdinalIgnoreCase))
return true;
if (!string.IsNullOrEmpty(msg.Message) && msg.Message.Contains("The other user will receive a request notification.", StringComparison.OrdinalIgnoreCase))
return true;
return false;
}
private static string AppendAutoDetectInstruction(string? message)
{
const string suffix = " | Ouvrez /autodetect pour gérer l'invitation.";
if (string.IsNullOrWhiteSpace(message))
return suffix.TrimStart(' ', '|');
if (message.Contains("/autodetect", StringComparison.OrdinalIgnoreCase))
return message;
return message.TrimEnd() + suffix;
}
private void ShowNotificationLocationBased(NotificationMessage msg, NotificationLocation location, bool forceChat)
{
bool showToast = location is NotificationLocation.Toast or NotificationLocation.Both;
bool showChat = forceChat || location is NotificationLocation.Chat or NotificationLocation.Both;
if (showToast)
{ {
case NotificationLocation.Toast:
ShowToast(msg); ShowToast(msg);
break; }
case NotificationLocation.Chat: if (showChat)
{
ShowChat(msg); ShowChat(msg);
break;
case NotificationLocation.Both:
ShowToast(msg);
ShowChat(msg);
break;
case NotificationLocation.Nowhere:
break;
} }
} }

View File

@@ -0,0 +1,78 @@
using MareSynchronos.MareConfiguration;
using System.Collections.Generic;
using MareSynchronos.PlayerData.Pairs;
using System;
using System.Linq;
using Dalamud.Game.Text;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Game.Text.SeStringHandling.Payloads;
using Dalamud.Plugin.Services;
using MareSynchronos.API.Dto.User;
using MareSynchronos.Services.Mediator;
using Microsoft.Extensions.Logging;
namespace MareSynchronos.Services;
public class PartyListTypingService : DisposableMediatorSubscriberBase
{
private readonly ILogger<PartyListTypingService> _logger;
private readonly IPartyList _partyList;
private readonly MareConfigService _configService;
private readonly PairManager _pairManager;
private readonly TypingIndicatorStateService _typingStateService;
private static readonly TimeSpan TypingDisplayTime = TimeSpan.FromSeconds(2);
private static readonly TimeSpan TypingDisplayDelay = TimeSpan.FromMilliseconds(500);
private static readonly TimeSpan TypingDisplayFade = TypingDisplayTime;
public PartyListTypingService(ILogger<PartyListTypingService> logger,
MareMediator mediator,
IPartyList partyList,
PairManager pairManager,
MareConfigService configService,
TypingIndicatorStateService typingStateService)
: base(logger, mediator)
{
_logger = logger;
_partyList = partyList;
_pairManager = pairManager;
_configService = configService;
_typingStateService = typingStateService;
}
public void Draw()
{
if (!_configService.Current.TypingIndicatorShowOnPartyList) return;
// Build map of visible users by AliasOrUID -> UID (case-insensitive)
var visibleByAlias = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
try
{
var visibleUsers = _pairManager.GetVisibleUsers();
foreach (var u in visibleUsers)
{
var alias = string.IsNullOrEmpty(u.AliasOrUID) ? u.UID : u.AliasOrUID;
if (!visibleByAlias.ContainsKey(alias)) visibleByAlias[alias] = u.UID;
}
}
catch (Exception ex)
{
_logger.LogDebug(ex, "PartyListTypingService: failed to get visible users");
}
var activeTypers = _typingStateService.GetActiveTypers(TypingDisplayTime);
var now = DateTime.UtcNow;
foreach (var member in _partyList)
{
if (string.IsNullOrEmpty(member.Name?.TextValue)) continue;
var displayName = member.Name.TextValue;
if (visibleByAlias.TryGetValue(displayName, out var uid)
&& activeTypers.TryGetValue(uid, out var entry)
&& (now - entry.LastUpdate) <= TypingDisplayFade)
{
_logger.LogDebug("PartyListTypingService: bubble would be shown for {name}", displayName);
}
}
}
}

View File

@@ -1,201 +0,0 @@
using Chaos.NaCl;
using MareSynchronos.MareConfiguration;
using Microsoft.Extensions.Logging;
using System.Net;
using System.Net.Http.Headers;
using System.Reflection;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
namespace MareSynchronos.Services;
public sealed class RemoteConfigurationService
{
private readonly static Dictionary<string, string> ConfigPublicKeys = new(StringComparer.Ordinal)
{
{ "UMBR4KEY", "+MwCXedODmU+yD7vtdI+Ho2iLx+PV3U0H2XRLP/gReA=" }
};
private readonly static string[] ConfigSources = [
"https://umbra-sync.net/config/umbra.json"
];
private readonly ILogger<RemoteConfigurationService> _logger;
private readonly RemoteConfigCacheService _configService;
private readonly Task _initTask;
public RemoteConfigurationService(ILogger<RemoteConfigurationService> logger, RemoteConfigCacheService configService)
{
_logger = logger;
_configService = configService;
_initTask = Task.Run(DownloadConfig);
}
public async Task<JsonObject> GetConfigAsync(string sectionName)
{
await _initTask.ConfigureAwait(false);
if (!_configService.Current.Configuration.TryGetPropertyValue(sectionName, out var section))
section = null;
return (section as JsonObject) ?? new();
}
public async Task<T?> GetConfigAsync<T>(string sectionName)
{
try
{
var json = await GetConfigAsync(sectionName).ConfigureAwait(false);
return JsonSerializer.Deserialize<T>(json);
}
catch (JsonException ex)
{
_logger.LogWarning(ex, "Invalid JSON in remote config: {sectionName}", sectionName);
return default;
}
}
private async Task DownloadConfig()
{
string? jsonResponse = null;
foreach (var remoteUrl in ConfigSources)
{
try
{
_logger.LogDebug("Fetching {url}", remoteUrl);
using var httpClient = new HttpClient(
new HttpClientHandler
{
AllowAutoRedirect = true,
MaxAutomaticRedirections = 5
}
);
httpClient.Timeout = TimeSpan.FromSeconds(6);
var ver = Assembly.GetExecutingAssembly().GetName().Version;
httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("MareSynchronos", ver!.Major + "." + ver!.Minor + "." + ver!.Build));
var request = new HttpRequestMessage(HttpMethod.Get, remoteUrl);
if (remoteUrl.Equals(_configService.Current.Origin, StringComparison.Ordinal))
{
if (!string.IsNullOrEmpty(_configService.Current.ETag))
request.Headers.IfNoneMatch.Add(new EntityTagHeaderValue(_configService.Current.ETag));
if (_configService.Current.LastModified != null)
request.Headers.IfModifiedSince = _configService.Current.LastModified;
}
var response = await httpClient.SendAsync(request).ConfigureAwait(false);
if (response.StatusCode == HttpStatusCode.NotModified)
{
_logger.LogDebug("Using cached remote configuration from {url}", remoteUrl);
return;
}
response.EnsureSuccessStatusCode();
var contentType = response.Content.Headers.ContentType?.MediaType;
if (contentType == null || !contentType.Equals("application/json", StringComparison.Ordinal))
{
_logger.LogWarning("HTTP request for remote config failed: wrong MIME type");
continue;
}
_logger.LogInformation("Downloaded new configuration from {url}", remoteUrl);
_configService.Current.Origin = remoteUrl;
_configService.Current.ETag = response.Headers.ETag?.ToString() ?? string.Empty;
try
{
if (response.Content.Headers.Contains("Last-Modified"))
{
var lastModified = response.Content.Headers.GetValues("Last-Modified").First();
_configService.Current.LastModified = DateTimeOffset.Parse(lastModified, System.Globalization.CultureInfo.InvariantCulture);
}
}
catch
{
_configService.Current.LastModified = null;
}
jsonResponse = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
break;
}
catch (Exception ex)
{
_logger.LogWarning(ex, "HTTP request for remote config failed");
if (remoteUrl.Equals(_configService.Current.Origin, StringComparison.Ordinal))
{
_configService.Current.ETag = string.Empty;
_configService.Current.LastModified = null;
_configService.Save();
}
}
}
if (jsonResponse == null)
{
_logger.LogWarning("Could not download remote config");
return;
}
try
{
var jsonDoc = JsonNode.Parse(jsonResponse) as JsonObject;
if (jsonDoc == null)
{
_logger.LogWarning("Downloaded remote config is not a JSON object");
return;
}
LoadConfig(jsonDoc);
}
catch (JsonException ex)
{
_logger.LogWarning(ex, "Invalid JSON in remote config response");
}
}
private static bool VerifySignature(string message, ulong ts, string signature, string pubKey)
{
byte[] msg = [.. BitConverter.GetBytes(ts), .. Encoding.UTF8.GetBytes(message)];
byte[] sig = Convert.FromBase64String(signature);
byte[] pub = Convert.FromBase64String(pubKey);
return Ed25519.Verify(sig, msg, pub);
}
private void LoadConfig(JsonObject jsonDoc)
{
var ts = jsonDoc["ts"]!.GetValue<ulong>();
if (ts <= _configService.Current.Timestamp)
{
_logger.LogDebug("Remote configuration is not newer than cached config");
return;
}
var signatures = jsonDoc["sig"]!.AsObject();
var configString = jsonDoc["config"]!.GetValue<string>();
bool verified = signatures.Any(sig =>
ConfigPublicKeys.TryGetValue(sig.Key, out var pubKey) &&
VerifySignature(configString, ts, sig.Value!.GetValue<string>(), pubKey));
if (!verified)
{
_logger.LogWarning("Could not verify signature for downloaded remote config");
return;
}
_configService.Current.Configuration = JsonNode.Parse(configString)!.AsObject();
_configService.Current.Timestamp = ts;
_configService.Save();
}
}

View File

@@ -1,12 +0,0 @@
using System.Text.Json.Serialization;
namespace MareSynchronos.Services;
public record RepoChangeConfig
{
[JsonPropertyName("current_repo")]
public string? CurrentRepo { get; set; }
[JsonPropertyName("valid_repos")]
public string[]? ValidRepos { get; set; }
}

View File

@@ -1,401 +0,0 @@
using Dalamud.Plugin;
using Dalamud.Plugin.Services;
using Dalamud.Utility;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System.Reflection;
namespace MareSynchronos.Services;
/* Reflection code based almost entirely on ECommons DalamudReflector
MIT License
Copyright (c) 2023 NightmareXIV
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.
*/
public sealed class RepoChangeService : IHostedService
{
#region Reflection Helpers
private const BindingFlags AllFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static;
private const BindingFlags StaticFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static;
private const BindingFlags InstanceFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
private static object GetFoP(object obj, string name)
{
Type? type = obj.GetType();
while (type != null)
{
var fieldInfo = type.GetField(name, AllFlags);
if (fieldInfo != null)
{
return fieldInfo.GetValue(obj)!;
}
var propertyInfo = type.GetProperty(name, AllFlags);
if (propertyInfo != null)
{
return propertyInfo.GetValue(obj)!;
}
type = type.BaseType;
}
throw new Exception($"Reflection GetFoP failed (not found: {obj.GetType().Name}.{name})");
}
private static T GetFoP<T>(object obj, string name)
{
return (T)GetFoP(obj, name);
}
private static void SetFoP(object obj, string name, object value)
{
var type = obj.GetType();
var field = type.GetField(name, AllFlags);
if (field != null)
{
field.SetValue(obj, value);
}
else
{
var prop = type.GetProperty(name, AllFlags)!;
if (prop == null)
throw new Exception($"Reflection SetFoP failed (not found: {type.Name}.{name})");
prop.SetValue(obj, value);
}
}
private static object? Call(object obj, string name, object[] @params, bool matchExactArgumentTypes = false)
{
MethodInfo? info;
var type = obj.GetType();
if (!matchExactArgumentTypes)
{
info = type.GetMethod(name, AllFlags);
}
else
{
info = type.GetMethod(name, AllFlags, @params.Select(x => x.GetType()).ToArray());
}
if (info == null)
throw new Exception($"Reflection Call failed (not found: {type.Name}.{name})");
return info.Invoke(obj, @params);
}
private static T Call<T>(object obj, string name, object[] @params, bool matchExactArgumentTypes = false)
{
return (T)Call(obj, name, @params, matchExactArgumentTypes)!;
}
#endregion
#region Dalamud Reflection
public object GetService(string serviceFullName)
{
return _pluginInterface.GetType().Assembly.
GetType("Dalamud.Service`1", true)!.MakeGenericType(_pluginInterface.GetType().Assembly.GetType(serviceFullName, true)!).
GetMethod("Get")!.Invoke(null, BindingFlags.Default, null, Array.Empty<object>(), null)!;
}
private object GetPluginManager()
{
return _pluginInterface.GetType().Assembly.
GetType("Dalamud.Service`1", true)!.MakeGenericType(_pluginInterface.GetType().Assembly.GetType("Dalamud.Plugin.Internal.PluginManager", true)!).
GetMethod("Get")!.Invoke(null, BindingFlags.Default, null, Array.Empty<object>(), null)!;
}
private void ReloadPluginMasters()
{
var mgr = GetService("Dalamud.Plugin.Internal.PluginManager");
var pluginReload = mgr.GetType().GetMethod("SetPluginReposFromConfigAsync", BindingFlags.Instance | BindingFlags.Public)!;
pluginReload.Invoke(mgr, [true]);
}
public void SaveDalamudConfig()
{
var conf = GetService("Dalamud.Configuration.Internal.DalamudConfiguration");
var configSave = conf?.GetType().GetMethod("QueueSave", BindingFlags.Instance | BindingFlags.Public);
configSave?.Invoke(conf, null);
}
private IEnumerable<object> GetRepoByURL(string repoURL)
{
var conf = GetService("Dalamud.Configuration.Internal.DalamudConfiguration");
var repolist = (System.Collections.IEnumerable)GetFoP(conf, "ThirdRepoList");
foreach (var r in repolist)
{
if (((string)GetFoP(r, "Url")).Equals(repoURL, StringComparison.OrdinalIgnoreCase))
yield return r;
}
}
private bool HasRepo(string repoURL)
{
var conf = GetService("Dalamud.Configuration.Internal.DalamudConfiguration");
var repolist = (System.Collections.IEnumerable)GetFoP(conf, "ThirdRepoList");
foreach (var r in repolist)
{
if (((string)GetFoP(r, "Url")).Equals(repoURL, StringComparison.OrdinalIgnoreCase))
return true;
}
return false;
}
private void AddRepo(string repoURL, bool enabled)
{
var conf = GetService("Dalamud.Configuration.Internal.DalamudConfiguration");
var repolist = (System.Collections.IEnumerable)GetFoP(conf, "ThirdRepoList");
foreach (var r in repolist)
{
if (((string)GetFoP(r, "Url")).Equals(repoURL, StringComparison.OrdinalIgnoreCase))
return;
}
var instance = Activator.CreateInstance(_pluginInterface.GetType().Assembly.GetType("Dalamud.Configuration.ThirdPartyRepoSettings")!)!;
SetFoP(instance, "Url", repoURL);
SetFoP(instance, "IsEnabled", enabled);
GetFoP<System.Collections.IList>(conf, "ThirdRepoList").Add(instance!);
}
private void RemoveRepo(string repoURL)
{
var toRemove = new List<object>();
var conf = GetService("Dalamud.Configuration.Internal.DalamudConfiguration");
var repolist = (System.Collections.IList)GetFoP(conf, "ThirdRepoList");
foreach (var r in repolist)
{
if (((string)GetFoP(r, "Url")).Equals(repoURL, StringComparison.OrdinalIgnoreCase))
toRemove.Add(r);
}
foreach (var r in toRemove)
repolist.Remove(r);
}
public List<(object LocalPlugin, string InstalledFromUrl)> GetLocalPluginsByName(string internalName)
{
List<(object LocalPlugin, string RepoURL)> result = [];
var pluginManager = GetPluginManager();
var installedPlugins = (System.Collections.IList)pluginManager.GetType().GetProperty("InstalledPlugins")!.GetValue(pluginManager)!;
foreach (var plugin in installedPlugins)
{
if (((string)plugin.GetType().GetProperty("InternalName")!.GetValue(plugin)!).Equals(internalName, StringComparison.Ordinal))
{
var type = plugin.GetType();
if (type.Name.Equals("LocalDevPlugin", StringComparison.Ordinal))
continue;
var manifest = GetFoP(plugin, "manifest");
string installedFromUrl = (string)GetFoP(manifest, "InstalledFromUrl");
result.Add((plugin, installedFromUrl));
}
}
return result;
}
#endregion
private readonly ILogger<RepoChangeService> _logger;
private readonly RemoteConfigurationService _remoteConfig;
private readonly IDalamudPluginInterface _pluginInterface;
private readonly IFramework _framework;
public RepoChangeService(ILogger<RepoChangeService> logger, RemoteConfigurationService remoteConfig, IDalamudPluginInterface pluginInterface, IFramework framework)
{
_logger = logger;
_remoteConfig = remoteConfig;
_pluginInterface = pluginInterface;
_framework = framework;
}
public async Task StartAsync(CancellationToken cancellationToken)
{
_logger.LogDebug("Starting RepoChange Service");
var repoChangeConfig = await _remoteConfig.GetConfigAsync<RepoChangeConfig>("repoChange").ConfigureAwait(false) ?? new();
var currentRepo = repoChangeConfig.CurrentRepo;
var validRepos = (repoChangeConfig.ValidRepos ?? []).ToList();
if (!currentRepo.IsNullOrEmpty() && !validRepos.Contains(currentRepo, StringComparer.Ordinal))
validRepos.Add(currentRepo);
if (validRepos.Count == 0)
{
_logger.LogInformation("No valid repos configured, skipping");
return;
}
await _framework.RunOnTick(() =>
{
try
{
var internalName = Assembly.GetExecutingAssembly().GetName().Name!;
var localPlugins = GetLocalPluginsByName(internalName);
var suffix = string.Empty;
if (localPlugins.Count == 0)
{
_logger.LogInformation("Skipping: No intalled plugin found");
return;
}
var hasValidCustomRepoUrl = false;
foreach (var vr in validRepos)
{
var vrCN = vr.Replace(".json", "_CN.json", StringComparison.Ordinal);
var vrKR = vr.Replace(".json", "_KR.json", StringComparison.Ordinal);
if (HasRepo(vr) || HasRepo(vrCN) || HasRepo(vrKR))
{
hasValidCustomRepoUrl = true;
break;
}
}
List<string> oldRepos = [];
var pluginRepoUrl = localPlugins[0].InstalledFromUrl;
if (pluginRepoUrl.Contains("_CN.json", StringComparison.Ordinal))
suffix = "_CN";
else if (pluginRepoUrl.Contains("_KR.json", StringComparison.Ordinal))
suffix = "_KR";
bool hasOldPluginRepoUrl = false;
foreach (var plugin in localPlugins)
{
foreach (var vr in validRepos)
{
var validRepo = vr.Replace(".json", $"{suffix}.json");
if (!plugin.InstalledFromUrl.Equals(validRepo, StringComparison.Ordinal))
{
oldRepos.Add(plugin.InstalledFromUrl);
hasOldPluginRepoUrl = true;
}
}
}
if (hasValidCustomRepoUrl)
{
if (hasOldPluginRepoUrl)
_logger.LogInformation("Result: Repo URL is up to date, but plugin install source is incorrect");
else
_logger.LogInformation("Result: Repo URL is up to date");
}
else
{
_logger.LogInformation("Result: Repo URL needs to be replaced");
}
if (currentRepo.IsNullOrEmpty())
{
_logger.LogWarning("No current repo URL configured");
return;
}
// Pre-test plugin repo url rewriting to ensure it succeeds before replacing the custom repo URL
if (hasOldPluginRepoUrl)
{
foreach (var plugin in localPlugins)
{
var manifest = GetFoP(plugin.LocalPlugin, "manifest");
if (manifest == null)
throw new Exception("Plugin manifest is null");
var manifestFile = GetFoP(plugin.LocalPlugin, "manifestFile");
if (manifestFile == null)
throw new Exception("Plugin manifestFile is null");
var repo = GetFoP(manifest, "InstalledFromUrl");
if (((string)repo).IsNullOrEmpty())
throw new Exception("Plugin repo url is null or empty");
SetFoP(manifest, "InstalledFromUrl", repo);
}
}
if (!hasValidCustomRepoUrl)
{
try
{
foreach (var oldRepo in oldRepos)
{
_logger.LogInformation("* Removing old repo: {r}", oldRepo);
RemoveRepo(oldRepo);
}
}
finally
{
_logger.LogInformation("* Adding current repo: {r}", currentRepo);
AddRepo(currentRepo, true);
}
}
// This time do it for real, and crash the game if we fail, to avoid saving a broken state
if (hasOldPluginRepoUrl)
{
try
{
_logger.LogInformation("* Updating plugins");
foreach (var plugin in localPlugins)
{
var manifest = GetFoP(plugin.LocalPlugin, "manifest");
if (manifest == null)
throw new Exception("Plugin manifest is null");
var manifestFile = GetFoP(plugin.LocalPlugin, "manifestFile");
if (manifestFile == null)
throw new Exception("Plugin manifestFile is null");
var repo = GetFoP(manifest, "InstalledFromUrl");
if (((string)repo).IsNullOrEmpty())
throw new Exception("Plugin repo url is null or empty");
SetFoP(manifest, "InstalledFromUrl", currentRepo);
Call(manifest, "Save", [manifestFile, "RepoChange"]);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Exception while changing plugin install repo");
foreach (var oldRepo in oldRepos)
{
_logger.LogInformation("* Restoring old repo: {r}", oldRepo);
AddRepo(oldRepo, true);
}
}
}
if (!hasValidCustomRepoUrl || hasOldPluginRepoUrl)
{
_logger.LogInformation("* Saving dalamud config");
SaveDalamudConfig();
_logger.LogInformation("* Reloading plugin masters");
ReloadPluginMasters();
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Exception in RepoChangeService");
}
}, default, 10, cancellationToken).ConfigureAwait(false);
_logger.LogInformation("Started RepoChangeService");
}
public Task StopAsync(CancellationToken cancellationToken)
{
_ = cancellationToken;
_logger.LogDebug("Stopping RepoChange Service");
return Task.CompletedTask;
}
}

View File

@@ -496,17 +496,17 @@ public class ServerConfigurationManager
private void EnsureMainExists() private void EnsureMainExists()
{ {
bool lopExists = false; bool elfExists = false;
for (int i = 0; i < _configService.Current.ServerStorage.Count; ++i) for (int i = 0; i < _configService.Current.ServerStorage.Count; ++i)
{ {
var x = _configService.Current.ServerStorage[i]; var x = _configService.Current.ServerStorage[i];
if (x.ServerUri.Equals(ApiController.UmbraSyncServiceUri, StringComparison.OrdinalIgnoreCase)) if (x.ServerUri.Equals(ApiController.UmbraServiceUri, StringComparison.OrdinalIgnoreCase))
lopExists = true; elfExists = true;
} }
if (!lopExists) if (!elfExists)
{ {
_logger.LogDebug("Re-adding missing server {uri}", ApiController.UmbraSyncServiceUri); _logger.LogDebug("Re-adding missing server {uri}", ApiController.UmbraServiceUri);
_configService.Current.ServerStorage.Insert(0, new ServerStorage() { ServerUri = ApiController.UmbraSyncServiceUri, ServerName = ApiController.UmbraSyncServer }); _configService.Current.ServerStorage.Insert(0, new ServerStorage() { ServerUri = ApiController.UmbraServiceUri, ServerName = ApiController.UmbraServer });
if (_configService.Current.CurrentServer >= 0) if (_configService.Current.CurrentServer >= 0)
_configService.Current.CurrentServer++; _configService.Current.CurrentServer++;
} }

View File

@@ -0,0 +1,358 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using MareSynchronos.API.Data;
using MareSynchronos.API.Data.Enum;
using MareSynchronos.API.Data.Extensions;
using MareSynchronos.API.Dto.Group;
using MareSynchronos.API.Dto.User;
using MareSynchronos.MareConfiguration;
using MareSynchronos.MareConfiguration.Configurations;
using MareSynchronos.MareConfiguration.Models;
using MareSynchronos.PlayerData.Pairs;
using MareSynchronos.Services.Mediator;
using MareSynchronos.WebAPI;
using Microsoft.Extensions.Logging;
using NotificationType = MareSynchronos.MareConfiguration.Models.NotificationType;
namespace MareSynchronos.Services;
public sealed class SyncDefaultsService : DisposableMediatorSubscriberBase
{
private readonly ApiController _apiController;
private readonly MareConfigService _configService;
private readonly PairManager _pairManager;
public SyncDefaultsService(ILogger<SyncDefaultsService> logger, MareMediator mediator,
MareConfigService configService, ApiController apiController, PairManager pairManager) : base(logger, mediator)
{
_configService = configService;
_apiController = apiController;
_pairManager = pairManager;
Mediator.Subscribe<ApplyDefaultPairPermissionsMessage>(this, OnApplyPairDefaults);
Mediator.Subscribe<ApplyDefaultGroupPermissionsMessage>(this, OnApplyGroupDefaults);
Mediator.Subscribe<ApplyDefaultsToAllSyncsMessage>(this, msg => ApplyDefaultsToAll(msg));
Mediator.Subscribe<PairSyncOverrideChanged>(this, OnPairOverrideChanged);
Mediator.Subscribe<GroupSyncOverrideChanged>(this, OnGroupOverrideChanged);
}
private void OnApplyPairDefaults(ApplyDefaultPairPermissionsMessage message)
{
var config = _configService.Current;
var permissions = message.Pair.OwnPermissions;
var overrides = TryGetPairOverride(message.Pair.User.UID);
if (!ApplyDefaults(ref permissions, config, overrides))
return;
_ = _apiController.UserSetPairPermissions(new UserPermissionsDto(message.Pair.User, permissions));
}
private void OnApplyGroupDefaults(ApplyDefaultGroupPermissionsMessage message)
{
if (!string.Equals(message.GroupPair.User.UID, _apiController.UID, StringComparison.Ordinal))
return;
var config = _configService.Current;
var permissions = message.GroupPair.GroupUserPermissions;
var overrides = TryGetGroupOverride(message.GroupPair.Group.GID);
if (!ApplyDefaults(ref permissions, config, overrides))
return;
_ = _apiController.GroupChangeIndividualPermissionState(new GroupPairUserPermissionDto(message.GroupPair.Group, message.GroupPair.User, permissions));
}
private async Task ApplyDefaultsToAllAsync(ApplyDefaultsToAllSyncsMessage message)
{
try
{
var config = _configService.Current;
var tasks = new List<Task>();
int updatedPairs = 0;
int updatedGroups = 0;
foreach (var pair in _pairManager.DirectPairs.Where(p => p.UserPair != null).ToList())
{
var permissions = pair.UserPair!.OwnPermissions;
var overrides = TryGetPairOverride(pair.UserData.UID);
if (!ApplyDefaults(ref permissions, config, overrides))
continue;
updatedPairs++;
tasks.Add(_apiController.UserSetPairPermissions(new UserPermissionsDto(pair.UserData, permissions)));
}
var selfUser = new UserData(_apiController.UID);
foreach (var groupInfo in _pairManager.Groups.Values.ToList())
{
var permissions = groupInfo.GroupUserPermissions;
var overrides = TryGetGroupOverride(groupInfo.Group.GID);
if (!ApplyDefaults(ref permissions, config, overrides))
continue;
updatedGroups++;
tasks.Add(_apiController.GroupChangeIndividualPermissionState(new GroupPairUserPermissionDto(groupInfo.Group, selfUser, permissions)));
}
if (tasks.Count > 0)
{
try
{
await Task.WhenAll(tasks).ConfigureAwait(false);
}
catch (Exception ex)
{
Logger.LogWarning(ex, "Failed applying default sync settings to all pairs/groups");
}
}
var summary = BuildSummaryMessage(updatedPairs, updatedGroups);
var primary = BuildPrimaryMessage(message);
var combined = string.IsNullOrEmpty(primary) ? summary : string.Concat(primary, ' ', summary);
Mediator.Publish(new DualNotificationMessage("Préférences appliquées", combined, NotificationType.Info));
}
catch (Exception ex)
{
Logger.LogError(ex, "Unexpected error while applying default sync settings to all pairs/groups");
Mediator.Publish(new DualNotificationMessage("Préférences appliquées", "Une erreur est survenue lors de l'application des paramètres par défaut.", NotificationType.Error));
}
}
private void ApplyDefaultsToAll(ApplyDefaultsToAllSyncsMessage message) => _ = ApplyDefaultsToAllAsync(message);
private static string? BuildPrimaryMessage(ApplyDefaultsToAllSyncsMessage message)
{
if (string.IsNullOrEmpty(message.Context) || message.Disabled == null)
return null;
var state = message.Disabled.Value ? "désactivée" : "activée";
return $"Synchronisation {message.Context} par défaut {state}.";
}
private static string BuildSummaryMessage(int pairs, int groups)
{
if (pairs == 0 && groups == 0)
return "Aucun pair ou syncshell n'avait besoin d'être modifié.";
if (pairs > 0 && groups > 0)
return $"Mise à jour de {pairs} pair(s) et {groups} syncshell(s).";
if (pairs > 0)
return $"Mise à jour de {pairs} pair(s).";
return $"Mise à jour de {groups} syncshell(s).";
}
private void OnPairOverrideChanged(PairSyncOverrideChanged message)
{
var overrides = _configService.Current.PairSyncOverrides ??= new(StringComparer.Ordinal);
var entry = overrides.TryGetValue(message.Uid, out var existing) ? existing : new SyncOverrideEntry();
bool changed = false;
if (message.DisableSounds.HasValue)
{
var val = message.DisableSounds.Value;
var defaultVal = _configService.Current.DefaultDisableSounds;
var newValue = val == defaultVal ? (bool?)null : val;
if (entry.DisableSounds != newValue)
{
entry.DisableSounds = newValue;
changed = true;
}
}
if (message.DisableAnimations.HasValue)
{
var val = message.DisableAnimations.Value;
var defaultVal = _configService.Current.DefaultDisableAnimations;
var newValue = val == defaultVal ? (bool?)null : val;
if (entry.DisableAnimations != newValue)
{
entry.DisableAnimations = newValue;
changed = true;
}
}
if (message.DisableVfx.HasValue)
{
var val = message.DisableVfx.Value;
var defaultVal = _configService.Current.DefaultDisableVfx;
var newValue = val == defaultVal ? (bool?)null : val;
if (entry.DisableVfx != newValue)
{
entry.DisableVfx = newValue;
changed = true;
}
}
if (!changed) return;
if (entry.IsEmpty)
overrides.Remove(message.Uid);
else
overrides[message.Uid] = entry;
_configService.Save();
}
private void OnGroupOverrideChanged(GroupSyncOverrideChanged message)
{
var overrides = _configService.Current.GroupSyncOverrides ??= new(StringComparer.Ordinal);
var entry = overrides.TryGetValue(message.Gid, out var existing) ? existing : new SyncOverrideEntry();
bool changed = false;
if (message.DisableSounds.HasValue)
{
var val = message.DisableSounds.Value;
var defaultVal = _configService.Current.DefaultDisableSounds;
var newValue = val == defaultVal ? (bool?)null : val;
if (entry.DisableSounds != newValue)
{
entry.DisableSounds = newValue;
changed = true;
}
}
if (message.DisableAnimations.HasValue)
{
var val = message.DisableAnimations.Value;
var defaultVal = _configService.Current.DefaultDisableAnimations;
var newValue = val == defaultVal ? (bool?)null : val;
if (entry.DisableAnimations != newValue)
{
entry.DisableAnimations = newValue;
changed = true;
}
}
if (message.DisableVfx.HasValue)
{
var val = message.DisableVfx.Value;
var defaultVal = _configService.Current.DefaultDisableVfx;
var newValue = val == defaultVal ? (bool?)null : val;
if (entry.DisableVfx != newValue)
{
entry.DisableVfx = newValue;
changed = true;
}
}
if (!changed) return;
if (entry.IsEmpty)
overrides.Remove(message.Gid);
else
overrides[message.Gid] = entry;
_configService.Save();
}
private SyncOverrideEntry? TryGetPairOverride(string uid)
{
var overrides = _configService.Current.PairSyncOverrides;
return overrides != null && overrides.TryGetValue(uid, out var entry) ? entry : null;
}
private SyncOverrideEntry? TryGetGroupOverride(string gid)
{
var overrides = _configService.Current.GroupSyncOverrides;
return overrides != null && overrides.TryGetValue(gid, out var entry) ? entry : null;
}
private static bool ApplyDefaults(ref UserPermissions permissions, MareConfig config, SyncOverrideEntry? overrides)
{
bool changed = false;
if (overrides?.DisableSounds is bool overrideSounds)
{
if (permissions.IsDisableSounds() != overrideSounds)
{
permissions.SetDisableSounds(overrideSounds);
changed = true;
}
}
else if (permissions.IsDisableSounds() != config.DefaultDisableSounds)
{
permissions.SetDisableSounds(config.DefaultDisableSounds);
changed = true;
}
if (overrides?.DisableAnimations is bool overrideAnims)
{
if (permissions.IsDisableAnimations() != overrideAnims)
{
permissions.SetDisableAnimations(overrideAnims);
changed = true;
}
}
else if (permissions.IsDisableAnimations() != config.DefaultDisableAnimations)
{
permissions.SetDisableAnimations(config.DefaultDisableAnimations);
changed = true;
}
if (overrides?.DisableVfx is bool overrideVfx)
{
if (permissions.IsDisableVFX() != overrideVfx)
{
permissions.SetDisableVFX(overrideVfx);
changed = true;
}
}
else if (permissions.IsDisableVFX() != config.DefaultDisableVfx)
{
permissions.SetDisableVFX(config.DefaultDisableVfx);
changed = true;
}
return changed;
}
private static bool ApplyDefaults(ref GroupUserPermissions permissions, MareConfig config, SyncOverrideEntry? overrides)
{
bool changed = false;
if (overrides?.DisableSounds is bool overrideSounds)
{
if (permissions.IsDisableSounds() != overrideSounds)
{
permissions.SetDisableSounds(overrideSounds);
changed = true;
}
}
else if (permissions.IsDisableSounds() != config.DefaultDisableSounds)
{
permissions.SetDisableSounds(config.DefaultDisableSounds);
changed = true;
}
if (overrides?.DisableAnimations is bool overrideAnims)
{
if (permissions.IsDisableAnimations() != overrideAnims)
{
permissions.SetDisableAnimations(overrideAnims);
changed = true;
}
}
else if (permissions.IsDisableAnimations() != config.DefaultDisableAnimations)
{
permissions.SetDisableAnimations(config.DefaultDisableAnimations);
changed = true;
}
if (overrides?.DisableVfx is bool overrideVfx)
{
if (permissions.IsDisableVFX() != overrideVfx)
{
permissions.SetDisableVFX(overrideVfx);
changed = true;
}
}
else if (permissions.IsDisableVFX() != config.DefaultDisableVfx)
{
permissions.SetDisableVFX(config.DefaultDisableVfx);
changed = true;
}
return changed;
}
}

View File

@@ -0,0 +1,225 @@
using System.Globalization;
using System.Threading;
using MareSynchronos.API.Dto.Group;
using MareSynchronos.MareConfiguration.Models;
using MareSynchronos.PlayerData.Pairs;
using MareSynchronos.Services.Mediator;
using MareSynchronos.WebAPI;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace MareSynchronos.Services;
public sealed class TemporarySyncshellNotificationService : MediatorSubscriberBase, IHostedService
{
private static readonly int[] NotificationThresholdMinutes = [30, 15, 5, 1];
private readonly ApiController _apiController;
private readonly PairManager _pairManager;
private readonly Lock _stateLock = new();
private readonly Dictionary<string, TrackedGroup> _trackedGroups = new(StringComparer.Ordinal);
private CancellationTokenSource? _loopCts;
private Task? _loopTask;
public TemporarySyncshellNotificationService(ILogger<TemporarySyncshellNotificationService> logger, MareMediator mediator, PairManager pairManager, ApiController apiController)
: base(logger, mediator)
{
_pairManager = pairManager;
_apiController = apiController;
}
public Task StartAsync(CancellationToken cancellationToken)
{
_loopCts = new CancellationTokenSource();
Mediator.Subscribe<ConnectedMessage>(this, _ => ResetTrackedGroups());
Mediator.Subscribe<DisconnectedMessage>(this, _ => ResetTrackedGroups());
_loopTask = Task.Run(() => MonitorLoopAsync(_loopCts.Token), _loopCts.Token);
return Task.CompletedTask;
}
public async Task StopAsync(CancellationToken cancellationToken)
{
Mediator.UnsubscribeAll(this);
if (_loopCts == null)
{
return;
}
try
{
_loopCts.Cancel();
if (_loopTask != null)
{
await _loopTask.ConfigureAwait(false);
}
}
catch (OperationCanceledException)
{
}
finally
{
_loopTask = null;
_loopCts.Dispose();
_loopCts = null;
}
}
private async Task MonitorLoopAsync(CancellationToken ct)
{
var delay = TimeSpan.FromSeconds(30);
while (!ct.IsCancellationRequested)
{
try
{
CheckGroups();
}
catch (Exception ex)
{
Logger.LogDebug(ex, "Failed to check temporary syncshell expirations");
}
try
{
await Task.Delay(delay, ct).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
break;
}
}
}
private void CheckGroups()
{
var nowUtc = DateTime.UtcNow;
var groupsSnapshot = _pairManager.Groups.Values.ToList();
var notifications = new List<NotificationPayload>();
var expiredGroups = new List<GroupFullInfoDto>();
var seenTemporaryGids = new HashSet<string>(StringComparer.Ordinal);
using (var guard = _stateLock.EnterScope())
{
foreach (var group in groupsSnapshot)
{
if (!group.IsTemporary || group.ExpiresAt == null)
{
continue;
}
if (string.IsNullOrEmpty(_apiController.UID) || !string.Equals(group.OwnerUID, _apiController.UID, StringComparison.Ordinal))
{
continue;
}
var gid = group.Group.GID;
seenTemporaryGids.Add(gid);
var expiresAtUtc = NormalizeToUtc(group.ExpiresAt.Value);
var remaining = expiresAtUtc - nowUtc;
if (!_trackedGroups.TryGetValue(gid, out var state))
{
state = new TrackedGroup(expiresAtUtc);
_trackedGroups[gid] = state;
}
else if (state.ExpiresAtUtc != expiresAtUtc)
{
state.UpdateExpiresAt(expiresAtUtc);
}
if (remaining <= TimeSpan.Zero)
{
_trackedGroups.Remove(gid);
expiredGroups.Add(group);
continue;
}
if (!state.LastRemaining.HasValue)
{
state.UpdateRemaining(remaining);
continue;
}
var previousRemaining = state.LastRemaining.Value;
foreach (var thresholdMinutes in NotificationThresholdMinutes)
{
var threshold = TimeSpan.FromMinutes(thresholdMinutes);
if (previousRemaining > threshold && remaining <= threshold)
{
notifications.Add(new NotificationPayload(group, thresholdMinutes, expiresAtUtc));
}
}
state.UpdateRemaining(remaining);
}
var toRemove = _trackedGroups.Keys.Where(k => !seenTemporaryGids.Contains(k)).ToList();
foreach (var gid in toRemove)
{
_trackedGroups.Remove(gid);
}
}
foreach (var expiredGroup in expiredGroups)
{
Logger.LogInformation("Temporary syncshell {gid} expired locally; removing", expiredGroup.Group.GID);
_pairManager.RemoveGroup(expiredGroup.Group);
}
foreach (var notification in notifications)
{
PublishNotification(notification.Group, notification.ThresholdMinutes, notification.ExpiresAtUtc);
}
}
private void PublishNotification(GroupFullInfoDto group, int thresholdMinutes, DateTime expiresAtUtc)
{
string displayName = string.IsNullOrWhiteSpace(group.GroupAlias) ? group.Group.GID : group.GroupAlias!;
string threshold = thresholdMinutes == 1 ? "1 minute" : $"{thresholdMinutes} minutes";
string expiresLocal = expiresAtUtc.ToLocalTime().ToString("t", CultureInfo.CurrentCulture);
string message = $"La Syncshell temporaire \"{displayName}\" sera supprimee dans {threshold} (a {expiresLocal}).";
Mediator.Publish(new NotificationMessage("Syncshell temporaire", message, NotificationType.Warning, TimeSpan.FromSeconds(6)));
}
private static DateTime NormalizeToUtc(DateTime expiresAt)
{
return expiresAt.Kind switch
{
DateTimeKind.Utc => expiresAt,
DateTimeKind.Local => expiresAt.ToUniversalTime(),
_ => DateTime.SpecifyKind(expiresAt, DateTimeKind.Utc)
};
}
private void ResetTrackedGroups()
{
using (var guard = _stateLock.EnterScope())
{
_trackedGroups.Clear();
}
}
private sealed class TrackedGroup
{
public TrackedGroup(DateTime expiresAtUtc)
{
ExpiresAtUtc = expiresAtUtc;
}
public DateTime ExpiresAtUtc { get; private set; }
public TimeSpan? LastRemaining { get; private set; }
public void UpdateExpiresAt(DateTime expiresAtUtc)
{
ExpiresAtUtc = expiresAtUtc;
LastRemaining = null;
}
public void UpdateRemaining(TimeSpan remaining)
{
LastRemaining = remaining;
}
}
private sealed record NotificationPayload(GroupFullInfoDto Group, int ThresholdMinutes, DateTime ExpiresAtUtc);
}

View File

@@ -0,0 +1,117 @@
using System;
using System.Collections.Concurrent;
using System.Linq;
using MareSynchronos.Services.Mediator;
using MareSynchronos.WebAPI;
using Microsoft.Extensions.Logging;
using MareSynchronos.API.Data;
namespace MareSynchronos.Services;
public sealed class TypingIndicatorStateService : IMediatorSubscriber, IDisposable
{
private sealed record TypingEntry(UserData User, DateTime FirstSeen, DateTime LastUpdate);
private readonly ConcurrentDictionary<string, TypingEntry> _typingUsers = new(StringComparer.Ordinal);
private readonly ApiController _apiController;
private readonly ILogger<TypingIndicatorStateService> _logger;
private DateTime _selfTypingLast = DateTime.MinValue;
private DateTime _selfTypingStart = DateTime.MinValue;
private bool _selfTypingActive;
public TypingIndicatorStateService(ILogger<TypingIndicatorStateService> logger, MareMediator mediator, ApiController apiController)
{
_logger = logger;
_apiController = apiController;
Mediator = mediator;
mediator.Subscribe<UserTypingStateMessage>(this, OnTypingState);
}
public void Dispose()
{
Mediator.UnsubscribeAll(this);
}
public MareMediator Mediator { get; }
public void SetSelfTypingLocal(bool isTyping)
{
if (isTyping)
{
if (!_selfTypingActive)
_selfTypingStart = DateTime.UtcNow;
_selfTypingLast = DateTime.UtcNow;
}
else
{
_selfTypingStart = DateTime.MinValue;
}
_selfTypingActive = isTyping;
}
private void OnTypingState(UserTypingStateMessage msg)
{
var uid = msg.Typing.User.UID;
var now = DateTime.UtcNow;
if (string.Equals(uid, _apiController.UID, StringComparison.Ordinal))
{
_selfTypingActive = msg.Typing.IsTyping;
if (_selfTypingActive)
{
if (_selfTypingStart == DateTime.MinValue)
_selfTypingStart = now;
_selfTypingLast = now;
}
else
{
_selfTypingStart = DateTime.MinValue;
}
_logger.LogInformation("Typing state self -> {state}", _selfTypingActive);
}
else if (msg.Typing.IsTyping)
{
_typingUsers.AddOrUpdate(uid,
_ => new TypingEntry(msg.Typing.User, now, now),
(_, existing) => new TypingEntry(msg.Typing.User, existing.FirstSeen, now));
}
else
{
_typingUsers.TryRemove(uid, out _);
}
}
public bool TryGetSelfTyping(TimeSpan maxAge, out DateTime startTyping, out DateTime lastTyping)
{
startTyping = _selfTypingStart;
lastTyping = _selfTypingLast;
if (!_selfTypingActive)
return false;
var now = DateTime.UtcNow;
if ((now - _selfTypingLast) >= maxAge)
{
_selfTypingActive = false;
_selfTypingStart = DateTime.MinValue;
return false;
}
return true;
}
public IReadOnlyDictionary<string, (UserData User, DateTime FirstSeen, DateTime LastUpdate)> GetActiveTypers(TimeSpan maxAge)
{
var now = DateTime.UtcNow;
foreach (var kvp in _typingUsers.ToArray())
{
if ((now - kvp.Value.LastUpdate) >= maxAge)
{
_typingUsers.TryRemove(kvp.Key, out _);
}
}
return _typingUsers.ToDictionary(k => k.Key, v => (v.Value.User, v.Value.FirstSeen, v.Value.LastUpdate), StringComparer.Ordinal);
}
}

View File

@@ -0,0 +1,351 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface.Colors;
using Dalamud.Game.ClientState.Objects.SubKinds;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Plugin.Services;
using MareSynchronos.MareConfiguration;
using MareSynchronos.PlayerData.Pairs;
using MareSynchronos.Services.Mediator;
using Microsoft.Extensions.Logging;
using System.Numerics;
using System.Globalization;
using System.Text;
using MareSynchronos.Services;
using MareSynchronos.Services.AutoDetect;
using NotificationType = MareSynchronos.MareConfiguration.Models.NotificationType;
namespace MareSynchronos.UI;
public class AutoDetectUi : WindowMediatorSubscriberBase
{
private readonly MareConfigService _configService;
private readonly DalamudUtilService _dalamud;
private readonly IObjectTable _objectTable;
private readonly AutoDetectRequestService _requestService;
private readonly NearbyPendingService _pendingService;
private readonly PairManager _pairManager;
private List<Services.Mediator.NearbyEntry> _entries = new();
private readonly HashSet<string> _acceptInFlight = new(StringComparer.Ordinal);
public AutoDetectUi(ILogger<AutoDetectUi> logger, MareMediator mediator,
MareConfigService configService, DalamudUtilService dalamudUtilService, IObjectTable objectTable,
AutoDetectRequestService requestService, NearbyPendingService pendingService, PairManager pairManager,
PerformanceCollectorService performanceCollectorService)
: base(logger, mediator, "AutoDetect", performanceCollectorService)
{
_configService = configService;
_dalamud = dalamudUtilService;
_objectTable = objectTable;
_requestService = requestService;
_pendingService = pendingService;
_pairManager = pairManager;
Flags |= ImGuiWindowFlags.NoScrollbar;
SizeConstraints = new WindowSizeConstraints()
{
MinimumSize = new Vector2(350, 220),
MaximumSize = new Vector2(600, 600),
};
}
public override bool DrawConditions()
{
return true;
}
protected override void DrawInternal()
{
using var idScope = ImRaii.PushId("autodetect-ui");
var incomingInvites = _pendingService.Pending.ToList();
var outgoingInvites = _requestService.GetPendingRequestsSnapshot();
Vector4 accent = UiSharedService.AccentColor;
if (accent.W <= 0f) accent = ImGuiColors.ParsedPurple;
Vector4 inactiveTab = new(accent.X * 0.45f, accent.Y * 0.45f, accent.Z * 0.45f, Math.Clamp(accent.W + 0.15f, 0f, 1f));
Vector4 hoverTab = UiSharedService.AccentHoverColor;
using var tabs = ImRaii.TabBar("AutoDetectTabs");
if (!tabs.Success) return;
var incomingCount = incomingInvites.Count;
DrawStyledTab($"Invitations ({incomingCount})", accent, inactiveTab, hoverTab, () =>
{
DrawInvitationsTab(incomingInvites, outgoingInvites);
});
DrawStyledTab("Proximité", accent, inactiveTab, hoverTab, DrawNearbyTab);
using (ImRaii.Disabled(true))
{
DrawStyledTab("Syncshell", accent, inactiveTab, hoverTab, () =>
{
UiSharedService.ColorTextWrapped("Disponible prochainement.", ImGuiColors.DalamudGrey3);
}, true);
}
}
private static void DrawStyledTab(string label, Vector4 accent, Vector4 inactive, Vector4 hover, Action draw, bool disabled = false)
{
var tabColor = disabled ? ImGuiColors.DalamudGrey3 : inactive;
var tabHover = disabled ? ImGuiColors.DalamudGrey3 : hover;
var tabActive = disabled ? ImGuiColors.DalamudGrey2 : accent;
using var baseColor = ImRaii.PushColor(ImGuiCol.Tab, tabColor);
using var hoverColor = ImRaii.PushColor(ImGuiCol.TabHovered, tabHover);
using var activeColor = ImRaii.PushColor(ImGuiCol.TabActive, tabActive);
using var activeText = ImRaii.PushColor(ImGuiCol.Text, disabled ? ImGuiColors.DalamudGrey2 : Vector4.One, false);
using var tab = ImRaii.TabItem(label);
if (tab.Success)
{
draw();
}
}
private void DrawInvitationsTab(List<KeyValuePair<string, string>> incomingInvites, IReadOnlyCollection<AutoDetectRequestService.PendingRequestInfo> outgoingInvites)
{
if (incomingInvites.Count == 0 && outgoingInvites.Count == 0)
{
UiSharedService.ColorTextWrapped("Aucune invitation en attente. Cette page regroupera les demandes reçues et celles que vous avez envoyées.", ImGuiColors.DalamudGrey3);
return;
}
if (incomingInvites.Count == 0)
{
UiSharedService.ColorTextWrapped("Vous n'avez aucune invitation de pair en attente pour le moment.", ImGuiColors.DalamudGrey3);
}
ImGuiHelpers.ScaledDummy(4);
float leftWidth = Math.Max(220f * ImGuiHelpers.GlobalScale, ImGui.CalcTextSize("Invitations reçues (00)").X + ImGui.GetStyle().FramePadding.X * 4f);
var avail = ImGui.GetContentRegionAvail();
ImGui.BeginChild("incoming-requests", new Vector2(leftWidth, avail.Y), true);
ImGui.TextColored(ImGuiColors.DalamudOrange, $"Invitations reçues ({incomingInvites.Count})");
ImGui.Separator();
if (incomingInvites.Count == 0)
{
ImGui.TextDisabled("Aucune invitation reçue.");
}
else
{
foreach (var (uid, name) in incomingInvites.OrderBy(k => k.Value, StringComparer.OrdinalIgnoreCase))
{
using var id = ImRaii.PushId(uid);
bool processing = _acceptInFlight.Contains(uid);
ImGui.TextUnformatted(name);
ImGui.TextDisabled(uid);
if (processing)
{
ImGui.TextDisabled("Traitement en cours...");
}
else
{
if (ImGui.Button("Accepter"))
{
TriggerAccept(uid);
}
ImGui.SameLine();
if (ImGui.Button("Refuser"))
{
_pendingService.Remove(uid);
}
}
ImGui.Separator();
}
}
ImGui.EndChild();
ImGui.SameLine();
ImGui.BeginChild("outgoing-requests", new Vector2(0, avail.Y), true);
ImGui.TextColored(ImGuiColors.DalamudOrange, $"Invitations envoyées ({outgoingInvites.Count})");
ImGui.Separator();
if (outgoingInvites.Count == 0)
{
ImGui.TextDisabled("Aucune invitation envoyée en attente.");
ImGui.EndChild();
return;
}
foreach (var info in outgoingInvites.OrderByDescending(i => i.SentAt))
{
using var id = ImRaii.PushId(info.Key);
ImGui.TextUnformatted(info.TargetDisplayName);
if (!string.IsNullOrEmpty(info.Uid))
{
ImGui.TextDisabled(info.Uid);
}
ImGui.TextDisabled($"Envoyée il y a {FormatDuration(DateTime.UtcNow - info.SentAt)}");
if (ImGui.Button("Retirer"))
{
_requestService.RemovePendingRequestByKey(info.Key);
}
UiSharedService.AttachToolTip("Retire uniquement cette entrée locale de suivi.");
ImGui.Separator();
}
ImGui.EndChild();
}
private void DrawNearbyTab()
{
if (!_configService.Current.EnableAutoDetectDiscovery)
{
UiSharedService.ColorTextWrapped("AutoDetect est désactivé. Activez-le dans les paramètres pour détecter les utilisateurs Umbra à proximité.", ImGuiColors.DalamudYellow);
ImGuiHelpers.ScaledDummy(6);
}
int maxDist = Math.Clamp(_configService.Current.AutoDetectMaxDistanceMeters, 5, 100);
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("Max distance (m)");
ImGui.SameLine();
ImGui.SetNextItemWidth(120 * ImGuiHelpers.GlobalScale);
if (ImGui.SliderInt("##autodetect-dist", ref maxDist, 5, 100))
{
_configService.Current.AutoDetectMaxDistanceMeters = maxDist;
_configService.Save();
}
ImGuiHelpers.ScaledDummy(6);
// Table header
if (ImGui.BeginTable("autodetect-nearby", 5, ImGuiTableFlags.SizingStretchProp))
{
ImGui.TableSetupColumn("Name");
ImGui.TableSetupColumn("World");
ImGui.TableSetupColumn("Distance");
ImGui.TableSetupColumn("Status");
ImGui.TableSetupColumn("Action");
ImGui.TableHeadersRow();
var data = _entries.Count > 0 ? _entries.Where(e => e.IsMatch).ToList() : new List<Services.Mediator.NearbyEntry>();
foreach (var e in data)
{
ImGui.TableNextColumn();
ImGui.TextUnformatted(e.Name);
ImGui.TableNextColumn();
ImGui.TextUnformatted(e.WorldId == 0 ? "-" : (_dalamud.WorldData.Value.TryGetValue(e.WorldId, out var w) ? w : e.WorldId.ToString()));
ImGui.TableNextColumn();
ImGui.TextUnformatted(float.IsNaN(e.Distance) ? "-" : $"{e.Distance:0.0} m");
ImGui.TableNextColumn();
bool alreadyPaired = IsAlreadyPairedByUidOrAlias(e);
string status = alreadyPaired ? "Paired" : (string.IsNullOrEmpty(e.Token) ? "Requests disabled" : "On Umbra");
ImGui.TextUnformatted(status);
ImGui.TableNextColumn();
using (ImRaii.Disabled(alreadyPaired || string.IsNullOrEmpty(e.Token)))
{
if (alreadyPaired)
{
ImGui.Button($"Already sync##{e.Name}");
}
else if (string.IsNullOrEmpty(e.Token))
{
ImGui.Button($"Requests disabled##{e.Name}");
}
else if (ImGui.Button($"Send request##{e.Name}"))
{
_ = _requestService.SendRequestAsync(e.Token!, e.Uid, e.DisplayName);
}
}
}
ImGui.EndTable();
}
}
public override void OnOpen()
{
base.OnOpen();
Mediator.Subscribe<Services.Mediator.DiscoveryListUpdated>(this, OnDiscoveryUpdated);
}
public override void OnClose()
{
Mediator.Unsubscribe<Services.Mediator.DiscoveryListUpdated>(this);
base.OnClose();
}
private void OnDiscoveryUpdated(Services.Mediator.DiscoveryListUpdated msg)
{
_entries = msg.Entries;
}
private bool IsAlreadyPairedByUidOrAlias(Services.Mediator.NearbyEntry e)
{
try
{
// 1) Match by UID when available (authoritative)
if (!string.IsNullOrEmpty(e.Uid))
{
foreach (var p in _pairManager.DirectPairs)
{
if (string.Equals(p.UserData.UID, e.Uid, StringComparison.Ordinal))
return true;
}
}
var key = NormalizeKey(e.DisplayName ?? e.Name);
if (string.IsNullOrEmpty(key)) return false;
foreach (var p in _pairManager.DirectPairs)
{
if (NormalizeKey(p.UserData.AliasOrUID) == key) return true;
if (!string.IsNullOrEmpty(p.UserData.Alias) && NormalizeKey(p.UserData.Alias!) == key) return true;
}
}
catch
{
}
return false;
}
private static string NormalizeKey(string? input)
{
if (string.IsNullOrWhiteSpace(input)) return string.Empty;
var formD = input.Normalize(NormalizationForm.FormD);
var sb = new StringBuilder(formD.Length);
foreach (var ch in formD)
{
var cat = CharUnicodeInfo.GetUnicodeCategory(ch);
if (cat != UnicodeCategory.NonSpacingMark)
sb.Append(char.ToLowerInvariant(ch));
}
return sb.ToString();
}
private void TriggerAccept(string uid)
{
if (!_acceptInFlight.Add(uid)) return;
Task.Run(async () =>
{
try
{
bool ok = await _pendingService.AcceptAsync(uid).ConfigureAwait(false);
if (!ok)
{
Mediator.Publish(new NotificationMessage("AutoDetect", $"Impossible d'accepter l'invitation {uid}.", NotificationType.Warning, TimeSpan.FromSeconds(5)));
}
}
finally
{
_acceptInFlight.Remove(uid);
}
});
}
private static string FormatDuration(TimeSpan span)
{
if (span.TotalMinutes >= 1)
{
var minutes = Math.Max(1, (int)Math.Round(span.TotalMinutes));
return minutes == 1 ? "1 minute" : $"{minutes} minutes";
}
var seconds = Math.Max(1, (int)Math.Round(span.TotalSeconds));
return seconds == 1 ? "1 seconde" : $"{seconds} secondes";
}
}

View File

@@ -0,0 +1,249 @@
using System;
using System.Collections.Generic;
using System.Numerics;
using System.Reflection;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Utility.Raii;
using MareSynchronos.MareConfiguration;
using MareSynchronos.Services;
using MareSynchronos.Services.Mediator;
using Microsoft.Extensions.Logging;
namespace MareSynchronos.UI;
public sealed class ChangelogUi : WindowMediatorSubscriberBase
{
private const int AlwaysExpandedEntryCount = 2;
private readonly MareConfigService _configService;
private readonly UiSharedService _uiShared;
private readonly Version _currentVersion;
private readonly string _currentVersionLabel;
private readonly IReadOnlyList<ChangelogEntry> _entries;
private bool _showAllEntries;
private bool _hasAcknowledgedVersion;
public ChangelogUi(ILogger<ChangelogUi> logger, UiSharedService uiShared, MareConfigService configService,
MareMediator mediator, PerformanceCollectorService performanceCollectorService)
: base(logger, mediator, "Umbra Sync - Notes de version", performanceCollectorService)
{
_uiShared = uiShared;
_configService = configService;
_currentVersion = Assembly.GetExecutingAssembly().GetName().Version ?? new Version(0, 0, 0, 0);
_currentVersionLabel = _currentVersion.ToString();
_entries = BuildEntries();
_hasAcknowledgedVersion = string.Equals(_configService.Current.LastChangelogVersionSeen, _currentVersionLabel, StringComparison.Ordinal);
RespectCloseHotkey = true;
SizeConstraints = new()
{
MinimumSize = new(520, 360),
MaximumSize = new(900, 1200)
};
Flags |= ImGuiWindowFlags.NoResize;
ShowCloseButton = true;
if (!string.Equals(_configService.Current.LastChangelogVersionSeen, _currentVersionLabel, StringComparison.Ordinal))
{
IsOpen = true;
}
}
public override void OnClose()
{
MarkCurrentVersionAsReadIfNeeded();
base.OnClose();
}
protected override void DrawInternal()
{
_ = _uiShared.DrawOtherPluginState();
DrawHeader();
DrawEntries();
DrawFooter();
}
private void DrawHeader()
{
using (_uiShared.UidFont.Push())
{
ImGui.TextUnformatted("Notes de version");
}
ImGui.TextColored(ImGuiColors.DalamudGrey, $"Version chargée : {_currentVersionLabel}");
ImGui.Separator();
}
private void DrawEntries()
{
bool expandedOldVersions = false;
for (int index = 0; index < _entries.Count; index++)
{
var entry = _entries[index];
if (!_showAllEntries && index >= AlwaysExpandedEntryCount)
{
if (!expandedOldVersions)
{
expandedOldVersions = ImGui.CollapsingHeader("Historique complet");
}
if (!expandedOldVersions)
{
continue;
}
}
DrawEntry(entry);
}
}
private void DrawEntry(ChangelogEntry entry)
{
using (ImRaii.PushId(entry.VersionLabel))
{
ImGui.Spacing();
UiSharedService.ColorText(entry.VersionLabel, entry.Version == _currentVersion
? ImGuiColors.HealerGreen
: ImGuiColors.DalamudWhite);
ImGui.Spacing();
foreach (var line in entry.Lines)
{
DrawLine(line);
}
ImGui.Spacing();
ImGui.Separator();
}
}
private static void DrawLine(ChangelogLine line)
{
using var indent = line.IndentLevel > 0 ? ImRaii.PushIndent(line.IndentLevel) : null;
if (line.Color != null)
{
ImGui.TextColored(line.Color.Value, $"- {line.Text}");
}
else
{
ImGui.TextUnformatted($"- {line.Text}");
}
}
private void DrawFooter()
{
ImGui.Spacing();
if (!_showAllEntries && _entries.Count > AlwaysExpandedEntryCount)
{
if (ImGui.Button("Tout afficher"))
{
_showAllEntries = true;
}
ImGui.SameLine();
}
if (ImGui.Button("Marquer comme lu"))
{
MarkCurrentVersionAsReadIfNeeded();
IsOpen = false;
}
}
private void MarkCurrentVersionAsReadIfNeeded()
{
if (_hasAcknowledgedVersion)
return;
_configService.Current.LastChangelogVersionSeen = _currentVersionLabel;
_configService.Save();
_hasAcknowledgedVersion = true;
}
private static IReadOnlyList<ChangelogEntry> BuildEntries()
{
return new List<ChangelogEntry>
{
new(new Version(0, 1, 9, 6), "0.1.9.6", new List<ChangelogLine>
{
new("Possibilité de désactiver l'alerte self-analysis (Settings => Performance)."),
}),
new(new Version(0, 1, 9, 5), "0.1.9.5", new List<ChangelogLine>
{
new("Fix l'affichage de la bulle dans la liste du groupe."),
new("Amélioration de l'ajout des utilisateurs via le bouton +."),
new("Possibilité de mettre en pause individuellement des utilisateurs d'une syncshell."),
new("Amélioration de la stabilité du plugin en cas de petite connexion / petite configuration."),
new("Divers fix de l'interface."),
}),
new(new Version(0, 1, 9, 4), "0.1.9.4", new List<ChangelogLine>
{
new("Réécriture complète de la bulle de frappe avec la possibilité de choisir la taille de la bulle."),
new("Désactivation de l'AutoDetect en zone instanciée."),
new("Réécriture interface AutoDetect pour acceuillir les invitations en attente et préparer les synchsells publiques."),
new("Amélioration de la compréhension des activations / désactivations des préférences de synchronisation par défaut."),
new("Mise en avant du Self Analyse avec une alerte lorsqu'un seuil de donnée a été atteint."),
new("Ajout de l'alerte de la non-compatibilité du plugin Chat2."),
new("Divers fix de l'interface."),
}),
new(new Version(0, 1, 9, 3), "0.1.9.3", new List<ChangelogLine>
{
new("Correctif de l'affichage de la bulle de frappe quand l'interface est à + de 100%."),
}),
new(new Version(0, 1, 9, 2), "0.1.9.2", new List<ChangelogLine>
{
new("Correctif de l'affichage de la bulle de frappe."),
}),
new(new Version(0, 1, 9, 1), "0.1.9.1", new List<ChangelogLine>
{
new("Début correctif pour la bulle de frappe."),
new("Les bascules de synchronisation n'affichent plus qu'une seule notification résumée."),
}),
new(new Version(0, 1, 9, 0), "0.1.9.0", new List<ChangelogLine>
{
new("Il est désormais possible de configurer par défaut nos choix de synchronisation (VFX, Music, Animation)."),
new("La catégorie 'En attente' ne s'affiche uniquement que si une invitation est en attente"),
new("(EN PRÉ VERSION) Il est désormais possible de voir quand une personne appairée est en train d'écrire avec une bulle qui s'affiche."),
new("(EN PRÉ VERSION) La bulle de frappe s'affiche également sur votre propre plaque de nom lorsque vous écrivez."),
new("Les bascules de synchronisation n'affichent plus qu'une seule notification résumée."),
new("Correctif : Désormais, les invitation entrantes ne s'affichent qu'une seule fois au lieu de deux."),
}),
new(new Version(0, 1, 8, 2), "0.1.8.2", new List<ChangelogLine>
{
new("Détection Nearby : la liste rapide ne montre plus que les joueurs réellement invitables."),
new("Sont filtrés automatiquement les personnes refusées ou déjà appairées."),
new("Invitations Nearby : anti-spam de 5 minutes par personne, blocage 15 minutes après trois refus."),
new("Affichage : Correction de l'affichage des notes par défaut plutôt que de l'ID si disponible."),
new("Les notifications de blocage sont envoyées directement dans le tchat."),
new("Overlay DTR : affiche le nombre d'invitations Nearby disponibles dans le titre et l'infobulle."),
new("Poses Nearby : le filtre re-fonctionne avec vos notes locales pour retrouver les entrées correspondantes."),
}),
new(new Version(0, 1, 8, 1), "0.1.8.1", new List<ChangelogLine>
{
new("Correctif 'Vu sous' : l'infobulle affiche désormais le dernier personnage observé."),
new("Invitations AutoDetect : triées en tête de liste pour mieux les repérer."),
new("Invitations AutoDetect : conservées entre les redémarrages du plugin ou du jeu."),
new("Barre de statut serveur : couleur violette adoptée par défaut."),
}),
new(new Version(0, 1, 8, 0), "0.1.8.0", new List<ChangelogLine>
{
new("AutoDetect : détection automatique des joueurs Umbra autour de vous et propositions d'appairage."),
new("AutoDetect : désactivé par défaut pour préserver la confidentialité.", 1, ImGuiColors.DalamudGrey),
new("AutoDetect : activez-le dans 'Transfers' avec les options Nearby detection et Allow pair requests.", 1, ImGuiColors.DalamudGrey),
new("Syncshell temporaire : durée configurable de 1 h à 7 jours, expiration automatique."),
new("Syncshell permanente : possibilité de nommer et d'organiser vos groupes sur la durée."),
new("Interface : palette UmbraSync harmonisée et menus allégés pour l'usage RP."),
}),
};
}
private readonly record struct ChangelogEntry(Version Version, string VersionLabel, IReadOnlyList<ChangelogLine> Lines);
private readonly record struct ChangelogLine(string Text, int IndentLevel = 0, System.Numerics.Vector4? Color = null);
}

View File

@@ -15,14 +15,14 @@ internal sealed partial class CharaDataHubUi
if (!_charaDataManager.BrioAvailable) if (!_charaDataManager.BrioAvailable)
{ {
ImGuiHelpers.ScaledDummy(5); ImGuiHelpers.ScaledDummy(5);
UiSharedService.DrawGroupedCenteredColorText("BRIO IS MANDATORY FOR GPOSE TOGETHER.", ImGuiColors.DalamudRed); UiSharedService.DrawGroupedCenteredColorText("BRIO IS MANDATORY FOR GPOSE TOGETHER.", UiSharedService.AccentColor);
ImGuiHelpers.ScaledDummy(5); ImGuiHelpers.ScaledDummy(5);
} }
if (!_uiSharedService.ApiController.IsConnected) if (!_uiSharedService.ApiController.IsConnected)
{ {
ImGuiHelpers.ScaledDummy(5); ImGuiHelpers.ScaledDummy(5);
UiSharedService.DrawGroupedCenteredColorText("CANNOT USE GPOSE TOGETHER WHILE DISCONNECTED FROM THE SERVER.", ImGuiColors.DalamudRed); UiSharedService.DrawGroupedCenteredColorText("CANNOT USE GPOSE TOGETHER WHILE DISCONNECTED FROM THE SERVER.", UiSharedService.AccentColor);
ImGuiHelpers.ScaledDummy(5); ImGuiHelpers.ScaledDummy(5);
} }
@@ -165,7 +165,7 @@ internal sealed partial class CharaDataHubUi
UiSharedService.AttachToolTip(user.WorldDataDescriptor + UiSharedService.TooltipSeparator); UiSharedService.AttachToolTip(user.WorldDataDescriptor + UiSharedService.TooltipSeparator);
ImGui.SameLine(); ImGui.SameLine();
_uiSharedService.IconText(FontAwesomeIcon.Map, sameMapAndServer.SameMap ? ImGuiColors.ParsedGreen : ImGuiColors.DalamudRed); _uiSharedService.IconText(FontAwesomeIcon.Map, sameMapAndServer.SameMap ? ImGuiColors.ParsedGreen : UiSharedService.AccentColor);
if (ImGui.IsItemClicked(ImGuiMouseButton.Left) && user.WorldData != null) if (ImGui.IsItemClicked(ImGuiMouseButton.Left) && user.WorldData != null)
{ {
_dalamudUtilService.SetMarkerAndOpenMap(new(user.WorldData.Value.PositionX, user.WorldData.Value.PositionY, user.WorldData.Value.PositionZ), user.Map); _dalamudUtilService.SetMarkerAndOpenMap(new(user.WorldData.Value.PositionX, user.WorldData.Value.PositionY, user.WorldData.Value.PositionZ), user.Map);
@@ -175,12 +175,12 @@ internal sealed partial class CharaDataHubUi
+ "Note: For GPose synchronization to work properly, you must be on the same map."); + "Note: For GPose synchronization to work properly, you must be on the same map.");
ImGui.SameLine(); ImGui.SameLine();
_uiSharedService.IconText(FontAwesomeIcon.Globe, sameMapAndServer.SameServer ? ImGuiColors.ParsedGreen : ImGuiColors.DalamudRed); _uiSharedService.IconText(FontAwesomeIcon.Globe, sameMapAndServer.SameServer ? ImGuiColors.ParsedGreen : UiSharedService.AccentColor);
UiSharedService.AttachToolTip((sameMapAndServer.SameMap ? "You are on the same server." : "You are not on the same server.") + UiSharedService.TooltipSeparator UiSharedService.AttachToolTip((sameMapAndServer.SameMap ? "You are on the same server." : "You are not on the same server.") + UiSharedService.TooltipSeparator
+ "Note: GPose synchronization is not dependent on the current server, but you will have to spawn a character for the other lobby users."); + "Note: GPose synchronization is not dependent on the current server, but you will have to spawn a character for the other lobby users.");
ImGui.SameLine(); ImGui.SameLine();
_uiSharedService.IconText(FontAwesomeIcon.Running, sameMapAndServer.SameEverything ? ImGuiColors.ParsedGreen : ImGuiColors.DalamudRed); _uiSharedService.IconText(FontAwesomeIcon.Running, sameMapAndServer.SameEverything ? ImGuiColors.ParsedGreen : UiSharedService.AccentColor);
UiSharedService.AttachToolTip(sameMapAndServer.SameEverything ? "You are in the same instanced area." : "You are not the same instanced area." + UiSharedService.TooltipSeparator + UiSharedService.AttachToolTip(sameMapAndServer.SameEverything ? "You are in the same instanced area." : "You are not the same instanced area." + UiSharedService.TooltipSeparator +
"Note: Users not in your instance, but on the same map, will be drawn as floating wisps." + Environment.NewLine "Note: Users not in your instance, but on the same map, will be drawn as floating wisps." + Environment.NewLine
+ "Note: GPose synchronization is not dependent on the current instance, but you will have to spawn a character for the other lobby users."); + "Note: GPose synchronization is not dependent on the current instance, but you will have to spawn a character for the other lobby users.");
@@ -217,7 +217,7 @@ internal sealed partial class CharaDataHubUi
if (_uiSharedService.IsInGpose && user.Address == nint.Zero) if (_uiSharedService.IsInGpose && user.Address == nint.Zero)
{ {
ImGui.SameLine(); ImGui.SameLine();
_uiSharedService.IconText(FontAwesomeIcon.ExclamationTriangle, ImGuiColors.DalamudRed); _uiSharedService.IconText(FontAwesomeIcon.ExclamationTriangle, UiSharedService.AccentColor);
UiSharedService.AttachToolTip("No valid character assigned for this user. Pose data will not be applied."); UiSharedService.AttachToolTip("No valid character assigned for this user. Pose data will not be applied.");
} }
} }

View File

@@ -45,7 +45,7 @@ internal sealed partial class CharaDataHubUi
if (canUpdate) if (canUpdate)
{ {
ImGui.AlignTextToFramePadding(); ImGui.AlignTextToFramePadding();
UiSharedService.ColorTextWrapped("Warning: You have unsaved changes!", ImGuiColors.DalamudRed); UiSharedService.ColorTextWrapped("Warning: You have unsaved changes!", UiSharedService.AccentColor);
ImGui.SameLine(); ImGui.SameLine();
using (ImRaii.Disabled(_charaDataManager.CharaUpdateTask != null && !_charaDataManager.CharaUpdateTask.IsCompleted)) using (ImRaii.Disabled(_charaDataManager.CharaUpdateTask != null && !_charaDataManager.CharaUpdateTask.IsCompleted))
{ {
@@ -216,7 +216,7 @@ internal sealed partial class CharaDataHubUi
} }
else else
{ {
UiSharedService.ColorTextWrapped($"{dataDto.MissingFiles.DistinctBy(k => k.HashOrFileSwap).Count()} files to download this character data are missing on the server", ImGuiColors.DalamudRed); UiSharedService.ColorTextWrapped($"{dataDto.MissingFiles.DistinctBy(k => k.HashOrFileSwap).Count()} files to download this character data are missing on the server", UiSharedService.AccentColor);
ImGui.NewLine(); ImGui.NewLine();
ImGui.SameLine(pos); ImGui.SameLine(pos);
if (_uiSharedService.IconTextButton(FontAwesomeIcon.ArrowCircleUp, "Attempt to upload missing files and restore Character Data")) if (_uiSharedService.IconTextButton(FontAwesomeIcon.ArrowCircleUp, "Attempt to upload missing files and restore Character Data"))
@@ -401,7 +401,7 @@ internal sealed partial class CharaDataHubUi
else if (!_charaDataManager.BrioAvailable) else if (!_charaDataManager.BrioAvailable)
{ {
ImGuiHelpers.ScaledDummy(5); ImGuiHelpers.ScaledDummy(5);
UiSharedService.DrawGroupedCenteredColorText("To attach pose and world data Brio requires to be installed.", ImGuiColors.DalamudRed); UiSharedService.DrawGroupedCenteredColorText("To attach pose and world data Brio requires to be installed.", UiSharedService.AccentColor);
ImGuiHelpers.ScaledDummy(5); ImGuiHelpers.ScaledDummy(5);
} }
@@ -685,7 +685,7 @@ internal sealed partial class CharaDataHubUi
} }
else if (_charaDataManager.DataCreationTask != null && _charaDataManager.DataCreationTask.IsCompleted) else if (_charaDataManager.DataCreationTask != null && _charaDataManager.DataCreationTask.IsCompleted)
{ {
var color = _charaDataManager.DataCreationTask.Result.Success ? ImGuiColors.HealerGreen : ImGuiColors.DalamudRed; var color = _charaDataManager.DataCreationTask.Result.Success ? ImGuiColors.HealerGreen : UiSharedService.AccentColor;
UiSharedService.ColorTextWrapped(_charaDataManager.DataCreationTask.Result.Output, color); UiSharedService.ColorTextWrapped(_charaDataManager.DataCreationTask.Result.Output, color);
} }

View File

@@ -57,6 +57,14 @@ internal partial class CharaDataHubUi
_configService.Save(); _configService.Save();
} }
_uiSharedService.DrawHelpText("Draw floating wisps where other's poses are in the world."); _uiSharedService.DrawHelpText("Draw floating wisps where other's poses are in the world.");
int maxWisps = _configService.Current.NearbyMaxWisps;
ImGui.SetNextItemWidth(140);
if (ImGui.SliderInt("Maximum wisps", ref maxWisps, 0, 200))
{
_configService.Current.NearbyMaxWisps = maxWisps;
_configService.Save();
}
_uiSharedService.DrawHelpText("Limit how many wisps are active at once. Set to 0 to disable wisps even when enabled above.");
int poseDetectionDistance = _configService.Current.NearbyDistanceFilter; int poseDetectionDistance = _configService.Current.NearbyDistanceFilter;
ImGui.SetNextItemWidth(100); ImGui.SetNextItemWidth(100);
if (ImGui.SliderInt("Detection Distance", ref poseDetectionDistance, 5, 1000)) if (ImGui.SliderInt("Detection Distance", ref poseDetectionDistance, 5, 1000))

View File

@@ -79,7 +79,7 @@ internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase
UiSharedService uiSharedService, ServerConfigurationManager serverConfigurationManager, UiSharedService uiSharedService, ServerConfigurationManager serverConfigurationManager,
DalamudUtilService dalamudUtilService, FileDialogManager fileDialogManager, PairManager pairManager, DalamudUtilService dalamudUtilService, FileDialogManager fileDialogManager, PairManager pairManager,
CharaDataGposeTogetherManager charaDataGposeTogetherManager) CharaDataGposeTogetherManager charaDataGposeTogetherManager)
: base(logger, mediator, "UmbraSync Character Data Hub###UmbraSyncCharaDataUI", performanceCollectorService) : base(logger, mediator, "Umbra Character Data Hub###UmbraCharaDataUI", performanceCollectorService)
{ {
SetWindowSizeConstraints(); SetWindowSizeConstraints();
@@ -170,7 +170,7 @@ internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase
if (!_charaDataManager.BrioAvailable) if (!_charaDataManager.BrioAvailable)
{ {
ImGuiHelpers.ScaledDummy(3); ImGuiHelpers.ScaledDummy(3);
UiSharedService.DrawGroupedCenteredColorText("To utilize any features related to posing or spawning characters you require to have Brio installed.", ImGuiColors.DalamudRed); UiSharedService.DrawGroupedCenteredColorText("To utilize any features related to posing or spawning characters you require to have Brio installed.", UiSharedService.AccentColor);
UiSharedService.DistanceSeparator(); UiSharedService.DistanceSeparator();
} }
@@ -194,7 +194,7 @@ internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase
} }
if (_charaDataManager.DataApplicationTask != null) if (_charaDataManager.DataApplicationTask != null)
{ {
UiSharedService.ColorTextWrapped("WARNING: During the data application avoid interacting with this actor to prevent potential crashes.", ImGuiColors.DalamudRed); UiSharedService.ColorTextWrapped("WARNING: During the data application avoid interacting with this actor to prevent potential crashes.", UiSharedService.AccentColor);
ImGuiHelpers.ScaledDummy(5); ImGuiHelpers.ScaledDummy(5);
ImGui.Separator(); ImGui.Separator();
} }
@@ -648,7 +648,7 @@ internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase
} }
if ((_charaDataManager.DownloadMetaInfoTask?.IsCompleted ?? false) && !_charaDataManager.DownloadMetaInfoTask.Result.Success) if ((_charaDataManager.DownloadMetaInfoTask?.IsCompleted ?? false) && !_charaDataManager.DownloadMetaInfoTask.Result.Success)
{ {
UiSharedService.ColorTextWrapped(_charaDataManager.DownloadMetaInfoTask.Result.Result, ImGuiColors.DalamudRed); UiSharedService.ColorTextWrapped(_charaDataManager.DownloadMetaInfoTask.Result.Result, UiSharedService.AccentColor);
} }
using (ImRaii.Disabled(_charaDataManager.LastDownloadedMetaInfo == null)) using (ImRaii.Disabled(_charaDataManager.LastDownloadedMetaInfo == null))
@@ -848,7 +848,7 @@ internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase
if ((_charaDataManager.LoadedMcdfHeader?.IsFaulted ?? false) || (_charaDataManager.McdfApplicationTask?.IsFaulted ?? false)) if ((_charaDataManager.LoadedMcdfHeader?.IsFaulted ?? false) || (_charaDataManager.McdfApplicationTask?.IsFaulted ?? false))
{ {
UiSharedService.ColorTextWrapped("Failure to read MCDF file. MCDF file is possibly corrupt. Re-export the MCDF file and try again.", UiSharedService.ColorTextWrapped("Failure to read MCDF file. MCDF file is possibly corrupt. Re-export the MCDF file and try again.",
ImGuiColors.DalamudRed); UiSharedService.AccentColor);
UiSharedService.ColorTextWrapped("Note: if this is your MCDF, try redrawing yourself, wait and re-export the file. " + UiSharedService.ColorTextWrapped("Note: if this is your MCDF, try redrawing yourself, wait and re-export the file. " +
"If you received it from someone else have them do the same.", ImGuiColors.DalamudYellow); "If you received it from someone else have them do the same.", ImGuiColors.DalamudYellow);
} }

View File

@@ -7,11 +7,13 @@ using Dalamud.Utility;
using MareSynchronos.API.Data.Extensions; using MareSynchronos.API.Data.Extensions;
using MareSynchronos.API.Dto.User; using MareSynchronos.API.Dto.User;
using MareSynchronos.MareConfiguration; using MareSynchronos.MareConfiguration;
using MareSynchronos.MareConfiguration.Models;
using MareSynchronos.PlayerData.Handlers; using MareSynchronos.PlayerData.Handlers;
using MareSynchronos.PlayerData.Pairs; using MareSynchronos.PlayerData.Pairs;
using MareSynchronos.Services; using MareSynchronos.Services;
using MareSynchronos.Services.Mediator; using MareSynchronos.Services.Mediator;
using MareSynchronos.Services.ServerConfiguration; using MareSynchronos.Services.ServerConfiguration;
using MareSynchronos.Services.AutoDetect;
using MareSynchronos.UI.Components; using MareSynchronos.UI.Components;
using MareSynchronos.UI.Handlers; using MareSynchronos.UI.Handlers;
using MareSynchronos.WebAPI; using MareSynchronos.WebAPI;
@@ -24,6 +26,7 @@ using System.Diagnostics;
using System.Globalization; using System.Globalization;
using System.Numerics; using System.Numerics;
using System.Reflection; using System.Reflection;
using System.Linq;
namespace MareSynchronos.UI; namespace MareSynchronos.UI;
@@ -43,6 +46,9 @@ public class CompactUi : WindowMediatorSubscriberBase
private readonly ServerConfigurationManager _serverManager; private readonly ServerConfigurationManager _serverManager;
private readonly Stopwatch _timeout = new(); private readonly Stopwatch _timeout = new();
private readonly CharaDataManager _charaDataManager; private readonly CharaDataManager _charaDataManager;
private readonly NearbyPendingService _nearbyPending;
private readonly AutoDetectRequestService _autoDetectRequestService;
private readonly CharacterAnalyzer _characterAnalyzer;
private readonly UidDisplayHandler _uidDisplayHandler; private readonly UidDisplayHandler _uidDisplayHandler;
private readonly UiSharedService _uiSharedService; private readonly UiSharedService _uiSharedService;
private bool _buttonState; private bool _buttonState;
@@ -56,11 +62,18 @@ public class CompactUi : WindowMediatorSubscriberBase
private bool _showModalForUserAddition; private bool _showModalForUserAddition;
private bool _showSyncShells; private bool _showSyncShells;
private bool _wasOpen; private bool _wasOpen;
private bool _nearbyOpen = true;
private List<Services.Mediator.NearbyEntry> _nearbyEntries = new();
private const long SelfAnalysisSizeWarningThreshold = 300L * 1024 * 1024;
private const long SelfAnalysisTriangleWarningThreshold = 150_000;
public CompactUi(ILogger<CompactUi> logger, UiSharedService uiShared, MareConfigService configService, ApiController apiController, PairManager pairManager, ChatService chatService, public CompactUi(ILogger<CompactUi> logger, UiSharedService uiShared, MareConfigService configService, ApiController apiController, PairManager pairManager, ChatService chatService,
ServerConfigurationManager serverManager, MareMediator mediator, FileUploadManager fileTransferManager, UidDisplayHandler uidDisplayHandler, CharaDataManager charaDataManager, ServerConfigurationManager serverManager, MareMediator mediator, FileUploadManager fileTransferManager, UidDisplayHandler uidDisplayHandler, CharaDataManager charaDataManager,
NearbyPendingService nearbyPendingService,
AutoDetectRequestService autoDetectRequestService,
CharacterAnalyzer characterAnalyzer,
PerformanceCollectorService performanceCollectorService) PerformanceCollectorService performanceCollectorService)
: base(logger, mediator, "###UmbraSyncSyncMainUI", performanceCollectorService) : base(logger, mediator, "###UmbraSyncMainUI", performanceCollectorService)
{ {
_uiSharedService = uiShared; _uiSharedService = uiShared;
_configService = configService; _configService = configService;
@@ -70,9 +83,12 @@ public class CompactUi : WindowMediatorSubscriberBase
_fileTransferManager = fileTransferManager; _fileTransferManager = fileTransferManager;
_uidDisplayHandler = uidDisplayHandler; _uidDisplayHandler = uidDisplayHandler;
_charaDataManager = charaDataManager; _charaDataManager = charaDataManager;
_nearbyPending = nearbyPendingService;
_autoDetectRequestService = autoDetectRequestService;
_characterAnalyzer = characterAnalyzer;
var tagHandler = new TagHandler(_serverManager); var tagHandler = new TagHandler(_serverManager);
_groupPanel = new(this, uiShared, _pairManager, chatService, uidDisplayHandler, _configService, _serverManager, _charaDataManager); _groupPanel = new(this, uiShared, _pairManager, chatService, uidDisplayHandler, _configService, _serverManager, _charaDataManager, _autoDetectRequestService);
_selectGroupForPairUi = new(tagHandler, uidDisplayHandler, _uiSharedService); _selectGroupForPairUi = new(tagHandler, uidDisplayHandler, _uiSharedService);
_selectPairsForGroupUi = new(tagHandler, uidDisplayHandler); _selectPairsForGroupUi = new(tagHandler, uidDisplayHandler);
_pairGroupsUi = new(configService, tagHandler, uidDisplayHandler, apiController, _selectPairsForGroupUi, _uiSharedService); _pairGroupsUi = new(configService, tagHandler, uidDisplayHandler, apiController, _selectPairsForGroupUi, _uiSharedService);
@@ -80,11 +96,11 @@ public class CompactUi : WindowMediatorSubscriberBase
#if DEBUG #if DEBUG
string dev = "Dev Build"; string dev = "Dev Build";
var ver = Assembly.GetExecutingAssembly().GetName().Version!; var ver = Assembly.GetExecutingAssembly().GetName().Version!;
WindowName = $"UmbraSync Sync {dev} ({ver.Major}.{ver.Minor}.{ver.Build})###UmbraSyncSyncMainUIDev"; WindowName = $"UmbraSync {dev} ({ver.Major}.{ver.Minor}.{ver.Build})###UmbraSyncMainUIDev";
Toggle(); Toggle();
#else #else
var ver = Assembly.GetExecutingAssembly().GetName().Version!; var ver = Assembly.GetExecutingAssembly().GetName().Version!;
WindowName = "UmbraSync Sync " + ver.Major + "." + ver.Minor + "." + ver.Build + "###UmbraSyncSyncMainUI"; WindowName = "UmbraSync " + ver.Major + "." + ver.Minor + "." + ver.Build + "###UmbracSyncMainUI";
#endif #endif
Mediator.Subscribe<SwitchToMainUiMessage>(this, (_) => IsOpen = true); Mediator.Subscribe<SwitchToMainUiMessage>(this, (_) => IsOpen = true);
Mediator.Subscribe<SwitchToIntroUiMessage>(this, (_) => IsOpen = false); Mediator.Subscribe<SwitchToIntroUiMessage>(this, (_) => IsOpen = false);
@@ -92,6 +108,20 @@ public class CompactUi : WindowMediatorSubscriberBase
Mediator.Subscribe<CutsceneEndMessage>(this, (_) => UiSharedService_GposeEnd()); Mediator.Subscribe<CutsceneEndMessage>(this, (_) => UiSharedService_GposeEnd());
Mediator.Subscribe<DownloadStartedMessage>(this, (msg) => _currentDownloads[msg.DownloadId] = msg.DownloadStatus); Mediator.Subscribe<DownloadStartedMessage>(this, (msg) => _currentDownloads[msg.DownloadId] = msg.DownloadStatus);
Mediator.Subscribe<DownloadFinishedMessage>(this, (msg) => _currentDownloads.TryRemove(msg.DownloadId, out _)); Mediator.Subscribe<DownloadFinishedMessage>(this, (msg) => _currentDownloads.TryRemove(msg.DownloadId, out _));
Mediator.Subscribe<DiscoveryListUpdated>(this, (msg) =>
{
_nearbyEntries = msg.Entries;
// Update last-seen character names for matched entries
foreach (var e in _nearbyEntries.Where(x => x.IsMatch))
{
var uid = e.Uid;
var lastSeen = e.DisplayName ?? e.Name;
if (!string.IsNullOrEmpty(uid) && !string.IsNullOrEmpty(lastSeen))
{
_serverManager.SetNameForUid(uid, lastSeen);
}
}
});
Flags |= ImGuiWindowFlags.NoDocking; Flags |= ImGuiWindowFlags.NoDocking;
@@ -104,10 +134,15 @@ public class CompactUi : WindowMediatorSubscriberBase
protected override void DrawInternal() protected override void DrawInternal()
{ {
if (_serverManager.CurrentApiUrl.Equals(ApiController.UmbraSyncServiceUri, StringComparison.Ordinal)) UiSharedService.AccentColor = new Vector4(0x8D / 255f, 0x37 / 255f, 0xC0 / 255f, 1f);
UiSharedService.AccentColor = new Vector4(1.0f, 0.8666f, 0.06666f, 1.0f); UiSharedService.AccentHoverColor = new Vector4(0x3A / 255f, 0x15 / 255f, 0x50 / 255f, 1f);
else UiSharedService.AccentActiveColor = UiSharedService.AccentHoverColor;
UiSharedService.AccentColor = ImGuiColors.ParsedGreen; var accent = UiSharedService.AccentColor;
using var titleBg = ImRaii.PushColor(ImGuiCol.TitleBg, accent);
using var titleBgActive = ImRaii.PushColor(ImGuiCol.TitleBgActive, accent);
using var titleBgCollapsed = ImRaii.PushColor(ImGuiCol.TitleBgCollapsed, accent);
using var buttonHover = ImRaii.PushColor(ImGuiCol.ButtonHovered, UiSharedService.AccentHoverColor);
using var buttonActive = ImRaii.PushColor(ImGuiCol.ButtonActive, UiSharedService.AccentActiveColor);
ImGui.SetCursorPosY(ImGui.GetCursorPosY() - ImGui.GetStyle().WindowPadding.Y - 1f * ImGuiHelpers.GlobalScale + ImGui.GetStyle().ItemSpacing.Y); ImGui.SetCursorPosY(ImGui.GetCursorPosY() - ImGui.GetStyle().WindowPadding.Y - 1f * ImGuiHelpers.GlobalScale + ImGui.GetStyle().ItemSpacing.Y);
WindowContentWidth = UiSharedService.GetWindowContentRegionWidth(); WindowContentWidth = UiSharedService.GetWindowContentRegionWidth();
if (!_apiController.IsCurrentVersion) if (!_apiController.IsCurrentVersion)
@@ -119,10 +154,10 @@ public class CompactUi : WindowMediatorSubscriberBase
var uidTextSize = ImGui.CalcTextSize(unsupported); var uidTextSize = ImGui.CalcTextSize(unsupported);
ImGui.SetCursorPosX((ImGui.GetWindowContentRegionMax().X + ImGui.GetWindowContentRegionMin().X) / 2 - uidTextSize.X / 2); ImGui.SetCursorPosX((ImGui.GetWindowContentRegionMax().X + ImGui.GetWindowContentRegionMin().X) / 2 - uidTextSize.X / 2);
ImGui.AlignTextToFramePadding(); ImGui.AlignTextToFramePadding();
ImGui.TextColored(ImGuiColors.DalamudRed, unsupported); ImGui.TextColored(UiSharedService.AccentColor, unsupported);
} }
UiSharedService.ColorTextWrapped($"Your UmbraSync installation is out of date, the current version is {ver.Major}.{ver.Minor}.{ver.Build}. " + UiSharedService.ColorTextWrapped($"Your UmbraSync installation is out of date, the current version is {ver.Major}.{ver.Minor}.{ver.Build}. " +
$"It is highly recommended to keep UmbraSync up to date. Open /xlplugins and update the plugin.", ImGuiColors.DalamudRed); $"It is highly recommended to keep UmbraSync up to date. Open /xlplugins and update the plugin.", UiSharedService.AccentColor);
} }
using (ImRaii.PushId("header")) DrawUIDHeader(); using (ImRaii.PushId("header")) DrawUIDHeader();
@@ -133,42 +168,61 @@ public class CompactUi : WindowMediatorSubscriberBase
{ {
var hasShownSyncShells = _showSyncShells; var hasShownSyncShells = _showSyncShells;
ImGui.PushFont(UiBuilder.IconFont); using (var hoverColor = ImRaii.PushColor(ImGuiCol.ButtonHovered, UiSharedService.AccentHoverColor))
using (var activeColor = ImRaii.PushColor(ImGuiCol.ButtonActive, UiSharedService.AccentActiveColor))
{
if (!hasShownSyncShells) if (!hasShownSyncShells)
{ {
ImGui.PushStyleColor(ImGuiCol.Button, ImGui.GetStyle().Colors[(int)ImGuiCol.ButtonHovered]); using var selectedColor = ImRaii.PushColor(ImGuiCol.Button, accent);
} using (ImRaii.PushFont(UiBuilder.IconFont))
{
if (ImGui.Button(FontAwesomeIcon.User.ToIconString(), new Vector2((UiSharedService.GetWindowContentRegionWidth() - ImGui.GetWindowContentRegionMin().X) / 2, 30 * ImGuiHelpers.GlobalScale))) if (ImGui.Button(FontAwesomeIcon.User.ToIconString(), new Vector2((UiSharedService.GetWindowContentRegionWidth() - ImGui.GetWindowContentRegionMin().X) / 2, 30 * ImGuiHelpers.GlobalScale)))
{ {
_showSyncShells = false; _showSyncShells = false;
} }
if (!hasShownSyncShells)
{
ImGui.PopStyleColor();
} }
ImGui.PopFont(); }
else
{
using (ImRaii.PushFont(UiBuilder.IconFont))
{
if (ImGui.Button(FontAwesomeIcon.User.ToIconString(), new Vector2((UiSharedService.GetWindowContentRegionWidth() - ImGui.GetWindowContentRegionMin().X) / 2, 30 * ImGuiHelpers.GlobalScale)))
{
_showSyncShells = false;
}
}
}
UiSharedService.AttachToolTip("Individual pairs"); UiSharedService.AttachToolTip("Individual pairs");
ImGui.SameLine(); ImGui.SameLine();
ImGui.PushFont(UiBuilder.IconFont);
if (hasShownSyncShells) if (hasShownSyncShells)
{ {
ImGui.PushStyleColor(ImGuiCol.Button, ImGui.GetStyle().Colors[(int)ImGuiCol.ButtonHovered]); using var selectedColor = ImRaii.PushColor(ImGuiCol.Button, accent);
} using (ImRaii.PushFont(UiBuilder.IconFont))
{
if (ImGui.Button(FontAwesomeIcon.UserFriends.ToIconString(), new Vector2((UiSharedService.GetWindowContentRegionWidth() - ImGui.GetWindowContentRegionMin().X) / 2, 30 * ImGuiHelpers.GlobalScale))) if (ImGui.Button(FontAwesomeIcon.UserFriends.ToIconString(), new Vector2((UiSharedService.GetWindowContentRegionWidth() - ImGui.GetWindowContentRegionMin().X) / 2, 30 * ImGuiHelpers.GlobalScale)))
{ {
_showSyncShells = true; _showSyncShells = true;
} }
if (hasShownSyncShells)
{
ImGui.PopStyleColor();
} }
ImGui.PopFont(); }
else
{
using (ImRaii.PushFont(UiBuilder.IconFont))
{
if (ImGui.Button(FontAwesomeIcon.UserFriends.ToIconString(), new Vector2((UiSharedService.GetWindowContentRegionWidth() - ImGui.GetWindowContentRegionMin().X) / 2, 30 * ImGuiHelpers.GlobalScale)))
{
_showSyncShells = true;
}
}
}
UiSharedService.AttachToolTip("Syncshells"); UiSharedService.AttachToolTip("Syncshells");
}
ImGui.Separator(); DrawDefaultSyncSettings();
if (!hasShownSyncShells) if (!hasShownSyncShells)
{ {
using (ImRaii.PushId("pairlist")) DrawPairList(); using (ImRaii.PushId("pairlist")) DrawPairList();
@@ -231,6 +285,293 @@ public class CompactUi : WindowMediatorSubscriberBase
base.OnClose(); base.OnClose();
} }
private void DrawDefaultSyncSettings()
{
ImGuiHelpers.ScaledDummy(4f);
using (ImRaii.PushId("sync-defaults"))
{
const string soundLabel = "Audio";
const string animLabel = "Anim";
const string vfxLabel = "VFX";
const string soundSubject = "de l'audio";
const string animSubject = "des animations";
const string vfxSubject = "des effets visuels";
bool soundsDisabled = _configService.Current.DefaultDisableSounds;
bool animsDisabled = _configService.Current.DefaultDisableAnimations;
bool vfxDisabled = _configService.Current.DefaultDisableVfx;
bool showNearby = _configService.Current.EnableAutoDetectDiscovery;
int pendingInvites = _nearbyPending.Pending.Count;
const string nearbyLabel = "AutoDetect";
var soundIcon = soundsDisabled ? FontAwesomeIcon.VolumeMute : FontAwesomeIcon.VolumeUp;
var animIcon = animsDisabled ? FontAwesomeIcon.WindowClose : FontAwesomeIcon.Running;
var vfxIcon = vfxDisabled ? FontAwesomeIcon.TimesCircle : FontAwesomeIcon.Sun;
float spacing = ImGui.GetStyle().ItemSpacing.X;
float audioWidth = _uiSharedService.GetIconTextButtonSize(soundIcon, soundLabel);
float animWidth = _uiSharedService.GetIconTextButtonSize(animIcon, animLabel);
float vfxWidth = _uiSharedService.GetIconTextButtonSize(vfxIcon, vfxLabel);
float nearbyWidth = showNearby ? _uiSharedService.GetIconTextButtonSize(FontAwesomeIcon.UserPlus, pendingInvites > 0 ? $"{nearbyLabel} ({pendingInvites})" : nearbyLabel) : 0f;
int buttonCount = 3 + (showNearby ? 1 : 0);
float totalWidth = audioWidth + animWidth + vfxWidth + nearbyWidth + spacing * (buttonCount - 1);
float available = ImGui.GetContentRegionAvail().X;
float startCursorX = ImGui.GetCursorPosX();
if (totalWidth < available)
{
ImGui.SetCursorPosX(startCursorX + (available - totalWidth) / 2f);
}
DrawDefaultSyncButton(soundIcon, soundLabel, audioWidth, soundsDisabled,
state =>
{
_configService.Current.DefaultDisableSounds = state;
_configService.Save();
Mediator.Publish(new ApplyDefaultsToAllSyncsMessage(soundSubject, state));
},
() => DisableStateTooltip(soundSubject, _configService.Current.DefaultDisableSounds));
DrawDefaultSyncButton(animIcon, animLabel, animWidth, animsDisabled,
state =>
{
_configService.Current.DefaultDisableAnimations = state;
_configService.Save();
Mediator.Publish(new ApplyDefaultsToAllSyncsMessage(animSubject, state));
},
() => DisableStateTooltip(animSubject, _configService.Current.DefaultDisableAnimations), spacing);
DrawDefaultSyncButton(vfxIcon, vfxLabel, vfxWidth, vfxDisabled,
state =>
{
_configService.Current.DefaultDisableVfx = state;
_configService.Save();
Mediator.Publish(new ApplyDefaultsToAllSyncsMessage(vfxSubject, state));
},
() => DisableStateTooltip(vfxSubject, _configService.Current.DefaultDisableVfx), spacing);
if (showNearby)
{
ImGui.SameLine(0, spacing);
var autodetectLabel = pendingInvites > 0 ? $"{nearbyLabel} ({pendingInvites})" : nearbyLabel;
if (_uiSharedService.IconTextButton(FontAwesomeIcon.UserPlus, autodetectLabel, nearbyWidth))
{
Mediator.Publish(new UiToggleMessage(typeof(AutoDetectUi)));
}
string tooltip = pendingInvites > 0
? string.Format("Vous avez {0} invitation{1} reçue. Ouvrez l\'interface AutoDetect pour y répondre.", pendingInvites, pendingInvites > 1 ? "s" : string.Empty)
: "Ouvrir les outils AutoDetect (invitations et proximité).\n\nLes demandes reçues sont listées dans l\'onglet 'Invitations'.";
UiSharedService.AttachToolTip(tooltip);
}
DrawSelfAnalysisPreview();
}
ImGui.Separator();
}
private void DrawSelfAnalysisPreview()
{
using (ImRaii.PushId("self-analysis"))
{
if (!ImGui.CollapsingHeader("Self Analysis"))
{
return;
}
var summary = _characterAnalyzer.CurrentSummary;
bool isAnalyzing = _characterAnalyzer.IsAnalysisRunning;
if (isAnalyzing)
{
UiSharedService.ColorTextWrapped(
$"Analyse en cours ({_characterAnalyzer.CurrentFile}/{System.Math.Max(_characterAnalyzer.TotalFiles, 1)})...",
ImGuiColors.DalamudYellow);
if (_uiSharedService.IconTextButton(FontAwesomeIcon.StopCircle, "Annuler l'analyse"))
{
_characterAnalyzer.CancelAnalyze();
}
UiSharedService.AttachToolTip("Stopper l'analyse en cours.");
}
else
{
bool recalculate = !summary.HasUncomputedEntries && !summary.IsEmpty;
var label = recalculate ? "Recalculer l'analyse" : "Lancer l'analyse";
var icon = recalculate ? FontAwesomeIcon.Sync : FontAwesomeIcon.PlayCircle;
if (_uiSharedService.IconTextButton(icon, label))
{
_ = _characterAnalyzer.ComputeAnalysis(print: false, recalculate: recalculate);
}
UiSharedService.AttachToolTip(recalculate
? "Recalcule toutes les entrées pour mettre à jour les tailles partagées."
: "Analyse vos fichiers actuels pour estimer le poids partagé.");
}
if (summary.IsEmpty && !isAnalyzing)
{
UiSharedService.ColorTextWrapped("Aucune donnée analysée pour l'instant. Lancez une analyse pour générer cet aperçu.",
ImGuiColors.DalamudGrey2);
return;
}
if (summary.HasUncomputedEntries && !isAnalyzing)
{
UiSharedService.ColorTextWrapped("Certaines entrées n'ont pas encore de taille calculée. Lancez l'analyse pour compléter les données.",
ImGuiColors.DalamudYellow);
}
ImGuiHelpers.ScaledDummy(4f);
UiSharedService.DrawGrouped(() =>
{
if (ImGui.BeginTable("self-analysis-stats", 2, ImGuiTableFlags.SizingStretchProp | ImGuiTableFlags.NoSavedSettings))
{
ImGui.TableSetupColumn("label", ImGuiTableColumnFlags.WidthStretch, 0.55f);
ImGui.TableSetupColumn("value", ImGuiTableColumnFlags.WidthStretch, 0.45f);
DrawSelfAnalysisStatRow("Fichiers moddés", summary.TotalFiles.ToString("N0", CultureInfo.CurrentCulture));
var compressedValue = UiSharedService.ByteToString(summary.TotalCompressedSize);
Vector4? compressedColor = null;
FontAwesomeIcon? compressedIcon = null;
Vector4? compressedIconColor = null;
string? compressedTooltip = null;
if (summary.HasUncomputedEntries)
{
compressedColor = ImGuiColors.DalamudYellow;
compressedTooltip = "Lancez l'analyse pour calculer la taille de téléchargement exacte.";
}
else if (summary.TotalCompressedSize >= SelfAnalysisSizeWarningThreshold)
{
compressedColor = ImGuiColors.DalamudYellow;
compressedTooltip = "Au-delà de 300 MiB, certains joueurs peuvent ne pas voir toutes vos modifications.";
compressedIcon = FontAwesomeIcon.ExclamationTriangle;
compressedIconColor = ImGuiColors.DalamudYellow;
}
DrawSelfAnalysisStatRow("Taille compressée", compressedValue, compressedColor, compressedTooltip, compressedIcon, compressedIconColor);
DrawSelfAnalysisStatRow("Taille extraite", UiSharedService.ByteToString(summary.TotalOriginalSize));
Vector4? trianglesColor = null;
FontAwesomeIcon? trianglesIcon = null;
Vector4? trianglesIconColor = null;
string? trianglesTooltip = null;
if (summary.TotalTriangles >= SelfAnalysisTriangleWarningThreshold)
{
trianglesColor = ImGuiColors.DalamudYellow;
trianglesTooltip = "Plus de 150k triangles peuvent entraîner un auto-pause et impacter les performances.";
trianglesIcon = FontAwesomeIcon.ExclamationTriangle;
trianglesIconColor = ImGuiColors.DalamudYellow;
}
DrawSelfAnalysisStatRow("Triangles moddés", UiSharedService.TrisToString(summary.TotalTriangles), trianglesColor, trianglesTooltip, trianglesIcon, trianglesIconColor);
ImGui.EndTable();
}
}, rounding: 4f, expectedWidth: ImGui.GetContentRegionAvail().X);
string lastAnalysisText;
Vector4 lastAnalysisColor = ImGuiColors.DalamudGrey2;
if (isAnalyzing)
{
lastAnalysisText = "Dernière analyse : en cours...";
lastAnalysisColor = ImGuiColors.DalamudYellow;
}
else if (_characterAnalyzer.LastCompletedAnalysis.HasValue)
{
var localTime = _characterAnalyzer.LastCompletedAnalysis.Value.ToLocalTime();
lastAnalysisText = $"Dernière analyse : {localTime.ToString("g", CultureInfo.CurrentCulture)}";
}
else
{
lastAnalysisText = "Dernière analyse : jamais";
}
ImGuiHelpers.ScaledDummy(2f);
UiSharedService.ColorTextWrapped(lastAnalysisText, lastAnalysisColor);
ImGuiHelpers.ScaledDummy(4f);
if (_uiSharedService.IconTextButton(FontAwesomeIcon.PersonCircleQuestion, "Ouvrir l'analyse détaillée"))
{
Mediator.Publish(new UiToggleMessage(typeof(DataAnalysisUi)));
}
}
}
private static void DrawSelfAnalysisStatRow(string label, string value, Vector4? valueColor = null, string? tooltip = null, FontAwesomeIcon? icon = null, Vector4? iconColor = null)
{
ImGui.TableNextRow();
ImGui.TableNextColumn();
ImGui.TextUnformatted(label);
ImGui.TableNextColumn();
if (icon.HasValue)
{
using (ImRaii.PushFont(UiBuilder.IconFont))
{
if (iconColor.HasValue)
{
using var iconColorPush = ImRaii.PushColor(ImGuiCol.Text, iconColor.Value);
ImGui.TextUnformatted(icon.Value.ToIconString());
}
else
{
ImGui.TextUnformatted(icon.Value.ToIconString());
}
}
ImGui.SameLine(0f, 4f);
}
if (valueColor.HasValue)
{
using var color = ImRaii.PushColor(ImGuiCol.Text, valueColor.Value);
ImGui.TextUnformatted(value);
}
else
{
ImGui.TextUnformatted(value);
}
if (!string.IsNullOrEmpty(tooltip))
{
UiSharedService.AttachToolTip(tooltip);
}
}
private void DrawDefaultSyncButton(FontAwesomeIcon icon, string label, float width, bool currentState,
Action<bool> onToggle, Func<string> tooltipProvider, float spacingOverride = -1f)
{
if (spacingOverride >= 0f)
{
ImGui.SameLine(0, spacingOverride);
}
var colorsPushed = 0;
if (currentState)
{
ImGui.PushStyleColor(ImGuiCol.Text, new Vector4(0.95f, 0.35f, 0.35f, 1f));
colorsPushed++;
}
if (_uiSharedService.IconTextButton(icon, label, width))
{
var newState = !currentState;
onToggle(newState);
}
if (colorsPushed > 0)
{
ImGui.PopStyleColor(colorsPushed);
}
UiSharedService.AttachToolTip(tooltipProvider());
}
private static string DisableStateTooltip(string context, bool disabled)
{
var state = disabled ? "désactivée" : "activée";
return $"Synchronisation {context} par défaut : {state}.\nCliquez pour modifier.";
}
private void DrawAddCharacter() private void DrawAddCharacter()
{ {
ImGui.Dummy(new(10)); ImGui.Dummy(new(10));
@@ -362,17 +703,112 @@ public class CompactUi : WindowMediatorSubscriberBase
: (ImGui.GetWindowContentRegionMax().Y - ImGui.GetWindowContentRegionMin().Y) - TransferPartHeight - ImGui.GetCursorPosY(); : (ImGui.GetWindowContentRegionMax().Y - ImGui.GetWindowContentRegionMin().Y) - TransferPartHeight - ImGui.GetCursorPosY();
var users = GetFilteredUsers().OrderBy(u => u.GetPairSortKey(), StringComparer.Ordinal); var users = GetFilteredUsers().OrderBy(u => u.GetPairSortKey(), StringComparer.Ordinal);
var onlineUsers = users.Where(u => u.UserPair!.OtherPermissions.IsPaired() && (u.IsOnline || u.UserPair!.OwnPermissions.IsPaused())).Select(c => new DrawUserPair("Online" + c.UserData.UID, c, _uidDisplayHandler, _apiController, Mediator, _selectGroupForPairUi, _uiSharedService, _charaDataManager)).ToList();
var visibleUsers = users.Where(u => u.IsVisible).Select(c => new DrawUserPair("Visible" + c.UserData.UID, c, _uidDisplayHandler, _apiController, Mediator, _selectGroupForPairUi, _uiSharedService, _charaDataManager)).ToList();
var offlineUsers = users.Where(u => !u.UserPair!.OtherPermissions.IsPaired() || (!u.IsOnline && !u.UserPair!.OwnPermissions.IsPaused())).Select(c => new DrawUserPair("Offline" + c.UserData.UID, c, _uidDisplayHandler, _apiController, Mediator, _selectGroupForPairUi, _uiSharedService, _charaDataManager)).ToList();
ImGui.BeginChild("list", new Vector2(WindowContentWidth, ySize), border: false); ImGui.BeginChild("list", new Vector2(WindowContentWidth, ySize), border: false);
var pendingCount = _nearbyPending?.Pending.Count ?? 0;
if (pendingCount > 0)
{
UiSharedService.ColorTextWrapped("Invitation AutoDetect en attente. Ouvrez l\'interface AutoDetect pour gérer vos demandes.", ImGuiColors.DalamudYellow);
ImGuiHelpers.ScaledDummy(4);
}
var onlineUsers = users.Where(u => u.UserPair!.OtherPermissions.IsPaired() && (u.IsOnline || u.UserPair!.OwnPermissions.IsPaused())).Select(c => new DrawUserPair("Online" + c.UserData.UID, c, _uidDisplayHandler, _apiController, Mediator, _selectGroupForPairUi, _uiSharedService, _charaDataManager, _serverManager)).ToList();
var visibleUsers = users.Where(u => u.IsVisible).Select(c => new DrawUserPair("Visible" + c.UserData.UID, c, _uidDisplayHandler, _apiController, Mediator, _selectGroupForPairUi, _uiSharedService, _charaDataManager, _serverManager)).ToList();
var offlineUsers = users.Where(u => !u.UserPair!.OtherPermissions.IsPaired() || (!u.IsOnline && !u.UserPair!.OwnPermissions.IsPaused())).Select(c => new DrawUserPair("Offline" + c.UserData.UID, c, _uidDisplayHandler, _apiController, Mediator, _selectGroupForPairUi, _uiSharedService, _charaDataManager, _serverManager)).ToList();
_pairGroupsUi.Draw(visibleUsers, onlineUsers, offlineUsers); _pairGroupsUi.Draw(visibleUsers, onlineUsers, offlineUsers);
if (_configService.Current.EnableAutoDetectDiscovery)
{
using (ImRaii.PushId("group-Nearby"))
{
var icon = _nearbyOpen ? FontAwesomeIcon.CaretSquareDown : FontAwesomeIcon.CaretSquareRight;
_uiSharedService.IconText(icon);
if (ImGui.IsItemClicked(ImGuiMouseButton.Left)) _nearbyOpen = !_nearbyOpen;
ImGui.SameLine();
var onUmbra = _nearbyEntries?.Count(e => e.IsMatch && e.AcceptPairRequests && !string.IsNullOrEmpty(e.Token) && !IsAlreadyPairedQuickMenu(e)) ?? 0;
ImGui.TextUnformatted($"Nearby ({onUmbra})");
if (ImGui.IsItemClicked(ImGuiMouseButton.Left)) _nearbyOpen = !_nearbyOpen;
if (_nearbyOpen)
{
ImGui.Indent();
var nearby = _nearbyEntries == null
? new List<Services.Mediator.NearbyEntry>()
: _nearbyEntries.Where(e => e.IsMatch && e.AcceptPairRequests && !string.IsNullOrEmpty(e.Token) && !IsAlreadyPairedQuickMenu(e))
.OrderBy(e => e.Distance)
.ToList();
if (nearby.Count == 0)
{
UiSharedService.ColorTextWrapped("Aucun nouveau joueur detecté.", ImGuiColors.DalamudGrey3);
}
else
{
foreach (var e in nearby)
{
if (!e.AcceptPairRequests || string.IsNullOrEmpty(e.Token))
continue;
var name = e.DisplayName ?? e.Name;
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted(name);
var right = ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth();
ImGui.SameLine();
var statusButtonSize = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.UserPlus);
ImGui.SetCursorPosX(right - statusButtonSize.X);
if (!e.AcceptPairRequests)
{
_uiSharedService.IconText(FontAwesomeIcon.Ban, ImGuiColors.DalamudGrey3);
UiSharedService.AttachToolTip("Les demandes sont désactivées pour ce joueur");
}
else if (!string.IsNullOrEmpty(e.Token))
{
using (ImRaii.PushId(e.Token ?? e.Uid ?? e.Name ?? string.Empty))
{
if (_uiSharedService.IconButton(FontAwesomeIcon.UserPlus))
{
_ = _autoDetectRequestService.SendRequestAsync(e.Token!, e.Uid, e.DisplayName);
}
}
UiSharedService.AttachToolTip("Envoyer une invitation d'apparaige");
}
else
{
_uiSharedService.IconText(FontAwesomeIcon.QuestionCircle, ImGuiColors.DalamudGrey3);
UiSharedService.AttachToolTip("Impossible d'inviter ce joueur");
}
}
}
ImGui.Unindent();
ImGui.Separator();
}
}
}
ImGui.EndChild(); ImGui.EndChild();
} }
private bool IsAlreadyPairedQuickMenu(Services.Mediator.NearbyEntry entry)
{
try
{
if (!string.IsNullOrEmpty(entry.Uid))
{
if (_pairManager.DirectPairs.Any(p => string.Equals(p.UserData.UID, entry.Uid, StringComparison.Ordinal)))
return true;
}
var key = (entry.DisplayName ?? entry.Name) ?? string.Empty;
if (string.IsNullOrEmpty(key)) return false;
return _pairManager.DirectPairs.Any(p => string.Equals(p.UserData.AliasOrUID, key, StringComparison.OrdinalIgnoreCase));
}
catch
{
return false;
}
}
private void DrawServerStatus() private void DrawServerStatus()
{ {
var buttonSize = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Link); var buttonSize = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Link);
@@ -387,7 +823,7 @@ public class CompactUi : WindowMediatorSubscriberBase
{ {
ImGui.SetCursorPosX((ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth()) / 2 - (userSize.X + textSize.X) / 2 - ImGui.GetStyle().ItemSpacing.X / 2); ImGui.SetCursorPosX((ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth()) / 2 - (userSize.X + textSize.X) / 2 - ImGui.GetStyle().ItemSpacing.X / 2);
if (!printShard) ImGui.AlignTextToFramePadding(); if (!printShard) ImGui.AlignTextToFramePadding();
ImGui.TextColored(ImGuiColors.ParsedGreen, userCount); ImGui.TextColored(UiSharedService.AccentColor, userCount);
ImGui.SameLine(); ImGui.SameLine();
if (!printShard) ImGui.AlignTextToFramePadding(); if (!printShard) ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("Users Online"); ImGui.TextUnformatted("Users Online");
@@ -395,7 +831,7 @@ public class CompactUi : WindowMediatorSubscriberBase
else else
{ {
ImGui.AlignTextToFramePadding(); ImGui.AlignTextToFramePadding();
ImGui.TextColored(ImGuiColors.DalamudRed, "Not connected to any server"); ImGui.TextColored(UiSharedService.AccentColor, "Not connected to any server");
} }
if (printShard) if (printShard)
@@ -410,8 +846,9 @@ public class CompactUi : WindowMediatorSubscriberBase
{ {
ImGui.SetCursorPosY(ImGui.GetCursorPosY() - ((userSize.Y + textSize.Y) / 2 + shardTextSize.Y) / 2 - ImGui.GetStyle().ItemSpacing.Y + buttonSize.Y / 2); ImGui.SetCursorPosY(ImGui.GetCursorPosY() - ((userSize.Y + textSize.Y) / 2 + shardTextSize.Y) / 2 - ImGui.GetStyle().ItemSpacing.Y + buttonSize.Y / 2);
} }
var color = UiSharedService.GetBoolColor(!_serverManager.CurrentServer!.FullPause); var isLinked = !_serverManager.CurrentServer!.FullPause;
var connectedIcon = !_serverManager.CurrentServer.FullPause ? FontAwesomeIcon.Link : FontAwesomeIcon.Unlink; var color = isLinked ? new Vector4(0.63f, 0.25f, 1f, 1f) : UiSharedService.GetBoolColor(isLinked);
var connectedIcon = isLinked ? FontAwesomeIcon.Link : FontAwesomeIcon.Unlink;
if (_apiController.ServerState is ServerState.Connected) if (_apiController.ServerState is ServerState.Connected)
{ {
@@ -486,22 +923,19 @@ public class CompactUi : WindowMediatorSubscriberBase
ImGui.SameLine(WindowContentWidth - textSize.X); ImGui.SameLine(WindowContentWidth - textSize.X);
ImGui.TextUnformatted(downloadText); ImGui.TextUnformatted(downloadText);
} }
var spacing = ImGui.GetStyle().ItemSpacing.X;
var bottomButtonWidth = (WindowContentWidth - ImGui.GetStyle().ItemSpacing.X) / 2; var bottomButtonWidth = (WindowContentWidth - spacing) / 2f;
if (_uiSharedService.IconTextButton(FontAwesomeIcon.PersonCircleQuestion, "Character Analysis", bottomButtonWidth)) if (_uiSharedService.IconTextButton(FontAwesomeIcon.PersonCircleQuestion, "Character Analysis", bottomButtonWidth))
{ {
Mediator.Publish(new UiToggleMessage(typeof(DataAnalysisUi))); Mediator.Publish(new UiToggleMessage(typeof(DataAnalysisUi)));
} }
ImGui.SameLine(); ImGui.SameLine();
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Running, "Character Data Hub", bottomButtonWidth)) if (_uiSharedService.IconTextButton(FontAwesomeIcon.Running, "Character Data Hub", bottomButtonWidth))
{ {
Mediator.Publish(new UiToggleMessage(typeof(CharaDataHubUi))); Mediator.Publish(new UiToggleMessage(typeof(CharaDataHubUi)));
} }
ImGuiHelpers.ScaledDummy(2);
ImGui.SameLine();
} }
private void DrawUIDHeader() private void DrawUIDHeader()
@@ -527,7 +961,7 @@ public class CompactUi : WindowMediatorSubscriberBase
} }
UiSharedService.AttachToolTip("Open the UmbraSync Settings"); UiSharedService.AttachToolTip("Open the UmbraSync Settings");
ImGui.SameLine(); //Important to draw the uidText consistently ImGui.SameLine();
ImGui.SetCursorPos(originalPos); ImGui.SetCursorPos(originalPos);
if (_apiController.ServerState is ServerState.Connected) if (_apiController.ServerState is ServerState.Connected)
@@ -549,8 +983,8 @@ public class CompactUi : WindowMediatorSubscriberBase
ImGui.TextColored(GetUidColor(), uidText); ImGui.TextColored(GetUidColor(), uidText);
if (_apiController.ServerState is not ServerState.Connected) if (_apiController.ServerState is not ServerState.Connected)
{
UiSharedService.ColorTextWrapped(GetServerError(), GetUidColor()); UiSharedService.ColorTextWrapped(GetServerError(), GetUidColor());
{
if (_apiController.ServerState is ServerState.NoSecretKey) if (_apiController.ServerState is ServerState.NoSecretKey)
{ {
DrawAddCharacter(); DrawAddCharacter();
@@ -594,17 +1028,17 @@ public class CompactUi : WindowMediatorSubscriberBase
return _apiController.ServerState switch return _apiController.ServerState switch
{ {
ServerState.Connecting => ImGuiColors.DalamudYellow, ServerState.Connecting => ImGuiColors.DalamudYellow,
ServerState.Reconnecting => ImGuiColors.DalamudRed, ServerState.Reconnecting => UiSharedService.AccentColor,
ServerState.Connected => UiSharedService.AccentColor, ServerState.Connected => UiSharedService.AccentColor,
ServerState.Disconnected => ImGuiColors.DalamudYellow, ServerState.Disconnected => ImGuiColors.DalamudYellow,
ServerState.Disconnecting => ImGuiColors.DalamudYellow, ServerState.Disconnecting => ImGuiColors.DalamudYellow,
ServerState.Unauthorized => ImGuiColors.DalamudRed, ServerState.Unauthorized => UiSharedService.AccentColor,
ServerState.VersionMisMatch => ImGuiColors.DalamudRed, ServerState.VersionMisMatch => UiSharedService.AccentColor,
ServerState.Offline => ImGuiColors.DalamudRed, ServerState.Offline => UiSharedService.AccentColor,
ServerState.RateLimited => ImGuiColors.DalamudYellow, ServerState.RateLimited => ImGuiColors.DalamudYellow,
ServerState.NoSecretKey => ImGuiColors.DalamudYellow, ServerState.NoSecretKey => ImGuiColors.DalamudYellow,
ServerState.MultiChara => ImGuiColors.DalamudYellow, ServerState.MultiChara => ImGuiColors.DalamudYellow,
_ => ImGuiColors.DalamudRed _ => UiSharedService.AccentColor
}; };
} }

View File

@@ -1,14 +1,21 @@
using Dalamud.Bindings.ImGui; using System;
using System.Numerics;
using System.Text;
using System.Threading.Tasks;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface; using Dalamud.Interface;
using Dalamud.Interface.Colors; using Dalamud.Interface.Colors;
using Dalamud.Interface.Utility; using Dalamud.Interface.Utility;
using MareSynchronos.API.Data;
using MareSynchronos.API.Data.Enum; using MareSynchronos.API.Data.Enum;
using MareSynchronos.API.Data.Extensions; using MareSynchronos.API.Data.Extensions;
using MareSynchronos.API.Dto.Group; using MareSynchronos.API.Dto.Group;
using MareSynchronos.API.Dto.User;
using MareSynchronos.PlayerData.Pairs; using MareSynchronos.PlayerData.Pairs;
using MareSynchronos.Services; using MareSynchronos.Services;
using MareSynchronos.Services.AutoDetect;
using MareSynchronos.Services.Mediator; using MareSynchronos.Services.Mediator;
using MareSynchronos.Services.ServerConfiguration;
using MareSynchronos.UI.Handlers; using MareSynchronos.UI.Handlers;
using MareSynchronos.WebAPI; using MareSynchronos.WebAPI;
@@ -20,16 +27,22 @@ public class DrawGroupPair : DrawPairBase
private readonly GroupPairFullInfoDto _fullInfoDto; private readonly GroupPairFullInfoDto _fullInfoDto;
private readonly GroupFullInfoDto _group; private readonly GroupFullInfoDto _group;
private readonly CharaDataManager _charaDataManager; private readonly CharaDataManager _charaDataManager;
private readonly AutoDetectRequestService _autoDetectRequestService;
private readonly ServerConfigurationManager _serverConfigurationManager;
private const string ManualPairInvitePrefix = "[UmbraPairInvite|";
public DrawGroupPair(string id, Pair entry, ApiController apiController, public DrawGroupPair(string id, Pair entry, ApiController apiController,
MareMediator mareMediator, GroupFullInfoDto group, GroupPairFullInfoDto fullInfoDto, MareMediator mareMediator, GroupFullInfoDto group, GroupPairFullInfoDto fullInfoDto,
UidDisplayHandler handler, UiSharedService uiSharedService, CharaDataManager charaDataManager) UidDisplayHandler handler, UiSharedService uiSharedService, CharaDataManager charaDataManager,
AutoDetectRequestService autoDetectRequestService, ServerConfigurationManager serverConfigurationManager)
: base(id, entry, apiController, handler, uiSharedService) : base(id, entry, apiController, handler, uiSharedService)
{ {
_group = group; _group = group;
_fullInfoDto = fullInfoDto; _fullInfoDto = fullInfoDto;
_mediator = mareMediator; _mediator = mareMediator;
_charaDataManager = charaDataManager; _charaDataManager = charaDataManager;
_autoDetectRequestService = autoDetectRequestService;
_serverConfigurationManager = serverConfigurationManager;
} }
protected override void DrawLeftSide(float textPosY, float originalY) protected override void DrawLeftSide(float textPosY, float originalY)
@@ -38,15 +51,15 @@ public class DrawGroupPair : DrawPairBase
var entryIsMod = _fullInfoDto.GroupPairStatusInfo.IsModerator(); var entryIsMod = _fullInfoDto.GroupPairStatusInfo.IsModerator();
var entryIsOwner = string.Equals(_pair.UserData.UID, _group.OwnerUID, StringComparison.Ordinal); var entryIsOwner = string.Equals(_pair.UserData.UID, _group.OwnerUID, StringComparison.Ordinal);
var entryIsPinned = _fullInfoDto.GroupPairStatusInfo.IsPinned(); var entryIsPinned = _fullInfoDto.GroupPairStatusInfo.IsPinned();
var presenceIcon = _pair.IsVisible ? FontAwesomeIcon.Eye : (_pair.IsOnline ? FontAwesomeIcon.Link : FontAwesomeIcon.Unlink); var presenceIcon = _pair.IsVisible ? FontAwesomeIcon.Eye : FontAwesomeIcon.CloudMoon;
var presenceColor = (_pair.IsOnline || _pair.IsVisible) ? ImGuiColors.ParsedGreen : ImGuiColors.DalamudRed; var presenceColor = (_pair.IsOnline || _pair.IsVisible) ? new Vector4(0.63f, 0.25f, 1f, 1f) : ImGuiColors.DalamudGrey;
var presenceText = entryUID + " is offline"; var presenceText = entryUID + " is offline";
ImGui.SetCursorPosY(textPosY); ImGui.SetCursorPosY(textPosY);
bool drewPrefixIcon = false;
if (_pair.IsPaused) if (_pair.IsPaused)
{ {
presenceIcon = FontAwesomeIcon.Question;
presenceColor = ImGuiColors.DalamudGrey;
presenceText = entryUID + " online status is unknown (paused)"; presenceText = entryUID + " online status is unknown (paused)";
ImGui.PushFont(UiBuilder.IconFont); ImGui.PushFont(UiBuilder.IconFont);
@@ -54,24 +67,32 @@ public class DrawGroupPair : DrawPairBase
ImGui.PopFont(); ImGui.PopFont();
UiSharedService.AttachToolTip("Pairing status with " + entryUID + " is paused"); UiSharedService.AttachToolTip("Pairing status with " + entryUID + " is paused");
drewPrefixIcon = true;
} }
else else
{
bool individuallyPaired = _pair.UserPair != null;
var violet = new Vector4(0.63f, 0.25f, 1f, 1f);
if (individuallyPaired && (_pair.IsOnline || _pair.IsVisible))
{ {
ImGui.PushFont(UiBuilder.IconFont); ImGui.PushFont(UiBuilder.IconFont);
UiSharedService.ColorText(FontAwesomeIcon.Check.ToIconString(), ImGuiColors.ParsedGreen); UiSharedService.ColorText(FontAwesomeIcon.Moon.ToIconString(), violet);
ImGui.PopFont(); ImGui.PopFont();
UiSharedService.AttachToolTip("You are individually paired with " + entryUID);
UiSharedService.AttachToolTip("You are paired with " + entryUID); drewPrefixIcon = true;
} }
}
if (_pair.IsOnline && !_pair.IsVisible) presenceText = entryUID + " is online"; if (drewPrefixIcon)
else if (_pair.IsOnline && _pair.IsVisible) presenceText = entryUID + " is visible: " + _pair.PlayerName + Environment.NewLine + "Click to target this player";
ImGui.SameLine(); ImGui.SameLine();
ImGui.SetCursorPosY(textPosY); ImGui.SetCursorPosY(textPosY);
ImGui.PushFont(UiBuilder.IconFont); ImGui.PushFont(UiBuilder.IconFont);
UiSharedService.ColorText(presenceIcon.ToIconString(), presenceColor); UiSharedService.ColorText(presenceIcon.ToIconString(), presenceColor);
ImGui.PopFont(); ImGui.PopFont();
if (_pair.IsOnline && !_pair.IsVisible) presenceText = entryUID + " is online";
else if (_pair.IsOnline && _pair.IsVisible) presenceText = entryUID + " is visible: " + _pair.PlayerName + Environment.NewLine + "Click to target this player";
if (_pair.IsVisible) if (_pair.IsVisible)
{ {
if (ImGui.IsItemClicked()) if (ImGui.IsItemClicked())
@@ -94,6 +115,7 @@ public class DrawGroupPair : DrawPairBase
} }
} }
} }
UiSharedService.AttachToolTip(presenceText); UiSharedService.AttachToolTip(presenceText);
if (entryIsOwner) if (entryIsOwner)
@@ -143,8 +165,9 @@ public class DrawGroupPair : DrawPairBase
bool showShared = _charaDataManager.SharedWithYouData.TryGetValue(_pair.UserData, out var sharedData); bool showShared = _charaDataManager.SharedWithYouData.TryGetValue(_pair.UserData, out var sharedData);
bool showInfo = (individualAnimDisabled || individualSoundsDisabled || animDisabled || soundsDisabled); bool showInfo = (individualAnimDisabled || individualSoundsDisabled || animDisabled || soundsDisabled);
bool showPlus = _pair.UserPair == null; bool showPlus = _pair.UserPair == null && _pair.IsOnline;
bool showBars = (userIsOwner || (userIsModerator && !entryIsMod && !entryIsOwner)) || !_pair.IsPaused; bool showBars = (userIsOwner || (userIsModerator && !entryIsMod && !entryIsOwner)) || !_pair.IsPaused;
bool showPause = true;
var spacing = ImGui.GetStyle().ItemSpacing.X; var spacing = ImGui.GetStyle().ItemSpacing.X;
var permIcon = (individualAnimDisabled || individualSoundsDisabled || individualVFXDisabled) ? FontAwesomeIcon.ExclamationTriangle var permIcon = (individualAnimDisabled || individualSoundsDisabled || individualVFXDisabled) ? FontAwesomeIcon.ExclamationTriangle
@@ -152,12 +175,15 @@ public class DrawGroupPair : DrawPairBase
var runningIconWidth = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Running).X; var runningIconWidth = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Running).X;
var infoIconWidth = UiSharedService.GetIconSize(permIcon).X; var infoIconWidth = UiSharedService.GetIconSize(permIcon).X;
var plusButtonWidth = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Plus).X; var plusButtonWidth = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Plus).X;
var pauseIcon = _fullInfoDto.GroupUserPermissions.IsPaused() ? FontAwesomeIcon.Play : FontAwesomeIcon.Pause;
var pauseButtonWidth = _uiSharedService.GetIconButtonSize(pauseIcon).X;
var barButtonWidth = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Bars).X; var barButtonWidth = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Bars).X;
var pos = ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth() + spacing var pos = ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth() + spacing
- (showShared ? (runningIconWidth + spacing) : 0) - (showShared ? (runningIconWidth + spacing) : 0)
- (showInfo ? (infoIconWidth + spacing) : 0) - (showInfo ? (infoIconWidth + spacing) : 0)
- (showPlus ? (plusButtonWidth + spacing) : 0) - (showPlus ? (plusButtonWidth + spacing) : 0)
- (showPause ? (pauseButtonWidth + spacing) : 0)
- (showBars ? (barButtonWidth + spacing) : 0); - (showBars ? (barButtonWidth + spacing) : 0);
ImGui.SameLine(pos); ImGui.SameLine(pos);
@@ -191,7 +217,7 @@ public class DrawGroupPair : DrawPairBase
if (individualSoundsDisabled) if (individualSoundsDisabled)
{ {
var userSoundsText = "Sound sync disabled with " + _pair.UserData.AliasOrUID; var userSoundsText = "Sound sync disabled with " + _pair.UserData.AliasOrUID;
_uiSharedService.IconText(FontAwesomeIcon.VolumeOff); _uiSharedService.IconText(FontAwesomeIcon.VolumeMute);
ImGui.SameLine(40 * ImGuiHelpers.GlobalScale); ImGui.SameLine(40 * ImGuiHelpers.GlobalScale);
ImGui.TextUnformatted(userSoundsText); ImGui.TextUnformatted(userSoundsText);
ImGui.NewLine(); ImGui.NewLine();
@@ -202,7 +228,7 @@ public class DrawGroupPair : DrawPairBase
if (individualAnimDisabled) if (individualAnimDisabled)
{ {
var userAnimText = "Animation sync disabled with " + _pair.UserData.AliasOrUID; var userAnimText = "Animation sync disabled with " + _pair.UserData.AliasOrUID;
_uiSharedService.IconText(FontAwesomeIcon.Stop); _uiSharedService.IconText(FontAwesomeIcon.WindowClose);
ImGui.SameLine(40 * ImGuiHelpers.GlobalScale); ImGui.SameLine(40 * ImGuiHelpers.GlobalScale);
ImGui.TextUnformatted(userAnimText); ImGui.TextUnformatted(userAnimText);
ImGui.NewLine(); ImGui.NewLine();
@@ -213,7 +239,7 @@ public class DrawGroupPair : DrawPairBase
if (individualVFXDisabled) if (individualVFXDisabled)
{ {
var userVFXText = "VFX sync disabled with " + _pair.UserData.AliasOrUID; var userVFXText = "VFX sync disabled with " + _pair.UserData.AliasOrUID;
_uiSharedService.IconText(FontAwesomeIcon.Circle); _uiSharedService.IconText(FontAwesomeIcon.TimesCircle);
ImGui.SameLine(40 * ImGuiHelpers.GlobalScale); ImGui.SameLine(40 * ImGuiHelpers.GlobalScale);
ImGui.TextUnformatted(userVFXText); ImGui.TextUnformatted(userVFXText);
ImGui.NewLine(); ImGui.NewLine();
@@ -238,7 +264,7 @@ public class DrawGroupPair : DrawPairBase
if (soundsDisabled) if (soundsDisabled)
{ {
var userSoundsText = "Sound sync disabled by " + _pair.UserData.AliasOrUID; var userSoundsText = "Sound sync disabled by " + _pair.UserData.AliasOrUID;
_uiSharedService.IconText(FontAwesomeIcon.VolumeOff); _uiSharedService.IconText(FontAwesomeIcon.VolumeMute);
ImGui.SameLine(40 * ImGuiHelpers.GlobalScale); ImGui.SameLine(40 * ImGuiHelpers.GlobalScale);
ImGui.TextUnformatted(userSoundsText); ImGui.TextUnformatted(userSoundsText);
} }
@@ -246,7 +272,7 @@ public class DrawGroupPair : DrawPairBase
if (animDisabled) if (animDisabled)
{ {
var userAnimText = "Animation sync disabled by " + _pair.UserData.AliasOrUID; var userAnimText = "Animation sync disabled by " + _pair.UserData.AliasOrUID;
_uiSharedService.IconText(FontAwesomeIcon.Stop); _uiSharedService.IconText(FontAwesomeIcon.WindowClose);
ImGui.SameLine(40 * ImGuiHelpers.GlobalScale); ImGui.SameLine(40 * ImGuiHelpers.GlobalScale);
ImGui.TextUnformatted(userAnimText); ImGui.TextUnformatted(userAnimText);
} }
@@ -254,7 +280,7 @@ public class DrawGroupPair : DrawPairBase
if (vfxDisabled) if (vfxDisabled)
{ {
var userVFXText = "VFX sync disabled by " + _pair.UserData.AliasOrUID; var userVFXText = "VFX sync disabled by " + _pair.UserData.AliasOrUID;
_uiSharedService.IconText(FontAwesomeIcon.Circle); _uiSharedService.IconText(FontAwesomeIcon.TimesCircle);
ImGui.SameLine(40 * ImGuiHelpers.GlobalScale); ImGui.SameLine(40 * ImGuiHelpers.GlobalScale);
ImGui.TextUnformatted(userVFXText); ImGui.TextUnformatted(userVFXText);
} }
@@ -270,9 +296,28 @@ public class DrawGroupPair : DrawPairBase
if (_uiSharedService.IconButton(FontAwesomeIcon.Plus)) if (_uiSharedService.IconButton(FontAwesomeIcon.Plus))
{ {
_ = _apiController.UserAddPair(new UserDto(new(_pair.UserData.UID))); var targetUid = _pair.UserData.UID;
if (!string.IsNullOrEmpty(targetUid))
{
_ = SendGroupPairInviteAsync(targetUid, entryUID);
} }
UiSharedService.AttachToolTip("Pair with " + entryUID + " individually"); }
UiSharedService.AttachToolTip(AppendSeenInfo("Send pairing invite to " + entryUID));
ImGui.SameLine();
}
if (showPause)
{
ImGui.SetCursorPosY(originalY);
if (_uiSharedService.IconButton(pauseIcon))
{
var newPermissions = _fullInfoDto.GroupUserPermissions ^ GroupUserPermissions.Paused;
_fullInfoDto.GroupUserPermissions = newPermissions;
_ = _apiController.GroupChangeIndividualPermissionState(new GroupPairUserPermissionDto(_group.Group, _pair.UserData, newPermissions));
}
UiSharedService.AttachToolTip(AppendSeenInfo((_fullInfoDto.GroupUserPermissions.IsPaused() ? "Resume" : "Pause") + " syncing with " + entryUID));
ImGui.SameLine(); ImGui.SameLine();
} }
@@ -373,4 +418,74 @@ public class DrawGroupPair : DrawPairBase
return pos - spacing; return pos - spacing;
} }
private string AppendSeenInfo(string tooltip)
{
if (_pair.IsVisible) return tooltip;
var lastSeen = _serverConfigurationManager.GetNameForUid(_pair.UserData.UID);
if (string.IsNullOrWhiteSpace(lastSeen)) return tooltip;
return tooltip + " (Vu sous : " + lastSeen + ")";
}
private async Task SendGroupPairInviteAsync(string targetUid, string displayName)
{
try
{
var ok = await _autoDetectRequestService.SendDirectUidRequestAsync(targetUid, displayName).ConfigureAwait(false);
if (!ok) return;
await SendManualInviteSignalAsync(targetUid, displayName).ConfigureAwait(false);
}
catch
{
// errors are logged within the request service; ignore here
}
}
private async Task SendManualInviteSignalAsync(string targetUid, string displayName)
{
if (string.IsNullOrEmpty(_apiController.UID)) return;
var senderAliasRaw = string.IsNullOrEmpty(_apiController.DisplayName) ? _apiController.UID : _apiController.DisplayName;
var senderAlias = EncodeInviteField(senderAliasRaw);
var targetDisplay = EncodeInviteField(displayName);
var inviteId = Guid.NewGuid().ToString("N");
var payloadText = new StringBuilder()
.Append(ManualPairInvitePrefix)
.Append(_apiController.UID)
.Append('|')
.Append(senderAlias)
.Append('|')
.Append(targetUid)
.Append('|')
.Append(targetDisplay)
.Append('|')
.Append(inviteId)
.Append(']')
.ToString();
var payload = new SeStringBuilder().AddText(payloadText).Build().Encode();
var chatMessage = new ChatMessage
{
SenderName = senderAlias,
PayloadContent = payload
};
try
{
await _apiController.GroupChatSendMsg(new GroupDto(_group.Group), chatMessage).ConfigureAwait(false);
}
catch
{
// ignore - invite remains tracked locally even if group chat signal fails
}
}
private static string EncodeInviteField(string value)
{
var bytes = Encoding.UTF8.GetBytes(value ?? string.Empty);
return Convert.ToBase64String(bytes);
}
} }

View File

@@ -11,18 +11,22 @@ using MareSynchronos.Services.Mediator;
using MareSynchronos.UI.Handlers; using MareSynchronos.UI.Handlers;
using MareSynchronos.WebAPI; using MareSynchronos.WebAPI;
using System.Numerics; using System.Numerics;
using MareSynchronos.Services.ServerConfiguration;
namespace MareSynchronos.UI.Components; namespace MareSynchronos.UI.Components;
public class DrawUserPair : DrawPairBase public class DrawUserPair : DrawPairBase
{ {
private static readonly Vector4 Violet = new(0.63f, 0.25f, 1f, 1f);
protected readonly MareMediator _mediator; protected readonly MareMediator _mediator;
private readonly SelectGroupForPairUi _selectGroupForPairUi; private readonly SelectGroupForPairUi _selectGroupForPairUi;
private readonly CharaDataManager _charaDataManager; private readonly CharaDataManager _charaDataManager;
private readonly ServerConfigurationManager _serverConfigurationManager;
public DrawUserPair(string id, Pair entry, UidDisplayHandler displayHandler, ApiController apiController, public DrawUserPair(string id, Pair entry, UidDisplayHandler displayHandler, ApiController apiController,
MareMediator mareMediator, SelectGroupForPairUi selectGroupForPairUi, MareMediator mareMediator, SelectGroupForPairUi selectGroupForPairUi,
UiSharedService uiSharedService, CharaDataManager charaDataManager) UiSharedService uiSharedService, CharaDataManager charaDataManager,
ServerConfigurationManager serverConfigurationManager)
: base(id, entry, apiController, displayHandler, uiSharedService) : base(id, entry, apiController, displayHandler, uiSharedService)
{ {
if (_pair.UserPair == null) throw new ArgumentException("Pair must be UserPair", nameof(entry)); if (_pair.UserPair == null) throw new ArgumentException("Pair must be UserPair", nameof(entry));
@@ -30,6 +34,7 @@ public class DrawUserPair : DrawPairBase
_selectGroupForPairUi = selectGroupForPairUi; _selectGroupForPairUi = selectGroupForPairUi;
_mediator = mareMediator; _mediator = mareMediator;
_charaDataManager = charaDataManager; _charaDataManager = charaDataManager;
_serverConfigurationManager = serverConfigurationManager;
} }
public bool IsOnline => _pair.IsOnline; public bool IsOnline => _pair.IsOnline;
@@ -38,39 +43,40 @@ public class DrawUserPair : DrawPairBase
protected override void DrawLeftSide(float textPosY, float originalY) protected override void DrawLeftSide(float textPosY, float originalY)
{ {
FontAwesomeIcon connectionIcon; var online = _pair.IsOnline;
Vector4 connectionColor; var offlineGrey = ImGuiColors.DalamudGrey3;
string connectionText;
if (!(_pair.UserPair!.OwnPermissions.IsPaired() && _pair.UserPair!.OtherPermissions.IsPaired()))
{
connectionIcon = FontAwesomeIcon.ArrowUp;
connectionText = _pair.UserData.AliasOrUID + " has not added you back";
connectionColor = ImGuiColors.DalamudRed;
}
else if (_pair.UserPair!.OwnPermissions.IsPaused() || _pair.UserPair!.OtherPermissions.IsPaused())
{
connectionIcon = FontAwesomeIcon.PauseCircle;
connectionText = "Pairing status with " + _pair.UserData.AliasOrUID + " is paused";
connectionColor = ImGuiColors.DalamudYellow;
}
else
{
connectionIcon = FontAwesomeIcon.Check;
connectionText = "You are paired with " + _pair.UserData.AliasOrUID;
connectionColor = ImGuiColors.ParsedGreen;
}
ImGui.SetCursorPosY(textPosY); ImGui.SetCursorPosY(textPosY);
ImGui.PushFont(UiBuilder.IconFont); ImGui.PushFont(UiBuilder.IconFont);
UiSharedService.ColorText(connectionIcon.ToIconString(), connectionColor); UiSharedService.ColorText(FontAwesomeIcon.Moon.ToIconString(), online ? Violet : offlineGrey);
ImGui.PopFont(); ImGui.PopFont();
UiSharedService.AttachToolTip(connectionText); UiSharedService.AttachToolTip(online
? "User is online"
: "User is offline");
if (!(_pair.UserPair!.OwnPermissions.IsPaired() && _pair.UserPair!.OtherPermissions.IsPaired()))
{
ImGui.SameLine();
ImGui.SetCursorPosY(textPosY);
ImGui.PushFont(UiBuilder.IconFont);
UiSharedService.ColorText(FontAwesomeIcon.ArrowUp.ToIconString(), UiSharedService.AccentColor);
ImGui.PopFont();
UiSharedService.AttachToolTip(_pair.UserData.AliasOrUID + " has not added you back");
}
else if (_pair.UserPair!.OwnPermissions.IsPaused() || _pair.UserPair!.OtherPermissions.IsPaused())
{
ImGui.SameLine();
ImGui.SetCursorPosY(textPosY);
ImGui.PushFont(UiBuilder.IconFont);
UiSharedService.ColorText(FontAwesomeIcon.PauseCircle.ToIconString(), ImGuiColors.DalamudYellow);
ImGui.PopFont();
UiSharedService.AttachToolTip("Pairing with " + _pair.UserData.AliasOrUID + " is paused");
}
if (_pair is { IsOnline: true, IsVisible: true }) if (_pair is { IsOnline: true, IsVisible: true })
{ {
ImGui.SameLine(); ImGui.SameLine();
ImGui.SetCursorPosY(textPosY); ImGui.SetCursorPosY(textPosY);
ImGui.PushFont(UiBuilder.IconFont); ImGui.PushFont(UiBuilder.IconFont);
UiSharedService.ColorText(FontAwesomeIcon.Eye.ToIconString(), ImGuiColors.ParsedGreen); UiSharedService.ColorText(FontAwesomeIcon.Eye.ToIconString(), Violet);
if (ImGui.IsItemClicked()) if (ImGui.IsItemClicked())
{ {
_mediator.Publish(new TargetPairMessage(_pair)); _mediator.Publish(new TargetPairMessage(_pair));
@@ -133,16 +139,14 @@ public class DrawUserPair : DrawPairBase
perm.SetPaused(!perm.IsPaused()); perm.SetPaused(!perm.IsPaused());
_ = _apiController.UserSetPairPermissions(new(_pair.UserData, perm)); _ = _apiController.UserSetPairPermissions(new(_pair.UserData, perm));
} }
UiSharedService.AttachToolTip(!_pair.UserPair!.OwnPermissions.IsPaused() UiSharedService.AttachToolTip(AppendSeenInfo(!_pair.UserPair!.OwnPermissions.IsPaused()
? "Pause pairing with " + entryUID ? "Pause pairing with " + entryUID
: "Resume pairing with " + entryUID); : "Resume pairing with " + entryUID));
var individualSoundsDisabled = (_pair.UserPair?.OwnPermissions.IsDisableSounds() ?? false) || (_pair.UserPair?.OtherPermissions.IsDisableSounds() ?? false); var individualSoundsDisabled = (_pair.UserPair?.OwnPermissions.IsDisableSounds() ?? false) || (_pair.UserPair?.OtherPermissions.IsDisableSounds() ?? false);
var individualAnimDisabled = (_pair.UserPair?.OwnPermissions.IsDisableAnimations() ?? false) || (_pair.UserPair?.OtherPermissions.IsDisableAnimations() ?? false); var individualAnimDisabled = (_pair.UserPair?.OwnPermissions.IsDisableAnimations() ?? false) || (_pair.UserPair?.OtherPermissions.IsDisableAnimations() ?? false);
var individualVFXDisabled = (_pair.UserPair?.OwnPermissions.IsDisableVFX() ?? false) || (_pair.UserPair?.OtherPermissions.IsDisableVFX() ?? false); var individualVFXDisabled = (_pair.UserPair?.OwnPermissions.IsDisableVFX() ?? false) || (_pair.UserPair?.OtherPermissions.IsDisableVFX() ?? false);
// Icon for individually applied permissions
if (individualSoundsDisabled || individualAnimDisabled || individualVFXDisabled) if (individualSoundsDisabled || individualAnimDisabled || individualVFXDisabled)
{ {
var icon = FontAwesomeIcon.ExclamationTriangle; var icon = FontAwesomeIcon.ExclamationTriangle;
@@ -163,7 +167,7 @@ public class DrawUserPair : DrawPairBase
if (individualSoundsDisabled) if (individualSoundsDisabled)
{ {
var userSoundsText = "Sound sync disabled with " + _pair.UserData.AliasOrUID; var userSoundsText = "Sound sync disabled with " + _pair.UserData.AliasOrUID;
_uiSharedService.IconText(FontAwesomeIcon.VolumeOff); _uiSharedService.IconText(FontAwesomeIcon.VolumeMute);
ImGui.SameLine(40 * ImGuiHelpers.GlobalScale); ImGui.SameLine(40 * ImGuiHelpers.GlobalScale);
ImGui.TextUnformatted(userSoundsText); ImGui.TextUnformatted(userSoundsText);
ImGui.NewLine(); ImGui.NewLine();
@@ -174,7 +178,7 @@ public class DrawUserPair : DrawPairBase
if (individualAnimDisabled) if (individualAnimDisabled)
{ {
var userAnimText = "Animation sync disabled with " + _pair.UserData.AliasOrUID; var userAnimText = "Animation sync disabled with " + _pair.UserData.AliasOrUID;
_uiSharedService.IconText(FontAwesomeIcon.Stop); _uiSharedService.IconText(FontAwesomeIcon.WindowClose);
ImGui.SameLine(40 * ImGuiHelpers.GlobalScale); ImGui.SameLine(40 * ImGuiHelpers.GlobalScale);
ImGui.TextUnformatted(userAnimText); ImGui.TextUnformatted(userAnimText);
ImGui.NewLine(); ImGui.NewLine();
@@ -185,7 +189,7 @@ public class DrawUserPair : DrawPairBase
if (individualVFXDisabled) if (individualVFXDisabled)
{ {
var userVFXText = "VFX sync disabled with " + _pair.UserData.AliasOrUID; var userVFXText = "VFX sync disabled with " + _pair.UserData.AliasOrUID;
_uiSharedService.IconText(FontAwesomeIcon.Circle); _uiSharedService.IconText(FontAwesomeIcon.TimesCircle);
ImGui.SameLine(40 * ImGuiHelpers.GlobalScale); ImGui.SameLine(40 * ImGuiHelpers.GlobalScale);
ImGui.TextUnformatted(userVFXText); ImGui.TextUnformatted(userVFXText);
ImGui.NewLine(); ImGui.NewLine();
@@ -197,8 +201,6 @@ public class DrawUserPair : DrawPairBase
} }
} }
} }
// Icon for shared character data
if (_charaDataManager.SharedWithYouData.TryGetValue(_pair.UserData, out var sharedData)) if (_charaDataManager.SharedWithYouData.TryGetValue(_pair.UserData, out var sharedData))
{ {
var icon = FontAwesomeIcon.Running; var icon = FontAwesomeIcon.Running;
@@ -265,35 +267,38 @@ public class DrawUserPair : DrawPairBase
{ {
_selectGroupForPairUi.Open(entry); _selectGroupForPairUi.Open(entry);
} }
UiSharedService.AttachToolTip("Choose pair groups for " + entryUID); UiSharedService.AttachToolTip(AppendSeenInfo("Choose pair groups for " + entryUID));
var isDisableSounds = entry.UserPair!.OwnPermissions.IsDisableSounds(); var isDisableSounds = entry.UserPair!.OwnPermissions.IsDisableSounds();
string disableSoundsText = isDisableSounds ? "Enable sound sync" : "Disable sound sync"; string disableSoundsText = isDisableSounds ? "Enable sound sync" : "Disable sound sync";
var disableSoundsIcon = isDisableSounds ? FontAwesomeIcon.VolumeUp : FontAwesomeIcon.VolumeMute; var disableSoundsIcon = isDisableSounds ? FontAwesomeIcon.VolumeMute : FontAwesomeIcon.VolumeUp;
if (_uiSharedService.IconTextButton(disableSoundsIcon, disableSoundsText)) if (_uiSharedService.IconTextButton(disableSoundsIcon, disableSoundsText))
{ {
var permissions = entry.UserPair.OwnPermissions; var permissions = entry.UserPair.OwnPermissions;
permissions.SetDisableSounds(!isDisableSounds); permissions.SetDisableSounds(!isDisableSounds);
_mediator.Publish(new PairSyncOverrideChanged(entry.UserData.UID, permissions.IsDisableSounds(), null, null));
_ = _apiController.UserSetPairPermissions(new UserPermissionsDto(entry.UserData, permissions)); _ = _apiController.UserSetPairPermissions(new UserPermissionsDto(entry.UserData, permissions));
} }
var isDisableAnims = entry.UserPair!.OwnPermissions.IsDisableAnimations(); var isDisableAnims = entry.UserPair!.OwnPermissions.IsDisableAnimations();
string disableAnimsText = isDisableAnims ? "Enable animation sync" : "Disable animation sync"; string disableAnimsText = isDisableAnims ? "Enable animation sync" : "Disable animation sync";
var disableAnimsIcon = isDisableAnims ? FontAwesomeIcon.Running : FontAwesomeIcon.Stop; var disableAnimsIcon = isDisableAnims ? FontAwesomeIcon.WindowClose : FontAwesomeIcon.Running;
if (_uiSharedService.IconTextButton(disableAnimsIcon, disableAnimsText)) if (_uiSharedService.IconTextButton(disableAnimsIcon, disableAnimsText))
{ {
var permissions = entry.UserPair.OwnPermissions; var permissions = entry.UserPair.OwnPermissions;
permissions.SetDisableAnimations(!isDisableAnims); permissions.SetDisableAnimations(!isDisableAnims);
_mediator.Publish(new PairSyncOverrideChanged(entry.UserData.UID, null, permissions.IsDisableAnimations(), null));
_ = _apiController.UserSetPairPermissions(new UserPermissionsDto(entry.UserData, permissions)); _ = _apiController.UserSetPairPermissions(new UserPermissionsDto(entry.UserData, permissions));
} }
var isDisableVFX = entry.UserPair!.OwnPermissions.IsDisableVFX(); var isDisableVFX = entry.UserPair!.OwnPermissions.IsDisableVFX();
string disableVFXText = isDisableVFX ? "Enable VFX sync" : "Disable VFX sync"; string disableVFXText = isDisableVFX ? "Enable VFX sync" : "Disable VFX sync";
var disableVFXIcon = isDisableVFX ? FontAwesomeIcon.Sun : FontAwesomeIcon.Circle; var disableVFXIcon = isDisableVFX ? FontAwesomeIcon.TimesCircle : FontAwesomeIcon.Sun;
if (_uiSharedService.IconTextButton(disableVFXIcon, disableVFXText)) if (_uiSharedService.IconTextButton(disableVFXIcon, disableVFXText))
{ {
var permissions = entry.UserPair.OwnPermissions; var permissions = entry.UserPair.OwnPermissions;
permissions.SetDisableVFX(!isDisableVFX); permissions.SetDisableVFX(!isDisableVFX);
_mediator.Publish(new PairSyncOverrideChanged(entry.UserData.UID, null, null, permissions.IsDisableVFX()));
_ = _apiController.UserSetPairPermissions(new UserPermissionsDto(entry.UserData, permissions)); _ = _apiController.UserSetPairPermissions(new UserPermissionsDto(entry.UserData, permissions));
} }
@@ -301,6 +306,16 @@ public class DrawUserPair : DrawPairBase
{ {
_ = _apiController.UserRemovePair(new(entry.UserData)); _ = _apiController.UserRemovePair(new(entry.UserData));
} }
UiSharedService.AttachToolTip("Hold CTRL and click to unpair permanently from " + entryUID); UiSharedService.AttachToolTip(AppendSeenInfo("Hold CTRL and click to unpair permanently from " + entryUID));
}
private string AppendSeenInfo(string tooltip)
{
if (_pair.IsVisible) return tooltip;
var lastSeen = _serverConfigurationManager.GetNameForUid(_pair.UserData.UID);
if (string.IsNullOrWhiteSpace(lastSeen)) return tooltip;
return tooltip + " (Vu sous : " + lastSeen + ")";
} }
} }

View File

@@ -1,4 +1,4 @@
using Dalamud.Bindings.ImGui; using Dalamud.Bindings.ImGui;
using Dalamud.Interface; using Dalamud.Interface;
using Dalamud.Interface.Colors; using Dalamud.Interface.Colors;
using Dalamud.Interface.Utility; using Dalamud.Interface.Utility;
@@ -12,11 +12,13 @@ using MareSynchronos.API.Dto.Group;
using MareSynchronos.MareConfiguration; using MareSynchronos.MareConfiguration;
using MareSynchronos.PlayerData.Pairs; using MareSynchronos.PlayerData.Pairs;
using MareSynchronos.Services; using MareSynchronos.Services;
using MareSynchronos.Services.AutoDetect;
using MareSynchronos.Services.Mediator; using MareSynchronos.Services.Mediator;
using MareSynchronos.Services.ServerConfiguration; using MareSynchronos.Services.ServerConfiguration;
using MareSynchronos.UI.Components; using MareSynchronos.UI.Components;
using MareSynchronos.UI.Handlers; using MareSynchronos.UI.Handlers;
using MareSynchronos.WebAPI; using MareSynchronos.WebAPI;
using System;
using System.Globalization; using System.Globalization;
using System.Numerics; using System.Numerics;
@@ -31,6 +33,7 @@ internal sealed class GroupPanel
private readonly MareConfigService _mareConfig; private readonly MareConfigService _mareConfig;
private readonly ServerConfigurationManager _serverConfigurationManager; private readonly ServerConfigurationManager _serverConfigurationManager;
private readonly CharaDataManager _charaDataManager; private readonly CharaDataManager _charaDataManager;
private readonly AutoDetectRequestService _autoDetectRequestService;
private readonly Dictionary<string, bool> _showGidForEntry = new(StringComparer.Ordinal); private readonly Dictionary<string, bool> _showGidForEntry = new(StringComparer.Ordinal);
private readonly UidDisplayHandler _uidDisplayHandler; private readonly UidDisplayHandler _uidDisplayHandler;
private readonly UiSharedService _uiShared; private readonly UiSharedService _uiShared;
@@ -40,6 +43,7 @@ internal sealed class GroupPanel
private string _editGroupComment = string.Empty; private string _editGroupComment = string.Empty;
private string _editGroupEntry = string.Empty; private string _editGroupEntry = string.Empty;
private bool _errorGroupCreate = false; private bool _errorGroupCreate = false;
private string _errorGroupCreateMessage = string.Empty;
private bool _errorGroupJoin; private bool _errorGroupJoin;
private bool _isPasswordValid; private bool _isPasswordValid;
private GroupPasswordDto? _lastCreatedGroup = null; private GroupPasswordDto? _lastCreatedGroup = null;
@@ -52,12 +56,27 @@ internal sealed class GroupPanel
private bool _showModalChangePassword; private bool _showModalChangePassword;
private bool _showModalCreateGroup; private bool _showModalCreateGroup;
private bool _showModalEnterPassword; private bool _showModalEnterPassword;
private string _newSyncShellAlias = string.Empty;
private bool _createIsTemporary = false;
private int _tempSyncshellDurationHours = 24;
private readonly int[] _temporaryDurationOptions = new[]
{
1,
12,
24,
48,
72,
96,
120,
144,
168
};
private string _syncShellPassword = string.Empty; private string _syncShellPassword = string.Empty;
private string _syncShellToJoin = string.Empty; private string _syncShellToJoin = string.Empty;
public GroupPanel(CompactUi mainUi, UiSharedService uiShared, PairManager pairManager, ChatService chatServivce, public GroupPanel(CompactUi mainUi, UiSharedService uiShared, PairManager pairManager, ChatService chatServivce,
UidDisplayHandler uidDisplayHandler, MareConfigService mareConfig, ServerConfigurationManager serverConfigurationManager, UidDisplayHandler uidDisplayHandler, MareConfigService mareConfig, ServerConfigurationManager serverConfigurationManager,
CharaDataManager charaDataManager) CharaDataManager charaDataManager, AutoDetectRequestService autoDetectRequestService)
{ {
_mainUi = mainUi; _mainUi = mainUi;
_uiShared = uiShared; _uiShared = uiShared;
@@ -67,6 +86,7 @@ internal sealed class GroupPanel
_mareConfig = mareConfig; _mareConfig = mareConfig;
_serverConfigurationManager = serverConfigurationManager; _serverConfigurationManager = serverConfigurationManager;
_charaDataManager = charaDataManager; _charaDataManager = charaDataManager;
_autoDetectRequestService = autoDetectRequestService;
} }
private ApiController ApiController => _uiShared.ApiController; private ApiController ApiController => _uiShared.ApiController;
@@ -82,7 +102,7 @@ internal sealed class GroupPanel
{ {
var buttonSize = _uiShared.GetIconButtonSize(FontAwesomeIcon.Plus); var buttonSize = _uiShared.GetIconButtonSize(FontAwesomeIcon.Plus);
ImGui.SetNextItemWidth(UiSharedService.GetWindowContentRegionWidth() - ImGui.GetWindowContentRegionMin().X - buttonSize.X); ImGui.SetNextItemWidth(UiSharedService.GetWindowContentRegionWidth() - ImGui.GetWindowContentRegionMin().X - buttonSize.X);
ImGui.InputTextWithHint("##syncshellid", "Syncshell GID/Alias (leave empty to create)", ref _syncShellToJoin, 20); ImGui.InputTextWithHint("##syncshellid", "Syncshell GID/Alias (leave empty to create)", ref _syncShellToJoin, 50);
ImGui.SameLine(ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth() - buttonSize.X); ImGui.SameLine(ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth() - buttonSize.X);
bool userCanJoinMoreGroups = _pairManager.GroupPairs.Count < ApiController.ServerInfo.MaxGroupsJoinedByUser; bool userCanJoinMoreGroups = _pairManager.GroupPairs.Count < ApiController.ServerInfo.MaxGroupsJoinedByUser;
@@ -108,6 +128,10 @@ internal sealed class GroupPanel
{ {
_lastCreatedGroup = null; _lastCreatedGroup = null;
_errorGroupCreate = false; _errorGroupCreate = false;
_newSyncShellAlias = string.Empty;
_createIsTemporary = false;
_tempSyncshellDurationHours = 24;
_errorGroupCreateMessage = string.Empty;
_showModalCreateGroup = true; _showModalCreateGroup = true;
ImGui.OpenPopup("Create Syncshell"); ImGui.OpenPopup("Create Syncshell");
} }
@@ -150,18 +174,97 @@ internal sealed class GroupPanel
if (ImGui.BeginPopupModal("Create Syncshell", ref _showModalCreateGroup, UiSharedService.PopupWindowFlags)) if (ImGui.BeginPopupModal("Create Syncshell", ref _showModalCreateGroup, UiSharedService.PopupWindowFlags))
{ {
UiSharedService.TextWrapped("Press the button below to create a new Syncshell."); UiSharedService.TextWrapped("Choisissez le type de Syncshell à créer.");
bool showPermanent = !_createIsTemporary;
if (ImGui.RadioButton("Permanente", showPermanent))
{
_createIsTemporary = false;
}
ImGui.SameLine();
if (ImGui.RadioButton("Temporaire", _createIsTemporary))
{
_createIsTemporary = true;
_newSyncShellAlias = string.Empty;
}
if (!_createIsTemporary)
{
UiSharedService.TextWrapped("Donnez un nom à votre Syncshell (optionnel) puis créez-la.");
ImGui.SetNextItemWidth(-1);
ImGui.InputTextWithHint("##syncshellalias", "Nom du Syncshell", ref _newSyncShellAlias, 50);
}
else
{
_newSyncShellAlias = string.Empty;
}
if (_createIsTemporary)
{
UiSharedService.TextWrapped("Durée maximale d'une Syncshell temporaire : 7 jours.");
if (_tempSyncshellDurationHours > 168) _tempSyncshellDurationHours = 168;
for (int i = 0; i < _temporaryDurationOptions.Length; i++)
{
var option = _temporaryDurationOptions[i];
var isSelected = _tempSyncshellDurationHours == option;
string label = option switch
{
>= 24 when option % 24 == 0 => option == 24 ? "24h" : $"{option / 24}j",
_ => option + "h"
};
if (ImGui.RadioButton(label, isSelected))
{
_tempSyncshellDurationHours = option;
}
// Start a new line after every 3 buttons
if ((i + 1) % 3 == 0)
{
ImGui.NewLine();
}
else
{
ImGui.SameLine();
}
}
var expiresLocal = DateTime.Now.AddHours(_tempSyncshellDurationHours);
UiSharedService.TextWrapped($"Expiration le {expiresLocal:g} (heure locale).");
}
UiSharedService.TextWrapped("Appuyez sur le bouton ci-dessous pour créer une nouvelle Syncshell.");
ImGui.SetNextItemWidth(200 * ImGuiHelpers.GlobalScale); ImGui.SetNextItemWidth(200 * ImGuiHelpers.GlobalScale);
if (ImGui.Button("Create Syncshell")) if (ImGui.Button("Create Syncshell"))
{ {
try try
{ {
_lastCreatedGroup = ApiController.GroupCreate().Result; if (_createIsTemporary)
{
var expiresAtUtc = DateTime.UtcNow.AddHours(_tempSyncshellDurationHours);
_lastCreatedGroup = ApiController.GroupCreateTemporary(expiresAtUtc).Result;
} }
catch else
{
var aliasInput = string.IsNullOrWhiteSpace(_newSyncShellAlias) ? null : _newSyncShellAlias.Trim();
_lastCreatedGroup = ApiController.GroupCreate(aliasInput).Result;
if (_lastCreatedGroup != null)
{
_newSyncShellAlias = string.Empty;
}
}
}
catch (Exception ex)
{ {
_lastCreatedGroup = null; _lastCreatedGroup = null;
_errorGroupCreate = true; _errorGroupCreate = true;
if (ex.Message.Contains("name is already in use", StringComparison.OrdinalIgnoreCase))
{
_errorGroupCreateMessage = "Le nom de la Syncshell est déjà utilisé.";
}
else
{
_errorGroupCreateMessage = ex.Message;
}
} }
} }
@@ -169,6 +272,11 @@ internal sealed class GroupPanel
{ {
ImGui.Separator(); ImGui.Separator();
_errorGroupCreate = false; _errorGroupCreate = false;
_errorGroupCreateMessage = string.Empty;
if (!string.IsNullOrWhiteSpace(_lastCreatedGroup.Group.Alias))
{
ImGui.TextUnformatted("Syncshell Name: " + _lastCreatedGroup.Group.Alias);
}
ImGui.TextUnformatted("Syncshell ID: " + _lastCreatedGroup.Group.GID); ImGui.TextUnformatted("Syncshell ID: " + _lastCreatedGroup.Group.GID);
ImGui.AlignTextToFramePadding(); ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("Syncshell Password: " + _lastCreatedGroup.Password); ImGui.TextUnformatted("Syncshell Password: " + _lastCreatedGroup.Password);
@@ -178,12 +286,19 @@ internal sealed class GroupPanel
ImGui.SetClipboardText(_lastCreatedGroup.Password); ImGui.SetClipboardText(_lastCreatedGroup.Password);
} }
UiSharedService.TextWrapped("You can change the Syncshell password later at any time."); UiSharedService.TextWrapped("You can change the Syncshell password later at any time.");
if (_lastCreatedGroup.IsTemporary && _lastCreatedGroup.ExpiresAt != null)
{
var expiresLocal = _lastCreatedGroup.ExpiresAt.Value.ToLocalTime();
UiSharedService.TextWrapped($"Cette Syncshell expirera le {expiresLocal:g} (heure locale).");
}
} }
if (_errorGroupCreate) if (_errorGroupCreate)
{ {
UiSharedService.ColorTextWrapped("You are already owner of the maximum amount of Syncshells (3) or joined the maximum amount of Syncshells (6). Relinquish ownership of your own Syncshells to someone else or leave existing Syncshells.", var msg = string.IsNullOrWhiteSpace(_errorGroupCreateMessage)
new Vector4(1, 0, 0, 1)); ? "You are already owner of the maximum amount of Syncshells (3) or joined the maximum amount of Syncshells (6). Relinquish ownership of your own Syncshells to someone else or leave existing Syncshells."
: _errorGroupCreateMessage;
UiSharedService.ColorTextWrapped(msg, new Vector4(1, 0, 0, 1));
} }
UiSharedService.SetScaledWindowSize(350); UiSharedService.SetScaledWindowSize(350);
@@ -243,11 +358,13 @@ internal sealed class GroupPanel
if (!string.Equals(_editGroupEntry, groupDto.GID, StringComparison.Ordinal)) if (!string.Equals(_editGroupEntry, groupDto.GID, StringComparison.Ordinal))
{ {
var shellConfig = _serverConfigurationManager.GetShellConfigForGid(groupDto.GID); var shellConfig = _serverConfigurationManager.GetShellConfigForGid(groupDto.GID);
if (!_mareConfig.Current.DisableSyncshellChat && shellConfig.Enabled) var totalMembers = pairsInGroup.Count + 1;
{ var connectedMembers = pairsInGroup.Count(p => p.IsOnline) + 1;
ImGui.TextUnformatted($"[{shellNumber}]"); var maxCapacity = ApiController.ServerInfo.MaxGroupUserCount;
UiSharedService.AttachToolTip("Chat command prefix: /ss" + shellNumber); ImGui.TextUnformatted($"{connectedMembers}/{totalMembers}");
} UiSharedService.AttachToolTip("Membres connectés / membres totaux" + Environment.NewLine +
$"Capacité maximale : {maxCapacity}" + Environment.NewLine +
"Syncshell ID: " + groupDto.Group.GID);
if (textIsGid) ImGui.PushFont(UiBuilder.MonoFont); if (textIsGid) ImGui.PushFont(UiBuilder.MonoFont);
ImGui.SameLine(); ImGui.SameLine();
ImGui.TextUnformatted(groupName); ImGui.TextUnformatted(groupName);
@@ -255,6 +372,20 @@ internal sealed class GroupPanel
UiSharedService.AttachToolTip("Left click to switch between GID display and comment" + Environment.NewLine + UiSharedService.AttachToolTip("Left click to switch between GID display and comment" + Environment.NewLine +
"Right click to change comment for " + groupName + Environment.NewLine + Environment.NewLine "Right click to change comment for " + groupName + Environment.NewLine + Environment.NewLine
+ "Users: " + (pairsInGroup.Count + 1) + ", Owner: " + groupDto.OwnerAliasOrUID); + "Users: " + (pairsInGroup.Count + 1) + ", Owner: " + groupDto.OwnerAliasOrUID);
if (groupDto.IsTemporary)
{
ImGui.SameLine();
UiSharedService.ColorText("(Temp)", ImGuiColors.DalamudOrange);
if (groupDto.ExpiresAt != null)
{
var tempExpireLocal = groupDto.ExpiresAt.Value.ToLocalTime();
UiSharedService.AttachToolTip($"Expire le {tempExpireLocal:g}");
}
else
{
UiSharedService.AttachToolTip("Syncshell temporaire");
}
}
if (ImGui.IsItemClicked(ImGuiMouseButton.Left)) if (ImGui.IsItemClicked(ImGuiMouseButton.Left))
{ {
var prevState = textIsGid; var prevState = textIsGid;
@@ -438,7 +569,9 @@ internal sealed class GroupPanel
).Value, ).Value,
_uidDisplayHandler, _uidDisplayHandler,
_uiShared, _uiShared,
_charaDataManager); _charaDataManager,
_autoDetectRequestService,
_serverConfigurationManager);
if (pair.IsVisible) if (pair.IsVisible)
visibleUsers.Add(drawPair); visibleUsers.Add(drawPair);
@@ -464,7 +597,9 @@ internal sealed class GroupPanel
if (offlineUsers.Count > 0) if (offlineUsers.Count > 0)
{ {
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey);
ImGui.TextUnformatted("Offline/Unknown"); ImGui.TextUnformatted("Offline/Unknown");
ImGui.PopStyleColor();
ImGui.Separator(); ImGui.Separator();
if (hideOfflineUsers) if (hideOfflineUsers)
{ {
@@ -497,12 +632,12 @@ internal sealed class GroupPanel
bool showInfoIcon = !invitesEnabled || soundsDisabled || animDisabled || vfxDisabled || userSoundsDisabled || userAnimDisabled || userVFXDisabled; bool showInfoIcon = !invitesEnabled || soundsDisabled || animDisabled || vfxDisabled || userSoundsDisabled || userAnimDisabled || userVFXDisabled;
var lockedIcon = invitesEnabled ? FontAwesomeIcon.LockOpen : FontAwesomeIcon.Lock; var lockedIcon = invitesEnabled ? FontAwesomeIcon.LockOpen : FontAwesomeIcon.Lock;
var animIcon = animDisabled ? FontAwesomeIcon.Stop : FontAwesomeIcon.Running; var animIcon = animDisabled ? FontAwesomeIcon.WindowClose : FontAwesomeIcon.Running;
var soundsIcon = soundsDisabled ? FontAwesomeIcon.VolumeOff : FontAwesomeIcon.VolumeUp; var soundsIcon = soundsDisabled ? FontAwesomeIcon.VolumeMute : FontAwesomeIcon.VolumeUp;
var vfxIcon = vfxDisabled ? FontAwesomeIcon.Circle : FontAwesomeIcon.Sun; var vfxIcon = vfxDisabled ? FontAwesomeIcon.TimesCircle : FontAwesomeIcon.Sun;
var userAnimIcon = userAnimDisabled ? FontAwesomeIcon.Stop : FontAwesomeIcon.Running; var userAnimIcon = userAnimDisabled ? FontAwesomeIcon.WindowClose : FontAwesomeIcon.Running;
var userSoundsIcon = userSoundsDisabled ? FontAwesomeIcon.VolumeOff : FontAwesomeIcon.VolumeUp; var userSoundsIcon = userSoundsDisabled ? FontAwesomeIcon.VolumeMute : FontAwesomeIcon.VolumeUp;
var userVFXIcon = userVFXDisabled ? FontAwesomeIcon.Circle : FontAwesomeIcon.Sun; var userVFXIcon = userVFXDisabled ? FontAwesomeIcon.TimesCircle : FontAwesomeIcon.Sun;
var iconSize = UiSharedService.GetIconSize(infoIcon); var iconSize = UiSharedService.GetIconSize(infoIcon);
var barbuttonSize = _uiShared.GetIconButtonSize(FontAwesomeIcon.Bars); var barbuttonSize = _uiShared.GetIconButtonSize(FontAwesomeIcon.Bars);
@@ -639,6 +774,7 @@ internal sealed class GroupPanel
ImGui.CloseCurrentPopup(); ImGui.CloseCurrentPopup();
var perm = groupDto.GroupUserPermissions; var perm = groupDto.GroupUserPermissions;
perm.SetDisableSounds(!perm.IsDisableSounds()); perm.SetDisableSounds(!perm.IsDisableSounds());
_mainUi.Mediator.Publish(new GroupSyncOverrideChanged(groupDto.Group.GID, perm.IsDisableSounds(), null, null));
_ = ApiController.GroupChangeIndividualPermissionState(new(groupDto.Group, new UserData(ApiController.UID), perm)); _ = ApiController.GroupChangeIndividualPermissionState(new(groupDto.Group, new UserData(ApiController.UID), perm));
} }
UiSharedService.AttachToolTip("Sets your allowance for sound synchronization for users of this syncshell." UiSharedService.AttachToolTip("Sets your allowance for sound synchronization for users of this syncshell."
@@ -652,6 +788,7 @@ internal sealed class GroupPanel
ImGui.CloseCurrentPopup(); ImGui.CloseCurrentPopup();
var perm = groupDto.GroupUserPermissions; var perm = groupDto.GroupUserPermissions;
perm.SetDisableAnimations(!perm.IsDisableAnimations()); perm.SetDisableAnimations(!perm.IsDisableAnimations());
_mainUi.Mediator.Publish(new GroupSyncOverrideChanged(groupDto.Group.GID, null, perm.IsDisableAnimations(), null));
_ = ApiController.GroupChangeIndividualPermissionState(new(groupDto.Group, new UserData(ApiController.UID), perm)); _ = ApiController.GroupChangeIndividualPermissionState(new(groupDto.Group, new UserData(ApiController.UID), perm));
} }
UiSharedService.AttachToolTip("Sets your allowance for animations synchronization for users of this syncshell." UiSharedService.AttachToolTip("Sets your allowance for animations synchronization for users of this syncshell."
@@ -666,6 +803,7 @@ internal sealed class GroupPanel
ImGui.CloseCurrentPopup(); ImGui.CloseCurrentPopup();
var perm = groupDto.GroupUserPermissions; var perm = groupDto.GroupUserPermissions;
perm.SetDisableVFX(!perm.IsDisableVFX()); perm.SetDisableVFX(!perm.IsDisableVFX());
_mainUi.Mediator.Publish(new GroupSyncOverrideChanged(groupDto.Group.GID, null, null, perm.IsDisableVFX()));
_ = ApiController.GroupChangeIndividualPermissionState(new(groupDto.Group, new UserData(ApiController.UID), perm)); _ = ApiController.GroupChangeIndividualPermissionState(new(groupDto.Group, new UserData(ApiController.UID), perm));
} }
UiSharedService.AttachToolTip("Sets your allowance for VFX synchronization for users of this syncshell." UiSharedService.AttachToolTip("Sets your allowance for VFX synchronization for users of this syncshell."

View File

@@ -54,17 +54,12 @@ public class PairGroupsUi
ImGui.SameLine(buttonPauseOffset); ImGui.SameLine(buttonPauseOffset);
if (_uiSharedService.IconButton(pauseButton)) if (_uiSharedService.IconButton(pauseButton))
{ {
// If all of the currently visible pairs (after applying filters to the pairs)
// are paused we display a resume button to resume all currently visible (after filters)
// pairs. Otherwise, we just pause all the remaining pairs.
if (allArePaused) if (allArePaused)
{ {
// If all are paused => resume all
ResumeAllPairs(availablePairsInThisTag); ResumeAllPairs(availablePairsInThisTag);
} }
else else
{ {
// otherwise pause all remaining
PauseRemainingPairs(availablePairsInThisTag); PauseRemainingPairs(availablePairsInThisTag);
} }
} }
@@ -120,7 +115,6 @@ public class PairGroupsUi
} }
else else
{ {
// Avoid uncomfortably close group names
if (!_tagHandler.IsTagOpen(tag)) if (!_tagHandler.IsTagOpen(tag))
{ {
var size = ImGui.CalcTextSize("").Y + ImGui.GetStyle().FramePadding.Y * 2f; var size = ImGui.CalcTextSize("").Y + ImGui.GetStyle().FramePadding.Y * 2f;
@@ -157,14 +151,12 @@ public class PairGroupsUi
{ {
TagHandler.CustomUnpairedTag => "Unpaired", TagHandler.CustomUnpairedTag => "Unpaired",
TagHandler.CustomOfflineTag => "Offline", TagHandler.CustomOfflineTag => "Offline",
TagHandler.CustomOnlineTag => _mareConfig.Current.ShowOfflineUsersSeparately ? "Online/Paused" : "Contacts", TagHandler.CustomOnlineTag => _mareConfig.Current.ShowOfflineUsersSeparately ? "Online" : "Contacts",
TagHandler.CustomVisibleTag => "Visible", TagHandler.CustomVisibleTag => "Visible",
_ => tag _ => tag
}; };
string resultFolderName = !isSpecialTag ? $"{displayedName} ({visible}/{online}/{total} Pairs)" : $"{displayedName} ({online} Pairs)"; string resultFolderName = !isSpecialTag ? $"{displayedName} ({visible}/{online}/{total} Pairs)" : $"{displayedName} ({online} Pairs)";
// FontAwesomeIcon.CaretSquareDown : FontAwesomeIcon.CaretSquareRight
var icon = _tagHandler.IsTagOpen(tag) ? FontAwesomeIcon.CaretSquareDown : FontAwesomeIcon.CaretSquareRight; var icon = _tagHandler.IsTagOpen(tag) ? FontAwesomeIcon.CaretSquareDown : FontAwesomeIcon.CaretSquareRight;
_uiSharedService.IconText(icon); _uiSharedService.IconText(icon);
if (ImGui.IsItemClicked(ImGuiMouseButton.Left)) if (ImGui.IsItemClicked(ImGuiMouseButton.Left))

View File

@@ -35,7 +35,7 @@ internal class ReportPopupHandler : IPopupHandler
UiSharedService.TextWrapped($"Note: Sending a report will disable the offending profile globally.{Environment.NewLine}" + UiSharedService.TextWrapped($"Note: Sending a report will disable the offending profile globally.{Environment.NewLine}" +
$"The report will be sent to the team of your currently connected server.{Environment.NewLine}" + $"The report will be sent to the team of your currently connected server.{Environment.NewLine}" +
$"Depending on the severity of the offense the users profile or account can be permanently disabled or banned."); $"Depending on the severity of the offense the users profile or account can be permanently disabled or banned.");
UiSharedService.ColorTextWrapped("Report spam and wrong reports will not be tolerated and can lead to permanent account suspension.", ImGuiColors.DalamudRed); UiSharedService.ColorTextWrapped("Report spam and wrong reports will not be tolerated and can lead to permanent account suspension.", UiSharedService.AccentColor);
UiSharedService.ColorTextWrapped("This is not for reporting misbehavior but solely for the actual profile. " + UiSharedService.ColorTextWrapped("This is not for reporting misbehavior but solely for the actual profile. " +
"Reports that are not solely for the profile will be ignored.", ImGuiColors.DalamudYellow); "Reports that are not solely for the profile will be ignored.", ImGuiColors.DalamudYellow);

View File

@@ -15,20 +15,8 @@ public class SelectGroupForPairUi
private readonly UidDisplayHandler _uidDisplayHandler; private readonly UidDisplayHandler _uidDisplayHandler;
private readonly UiSharedService _uiSharedService; private readonly UiSharedService _uiSharedService;
/// <summary>
/// The group UI is always open for a specific pair. This defines which pair the UI is open for.
/// </summary>
/// <returns></returns>
private Pair? _pair; private Pair? _pair;
/// <summary>
/// Should the panel show, yes/no
/// </summary>
private bool _show; private bool _show;
/// <summary>
/// For the add category option, this stores the currently typed in tag name
/// </summary>
private string _tagNameToAdd = ""; private string _tagNameToAdd = "";
public SelectGroupForPairUi(TagHandler tagHandler, UidDisplayHandler uidDisplayHandler, UiSharedService uiSharedService) public SelectGroupForPairUi(TagHandler tagHandler, UidDisplayHandler uidDisplayHandler, UiSharedService uiSharedService)
@@ -49,7 +37,6 @@ public class SelectGroupForPairUi
var name = PairName(_pair); var name = PairName(_pair);
var popupName = $"Choose Groups for {name}"; var popupName = $"Choose Groups for {name}";
// Is the popup supposed to show but did not open yet? Open it
if (_show) if (_show)
{ {
ImGui.OpenPopup(popupName); ImGui.OpenPopup(popupName);
@@ -91,10 +78,6 @@ public class SelectGroupForPairUi
public void Open(Pair pair) public void Open(Pair pair)
{ {
_pair = pair; _pair = pair;
// Using "_show" here to de-couple the opening of the popup
// The popup name is derived from the name the user currently sees, which is
// based on the showUidForEntry dictionary.
// We'd have to derive the name here to open it popup modal here, when the Open() is called
_show = true; _show = true;
} }

View File

@@ -285,7 +285,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
{ {
UiSharedService.ColorText("WARNING BC7 CONVERSION:", ImGuiColors.DalamudYellow); UiSharedService.ColorText("WARNING BC7 CONVERSION:", ImGuiColors.DalamudYellow);
ImGui.SameLine(); ImGui.SameLine();
UiSharedService.ColorText("Converting textures to BC7 is irreversible!", ImGuiColors.DalamudRed); UiSharedService.ColorText("Converting textures to BC7 is irreversible!", UiSharedService.AccentColor);
UiSharedService.ColorTextWrapped("- Converting textures to BC7 will reduce their size (compressed and uncompressed) drastically. It is recommended to be used for large (4k+) textures." + UiSharedService.ColorTextWrapped("- Converting textures to BC7 will reduce their size (compressed and uncompressed) drastically. It is recommended to be used for large (4k+) textures." +
Environment.NewLine + "- Some textures, especially ones utilizing colorsets, might not be suited for BC7 conversion and might produce visual artifacts." + Environment.NewLine + "- Some textures, especially ones utilizing colorsets, might not be suited for BC7 conversion and might produce visual artifacts." +
Environment.NewLine + "- Before converting textures, make sure to have the original files of the mod you are converting so you can reimport it in case of issues." + Environment.NewLine + "- Before converting textures, make sure to have the original files of the mod you are converting so you can reimport it in case of issues." +

View File

@@ -23,7 +23,7 @@ public class DownloadUi : WindowMediatorSubscriberBase
public DownloadUi(ILogger<DownloadUi> logger, DalamudUtilService dalamudUtilService, MareConfigService configService, public DownloadUi(ILogger<DownloadUi> logger, DalamudUtilService dalamudUtilService, MareConfigService configService,
FileUploadManager fileTransferManager, MareMediator mediator, UiSharedService uiShared, PerformanceCollectorService performanceCollectorService) FileUploadManager fileTransferManager, MareMediator mediator, UiSharedService uiShared, PerformanceCollectorService performanceCollectorService)
: base(logger, mediator, "UmbraSync Downloads", performanceCollectorService) : base(logger, mediator, "Umbra Downloads", performanceCollectorService)
{ {
_dalamudUtilService = dalamudUtilService; _dalamudUtilService = dalamudUtilService;
_configService = configService; _configService = configService;
@@ -163,13 +163,13 @@ public class DownloadUi : WindowMediatorSubscriberBase
UiSharedService.Color(0, 0, 0, transparency), 1); UiSharedService.Color(0, 0, 0, transparency), 1);
drawList.AddRectFilled(dlBarStart with { X = dlBarStart.X - dlBarBorder, Y = dlBarStart.Y - dlBarBorder }, drawList.AddRectFilled(dlBarStart with { X = dlBarStart.X - dlBarBorder, Y = dlBarStart.Y - dlBarBorder },
dlBarEnd with { X = dlBarEnd.X + dlBarBorder, Y = dlBarEnd.Y + dlBarBorder }, dlBarEnd with { X = dlBarEnd.X + dlBarBorder, Y = dlBarEnd.Y + dlBarBorder },
UiSharedService.Color(220, 220, 220, transparency), 1); UiSharedService.Color(230, 200, 255, transparency), 1);
drawList.AddRectFilled(dlBarStart, dlBarEnd, drawList.AddRectFilled(dlBarStart, dlBarEnd,
UiSharedService.Color(0, 0, 0, transparency), 1); UiSharedService.Color(0, 0, 0, transparency), 1);
var dlProgressPercent = transferredBytes / (double)totalBytes; var dlProgressPercent = transferredBytes / (double)totalBytes;
drawList.AddRectFilled(dlBarStart, drawList.AddRectFilled(dlBarStart,
dlBarEnd with { X = dlBarStart.X + (float)(dlProgressPercent * dlBarWidth) }, dlBarEnd with { X = dlBarStart.X + (float)(dlProgressPercent * dlBarWidth) },
UiSharedService.Color(50, 205, 50, transparency), 1); UiSharedService.Color(160, 64, 255, transparency), 1);
if (_configService.Current.TransferBarsShowText) if (_configService.Current.TransferBarsShowText)
{ {

View File

@@ -2,9 +2,11 @@
using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Game.Text.SeStringHandling.Payloads; using Dalamud.Game.Text.SeStringHandling.Payloads;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using Dalamud.Interface;
using MareSynchronos.MareConfiguration; using MareSynchronos.MareConfiguration;
using MareSynchronos.MareConfiguration.Configurations; using MareSynchronos.MareConfiguration.Configurations;
using MareSynchronos.PlayerData.Pairs; using MareSynchronos.PlayerData.Pairs;
using MareSynchronos.Services.AutoDetect;
using MareSynchronos.Services.Mediator; using MareSynchronos.Services.Mediator;
using MareSynchronos.WebAPI; using MareSynchronos.WebAPI;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
@@ -15,6 +17,7 @@ namespace MareSynchronos.UI;
public sealed class DtrEntry : IDisposable, IHostedService public sealed class DtrEntry : IDisposable, IHostedService
{ {
public const string DefaultGlyph = "\u25CB";
private enum DtrStyle private enum DtrStyle
{ {
Default, Default,
@@ -39,12 +42,13 @@ public sealed class DtrEntry : IDisposable, IHostedService
private readonly ILogger<DtrEntry> _logger; private readonly ILogger<DtrEntry> _logger;
private readonly MareMediator _mareMediator; private readonly MareMediator _mareMediator;
private readonly PairManager _pairManager; private readonly PairManager _pairManager;
private readonly NearbyPendingService _nearbyPendingService;
private Task? _runTask; private Task? _runTask;
private string? _text; private string? _text;
private string? _tooltip; private string? _tooltip;
private Colors _colors; private Colors _colors;
public DtrEntry(ILogger<DtrEntry> logger, IDtrBar dtrBar, MareConfigService configService, MareMediator mareMediator, PairManager pairManager, ApiController apiController) public DtrEntry(ILogger<DtrEntry> logger, IDtrBar dtrBar, MareConfigService configService, MareMediator mareMediator, PairManager pairManager, ApiController apiController, NearbyPendingService nearbyPendingService)
{ {
_logger = logger; _logger = logger;
_dtrBar = dtrBar; _dtrBar = dtrBar;
@@ -53,6 +57,7 @@ public sealed class DtrEntry : IDisposable, IHostedService
_mareMediator = mareMediator; _mareMediator = mareMediator;
_pairManager = pairManager; _pairManager = pairManager;
_apiController = apiController; _apiController = apiController;
_nearbyPendingService = nearbyPendingService;
} }
public void Dispose() public void Dispose()
@@ -104,7 +109,7 @@ public sealed class DtrEntry : IDisposable, IHostedService
private IDtrBarEntry CreateEntry() private IDtrBarEntry CreateEntry()
{ {
_logger.LogTrace("Creating new DtrBar entry"); _logger.LogTrace("Creating new DtrBar entry");
var entry = _dtrBar.Get("UmbraSync"); var entry = _dtrBar.Get("Umbra");
entry.OnClick = _ => _mareMediator.Publish(new UiToggleMessage(typeof(CompactUi))); entry.OnClick = _ => _mareMediator.Publish(new UiToggleMessage(typeof(CompactUi)));
return entry; return entry;
@@ -145,8 +150,9 @@ public sealed class DtrEntry : IDisposable, IHostedService
if (_apiController.IsConnected) if (_apiController.IsConnected)
{ {
var pairCount = _pairManager.GetVisibleUserCount(); var pairCount = _pairManager.GetVisibleUserCount();
var baseText = RenderDtrStyle(_configService.Current.DtrStyle, pairCount.ToString());
text = RenderDtrStyle(_configService.Current.DtrStyle, pairCount.ToString()); var pendingNearby = _nearbyPendingService.Pending.Count;
text = pendingNearby > 0 ? $"{baseText} ({pendingNearby})" : baseText;
if (pairCount > 0) if (pairCount > 0)
{ {
IEnumerable<string> visiblePairs; IEnumerable<string> visiblePairs;
@@ -163,19 +169,28 @@ public sealed class DtrEntry : IDisposable, IHostedService
.Select(x => string.Format("{0}", _configService.Current.PreferNoteInDtrTooltip ? x.GetNoteOrName() : x.PlayerName)); .Select(x => string.Format("{0}", _configService.Current.PreferNoteInDtrTooltip ? x.GetNoteOrName() : x.PlayerName));
} }
tooltip = $"UmbraSync: Connected{Environment.NewLine}----------{Environment.NewLine}{string.Join(Environment.NewLine, visiblePairs)}"; tooltip = $"Umbra: Connected";
if (pendingNearby > 0)
{
tooltip += $"{Environment.NewLine}Invitation en attente : {pendingNearby}";
}
tooltip += $"{Environment.NewLine}----------{Environment.NewLine}{string.Join(Environment.NewLine, visiblePairs)}";
colors = _configService.Current.DtrColorsPairsInRange; colors = _configService.Current.DtrColorsPairsInRange;
} }
else else
{ {
tooltip = "UmbraSync: Connected"; tooltip = "Umbra: Connected";
if (pendingNearby > 0)
{
tooltip += $"{Environment.NewLine}Invitation en attente : {pendingNearby}";
}
colors = _configService.Current.DtrColorsDefault; colors = _configService.Current.DtrColorsDefault;
} }
} }
else else
{ {
text = RenderDtrStyle(_configService.Current.DtrStyle, "\uE04C"); text = RenderDtrStyle(_configService.Current.DtrStyle, "\uE04C");
tooltip = "UmbraSync: Not Connected"; tooltip = "Umbra: Not Connected";
colors = _configService.Current.DtrColorsNotConnected; colors = _configService.Current.DtrColorsNotConnected;
} }
@@ -196,7 +211,8 @@ public sealed class DtrEntry : IDisposable, IHostedService
{ {
var style = (DtrStyle)styleNum; var style = (DtrStyle)styleNum;
return style switch { return style switch
{
DtrStyle.Style1 => $"\xE039 {text}", DtrStyle.Style1 => $"\xE039 {text}",
DtrStyle.Style2 => $"\xE0BC {text}", DtrStyle.Style2 => $"\xE0BC {text}",
DtrStyle.Style3 => $"\xE0BD {text}", DtrStyle.Style3 => $"\xE0BD {text}",
@@ -206,7 +222,7 @@ public sealed class DtrEntry : IDisposable, IHostedService
DtrStyle.Style7 => $"\xE05D {text}", DtrStyle.Style7 => $"\xE05D {text}",
DtrStyle.Style8 => $"\xE03C{text}", DtrStyle.Style8 => $"\xE03C{text}",
DtrStyle.Style9 => $"\xE040 {text} \xE041", DtrStyle.Style9 => $"\xE040 {text} \xE041",
_ => $"\uE044 {text}" _ => DefaultGlyph + " " + text
}; };
} }

View File

@@ -12,6 +12,7 @@ using MareSynchronos.Services.ServerConfiguration;
using MareSynchronos.Utils; using MareSynchronos.Utils;
using MareSynchronos.WebAPI; using MareSynchronos.WebAPI;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System.Numerics;
namespace MareSynchronos.UI; namespace MareSynchronos.UI;
@@ -35,7 +36,7 @@ public class EditProfileUi : WindowMediatorSubscriberBase
ApiController apiController, UiSharedService uiSharedService, FileDialogManager fileDialogManager, ApiController apiController, UiSharedService uiSharedService, FileDialogManager fileDialogManager,
ServerConfigurationManager serverConfigurationManager, ServerConfigurationManager serverConfigurationManager,
MareProfileManager mareProfileManager, PerformanceCollectorService performanceCollectorService) MareProfileManager mareProfileManager, PerformanceCollectorService performanceCollectorService)
: base(logger, mediator, "UmbraSync Edit Profile###UmbraSyncSyncEditProfileUI", performanceCollectorService) : base(logger, mediator, "Umbra Edit Profile###UmbraSyncEditProfileUI", performanceCollectorService)
{ {
IsOpen = false; IsOpen = false;
this.SizeConstraints = new() this.SizeConstraints = new()
@@ -65,12 +66,13 @@ public class EditProfileUi : WindowMediatorSubscriberBase
protected override void DrawInternal() protected override void DrawInternal()
{ {
_uiSharedService.BigText("Current Profile (as saved on server)"); _uiSharedService.BigText("Current Profile (as saved on server)");
ImGuiHelpers.ScaledDummy(new Vector2(0f, ImGui.GetStyle().ItemSpacing.Y / 2));
var profile = _mareProfileManager.GetMareProfile(new UserData(_apiController.UID)); var profile = _mareProfileManager.GetMareProfile(new UserData(_apiController.UID));
if (profile.IsFlagged) if (profile.IsFlagged)
{ {
UiSharedService.ColorTextWrapped(profile.Description, ImGuiColors.DalamudRed); UiSharedService.ColorTextWrapped(profile.Description, UiSharedService.AccentColor);
return; return;
} }
@@ -87,13 +89,12 @@ public class EditProfileUi : WindowMediatorSubscriberBase
_descriptionText = _profileDescription; _descriptionText = _profileDescription;
} }
var spacing = ImGui.GetStyle().ItemSpacing.X;
if (_pfpTextureWrap != null) if (_pfpTextureWrap != null)
{ {
ImGui.Image(_pfpTextureWrap.Handle, ImGuiHelpers.ScaledVector2(_pfpTextureWrap.Width, _pfpTextureWrap.Height)); ImGui.Image(_pfpTextureWrap.Handle, ImGuiHelpers.ScaledVector2(_pfpTextureWrap.Width, _pfpTextureWrap.Height));
}
var spacing = ImGui.GetStyle().ItemSpacing.X;
ImGuiHelpers.ScaledRelativeSameLine(256, spacing); ImGuiHelpers.ScaledRelativeSameLine(256, spacing);
}
using (_uiSharedService.GameFont.Push()) using (_uiSharedService.GameFont.Push())
{ {
var descriptionTextSize = ImGui.CalcTextSize(profile.Description, hideTextAfterDoubleHash: false, 256f); var descriptionTextSize = ImGui.CalcTextSize(profile.Description, hideTextAfterDoubleHash: false, 256f);
@@ -157,7 +158,7 @@ public class EditProfileUi : WindowMediatorSubscriberBase
UiSharedService.AttachToolTip("Clear your currently uploaded profile picture"); UiSharedService.AttachToolTip("Clear your currently uploaded profile picture");
if (_showFileDialogError) if (_showFileDialogError)
{ {
UiSharedService.ColorTextWrapped("The profile picture must be a PNG file with a maximum height and width of 256px and 250KiB size", ImGuiColors.DalamudRed); UiSharedService.ColorTextWrapped("The profile picture must be a PNG file with a maximum height and width of 256px and 250KiB size", UiSharedService.AccentColor);
} }
var isNsfw = profile.IsNSFW; var isNsfw = profile.IsNSFW;
if (ImGui.Checkbox("Profile is NSFW", ref isNsfw)) if (ImGui.Checkbox("Profile is NSFW", ref isNsfw))

View File

@@ -175,7 +175,7 @@ internal class EventViewerUI : WindowMediatorSubscriberBase
{ {
EventSeverity.Informational => new Vector4(), EventSeverity.Informational => new Vector4(),
EventSeverity.Warning => ImGuiColors.DalamudYellow, EventSeverity.Warning => ImGuiColors.DalamudYellow,
EventSeverity.Error => ImGuiColors.DalamudRed, EventSeverity.Error => UiSharedService.AccentColor,
_ => new Vector4() _ => new Vector4()
}; };

View File

@@ -77,9 +77,21 @@ public class UidDisplayHandler
if (_popupTime > DateTime.UtcNow || !_mareConfigService.Current.ProfilesShow) if (_popupTime > DateTime.UtcNow || !_mareConfigService.Current.ProfilesShow)
{ {
ImGui.SetTooltip("Left click to switch between UID display and nick" + Environment.NewLine // Build tooltip; prepend last-seen when player is offline or not visible
string tooltip = "Left click to switch between UID display and nick" + Environment.NewLine
+ "Right click to change nick for " + pair.UserData.AliasOrUID + Environment.NewLine + "Right click to change nick for " + pair.UserData.AliasOrUID + Environment.NewLine
+ "Middle Mouse Button to open their profile in a separate window"); + "Middle Mouse Button to open their profile in a separate window";
if (!pair.IsOnline || !pair.IsVisible)
{
var lastSeen = _serverManager.GetNameForUid(pair.UserData.UID);
if (!string.IsNullOrEmpty(lastSeen))
{
tooltip = "Vu sous : " + lastSeen + Environment.NewLine + tooltip;
}
}
ImGui.SetTooltip(tooltip);
} }
else if (_popupTime < DateTime.UtcNow && !_popupShown) else if (_popupTime < DateTime.UtcNow && !_popupShown)
{ {
@@ -142,10 +154,15 @@ public class UidDisplayHandler
public (bool isUid, string text) GetPlayerText(Pair pair) public (bool isUid, string text) GetPlayerText(Pair pair)
{ {
var textIsUid = true;
bool showUidInsteadOfName = ShowUidInsteadOfName(pair); bool showUidInsteadOfName = ShowUidInsteadOfName(pair);
if (showUidInsteadOfName)
{
return (true, pair.UserData.UID);
}
var textIsUid = true;
string? playerText = _serverManager.GetNoteForUid(pair.UserData.UID); string? playerText = _serverManager.GetNoteForUid(pair.UserData.UID);
if (!showUidInsteadOfName && playerText != null) if (playerText != null)
{ {
if (string.IsNullOrEmpty(playerText)) if (string.IsNullOrEmpty(playerText))
{ {
@@ -161,7 +178,7 @@ public class UidDisplayHandler
playerText = pair.UserData.AliasOrUID; playerText = pair.UserData.AliasOrUID;
} }
if (_mareConfigService.Current.ShowCharacterNames && textIsUid && !showUidInsteadOfName) if (_mareConfigService.Current.ShowCharacterNames && textIsUid && pair.IsOnline && pair.IsVisible)
{ {
var name = pair.PlayerName; var name = pair.PlayerName;
if (name != null) if (name != null)
@@ -197,8 +214,11 @@ public class UidDisplayHandler
private bool ShowUidInsteadOfName(Pair pair) private bool ShowUidInsteadOfName(Pair pair)
{ {
_showUidForEntry.TryGetValue(pair.UserData.UID, out var showUidInsteadOfName); if (_showUidForEntry.TryGetValue(pair.UserData.UID, out var showUidInsteadOfName))
{
return showUidInsteadOfName; return showUidInsteadOfName;
} }
return false;
}
} }

View File

@@ -38,7 +38,7 @@ public partial class IntroUi : WindowMediatorSubscriberBase
public IntroUi(ILogger<IntroUi> logger, UiSharedService uiShared, MareConfigService configService, public IntroUi(ILogger<IntroUi> logger, UiSharedService uiShared, MareConfigService configService,
CacheMonitor fileCacheManager, ServerConfigurationManager serverConfigurationManager, MareMediator mareMediator, CacheMonitor fileCacheManager, ServerConfigurationManager serverConfigurationManager, MareMediator mareMediator,
PerformanceCollectorService performanceCollectorService, DalamudUtilService dalamudUtilService, AccountRegistrationService registerService) : base(logger, mareMediator, "UmbraSync Setup", performanceCollectorService) PerformanceCollectorService performanceCollectorService, DalamudUtilService dalamudUtilService, AccountRegistrationService registerService) : base(logger, mareMediator, "Umbra Setup", performanceCollectorService)
{ {
_uiShared = uiShared; _uiShared = uiShared;
_configService = configService; _configService = configService;
@@ -69,17 +69,17 @@ public partial class IntroUi : WindowMediatorSubscriberBase
return _uiShared.ApiController.ServerState switch return _uiShared.ApiController.ServerState switch
{ {
ServerState.Connecting => ImGuiColors.DalamudYellow, ServerState.Connecting => ImGuiColors.DalamudYellow,
ServerState.Reconnecting => ImGuiColors.DalamudRed, ServerState.Reconnecting => UiSharedService.AccentColor,
ServerState.Connected => ImGuiColors.HealerGreen, ServerState.Connected => ImGuiColors.HealerGreen,
ServerState.Disconnected => ImGuiColors.DalamudYellow, ServerState.Disconnected => ImGuiColors.DalamudYellow,
ServerState.Disconnecting => ImGuiColors.DalamudYellow, ServerState.Disconnecting => ImGuiColors.DalamudYellow,
ServerState.Unauthorized => ImGuiColors.DalamudRed, ServerState.Unauthorized => UiSharedService.AccentColor,
ServerState.VersionMisMatch => ImGuiColors.DalamudRed, ServerState.VersionMisMatch => UiSharedService.AccentColor,
ServerState.Offline => ImGuiColors.DalamudRed, ServerState.Offline => UiSharedService.AccentColor,
ServerState.RateLimited => ImGuiColors.DalamudYellow, ServerState.RateLimited => ImGuiColors.DalamudYellow,
ServerState.NoSecretKey => ImGuiColors.DalamudYellow, ServerState.NoSecretKey => ImGuiColors.DalamudYellow,
ServerState.MultiChara => ImGuiColors.DalamudYellow, ServerState.MultiChara => ImGuiColors.DalamudYellow,
_ => ImGuiColors.DalamudRed _ => UiSharedService.AccentColor
}; };
} }
@@ -108,9 +108,9 @@ public partial class IntroUi : WindowMediatorSubscriberBase
if (!_configService.Current.AcceptedAgreement && !_readFirstPage) if (!_configService.Current.AcceptedAgreement && !_readFirstPage)
{ {
_uiShared.BigText("Welcome to UmbraSync"); _uiShared.BigText("Welcome to Umbra");
ImGui.Separator(); ImGui.Separator();
UiSharedService.TextWrapped("UmbraSync is a plugin that will replicate your full current character state including all Penumbra mods to other paired users. " + UiSharedService.TextWrapped("Umbra is a plugin that will replicate your full current character state including all Penumbra mods to other paired users. " +
"Note that you will have to have Penumbra as well as Glamourer installed to use this plugin."); "Note that you will have to have Penumbra as well as Glamourer installed to use this plugin.");
UiSharedService.TextWrapped("We will have to setup a few things first before you can start using this plugin. Click on next to continue."); UiSharedService.TextWrapped("We will have to setup a few things first before you can start using this plugin. Click on next to continue.");
@@ -140,36 +140,47 @@ public partial class IntroUi : WindowMediatorSubscriberBase
{ {
using (_uiShared.UidFont.Push()) using (_uiShared.UidFont.Push())
{ {
ImGui.TextUnformatted("Agreement of Usage of Service"); ImGui.TextUnformatted("Conditions d'utilisation");
} }
ImGui.Separator(); ImGui.Separator();
ImGui.SetWindowFontScale(1.5f); ImGui.SetWindowFontScale(1.5f);
string readThis = "READ THIS CAREFULLY"; string readThis = "MERCI DE LIRE ATTENTIVEMENT";
Vector2 textSize = ImGui.CalcTextSize(readThis); Vector2 textSize = ImGui.CalcTextSize(readThis);
ImGui.SetCursorPosX(ImGui.GetWindowSize().X / 2 - textSize.X / 2); ImGui.SetCursorPosX(ImGui.GetWindowSize().X / 2 - textSize.X / 2);
UiSharedService.ColorText(readThis, ImGuiColors.DalamudRed); UiSharedService.ColorText(readThis, UiSharedService.AccentColor);
ImGui.SetWindowFontScale(1.0f); ImGui.SetWindowFontScale(1.0f);
ImGui.Separator(); ImGui.Separator();
UiSharedService.TextWrapped(""" UiSharedService.TextWrapped("""
All of the mod files currently active on your character as well as your current character state will be uploaded to the service you registered yourself at automatically. The plugin will exclusively upload the necessary mod files and not the whole mod. Pour utiliser les services UmbraSync, vous devez être â de plus de 18 ans, plus de 21 ans dans certaines juridictions.
"""); """);
UiSharedService.TextWrapped(""" UiSharedService.TextWrapped("""
If you are on a data capped internet connection, higher fees due to data usage depending on the amount of downloaded and uploaded mod files might occur. Mod files will be compressed on up- and download to save on bandwidth usage. Due to varying up- and download speeds, changes in characters might not be visible immediately. Files present on the service that already represent your active mod files will not be uploaded again. Tout les mods actuellement actifs sur votre personnage et ses états associés seront automatiquement téléchargés vers le serveur UmbraSync auquel vous vous êtes inscrit.Il sera téléchargé exclusivement les fichiers nécessaires à la synchronisation et non l'intégralité du mod.
"""); """);
UiSharedService.TextWrapped(""" UiSharedService.TextWrapped("""
The mod files you are uploading are confidential and will not be distributed to parties other than the ones who are requesting the exact same mod files. Please think about who you are going to pair since it is unavoidable that they will receive and locally cache the necessary mod files that you have currently in use. Locally cached mod files will have arbitrary file names to discourage attempts at replicating the original mod. Si vous disposez d'une connexion Internet limitée, des frais supplémentaires peuvent s'appliquer en fonction du nombre de fichiers envoyés et reçus. Les fichiers seront compressés afin d'économiser la bande passante. En raison des variations de vitesse de bit, les sychronisations peuvent ne pas être visible immédiatement.
"""); """);
UiSharedService.TextWrapped(""" UiSharedService.TextWrapped("""
The plugin creator tried their best to keep you secure. However, there is no guarantee for 100% security. Do not blindly pair your client with everyone. Les fichiers téléchargés sont confidentiels et ne seront pas distribués à des solutions tierces autres personnes. Uniquement les personnes avec qui vous êtes appairés demandent exactement les mêmes fichiers. Réfléchissez donc bien avec qui vous allez vous appairer.
"""); """);
UiSharedService.TextWrapped(""" UiSharedService.TextWrapped("""
Mod files that are saved on the service will remain on the service as long as there are requests for the files from clients. After a period of not being used, the mod files will be automatically deleted. Le gentil dev' a fait de son mieux pour assurer votre sécurité. Cependant le risque 0 n'existe pas. Ne vous appairez pas aveuglément avec n'importe qui.
"""); """);
UiSharedService.TextWrapped(""" UiSharedService.TextWrapped("""
This service is provided as-is. Après une periode d'inactivité, les mods enregistrés sur le serveur UmbraSync seront automatiquement supprimés.
"""); """);
UiSharedService.TextWrapped("""
Les comptes inactifs pendant 90 jours seront supprimés pour des raisons de stockage et de confidentialité.
""");
UiSharedService.TextWrapped("""
L'infrastructure Umbrasync est hebergé dans l'Union Européenne (Allemagne) et en Suisse. Vous acceptez alors de ne pas télécharger de contenu qui pourrait aller à l'encontre des législations de ces deux pays.
""");
UiSharedService.TextWrapped("""
Vous pouvez supprimer votre compte à tout moment. Votre compte et toutes les données associées seront supprimés dans un délai de 14 jours.
""");
UiSharedService.TextWrapped("""
Ce service est fourni tel quel.
""");
ImGui.Separator(); ImGui.Separator();
if (_timeoutTask?.IsCompleted ?? true) if (_timeoutTask?.IsCompleted ?? true)
@@ -197,15 +208,15 @@ This service is provided as-is.
if (!_uiShared.HasValidPenumbraModPath) if (!_uiShared.HasValidPenumbraModPath)
{ {
UiSharedService.ColorTextWrapped("You do not have a valid Penumbra path set. Open Penumbra and set up a valid path for the mod directory.", ImGuiColors.DalamudRed); UiSharedService.ColorTextWrapped("You do not have a valid Penumbra path set. Open Penumbra and set up a valid path for the mod directory.", UiSharedService.AccentColor);
} }
else else
{ {
UiSharedService.TextWrapped("To not unnecessary download files already present on your computer, UmbraSync will have to scan your Penumbra mod directory. " + UiSharedService.TextWrapped("To not unnecessary download files already present on your computer, Umbra will have to scan your Penumbra mod directory. " +
"Additionally, a local storage folder must be set where UmbraSync will download other character files to. " + "Additionally, a local storage folder must be set where Umbra will download other character files to. " +
"Once the storage folder is set and the scan complete, this page will automatically forward to registration at a service."); "Once the storage folder is set and the scan complete, this page will automatically forward to registration at a service.");
UiSharedService.TextWrapped("Note: The initial scan, depending on the amount of mods you have, might take a while. Please wait until it is completed."); UiSharedService.TextWrapped("Note: The initial scan, depending on the amount of mods you have, might take a while. Please wait until it is completed.");
UiSharedService.ColorTextWrapped("Warning: once past this step you should not delete the FileCache.csv of UmbraSync in the Plugin Configurations folder of Dalamud. " + UiSharedService.ColorTextWrapped("Warning: once past this step you should not delete the FileCache.csv of Umbra in the Plugin Configurations folder of Dalamud. " +
"Otherwise on the next launch a full re-scan of the file cache database will be initiated.", ImGuiColors.DalamudYellow); "Otherwise on the next launch a full re-scan of the file cache database will be initiated.", ImGuiColors.DalamudYellow);
UiSharedService.ColorTextWrapped("Warning: if the scan is hanging and does nothing for a long time, chances are high your Penumbra folder is not set up properly.", ImGuiColors.DalamudYellow); UiSharedService.ColorTextWrapped("Warning: if the scan is hanging and does nothing for a long time, chances are high your Penumbra folder is not set up properly.", ImGuiColors.DalamudYellow);
_uiShared.DrawCacheDirectorySetting(); _uiShared.DrawCacheDirectorySetting();
@@ -230,8 +241,8 @@ This service is provided as-is.
_configService.Current.UseCompactor = useFileCompactor; _configService.Current.UseCompactor = useFileCompactor;
_configService.Save(); _configService.Save();
} }
UiSharedService.ColorTextWrapped("The File Compactor can save a tremendeous amount of space on the hard disk for downloads through UmbraSync. It will incur a minor CPU penalty on download but can speed up " + UiSharedService.ColorTextWrapped("The File Compactor can save a tremendeous amount of space on the hard disk for downloads through Umbra. It will incur a minor CPU penalty on download but can speed up " +
"loading of other characters. It is recommended to keep it enabled. You can change this setting later anytime in the UmbraSync settings.", ImGuiColors.DalamudYellow); "loading of other characters. It is recommended to keep it enabled. You can change this setting later anytime in the Umbra settings.", ImGuiColors.DalamudYellow);
} }
} }
else if (!_uiShared.ApiController.IsConnected) else if (!_uiShared.ApiController.IsConnected)
@@ -239,7 +250,7 @@ This service is provided as-is.
using (_uiShared.UidFont.Push()) using (_uiShared.UidFont.Push())
ImGui.TextUnformatted("Service Registration"); ImGui.TextUnformatted("Service Registration");
ImGui.Separator(); ImGui.Separator();
UiSharedService.TextWrapped("To be able to use UmbraSync you will have to register an account."); UiSharedService.TextWrapped("To be able to use Umbra you will have to register an account.");
UiSharedService.TextWrapped("Refer to the instructions at the location you obtained this plugin for more information or support."); UiSharedService.TextWrapped("Refer to the instructions at the location you obtained this plugin for more information or support.");
ImGui.Separator(); ImGui.Separator();
@@ -251,8 +262,8 @@ This service is provided as-is.
{ {
ImGui.BeginDisabled(_registrationInProgress || _registrationSuccess || _secretKey.Length > 0); ImGui.BeginDisabled(_registrationInProgress || _registrationSuccess || _secretKey.Length > 0);
ImGui.Separator(); ImGui.Separator();
ImGui.TextUnformatted("If you have not used UmbraSync before, click below to register a new account."); ImGui.TextUnformatted("If you have not used Umbra before, click below to register a new account.");
if (_uiShared.IconTextButton(FontAwesomeIcon.Plus, "Register a new UmbraSync account")) if (_uiShared.IconTextButton(FontAwesomeIcon.Plus, "Register a new Umbra account"))
{ {
_registrationInProgress = true; _registrationInProgress = true;
_ = Task.Run(async () => { _ = Task.Run(async () => {
@@ -319,11 +330,11 @@ This service is provided as-is.
ImGui.InputText("", ref _secretKey, 64); ImGui.InputText("", ref _secretKey, 64);
if (_secretKey.Length > 0 && _secretKey.Length != 64) if (_secretKey.Length > 0 && _secretKey.Length != 64)
{ {
UiSharedService.ColorTextWrapped("Your secret key must be exactly 64 characters long.", ImGuiColors.DalamudRed); UiSharedService.ColorTextWrapped("Your secret key must be exactly 64 characters long.", UiSharedService.AccentColor);
} }
else if (_secretKey.Length == 64 && !HexRegex().IsMatch(_secretKey)) else if (_secretKey.Length == 64 && !HexRegex().IsMatch(_secretKey))
{ {
UiSharedService.ColorTextWrapped("Your secret key can only contain ABCDEF and the numbers 0-9.", ImGuiColors.DalamudRed); UiSharedService.ColorTextWrapped("Your secret key can only contain ABCDEF and the numbers 0-9.", UiSharedService.AccentColor);
} }
else if (_secretKey.Length == 64) else if (_secretKey.Length == 64)
{ {

View File

@@ -22,7 +22,7 @@ public class PermissionWindowUI : WindowMediatorSubscriberBase
public PermissionWindowUI(ILogger<PermissionWindowUI> logger, Pair pair, MareMediator mediator, UiSharedService uiSharedService, public PermissionWindowUI(ILogger<PermissionWindowUI> logger, Pair pair, MareMediator mediator, UiSharedService uiSharedService,
ApiController apiController, PerformanceCollectorService performanceCollectorService) ApiController apiController, PerformanceCollectorService performanceCollectorService)
: base(logger, mediator, "Permissions for " + pair.UserData.AliasOrUID + "###UmbraSyncSyncPermissions" + pair.UserData.UID, performanceCollectorService) : base(logger, mediator, "Permissions for " + pair.UserData.AliasOrUID + "###UmbraSyncPermissions" + pair.UserData.UID, performanceCollectorService)
{ {
Pair = pair; Pair = pair;
_uiSharedService = uiSharedService; _uiSharedService = uiSharedService;
@@ -128,6 +128,10 @@ public class PermissionWindowUI : WindowMediatorSubscriberBase
using (ImRaii.Disabled(!hasChanges)) using (ImRaii.Disabled(!hasChanges))
if (_uiSharedService.IconTextButton(Dalamud.Interface.FontAwesomeIcon.Save, "Save")) if (_uiSharedService.IconTextButton(Dalamud.Interface.FontAwesomeIcon.Save, "Save"))
{ {
Mediator.Publish(new PairSyncOverrideChanged(Pair.UserData.UID,
_ownPermissions.IsDisableSounds(),
_ownPermissions.IsDisableAnimations(),
_ownPermissions.IsDisableVFX()));
_ = _apiController.UserSetPairPermissions(new(Pair.UserData, _ownPermissions)); _ = _apiController.UserSetPairPermissions(new(Pair.UserData, _ownPermissions));
} }
UiSharedService.AttachToolTip("Save and apply all changes"); UiSharedService.AttachToolTip("Save and apply all changes");
@@ -148,10 +152,15 @@ public class PermissionWindowUI : WindowMediatorSubscriberBase
ImGui.SameLine(); ImGui.SameLine();
if (_uiSharedService.IconTextButton(Dalamud.Interface.FontAwesomeIcon.ArrowsSpin, "Reset to Default")) if (_uiSharedService.IconTextButton(Dalamud.Interface.FontAwesomeIcon.ArrowsSpin, "Reset to Default"))
{ {
var defaults = _uiSharedService.ConfigService.Current;
_ownPermissions.SetPaused(false); _ownPermissions.SetPaused(false);
_ownPermissions.SetDisableVFX(false); _ownPermissions.SetDisableSounds(defaults.DefaultDisableSounds);
_ownPermissions.SetDisableSounds(false); _ownPermissions.SetDisableAnimations(defaults.DefaultDisableAnimations);
_ownPermissions.SetDisableAnimations(false); _ownPermissions.SetDisableVFX(defaults.DefaultDisableVfx);
Mediator.Publish(new PairSyncOverrideChanged(Pair.UserData.UID,
_ownPermissions.IsDisableSounds(),
_ownPermissions.IsDisableAnimations(),
_ownPermissions.IsDisableVFX()));
_ = _apiController.UserSetPairPermissions(new(Pair.UserData, _ownPermissions)); _ = _apiController.UserSetPairPermissions(new(Pair.UserData, _ownPermissions));
} }
UiSharedService.AttachToolTip("This will set all permissions to their default setting"); UiSharedService.AttachToolTip("This will set all permissions to their default setting");

View File

@@ -24,7 +24,7 @@ public class PlayerAnalysisUI : WindowMediatorSubscriberBase
public PlayerAnalysisUI(ILogger<PlayerAnalysisUI> logger, Pair pair, MareMediator mediator, UiSharedService uiSharedService, public PlayerAnalysisUI(ILogger<PlayerAnalysisUI> logger, Pair pair, MareMediator mediator, UiSharedService uiSharedService,
PerformanceCollectorService performanceCollectorService) PerformanceCollectorService performanceCollectorService)
: base(logger, mediator, "Character Data Analysis for " + pair.UserData.AliasOrUID + "###UmbraSyncPairAnalysis" + pair.UserData.UID, performanceCollectorService) : base(logger, mediator, "Character Data Analysis for " + pair.UserData.AliasOrUID + "###UmbraPairAnalysis" + pair.UserData.UID, performanceCollectorService)
{ {
Pair = pair; Pair = pair;
_uiSharedService = uiSharedService; _uiSharedService = uiSharedService;

View File

@@ -29,7 +29,7 @@ public class PopoutProfileUi : WindowMediatorSubscriberBase
public PopoutProfileUi(ILogger<PopoutProfileUi> logger, MareMediator mediator, UiSharedService uiSharedService, public PopoutProfileUi(ILogger<PopoutProfileUi> logger, MareMediator mediator, UiSharedService uiSharedService,
ServerConfigurationManager serverManager, MareConfigService mareConfigService, ServerConfigurationManager serverManager, MareConfigService mareConfigService,
MareProfileManager mareProfileManager, PairManager pairManager, PerformanceCollectorService performanceCollectorService) : base(logger, mediator, "###UmbraSyncSyncPopoutProfileUI", performanceCollectorService) MareProfileManager mareProfileManager, PairManager pairManager, PerformanceCollectorService performanceCollectorService) : base(logger, mediator, "###UmbraSyncPopoutProfileUI", performanceCollectorService)
{ {
_uiSharedService = uiSharedService; _uiSharedService = uiSharedService;
_serverManager = serverManager; _serverManager = serverManager;
@@ -113,7 +113,7 @@ public class PopoutProfileUi : WindowMediatorSubscriberBase
UiSharedService.ColorText(note, ImGuiColors.DalamudGrey); UiSharedService.ColorText(note, ImGuiColors.DalamudGrey);
} }
string status = _pair.IsVisible ? "Visible" : (_pair.IsOnline ? "Online" : "Offline"); string status = _pair.IsVisible ? "Visible" : (_pair.IsOnline ? "Online" : "Offline");
UiSharedService.ColorText(status, (_pair.IsVisible || _pair.IsOnline) ? ImGuiColors.HealerGreen : ImGuiColors.DalamudRed); UiSharedService.ColorText(status, (_pair.IsVisible || _pair.IsOnline) ? ImGuiColors.HealerGreen : UiSharedService.AccentColor);
if (_pair.IsVisible) if (_pair.IsVisible)
{ {
ImGui.SameLine(); ImGui.SameLine();

View File

@@ -14,6 +14,7 @@ using MareSynchronos.MareConfiguration.Models;
using MareSynchronos.PlayerData.Handlers; using MareSynchronos.PlayerData.Handlers;
using MareSynchronos.PlayerData.Pairs; using MareSynchronos.PlayerData.Pairs;
using MareSynchronos.Services; using MareSynchronos.Services;
using MareSynchronos.Services.AutoDetect;
using MareSynchronos.Services.Mediator; using MareSynchronos.Services.Mediator;
using MareSynchronos.Services.ServerConfiguration; using MareSynchronos.Services.ServerConfiguration;
using MareSynchronos.WebAPI; using MareSynchronos.WebAPI;
@@ -44,12 +45,14 @@ public class SettingsUi : WindowMediatorSubscriberBase
private readonly PairManager _pairManager; private readonly PairManager _pairManager;
private readonly ChatService _chatService; private readonly ChatService _chatService;
private readonly GuiHookService _guiHookService; private readonly GuiHookService _guiHookService;
private readonly AutoDetectSuppressionService _autoDetectSuppressionService;
private readonly PerformanceCollectorService _performanceCollector; private readonly PerformanceCollectorService _performanceCollector;
private readonly PlayerPerformanceConfigService _playerPerformanceConfigService; private readonly PlayerPerformanceConfigService _playerPerformanceConfigService;
private readonly PlayerPerformanceService _playerPerformanceService; private readonly PlayerPerformanceService _playerPerformanceService;
private readonly AccountRegistrationService _registerService; private readonly AccountRegistrationService _registerService;
private readonly ServerConfigurationManager _serverConfigurationManager; private readonly ServerConfigurationManager _serverConfigurationManager;
private readonly UiSharedService _uiShared; private readonly UiSharedService _uiShared;
private static readonly string DtrDefaultPreviewText = DtrEntry.DefaultGlyph + " 123";
private bool _deleteAccountPopupModalShown = false; private bool _deleteAccountPopupModalShown = false;
private string _lastTab = string.Empty; private string _lastTab = string.Empty;
private bool? _notesSuccessfullyApplied = null; private bool? _notesSuccessfullyApplied = null;
@@ -76,7 +79,8 @@ public class SettingsUi : WindowMediatorSubscriberBase
FileCacheManager fileCacheManager, FileCacheManager fileCacheManager,
FileCompactor fileCompactor, ApiController apiController, FileCompactor fileCompactor, ApiController apiController,
IpcManager ipcManager, IpcProvider ipcProvider, CacheMonitor cacheMonitor, IpcManager ipcManager, IpcProvider ipcProvider, CacheMonitor cacheMonitor,
DalamudUtilService dalamudUtilService, AccountRegistrationService registerService) : base(logger, mediator, "UmbraSync Settings", performanceCollector) DalamudUtilService dalamudUtilService, AccountRegistrationService registerService,
AutoDetectSuppressionService autoDetectSuppressionService) : base(logger, mediator, "Umbra Settings", performanceCollector)
{ {
_configService = configService; _configService = configService;
_pairManager = pairManager; _pairManager = pairManager;
@@ -95,6 +99,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
_cacheMonitor = cacheMonitor; _cacheMonitor = cacheMonitor;
_dalamudUtilService = dalamudUtilService; _dalamudUtilService = dalamudUtilService;
_registerService = registerService; _registerService = registerService;
_autoDetectSuppressionService = autoDetectSuppressionService;
_fileCompactor = fileCompactor; _fileCompactor = fileCompactor;
_uiShared = uiShared; _uiShared = uiShared;
AllowClickthrough = false; AllowClickthrough = false;
@@ -204,12 +209,95 @@ public class SettingsUi : WindowMediatorSubscriberBase
ImGui.AlignTextToFramePadding(); ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("0 = No limit/infinite"); ImGui.TextUnformatted("0 = No limit/infinite");
bool enableDownloadQueue = _configService.Current.EnableDownloadQueue;
if (ImGui.Checkbox("Activer la file de téléchargements", ref enableDownloadQueue))
{
_configService.Current.EnableDownloadQueue = enableDownloadQueue;
_configService.Save();
}
UiSharedService.AttachToolTip("Lance les téléchargements de personnages de manière séquentielle plutôt que tous en même temps. "
+ "Quand l'option est activée, seul le nombre configuré de téléchargements fonctionne en parallèle.");
ImGui.SetNextItemWidth(250 * ImGuiHelpers.GlobalScale); ImGui.SetNextItemWidth(250 * ImGuiHelpers.GlobalScale);
if (ImGui.SliderInt("Maximum Parallel Downloads", ref maxParallelDownloads, 1, 10)) if (ImGui.SliderInt("Maximum Parallel Downloads", ref maxParallelDownloads, 1, 10))
{ {
_configService.Current.ParallelDownloads = maxParallelDownloads; _configService.Current.ParallelDownloads = maxParallelDownloads;
_configService.Save(); _configService.Save();
} }
UiSharedService.AttachToolTip(enableDownloadQueue
? "Nombre maximal de téléchargements de personnages autorisés simultanément lorsque la file est activée."
: "Nombre maximal de flux de fichiers lancés en parallèle pour chaque téléchargement.");
ImGui.Separator();
_uiShared.BigText("AutoDetect");
bool isAutoDetectSuppressed = _autoDetectSuppressionService?.IsSuppressed ?? false;
bool enableDiscovery = _configService.Current.EnableAutoDetectDiscovery;
using (ImRaii.Disabled(isAutoDetectSuppressed))
{
if (ImGui.Checkbox("Activer l'AutoDetect", ref enableDiscovery))
{
_configService.Current.EnableAutoDetectDiscovery = enableDiscovery;
_configService.Save();
// notify services of toggle
Mediator.Publish(new NearbyDetectionToggled(enableDiscovery));
// if Nearby is turned OFF, force Allow Pair Requests OFF as well
if (!enableDiscovery && _configService.Current.AllowAutoDetectPairRequests)
{
_configService.Current.AllowAutoDetectPairRequests = false;
_configService.Save();
Mediator.Publish(new AllowPairRequestsToggled(false));
}
}
if (isAutoDetectSuppressed && ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
{
UiSharedService.AttachToolTip("AutoDetect est temporairement désactivé dans cette zone instanciée.");
}
}
// Allow Pair Requests is disabled when Nearby is OFF
using (ImRaii.Disabled(isAutoDetectSuppressed || !enableDiscovery))
{
bool allowRequests = _configService.Current.AllowAutoDetectPairRequests;
if (ImGui.Checkbox("Activer les invitations entrantes", ref allowRequests))
{
_configService.Current.AllowAutoDetectPairRequests = allowRequests;
_configService.Save();
// notify services of toggle
Mediator.Publish(new AllowPairRequestsToggled(allowRequests));
// user-facing info toast
Mediator.Publish(new NotificationMessage(
"AutoDetect",
allowRequests ? "Invitations entrantes autorisées : les autres peuvent vous inviter." : "Invitations entrantes désactivées : les autres ne peuvent pas vous inviter.",
NotificationType.Info,
default));
}
if (isAutoDetectSuppressed && ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
{
UiSharedService.AttachToolTip("AutoDetect est temporairement désactivé dans cette zone instanciée.");
}
}
// Radius only available when both Nearby and Allow Pair Requests are ON
if (!isAutoDetectSuppressed && enableDiscovery && _configService.Current.AllowAutoDetectPairRequests)
{
ImGui.Indent();
int maxMeters = _configService.Current.AutoDetectMaxDistanceMeters;
ImGui.SetNextItemWidth(200 * ImGuiHelpers.GlobalScale);
if (ImGui.SliderInt("Distance max (en mètre)", ref maxMeters, 5, 100))
{
_configService.Current.AutoDetectMaxDistanceMeters = maxMeters;
_configService.Save();
}
ImGui.Unindent();
}
else if (isAutoDetectSuppressed)
{
UiSharedService.ColorTextWrapped("AutoDetect est verrouillé tant que vous restez dans une zone instanciée.", ImGuiColors.DalamudYellow);
}
ImGui.Separator(); ImGui.Separator();
_uiShared.BigText("Transfer UI"); _uiShared.BigText("Transfer UI");
@@ -492,7 +580,6 @@ public class SettingsUi : WindowMediatorSubscriberBase
if (ImGui.Checkbox($"Enable chat for this syncshell##{gid}", ref shellEnabled)) if (ImGui.Checkbox($"Enable chat for this syncshell##{gid}", ref shellEnabled))
{ {
// If there is an active group with the same syncshell number, pick a new one
int nextNumber = 1; int nextNumber = 1;
bool conflict = false; bool conflict = false;
foreach (var otherGroup in _pairManager.Groups) foreach (var otherGroup in _pairManager.Groups)
@@ -512,17 +599,13 @@ public class SettingsUi : WindowMediatorSubscriberBase
using var pushDisabled = ImRaii.Disabled(!shellEnabled); using var pushDisabled = ImRaii.Disabled(!shellEnabled);
ImGui.SetNextItemWidth(50 * ImGuiHelpers.GlobalScale); ImGui.SetNextItemWidth(50 * ImGuiHelpers.GlobalScale);
// _uiShared.DrawCombo() remembers the selected option -- we don't want that, because the value can change
if (ImGui.BeginCombo("Syncshell number##{gid}", $"{shellNumber}")) if (ImGui.BeginCombo("Syncshell number##{gid}", $"{shellNumber}"))
{ {
// Same hard-coded number in CommandManagerService
for (int i = 1; i <= ChatService.CommandMaxNumber; ++i) for (int i = 1; i <= ChatService.CommandMaxNumber; ++i)
{ {
if (ImGui.Selectable($"{i}", i == shellNumber)) if (ImGui.Selectable($"{i}", i == shellNumber))
{ {
// Find an active group with the same syncshell number as selected, and swap it
// This logic can leave duplicate IDs present in the config but its not critical
foreach (var otherGroup in _pairManager.Groups) foreach (var otherGroup in _pairManager.Groups)
{ {
if (gid.Equals(otherGroup.Key.GID, StringComparison.Ordinal)) continue; if (gid.Equals(otherGroup.Key.GID, StringComparison.Ordinal)) continue;
@@ -581,32 +664,32 @@ public class SettingsUi : WindowMediatorSubscriberBase
_uiShared.BigText("Advanced"); _uiShared.BigText("Advanced");
bool mareApi = _configService.Current.MareAPI; bool mareApi = _configService.Current.MareAPI;
if (ImGui.Checkbox("Enable Mare Synchronos API", ref mareApi)) if (ImGui.Checkbox("Enable Umbra Sync API", ref mareApi))
{ {
_configService.Current.MareAPI = mareApi; _configService.Current.MareAPI = mareApi;
_configService.Save(); _configService.Save();
_ipcProvider.HandleMareImpersonation(); _ipcProvider.HandleMareImpersonation();
} }
_uiShared.DrawHelpText("Enables handling of the Mare Synchronos API. This currently includes:\n\n" + _uiShared.DrawHelpText("Enables handling of the Umbra Sync API. This currently includes:\n\n" +
" - MCDF loading support for other plugins\n" + " - MCDF loading support for other plugins\n" +
" - Blocking Moodles applications to paired users\n\n" + " - Blocking Moodles applications to paired users\n\n" +
"If the Mare Synchronos plugin is loaded while this option is enabled, control of its API will be relinquished."); "If the Umbra Sync plugin is loaded while this option is enabled, control of its API will be relinquished.");
using (_ = ImRaii.PushIndent()) using (_ = ImRaii.PushIndent())
{ {
ImGui.SameLine(300.0f * ImGuiHelpers.GlobalScale); ImGui.SameLine(300.0f * ImGuiHelpers.GlobalScale);
if (_ipcProvider.ImpersonationActive) if (_ipcProvider.ImpersonationActive)
{ {
UiSharedService.ColorTextWrapped("Mare API active!", ImGuiColors.HealerGreen); UiSharedService.ColorTextWrapped("Umbra API active!", UiSharedService.AccentColor);
} }
else else
{ {
if (!mareApi) if (!mareApi)
UiSharedService.ColorTextWrapped("Mare API inactive: Option is disabled", ImGuiColors.DalamudYellow); UiSharedService.ColorTextWrapped("Umbra API inactive: Option is disabled", ImGuiColors.DalamudYellow);
else if (_ipcProvider.MarePluginEnabled) else if (_ipcProvider.MarePluginEnabled)
UiSharedService.ColorTextWrapped("Mare API inactive: Mare plugin is loaded", ImGuiColors.DalamudYellow); UiSharedService.ColorTextWrapped("Umbra API inactive: Umbra plugin is loaded", ImGuiColors.DalamudYellow);
else else
UiSharedService.ColorTextWrapped("Mare API inactive: Unknown reason", ImGuiColors.DalamudRed); UiSharedService.ColorTextWrapped("Umbra API inactive: Unknown reason", UiSharedService.AccentColor);
} }
} }
@@ -728,7 +811,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
_uiShared.BigText("Storage"); _uiShared.BigText("Storage");
UiSharedService.TextWrapped("UmbraSync stores downloaded files from paired people permanently. This is to improve loading performance and requiring less downloads. " + UiSharedService.TextWrapped("Umbra stores downloaded files from paired people permanently. This is to improve loading performance and requiring less downloads. " +
"The storage governs itself by clearing data beyond the set storage size. Please set the storage size accordingly. It is not necessary to manually clear the storage."); "The storage governs itself by clearing data beyond the set storage size. Please set the storage size accordingly. It is not necessary to manually clear the storage.");
_uiShared.DrawFileScanState(); _uiShared.DrawFileScanState();
@@ -745,7 +828,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
} }
ImGui.AlignTextToFramePadding(); ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("Monitoring UmbraSync Storage Folder: " + (_cacheMonitor.MareWatcher?.Path ?? "Not monitoring")); ImGui.TextUnformatted("Monitoring Umbra Storage Folder: " + (_cacheMonitor.MareWatcher?.Path ?? "Not monitoring"));
if (string.IsNullOrEmpty(_cacheMonitor.MareWatcher?.Path)) if (string.IsNullOrEmpty(_cacheMonitor.MareWatcher?.Path))
{ {
ImGui.SameLine(); ImGui.SameLine();
@@ -763,7 +846,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
_cacheMonitor.StartPenumbraWatcher(_ipcManager.Penumbra.ModDirectory); _cacheMonitor.StartPenumbraWatcher(_ipcManager.Penumbra.ModDirectory);
_cacheMonitor.InvokeScan(); _cacheMonitor.InvokeScan();
} }
UiSharedService.AttachToolTip("Attempts to resume monitoring for both Penumbra and UmbraSync Storage. " UiSharedService.AttachToolTip("Attempts to resume monitoring for both Penumbra and Umbra Storage. "
+ "Resuming the monitoring will also force a full scan to run." + Environment.NewLine + "Resuming the monitoring will also force a full scan to run." + Environment.NewLine
+ "If the button remains present after clicking it, consult /xllog for errors"); + "If the button remains present after clicking it, consult /xllog for errors");
} }
@@ -776,8 +859,8 @@ public class SettingsUi : WindowMediatorSubscriberBase
_cacheMonitor.StopMonitoring(); _cacheMonitor.StopMonitoring();
} }
} }
UiSharedService.AttachToolTip("Stops the monitoring for both Penumbra and UmbraSync Storage. " UiSharedService.AttachToolTip("Stops the monitoring for both Penumbra and Umbra Storage. "
+ "Do not stop the monitoring, unless you plan to move the Penumbra and UmbraSync Storage folders, to ensure correct functionality of UmbraSync." + Environment.NewLine + "Do not stop the monitoring, unless you plan to move the Penumbra and Umbra Storage folders, to ensure correct functionality of Umbra." + Environment.NewLine
+ "If you stop the monitoring to move folders around, resume it after you are finished moving the files." + "If you stop the monitoring to move folders around, resume it after you are finished moving the files."
+ UiSharedService.TooltipSeparator + "Hold CTRL to enable this button"); + UiSharedService.TooltipSeparator + "Hold CTRL to enable this button");
} }
@@ -794,7 +877,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
bool useFileCompactor = _configService.Current.UseCompactor; bool useFileCompactor = _configService.Current.UseCompactor;
if (!useFileCompactor && !isLinux) if (!useFileCompactor && !isLinux)
{ {
UiSharedService.ColorTextWrapped("Hint: To free up space when using UmbraSync consider enabling the File Compactor", ImGuiColors.DalamudYellow); UiSharedService.ColorTextWrapped("Hint: To free up space when using Umbra consider enabling the File Compactor", ImGuiColors.DalamudYellow);
} }
if (isLinux || !_cacheMonitor.StorageisNTFS) ImGui.BeginDisabled(); if (isLinux || !_cacheMonitor.StorageisNTFS) ImGui.BeginDisabled();
if (ImGui.Checkbox("Use file compactor", ref useFileCompactor)) if (ImGui.Checkbox("Use file compactor", ref useFileCompactor))
@@ -903,7 +986,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
} }
UiSharedService.AttachToolTip("You normally do not need to do this. THIS IS NOT SOMETHING YOU SHOULD BE DOING TO TRY TO FIX SYNC ISSUES." + Environment.NewLine UiSharedService.AttachToolTip("You normally do not need to do this. THIS IS NOT SOMETHING YOU SHOULD BE DOING TO TRY TO FIX SYNC ISSUES." + Environment.NewLine
+ "This will solely remove all downloaded data from all players and will require you to re-download everything again." + Environment.NewLine + "This will solely remove all downloaded data from all players and will require you to re-download everything again." + Environment.NewLine
+ "UmbraSync's storage is self-clearing and will not surpass the limit you have set it to." + Environment.NewLine + "Umbra's storage is self-clearing and will not surpass the limit you have set it to." + Environment.NewLine
+ "If you still think you need to do this hold CTRL while pressing the button."); + "If you still think you need to do this hold CTRL while pressing the button.");
if (!_readClearCache) if (!_readClearCache)
ImGui.EndDisabled(); ImGui.EndDisabled();
@@ -936,11 +1019,11 @@ public class SettingsUi : WindowMediatorSubscriberBase
_uiShared.DrawHelpText("If this option is selected all already existing notes for UIDs will be overwritten by the imported notes."); _uiShared.DrawHelpText("If this option is selected all already existing notes for UIDs will be overwritten by the imported notes.");
if (_notesSuccessfullyApplied.HasValue && _notesSuccessfullyApplied.Value) if (_notesSuccessfullyApplied.HasValue && _notesSuccessfullyApplied.Value)
{ {
UiSharedService.ColorTextWrapped("User Notes successfully imported", ImGuiColors.HealerGreen); UiSharedService.ColorTextWrapped("User Notes successfully imported", UiSharedService.AccentColor);
} }
else if (_notesSuccessfullyApplied.HasValue && !_notesSuccessfullyApplied.Value) else if (_notesSuccessfullyApplied.HasValue && !_notesSuccessfullyApplied.Value)
{ {
UiSharedService.ColorTextWrapped("Attempt to import notes from clipboard failed. Check formatting and try again", ImGuiColors.DalamudRed); UiSharedService.ColorTextWrapped("Attempt to import notes from clipboard failed. Check formatting and try again", UiSharedService.AccentColor);
} }
var openPopupOnAddition = _configService.Current.OpenPopupOnAdd; var openPopupOnAddition = _configService.Current.OpenPopupOnAdd;
@@ -975,14 +1058,14 @@ public class SettingsUi : WindowMediatorSubscriberBase
_configService.Current.EnableRightClickMenus = enableRightClickMenu; _configService.Current.EnableRightClickMenus = enableRightClickMenu;
_configService.Save(); _configService.Save();
} }
_uiShared.DrawHelpText("This will add UmbraSync related right click menu entries in the game UI on paired players."); _uiShared.DrawHelpText("This will add Umbra related right click menu entries in the game UI on paired players.");
if (ImGui.Checkbox("Display status and visible pair count in Server Info Bar", ref enableDtrEntry)) if (ImGui.Checkbox("Display status and visible pair count in Server Info Bar", ref enableDtrEntry))
{ {
_configService.Current.EnableDtrEntry = enableDtrEntry; _configService.Current.EnableDtrEntry = enableDtrEntry;
_configService.Save(); _configService.Save();
} }
_uiShared.DrawHelpText("This will add UmbraSync connection status and visible pair count in the Server Info Bar.\nYou can further configure this through your Dalamud Settings."); _uiShared.DrawHelpText("This will add Umbra connection status and visible pair count in the Server Info Bar.\nYou can further configure this through your Dalamud Settings.");
using (ImRaii.Disabled(!enableDtrEntry)) using (ImRaii.Disabled(!enableDtrEntry))
{ {
@@ -999,13 +1082,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
_configService.Save(); _configService.Save();
} }
ImGui.SetNextItemWidth(250 * ImGuiHelpers.GlobalScale); DrawDtrStyleCombo();
_uiShared.DrawCombo("Server Info Bar style", Enumerable.Range(0, DtrEntry.NumStyles), (i) => DtrEntry.RenderDtrStyle(i, "123"),
(i) =>
{
_configService.Current.DtrStyle = i;
_configService.Save();
}, _configService.Current.DtrStyle);
if (ImGui.Checkbox("Color-code the Server Info Bar entry according to status", ref useColorsInDtr)) if (ImGui.Checkbox("Color-code the Server Info Bar entry according to status", ref useColorsInDtr))
{ {
@@ -1041,7 +1118,9 @@ public class SettingsUi : WindowMediatorSubscriberBase
var useNameColors = _configService.Current.UseNameColors; var useNameColors = _configService.Current.UseNameColors;
var nameColors = _configService.Current.NameColors; var nameColors = _configService.Current.NameColors;
var autoPausedNameColors = _configService.Current.BlockedNameColors; var autoPausedNameColors = _configService.Current.BlockedNameColors;
if (ImGui.Checkbox("Color nameplates of paired players", ref useNameColors)) var typingIndicatorNameplates = _configService.Current.TypingIndicatorShowOnNameplates;
var typingIndicatorPartyList = _configService.Current.TypingIndicatorShowOnPartyList;
if (ImGui.Checkbox("Coloriser les plaques de nom des paires", ref useNameColors))
{ {
_configService.Current.UseNameColors = useNameColors; _configService.Current.UseNameColors = useNameColors;
_configService.Save(); _configService.Save();
@@ -1051,7 +1130,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
using (ImRaii.Disabled(!useNameColors)) using (ImRaii.Disabled(!useNameColors))
{ {
using var indent = ImRaii.PushIndent(); using var indent = ImRaii.PushIndent();
if (InputDtrColors("Character Name Color", ref nameColors)) if (InputDtrColors("Couleur du nom", ref nameColors))
{ {
_configService.Current.NameColors = nameColors; _configService.Current.NameColors = nameColors;
_configService.Save(); _configService.Save();
@@ -1060,7 +1139,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
ImGui.SameLine(); ImGui.SameLine();
if (InputDtrColors("Blocked Character Color", ref autoPausedNameColors)) if (InputDtrColors("Couleur des noms bloqués", ref autoPausedNameColors))
{ {
_configService.Current.BlockedNameColors = autoPausedNameColors; _configService.Current.BlockedNameColors = autoPausedNameColors;
_configService.Save(); _configService.Save();
@@ -1068,6 +1147,43 @@ public class SettingsUi : WindowMediatorSubscriberBase
} }
} }
if (ImGui.Checkbox("Afficher la bulle de frappe sur les plaques", ref typingIndicatorNameplates))
{
_configService.Current.TypingIndicatorShowOnNameplates = typingIndicatorNameplates;
_configService.Save();
}
_uiShared.DrawHelpText("Ajoute une bulle '...' sur la plaque des paires en train d'écrire.");
using (ImRaii.Disabled(!typingIndicatorNameplates))
{
using var indentTyping = ImRaii.PushIndent();
var bubbleSize = _configService.Current.TypingIndicatorBubbleSize;
TypingIndicatorBubbleSize? selectedBubbleSize = _uiShared.DrawCombo("Taille de la bulle de frappe##typingBubbleSize",
Enum.GetValues<TypingIndicatorBubbleSize>(),
size => size switch
{
TypingIndicatorBubbleSize.Small => "Petite",
TypingIndicatorBubbleSize.Medium => "Moyenne",
TypingIndicatorBubbleSize.Large => "Grande",
_ => size.ToString()
},
null,
bubbleSize);
if (selectedBubbleSize.HasValue && selectedBubbleSize.Value != bubbleSize)
{
_configService.Current.TypingIndicatorBubbleSize = selectedBubbleSize.Value;
_configService.Save();
}
}
if (ImGui.Checkbox("Tracer la frappe dans la liste de groupe", ref typingIndicatorPartyList))
{
_configService.Current.TypingIndicatorShowOnPartyList = typingIndicatorPartyList;
_configService.Save();
}
_uiShared.DrawHelpText("Consigne dans les journaux quand une paire du groupe est en train d'écrire (bulle visuelle ultérieure).");
if (ImGui.Checkbox("Show separate Visible group", ref showVisibleSeparate)) if (ImGui.Checkbox("Show separate Visible group", ref showVisibleSeparate))
{ {
_configService.Current.ShowVisibleUsersSeparately = showVisibleSeparate; _configService.Current.ShowVisibleUsersSeparately = showVisibleSeparate;
@@ -1212,6 +1328,14 @@ public class SettingsUi : WindowMediatorSubscriberBase
_uiShared.BigText("Global Configuration"); _uiShared.BigText("Global Configuration");
bool showSelfAnalysisWarnings = _playerPerformanceConfigService.Current.ShowSelfAnalysisWarnings;
if (ImGui.Checkbox("Display self-analysis warnings", ref showSelfAnalysisWarnings))
{
_playerPerformanceConfigService.Current.ShowSelfAnalysisWarnings = showSelfAnalysisWarnings;
_playerPerformanceConfigService.Save();
}
_uiShared.DrawHelpText("Disable to suppress UmbraSync chat warnings when your character exceeds the self-analysis thresholds.");
bool alwaysShrinkTextures = _playerPerformanceConfigService.Current.TextureShrinkMode == TextureShrinkMode.Always; bool alwaysShrinkTextures = _playerPerformanceConfigService.Current.TextureShrinkMode == TextureShrinkMode.Always;
bool deleteOriginalTextures = _playerPerformanceConfigService.Current.TextureShrinkDeleteOriginal; bool deleteOriginalTextures = _playerPerformanceConfigService.Current.TextureShrinkDeleteOriginal;
@@ -1255,9 +1379,9 @@ public class SettingsUi : WindowMediatorSubscriberBase
ImGui.TextUnformatted("Current VRAM utilization by all nearby players:"); ImGui.TextUnformatted("Current VRAM utilization by all nearby players:");
ImGui.SameLine(); ImGui.SameLine();
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.HealerGreen, totalVramBytes < 2.0 * 1024.0 * 1024.0 * 1024.0)) using (ImRaii.PushColor(ImGuiCol.Text, UiSharedService.AccentColor, totalVramBytes < 2.0 * 1024.0 * 1024.0 * 1024.0))
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudYellow, totalVramBytes >= 4.0 * 1024.0 * 1024.0 * 1024.0)) using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudYellow, totalVramBytes >= 4.0 * 1024.0 * 1024.0 * 1024.0))
using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed, totalVramBytes >= 6.0 * 1024.0 * 1024.0 * 1024.0)) using (ImRaii.PushColor(ImGuiCol.Text, UiSharedService.AccentColor, totalVramBytes >= 6.0 * 1024.0 * 1024.0 * 1024.0))
ImGui.TextUnformatted($"{totalVramBytes / 1024.0 / 1024.0 / 1024.0:0.00} GiB"); ImGui.TextUnformatted($"{totalVramBytes / 1024.0 / 1024.0 / 1024.0:0.00} GiB");
ImGui.Separator(); ImGui.Separator();
@@ -1610,8 +1734,6 @@ public class SettingsUi : WindowMediatorSubscriberBase
string selectedKeyName = string.Empty; string selectedKeyName = string.Empty;
if (selectedServer.SecretKeys.TryGetValue(item.SecretKeyIdx, out var selectedKey)) if (selectedServer.SecretKeys.TryGetValue(item.SecretKeyIdx, out var selectedKey))
selectedKeyName = selectedKey.FriendlyName; selectedKeyName = selectedKey.FriendlyName;
// _uiShared.DrawCombo() remembers the selected option -- we don't want that, because the value can change
if (ImGui.BeginCombo($"##{item.CharacterName}{i}", selectedKeyName)) if (ImGui.BeginCombo($"##{item.CharacterName}{i}", selectedKeyName))
{ {
foreach (var key in selectedServer.SecretKeys) foreach (var key in selectedServer.SecretKeys)
@@ -1744,7 +1866,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
if (true) // Enable registration button for all servers if (true) // Enable registration button for all servers
{ {
ImGui.SameLine(); ImGui.SameLine();
if (_uiShared.IconTextButton(FontAwesomeIcon.Plus, "Register a new UmbraSync account")) if (_uiShared.IconTextButton(FontAwesomeIcon.Plus, "Register a new Umbra account"))
{ {
_registrationInProgress = true; _registrationInProgress = true;
_ = Task.Run(async () => { _ = Task.Run(async () => {
@@ -1800,7 +1922,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
{ {
var serverName = selectedServer.ServerName; var serverName = selectedServer.ServerName;
var serverUri = selectedServer.ServerUri; var serverUri = selectedServer.ServerUri;
var isMain = string.Equals(serverName, ApiController.UmbraSyncServer, StringComparison.OrdinalIgnoreCase); var isMain = string.Equals(serverName, ApiController.UmbraServer, StringComparison.OrdinalIgnoreCase);
var flags = isMain ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None; var flags = isMain ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None;
if (ImGui.InputText("Service URI", ref serverUri, 255, flags)) if (ImGui.InputText("Service URI", ref serverUri, 255, flags))
@@ -1848,11 +1970,11 @@ public class SettingsUi : WindowMediatorSubscriberBase
{ {
ImGui.TextUnformatted("Service " + _serverConfigurationManager.CurrentServer!.ServerName + ":"); ImGui.TextUnformatted("Service " + _serverConfigurationManager.CurrentServer!.ServerName + ":");
ImGui.SameLine(); ImGui.SameLine();
ImGui.TextColored(ImGuiColors.ParsedGreen, "Available"); ImGui.TextColored(UiSharedService.AccentColor, "Available");
ImGui.SameLine(); ImGui.SameLine();
ImGui.TextUnformatted("("); ImGui.TextUnformatted("(");
ImGui.SameLine(); ImGui.SameLine();
ImGui.TextColored(ImGuiColors.ParsedGreen, _apiController.OnlineUsers.ToString(CultureInfo.InvariantCulture)); ImGui.TextColored(UiSharedService.AccentColor, _apiController.OnlineUsers.ToString(CultureInfo.InvariantCulture));
ImGui.SameLine(); ImGui.SameLine();
ImGui.TextUnformatted("Users Online"); ImGui.TextUnformatted("Users Online");
ImGui.SameLine(); ImGui.SameLine();
@@ -1861,6 +1983,11 @@ public class SettingsUi : WindowMediatorSubscriberBase
ImGui.Separator(); ImGui.Separator();
if (ImGui.BeginTabBar("mainTabBar")) if (ImGui.BeginTabBar("mainTabBar"))
{ {
var accent = UiSharedService.AccentColor;
var accentColor = ImGui.ColorConvertFloat4ToU32(accent);
ImGui.PushStyleColor(ImGuiCol.TabActive, accentColor);
ImGui.PushStyleColor(ImGuiCol.TabHovered, accentColor);
if (ImGui.BeginTabItem("General")) if (ImGui.BeginTabItem("General"))
{ {
DrawGeneral(); DrawGeneral();
@@ -1890,13 +2017,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
ImGui.BeginDisabled(_registrationInProgress); ImGui.BeginDisabled(_registrationInProgress);
DrawServerConfiguration(); DrawServerConfiguration();
ImGui.EndTabItem(); ImGui.EndTabItem();
ImGui.EndDisabled(); // _registrationInProgress ImGui.EndDisabled();
}
if (ImGui.BeginTabItem("Chat"))
{
DrawChatConfig();
ImGui.EndTabItem();
} }
if (ImGui.BeginTabItem("Advanced")) if (ImGui.BeginTabItem("Advanced"))
@@ -1904,11 +2025,44 @@ public class SettingsUi : WindowMediatorSubscriberBase
DrawAdvanced(); DrawAdvanced();
ImGui.EndTabItem(); ImGui.EndTabItem();
} }
ImGui.PopStyleColor(2);
ImGui.EndTabBar(); ImGui.EndTabBar();
} }
} }
private void DrawDtrStyleCombo()
{
var styleIndex = _configService.Current.DtrStyle;
string previewText = styleIndex == 0 ? DtrDefaultPreviewText : DtrEntry.RenderDtrStyle(styleIndex, "123");
ImGui.SetNextItemWidth(250 * ImGuiHelpers.GlobalScale);
bool comboOpen = ImGui.BeginCombo("Server Info Bar style", previewText);
if (comboOpen)
{
for (int i = 0; i < DtrEntry.NumStyles; i++)
{
string label = i == 0 ? DtrDefaultPreviewText : DtrEntry.RenderDtrStyle(i, "123");
bool isSelected = i == styleIndex;
if (ImGui.Selectable(label, isSelected))
{
_configService.Current.DtrStyle = i;
_configService.Save();
}
if (isSelected)
{
ImGui.SetItemDefaultFocus();
}
}
ImGui.EndCombo();
}
}
private void UiSharedService_GposeEnd() private void UiSharedService_GposeEnd()
{ {
IsOpen = _wasOpen; IsOpen = _wasOpen;

View File

@@ -26,7 +26,7 @@ public class StandaloneProfileUi : WindowMediatorSubscriberBase
public StandaloneProfileUi(ILogger<StandaloneProfileUi> logger, MareMediator mediator, UiSharedService uiBuilder, public StandaloneProfileUi(ILogger<StandaloneProfileUi> logger, MareMediator mediator, UiSharedService uiBuilder,
ServerConfigurationManager serverManager, MareProfileManager mareProfileManager, PairManager pairManager, Pair pair, ServerConfigurationManager serverManager, MareProfileManager mareProfileManager, PairManager pairManager, Pair pair,
PerformanceCollectorService performanceCollector) PerformanceCollectorService performanceCollector)
: base(logger, mediator, "Profile of " + pair.UserData.AliasOrUID + "##UmbraSyncSyncStandaloneProfileUI" + pair.UserData.AliasOrUID, performanceCollector) : base(logger, mediator, "Profile of " + pair.UserData.AliasOrUID + "##UmbraSyncStandaloneProfileUI" + pair.UserData.AliasOrUID, performanceCollector)
{ {
_uiSharedService = uiBuilder; _uiSharedService = uiBuilder;
_serverManager = serverManager; _serverManager = serverManager;
@@ -111,7 +111,7 @@ public class StandaloneProfileUi : WindowMediatorSubscriberBase
UiSharedService.ColorText(note, ImGuiColors.DalamudGrey); UiSharedService.ColorText(note, ImGuiColors.DalamudGrey);
} }
string status = Pair.IsVisible ? "Visible" : (Pair.IsOnline ? "Online" : "Offline"); string status = Pair.IsVisible ? "Visible" : (Pair.IsOnline ? "Online" : "Offline");
UiSharedService.ColorText(status, (Pair.IsVisible || Pair.IsOnline) ? ImGuiColors.HealerGreen : ImGuiColors.DalamudRed); UiSharedService.ColorText(status, (Pair.IsVisible || Pair.IsOnline) ? ImGuiColors.HealerGreen : UiSharedService.AccentColor);
if (Pair.IsVisible) if (Pair.IsVisible)
{ {
ImGui.SameLine(); ImGui.SameLine();

View File

@@ -374,7 +374,7 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
ImGui.TextUnformatted("Sound Sync"); ImGui.TextUnformatted("Sound Sync");
_uiSharedService.BooleanToColoredIcon(!isDisableSounds); _uiSharedService.BooleanToColoredIcon(!isDisableSounds);
ImGui.SameLine(230); ImGui.SameLine(230);
if (_uiSharedService.IconTextButton(isDisableSounds ? FontAwesomeIcon.VolumeUp : FontAwesomeIcon.VolumeMute, if (_uiSharedService.IconTextButton(isDisableSounds ? FontAwesomeIcon.VolumeMute : FontAwesomeIcon.VolumeUp,
isDisableSounds ? "Enable sound sync" : "Disable sound sync")) isDisableSounds ? "Enable sound sync" : "Disable sound sync"))
{ {
perm.SetDisableSounds(!perm.IsDisableSounds()); perm.SetDisableSounds(!perm.IsDisableSounds());
@@ -385,7 +385,7 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
ImGui.TextUnformatted("Animation Sync"); ImGui.TextUnformatted("Animation Sync");
_uiSharedService.BooleanToColoredIcon(!isDisableAnimations); _uiSharedService.BooleanToColoredIcon(!isDisableAnimations);
ImGui.SameLine(230); ImGui.SameLine(230);
if (_uiSharedService.IconTextButton(isDisableAnimations ? FontAwesomeIcon.Running : FontAwesomeIcon.Stop, if (_uiSharedService.IconTextButton(isDisableAnimations ? FontAwesomeIcon.WindowClose : FontAwesomeIcon.Running,
isDisableAnimations ? "Enable animation sync" : "Disable animation sync")) isDisableAnimations ? "Enable animation sync" : "Disable animation sync"))
{ {
perm.SetDisableAnimations(!perm.IsDisableAnimations()); perm.SetDisableAnimations(!perm.IsDisableAnimations());
@@ -396,7 +396,7 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
ImGui.TextUnformatted("VFX Sync"); ImGui.TextUnformatted("VFX Sync");
_uiSharedService.BooleanToColoredIcon(!isDisableVfx); _uiSharedService.BooleanToColoredIcon(!isDisableVfx);
ImGui.SameLine(230); ImGui.SameLine(230);
if (_uiSharedService.IconTextButton(isDisableVfx ? FontAwesomeIcon.Sun : FontAwesomeIcon.Circle, if (_uiSharedService.IconTextButton(isDisableVfx ? FontAwesomeIcon.TimesCircle : FontAwesomeIcon.Sun,
isDisableVfx ? "Enable VFX sync" : "Disable VFX sync")) isDisableVfx ? "Enable VFX sync" : "Disable VFX sync"))
{ {
perm.SetDisableVFX(!perm.IsDisableVFX()); perm.SetDisableVFX(!perm.IsDisableVFX());

View File

@@ -0,0 +1,533 @@
using System;
using System.Collections.Generic;
using System.Numerics;
using System.Linq;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface.Utility;
using Dalamud.Plugin.Services;
using MareSynchronos.API.Data;
using MareSynchronos.API.Data.Extensions;
using MareSynchronos.MareConfiguration;
using MareSynchronos.MareConfiguration.Models;
using MareSynchronos.PlayerData.Pairs;
using MareSynchronos.Services;
using MareSynchronos.Services.Mediator;
using MareSynchronos.WebAPI;
using Microsoft.Extensions.Logging;
using Dalamud.Interface.Textures.TextureWraps;
using FFXIVClientStructs.Interop;
using FFXIVClientStructs.FFXIV.Client.System.Framework;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Component.GUI;
namespace MareSynchronos.UI;
public sealed class TypingIndicatorOverlay : WindowMediatorSubscriberBase
{
private const int NameplateIconId = 61397;
private static readonly TimeSpan TypingDisplayTime = TimeSpan.FromSeconds(2);
private static readonly TimeSpan TypingDisplayDelay = TimeSpan.FromMilliseconds(500);
private static readonly TimeSpan TypingDisplayFade = TypingDisplayTime;
private readonly ILogger<TypingIndicatorOverlay> _logger;
private readonly MareConfigService _configService;
private readonly IGameGui _gameGui;
private readonly ITextureProvider _textureProvider;
private readonly IClientState _clientState;
private readonly PairManager _pairManager;
private readonly IPartyList _partyList;
private readonly IObjectTable _objectTable;
private readonly DalamudUtilService _dalamudUtil;
private readonly TypingIndicatorStateService _typingStateService;
private readonly ApiController _apiController;
public TypingIndicatorOverlay(ILogger<TypingIndicatorOverlay> logger, MareMediator mediator, PerformanceCollectorService performanceCollectorService,
MareConfigService configService, IGameGui gameGui, ITextureProvider textureProvider, IClientState clientState,
IPartyList partyList, IObjectTable objectTable, DalamudUtilService dalamudUtil, PairManager pairManager,
TypingIndicatorStateService typingStateService, ApiController apiController)
: base(logger, mediator, nameof(TypingIndicatorOverlay), performanceCollectorService)
{
_logger = logger;
_configService = configService;
_gameGui = gameGui;
_textureProvider = textureProvider;
_clientState = clientState;
_partyList = partyList;
_objectTable = objectTable;
_dalamudUtil = dalamudUtil;
_pairManager = pairManager;
_typingStateService = typingStateService;
_apiController = apiController;
RespectCloseHotkey = false;
IsOpen = true;
Flags |= ImGuiWindowFlags.NoDecoration | ImGuiWindowFlags.NoSavedSettings | ImGuiWindowFlags.NoFocusOnAppearing
| ImGuiWindowFlags.NoBackground | ImGuiWindowFlags.NoInputs | ImGuiWindowFlags.NoNav;
}
protected override void DrawInternal()
{
var viewport = ImGui.GetMainViewport();
ImGuiHelpers.ForceNextWindowMainViewport();
ImGui.SetWindowPos(viewport.Pos);
ImGui.SetWindowSize(viewport.Size);
if (!_clientState.IsLoggedIn)
return;
var showParty = _configService.Current.TypingIndicatorShowOnPartyList;
var showNameplates = _configService.Current.TypingIndicatorShowOnNameplates;
if (!showParty && !showNameplates)
return;
var overlayDrawList = ImGui.GetWindowDrawList();
var activeTypers = _typingStateService.GetActiveTypers(TypingDisplayTime);
var hasSelf = _typingStateService.TryGetSelfTyping(TypingDisplayTime, out var selfStart, out var selfLast);
var now = DateTime.UtcNow;
if (showParty)
{
DrawPartyIndicators(overlayDrawList, activeTypers, hasSelf, now, selfStart, selfLast);
}
if (showNameplates)
{
DrawNameplateIndicators(ImGui.GetWindowDrawList(), activeTypers, hasSelf, now, selfStart, selfLast);
}
}
private unsafe void DrawPartyIndicators(ImDrawListPtr drawList, IReadOnlyDictionary<string, (UserData User, DateTime FirstSeen, DateTime LastUpdate)> activeTypers,
bool selfActive, DateTime now, DateTime selfStart, DateTime selfLast)
{
var partyAddon = (AtkUnitBase*)_gameGui.GetAddonByName("_PartyList", 1).Address;
if (partyAddon == null || !partyAddon->IsVisible)
return;
if (selfActive
&& (now - selfStart) >= TypingDisplayDelay
&& (now - selfLast) <= TypingDisplayFade)
{
DrawPartyMemberTyping(drawList, partyAddon, 0);
}
foreach (var (uid, entry) in activeTypers)
{
if ((now - entry.LastUpdate) > TypingDisplayFade)
continue;
var pair = _pairManager.GetPairByUID(uid);
var targetIndex = -1;
var playerName = pair?.PlayerName;
var objectId = pair?.PlayerCharacterId ?? uint.MaxValue;
if (objectId != 0 && objectId != uint.MaxValue)
{
targetIndex = GetPartyIndexForObjectId(objectId);
if (targetIndex >= 0 && !string.IsNullOrEmpty(playerName))
{
var member = _partyList[targetIndex];
var memberName = member?.Name?.TextValue;
if (!string.IsNullOrEmpty(memberName) && !memberName.Equals(playerName, StringComparison.OrdinalIgnoreCase))
{
var nameIndex = GetPartyIndexForName(playerName);
targetIndex = nameIndex;
}
}
}
if (targetIndex < 0 && !string.IsNullOrEmpty(playerName))
{
targetIndex = GetPartyIndexForName(playerName);
}
if (targetIndex < 0)
continue;
DrawPartyMemberTyping(drawList, partyAddon, targetIndex);
}
}
private unsafe void DrawPartyMemberTyping(ImDrawListPtr drawList, AtkUnitBase* partyList, int memberIndex)
{
if (memberIndex < 0 || memberIndex > 7) return;
var nodeIndex = 23 - memberIndex;
if (partyList->UldManager.NodeListCount <= nodeIndex) return;
var memberNode = (AtkComponentNode*)partyList->UldManager.NodeList[nodeIndex];
if (memberNode == null || !memberNode->AtkResNode.IsVisible()) return;
var iconNode = memberNode->Component->UldManager.NodeListCount > 4 ? memberNode->Component->UldManager.NodeList[4] : null;
if (iconNode == null) return;
var align = partyList->UldManager.NodeList[3]->Y;
var partyScale = partyList->Scale;
var iconOffset = new Vector2(-14, 8) * partyScale;
var iconSize = new Vector2(iconNode->Width / 2f, iconNode->Height / 2f) * partyScale;
var iconPos = new Vector2(
partyList->X + (memberNode->AtkResNode.X * partyScale) + (iconNode->X * partyScale) + (iconNode->Width * partyScale / 2f),
partyList->Y + align + (memberNode->AtkResNode.Y * partyScale) + (iconNode->Y * partyScale) + (iconNode->Height * partyScale / 2f));
iconPos += iconOffset;
var texture = _textureProvider.GetFromGame("ui/uld/charamake_dataimport.tex").GetWrapOrEmpty();
if (texture == null) return;
drawList.AddImage(texture.Handle, iconPos, iconPos + iconSize, Vector2.Zero, Vector2.One,
ImGui.ColorConvertFloat4ToU32(new Vector4(1f, 1f, 1f, 0.9f)));
}
private unsafe void DrawNameplateIndicators(ImDrawListPtr drawList, IReadOnlyDictionary<string, (UserData User, DateTime FirstSeen, DateTime LastUpdate)> activeTypers,
bool selfActive, DateTime now, DateTime selfStart, DateTime selfLast)
{
var iconWrap = _textureProvider.GetFromGameIcon(NameplateIconId).GetWrapOrEmpty();
if (iconWrap == null || iconWrap.Handle == IntPtr.Zero)
return;
if (selfActive
&& _clientState.LocalPlayer != null
&& (now - selfStart) >= TypingDisplayDelay
&& (now - selfLast) <= TypingDisplayFade)
{
var selfId = GetEntityId(_clientState.LocalPlayer.Address);
if (selfId != 0 && !TryDrawNameplateBubble(drawList, iconWrap, selfId))
{
DrawWorldFallbackIcon(drawList, iconWrap, _clientState.LocalPlayer.Position);
}
}
foreach (var (uid, entry) in activeTypers)
{
if ((now - entry.LastUpdate) > TypingDisplayFade)
continue;
if (string.Equals(uid, _apiController.UID, StringComparison.Ordinal))
continue;
var pair = _pairManager.GetPairByUID(uid);
var objectId = pair?.PlayerCharacterId ?? 0;
var pairName = pair?.PlayerName ?? entry.User.AliasOrUID ?? string.Empty;
var pairIdent = pair?.Ident ?? string.Empty;
var isPartyMember = IsPartyMember(objectId, pairName);
var isRelevantMember = IsPlayerRelevant(pair, isPartyMember);
if (objectId != uint.MaxValue && objectId != 0 && TryDrawNameplateBubble(drawList, iconWrap, objectId))
{
_logger.LogTrace("TypingIndicator: drew nameplate bubble for {uid} (objectId={objectId})", uid, objectId);
continue;
}
var hasWorldPosition = TryResolveWorldPosition(pair, entry.User, objectId, out var worldPos);
var isNearby = hasWorldPosition && IsWithinRelevantDistance(worldPos);
if (!isRelevantMember && !isNearby)
continue;
if (pair == null)
{
_logger.LogTrace("TypingIndicator: no pair found for {uid}, attempting fallback", uid);
}
_logger.LogTrace("TypingIndicator: fallback draw for {uid} (objectId={objectId}, name={name}, ident={ident})",
uid, objectId, pairName, pairIdent);
if (hasWorldPosition)
{
DrawWorldFallbackIcon(drawList, iconWrap, worldPos);
_logger.LogTrace("TypingIndicator: fallback world draw for {uid} at {pos}", uid, worldPos);
}
else
{
_logger.LogTrace("TypingIndicator: could not resolve position for {uid}", uid);
}
}
}
private Vector2 GetConfiguredBubbleSize(float scaleX, float scaleY, bool isNameplateVisible, TypingIndicatorBubbleSize? overrideSize = null)
{
var sizeSetting = overrideSize ?? _configService.Current.TypingIndicatorBubbleSize;
var baseSize = sizeSetting switch
{
TypingIndicatorBubbleSize.Small when isNameplateVisible => 32f,
TypingIndicatorBubbleSize.Medium when isNameplateVisible => 44f,
TypingIndicatorBubbleSize.Large when isNameplateVisible => 56f,
TypingIndicatorBubbleSize.Small => 15f,
TypingIndicatorBubbleSize.Medium => 25f,
TypingIndicatorBubbleSize.Large => 35f,
_ => 35f,
};
return new Vector2(baseSize * scaleX, baseSize * scaleY);
}
private unsafe bool TryDrawNameplateBubble(ImDrawListPtr drawList, IDalamudTextureWrap textureWrap, uint objectId)
{
if (textureWrap == null || textureWrap.Handle == IntPtr.Zero)
return false;
var framework = Framework.Instance();
if (framework == null)
return false;
var ui3D = framework->GetUIModule()->GetUI3DModule();
if (ui3D == null)
return false;
var addonNamePlate = (AddonNamePlate*)_gameGui.GetAddonByName("NamePlate", 1).Address;
if (addonNamePlate == null)
return false;
AddonNamePlate.NamePlateObject* namePlate = null;
float distance = 0f;
for (var i = 0; i < ui3D->NamePlateObjectInfoCount; i++)
{
var objectInfo = ui3D->NamePlateObjectInfoPointers[i];
if (objectInfo.Value == null || objectInfo.Value->GameObject == null)
continue;
if (objectInfo.Value->GameObject->EntityId != objectId)
continue;
if (objectInfo.Value->GameObject->YalmDistanceFromPlayerX > 35f)
return false;
namePlate = &addonNamePlate->NamePlateObjectArray[objectInfo.Value->NamePlateIndex];
distance = objectInfo.Value->GameObject->YalmDistanceFromPlayerX;
break;
}
if (namePlate == null || namePlate->RootComponentNode == null)
return false;
var iconNode = namePlate->RootComponentNode->Component->UldManager.NodeList[0];
if (iconNode == null)
return false;
var scaleX = namePlate->RootComponentNode->AtkResNode.ScaleX;
var scaleY = namePlate->RootComponentNode->AtkResNode.ScaleY;
var iconVisible = iconNode->IsVisible();
var sizeScaleFactor = 1f;
var scaleVector = new Vector2(scaleX, scaleY);
var rootPosition = new Vector2(namePlate->RootComponentNode->AtkResNode.X, namePlate->RootComponentNode->AtkResNode.Y);
var iconLocalPosition = new Vector2(iconNode->X, iconNode->Y) * scaleVector;
var iconDimensions = new Vector2(iconNode->Width, iconNode->Height) * scaleVector;
if (!iconVisible)
{
sizeScaleFactor = 2.5f;
var anchor = rootPosition + iconLocalPosition + new Vector2(iconDimensions.X * 0.5f, 0f);
var distanceOffset = new Vector2(0f, -16f + distance) * scaleVector;
if (iconNode->Height == 24)
{
distanceOffset.Y += 16f * scaleY;
}
distanceOffset.Y += 64f * scaleY;
var referenceSize = GetConfiguredBubbleSize(scaleX * sizeScaleFactor, scaleY * sizeScaleFactor, false, TypingIndicatorBubbleSize.Small);
var manualOffset = new Vector2(referenceSize.X * 2.00f, referenceSize.Y * 2.00f);
var iconSizeHidden = GetConfiguredBubbleSize(scaleX * sizeScaleFactor, scaleY * sizeScaleFactor, false);
var center = anchor + distanceOffset + manualOffset;
var topLeft = center - (iconSizeHidden / 2f);
drawList.AddImage(textureWrap.Handle, topLeft, topLeft + iconSizeHidden, Vector2.Zero, Vector2.One,
ImGui.ColorConvertFloat4ToU32(new Vector4(1f, 1f, 1f, 0.95f)));
return true;
}
var iconPos = rootPosition + iconLocalPosition + new Vector2(iconDimensions.X, 0f);
var iconOffset = new Vector2(distance / 1.5f, distance / 3.5f) * scaleVector;
if (iconNode->Height == 24)
{
iconOffset.Y -= 8f * scaleY;
}
iconPos += iconOffset;
var iconSize = GetConfiguredBubbleSize(scaleX * sizeScaleFactor, scaleY * sizeScaleFactor, true);
drawList.AddImage(textureWrap.Handle, iconPos, iconPos + iconSize, Vector2.Zero, Vector2.One,
ImGui.ColorConvertFloat4ToU32(new Vector4(1f, 1f, 1f, 0.95f)));
return true;
}
private void DrawWorldFallbackIcon(ImDrawListPtr drawList, IDalamudTextureWrap textureWrap, Vector3 worldPosition)
{
var offsetPosition = worldPosition + new Vector3(0f, 1.8f, 0f);
if (!_gameGui.WorldToScreen(offsetPosition, out var screenPos))
return;
var iconSize = GetConfiguredBubbleSize(ImGuiHelpers.GlobalScale, ImGuiHelpers.GlobalScale, false);
var iconPos = screenPos - (iconSize / 2f) - new Vector2(0f, iconSize.Y * 0.6f);
drawList.AddImage(textureWrap.Handle, iconPos, iconPos + iconSize, Vector2.Zero, Vector2.One,
ImGui.ColorConvertFloat4ToU32(new Vector4(1f, 1f, 1f, 0.95f)));
}
private bool TryGetWorldPosition(uint objectId, out Vector3 position)
{
position = Vector3.Zero;
if (objectId == 0 || objectId == uint.MaxValue)
return false;
foreach (var obj in _objectTable)
{
if (obj?.EntityId == objectId)
{
position = obj.Position;
return true;
}
}
return false;
}
private bool TryResolveWorldPosition(Pair? pair, UserData userData, uint objectId, out Vector3 position)
{
if (TryGetWorldPosition(objectId, out position))
{
_logger.LogTrace("TypingIndicator: resolved by objectId {objectId}", objectId);
return true;
}
if (pair != null)
{
var name = pair.PlayerName;
if (!string.IsNullOrEmpty(name) && TryGetWorldPositionByName(name!, out position))
{
_logger.LogTrace("TypingIndicator: resolved by pair name {name}", name);
return true;
}
var ident = pair.Ident;
if (!string.IsNullOrEmpty(ident))
{
var cached = _dalamudUtil.FindPlayerByNameHash(ident);
if (!string.IsNullOrEmpty(cached.Name) && TryGetWorldPositionByName(cached.Name, out position))
{
_logger.LogTrace("TypingIndicator: resolved by cached name {name}", cached.Name);
return true;
}
if (cached.Address != IntPtr.Zero)
{
var objRef = _objectTable.CreateObjectReference(cached.Address);
if (objRef != null)
{
position = objRef.Position;
_logger.LogTrace("TypingIndicator: resolved by cached address {addr}", cached.Address);
return true;
}
}
}
}
var alias = userData.AliasOrUID;
if (!string.IsNullOrEmpty(alias) && TryGetWorldPositionByName(alias, out position))
{
_logger.LogTrace("TypingIndicator: resolved by user alias {alias}", alias);
return true;
}
return false;
}
private bool TryGetWorldPositionByName(string name, out Vector3 position)
{
position = Vector3.Zero;
foreach (var obj in _objectTable)
{
if (obj != null && obj.Name.TextValue.Equals(name, StringComparison.OrdinalIgnoreCase))
{
position = obj.Position;
return true;
}
}
return false;
}
private int GetPartyIndexForObjectId(uint objectId)
{
for (var i = 0; i < _partyList.Count; ++i)
{
var member = _partyList[i];
if (member == null) continue;
var gameObject = member.GameObject;
if (gameObject != null && GetEntityId(gameObject.Address) == objectId)
return i;
}
return -1;
}
private int GetPartyIndexForName(string name)
{
for (var i = 0; i < _partyList.Count; ++i)
{
var member = _partyList[i];
if (member?.Name == null) continue;
if (member.Name.TextValue.Equals(name, StringComparison.OrdinalIgnoreCase))
return i;
}
return -1;
}
private bool IsPartyMember(uint objectId, string? playerName)
{
if (objectId != 0 && objectId != uint.MaxValue && GetPartyIndexForObjectId(objectId) >= 0)
return true;
if (!string.IsNullOrEmpty(playerName) && GetPartyIndexForName(playerName) >= 0)
return true;
return false;
}
private bool IsPlayerRelevant(Pair? pair, bool isPartyMember)
{
if (isPartyMember)
return true;
if (pair?.UserPair != null)
{
var userPair = pair.UserPair;
if (userPair.OtherPermissions.IsPaired() || userPair.OwnPermissions.IsPaired())
return true;
}
if (pair?.GroupPair != null && pair.GroupPair.Any(g =>
!g.Value.GroupUserPermissions.IsPaused() &&
!g.Key.GroupUserPermissions.IsPaused()))
{
return true;
}
return false;
}
private bool IsWithinRelevantDistance(Vector3 position)
{
if (_clientState.LocalPlayer == null)
return false;
var distance = Vector3.Distance(_clientState.LocalPlayer.Position, position);
return distance <= 40f;
}
private static unsafe uint GetEntityId(nint address)
{
if (address == nint.Zero) return 0;
return ((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)address)->EntityId;
}
}

View File

@@ -36,7 +36,9 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollbar |
ImGuiWindowFlags.NoScrollWithMouse; ImGuiWindowFlags.NoScrollWithMouse;
public static Vector4 AccentColor { get; set; } = ImGuiColors.DalamudYellow; public static Vector4 AccentColor { get; set; } = ImGuiColors.DalamudViolet;
public static Vector4 AccentHoverColor { get; set; } = new Vector4(0x3A / 255f, 0x15 / 255f, 0x50 / 255f, 1f);
public static Vector4 AccentActiveColor { get; set; } = AccentHoverColor;
public readonly FileDialogManager FileDialogManager; public readonly FileDialogManager FileDialogManager;
@@ -136,6 +138,7 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
public string PlayerName => _dalamudUtil.GetPlayerName(); public string PlayerName => _dalamudUtil.GetPlayerName();
public IFontHandle UidFont { get; init; } public IFontHandle UidFont { get; init; }
public MareConfigService ConfigService => _configService;
public Dictionary<ushort, string> WorldData => _dalamudUtil.WorldData.Value; public Dictionary<ushort, string> WorldData => _dalamudUtil.WorldData.Value;
public uint WorldId => _dalamudUtil.GetHomeWorldId(); public uint WorldId => _dalamudUtil.GetHomeWorldId();
@@ -310,7 +313,7 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
} }
} }
public static Vector4 GetBoolColor(bool input) => input ? ImGuiColors.ParsedGreen : ImGuiColors.DalamudRed; public static Vector4 GetBoolColor(bool input) => input ? AccentColor : UiSharedService.AccentColor;
public float GetIconTextButtonSize(FontAwesomeIcon icon, string text) public float GetIconTextButtonSize(FontAwesomeIcon icon, string text)
{ {
@@ -368,6 +371,8 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
Vector2 cursorScreenPos = ImGui.GetCursorScreenPos(); Vector2 cursorScreenPos = ImGui.GetCursorScreenPos();
float x = vector.X + ImGui.GetStyle().FramePadding.X * 2f; float x = vector.X + ImGui.GetStyle().FramePadding.X * 2f;
float frameHeight = height ?? ImGui.GetFrameHeight(); float frameHeight = height ?? ImGui.GetFrameHeight();
using var hoverColor = ImRaii.PushColor(ImGuiCol.ButtonHovered, AccentHoverColor);
using var activeColor = ImRaii.PushColor(ImGuiCol.ButtonActive, AccentActiveColor);
bool result = ImGui.Button(string.Empty, new Vector2(x, frameHeight)); bool result = ImGui.Button(string.Empty, new Vector2(x, frameHeight));
Vector2 pos = new Vector2(cursorScreenPos.X + ImGui.GetStyle().FramePadding.X, Vector2 pos = new Vector2(cursorScreenPos.X + ImGui.GetStyle().FramePadding.X,
cursorScreenPos.Y + (height ?? ImGui.GetFrameHeight()) / 2f - (vector.Y / 2f)); cursorScreenPos.Y + (height ?? ImGui.GetFrameHeight()) / 2f - (vector.Y / 2f));
@@ -378,13 +383,19 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
return result; return result;
} }
private bool IconTextButtonInternal(FontAwesomeIcon icon, string text, Vector4? defaultColor = null, float? width = null) private bool IconTextButtonInternal(FontAwesomeIcon icon, string text, Vector4? defaultColor = null, float? width = null, bool useAccentHover = true)
{ {
int num = 0; int colorsPushed = 0;
if (defaultColor.HasValue) if (defaultColor.HasValue)
{ {
ImGui.PushStyleColor(ImGuiCol.Button, defaultColor.Value); ImGui.PushStyleColor(ImGuiCol.Button, defaultColor.Value);
num++; colorsPushed++;
}
if (useAccentHover)
{
ImGui.PushStyleColor(ImGuiCol.ButtonHovered, AccentHoverColor);
ImGui.PushStyleColor(ImGuiCol.ButtonActive, AccentActiveColor);
colorsPushed += 2;
} }
ImGui.PushID(text); ImGui.PushID(text);
@@ -404,9 +415,9 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
Vector2 pos2 = new Vector2(pos.X + vector.X + num2, cursorScreenPos.Y + ImGui.GetStyle().FramePadding.Y); Vector2 pos2 = new Vector2(pos.X + vector.X + num2, cursorScreenPos.Y + ImGui.GetStyle().FramePadding.Y);
windowDrawList.AddText(pos2, ImGui.GetColorU32(ImGuiCol.Text), text); windowDrawList.AddText(pos2, ImGui.GetColorU32(ImGuiCol.Text), text);
ImGui.PopID(); ImGui.PopID();
if (num > 0) if (colorsPushed > 0)
{ {
ImGui.PopStyleColor(num); ImGui.PopStyleColor(colorsPushed);
} }
return result; return result;
@@ -416,7 +427,8 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
{ {
return IconTextButtonInternal(icon, text, return IconTextButtonInternal(icon, text,
isInPopup ? ColorHelpers.RgbaUintToVector4(ImGui.GetColorU32(ImGuiCol.PopupBg)) : null, isInPopup ? ColorHelpers.RgbaUintToVector4(ImGui.GetColorU32(ImGuiCol.PopupBg)) : null,
width <= 0 ? null : width); width <= 0 ? null : width,
!isInPopup);
} }
public static bool IsDirectoryWritable(string dirPath, bool throwIfFails = false) public static bool IsDirectoryWritable(string dirPath, bool throwIfFails = false)
@@ -517,8 +529,8 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
public void BooleanToColoredIcon(bool value, bool inline = true) public void BooleanToColoredIcon(bool value, bool inline = true)
{ {
using var colorgreen = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.HealerGreen, value); using var colorgreen = ImRaii.PushColor(ImGuiCol.Text, AccentColor, value);
using var colorred = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed, !value); using var colorred = ImRaii.PushColor(ImGuiCol.Text, UiSharedService.AccentColor, !value);
if (inline) ImGui.SameLine(); if (inline) ImGui.SameLine();
@@ -534,7 +546,7 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
public void DrawCacheDirectorySetting() public void DrawCacheDirectorySetting()
{ {
ColorTextWrapped("Note: The storage folder should be somewhere close to root (i.e. C:\\UmbraSyncStorage) in a new empty folder. DO NOT point this to your game folder. DO NOT point this to your Penumbra folder.", ImGuiColors.DalamudYellow); ColorTextWrapped("Note: The storage folder should be somewhere close to root (i.e. C:\\UmbraStorage) in a new empty folder. DO NOT point this to your game folder. DO NOT point this to your Penumbra folder.", ImGuiColors.DalamudYellow);
var cacheDirectory = _configService.Current.CacheFolder; var cacheDirectory = _configService.Current.CacheFolder;
ImGui.SetNextItemWidth(400 * ImGuiHelpers.GlobalScale); ImGui.SetNextItemWidth(400 * ImGuiHelpers.GlobalScale);
ImGui.InputText("Storage Folder##cache", ref cacheDirectory, 255, ImGuiInputTextFlags.ReadOnly); ImGui.InputText("Storage Folder##cache", ref cacheDirectory, 255, ImGuiInputTextFlags.ReadOnly);
@@ -544,7 +556,7 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
{ {
if (IconButton(FontAwesomeIcon.Folder)) if (IconButton(FontAwesomeIcon.Folder))
{ {
FileDialogManager.OpenFolderDialog("Pick UmbraSync Storage Folder", (success, path) => FileDialogManager.OpenFolderDialog("Pick Umbra Storage Folder", (success, path) =>
{ {
if (!success) return; if (!success) return;
@@ -592,24 +604,24 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
if (_isPenumbraDirectory) if (_isPenumbraDirectory)
{ {
ColorTextWrapped("Do not point the storage path directly to the Penumbra directory. If necessary, make a subfolder in it.", ImGuiColors.DalamudRed); ColorTextWrapped("Do not point the storage path directly to the Penumbra directory. If necessary, make a subfolder in it.", UiSharedService.AccentColor);
} }
else if (_isOneDrive) else if (_isOneDrive)
{ {
ColorTextWrapped("Do not point the storage path to a folder in OneDrive. Do not use OneDrive folders for any Mod related functionality.", ImGuiColors.DalamudRed); ColorTextWrapped("Do not point the storage path to a folder in OneDrive. Do not use OneDrive folders for any Mod related functionality.", UiSharedService.AccentColor);
} }
else if (!_isDirectoryWritable) else if (!_isDirectoryWritable)
{ {
ColorTextWrapped("The folder you selected does not exist or cannot be written to. Please provide a valid path.", ImGuiColors.DalamudRed); ColorTextWrapped("The folder you selected does not exist or cannot be written to. Please provide a valid path.", UiSharedService.AccentColor);
} }
else if (_cacheDirectoryHasOtherFilesThanCache) else if (_cacheDirectoryHasOtherFilesThanCache)
{ {
ColorTextWrapped("Your selected directory has files or directories inside that are not UmbraSync related. Use an empty directory or a previous storage directory only.", ImGuiColors.DalamudRed); ColorTextWrapped("Your selected directory has files or directories inside that are not Umbra related. Use an empty directory or a previous storage directory only.", UiSharedService.AccentColor);
} }
else if (!_cacheDirectoryIsValidPath) else if (!_cacheDirectoryIsValidPath)
{ {
ColorTextWrapped("Your selected directory contains illegal characters unreadable by FFXIV. " + ColorTextWrapped("Your selected directory contains illegal characters unreadable by FFXIV. " +
"Restrict yourself to latin letters (A-Z), underscores (_), dashes (-) and arabic numbers (0-9).", ImGuiColors.DalamudRed); "Restrict yourself to latin letters (A-Z), underscores (_), dashes (-) and arabic numbers (0-9).", UiSharedService.AccentColor);
} }
float maxCacheSize = (float)_configService.Current.MaxLocalCacheInGiB; float maxCacheSize = (float)_configService.Current.MaxLocalCacheInGiB;
@@ -619,7 +631,7 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
_configService.Current.MaxLocalCacheInGiB = maxCacheSize; _configService.Current.MaxLocalCacheInGiB = maxCacheSize;
_configService.Save(); _configService.Save();
} }
DrawHelpText("The storage is automatically governed by UmbraSync. It will clear itself automatically once it reaches the set capacity by removing the oldest unused files. You typically do not need to clear it yourself."); DrawHelpText("The storage is automatically governed by Umbra. It will clear itself automatically once it reaches the set capacity by removing the oldest unused files. You typically do not need to clear it yourself.");
} }
public T? DrawCombo<T>(string comboName, IEnumerable<T> comboItems, Func<T, string> toName, public T? DrawCombo<T>(string comboName, IEnumerable<T> comboItems, Func<T, string> toName,
@@ -848,17 +860,7 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
if (!_penumbraExists || !_glamourerExists) if (!_penumbraExists || !_glamourerExists)
{ {
ImGui.TextColored(ImGuiColors.DalamudRed, "You need to install both Penumbra and Glamourer and keep them up to date to use UmbraSync."); ImGui.TextColored(UiSharedService.AccentColor, "You need to install both Penumbra and Glamourer and keep them up to date to use Umbra.");
return false;
}
else if (NoSnapService.AnyLoaded)
{
IconText(FontAwesomeIcon.ExclamationTriangle, ImGuiColors.DalamudYellow);
ImGui.SameLine();
var cursorX = ImGui.GetCursorPosX();
ImGui.TextColored(ImGuiColors.DalamudYellow, "Synced player appearances will not apply until incompatible plugins are disabled:");
ImGui.SetCursorPosX(cursorX + 16.0f);
ImGui.TextColored(ImGuiColors.DalamudYellow, NoSnapService.ActivePlugins);
return false; return false;
} }

View File

@@ -1,14 +1,14 @@
{ {
"Author": "SirConstance", "Author": "Keda",
"Name": "UmbraSync", "Name": "UmbraSync",
"Punchline": "Il revient!", "Punchline": "Parce que nous le valons bien.",
"Description": "This plugin will synchronize your Penumbra mods and current Glamourer state with other paired clients automatically.", "Description": "Ce plugin synchronisera automatiquement vos mods Penumbra et l'état actuel de Glamourer avec les autres clients appairés.",
"InternalName": "UmbraSync", "InternalName": "UmbraSync",
"ApplicableVersion": "any", "ApplicableVersion": "any",
"Tags": [ "Tags": [
"customization" "customization"
], ],
"IconUrl": "", "IconUrl": "https://repo.umbra-sync.net/images/logo.png",
"RepoUrl": "", "RepoUrl": "https://repo.umbra-sync.net/plugin.json",
"CanUnloadAsync": true "CanUnloadAsync": true
} }

View File

@@ -17,18 +17,16 @@ public sealed class AccountRegistrationService : IDisposable
private readonly HttpClient _httpClient; private readonly HttpClient _httpClient;
private readonly ILogger<AccountRegistrationService> _logger; private readonly ILogger<AccountRegistrationService> _logger;
private readonly ServerConfigurationManager _serverManager; private readonly ServerConfigurationManager _serverManager;
private readonly RemoteConfigurationService _remoteConfig;
private string GenerateSecretKey() private string GenerateSecretKey()
{ {
return Convert.ToHexString(SHA256.HashData(RandomNumberGenerator.GetBytes(64))); return Convert.ToHexString(SHA256.HashData(RandomNumberGenerator.GetBytes(64)));
} }
public AccountRegistrationService(ILogger<AccountRegistrationService> logger, ServerConfigurationManager serverManager, RemoteConfigurationService remoteConfig) public AccountRegistrationService(ILogger<AccountRegistrationService> logger, ServerConfigurationManager serverManager)
{ {
_logger = logger; _logger = logger;
_serverManager = serverManager; _serverManager = serverManager;
_remoteConfig = remoteConfig;
_httpClient = new( _httpClient = new(
new HttpClientHandler new HttpClientHandler
{ {
@@ -47,22 +45,10 @@ public sealed class AccountRegistrationService : IDisposable
public async Task<RegisterReplyDto> RegisterAccount(CancellationToken token) public async Task<RegisterReplyDto> RegisterAccount(CancellationToken token)
{ {
var authApiUrl = _serverManager.CurrentApiUrl;
// Override the API URL used for auth from remote config, if one is available
if (authApiUrl.Equals(ApiController.UmbraSyncServiceUri, StringComparison.Ordinal))
{
var config = await _remoteConfig.GetConfigAsync<HubConnectionConfig>("mainServer").ConfigureAwait(false) ?? new();
if (!string.IsNullOrEmpty(config.ApiUrl))
authApiUrl = config.ApiUrl;
else
authApiUrl = ApiController.UmbraSyncServiceApiUri;
}
var secretKey = GenerateSecretKey(); var secretKey = GenerateSecretKey();
var hashedSecretKey = secretKey.GetHash256(); var hashedSecretKey = secretKey.GetHash256();
Uri postUri = MareAuth.AuthRegisterV2FullPath(new Uri(authApiUrl Uri postUri = MareAuth.AuthRegisterV2FullPath(new Uri(_serverManager.CurrentApiUrl
.Replace("wss://", "https://", StringComparison.OrdinalIgnoreCase) .Replace("wss://", "https://", StringComparison.OrdinalIgnoreCase)
.Replace("ws://", "http://", StringComparison.OrdinalIgnoreCase))); .Replace("ws://", "http://", StringComparison.OrdinalIgnoreCase)));

View File

@@ -0,0 +1,224 @@
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.Logging;
using MareSynchronos.WebAPI.SignalR;
using MareSynchronos.Services.AutoDetect;
namespace MareSynchronos.WebAPI.AutoDetect;
public class DiscoveryApiClient
{
private readonly ILogger<DiscoveryApiClient> _logger;
private readonly TokenProvider _tokenProvider;
private readonly DiscoveryConfigProvider _configProvider;
private readonly HttpClient _httpClient = new();
private static readonly JsonSerializerOptions JsonOpt = new() { PropertyNameCaseInsensitive = true };
public DiscoveryApiClient(ILogger<DiscoveryApiClient> logger, TokenProvider tokenProvider, DiscoveryConfigProvider configProvider)
{
_logger = logger;
_tokenProvider = tokenProvider;
_configProvider = configProvider;
_httpClient.Timeout = TimeSpan.FromSeconds(30);
}
public async Task<List<ServerMatch>> QueryAsync(string endpoint, IEnumerable<string> hashes, CancellationToken ct)
{
try
{
var token = await _tokenProvider.GetOrUpdateToken(ct).ConfigureAwait(false);
if (string.IsNullOrEmpty(token)) return [];
var distinctHashes = hashes.Distinct(StringComparer.Ordinal).ToArray();
using var req = new HttpRequestMessage(HttpMethod.Post, endpoint);
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
var body = JsonSerializer.Serialize(new
{
hashes = distinctHashes,
salt = _configProvider.SaltB64
});
req.Content = new StringContent(body, Encoding.UTF8, "application/json");
var resp = await _httpClient.SendAsync(req, ct).ConfigureAwait(false);
if (resp.StatusCode == System.Net.HttpStatusCode.Unauthorized)
{
var token2 = await _tokenProvider.ForceRefreshToken(ct).ConfigureAwait(false);
if (string.IsNullOrEmpty(token2)) return [];
using var req2 = new HttpRequestMessage(HttpMethod.Post, endpoint);
req2.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token2);
var body2 = JsonSerializer.Serialize(new
{
hashes = distinctHashes,
salt = _configProvider.SaltB64
});
req2.Content = new StringContent(body2, Encoding.UTF8, "application/json");
resp = await _httpClient.SendAsync(req2, ct).ConfigureAwait(false);
}
resp.EnsureSuccessStatusCode();
var json = await resp.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
var result = JsonSerializer.Deserialize<List<ServerMatch>>(json, JsonOpt) ?? [];
return result;
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Discovery query failed");
return [];
}
}
public async Task<bool> SendRequestAsync(string endpoint, string? token, string? targetUid, string? displayName, CancellationToken ct)
{
try
{
if (string.IsNullOrEmpty(token) && string.IsNullOrEmpty(targetUid))
{
_logger.LogWarning("Discovery request aborted: no token or targetUid provided");
return false;
}
var jwt = await _tokenProvider.GetOrUpdateToken(ct).ConfigureAwait(false);
if (string.IsNullOrEmpty(jwt)) return false;
using var req = new HttpRequestMessage(HttpMethod.Post, endpoint);
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", jwt);
var body = JsonSerializer.Serialize(new RequestPayload(token, targetUid, displayName));
req.Content = new StringContent(body, Encoding.UTF8, "application/json");
var resp = await _httpClient.SendAsync(req, ct).ConfigureAwait(false);
if (resp.StatusCode == System.Net.HttpStatusCode.Unauthorized)
{
var jwt2 = await _tokenProvider.ForceRefreshToken(ct).ConfigureAwait(false);
if (string.IsNullOrEmpty(jwt2)) return false;
using var req2 = new HttpRequestMessage(HttpMethod.Post, endpoint);
req2.Headers.Authorization = new AuthenticationHeaderValue("Bearer", jwt2);
var body2 = JsonSerializer.Serialize(new RequestPayload(token, targetUid, displayName));
req2.Content = new StringContent(body2, Encoding.UTF8, "application/json");
resp = await _httpClient.SendAsync(req2, ct).ConfigureAwait(false);
}
if (!resp.IsSuccessStatusCode)
{
string txt = string.Empty;
try { txt = await resp.Content.ReadAsStringAsync(ct).ConfigureAwait(false); } catch { }
_logger.LogWarning("Discovery request failed: {code} {reason} {body}", (int)resp.StatusCode, resp.ReasonPhrase, txt);
return false;
}
return true;
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Discovery send request failed");
return false;
}
}
private sealed record RequestPayload(
[property: JsonPropertyName("token"), JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
string? Token,
[property: JsonPropertyName("targetUid"), JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
string? TargetUid,
[property: JsonPropertyName("displayName"), JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
string? DisplayName);
public async Task<bool> PublishAsync(string endpoint, IEnumerable<string> hashes, string? displayName, CancellationToken ct, bool allowRequests = true)
{
try
{
var jwt = await _tokenProvider.GetOrUpdateToken(ct).ConfigureAwait(false);
if (string.IsNullOrEmpty(jwt)) return false;
using var req = new HttpRequestMessage(HttpMethod.Post, endpoint);
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", jwt);
var bodyObj = new
{
hashes = hashes.Distinct(StringComparer.Ordinal).ToArray(),
displayName,
salt = _configProvider.SaltB64,
allowRequests
};
var body = JsonSerializer.Serialize(bodyObj);
req.Content = new StringContent(body, Encoding.UTF8, "application/json");
var resp = await _httpClient.SendAsync(req, ct).ConfigureAwait(false);
if (resp.StatusCode == System.Net.HttpStatusCode.Unauthorized)
{
var jwt2 = await _tokenProvider.ForceRefreshToken(ct).ConfigureAwait(false);
if (string.IsNullOrEmpty(jwt2)) return false;
using var req2 = new HttpRequestMessage(HttpMethod.Post, endpoint);
req2.Headers.Authorization = new AuthenticationHeaderValue("Bearer", jwt2);
var body2 = JsonSerializer.Serialize(bodyObj);
req2.Content = new StringContent(body2, Encoding.UTF8, "application/json");
resp = await _httpClient.SendAsync(req2, ct).ConfigureAwait(false);
}
return resp.IsSuccessStatusCode;
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Discovery publish failed");
return false;
}
}
public async Task<bool> SendAcceptAsync(string endpoint, string targetUid, string? displayName, CancellationToken ct)
{
try
{
var jwt = await _tokenProvider.GetOrUpdateToken(ct).ConfigureAwait(false);
if (string.IsNullOrEmpty(jwt)) return false;
using var req = new HttpRequestMessage(HttpMethod.Post, endpoint);
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", jwt);
var bodyObj = new { targetUid, displayName };
var body = JsonSerializer.Serialize(bodyObj);
req.Content = new StringContent(body, Encoding.UTF8, "application/json");
var resp = await _httpClient.SendAsync(req, ct).ConfigureAwait(false);
if (resp.StatusCode == System.Net.HttpStatusCode.Unauthorized)
{
var jwt2 = await _tokenProvider.ForceRefreshToken(ct).ConfigureAwait(false);
if (string.IsNullOrEmpty(jwt2)) return false;
using var req2 = new HttpRequestMessage(HttpMethod.Post, endpoint);
req2.Headers.Authorization = new AuthenticationHeaderValue("Bearer", jwt2);
var body2 = JsonSerializer.Serialize(bodyObj);
req2.Content = new StringContent(body2, Encoding.UTF8, "application/json");
resp = await _httpClient.SendAsync(req2, ct).ConfigureAwait(false);
}
return resp.IsSuccessStatusCode;
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Discovery accept notify failed");
return false;
}
}
public async Task DisableAsync(string endpoint, CancellationToken ct)
{
try
{
var jwt = await _tokenProvider.GetOrUpdateToken(ct).ConfigureAwait(false);
if (string.IsNullOrEmpty(jwt)) return;
using var req = new HttpRequestMessage(HttpMethod.Post, endpoint);
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", jwt);
var resp = await _httpClient.SendAsync(req, ct).ConfigureAwait(false);
if (resp.StatusCode == System.Net.HttpStatusCode.Unauthorized)
{
var jwt2 = await _tokenProvider.ForceRefreshToken(ct).ConfigureAwait(false);
if (string.IsNullOrEmpty(jwt2)) return;
using var req2 = new HttpRequestMessage(HttpMethod.Post, endpoint);
req2.Headers.Authorization = new AuthenticationHeaderValue("Bearer", jwt2);
resp = await _httpClient.SendAsync(req2, ct).ConfigureAwait(false);
}
if (!resp.IsSuccessStatusCode)
{
string txt = string.Empty;
try { txt = await resp.Content.ReadAsStringAsync(ct).ConfigureAwait(false); } catch { }
_logger.LogWarning("Discovery disable failed: {code} {reason} {body}", (int)resp.StatusCode, resp.ReasonPhrase, txt);
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Discovery disable failed");
}
}
}
public sealed class ServerMatch
{
public string Hash { get; set; } = string.Empty;
public string? Token { get; set; }
public string? Uid { get; set; }
public string? DisplayName { get; set; }
}

View File

@@ -4,6 +4,7 @@ using MareSynchronos.API.Data;
using MareSynchronos.API.Dto.Files; using MareSynchronos.API.Dto.Files;
using MareSynchronos.API.Routes; using MareSynchronos.API.Routes;
using MareSynchronos.FileCache; using MareSynchronos.FileCache;
using MareSynchronos.MareConfiguration;
using MareSynchronos.PlayerData.Handlers; using MareSynchronos.PlayerData.Handlers;
using MareSynchronos.Services.Mediator; using MareSynchronos.Services.Mediator;
using MareSynchronos.Utils; using MareSynchronos.Utils;
@@ -20,17 +21,22 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
private readonly Dictionary<string, FileDownloadStatus> _downloadStatus; private readonly Dictionary<string, FileDownloadStatus> _downloadStatus;
private readonly FileCompactor _fileCompactor; private readonly FileCompactor _fileCompactor;
private readonly FileCacheManager _fileDbManager; private readonly FileCacheManager _fileDbManager;
private readonly MareConfigService _mareConfigService;
private readonly FileTransferOrchestrator _orchestrator; private readonly FileTransferOrchestrator _orchestrator;
private readonly List<ThrottledStream> _activeDownloadStreams; private readonly List<ThrottledStream> _activeDownloadStreams;
private readonly object _queueLock = new();
private SemaphoreSlim? _downloadQueueSemaphore;
private int _downloadQueueCapacity = -1;
public FileDownloadManager(ILogger<FileDownloadManager> logger, MareMediator mediator, public FileDownloadManager(ILogger<FileDownloadManager> logger, MareMediator mediator,
FileTransferOrchestrator orchestrator, FileTransferOrchestrator orchestrator,
FileCacheManager fileCacheManager, FileCompactor fileCompactor) : base(logger, mediator) FileCacheManager fileCacheManager, FileCompactor fileCompactor, MareConfigService mareConfigService) : base(logger, mediator)
{ {
_downloadStatus = new Dictionary<string, FileDownloadStatus>(StringComparer.Ordinal); _downloadStatus = new Dictionary<string, FileDownloadStatus>(StringComparer.Ordinal);
_orchestrator = orchestrator; _orchestrator = orchestrator;
_fileDbManager = fileCacheManager; _fileDbManager = fileCacheManager;
_fileCompactor = fileCompactor; _fileCompactor = fileCompactor;
_mareConfigService = mareConfigService;
_activeDownloadStreams = []; _activeDownloadStreams = [];
Mediator.Subscribe<DownloadLimitChangedMessage>(this, (msg) => Mediator.Subscribe<DownloadLimitChangedMessage>(this, (msg) =>
@@ -49,7 +55,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
public List<FileTransfer> ForbiddenTransfers => _orchestrator.ForbiddenTransfers; public List<FileTransfer> ForbiddenTransfers => _orchestrator.ForbiddenTransfers;
public bool IsDownloading => !CurrentDownloads.Any(); public bool IsDownloading => CurrentDownloads.Any();
public void ClearDownload() public void ClearDownload()
{ {
@@ -59,6 +65,14 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
public async Task DownloadFiles(GameObjectHandler gameObject, List<FileReplacementData> fileReplacementDto, CancellationToken ct) public async Task DownloadFiles(GameObjectHandler gameObject, List<FileReplacementData> fileReplacementDto, CancellationToken ct)
{ {
SemaphoreSlim? queueSemaphore = null;
if (_mareConfigService.Current.EnableDownloadQueue)
{
queueSemaphore = GetQueueSemaphore();
Logger.LogTrace("Queueing download for {name}. Currently queued: {queued}", gameObject.Name, queueSemaphore.CurrentCount);
await queueSemaphore.WaitAsync(ct).ConfigureAwait(false);
}
Mediator.Publish(new HaltScanMessage(nameof(DownloadFiles))); Mediator.Publish(new HaltScanMessage(nameof(DownloadFiles)));
try try
{ {
@@ -70,6 +84,11 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
} }
finally finally
{ {
if (queueSemaphore != null)
{
queueSemaphore.Release();
}
Mediator.Publish(new DownloadFinishedMessage(gameObject)); Mediator.Publish(new DownloadFinishedMessage(gameObject));
Mediator.Publish(new ResumeScanMessage(nameof(DownloadFiles))); Mediator.Publish(new ResumeScanMessage(nameof(DownloadFiles)));
} }
@@ -132,6 +151,22 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
return (string.Join("", hashName), long.Parse(string.Join("", fileLength))); return (string.Join("", hashName), long.Parse(string.Join("", fileLength)));
} }
private SemaphoreSlim GetQueueSemaphore()
{
var desiredCapacity = Math.Clamp(_mareConfigService.Current.ParallelDownloads, 1, 10);
lock (_queueLock)
{
if (_downloadQueueSemaphore == null || _downloadQueueCapacity != desiredCapacity)
{
_downloadQueueSemaphore = new SemaphoreSlim(desiredCapacity, desiredCapacity);
_downloadQueueCapacity = desiredCapacity;
}
return _downloadQueueSemaphore;
}
}
private async Task DownloadAndMungeFileHttpClient(string downloadGroup, Guid requestId, List<DownloadFileTransfer> fileTransfer, string tempPath, IProgress<long> progress, CancellationToken ct) private async Task DownloadAndMungeFileHttpClient(string downloadGroup, Guid requestId, List<DownloadFileTransfer> fileTransfer, string tempPath, IProgress<long> progress, CancellationToken ct)
{ {
Logger.LogDebug("GUID {requestId} on server {uri} for files {files}", requestId, fileTransfer[0].DownloadUri, string.Join(", ", fileTransfer.Select(c => c.Hash).ToList())); Logger.LogDebug("GUID {requestId} on server {uri} for files {files}", requestId, fileTransfer[0].DownloadUri, string.Join(", ", fileTransfer.Select(c => c.Hash).ToList()));

View File

@@ -97,6 +97,12 @@ public partial class ApiController
await _mareHub!.InvokeAsync(nameof(UserSetProfile), userDescription).ConfigureAwait(false); await _mareHub!.InvokeAsync(nameof(UserSetProfile), userDescription).ConfigureAwait(false);
} }
public async Task UserSetTypingState(bool isTyping)
{
CheckConnection();
await _mareHub!.SendAsync(nameof(UserSetTypingState), isTyping).ConfigureAwait(false);
}
private async Task PushCharacterDataInternal(CharacterData character, List<UserData> visibleCharacters) private async Task PushCharacterDataInternal(CharacterData character, List<UserData> visibleCharacters)
{ {
Logger.LogInformation("Pushing character data for {hash} to {charas}", character.DataHash.Value, string.Join(", ", visibleCharacters.Select(c => c.AliasOrUID))); Logger.LogInformation("Pushing character data for {hash} to {charas}", character.DataHash.Value, string.Join(", ", visibleCharacters.Select(c => c.AliasOrUID)));

View File

@@ -138,6 +138,13 @@ public partial class ApiController
return Task.CompletedTask; return Task.CompletedTask;
} }
public Task Client_UserTypingState(TypingStateDto dto)
{
Logger.LogTrace("Client_UserTypingState: {uid} typing={typing}", dto.User.UID, dto.IsTyping);
Mediator.Publish(new UserTypingStateMessage(dto));
return Task.CompletedTask;
}
public Task Client_UserReceiveCharacterData(OnlineUserCharaDataDto dataDto) public Task Client_UserReceiveCharacterData(OnlineUserCharaDataDto dataDto)
{ {
Logger.LogTrace("Client_UserReceiveCharacterData: {user}", dataDto.User); Logger.LogTrace("Client_UserReceiveCharacterData: {user}", dataDto.User);
@@ -313,6 +320,12 @@ public partial class ApiController
_mareHub!.On(nameof(Client_UserChatMsg), act); _mareHub!.On(nameof(Client_UserChatMsg), act);
} }
public void OnUserTypingState(Action<TypingStateDto> act)
{
if (_initialized) return;
_mareHub!.On(nameof(Client_UserTypingState), act);
}
public void OnUserReceiveCharacterData(Action<OnlineUserCharaDataDto> act) public void OnUserReceiveCharacterData(Action<OnlineUserCharaDataDto> act)
{ {
if (_initialized) return; if (_initialized) return;

View File

@@ -1,4 +1,5 @@
using MareSynchronos.API.Data; using System;
using MareSynchronos.API.Data;
using MareSynchronos.API.Dto.Group; using MareSynchronos.API.Dto.Group;
using MareSynchronos.WebAPI.SignalR.Utils; using MareSynchronos.WebAPI.SignalR.Utils;
using Microsoft.AspNetCore.SignalR.Client; using Microsoft.AspNetCore.SignalR.Client;
@@ -49,10 +50,21 @@ public partial class ApiController
await _mareHub!.SendAsync(nameof(GroupClear), group).ConfigureAwait(false); await _mareHub!.SendAsync(nameof(GroupClear), group).ConfigureAwait(false);
} }
public async Task<GroupPasswordDto> GroupCreate() public Task<GroupPasswordDto> GroupCreate()
{
return GroupCreate(null);
}
public async Task<GroupPasswordDto> GroupCreate(string? alias)
{ {
CheckConnection(); CheckConnection();
return await _mareHub!.InvokeAsync<GroupPasswordDto>(nameof(GroupCreate)).ConfigureAwait(false); return await _mareHub!.InvokeAsync<GroupPasswordDto>(nameof(GroupCreate), string.IsNullOrWhiteSpace(alias) ? null : alias.Trim()).ConfigureAwait(false);
}
public async Task<GroupPasswordDto> GroupCreateTemporary(DateTime expiresAtUtc)
{
CheckConnection();
return await _mareHub!.InvokeAsync<GroupPasswordDto>(nameof(GroupCreateTemporary), expiresAtUtc).ConfigureAwait(false);
} }
public async Task<List<string>> GroupCreateTempInvite(GroupDto group, int amount) public async Task<List<string>> GroupCreateTempInvite(GroupDto group, int amount)

View File

@@ -21,10 +21,10 @@ namespace MareSynchronos.WebAPI;
#pragma warning disable MA0040 #pragma warning disable MA0040
public sealed partial class ApiController : DisposableMediatorSubscriberBase, IMareHubClient public sealed partial class ApiController : DisposableMediatorSubscriberBase, IMareHubClient
{ {
public const string UmbraSyncServer = "UmbraSync Main Server (BETA)"; public const string UmbraServer = "UmbraSync Main Server (BETA)";
public const string UmbraSyncServiceUri = "wss://umbra-sync.net/"; public const string UmbraServiceUri = "wss://umbra-sync.net/";
public const string UmbraSyncServiceApiUri = "wss://umbra-sync.net/"; public const string UmbraServiceApiUri = "wss://umbra-sync.net/";
public const string UmbraSyncServiceHubUri = "wss://umbra-sync.net/mare"; public const string UmbraServiceHubUri = "wss://umbra-sync.net/mare";
private readonly DalamudUtilService _dalamudUtil; private readonly DalamudUtilService _dalamudUtil;
private readonly HubFactory _hubFactory; private readonly HubFactory _hubFactory;
@@ -194,7 +194,7 @@ public const string UmbraSyncServiceHubUri = "wss://umbra-sync.net/mare";
Mediator.Publish(new NotificationMessage("Client incompatible", Mediator.Publish(new NotificationMessage("Client incompatible",
$"Your client is outdated ({currentClientVer.Major}.{currentClientVer.Minor}.{currentClientVer.Build}), current is: " + $"Your client is outdated ({currentClientVer.Major}.{currentClientVer.Minor}.{currentClientVer.Build}), current is: " +
$"{_connectionDto.CurrentClientVersion.Major}.{_connectionDto.CurrentClientVersion.Minor}.{_connectionDto.CurrentClientVersion.Build}. " + $"{_connectionDto.CurrentClientVersion.Major}.{_connectionDto.CurrentClientVersion.Minor}.{_connectionDto.CurrentClientVersion.Build}. " +
$"This client version is incompatible and will not be able to connect. Please update your UmbraSync client.", $"This client version is incompatible and will not be able to connect. Please update your Umbra client.",
NotificationType.Error)); NotificationType.Error));
} }
await StopConnection(ServerState.VersionMisMatch).ConfigureAwait(false); await StopConnection(ServerState.VersionMisMatch).ConfigureAwait(false);
@@ -206,7 +206,7 @@ public const string UmbraSyncServiceHubUri = "wss://umbra-sync.net/mare";
Mediator.Publish(new NotificationMessage("Client outdated", Mediator.Publish(new NotificationMessage("Client outdated",
$"Your client is outdated ({currentClientVer.Major}.{currentClientVer.Minor}.{currentClientVer.Build}), current is: " + $"Your client is outdated ({currentClientVer.Major}.{currentClientVer.Minor}.{currentClientVer.Build}), current is: " +
$"{_connectionDto.CurrentClientVersion.Major}.{_connectionDto.CurrentClientVersion.Minor}.{_connectionDto.CurrentClientVersion.Build}. " + $"{_connectionDto.CurrentClientVersion.Major}.{_connectionDto.CurrentClientVersion.Minor}.{_connectionDto.CurrentClientVersion.Build}. " +
$"Please keep your UmbraSync client up-to-date.", $"Please keep your Umbra client up-to-date.",
NotificationType.Warning, TimeSpan.FromSeconds(15))); NotificationType.Warning, TimeSpan.FromSeconds(15)));
} }
@@ -222,7 +222,7 @@ public const string UmbraSyncServiceHubUri = "wss://umbra-sync.net/mare";
#endif #endif
} }
await LoadIninitialPairs().ConfigureAwait(false); await LoadInitialPairs().ConfigureAwait(false);
await LoadOnlinePairs().ConfigureAwait(false); await LoadOnlinePairs().ConfigureAwait(false);
} }
catch (OperationCanceledException) catch (OperationCanceledException)
@@ -348,6 +348,7 @@ public const string UmbraSyncServiceHubUri = "wss://umbra-sync.net/mare";
OnUserUpdateSelfPairPermissions(dto => _ = Client_UserUpdateSelfPairPermissions(dto)); OnUserUpdateSelfPairPermissions(dto => _ = Client_UserUpdateSelfPairPermissions(dto));
OnUserReceiveUploadStatus(dto => _ = Client_UserReceiveUploadStatus(dto)); OnUserReceiveUploadStatus(dto => _ = Client_UserReceiveUploadStatus(dto));
OnUserUpdateProfile(dto => _ = Client_UserUpdateProfile(dto)); OnUserUpdateProfile(dto => _ = Client_UserUpdateProfile(dto));
OnUserTypingState(dto => _ = Client_UserTypingState(dto));
OnGroupChangePermissions((dto) => _ = Client_GroupChangePermissions(dto)); OnGroupChangePermissions((dto) => _ = Client_GroupChangePermissions(dto));
OnGroupDelete((dto) => _ = Client_GroupDelete(dto)); OnGroupDelete((dto) => _ = Client_GroupDelete(dto));
@@ -375,7 +376,7 @@ public const string UmbraSyncServiceHubUri = "wss://umbra-sync.net/mare";
_initialized = true; _initialized = true;
} }
private async Task LoadIninitialPairs() private async Task LoadInitialPairs()
{ {
foreach (var userPair in await UserGetPairedClients().ConfigureAwait(false)) foreach (var userPair in await UserGetPairedClients().ConfigureAwait(false))
{ {
@@ -393,7 +394,7 @@ public const string UmbraSyncServiceHubUri = "wss://umbra-sync.net/mare";
foreach (var user in users) foreach (var user in users)
{ {
Logger.LogDebug("Group Pair: {user}", user); Logger.LogDebug("Group Pair: {user}", user);
_pairManager.AddGroupPair(user); _pairManager.AddGroupPair(user, isInitialLoad: true);
} }
} }
} }
@@ -435,7 +436,7 @@ public const string UmbraSyncServiceHubUri = "wss://umbra-sync.net/mare";
return; return;
} }
ServerState = ServerState.Connected; ServerState = ServerState.Connected;
await LoadIninitialPairs().ConfigureAwait(false); await LoadInitialPairs().ConfigureAwait(false);
await LoadOnlinePairs().ConfigureAwait(false); await LoadOnlinePairs().ConfigureAwait(false);
Mediator.Publish(new ConnectedMessage(_connectionDto)); Mediator.Publish(new ConnectedMessage(_connectionDto));
} }

View File

@@ -19,7 +19,6 @@ public class HubFactory : MediatorSubscriberBase
{ {
private readonly ILoggerProvider _loggingProvider; private readonly ILoggerProvider _loggingProvider;
private readonly ServerConfigurationManager _serverConfigurationManager; private readonly ServerConfigurationManager _serverConfigurationManager;
private readonly RemoteConfigurationService _remoteConfig;
private readonly TokenProvider _tokenProvider; private readonly TokenProvider _tokenProvider;
private HubConnection? _instance; private HubConnection? _instance;
private string _cachedConfigFor = string.Empty; private string _cachedConfigFor = string.Empty;
@@ -27,11 +26,10 @@ public class HubFactory : MediatorSubscriberBase
private bool _isDisposed = false; private bool _isDisposed = false;
public HubFactory(ILogger<HubFactory> logger, MareMediator mediator, public HubFactory(ILogger<HubFactory> logger, MareMediator mediator,
ServerConfigurationManager serverConfigurationManager, RemoteConfigurationService remoteConfig, ServerConfigurationManager serverConfigurationManager,
TokenProvider tokenProvider, ILoggerProvider pluginLog) : base(logger, mediator) TokenProvider tokenProvider, ILoggerProvider pluginLog) : base(logger, mediator)
{ {
_serverConfigurationManager = serverConfigurationManager; _serverConfigurationManager = serverConfigurationManager;
_remoteConfig = remoteConfig;
_tokenProvider = tokenProvider; _tokenProvider = tokenProvider;
_loggingProvider = pluginLog; _loggingProvider = pluginLog;
} }
@@ -87,16 +85,6 @@ public class HubFactory : MediatorSubscriberBase
}; };
} }
if (_serverConfigurationManager.CurrentApiUrl.Equals(ApiController.UmbraSyncServiceUri, StringComparison.Ordinal))
{
var mainServerConfig = await _remoteConfig.GetConfigAsync<HubConnectionConfig>("mainServer").ConfigureAwait(false) ?? new();
defaultConfig = mainServerConfig;
if (string.IsNullOrEmpty(mainServerConfig.ApiUrl))
defaultConfig.ApiUrl = ApiController.UmbraSyncServiceApiUri;
if (string.IsNullOrEmpty(mainServerConfig.HubUrl))
defaultConfig.HubUrl = ApiController.UmbraSyncServiceHubUri;
}
string jsonResponse; string jsonResponse;
if (stapledWellKnown != null) if (stapledWellKnown != null)
@@ -115,7 +103,7 @@ public class HubFactory : MediatorSubscriberBase
_ => apiUrl.Scheme _ => apiUrl.Scheme
}; };
var wellKnownUrl = $"{httpScheme}://{apiUrl.Host}/.well-known/umbra/client"; var wellKnownUrl = $"{httpScheme}://{apiUrl.Host}/.well-known/Umbra/client";
Logger.LogTrace("Fetching hub config for {uri} via {wk}", _serverConfigurationManager.CurrentApiUrl, wellKnownUrl); Logger.LogTrace("Fetching hub config for {uri} via {wk}", _serverConfigurationManager.CurrentApiUrl, wellKnownUrl);
using var httpClient = new HttpClient( using var httpClient = new HttpClient(

View File

@@ -20,16 +20,14 @@ public sealed class TokenProvider : IDisposable, IMediatorSubscriber
private readonly HttpClient _httpClient; private readonly HttpClient _httpClient;
private readonly ILogger<TokenProvider> _logger; private readonly ILogger<TokenProvider> _logger;
private readonly ServerConfigurationManager _serverManager; private readonly ServerConfigurationManager _serverManager;
private readonly RemoteConfigurationService _remoteConfig;
private readonly ConcurrentDictionary<JwtIdentifier, string> _tokenCache = new(); private readonly ConcurrentDictionary<JwtIdentifier, string> _tokenCache = new();
private readonly ConcurrentDictionary<string, string?> _wellKnownCache = new(StringComparer.Ordinal); private readonly ConcurrentDictionary<string, string?> _wellKnownCache = new(StringComparer.Ordinal);
public TokenProvider(ILogger<TokenProvider> logger, ServerConfigurationManager serverManager, RemoteConfigurationService remoteConfig, public TokenProvider(ILogger<TokenProvider> logger, ServerConfigurationManager serverManager,
DalamudUtilService dalamudUtil, MareMediator mareMediator) DalamudUtilService dalamudUtil, MareMediator mareMediator)
{ {
_logger = logger; _logger = logger;
_serverManager = serverManager; _serverManager = serverManager;
_remoteConfig = remoteConfig;
_dalamudUtil = dalamudUtil; _dalamudUtil = dalamudUtil;
_httpClient = new( _httpClient = new(
new HttpClientHandler new HttpClientHandler
@@ -70,23 +68,11 @@ public sealed class TokenProvider : IDisposable, IMediatorSubscriber
Uri tokenUri; Uri tokenUri;
HttpResponseMessage result; HttpResponseMessage result;
var authApiUrl = _serverManager.CurrentApiUrl;
// Override the API URL used for auth from remote config, if one is available
if (authApiUrl.Equals(ApiController.UmbraSyncServiceUri, StringComparison.Ordinal))
{
var config = await _remoteConfig.GetConfigAsync<HubConnectionConfig>("mainServer").ConfigureAwait(false) ?? new();
if (!string.IsNullOrEmpty(config.ApiUrl))
authApiUrl = config.ApiUrl;
else
authApiUrl = ApiController.UmbraSyncServiceApiUri;
}
try try
{ {
_logger.LogDebug("GetNewToken: Requesting"); _logger.LogDebug("GetNewToken: Requesting");
tokenUri = MareAuth.AuthV2FullPath(new Uri(authApiUrl tokenUri = MareAuth.AuthV2FullPath(new Uri(_serverManager.CurrentApiUrl
.Replace("wss://", "https://", StringComparison.OrdinalIgnoreCase) .Replace("wss://", "https://", StringComparison.OrdinalIgnoreCase)
.Replace("ws://", "http://", StringComparison.OrdinalIgnoreCase))); .Replace("ws://", "http://", StringComparison.OrdinalIgnoreCase)));
var secretKey = _serverManager.GetSecretKey(out _)!; var secretKey = _serverManager.GetSecretKey(out _)!;
@@ -186,6 +172,16 @@ public sealed class TokenProvider : IDisposable, IMediatorSubscriber
return await GetNewToken(jwtIdentifier, ct).ConfigureAwait(false); return await GetNewToken(jwtIdentifier, ct).ConfigureAwait(false);
} }
public async Task<string?> ForceRefreshToken(CancellationToken ct)
{
JwtIdentifier? jwtIdentifier = await GetIdentifier().ConfigureAwait(false);
if (jwtIdentifier == null) return null;
_tokenCache.TryRemove(jwtIdentifier, out _);
_logger.LogTrace("ForceRefresh: Getting new token");
return await GetNewToken(jwtIdentifier, ct).ConfigureAwait(false);
}
public string? GetStapledWellKnown(string apiUrl) public string? GetStapledWellKnown(string apiUrl)
{ {
_wellKnownCache.TryGetValue(apiUrl, out var wellKnown); _wellKnownCache.TryGetValue(apiUrl, out var wellKnown);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 928 KiB

View File

@@ -2,6 +2,12 @@
"version": 1, "version": 1,
"dependencies": { "dependencies": {
"net9.0-windows7.0": { "net9.0-windows7.0": {
"Chaos.NaCl.Standard": {
"type": "Direct",
"requested": "[1.0.0, )",
"resolved": "1.0.0",
"contentHash": "8ajPyzu49LSIdPgeg56eDdKu1j8FZJngKgTn9rXHV3GNVly49XFvPAwoWuUT8xPze3OjVUOWQZaO82HWrYFFEw=="
},
"DalamudPackager": { "DalamudPackager": {
"type": "Direct", "type": "Direct",
"requested": "[13.0.0, )", "requested": "[13.0.0, )",
@@ -25,9 +31,9 @@
}, },
"Glamourer.Api": { "Glamourer.Api": {
"type": "Direct", "type": "Direct",
"requested": "[2.4.1, )", "requested": "[2.6.0, )",
"resolved": "2.4.1", "resolved": "2.6.0",
"contentHash": "Q9jrGWDlIAXoMXihu+HbSRu8FpgR9xNy2j5YpAoqDHzhl2MZLgqAJPlQ+N5ISp6cn7xqQVMmxB9PchW2uEYoEA==" "contentHash": "zysCZgNBRm3k3qvibyw/31MmEckX0Uh0ZsT+Sax3ZHnYIRELr9Qhbz3cjJz7u0RHGIrNJiRpktu/LxgHEqDItw=="
}, },
"K4os.Compression.LZ4.Legacy": { "K4os.Compression.LZ4.Legacy": {
"type": "Direct", "type": "Direct",
@@ -51,80 +57,80 @@
}, },
"Meziantou.Analyzer": { "Meziantou.Analyzer": {
"type": "Direct", "type": "Direct",
"requested": "[2.0.189, )", "requested": "[2.0.212, )",
"resolved": "2.0.189", "resolved": "2.0.212",
"contentHash": "/e+dh95vDdvCTbViV2cWpXJEXAj+VHq7FsBXCTTTsLcffV0bkgXDFAPY0zMpy+Vt91Cl2cBoSOfaAoSdtn796Q==" "contentHash": "U91ktjjTRTccUs3Lk+hrLD9vW+2+lhnsOf4G1GpRSJi1pLn3uK5CU6wGP9Bmz1KlJs6Oz1GGoMhxQBoqQsmAuQ=="
}, },
"Microsoft.AspNetCore.SignalR.Client": { "Microsoft.AspNetCore.SignalR.Client": {
"type": "Direct", "type": "Direct",
"requested": "[9.0.3, )", "requested": "[9.0.8, )",
"resolved": "9.0.3", "resolved": "9.0.8",
"contentHash": "V8K94AN9ADbpP2jxwt8Y++g7t/XZ7oEV+GZizNvLnR8dpCYWeveIZ/tItO54jfZJ5jmt5YyideOc+ErZbr1IZg==", "contentHash": "cO+TZaWdhMn2cIYfPH9oFZaisJrx7X6SBAYdmGektPUAW2BYtMbH4HyLOnJ5CYo42zP9WgqhWHKqmoDm7+Ol5w==",
"dependencies": { "dependencies": {
"Microsoft.AspNetCore.Http.Connections.Client": "9.0.3", "Microsoft.AspNetCore.Http.Connections.Client": "9.0.8",
"Microsoft.AspNetCore.SignalR.Client.Core": "9.0.3" "Microsoft.AspNetCore.SignalR.Client.Core": "9.0.8"
} }
}, },
"Microsoft.AspNetCore.SignalR.Protocols.MessagePack": { "Microsoft.AspNetCore.SignalR.Protocols.MessagePack": {
"type": "Direct", "type": "Direct",
"requested": "[9.0.3, )", "requested": "[9.0.8, )",
"resolved": "9.0.3", "resolved": "9.0.8",
"contentHash": "mMQ21T4NuqGrX1UzSe1WBmg6TUlOmpMgCoA9kAy/uBWBZlAA4+NFavbCULyJy6zTSUAvZkG3cGSnQN4dLJlF/w==", "contentHash": "e6SC/Tp+SZKeEVYdu8blz9q4MkFW08D56IkQv9V3perF3a7v+GgGZ0DAY/HRS9zBuhFrqpXhJvxeHMw3PJLcOg==",
"dependencies": { "dependencies": {
"MessagePack": "2.5.187", "MessagePack": "2.5.187",
"Microsoft.AspNetCore.SignalR.Common": "9.0.3" "Microsoft.AspNetCore.SignalR.Common": "9.0.8"
} }
}, },
"Microsoft.Extensions.Hosting": { "Microsoft.Extensions.Hosting": {
"type": "Direct", "type": "Direct",
"requested": "[9.0.3, )", "requested": "[9.0.8, )",
"resolved": "9.0.3", "resolved": "9.0.8",
"contentHash": "ioFXglqFA9uCYcKHI3CLVTO3I75jWIhvVxiZBzGeSPxw7XdhDLh0QvbNFrMTbZk9qqEVQcylblcvcNXnFHYXyA==", "contentHash": "O2VlzORrBbS2it203k5FOHrudDdmdrJovA73P/shdRGeLzvet4e4yXhGx52V2PNjYBQ0IO5M4xiNcL+6xIX6Bg==",
"dependencies": { "dependencies": {
"Microsoft.Extensions.Configuration": "9.0.3", "Microsoft.Extensions.Configuration": "9.0.8",
"Microsoft.Extensions.Configuration.Abstractions": "9.0.3", "Microsoft.Extensions.Configuration.Abstractions": "9.0.8",
"Microsoft.Extensions.Configuration.Binder": "9.0.3", "Microsoft.Extensions.Configuration.Binder": "9.0.8",
"Microsoft.Extensions.Configuration.CommandLine": "9.0.3", "Microsoft.Extensions.Configuration.CommandLine": "9.0.8",
"Microsoft.Extensions.Configuration.EnvironmentVariables": "9.0.3", "Microsoft.Extensions.Configuration.EnvironmentVariables": "9.0.8",
"Microsoft.Extensions.Configuration.FileExtensions": "9.0.3", "Microsoft.Extensions.Configuration.FileExtensions": "9.0.8",
"Microsoft.Extensions.Configuration.Json": "9.0.3", "Microsoft.Extensions.Configuration.Json": "9.0.8",
"Microsoft.Extensions.Configuration.UserSecrets": "9.0.3", "Microsoft.Extensions.Configuration.UserSecrets": "9.0.8",
"Microsoft.Extensions.DependencyInjection": "9.0.3", "Microsoft.Extensions.DependencyInjection": "9.0.8",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.3", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.8",
"Microsoft.Extensions.Diagnostics": "9.0.3", "Microsoft.Extensions.Diagnostics": "9.0.8",
"Microsoft.Extensions.FileProviders.Abstractions": "9.0.3", "Microsoft.Extensions.FileProviders.Abstractions": "9.0.8",
"Microsoft.Extensions.FileProviders.Physical": "9.0.3", "Microsoft.Extensions.FileProviders.Physical": "9.0.8",
"Microsoft.Extensions.Hosting.Abstractions": "9.0.3", "Microsoft.Extensions.Hosting.Abstractions": "9.0.8",
"Microsoft.Extensions.Logging": "9.0.3", "Microsoft.Extensions.Logging": "9.0.8",
"Microsoft.Extensions.Logging.Abstractions": "9.0.3", "Microsoft.Extensions.Logging.Abstractions": "9.0.8",
"Microsoft.Extensions.Logging.Configuration": "9.0.3", "Microsoft.Extensions.Logging.Configuration": "9.0.8",
"Microsoft.Extensions.Logging.Console": "9.0.3", "Microsoft.Extensions.Logging.Console": "9.0.8",
"Microsoft.Extensions.Logging.Debug": "9.0.3", "Microsoft.Extensions.Logging.Debug": "9.0.8",
"Microsoft.Extensions.Logging.EventLog": "9.0.3", "Microsoft.Extensions.Logging.EventLog": "9.0.8",
"Microsoft.Extensions.Logging.EventSource": "9.0.3", "Microsoft.Extensions.Logging.EventSource": "9.0.8",
"Microsoft.Extensions.Options": "9.0.3" "Microsoft.Extensions.Options": "9.0.8"
} }
}, },
"Penumbra.Api": { "Penumbra.Api": {
"type": "Direct", "type": "Direct",
"requested": "[5.6.1, )", "requested": "[5.12.0, )",
"resolved": "5.6.1", "resolved": "5.12.0",
"contentHash": "pZesXuBB9cMXKN10EfLJclLay3vjRYQy376PXXcPdx33kNI0jHwgPXGcy6BtVmoaMvn4UvK+EqGVYlt2b/Ns9Q==" "contentHash": "XGWviAZgokj2djpH50FWgM24jOTpKUuDHvd0HwrzBRY6BEMmpb3HfGIl8+BDE/DqbpH63u6aO2TvzUV6BmXT5w=="
}, },
"SonarAnalyzer.CSharp": { "SonarAnalyzer.CSharp": {
"type": "Direct", "type": "Direct",
"requested": "[10.7.0.110445, )", "requested": "[10.15.0.120848, )",
"resolved": "10.7.0.110445", "resolved": "10.15.0.120848",
"contentHash": "U4v2LWopxADYkUv7Z5CX7ifKMdDVqHb7a1bzppIQnQi4WQR6z1Zi5rDkCHlVYGEd1U/WMz1IJCU8OmFZLJpVig==" "contentHash": "1hM3HVRl5jdC/ZBDu+G7CCYLXRGe/QaP01Zy+c9ETPhY7lWD8g8HiefY6sGaH0T3CJ4wAy0/waGgQTh0TYy0oQ=="
}, },
"System.IdentityModel.Tokens.Jwt": { "System.IdentityModel.Tokens.Jwt": {
"type": "Direct", "type": "Direct",
"requested": "[8.7.0, )", "requested": "[8.14.0, )",
"resolved": "8.7.0", "resolved": "8.14.0",
"contentHash": "8dKL3A9pVqYCJIXHd4H2epQqLxSvKeNxGonR0e5g89yMchyvsM/NLuB06otx29BicUd6+LUJZgNZmvYjjPsPGg==", "contentHash": "EYGgN/S+HK7S6F3GaaPLFAfK0UzMrkXFyWCvXpQWFYmZln3dqtbyIO7VuTM/iIIPMzkelg8ZLlBPvMhxj6nOAA==",
"dependencies": { "dependencies": {
"Microsoft.IdentityModel.JsonWebTokens": "8.7.0", "Microsoft.IdentityModel.JsonWebTokens": "8.14.0",
"Microsoft.IdentityModel.Tokens": "8.7.0" "Microsoft.IdentityModel.Tokens": "8.14.0"
} }
}, },
"K4os.Compression.LZ4": { "K4os.Compression.LZ4": {
@@ -153,342 +159,342 @@
}, },
"Microsoft.AspNetCore.Connections.Abstractions": { "Microsoft.AspNetCore.Connections.Abstractions": {
"type": "Transitive", "type": "Transitive",
"resolved": "9.0.3", "resolved": "9.0.8",
"contentHash": "MWkNy/Yhv2q5ZVYPHjHN6pEE5Ya1r4opqSpnsW60bgpDOT54zZ6Kpqub4Tcat8ENsR5PZcTZ3eeSAthweUb/KA==", "contentHash": "mONfcKx7I4h6Rg+3b20bRyuy/GWz2yLsCNzKKqh1X4OfxnI7l0rdSxBwO203ebZFhjrdXnqMl7Op0N1FQ1Q5DQ==",
"dependencies": { "dependencies": {
"Microsoft.Extensions.Features": "9.0.3" "Microsoft.Extensions.Features": "9.0.8"
} }
}, },
"Microsoft.AspNetCore.Http.Connections.Client": { "Microsoft.AspNetCore.Http.Connections.Client": {
"type": "Transitive", "type": "Transitive",
"resolved": "9.0.3", "resolved": "9.0.8",
"contentHash": "bLoLX67FBeYK1KKGfXrmBki/F9EAK8EKCNkyADtfFjQkJ1Qhhw1sjBlcL8TbVnZxk+FaFsyCeBPmSHgOwNIJ/A==", "contentHash": "Ob2n+H3358kvubgXu9hY95MZB6X91PUGJvtWaHGEX7eZ+9bYdUCYs57ukJiIziH+aD9yO9e36bgKIT1WJEtfmA==",
"dependencies": { "dependencies": {
"Microsoft.AspNetCore.Http.Connections.Common": "9.0.3", "Microsoft.AspNetCore.Http.Connections.Common": "9.0.8",
"Microsoft.Extensions.Logging.Abstractions": "9.0.3", "Microsoft.Extensions.Logging.Abstractions": "9.0.8",
"Microsoft.Extensions.Options": "9.0.3", "Microsoft.Extensions.Options": "9.0.8",
"System.Net.ServerSentEvents": "9.0.3" "System.Net.ServerSentEvents": "9.0.8"
} }
}, },
"Microsoft.AspNetCore.Http.Connections.Common": { "Microsoft.AspNetCore.Http.Connections.Common": {
"type": "Transitive", "type": "Transitive",
"resolved": "9.0.3", "resolved": "9.0.8",
"contentHash": "GYDAXEmaG/q9UgixPchsLAVbBUbdgG3hd8J7Af4k4GIKLsibAhos7QY7hHicyULJvRtl03totiRi5Z+JIKEnUA==", "contentHash": "150BRlecnjL+6C+yw/bDP49+ONh7BmaJZTRik6KtbaS+cWnEDVXnhE5PTKlFqCYBD5T8wdjKoF5+lzKHJUK47A==",
"dependencies": { "dependencies": {
"Microsoft.AspNetCore.Connections.Abstractions": "9.0.3" "Microsoft.AspNetCore.Connections.Abstractions": "9.0.8"
} }
}, },
"Microsoft.AspNetCore.SignalR.Client.Core": { "Microsoft.AspNetCore.SignalR.Client.Core": {
"type": "Transitive", "type": "Transitive",
"resolved": "9.0.3", "resolved": "9.0.8",
"contentHash": "R2N03AK5FH8KIENfJGER4SgjJFMJTBiYuLbovbRunp5R4knO+iysfbYMfEFO3kn98ElWr/747dS4AeWQOEEQsg==", "contentHash": "EZ4KaPVQ9rDxZYWQ1sYiPfXEbomhKwp5Fn/0q1XtOgTilV/nN2lgA06KTofVJSeVVRwYdlZggflcQNcKCG0xcg==",
"dependencies": { "dependencies": {
"Microsoft.AspNetCore.SignalR.Common": "9.0.3", "Microsoft.AspNetCore.SignalR.Common": "9.0.8",
"Microsoft.AspNetCore.SignalR.Protocols.Json": "9.0.3", "Microsoft.AspNetCore.SignalR.Protocols.Json": "9.0.8",
"Microsoft.Extensions.DependencyInjection": "9.0.3", "Microsoft.Extensions.DependencyInjection": "9.0.8",
"Microsoft.Extensions.Logging": "9.0.3", "Microsoft.Extensions.Logging": "9.0.8",
"System.Threading.Channels": "9.0.3" "System.Threading.Channels": "9.0.8"
} }
}, },
"Microsoft.AspNetCore.SignalR.Common": { "Microsoft.AspNetCore.SignalR.Common": {
"type": "Transitive", "type": "Transitive",
"resolved": "9.0.3", "resolved": "9.0.8",
"contentHash": "/568tq8YVas1mDgeScmQdQV4ZDRjdyqDS3rAo17R5Bs4puMaNM80wQSwcvsmN5gSwH6L/XRTmD1J1uRIyKXrCg==", "contentHash": "oNOEDf2UGLU63Qi7LB8OJdfG1CGybVO34bhotpkvAQUJ5zH8Ewf7EvqeHlUgg6cVyrdC+vewOFxTysw212FTyw==",
"dependencies": { "dependencies": {
"Microsoft.AspNetCore.Connections.Abstractions": "9.0.3", "Microsoft.AspNetCore.Connections.Abstractions": "9.0.8",
"Microsoft.Extensions.Options": "9.0.3" "Microsoft.Extensions.Options": "9.0.8"
} }
}, },
"Microsoft.AspNetCore.SignalR.Protocols.Json": { "Microsoft.AspNetCore.SignalR.Protocols.Json": {
"type": "Transitive", "type": "Transitive",
"resolved": "9.0.3", "resolved": "9.0.8",
"contentHash": "jvOdsquqrbWMP3/Aq4s8/yVeCxBkjvxarv/2WgubKkQT8nZ46aKY3Rvj1uolp4N3TuaMGlnd6mhK/tF7jCat2Q==", "contentHash": "9LtBkzS2iYOSiUx1NDI91abM5xxD5MUYtdlvwCtMMr6YdsMzHvDUrgPK2N3hpYE94vmj0srt423Kwd1aOqmGPg==",
"dependencies": { "dependencies": {
"Microsoft.AspNetCore.SignalR.Common": "9.0.3" "Microsoft.AspNetCore.SignalR.Common": "9.0.8"
} }
}, },
"Microsoft.Extensions.Configuration": { "Microsoft.Extensions.Configuration": {
"type": "Transitive", "type": "Transitive",
"resolved": "9.0.3", "resolved": "9.0.8",
"contentHash": "RIEeZxWYm77+OWLwgik7DzSVSONjqkmcbuCb1koZdGAV7BgOUWnLz80VMyHZMw3onrVwFCCMHBBdruBPuQTvkg==", "contentHash": "6m+8Xgmf8UWL0p/oGqBM+0KbHE5/ePXbV1hKXgC59zEv0aa0DW5oiiyxDbK5kH5j4gIvyD5uWL0+HadKBJngvQ==",
"dependencies": { "dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "9.0.3", "Microsoft.Extensions.Configuration.Abstractions": "9.0.8",
"Microsoft.Extensions.Primitives": "9.0.3" "Microsoft.Extensions.Primitives": "9.0.8"
} }
}, },
"Microsoft.Extensions.Configuration.Abstractions": { "Microsoft.Extensions.Configuration.Abstractions": {
"type": "Transitive", "type": "Transitive",
"resolved": "9.0.3", "resolved": "9.0.8",
"contentHash": "q5qlbm6GRUrle2ZZxy9aqS/wWoc+mRD3JeP6rcpiJTh5XcemYkplAcJKq8lU11ZfPom5lfbZZfnQvDqcUhqD5Q==", "contentHash": "yNou2KM35RvzOh4vUFtl2l33rWPvOCoba+nzEDJ+BgD8aOL/jew4WPCibQvntRfOJ2pJU8ARygSMD+pdjvDHuA==",
"dependencies": { "dependencies": {
"Microsoft.Extensions.Primitives": "9.0.3" "Microsoft.Extensions.Primitives": "9.0.8"
} }
}, },
"Microsoft.Extensions.Configuration.Binder": { "Microsoft.Extensions.Configuration.Binder": {
"type": "Transitive", "type": "Transitive",
"resolved": "9.0.3", "resolved": "9.0.8",
"contentHash": "ad82pYBUSQbd3WIboxsS1HzFdRuHKRa2CpYwie/o6dZAxUjt62yFwjoVdM7Iw2VO5fHV1rJwa7jJZBNZin0E7Q==", "contentHash": "0vK9DnYrYChdiH3yRZWkkp4x4LbrfkWEdBc5HOsQ8t/0CLOWKXKkkhOE8A1shlex0hGydbGrhObeypxz/QTm+w==",
"dependencies": { "dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "9.0.3" "Microsoft.Extensions.Configuration.Abstractions": "9.0.8"
} }
}, },
"Microsoft.Extensions.Configuration.CommandLine": { "Microsoft.Extensions.Configuration.CommandLine": {
"type": "Transitive", "type": "Transitive",
"resolved": "9.0.3", "resolved": "9.0.8",
"contentHash": "rVwz4ml/Jve/QzzUlyTVOKXVZ37op9RK6Ize4uPmJ3S5c2ErExoy816+dslBQ06ZrFq8M9bpnV5LVBuPD1ONHQ==", "contentHash": "vB6eDQ5prED5jHBqmSDNYzlCXsTSylYY7co9c7guhnz0zhx+jZ8BTHgO7y/Wl1dV2jAO15mKNWuyHRIRtWwGQg==",
"dependencies": { "dependencies": {
"Microsoft.Extensions.Configuration": "9.0.3", "Microsoft.Extensions.Configuration": "9.0.8",
"Microsoft.Extensions.Configuration.Abstractions": "9.0.3" "Microsoft.Extensions.Configuration.Abstractions": "9.0.8"
} }
}, },
"Microsoft.Extensions.Configuration.EnvironmentVariables": { "Microsoft.Extensions.Configuration.EnvironmentVariables": {
"type": "Transitive", "type": "Transitive",
"resolved": "9.0.3", "resolved": "9.0.8",
"contentHash": "fo84UIa8aSBG3pOtzLsgkj1YkOVfYFy2YWcRTCevHHAkuVsxnYnKBrcW2pyFgqqfQ/rT8K1nmRXHDdQIZ8PDig==", "contentHash": "9qileEYXDodlPN9DfPd5sHSfU2nSrI1r5BHVqLaLyb/7mPi335cy4ar/0ix4tXb2Aer/Pu4e5/zdwxt7lrtSyQ==",
"dependencies": { "dependencies": {
"Microsoft.Extensions.Configuration": "9.0.3", "Microsoft.Extensions.Configuration": "9.0.8",
"Microsoft.Extensions.Configuration.Abstractions": "9.0.3" "Microsoft.Extensions.Configuration.Abstractions": "9.0.8"
} }
}, },
"Microsoft.Extensions.Configuration.FileExtensions": { "Microsoft.Extensions.Configuration.FileExtensions": {
"type": "Transitive", "type": "Transitive",
"resolved": "9.0.3", "resolved": "9.0.8",
"contentHash": "tBNMSDJ2q7WQK2zwPhHY5I/q95t7sf6dT079mGrNm0yOZF/gM9JvR/LtCb/rwhRmh7A6XMnzv5WbpCh9KLq9EQ==", "contentHash": "2jgx58Jpk3oKT7KRn8x/cFf3QDTjQP+KUbyBnynAcB2iBx1Eq9EdNMCu0QEbYuaZOaQru/Kwdffary+hn58Wwg==",
"dependencies": { "dependencies": {
"Microsoft.Extensions.Configuration": "9.0.3", "Microsoft.Extensions.Configuration": "9.0.8",
"Microsoft.Extensions.Configuration.Abstractions": "9.0.3", "Microsoft.Extensions.Configuration.Abstractions": "9.0.8",
"Microsoft.Extensions.FileProviders.Abstractions": "9.0.3", "Microsoft.Extensions.FileProviders.Abstractions": "9.0.8",
"Microsoft.Extensions.FileProviders.Physical": "9.0.3", "Microsoft.Extensions.FileProviders.Physical": "9.0.8",
"Microsoft.Extensions.Primitives": "9.0.3" "Microsoft.Extensions.Primitives": "9.0.8"
} }
}, },
"Microsoft.Extensions.Configuration.Json": { "Microsoft.Extensions.Configuration.Json": {
"type": "Transitive", "type": "Transitive",
"resolved": "9.0.3", "resolved": "9.0.8",
"contentHash": "mjkp3ZwynNacZk4uq93I0DyCY48FZmi3yRV0xlfeDuWh44KcDunPXHwt8IWr4kL7cVM6eiFVe6YTJg97KzUAUA==", "contentHash": "vjxzcnL7ul322+kpvELisXaZl8/5MYs6JfI9DZLQWsao1nA/4FL48yPwDK986hbJTWc64JxOOaMym0SQ/dy32w==",
"dependencies": { "dependencies": {
"Microsoft.Extensions.Configuration": "9.0.3", "Microsoft.Extensions.Configuration": "9.0.8",
"Microsoft.Extensions.Configuration.Abstractions": "9.0.3", "Microsoft.Extensions.Configuration.Abstractions": "9.0.8",
"Microsoft.Extensions.Configuration.FileExtensions": "9.0.3", "Microsoft.Extensions.Configuration.FileExtensions": "9.0.8",
"Microsoft.Extensions.FileProviders.Abstractions": "9.0.3" "Microsoft.Extensions.FileProviders.Abstractions": "9.0.8"
} }
}, },
"Microsoft.Extensions.Configuration.UserSecrets": { "Microsoft.Extensions.Configuration.UserSecrets": {
"type": "Transitive", "type": "Transitive",
"resolved": "9.0.3", "resolved": "9.0.8",
"contentHash": "vwkBQ5jqmfX7nD7CFvB3k1uSeNBKRcYRDvlk3pxJzJfm/cgT4R+hQg5AFXW/1aLKjz0q7brpRocHC5GK2sjvEw==", "contentHash": "UgH18nQkuMJgxjn1539I83N6LhnKQlLhQm3ppe+PGsFpYsC6eGpF/1KvDRm/bmqsrg0NXhurrv4k2r0e8vWX/Q==",
"dependencies": { "dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "9.0.3", "Microsoft.Extensions.Configuration.Abstractions": "9.0.8",
"Microsoft.Extensions.Configuration.Json": "9.0.3", "Microsoft.Extensions.Configuration.Json": "9.0.8",
"Microsoft.Extensions.FileProviders.Abstractions": "9.0.3", "Microsoft.Extensions.FileProviders.Abstractions": "9.0.8",
"Microsoft.Extensions.FileProviders.Physical": "9.0.3" "Microsoft.Extensions.FileProviders.Physical": "9.0.8"
} }
}, },
"Microsoft.Extensions.DependencyInjection": { "Microsoft.Extensions.DependencyInjection": {
"type": "Transitive", "type": "Transitive",
"resolved": "9.0.3", "resolved": "9.0.8",
"contentHash": "lDbxJpkl6X8KZGpkAxgrrthQ42YeiR0xjPp7KPx+sCPc3ZbpaIbjzd0QQ+9kDdK2RU2DOl3pc6tQyAgEZY3V0A==", "contentHash": "JJjI2Fa+QtZcUyuNjbKn04OjIUX5IgFGFu/Xc+qvzh1rXdZHLcnqqVXhR4093bGirTwacRlHiVg1XYI9xum6QQ==",
"dependencies": { "dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.3" "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.8"
} }
}, },
"Microsoft.Extensions.DependencyInjection.Abstractions": { "Microsoft.Extensions.DependencyInjection.Abstractions": {
"type": "Transitive", "type": "Transitive",
"resolved": "9.0.3", "resolved": "9.0.8",
"contentHash": "TfaHPSe39NyL2wxkisRxXK7xvHGZYBZ+dy3r+mqGvnxKgAPdHkMu3QMQZI4pquP6W5FIQBqs8FJpWV8ffCgDqQ==" "contentHash": "xY3lTjj4+ZYmiKIkyWitddrp1uL5uYiweQjqo4BKBw01ZC4HhcfgLghDpPZcUlppgWAFqFy9SgkiYWOMx365pw=="
}, },
"Microsoft.Extensions.Diagnostics": { "Microsoft.Extensions.Diagnostics": {
"type": "Transitive", "type": "Transitive",
"resolved": "9.0.3", "resolved": "9.0.8",
"contentHash": "gqhbIq6adm0+/9IlDYmchekoxNkmUTm7rfTG3k4zzoQkjRuD8TQGwL1WnIcTDt4aQ+j+Vu0OQrjI8GlpJQQhIA==", "contentHash": "BKkLCFXzJvNmdngeYBf72VXoZqTJSb1orvjdzDLaGobicoGFBPW8ug2ru1nnEewMEwJzMgnsjHQY8EaKWmVhKg==",
"dependencies": { "dependencies": {
"Microsoft.Extensions.Configuration": "9.0.3", "Microsoft.Extensions.Configuration": "9.0.8",
"Microsoft.Extensions.Diagnostics.Abstractions": "9.0.3", "Microsoft.Extensions.Diagnostics.Abstractions": "9.0.8",
"Microsoft.Extensions.Options.ConfigurationExtensions": "9.0.3" "Microsoft.Extensions.Options.ConfigurationExtensions": "9.0.8"
} }
}, },
"Microsoft.Extensions.Diagnostics.Abstractions": { "Microsoft.Extensions.Diagnostics.Abstractions": {
"type": "Transitive", "type": "Transitive",
"resolved": "9.0.3", "resolved": "9.0.8",
"contentHash": "/fn0Xe8t+3YbMfwyTk4hFirWyAG1pBA5ogVYsrKAuuD2gbqOWhFuSA28auCmS3z8Y2eq3miDIKq4pFVRWA+J6g==", "contentHash": "UDY7blv4DCyIJ/8CkNrQKLaAZFypXQavRZ2DWf/2zi1mxYYKKw2t8AOCBWxNntyPZHPGhtEmL3snFM98ADZqTw==",
"dependencies": { "dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.3", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.8",
"Microsoft.Extensions.Options": "9.0.3" "Microsoft.Extensions.Options": "9.0.8"
} }
}, },
"Microsoft.Extensions.Features": { "Microsoft.Extensions.Features": {
"type": "Transitive", "type": "Transitive",
"resolved": "9.0.3", "resolved": "9.0.8",
"contentHash": "jZuO3APLh0ePwtT9PDxiMdPwpDdct/kuExlXLCZZ+XFje/Xt815MM827EFJuxTBAbL148ywyfJyjIZ92osP5WA==" "contentHash": "oyPrbpRFa0uWik3PMwpK1mbAr+inZTEkaBsnMjHyT74YN0ot6knA7OnyFLg+oM4MwW5PZIS4HHW9efy0+gj+oQ=="
}, },
"Microsoft.Extensions.FileProviders.Abstractions": { "Microsoft.Extensions.FileProviders.Abstractions": {
"type": "Transitive", "type": "Transitive",
"resolved": "9.0.3", "resolved": "9.0.8",
"contentHash": "umczZ3+QPpzlrW/lkvy+IB0p52+qZ5w++aqx2lTCMOaPKzwcbVdrJgiQ3ajw5QWBp7gChLUiCYkSlWUpfjv24g==", "contentHash": "4zZbQ4w+hCMm9J+z5NOj3giIPT2MhZxx05HX/MGuAmDBbjOuXlYIIRN+t4V6OLxy5nXZIcXO+dQMB/OWubuDkw==",
"dependencies": { "dependencies": {
"Microsoft.Extensions.Primitives": "9.0.3" "Microsoft.Extensions.Primitives": "9.0.8"
} }
}, },
"Microsoft.Extensions.FileProviders.Physical": { "Microsoft.Extensions.FileProviders.Physical": {
"type": "Transitive", "type": "Transitive",
"resolved": "9.0.3", "resolved": "9.0.8",
"contentHash": "th2+tQBV5oWjgKhip9GjiIv2AEK3QvfAO3tZcqV3F3dEt5D6Gb411RntCj1+8GS9HaRRSxjSGx/fCrMqIjkb1Q==", "contentHash": "FlOe2i7UUIfY0l0ChaIYtlXjdWWutR4DMRKZaGD6z4G1uVTteFkbBfxUIoi1uGmrZQxXe/yv/cfwiT0tK2xyXA==",
"dependencies": { "dependencies": {
"Microsoft.Extensions.FileProviders.Abstractions": "9.0.3", "Microsoft.Extensions.FileProviders.Abstractions": "9.0.8",
"Microsoft.Extensions.FileSystemGlobbing": "9.0.3", "Microsoft.Extensions.FileSystemGlobbing": "9.0.8",
"Microsoft.Extensions.Primitives": "9.0.3" "Microsoft.Extensions.Primitives": "9.0.8"
} }
}, },
"Microsoft.Extensions.FileSystemGlobbing": { "Microsoft.Extensions.FileSystemGlobbing": {
"type": "Transitive", "type": "Transitive",
"resolved": "9.0.3", "resolved": "9.0.8",
"contentHash": "Rec77KHk4iNpFznHi5/6wF3MlUDcKqg26t8gRYbUm1PSukZ4B6mrXpZsJSNOiwyhhQVkjYbaoZxi5XJgRQ5lFg==" "contentHash": "96Ub5LmwYfIGVoXkbe4kjs+ivK6fLBTwKJAOMfUNV0R+AkZRItlgROFqXEWMUlXBTPM1/kKu26Ueu5As6RDzJA=="
}, },
"Microsoft.Extensions.Hosting.Abstractions": { "Microsoft.Extensions.Hosting.Abstractions": {
"type": "Transitive", "type": "Transitive",
"resolved": "9.0.3", "resolved": "9.0.8",
"contentHash": "rHabYVhQsGYNfgnfnYLqZRx/hLe85i6jW5rnDjA9pjt3x7yjPv8T/EXcgN5T9T38FAVwZRA+RMGUkEHbxvCOBQ==", "contentHash": "WNrad20tySNCPe9aJUK7Wfwh+RiyLF+id02FKW8Qfc+HAzNQHazcqMXAbwG/kmbS89uvan/nKK1MufkRahjrJA==",
"dependencies": { "dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "9.0.3", "Microsoft.Extensions.Configuration.Abstractions": "9.0.8",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.3", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.8",
"Microsoft.Extensions.Diagnostics.Abstractions": "9.0.3", "Microsoft.Extensions.Diagnostics.Abstractions": "9.0.8",
"Microsoft.Extensions.FileProviders.Abstractions": "9.0.3", "Microsoft.Extensions.FileProviders.Abstractions": "9.0.8",
"Microsoft.Extensions.Logging.Abstractions": "9.0.3" "Microsoft.Extensions.Logging.Abstractions": "9.0.8"
} }
}, },
"Microsoft.Extensions.Logging": { "Microsoft.Extensions.Logging": {
"type": "Transitive", "type": "Transitive",
"resolved": "9.0.3", "resolved": "9.0.8",
"contentHash": "utIi2R1nm+PCWkvWBf1Ou6LWqg9iLfHU23r8yyU9VCvda4dEs7xbTZSwGa5KuwbpzpgCbHCIuKaFHB3zyFmnGw==", "contentHash": "Z/7ze+0iheT7FJeZPqJKARYvyC2bmwu3whbm/48BJjdlGVvgDguoCqJIkI/67NkroTYobd5geai1WheNQvWrgA==",
"dependencies": { "dependencies": {
"Microsoft.Extensions.DependencyInjection": "9.0.3", "Microsoft.Extensions.DependencyInjection": "9.0.8",
"Microsoft.Extensions.Logging.Abstractions": "9.0.3", "Microsoft.Extensions.Logging.Abstractions": "9.0.8",
"Microsoft.Extensions.Options": "9.0.3" "Microsoft.Extensions.Options": "9.0.8"
} }
}, },
"Microsoft.Extensions.Logging.Abstractions": { "Microsoft.Extensions.Logging.Abstractions": {
"type": "Transitive", "type": "Transitive",
"resolved": "9.0.3", "resolved": "9.0.8",
"contentHash": "H/MBMLt9A/69Ux4OrV7oCKt3DcMT04o5SCqDolulzQA66TLFEpYYb4qedMs/uwrLtyHXGuDGWKZse/oa8W9AZw==", "contentHash": "pYnAffJL7ARD/HCnnPvnFKSIHnTSmWz84WIlT9tPeQ4lHNiu0Az7N/8itihWvcF8sT+VVD5lq8V+ckMzu4SbOw==",
"dependencies": { "dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.3" "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.8"
} }
}, },
"Microsoft.Extensions.Logging.Configuration": { "Microsoft.Extensions.Logging.Configuration": {
"type": "Transitive", "type": "Transitive",
"resolved": "9.0.3", "resolved": "9.0.8",
"contentHash": "eVZsaKNyK0g0C1qp0mmn4Q2PiX+bXdkz8+zVkXyVMk8IvoWfmTjLjEq1MQlwt1A22lToANPiUrxPJ7Tt3V5puw==", "contentHash": "Us4evDN3lbp1beVgrpxkSXKrbntVGAK+YbSo9P9driiU9PK05+ShhgesJ3aj7SuDfr3mqqcEgrMJ87Vu8t5dhw==",
"dependencies": { "dependencies": {
"Microsoft.Extensions.Configuration": "9.0.3", "Microsoft.Extensions.Configuration": "9.0.8",
"Microsoft.Extensions.Configuration.Abstractions": "9.0.3", "Microsoft.Extensions.Configuration.Abstractions": "9.0.8",
"Microsoft.Extensions.Configuration.Binder": "9.0.3", "Microsoft.Extensions.Configuration.Binder": "9.0.8",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.3", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.8",
"Microsoft.Extensions.Logging": "9.0.3", "Microsoft.Extensions.Logging": "9.0.8",
"Microsoft.Extensions.Logging.Abstractions": "9.0.3", "Microsoft.Extensions.Logging.Abstractions": "9.0.8",
"Microsoft.Extensions.Options": "9.0.3", "Microsoft.Extensions.Options": "9.0.8",
"Microsoft.Extensions.Options.ConfigurationExtensions": "9.0.3" "Microsoft.Extensions.Options.ConfigurationExtensions": "9.0.8"
} }
}, },
"Microsoft.Extensions.Logging.Console": { "Microsoft.Extensions.Logging.Console": {
"type": "Transitive", "type": "Transitive",
"resolved": "9.0.3", "resolved": "9.0.8",
"contentHash": "o9VXLOdpTAro1q7ZThIB3S8OHrRn5pr8cFUCiN85fiwlfAt2DhU4ZIfHy+jCNbf7y7S5Exbr3dlDE8mKNrs0Yg==", "contentHash": "mPp9xB9MjiPuodh9z/+6zEGNj2kSVeXQtdbIBHlhUYqxX22gzJkx0ycPY42q4/OT/SzFV/TJ989Pa3sA/8ZBeA==",
"dependencies": { "dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.3", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.8",
"Microsoft.Extensions.Logging": "9.0.3", "Microsoft.Extensions.Logging": "9.0.8",
"Microsoft.Extensions.Logging.Abstractions": "9.0.3", "Microsoft.Extensions.Logging.Abstractions": "9.0.8",
"Microsoft.Extensions.Logging.Configuration": "9.0.3", "Microsoft.Extensions.Logging.Configuration": "9.0.8",
"Microsoft.Extensions.Options": "9.0.3" "Microsoft.Extensions.Options": "9.0.8"
} }
}, },
"Microsoft.Extensions.Logging.Debug": { "Microsoft.Extensions.Logging.Debug": {
"type": "Transitive", "type": "Transitive",
"resolved": "9.0.3", "resolved": "9.0.8",
"contentHash": "BlKgvNYjD6mY5GXpMCf9zPAsrovMgW5mzCOT7SpoOSyI1478zldf+7PKvDIscC277z5zjSO3yi/OuIWpnTZmdA==", "contentHash": "OwHQFVITsONEoizShc1yNYTUvMq0kT9j/LhwAKMsA7OZqtrBXuqjosbSvzkJZ9o+KWAozDh5Y1Vtpe5p/8/1qA==",
"dependencies": { "dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.3", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.8",
"Microsoft.Extensions.Logging": "9.0.3", "Microsoft.Extensions.Logging": "9.0.8",
"Microsoft.Extensions.Logging.Abstractions": "9.0.3" "Microsoft.Extensions.Logging.Abstractions": "9.0.8"
} }
}, },
"Microsoft.Extensions.Logging.EventLog": { "Microsoft.Extensions.Logging.EventLog": {
"type": "Transitive", "type": "Transitive",
"resolved": "9.0.3", "resolved": "9.0.8",
"contentHash": "/+elZUHGgB3oHKO9St/Ql/qfze9O+UbXj+9FOj1gIshLCFXcPlhpKoI11jE6eIV0kbs1P/EeffJl4KDFyvAiJQ==", "contentHash": "/gMwlll21UJcaXlitUqd+rs9jH36EJz5BpFVPshyOqz5u0qyV1pFnTWm5vhyx+g6gwVYENSLgpazR1urNv83xw==",
"dependencies": { "dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.3", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.8",
"Microsoft.Extensions.Logging": "9.0.3", "Microsoft.Extensions.Logging": "9.0.8",
"Microsoft.Extensions.Logging.Abstractions": "9.0.3", "Microsoft.Extensions.Logging.Abstractions": "9.0.8",
"Microsoft.Extensions.Options": "9.0.3", "Microsoft.Extensions.Options": "9.0.8",
"System.Diagnostics.EventLog": "9.0.3" "System.Diagnostics.EventLog": "9.0.8"
} }
}, },
"Microsoft.Extensions.Logging.EventSource": { "Microsoft.Extensions.Logging.EventSource": {
"type": "Transitive", "type": "Transitive",
"resolved": "9.0.3", "resolved": "9.0.8",
"contentHash": "hgG0EGEHnngQFQNqJ5ungEykqaQ5Tik0Gpkb38pea2a5cR3pWlZR4vuYLDdtTgSiKEKByXz/3wNQ7qAqXamEEA==", "contentHash": "aGMFc/1P+315d07iyxSe6lEoZ0JjOPJ+Mfv9rrV2PvR2DFu1/pSi/SItHw1iChJOZgslNKJE97g1a9nLX3qQYA==",
"dependencies": { "dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.3", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.8",
"Microsoft.Extensions.Logging": "9.0.3", "Microsoft.Extensions.Logging": "9.0.8",
"Microsoft.Extensions.Logging.Abstractions": "9.0.3", "Microsoft.Extensions.Logging.Abstractions": "9.0.8",
"Microsoft.Extensions.Options": "9.0.3", "Microsoft.Extensions.Options": "9.0.8",
"Microsoft.Extensions.Primitives": "9.0.3" "Microsoft.Extensions.Primitives": "9.0.8"
} }
}, },
"Microsoft.Extensions.Options": { "Microsoft.Extensions.Options": {
"type": "Transitive", "type": "Transitive",
"resolved": "9.0.3", "resolved": "9.0.8",
"contentHash": "xE7MpY70lkw1oiid5y6FbL9dVw8oLfkx8RhSNGN8sSzBlCqGn0SyT3Fqc8tZnDaPIq7Z8R9RTKlS564DS+MV3g==", "contentHash": "OmTaQ0v4gxGQkehpwWIqPoEiwsPuG/u4HUsbOFoWGx4DKET2AXzopnFe/fE608FIhzc/kcg2p8JdyMRCCUzitQ==",
"dependencies": { "dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.3", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.8",
"Microsoft.Extensions.Primitives": "9.0.3" "Microsoft.Extensions.Primitives": "9.0.8"
} }
}, },
"Microsoft.Extensions.Options.ConfigurationExtensions": { "Microsoft.Extensions.Options.ConfigurationExtensions": {
"type": "Transitive", "type": "Transitive",
"resolved": "9.0.3", "resolved": "9.0.8",
"contentHash": "PcyYHQglKnWVZHSPaL6v2qnfsIuFw8tSq7cyXHg3OeuDVn/CqmdWUjRiZomCF/Gi+qCi+ksz0lFphg2cNvB8zQ==", "contentHash": "eW2s6n06x0w6w4nsX+SvpgsFYkl+Y0CttYAt6DKUXeqprX+hzNqjSfOh637fwNJBg7wRBrOIRHe49gKiTgJxzQ==",
"dependencies": { "dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "9.0.3", "Microsoft.Extensions.Configuration.Abstractions": "9.0.8",
"Microsoft.Extensions.Configuration.Binder": "9.0.3", "Microsoft.Extensions.Configuration.Binder": "9.0.8",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.3", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.8",
"Microsoft.Extensions.Options": "9.0.3", "Microsoft.Extensions.Options": "9.0.8",
"Microsoft.Extensions.Primitives": "9.0.3" "Microsoft.Extensions.Primitives": "9.0.8"
} }
}, },
"Microsoft.Extensions.Primitives": { "Microsoft.Extensions.Primitives": {
"type": "Transitive", "type": "Transitive",
"resolved": "9.0.3", "resolved": "9.0.8",
"contentHash": "yCCJHvBcRyqapMSNzP+kTc57Eaavq2cr5Tmuil6/XVnipQf5xmskxakSQ1enU6S4+fNg3sJ27WcInV64q24JsA==" "contentHash": "tizSIOEsIgSNSSh+hKeUVPK7xmTIjR8s+mJWOu1KXV3htvNQiPMFRMO17OdI1y/4ZApdBVk49u/08QGC9yvLug=="
}, },
"Microsoft.IdentityModel.Abstractions": { "Microsoft.IdentityModel.Abstractions": {
"type": "Transitive", "type": "Transitive",
"resolved": "8.7.0", "resolved": "8.14.0",
"contentHash": "OQd5aVepYvh5evOmBMeAYjMIpEcTf1ZCBZaU7Nh/RlhhdXefjFDJeP1L2F2zeNT1unFr+wUu/h3Ac2Xb4BXU6w==" "contentHash": "iwbCpSjD3ehfTwBhtSNEtKPK0ICun6ov7Ibx6ISNA9bfwIyzI2Siwyi9eJFCJBwxowK9xcA1mj+jBWiigeqgcQ=="
}, },
"Microsoft.IdentityModel.JsonWebTokens": { "Microsoft.IdentityModel.JsonWebTokens": {
"type": "Transitive", "type": "Transitive",
"resolved": "8.7.0", "resolved": "8.14.0",
"contentHash": "uzsSAWhNhbrkWbQKBTE8QhzviU6sr3bJ1Bkv7gERlhswfSKOp7HsxTRLTPBpx/whQ/GRRHEwMg8leRIPbMrOgw==", "contentHash": "4jOpiA4THdtpLyMdAb24dtj7+6GmvhOhxf5XHLYWmPKF8ApEnApal1UnJsKO4HxUWRXDA6C4WQVfYyqsRhpNpQ==",
"dependencies": { "dependencies": {
"Microsoft.IdentityModel.Tokens": "8.7.0" "Microsoft.IdentityModel.Tokens": "8.14.0"
} }
}, },
"Microsoft.IdentityModel.Logging": { "Microsoft.IdentityModel.Logging": {
"type": "Transitive", "type": "Transitive",
"resolved": "8.7.0", "resolved": "8.14.0",
"contentHash": "Bs0TznPAu+nxa9rAVHJ+j3CYECHJkT3tG8AyBfhFYlT5ldsDhoxFT7J+PKxJHLf+ayqWfvDZHHc4639W2FQCxA==", "contentHash": "eqqnemdW38CKZEHS6diA50BV94QICozDZEvSrsvN3SJXUFwVB9gy+/oz76gldP7nZliA16IglXjXTCTdmU/Ejg==",
"dependencies": { "dependencies": {
"Microsoft.IdentityModel.Abstractions": "8.7.0" "Microsoft.IdentityModel.Abstractions": "8.14.0"
} }
}, },
"Microsoft.IdentityModel.Tokens": { "Microsoft.IdentityModel.Tokens": {
"type": "Transitive", "type": "Transitive",
"resolved": "8.7.0", "resolved": "8.14.0",
"contentHash": "5Z6voXjRXAnGklhmZd1mKz89UhcF5ZQQZaZc2iKrOuL4Li1UihG2vlJx8IbiFAOIxy/xdbsAm0A+WZEaH5fxng==", "contentHash": "lKIZiBiGd36k02TCdMHp1KlNWisyIvQxcYJvIkz7P4gSQ9zi8dgh6S5Grj8NNG7HWYIPfQymGyoZ6JB5d1Lo1g==",
"dependencies": { "dependencies": {
"Microsoft.Extensions.Logging.Abstractions": "8.0.2", "Microsoft.Extensions.Logging.Abstractions": "8.0.0",
"Microsoft.IdentityModel.Logging": "8.7.0" "Microsoft.IdentityModel.Logging": "8.14.0"
} }
}, },
"Microsoft.NET.StringTools": { "Microsoft.NET.StringTools": {
@@ -498,8 +504,8 @@
}, },
"System.Diagnostics.EventLog": { "System.Diagnostics.EventLog": {
"type": "Transitive", "type": "Transitive",
"resolved": "9.0.3", "resolved": "9.0.8",
"contentHash": "0nDJBZ06DVdTG2vvCZ4XjazLVaFawdT0pnji23ISX8I8fEOlRJyzH2I0kWiAbCtFwry2Zir4qE4l/GStLATfFw==" "contentHash": "gebRF3JLLJ76jz1CQpvwezNapZUjFq20JQsaGHzBH0DzlkHBLpdhwkOei9usiOkIGMwU/L0ALWpNe1JE+5/itw=="
}, },
"System.IO.Pipelines": { "System.IO.Pipelines": {
"type": "Transitive", "type": "Transitive",
@@ -508,16 +514,13 @@
}, },
"System.Net.ServerSentEvents": { "System.Net.ServerSentEvents": {
"type": "Transitive", "type": "Transitive",
"resolved": "9.0.3", "resolved": "9.0.8",
"contentHash": "Vs/C2V27bjtwLqYag9ATzHilcUn8VQTICre4jSBMGFUeSTxEZffTjb+xZwjcmPsVAjmSZmBI5N7Ezq8UFvqQQg==" "contentHash": "wrpra4YvKXL7VdsQMKPcPxyA8pXK22LcxaKGA8oEndgjLZ1ZSdKXTxEA2cPvvNpMEUBwZlgJ6oZYQ8aJcpapPg=="
}, },
"System.Threading.Channels": { "System.Threading.Channels": {
"type": "Transitive", "type": "Transitive",
"resolved": "9.0.3", "resolved": "9.0.8",
"contentHash": "Ao0iegVONKYVw0eWxJv0ArtMVfkFjgyyYKtUXru6xX5H95flSZWW3QCavD4PAgwpc0ETP38kGHaYbPzSE7sw2w==" "contentHash": "kpvkzWJoHR9os3/4LL5feaTTLD92+XzTqPyYLU2tw2BoJ4MrWCfkjGXtL7MsdpV/20e1+SamCbrPj2L9ptwgBA=="
},
"chaos.nacl": {
"type": "Project"
}, },
"maresynchronos.api": { "maresynchronos.api": {
"type": "Project", "type": "Project",

1
Penumbra.Api Submodule

Submodule Penumbra.Api added at c23ee05c1e

15
Program.cs Normal file
View File

@@ -0,0 +1,15 @@
using System;
using Mono.Cecil;
class Program
{
static void Main()
{
var asm = AssemblyDefinition.ReadAssembly("/Users/luca.genovese/Library/Application Support/XIV on Mac/dalamud/Hooks/dev/FFXIVClientStructs.dll");
var type = asm.MainModule.GetType("FFXIVClientStructs.FFXIV.Client.UI.AddonNamePlate/NamePlateObject");
foreach (var field in type.Fields)
{
Console.WriteLine($"FIELD {field.Name}: {field.FieldType}");
}
}
}

View File

@@ -1,2 +1,4 @@
# UmbraClient # Umbra Sync
🇫🇷
Ce projet est basé sur Mare Synchronos de DarkArchon. Le code original est sous licence MIT ; voir le fichier LICENSE_MIT pour plus de détails. Les commits après celui-ci sont sous licence AGPL v3.

351
UmbraAPI/.gitignore vendored
View File

@@ -1,351 +0,0 @@
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
.DS_Store
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
mono_crash.*
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
[Ll]ogs/
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# NuGet Symbol Packages
*.snupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
*.appxbundle
*.appxupload
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# CodeRush personal settings
.cr/personal
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# Local History for Visual Studio
.localhistory/
# BeatPulse healthcheck temp database
healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/

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