mirror of
https://github.com/NotAKidoS/NAK_CVR_Mods.git
synced 2025-09-02 22:39:22 +00:00
Move many mods to Deprecated folder, fix spelling
This commit is contained in:
parent
5e822cec8d
commit
0042590aa6
539 changed files with 7475 additions and 3120 deletions
|
@ -0,0 +1,43 @@
|
|||
namespace NAK.BetterContentLoading;
|
||||
|
||||
public class ConcurrentPriorityQueue<TElement, TPriority> where TPriority : IComparable<TPriority>
|
||||
{
|
||||
private readonly object _lock = new();
|
||||
private readonly SortedDictionary<TPriority, Queue<TElement>> _queues = new();
|
||||
|
||||
public void Enqueue(TElement item, TPriority priority)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (!_queues.TryGetValue(priority, out var queue))
|
||||
{
|
||||
queue = new Queue<TElement>();
|
||||
_queues[priority] = queue;
|
||||
}
|
||||
queue.Enqueue(item);
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryDequeue(out TElement item, out TPriority priority)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_queues.Count == 0)
|
||||
{
|
||||
item = default;
|
||||
priority = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
var firstQueue = _queues.First();
|
||||
priority = firstQueue.Key;
|
||||
var queue = firstQueue.Value;
|
||||
item = queue.Dequeue();
|
||||
|
||||
if (queue.Count == 0)
|
||||
_queues.Remove(priority);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
namespace NAK.BetterContentLoading;
|
||||
|
||||
public partial class DownloadManager2
|
||||
{
|
||||
private void StartBandwidthMonitor()
|
||||
{
|
||||
Task.Run(async () =>
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
await Task.Delay(1000);
|
||||
Interlocked.Exchange(ref _bytesReadLastSecond, 0);
|
||||
Interlocked.Exchange(ref _completedDownloads, 0);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private int ComputeUsableBandwidthPerDownload()
|
||||
{
|
||||
var activeCount = _activeDownloads.Count;
|
||||
if (activeCount == 0) return MaxDownloadBandwidth;
|
||||
return MaxDownloadBandwidth / activeCount;
|
||||
}
|
||||
}
|
126
BetterContentLoading/DownloadManager2/DownloadManager.Core.cs
Normal file
126
BetterContentLoading/DownloadManager2/DownloadManager.Core.cs
Normal file
|
@ -0,0 +1,126 @@
|
|||
using System.Collections.Concurrent;
|
||||
using ABI_RC.Core;
|
||||
using ABI_RC.Core.IO;
|
||||
using ABI_RC.Core.IO.AssetManagement;
|
||||
|
||||
namespace NAK.BetterContentLoading;
|
||||
|
||||
public partial class DownloadManager2
|
||||
{
|
||||
#region Singleton
|
||||
private static DownloadManager2 _instance;
|
||||
public static DownloadManager2 Instance => _instance ??= new DownloadManager2();
|
||||
#endregion
|
||||
|
||||
#region Settings
|
||||
public bool IsDebugEnabled { get; set; } = true;
|
||||
public bool PrioritizeFriends { get; set; } = true;
|
||||
public bool PrioritizeDistance { get; set; } = true;
|
||||
public float PriorityDownloadDistance { get; set; } = 25f;
|
||||
public int MaxConcurrentDownloads { get; set; } = 5;
|
||||
public int MaxDownloadBandwidth { get; set; } // 100MB default
|
||||
private const int THROTTLE_THRESHOLD = 25 * 1024 * 1024; // 25MB threshold for throttling
|
||||
#endregion
|
||||
|
||||
#region State
|
||||
private readonly ConcurrentDictionary<string, DownloadTask2> _activeDownloads;
|
||||
private readonly ConcurrentPriorityQueue<DownloadTask2, float> _queuedDownloads;
|
||||
private readonly ConcurrentDictionary<string, DownloadTask2> _completedDownloads;
|
||||
private readonly object _downloadLock = new();
|
||||
private long _bytesReadLastSecond;
|
||||
#endregion
|
||||
|
||||
private DownloadManager2()
|
||||
{
|
||||
_activeDownloads = new ConcurrentDictionary<string, DownloadTask2>();
|
||||
_queuedDownloads = new ConcurrentPriorityQueue<DownloadTask2, float>();
|
||||
MaxDownloadBandwidth = 100 * 1024 * 1024;
|
||||
StartBandwidthMonitor();
|
||||
}
|
||||
|
||||
public async Task<bool> QueueAvatarDownload(DownloadInfo info, string playerId)
|
||||
{
|
||||
if (await ValidateAndCheckCache(info))
|
||||
return true;
|
||||
|
||||
DownloadTask2 task2 = GetOrCreateDownloadTask(info, DownloadTaskType.Avatar);
|
||||
task2.AddTarget(playerId);
|
||||
QueueDownload(task2);
|
||||
return false;
|
||||
}
|
||||
|
||||
public async Task<bool> QueuePropDownload(DownloadInfo info, string instanceId, string spawnerId)
|
||||
{
|
||||
if (await ValidateAndCheckCache(info))
|
||||
return true;
|
||||
|
||||
DownloadTask2 task2 = GetOrCreateDownloadTask(info, DownloadTaskType.Prop);
|
||||
task2.AddTarget(instanceId, spawnerId);
|
||||
QueueDownload(task2);
|
||||
return false;
|
||||
}
|
||||
|
||||
public async Task<bool> QueueWorldDownload(DownloadInfo info, bool loadOnComplete)
|
||||
{
|
||||
if (await ValidateAndCheckCache(info))
|
||||
return true;
|
||||
|
||||
DownloadTask2 task2 = GetOrCreateDownloadTask(info, DownloadTaskType.World, loadOnComplete);
|
||||
QueueDownload(task2);
|
||||
return false;
|
||||
}
|
||||
|
||||
private async Task<bool> ValidateAndCheckCache(DownloadInfo info)
|
||||
{
|
||||
// Check if already cached and up to date
|
||||
if (await CacheManager.Instance.IsCachedFileUpToDate(
|
||||
info.AssetId,
|
||||
info.FileId,
|
||||
info.FileHash))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Validate disk space
|
||||
var filePath = CacheManager.Instance.GetCachePath(info.AssetId, info.FileId);
|
||||
if (!CVRTools.HasEnoughDiskSpace(filePath, info.FileSize))
|
||||
{
|
||||
BetterContentLoadingMod.Logger.Error($"Not enough disk space to download {info.AssetId}");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ensure cache directory exists
|
||||
CacheManager.Instance.EnsureCacheDirectoryExists(info.AssetId);
|
||||
return false;
|
||||
}
|
||||
|
||||
private DownloadTask2 GetOrCreateDownloadTask(DownloadInfo info, DownloadTaskType type, bool loadOnComplete = false)
|
||||
{
|
||||
// Check if task already exists in active downloads
|
||||
if (_activeDownloads.TryGetValue(info.DownloadId, out var activeTask))
|
||||
return activeTask;
|
||||
|
||||
// Check if task exists in queued downloads
|
||||
var queuedTask = _queuedDownloads.TryFind(t => t.Info.DownloadId == info.DownloadId);
|
||||
if (queuedTask != null)
|
||||
return queuedTask;
|
||||
|
||||
// Create new task
|
||||
var cachePath = CacheManager.Instance.GetCachePath(info.AssetId, info.FileId);
|
||||
return new DownloadTask2(info, cachePath, type, loadOnComplete);
|
||||
}
|
||||
|
||||
public bool TryFindTask(string downloadId, out DownloadTask2 task2)
|
||||
{
|
||||
return _activeDownloads.TryGetValue(downloadId, out task2) ||
|
||||
_completedDownloads.TryGetValue(downloadId, out task2) ||
|
||||
TryFindQueuedTask(downloadId, out task2);
|
||||
}
|
||||
|
||||
private bool TryFindQueuedTask(string downloadId, out DownloadTask2 task2)
|
||||
{
|
||||
task2 = _queuedDownloads.UnorderedItems
|
||||
.FirstOrDefault(x => x.Element.Info.DownloadId == downloadId).Element;
|
||||
return task2 != null;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
using ABI_RC.Core.Networking.IO.Social;
|
||||
using ABI_RC.Core.Player;
|
||||
using ABI_RC.Core.Savior;
|
||||
using ABI_RC.Core.Util;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NAK.BetterContentLoading;
|
||||
|
||||
public partial class DownloadManager2
|
||||
{
|
||||
internal static bool TryGetPlayerEntity(string playerId, out CVRPlayerEntity playerEntity)
|
||||
{
|
||||
CVRPlayerEntity player = CVRPlayerManager.Instance.NetworkPlayers.Find(p => p.Uuid == playerId);
|
||||
if (player == null)
|
||||
{
|
||||
// BetterContentLoadingMod.Logger.Error($"Player entity not found for ID: {playerId}");
|
||||
playerEntity = null;
|
||||
return false;
|
||||
}
|
||||
playerEntity = player;
|
||||
return true;
|
||||
}
|
||||
|
||||
internal static bool TryGetPropData(string instanceId, out CVRSyncHelper.PropData propData)
|
||||
{
|
||||
CVRSyncHelper.PropData prop = CVRSyncHelper.Props.Find(p => p.InstanceId == instanceId);
|
||||
if (prop == null)
|
||||
{
|
||||
// BetterContentLoadingMod.Logger.Error($"Prop data not found for ID: {instanceId}");
|
||||
propData = null;
|
||||
return false;
|
||||
}
|
||||
propData = prop;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool IsPlayerLocal(string playerId)
|
||||
{
|
||||
return playerId == MetaPort.Instance.ownerId;
|
||||
}
|
||||
|
||||
private static bool IsPlayerFriend(string playerId)
|
||||
{
|
||||
return Friends.FriendsWith(playerId);
|
||||
}
|
||||
|
||||
private bool IsPlayerWithinPriorityDistance(CVRPlayerEntity player)
|
||||
{
|
||||
if (player.PuppetMaster == null) return false;
|
||||
return player.PuppetMaster.animatorManager.DistanceTo < PriorityDownloadDistance;
|
||||
}
|
||||
|
||||
internal bool IsPropWithinPriorityDistance(CVRSyncHelper.PropData prop)
|
||||
{
|
||||
Vector3 propPosition = new(prop.PositionX, prop.PositionY, prop.PositionZ);
|
||||
return Vector3.Distance(propPosition, PlayerSetup.Instance.GetPlayerPosition()) < PriorityDownloadDistance;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
using ABI_RC.Core.IO;
|
||||
|
||||
namespace NAK.BetterContentLoading;
|
||||
|
||||
public partial class DownloadManager2
|
||||
{
|
||||
private float CalculatePriority(DownloadTask2 task2)
|
||||
{
|
||||
return task2.Type switch
|
||||
{
|
||||
DownloadTaskType.Avatar => CalculateAvatarPriority(task2),
|
||||
DownloadTaskType.Prop => CalculatePropPriority(task2),
|
||||
DownloadTaskType.World => CalculateWorldPriority(task2),
|
||||
_ => task2.Info.FileSize
|
||||
};
|
||||
}
|
||||
|
||||
private float CalculateAvatarPriority(DownloadTask2 task2)
|
||||
{
|
||||
float priority = task2.Info.FileSize;
|
||||
|
||||
if (IsPlayerLocal(task2.PlayerId))
|
||||
return 0f;
|
||||
|
||||
if (!TryGetPlayerEntity(task2.PlayerId, out var player))
|
||||
return priority;
|
||||
|
||||
if (PrioritizeFriends && IsPlayerFriend(task2.PlayerId))
|
||||
priority *= 0.5f;
|
||||
|
||||
if (PrioritizeDistance && IsPlayerWithinPriorityDistance(player))
|
||||
priority *= 0.75f;
|
||||
|
||||
// Factor in download progress
|
||||
priority *= (1 + task2.Progress / 100f);
|
||||
|
||||
return priority;
|
||||
}
|
||||
|
||||
private float CalculatePropPriority(DownloadTask2 task2)
|
||||
{
|
||||
float priority = task2.Info.FileSize;
|
||||
|
||||
if (IsPlayerLocal(task2.PlayerId))
|
||||
return 0f;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,144 @@
|
|||
using System.Diagnostics;
|
||||
using System.Net.Http.Headers;
|
||||
using ABI_RC.Core.IO;
|
||||
|
||||
namespace NAK.BetterContentLoading;
|
||||
|
||||
public partial class DownloadManager2
|
||||
{
|
||||
private async Task ProcessDownload(DownloadTask2 task2)
|
||||
{
|
||||
using var client = new HttpClient();
|
||||
|
||||
// Set up resume headers if we have a resume token
|
||||
if (!string.IsNullOrEmpty(task2.ResumeToken))
|
||||
{
|
||||
client.DefaultRequestHeaders.Range = new RangeHeaderValue(task2.BytesRead, null);
|
||||
}
|
||||
|
||||
using var response = await client.GetAsync(task2.Info.AssetUrl, HttpCompletionOption.ResponseHeadersRead);
|
||||
using var dataStream = await response.Content.ReadAsStreamAsync();
|
||||
|
||||
// Open file in append mode if resuming, otherwise create new
|
||||
using var fileStream = new FileStream(
|
||||
task2.TargetPath,
|
||||
string.IsNullOrEmpty(task2.ResumeToken) ? FileMode.Create : FileMode.Append,
|
||||
FileAccess.Write);
|
||||
|
||||
bool isEligibleForThrottle = task2.Info.FileSize > THROTTLE_THRESHOLD;
|
||||
var stopwatch = new Stopwatch();
|
||||
|
||||
int bytesRead;
|
||||
do
|
||||
{
|
||||
if (task2.Status == DownloadTaskStatus.Paused)
|
||||
{
|
||||
task2.ResumeToken = GenerateResumeToken(task2);
|
||||
await fileStream.FlushAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
if (task2.Status != DownloadTaskStatus.Downloading)
|
||||
{
|
||||
HandleCancellation(task2, dataStream, fileStream);
|
||||
return;
|
||||
}
|
||||
|
||||
stopwatch.Restart();
|
||||
var lengthToRead = isEligibleForThrottle ?
|
||||
ComputeUsableBandwidthPerDownload() :
|
||||
16384;
|
||||
|
||||
var buffer = new byte[lengthToRead];
|
||||
bytesRead = await dataStream.ReadAsync(buffer, 0, lengthToRead);
|
||||
|
||||
if (isEligibleForThrottle)
|
||||
Interlocked.Add(ref _bytesReadLastSecond, bytesRead);
|
||||
|
||||
await fileStream.WriteAsync(buffer, 0, bytesRead);
|
||||
UpdateProgress(task2, bytesRead);
|
||||
|
||||
} while (bytesRead > 0);
|
||||
|
||||
CompleteDownload(task2);
|
||||
}
|
||||
|
||||
private string GenerateResumeToken(DownloadTask2 task2)
|
||||
{
|
||||
// Generate a unique token that includes file position and hash
|
||||
return $"{task2.BytesRead}:{DateTime.UtcNow.Ticks}";
|
||||
}
|
||||
|
||||
private async Task FinalizeDownload(DownloadTask2 task2)
|
||||
{
|
||||
var tempPath = task2.CachePath + ".tmp";
|
||||
|
||||
try
|
||||
{
|
||||
// Decrypt the file if needed
|
||||
if (task2.Info.EncryptionAlgorithm != 0)
|
||||
{
|
||||
await DecryptFile(tempPath, task2.CachePath, task2.Info);
|
||||
File.Delete(tempPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
File.Move(tempPath, task2.CachePath);
|
||||
}
|
||||
|
||||
CompleteDownload(task2);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// _logger.Error($"Failed to finalize download for {task.Info.AssetId}: {ex.Message}");
|
||||
task2.Status = DownloadTaskStatus.Failed;
|
||||
File.Delete(tempPath);
|
||||
_activeDownloads.TryRemove(task2.Info.DownloadId, out _);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DecryptFile(string sourcePath, string targetPath, DownloadInfo info)
|
||||
{
|
||||
// Implementation of file decryption based on EncryptionAlgorithm
|
||||
// This would use the FileKey from the DownloadInfo
|
||||
throw new NotImplementedException("File decryption not implemented");
|
||||
}
|
||||
|
||||
private void HandleCancellation(DownloadTask2 task2, Stream dataStream, Stream fileStream)
|
||||
{
|
||||
if (task2.Status != DownloadTaskStatus.Failed)
|
||||
task2.Status = DownloadTaskStatus.Cancelled;
|
||||
|
||||
dataStream.Close();
|
||||
fileStream.Close();
|
||||
|
||||
task2.Progress = 0;
|
||||
task2.BytesRead = 0;
|
||||
|
||||
_activeDownloads.TryRemove(task2.Info.DownloadId, out _);
|
||||
}
|
||||
|
||||
private void UpdateProgress(DownloadTask2 task2, int bytesRead)
|
||||
{
|
||||
task2.BytesRead += bytesRead;
|
||||
task2.Progress = Math.Clamp(
|
||||
(int)(((float)task2.BytesRead / task2.Info.FileSize) * 100f),
|
||||
0, 100);
|
||||
}
|
||||
|
||||
private void CompleteDownload(DownloadTask2 task2)
|
||||
{
|
||||
task2.Status = DownloadTaskStatus.Complete;
|
||||
task2.Progress = 100;
|
||||
_activeDownloads.TryRemove(task2.Info.DownloadId, out _);
|
||||
_completedDownloads.TryAdd(task2.Info.DownloadId, task2);
|
||||
|
||||
lock (_downloadLock)
|
||||
{
|
||||
if (_queuedDownloads.TryDequeue(out var nextTask, out _))
|
||||
{
|
||||
StartDownload(nextTask);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
using ABI_RC.Core.IO;
|
||||
|
||||
namespace NAK.BetterContentLoading;
|
||||
|
||||
public partial class DownloadManager2
|
||||
{
|
||||
public void QueueDownload(DownloadTask2 newTask2)
|
||||
{
|
||||
if (_completedDownloads.TryGetValue(newTask2.Info.DownloadId, out var completedTask))
|
||||
{
|
||||
completedTask.AddPlayer(newTask2.PlayerIds.FirstOrDefault());
|
||||
return;
|
||||
}
|
||||
|
||||
if (TryFindTask(newTask2.Info.DownloadId, out var existingTask))
|
||||
{
|
||||
existingTask.AddPlayer(newTask2.PlayerIds.FirstOrDefault());
|
||||
RecalculatePriority(existingTask);
|
||||
return;
|
||||
}
|
||||
|
||||
float priority = CalculatePriority(newTask2);
|
||||
newTask2.CurrentPriority = priority;
|
||||
|
||||
lock (_downloadLock)
|
||||
{
|
||||
if (_activeDownloads.Count < MaxConcurrentDownloads)
|
||||
{
|
||||
StartDownload(newTask2);
|
||||
return;
|
||||
}
|
||||
|
||||
var lowestPriorityTask = _activeDownloads.Values
|
||||
.OrderByDescending(t => t.CurrentPriority * (1 + t.Progress / 100f))
|
||||
.LastOrDefault();
|
||||
|
||||
if (lowestPriorityTask != null && priority < lowestPriorityTask.CurrentPriority)
|
||||
{
|
||||
PauseDownload(lowestPriorityTask);
|
||||
StartDownload(newTask2);
|
||||
}
|
||||
else
|
||||
{
|
||||
_queuedDownloads.Enqueue(newTask2, priority);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void StartDownload(DownloadTask2 task2)
|
||||
{
|
||||
task2.Status = DownloadTaskStatus.Downloading;
|
||||
_activeDownloads.TryAdd(task2.Info.DownloadId, task2);
|
||||
Task.Run(() => ProcessDownload(task2));
|
||||
}
|
||||
|
||||
private void PauseDownload(DownloadTask2 task2)
|
||||
{
|
||||
task2.Status = DownloadTaskStatus.Queued;
|
||||
_activeDownloads.TryRemove(task2.Info.DownloadId, out _);
|
||||
_queuedDownloads.Enqueue(task2, task2.CurrentPriority);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
private void RecalculatePriority(DownloadTask2 task2)
|
||||
{
|
||||
var newPriority = CalculatePriority(task2);
|
||||
if (Math.Abs(newPriority - task2.CurrentPriority) < float.Epsilon)
|
||||
return;
|
||||
|
||||
task2.CurrentPriority = newPriority;
|
||||
|
||||
if (task2.Status == DownloadTaskStatus.Queued || task2.Status == DownloadTaskStatus.Paused)
|
||||
{
|
||||
// Re-enqueue with new priority
|
||||
_queuedDownloads.UpdatePriority(task2, newPriority);
|
||||
}
|
||||
}
|
||||
}
|
62
BetterContentLoading/DownloadManager2/DownloadTask2.cs
Normal file
62
BetterContentLoading/DownloadManager2/DownloadTask2.cs
Normal file
|
@ -0,0 +1,62 @@
|
|||
namespace NAK.BetterContentLoading;
|
||||
|
||||
public class DownloadTask2
|
||||
{
|
||||
public DownloadInfo Info { get; }
|
||||
public DownloadTaskStatus Status { get; set; }
|
||||
public DownloadTaskType Type { get; }
|
||||
|
||||
public float BasePriority { get; set; }
|
||||
public float CurrentPriority => BasePriority * (1 + Progress / 100f);
|
||||
|
||||
public long BytesRead { get; set; }
|
||||
public int Progress { get; set; }
|
||||
|
||||
public string CachePath { get; }
|
||||
public Dictionary<string, string> Targets { get; } // Key: targetId (playerId/instanceId), Value: spawnerId
|
||||
public bool LoadOnComplete { get; } // For worlds only
|
||||
|
||||
public DownloadTask2(
|
||||
DownloadInfo info,
|
||||
string cachePath,
|
||||
DownloadTaskType type,
|
||||
bool loadOnComplete = false)
|
||||
{
|
||||
Info = info;
|
||||
CachePath = cachePath;
|
||||
Type = type;
|
||||
LoadOnComplete = loadOnComplete;
|
||||
Targets = new Dictionary<string, string>();
|
||||
Status = DownloadTaskStatus.Queued;
|
||||
}
|
||||
|
||||
public void AddTarget(string targetId, string spawnerId = null)
|
||||
{
|
||||
if (Type == DownloadTaskType.World && Targets.Count > 0)
|
||||
throw new InvalidOperationException("World downloads cannot have multiple targets");
|
||||
|
||||
Targets[targetId] = spawnerId;
|
||||
}
|
||||
|
||||
public void RemoveTarget(string targetId)
|
||||
{
|
||||
Targets.Remove(targetId);
|
||||
}
|
||||
}
|
||||
|
||||
public enum DownloadTaskType
|
||||
{
|
||||
Avatar,
|
||||
Prop,
|
||||
World
|
||||
}
|
||||
|
||||
public enum DownloadTaskStatus
|
||||
{
|
||||
Queued,
|
||||
Downloading,
|
||||
Paused,
|
||||
Complete,
|
||||
Failed,
|
||||
Cancelled
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue