using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.IO.Compression; using System.Linq; using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Numerics; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Security.Cryptography; using System.Text; using System.Text.Json; using System.Text.Json.Nodes; using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; using BoneHub.Api; using BoneHub.Avatars; using BoneHub.Configuration; using BoneHub.Downloads; using BoneHub.Installation; using BoneHub.Interaction; using BoneHub.Models; using BoneHub.Persistence; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETCoreApp,Version=v8.0", FrameworkDisplayName = ".NET 8.0")] [assembly: AssemblyCompany("WristHub.Core")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("4.0.1.0")] [assembly: AssemblyInformationalVersion("4.0.1")] [assembly: AssemblyProduct("WristHub.Core")] [assembly: AssemblyTitle("WristHub.Core")] [assembly: AssemblyVersion("4.0.1.0")] [module: RefSafetyRules(11)] namespace BoneHub { public sealed class BoneHubService : IDisposable { private readonly IModIoClient _client; private readonly DownloadManager _downloads; private readonly BoneHubOptions _options; private readonly SearchCache _cache; private readonly JsonFileStore _stateStore; private readonly SemaphoreSlim _stateGate = new SemaphoreSlim(1, 1); private BoneHubState _state = new BoneHubState(); public int FavoriteCount => _state.FavoriteModIds.Count; public IReadOnlyCollection Installed => (IReadOnlyCollection)(object)_state.InstalledMods.Values.ToArray(); public BoneHubPreferences Preferences => _state.Preferences; public IReadOnlyList RecentlyEquippedAvatarBarcodes => _state.RecentlyEquippedAvatarBarcodes.ToArray(); public string LastEquippedAvatarBarcode { get { if (!string.IsNullOrWhiteSpace(_state.LastEquippedAvatarBarcode)) { return _state.LastEquippedAvatarBarcode; } return _state.RecentlyEquippedAvatarBarcodes.FirstOrDefault() ?? string.Empty; } } public TrashedModRecord? LastTrashed => _state.TrashHistory.LastOrDefault(); public IReadOnlyCollection PendingDeletionModIds => (IReadOnlyCollection)(object)_state.PendingDeletions.Select((PendingDeletionRecord item) => item.Mod.ModId).ToArray(); public bool ModIoReady => _options.CanUseModIo; public string ApiKeyFilePath => _options.ApiKeyFilePath; public event Action? DownloadChanged; public event Action? InstalledChanged; public BoneHubService(IModIoClient client, DownloadManager downloads, BoneHubOptions options) { _client = client; _downloads = downloads; _options = options; _cache = new SearchCache(Path.Combine(options.DataDirectory, "cache", "search")); _stateStore = new JsonFileStore(Path.Combine(options.DataDirectory, "state.json")); _downloads.JobChanged += OnJobChanged; } public async Task InitializeAsync(CancellationToken cancellationToken = default(CancellationToken)) { _state = await _stateStore.LoadAsync(cancellationToken).ConfigureAwait(continueOnCapturedContext: false); if (_state.Preferences.MiniMapRange == MiniMapRange.TwentyFiveMeters) { _state.Preferences.MiniMapRange = MiniMapRange.ThirtyMeters; } } public async Task> SearchAsync(ModSearchRequest request, CancellationToken cancellationToken = default(CancellationToken)) { ModSearchRequest normalized = request with { Query = request.Query.Trim(), Limit = Math.Clamp(request.Limit, 1, 100), Offset = Math.Max(request.Offset, 0) }; string key = $"{normalized.Query}|{normalized.Tag}|{normalized.Creator}|{normalized.Sort}|{normalized.Offset}|{normalized.Limit}|{_options.TargetPlatform}"; ModIoPage modIoPage = await _cache.ReadAsync(key, TimeSpan.FromMinutes(_options.CacheMinutes), cancellationToken).ConfigureAwait(continueOnCapturedContext: false); if (modIoPage != null) { return modIoPage; } try { ModIoPage page = await _client.SearchAsync(normalized, cancellationToken).ConfigureAwait(continueOnCapturedContext: false); await _cache.WriteAsync(key, page, cancellationToken).ConfigureAwait(continueOnCapturedContext: false); return page; } catch when (!cancellationToken.IsCancellationRequested) { ModIoPage modIoPage2 = await _cache.ReadAsync(key, TimeSpan.FromDays(7.0), cancellationToken).ConfigureAwait(continueOnCapturedContext: false); if (modIoPage2 != null) { return modIoPage2; } throw; } } public DownloadJob Install(ModIoMod mod, CancellationToken cancellationToken = default(CancellationToken)) { return _downloads.Enqueue(mod, cancellationToken); } public async Task GetInstallPlanAsync(ModIoMod root, CancellationToken cancellationToken = default(CancellationToken)) { if (!ModIoReady) { throw new InvalidOperationException("WristHub's online avatar service is unavailable."); } List ordered = new List(); HashSet installed = new HashSet(); HashSet visited = new HashSet(); HashSet visiting = new HashSet(); await VisitAsync(root, isRoot: true).ConfigureAwait(continueOnCapturedContext: false); return new ModInstallPlan(root, ordered, installed.OrderBy((long id) => id).ToArray()); async Task VisitAsync(ModIoMod mod, bool isRoot) { cancellationToken.ThrowIfCancellationRequested(); if (!visited.Contains(mod.Id)) { if (!visiting.Add(mod.Id)) { throw new InvalidDataException($"mod.io reported a dependency cycle involving mod {mod.Id}."); } if (visited.Count + visiting.Count > 32) { throw new InvalidDataException("This dependency graph exceeds WristHub's 32-mod safety limit."); } foreach (ModIoDependency item in (await _client.GetDependenciesAsync(mod.Id, cancellationToken).ConfigureAwait(continueOnCapturedContext: false)).Data.Where((ModIoDependency item) => item.ModId > 0).DistinctBy((ModIoDependency item) => item.ModId)) { if (_state.InstalledMods.ContainsKey(item.ModId)) { installed.Add(item.ModId); } else { await VisitAsync(await _client.GetModAsync(item.ModId, cancellationToken).ConfigureAwait(continueOnCapturedContext: false), isRoot: false).ConfigureAwait(continueOnCapturedContext: false); } } visiting.Remove(mod.Id); visited.Add(mod.Id); if (isRoot || !_state.InstalledMods.ContainsKey(mod.Id)) { ordered.Add(mod); } else { installed.Add(mod.Id); } } } } public async Task> InstallWithDependenciesAsync(ModIoMod root, CancellationToken cancellationToken = default(CancellationToken)) { return (await GetInstallPlanAsync(root, cancellationToken).ConfigureAwait(continueOnCapturedContext: false)).OrderedMods.Select((ModIoMod mod) => Install(mod, cancellationToken)).ToArray(); } public bool CancelDownload(Guid jobId) { return _downloads.Cancel(jobId); } public bool IsFavorite(long modId) { return _state.FavoriteModIds.Contains(modId); } public bool IsInstalled(long modId) { return _state.InstalledMods.ContainsKey(modId); } public async Task ConfigureApiKeyAsync(string value, CancellationToken cancellationToken = default(CancellationToken)) { string normalized = value?.Trim() ?? string.Empty; if (!BoneHubOptions.IsValidApiKey(normalized)) { throw new InvalidDataException("The mod.io API key must contain exactly 32 letters or numbers."); } if (string.IsNullOrWhiteSpace(_options.ApiKeyFilePath)) { throw new InvalidOperationException("WristHub's API-key file path is unavailable."); } string path = Path.GetFullPath(_options.ApiKeyFilePath); Directory.CreateDirectory(Path.GetDirectoryName(path) ?? throw new InvalidOperationException("The API-key file has no parent directory.")); string temporary = path + ".tmp"; await File.WriteAllTextAsync(temporary, normalized, cancellationToken).ConfigureAwait(continueOnCapturedContext: false); File.Move(temporary, path, overwrite: true); _options.ApiKey = normalized; } public async Task SetFavoriteAsync(long modId, bool favorite, CancellationToken cancellationToken = default(CancellationToken)) { await _stateGate.WaitAsync(cancellationToken).ConfigureAwait(continueOnCapturedContext: false); try { if (favorite) { _state.FavoriteModIds.Add(modId); } else { _state.FavoriteModIds.Remove(modId); } await _stateStore.SaveAsync(_state, cancellationToken).ConfigureAwait(continueOnCapturedContext: false); } finally { _stateGate.Release(); } } public async Task> GetFavoriteModsAsync(CancellationToken cancellationToken = default(CancellationToken)) { await _stateGate.WaitAsync(cancellationToken).ConfigureAwait(continueOnCapturedContext: false); long[] array; try { array = _state.FavoriteModIds.Take(50).ToArray(); } finally { _stateGate.Release(); } List results = new List(); long[] array2 = array; foreach (long modId in array2) { cancellationToken.ThrowIfCancellationRequested(); try { List list = results; list.Add(await _client.GetModAsync(modId, cancellationToken).ConfigureAwait(continueOnCapturedContext: false)); } catch when (!cancellationToken.IsCancellationRequested) { } } return results.OrderBy((ModIoMod mod) => mod.Name).ToArray(); } public Task GetModDetailsAsync(long modId, CancellationToken cancellationToken = default(CancellationToken)) { return _client.GetModAsync(modId, cancellationToken); } public async Task MarkViewedAsync(long modId, CancellationToken cancellationToken = default(CancellationToken)) { await _stateGate.WaitAsync(cancellationToken).ConfigureAwait(continueOnCapturedContext: false); try { _state.RecentlyViewedModIds.Remove(modId); _state.RecentlyViewedModIds.Insert(0, modId); if (_state.RecentlyViewedModIds.Count > 50) { _state.RecentlyViewedModIds.RemoveRange(50, _state.RecentlyViewedModIds.Count - 50); } await _stateStore.SaveAsync(_state, cancellationToken).ConfigureAwait(continueOnCapturedContext: false); } finally { _stateGate.Release(); } } public async Task MarkAvatarEquippedAsync(string barcode, CancellationToken cancellationToken = default(CancellationToken)) { string normalized = barcode?.Trim() ?? string.Empty; if (normalized.Length == 0) { return; } await _stateGate.WaitAsync(cancellationToken).ConfigureAwait(continueOnCapturedContext: false); try { _state.RecentlyEquippedAvatarBarcodes.RemoveAll((string item) => string.Equals(item, normalized, StringComparison.OrdinalIgnoreCase)); _state.RecentlyEquippedAvatarBarcodes.Insert(0, normalized); _state.LastEquippedAvatarBarcode = normalized; if (_state.RecentlyEquippedAvatarBarcodes.Count > 50) { _state.RecentlyEquippedAvatarBarcodes.RemoveRange(50, _state.RecentlyEquippedAvatarBarcodes.Count - 50); } await _stateStore.SaveAsync(_state, cancellationToken).ConfigureAwait(continueOnCapturedContext: false); } finally { _stateGate.Release(); } } public async Task ForgetAvatarBarcodesAsync(IEnumerable barcodes, CancellationToken cancellationToken = default(CancellationToken)) { HashSet removed = (from item in barcodes where !string.IsNullOrWhiteSpace(item) select item.Trim()).ToHashSet(StringComparer.OrdinalIgnoreCase); if (removed.Count == 0) { return; } await _stateGate.WaitAsync(cancellationToken).ConfigureAwait(continueOnCapturedContext: false); try { _state.RecentlyEquippedAvatarBarcodes.RemoveAll(removed.Contains); if (removed.Contains(_state.LastEquippedAvatarBarcode)) { _state.LastEquippedAvatarBarcode = _state.RecentlyEquippedAvatarBarcodes.FirstOrDefault() ?? string.Empty; } await _stateStore.SaveAsync(_state, cancellationToken).ConfigureAwait(continueOnCapturedContext: false); } finally { _stateGate.Release(); } } public async Task SavePreferencesAsync(CancellationToken cancellationToken = default(CancellationToken)) { await _stateGate.WaitAsync(cancellationToken).ConfigureAwait(continueOnCapturedContext: false); try { if (!Enum.IsDefined(_state.Preferences.ComfortPreset)) { _state.Preferences.ComfortPreset = BoneHubComfortPreset.Default; } if (!Enum.IsDefined(_state.Preferences.AvatarBrowserStartupMode)) { _state.Preferences.AvatarBrowserStartupMode = BrowserStartupMode.AllInstalled; } if (!Enum.IsDefined(_state.Preferences.LastAvatarBrowseScope)) { _state.Preferences.LastAvatarBrowseScope = AvatarBrowseScope.AllInstalled; } if (!Enum.IsDefined(_state.Preferences.MiniMapRange)) { _state.Preferences.MiniMapRange = MiniMapRange.ThirtyMeters; } if (!Enum.IsDefined(_state.Preferences.MiniMapOrientation)) { _state.Preferences.MiniMapOrientation = MiniMapOrientation.HeadingUp; } if (!Enum.IsDefined(_state.Preferences.HologramTheme)) { _state.Preferences.HologramTheme = WristHubTheme.Cyan; } _state.Preferences.WatchScale = Math.Clamp(_state.Preferences.WatchScale, 0.65f, 1.6f); _state.Preferences.DwellSeconds = 1f; _state.Preferences.OutwardOffset = Math.Clamp(_state.Preferences.OutwardOffset, 0.015f, 0.06f); _state.Preferences.FingerOffset = Math.Clamp(_state.Preferences.FingerOffset, -0.055f, 0.025f); _state.Preferences.WatchForearmOffset = Math.Clamp(_state.Preferences.WatchForearmOffset, -0.08f, 0.03f); _state.Preferences.CarouselDistance = Math.Clamp(_state.Preferences.CarouselDistance, 0.45f, 0.95f); _state.Preferences.HologramScale = Math.Clamp(_state.Preferences.HologramScale, 0.7f, 1.4f); _state.Preferences.EquipZoneScale = Math.Clamp(_state.Preferences.EquipZoneScale, 0.7f, 1.6f); _state.Preferences.HologramBrightness = Math.Clamp(_state.Preferences.HologramBrightness, 0.35f, 1.5f); foreach (CompassCalibrationRecord compassCalibration in _state.Preferences.CompassCalibrations) { compassCalibration.NorthYawDegrees = CompassHeadingMath.Normalize(compassCalibration.NorthYawDegrees); } while (_state.Preferences.CompassCalibrations.Count > 64) { CompassCalibrationRecord item = _state.Preferences.CompassCalibrations.OrderBy((CompassCalibrationRecord record) => record.LastUsedUtc).First(); _state.Preferences.CompassCalibrations.Remove(item); } TrimDistinct(_state.Preferences.FavoriteSpawnableBarcodes, 128); TrimDistinct(_state.Preferences.RecentSpawnableBarcodes, 64); await _stateStore.SaveAsync(_state, cancellationToken).ConfigureAwait(continueOnCapturedContext: false); } finally { _stateGate.Release(); } } private static void TrimDistinct(List values, int maximum) { HashSet hashSet = new HashSet(StringComparer.OrdinalIgnoreCase); for (int num = values.Count - 1; num >= 0; num--) { string text = values[num]?.Trim() ?? string.Empty; if (text.Length == 0 || !hashSet.Add(text)) { values.RemoveAt(num); } else { values[num] = text; } } if (values.Count > maximum) { values.RemoveRange(maximum, values.Count - maximum); } } public InstalledModHealth VerifyInstalled(long modId) { if (!_state.InstalledMods.TryGetValue(modId, out BoneHub.Persistence.InstalledModRecord value)) { return new InstalledModHealth(Healthy: false, new string[1] { "WristHub has no installation record for this mod." }); } List list = new List(); foreach (string installedDirectory in value.InstalledDirectories) { string text; try { text = ValidateInstalledPath(installedDirectory); } catch (Exception ex) { list.Add(ex.Message); continue; } if (!Directory.Exists(text)) { list.Add("Missing folder: " + Path.GetFileName(text)); } else if (!PalletManifestLocator.HasManifest(text)) { list.Add("Missing pallet manifest: " + Path.GetFileName(text)); } } return new InstalledModHealth(list.Count == 0, list); } public async Task CheckForUpdateAsync(long modId, CancellationToken cancellationToken = default(CancellationToken)) { if (!_state.InstalledMods.TryGetValue(modId, out BoneHub.Persistence.InstalledModRecord installed)) { throw new InvalidOperationException("This mod is not installed."); } ModIoMod mod = await _client.GetModAsync(modId, cancellationToken).ConfigureAwait(continueOnCapturedContext: false); ModIoFile modIoFile = await ModIoFileResolver.ResolveAsync(_client, mod, _options, cancellationToken).ConfigureAwait(continueOnCapturedContext: false); return new ModUpdateInfo(mod, modIoFile.Id != installed.FileId, installed.FileId, modIoFile.Id); } public async Task> CheckAllUpdatesAsync(CancellationToken cancellationToken = default(CancellationToken)) { List results = new List(); foreach (BoneHub.Persistence.InstalledModRecord item in _state.InstalledMods.Values.OrderBy((BoneHub.Persistence.InstalledModRecord item) => item.Name)) { cancellationToken.ThrowIfCancellationRequested(); List list = results; list.Add(await CheckForUpdateAsync(item.ModId, cancellationToken).ConfigureAwait(continueOnCapturedContext: false)); } return results; } public async Task RepairOrUpdateAsync(long modId, CancellationToken cancellationToken = default(CancellationToken)) { return Install((await CheckForUpdateAsync(modId, cancellationToken).ConfigureAwait(continueOnCapturedContext: false)).Mod, cancellationToken); } public async Task RepairSharingRegistrationsAsync(CancellationToken cancellationToken = default(CancellationToken)) { BoneHub.Persistence.InstalledModRecord[] array = _state.InstalledMods.Values.Where((BoneHub.Persistence.InstalledModRecord installedModRecord) => installedModRecord.ModId > 0 && installedModRecord.InstalledDirectories.Any(Directory.Exists) && (installedModRecord.Sharing == null || installedModRecord.Sharing.RegistrationVersion < 3 || installedModRecord.Sharing.SidecarManifestPaths.Count == 0 || installedModRecord.Sharing.SidecarManifestPaths.Any((string path) => !File.Exists(path)))).ToArray(); int repaired = 0; BoneHub.Persistence.InstalledModRecord[] array2 = array; foreach (BoneHub.Persistence.InstalledModRecord record in array2) { cancellationToken.ThrowIfCancellationRequested(); try { ModIoMod mod = await _client.GetModAsync(record.ModId, cancellationToken).ConfigureAwait(continueOnCapturedContext: false); ModIoFile modIoFile = ((record.FileId <= 0) ? (await ModIoFileResolver.ResolveAsync(_client, mod, _options, cancellationToken).ConfigureAwait(continueOnCapturedContext: false)) : (await _client.GetFileAsync(record.ModId, record.FileId, cancellationToken).ConfigureAwait(continueOnCapturedContext: false))); ModIoFile file = modIoFile; ModIoSharingMetadata sharing = await NetworkerRegistrationWriter.WriteAsync(mod, file, await ModIoFileResolver.ResolvePlatformFilesAsync(_client, record.ModId, cancellationToken).ConfigureAwait(continueOnCapturedContext: false), record.InstalledDirectories, _options.ModsDirectory, cancellationToken).ConfigureAwait(continueOnCapturedContext: false); await _stateGate.WaitAsync(cancellationToken).ConfigureAwait(continueOnCapturedContext: false); try { if (_state.InstalledMods.TryGetValue(record.ModId, out BoneHub.Persistence.InstalledModRecord value)) { value.Sharing = sharing; } await _stateStore.SaveAsync(_state, cancellationToken).ConfigureAwait(continueOnCapturedContext: false); } finally { _stateGate.Release(); } repaired++; } catch when (!cancellationToken.IsCancellationRequested) { } } if (repaired > 0) { this.InstalledChanged?.Invoke(); } return repaired; } public Task UninstallAsync(long modId, CancellationToken cancellationToken = default(CancellationToken)) { return UninstallCoreAsync(modId, null, null, wasSubscribed: false, cancellationToken); } public Task UninstallAsync(long modId, string? localName, IReadOnlyList? localDirectories, CancellationToken cancellationToken = default(CancellationToken)) { return UninstallCoreAsync(modId, localName, localDirectories, wasSubscribed: false, cancellationToken); } public Task UninstallSubscribedAsync(long modId, string? localName, IReadOnlyList? localDirectories, bool wasSubscribed, CancellationToken cancellationToken = default(CancellationToken)) { return UninstallCoreAsync(modId, localName, localDirectories, wasSubscribed, cancellationToken); } public async Task QueueDeletionAsync(long modId, string? localName, IReadOnlyList? localDirectories, bool wasSubscribed, IEnumerable? avatarBarcodes, CancellationToken cancellationToken = default(CancellationToken)) { await _stateGate.WaitAsync(cancellationToken).ConfigureAwait(continueOnCapturedContext: false); PendingDeletionRecord queued; try { PendingDeletionRecord pendingDeletionRecord = _state.PendingDeletions.FirstOrDefault((PendingDeletionRecord item) => item.Mod.ModId == modId); if (pendingDeletionRecord != null) { return pendingDeletionRecord; } _state.InstalledMods.TryGetValue(modId, out BoneHub.Persistence.InstalledModRecord value); IEnumerable enumerable = localDirectories; object obj = enumerable; if (obj == null) { enumerable = value?.InstalledDirectories; obj = enumerable ?? Array.Empty(); } List list = ((IEnumerable)obj).Where((string path) => !string.IsNullOrWhiteSpace(path)).Select(ValidateInstalledPath).Where(Directory.Exists) .Distinct(StringComparer.OrdinalIgnoreCase) .ToList(); if (list.Count == 0) { throw new DirectoryNotFoundException("No installed avatar-pack folders were found to queue."); } if (value == null) { value = new BoneHub.Persistence.InstalledModRecord { ModId = modId, Name = (string.IsNullOrWhiteSpace(localName) ? "Local avatar pack" : localName.Trim()), InstalledDirectories = list, InstalledAt = DateTimeOffset.UtcNow }; } List sidecarFiles = (from path in (value.Sharing?.SidecarManifestPaths ?? new List()).Concat(FindRegistrationSidecars(list)) where !string.IsNullOrWhiteSpace(path) select path).Select(ValidateInstalledFile).Where(File.Exists).Distinct(StringComparer.OrdinalIgnoreCase) .ToList(); List barcodes = (from text in avatarBarcodes ?? Array.Empty() where !string.IsNullOrWhiteSpace(text) select text.Trim()).Distinct(StringComparer.OrdinalIgnoreCase).ToList(); queued = new PendingDeletionRecord { Mod = value, Directories = list, SidecarFiles = sidecarFiles, AvatarBarcodes = barcodes, WasSubscribed = wasSubscribed }; _state.PendingDeletions.Add(queued); _state.InstalledMods.Remove(modId); _state.RecentlyEquippedAvatarBarcodes.RemoveAll((string value2) => barcodes.Contains(value2, StringComparer.OrdinalIgnoreCase)); if (barcodes.Contains(_state.LastEquippedAvatarBarcode, StringComparer.OrdinalIgnoreCase)) { _state.LastEquippedAvatarBarcode = _state.RecentlyEquippedAvatarBarcodes.FirstOrDefault() ?? string.Empty; } await _stateStore.SaveAsync(_state, cancellationToken).ConfigureAwait(continueOnCapturedContext: false); } finally { _stateGate.Release(); } this.InstalledChanged?.Invoke(); return queued; } public async Task ProcessPendingDeletionsAsync(CancellationToken cancellationToken = default(CancellationToken)) { int completed = 0; List problems = new List(); await _stateGate.WaitAsync(cancellationToken).ConfigureAwait(continueOnCapturedContext: false); try { PendingDeletionRecord[] array = _state.PendingDeletions.ToArray(); foreach (PendingDeletionRecord pending in array) { cancellationToken.ThrowIfCancellationRequested(); string text = Path.Combine(Path.GetDirectoryName(Path.GetFullPath(_options.ModsDirectory).TrimEnd(Path.DirectorySeparatorChar)) ?? throw new InvalidOperationException("Mods directory has no parent."), ".wristhub-trash", $"{pending.Mod.ModId}-{DateTimeOffset.UtcNow:yyyyMMddHHmmss}-{Guid.NewGuid():N}"); Directory.CreateDirectory(text); List list = new List(); List list2 = new List(); try { for (int j = 0; j < pending.Directories.Count; j++) { string text2 = ValidateInstalledPath(pending.Directories[j]); if (Directory.Exists(text2)) { string text3 = Path.Combine(text, $"{j:D2}-{Path.GetFileName(text2)}"); Directory.Move(text2, text3); list.Add(new TrashedDirectoryRecord { OriginalPath = text2, TrashPath = text3 }); } } for (int k = 0; k < pending.SidecarFiles.Count; k++) { string text4 = ValidateInstalledFile(pending.SidecarFiles[k]); if (File.Exists(text4)) { string text5 = Path.Combine(text, $"manifest-{k:D2}-{Path.GetFileName(text4)}"); File.Move(text4, text5); list2.Add(new TrashedFileRecord { OriginalPath = text4, TrashPath = text5 }); } } if (list.Count > 0 || list2.Count > 0) { _state.TrashHistory.Add(new TrashedModRecord { Mod = pending.Mod, Directories = list, Files = list2, WasSubscribed = pending.WasSubscribed }); if (_state.TrashHistory.Count > 20) { _state.TrashHistory.RemoveRange(0, _state.TrashHistory.Count - 20); } } else if (Directory.Exists(text)) { Directory.Delete(text, recursive: false); } _state.PendingDeletions.Remove(pending); _state.InstalledMods.Remove(pending.Mod.ModId); _state.RecentlyEquippedAvatarBarcodes.RemoveAll((string value) => pending.AvatarBarcodes.Contains(value, StringComparer.OrdinalIgnoreCase)); if (pending.AvatarBarcodes.Contains(_state.LastEquippedAvatarBarcode, StringComparer.OrdinalIgnoreCase)) { _state.LastEquippedAvatarBarcode = _state.RecentlyEquippedAvatarBarcodes.FirstOrDefault() ?? string.Empty; } completed++; } catch (Exception ex) { for (int num = list.Count - 1; num >= 0; num--) { TrashedDirectoryRecord trashedDirectoryRecord = list[num]; if (Directory.Exists(trashedDirectoryRecord.TrashPath) && !Directory.Exists(trashedDirectoryRecord.OriginalPath)) { Directory.Move(trashedDirectoryRecord.TrashPath, trashedDirectoryRecord.OriginalPath); } } for (int num2 = list2.Count - 1; num2 >= 0; num2--) { TrashedFileRecord trashedFileRecord = list2[num2]; if (File.Exists(trashedFileRecord.TrashPath) && !File.Exists(trashedFileRecord.OriginalPath)) { File.Move(trashedFileRecord.TrashPath, trashedFileRecord.OriginalPath); } } try { if (Directory.Exists(text) && !Directory.EnumerateFileSystemEntries(text).Any()) { Directory.Delete(text); } } catch { } problems.Add(pending.Mod.Name + ": " + ex.Message); } } await _stateStore.SaveAsync(_state, cancellationToken).ConfigureAwait(continueOnCapturedContext: false); } finally { _stateGate.Release(); } if (completed > 0) { this.InstalledChanged?.Invoke(); } return new PendingDeletionProcessResult(completed, _state.PendingDeletions.Count, problems); } private async Task UninstallCoreAsync(long modId, string? localName, IReadOnlyList? localDirectories, bool wasSubscribed, CancellationToken cancellationToken) { await _stateGate.WaitAsync(cancellationToken).ConfigureAwait(continueOnCapturedContext: false); try { BoneHub.Persistence.InstalledModRecord installed; bool wasTracked = _state.InstalledMods.TryGetValue(modId, out installed); if (!wasTracked) { List list = (localDirectories ?? Array.Empty()).Where((string path) => !string.IsNullOrWhiteSpace(path)).Distinct(StringComparer.OrdinalIgnoreCase).ToList(); if (list.Count == 0) { throw new InvalidOperationException("WristHub could not identify this avatar pack's local folder."); } installed = new BoneHub.Persistence.InstalledModRecord { ModId = modId, Name = (string.IsNullOrWhiteSpace(localName) ? "Local avatar pack" : localName.Trim()), InstalledDirectories = list, InstalledAt = DateTimeOffset.UtcNow }; } BoneHub.Persistence.InstalledModRecord installedModRecord = installed ?? throw new InvalidOperationException("The avatar pack record is unavailable."); string text = Path.Combine(Path.GetDirectoryName(Path.GetFullPath(_options.ModsDirectory).TrimEnd(Path.DirectorySeparatorChar)) ?? throw new InvalidOperationException("Mods directory has no parent."), ".wristhub-trash", $"{modId}-{DateTimeOffset.UtcNow:yyyyMMddHHmmss}-{Guid.NewGuid():N}"); Directory.CreateDirectory(text); List moved = new List(); List movedFiles = new List(); TrashedModRecord stateRecord = null; try { for (int num = 0; num < installedModRecord.InstalledDirectories.Count; num++) { cancellationToken.ThrowIfCancellationRequested(); string text2 = ValidateInstalledPath(installedModRecord.InstalledDirectories[num]); if (Directory.Exists(text2)) { string text3 = Path.Combine(text, $"{num:D2}-{Path.GetFileName(text2)}"); Directory.Move(text2, text3); moved.Add(new TrashedDirectoryRecord { OriginalPath = text2, TrashPath = text3 }); } } string[] array = (installedModRecord.Sharing?.SidecarManifestPaths ?? new List()).Concat(FindRegistrationSidecars(installedModRecord.InstalledDirectories)).Distinct(StringComparer.OrdinalIgnoreCase).ToArray(); for (int num2 = 0; num2 < array.Length; num2++) { cancellationToken.ThrowIfCancellationRequested(); string text4 = ValidateInstalledFile(array[num2]); if (File.Exists(text4)) { string text5 = Path.Combine(text, $"manifest-{num2:D2}-{Path.GetFileName(text4)}"); File.Move(text4, text5); movedFiles.Add(new TrashedFileRecord { OriginalPath = text4, TrashPath = text5 }); } } if (moved.Count == 0) { throw new DirectoryNotFoundException("No installed avatar-pack folders were found to remove."); } TrashedModRecord record = new TrashedModRecord { Mod = installedModRecord, Directories = moved, Files = movedFiles, WasSubscribed = wasSubscribed }; stateRecord = record; _state.InstalledMods.Remove(modId); _state.TrashHistory.Add(record); if (_state.TrashHistory.Count > 20) { _state.TrashHistory.RemoveRange(0, _state.TrashHistory.Count - 20); } await _stateStore.SaveAsync(_state, cancellationToken).ConfigureAwait(continueOnCapturedContext: false); this.InstalledChanged?.Invoke(); return record; } catch { if (wasTracked) { _state.InstalledMods[modId] = installed; } else { _state.InstalledMods.Remove(modId); } if (stateRecord != null) { _state.TrashHistory.Remove(stateRecord); } for (int num3 = moved.Count - 1; num3 >= 0; num3--) { TrashedDirectoryRecord trashedDirectoryRecord = moved[num3]; if (Directory.Exists(trashedDirectoryRecord.TrashPath) && !Directory.Exists(trashedDirectoryRecord.OriginalPath)) { Directory.Move(trashedDirectoryRecord.TrashPath, trashedDirectoryRecord.OriginalPath); } } for (int num4 = movedFiles.Count - 1; num4 >= 0; num4--) { TrashedFileRecord trashedFileRecord = movedFiles[num4]; if (File.Exists(trashedFileRecord.TrashPath) && !File.Exists(trashedFileRecord.OriginalPath)) { File.Move(trashedFileRecord.TrashPath, trashedFileRecord.OriginalPath); } } throw; } } finally { _stateGate.Release(); } } public async Task UndoLastUninstallAsync(CancellationToken cancellationToken = default(CancellationToken)) { await _stateGate.WaitAsync(cancellationToken).ConfigureAwait(continueOnCapturedContext: false); try { TrashedModRecord record = _state.TrashHistory.LastOrDefault() ?? throw new InvalidOperationException("There is no WristHub uninstall to undo."); if (_state.InstalledMods.ContainsKey(record.Mod.ModId)) { throw new InvalidOperationException("That mod is already installed again."); } List restored = new List(); List restoredFiles = new List(); try { foreach (TrashedDirectoryRecord directory in record.Directories) { cancellationToken.ThrowIfCancellationRequested(); string text = ValidateInstalledPath(directory.OriginalPath); string text2 = ValidateTrashPath(directory.TrashPath); if (Directory.Exists(text)) { throw new IOException("Cannot restore " + Path.GetFileName(text) + " because that folder already exists."); } if (!Directory.Exists(text2)) { throw new DirectoryNotFoundException("Trash data is missing for " + record.Mod.Name + "."); } Directory.Move(text2, text); restored.Add(new TrashedDirectoryRecord { OriginalPath = text, TrashPath = text2 }); } foreach (TrashedFileRecord file in record.Files) { cancellationToken.ThrowIfCancellationRequested(); string text3 = ValidateInstalledFile(file.OriginalPath); string text4 = ValidateTrashPath(file.TrashPath); if (File.Exists(text3)) { throw new IOException("Cannot restore " + Path.GetFileName(text3) + " because it already exists."); } if (!File.Exists(text4)) { throw new FileNotFoundException("Trash sidecar data is missing.", text4); } File.Move(text4, text3); restoredFiles.Add(new TrashedFileRecord { OriginalPath = text3, TrashPath = text4 }); } _state.InstalledMods[record.Mod.ModId] = record.Mod; _state.TrashHistory.Remove(record); await _stateStore.SaveAsync(_state, cancellationToken).ConfigureAwait(continueOnCapturedContext: false); this.InstalledChanged?.Invoke(); return record.Mod; } catch { _state.InstalledMods.Remove(record.Mod.ModId); if (!_state.TrashHistory.Contains(record)) { _state.TrashHistory.Add(record); } for (int num = restored.Count - 1; num >= 0; num--) { TrashedDirectoryRecord trashedDirectoryRecord = restored[num]; if (Directory.Exists(trashedDirectoryRecord.OriginalPath) && !Directory.Exists(trashedDirectoryRecord.TrashPath)) { Directory.Move(trashedDirectoryRecord.OriginalPath, trashedDirectoryRecord.TrashPath); } } for (int num2 = restoredFiles.Count - 1; num2 >= 0; num2--) { TrashedFileRecord trashedFileRecord = restoredFiles[num2]; if (File.Exists(trashedFileRecord.OriginalPath) && !File.Exists(trashedFileRecord.TrashPath)) { File.Move(trashedFileRecord.OriginalPath, trashedFileRecord.TrashPath); } } throw; } } finally { _stateGate.Release(); } } private void OnJobChanged(DownloadJob job) { if (job.State == DownloadState.Completed && job.Result != null) { PersistCompletedJobAsync(job, job.Result); } else { this.DownloadChanged?.Invoke(job); } } private async Task PersistCompletedJobAsync(DownloadJob job, BoneHub.Downloads.InstalledModRecord result) { await _stateGate.WaitAsync().ConfigureAwait(continueOnCapturedContext: false); try { _state.InstalledMods[result.ModId] = new BoneHub.Persistence.InstalledModRecord { ModId = result.ModId, FileId = result.FileId, Name = result.Name, Version = result.Version, Md5 = result.Md5, InstalledDirectories = result.InstalledDirectories.ToList(), Sharing = result.Sharing, InstalledAt = DateTimeOffset.UtcNow }; await _stateStore.SaveAsync(_state).ConfigureAwait(continueOnCapturedContext: false); this.DownloadChanged?.Invoke(job); this.InstalledChanged?.Invoke(); } finally { _stateGate.Release(); } } private string ValidateInstalledPath(string path) { string text = Path.GetFullPath(_options.ModsDirectory).TrimEnd(Path.DirectorySeparatorChar); string text2 = Path.GetFullPath(path).TrimEnd(Path.DirectorySeparatorChar); if (!text2.StartsWith(text + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase) || string.Equals(text2, text, StringComparison.OrdinalIgnoreCase)) { throw new InvalidDataException("An installed-mod path falls outside BONELAB's Mods directory."); } return text2; } private string ValidateInstalledFile(string path) { string text = Path.GetFullPath(_options.ModsDirectory).TrimEnd(Path.DirectorySeparatorChar); string fullPath = Path.GetFullPath(path); if (!fullPath.StartsWith(text + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase) || !string.Equals(Path.GetExtension(fullPath), ".manifest", StringComparison.OrdinalIgnoreCase)) { throw new InvalidDataException("A mod registration sidecar falls outside BONELAB's Mods directory."); } return fullPath; } private IReadOnlyList FindRegistrationSidecars(IEnumerable packageDirectories) { string[] source = packageDirectories.Select(Path.GetFullPath).ToArray(); List list = new List(); if (!Directory.Exists(_options.ModsDirectory)) { return list; } foreach (string item in Directory.EnumerateFiles(_options.ModsDirectory, "*.manifest", SearchOption.TopDirectoryOnly)) { try { using JsonDocument jsonDocument = JsonDocument.Parse(File.ReadAllText(item)); JsonElement rootElement = jsonDocument.RootElement; string propertyName = rootElement.GetProperty("root").GetProperty("ref").GetString() ?? "1"; string text = rootElement.GetProperty("objects").GetProperty(propertyName).GetProperty("palletPath") .GetString(); if (!string.IsNullOrWhiteSpace(text)) { string fullPallet = Path.GetFullPath(text); if (source.Any((string package) => fullPallet.StartsWith(package.TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase))) { list.Add(item); } } } catch { } } return list; } private string ValidateTrashPath(string path) { string? path2 = Path.GetDirectoryName(Path.GetFullPath(_options.ModsDirectory).TrimEnd(Path.DirectorySeparatorChar)) ?? throw new InvalidOperationException("Mods directory has no parent."); string text = Path.GetFullPath(Path.Combine(path2, ".wristhub-trash")).TrimEnd(Path.DirectorySeparatorChar); string text2 = Path.GetFullPath(Path.Combine(path2, ".bonehub-trash")).TrimEnd(Path.DirectorySeparatorChar); string text3 = Path.GetFullPath(path).TrimEnd(Path.DirectorySeparatorChar); bool num = text3.StartsWith(text + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase) && !string.Equals(text3, text, StringComparison.OrdinalIgnoreCase); bool flag = text3.StartsWith(text2 + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase) && !string.Equals(text3, text2, StringComparison.OrdinalIgnoreCase); if (!num && !flag) { throw new InvalidDataException("A restore path falls outside WristHub Trash."); } return text3; } public void Dispose() { _downloads.JobChanged -= OnJobChanged; _downloads.Dispose(); (_client as IDisposable)?.Dispose(); _stateGate.Dispose(); } } } namespace BoneHub.Persistence { public sealed class BoneHubState { public HashSet FavoriteModIds { get; init; } = new HashSet(); public List RecentlyViewedModIds { get; init; } = new List(); public List RecentlyEquippedAvatarBarcodes { get; init; } = new List(); public string LastEquippedAvatarBarcode { get; set; } = string.Empty; public Dictionary InstalledMods { get; init; } = new Dictionary(); public BoneHubPreferences Preferences { get; init; } = new BoneHubPreferences(); public List TrashHistory { get; init; } = new List(); public List PendingDeletions { get; init; } = new List(); } public sealed class BoneHubPreferences { public BoneHubComfortPreset ComfortPreset { get; set; } public bool WatchEnabled { get; set; } = true; public bool RaiseToOpen { get; set; } = true; public bool UseLeftWrist { get; set; } = true; public float WatchScale { get; set; } = 1f; public float DwellSeconds { get; set; } = 1f; public float OutwardOffset { get; set; } = 0.029f; public float FingerOffset { get; set; } = -0.018f; public float WatchForearmOffset { get; set; } public bool ReducedMotion { get; set; } public bool HologramEffects { get; set; } = true; public bool HapticsEnabled { get; set; } = true; public bool SoundsEnabled { get; set; } = true; public float CarouselDistance { get; set; } = 0.62f; public float HologramScale { get; set; } = 1f; public float EquipZoneScale { get; set; } = 1f; public float HologramBrightness { get; set; } = 1f; public WristHubTheme HologramTheme { get; set; } public bool Use24HourClock { get; set; } = true; public BrowserStartupMode AvatarBrowserStartupMode { get; set; } = BrowserStartupMode.AllInstalled; public AvatarBrowseScope LastAvatarBrowseScope { get; set; } public List CompassCalibrations { get; init; } = new List(); public MiniMapRange MiniMapRange { get; set; } = MiniMapRange.ThirtyMeters; public MiniMapOrientation MiniMapOrientation { get; set; } public List FavoriteSpawnableBarcodes { get; init; } = new List(); public List RecentSpawnableBarcodes { get; init; } = new List(); } public enum WristHubTheme { Cyan, Graphite, Crimson, Emerald, Violet, Amber } public sealed class CompassCalibrationRecord { public string SceneKey { get; init; } = string.Empty; public float NorthYawDegrees { get; set; } public DateTimeOffset LastUsedUtc { get; set; } = DateTimeOffset.UtcNow; } public enum BoneHubComfortPreset { Default, Seated, LargeTargets, ReducedMotion } public sealed class InstalledModRecord { public long ModId { get; init; } public long FileId { get; init; } public string Name { get; init; } = string.Empty; public string Version { get; init; } = string.Empty; public string Md5 { get; init; } = string.Empty; public List InstalledDirectories { get; init; } = new List(); public ModIoSharingMetadata? Sharing { get; set; } public DateTimeOffset InstalledAt { get; init; } } public sealed class TrashedModRecord { public Guid Id { get; init; } = Guid.NewGuid(); public InstalledModRecord Mod { get; init; } = new InstalledModRecord(); public List Directories { get; init; } = new List(); public List Files { get; init; } = new List(); public bool WasSubscribed { get; init; } public DateTimeOffset TrashedAt { get; init; } = DateTimeOffset.UtcNow; } public sealed class TrashedFileRecord { public string OriginalPath { get; init; } = string.Empty; public string TrashPath { get; init; } = string.Empty; } public sealed class TrashedDirectoryRecord { public string OriginalPath { get; init; } = string.Empty; public string TrashPath { get; init; } = string.Empty; } public sealed class PendingDeletionRecord { public InstalledModRecord Mod { get; init; } = new InstalledModRecord(); public List Directories { get; init; } = new List(); public List SidecarFiles { get; init; } = new List(); public List AvatarBarcodes { get; init; } = new List(); public bool WasSubscribed { get; init; } public DateTimeOffset QueuedAt { get; init; } = DateTimeOffset.UtcNow; } public sealed record PendingDeletionProcessResult(int Completed, int Remaining, IReadOnlyList Problems); public sealed class JsonFileStore where T : class, new() { private readonly string _path; private readonly SemaphoreSlim _gate = new SemaphoreSlim(1, 1); private readonly JsonSerializerOptions _json = new JsonSerializerOptions { WriteIndented = true, PropertyNameCaseInsensitive = true }; public JsonFileStore(string path) { _path = Path.GetFullPath(path); } public async Task LoadAsync(CancellationToken cancellationToken = default(CancellationToken)) { await _gate.WaitAsync(cancellationToken).ConfigureAwait(continueOnCapturedContext: false); try { if (!File.Exists(_path)) { return new T(); } T result; await using (FileStream stream = File.OpenRead(_path)) { result = (await JsonSerializer.DeserializeAsync(stream, _json, cancellationToken).ConfigureAwait(continueOnCapturedContext: false)) ?? new T(); } return result; } catch (JsonException) { string destFileName = _path + $".corrupt-{DateTimeOffset.UtcNow:yyyyMMddHHmmss}"; File.Move(_path, destFileName, overwrite: false); return new T(); } finally { _gate.Release(); } } public async Task SaveAsync(T value, CancellationToken cancellationToken = default(CancellationToken)) { await _gate.WaitAsync(cancellationToken).ConfigureAwait(continueOnCapturedContext: false); try { Directory.CreateDirectory(Path.GetDirectoryName(_path)); string temporary = _path + ".tmp"; await using (FileStream stream = new FileStream(temporary, FileMode.Create, FileAccess.Write, FileShare.None)) { await JsonSerializer.SerializeAsync(stream, value, _json, cancellationToken).ConfigureAwait(continueOnCapturedContext: false); await stream.FlushAsync(cancellationToken).ConfigureAwait(continueOnCapturedContext: false); } File.Move(temporary, _path, overwrite: true); } finally { _gate.Release(); } } } public sealed class SearchCache { public sealed class CachedSearch { public DateTimeOffset SavedAt { get; init; } public ModIoPage Page { get; init; } = new ModIoPage(); } private readonly string _directory; public SearchCache(string directory) { _directory = Path.GetFullPath(directory); } public async Task?> ReadAsync(string key, TimeSpan maximumAge, CancellationToken cancellationToken = default(CancellationToken)) { CachedSearch cachedSearch = await new JsonFileStore(PathFor(key)).LoadAsync(cancellationToken).ConfigureAwait(continueOnCapturedContext: false); return (cachedSearch.SavedAt != default(DateTimeOffset) && DateTimeOffset.UtcNow - cachedSearch.SavedAt <= maximumAge) ? cachedSearch.Page : null; } public Task WriteAsync(string key, ModIoPage page, CancellationToken cancellationToken = default(CancellationToken)) { return new JsonFileStore(PathFor(key)).SaveAsync(new CachedSearch { SavedAt = DateTimeOffset.UtcNow, Page = page }, cancellationToken); } private string PathFor(string key) { Directory.CreateDirectory(_directory); byte[] inArray = SHA256.HashData(Encoding.UTF8.GetBytes(key)); return Path.Combine(_directory, Convert.ToHexString(inArray).ToLowerInvariant() + ".json"); } } } namespace BoneHub.Models { public sealed class ModIoPage { [JsonPropertyName("data")] public List Data { get; init; } = new List(); [JsonPropertyName("result_count")] public int ResultCount { get; init; } [JsonPropertyName("result_offset")] public int ResultOffset { get; init; } [JsonPropertyName("result_limit")] public int ResultLimit { get; init; } [JsonPropertyName("result_total")] public int ResultTotal { get; init; } } public sealed class ModIoMod { [JsonPropertyName("id")] public long Id { get; init; } [JsonPropertyName("name")] public string Name { get; init; } = string.Empty; [JsonPropertyName("name_id")] public string NameId { get; init; } = string.Empty; [JsonPropertyName("summary")] public string Summary { get; init; } = string.Empty; [JsonPropertyName("description_plaintext")] public string Description { get; init; } = string.Empty; [JsonPropertyName("profile_url")] public string ProfileUrl { get; init; } = string.Empty; [JsonPropertyName("date_updated")] public long DateUpdated { get; init; } [JsonPropertyName("submitted_by")] public ModIoUser Author { get; init; } = new ModIoUser(); [JsonPropertyName("logo")] public ModIoLogo Logo { get; init; } = new ModIoLogo(); [JsonPropertyName("modfile")] public ModIoFile? Modfile { get; init; } [JsonPropertyName("stats")] public ModIoStats Stats { get; init; } = new ModIoStats(); [JsonPropertyName("tags")] public List Tags { get; init; } = new List(); [JsonPropertyName("maturity_option")] public int MaturityOption { get; init; } public bool HasTag(string value) { return Tags.Any((ModIoTag tag) => string.Equals(tag.Name, value, StringComparison.OrdinalIgnoreCase)); } } public sealed class ModIoUser { [JsonPropertyName("username")] public string Username { get; init; } = string.Empty; [JsonPropertyName("display_name_portal")] public string? DisplayNamePortal { get; init; } public string DisplayName { get { if (!string.IsNullOrWhiteSpace(DisplayNamePortal)) { return DisplayNamePortal; } return Username; } } } public sealed class ModIoLogo { [JsonPropertyName("original")] public string Original { get; init; } = string.Empty; [JsonPropertyName("thumb_320x180")] public string Thumbnail { get; init; } = string.Empty; } public sealed class ModIoStats { [JsonPropertyName("downloads_total")] public long DownloadsTotal { get; init; } [JsonPropertyName("subscribers_total")] public long SubscribersTotal { get; init; } [JsonPropertyName("ratings_weighted_aggregate")] public double Rating { get; init; } } public sealed class ModIoTag { [JsonPropertyName("name")] public string Name { get; init; } = string.Empty; } public sealed class ModIoFile { [JsonPropertyName("id")] public long Id { get; init; } [JsonPropertyName("mod_id")] public long ModId { get; init; } [JsonPropertyName("filename")] public string Filename { get; init; } = string.Empty; [JsonPropertyName("version")] public string Version { get; init; } = string.Empty; [JsonPropertyName("date_added")] public long DateAdded { get; init; } [JsonPropertyName("filesize")] public long FileSize { get; init; } [JsonPropertyName("filesize_uncompressed")] public long UncompressedSize { get; init; } [JsonPropertyName("virus_status")] public int VirusStatus { get; init; } [JsonPropertyName("virus_positive")] public int VirusPositive { get; init; } [JsonPropertyName("filehash")] public ModIoFileHash Hash { get; init; } = new ModIoFileHash(); [JsonPropertyName("download")] public ModIoDownload Download { get; init; } = new ModIoDownload(); [JsonPropertyName("platforms")] public List Platforms { get; init; } = new List(); } public sealed class ModIoFileHash { [JsonPropertyName("md5")] public string Md5 { get; init; } = string.Empty; } public sealed class ModIoDownload { [JsonPropertyName("binary_url")] public string BinaryUrl { get; init; } = string.Empty; [JsonPropertyName("date_expires")] public long DateExpires { get; init; } } public sealed class ModIoPlatform { [JsonPropertyName("platform")] public string Platform { get; init; } = string.Empty; [JsonPropertyName("status")] public int Status { get; init; } } public sealed class ModIoDependency { [JsonPropertyName("mod_id")] public long ModId { get; init; } [JsonPropertyName("date_added")] public long DateAdded { get; init; } } public enum ModSort { Trending, Newest, MostDownloaded, RecentlyUpdated } public sealed record ModSearchRequest(string Query, string? Tag = null, string? Creator = null, ModSort Sort = ModSort.Trending, int Offset = 0, int Limit = 20); public sealed record ModUpdateInfo(ModIoMod Mod, bool UpdateAvailable, long InstalledFileId, long LatestFileId); public sealed record ModInstallPlan(ModIoMod Root, IReadOnlyList OrderedMods, IReadOnlyList AlreadyInstalledIds); public sealed record InstalledModHealth(bool Healthy, IReadOnlyList Problems); } namespace BoneHub.Interaction { public enum AvatarBrowseScope { AllInstalled, Groups } public enum BrowserStartupMode { RememberLast, AllInstalled, Groups } public static class AvatarBrowseStartup { public static AvatarBrowseScope Resolve(BrowserStartupMode mode, AvatarBrowseScope lastScope) { return mode switch { BrowserStartupMode.RememberLast => Enum.IsDefined(lastScope) ? lastScope : AvatarBrowseScope.AllInstalled, BrowserStartupMode.Groups => AvatarBrowseScope.Groups, _ => AvatarBrowseScope.AllInstalled, }; } } public enum AvatarBrowserViewState { PushPrompt, Hub, Library, Pack, Downloads, Compass, CodeMods, MiniMap, Portals, PortalLevels, FusionServers, Spawnables, PackOptions, Options, Keyboard, Searching, Result, Downloading, PreparingPreview, Error } public enum AvatarPackSort { RecentlyUsed, Name, Creator } public sealed record AvatarPackEntry(long ModId, string Name, string Creator, string ThumbnailUrl, IReadOnlyList Avatars, bool Installed, AvatarDownloadState DownloadState = AvatarDownloadState.None, double DownloadProgress = 0.0, string? Status = null) { public string Identity => "pack:" + ModId; public int AvatarCount => Avatars.Count((AvatarEntry entry) => entry.Installed && !string.IsNullOrWhiteSpace(entry.Barcode)); } public static class AvatarPackLibrary { public static IReadOnlyList Group(IEnumerable entries, IEnumerable? recentBarcodes = null, AvatarPackSort sort = AvatarPackSort.RecentlyUsed) { Dictionary recent = (recentBarcodes ?? Array.Empty()).Select((string barcode, int index) => (barcode: barcode, index: index)).ToDictionary<(string, int), string, int>(((string barcode, int index) item) => item.barcode, ((string barcode, int index) item) => item.index, StringComparer.OrdinalIgnoreCase); IEnumerable source = (from entry in entries group entry by entry.ModId).Select(delegate(IGrouping group) { AvatarEntry[] array = group.OrderBy((AvatarEntry item) => item.AvatarName, StringComparer.OrdinalIgnoreCase).ToArray(); AvatarEntry avatarEntry = array.OrderByDescending((AvatarEntry item) => item.Installed).First(); AvatarDownloadState downloadState = array.Select((AvatarEntry item) => item.DownloadState).OrderByDescending(DownloadRank).FirstOrDefault(); return new AvatarPackEntry(group.Key, avatarEntry.ModName, avatarEntry.Creator, avatarEntry.ThumbnailUrl, array, array.Any((AvatarEntry item) => item.Installed), downloadState, array.Max((AvatarEntry item) => item.DownloadProgress), array.Select((AvatarEntry item) => item.Status).FirstOrDefault((string value) => !string.IsNullOrWhiteSpace(value))); }); return sort switch { AvatarPackSort.Name => source.OrderBy((AvatarPackEntry pack) => pack.Name, StringComparer.OrdinalIgnoreCase).ToArray(), AvatarPackSort.Creator => source.OrderBy((AvatarPackEntry pack) => pack.Creator, StringComparer.OrdinalIgnoreCase).ThenBy((AvatarPackEntry pack) => pack.Name, StringComparer.OrdinalIgnoreCase).ToArray(), _ => source.OrderBy((AvatarPackEntry pack) => RecentIndex(pack, recent)).ThenBy((AvatarPackEntry pack) => pack.Name, StringComparer.OrdinalIgnoreCase).ToArray(), }; } private static int RecentIndex(AvatarPackEntry pack, IReadOnlyDictionary recent) { int[] array = (from item in pack.Avatars where recent.ContainsKey(item.Barcode) select recent[item.Barcode]).ToArray(); if (array.Length != 0) { return array.Min(); } return int.MaxValue; } private static int DownloadRank(AvatarDownloadState state) { return state switch { AvatarDownloadState.Failed => 7, AvatarDownloadState.Installing => 6, AvatarDownloadState.Downloading => 5, AvatarDownloadState.Queued => 4, AvatarDownloadState.Cancelled => 3, AvatarDownloadState.Installed => 2, _ => 1, }; } } public sealed class FourRowPageCursor { public const int RowsPerPage = 4; public int Page { get; private set; } public int Count { get; private set; } public int PageCount => Math.Max(1, (Count + 4 - 1) / 4); public string PositionText => $"{Page + 1} / {PageCount}"; public int Reset(int count) { Count = Math.Max(0, count); Page = 0; return Page; } public int Keep(int count) { Count = Math.Max(0, count); Page = Math.Clamp(Page, 0, PageCount - 1); return Page; } public int Move(int direction) { if (PageCount <= 1 || direction == 0) { return Page; } Page = (Page + Math.Sign(direction) + PageCount) % PageCount; return Page; } } public sealed class FocusedResultCursor { public int Index { get; private set; } public int Count { get; private set; } public string PositionText { get { if (Count != 0) { return $"{Index + 1} / {Count}"; } return "0 / 0"; } } public bool CanMovePrevious => Count > 1; public bool CanMoveNext => Count > 1; public int Reset(int count) { Count = Math.Max(0, count); Index = 0; return Index; } public int Keep(int count, int preferredIndex) { Count = Math.Max(0, count); Index = ((Count != 0) ? Math.Clamp(preferredIndex, 0, Count - 1) : 0); return Index; } public int Move(int direction) { if (Count <= 1 || direction == 0) { return Index; } Index = (Index + Math.Sign(direction) + Count) % Count; return Index; } } public readonly record struct TabletRect(float X, float Y, float Width, float Height) { public float Left => X - Width * 0.5f; public float Right => X + Width * 0.5f; public float Bottom => Y - Height * 0.5f; public float Top => Y + Height * 0.5f; public bool IsInside(TabletRect parent) { if (Left >= parent.Left && Right <= parent.Right && Bottom >= parent.Bottom) { return Top <= parent.Top; } return false; } public bool Overlaps(TabletRect other) { if (Left < other.Right && Right > other.Left && Bottom < other.Top) { return Top > other.Bottom; } return false; } } public static class AvatarTabletLayout { public static readonly TabletRect Tablet = new TabletRect(0f, 0f, 340f, 210f); public static readonly TabletRect Search = new TabletRect(-34f, 82f, 150f, 30f); public static readonly TabletRect Close = new TabletRect(148f, 82f, 30f, 30f); public static readonly TabletRect Artwork = new TabletRect(-66f, -4f, 124f, 128f); public static readonly TabletRect Details = new TabletRect(65f, 3f, 126f, 112f); public static readonly TabletRect Previous = new TabletRect(-151f, 14f, 26f, 54f); public static readonly TabletRect Next = new TabletRect(151f, 14f, 26f, 54f); public static readonly TabletRect Action = new TabletRect(65f, -69f, 126f, 30f); public static readonly TabletRect Trash = new TabletRect(-66f, -91f, 116f, 18f); public static readonly TabletRect Progress = new TabletRect(65f, -91f, 126f, 14f); public static IReadOnlyList ContentRects => new TabletRect[9] { Search, Close, Artwork, Details, Previous, Next, Action, Trash, Progress }; public static TabletRect PhysicalTouchRect(TabletRect rect) { return new TabletRect(rect.X, rect.Y, rect.Width * 1.1f, rect.Height * 1.2f); } } public static class ArcDashboardLayout { public static readonly TabletRect Presentation = new TabletRect(0f, 0f, 300f, 190f); public static readonly TabletRect Stage = new TabletRect(0f, 7f, 170f, 124f); public static readonly TabletRect LeftWing = new TabletRect(-122f, 16f, 50f, 146f); public static readonly TabletRect RightWing = new TabletRect(122f, 16f, 50f, 146f); public static readonly TabletRect Back = new TabletRect(-122f, 72f, 42f, 22f); public static readonly TabletRect Search = new TabletRect(-122f, 44f, 46f, 24f); public static readonly TabletRect Previous = new TabletRect(-122f, 7f, 42f, 42f); public static readonly TabletRect Scope = new TabletRect(-122f, -35f, 46f, 25f); public static readonly TabletRect Close = new TabletRect(122f, 72f, 30f, 26f); public static readonly TabletRect Pack = new TabletRect(122f, 44f, 46f, 24f); public static readonly TabletRect Next = new TabletRect(122f, 7f, 42f, 42f); public static readonly TabletRect Options = new TabletRect(122f, -35f, 46f, 25f); public static readonly TabletRect Name = new TabletRect(0f, 82f, 166f, 20f); public static readonly TabletRect Creator = new TabletRect(-45f, 65f, 78f, 14f); public static readonly TabletRect PackName = new TabletRect(45f, 65f, 92f, 14f); public static readonly TabletRect Height = new TabletRect(-54f, -53f, 94f, 16f); public static readonly TabletRect Position = new TabletRect(0f, -53f, 48f, 16f); public static readonly TabletRect Status = new TabletRect(54f, -53f, 94f, 18f); public static readonly TabletRect Action = new TabletRect(0f, -80f, 116f, 28f); public static readonly TabletRect Progress = Action; public static IReadOnlyList TouchControls => new TabletRect[9] { Back, Search, Previous, Scope, Close, Pack, Next, Options, Action }; } public static class WristOsHomeHeaderLayout { public static readonly TabletRect Download = new TabletRect(-86f, 70f, 28f, 23f); public static readonly TabletRect Wordmark = new TabletRect(-25f, 70f, 88f, 20f); public static readonly TabletRect OsMark = new TabletRect(31f, 70f, 20f, 20f); public static readonly TabletRect AccentRail = new TabletRect(-19f, 56f, 120f, 1.5f); public static readonly TabletRect Settings = new TabletRect(66f, 70f, 28f, 23f); public static readonly TabletRect Close = new TabletRect(106f, 70f, 24f, 24f); public static IReadOnlyList InteractiveControls => new TabletRect[3] { Download, Settings, Close }; } public static class AvatarPackTabletLayout { public static readonly IReadOnlyList HeaderControls = new TabletRect[5] { new TabletRect(-145f, 83f, 42f, 28f), new TabletRect(-90f, 83f, 64f, 28f), new TabletRect(-34f, 83f, 46f, 28f), new TabletRect(25f, 83f, 68f, 28f), new TabletRect(148f, 83f, 26f, 28f) }; public static readonly IReadOnlyList Rows = (from index in Enumerable.Range(0, 4) select new TabletRect(-36f, 50f - (float)index * 34f, 228f, 30f)).ToArray(); public static readonly IReadOnlyList DeleteRows = (from index in Enumerable.Range(0, 4) select new TabletRect(125f, 50f - (float)index * 34f, 52f, 30f)).ToArray(); public static readonly TabletRect Status = new TabletRect(0f, -72f, 250f, 10f); public static readonly IReadOnlyList FooterControls = new TabletRect[3] { new TabletRect(-135f, -91f, 50f, 26f), new TabletRect(0f, -91f, 90f, 24f), new TabletRect(135f, -91f, 50f, 26f) }; public static IReadOnlyList AllControls => HeaderControls.Concat(Rows).Concat(DeleteRows).Append(Status) .Concat(FooterControls) .ToArray(); } public sealed class PresentationGeneration { public int Value { get; private set; } public int Next() { return ++Value; } public bool IsCurrent(int generation) { return generation == Value; } } public sealed class SurfaceInputRearmGate { private readonly double _clearSeconds; private double _clearSince = -1.0; public bool Required { get; private set; } public SurfaceInputRearmGate(double clearSeconds = 0.1) { _clearSeconds = Math.Max(0.0, clearSeconds); } public void Require() { Required = true; _clearSince = -1.0; } public bool CanInteract(bool touchingAnyActiveControl, double nowSeconds) { if (!Required) { return true; } if (touchingAnyActiveControl) { _clearSince = -1.0; return false; } if (_clearSince < 0.0) { _clearSince = nowSeconds; return _clearSeconds <= 0.0; } if (nowSeconds - _clearSince < _clearSeconds) { return false; } Required = false; _clearSince = -1.0; return true; } } public sealed class AvatarSearchInputSession { public int Generation { get; private set; } public bool IsOpen { get; private set; } public string InitialValue { get; private set; } = string.Empty; public int Open(string? initialValue) { InitialValue = initialValue?.Trim() ?? string.Empty; IsOpen = true; return ++Generation; } public bool TrySubmit(int generation, string? value, out string query) { query = value?.Trim() ?? string.Empty; if (!IsOpen || generation != Generation) { return false; } IsOpen = false; return true; } public bool Cancel(int generation) { if (!IsOpen || generation != Generation) { return false; } IsOpen = false; return true; } } public sealed class TabletKeyboardBuffer { public const int MaximumLength = 64; public string Value { get; private set; } = string.Empty; public bool Shifted { get; private set; } public void Set(string? value) { Value = (((value ?? string.Empty).Length <= 64) ? (value ?? string.Empty) : value.Substring(0, 64)); } public bool Append(char character) { if (Value.Length >= 64 || char.IsControl(character)) { return false; } Value += (Shifted ? char.ToUpperInvariant(character) : char.ToLowerInvariant(character)); return true; } public bool AppendSpace() { if (Value.Length >= 64 || Value.EndsWith(' ')) { return false; } Value += " "; return true; } public bool Backspace() { if (Value.Length == 0) { return false; } string value = Value; Value = value.Substring(0, value.Length - 1); return true; } public void Clear() { Value = string.Empty; } public void ToggleShift() { Shifted = !Shifted; } public string VisibleValue(int maximumCharacters = 36) { int num = Math.Clamp(maximumCharacters, 4, 64); if (Value.Length > num) { string value = Value; int num2 = num - 1; int length = value.Length; int num3 = length - num2; return "…" + value.Substring(num3, length - num3); } return Value; } } public static class PreviewDragMath { public static bool IsInside(Vector3 localPoint, Vector3 halfExtents) { if (MathF.Abs(localPoint.X) <= MathF.Max(0f, halfExtents.X) && MathF.Abs(localPoint.Y) <= MathF.Max(0f, halfExtents.Y)) { return MathF.Abs(localPoint.Z) <= MathF.Max(0f, halfExtents.Z); } return false; } public static bool PinchActive(float thumbIndexDistance, bool alreadyDragging, float beginDistance = 0.03f, float releaseDistance = 0.045f) { if (float.IsFinite(thumbIndexDistance)) { return thumbIndexDistance <= (alreadyDragging ? MathF.Max(beginDistance, releaseDistance) : beginDistance); } return false; } public static bool GripActive(float gripForce, bool alreadyDragging, float beginForce = 0.62f, float releaseForce = 0.28f) { if (float.IsFinite(gripForce)) { return gripForce >= (alreadyDragging ? MathF.Min(beginForce, releaseForce) : beginForce); } return false; } public static bool IsChestRelease(Vector3 previewPosition, Vector3 chestPosition, float radius) { if (radius > 0f) { return Vector3.Distance(previewPosition, chestPosition) <= radius; } return false; } public static Vector3 LimitAnchorOffset(Vector3 offset, float maximumDistance) { if (!float.IsFinite(offset.X) || !float.IsFinite(offset.Y) || !float.IsFinite(offset.Z)) { return Vector3.Zero; } float num = Math.Clamp(maximumDistance, 0.01f, 0.2f); float num2 = offset.LengthSquared(); if (!float.IsFinite(num2) || num2 <= 0f) { return Vector3.Zero; } if (num2 <= num * num) { return offset; } return Vector3.Normalize(offset) * num; } } public static class ChestEquipGuideMath { public static bool ResolveLocked(float distance, bool wasLocked, float radius, float releaseMultiplier = 1.12f) { if (!float.IsFinite(distance) || !float.IsFinite(radius) || radius <= 0f) { return false; } float num = (wasLocked ? (radius * Math.Clamp(releaseMultiplier, 1f, 1.5f)) : radius); return distance <= num; } public static float Proximity(float distance, float radius, float visibleRadiusMultiplier = 2.75f) { if (!float.IsFinite(distance) || !float.IsFinite(radius) || radius <= 0f) { return 0f; } float num = radius * Math.Clamp(visibleRadiusMultiplier, 1.1f, 6f); if (distance <= radius) { return 1f; } return Math.Clamp(1f - (distance - radius) / (num - radius), 0f, 1f); } } public sealed class LookAwayDismissal { public double AwaySeconds { get; private set; } public bool Update(bool observed, double deltaSeconds, double delaySeconds = 0.35) { if (observed) { AwaySeconds = 0.0; return false; } AwaySeconds += Math.Clamp(deltaSeconds, 0.0, 0.25); return AwaySeconds >= Math.Max(0.05, delaySeconds); } public void Reset() { AwaySeconds = 0.0; } } public readonly record struct AvatarPreviewMetrics(string Barcode, float GameplayHeightMeters, bool Valid) { public string DisplayText { get { if (!Valid || !float.IsFinite(GameplayHeightMeters) || !(GameplayHeightMeters > 0f)) { return "Height unavailable"; } if (!(GameplayHeightMeters < 10f)) { if (GameplayHeightMeters < 100f) { return $"{GameplayHeightMeters:0.0} m"; } return $"{GameplayHeightMeters:#,##0} m"; } return $"{GameplayHeightMeters:0.00} m"; } } public string HeightDisplayText { get { if (!Valid) { return "Avatar height unavailable"; } return "Avatar height " + DisplayText; } } } public enum AvatarPreviewVisualSource { Unavailable, FullColor, NativePreviewMesh } public enum AvatarPreviewHeightSource { Unavailable, AvatarMetadata, RendererBounds, PreviewMeshBounds } public readonly record struct AvatarResolvedHeight(float Meters, AvatarPreviewHeightSource Source) { public bool Valid => AvatarPreviewMath.IsDisplayableGameplayHeight(Meters); } public readonly record struct AvatarPreviewRouteMetadata(AvatarPreviewVisualSource VisualSource, AvatarPreviewHeightSource HeightSource, bool InteractionReady); public readonly record struct AvatarPreviewPlacement(float UniformScale, Vector3 Offset) { public bool Valid { get { if (float.IsFinite(UniformScale) && UniformScale > 0f && float.IsFinite(Offset.X) && float.IsFinite(Offset.Y)) { return float.IsFinite(Offset.Z); } return false; } } } public static class AvatarPreviewMath { public const float DisplayHeightMeters = 0.16f; public const float DisplayWidthMeters = 0.135f; public const float DisplayDepthMeters = 0.1f; public const float FrontFacingYawDegrees = 180f; public static float NormalizeScale(float visualHeightMeters, float displayHeightMeters = 0.16f) { if (!IsUsableHeight(visualHeightMeters)) { return 1f; } return Math.Clamp(displayHeightMeters, 0.05f, 0.3f) / visualHeightMeters; } public static bool IsUsableHeight(float value) { if (float.IsFinite(value)) { if (value >= 0.05f) { return value <= 100f; } return false; } return false; } public static bool IsDisplayableGameplayHeight(float value) { if (float.IsFinite(value)) { if (value >= 0.01f) { return value <= 10000f; } return false; } return false; } public static float NormalizeBoundsScale(Vector3 visualSize) { if (!float.IsFinite(visualSize.Y) || visualSize.Y <= 0.0001f) { return 1f; } return 0.16f / visualSize.Y; } public static AvatarPreviewPlacement FitBounds(Vector3 visualCenter, Vector3 visualSize) { float num = NormalizeBoundsScale(visualSize); return new AvatarPreviewPlacement(num, -visualCenter * num); } public static AvatarResolvedHeight ResolveHeight(float avatarMetadataHeight, float rendererBoundsHeight, float previewMeshBoundsHeight = 0f) { if (IsUsableHeight(avatarMetadataHeight)) { return new AvatarResolvedHeight(avatarMetadataHeight, AvatarPreviewHeightSource.AvatarMetadata); } if (IsUsableHeight(rendererBoundsHeight)) { return new AvatarResolvedHeight(rendererBoundsHeight, AvatarPreviewHeightSource.RendererBounds); } if (IsUsableHeight(previewMeshBoundsHeight)) { return new AvatarResolvedHeight(previewMeshBoundsHeight, AvatarPreviewHeightSource.PreviewMeshBounds); } return new AvatarResolvedHeight(0f, AvatarPreviewHeightSource.Unavailable); } public static AvatarResolvedHeight ResolveGameplayHeight(float selectedAvatarHeight, float fallbackAvatarHeight) { if (IsDisplayableGameplayHeight(selectedAvatarHeight)) { return new AvatarResolvedHeight(selectedAvatarHeight, AvatarPreviewHeightSource.AvatarMetadata); } if (IsDisplayableGameplayHeight(fallbackAvatarHeight)) { return new AvatarResolvedHeight(fallbackAvatarHeight, AvatarPreviewHeightSource.AvatarMetadata); } return new AvatarResolvedHeight(0f, AvatarPreviewHeightSource.Unavailable); } public static bool IsVisualRendererType(string? typeName) { if (!string.IsNullOrWhiteSpace(typeName) && !typeName.Contains("Particle", StringComparison.OrdinalIgnoreCase) && !typeName.Contains("Trail", StringComparison.OrdinalIgnoreCase)) { return !typeName.Contains("LineRenderer", StringComparison.OrdinalIgnoreCase); } return false; } public static bool IsSafePreview(float visualHeightMeters, int rendererCount, int vertexCount, bool constrained) { int num = (constrained ? 24 : 64); int num2 = (constrained ? 350000 : 1250000); if (IsUsableHeight(visualHeightMeters) && rendererCount > 0 && rendererCount <= num && vertexCount >= 0) { return vertexCount <= num2; } return false; } } public static class AvatarUiScaleMath { public const float ReferencePlayerHeightMeters = 1.75f; public static float ResolveMenuScale(float playerHeightMeters) { if (!AvatarPreviewMath.IsUsableHeight(playerHeightMeters)) { return 1f; } return playerHeightMeters / 1.75f; } public static float ResolveDetachedContentScale(float tabletWorldScale) { if (!float.IsFinite(tabletWorldScale) || !(tabletWorldScale > 0.0001f)) { return 1f; } return tabletWorldScale; } } public readonly record struct NativeMenuPlacement(Vector3 Position, Vector3 Forward, float DistanceMeters); public static class NativeMenuPlacementMath { public const float ReferenceDistanceMeters = 1.05f; public static Vector3 ResolveForward(Vector3 headForward, Vector3 fallbackForward) { Vector3 value = new Vector3(headForward.X, 0f, headForward.Z); if (value.LengthSquared() < 0.04f) { value = new Vector3(fallbackForward.X, 0f, fallbackForward.Z); } if (value.LengthSquared() < 0.0001f) { value = Vector3.UnitZ; } return Vector3.Normalize(value); } public static NativeMenuPlacement Resolve(Vector3 headPosition, Vector3 headForward, float avatarHeightMeters) { Vector3 vector = ResolveForward(headForward, Vector3.UnitZ); float num = Math.Clamp(AvatarUiScaleMath.ResolveMenuScale(avatarHeightMeters), 0.65f, 1.75f); float num2 = 1.05f * num; return new NativeMenuPlacement(headPosition + vector * num2 - Vector3.UnitY * (0.06f * num), vector, num2); } } public static class DownloadPresentationMath { public static double Smooth(double displayed, double authoritative, double deltaSeconds, double speed = 8.0) { double num = Math.Clamp(displayed, 0.0, 1.0); double num2 = Math.Clamp(authoritative, 0.0, 1.0); if (num2 <= num || deltaSeconds <= 0.0) { return Math.Min(num, (num2 < num) ? num : num2); } double num3 = 1.0 - Math.Exp((0.0 - Math.Max(0.1, speed)) * Math.Min(deltaSeconds, 0.25)); double num4 = num + (num2 - num) * num3; if (!(num2 - num4 < 0.0005)) { return Math.Min(num4, num2); } return num2; } } public static class FingertipProbeMath { public static Vector3 ControllerTip(Vector3 position, Vector3 forward, float reachMeters = 0.075f) { Vector3 vector = ((forward.LengthSquared() > 0.0001f) ? Vector3.Normalize(forward) : Vector3.UnitZ); return position + vector * Math.Clamp(reachMeters, 0f, 0.15f); } public static Vector3 Extrapolate(Vector3 intermediate, Vector3 distal, float distalFraction = 0.65f) { Vector3 value = distal - intermediate; float num = value.Length(); if (!(num > 0.0001f)) { return distal; } return distal + Vector3.Normalize(value) * num * Math.Clamp(distalFraction, 0f, 1.5f); } } public static class WorldTextFacingMath { public const float FrontFacingLocalYawDegrees = 0f; public static bool IsFrontFacing(float localYawDegrees) { return Math.Abs(Math.IEEERemainder(localYawDegrees - 0f, 360.0)) < 0.0010000000474974513; } public static bool FacesViewer(Vector3 rootForward, Vector3 directionToViewer, float localYawDegrees) { if (rootForward.LengthSquared() < 0.0001f || directionToViewer.LengthSquared() < 0.0001f) { return false; } Vector3 vector = Vector3.Normalize(rootForward); Vector3 value = Vector3.Cross(Vector3.UnitY, vector); value = ((!(value.LengthSquared() < 0.0001f)) ? Vector3.Normalize(value) : Vector3.UnitX); float x = localYawDegrees * (float)Math.PI / 180f; return Vector3.Dot(Vector3.Normalize(-vector * MathF.Cos(x) - value * MathF.Sin(x)), Vector3.Normalize(directionToViewer)) > 0.999f; } } public enum AvatarRestoreDecision { NativeSaveOwnsSelection, WaitForNativeSave, RestorePrivateFallback, NoRestore } public static class AvatarRestorePolicy { public static AvatarRestoreDecision Decide(bool nativeSaveReadable, bool nativeSaveWaitExpired, string? privateFallbackBarcode) { if (nativeSaveReadable) { return AvatarRestoreDecision.NativeSaveOwnsSelection; } if (!nativeSaveWaitExpired) { return AvatarRestoreDecision.WaitForNativeSave; } if (!string.IsNullOrWhiteSpace(privateFallbackBarcode)) { return AvatarRestoreDecision.RestorePrivateFallback; } return AvatarRestoreDecision.NoRestore; } } public enum AvatarRigCompatibility { UnverifiedAllowed, Verified, Invalid } public readonly record struct AvatarRigValidationResult(AvatarRigCompatibility Compatibility, string Reason) { public bool CanEquip => Compatibility != AvatarRigCompatibility.Invalid; public bool IsVerified => Compatibility == AvatarRigCompatibility.Verified; public static AvatarRigValidationResult Ready() { return new AvatarRigValidationResult(AvatarRigCompatibility.Verified, string.Empty); } public static AvatarRigValidationResult Allow(string reason) { return new AvatarRigValidationResult(AvatarRigCompatibility.UnverifiedAllowed, reason); } public static AvatarRigValidationResult Reject(string reason) { return new AvatarRigValidationResult(AvatarRigCompatibility.Invalid, reason); } } public enum AvatarSharingState { Unregistered, Registering, Ready, Announced, PlatformLimited, Failed } public readonly record struct AvatarPlatformAvailability(long? WindowsFileId, long? AndroidFileId) { public bool HasWindows { get { long? windowsFileId = WindowsFileId; if (windowsFileId.HasValue) { return windowsFileId.GetValueOrDefault() > 0; } return false; } } public bool HasAndroid { get { long? androidFileId = AndroidFileId; if (androidFileId.HasValue) { return androidFileId.GetValueOrDefault() > 0; } return false; } } public bool HasAny { get { if (!HasWindows) { return HasAndroid; } return true; } } public bool IsLimited { get { if (HasAny) { if (HasWindows) { return !HasAndroid; } return true; } return false; } } public string Badge { get { if (!HasWindows || !HasAndroid) { if (!HasWindows) { if (!HasAndroid) { return "LOCAL ONLY"; } return "QUEST ONLY"; } return "PC ONLY"; } return "PC + QUEST"; } } } public readonly record struct CompassPresentation(float HeadingDegrees, float SmoothedHeadingDegrees, float NorthOffsetDegrees, string Cardinal, string SceneKey, bool HasCalibration); public static class CompassHeadingMath { private static readonly string[] Cardinals = new string[8] { "N", "NE", "E", "SE", "S", "SW", "W", "NW" }; public static float Normalize(float degrees) { if (!float.IsFinite(degrees)) { return 0f; } float num = degrees % 360f; if (!(num < 0f)) { return num; } return num + 360f; } public static float Heading(float worldYawDegrees, float northOffsetDegrees) { return Normalize(worldYawDegrees - northOffsetDegrees); } public static float SignedDelta(float fromDegrees, float toDegrees) { float num = Normalize(toDegrees) - Normalize(fromDegrees); if (num > 180f) { num -= 360f; } if (num < -180f) { num += 360f; } return num; } public static float Smooth(float current, float target, float deltaSeconds, float response = 12f) { if (!float.IsFinite(deltaSeconds) || deltaSeconds <= 0f) { return Normalize(current); } float num = 1f - MathF.Exp((0f - MathF.Max(0.01f, response)) * deltaSeconds); return Normalize(current + SignedDelta(current, target) * num); } public static string Cardinal(float headingDegrees) { int num = (int)MathF.Floor((Normalize(headingDegrees) + 22.5f) / 45f) % Cardinals.Length; return Cardinals[num]; } public static float NeedleRotation(float headingDegrees) { return 0f - Normalize(headingDegrees); } } public static class CompassCalibrationStore { public const int MaximumRecords = 64; public static CompassCalibrationRecord? Find(IReadOnlyList records, string sceneKey) { return records.FirstOrDefault((CompassCalibrationRecord record) => string.Equals(record.SceneKey, sceneKey, StringComparison.OrdinalIgnoreCase)); } public static void Upsert(List records, string sceneKey, float northYaw, DateTimeOffset usedAt) { string sceneKey2 = (string.IsNullOrWhiteSpace(sceneKey) ? "unknown-scene" : sceneKey.Trim()); CompassCalibrationRecord compassCalibrationRecord = Find(records, sceneKey2); if (compassCalibrationRecord != null) { compassCalibrationRecord.NorthYawDegrees = CompassHeadingMath.Normalize(northYaw); compassCalibrationRecord.LastUsedUtc = usedAt; } else { records.Add(new CompassCalibrationRecord { SceneKey = sceneKey2, NorthYawDegrees = CompassHeadingMath.Normalize(northYaw), LastUsedUtc = usedAt }); } while (records.Count > 64) { CompassCalibrationRecord item = records.OrderBy((CompassCalibrationRecord record) => record.LastUsedUtc).First(); records.Remove(item); } } } public enum HologramCarouselRole { Previous, Selected, Next } public enum HologramAvatarMaterialMode { BlueHologram, FullColor } public readonly record struct HologramCarouselSlot(HologramCarouselRole Role, int Index, string Identity, float HorizontalOffset, float Scale, HologramAvatarMaterialMode MaterialMode, bool InteractionReady); public static class HologramCarouselPresentation { public const float SideOffsetMeters = 0.105f; public const float SideScale = 0.62f; public static IReadOnlyList Build(IReadOnlyList identities, int selectedIndex) { if (identities.Count == 0) { return Array.Empty(); } int num = Wrap(selectedIndex, identities.Count); if (identities.Count != 1) { int index = Wrap(num - 1, identities.Count); int index2 = Wrap(num + 1, identities.Count); return new HologramCarouselSlot[3] { Slot(HologramCarouselRole.Previous, index, identities[index], -0.105f, 0.62f, interactive: false), Slot(HologramCarouselRole.Selected, num, identities[num], 0f, 1f, interactive: true), Slot(HologramCarouselRole.Next, index2, identities[index2], 0.105f, 0.62f, interactive: false) }; } return new HologramCarouselSlot[1] { Slot(HologramCarouselRole.Selected, num, identities[num], 0f, 1f, interactive: true) }; } public static int Wrap(int index, int count) { if (count <= 0) { return 0; } int num = index % count; if (num >= 0) { return num; } return num + count; } private static HologramCarouselSlot Slot(HologramCarouselRole role, int index, string identity, float offset, float scale, bool interactive) { return new HologramCarouselSlot(role, index, identity, offset, scale, interactive ? HologramAvatarMaterialMode.FullColor : HologramAvatarMaterialMode.BlueHologram, interactive); } } public enum CinematicProjectionPhase { Hidden, Charging, RingExpansion, SideAvatarAssembly, CenterAvatarAssembly, Stable, CarouselTransition, PanelTransition, Collapsing, TrackingGrace } public readonly record struct CinematicProjectionProgress(CinematicProjectionPhase Phase, float Emitter, float Rings, float SideAvatars, float CenterAvatar, float Controls); public static class CinematicProjectionTimeline { public const float ChargeEnd = 0.15f; public const float RingEnd = 0.35f; public const float SideEnd = 0.45f; public const float CenterEnd = 0.55f; public const float CollapseSeconds = 0.35f; public static CinematicProjectionProgress Resolve(float elapsed, bool reducedMotion) { if (reducedMotion) { return new CinematicProjectionProgress(CinematicProjectionPhase.Stable, 1f, 1f, 1f, 1f, 1f); } float num = Math.Max(0f, elapsed); return new CinematicProjectionProgress((num < 0.15f) ? CinematicProjectionPhase.Charging : ((num < 0.35f) ? CinematicProjectionPhase.RingExpansion : ((num < 0.45f) ? CinematicProjectionPhase.SideAvatarAssembly : ((num < 0.55f) ? CinematicProjectionPhase.CenterAvatarAssembly : CinematicProjectionPhase.Stable))), Smooth(num / 0.15f), Smooth((num - 0.0825f) / 0.26749998f), Smooth((num - 0.22749999f) / 0.2225f), Smooth((num - 0.35f) / 0.20000002f), Smooth((num - 0.35099998f) / 0.19900003f)); } private static float Smooth(float value) { float num = Math.Clamp(value, 0f, 1f); return num * num * (3f - 2f * num); } } public static class HoloInteractionMath { public static int WrapIndex(int current, int delta, int count) { if (count <= 0) { return 0; } return ((current + delta) % count + count) % count; } public static float SmoothStep01(float value) { float num = Math.Clamp(value, 0f, 1f); return num * num * (3f - 2f * num); } public static bool IsIntentionalFlick(float lateralVelocity, float threshold) { return Math.Abs(lateralVelocity) >= Math.Max(0f, threshold); } } public enum HoloPerformanceTier { Full, Balanced, Minimal } public sealed class HoloPerformanceGovernor { private readonly bool _constrained; private double _averageFrameMs; private int _slowSamples; private int _fastSamples; public HoloPerformanceTier Tier { get; private set; } public int RendererLimit => Tier switch { HoloPerformanceTier.Full => 64, HoloPerformanceTier.Balanced => 24, _ => 12, }; public bool EffectsAllowed => Tier != HoloPerformanceTier.Minimal; public double AverageFrameMs => _averageFrameMs; public HoloPerformanceGovernor(bool constrained) { _constrained = constrained; Tier = (constrained ? HoloPerformanceTier.Balanced : HoloPerformanceTier.Full); } public bool Sample(double frameMilliseconds) { if (!_constrained || double.IsNaN(frameMilliseconds) || double.IsInfinity(frameMilliseconds)) { return false; } double num = Math.Clamp(frameMilliseconds, 1.0, 100.0); _averageFrameMs = ((_averageFrameMs <= 0.0) ? num : (_averageFrameMs * 0.94 + num * 0.06)); if (_averageFrameMs >= 21.0) { _slowSamples++; _fastSamples = 0; } else if (_averageFrameMs <= 15.5) { _fastSamples++; _slowSamples = 0; } else { _slowSamples = Math.Max(0, _slowSamples - 1); _fastSamples = Math.Max(0, _fastSamples - 1); } if (Tier == HoloPerformanceTier.Balanced && _slowSamples >= 45) { Tier = HoloPerformanceTier.Minimal; _slowSamples = 0; return true; } if (Tier == HoloPerformanceTier.Minimal && _fastSamples >= 240) { Tier = HoloPerformanceTier.Balanced; _fastSamples = 0; return true; } return false; } } public enum InstalledPackFocusState { None, Downloading, WaitingForPallet, Ready, TimedOut } public sealed class InstalledPackFocus { public long ModId { get; private set; } public int Generation { get; private set; } public double DeadlineSeconds { get; private set; } public InstalledPackFocusState State { get; private set; } public string SelectedBarcode { get; private set; } = string.Empty; public IReadOnlyList InstallDirectories { get; private set; } = Array.Empty(); public bool Active => State != InstalledPackFocusState.None; public int Begin(long modId, double nowSeconds, IEnumerable? installDirectories = null, double timeoutSeconds = 12.0) { Generation++; ModId = modId; DeadlineSeconds = nowSeconds + Math.Max(1.0, timeoutSeconds); State = InstalledPackFocusState.WaitingForPallet; SelectedBarcode = string.Empty; InstallDirectories = (installDirectories ?? Array.Empty()).Where((string path) => !string.IsNullOrWhiteSpace(path)).Distinct(StringComparer.OrdinalIgnoreCase).ToArray(); return Generation; } public int BeginDownload(long modId) { Generation++; ModId = modId; DeadlineSeconds = double.PositiveInfinity; State = InstalledPackFocusState.Downloading; SelectedBarcode = string.Empty; InstallDirectories = Array.Empty(); return Generation; } public void MarkDownloaded(long modId, double nowSeconds, IEnumerable? installDirectories, double timeoutSeconds = 12.0) { if (modId == ModId && State != InstalledPackFocusState.None) { UpdateDirectories(modId, installDirectories); DeadlineSeconds = nowSeconds + Math.Max(1.0, timeoutSeconds); State = InstalledPackFocusState.WaitingForPallet; } } public void UpdateDirectories(long modId, IEnumerable? installDirectories) { if (modId == ModId && State != InstalledPackFocusState.None) { InstallDirectories = (installDirectories ?? Array.Empty()).Where((string path) => !string.IsNullOrWhiteSpace(path)).Distinct(StringComparer.OrdinalIgnoreCase).ToArray(); } } public bool TryBind(long modId, IEnumerable barcodes) { if (modId != ModId || State == InstalledPackFocusState.None) { return false; } string text = barcodes.FirstOrDefault((string value) => !string.IsNullOrWhiteSpace(value))?.Trim(); if (string.IsNullOrWhiteSpace(text)) { return false; } SelectedBarcode = text; State = InstalledPackFocusState.Ready; return true; } public bool UpdateTimeout(double nowSeconds) { if (State != InstalledPackFocusState.WaitingForPallet || nowSeconds < DeadlineSeconds) { return false; } State = InstalledPackFocusState.TimedOut; return true; } public void Retry(double nowSeconds, double timeoutSeconds = 12.0) { if (Active) { DeadlineSeconds = nowSeconds + Math.Max(1.0, timeoutSeconds); State = InstalledPackFocusState.WaitingForPallet; } } public void Clear() { ModId = 0L; DeadlineSeconds = 0.0; State = InstalledPackFocusState.None; SelectedBarcode = string.Empty; InstallDirectories = Array.Empty(); } } public sealed class LibraryRefreshGate { public bool Pending { get; private set; } public double NotBeforeSeconds { get; private set; } public LibraryRefreshGate(bool initiallyPending = true) { Pending = initiallyPending; } public void Request(double nowSeconds, double debounceSeconds = 0.35) { double num = nowSeconds + Math.Max(0.0, debounceSeconds); NotBeforeSeconds = (Pending ? Math.Max(NotBeforeSeconds, num) : num); Pending = true; } public bool TryConsume(double nowSeconds) { if (!Pending || nowSeconds < NotBeforeSeconds) { return false; } Pending = false; NotBeforeSeconds = 0.0; return true; } } public readonly record struct MiniMapOverlayColor(byte R, byte G, byte B, byte A = byte.MaxValue) { public static readonly MiniMapOverlayColor Outline = new MiniMapOverlayColor(0, 15, 24, 235); } public enum MiniMapPeerRole { Player, Operator, Owner } public static class MiniMapPeerColors { public static readonly MiniMapOverlayColor Cyan = new MiniMapOverlayColor(44, 220, byte.MaxValue); public static readonly MiniMapOverlayColor OperatorViolet = new MiniMapOverlayColor(190, 112, byte.MaxValue); public static readonly MiniMapOverlayColor OwnerAmber = new MiniMapOverlayColor(byte.MaxValue, 184, 46); public static readonly MiniMapOverlayColor Above = new MiniMapOverlayColor(117, byte.MaxValue, 184); public static readonly MiniMapOverlayColor Below = new MiniMapOverlayColor(92, 158, byte.MaxValue, 235); public static MiniMapOverlayColor Resolve(MiniMapPeerRole role, MiniMapAltitudeBand altitude) { return role switch { MiniMapPeerRole.Owner => OwnerAmber, MiniMapPeerRole.Operator => OperatorViolet, _ => altitude switch { MiniMapAltitudeBand.Above => Above, MiniMapAltitudeBand.Below => Below, _ => Cyan, }, }; } } public readonly record struct MiniMapOverlayMarker(Vector2 NormalizedPosition, float RotationDegrees, MiniMapOverlayColor Color, float Scale = 1f); public static class MiniMapPeerOverlayRaster { public const int DefaultTextureSize = 192; private const int ReferenceTextureSize = 96; private const int AntiAliasSamples = 3; public static int Render(Span rgba, int width, int height, ReadOnlySpan markers) { if (width <= 0 || height <= 0 || rgba.Length < width * height * 4) { throw new ArgumentOutOfRangeException("rgba"); } rgba.Slice(0, width * height * 4).Clear(); int written = 0; float num = MathF.Min(width, height) * 0.4f; float num2 = MathF.Min(width, height) / 96f; for (int i = 0; i < markers.Length; i++) { MiniMapOverlayMarker miniMapOverlayMarker = markers[i]; Vector2 normalizedPosition = miniMapOverlayMarker.NormalizedPosition; float num3 = normalizedPosition.Length(); if (float.IsFinite(num3)) { if (num3 > 1f && num3 > float.Epsilon) { normalizedPosition /= num3; } float centerX = (float)(width - 1) * 0.5f + normalizedPosition.X * num; float centerY = (float)(height - 1) * 0.5f + normalizedPosition.Y * num; float scale = Math.Clamp(float.IsFinite(miniMapOverlayMarker.Scale) ? miniMapOverlayMarker.Scale : 1f, 0.45f, 1.65f) * num2; DrawPlane(rgba, width, height, centerX, centerY, miniMapOverlayMarker.RotationDegrees, scale, miniMapOverlayMarker.Color, ref written); } } return written; } private static void DrawPlane(Span pixels, int width, int height, float centerX, float centerY, float rotationDegrees, float scale, MiniMapOverlayColor color, ref int written) { float x = rotationDegrees * (float)Math.PI / 180f; float cos = MathF.Cos(x); float sin = MathF.Sin(x); Vector2 a = Rotate(new Vector2(0f, 6.4f) * scale, cos, sin) + new Vector2(centerX, centerY); Vector2 b = Rotate(new Vector2(-5f, -4.8f) * scale, cos, sin) + new Vector2(centerX, centerY); Vector2 vector = Rotate(new Vector2(0f, -1.4f) * scale, cos, sin) + new Vector2(centerX, centerY); Vector2 c = Rotate(new Vector2(5f, -4.8f) * scale, cos, sin) + new Vector2(centerX, centerY); DrawTriangle(pixels, width, height, a, b, vector, color, ref written); DrawTriangle(pixels, width, height, a, vector, c, color, ref written); } private static Vector2 Rotate(Vector2 value, float cos, float sin) { return new Vector2(value.X * cos - value.Y * sin, value.X * sin + value.Y * cos); } private static void DrawTriangle(Span pixels, int width, int height, Vector2 a, Vector2 b, Vector2 c, MiniMapOverlayColor color, ref int written) { int num = Math.Clamp((int)MathF.Floor(MathF.Min(a.X, MathF.Min(b.X, c.X))), 0, width - 1); int num2 = Math.Clamp((int)MathF.Ceiling(MathF.Max(a.X, MathF.Max(b.X, c.X))), 0, width - 1); int num3 = Math.Clamp((int)MathF.Floor(MathF.Min(a.Y, MathF.Min(b.Y, c.Y))), 0, height - 1); int num4 = Math.Clamp((int)MathF.Ceiling(MathF.Max(a.Y, MathF.Max(b.Y, c.Y))), 0, height - 1); for (int i = num3; i <= num4; i++) { for (int j = num; j <= num2; j++) { int num5 = 0; for (int k = 0; k < 3; k++) { for (int l = 0; l < 3; l++) { if (InsideTriangle(new Vector2((float)j + ((float)l + 0.5f) / 3f, (float)i + ((float)k + 0.5f) / 3f), a, b, c)) { num5++; } } } if (num5 != 0) { int offset = (i * width + j) * 4; int sourceAlpha = (color.A * num5 + 4) / 9; BlendPixel(pixels, offset, color, sourceAlpha, ref written); } } } } private static bool InsideTriangle(Vector2 point, Vector2 a, Vector2 b, Vector2 c) { float num = Cross(b - a, point - a); float num2 = Cross(c - b, point - b); float num3 = Cross(a - c, point - c); if (!(num >= 0f) || !(num2 >= 0f) || !(num3 >= 0f)) { if (num <= 0f && num2 <= 0f) { return num3 <= 0f; } return false; } return true; } private static void BlendPixel(Span pixels, int offset, MiniMapOverlayColor color, int sourceAlpha, ref int written) { if (sourceAlpha > 0) { byte b = pixels[offset + 3]; if (b == 0) { written++; } int num = 255 - sourceAlpha; int num2 = sourceAlpha + (b * num + 127) / 255; if (num2 > 0) { pixels[offset] = BlendChannel(color.R, pixels[offset], sourceAlpha, b, num, num2); pixels[offset + 1] = BlendChannel(color.G, pixels[offset + 1], sourceAlpha, b, num, num2); pixels[offset + 2] = BlendChannel(color.B, pixels[offset + 2], sourceAlpha, b, num, num2); pixels[offset + 3] = (byte)num2; } } } private static byte BlendChannel(byte source, byte destination, int sourceAlpha, int destinationAlpha, int inverseAlpha, int outputAlpha) { return (byte)Math.Clamp((source * sourceAlpha + (destination * destinationAlpha * inverseAlpha + 127) / 255 + outputAlpha / 2) / outputAlpha, 0, 255); } private static float Cross(Vector2 left, Vector2 right) { return left.X * right.Y - left.Y * right.X; } } public enum MiniMapRange { TenMeters = 10, TwentyMeters = 20, TwentyFiveMeters = 25, ThirtyMeters = 30, FortyMeters = 40, FiftyMeters = 50 } public enum MiniMapOrientation { HeadingUp, NorthUp } public enum MiniMapMarkerKind { LocalPlayer, FusionPeer, Waypoint } public enum MiniMapAltitudeBand { Below, SameLevel, Above } public readonly record struct MiniMapMarker(string Id, string Label, MiniMapMarkerKind Kind, Vector2 WorldPosition, float HeadingDegrees, bool Valid = true); public sealed record MiniMapSnapshot(float HeadingDegrees, MiniMapRange Range, MiniMapOrientation Orientation, IReadOnlyList Markers, IReadOnlyList GeometryPoints); public readonly record struct GeometryScanBudget(int RayCount, double RefreshSeconds, bool Enabled) { public static GeometryScanBudget Resolve(bool constrained, HoloPerformanceTier tier) { return tier switch { HoloPerformanceTier.Minimal => new GeometryScanBudget(0, 1.0, Enabled: false), HoloPerformanceTier.Balanced => new GeometryScanBudget(constrained ? 24 : 40, constrained ? 0.5 : 0.3, Enabled: true), _ => new GeometryScanBudget(constrained ? 36 : 72, constrained ? 0.35 : 0.2, Enabled: true), }; } } public static class MiniMapMath { public static float Normalize(float degrees) { float num = degrees % 360f; if (!(num < 0f)) { return num; } return num + 360f; } public static Vector2 Project(Vector2 worldOffset, float headingDegrees, MiniMapOrientation orientation, MiniMapRange range) { float num = Math.Max(1f, (float)range); Vector2 vector = worldOffset / num; if (orientation == MiniMapOrientation.HeadingUp) { float x = headingDegrees * (float)Math.PI / 180f; float num2 = MathF.Cos(x); float num3 = MathF.Sin(x); vector = new Vector2(vector.X * num2 - vector.Y * num3, vector.X * num3 + vector.Y * num2); } float num4 = vector.Length(); if (!(num4 <= 1f) && !(num4 <= float.Epsilon)) { return vector / num4; } return vector; } public static string Cardinal(float headingDegrees) { return CompassHeadingMath.Cardinal(Normalize(headingDegrees)); } public static MiniMapRange StepRange(MiniMapRange current, int direction) { int num = Math.Clamp((int)current, 10, 50); if (num == 25) { num = ((direction < 0) ? 30 : 20); } return (MiniMapRange)Math.Clamp(num + Math.Sign(direction) * 10, 10, 50); } public static float MarkerRotation(float markerHeadingDegrees, float playerHeadingDegrees, MiniMapOrientation orientation) { return 0f - Normalize((orientation == MiniMapOrientation.HeadingUp) ? (markerHeadingDegrees - playerHeadingDegrees) : markerHeadingDegrees); } public static MiniMapAltitudeBand AltitudeBand(float deltaMeters, float thresholdMeters = 1.5f) { if (!float.IsFinite(deltaMeters)) { return MiniMapAltitudeBand.SameLevel; } float num = Math.Clamp(Math.Abs(thresholdMeters), 0.25f, 10f); if (!(deltaMeters > num)) { if (!(deltaMeters < 0f - num)) { return MiniMapAltitudeBand.SameLevel; } return MiniMapAltitudeBand.Below; } return MiniMapAltitudeBand.Above; } public static string DistanceLabel(float meters) { if (!float.IsFinite(meters) || meters < 0f) { return "-- m"; } if (meters < 10f) { return $"{meters:0.0} m"; } return $"{MathF.Round(meters):0} m"; } } public readonly record struct TouchBounds(float CenterX, float CenterY, float CenterZ, float HalfX, float HalfY, float HalfZ) { public bool Contains(float x, float y, float z) { if (Math.Abs(x - CenterX) <= Math.Max(0f, HalfX) && Math.Abs(y - CenterY) <= Math.Max(0f, HalfY)) { return Math.Abs(z - CenterZ) <= Math.Max(0f, HalfZ); } return false; } } public sealed class TouchDebounce { public bool Latched { get; private set; } public bool Update(bool touching) { if (!touching) { Latched = false; return false; } if (Latched) { return false; } Latched = true; return true; } } public sealed class ErrorLogThrottle { private readonly Dictionary _lastLogged = new Dictionary(StringComparer.Ordinal); public bool ShouldLog(string signature, double nowSeconds, double intervalSeconds = 5.0) { if (string.IsNullOrWhiteSpace(signature)) { signature = "unknown"; } if (_lastLogged.TryGetValue(signature, out var value) && nowSeconds - value < Math.Max(0.1, intervalSeconds)) { return false; } _lastLogged[signature] = nowSeconds; if (_lastLogged.Count > 64) { double cutoff = nowSeconds - Math.Max(30.0, intervalSeconds * 4.0); string[] array = (from item in _lastLogged where item.Value < cutoff select item.Key).ToArray(); foreach (string key in array) { _lastLogged.Remove(key); } } return true; } } public enum PortalLinkKind { LocalPair, WorldGate, FusionServerGate } public enum PortalLifecycle { Placement, Charging, Opening, Stable, Entering, Rejected, Collapsing, Hidden } public readonly record struct PortalEndpoint(Vector3 Position, Quaternion Rotation) { public Vector3 Forward => Vector3.Transform(Vector3.UnitZ, Rotation); public Vector3 Up => Vector3.Transform(Vector3.UnitY, Rotation); public Vector3 Right => Vector3.Transform(Vector3.UnitX, Rotation); public bool IsValid { get { if (PortalMath.IsFinite(Position) && PortalMath.IsFinite(Rotation)) { return Rotation.LengthSquared() > 0.5f; } return false; } } } public sealed record PortalLinkState(int Generation, PortalLinkKind Kind, string SourceScene, PortalEndpoint Entrance, PortalEndpoint? Exit, string DestinationLevelBarcode, string DestinationLoadingScreenBarcode, string DestinationTitle, float Diameter, ulong Owner, PortalLifecycle Lifecycle) { public string DestinationServerCode { get; init; } = string.Empty; public ulong DestinationServerLobbyId { get; init; } public bool IsValid { get { bool flag = Generation > 0 && !string.IsNullOrWhiteSpace(SourceScene) && Entrance.IsValid; if (flag) { flag = Kind switch { PortalLinkKind.WorldGate => !string.IsNullOrWhiteSpace(DestinationLevelBarcode), PortalLinkKind.FusionServerGate => FusionServerEntry.IsValidCode(DestinationServerCode), _ => Exit?.IsValid ?? false, }; } if (flag && float.IsFinite(Diameter)) { float diameter = Diameter; if (diameter >= 2.2f) { return diameter <= 5f; } return false; } return false; } } } public sealed record FusionServerPlayer(string Name, string Role, string AvatarTitle, long AvatarModId, ulong PlatformId = 0uL); public sealed record FusionServerEntry(string Code, string Name, string Host, string Level, int Players, int MaximumPlayers, bool Full, bool HasLevel, long LevelModId = -1L, string LevelBarcode = "", string Gamemode = "Sandbox", string Version = "", string Description = "", IReadOnlyList? MemberNames = null, IReadOnlyList? PlayerDetails = null, ulong LobbyId = 0uL) { public bool IsValid { get { if (IsValidCode(Code) && !string.IsNullOrWhiteSpace(Name) && Players >= 0 && MaximumPlayers > 0) { return Players <= MaximumPlayers; } return false; } } public string PopulationLabel => $"{Players} / {MaximumPlayers}"; public IReadOnlyList Members => MemberNames ?? Array.Empty(); public IReadOnlyList DetailedPlayers => PlayerDetails ?? Array.Empty(); public static bool IsValidCode(string? value) { if (!string.IsNullOrWhiteSpace(value) && value.Length <= 16) { return value.All(delegate(char character) { bool flag = char.IsLetterOrDigit(character); if (!flag) { bool flag2 = ((character == '-' || character == '_') ? true : false); flag = flag2; } return flag; }); } return false; } public static bool TryNormalizeCode(string? value, out string normalized) { normalized = (value ?? string.Empty).Trim().ToUpperInvariant(); if (IsValidCode(normalized)) { return true; } normalized = string.Empty; return false; } } public sealed record PortalDestinationEntry(long ModId, string LevelBarcode, string Name, string Pack, string Creator, string ArtworkUrl, long DownloadSize, bool Installed, bool WindowsAvailable, bool AndroidAvailable) { public string PlatformLabel { get { if (!WindowsAvailable || !AndroidAvailable) { if (!WindowsAvailable) { if (!AndroidAvailable) { return "NO COMPATIBLE BUILD"; } return "QUEST ONLY"; } return "PC ONLY"; } return "PC + QUEST"; } } } public readonly record struct PortalCrossingResult(bool Crossed, Vector3 Position, Quaternion Rotation); public static class PortalMath { public const float MinimumDiameter = 2.2f; public const float MaximumDiameter = 5f; public const float ExitClearance = 0.35f; public static float ResolveDiameter(float largestAvatarHeight) { return Math.Clamp((float.IsFinite(largestAvatarHeight) && largestAvatarHeight > 0f) ? (largestAvatarHeight * 1.35f) : 2.2f, 2.2f, 5f); } public static PortalEndpoint ResolvePlacement(Vector3 point, Vector3 surfaceNormal, Vector3 hostForward, bool floor, float diameter = 2.2f) { Vector3 unitY = Vector3.UnitY; Vector3 vector = (floor ? HorizontalOrDefault(-hostForward, Vector3.UnitZ) : HorizontalOrDefault(surfaceNormal, -hostForward)); Vector3 vector2 = Vector3.Normalize(Vector3.Cross(unitY, vector)); vector = Vector3.Normalize(Vector3.Cross(vector2, unitY)); Vector3 position = point + vector * 0.035f; if (floor) { position += unitY * Math.Clamp(diameter, 2.2f, 5f) * 0.5f; } return new PortalEndpoint(position, QuaternionFromBasis(vector2, unitY, vector)); } public static bool IsApertureCrossing(PortalEndpoint portal, float diameter, Vector3 previous, Vector3 current) { if (!portal.IsValid || !IsFinite(previous) || !IsFinite(current)) { return false; } float num = Vector3.Dot(previous - portal.Position, portal.Forward); float num2 = Vector3.Dot(current - portal.Position, portal.Forward); if (num == 0f || num2 == 0f || MathF.Sign(num) == MathF.Sign(num2)) { return false; } float num3 = num / (num - num2); if ((num3 < 0f || num3 > 1f) ? true : false) { return false; } Vector3 vector = Vector3.Lerp(previous, current, num3) - portal.Position; float num4 = Vector3.Dot(vector, portal.Right); float num5 = Vector3.Dot(vector, portal.Up); float num6 = Math.Clamp(diameter, 2.2f, 5f) * 0.5f; return num4 * num4 + num5 * num5 <= num6 * num6; } public static bool IsInsideAperture(PortalEndpoint portal, float diameter, Vector3 position, float halfDepth) { if (!portal.IsValid || !IsFinite(position) || !float.IsFinite(halfDepth) || halfDepth < 0f) { return false; } Vector3 vector = position - portal.Position; if (MathF.Abs(Vector3.Dot(vector, portal.Forward)) > halfDepth) { return false; } float num = Vector3.Dot(vector, portal.Right); float num2 = Vector3.Dot(vector, portal.Up); float num3 = Math.Clamp(diameter, 2.2f, 5f) * 0.5f; return num * num + num2 * num2 <= num3 * num3; } public static PortalCrossingResult MapBetween(PortalEndpoint entry, PortalEndpoint exit, Vector3 position, Quaternion rotation) { if (!entry.IsValid || !exit.IsValid || !IsFinite(position) || !IsFinite(rotation)) { return default(PortalCrossingResult); } Quaternion quaternion = Quaternion.Inverse(entry.Rotation); Quaternion quaternion2 = Quaternion.CreateFromAxisAngle(Vector3.UnitY, (float)Math.PI); Vector3 value = Vector3.Transform(position - entry.Position, quaternion); value = Vector3.Transform(value, quaternion2); Vector3 position2 = exit.Position + Vector3.Transform(value, exit.Rotation) + exit.Forward * 0.35f; Quaternion rotation2 = Quaternion.Normalize(exit.Rotation * quaternion2 * quaternion * rotation); return new PortalCrossingResult(Crossed: true, position2, rotation2); } public static bool IsFinite(Vector3 value) { if (float.IsFinite(value.X) && float.IsFinite(value.Y)) { return float.IsFinite(value.Z); } return false; } public static bool IsFinite(Quaternion value) { if (float.IsFinite(value.X) && float.IsFinite(value.Y) && float.IsFinite(value.Z)) { return float.IsFinite(value.W); } return false; } private static Vector3 HorizontalOrDefault(Vector3 value, Vector3 fallback) { value.Y = 0f; if (value.LengthSquared() < 0.0001f) { value = fallback; } value.Y = 0f; return Vector3.Normalize(value); } private static Quaternion QuaternionFromBasis(Vector3 right, Vector3 up, Vector3 forward) { return Quaternion.Normalize(Quaternion.CreateFromRotationMatrix(new Matrix4x4(right.X, right.Y, right.Z, 0f, up.X, up.Y, up.Z, 0f, forward.X, forward.Y, forward.Z, 0f, 0f, 0f, 0f, 1f))); } } public sealed class PortalTraversalGuard { private Vector3 _previous; private bool _hasPrevious; private double _cooldownUntil; public void Reset() { _hasPrevious = false; _cooldownUntil = 0.0; } public bool TryCross(PortalEndpoint endpoint, float diameter, Vector3 currentPosition, double nowSeconds) { if (!_hasPrevious) { _previous = currentPosition; _hasPrevious = true; return false; } bool flag = PortalMath.IsApertureCrossing(endpoint, diameter, _previous, currentPosition); bool flag2 = PortalMath.IsInsideAperture(endpoint, diameter, currentPosition, 0.16f) && !PortalMath.IsInsideAperture(endpoint, diameter, _previous, 0.16f) && Vector3.DistanceSquared(_previous, currentPosition) > 0.0004f; bool num = nowSeconds >= _cooldownUntil && (flag || flag2); _previous = currentPosition; if (num) { _cooldownUntil = nowSeconds + 0.8; } return num; } } public static class PreviewShellRecovery { public static bool RequiresRebuild(bool shellAlive, bool fallbackAlive) { if (shellAlive) { return !fallbackAlive; } return true; } public static bool ShouldSchedule(string? selectedBarcode, string? loadedBarcode, string? requestedBarcode) { if (string.IsNullOrWhiteSpace(selectedBarcode)) { return false; } if (!string.Equals(selectedBarcode, loadedBarcode, StringComparison.OrdinalIgnoreCase)) { return !string.Equals(selectedBarcode, requestedBarcode, StringComparison.OrdinalIgnoreCase); } return false; } } public enum SmartWatchState { Sleeping, GazeWaking, Awake, Pressed, Projecting, MenuOpen, TrackingGrace } public enum WatchStatusTone { Normal, Downloading, Success, Failure } public enum WristHubModuleDestination { Avatars, Search, Downloads, Compass } public readonly record struct WatchFacePresentation(string Time, string Date, string Status, float Progress, WatchStatusTone Tone, double AwakeDeadline, bool TouchReady); public static class CircularWatchTouchMath { public const float FaceRadiusMeters = 0.0325f; public const float TouchRadiusMeters = 0.036f; public const float TouchHalfDepthMeters = 0.014f; public static bool Contains(float localX, float localNormal, float localZ, float scale = 1f) { float num = ((float.IsFinite(scale) && scale > 0f) ? scale : 1f); float num2 = 0.036f * num; if (localX * localX + localZ * localZ <= num2 * num2) { return Math.Abs(localNormal) <= 0.014f * num; } return false; } } public enum HologramDashboardLifecycle { Hidden, Opening, Stable, Closing } public enum HologramProjectionPhase { WatchCharge, RayBuild, StageBuild, WingReveal, AvatarReveal, Stable, WingCollapse, RayCollapse, Hidden } public readonly record struct HologramDashboardProgress(HologramProjectionPhase Phase, float Rays, float Stage, float Wings, float Avatars, float Labels, bool ControlsReady); public static class HologramDashboardTimeline { public const double OpeningSeconds = 0.55; public const double ClosingSeconds = 0.35; public static HologramDashboardProgress Opening(double elapsed, bool reducedMotion) { if (reducedMotion) { return new HologramDashboardProgress(HologramProjectionPhase.Stable, 1f, 1f, 1f, 1f, 1f, ControlsReady: true); } double num = Math.Max(0.0, elapsed); return new HologramDashboardProgress((!(num < 0.12)) ? ((num < 0.25) ? HologramProjectionPhase.RayBuild : ((num < 0.36) ? HologramProjectionPhase.StageBuild : ((num < 0.45) ? HologramProjectionPhase.WingReveal : ((num < 0.55) ? HologramProjectionPhase.AvatarReveal : HologramProjectionPhase.Stable)))) : HologramProjectionPhase.WatchCharge, Smooth((num - 0.06) / 0.2), Smooth((num - 0.18) / 0.19), Smooth((num - 0.31) / 0.15), Smooth((num - 0.38) / 0.17), Smooth((num - 0.43) / 0.12), num >= 0.55); } public static HologramDashboardProgress Closing(double elapsed, bool reducedMotion) { if (reducedMotion) { return new HologramDashboardProgress(HologramProjectionPhase.Hidden, 0f, 0f, 0f, 0f, 0f, ControlsReady: false); } double num = Math.Clamp(Math.Max(0.0, elapsed) / 0.35, 0.0, 1.0); float wings = 1f - Smooth(num / 0.62); float rays = 1f - Smooth((num - 0.42) / 0.58); float stage = 1f - Smooth((num - 0.18) / 0.7); float avatars = 1f - Smooth(num / 0.48); float labels = 1f - Smooth(num / 0.34); return new HologramDashboardProgress((num < 0.62) ? HologramProjectionPhase.WingCollapse : ((num < 1.0) ? HologramProjectionPhase.RayCollapse : HologramProjectionPhase.Hidden), rays, stage, wings, avatars, labels, ControlsReady: false); } private static float Smooth(double value) { double num = Math.Clamp(value, 0.0, 1.0); return (float)(num * num * (3.0 - 2.0 * num)); } } public static class SmartWatchWakeMath { public const double GazeSeconds = 1.0; public const double AwakeHoldSeconds = 5.0; public const double ProjectionSeconds = 0.55; public static SmartWatchState Resolve(bool menuOpen, bool projecting, bool pressed, bool observed, double observedFor, double now, double awakeDeadline) { if (menuOpen) { return SmartWatchState.MenuOpen; } if (projecting) { return SmartWatchState.Projecting; } if (pressed) { return SmartWatchState.Pressed; } if (observedFor > 0.0 && observedFor < 1.0) { return SmartWatchState.GazeWaking; } if (observed || now < awakeDeadline || observedFor >= 1.0) { return SmartWatchState.Awake; } return SmartWatchState.Sleeping; } public static double ExtendDeadline(bool observed, double observedFor, double now, double currentDeadline) { if (!observed || !(observedFor >= 1.0)) { return currentDeadline; } return Math.Max(currentDeadline, now + 5.0); } } public readonly record struct ModuleProjectionProgress(float Charge, float Rings, float Rays, float Controls, bool ControlsReady); public static class ModuleProjectionTimeline { public static ModuleProjectionProgress Resolve(double elapsed, bool reducedMotion) { if (reducedMotion) { return new ModuleProjectionProgress(1f, 1f, 1f, 1f, ControlsReady: true); } double num = Math.Max(0.0, elapsed); float charge = Smooth(num / 0.15); float rings = Smooth((num - 0.08) / 0.24); float rays = Smooth((num - 0.2) / 0.23); float controls = Smooth((num - 0.32) / 0.23); return new ModuleProjectionProgress(charge, rings, rays, controls, num >= 0.55); } private static float Smooth(double value) { double num = Math.Clamp(value, 0.0, 1.0); return (float)(num * num * (3.0 - 2.0 * num)); } } public sealed record SpawnableEntry(string Barcode, string Name, string Pack, string Creator, string Description, bool Compatible = true) { public string Identity => "spawnable:" + Barcode; } public enum SpawnPlacementState { None, Preview, Held, Ready, Spawning, Failed } public sealed record SpawnPlacementSession(string Barcode, SpawnPlacementState State, Vector3 Position, float YawDegrees, float DistanceMeters, bool SurfaceValid, string? Error = null); public static class SpawnPlacementMath { public const float MinimumDistance = 0.45f; public const float MaximumDistance = 6f; public static float ClampDistance(float meters) { return Math.Clamp(meters, 0.45f, 6f); } public static float NormalizeYaw(float degrees) { return MiniMapMath.Normalize(degrees); } public static float Rotate(float currentDegrees, float deltaDegrees) { return NormalizeYaw(currentDegrees + deltaDegrees); } public static bool TriggerHeld(float axis, bool wasHeld) { if (float.IsFinite(axis)) { return axis >= (wasHeld ? 0.25f : 0.72f); } return false; } public static IReadOnlyList Filter(IEnumerable entries, string? query) { string text = query?.Trim() ?? string.Empty; return entries.Where((SpawnableEntry item) => text.Length == 0 || item.Name.Contains(text, StringComparison.OrdinalIgnoreCase) || item.Pack.Contains(text, StringComparison.OrdinalIgnoreCase) || item.Creator.Contains(text, StringComparison.OrdinalIgnoreCase)).OrderBy((SpawnableEntry item) => item.Name, StringComparer.OrdinalIgnoreCase).ToArray(); } } public readonly record struct WristActivationState(bool Armed, double RingProgress, bool Activate); public static class WristActivationMath { public const double ActivationSeconds = 1.0; public static WristActivationState Evaluate(double observedSeconds, double armSeconds) { double num = Math.Max(0.0, observedSeconds); double num2 = ((double.IsFinite(armSeconds) && armSeconds > 0.0) ? armSeconds : 1.0); double num3 = Math.Clamp(num / num2, 0.0, 1.0); return new WristActivationState(num > 0.0, num3, num3 >= 1.0); } } public sealed class WristRearmGate { public const double RequiredLookAwaySeconds = 0.25; private double _lookAwaySeconds; public bool RequiresLookAway { get; private set; } public void RequireLookAway() { RequiresLookAway = true; _lookAwaySeconds = 0.0; } public bool Update(bool observed, double deltaSeconds) { if (!RequiresLookAway) { return true; } if (observed) { _lookAwaySeconds = 0.0; } else { _lookAwaySeconds += Math.Max(0.0, deltaSeconds); } if (_lookAwaySeconds >= 0.25) { RequiresLookAway = false; _lookAwaySeconds = 0.0; } return !RequiresLookAway; } } public enum WristOsAppId { Avatars, Fusion, CodeMods, MiniMap, Compass, Portals } public enum WristOsRouteState { Hidden, Opening, Home, App, SystemDrawer, Closing, TrackingGrace } public readonly record struct WristOsNotification(string Source, string Message, bool IsError = false); public interface IWristOsApp { WristOsAppId Id { get; } string DisplayName { get; } bool IsAvailable { get; } int NotificationCount { get; } void Open(); void Close(); void Tick(double nowSeconds); bool HandleBack(); } public sealed class WristOsRouter { private readonly Dictionary _apps = new Dictionary(); public WristOsRouteState State { get; private set; } public WristOsAppId? ActiveApp { get; private set; } public string? ActiveDrawer { get; private set; } public IReadOnlyCollection Apps => _apps.Values; public void Register(IWristOsApp app) { ArgumentNullException.ThrowIfNull(app, "app"); _apps[app.Id] = app; } public void BeginOpen() { State = WristOsRouteState.Opening; } public void ShowHome() { CloseActiveApp(); ActiveDrawer = null; State = WristOsRouteState.Home; } public bool OpenApp(WristOsAppId id) { if (!_apps.TryGetValue(id, out IWristOsApp value) || !value.IsAvailable) { return false; } CloseActiveApp(); ActiveDrawer = null; ActiveApp = id; value.Open(); State = WristOsRouteState.App; return true; } public void OpenDrawer(string id) { CloseActiveApp(); ActiveDrawer = (string.IsNullOrWhiteSpace(id) ? "system" : id.Trim()); State = WristOsRouteState.SystemDrawer; } public bool Back() { if (State == WristOsRouteState.App) { WristOsAppId? activeApp = ActiveApp; if (activeApp.HasValue) { WristOsAppId valueOrDefault = activeApp.GetValueOrDefault(); if (_apps.TryGetValue(valueOrDefault, out IWristOsApp value) && value.HandleBack()) { return true; } } } ShowHome(); return false; } public void BeginClose() { CloseActiveApp(); ActiveDrawer = null; State = WristOsRouteState.Closing; } public void Hide() { CloseActiveApp(); ActiveDrawer = null; State = WristOsRouteState.Hidden; } public void Tick(double nowSeconds) { if (State != WristOsRouteState.App) { return; } WristOsAppId? activeApp = ActiveApp; if (activeApp.HasValue) { WristOsAppId valueOrDefault = activeApp.GetValueOrDefault(); if (_apps.TryGetValue(valueOrDefault, out IWristOsApp value)) { value.Tick(nowSeconds); } } } private void CloseActiveApp() { WristOsAppId? activeApp = ActiveApp; if (activeApp.HasValue) { WristOsAppId valueOrDefault = activeApp.GetValueOrDefault(); if (_apps.TryGetValue(valueOrDefault, out IWristOsApp value)) { value.Close(); } } ActiveApp = null; } } } namespace BoneHub.Installation { public static class NetworkerRegistrationWriter { private sealed record PalletInfo(string Barcode, string Version, string Author); private const long GameId = 3809L; private static readonly JsonSerializerOptions JsonOptions = new JsonSerializerOptions { WriteIndented = true }; public static async Task WriteAsync(ModIoMod mod, ModIoFile installedFile, ModIoFileResolver.PlatformFiles platformFiles, IReadOnlyList installedDirectories, string modsDirectory, CancellationToken cancellationToken = default(CancellationToken)) { string root = Path.GetFullPath(modsDirectory).TrimEnd(Path.DirectorySeparatorChar); List sidecars = new List(); List pallets = new List(); foreach (string installedDirectory in installedDirectories) { string package = ValidateChild(installedDirectory, root); await WriteModInfoAsync(package, mod, installedFile, platformFiles, cancellationToken).ConfigureAwait(continueOnCapturedContext: false); foreach (string palletPath in PalletManifestLocator.FindAll(package, SearchOption.TopDirectoryOnly)) { cancellationToken.ThrowIfCancellationRequested(); PalletInfo pallet = await ReadPalletAsync(palletPath, cancellationToken).ConfigureAwait(continueOnCapturedContext: false); string manifestPath = Path.Combine(root, SafeFileName(pallet.Barcode) + ".manifest"); string catalogPath = Directory.EnumerateFiles(package, "catalog_*.json", SearchOption.TopDirectoryOnly).FirstOrDefault() ?? Path.Combine(package, "catalog_" + pallet.Barcode + ".json"); JsonObject value = BuildManifest(pallet, palletPath, catalogPath, mod, installedFile, platformFiles); await WriteAtomicAsync(manifestPath, value, cancellationToken).ConfigureAwait(continueOnCapturedContext: false); sidecars.Add(manifestPath); pallets.Add(pallet.Barcode); } } string warning = ((platformFiles.Windows == null || platformFiles.Android == null) ? ("This pack has no active file for " + ((platformFiles.Windows == null) ? "PC" : "Quest") + "; Fusion sharing is disabled for that platform.") : null); return new ModIoSharingMetadata { RegistrationVersion = 3, ModId = mod.Id, ModSlug = NonEmpty(mod.NameId, SafeSlug(mod.Name, mod.Id)), WindowsFileId = platformFiles.Windows?.Id, AndroidFileId = platformFiles.Android?.Id, SidecarManifestPaths = sidecars, PalletBarcodes = pallets, Warning = warning }; } private static JsonObject BuildManifest(PalletInfo pallet, string palletPath, string catalogPath, ModIoMod mod, ModIoFile installedFile, ModIoFileResolver.PlatformFiles files) { JsonObject jsonObject = new JsonObject(); int num = 3; string text = null; string text2 = null; JsonObject jsonObject2 = new JsonObject(); if (files.Windows != null) { text = num++.ToString(); jsonObject["pc"] = Ref(text, "mod-target-modio#0"); } if (files.Android != null) { text2 = num++.ToString(); jsonObject["android"] = Ref(text2, "mod-target-modio#0"); } string text3 = text ?? text2; if (text3 != null) { jsonObject[NetworkerKey(mod, installedFile)] = Ref(text3, "mod-target-modio#0"); } string text4 = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds().ToString(); jsonObject2["1"] = new JsonObject { ["palletBarcode"] = pallet.Barcode, ["palletPath"] = Path.GetFullPath(palletPath), ["catalogPath"] = Path.GetFullPath(catalogPath), ["version"] = NonEmpty(pallet.Version, "0.0.0"), ["installedDate"] = text4, ["updateDate"] = text4, ["modListing"] = Ref("2", "mod-listing#0"), ["active"] = true, ["isa"] = Type("pallet-manifest#0") }; jsonObject2["2"] = new JsonObject { ["barcode"] = pallet.Barcode, ["title"] = NonEmpty(mod.NameId, SafeSlug(mod.Name, mod.Id)), ["description"] = NonEmpty(mod.Summary, string.Empty), ["author"] = NonEmpty(mod.Author.DisplayName, NonEmpty(pallet.Author, "Unknown")), ["version"] = NonEmpty(installedFile.Version, "0.0.0"), ["thumbnailUrl"] = NonEmpty(mod.Logo.Thumbnail, string.Empty), ["targets"] = jsonObject, ["isa"] = Type("mod-listing#0") }; if (files.Windows != null && text != null) { jsonObject2[text] = Target(mod.Id, files.Windows.Id); } if (files.Android != null && text2 != null) { jsonObject2[text2] = Target(mod.Id, files.Android.Id); } return new JsonObject { ["version"] = 2, ["root"] = Ref("1", "pallet-manifest#0"), ["objects"] = jsonObject2 }; } private static async Task WriteModInfoAsync(string package, ModIoMod mod, ModIoFile installedFile, ModIoFileResolver.PlatformFiles files, CancellationToken cancellationToken) { JsonArray value = new JsonArray(((IEnumerable)mod.Tags).Select((Func)((ModIoTag tag) => Sanitize(tag.Name))).ToArray()); JsonObject value2 = new JsonObject { ["isValidMod"] = true, ["downloading"] = false, ["mature"] = mod.MaturityOption != 0, ["isTracked"] = true, ["modId"] = NonEmpty(mod.NameId, SafeSlug(mod.Name, mod.Id)), ["thumbnailLink"] = NonEmpty(mod.Logo.Thumbnail, string.Empty), ["modName"] = NonEmpty(mod.Name, "Avatar Pack"), ["modSummary"] = NonEmpty(mod.Summary, string.Empty), ["fileName"] = NonEmpty(installedFile.Filename, "modfile.zip"), ["windowsDownloadLink"] = files.Windows?.Download.BinaryUrl ?? "nothing", ["androidDownloadLink"] = files.Android?.Download.BinaryUrl ?? "nothing", ["directDownloadLink"] = "nothing", ["modDownloadPercentage"] = 100, ["numericalId"] = mod.Id.ToString(), ["version"] = NonEmpty(installedFile.Version, "0.0.0"), ["tags"] = value, ["author"] = NonEmpty(mod.Author.DisplayName, "Unknown"), ["structureVersion"] = 4, ["temp"] = false }; await WriteAtomicAsync(Path.Combine(package, "modinfo.json"), value2, cancellationToken).ConfigureAwait(continueOnCapturedContext: false); } private static async Task ReadPalletAsync(string path, CancellationToken cancellationToken) { JsonNode? obj = JsonNode.Parse(await File.ReadAllTextAsync(path, cancellationToken).ConfigureAwait(continueOnCapturedContext: false)) ?? throw new InvalidDataException("The pallet manifest is empty."); string propertyName = obj["root"]?["ref"]?.GetValue() ?? "1"; JsonNode jsonNode = obj["objects"]?[propertyName] ?? throw new InvalidDataException("The pallet root is missing."); string obj2 = jsonNode["barcode"]?.GetValue()?.Trim(); if (string.IsNullOrWhiteSpace(obj2)) { throw new InvalidDataException("The pallet barcode is missing."); } return new PalletInfo(obj2, jsonNode["version"]?.GetValue() ?? "0.0.0", jsonNode["author"]?.GetValue() ?? "Unknown"); } private static string NetworkerKey(ModIoMod mod, ModIoFile file) { string[] array = (from tag in mod.Tags select Sanitize(tag.Name) into tag where tag.Length > 0 select tag).ToArray(); return $"networker;{mod.MaturityOption != 0};False;{file.FileSize};{Sanitize(file.Filename)};4;{Sanitize(mod.Name)};{array.Length}" + ((array.Length == 0) ? string.Empty : (";" + string.Join(";", array))); } private static JsonObject Ref(string value, string type) { return new JsonObject { ["ref"] = value, ["type"] = type }; } private static JsonObject Type(string type) { return new JsonObject { ["type"] = type }; } private static JsonObject Target(long modId, long fileId) { return new JsonObject { ["thumbnailOverride"] = string.Empty, ["gameId"] = 3809L, ["modId"] = modId, ["modfileId"] = fileId, ["isa"] = Type("mod-target-modio#0") }; } private static string NonEmpty(string? value, string fallback) { if (!string.IsNullOrWhiteSpace(value)) { return value.Trim(); } return fallback; } private static string SafeSlug(string? name, long modId) { string text = new string((from character in (name ?? string.Empty).Trim().ToLowerInvariant() select (!char.IsLetterOrDigit(character)) ? '-' : character).ToArray()).Trim('-'); if (text.Length != 0) { return text; } return "wristhub-mod-" + modId; } private static string Sanitize(string? value) { return (value ?? string.Empty).Replace(';', ' ').Trim(); } private static string SafeFileName(string value) { return string.Concat(value.Select((char character) => (!Path.GetInvalidFileNameChars().Contains(character)) ? character : '_')); } private static string ValidateChild(string path, string root) { string text = Path.GetFullPath(path).TrimEnd(Path.DirectorySeparatorChar); if (!text.StartsWith(root + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase)) { throw new InvalidDataException("An installed directory is outside BONELAB's Mods directory."); } return text; } private static async Task WriteAtomicAsync(string path, JsonNode value, CancellationToken cancellationToken) { string temp = path + ".wristhub.tmp"; try { await File.WriteAllTextAsync(temp, value.ToJsonString(JsonOptions), cancellationToken).ConfigureAwait(continueOnCapturedContext: false); File.Move(temp, path, overwrite: true); } finally { if (File.Exists(temp)) { File.Delete(temp); } } } } public static class PalletManifestLocator { public static bool IsManifestFileName(string? fileName) { if (string.IsNullOrWhiteSpace(fileName)) { return false; } if (!fileName.Equals("pallet.json", StringComparison.OrdinalIgnoreCase)) { return fileName.EndsWith(".pallet.json", StringComparison.OrdinalIgnoreCase); } return true; } public static IReadOnlyList FindAll(string directory, SearchOption searchOption) { if (!Directory.Exists(directory)) { return Array.Empty(); } return (from path in Directory.EnumerateFiles(directory, "*.json", searchOption) where IsManifestFileName(Path.GetFileName(path)) select path).Distinct(StringComparer.OrdinalIgnoreCase).ToArray(); } public static bool HasManifest(string directory) { return FindAll(directory, SearchOption.TopDirectoryOnly).Count > 0; } } public sealed class SafeModInstaller { private static readonly HashSet BlockedExtensions = new HashSet(StringComparer.OrdinalIgnoreCase) { ".exe", ".dll", ".com", ".bat", ".cmd", ".ps1", ".vbs", ".js", ".jar", ".sh", ".so", ".dylib" }; private readonly BoneHubOptions _options; public SafeModInstaller(BoneHubOptions options) { _options = options; } public async Task> InstallAsync(string zipPath, long modId, CancellationToken cancellationToken = default(CancellationToken)) { string modsRoot = EnsureDirectory(_options.ModsDirectory); string path = $"{modId}-{Guid.NewGuid():N}"; string? directoryName = Path.GetDirectoryName(modsRoot.TrimEnd(Path.DirectorySeparatorChar)); string text = Path.Combine(directoryName, ".bonehub-staging"); string text2 = Path.Combine(directoryName, ".bonehub-backups"); RecoverInterruptedTransactions(modsRoot, text2); DeleteStaleStaging(text); string stagingRoot = Path.Combine(text, path); string backupRoot = Path.Combine(text2, path); Directory.CreateDirectory(stagingRoot); try { await ExtractValidatedAsync(zipPath, stagingRoot, cancellationToken).ConfigureAwait(continueOnCapturedContext: false); List list = FindPackageRoots(stagingRoot); if (list.Count == 0) { throw new InvalidDataException("The archive has no pallet manifest (pallet.json or *.pallet.json) and is not a BONELAB content mod."); } Directory.CreateDirectory(backupRoot); List list2 = new List(); List<(string, string)> list3 = new List<(string, string)>(); try { foreach (string item in list) { cancellationToken.ThrowIfCancellationRequested(); string text3 = (string.Equals(item, stagingRoot, StringComparison.OrdinalIgnoreCase) ? RootPackageFolderName(stagingRoot, modId) : Path.GetFileName(item.TrimEnd(Path.DirectorySeparatorChar))); ValidateFolderName(text3); string text4 = Path.Combine(modsRoot, text3); string text5 = null; if (Directory.Exists(text4)) { if (PackagesAreEquivalent(item, text4)) { list2.Add(text4); continue; } text5 = Path.Combine(backupRoot, text3); Directory.Move(text4, text5); } try { Directory.Move(item, text4); list3.Add((text4, text5)); } catch { if (text5 != null && Directory.Exists(text5) && !Directory.Exists(text4)) { Directory.Move(text5, text4); } throw; } list2.Add(text4); } return list2; } catch { for (int num = list3.Count - 1; num >= 0; num--) { (string, string) tuple = list3[num]; if (Directory.Exists(tuple.Item1)) { Directory.Delete(tuple.Item1, recursive: true); } if (tuple.Item2 != null && Directory.Exists(tuple.Item2)) { Directory.Move(tuple.Item2, tuple.Item1); } } throw; } } finally { TryDeleteDirectory(stagingRoot); TryDeleteDirectory(backupRoot); } } private async Task ExtractValidatedAsync(string zipPath, string destination, CancellationToken cancellationToken) { await using FileStream zipStream = File.OpenRead(zipPath); using ZipArchive archive = new ZipArchive(zipStream, ZipArchiveMode.Read, leaveOpen: false); long extractedBytes = 0L; string destinationPrefix = Path.GetFullPath(destination) + Path.DirectorySeparatorChar; foreach (ZipArchiveEntry entry in archive.Entries) { cancellationToken.ThrowIfCancellationRequested(); if (IsSymbolicLink(entry)) { throw new InvalidDataException("Archive contains a symbolic link: " + entry.FullName); } string fullPath = Path.GetFullPath(Path.Combine(destination, entry.FullName)); if (!fullPath.StartsWith(destinationPrefix, StringComparison.OrdinalIgnoreCase)) { throw new InvalidDataException("Archive path escapes the install directory: " + entry.FullName); } if (string.IsNullOrEmpty(entry.Name)) { Directory.CreateDirectory(fullPath); continue; } if (BlockedExtensions.Contains(Path.GetExtension(entry.Name))) { throw new InvalidDataException("Archive contains blocked executable content: " + entry.FullName); } extractedBytes = checked(extractedBytes + entry.Length); if (extractedBytes > _options.MaximumExtractedBytes) { throw new InvalidDataException("Archive exceeds the configured extracted-size limit."); } Directory.CreateDirectory(Path.GetDirectoryName(fullPath)); await using Stream input = entry.Open(); await using FileStream output = new FileStream(fullPath, FileMode.CreateNew, FileAccess.Write, FileShare.None); byte[] buffer = new byte[131072]; long writtenForEntry = 0L; while (true) { int num = await input.ReadAsync(buffer.AsMemory(0, buffer.Length), cancellationToken).ConfigureAwait(continueOnCapturedContext: false); if (num != 0) { writtenForEntry = checked(writtenForEntry + num); if (extractedBytes - entry.Length + writtenForEntry > _options.MaximumExtractedBytes) { throw new InvalidDataException("Archive exceeds the configured extracted-size limit."); } await output.WriteAsync(buffer.AsMemory(0, num), cancellationToken).ConfigureAwait(continueOnCapturedContext: false); continue; } break; } } } private static List FindPackageRoots(string stagingRoot) { List roots = (from path in PalletManifestLocator.FindAll(stagingRoot, SearchOption.AllDirectories) select Path.GetDirectoryName(path)).Distinct(StringComparer.OrdinalIgnoreCase).ToList(); return roots.Where((string candidate) => !roots.Any((string other) => !string.Equals(candidate, other, StringComparison.OrdinalIgnoreCase) && candidate.StartsWith(other + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase))).ToList(); } private static string RootPackageFolderName(string stagingRoot, long modId) { string[] array = (from name in PalletManifestLocator.FindAll(stagingRoot, SearchOption.TopDirectoryOnly).Select(Path.GetFileName) where name != null && !name.Equals("pallet.json", StringComparison.OrdinalIgnoreCase) select name).ToArray(); if (array.Length == 1) { string text = array[0]; int length = ".pallet.json".Length; string text2 = text.Substring(0, text.Length - length); if (!string.IsNullOrWhiteSpace(text2) && text2.IndexOfAny(Path.GetInvalidFileNameChars()) < 0) { return text2; } } return $"WristHub.Modio.{modId}"; } private static bool PackagesAreEquivalent(string candidateRoot, string installedRoot) { try { (string, string)[] array = (from path in PalletManifestLocator.FindAll(candidateRoot, SearchOption.AllDirectories) select (Relative: Path.GetRelativePath(candidateRoot, path), Path: path)).OrderBy<(string, string), string>(((string Relative, string Path) item) => item.Relative, StringComparer.OrdinalIgnoreCase).ToArray(); (string, string)[] array2 = (from path in PalletManifestLocator.FindAll(installedRoot, SearchOption.AllDirectories) select (Relative: Path.GetRelativePath(installedRoot, path), Path: path)).OrderBy<(string, string), string>(((string Relative, string Path) item) => item.Relative, StringComparer.OrdinalIgnoreCase).ToArray(); if (array.Length == 0 || array.Length != array2.Length) { return false; } for (int num = 0; num < array.Length; num++) { if (!array[num].Item1.Equals(array2[num].Item1, StringComparison.OrdinalIgnoreCase)) { return false; } FileInfo fileInfo = new FileInfo(array[num].Item2); FileInfo fileInfo2 = new FileInfo(array2[num].Item2); if (fileInfo.Length != fileInfo2.Length || !FilesEqual(array[num].Item2, array2[num].Item2)) { return false; } } return true; } catch (IOException) { return false; } catch (UnauthorizedAccessException) { return false; } } private static bool FilesEqual(string leftPath, string rightPath) { using FileStream fileStream = new FileStream(leftPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete, 32768, FileOptions.SequentialScan); using FileStream fileStream2 = new FileStream(rightPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete, 32768, FileOptions.SequentialScan); if (fileStream.Length != fileStream2.Length) { return false; } byte[] array = new byte[32768]; byte[] array2 = new byte[32768]; int num; int num2; do { num = fileStream.Read(array, 0, array.Length); num2 = fileStream2.Read(array2, 0, array2.Length); if (num != num2) { return false; } if (num == 0) { return true; } } while (array.AsSpan(0, num).SequenceEqual(array2.AsSpan(0, num2))); return false; } private static bool IsSymbolicLink(ZipArchiveEntry entry) { return ((entry.ExternalAttributes >> 16) & 0xF000) == 40960; } private static string EnsureDirectory(string path) { string fullPath = Path.GetFullPath(path); Directory.CreateDirectory(fullPath); return fullPath; } private static void ValidateFolderName(string name) { bool flag = string.IsNullOrWhiteSpace(name); if (!flag) { bool flag2 = ((name == "." || name == "..") ? true : false); flag = flag2; } if (flag || name.IndexOfAny(Path.GetInvalidFileNameChars()) >= 0) { throw new InvalidDataException("The pallet has an invalid package folder name."); } } private static void TryDeleteDirectory(string path) { try { if (Directory.Exists(path)) { Directory.Delete(path, recursive: true); } } catch { } } private static void RecoverInterruptedTransactions(string modsRoot, string backupBase) { if (!Directory.Exists(backupBase)) { return; } foreach (string item in Directory.EnumerateDirectories(backupBase)) { foreach (string item2 in Directory.EnumerateDirectories(item)) { string fileName = Path.GetFileName(item2); ValidateFolderName(fileName); string text = Path.Combine(modsRoot, fileName); if (Directory.Exists(text)) { Directory.Delete(item2, recursive: true); } else { Directory.Move(item2, text); } } TryDeleteDirectory(item); } } private static void DeleteStaleStaging(string stagingBase) { if (!Directory.Exists(stagingBase)) { return; } foreach (string item in Directory.EnumerateDirectories(stagingBase)) { TryDeleteDirectory(item); } } } public static class StorageBudget { public const long SafetyReserveBytes = 134217728L; public static long RequiredBytes(long compressedBytes, long extractedBytes) { long num = Math.Max(0L, compressedBytes); long num2 = Math.Max(0L, extractedBytes); try { return checked(num + num2 + 134217728); } catch (OverflowException) { return long.MaxValue; } } public static bool HasCapacity(long availableBytes, long compressedBytes, long extractedBytes) { return availableBytes >= RequiredBytes(compressedBytes, extractedBytes); } public static long? TryGetAvailableBytes(string directory) { try { string fullPath = Path.GetFullPath(directory); return (from item in DriveInfo.GetDrives() where item.IsReady && fullPath.StartsWith(item.RootDirectory.FullName, StringComparison.OrdinalIgnoreCase) orderby item.RootDirectory.FullName.Length descending select item).FirstOrDefault()?.AvailableFreeSpace; } catch { return null; } } } } namespace BoneHub.Downloads { public enum DownloadState { Queued, Resolving, Downloading, Verifying, Installing, Completed, Failed, Cancelled } public sealed class DownloadJob { private readonly object _sync = new object(); public Guid Id { get; } = Guid.NewGuid(); public ModIoMod Mod { get; } public DownloadState State { get; private set; } public double Progress { get; private set; } public string? Error { get; private set; } public InstalledModRecord? Result { get; private set; } internal DownloadJob(ModIoMod mod) { Mod = mod; } internal void Update(DownloadState state, double progress, string? error = null, InstalledModRecord? result = null) { lock (_sync) { State = state; Progress = Math.Clamp(progress, 0.0, 1.0); Error = error; Result = result; } } } public sealed class InstalledModRecord { public long ModId { get; init; } public long FileId { get; init; } public string Name { get; init; } = string.Empty; public string Version { get; init; } = string.Empty; public string Md5 { get; init; } = string.Empty; public IReadOnlyList InstalledDirectories { get; init; } = Array.Empty(); public ModIoSharingMetadata? Sharing { get; init; } } public sealed class ModIoSharingMetadata { public int RegistrationVersion { get; init; } public long ModId { get; init; } public string ModSlug { get; init; } = string.Empty; public long? WindowsFileId { get; init; } public long? AndroidFileId { get; init; } public List SidecarManifestPaths { get; init; } = new List(); public List PalletBarcodes { get; init; } = new List(); public string? Warning { get; init; } } public sealed class DownloadManager : IDisposable { private sealed class InlineProgress : IProgress { private readonly Action _callback; public InlineProgress(Action callback) { _callback = callback; } public void Report(T value) { _callback(value); } } private readonly IModIoClient _client; private readonly SafeModInstaller _installer; private readonly BoneHubOptions _options; private readonly SemaphoreSlim _queue = new SemaphoreSlim(1, 1); private readonly CancellationTokenSource _shutdown = new CancellationTokenSource(); private readonly ConcurrentDictionary _jobCancellation = new ConcurrentDictionary(); private readonly ConcurrentDictionary _activeByMod = new ConcurrentDictionary(); private int _disposed; public event Action? JobChanged; public DownloadManager(IModIoClient client, SafeModInstaller installer, BoneHubOptions options) { _client = client; _installer = installer; _options = options; } public DownloadJob Enqueue(ModIoMod mod, CancellationToken cancellationToken = default(CancellationToken)) { if (_activeByMod.TryGetValue(mod.Id, out DownloadJob value)) { return value; } DownloadJob downloadJob = new DownloadJob(mod); CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); if (!_activeByMod.TryAdd(mod.Id, downloadJob)) { cancellationTokenSource.Dispose(); if (!_activeByMod.TryGetValue(mod.Id, out value)) { return Enqueue(mod, cancellationToken); } return value; } if (!_jobCancellation.TryAdd(downloadJob.Id, cancellationTokenSource)) { _activeByMod.TryRemove(mod.Id, out DownloadJob _); cancellationTokenSource.Dispose(); throw new InvalidOperationException("Could not register the download job."); } Notify(downloadJob); RunGuardedAsync(downloadJob, cancellationTokenSource, cancellationToken); return downloadJob; } public bool Cancel(Guid jobId) { if (!_jobCancellation.TryGetValue(jobId, out CancellationTokenSource value)) { return false; } try { value.Cancel(); return true; } catch (ObjectDisposedException) { return false; } } private async Task RunGuardedAsync(DownloadJob job, CancellationTokenSource jobCancellation, CancellationToken callerToken) { using CancellationTokenSource linked = CancellationTokenSource.CreateLinkedTokenSource(callerToken, jobCancellation.Token, _shutdown.Token); _ = 1; try { await _queue.WaitAsync(linked.Token).ConfigureAwait(continueOnCapturedContext: false); try { await RunAsync(job, linked.Token).ConfigureAwait(continueOnCapturedContext: false); } finally { _queue.Release(); } } catch (OperationCanceledException) { job.Update(DownloadState.Cancelled, job.Progress); Notify(job); } catch (Exception ex2) { job.Update(DownloadState.Failed, job.Progress, ex2.Message); Notify(job); } finally { _jobCancellation.TryRemove(job.Id, out CancellationTokenSource _); _activeByMod.TryRemove(job.Mod.Id, out DownloadJob _); jobCancellation.Dispose(); } } private async Task RunAsync(DownloadJob job, CancellationToken cancellationToken) { ModIoFile modIoFile = job.Mod.Modfile ?? throw new InvalidOperationException("This mod has no live file."); Directory.CreateDirectory(_options.DataDirectory); string archivePath = Path.Combine(_options.DataDirectory, $"{job.Mod.Id}-{modIoFile.Id}-{job.Id:N}.zip.part"); try { ModIoFile file = null; for (int attempt = 0; attempt <= _options.DownloadRetryCount; attempt++) { cancellationToken.ThrowIfCancellationRequested(); try { job.Update(DownloadState.Resolving, 0.0, (attempt == 0) ? null : $"Retry {attempt} of {_options.DownloadRetryCount}"); Notify(job); file = await ModIoFileResolver.ResolveAsync(_client, job.Mod, _options, cancellationToken).ConfigureAwait(continueOnCapturedContext: false); ValidateFile(file); long? num = StorageBudget.TryGetAvailableBytes(_options.DataDirectory); if (num.HasValue && !StorageBudget.HasCapacity(num.Value, file.FileSize, file.UncompressedSize)) { throw new IOException("There is not enough free storage to download, verify, and install this mod safely."); } if (!Uri.TryCreate(file.Download.BinaryUrl, UriKind.Absolute, out Uri result)) { throw new InvalidDataException("mod.io returned an invalid download URL."); } job.Update(DownloadState.Downloading, 0.02); Notify(job); int lastPercent = -1; InlineProgress progress = new InlineProgress(delegate(double value) { job.Update(DownloadState.Downloading, 0.02 + value * 0.73); int num2 = (int)(value * 100.0); if (num2 != lastPercent) { lastPercent = num2; Notify(job); } }); await using FileStream destination = new FileStream(archivePath, FileMode.Create, FileAccess.Write, FileShare.None); await _client.DownloadAsync(result, destination, _options.MaximumDownloadBytes, progress, cancellationToken).ConfigureAwait(continueOnCapturedContext: false); } catch (Exception exception) when (attempt < _options.DownloadRetryCount && IsTransient(exception, cancellationToken)) { TimeSpan delay = TimeSpan.FromSeconds(Math.Min(8, 1 << attempt)); job.Update(DownloadState.Resolving, job.Progress, $"Connection interrupted; retrying in {delay.TotalSeconds:0}s"); Notify(job); await Task.Delay(delay, cancellationToken).ConfigureAwait(continueOnCapturedContext: false); continue; } break; } if (file == null) { throw new InvalidOperationException("Download resolution did not produce a mod file."); } ModIoFileResolver.PlatformFiles platformFiles; try { platformFiles = await ModIoFileResolver.ResolvePlatformFilesAsync(_client, job.Mod.Id, cancellationToken).ConfigureAwait(continueOnCapturedContext: false); } catch { platformFiles = (_options.CompatiblePlatforms.Any((string platform) => platform.Contains("android", StringComparison.OrdinalIgnoreCase) || platform.Contains("quest", StringComparison.OrdinalIgnoreCase) || platform.Contains("oculus", StringComparison.OrdinalIgnoreCase)) ? new ModIoFileResolver.PlatformFiles(null, file) : new ModIoFileResolver.PlatformFiles(file, null)); } job.Update(DownloadState.Verifying, 0.78); Notify(job); string actualHash = await ComputeMd5Async(archivePath, cancellationToken).ConfigureAwait(continueOnCapturedContext: false); if (!string.IsNullOrWhiteSpace(file.Hash.Md5) && !string.Equals(actualHash, file.Hash.Md5, StringComparison.OrdinalIgnoreCase)) { throw new InvalidDataException("Downloaded file failed its mod.io MD5 integrity check."); } job.Update(DownloadState.Installing, 0.84); Notify(job); IReadOnlyList directories = await _installer.InstallAsync(archivePath, job.Mod.Id, cancellationToken).ConfigureAwait(continueOnCapturedContext: false); ModIoSharingMetadata sharing; try { sharing = await NetworkerRegistrationWriter.WriteAsync(job.Mod, file, platformFiles, directories, _options.ModsDirectory, cancellationToken).ConfigureAwait(continueOnCapturedContext: false); } catch (Exception ex) { sharing = new ModIoSharingMetadata { ModId = job.Mod.Id, ModSlug = job.Mod.NameId, WindowsFileId = platformFiles.Windows?.Id, AndroidFileId = platformFiles.Android?.Id, Warning = "Fusion sharing registration failed: " + ex.Message }; } InstalledModRecord result2 = new InstalledModRecord { ModId = job.Mod.Id, FileId = file.Id, Name = job.Mod.Name, Version = file.Version, Md5 = actualHash, InstalledDirectories = directories, Sharing = sharing }; job.Update(DownloadState.Completed, 1.0, null, result2); Notify(job); } finally { try { if (File.Exists(archivePath)) { File.Delete(archivePath); } } catch { } } } private static bool IsTransient(Exception exception, CancellationToken cancellationToken) { if (cancellationToken.IsCancellationRequested || exception is OperationCanceledException) { return false; } if ((exception is HttpRequestException || exception is IOException) ? true : false) { return true; } ModIoException ex = exception as ModIoException; bool flag = ex != null; int num; if (flag) { bool flag2; switch (ex.StatusCode) { case 408: case 425: case 429: flag2 = true; break; default: flag2 = false; break; } if (!flag2) { int? statusCode = ex.StatusCode; if (statusCode.HasValue) { int valueOrDefault = statusCode.GetValueOrDefault(); if (valueOrDefault >= 500) { num = ((valueOrDefault <= 599) ? 1 : 0); goto IL_00ae; } } num = 0; } else { num = 1; } goto IL_00ae; } goto IL_00af; IL_00af: return flag; IL_00ae: flag = (byte)num != 0; goto IL_00af; } private void ValidateFile(ModIoFile file) { if (file.VirusPositive != 0) { throw new InvalidDataException("mod.io flagged this file as harmful; installation was blocked."); } if (_options.RequireCompletedVirusScan && file.VirusStatus != 1) { throw new InvalidDataException("This file has not completed mod.io virus scanning."); } if (file.FileSize > _options.MaximumDownloadBytes) { throw new InvalidDataException("This file exceeds WristHub's configured download limit."); } if (file.UncompressedSize > _options.MaximumExtractedBytes) { throw new InvalidDataException("This file exceeds WristHub's configured extracted-size limit."); } if (!ModIoFileResolver.IsCompatible(file, _options)) { throw new InvalidDataException("This file is not approved for " + _options.PlatformDisplayName + "."); } } private static async Task ComputeMd5Async(string path, CancellationToken cancellationToken) { using MD5 md5 = MD5.Create(); string result; await using (FileStream stream = File.OpenRead(path)) { result = Convert.ToHexString(await md5.ComputeHashAsync(stream, cancellationToken).ConfigureAwait(continueOnCapturedContext: false)).ToLowerInvariant(); } return result; } private void Notify(DownloadJob job) { this.JobChanged?.Invoke(job); } public void Dispose() { if (Interlocked.Exchange(ref _disposed, 1) != 0) { return; } _shutdown.Cancel(); foreach (CancellationTokenSource value in _jobCancellation.Values) { try { value.Cancel(); } catch (ObjectDisposedException) { } } _shutdown.Dispose(); } } public static class ModIoFileResolver { public sealed record PlatformFiles(ModIoFile? Windows, ModIoFile? Android); public static async Task ResolveAsync(IModIoClient client, ModIoMod mod, BoneHubOptions options, CancellationToken cancellationToken = default(CancellationToken)) { ModIoFile modIoFile = mod.Modfile ?? throw new InvalidOperationException("This mod has no live file."); ModIoFile current = await client.GetFileAsync(mod.Id, modIoFile.Id, cancellationToken).ConfigureAwait(continueOnCapturedContext: false); if (IsReadyForInstall(current, options)) { return current; } ModIoFile[] source = (from file in (await client.GetFilesAsync(mod.Id, cancellationToken).ConfigureAwait(continueOnCapturedContext: false)).Data where IsCompatible(file, options) orderby file.DateAdded descending, file.Id descending select file).ToArray(); return source.FirstOrDefault((ModIoFile file) => IsReadyForInstall(file, options)) ?? (IsCompatible(current, options) ? current : source.FirstOrDefault()) ?? throw new InvalidDataException("No active " + options.PlatformDisplayName + " file is available for this mod."); } public static bool IsCompatible(ModIoFile file, BoneHubOptions options) { if (file.Platforms.Count != 0) { return file.Platforms.Any((ModIoPlatform platform) => platform.Status == 1 && (platform.Platform.Equals("ALL", StringComparison.OrdinalIgnoreCase) || options.CompatiblePlatforms.Any((string candidate) => platform.Platform.Equals(candidate, StringComparison.OrdinalIgnoreCase)))); } return true; } private static bool IsReadyForInstall(ModIoFile file, BoneHubOptions options) { if (IsCompatible(file, options) && file.VirusPositive == 0) { if (options.RequireCompletedVirusScan) { return file.VirusStatus == 1; } return true; } return false; } public static async Task ResolvePlatformFilesAsync(IModIoClient client, long modId, CancellationToken cancellationToken = default(CancellationToken)) { Task> windowsTask = client.GetFilesAsync(modId, "windows", cancellationToken); Task> androidTask = client.GetFilesAsync(modId, "android", cancellationToken); await Task.WhenAll>(windowsTask, androidTask).ConfigureAwait(continueOnCapturedContext: false); ModIoPage windowsFiles = await windowsTask.ConfigureAwait(continueOnCapturedContext: false); ModIoPage files = await androidTask.ConfigureAwait(continueOnCapturedContext: false); return new PlatformFiles(Pick(windowsFiles, new string[2] { "windows", "win" }), Pick(files, new string[3] { "android", "oculus", "quest" })); static ModIoFile? Pick(ModIoPage modIoPage, string[] platforms) { return (from file in modIoPage.Data where file.VirusPositive == 0 && (file.Platforms.Count == 0 || file.Platforms.Any((ModIoPlatform platform) => platform.Status == 1 && (platform.Platform.Equals("ALL", StringComparison.OrdinalIgnoreCase) || platforms.Any((string candidate) => platform.Platform.Equals(candidate, StringComparison.OrdinalIgnoreCase))))) orderby file.DateAdded descending, file.Id descending select file).FirstOrDefault(); } } } } namespace BoneHub.Configuration { public sealed class BoneHubOptions { public const int BonelabGameId = 3809; public const string DefaultHostedServiceBaseUrl = "https://wristhub-api.wristhub.workers.dev/v1"; public int GameId { get; init; } = 3809; public string ApiBaseUrl { get; init; } = "https://g-3809.modapi.io/v1"; public string ApiKey { get; set; } = string.Empty; public string ApiKeyFilePath { get; init; } = string.Empty; public string HostedServiceBaseUrl { get; init; } = "https://wristhub-api.wristhub.workers.dev/v1"; public string TargetPlatform { get; init; } = "windows"; public IReadOnlyList CompatiblePlatforms { get; init; } = new string[1] { "windows" }; public string PlatformDisplayName { get; init; } = "Windows PCVR"; public bool ConstrainedRendering { get; init; } public string ModsDirectory { get; init; } = string.Empty; public string DataDirectory { get; init; } = string.Empty; public int SearchPageSize { get; init; } = 20; public int CacheMinutes { get; init; } = 30; public long MaximumDownloadBytes { get; init; } = 2147483648L; public long MaximumExtractedBytes { get; init; } = 6442450944L; public bool RequireCompletedVirusScan { get; init; } = true; public int DownloadRetryCount { get; init; } = 3; public bool HasValidApiKey => IsValidApiKey(ApiKey); public bool HasHostedService { get { if (Uri.TryCreate(HostedServiceBaseUrl, UriKind.Absolute, out Uri result)) { return result.Scheme == Uri.UriSchemeHttps; } return false; } } public bool CanUseModIo { get { if (!HasHostedService) { return HasValidApiKey; } return true; } } public static bool IsValidApiKey(string? value) { if (value != null && value.Trim().Length == 32) { return value.Trim().All(char.IsLetterOrDigit); } return false; } public void Validate() { if (GameId <= 0) { throw new InvalidOperationException("GameId must be positive."); } if (!Uri.TryCreate(ApiBaseUrl, UriKind.Absolute, out Uri result) || result.Scheme != Uri.UriSchemeHttps) { throw new InvalidOperationException("ApiBaseUrl must be an absolute HTTPS URL."); } if (!string.IsNullOrWhiteSpace(HostedServiceBaseUrl) && !HasHostedService) { throw new InvalidOperationException("HostedServiceBaseUrl must be empty or an absolute HTTPS URL."); } if (string.IsNullOrWhiteSpace(ModsDirectory)) { throw new InvalidOperationException("ModsDirectory is required."); } if (string.IsNullOrWhiteSpace(DataDirectory)) { throw new InvalidOperationException("DataDirectory is required."); } if (CompatiblePlatforms.Count == 0 || CompatiblePlatforms.Any(string.IsNullOrWhiteSpace)) { throw new InvalidOperationException("At least one valid compatible platform is required."); } int searchPageSize = SearchPageSize; if ((searchPageSize < 1 || searchPageSize > 100) ? true : false) { throw new InvalidOperationException("SearchPageSize must be between 1 and 100."); } if (CacheMinutes < 0) { throw new InvalidOperationException("CacheMinutes cannot be negative."); } if (MaximumDownloadBytes <= 0 || MaximumExtractedBytes <= 0) { throw new InvalidOperationException("Download limits must be positive."); } searchPageSize = DownloadRetryCount; if ((searchPageSize < 0 || searchPageSize > 5) ? true : false) { throw new InvalidOperationException("DownloadRetryCount must be between 0 and 5."); } } } public sealed record BoneHubPlatformProfile(string Id, string DisplayName, string ModIoHeader, IReadOnlyList CompatibleModIoPlatforms, long MaximumDownloadBytes, long MaximumExtractedBytes, bool ConstrainedRendering) { public static BoneHubPlatformProfile Pcvr { get; } = new BoneHubPlatformProfile("pcvr", "Windows PCVR", "windows", new string[1] { "windows" }, 2147483648L, 6442450944L, ConstrainedRendering: false); public static BoneHubPlatformProfile Quest { get; } = new BoneHubPlatformProfile("quest", "Standalone Quest", "android", new string[2] { "android", "oculus" }, 1073741824L, 3221225472L, ConstrainedRendering: true); public static BoneHubPlatformProfile Detect(bool isAndroid) { if (!isAndroid) { return Pcvr; } return Quest; } [CompilerGenerated] private BoneHubPlatformProfile(BoneHubPlatformProfile original) { Id = original.Id; DisplayName = original.DisplayName; ModIoHeader = original.ModIoHeader; CompatibleModIoPlatforms = original.CompatibleModIoPlatforms; MaximumDownloadBytes = original.MaximumDownloadBytes; MaximumExtractedBytes = original.MaximumExtractedBytes; ConstrainedRendering = original.ConstrainedRendering; } } } namespace BoneHub.Compatibility { public static class FusionAvatarAnnouncementPlan { private static readonly double[] DelaysAfterSuccessfulSendSeconds = new double[3] { 8.0, 24.0, 60.0 }; public static int MaximumSuccessfulSends => DelaysAfterSuccessfulSendSeconds.Length + 1; public static double InitialDelaySeconds => 0.75; public static bool TryGetDelayAfterSuccessfulSend(int successfulSendCount, out double delaySeconds) { if (successfulSendCount < 1 || successfulSendCount > DelaysAfterSuccessfulSendSeconds.Length) { delaySeconds = 0.0; return false; } delaySeconds = DelaysAfterSuccessfulSendSeconds[successfulSendCount - 1]; return true; } } public sealed class FusionCompatibilityBridge { private readonly Func> _assemblies; private Assembly? _fusionAssembly; private Type? _networkInfoType; private Type? _playerIdManagerType; private Type? _localAvatarType; private Type? _rigDataType; private Type? _playerSenderType; private long _nextRefreshAt; public bool Installed { get { Refresh(); return _fusionAssembly != null; } } public bool SessionActive { get { Refresh(); if (!ReadStaticBool(_networkInfoType, "HasServer")) { return false; } if (!ReadStaticBool(_networkInfoType, "IsHost")) { return ReadStaticMember(_playerIdManagerType, "LocalID") != null; } return true; } } public string Version { get { Refresh(); return (_fusionAssembly?.GetType("LabFusion.FusionVersion")?.GetField("VersionString", BindingFlags.Static | BindingFlags.Public))?.GetValue(null)?.ToString() ?? "unknown"; } } public string Status { get { if (Installed) { if (!SessionActive) { return "Fusion " + Version + " detected · ready"; } return "Fusion " + Version + " session · avatar sync active"; } return "Fusion not detected · solo mode"; } } public FusionCompatibilityBridge(Func>? assemblies = null) { _assemblies = assemblies ?? ((Func>)(() => AppDomain.CurrentDomain.GetAssemblies())); } public bool CanEquip(string barcode, out string? error) { error = null; Refresh(); if (_localAvatarType == null) { return true; } string text = ReadStaticProperty(_localAvatarType, "AvatarOverride")?.ToString(); if (string.IsNullOrWhiteSpace(text) || string.Equals(text, barcode, StringComparison.OrdinalIgnoreCase)) { return true; } error = "Fusion currently has an avatar override. Disable the active Fusion gamemode/override before changing avatars."; return false; } public bool IsAvatarSynchronized(string barcode) { Refresh(); if (!SessionActive || _localAvatarType == null) { return true; } return string.Equals(ReadStaticProperty(_localAvatarType, "AvatarBarcode")?.ToString(), barcode, StringComparison.OrdinalIgnoreCase); } public bool TrySwapAvatar(string barcode, out string? error) { error = null; Refresh(); if (!SessionActive || _localAvatarType == null) { return false; } try { MethodInfo method = _localAvatarType.GetMethod("SwapAvatarCrate", BindingFlags.Static | BindingFlags.Public, null, new Type[1] { typeof(string) }, null); if (method == null) { error = "The installed Fusion version does not expose its avatar broadcast method."; return false; } method.Invoke(null, new object[1] { barcode }); return true; } catch (Exception ex) { error = ex.InnerException?.Message ?? ex.Message; return false; } } public bool TryResendCurrentAvatar(string barcode, out string? error) { error = null; Refresh(); if (!SessionActive || _rigDataType == null || _playerSenderType == null) { error = "Fusion's avatar sender is not ready."; return false; } try { if (!string.Equals(ReadStaticMember(_rigDataType, "RigAvatarId")?.ToString() ?? ReadStaticProperty(_localAvatarType, "AvatarBarcode")?.ToString(), barcode, StringComparison.OrdinalIgnoreCase)) { error = "Fusion is synchronized to a different avatar."; return false; } object obj = ReadStaticMember(_rigDataType, "RigAvatarStats"); if (obj == null) { error = "Fusion has not produced synchronized avatar statistics yet."; return false; } MethodInfo methodInfo = _playerSenderType.GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic).FirstOrDefault((MethodInfo method) => method.Name == "SendPlayerAvatar" && method.GetParameters().Length == 2); if (methodInfo == null) { error = "The installed Fusion version does not expose its avatar sender."; return false; } methodInfo.Invoke(null, new object[2] { obj, barcode }); return true; } catch (Exception ex) { error = ex.InnerException?.Message ?? ex.Message; return false; } } public void ForceRefresh() { _nextRefreshAt = 0L; Refresh(); } private void Refresh() { long tickCount = Environment.TickCount64; if (_fusionAssembly != null || tickCount < _nextRefreshAt) { return; } _nextRefreshAt = tickCount + 1000; try { _fusionAssembly = _assemblies().FirstOrDefault((Assembly assembly) => string.Equals(assembly.GetName().Name, "LabFusion", StringComparison.OrdinalIgnoreCase)); _networkInfoType = _fusionAssembly?.GetType("LabFusion.Network.NetworkInfo"); _playerIdManagerType = _fusionAssembly?.GetType("LabFusion.Player.PlayerIDManager"); _localAvatarType = _fusionAssembly?.GetType("LabFusion.Player.LocalAvatar"); _rigDataType = _fusionAssembly?.GetType("LabFusion.Data.RigData"); _playerSenderType = _fusionAssembly?.GetType("LabFusion.Senders.PlayerSender"); } catch { _fusionAssembly = null; _networkInfoType = null; _playerIdManagerType = null; _localAvatarType = null; _rigDataType = null; _playerSenderType = null; } } private static bool ReadStaticBool(Type? type, string property) { object obj = ReadStaticProperty(type, property); bool flag = default(bool); int num; if (obj is bool) { flag = (bool)obj; num = 1; } else { num = 0; } return (byte)((uint)num & (flag ? 1u : 0u)) != 0; } private static object? ReadStaticProperty(Type? type, string property) { try { return type?.GetProperty(property, BindingFlags.Static | BindingFlags.Public)?.GetValue(null); } catch { return null; } } private static object? ReadStaticMember(Type? type, string name) { try { return type?.GetProperty(name, BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(null) ?? type?.GetField(name, BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(null); } catch { return null; } } } public enum FusionServerSwitchAction { None, RestoreNetworkLayer, Disconnect, JoinDestination, Completed, TimedOut } public sealed class FusionServerSwitchCoordinator { private const double NativeJoinGraceSeconds = 4.0; private const double DirectJoinGraceSeconds = 18.0; private const double StableDisconnectSeconds = 1.25; private const double SwitchTimeoutSeconds = 40.0; private double _expiresAt; private double _joinGraceUntil; private double _loginRetryAt; private double _disconnectedSince = double.NaN; private bool _disconnectRequested; private bool _fallbackUsed; public bool Active { get; private set; } public int JoinAttempts { get; private set; } public void Begin(double nowSeconds, bool joinAlreadyStarted, bool disconnectAlreadyRequested) { Active = true; JoinAttempts = (joinAlreadyStarted ? 1 : 0); _fallbackUsed = false; _disconnectRequested = disconnectAlreadyRequested; _disconnectedSince = double.NaN; _expiresAt = nowSeconds + 40.0; _joinGraceUntil = (joinAlreadyStarted ? (nowSeconds + 4.0) : nowSeconds); _loginRetryAt = nowSeconds + 0.1; } public FusionServerSwitchAction Evaluate(double nowSeconds, bool destinationConnected, bool hasNetworkLayer, bool hasServer) { if (!Active) { return FusionServerSwitchAction.None; } if (destinationConnected) { Active = false; return FusionServerSwitchAction.Completed; } if (nowSeconds >= _expiresAt) { Active = false; return FusionServerSwitchAction.TimedOut; } if (!hasNetworkLayer) { if (nowSeconds < _loginRetryAt) { return FusionServerSwitchAction.None; } _loginRetryAt = nowSeconds + 1.25; return FusionServerSwitchAction.RestoreNetworkLayer; } if (_disconnectRequested) { if (hasServer) { _disconnectedSince = double.NaN; return FusionServerSwitchAction.None; } if (double.IsNaN(_disconnectedSince)) { _disconnectedSince = nowSeconds; } if (nowSeconds - _disconnectedSince < 1.25) { return FusionServerSwitchAction.None; } return StartJoin(nowSeconds); } _disconnectedSince = double.NaN; if (nowSeconds < _joinGraceUntil) { return FusionServerSwitchAction.None; } if (hasServer) { if (_fallbackUsed) { return FusionServerSwitchAction.None; } _disconnectRequested = true; _fallbackUsed = true; return FusionServerSwitchAction.Disconnect; } return StartJoin(nowSeconds); } public void Cancel() { Active = false; _disconnectRequested = false; _disconnectedSince = double.NaN; } private FusionServerSwitchAction StartJoin(double nowSeconds) { if (JoinAttempts >= 2) { return FusionServerSwitchAction.None; } JoinAttempts++; _fallbackUsed = true; _disconnectRequested = false; _disconnectedSince = double.NaN; _joinGraceUntil = nowSeconds + 18.0; return FusionServerSwitchAction.JoinDestination; } } } namespace BoneHub.Avatars { public enum AvatarDownloadState { None, Queued, Downloading, Installing, Installed, Failed, Cancelled } public sealed record AvatarEntry(long ModId, string Barcode, string AvatarName, string ModName, string Creator, string Description, string ThumbnailUrl, bool Installed, bool Compatible = true, AvatarDownloadState DownloadState = AvatarDownloadState.None, double DownloadProgress = 0.0, string? Status = null) { public string Identity { get { if (string.IsNullOrWhiteSpace(Barcode)) { return "mod:" + ModId; } return "avatar:" + Barcode.Trim().ToLowerInvariant(); } } public AvatarEntry WithDownload(AvatarDownloadState state, double progress = 0.0, string? status = null) { return this with { DownloadState = state, DownloadProgress = Math.Clamp(progress, 0.0, 1.0), Status = status }; } public static AvatarEntry FromOnline(ModIoMod mod, bool installed) { return new AvatarEntry(mod.Id, string.Empty, mod.Name, mod.Name, mod.Author.DisplayName, mod.Summary, string.IsNullOrWhiteSpace(mod.Logo.Thumbnail) ? mod.Logo.Original : mod.Logo.Thumbnail, installed, Compatible: true, installed ? AvatarDownloadState.Installed : AvatarDownloadState.None); } } public sealed record LocalModMetadata(long ModId, string ModName, string Creator, string Summary, string ThumbnailUrl, IReadOnlyList Tags) { public static LocalModMetadata Read(string directory) { string fileName = Path.GetFileName(Path.GetFullPath(directory).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)); string path = Path.Combine(directory, "modinfo.json"); if (!File.Exists(path)) { return new LocalModMetadata(StableLocalId(directory), fileName, "Unknown creator", string.Empty, string.Empty, Array.Empty()); } try { using JsonDocument jsonDocument = JsonDocument.Parse(File.ReadAllText(path)); JsonElement rootElement = jsonDocument.RootElement; long num = ReadLong(rootElement, "numericalId"); return new LocalModMetadata((num > 0) ? num : StableLocalId(directory), ReadString(rootElement, "modName", fileName), ReadString(rootElement, "author", "Unknown creator"), ReadString(rootElement, "modSummary", string.Empty), ReadString(rootElement, "thumbnailLink", string.Empty), ReadStrings(rootElement, "tags")); } catch (JsonException) { return new LocalModMetadata(StableLocalId(directory), fileName, "Unknown creator", string.Empty, string.Empty, Array.Empty()); } catch (IOException) { return new LocalModMetadata(StableLocalId(directory), fileName, "Unknown creator", string.Empty, string.Empty, Array.Empty()); } } private static string ReadString(JsonElement root, string name, string fallback) { if (!root.TryGetProperty(name, out var value) || value.ValueKind != JsonValueKind.String || string.IsNullOrWhiteSpace(value.GetString())) { return fallback; } return value.GetString().Trim(); } private static long ReadLong(JsonElement root, string name) { if (!root.TryGetProperty(name, out var value)) { return 0L; } if (value.ValueKind != JsonValueKind.Number || !value.TryGetInt64(out var result)) { if (value.ValueKind != JsonValueKind.String || !long.TryParse(value.GetString(), out result)) { return 0L; } return result; } return result; } private static IReadOnlyList ReadStrings(JsonElement root, string name) { if (!root.TryGetProperty(name, out var value) || value.ValueKind != JsonValueKind.Array) { return Array.Empty(); } return (from item in value.EnumerateArray() where item.ValueKind == JsonValueKind.String select item.GetString().Trim() into item where item.Length > 0 select item).ToArray(); } private static long StableLocalId(string path) { long num = 1469598103934665603L; string text = Path.GetFullPath(path).ToUpperInvariant(); foreach (char c in text) { num = (num ^ c) * 1099511628211L; } if (num <= 0) { if (num != 0L) { return num; } return -9223372036854775807L; } return -num; } } public static class AvatarLibrary { public static IReadOnlyList Downloadable(IEnumerable entries) { return entries.Where((AvatarEntry entry) => !entry.Installed).OrderBy((AvatarEntry entry) => entry.AvatarName, StringComparer.OrdinalIgnoreCase).ThenBy((AvatarEntry entry) => entry.ModId) .ToArray(); } public static IReadOnlyList Merge(IEnumerable installed, IEnumerable online, string? query, IEnumerable? recentBarcodes = null) { string normalized = query?.Trim() ?? string.Empty; AvatarEntry[] source = installed.ToArray(); AvatarEntry[] first = source.Where((AvatarEntry entry) => Matches(entry, normalized)).ToArray(); HashSet installedModIds = (from entry in source where entry.ModId > 0 select entry.ModId).ToHashSet(); IEnumerable source2 = from @group in first.Concat(online.Where((AvatarEntry entry) => !installedModIds.Contains(entry.ModId))).GroupBy((AvatarEntry entry) => entry.Identity, StringComparer.OrdinalIgnoreCase) select @group.OrderByDescending((AvatarEntry entry) => entry.Installed).First(); Dictionary recent = (recentBarcodes ?? Array.Empty()).Select((string barcode, int index) => (barcode: barcode, index: index)).ToDictionary<(string, int), string, int>(((string barcode, int index) item) => item.barcode, ((string barcode, int index) item) => item.index, StringComparer.OrdinalIgnoreCase); int value; return (from entry in source2 orderby (!entry.Installed) ? 1 : 0, (!entry.Installed || !recent.TryGetValue(entry.Barcode, out value)) ? int.MaxValue : value select entry).ThenBy((AvatarEntry entry) => entry.AvatarName, StringComparer.OrdinalIgnoreCase).ToArray(); } private static bool Matches(AvatarEntry entry, string query) { if (query.Length != 0 && !entry.AvatarName.Contains(query, StringComparison.OrdinalIgnoreCase) && !entry.ModName.Contains(query, StringComparison.OrdinalIgnoreCase) && !entry.Creator.Contains(query, StringComparison.OrdinalIgnoreCase)) { return entry.Barcode.Contains(query, StringComparison.OrdinalIgnoreCase); } return true; } } } namespace BoneHub.Api { public interface IModIoClient { Task> SearchAsync(ModSearchRequest request, CancellationToken cancellationToken = default(CancellationToken)); Task GetModAsync(long modId, CancellationToken cancellationToken = default(CancellationToken)); Task GetFileAsync(long modId, long fileId, CancellationToken cancellationToken = default(CancellationToken)); Task> GetFilesAsync(long modId, CancellationToken cancellationToken = default(CancellationToken)); Task> GetFilesAsync(long modId, string platform, CancellationToken cancellationToken = default(CancellationToken)) { return GetFilesAsync(modId, cancellationToken); } Task> GetDependenciesAsync(long modId, CancellationToken cancellationToken = default(CancellationToken)); Task DownloadAsync(Uri binaryUri, Stream destination, long maximumBytes, IProgress? progress = null, CancellationToken cancellationToken = default(CancellationToken)); } public sealed class ModIoClient : IModIoClient, IDisposable { private readonly BoneHubOptions _options; private readonly HttpClient _http; private readonly bool _ownsClient; private readonly JsonSerializerOptions _json = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; public ModIoClient(BoneHubOptions options, HttpClient? httpClient = null) { _options = options; _options.Validate(); _http = httpClient ?? new HttpClient(); _ownsClient = httpClient == null; _http.Timeout = TimeSpan.FromMinutes(10.0); _http.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); _http.DefaultRequestHeaders.UserAgent.ParseAdd("WristHub/4.0.1"); } public Task> SearchAsync(ModSearchRequest request, CancellationToken cancellationToken = default(CancellationToken)) { EnsureAvailable(); int limit = request.Limit; if ((limit < 1 || limit > 100) ? true : false) { throw new ArgumentOutOfRangeException("request", "Limit must be 1-100."); } Dictionary hostedQuery = new Dictionary { ["q"] = (string.IsNullOrWhiteSpace(request.Query) ? null : request.Query.Trim()), ["creator"] = (string.IsNullOrWhiteSpace(request.Creator) ? null : request.Creator.Trim()), ["sort"] = SortValue(request.Sort), ["offset"] = request.Offset.ToString(), ["limit"] = request.Limit.ToString() }; Dictionary directQuery = new Dictionary { ["api_key"] = _options.ApiKey, ["_q"] = (string.IsNullOrWhiteSpace(request.Query) ? null : request.Query.Trim()), ["tags"] = (string.IsNullOrWhiteSpace(request.Tag) ? "Avatar" : request.Tag.Trim()), ["submitted_by_display_name-lk"] = (string.IsNullOrWhiteSpace(request.Creator) ? null : ("*" + request.Creator.Trim() + "*")), ["_sort"] = SortValue(request.Sort), ["_offset"] = request.Offset.ToString(), ["_limit"] = request.Limit.ToString() }; return GetWithFallbackAsync>("mods", hostedQuery, $"games/{_options.GameId}/mods", directQuery, _options.TargetPlatform, cancellationToken); } public Task GetFileAsync(long modId, long fileId, CancellationToken cancellationToken = default(CancellationToken)) { EnsureAvailable(); if (modId <= 0) { throw new ArgumentOutOfRangeException("modId"); } if (fileId <= 0) { throw new ArgumentOutOfRangeException("fileId"); } return GetWithFallbackAsync($"mods/{modId}/files/{fileId}", EmptyQuery(), $"games/{_options.GameId}/mods/{modId}/files/{fileId}", ApiKeyQuery(), _options.TargetPlatform, cancellationToken); } public Task> GetFilesAsync(long modId, CancellationToken cancellationToken = default(CancellationToken)) { return GetFilesAsync(modId, _options.TargetPlatform, cancellationToken); } public Task> GetFilesAsync(long modId, string platform, CancellationToken cancellationToken = default(CancellationToken)) { EnsureAvailable(); if (modId <= 0) { throw new ArgumentOutOfRangeException("modId"); } if (string.IsNullOrWhiteSpace(platform)) { throw new ArgumentException("A mod.io platform is required.", "platform"); } Dictionary dictionary = ApiKeyQuery(); dictionary["_sort"] = "-date_added"; dictionary["_limit"] = "100"; return GetWithFallbackAsync>($"mods/{modId}/files", EmptyQuery(), $"games/{_options.GameId}/mods/{modId}/files", dictionary, platform.Trim(), cancellationToken); } public Task GetModAsync(long modId, CancellationToken cancellationToken = default(CancellationToken)) { EnsureAvailable(); if (modId <= 0) { throw new ArgumentOutOfRangeException("modId"); } return GetWithFallbackAsync($"mods/{modId}", EmptyQuery(), $"games/{_options.GameId}/mods/{modId}", ApiKeyQuery(), _options.TargetPlatform, cancellationToken); } public Task> GetDependenciesAsync(long modId, CancellationToken cancellationToken = default(CancellationToken)) { EnsureAvailable(); if (modId <= 0) { throw new ArgumentOutOfRangeException("modId"); } Dictionary dictionary = ApiKeyQuery(); dictionary["recursive"] = "false"; dictionary["_limit"] = "100"; return GetWithFallbackAsync>($"mods/{modId}/dependencies", EmptyQuery(), $"games/{_options.GameId}/mods/{modId}/dependencies", dictionary, _options.TargetPlatform, cancellationToken); } public async Task DownloadAsync(Uri binaryUri, Stream destination, long maximumBytes, IProgress? progress = null, CancellationToken cancellationToken = default(CancellationToken)) { if (binaryUri.Scheme != Uri.UriSchemeHttps) { throw new ModIoException("Refusing a non-HTTPS download URL."); } using HttpResponseMessage response = await _http.GetAsync(binaryUri, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(continueOnCapturedContext: false); await EnsureSuccessAsync(response, cancellationToken).ConfigureAwait(continueOnCapturedContext: false); long? length = response.Content.Headers.ContentLength; if (length > maximumBytes) { throw new ModIoException($"Download is larger than the {maximumBytes} byte limit."); } await using Stream source = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(continueOnCapturedContext: false); byte[] buffer = new byte[131072]; long total = 0L; while (true) { int num = await source.ReadAsync(buffer.AsMemory(0, buffer.Length), cancellationToken).ConfigureAwait(continueOnCapturedContext: false); if (num != 0) { total += num; if (total > maximumBytes) { throw new ModIoException($"Download exceeded the {maximumBytes} byte limit."); } await destination.WriteAsync(buffer.AsMemory(0, num), cancellationToken).ConfigureAwait(continueOnCapturedContext: false); if (length.HasValue && length.GetValueOrDefault() > 0) { progress?.Report(Math.Min(1.0, (double)total / (double)length.Value)); } continue; } break; } } private async Task GetWithFallbackAsync(string hostedPath, IReadOnlyDictionary hostedQuery, string directPath, IReadOnlyDictionary directQuery, string platform, CancellationToken cancellationToken) { Exception ex = null; if (_options.HasHostedService) { try { return await GetJsonAsync(BuildUri(_options.HostedServiceBaseUrl, hostedPath, hostedQuery), platform, cancellationToken).ConfigureAwait(continueOnCapturedContext: false); } catch (Exception ex2) when (!cancellationToken.IsCancellationRequested && CanUseLocalFallback(ex2)) { ex = ex2; } } if (_options.HasValidApiKey) { return await GetJsonAsync(BuildUri(_options.ApiBaseUrl, directPath, directQuery), platform, cancellationToken).ConfigureAwait(continueOnCapturedContext: false); } if (ex is ModIoException ex3) { throw new ModIoException("WristHub's avatar service is temporarily unavailable. Try again shortly.", ex3.StatusCode, ex3); } if (ex != null) { Exception inner = ex; throw new ModIoException("WristHub's avatar service is temporarily unavailable. Try again shortly.", null, inner); } throw new ModIoException("WristHub's avatar service is not configured."); } private async Task GetJsonAsync(Uri uri, string platform, CancellationToken cancellationToken) { using HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, uri); request.Headers.TryAddWithoutValidation("X-Modio-Platform", platform); using HttpResponseMessage response = await _http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(continueOnCapturedContext: false); await EnsureSuccessAsync(response, cancellationToken).ConfigureAwait(continueOnCapturedContext: false); T result; await using (Stream stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(continueOnCapturedContext: false)) { T val = await JsonSerializer.DeserializeAsync(stream, _json, cancellationToken).ConfigureAwait(continueOnCapturedContext: false); if (val == null) { throw new ModIoException("The avatar catalog returned an empty JSON response."); } result = val; } return result; } private static async Task EnsureSuccessAsync(HttpResponseMessage response, CancellationToken cancellationToken) { if (response.IsSuccessStatusCode) { return; } string text = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(continueOnCapturedContext: false); if (text.Length > 600) { text = text.Substring(0, 600); } if (response.StatusCode == HttpStatusCode.TooManyRequests) { throw new ModIoException("The avatar catalog is busy; wait and try again.", (int)response.StatusCode); } throw new ModIoException($"The avatar catalog returned {response.StatusCode} {response.ReasonPhrase}: {text}", (int)response.StatusCode); } private static Uri BuildUri(string baseUrl, string relativePath, IReadOnlyDictionary values) { string text = string.Join("&", from pair in values where pair.Value != null select Uri.EscapeDataString(pair.Key) + "=" + Uri.EscapeDataString(pair.Value)); string value = ((text.Length == 0) ? string.Empty : "?"); return new Uri($"{baseUrl.TrimEnd('/')}/{relativePath.TrimStart('/')}{value}{text}", UriKind.Absolute); } private Dictionary ApiKeyQuery() { return new Dictionary { ["api_key"] = _options.ApiKey }; } private static Dictionary EmptyQuery() { return new Dictionary(); } private void EnsureAvailable() { if (!_options.CanUseModIo) { throw new ModIoException("WristHub's avatar service is unavailable. A local mod.io key can be added in Setup as a fallback."); } } private bool CanUseLocalFallback(Exception exception) { if ((exception is HttpRequestException || exception is IOException) ? true : false) { return true; } ModIoException ex = exception as ModIoException; bool flag = ex != null; int num; if (flag) { bool flag2; switch (ex.StatusCode) { case 401: case 403: case 408: case 425: case 429: flag2 = true; break; default: flag2 = false; break; } if (!flag2) { int? statusCode = ex.StatusCode; if (statusCode.HasValue) { int valueOrDefault = statusCode.GetValueOrDefault(); if (valueOrDefault >= 500) { num = ((valueOrDefault <= 599) ? 1 : 0); goto IL_00b8; } } num = 0; } else { num = 1; } goto IL_00b8; } goto IL_00b9; IL_00b9: return flag; IL_00b8: flag = (byte)num != 0; goto IL_00b9; } private static string SortValue(ModSort sort) { return sort switch { ModSort.Newest => "-date_live", ModSort.MostDownloaded => "-downloads", ModSort.RecentlyUpdated => "-date_updated", _ => "-popular", }; } public void Dispose() { if (_ownsClient) { _http.Dispose(); } } } public sealed class ModIoException : Exception { public int? StatusCode { get; } public ModIoException(string message, int? statusCode = null, Exception? inner = null) : base(message, inner) { StatusCode = statusCode; } } }