using Dalamud.Game.ClientState.Objects.Types; using LightlessSync.API.Data; using LightlessSync.API.Data.Enum; using LightlessSync.PlayerData.Pairs; using Microsoft.Extensions.Logging; using System.Text.Json; namespace LightlessSync.Utils; public static class VariousExtensions { public static string ToByteString(this int bytes, bool addSuffix = true) { string[] suffix = ["B", "KiB", "MiB", "GiB", "TiB"]; int i; double dblSByte = bytes; for (i = 0; i < suffix.Length && bytes >= 1024; i++, bytes /= 1024) { dblSByte = bytes / 1024.0; } return addSuffix ? $"{dblSByte:0.00} {suffix[i]}" : $"{dblSByte:0.00}"; } public static string ToByteString(this long bytes, bool addSuffix = true) { string[] suffix = ["B", "KiB", "MiB", "GiB", "TiB"]; int i; double dblSByte = bytes; for (i = 0; i < suffix.Length && bytes >= 1024; i++, bytes /= 1024) { dblSByte = bytes / 1024.0; } return addSuffix ? $"{dblSByte:0.00} {suffix[i]}" : $"{dblSByte:0.00}"; } public static void CancelDispose(this CancellationTokenSource? cts) { try { cts?.Cancel(); cts?.Dispose(); } catch (ObjectDisposedException) { // swallow it } } public static CancellationTokenSource CancelRecreate(this CancellationTokenSource? cts) { cts?.CancelDispose(); return new CancellationTokenSource(); } public static Dictionary> CheckUpdatedData( this CharacterData newData, Guid applicationBase, CharacterData? oldData, ILogger logger, IPairPerformanceSubject cachedPlayer, bool forceApplyCustomization, bool forceApplyMods, bool suppressForcedRedrawOnForcedModApply = false) { oldData ??= new(); static bool HasFiles(List? list) => list is { Count: > 0 }; static bool HasText(string? s) => !string.IsNullOrEmpty(s); static string Norm(string? s) => s ?? string.Empty; var forceRedrawOnForcedApply = forceApplyMods && !suppressForcedRedrawOnForcedModApply; var charaDataToUpdate = new Dictionary>(); foreach (ObjectKind objectKind in Enum.GetValues()) { var set = new HashSet(); oldData.FileReplacements.TryGetValue(objectKind, out var oldFileRepls); newData.FileReplacements.TryGetValue(objectKind, out var newFileRepls); oldData.GlamourerData.TryGetValue(objectKind, out var oldGlam); newData.GlamourerData.TryGetValue(objectKind, out var newGlam); var oldHasFiles = HasFiles(oldFileRepls); var newHasFiles = HasFiles(newFileRepls); if (oldHasFiles != newHasFiles) { logger.LogDebug("[BASE-{appBase}] Updating {object}/{kind} (File presence changed old={old} new={new}) => {change}", applicationBase, cachedPlayer, objectKind, oldHasFiles, newHasFiles, PlayerChanges.ModFiles); set.Add(PlayerChanges.ModFiles); if (objectKind != ObjectKind.Player || forceRedrawOnForcedApply) { set.Add(PlayerChanges.ForcedRedraw); } } else if (newHasFiles) { var listsAreEqual = oldFileRepls!.SequenceEqual(newFileRepls!, PlayerData.Data.FileReplacementDataComparer.Instance); if (!listsAreEqual || forceApplyMods) { logger.LogDebug("[BASE-{appBase}] Updating {object}/{kind} (FileReplacements changed or forceApplyMods) => {change}", applicationBase, cachedPlayer, objectKind, PlayerChanges.ModFiles); set.Add(PlayerChanges.ModFiles); if (objectKind != ObjectKind.Player || forceRedrawOnForcedApply) { set.Add(PlayerChanges.ForcedRedraw); } else { var existingFace = oldFileRepls.Where(g => g.GamePaths.Any(p => p.Contains("/face/", StringComparison.OrdinalIgnoreCase))) .OrderBy(g => string.IsNullOrEmpty(g.Hash) ? g.FileSwapPath : g.Hash, StringComparer.OrdinalIgnoreCase).ToList(); var existingHair = oldFileRepls.Where(g => g.GamePaths.Any(p => p.Contains("/hair/", StringComparison.OrdinalIgnoreCase))) .OrderBy(g => string.IsNullOrEmpty(g.Hash) ? g.FileSwapPath : g.Hash, StringComparer.OrdinalIgnoreCase).ToList(); var existingTail = oldFileRepls.Where(g => g.GamePaths.Any(p => p.Contains("/tail/", StringComparison.OrdinalIgnoreCase))) .OrderBy(g => string.IsNullOrEmpty(g.Hash) ? g.FileSwapPath : g.Hash, StringComparer.OrdinalIgnoreCase).ToList(); var newFace = newFileRepls!.Where(g => g.GamePaths.Any(p => p.Contains("/face/", StringComparison.OrdinalIgnoreCase))) .OrderBy(g => string.IsNullOrEmpty(g.Hash) ? g.FileSwapPath : g.Hash, StringComparer.OrdinalIgnoreCase).ToList(); var newHair = newFileRepls.Where(g => g.GamePaths.Any(p => p.Contains("/hair/", StringComparison.OrdinalIgnoreCase))) .OrderBy(g => string.IsNullOrEmpty(g.Hash) ? g.FileSwapPath : g.Hash, StringComparer.OrdinalIgnoreCase).ToList(); var newTail = newFileRepls.Where(g => g.GamePaths.Any(p => p.Contains("/tail/", StringComparison.OrdinalIgnoreCase))) .OrderBy(g => string.IsNullOrEmpty(g.Hash) ? g.FileSwapPath : g.Hash, StringComparer.OrdinalIgnoreCase).ToList(); var existingTransients = oldFileRepls.Where(g => g.GamePaths.Any(p => !p.EndsWith("mdl", StringComparison.OrdinalIgnoreCase) && !p.EndsWith("tex", StringComparison.OrdinalIgnoreCase) && !p.EndsWith("mtrl", StringComparison.OrdinalIgnoreCase))) .OrderBy(g => string.IsNullOrEmpty(g.Hash) ? g.FileSwapPath : g.Hash, StringComparer.OrdinalIgnoreCase).ToList(); var newTransients = newFileRepls.Where(g => g.GamePaths.Any(p => !p.EndsWith("mdl", StringComparison.OrdinalIgnoreCase) && !p.EndsWith("tex", StringComparison.OrdinalIgnoreCase) && !p.EndsWith("mtrl", StringComparison.OrdinalIgnoreCase))) .OrderBy(g => string.IsNullOrEmpty(g.Hash) ? g.FileSwapPath : g.Hash, StringComparer.OrdinalIgnoreCase).ToList(); var differentFace = !existingFace.SequenceEqual(newFace, PlayerData.Data.FileReplacementDataComparer.Instance); var differentHair = !existingHair.SequenceEqual(newHair, PlayerData.Data.FileReplacementDataComparer.Instance); var differentTail = !existingTail.SequenceEqual(newTail, PlayerData.Data.FileReplacementDataComparer.Instance); var differentTransients = !existingTransients.SequenceEqual(newTransients, PlayerData.Data.FileReplacementDataComparer.Instance); if (differentFace || differentHair || differentTail || differentTransients) set.Add(PlayerChanges.ForcedRedraw); } } } var oldGlamNorm = Norm(oldGlam); var newGlamNorm = Norm(newGlam); if (!string.Equals(oldGlamNorm, newGlamNorm, StringComparison.Ordinal) || (forceApplyCustomization && HasText(newGlamNorm))) { logger.LogDebug("[BASE-{appBase}] Updating {object}/{kind} (Glamourer different) => {change}", applicationBase, cachedPlayer, objectKind, PlayerChanges.Glamourer); set.Add(PlayerChanges.Glamourer); } oldData.CustomizePlusData.TryGetValue(objectKind, out var oldC); newData.CustomizePlusData.TryGetValue(objectKind, out var newC); var oldCNorm = Norm(oldC); var newCNorm = Norm(newC); if (!string.Equals(oldCNorm, newCNorm, StringComparison.Ordinal) || (forceApplyCustomization && HasText(newCNorm))) { logger.LogDebug("[BASE-{appBase}] Updating {object}/{kind} (Customize+ different) => {change}", applicationBase, cachedPlayer, objectKind, PlayerChanges.Customize); set.Add(PlayerChanges.Customize); } if (objectKind == ObjectKind.Player) { var oldManip = Norm(oldData.ManipulationData); var newManip = Norm(newData.ManipulationData); if (!string.Equals(oldManip, newManip, StringComparison.Ordinal) || forceRedrawOnForcedApply) { logger.LogDebug("[BASE-{appBase}] Updating {object}/{kind} (Manip different) => {change}", applicationBase, cachedPlayer, objectKind, PlayerChanges.ModManip); set.Add(PlayerChanges.ModManip); set.Add(PlayerChanges.ForcedRedraw); } if (!string.Equals(Norm(oldData.HeelsData), Norm(newData.HeelsData), StringComparison.Ordinal) || (forceApplyCustomization && HasText(newData.HeelsData))) set.Add(PlayerChanges.Heels); if (!string.Equals(Norm(oldData.HonorificData), Norm(newData.HonorificData), StringComparison.Ordinal) || (forceApplyCustomization && HasText(newData.HonorificData))) set.Add(PlayerChanges.Honorific); if (!string.Equals(Norm(oldData.MoodlesData), Norm(newData.MoodlesData), StringComparison.Ordinal) || (forceApplyCustomization && HasText(newData.MoodlesData))) set.Add(PlayerChanges.Moodles); if (!string.Equals(Norm(oldData.PetNamesData), Norm(newData.PetNamesData), StringComparison.Ordinal) || (forceApplyCustomization && HasText(newData.PetNamesData))) set.Add(PlayerChanges.PetNames); } if (set.Count > 0) charaDataToUpdate[objectKind] = set; } foreach (var k in charaDataToUpdate.Keys.ToList()) charaDataToUpdate[k] = [.. charaDataToUpdate[k].OrderBy(p => (int)p)]; return charaDataToUpdate; } public static T DeepClone(this T obj) { return JsonSerializer.Deserialize(JsonSerializer.Serialize(obj))!; } public static unsafe int? ObjectTableIndex(this IGameObject? gameObject) { if (gameObject == null || gameObject.Address == IntPtr.Zero) { return null; } return ((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)gameObject.Address)->ObjectIndex; } }