init 2
This commit is contained in:
@@ -1,4 +1,7 @@
|
||||
using System.Security.Cryptography;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace LightlessSync.Utils;
|
||||
@@ -9,8 +12,8 @@ public static class Crypto
|
||||
private const int _bufferSize = 65536;
|
||||
#pragma warning disable SYSLIB0021 // Type or member is obsolete
|
||||
|
||||
private static readonly Dictionary<(string, ushort), string> _hashListPlayersSHA256 = [];
|
||||
private static readonly Dictionary<string, string> _hashListSHA256 = new(StringComparer.Ordinal);
|
||||
private static readonly ConcurrentDictionary<(string, ushort), string> _hashListPlayersSHA256 = new();
|
||||
private static readonly ConcurrentDictionary<string, string> _hashListSHA256 = new(StringComparer.Ordinal);
|
||||
private static readonly SHA256CryptoServiceProvider _sha256CryptoProvider = new();
|
||||
|
||||
public static string GetFileHash(this string filePath)
|
||||
@@ -42,25 +45,18 @@ public static class Crypto
|
||||
|
||||
public static string GetHash256(this (string, ushort) playerToHash)
|
||||
{
|
||||
if (_hashListPlayersSHA256.TryGetValue(playerToHash, out var hash))
|
||||
return hash;
|
||||
|
||||
return _hashListPlayersSHA256[playerToHash] =
|
||||
BitConverter.ToString(_sha256CryptoProvider.ComputeHash(Encoding.UTF8.GetBytes(playerToHash.Item1 + playerToHash.Item2.ToString()))).Replace("-", "", StringComparison.Ordinal);
|
||||
return _hashListPlayersSHA256.GetOrAdd(playerToHash, key => ComputeHashSHA256(key.Item1 + key.Item2.ToString()));
|
||||
}
|
||||
|
||||
public static string GetHash256(this string stringToHash)
|
||||
{
|
||||
return GetOrComputeHashSHA256(stringToHash);
|
||||
return _hashListSHA256.GetOrAdd(stringToHash, ComputeHashSHA256);
|
||||
}
|
||||
|
||||
private static string GetOrComputeHashSHA256(string stringToCompute)
|
||||
private static string ComputeHashSHA256(string stringToCompute)
|
||||
{
|
||||
if (_hashListSHA256.TryGetValue(stringToCompute, out var hash))
|
||||
return hash;
|
||||
|
||||
return _hashListSHA256[stringToCompute] =
|
||||
BitConverter.ToString(_sha256CryptoProvider.ComputeHash(Encoding.UTF8.GetBytes(stringToCompute))).Replace("-", "", StringComparison.Ordinal);
|
||||
using var sha = SHA256.Create();
|
||||
return BitConverter.ToString(sha.ComputeHash(Encoding.UTF8.GetBytes(stringToCompute))).Replace("-", "", StringComparison.Ordinal);
|
||||
}
|
||||
#pragma warning restore SYSLIB0021 // Type or member is obsolete
|
||||
}
|
||||
@@ -4,9 +4,17 @@ using Dalamud.Game.Text.SeStringHandling.Payloads;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.ImGuiSeStringRenderer;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Interface.Textures.TextureWraps;
|
||||
using Lumina.Text;
|
||||
using Lumina.Text.Parse;
|
||||
using Lumina.Text.ReadOnly;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using DalamudSeString = Dalamud.Game.Text.SeStringHandling.SeString;
|
||||
using DalamudSeStringBuilder = Dalamud.Game.Text.SeStringHandling.SeStringBuilder;
|
||||
@@ -19,6 +27,438 @@ public static class SeStringUtils
|
||||
private static int _seStringHitboxCounter;
|
||||
private static int _iconHitboxCounter;
|
||||
|
||||
public static bool TryRenderSeStringMarkupAtCursor(string payload)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(payload))
|
||||
return false;
|
||||
|
||||
var wrapWidth = ImGui.GetContentRegionAvail().X;
|
||||
if (wrapWidth <= 0f || float.IsNaN(wrapWidth) || float.IsInfinity(wrapWidth))
|
||||
wrapWidth = float.MaxValue;
|
||||
|
||||
var normalizedPayload = payload.ReplaceLineEndings("<br>");
|
||||
try
|
||||
{
|
||||
_ = ReadOnlySeString.FromMacroString(normalizedPayload, new MacroStringParseOptions
|
||||
{
|
||||
ExceptionMode = MacroStringParseExceptionMode.Throw
|
||||
});
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var drawParams = new SeStringDrawParams
|
||||
{
|
||||
WrapWidth = wrapWidth,
|
||||
Font = ImGui.GetFont(),
|
||||
Color = ImGui.GetColorU32(ImGuiCol.Text),
|
||||
};
|
||||
|
||||
var renderId = ImGui.GetID($"SeStringMarkup##{normalizedPayload.GetHashCode()}");
|
||||
var drawResult = ImGuiHelpers.CompileSeStringWrapped(normalizedPayload, drawParams, renderId);
|
||||
var height = drawResult.Size.Y;
|
||||
if (height <= 0f)
|
||||
height = ImGui.GetTextLineHeight();
|
||||
|
||||
ImGui.Dummy(new Vector2(0f, height));
|
||||
|
||||
if (drawResult.InteractedPayloadEnvelope.Length > 0 &&
|
||||
TryExtractLink(drawResult.InteractedPayloadEnvelope, drawResult.InteractedPayload, out var linkUrl, out var tooltipText))
|
||||
{
|
||||
var hoverText = !string.IsNullOrEmpty(linkUrl) ? linkUrl : tooltipText;
|
||||
|
||||
if (!string.IsNullOrEmpty(hoverText))
|
||||
{
|
||||
ImGui.BeginTooltip();
|
||||
ImGui.TextUnformatted(hoverText);
|
||||
ImGui.EndTooltip();
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(linkUrl))
|
||||
ImGui.SetMouseCursor(ImGuiMouseCursor.Hand);
|
||||
|
||||
if (drawResult.Clicked && !string.IsNullOrEmpty(linkUrl))
|
||||
Dalamud.Utility.Util.OpenLink(linkUrl);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ImGui.TextDisabled($"[SeString error] {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public enum SeStringSegmentType
|
||||
{
|
||||
Text,
|
||||
Icon
|
||||
}
|
||||
|
||||
public readonly record struct SeStringSegment(
|
||||
SeStringSegmentType Type,
|
||||
string? Text,
|
||||
Vector4? Color,
|
||||
uint IconId,
|
||||
IDalamudTextureWrap? Texture,
|
||||
Vector2 Size);
|
||||
|
||||
public static bool TryResolveSegments(
|
||||
string payload,
|
||||
float scale,
|
||||
Func<uint, IDalamudTextureWrap?> iconResolver,
|
||||
List<SeStringSegment> resolvedSegments,
|
||||
out Vector2 totalSize)
|
||||
{
|
||||
totalSize = Vector2.Zero;
|
||||
if (string.IsNullOrWhiteSpace(payload))
|
||||
return false;
|
||||
|
||||
var parsedSegments = new List<ParsedSegment>(Math.Max(1, payload.Length / 4));
|
||||
if (!ParseSegments(payload, parsedSegments))
|
||||
return false;
|
||||
|
||||
float width = 0f;
|
||||
float height = 0f;
|
||||
|
||||
foreach (var segment in parsedSegments)
|
||||
{
|
||||
switch (segment.Type)
|
||||
{
|
||||
case ParsedSegmentType.Text:
|
||||
{
|
||||
var text = segment.Text ?? string.Empty;
|
||||
if (text.Length == 0)
|
||||
break;
|
||||
|
||||
var textSize = ImGui.CalcTextSize(text);
|
||||
resolvedSegments.Add(new SeStringSegment(SeStringSegmentType.Text, text, segment.Color, 0, null, textSize));
|
||||
width += textSize.X;
|
||||
height = MathF.Max(height, textSize.Y);
|
||||
break;
|
||||
}
|
||||
case ParsedSegmentType.Icon:
|
||||
{
|
||||
var wrap = iconResolver(segment.IconId);
|
||||
Vector2 iconSize;
|
||||
string? fallback = null;
|
||||
if (wrap != null)
|
||||
{
|
||||
iconSize = CalculateIconSize(wrap, scale);
|
||||
}
|
||||
else
|
||||
{
|
||||
fallback = $"[{segment.IconId}]";
|
||||
iconSize = ImGui.CalcTextSize(fallback);
|
||||
}
|
||||
|
||||
resolvedSegments.Add(new SeStringSegment(SeStringSegmentType.Icon, fallback, segment.Color, segment.IconId, wrap, iconSize));
|
||||
width += iconSize.X;
|
||||
height = MathF.Max(height, iconSize.Y);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
totalSize = new Vector2(width, height);
|
||||
parsedSegments.Clear();
|
||||
return resolvedSegments.Count > 0;
|
||||
}
|
||||
|
||||
private enum ParsedSegmentType
|
||||
{
|
||||
Text,
|
||||
Icon
|
||||
}
|
||||
|
||||
private readonly record struct ParsedSegment(
|
||||
ParsedSegmentType Type,
|
||||
string? Text,
|
||||
uint IconId,
|
||||
Vector4? Color);
|
||||
|
||||
private static bool ParseSegments(string payload, List<ParsedSegment> segments)
|
||||
{
|
||||
var builder = new StringBuilder(payload.Length);
|
||||
Vector4? activeColor = null;
|
||||
var index = 0;
|
||||
while (index < payload.Length)
|
||||
{
|
||||
if (payload[index] == '<')
|
||||
{
|
||||
var end = payload.IndexOf('>', index);
|
||||
if (end == -1)
|
||||
break;
|
||||
|
||||
var tagContent = payload.Substring(index + 1, end - index - 1);
|
||||
if (TryHandleIconTag(tagContent, segments, builder, activeColor))
|
||||
{
|
||||
index = end + 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (TryHandleColorTag(tagContent, segments, builder, ref activeColor))
|
||||
{
|
||||
index = end + 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
builder.Append('<');
|
||||
builder.Append(tagContent);
|
||||
builder.Append('>');
|
||||
index = end + 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.Append(payload[index]);
|
||||
index++;
|
||||
}
|
||||
}
|
||||
|
||||
if (index < payload.Length)
|
||||
builder.Append(payload, index, payload.Length - index);
|
||||
|
||||
FlushTextBuilder(builder, activeColor, segments);
|
||||
return segments.Count > 0;
|
||||
}
|
||||
|
||||
private static bool TryHandleIconTag(string tagContent, List<ParsedSegment> segments, StringBuilder textBuilder, Vector4? activeColor)
|
||||
{
|
||||
if (!tagContent.StartsWith("icon(", StringComparison.OrdinalIgnoreCase) || !tagContent.EndsWith(')'))
|
||||
return false;
|
||||
|
||||
var inner = tagContent.AsSpan(5, tagContent.Length - 6).Trim();
|
||||
if (!uint.TryParse(inner, NumberStyles.Integer, CultureInfo.InvariantCulture, out var iconId))
|
||||
return false;
|
||||
|
||||
FlushTextBuilder(textBuilder, activeColor, segments);
|
||||
segments.Add(new ParsedSegment(ParsedSegmentType.Icon, null, iconId, null));
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryHandleColorTag(string tagContent, List<ParsedSegment> segments, StringBuilder textBuilder, ref Vector4? activeColor)
|
||||
{
|
||||
if (tagContent.StartsWith("color", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var equalsIndex = tagContent.IndexOf('=');
|
||||
if (equalsIndex == -1)
|
||||
return false;
|
||||
|
||||
var value = tagContent.Substring(equalsIndex + 1).Trim().Trim('"');
|
||||
if (!TryParseColor(value, out var color))
|
||||
return false;
|
||||
|
||||
FlushTextBuilder(textBuilder, activeColor, segments);
|
||||
activeColor = color;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (tagContent.Equals("/color", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
FlushTextBuilder(textBuilder, activeColor, segments);
|
||||
activeColor = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void FlushTextBuilder(StringBuilder builder, Vector4? color, List<ParsedSegment> segments)
|
||||
{
|
||||
if (builder.Length == 0)
|
||||
return;
|
||||
|
||||
segments.Add(new ParsedSegment(ParsedSegmentType.Text, builder.ToString(), 0, color));
|
||||
builder.Clear();
|
||||
}
|
||||
|
||||
private static bool TryExtractLink(ReadOnlySpan<byte> envelope, Payload? payload, out string url, out string? tooltipText)
|
||||
{
|
||||
url = string.Empty;
|
||||
tooltipText = null;
|
||||
|
||||
if (envelope.Length == 0 && payload is null)
|
||||
return false;
|
||||
|
||||
tooltipText = envelope.Length > 0 ? DalamudSeString.Parse(envelope.ToArray()).TextValue : null;
|
||||
|
||||
if (payload is not null && TryReadUrlFromPayload(payload, out url))
|
||||
return true;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(tooltipText))
|
||||
{
|
||||
var candidate = FindFirstUrl(tooltipText);
|
||||
if (!string.IsNullOrEmpty(candidate))
|
||||
{
|
||||
url = candidate;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
url = string.Empty;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryReadUrlFromPayload(object payload, out string url)
|
||||
{
|
||||
url = string.Empty;
|
||||
var type = payload.GetType();
|
||||
|
||||
static string? ReadStringProperty(Type type, object instance, string propertyName)
|
||||
=> type.GetProperty(propertyName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
|
||||
?.GetValue(instance) as string;
|
||||
|
||||
string? candidate = ReadStringProperty(type, payload, "Uri")
|
||||
?? ReadStringProperty(type, payload, "Url")
|
||||
?? ReadStringProperty(type, payload, "Target")
|
||||
?? ReadStringProperty(type, payload, "Destination");
|
||||
|
||||
if (IsHttpUrl(candidate))
|
||||
{
|
||||
url = candidate!;
|
||||
return true;
|
||||
}
|
||||
|
||||
var dataProperty = type.GetProperty("Data", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
if (dataProperty?.GetValue(payload) is IEnumerable<string> sequence)
|
||||
{
|
||||
foreach (var entry in sequence)
|
||||
{
|
||||
if (IsHttpUrl(entry))
|
||||
{
|
||||
url = entry;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var extraStringProp = type.GetProperty("ExtraString", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
if (IsHttpUrl(extraStringProp?.GetValue(payload) as string))
|
||||
{
|
||||
url = (extraStringProp!.GetValue(payload) as string)!;
|
||||
return true;
|
||||
}
|
||||
|
||||
var textProp = type.GetProperty("Text", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
if (IsHttpUrl(textProp?.GetValue(payload) as string))
|
||||
{
|
||||
url = (textProp!.GetValue(payload) as string)!;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string? FindFirstUrl(string? text)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
return null;
|
||||
|
||||
var index = text.IndexOf("http", StringComparison.OrdinalIgnoreCase);
|
||||
while (index >= 0)
|
||||
{
|
||||
var end = index;
|
||||
while (end < text.Length && !char.IsWhiteSpace(text[end]) && !"\"')]>".Contains(text[end]))
|
||||
end++;
|
||||
|
||||
var candidate = text.Substring(index, end - index).TrimEnd('.', ',', ';');
|
||||
if (IsHttpUrl(candidate))
|
||||
return candidate;
|
||||
|
||||
index = text.IndexOf("http", end, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static bool IsHttpUrl(string? value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
return false;
|
||||
|
||||
return Uri.TryCreate(value, UriKind.Absolute, out var uri)
|
||||
&& (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps);
|
||||
}
|
||||
|
||||
public static string StripMarkup(string value)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value))
|
||||
return string.Empty;
|
||||
|
||||
var builder = new StringBuilder(value.Length);
|
||||
int depth = 0;
|
||||
foreach (var c in value)
|
||||
{
|
||||
if (c == '<')
|
||||
{
|
||||
depth++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (c == '>' && depth > 0)
|
||||
{
|
||||
depth--;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (depth == 0)
|
||||
builder.Append(c);
|
||||
}
|
||||
|
||||
return builder.ToString().Trim();
|
||||
}
|
||||
|
||||
private static bool TryParseColor(string value, out Vector4 color)
|
||||
{
|
||||
color = default;
|
||||
if (string.IsNullOrEmpty(value) || value[0] != '#')
|
||||
return false;
|
||||
|
||||
var hex = value.AsSpan(1);
|
||||
if (!uint.TryParse(hex, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var parsed))
|
||||
return false;
|
||||
|
||||
byte a, r, g, b;
|
||||
if (hex.Length == 6)
|
||||
{
|
||||
a = 0xFF;
|
||||
r = (byte)(parsed >> 16);
|
||||
g = (byte)(parsed >> 8);
|
||||
b = (byte)parsed;
|
||||
}
|
||||
else if (hex.Length == 8)
|
||||
{
|
||||
a = (byte)(parsed >> 24);
|
||||
r = (byte)(parsed >> 16);
|
||||
g = (byte)(parsed >> 8);
|
||||
b = (byte)parsed;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const float inv = 1.0f / 255f;
|
||||
color = new Vector4(r * inv, g * inv, b * inv, a * inv);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static Vector2 CalculateIconSize(IDalamudTextureWrap wrap, float scale)
|
||||
{
|
||||
const float IconHeightScale = 1.25f;
|
||||
|
||||
var textHeight = ImGui.GetTextLineHeight();
|
||||
var baselineHeight = MathF.Max(1f, textHeight - 2f * scale);
|
||||
var targetHeight = MathF.Max(baselineHeight, baselineHeight * IconHeightScale);
|
||||
var aspect = wrap.Width > 0 ? wrap.Width / (float)wrap.Height : 1f;
|
||||
return new Vector2(targetHeight * aspect, targetHeight);
|
||||
}
|
||||
|
||||
public static DalamudSeString BuildFormattedPlayerName(string text, Vector4? textColor, Vector4? glowColor)
|
||||
{
|
||||
var b = new DalamudSeStringBuilder();
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
using LightlessSync.API.Data;
|
||||
using LightlessSync.API.Data.Enum;
|
||||
using LightlessSync.PlayerData.Handlers;
|
||||
using LightlessSync.PlayerData.Pairs;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace LightlessSync.Utils;
|
||||
@@ -56,9 +57,20 @@ public static class VariousExtensions
|
||||
}
|
||||
|
||||
public static Dictionary<ObjectKind, HashSet<PlayerChanges>> CheckUpdatedData(this CharacterData newData, Guid applicationBase,
|
||||
CharacterData? oldData, ILogger logger, PairHandler cachedPlayer, bool forceApplyCustomization, bool forceApplyMods)
|
||||
CharacterData? oldData, ILogger logger, IPairPerformanceSubject cachedPlayer, bool forceApplyCustomization, bool forceApplyMods)
|
||||
{
|
||||
oldData ??= new();
|
||||
static bool FileReplacementsEquivalent(ICollection<FileReplacementData> left, ICollection<FileReplacementData> right)
|
||||
{
|
||||
if (left.Count != right.Count)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var comparer = LightlessSync.PlayerData.Data.FileReplacementDataComparer.Instance;
|
||||
return !left.Except(right, comparer).Any() && !right.Except(left, comparer).Any();
|
||||
}
|
||||
|
||||
var charaDataToUpdate = new Dictionary<ObjectKind, HashSet<PlayerChanges>>();
|
||||
foreach (ObjectKind objectKind in Enum.GetValues<ObjectKind>())
|
||||
{
|
||||
@@ -91,7 +103,9 @@ public static class VariousExtensions
|
||||
{
|
||||
if (hasNewAndOldFileReplacements)
|
||||
{
|
||||
bool listsAreEqual = oldData.FileReplacements[objectKind].SequenceEqual(newData.FileReplacements[objectKind], PlayerData.Data.FileReplacementDataComparer.Instance);
|
||||
var oldList = oldData.FileReplacements[objectKind];
|
||||
var newList = newData.FileReplacements[objectKind];
|
||||
var listsAreEqual = FileReplacementsEquivalent(oldList, newList);
|
||||
if (!listsAreEqual || forceApplyMods)
|
||||
{
|
||||
logger.LogDebug("[BASE-{appBase}] Updating {object}/{kind} (FileReplacements not equal) => {change}", applicationBase, cachedPlayer, objectKind, PlayerChanges.ModFiles);
|
||||
@@ -114,9 +128,9 @@ public static class VariousExtensions
|
||||
.OrderBy(g => string.IsNullOrEmpty(g.Hash) ? g.FileSwapPath : g.Hash, StringComparer.OrdinalIgnoreCase).ToList();
|
||||
var newTail = newFileReplacements.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 = existingFileReplacements.Where(g => g.GamePaths.Any(g => !g.EndsWith("mdl") && !g.EndsWith("tex") && !g.EndsWith("mtrl")))
|
||||
var existingTransients = existingFileReplacements.Where(g => g.GamePaths.Any(g => !g.EndsWith("mdl", StringComparison.OrdinalIgnoreCase) && !g.EndsWith("tex", StringComparison.OrdinalIgnoreCase) && !g.EndsWith("mtrl", StringComparison.OrdinalIgnoreCase)))
|
||||
.OrderBy(g => string.IsNullOrEmpty(g.Hash) ? g.FileSwapPath : g.Hash, StringComparer.OrdinalIgnoreCase).ToList();
|
||||
var newTransients = newFileReplacements.Where(g => g.GamePaths.Any(g => !g.EndsWith("mdl") && !g.EndsWith("tex") && !g.EndsWith("mtrl")))
|
||||
var newTransients = newFileReplacements.Where(g => g.GamePaths.Any(g => !g.EndsWith("mdl", StringComparison.OrdinalIgnoreCase) && !g.EndsWith("tex", StringComparison.OrdinalIgnoreCase) && !g.EndsWith("mtrl", StringComparison.OrdinalIgnoreCase)))
|
||||
.OrderBy(g => string.IsNullOrEmpty(g.Hash) ? g.FileSwapPath : g.Hash, StringComparer.OrdinalIgnoreCase).ToList();
|
||||
|
||||
logger.LogTrace("[BASE-{appbase}] ExistingFace: {of}, NewFace: {fc}; ExistingHair: {eh}, NewHair: {nh}; ExistingTail: {et}, NewTail: {nt}; ExistingTransient: {etr}, NewTransient: {ntr}", applicationBase,
|
||||
|
||||
Reference in New Issue
Block a user