All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 2m9s
# Patchnotes 2.1.0 The changes in this update are more than just "patches". With a new UI, a new feature, and a bunch of bug fixes, improvements and a new member on the dev team, we thought this was more of a minor update. We would like to introduce @tsubasahane of MareCN to the team! We’re happy to work with them to bring Lightless and its features to the CN client as well as having another talented dev bring features and ideas to us. Speaking of which: # Location Sharing (Big shout out to @tsubasahane for bringing this feature) - Are you TIRED of scrambling to find the address of the venue you're in to share with your friends? We are introducing Location Sharing! An optional feature where you can share your location with direct pairs temporarily [30 minutes, 1 hour, 3 hours] minutes or until you turn it off for them. That's up to you! [#125](<#125>) [#49](<Lightless-Sync/LightlessServer#49>) - To share your location with a pair, click the three dots beside the pair and choose a duration to share with them. [#125](<#125>) [#49](<Lightless-Sync/LightlessServer#49>) - To view the location of someone who's shared with you, simply hover over the globe icon! [#125](<#125>) [#49](<Lightless-Sync/LightlessServer#49>) [1] # Model Optimization (Mesh Decimating) - This new option can automatically “simplify” incoming character meshes to help performance by reducing triangle counts. You choose how strong the reduction is (default/recommended is 80%). [#131](<#131>) - Decimation only kicks in when a mesh is above a certain triangle threshold, and only for the items that qualify for it and you selected for. [#131](<#131>) - Hair meshes is always excluded, since simplifying hair meshes is very prone to breaking. - You can find everything under Settings → Performance → Model Optimization. [#131](<#131>) + ** IF YOU HAVE USED DECIMATION IN TESTING, PLEASE CLEAR YOUR CACHE ❗ ** [2] # Animation (PAP) Validation (Safer animations) - Lightless now checks your currently animations to see if they work with your local skeleton/bone mod. If an animation matches, it’s included in what gets sent to other players. If it doesn’t, Lightless will skip it and write a warning to your log showing how many were skipped due to skeleton changes. Its defaulted to Unsafe (off). turn it on if you experience crashes from others users. [#131](<#131>) - Lightless also does the same kind of check for incoming animation files, to make sure they match the body/skeleton they were sent with. [#131](<#131>) - Because these checks can sometimes be a little picky, you can adjust how strict they are in Settings -> General -> Animation & Bones to reduce false positives. [#131](<#131>) # UI Changes (Thanks to @kyuwu for UI Changes) - The top part of the main screen has gotten a makeover. You can adjust the colors of the gradiant in the Color settings of Lightless. [#127](<#127>) [3] - Settings have gotten some changes as well to make this change more universal, and will use the same color settings. [#127](<#127>) - The particle effects of the gradient are toggleable in 'Settings -> UI -> Behavior' [#127](<#127>) - Instead of showing download/upload on bottom of Main UI, it will show VRAM usage and triangles with their optimization options next to it [#138](<#138>) # LightFinder / ShellFinder - UI Changes that follow our new design follow the color codes for the Gradient top as the main screen does. [#127](<#127>) [4] Co-authored-by: defnotken <itsdefnotken@gmail.com> Co-authored-by: azyges <aaaaaa@aaa.aaa> Co-authored-by: cake <admin@cakeandbanana.nl> Co-authored-by: Tsubasa <tsubasa@noreply.git.lightless-sync.org> Co-authored-by: choco <choco@patat.nl> Co-authored-by: celine <aaa@aaa.aaa> Co-authored-by: celine <celine@noreply.git.lightless-sync.org> Co-authored-by: Tsubasahane <wozaiha@gmail.com> Co-authored-by: cake <cake@noreply.git.lightless-sync.org> Reviewed-on: #123
971 lines
34 KiB
C#
971 lines
34 KiB
C#
using Dalamud.Bindings.ImGui;
|
|
using Dalamud.Interface.Colors;
|
|
using LightlessSync.LightlessConfiguration;
|
|
using LightlessSync.LightlessConfiguration.Models;
|
|
using LightlessSync.PlayerData.Handlers;
|
|
using LightlessSync.Services;
|
|
using LightlessSync.Services.Mediator;
|
|
using LightlessSync.Services.PairProcessing;
|
|
using LightlessSync.WebAPI.Files;
|
|
using LightlessSync.WebAPI.Files.Models;
|
|
using Microsoft.Extensions.Logging;
|
|
using System.Collections.Concurrent;
|
|
using System.Numerics;
|
|
|
|
namespace LightlessSync.UI;
|
|
|
|
public class DownloadUi : WindowMediatorSubscriberBase
|
|
{
|
|
private readonly LightlessConfigService _configService;
|
|
private readonly ConcurrentDictionary<GameObjectHandler, IReadOnlyDictionary<string, FileDownloadStatus>> _currentDownloads = new();
|
|
private readonly DalamudUtilService _dalamudUtilService;
|
|
private readonly FileUploadManager _fileTransferManager;
|
|
private readonly UiSharedService _uiShared;
|
|
private readonly PairProcessingLimiter _pairProcessingLimiter;
|
|
private readonly ConcurrentDictionary<GameObjectHandler, bool> _uploadingPlayers = new();
|
|
private readonly Dictionary<GameObjectHandler, Vector2> _smoothed = [];
|
|
private readonly Dictionary<GameObjectHandler, DownloadSpeedTracker> _downloadSpeeds = [];
|
|
private readonly Dictionary<GameObjectHandler, (int TotalFiles, long TotalBytes)> _downloadInitialTotals = [];
|
|
|
|
|
|
private byte _transferBoxTransparency = 100;
|
|
private bool _notificationDismissed = true;
|
|
private int _lastDownloadStateHash = 0;
|
|
|
|
public DownloadUi(ILogger<DownloadUi> logger, DalamudUtilService dalamudUtilService, LightlessConfigService configService,
|
|
PairProcessingLimiter pairProcessingLimiter, FileUploadManager fileTransferManager, LightlessMediator mediator, UiSharedService uiShared,
|
|
PerformanceCollectorService performanceCollectorService)
|
|
: base(logger, mediator, "Lightless Sync Downloads", performanceCollectorService)
|
|
{
|
|
_dalamudUtilService = dalamudUtilService;
|
|
_configService = configService;
|
|
_pairProcessingLimiter = pairProcessingLimiter;
|
|
_fileTransferManager = fileTransferManager;
|
|
_uiShared = uiShared;
|
|
|
|
SizeConstraints = new WindowSizeConstraints()
|
|
{
|
|
MaximumSize = new Vector2(500, 90),
|
|
MinimumSize = new Vector2(500, 90),
|
|
};
|
|
|
|
Flags |= ImGuiWindowFlags.NoMove;
|
|
Flags |= ImGuiWindowFlags.NoBackground;
|
|
Flags |= ImGuiWindowFlags.NoInputs;
|
|
Flags |= ImGuiWindowFlags.NoNavFocus;
|
|
Flags |= ImGuiWindowFlags.NoResize;
|
|
Flags |= ImGuiWindowFlags.NoScrollbar;
|
|
Flags |= ImGuiWindowFlags.NoTitleBar;
|
|
Flags |= ImGuiWindowFlags.NoDecoration;
|
|
Flags |= ImGuiWindowFlags.NoFocusOnAppearing;
|
|
|
|
DisableWindowSounds = true;
|
|
|
|
ForceMainWindow = true;
|
|
|
|
IsOpen = true;
|
|
|
|
Mediator.Subscribe<DownloadStartedMessage>(this, msg =>
|
|
{
|
|
_currentDownloads[msg.DownloadId] = msg.DownloadStatus;
|
|
|
|
var snap = msg.DownloadStatus.ToArray();
|
|
var totalFiles = snap.Sum(kv => kv.Value?.TotalFiles ?? 0);
|
|
var totalBytes = snap.Sum(kv => kv.Value?.TotalBytes ?? 0);
|
|
|
|
_downloadInitialTotals[msg.DownloadId] = (totalFiles, totalBytes);
|
|
_notificationDismissed = false;
|
|
});
|
|
Mediator.Subscribe<DownloadFinishedMessage>(this, (msg) =>
|
|
{
|
|
_currentDownloads.TryRemove(msg.DownloadId, out _);
|
|
|
|
// Dismiss notification if all downloads are complete
|
|
if (_currentDownloads.IsEmpty && !_notificationDismissed)
|
|
{
|
|
Mediator.Publish(new LightlessNotificationDismissMessage("pair_download_progress"));
|
|
_notificationDismissed = true;
|
|
_lastDownloadStateHash = 0;
|
|
}
|
|
});
|
|
Mediator.Subscribe<GposeStartMessage>(this, (_) => IsOpen = false);
|
|
Mediator.Subscribe<GposeEndMessage>(this, (_) => IsOpen = true);
|
|
Mediator.Subscribe<PlayerUploadingMessage>(this, (msg) =>
|
|
{
|
|
if (msg.IsUploading)
|
|
{
|
|
_uploadingPlayers[msg.Handler] = true;
|
|
}
|
|
else
|
|
{
|
|
_uploadingPlayers.TryRemove(msg.Handler, out _);
|
|
}
|
|
});
|
|
}
|
|
|
|
protected override void DrawInternal()
|
|
{
|
|
if (_configService.Current.ShowTransferWindow)
|
|
{
|
|
var limiterSnapshot = _pairProcessingLimiter.GetSnapshot();
|
|
|
|
// Check if download notifications are enabled (not set to TextOverlay)
|
|
var useNotifications =
|
|
_configService.Current.UseLightlessNotifications &&
|
|
_configService.Current.LightlessDownloadNotification == NotificationLocation.LightlessUi;
|
|
|
|
if (useNotifications)
|
|
{
|
|
if (!_currentDownloads.IsEmpty)
|
|
{
|
|
UpdateDownloadNotificationIfChanged(limiterSnapshot);
|
|
_notificationDismissed = false;
|
|
}
|
|
else if (!_notificationDismissed)
|
|
{
|
|
Mediator.Publish(new LightlessNotificationDismissMessage("pair_download_progress"));
|
|
_notificationDismissed = true;
|
|
_lastDownloadStateHash = 0;
|
|
}
|
|
}
|
|
|
|
DrawDownloadSummaryBox();
|
|
|
|
if (_configService.Current.ShowUploading)
|
|
{
|
|
const int transparency = 100;
|
|
foreach (var player in _uploadingPlayers.Select(p => p.Key).ToList())
|
|
{
|
|
var screenPos = _dalamudUtilService.WorldToScreen(player.GetGameObject());
|
|
if (screenPos == Vector2.Zero) continue;
|
|
|
|
try
|
|
{
|
|
using var _ = _uiShared.UidFont.Push();
|
|
var uploadText = "Uploading";
|
|
var textSize = ImGui.CalcTextSize(uploadText);
|
|
|
|
var drawList = ImGui.GetBackgroundDrawList();
|
|
UiSharedService.DrawOutlinedFont(drawList, uploadText, screenPos with { X = screenPos.X - textSize.X / 2f - 1, Y = screenPos.Y - textSize.Y / 2f - 1 },
|
|
UiSharedService.Color(255, 255, 0, transparency),
|
|
UiSharedService.Color(0, 0, 0, transparency),
|
|
2
|
|
);
|
|
}
|
|
catch
|
|
{
|
|
_logger.LogDebug("Error drawing upload progress");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (_configService.Current.ShowTransferBars)
|
|
{
|
|
DrawTransferBar();
|
|
}
|
|
}
|
|
|
|
private void DrawTransferBar()
|
|
{
|
|
const int dlBarBorder = 3;
|
|
const float rounding = 6f;
|
|
var shadowOffset = new Vector2(2, 2);
|
|
|
|
List<KeyValuePair<GameObjectHandler, IReadOnlyDictionary<string, FileDownloadStatus>>> transfers;
|
|
try
|
|
{
|
|
transfers = [.. _currentDownloads];
|
|
}
|
|
catch (ArgumentException)
|
|
{
|
|
return;
|
|
}
|
|
|
|
|
|
foreach (var transfer in transfers)
|
|
{
|
|
var transferKey = transfer.Key;
|
|
|
|
// Skip if no valid game object
|
|
if (transferKey.GetGameObject() == null)
|
|
continue;
|
|
|
|
var rawPos = _dalamudUtilService.WorldToScreen(transferKey.GetGameObject());
|
|
|
|
// If RawPos is zero, remove it from smoothed dictionary
|
|
if (rawPos == Vector2.Zero)
|
|
{
|
|
_smoothed.Remove(transferKey);
|
|
continue;
|
|
}
|
|
|
|
// Smoothing out the movement and fix jitter around the position.
|
|
Vector2 screenPos = _smoothed.TryGetValue(transferKey, out var lastPos)
|
|
? (rawPos - lastPos).Length() < 4f ? lastPos : rawPos
|
|
: rawPos;
|
|
_smoothed[transferKey] = screenPos;
|
|
|
|
var totalBytes = transfer.Value.Sum(c => c.Value.TotalBytes);
|
|
var transferredBytes = transfer.Value.Sum(c => c.Value.TransferredBytes);
|
|
|
|
// Per-player state counts
|
|
var dlSlot = 0;
|
|
var dlQueue = 0;
|
|
var dlProg = 0;
|
|
var dlDecomp = 0;
|
|
var dlComplete = 0;
|
|
|
|
foreach (var entry in transfer.Value)
|
|
{
|
|
var fileStatus = entry.Value;
|
|
switch (fileStatus.DownloadStatus)
|
|
{
|
|
case DownloadStatus.Initializing:
|
|
dlQueue++;
|
|
break;
|
|
case DownloadStatus.WaitingForSlot:
|
|
dlSlot++;
|
|
break;
|
|
case DownloadStatus.WaitingForQueue:
|
|
dlQueue++;
|
|
break;
|
|
case DownloadStatus.Downloading:
|
|
dlProg++;
|
|
break;
|
|
case DownloadStatus.Decompressing:
|
|
dlDecomp++;
|
|
break;
|
|
case DownloadStatus.Completed:
|
|
dlComplete++;
|
|
break;
|
|
}
|
|
}
|
|
|
|
var isAllComplete = dlComplete > 0 && dlProg == 0 && dlDecomp == 0 && dlQueue == 0 && dlSlot == 0;
|
|
|
|
string statusText;
|
|
if (dlProg > 0)
|
|
{
|
|
statusText = "Downloading";
|
|
}
|
|
else if (dlDecomp > 0)
|
|
{
|
|
statusText = "Decompressing";
|
|
}
|
|
else if (dlQueue > 0)
|
|
{
|
|
statusText = "Waiting for queue";
|
|
}
|
|
else if (dlSlot > 0)
|
|
{
|
|
statusText = "Waiting for slot";
|
|
}
|
|
else if (isAllComplete)
|
|
{
|
|
statusText = "Completed";
|
|
}
|
|
else
|
|
{
|
|
statusText = "Waiting";
|
|
}
|
|
|
|
var hasValidSize = totalBytes > 0;
|
|
|
|
var maxDlText = $"{UiSharedService.ByteToString(totalBytes, addSuffix: false)}/{UiSharedService.ByteToString(totalBytes)}";
|
|
var textSize = _configService.Current.TransferBarsShowText
|
|
? ImGui.CalcTextSize(maxDlText)
|
|
: new Vector2(10, 10);
|
|
|
|
int dlBarHeight = _configService.Current.TransferBarsHeight > ((int)textSize.Y + 5)
|
|
? _configService.Current.TransferBarsHeight
|
|
: (int)textSize.Y + 5;
|
|
int dlBarWidth = _configService.Current.TransferBarsWidth > ((int)textSize.X + 10)
|
|
? _configService.Current.TransferBarsWidth
|
|
: (int)textSize.X + 10;
|
|
|
|
var dlBarStart = new Vector2(screenPos.X - dlBarWidth / 2f, screenPos.Y - dlBarHeight / 2f);
|
|
var dlBarEnd = new Vector2(screenPos.X + dlBarWidth / 2f, screenPos.Y + dlBarHeight / 2f);
|
|
|
|
// Precompute rects
|
|
var outerStart = new Vector2(dlBarStart.X - dlBarBorder - 1, dlBarStart.Y - dlBarBorder - 1);
|
|
var outerEnd = new Vector2(dlBarEnd.X + dlBarBorder + 1, dlBarEnd.Y + dlBarBorder + 1);
|
|
var borderStart = new Vector2(dlBarStart.X - dlBarBorder, dlBarStart.Y - dlBarBorder);
|
|
var borderEnd = new Vector2(dlBarEnd.X + dlBarBorder, dlBarEnd.Y + dlBarBorder);
|
|
|
|
var drawList = ImGui.GetBackgroundDrawList();
|
|
|
|
drawList.AddRectFilled(
|
|
outerStart + shadowOffset,
|
|
outerEnd + shadowOffset,
|
|
UiSharedService.Color(0, 0, 0, 100 / 2),
|
|
rounding + 2
|
|
);
|
|
drawList.AddRectFilled(
|
|
outerStart,
|
|
outerEnd,
|
|
UiSharedService.Color(0, 0, 0, 100),
|
|
rounding + 2
|
|
);
|
|
drawList.AddRectFilled(
|
|
borderStart,
|
|
borderEnd,
|
|
UiSharedService.Color(ImGuiColors.DalamudGrey),
|
|
rounding
|
|
);
|
|
drawList.AddRectFilled(
|
|
dlBarStart,
|
|
dlBarEnd,
|
|
UiSharedService.Color(0, 0, 0, 100),
|
|
rounding
|
|
);
|
|
|
|
bool showFill = false;
|
|
double fillPercent = 0.0;
|
|
|
|
if (hasValidSize)
|
|
{
|
|
fillPercent = totalBytes > 0 ? transferredBytes / (double)totalBytes : 0.0;
|
|
if (isAllComplete && totalBytes > 0)
|
|
{
|
|
fillPercent = 1.0;
|
|
}
|
|
|
|
showFill = transferredBytes > 0 || isAllComplete;
|
|
}
|
|
|
|
if (showFill)
|
|
{
|
|
if (fillPercent < 0) fillPercent = 0;
|
|
if (fillPercent > 1) fillPercent = 1;
|
|
|
|
var progressEndX = dlBarStart.X + (float)(fillPercent * dlBarWidth);
|
|
var progressEnd = new Vector2(progressEndX, dlBarEnd.Y);
|
|
|
|
drawList.AddRectFilled(
|
|
dlBarStart,
|
|
progressEnd,
|
|
UiSharedService.Color(UIColors.Get("LightlessPurple")),
|
|
rounding
|
|
);
|
|
}
|
|
|
|
if (_configService.Current.TransferBarsShowText)
|
|
{
|
|
string downloadText;
|
|
|
|
if (dlProg > 0 && hasValidSize)
|
|
{
|
|
downloadText =
|
|
$"{statusText} {UiSharedService.ByteToString(transferredBytes, addSuffix: false)}/{UiSharedService.ByteToString(totalBytes)}";
|
|
}
|
|
else if (dlDecomp > 0)
|
|
{
|
|
downloadText = "Decompressing";
|
|
}
|
|
else if (isAllComplete)
|
|
{
|
|
downloadText = "Completed";
|
|
}
|
|
else
|
|
{
|
|
// Waiting states
|
|
downloadText = statusText;
|
|
}
|
|
|
|
var actualTextSize = ImGui.CalcTextSize(downloadText);
|
|
|
|
UiSharedService.DrawOutlinedFont(
|
|
drawList,
|
|
downloadText,
|
|
screenPos with
|
|
{
|
|
X = screenPos.X - actualTextSize.X / 2f - 1,
|
|
Y = screenPos.Y - actualTextSize.Y / 2f - 1
|
|
},
|
|
UiSharedService.Color(ImGuiColors.DalamudGrey),
|
|
UiSharedService.Color(0, 0, 0, 100),
|
|
1
|
|
);
|
|
}
|
|
}
|
|
|
|
if (_configService.Current.ShowUploading)
|
|
{
|
|
foreach (var player in _uploadingPlayers.Select(p => p.Key).ToList())
|
|
{
|
|
var screenPos = _dalamudUtilService.WorldToScreen(player.GetGameObject());
|
|
if (screenPos == Vector2.Zero) continue;
|
|
|
|
try
|
|
{
|
|
using var _ = _uiShared.UidFont.Push();
|
|
var uploadText = "Uploading";
|
|
|
|
var textSize = ImGui.CalcTextSize(uploadText);
|
|
|
|
var drawList = ImGui.GetBackgroundDrawList();
|
|
UiSharedService.DrawOutlinedFont(drawList, uploadText, screenPos with { X = screenPos.X - textSize.X / 2f - 1, Y = screenPos.Y - textSize.Y / 2f - 1 },
|
|
UiSharedService.Color(ImGuiColors.DalamudYellow),
|
|
UiSharedService.Color(0, 0, 0, 100),
|
|
2
|
|
);
|
|
}
|
|
catch
|
|
{
|
|
_logger.LogDebug("Error drawing upload progress");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void DrawDownloadSummaryBox()
|
|
{
|
|
if (_currentDownloads.IsEmpty)
|
|
return;
|
|
|
|
const float padding = 6f;
|
|
const float spacingY = 2f;
|
|
const float minBoxWidth = 320f;
|
|
|
|
var now = ImGui.GetTime();
|
|
|
|
int totalFiles = 0;
|
|
int transferredFiles = 0;
|
|
long totalBytes = 0;
|
|
long transferredBytes = 0;
|
|
|
|
var totalDlSlot = 0;
|
|
var totalDlQueue = 0;
|
|
var totalDlProg = 0;
|
|
var totalDlDecomp = 0;
|
|
var totalDlComplete = 0;
|
|
|
|
var perPlayer = new List<(
|
|
string Name,
|
|
int TransferredFiles,
|
|
int TotalFiles,
|
|
long TransferredBytes,
|
|
long TotalBytes,
|
|
double SpeedBytesPerSecond,
|
|
int DlSlot,
|
|
int DlQueue,
|
|
int DlProg,
|
|
int DlDecomp,
|
|
int DlComplete)>();
|
|
|
|
foreach (var transfer in _currentDownloads)
|
|
{
|
|
var handler = transfer.Key;
|
|
var statuses = transfer.Value.Values;
|
|
|
|
var (playerTotalFiles, playerTotalBytes) = _downloadInitialTotals.TryGetValue(handler, out var totals)
|
|
? totals
|
|
: (statuses.Sum(s => s.TotalFiles), statuses.Sum(s => s.TotalBytes));
|
|
|
|
var playerTransferredFiles = statuses.Count(s =>
|
|
s.DownloadStatus == DownloadStatus.Decompressing ||
|
|
s.TransferredBytes >= s.TotalBytes);
|
|
var playerTransferredBytes = statuses.Sum(s => s.TransferredBytes);
|
|
|
|
totalFiles += playerTotalFiles;
|
|
transferredFiles += playerTransferredFiles;
|
|
totalBytes += playerTotalBytes;
|
|
transferredBytes += playerTransferredBytes;
|
|
|
|
// per-player W/Q/P/D/C
|
|
var playerDlSlot = 0;
|
|
var playerDlQueue = 0;
|
|
var playerDlProg = 0;
|
|
var playerDlDecomp = 0;
|
|
var playerDlComplete = 0;
|
|
|
|
foreach (var entry in transfer.Value)
|
|
{
|
|
var fileStatus = entry.Value;
|
|
switch (fileStatus.DownloadStatus)
|
|
{
|
|
case DownloadStatus.Initializing:
|
|
case DownloadStatus.WaitingForQueue:
|
|
playerDlQueue++;
|
|
totalDlQueue++;
|
|
break;
|
|
case DownloadStatus.WaitingForSlot:
|
|
playerDlSlot++;
|
|
totalDlSlot++;
|
|
break;
|
|
case DownloadStatus.Downloading:
|
|
playerDlProg++;
|
|
totalDlProg++;
|
|
break;
|
|
case DownloadStatus.Decompressing:
|
|
playerDlDecomp++;
|
|
totalDlDecomp++;
|
|
break;
|
|
case DownloadStatus.Completed:
|
|
playerDlComplete++;
|
|
totalDlComplete++;
|
|
break;
|
|
}
|
|
}
|
|
|
|
double speed = 0;
|
|
if (playerTotalBytes > 0)
|
|
{
|
|
if (!_downloadSpeeds.TryGetValue(handler, out var tracker))
|
|
{
|
|
tracker = new DownloadSpeedTracker(windowSeconds: 3.0);
|
|
_downloadSpeeds[handler] = tracker;
|
|
}
|
|
|
|
speed = tracker.Update(now, playerTransferredBytes);
|
|
}
|
|
|
|
perPlayer.Add((
|
|
handler.Name,
|
|
playerTransferredFiles,
|
|
playerTotalFiles,
|
|
playerTransferredBytes,
|
|
playerTotalBytes,
|
|
speed,
|
|
playerDlSlot,
|
|
playerDlQueue,
|
|
playerDlProg,
|
|
playerDlDecomp,
|
|
playerDlComplete
|
|
));
|
|
}
|
|
|
|
// Clean speed trackers for players with no active downloads
|
|
foreach (var handler in _downloadSpeeds.Keys.ToList())
|
|
{
|
|
if (!_currentDownloads.ContainsKey(handler))
|
|
_downloadSpeeds.Remove(handler);
|
|
}
|
|
|
|
if (totalFiles == 0 || totalBytes == 0)
|
|
return;
|
|
|
|
var drawList = ImGui.GetBackgroundDrawList();
|
|
var windowPos = ImGui.GetWindowPos();
|
|
|
|
// Overall texts
|
|
var headerText =
|
|
$"Downloading {transferredFiles}/{totalFiles} files [W:{totalDlSlot}/Q:{totalDlQueue}/P:{totalDlProg}/D:{totalDlDecomp}/C:{totalDlComplete}]";
|
|
|
|
var bytesText =
|
|
$"{UiSharedService.ByteToString(transferredBytes, addSuffix: false)}/{UiSharedService.ByteToString(totalBytes)}";
|
|
|
|
var totalSpeed = perPlayer.Sum(p => p.SpeedBytesPerSecond);
|
|
var speedText = totalSpeed > 0
|
|
? $"{UiSharedService.ByteToString((long)totalSpeed)}/s"
|
|
: "Calculating in lightspeed...";
|
|
|
|
var headerSize = ImGui.CalcTextSize(headerText);
|
|
var bytesSize = ImGui.CalcTextSize(bytesText);
|
|
var totalSpeedSize = ImGui.CalcTextSize(speedText);
|
|
|
|
float contentWidth = headerSize.X;
|
|
if (bytesSize.X > contentWidth) contentWidth = bytesSize.X;
|
|
if (totalSpeedSize.X > contentWidth) contentWidth = totalSpeedSize.X;
|
|
|
|
if (_configService.Current.ShowPlayerLinesTransferWindow)
|
|
{
|
|
foreach (var p in perPlayer)
|
|
{
|
|
var line =
|
|
$"{p.Name} [W:{p.DlSlot}/Q:{p.DlQueue}/P:{p.DlProg}/D:{p.DlDecomp}/C:{p.DlComplete}] {p.TransferredFiles}/{p.TotalFiles}";
|
|
|
|
var lineSize = ImGui.CalcTextSize(line);
|
|
if (lineSize.X > contentWidth)
|
|
contentWidth = lineSize.X;
|
|
}
|
|
|
|
}
|
|
|
|
var lineHeight = ImGui.GetTextLineHeight();
|
|
var globalBarHeight = lineHeight * 0.8f;
|
|
var perPlayerBarHeight = lineHeight * 0.4f;
|
|
|
|
// Box width
|
|
float boxWidth = contentWidth + padding * 2;
|
|
if (boxWidth < minBoxWidth)
|
|
boxWidth = minBoxWidth;
|
|
|
|
// Box height
|
|
float boxHeight = 0;
|
|
boxHeight += padding;
|
|
boxHeight += globalBarHeight;
|
|
boxHeight += padding;
|
|
|
|
boxHeight += lineHeight + spacingY;
|
|
boxHeight += lineHeight + spacingY;
|
|
boxHeight += lineHeight * 1.4f + spacingY;
|
|
|
|
if (_configService.Current.ShowPlayerLinesTransferWindow)
|
|
{
|
|
foreach (var p in perPlayer)
|
|
{
|
|
boxHeight += lineHeight + spacingY;
|
|
|
|
var showBar = _configService.Current.ShowPlayerSpeedBarsTransferWindow
|
|
&& p.TransferredBytes > 0;
|
|
|
|
if (showBar)
|
|
{
|
|
boxHeight += perPlayerBarHeight + spacingY;
|
|
}
|
|
}
|
|
}
|
|
|
|
boxHeight += padding;
|
|
|
|
var boxMin = windowPos;
|
|
var boxMax = new Vector2(windowPos.X + boxWidth, windowPos.Y + boxHeight);
|
|
|
|
// Background + border
|
|
drawList.AddRectFilled(boxMin, boxMax, UiSharedService.Color(0, 0, 0, _transferBoxTransparency), 5f);
|
|
drawList.AddRect(boxMin, boxMax, UiSharedService.Color(ImGuiColors.DalamudGrey), 5f);
|
|
|
|
var cursor = boxMin + new Vector2(padding, padding);
|
|
|
|
var barMin = cursor;
|
|
var barMax = new Vector2(boxMin.X + boxWidth - padding, cursor.Y + globalBarHeight);
|
|
|
|
var progress = (float)transferredBytes / totalBytes;
|
|
if (progress < 0f) progress = 0f;
|
|
if (progress > 1f) progress = 1f;
|
|
|
|
drawList.AddRectFilled(barMin, barMax, UiSharedService.Color(40, 40, 40, _transferBoxTransparency), 3f);
|
|
drawList.AddRectFilled(
|
|
barMin,
|
|
new Vector2(barMin.X + (barMax.X - barMin.X) * progress, barMax.Y),
|
|
UiSharedService.Color(UIColors.Get("LightlessPurple")),
|
|
3f
|
|
);
|
|
|
|
cursor.Y = barMax.Y + padding;
|
|
|
|
// Header
|
|
UiSharedService.DrawOutlinedFont(
|
|
drawList,
|
|
headerText,
|
|
cursor,
|
|
UiSharedService.Color(ImGuiColors.DalamudWhite),
|
|
UiSharedService.Color(0, 0, 0, _transferBoxTransparency),
|
|
1
|
|
);
|
|
cursor.Y += lineHeight + spacingY;
|
|
|
|
// Bytes
|
|
UiSharedService.DrawOutlinedFont(
|
|
drawList,
|
|
bytesText,
|
|
cursor,
|
|
UiSharedService.Color(ImGuiColors.DalamudWhite),
|
|
UiSharedService.Color(0, 0, 0, _transferBoxTransparency),
|
|
1
|
|
);
|
|
cursor.Y += lineHeight + spacingY;
|
|
|
|
// Total speed
|
|
UiSharedService.DrawOutlinedFont(
|
|
drawList,
|
|
speedText,
|
|
cursor,
|
|
UiSharedService.Color(UIColors.Get("LightlessPurple")),
|
|
UiSharedService.Color(0, 0, 0, _transferBoxTransparency),
|
|
1
|
|
);
|
|
cursor.Y += lineHeight * 1.4f + spacingY;
|
|
|
|
var orderedPlayers = perPlayer.OrderByDescending(p => p.TotalBytes).ToList();
|
|
|
|
foreach (var p in orderedPlayers)
|
|
{
|
|
var hasSpeed = p.SpeedBytesPerSecond > 0;
|
|
var playerSpeedText = hasSpeed
|
|
? $"{UiSharedService.ByteToString((long)p.SpeedBytesPerSecond)}/s"
|
|
: "-";
|
|
|
|
var showBar = _configService.Current.ShowPlayerSpeedBarsTransferWindow
|
|
&& p.TransferredBytes > 0;
|
|
|
|
var labelLine =
|
|
$"{p.Name} [W:{p.DlSlot}/Q:{p.DlQueue}/P:{p.DlProg}/D:{p.DlDecomp}/C:{p.DlComplete}] {p.TransferredFiles}/{p.TotalFiles}";
|
|
|
|
if (!showBar)
|
|
{
|
|
UiSharedService.DrawOutlinedFont(
|
|
drawList,
|
|
labelLine,
|
|
cursor,
|
|
UiSharedService.Color(255, 255, 255, _transferBoxTransparency),
|
|
UiSharedService.Color(0, 0, 0, _transferBoxTransparency),
|
|
1
|
|
);
|
|
|
|
cursor.Y += lineHeight + spacingY;
|
|
continue;
|
|
}
|
|
|
|
UiSharedService.DrawOutlinedFont(
|
|
drawList,
|
|
labelLine,
|
|
cursor,
|
|
UiSharedService.Color(255, 255, 255, _transferBoxTransparency),
|
|
UiSharedService.Color(0, 0, 0, _transferBoxTransparency),
|
|
1
|
|
);
|
|
cursor.Y += lineHeight + spacingY;
|
|
|
|
// Bar background
|
|
var barBgMin = new Vector2(boxMin.X + padding, cursor.Y);
|
|
var barBgMax = new Vector2(boxMax.X - padding, cursor.Y + perPlayerBarHeight);
|
|
|
|
drawList.AddRectFilled(
|
|
barBgMin,
|
|
barBgMax,
|
|
UiSharedService.Color(40, 40, 40, _transferBoxTransparency),
|
|
3f
|
|
);
|
|
|
|
// Fill based on Progress of download
|
|
float ratio = 0f;
|
|
if (p.TotalBytes > 0)
|
|
ratio = (float)p.TransferredBytes / p.TotalBytes;
|
|
|
|
if (ratio < 0f) ratio = 0f;
|
|
if (ratio > 1f) ratio = 1f;
|
|
|
|
var fillX = barBgMin.X + (barBgMax.X - barBgMin.X) * ratio;
|
|
var barFillMax = new Vector2(fillX, barBgMax.Y);
|
|
|
|
drawList.AddRectFilled(
|
|
barBgMin,
|
|
barFillMax,
|
|
UiSharedService.Color(UIColors.Get("LightlessPurple")),
|
|
3f
|
|
);
|
|
|
|
// Text inside bar: downloading vs decompressing
|
|
string barText;
|
|
|
|
var isDecompressing = p.DlDecomp > 0;
|
|
var isAllComplete = p.DlComplete > 0 && p.DlProg == 0 && p.DlDecomp == 0 && p.DlQueue == 0 && p.DlSlot == 0;
|
|
|
|
if (isDecompressing)
|
|
{
|
|
// Keep bar full, static text showing decompressing
|
|
barText = "Decompressing...";
|
|
}
|
|
else if (isAllComplete)
|
|
{
|
|
barText = "Completed";
|
|
}
|
|
else
|
|
{
|
|
var bytesInside =
|
|
$"{UiSharedService.ByteToString(p.TransferredBytes, addSuffix: false)}/{UiSharedService.ByteToString(p.TotalBytes)}";
|
|
|
|
barText = hasSpeed
|
|
? $"{bytesInside} @ {playerSpeedText}"
|
|
: bytesInside;
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(barText))
|
|
{
|
|
var barTextSize = ImGui.CalcTextSize(barText);
|
|
|
|
var barTextPos = new Vector2(
|
|
barBgMin.X + ((barBgMax.X - barBgMin.X) - barTextSize.X) / 2f - 1,
|
|
barBgMin.Y + ((perPlayerBarHeight - barTextSize.Y) / 2f) - 1
|
|
);
|
|
|
|
UiSharedService.DrawOutlinedFont(
|
|
drawList,
|
|
barText,
|
|
barTextPos,
|
|
UiSharedService.Color(255, 255, 255, _transferBoxTransparency),
|
|
UiSharedService.Color(0, 0, 0, _transferBoxTransparency),
|
|
1
|
|
);
|
|
}
|
|
|
|
cursor.Y += perPlayerBarHeight + spacingY;
|
|
}
|
|
}
|
|
|
|
public override bool DrawConditions()
|
|
{
|
|
if (_uiShared.EditTrackerPosition) return true;
|
|
if (!_configService.Current.ShowTransferWindow && !_configService.Current.ShowTransferBars) return false;
|
|
if (_currentDownloads.IsEmpty && !_fileTransferManager.IsUploading && _uploadingPlayers.IsEmpty) return false;
|
|
if (!IsOpen) return false;
|
|
return true;
|
|
}
|
|
|
|
public override void PreDraw()
|
|
{
|
|
base.PreDraw();
|
|
|
|
if (_uiShared.EditTrackerPosition)
|
|
{
|
|
Flags &= ~ImGuiWindowFlags.NoMove;
|
|
Flags &= ~ImGuiWindowFlags.NoBackground;
|
|
Flags &= ~ImGuiWindowFlags.NoInputs;
|
|
Flags &= ~ImGuiWindowFlags.NoResize;
|
|
}
|
|
else
|
|
{
|
|
Flags |= ImGuiWindowFlags.NoMove;
|
|
Flags |= ImGuiWindowFlags.NoBackground;
|
|
Flags |= ImGuiWindowFlags.NoInputs;
|
|
Flags |= ImGuiWindowFlags.NoResize;
|
|
}
|
|
|
|
var maxHeight = ImGui.GetTextLineHeight() * (_configService.Current.ParallelDownloads + 3);
|
|
SizeConstraints = new()
|
|
{
|
|
MinimumSize = new Vector2(300, maxHeight),
|
|
MaximumSize = new Vector2(300, maxHeight),
|
|
};
|
|
}
|
|
|
|
private void UpdateDownloadNotificationIfChanged(PairProcessingLimiterSnapshot limiterSnapshot)
|
|
{
|
|
var downloadStatus = new List<(string playerName, float progress, string status)>(_currentDownloads.Count);
|
|
var hashCode = new HashCode();
|
|
|
|
foreach (var item in _currentDownloads)
|
|
{
|
|
var dlSlot = 0;
|
|
var dlQueue = 0;
|
|
var dlProg = 0;
|
|
var dlDecomp = 0;
|
|
var dlComplete = 0;
|
|
long totalBytes = 0;
|
|
long transferredBytes = 0;
|
|
|
|
// Single pass through the dictionary to count everything - avoid multiple LINQ iterations
|
|
foreach (var entry in item.Value)
|
|
{
|
|
var fileStatus = entry.Value;
|
|
switch (fileStatus.DownloadStatus)
|
|
{
|
|
case DownloadStatus.Initializing: dlQueue++; break;
|
|
case DownloadStatus.WaitingForSlot: dlSlot++; break;
|
|
case DownloadStatus.WaitingForQueue: dlQueue++; break;
|
|
case DownloadStatus.Downloading: dlProg++; break;
|
|
case DownloadStatus.Decompressing: dlDecomp++; break;
|
|
case DownloadStatus.Completed: dlComplete++; break;
|
|
}
|
|
totalBytes += fileStatus.TotalBytes;
|
|
transferredBytes += fileStatus.TransferredBytes;
|
|
}
|
|
|
|
var progress = totalBytes > 0 ? (float)transferredBytes / totalBytes : 0f;
|
|
if (dlComplete > 0 && dlProg == 0 && dlDecomp == 0 && dlQueue == 0 && dlSlot == 0)
|
|
{
|
|
progress = 1f;
|
|
}
|
|
|
|
string status;
|
|
if (dlDecomp > 0) status = "decompressing";
|
|
else if (dlProg > 0) status = "downloading";
|
|
else if (dlQueue > 0) status = "queued";
|
|
else if (dlSlot > 0) status = "waiting";
|
|
else if (dlComplete > 0) status = "completed";
|
|
else status = "completed";
|
|
|
|
downloadStatus.Add((item.Key.Name, progress, status));
|
|
|
|
// Build hash from meaningful state
|
|
hashCode.Add(item.Key.Name);
|
|
hashCode.Add(transferredBytes);
|
|
hashCode.Add(totalBytes);
|
|
hashCode.Add(status);
|
|
}
|
|
|
|
var queueWaiting = limiterSnapshot.IsEnabled ? limiterSnapshot.Waiting : 0;
|
|
hashCode.Add(queueWaiting);
|
|
|
|
var currentHash = hashCode.ToHashCode();
|
|
|
|
// Only update notification if state has actually changed
|
|
if (currentHash != _lastDownloadStateHash)
|
|
{
|
|
_lastDownloadStateHash = currentHash;
|
|
if (downloadStatus.Count > 0 || queueWaiting > 0)
|
|
{
|
|
Mediator.Publish(new PairDownloadStatusMessage(downloadStatus, queueWaiting));
|
|
}
|
|
}
|
|
}
|
|
|
|
private sealed class DownloadSpeedTracker
|
|
{
|
|
private readonly Queue<(double Time, long Bytes)> _samples = new();
|
|
private readonly double _windowSeconds;
|
|
|
|
public double SpeedBytesPerSecond { get; private set; }
|
|
|
|
public DownloadSpeedTracker(double windowSeconds = 3.0)
|
|
{
|
|
_windowSeconds = windowSeconds;
|
|
}
|
|
|
|
public double Update(double now, long totalBytes)
|
|
{
|
|
if (_samples.Count > 0 && totalBytes < _samples.Last().Bytes)
|
|
{
|
|
_samples.Clear();
|
|
}
|
|
|
|
_samples.Enqueue((now, totalBytes));
|
|
|
|
while (_samples.Count > 0 && now - _samples.Peek().Time > _windowSeconds)
|
|
_samples.Dequeue();
|
|
|
|
if (_samples.Count < 2)
|
|
{
|
|
SpeedBytesPerSecond = 0;
|
|
return SpeedBytesPerSecond;
|
|
}
|
|
|
|
var oldest = _samples.Peek();
|
|
var newest = _samples.Last();
|
|
|
|
var dt = newest.Time - oldest.Time;
|
|
if (dt <= 0.0001)
|
|
{
|
|
SpeedBytesPerSecond = 0;
|
|
return SpeedBytesPerSecond;
|
|
}
|
|
|
|
var dBytes = newest.Bytes - oldest.Bytes;
|
|
if (dBytes <= 0)
|
|
{
|
|
SpeedBytesPerSecond = 0;
|
|
return SpeedBytesPerSecond;
|
|
}
|
|
|
|
|
|
const long minBytesForSpeed = 32 * 1024;
|
|
if (dBytes < minBytesForSpeed)
|
|
{
|
|
|
|
return SpeedBytesPerSecond;
|
|
}
|
|
|
|
var avg = dBytes / dt;
|
|
|
|
const double alpha = 0.3;
|
|
SpeedBytesPerSecond = SpeedBytesPerSecond <= 0
|
|
? avg
|
|
: SpeedBytesPerSecond * (1 - alpha) + avg * alpha;
|
|
|
|
return SpeedBytesPerSecond;
|
|
}
|
|
}
|
|
} |